feat: Add Chrome Network Monitor extension with popup UI and request handling

- Implemented popup.html for user interface with search functionality and request display.
- Developed popup.js to manage search items, matched requests, and clipboard operations.
- Created mergeConflictManager.js for automated git operations in specified repositories.
- Added projects.txt to maintain a list of relevant projects.
- Introduced pushReleaseBranches.js for managing release branches across multiple projects.
- Developed releasePrepper.js to prepare projects for release with branch management.
- Created stashUpdater.js to update git origins for projects.
- Added updatedProjects.txt to track projects that have been updated.
This commit is contained in:
Greg Jacobs
2026-01-26 16:18:42 -05:00
parent 6702f04050
commit f7d928506a
82 changed files with 9285 additions and 122 deletions

View File

@@ -23,21 +23,13 @@ dotenv.config();
const bearerToken = process.env.BEARER_TOKEN;
const securities = [
{
symbol: 'HOU',
xid: '10020415'
},
{
symbol: 'HZU',
xid: '19728747'
}
];
const dataCenters = ['ptc', 'ctc'];
const dataCenters = ['ptc'];
const users = [
{
userTier: 'QT1a',
modAccessToken: '{"multex":"Y","active":true,"externalId":"testuser","customerTier":5824,"language":"Y","user_tier":"QT1a","token_type":"Bearer","pro":"Y","client_id":"szbv6INGApq0e1Ognvsmzmpq8dGTURTr","realTime":"Y","internalId":"TESTUSER","marketer":"RC","customerType":3870,"levelTwo":"Y","siteId":1614,"exchangeAgreements":"testExchangeAgreements","exp":1689789174}'
},
{
userTier: 'GT2',
modAccessToken: '{"multex":"Y","active":true,"externalId":"testuser","customerTier":5999,"language":"Y","user_tier":"GT2","token_type":"Bearer","pro":"Y","client_id":"szbv6INGApq0e1Ognvsmzmpq8dGTURTr","realTime":"Y","internalId":"TESTUSER","marketer":"RC","customerType":3879,"levelTwo":"Y","siteId":1614,"exchangeAgreements":"testExchangeAgreements","exp":1689789174}'
@@ -101,6 +93,6 @@ const intervalId = setInterval(async () => {
} catch (error) {
console.error('Error:', error);
}
}, 2 * 60 * 1000); // 2 minutes in milliseconds
}, 30 * 1000); // 2 minutes in milliseconds
console.log('Interval started at:', startTime);

View File

