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:
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
33
src/utils/bookmarklets/bmo-gam-auth.js
Normal file
33
src/utils/bookmarklets/bmo-gam-auth.js
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
0
src/utils/bookmarklets/openNomad
Normal file
0
src/utils/bookmarklets/openNomad
Normal 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);
|
||||
|
||||
89
src/utils/bookmarklets/rbc-di-featureflags.md
Normal file
89
src/utils/bookmarklets/rbc-di-featureflags.md
Normal 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
|
||||
20
src/utils/bookmarklets/tokenGrabber.js
Normal file
20
src/utils/bookmarklets/tokenGrabber.js
Normal 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();
|
||||
BIN
src/utils/chrome-extensions/chrome-network-monitor.zip
Normal file
BIN
src/utils/chrome-extensions/chrome-network-monitor.zip
Normal file
Binary file not shown.
141
src/utils/chrome-extensions/chrome-network-monitor/README.md
Normal file
141
src/utils/chrome-extensions/chrome-network-monitor/README.md
Normal 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
|
||||
132
src/utils/chrome-extensions/chrome-network-monitor/background.js
Normal file
132
src/utils/chrome-extensions/chrome-network-monitor/background.js
Normal 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;
|
||||
});
|
||||
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon128.png
Normal file
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon16.png
Normal file
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 593 B |
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon48.png
Normal file
BIN
src/utils/chrome-extensions/chrome-network-monitor/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
297
src/utils/chrome-extensions/chrome-network-monitor/popup.css
Normal file
297
src/utils/chrome-extensions/chrome-network-monitor/popup.css
Normal 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;
|
||||
}
|
||||
@@ -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) Example: api/users authentication 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>
|
||||
216
src/utils/chrome-extensions/chrome-network-monitor/popup.js
Normal file
216
src/utils/chrome-extensions/chrome-network-monitor/popup.js
Normal 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);
|
||||
59
src/utils/oddsNends/mergeConflictManager.js
Normal file
59
src/utils/oddsNends/mergeConflictManager.js
Normal 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!');
|
||||
30
src/utils/oddsNends/projects.txt
Normal file
30
src/utils/oddsNends/projects.txt
Normal 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
|
||||
310
src/utils/oddsNends/pushReleaseBranches.js
Normal file
310
src/utils/oddsNends/pushReleaseBranches.js
Normal 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);
|
||||
});
|
||||
347
src/utils/oddsNends/releasePrepper.js
Normal file
347
src/utils/oddsNends/releasePrepper.js
Normal 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();
|
||||
203
src/utils/oddsNends/stashUpdater.js
Normal file
203
src/utils/oddsNends/stashUpdater.js
Normal 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);
|
||||
});
|
||||
3
src/utils/oddsNends/updatedProjects.txt
Normal file
3
src/utils/oddsNends/updatedProjects.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
rbc-analyst-picklists
|
||||
rbc-charting
|
||||
rbc-news
|
||||
Reference in New Issue
Block a user