Files
Google-Sheets-Rate-Assistant/lib/onboarding.js
T
LZStealth 2dfe6aac03
build-windows-exe / build (push) Successful in 26s
Warn API key users and bump version to 2.1.5
2026-07-18 12:06:18 +01:00

252 lines
7.1 KiB
JavaScript

const readline = require('readline');
const {createStarterConfig, getDefaultConfigPath, writeConfigFile} = require('./config');
const {getFirstServiceAccountAuthClient} = require('./auth');
const {fetchSpreadsheetTitle} = require('./sheets');
function createPrompt() {
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
const ask = question => new Promise(resolve => {
rl.question(question, answer => resolve((answer || '').trim()));
});
const close = () => new Promise(resolve => {
rl.close();
resolve();
});
return {ask, close};
}
async function askNonEmpty(ask, question) {
while (true) {
const answer = await ask(question);
if (answer) {
return answer;
}
console.log('Please enter a value.');
}
}
async function askPositiveInteger(ask, question) {
while (true) {
const answer = await ask(question);
const value = Number.parseInt(answer, 10);
if (Number.isInteger(value) && value > 0) {
return value;
}
console.log('Please enter a whole number greater than zero.');
}
}
async function askChoice(ask, question, options) {
const optionText = options
.map((option, index) => `${index + 1}. ${option.label}`)
.join('\n');
while (true) {
const answer = await ask(`${question}\n${optionText}\n> `);
const lower = answer.toLowerCase();
const byNumber = Number.parseInt(answer, 10);
if (Number.isInteger(byNumber) && byNumber >= 1 && byNumber <= options.length) {
return options[byNumber - 1].value;
}
const match = options.find(option => option.value.toLowerCase() === lower || option.label.toLowerCase() === lower);
if (match) {
return match.value;
}
console.log('Please choose one of the listed options.');
}
}
async function askOptionalPositiveInteger(ask, question, defaultValue) {
while (true) {
const answer = await ask(question);
if (!answer) {
return defaultValue;
}
const value = Number.parseInt(answer, 10);
if (Number.isInteger(value) && value > 0) {
return value;
}
console.log('Please enter a whole number greater than zero, or press Enter to use the default.');
}
}
function extractSpreadsheetId(input) {
const value = (input || '').trim();
if (!value) {
return '';
}
const urlMatch = value.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/i);
if (urlMatch && urlMatch[1]) {
return urlMatch[1];
}
return value;
}
async function askSpreadsheetIdOrUrl(ask, question) {
while (true) {
const answer = await ask(question);
const documentId = extractSpreadsheetId(answer);
if (documentId) {
return documentId;
}
console.log('Please enter a Google Sheets URL or spreadsheet ID.');
}
}
async function pauseForEnter(ask) {
await ask('Press Enter to close this window...');
}
function buildTitleContext(authMode, config) {
if (authMode === 'apiKeys' && Array.isArray(config.apiKeys) && config.apiKeys.length > 0) {
return {apiKey: config.apiKeys[0].key};
}
if (authMode === 'serviceAccounts' && Array.isArray(config.serviceAccounts) && config.serviceAccounts.length > 0) {
const authClient = getFirstServiceAccountAuthClient(config);
if (authClient) {
return {authClient};
}
}
return {};
}
async function runOnboarding() {
const {ask, close} = createPrompt();
const configPath = getDefaultConfigPath();
try {
console.log('Google Sheets Rate Assistant setup');
console.log('');
const setupMode = await askChoice(
ask,
'Would you like to use the wizard or just create a config file?',
[
{label: 'Wizard', value: 'wizard'},
{label: 'Config file', value: 'config'},
]
);
if (setupMode === 'config') {
createStarterConfig(configPath);
console.log('');
console.log(`Starter config created at ${configPath}`);
console.log('Edit that file, then run the app again.');
await pauseForEnter(ask);
return {mode: 'config', configPath};
}
const authMode = await askChoice(
ask,
'Which authentication method do you want to use?',
[
{label: 'API keys', value: 'apiKeys'},
{label: 'Service accounts', value: 'serviceAccounts'},
]
);
if (authMode === 'apiKeys') {
console.log('Warning: when using API keys, the spreadsheet must be publicly accessible.');
console.log('');
}
const credentialCount = await askPositiveInteger(
ask,
`How many ${authMode === 'apiKeys' ? 'API keys' : 'service accounts'} do you want to add? `
);
const config = {
apiKeys: [],
serviceAccounts: [],
documents: [],
};
Object.defineProperty(config, '__configPath', {
value: configPath,
enumerable: false,
configurable: true,
writable: true,
});
if (authMode === 'apiKeys') {
for (let index = 0; index < credentialCount; index += 1) {
const key = await askNonEmpty(ask, `Enter API key ${index + 1}: `);
const rateLimitPerMinute = await askOptionalPositiveInteger(
ask,
`Rate limit per minute for API key ${index + 1} (press Enter for 50): `,
50
);
config.apiKeys.push({key, rateLimitPerMinute});
}
} else {
for (let index = 0; index < credentialCount; index += 1) {
const pathValue = await askNonEmpty(ask, `Enter service account path ${index + 1}: `);
const rateLimitPerMinute = await askOptionalPositiveInteger(
ask,
`Rate limit per minute for service account ${index + 1} (press Enter for 50): `,
50
);
config.serviceAccounts.push({path: pathValue, rateLimitPerMinute});
}
}
const documentCount = await askPositiveInteger(ask, 'How many documents do you want to pull from? ');
for (let documentIndex = 0; documentIndex < documentCount; documentIndex += 1) {
const documentId = await askSpreadsheetIdOrUrl(
ask,
`Enter Google Sheets URL or document ID ${documentIndex + 1}: `
);
let documentTitle = documentId;
try {
const titleContext = buildTitleContext(authMode, config);
documentTitle = await fetchSpreadsheetTitle(titleContext, documentId);
} catch (err) {
console.log(`Could not load spreadsheet title for ${documentId}; using the document ID instead.`);
}
const sheetCount = await askPositiveInteger(ask, `How many sheets for document ${documentIndex + 1}? `);
const sheets = [];
for (let sheetIndex = 0; sheetIndex < sheetCount; sheetIndex += 1) {
const sheetName = await askNonEmpty(ask, `Enter sheet name ${sheetIndex + 1} for document ${documentIndex + 1}: `);
sheets.push({
name: sheetName,
outputFilename: `${sheetName}.csv`,
});
}
config.documents.push({
documentId,
outputDir: `output/${documentTitle}`,
sheets,
});
}
writeConfigFile(configPath, config);
console.log('');
console.log(`Config saved to ${configPath}`);
return {mode: 'wizard', configPath, config};
} finally {
await close();
}
}
module.exports = {runOnboarding};