@@ -9,8 +9,9 @@
* @param {string} filePath - The path to the JavaScript file.
* @returns {string} The generated bookmarklet.
*/
const fs = require('fs');
const path = require('path');
import fs from 'fs';
import path from 'path';
import clipboardy from 'clipboardy';
// Get the file path from command line arguments
const filePath = process.argv[2];
@@ -18,11 +19,38 @@ const filePath = process.argv[2];
// Read the JavaScript file
const jsCode = fs.readFileSync(filePath, 'utf8');
const cleanedCode = jsCode.replace(/\/\*[\s\S]*?\*\//, '');
// Remove multiline comments and single-line comments, but preserve string literals
let cleanedCode = jsCode
// Remove multiline comments
.replace(/\/\*[\s\S]*?\*\//g, '')
// Remove single-line comments (but not URLs)
.replace(/\/\/.*$/gm, '')
// Remove empty lines and extra whitespace
.replace(/^\s*\n/gm, '')
.trim();
// Create the bookmarklet
const bookmarklet = `javascript:(function() {
${cleanedCode}
})();`;
console.log(bookmarklet);
// Validate the generated JavaScript
try {
// Test if the code is syntactically valid
new Function(cleanedCode);
console.log('Generated bookmarklet is syntactically valid.');
} catch (syntaxError) {
console.error('Syntax error in generated code:', syntaxError.message);
console.log('Generated code:');
console.log(cleanedCode);
process.exit(1);
}
// Copy the bookmarklet to the clipboard
try {
clipboardy.writeSync(bookmarklet);
console.log('Bookmarklet copied to clipboard successfully.');
console.log('Length:', bookmarklet.length, 'characters');
} catch (error) {
console.error('Failed to copy bookmarklet to clipboard:', error);
}

View File

@@ -0,0 +1,33 @@
/**
* BMO GAM Authentication Bookmarklet
* Checks localStorage for BMO GAM authentication keys (bmo.gam.auth.consumerKey and bmo.gam.auth.consumerSecret).
* If they don't exist, modifies the current URL to include the authentication parameters and reloads the page.
*
* Requires the use of utils/bookmarkletMaker.js to generate the bookmarklet.
*/
// Configuration - Update these values as needed
const DEFAULT_CONSUMER_KEY = 'RR5x0svWu5GqUEmoDzFRX297b8mjszeA';
const DEFAULT_CONSUMER_SECRET = '5haGeZQUfNlHVegw';
// Check if BMO GAM auth keys exist in localStorage
const consumerKey = localStorage.getItem('bmo.gam.auth.consumerKey');
const consumerSecret = localStorage.getItem('bmo.gam.auth.consumerSecret');
if (!consumerKey || !consumerSecret) {
// Keys don't exist, modify the URL
const currentUrl = window.location.href;
// Remove existing query parameters (everything after ?)
const baseUrl = currentUrl.split('?')[0];
// Add the authentication parameters
const newUrl = baseUrl + '?consumerKey=' + DEFAULT_CONSUMER_KEY + '&consumerSecret=' + DEFAULT_CONSUMER_SECRET;
// Reload with the new URL
window.location.href = newUrl;
} else {
// Keys exist, show confirmation
console.log('BMO GAM authentication keys found in localStorage');
alert('BMO GAM authentication keys already exist in localStorage:\n\nConsumer Key: ' + consumerKey + '\nConsumer Secret: ' + consumerSecret);
}

View File

@@ -14,10 +14,10 @@
*/
let answers = prompt("Enter the JIRA Tickets, separated by commas");
const splitAnswers = answers ? answers.split(",") : false;
let url = "https://fincentric.atlassian.net/jira/software/c/projects/DIP/boards/513";
let url = "https://communify.atlassian.net/jira/software/c/projects/DIP/boards/444";
if (splitAnswers) {
splitAnswers.forEach(answer => {
let moidUrl = "https://fincentric.atlassian.net/browse/" + answer.trim();
let moidUrl = "https://communify.atlassian.net/browse/" + answer.trim();
window.open(moidUrl, '_blank');
});
} else {

View File

@@ -14,6 +14,6 @@ function jsToMsDate(jsdate) {
const date = prompt("Enter date");
if (date) {
const parsedDate = new Date(date);
const parsedDate = new Date();
alert(jsToMsDate(parsedDate));
}

View File

View File

@@ -6,91 +6,921 @@
*
* Requires the use of utils/bookmarkletMaker.js to generate the bookmarklet.
*/
let data = sessionStorage.getItem('rbc_di_session');
let parsedData = JSON.parse(data);
let features = parsedData.features || {};
let modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '50%';
modal.style.left = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.style.backgroundColor = 'white';
modal.style.padding = '20px';
modal.style.border = '1px solid black';
modal.style.zIndex = '999';
modal.style.display = 'flex';
modal.style.flexWrap = 'wrap';
for (let key in features) {
if (features.hasOwnProperty(key) && typeof features[key] === 'boolean') {
let checkboxContainer = document.createElement('div');
checkboxContainer.style.width = '50%';
let checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = key;
checkbox.checked = features[key];
let label = document.createElement('label');
label.htmlFor = key;
label.innerText = key;
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
modal.appendChild(checkboxContainer);
}
// Check if modal is already open to prevent duplicates
if (document.querySelector('.rbc-feature-flags-modal')) {
console.log('Feature Flags Manager is already open');
return;
}
let saveButton = document.createElement('button');
saveButton.innerText = 'Save';
saveButton.addEventListener('click', function() {
const checkboxes = modal.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(function(checkbox) {
features[checkbox.id] = checkbox.checked;
});
parsedData.features = features;
let data = sessionStorage.getItem('rbc_di_session');
let parsedData;
let features;
// Handle missing or invalid rbc_di_session data
try {
if (!data) {
// Create default structure if no data exists
parsedData = {
"features": {}
};
sessionStorage.setItem('rbc_di_session', JSON.stringify(parsedData));
location.reload();
features = {};
} else {
parsedData = JSON.parse(data);
// Ensure features object exists
if (!parsedData.features || typeof parsedData.features !== 'object') {
parsedData.features = {};
}
features = parsedData.features;
}
} catch (error) {
// Handle invalid JSON or other parsing errors
console.warn('Invalid rbc_di_session data, creating new structure:', error);
parsedData = {
"features": {}
};
sessionStorage.setItem('rbc_di_session', JSON.stringify(parsedData));
features = {};
}
let modal = document.createElement('div');
modal.className = 'rbc-feature-flags-modal';
modal.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 9999;
width: min(90vw, 1200px);
max-width: 95vw;
max-height: min(90vh, 800px);
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: clamp(12px, 2.5vw, 14px);
line-height: 1.5;
`;
// Add modal header
let modalHeader = document.createElement('div');
modalHeader.style.cssText = `
position: sticky;
top: 0;
padding: clamp(12px, 3vw, 24px);
border-bottom: 1px solid #e6e6e6;
background-color: #ffffff;
z-index: 1;
`;
let headerTitle = document.createElement('h1');
headerTitle.innerText = 'Feature Flags Manager';
headerTitle.style.cssText = `
margin: 0 0 16px 0;
font-size: clamp(18px, 4vw, 24px);
font-weight: 600;
color: #333333;
line-height: 1.3;
padding-right: 40px;
`;
// Header close button
let headerCloseButton = document.createElement('button');
headerCloseButton.innerHTML = '×';
headerCloseButton.style.cssText = `
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
font-size: 24px;
color: #666666;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 2;
`;
headerCloseButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#f5f5f5';
this.style.color = '#333333';
});
let closeButton = document.createElement('button');
closeButton.innerText = 'X';
closeButton.style.position = 'absolute';
closeButton.style.right = '10px';
closeButton.style.top = '10px';
closeButton.addEventListener('click', function() {
document.body.removeChild(modal);
document.body.removeChild(overlay);
headerCloseButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = 'transparent';
this.style.color = '#666666';
});
headerCloseButton.addEventListener('click', function () {
if (modal && modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
});
modalHeader.appendChild(headerTitle);
modalHeader.appendChild(headerCloseButton);
modal.appendChild(modalHeader);
// Container for new features (moved to top)
let newFeaturesSection = document.createElement('div');
newFeaturesSection.className = 'rbc-modal-padding';
newFeaturesSection.style.cssText = `
padding: 0 24px 24px 24px;
border-bottom: 1px solid #e6e6e6;
margin-bottom: 24px;
`;
let newFeatureTitle = document.createElement('h3');
newFeatureTitle.innerText = 'Add New Feature Flags';
newFeatureTitle.style.cssText = `
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #333333;
`;
newFeaturesSection.appendChild(newFeatureTitle);
// Two-column container for inputs and suggestions
let twoColumnContainer = document.createElement('div');
twoColumnContainer.className = 'rbc-two-column-container';
twoColumnContainer.style.cssText = `
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
`;
// Left column - Container for all new feature inputs
let leftColumn = document.createElement('div');
let newFeaturesContainer = document.createElement('div');
newFeaturesContainer.id = 'newFeaturesContainer';
leftColumn.appendChild(newFeaturesContainer);
// Flag to prevent rapid row creation
let isCreatingRow = false;
// Function to create a new feature input row
function createNewFeatureRow() {
// Prevent rapid successive row creation
if (isCreatingRow) {
return null;
}
isCreatingRow = true;
// Reset the flag after a short delay
setTimeout(function () {
isCreatingRow = false;
}, 100);
let newFeatureContainer = document.createElement('div');
newFeatureContainer.className = 'new-feature-row rbc-feature-row';
newFeatureContainer.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 6px;
background-color: #fafafa;
`;
let newFeatureCheckbox = document.createElement('input');
newFeatureCheckbox.type = 'checkbox';
newFeatureCheckbox.checked = true;
newFeatureCheckbox.className = 'new-feature-checkbox';
newFeatureCheckbox.style.cssText = `
width: 16px;
height: 16px;
accent-color: #006ac3;
`;
let newFeatureInput = document.createElement('input');
newFeatureInput.type = 'text';
newFeatureInput.placeholder = 'Enter feature flag name...';
newFeatureInput.className = 'new-feature-input rbc-feature-input';
newFeatureInput.style.cssText = `
flex: 1;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s ease;
`;
newFeatureInput.addEventListener('focus', function () {
this.style.borderColor = '#006ac3';
this.style.outline = 'none';
});
newFeatureInput.addEventListener('blur', function () {
this.style.borderColor = '#d1d5db';
});
// Update suggestions when input changes
newFeatureInput.addEventListener('input', function () {
updateSuggestionStates();
});
newFeatureInput.addEventListener('keydown', handleNewFeatureInputKeydown); // Attach keydown listener
// Remove row on backspace if input is empty - cross-browser compatible
newFeatureInput.addEventListener('keydown', function (e) {
var key = e.key || e.keyCode;
var isBackspace = key === 'Backspace' || key === 8;
if (isBackspace && this.value === '') {
var parentRow = this.closest ? this.closest('.new-feature-row') : this.parentNode.parentNode;
if (parentRow && newFeaturesContainer.children.length > 1) {
parentRow.remove ? parentRow.remove() : parentRow.parentNode.removeChild(parentRow);
// Update suggestions after removing a row
updateSuggestionStates();
}
}
});
let removeButton = document.createElement('button');
removeButton.innerText = 'Remove';
removeButton.style.cssText = `
padding: 6px 12px;
background-color: transparent;
color: #666666;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
`;
removeButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#ffebee';
this.style.borderColor = '#d32f2f';
this.style.color = '#d32f2f';
});
removeButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = 'transparent';
this.style.borderColor = '#d1d5db';
this.style.color = '#666666';
});
removeButton.addEventListener('click', function () {
if (newFeatureContainer.remove) {
newFeatureContainer.remove();
} else {
newFeatureContainer.parentNode.removeChild(newFeatureContainer);
}
// Update suggestions after removing a row
updateSuggestionStates();
});
newFeatureContainer.appendChild(newFeatureCheckbox);
newFeatureContainer.appendChild(newFeatureInput);
newFeatureContainer.appendChild(removeButton);
return newFeatureContainer;
}
// Add initial new feature row
newFeaturesContainer.appendChild(createNewFeatureRow());
// Add button to add more feature rows
let addFeatureButton = document.createElement('button');
addFeatureButton.innerText = '+ Add Another Feature Flag';
addFeatureButton.style.cssText = `
padding: 8px 16px;
background-color: transparent;
color: #006ac3;
border: 1px solid #006ac3;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin-top: 8px;
transition: all 0.2s ease;
`;
addFeatureButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#f3f7f8';
});
addFeatureButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = 'transparent';
});
addFeatureButton.addEventListener('click', function () {
var newRow = createNewFeatureRow();
if (newRow) {
newFeaturesContainer.appendChild(newRow);
attachKeydownListenerToNewFeatureInputs();
}
});
leftColumn.appendChild(addFeatureButton);
// Right column - Create suggestions column
let suggestionsColumn = document.createElement('div');
suggestionsColumn.style.cssText = `
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 6px;
background-color: #fafafa;
overflow-y: auto;
max-height: 200px;
`;
let suggestionsTitle = document.createElement('h3');
suggestionsTitle.innerText = 'Suggestions';
suggestionsTitle.style.cssText = `
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333333;
`;
suggestionsColumn.appendChild(suggestionsTitle);
let suggestionsList = document.createElement('div');
suggestionsList.id = 'suggestionsList';
suggestionsColumn.appendChild(suggestionsList);
// Add columns to two-column container
twoColumnContainer.appendChild(leftColumn);
twoColumnContainer.appendChild(suggestionsColumn);
// Add two-column container to the section
newFeaturesSection.appendChild(twoColumnContainer);
modal.appendChild(newFeaturesSection);
// Load suggestions from local storage
let savedFlags = JSON.parse(localStorage.getItem('savedFeatureFlags') || '[]');
// Initialize suggestions section with predefined feature flags
let initialSuggestions = [
'marketsResearchWebComponent',
'rcSectorAndEvents',
'rcMarketmovers',
'rcBreakingNews',
'rcReportsAndCommentary',
'optionsStrikePriceExperience',
'useRbcXrefForUs',
'sentryLogging'
];
// Merge initial suggestions with saved flags
savedFlags = [...new Set([...savedFlags, ...initialSuggestions])];
localStorage.setItem('savedFeatureFlags', JSON.stringify(savedFlags));
// Function to check if a flag is already in use (existing features or current inputs)
function isFlagInUse(flagValue) {
// Check if it exists in the features object
if (Object.keys(features).indexOf(flagValue) !== -1) {
return true;
}
// Check if it's in any of the current input fields
var currentInputs = newFeaturesContainer.querySelectorAll('.new-feature-input');
for (var i = 0; i < currentInputs.length; i++) {
if (currentInputs[i].value.trim() === flagValue) {
return true;
}
}
return false;
}
// Function to update suggestion states
function updateSuggestionStates() {
var suggestions = suggestionsList.querySelectorAll('.rbc-suggestion-item');
for (var i = 0; i < suggestions.length; i++) {
var suggestion = suggestions[i];
var flagValue = suggestion.innerText;
var isInUse = isFlagInUse(flagValue);
if (isInUse) {
suggestion.disabled = true;
suggestion.style.color = '#999999';
suggestion.style.borderColor = '#cccccc';
suggestion.style.cursor = 'not-allowed';
suggestion.style.opacity = '0.6';
} else {
suggestion.disabled = false;
suggestion.style.color = '#006ac3';
suggestion.style.borderColor = '#006ac3';
suggestion.style.cursor = 'pointer';
suggestion.style.opacity = '1';
}
}
}
// Populate suggestions list - cross-browser compatible
for (var k = 0; k < savedFlags.length; k++) {
var flag = savedFlags[k];
var suggestionItem = document.createElement('button');
suggestionItem.className = 'rbc-suggestion-item';
suggestionItem.innerText = flag;
var isInUse = isFlagInUse(flag);
suggestionItem.style.cssText =
'padding: 6px 12px;' +
'margin-bottom: 6px;' +
'margin-right: 6px;' +
'background-color: transparent;' +
'color: ' + (isInUse ? '#999999' : '#006ac3') + ';' +
'border: 1px solid ' + (isInUse ? '#cccccc' : '#006ac3') + ';' +
'border-radius: 4px;' +
'font-size: 12px;' +
'cursor: ' + (isInUse ? 'not-allowed' : 'pointer') + ';' +
'transition: all 0.2s ease;' +
'display: inline-block;' +
'width: 100%;' +
'text-align: left;' +
(isInUse ? 'opacity: 0.6;' : '');
if (isInUse) {
suggestionItem.disabled = true;
} else {
suggestionItem.addEventListener('mouseenter', function () {
if (!this.disabled) {
this.style.backgroundColor = '#f3f7f8';
}
});
suggestionItem.addEventListener('mouseleave', function () {
if (!this.disabled) {
this.style.backgroundColor = 'transparent';
}
});
// Use closure to capture the flag value
(function (flagValue) {
suggestionItem.addEventListener('click', function () {
if (this.disabled) return; // Don't proceed if disabled
// Check if there's an empty input to fill first
var existingInputs = newFeaturesContainer.querySelectorAll('.new-feature-input');
var emptyInput = null;
for (var i = 0; i < existingInputs.length; i++) {
if (!existingInputs[i].value.trim()) {
emptyInput = existingInputs[i];
break;
}
}
if (emptyInput) {
// Fill the empty input and focus it
emptyInput.value = flagValue;
emptyInput.focus();
// Only create a new empty row if this was the last empty input
var hasOtherEmptyInputs = false;
for (var j = 0; j < existingInputs.length; j++) {
if (existingInputs[j] !== emptyInput && !existingInputs[j].value.trim()) {
hasOtherEmptyInputs = true;
break;
}
}
if (!hasOtherEmptyInputs) {
var newRow = createNewFeatureRow();
if (newRow) {
newFeaturesContainer.appendChild(newRow);
attachKeydownListenerToNewFeatureInputs();
}
}
} else {
// No empty input found, create a new row with the suggestion
var newRow = createNewFeatureRow();
if (newRow) {
newRow.querySelector('.new-feature-input').value = flagValue;
newFeaturesContainer.appendChild(newRow);
attachKeydownListenerToNewFeatureInputs();
}
}
// Update suggestion states after adding the flag
updateSuggestionStates();
});
})(flag);
}
suggestionsList.appendChild(suggestionItem);
}
// Initial update of suggestion states
updateSuggestionStates();
// Save new feature flags to local storage
function saveFeatureFlagToLocalStorage(flag) {
if (!savedFlags.includes(flag)) {
savedFlags.push(flag);
localStorage.setItem('savedFeatureFlags', JSON.stringify(savedFlags));
}
}
// Define saveButton earlier in the code
let saveButton = document.createElement('button');
saveButton.innerText = 'Save Changes';
saveButton.style.cssText = `
padding: 12px 24px;
background-color: #006ac3;
color: #ffffff;
border: 2px solid #006ac3;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`;
saveButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#0051a5';
this.style.borderColor = '#0051a5';
});
saveButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = '#006ac3';
this.style.borderColor = '#006ac3';
});
// Update save button to store new flags in local storage - cross-browser compatible
saveButton.addEventListener('click', function () {
var checkboxes = modal.querySelectorAll('input[type="checkbox"]:not(.new-feature-checkbox)');
for (var i = 0; i < checkboxes.length; i++) {
var checkbox = checkboxes[i];
features[checkbox.id] = checkbox.checked;
}
// Process all new feature inputs
var newFeatureRows = modal.querySelectorAll('.new-feature-row');
for (var j = 0; j < newFeatureRows.length; j++) {
var row = newFeatureRows[j];
var input = row.querySelector('.new-feature-input');
var checkbox = row.querySelector('.new-feature-checkbox');
if (input.value && input.value.trim() !== '') {
features[input.value] = checkbox.checked;
saveFeatureFlagToLocalStorage(input.value.trim());
}
}
parsedData.features = features;
sessionStorage.setItem('rbc_di_session', JSON.stringify(parsedData));
// Only remove modal/overlay if they exist in the DOM
if (modal && modal.parentNode) modal.parentNode.removeChild(modal);
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
location.reload();
});
// Action buttons container
let actionButtons = document.createElement('div');
actionButtons.className = 'rbc-action-buttons';
actionButtons.style.cssText = `
position: sticky;
bottom: 0;
padding: 24px;
border-top: 1px solid #e6e6e6;
display: flex;
justify-content: flex-end;
gap: 12px;
background-color: #fafafa;
border-radius: 0 0 8px 8px;
`;
// Append actionButtons to the modal later in the code
modal.appendChild(actionButtons);
// Append saveButton to actionButtons after its definition
actionButtons.appendChild(saveButton);
// Existing Features Section
let existingFeaturesHeader = document.createElement('h3');
existingFeaturesHeader.innerText = 'Existing Feature Flags';
existingFeaturesHeader.style.cssText = `
margin: 0 0 16px 0;
padding: 0 24px;
font-size: 18px;
font-weight: 600;
color: #333333;
`;
modal.appendChild(existingFeaturesHeader);
// Main content container for existing features
let modalContent = document.createElement('div');
modalContent.className = 'rbc-existing-features-grid';
modalContent.style.cssText = `
padding: 0 24px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
overflow-y: auto;
max-height: calc(100vh - 400px);
`;
for (let key in features) {
if (features.hasOwnProperty(key) && typeof features[key] === 'boolean') {
let checkboxContainer = document.createElement('div');
checkboxContainer.style.cssText = `
display: flex;
align-items: center;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 6px;
transition: all 0.2s ease;
`;
checkboxContainer.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#f3f7f8';
this.style.borderColor = '#006ac3';
});
checkboxContainer.addEventListener('mouseleave', function () {
this.style.backgroundColor = '#fafafa';
this.style.borderColor = '#e6e6e6';
});
let checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = key;
checkbox.checked = features[key];
checkbox.style.cssText = `
margin-right: 12px;
width: 16px;
height: 16px;
accent-color: #006ac3;
`;
let label = document.createElement('label');
label.htmlFor = key;
label.innerText = key;
label.style.cssText = `
flex: 1;
font-weight: 400;
color: #333333;
cursor: pointer;
user-select: none;
`;
let deleteButton = document.createElement('button');
deleteButton.innerHTML = '×';
deleteButton.style.cssText = `
background: none;
border: none;
color: #666666;
font-size: 18px;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
`;
deleteButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#ffebee';
this.style.color = '#d32f2f';
});
deleteButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = 'transparent';
this.style.color = '#666666';
});
deleteButton.addEventListener('click', function () {
delete features[key];
if (checkboxContainer.remove) {
checkboxContainer.remove();
} else {
checkboxContainer.parentNode.removeChild(checkboxContainer);
}
});
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
checkboxContainer.appendChild(deleteButton);
modalContent.appendChild(checkboxContainer);
}
}
modal.appendChild(modalContent);
let cancelButton = document.createElement('button');
cancelButton.innerText = 'Cancel';
cancelButton.addEventListener('click', function() {
document.body.removeChild(modal);
document.body.removeChild(overlay);
cancelButton.style.cssText = `
padding: 12px 24px;
background-color: transparent;
color: #006ac3;
border: 2px solid #006ac3;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`;
cancelButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#f3f7f8';
this.style.borderColor = '#0051a5';
this.style.color = '#0051a5';
});
let buttonContainer = document.createElement('div');
buttonContainer.style.width = '100%';
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'center';
buttonContainer.style.marginTop = '20px';
cancelButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = 'transparent';
this.style.borderColor = '#006ac3';
this.style.color = '#006ac3';
});
buttonContainer.appendChild(saveButton);
buttonContainer.appendChild(cancelButton);
cancelButton.addEventListener('click', function () {
if (modal && modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
});
modal.appendChild(closeButton);
modal.appendChild(buttonContainer);
actionButtons.appendChild(cancelButton);
actionButtons.appendChild(saveButton);
modal.appendChild(actionButtons);
let overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
overlay.style.zIndex = '998';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
`;
document.body.appendChild(overlay);
document.body.appendChild(modal);
// Close modal on Escape key - cross-browser compatible
function closeFeatureFlagsModal() {
if (modal && modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
document.removeEventListener('keydown', escListener);
}
function escListener(e) {
var key = e.key || e.keyCode;
if (key === 'Escape' || key === 27) closeFeatureFlagsModal();
}
document.addEventListener('keydown', escListener);
// Update Enter key behavior to create only one new feature flag input - cross-browser compatible
function handleNewFeatureInputKeydown(e) {
var key = e.key || e.keyCode;
var isEnter = key === 'Enter' || key === 13;
var isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey for Mac compatibility
if (isEnter && !isCtrlPressed) {
// Add a single new feature flag row
e.preventDefault();
var lastInput = newFeaturesContainer.querySelector('.new-feature-row:last-child .new-feature-input');
if (!lastInput || !lastInput.value.trim()) {
return; // Prevent adding a new row if the last input is empty
}
var newRow = createNewFeatureRow();
if (newRow) {
newFeaturesContainer.appendChild(newRow);
attachKeydownListenerToNewFeatureInputs();
}
} else if (isEnter && isCtrlPressed) {
// Trigger the Save Changes button
e.preventDefault();
saveButton.click();
}
}
// Attach the keydown listener to all new feature inputs - cross-browser compatible
function attachKeydownListenerToNewFeatureInputs() {
var newFeatureInputs = modal.querySelectorAll('.new-feature-input');
for (var i = 0; i < newFeatureInputs.length; i++) {
var input = newFeatureInputs[i];
// Remove existing listener to avoid duplicates
input.removeEventListener('keydown', handleNewFeatureInputKeydown);
input.addEventListener('keydown', handleNewFeatureInputKeydown);
}
}
// Attach listeners to initial rows
attachKeydownListenerToNewFeatureInputs();
// Remove the duplicate event listener that was causing double row creation
// The handleNewFeatureInputKeydown function already handles Enter key presses
// Add a comprehensive style block for responsive design and isolation from main website
let styleBlock = document.createElement('style');
styleBlock.innerHTML = `
/* Base styles and website isolation */
.rbc-feature-flags-modal input[type="checkbox"] + label::before {
content: "";
display: inline-block;
vertical-align: -25%;
background-color: transparent;
height: 26px;
width: 26px;
margin: 1px;
margin-bottom:-1px;
}
/* Responsive modal styles */
@media (max-width: 768px) {
.rbc-feature-flags-modal {
width: 95vw !important;
max-width: 95vw !important;
max-height: 95vh !important;
font-size: 16px !important; /* Prevent zoom on iOS */
}
/* Stack columns on mobile */
.rbc-two-column-container {
grid-template-columns: 1fr !important;
gap: 16px !important;
}
/* Adjust existing features grid for mobile */
.rbc-existing-features-grid {
grid-template-columns: 1fr !important;
gap: 12px !important;
}
/* Make feature rows more mobile-friendly */
.rbc-feature-row {
flex-wrap: wrap !important;
gap: 8px !important;
}
.rbc-feature-input {
min-width: 150px !important;
font-size: 16px !important; /* Prevent zoom on iOS */
}
/* Adjust button sizes for mobile */
.rbc-action-buttons {
flex-direction: column !important;
gap: 8px !important;
}
.rbc-action-buttons button {
width: 100% !important;
padding: 12px !important;
}
/* Make suggestions more mobile-friendly */
.rbc-suggestion-item {
font-size: 14px !important;
padding: 8px 12px !important;
}
/* Adjust close button for mobile */
.rbc-close-button {
top: 8px !important;
right: 8px !important;
width: 40px !important;
height: 40px !important;
font-size: 20px !important;
}
}
@media (max-width: 480px) {
.rbc-feature-flags-modal {
width: 98vw !important;
max-height: 98vh !important;
}
.rbc-modal-padding {
padding: 12px !important;
}
.rbc-feature-input {
min-width: 120px !important;
}
}
/* Landscape mobile adjustments */
@media (max-width: 768px) and (orientation: landscape) {
.rbc-feature-flags-modal {
max-height: 90vh !important;
}
}
`;
// Append the style block to the modal
modal.appendChild(styleBlock);

View File

@@ -0,0 +1,89 @@
# Feature Flags Modal RBC Style
## Overview
This modal allows users to view, add, toggle, and remove feature flags stored in session storage. It is styled to match the RBC Direct Investing modal design, with a professional, modern look and clear sectioning.
---
## 🆕 New Layout (as of July 2025)
### 1. **Header**
- **Title:** `Feature Flags Manager`
- Large, bold, and visually separated from the rest of the modal.
### 2. **Add New Feature Flags (Top Section)**
- **Header:** `Add New Feature Flags`
- Always at the top, right below the main title.
- Allows adding multiple new feature flags before saving.
- Each new flag row includes:
- Checkbox (default: checked)
- Text input for flag name
- Remove button
- "+ Add Another Feature Flag" button for more rows
- Section is visually separated with a border below.
### 3. **Existing Feature Flags**
- **Header:** `Existing Feature Flags`
- Appears below the new flags section.
- Flags are displayed in a two-column grid.
- Each flag is shown as a card with:
- Checkbox (toggle on/off)
- Flag name
- Delete (×) button
- Cards have hover and focus effects for clarity.
### 4. **Action Buttons (Footer)**
- **Buttons:** `Cancel` (secondary), `Save Changes` (primary)
- Right-aligned, styled to match RBC's button system
- Cancel closes the modal without saving
- Save applies all changes and reloads the page
### 5. **Close Button**
- Top-right corner (×), styled as a circular icon
- Closes the modal and overlay
### 6. **Overlay**
- Dimmed, blurred background overlay for focus
---
## 🎨 Styling Details
- **Colors:** RBC blue (`#006ac3`), hover blue (`#0051a5`), error red (`#d32f2f`), backgrounds (`#fafafa`, `#f3f7f8`)
- **Typography:** System font stack, clear hierarchy
- **Spacing:** Generous padding, grid layout, rounded corners
- **Accessibility:** High contrast, focus states, large click targets
---
## 🛠️ Usage
- The modal is generated and injected by the `rbc-di-featureflags.js` bookmarklet.
- All changes are saved to `sessionStorage` under the `rbc_di_session` key.
- Use the provided script to generate a bookmarklet for browser use.
---
## 📋 Example Structure
```
┌─────────────────────────────────────┐
│ Feature Flags Manager │ ← Main Header
├─────────────────────────────────────┤
│ Add New Feature Flags │ ← New section (top)
│ [+] Add Another Feature Flag │
├─────────────────────────────────────┤
│ Existing Feature Flags │ ← New header
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Flag 1 [×] │ │ Flag 2 [×] │ │
│ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────┤
│ [Cancel] [Save Changes] │
└─────────────────────────────────────┘
```
---
## ✅ Improvements Over Previous Version
- New flags section is always visible and prioritized
- Existing flags are clearly separated and labeled
- Modern, accessible, and brand-consistent design
- All actions (add, remove, toggle, save, cancel) are visually clear and easy to use

View File

@@ -0,0 +1,20 @@
/**
* Grabs the user session token from the 'fnc-rbc-session' session storage and copies it to the clipboard.
*/
function grabToken() {
const rbcSession = sessionStorage.getItem('fnc-rbc-session');
if (rbcSession) {
const sessionValues = JSON.parse(rbcSession);
navigator.clipboard.writeText(sessionValues.token)
.then(() => {
console.log(`user session token ${sessionValues.token} copied to clipboard!`);
})
.catch(error => {
console.error('Failed to copy session token:', error);
});
} else {
console.error('fnc-rbc-session session not found!');
}
}
grabToken();

Binary file not shown.

View File

@@ -0,0 +1,141 @@
# Network Request Monitor - Chrome Extension
A Chrome extension that monitors network requests on web pages and alerts you when specific keywords or patterns are found in the requests.
## Features
- **Custom Search Items**: Define a list of keywords, API endpoints, or patterns to monitor
- **Real-time Monitoring**: Automatically captures network requests as pages load
- **Match Detection**: Highlights requests that contain your search items
- **Request Details**: Shows HTTP method, URL, timestamp, status code, and matched items
- **Request Body Preview**: Displays a preview of POST request bodies
- **Badge Counter**: Shows the number of matches on the extension icon
- **Auto-refresh**: Updates the list every 2 seconds while the popup is open
## Installation Instructions
### Method 1: Load Unpacked Extension (Development Mode)
1. **Open Chrome Extensions Page**
- Navigate to `chrome://extensions/` in your Chrome browser
- Or click the three-dot menu → More Tools → Extensions
2. **Enable Developer Mode**
- Toggle the "Developer mode" switch in the top-right corner
3. **Load the Extension**
- Click the "Load unpacked" button
- Navigate to the extension folder: `C:\memorypalace\src\utils\chrome-network-monitor`
- Click "Select Folder"
4. **Verify Installation**
- You should see the "Network Request Monitor" extension appear in your extensions list
- The extension icon should appear in your Chrome toolbar
### Method 2: Pin the Extension (Recommended)
1. Click the puzzle piece icon in the Chrome toolbar
2. Find "Network Request Monitor" in the list
3. Click the pin icon to keep it visible in your toolbar
## How to Use
### 1. Add Search Items
1. Click the extension icon in your Chrome toolbar
2. In the "Search Items" section, enter the keywords or patterns you want to monitor (one per line)
Example:
```
api/users
authentication
featureFlag
rbc_di_session
consumerKey
```
3. Click "Save Search Items"
### 2. Browse Websites
- Navigate to any website
- The extension will automatically monitor all network requests
- When a match is found, the extension badge will show the number of matches
### 3. View Matched Requests
1. Click the extension icon to open the popup
2. View all matched requests for the current tab
3. See details including:
- HTTP method (GET, POST, etc.)
- Full request URL
- Timestamp
- Status code
- Matched search items
- Request body preview (for POST requests)
### 4. Manage Matches
- **Refresh**: Click "Refresh" to manually update the list
- **Clear All**: Click "Clear All" to remove all matches for the current tab
- Matches are automatically cleared when you close the tab
## Technical Details
### Files Structure
```
chrome-network-monitor/
├── manifest.json # Extension configuration
├── background.js # Background service worker
├── popup.html # Popup UI structure
├── popup.css # Popup styles
├── popup.js # Popup logic
├── icon16.png # 16x16 icon
├── icon48.png # 48x48 icon
├── icon128.png # 128x128 icon
└── README.md # This file
```
### Permissions
- **webRequest**: Monitor network requests
- **storage**: Save search items
- **activeTab**: Access the current tab information
- **host_permissions**: Monitor requests from all URLs
### Limitations
- Maximum 100 matched requests are stored per session
- Request body preview is limited to 500 characters
- Search is case-insensitive
- Matches are tab-specific and cleared when the tab is closed
## Troubleshooting
**Extension not appearing:**
- Make sure Developer Mode is enabled
- Try reloading the extension from `chrome://extensions/`
**No matches detected:**
- Verify your search items are saved
- Check that the search terms match the actual network request URLs
- Some requests may be blocked by CORS or browser security policies
**Badge not updating:**
- Close and reopen the extension popup
- Reload the web page
## Development
To modify the extension:
1. Edit the files in `C:\memorypalace\src\utils\chrome-network-monitor`
2. Go to `chrome://extensions/`
3. Click the refresh icon on the extension card
4. Test your changes
## Notes
- The extension stores search items using Chrome's sync storage, so they'll sync across your Chrome browsers
- Network monitoring only works on http/https pages (not on chrome:// pages)
- The extension respects Chrome's security policies and cannot monitor certain protected requests

View File

@@ -0,0 +1,132 @@
// Network Request Monitor - Background Service Worker
let monitoredRequests = [];
let searchItems = [];
let autoClearOnNavigate = false;
// Load settings from storage on startup
chrome.storage.sync.get(['searchItems', 'autoClearOnNavigate'], (result) => {
searchItems = result.searchItems || [];
autoClearOnNavigate = result.autoClearOnNavigate || false;
console.log('Loaded settings:', { searchItems, autoClearOnNavigate });
});// Listen for storage changes
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'sync') {
if (changes.searchItems) {
searchItems = changes.searchItems.newValue || [];
console.log('Search items updated:', searchItems);
}
if (changes.autoClearOnNavigate) {
autoClearOnNavigate = changes.autoClearOnNavigate.newValue || false;
console.log('Auto-clear preference updated:', autoClearOnNavigate);
}
}
});// Monitor web requests
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (searchItems.length === 0) return;
const url = details.url;
const method = details.method;
let requestBody = '';
// Get request body if available
if (details.requestBody) {
if (details.requestBody.raw) {
const decoder = new TextDecoder('utf-8');
requestBody = details.requestBody.raw.map(data => decoder.decode(data.bytes)).join('');
} else if (details.requestBody.formData) {
requestBody = JSON.stringify(details.requestBody.formData);
}
}
// Check if URL or request body contains any search items
const matches = searchItems.filter(item => {
const searchString = item.toLowerCase();
return url.toLowerCase().includes(searchString) ||
requestBody.toLowerCase().includes(searchString);
});
if (matches.length > 0) {
const matchedRequest = {
url: url,
method: method,
timestamp: new Date().toISOString(),
tabId: details.tabId,
matches: matches,
requestBody: requestBody.substring(0, 500) // Limit body preview
};
monitoredRequests.push(matchedRequest);
// Keep only last 100 requests
if (monitoredRequests.length > 100) {
monitoredRequests.shift();
}
// Update badge to show match count
updateBadge(details.tabId);
console.log('Match found!', matchedRequest);
}
},
{ urls: ["<all_urls>"] },
["requestBody"]
);
// Monitor response headers for additional context
chrome.webRequest.onCompleted.addListener(
(details) => {
// Find matching request and add response info
const matchedIndex = monitoredRequests.findIndex(
req => req.url === details.url && req.tabId === details.tabId
);
if (matchedIndex !== -1) {
monitoredRequests[matchedIndex].statusCode = details.statusCode;
monitoredRequests[matchedIndex].responseHeaders = details.responseHeaders;
}
},
{ urls: ["<all_urls>"] },
["responseHeaders"]
);
// Update badge with match count
function updateBadge(tabId) {
const tabMatches = monitoredRequests.filter(req => req.tabId === tabId);
const count = tabMatches.length;
if (count > 0) {
chrome.action.setBadgeText({ text: count.toString(), tabId: tabId });
chrome.action.setBadgeBackgroundColor({ color: '#FF0000', tabId: tabId });
}
}
// Clear requests for a specific tab
chrome.tabs.onRemoved.addListener((tabId) => {
monitoredRequests = monitoredRequests.filter(req => req.tabId !== tabId);
});
// Clear requests on navigation if auto-clear is enabled
chrome.webNavigation.onCommitted.addListener((details) => {
if (autoClearOnNavigate && details.frameId === 0) {
// Clear requests for this tab when navigating
monitoredRequests = monitoredRequests.filter(req => req.tabId !== details.tabId);
// Clear badge
chrome.action.setBadgeText({ text: '', tabId: details.tabId });
console.log('Cleared requests for tab', details.tabId, 'on navigation');
}
});// Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getRequests') {
const tabId = request.tabId;
const tabRequests = monitoredRequests.filter(req => req.tabId === tabId);
sendResponse({ requests: tabRequests });
} else if (request.action === 'clearRequests') {
const tabId = request.tabId;
monitoredRequests = monitoredRequests.filter(req => req.tabId !== tabId);
chrome.action.setBadgeText({ text: '', tabId: tabId });
sendResponse({ success: true });
}
return true;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,31 @@
{
"manifest_version": 3,
"name": "Network Request Monitor",
"version": "1.0.0",
"description": "Monitor network requests for specific keywords and patterns",
"permissions": [
"webRequest",
"webNavigation",
"storage",
"activeTab"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}

View File

@@ -0,0 +1,297 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
background-color: #f5f5f5;
width: 600px;
max-height: 600px;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
header h1 {
font-size: 18px;
font-weight: 600;
}
.section {
padding: 16px 20px;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.section h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #555;
display: flex;
align-items: center;
gap: 8px;
}
.badge {
background-color: #667eea;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.search-items-container {
display: flex;
flex-direction: column;
gap: 8px;
}
#searchItemsInput {
width: 100%;
min-height: 100px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
transition: border-color 0.2s;
}
#searchItemsInput:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: #667eea;
color: white;
}
.btn-primary:hover {
background-color: #5568d3;
transform: translateY(-1px);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.status-message {
min-height: 20px;
font-size: 12px;
color: #28a745;
font-weight: 500;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.auto-clear-container {
margin-bottom: 12px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
cursor: pointer;
width: 14px;
height: 14px;
accent-color: #667eea;
}
.checkbox-label:hover {
color: #333;
}
.requests-list {
max-height: 300px;
overflow-y: auto;
}
.empty-state {
text-align: center;
color: #999;
padding: 20px;
font-size: 13px;
}
.request-item {
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
transition: all 0.2s;
}
.request-item:hover {
background: #f0f0f0;
border-color: #667eea;
}
.request-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.method-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
color: white;
}
.method-GET {
background-color: #28a745;
}
.method-POST {
background-color: #007bff;
}
.method-PUT {
background-color: #ffc107;
color: #333;
}
.method-DELETE {
background-color: #dc3545;
}
.method-PATCH {
background-color: #17a2b8;
}
.request-url {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #333;
word-break: break-all;
flex: 1;
}
.request-details {
font-size: 11px;
color: #666;
margin-top: 4px;
}
.matched-items {
margin-top: 6px;
padding: 6px;
background: #e7f3ff;
border-radius: 4px;
}
.matched-items-label {
font-size: 11px;
font-weight: 600;
color: #0066cc;
margin-bottom: 4px;
}
.matched-item {
display: inline-block;
background: #667eea;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
margin-right: 4px;
margin-top: 2px;
}
.request-body-preview {
margin-top: 6px;
padding: 6px;
background: #f5f5f5;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 11px;
color: #666;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Network Request Monitor</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>Network Monitor</h1>
</header>
<div class="section">
<h2>Search Items</h2>
<div class="search-items-container">
<textarea id="searchItemsInput" placeholder="Enter items to search for (one per line)&#10;Example:&#10;api/users&#10;authentication&#10;featureFlag"></textarea>
<button id="saveSearchItems" class="btn btn-primary">Save Search Items</button>
<div id="saveStatus" class="status-message"></div>
</div>
</div>
<div class="section">
<h2>Matched Requests <span id="matchCount" class="badge">0</span></h2>
<div class="controls">
<button id="refreshRequests" class="btn btn-secondary">Refresh</button>
<button id="copyRequests" class="btn btn-success">Copy to Clipboard</button>
<button id="clearRequests" class="btn btn-danger">Clear All</button>
</div>
<div class="auto-clear-container">
<label class="checkbox-label">
<input type="checkbox" id="autoClearOnNavigate">
<span>Clear on page navigation</span>
</label>
</div>
<div id="requestsList" class="requests-list">
<p class="empty-state">No matches found yet. Add search items above and browse websites.</p>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,216 @@
// Popup script for Network Request Monitor
let currentTabId = null;
// Initialize popup
document.addEventListener('DOMContentLoaded', async () => {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
currentTabId = tab.id;
// Load saved search items
loadSearchItems();
// Load matched requests
loadRequests();
// Load auto-clear preference
loadAutoClearPreference();
// Setup event listeners
document.getElementById('saveSearchItems').addEventListener('click', saveSearchItems);
document.getElementById('refreshRequests').addEventListener('click', loadRequests);
document.getElementById('copyRequests').addEventListener('click', copyRequestsToClipboard);
document.getElementById('clearRequests').addEventListener('click', clearRequests);
document.getElementById('autoClearOnNavigate').addEventListener('change', saveAutoClearPreference);
});// Load search items from storage
async function loadSearchItems() {
const result = await chrome.storage.sync.get(['searchItems']);
const searchItems = result.searchItems || [];
document.getElementById('searchItemsInput').value = searchItems.join('\n');
}
// Load auto-clear preference
async function loadAutoClearPreference() {
const result = await chrome.storage.sync.get(['autoClearOnNavigate']);
document.getElementById('autoClearOnNavigate').checked = result.autoClearOnNavigate || false;
}
// Save auto-clear preference
async function saveAutoClearPreference() {
const autoClear = document.getElementById('autoClearOnNavigate').checked;
await chrome.storage.sync.set({ autoClearOnNavigate: autoClear });
console.log('Auto-clear preference saved:', autoClear);
}// Save search items to storage
async function saveSearchItems() {
const input = document.getElementById('searchItemsInput').value;
const searchItems = input
.split('\n')
.map(item => item.trim())
.filter(item => item.length > 0);
await chrome.storage.sync.set({ searchItems: searchItems });
// Show success message
const statusEl = document.getElementById('saveStatus');
statusEl.textContent = `✓ Saved ${searchItems.length} search item${searchItems.length !== 1 ? 's' : ''}`;
setTimeout(() => {
statusEl.textContent = '';
}, 3000);
console.log('Search items saved:', searchItems);
}
// Load matched requests from background
async function loadRequests() {
chrome.runtime.sendMessage(
{ action: 'getRequests', tabId: currentTabId },
(response) => {
displayRequests(response.requests || []);
}
);
}
// Display requests in the UI
function displayRequests(requests) {
const listEl = document.getElementById('requestsList');
const countEl = document.getElementById('matchCount');
countEl.textContent = requests.length;
if (requests.length === 0) {
listEl.innerHTML = '<p class="empty-state">No matches found yet. Add search items above and browse websites.</p>';
return;
}
// Sort by timestamp (newest first)
requests.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
listEl.innerHTML = requests.map(req => createRequestElement(req)).join('');
}
// Create HTML for a single request
function createRequestElement(request) {
const timestamp = new Date(request.timestamp).toLocaleTimeString();
const statusCode = request.statusCode ? ` - ${request.statusCode}` : '';
const matchedItemsHtml = request.matches && request.matches.length > 0
? `
<div class="matched-items">
<div class="matched-items-label">Matched Items:</div>
${request.matches.map(item => `<span class="matched-item">${escapeHtml(item)}</span>`).join('')}
</div>
`
: '';
const bodyPreviewHtml = request.requestBody && request.requestBody.length > 0
? `<div class="request-body-preview">${escapeHtml(request.requestBody)}</div>`
: '';
return `
<div class="request-item">
<div class="request-header">
<span class="method-badge method-${request.method}">${request.method}</span>
<span class="request-url">${escapeHtml(request.url)}</span>
</div>
<div class="request-details">
${timestamp}${statusCode}
</div>
${matchedItemsHtml}
${bodyPreviewHtml}
</div>
`;
}
// Copy requests to clipboard
async function copyRequestsToClipboard() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.runtime.sendMessage(
{ action: 'getRequests', tabId: currentTabId },
async (response) => {
const requests = response.requests || [];
if (requests.length === 0) {
alert('No requests to copy');
return;
}
// Group URLs by matched keywords
const matchedUrlsByKeyword = new Map();
requests.forEach(req => {
if (req.matches && req.matches.length > 0) {
req.matches.forEach(match => {
if (!matchedUrlsByKeyword.has(match)) {
matchedUrlsByKeyword.set(match, []);
}
// Add URL if not already in the list for this keyword
if (!matchedUrlsByKeyword.get(match).includes(req.url)) {
matchedUrlsByKeyword.get(match).push(req.url);
}
});
}
});
// Format the output
const pageTitle = tab.title || 'Unknown Page';
const pageUrl = tab.url || 'Unknown URL';
let output = `${pageTitle} - ${pageUrl}\n`;
const sortedMatches = Array.from(matchedUrlsByKeyword.keys()).sort();
sortedMatches.forEach(match => {
output += `- ${match}\n`;
const urls = matchedUrlsByKeyword.get(match);
// Add sub-bullets for each URL
urls.forEach(url => {
output += ` - ${url}\n`;
});
});
// Copy to clipboard
try {
await navigator.clipboard.writeText(output);
// Show success feedback
const copyBtn = document.getElementById('copyRequests');
const originalText = copyBtn.textContent;
copyBtn.textContent = '✓ Copied!';
copyBtn.style.backgroundColor = '#218838';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.backgroundColor = '';
}, 2000);
} catch (err) {
alert('Failed to copy to clipboard: ' + err.message);
}
}
);
}
// Clear all requests
async function clearRequests() {
chrome.runtime.sendMessage(
{ action: 'clearRequests', tabId: currentTabId },
(response) => {
if (response.success) {
loadRequests();
}
}
);
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Auto-refresh every 2 seconds
setInterval(() => {
loadRequests();
}, 2000);

View File

@@ -0,0 +1,59 @@
import { execSync } from 'child_process';
import path from 'path';
// Get the repo path from command line arguments
const repoPath = process.argv[2];
if (!repoPath) {
console.error('Error: Please provide a repository path');
console.error('Usage: node script.js <path-to-repo>');
process.exit(1);
}
// Resolve the absolute path
const absolutePath = path.resolve(repoPath);
console.log(`Working in repository: ${absolutePath}\n`);
// Commands to run in sequence
const commands = [
'git stash',
'git checkout develop',
'git pull',
'git checkout main',
'git pull',
'git checkout -b feature/DIP-13605-main-to-develop',
'git push',
'git merge develop'
];
// Execute each command
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
console.log(`[${i + 1}/${commands.length}] Running: ${cmd}`);
try {
execSync(cmd, {
cwd: absolutePath,
stdio: 'inherit',
encoding: 'utf8'
});
console.log(`✓ Success\n`);
} catch (error) {
console.error(`\n✗ Failed at command: ${cmd}`);
console.error(`Exit code: ${error.status}`);
// If the merge command fails, open VS Code with the repository
if (cmd.startsWith('git merge')) {
console.log(`\nOpening VS Code to resolve merge conflicts...`);
try {
execSync(`code "${absolutePath}"`, { stdio: 'inherit' });
} catch (codeError) {
console.error('Failed to open VS Code');
}
}
process.exit(1);
}
}
console.log('All commands completed successfully!');

View File

@@ -0,0 +1,30 @@
rbc-preferences
rbc-xref
rbc-fundamentals
rbc-smart-text
trading-central-technical-insight
rbc-direct-investing-detailed-quote
rbc-direct-investing
rbc-charting
rbc-quote
rbc-news
rbc-market-events
rbc-income-projection
rbc-analyze-and-rebalance
rbc-curated-research
rbc-sector-industry
rbc-recent-symbols
user-admin-service-layer
rbc-analyst-picklists
rbc-options
rbc-investing
rbc-web-component-poc
rbc-markets
rbc-screener
rbc-direct-investing-frontend
rbc
rbc-rc
rbc-direct-investing-research
rbc-expansion-drawer
rbc-wealth-management-api
rbc-ideas-hub

View File

@@ -0,0 +1,310 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import readline from 'readline';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(query) {
return new Promise(resolve => rl.question(query, resolve));
}
function readProjectList(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return content.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
} catch (error) {
console.error(`Error reading file ${filePath}:`, error.message);
return [];
}
}
function hasPackageJson(projectPath) {
const packageJsonPath = path.join(projectPath, 'package.json');
return fs.existsSync(packageJsonPath);
}
function executeGitCommand(command, cwd) {
try {
const result = execSync(command, {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
return { success: true, output: result.trim() };
} catch (error) {
return {
success: false,
error: error.message,
stderr: error.stderr ? error.stderr.toString() : ''
};
}
}
function branchExists(projectPath, branchName) {
const result = executeGitCommand('git branch -a', projectPath);
if (!result.success) {
return false;
}
// Check both local and remote branches
const branches = result.output.split('\n').map(b => b.trim().replace(/^\*\s*/, ''));
return branches.some(b =>
b === branchName ||
b === `remotes/origin/${branchName}` ||
b.endsWith(`/${branchName}`)
);
}
/**
* Recursively search for a project folder by name starting from basePath
*/
function findProjectFolder(basePath, targetName) {
try {
const entries = fs.readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const fullPath = path.join(basePath, entry.name);
// Check if this is the target folder
if (entry.name.toLowerCase() === targetName.toLowerCase()) {
// Verify it has a package.json (top-level project folder)
const packageJsonPath = path.join(fullPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return fullPath;
}
// If no package.json, continue searching
}
// Skip node_modules, .git, and other common large directories
if (['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
continue;
}
// Recursively search subdirectories
const found = findProjectFolder(fullPath, targetName);
if (found) return found;
}
}
} catch (error) {
// Skip directories we don't have permission to read
if (error.code !== 'EACCES' && error.code !== 'EPERM') {
// Silently skip permission errors
}
}
return null;
}
/**
* Find all projects from the list by searching recursively in base path
*/
function findAllProjectFolders(basePath, projectNames) {
const allProjects = [];
console.log(`🔍 Searching for ${projectNames.length} projects in ${basePath}...\n`);
for (const projectName of projectNames) {
console.log(`Searching for: ${projectName}...`);
const projectPath = findProjectFolder(basePath, projectName);
if (projectPath) {
console.log(`✓ Found: ${projectPath}`);
allProjects.push({ projectPath });
} else {
console.log(`✗ Not found: ${projectName}`);
allProjects.push({
projectPath: projectName,
error: `Project '${projectName}' not found`
});
}
}
return allProjects;
}
function processProject(projectPath, releaseBranch) {
const projectName = path.basename(projectPath);
const steps = [];
console.log(`\n${'='.repeat(60)}`);
console.log(`Processing: ${projectName}`);
console.log(`Path: ${projectPath}`);
console.log(`${'='.repeat(60)}`);
steps.push({ step: 'check package.json', success: true, message: 'Found' });
console.log('✓ Found package.json');
// Check if the release branch exists
const releaseExists = branchExists(projectPath, releaseBranch);
if (releaseExists) {
steps.push({ step: 'check release branch', success: true, message: `Branch ${releaseBranch} exists` });
console.log(`✓ Found existing branch ${releaseBranch}`);
} else {
steps.push({ step: 'check release branch', success: true, message: `Branch ${releaseBranch} does not exist yet - will create` });
console.log(` Branch ${releaseBranch} does not exist yet - will create it`);
}
// Step 1: Switch to develop branch
console.log('\n1⃣ Switching to develop branch...');
const checkoutDevelop = executeGitCommand('git checkout develop', projectPath);
if (!checkoutDevelop.success) {
steps.push({ step: 'checkout develop', success: false, message: checkoutDevelop.error });
console.log(`❌ Failed to checkout develop: ${checkoutDevelop.stderr}`);
return { projectName, projectPath, success: false, steps };
}
steps.push({ step: 'checkout develop', success: true, message: 'Switched to develop' });
console.log('✓ Switched to develop');
// Step 2: Pull latest from develop
console.log('\n2⃣ Pulling latest from develop...');
const pullDevelop = executeGitCommand('git pull origin develop', projectPath);
if (!pullDevelop.success) {
steps.push({ step: 'pull develop', success: false, message: pullDevelop.error });
console.log(`❌ Failed to pull develop: ${pullDevelop.stderr}`);
return { projectName, projectPath, success: false, steps };
}
steps.push({ step: 'pull develop', success: true, message: 'Pulled latest' });
console.log('✓ Pulled latest from develop');
// Step 3: Delete old release branch locally if it exists, then create new one
console.log(`\n3⃣ Creating ${releaseBranch} branch from develop...`);
// Delete local branch if it exists
if (releaseExists) {
const deleteLocal = executeGitCommand(`git branch -D ${releaseBranch}`, projectPath);
if (deleteLocal.success) {
console.log(`✓ Deleted existing local ${releaseBranch}`);
} else {
console.log(` Could not delete local ${releaseBranch} - will try to create anyway`);
}
}
// Create new release branch from develop
const createBranch = executeGitCommand(`git checkout -b ${releaseBranch}`, projectPath);
if (!createBranch.success) {
steps.push({ step: 'create release branch', success: false, message: createBranch.error });
console.log(`❌ Failed to create ${releaseBranch}: ${createBranch.stderr}`);
return { projectName, projectPath, success: false, steps };
}
steps.push({ step: 'create release branch', success: true, message: `Created ${releaseBranch} from develop` });
console.log(`✓ Created ${releaseBranch} from develop`);
// Step 4: Force push the new release branch
console.log(`\n4⃣ Pushing ${releaseBranch} to remote...`);
const pushBranch = executeGitCommand(`git push -f origin ${releaseBranch}`, projectPath);
if (!pushBranch.success) {
steps.push({ step: 'push release branch', success: false, message: pushBranch.error });
console.log(`❌ Failed to push ${releaseBranch}: ${pushBranch.stderr}`);
return { projectName, projectPath, success: false, steps };
}
steps.push({ step: 'push release branch', success: true, message: `Pushed ${releaseBranch} to remote` });
console.log(`✓ Successfully pushed ${releaseBranch} to remote`);
console.log(`\n${projectName} completed successfully!`);
return { projectName, projectPath, success: true, steps };
}
async function main() {
console.log('🚀 Release Branch Manager\n');
// Ask for the project list file path
const projectListPath = await question('Enter path to project list file (one folder name per line): ');
if (!fs.existsSync(projectListPath)) {
console.error(`Error: File not found at ${projectListPath}`);
rl.close();
return;
}
// Ask for the release branch name
const releaseBranch = await question('Enter release branch name (e.g., release/TICKET-123): ');
if (!releaseBranch.match(/^release\/.+/)) {
console.error('Error: Branch name should match pattern "release/TICKET_NUMBER"');
rl.close();
return;
}
console.log(`\n📋 Reading project list from: ${projectListPath}`);
console.log(`🌿 Target release branch: ${releaseBranch}\n`);
const projectFolders = readProjectList(projectListPath);
if (projectFolders.length === 0) {
console.error('No projects found in the list file.');
rl.close();
return;
}
console.log(`Found ${projectFolders.length} projects to search for\n`);
const BASE_PATH = 'C:\\Projects';
// Recursively search for each project in the base path
const allProjectPaths = findAllProjectFolders(BASE_PATH, projectFolders);
const foundProjects = allProjectPaths.filter(p => !p.error);
console.log(`\n📦 Found ${foundProjects.length}/${projectFolders.length} projects\n`);
const results = [];
for (const { projectPath, error } of allProjectPaths) {
if (error) {
results.push({
projectName: path.basename(projectPath),
projectPath,
success: false,
steps: [{ step: 'find project', success: false, message: error }]
});
continue;
}
const result = processProject(projectPath, releaseBranch);
results.push(result);
}
// Summary
console.log('\n\n');
console.log('═'.repeat(70));
console.log(' SUMMARY REPORT');
console.log('═'.repeat(70));
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`\n✅ Successful: ${successful.length}/${results.length}`);
if (successful.length > 0) {
successful.forEach(r => {
console.log(`${r.projectName}`);
});
}
console.log(`\n❌ Failed: ${failed.length}/${results.length}`);
if (failed.length > 0) {
failed.forEach(r => {
console.log(`${r.projectName}`);
const failedStep = r.steps.find(s => !s.success);
if (failedStep) {
console.log(` Reason: ${failedStep.message}`);
}
});
}
console.log('\n' + '═'.repeat(70));
console.log(`\n🏁 Processing complete!\n`);
rl.close();
}
main().catch(error => {
console.error('Fatal error:', error);
rl.close();
process.exit(1);
});

View File

@@ -0,0 +1,347 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import { execSync, spawn } from 'child_process';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const BASE_DIRECTORY = 'C:\\Projects';
const notFoundProjects = [];
const processedProjects = [];
const skippedProjects = [];
/**
* Promisify readline question
*/
function question(query) {
return new Promise(resolve => rl.question(query, resolve));
}
/**
* Parse project list input - handles comma-separated, line-separated, or file path
*/
async function getProjectList() {
console.log('\n=== Release Preparation Tool ===\n');
const input = await question('Enter project names (comma-separated, one per line, or path to txt file):\n');
// Check if input is a file path
if (input.trim().endsWith('.txt') && fs.existsSync(input.trim())) {
const fileContent = fs.readFileSync(input.trim(), 'utf-8');
return parseProjectNames(fileContent);
}
return parseProjectNames(input);
}
/**
* Parse project names from string content
*/
function parseProjectNames(content) {
// Split by newlines and commas, filter empty strings
const projects = content
.split(/[\n,]/)
.map(p => p.trim())
.filter(p => p.length > 0);
return [...new Set(projects)]; // Remove duplicates
}
/**
* Recursively search for a folder by name
*/
function findProjectFolder(basePath, targetName) {
try {
const entries = fs.readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const fullPath = path.join(basePath, entry.name);
// Check if this is the target folder
if (entry.name.toLowerCase() === targetName.toLowerCase()) {
// Verify it has a package.json (top-level project folder)
const packageJsonPath = path.join(fullPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return fullPath;
}
// If no package.json, continue searching
}
// Skip node_modules, .git, and other common large directories
if (['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
continue;
}
// Recursively search subdirectories
const found = findProjectFolder(fullPath, targetName);
if (found) return found;
}
}
} catch (error) {
// Skip directories we don't have permission to read
if (error.code !== 'EACCES' && error.code !== 'EPERM') {
console.error(`Error reading directory ${basePath}: ${error.message}`);
}
}
return null;
}
/**
* Execute git command in a directory
*/
function execGit(command, cwd) {
try {
return execSync(command, {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
} catch (error) {
throw new Error(`Git command failed: ${error.message}\n${error.stderr || ''}`);
}
}
/**
* Check if there are uncommitted changes
*/
function hasUncommittedChanges(projectPath) {
try {
const status = execGit('git status --porcelain', projectPath);
return status.length > 0;
} catch (error) {
console.error(`Error checking git status: ${error.message}`);
return false;
}
}
/**
* Get current branch name
*/
function getCurrentBranch(projectPath) {
try {
return execGit('git rev-parse --abbrev-ref HEAD', projectPath);
} catch (error) {
throw new Error(`Failed to get current branch: ${error.message}`);
}
}
/**
* Check if a branch exists locally or remotely
*/
function branchExists(projectPath, branchName) {
try {
// Check local branches
const localBranches = execGit('git branch --list', projectPath);
if (localBranches.includes(branchName)) {
return true;
}
// Check remote branches
const remoteBranches = execGit('git branch -r --list', projectPath);
if (remoteBranches.includes(`origin/${branchName}`)) {
return true;
}
return false;
} catch (error) {
console.error(`Error checking branch existence: ${error.message}`);
return false;
}
}
/**
* Process a single project for release
*/
async function processProject(projectPath, projectName, releaseTicket) {
console.log(`\n--- Processing: ${projectName} ---`);
console.log(`Path: ${projectPath}`);
try {
// Check if release branch already exists
const releaseBranch = `release/${releaseTicket}`;
if (branchExists(projectPath, releaseBranch)) {
console.log(`⚠ Release branch '${releaseBranch}' already exists. Skipping...`);
skippedProjects.push(projectName);
return;
}
// Check current branch
const currentBranch = getCurrentBranch(projectPath);
console.log(`Current branch: ${currentBranch}`);
// Check for uncommitted changes
if (hasUncommittedChanges(projectPath)) {
console.log('Uncommitted changes detected. Stashing...');
execGit('git stash push -m "stashing for release prep"', projectPath);
console.log('✓ Changes stashed');
}
// Switch to develop branch
console.log('Switching to develop branch...');
try {
execGit('git checkout develop', projectPath);
console.log('✓ Switched to develop');
} catch (error) {
console.log('⚠ Could not switch to develop branch. Trying main...');
try {
execGit('git checkout main', projectPath);
console.log('✓ Switched to main');
} catch (error2) {
throw new Error('Could not switch to develop or main branch');
}
}
// Pull latest changes
console.log('Pulling latest changes...');
execGit('git pull', projectPath);
console.log('✓ Pulled latest changes');
// Create release branch (releaseBranch already declared above)
console.log(`Creating release branch: ${releaseBranch}...`);
execGit(`git checkout -b ${releaseBranch}`, projectPath);
console.log('✓ Release branch created');
// Get remote URL for confirmation
const remoteUrl = execGit('git remote get-url origin', projectPath);
console.log(`\nRemote URL: ${remoteUrl}`);
const confirm = await question(`Push ${releaseBranch} to remote? (y/n): `);
if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') {
console.log('Pushing to remote...');
const pushOutput = execGit(`git push -u origin ${releaseBranch}`, projectPath);
console.log('✓ Pushed to remote');
// Extract merge request URL from git output
const mrUrlMatch = pushOutput.match(/https?:\/\/[^\s]+\/merge_requests\/new[^\s]*/);
if (mrUrlMatch) {
const mrUrl = mrUrlMatch[0];
console.log(`\nOpening merge request URL in Chrome...`);
console.log(`URL: ${mrUrl}`);
// Open in Chrome
try {
spawn('cmd.exe', ['/c', 'start', 'chrome', mrUrl], {
detached: true,
stdio: 'ignore'
}).unref();
console.log('✓ Opened in Chrome');
} catch (error) {
console.log(`⚠ Could not open Chrome automatically. Please visit: ${mrUrl}`);
}
} else {
// Construct GitLab MR URL manually
const gitlabMatch = remoteUrl.match(/gitlab\.com[:/]([^/]+\/[^/.]+)/);
if (gitlabMatch) {
const projectPath = gitlabMatch[1].replace('.git', '');
const mrUrl = `https://gitlab.com/${projectPath}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${releaseBranch}`;
console.log(`\nMerge Request URL: ${mrUrl}`);
try {
spawn('cmd.exe', ['/c', 'start', 'chrome', mrUrl], {
detached: true,
stdio: 'ignore'
}).unref();
console.log('✓ Opened in Chrome');
} catch (error) {
console.log(`⚠ Could not open Chrome automatically. Please visit the URL above.`);
}
}
}
} else {
console.log('⚠ Skipped pushing to remote');
}
processedProjects.push(projectName);
console.log(`${projectName} successfully prepared for release!`);
} catch (error) {
console.error(`✗ Error processing ${projectName}: ${error.message}`);
notFoundProjects.push(`${projectName} (processing error: ${error.message})`);
}
}
/**
* Main execution function
*/
async function main() {
try {
// Get project list
const projects = await getProjectList();
if (projects.length === 0) {
console.log('No projects provided. Exiting...');
rl.close();
return;
}
console.log(`\nProjects to prepare: ${projects.join(', ')}`);
// Get release ticket
const releaseTicket = await question('\nEnter release ticket number: ');
if (!releaseTicket.trim()) {
console.log('No release ticket provided. Exiting...');
rl.close();
return;
}
console.log(`\nSearching for projects in ${BASE_DIRECTORY}...`);
// Find and process each project
for (const projectName of projects) {
console.log(`\nSearching for: ${projectName}...`);
const projectPath = findProjectFolder(BASE_DIRECTORY, projectName);
if (projectPath) {
console.log(`✓ Found: ${projectPath}`);
// Check if it's a git repository
if (fs.existsSync(path.join(projectPath, '.git'))) {
await processProject(projectPath, projectName, releaseTicket);
} else {
console.log(`${projectPath} is not a git repository`);
notFoundProjects.push(`${projectName} (not a git repo)`);
}
} else {
console.log(`✗ Not found: ${projectName}`);
notFoundProjects.push(projectName);
}
}
// Summary
console.log('\n=== Summary ===');
console.log(`\nTotal projects requested: ${projects.length}`);
console.log(`\n✓ Successfully processed: ${processedProjects.length} project(s)`);
if (processedProjects.length > 0) {
processedProjects.forEach(p => console.log(` ${p}`));
}
if (skippedProjects.length > 0) {
console.log(`\n⊘ Already have release branch: ${skippedProjects.length} project(s)`);
skippedProjects.forEach(p => console.log(` ${p}`));
}
if (notFoundProjects.length > 0) {
console.log(`\n✗ Failed/Not found: ${notFoundProjects.length} project(s)`);
notFoundProjects.forEach(p => console.log(` ${p}`));
}
console.log('\n=== Release Preparation Complete ===\n');
} catch (error) {
console.error(`\nFatal error: ${error.message}`);
} finally {
rl.close();
}
}
// Run the script
main();

View File

@@ -0,0 +1,203 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import readline from 'readline';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
function isGitRepo(folderPath) {
try {
const gitDir = path.join(folderPath, '.git');
const packageJson = path.join(folderPath, 'package.json');
return fs.existsSync(gitDir) && fs.existsSync(packageJson);
} catch (error) {
return false;
}
}
function getGitOrigin(folderPath) {
try {
const origin = execSync('git remote get-url origin', {
cwd: folderPath,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
return origin;
} catch (error) {
return null;
}
}
function setGitOrigin(folderPath, newOrigin) {
try {
execSync(`git remote set-url origin "${newOrigin}"`, {
cwd: folderPath,
encoding: 'utf8',
stdio: 'inherit'
});
return true;
} catch (error) {
console.error(`Error setting origin for ${folderPath}:`, error.message);
return false;
}
}
function scanProjects(baseDir, results = null, depth = 0, maxDepth = 5) {
if (!results) {
results = {
needsUpdate: [],
alreadyGitlab: [],
notGitRepo: [],
noOrigin: []
};
console.log(`Scanning folders in: ${baseDir}\n`);
}
if (!fs.existsSync(baseDir)) {
console.error(`Directory does not exist: ${baseDir}`);
return results;
}
if (depth > maxDepth) {
return results;
}
const folders = fs.readdirSync(baseDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const folder of folders) {
const folderPath = path.join(baseDir, folder);
const hasPackageJson = fs.existsSync(path.join(folderPath, 'package.json'));
const hasGitDir = fs.existsSync(path.join(folderPath, '.git'));
// If it has package.json and .git, check it
if (hasPackageJson && hasGitDir) {
const origin = getGitOrigin(folderPath);
if (!origin) {
results.noOrigin.push(folderPath);
continue;
}
if (origin.toLowerCase().includes('gitlab')) {
results.alreadyGitlab.push({ folder: folderPath, origin });
} else {
results.needsUpdate.push({ folder: folderPath, origin, path: folderPath });
}
}
// If no package.json, recurse into subdirectories
else if (!hasPackageJson) {
try {
scanProjects(folderPath, results, depth + 1, maxDepth);
} catch (error) {
// Skip folders we can't access
}
}
// If has package.json but no .git, skip it
else if (hasPackageJson && !hasGitDir) {
results.notGitRepo.push(folderPath);
}
}
return results;
}
async function updateOrigins(foldersToUpdate) {
console.log('\n=== Updating Git Origins ===\n');
for (let i = 0; i < foldersToUpdate.length; i++) {
const { folder, origin, path: folderPath } = foldersToUpdate[i];
console.log(`\n[${i + 1}/${foldersToUpdate.length}] Folder: ${path.basename(folder)}`);
console.log(`Path: ${folder}`);
console.log(`Current origin: ${origin}`);
const newOrigin = await question('Enter new GitLab origin URL (or "skip" to skip): ');
if (newOrigin.toLowerCase() === 'skip' || newOrigin.trim() === '') {
console.log('Skipped.');
continue;
}
const success = setGitOrigin(folderPath, newOrigin);
if (success) {
console.log('✓ Origin updated successfully!');
// Verify the update
const verifiedOrigin = getGitOrigin(folderPath);
console.log(`Verified new origin: ${verifiedOrigin}`);
}
}
}
async function main() {
const projectsDir = 'C:\\Projects';
console.log('=== Git Origin Scanner ===\n');
const results = scanProjects(projectsDir);
console.log('\n=== Scan Results ===\n');
if (results.notGitRepo.length > 0) {
console.log(`Not Git Repositories (${results.notGitRepo.length}):`);
results.notGitRepo.forEach(f => console.log(` - ${f}`));
console.log();
}
if (results.noOrigin.length > 0) {
console.log(`Git Repos Without Origin (${results.noOrigin.length}):`);
results.noOrigin.forEach(f => console.log(` - ${f}`));
console.log();
}
if (results.alreadyGitlab.length > 0) {
console.log(`Already Using GitLab (${results.alreadyGitlab.length}):`);
results.alreadyGitlab.forEach(({ folder, origin }) => {
console.log(` - ${path.basename(folder)}`);
console.log(` Path: ${folder}`);
console.log(` Origin: ${origin}`);
});
console.log();
}
if (results.needsUpdate.length === 0) {
console.log('✓ No repositories need updating. All non-GitLab origins are already set correctly!');
rl.close();
return;
}
console.log(`\nRepositories That Need Updating (${results.needsUpdate.length}):`);
results.needsUpdate.forEach(({ folder, origin }) => {
console.log(` - ${path.basename(folder)}`);
console.log(` Path: ${folder}`);
console.log(` Current: ${origin}`);
});
const answer = await question('\nDo you want to update these repositories? (yes/no): ');
if (answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') {
await updateOrigins(results.needsUpdate);
console.log('\n=== Update Complete ===');
} else {
console.log('\nUpdate cancelled.');
}
rl.close();
}
main().catch(error => {
console.error('Error:', error);
rl.close();
process.exit(1);
});

View File

@@ -0,0 +1,3 @@
rbc-analyst-picklists
rbc-charting
rbc-news