From 1432f6316ea1a8cf4360141ed9a883b56ae5f520 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 16 Jul 2025 08:49:47 -0700 Subject: [PATCH 1/7] fix(init): adding a fallback to the author for when the user.name is undefined --- src/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.js b/src/init.js index 3643f05..ec46ead 100644 --- a/src/init.js +++ b/src/init.js @@ -365,7 +365,7 @@ export const init = async (options) => { const scaffoldVariables = { 'snapfu.name': answers.name, 'snapfu.siteId': answers.siteId, - 'snapfu.author': user.name, + 'snapfu.author': user.name || user.login, 'snapfu.framework': answers.framework, }; From 2050f2b5ece50096788829795444beceadd4c54f Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Fri, 17 Oct 2025 15:59:38 -0400 Subject: [PATCH 2/7] feat: remove login requirement for init command and make repo creation optional --- src/badges.js | 4 + src/cli.js | 153 +++++++-------- src/help.js | 2 +- src/init.js | 529 +++++++++++++++++++++++++++++--------------------- src/recs.js | 5 +- 5 files changed, 388 insertions(+), 305 deletions(-) diff --git a/src/badges.js b/src/badges.js index 5abe954..9e75e5b 100644 --- a/src/badges.js +++ b/src/badges.js @@ -236,6 +236,7 @@ export async function listBadgeTemplates(options) { const list = async (secretKey, siteId = '', name = '') => { if (!secretKey) { console.log(chalk.red('Unable to list remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const remoteTemplates = await new ConfigApi(secretKey, options).getBadgeTemplates({ siteId }); @@ -318,6 +319,7 @@ export async function removeBadgeTemplate(options) { if (!secretKey) { console.log(chalk.red('Unable to archive remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const { message } = await new ConfigApi(secretKey, options).archiveBadgeTemplate({ payload, siteId }); @@ -767,6 +769,7 @@ export async function syncBadgeTemplate(options) { const sync = async (template, secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } // validate template against remote locations (locations get validated and synced first) @@ -824,6 +827,7 @@ export async function syncBadgeTemplate(options) { const syncLocations = async (secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote badge locations due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } console.log(` synchronizing locations`); diff --git a/src/cli.js b/src/cli.js index 9484a19..f01e669 100644 --- a/src/cli.js +++ b/src/cli.js @@ -61,102 +61,95 @@ async function parseArgumentsIntoOptions(rawArgs) { let multipleSites = []; // drop out if not logged in for certain commands - const userCommands = ['init', 'badges', 'recs', 'recommendation', 'recommendations', 'secret', 'secrets', 'logout', 'whoami', 'org-access']; - const secretCommands = ['badges', 'recs', 'recommendation', 'recommendations', 'secret', 'secrets']; + const requiredLoginCommands = ['logout', 'whoami', 'org-access']; const templatesRestrictedCommands = ['recs', 'recommendation', 'recommendations']; const loggedIn = user && user.token; const secretOptions = args['--secrets-ci'] || secretKey; - if (userCommands.includes(command) && !(loggedIn || secretOptions || args['--ci'])) { - console.log(chalk.yellow(`Login is required. Please login.`)); - console.log(chalk.grey(`\n\tsnapfu login\n`)); + if (requiredLoginCommands.includes(command) && !(loggedIn || secretOptions || args['--ci'])) { + console.log(chalk.yellow(`Login is required when using the '${command}' command.`)); exit(1); } else if (context.project.distribution == 'SnapTemplates' && templatesRestrictedCommands.includes(command)) { console.log(chalk.yellow(`The '${command}' command is not supported when using SnapTemplates.`)); exit(0); - } else if (secretCommands.includes(command) && (loggedIn || secretOptions || args['--ci'])) { - const getSecretKeyFromCLI = (siteId) => { - try { - const secrets = JSON.parse(args['--secrets-ci']); - const secretKey = secrets[`WEBSITE_SECRET_KEY_${siteId.toUpperCase()}`]; - return secretKey; - } catch (e) { - return; - } - }; + } - if (context.searchspring && typeof context.searchspring.siteId === 'object') { - // searchsoring.siteId contains multiple sites + const getSecretKeyFromCLI = (siteId) => { + try { + const secrets = JSON.parse(args['--secrets-ci']); + const secretKey = secrets[`WEBSITE_SECRET_KEY_${siteId.toUpperCase()}`]; + return secretKey; + } catch (e) { + return; + } + }; - const siteIds = Object.keys(context.searchspring.siteId); - if (!siteIds || !siteIds.length) { - console.log(chalk.red('searchspring.siteId object in package.json is empty')); - exit(1); - } + if (context.searchspring && typeof context.searchspring.siteId === 'object') { + // searchsoring.siteId contains multiple sites - multipleSites = siteIds - .map((siteId) => { - try { - const { name } = context.searchspring.siteId[siteId]; - const secretKey = getSecretKeyFromCLI(siteId) || user.keys[siteId]; - - if (!secretKey) { - console.log(chalk.red(`Cannot find the secretKey for siteId '${siteId}'.`)); - console.log(chalk.bold.white(`Please run the following command:`)); - console.log(chalk.gray(`\tsnapfu secrets add\n`)); - } - if (!secretKey && args['--secrets-ci']) { - console.log( - chalk.red(`Could not find Github secret 'WEBSITE_SECRET_KEY_${siteId.toUpperCase()}' in 'secrets' input - It can be added by running 'snapfu secrets add' in the project's directory locally, - or added manual in the project's repository secrets. - The value can be obtained in the Searchspring Management Console. - Then ensure that you are providing 'secrets' when running the action. ie: - - jobs: - Publish: - runs-on: ubuntu-latest - name: Snap Action - steps: - - name: Checkout action - uses: actions/checkout@v2 - with: - repository: searchspring/snap-action - - name: Run @searchspring/snap-action - uses: ./ - with: - secrets: \${{ toJSON(secrets) }} - ... - `) - ); - } - - return { - siteId, - name, - secretKey, - }; - } catch (e) { - console.log(chalk.red('The searchspring.siteId object in package.json is invalid. Expected format:')); + const siteIds = Object.keys(context.searchspring.siteId); + if (!siteIds || !siteIds.length) { + console.log(chalk.red('searchspring.siteId object in package.json is empty')); + exit(1); + } + + multipleSites = siteIds + .map((siteId) => { + try { + const { name } = context.searchspring.siteId[siteId]; + const secretKey = getSecretKeyFromCLI(siteId) || user.keys[siteId]; + + if (!secretKey && args['--secrets-ci']) { console.log( - chalk.red(` - "searchspring": { - "siteId": { - "xxxxx1": { - "name": "site1.com.au" - }, - "xxxxx2": { - "name": "site2.hk" - } - }, - }`) + chalk.red(`Could not find Github secret 'WEBSITE_SECRET_KEY_${siteId.toUpperCase()}' in 'secrets' input +It can be added by running 'snapfu secrets add' in the project's directory locally, +or added manual in the project's repository secrets. +The value can be obtained in the Searchspring Management Console. +Then ensure that you are providing 'secrets' when running the action. ie: + +jobs: + Publish: + runs-on: ubuntu-latest + name: Snap Action + steps: + - name: Checkout action + uses: actions/checkout@v2 + with: + repository: searchspring/snap-action + - name: Run @searchspring/snap-action + uses: ./ + with: + secrets: \${{ toJSON(secrets) }} + ... +`) ); - exit(1); } - }) - .filter((site) => site.secretKey); + + return { + siteId, + name, + secretKey, + }; + } catch (e) { + console.log(chalk.red('The searchspring.siteId object in package.json is invalid. Expected format:')); + console.log( + chalk.red(` +"searchspring": { + "siteId": { + "xxxxx1": { + "name": "site1.com.au" + }, + "xxxxx2": { + "name": "site2.hk" } + }, +}`) + ); + exit(1); + } + }) + .filter((site) => site.secretKey); } let packageJSON = {}; diff --git a/src/help.js b/src/help.js index 2015021..ba89ecc 100644 --- a/src/help.js +++ b/src/help.js @@ -12,7 +12,7 @@ These are the snapfu commands used in various situations ${chalk.whiteBright('recs')} Recommendation template management ${chalk.whiteBright('secrets')} Project secret management ${chalk.whiteBright('patch')} Apply patches to update project - ${chalk.whiteBright('login')} Oauths with github + ${chalk.whiteBright('login')} Oauths with github to retrieve additional templates and create repositories when using the init command ${chalk.whiteBright('logout')} Removes login credentials ${chalk.whiteBright('org-access')} Review and change organization access for the tool ${chalk.whiteBright('whoami')} Shows the current user diff --git a/src/init.js b/src/init.js index b7d98ba..5858768 100644 --- a/src/init.js +++ b/src/init.js @@ -35,6 +35,7 @@ export const createDir = (dir) => { export const init = async (options) => { try { const { user } = options; + const isLoggedIn = user && user.token; let dir; if (options.args.length === 1) { @@ -45,60 +46,57 @@ export const init = async (options) => { console.log(chalk.yellow(`A parameter was not provided to the init command. The current working directory will be initialized.`)); } - let octokit = new Octokit({ - auth: user.token, - request: { - fetch: fetch, - }, - }); + let octokit; + let orgs = []; + let snapfuScaffoldRepos = []; - let orgs = await octokit.orgs.listForAuthenticatedUser().then(({ data }) => { - return data.map((org) => { - return org.login; + if (isLoggedIn) { + octokit = new Octokit({ + auth: user.token, + request: { + fetch: fetch, + }, }); - }); - const fetchScaffoldRepos = async () => { - // using search modifiers - https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories - const searchOrgs = orgs - .concat(user.login) - .concat('searchspring') - .map((org) => `org:${org}`); - - let page = 0; - let per_page = 100; - let repos = []; - let response; - do { - page++; - response = await octokit.rest.search.repos({ - q: `snapfu-scaffold-+archived:false+${searchOrgs.join('+')}`, - per_page, - page, + orgs = await octokit.orgs.listForAuthenticatedUser().then(({ data }) => { + return data.map((org) => { + return org.login; }); + }); - response.data?.items?.map((repo) => { - repos.push(repo); - }); - } while (response.data?.items.length == per_page); - return repos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-`)); - }; + const fetchScaffoldRepos = async () => { + // using search modifiers - https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories + const searchOrgs = orgs + .concat(user.login) + .concat('searchspring') + .map((org) => `org:${org}`); + + let page = 0; + let per_page = 100; + let repos = []; + let response; + do { + page++; + response = await octokit.rest.search.repos({ + q: `snapfu-scaffold-+archived:false+${searchOrgs.join('+')}`, + per_page, + page, + }); - const snapfuScaffoldRepos = await fetchScaffoldRepos(); + response.data?.items?.map((repo) => { + repos.push(repo); + }); + } while (response.data?.items.length == per_page); + return repos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-`)); + }; + + snapfuScaffoldRepos = await fetchScaffoldRepos(); + } - if (!snapfuScaffoldRepos?.length) { + if (isLoggedIn && !snapfuScaffoldRepos?.length) { console.log(chalk.red('failed to fetch scaffolds...')); } else { - let questions = [ - { - type: 'input', - name: 'name', - validate: (input) => { - return input && input.length > 0; - }, - message: 'Please choose the name of this repository:', - default: path.basename(dir), - }, + let questions1 = [ { type: 'list', name: 'framework', @@ -108,54 +106,68 @@ export const init = async (options) => { }, ]; - const answers1 = await inquirer.prompt(questions); - - // filter out repos that apply to the framework - const scaffoldRepos = snapfuScaffoldRepos - .filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)) - .map((repo) => repo.full_name) - .sort(); - - const scaffolds = {}; - // map repos - scaffoldRepos.forEach((repository) => { - const [owner, repo] = repository.split('/'); - - scaffolds[repository] = { - repo, - owner, - ssh: `git@github.com:${repository}.git`, - http: `https://github.com/${repository}`, - }; - }); + const answers1 = await inquirer.prompt(questions1); + + // Step 2: Choose scaffold + let scaffoldRepos = []; + let scaffolds = {}; + + if (isLoggedIn && snapfuScaffoldRepos?.length) { + // filter out repos that apply to the framework + scaffoldRepos = snapfuScaffoldRepos + .filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)) + .map((repo) => repo.full_name) + .sort(); + + // map repos + scaffoldRepos.forEach((repository) => { + const [owner, repo] = repository.split('/'); + + scaffolds[repository] = { + repo, + owner, + ssh: `git@github.com:${repository}.git`, + http: `https://github.com/${repository}`, + }; + }); - if (user?.settings?.scaffolds?.repositories?.length) { - // add separator for clear delimiting - const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); + if (user?.settings?.scaffolds?.repositories?.length) { + // add separator for clear delimiting + const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); - scaffoldRepos.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); - scaffoldRepos.push(new inquirer.Separator('Custom Scaffolds')); + scaffoldRepos.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); + scaffoldRepos.push(new inquirer.Separator('Custom Scaffolds')); - // loop through custom repos and add to scaffoldRepos list and scaffolds mapping - user.settings.scaffolds.repositories.forEach((url) => { - // supporting HTTP only list for now - const split = url.split('/'); + // loop through custom repos and add to scaffoldRepos list and scaffolds mapping + user.settings.scaffolds.repositories.forEach((url) => { + // supporting HTTP only list for now + const split = url.split('/'); - if (split.length > 2) { - const repo = split[split.length - 1]; - const owner = split[split.length - 2]; - const repository = `${owner}/${repo}`; + if (split.length > 2) { + const repo = split[split.length - 1]; + const owner = split[split.length - 2]; + const repository = `${owner}/${repo}`; - scaffoldRepos.push(repository); + scaffoldRepos.push(repository); - scaffolds[repository] = { - repo, - owner, - ssh: `git@github.com:${repository}.git`, - http: url, - }; - } - }); + scaffolds[repository] = { + repo, + owner, + ssh: `git@github.com:${repository}.git`, + http: url, + }; + } + }); + } + } else { + // Fallback for when not logged in - use default scaffold + scaffoldRepos = [`snapfu-scaffold-${answers1.framework}`]; + scaffolds[`snapfu-scaffold-${answers1.framework}`] = { + repo: `snapfu-scaffold-${answers1.framework}`, + owner: 'searchspring', + ssh: `git@github.com:searchspring/snapfu-scaffold-${answers1.framework}.git`, + http: `https://github.com/searchspring/snapfu-scaffold-${answers1.framework}`, + }; } const questions2 = [ @@ -177,29 +189,32 @@ export const init = async (options) => { exit(1); } - try { - const contentResponse = await octokit.rest.repos.getContent({ - owner: scaffold.owner, - repo: scaffold.repo, - path: 'snapfu.config.yml', - }); - + // Fetch scaffold configuration if logged in + if (isLoggedIn && octokit) { try { - const buffer = new Buffer.from(contentResponse.data.content, 'base64'); - const fileContents = buffer.toString('ascii'); - scaffold.advanced = YAML.parse(fileContents); + const contentResponse = await octokit.rest.repos.getContent({ + owner: scaffold.owner, + repo: scaffold.repo, + path: 'snapfu.config.yml', + }); + + try { + const buffer = new Buffer.from(contentResponse.data.content, 'base64'); + const fileContents = buffer.toString('ascii'); + scaffold.advanced = YAML.parse(fileContents); + } catch (err) { + console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n`)); + exit(1); + } } catch (err) { - console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n`)); - exit(1); - } - } catch (err) { - if (err.status !== 404) { - console.log(chalk.red(`Failed to fetch snapfu.config.yml...\n`)); - exit(1); + if (err.status !== 404) { + console.log(chalk.red(`Failed to fetch snapfu.config.yml...\n`)); + exit(1); + } } } - // ask additional questions (for advanced scaffolds) + // Step 3: Ask follow up scaffold questions (if they exist) if (scaffold.advanced?.variables?.length) { let advancedQuestions = []; scaffold.advanced.variables.forEach((variable) => { @@ -213,17 +228,8 @@ export const init = async (options) => { scaffold.answers = await inquirer.prompt(advancedQuestions); } + // Step 4: Ask for siteId (as this is used as a snapfu variable when copying over scaffold) const questions3 = [ - { - type: 'list', - name: 'organization', - message: 'Please choose which github organization to create this repository in:', - choices: orgs.concat(user.login), - default: 'searchspring-implementations', - when: () => { - return orgs && orgs.length > 0; - }, - }, { type: 'input', name: 'siteId', @@ -232,125 +238,184 @@ export const init = async (options) => { return input && input.length > 0 && /^[0-9a-z]{6}$/.test(input); }, }, + ]; + const answers3 = await inquirer.prompt(questions3); + + let useGitHubRepo = false; + let repositoryAnswers = {}; + + const githubQuestions = [ { - type: 'input', - name: 'secretKey', - message: 'Please enter the secretKey as found in the SMC console (32 characters):', - validate: (input) => { - return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); - }, + type: 'confirm', + name: 'useGitHubRepo', + message: 'Would you like to create a GitHub repository? (requires login)', + default: false, }, ]; - const answers3 = await inquirer.prompt(questions3); + + const githubAnswers = await inquirer.prompt(githubQuestions); + useGitHubRepo = githubAnswers.useGitHubRepo; + + if (useGitHubRepo) { + if (!isLoggedIn) { + console.log(chalk.yellow('You must be logged in to create a GitHub repository. Please run "snapfu login" first.')); + useGitHubRepo = false; + } else { + const repoQuestions = [ + { + type: 'input', + name: 'name', + validate: (input) => { + return input && input.length > 0; + }, + message: 'Please choose the name of this repository:', + default: path.basename(dir), + }, + { + type: 'list', + name: 'organization', + message: 'Please choose which github organization to create this repository in:', + choices: orgs.concat(user.login), + default: 'searchspring-implementations', + when: () => { + return orgs && orgs.length > 0; + }, + }, + ]; + + repositoryAnswers = await inquirer.prompt(repoQuestions); + + // Step 5c: Ask for secretKey if org is implementations + if (repositoryAnswers.organization === 'searchspring-implementations') { + const secretQuestions = [ + { + type: 'input', + name: 'secretKey', + message: 'Please enter the secretKey as found in the SMC console (32 characters):', + validate: (input) => { + return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); + }, + }, + ]; + const secretAnswers = await inquirer.prompt(secretQuestions); + repositoryAnswers.secretKey = secretAnswers.secretKey; + } + } + } // combined answers - const answers = { ...answers1, ...answers2, ...answers3 }; + const answers = { ...answers1, ...answers2, ...answers3, ...repositoryAnswers }; // validate siteId and secretKey - try { - await new ConfigApi(answers.secretKey, options).validateSite({ siteId: answers.siteId }); - } catch (err) { - console.log(chalk.red('\nSite verification failed.')); - console.log(chalk.red(err)); - exit(1); + if (answers.secretKey) { + try { + await new ConfigApi(answers.secretKey, options).validateSite({ siteId: answers.siteId }); + } catch (err) { + console.log(chalk.red('\nSite verification failed.')); + console.log(chalk.red(err)); + exit(1); + } } // create local directory let folderName = await createDir(dir); - // set organization to user.login when answer is undefined (question never asked) - answers.organization = answers.organization || user.login; - - // determine if using org or userspace - let creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; - - if (options.dev) { - console.log(chalk.blueBright('\nSkipping new repo creation...')); - } else { - // create the remote repo - console.log(`\nCreating repository...`); - let exists = false; - - await octokit.repos[creationMethod]({ - org: answers.organization, - name: answers.name, - private: true, - auto_init: true, - }) - .then(() => console.log(chalk.cyan(`${answers.organization}/${answers.name}\n`))) - .then(async () => { - // giving github some time - await wait(1000); - - // getting default branch name - const response = await octokit.repos.get({ - owner: answers.organization, - repo: answers.name, - }); - - const { default_branch } = response.data; + if (useGitHubRepo && octokit) { + // set organization to user.login when answer is undefined (question never asked) + answers.organization = answers.organization || user.login; + + // determine if using org or userspace + let creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; + + if (options.dev) { + console.log(chalk.blueBright('\nSkipping new repo creation...')); + } else { + // create the remote repo + console.log(`\nCreating repository...`); + let exists = false; + + await octokit.repos[creationMethod]({ + org: answers.organization, + name: answers.name, + private: true, + auto_init: true, + }) + .then(() => console.log(chalk.cyan(`${answers.organization}/${answers.name}\n`))) + .then(async () => { + // giving github some time + await wait(1000); - if (default_branch !== DEFAULT_BRANCH) { - console.log(`Renaming default branch ${chalk.cyan(default_branch)} to ${chalk.cyan(DEFAULT_BRANCH)}\n`); - const response2 = await octokit.repos.renameBranch({ + // getting default branch name + const response = await octokit.repos.get({ owner: answers.organization, repo: answers.name, - branch: default_branch, - new_name: DEFAULT_BRANCH, }); - } - }) - .catch((err) => { - if (!err.message.includes('already exists')) { - console.log(chalk.red(err.message)); - exit(1); - } else { - console.log(chalk.yellow('*** WARNING *** repository already exists\n')); - exists = true; - } - }); - if (exists) { - let question3 = [ - { - type: 'confirm', - name: 'continue', - message: 'Do you want to continue? This may overwrite existing files in the repo.', - default: false, - }, - ]; - - let question4 = [ - { - type: 'confirm', - name: 'sure', - message: 'Are you SURE? This may overwrite existing files in the repo.', - default: false, - }, - ]; + const { default_branch } = response.data; + + if (default_branch !== DEFAULT_BRANCH) { + console.log(`Renaming default branch ${chalk.cyan(default_branch)} to ${chalk.cyan(DEFAULT_BRANCH)}\n`); + const response2 = await octokit.repos.renameBranch({ + owner: answers.organization, + repo: answers.name, + branch: default_branch, + new_name: DEFAULT_BRANCH, + }); + } + }) + .catch((err) => { + if (!err.message.includes('already exists')) { + console.log(chalk.red(err.message)); + exit(1); + } else { + console.log(chalk.yellow('*** WARNING *** repository already exists\n')); + exists = true; + } + }); - const answers3 = await inquirer.prompt(question3); - if (answers3.continue) { - const answers4 = await inquirer.prompt(question4); - if (!answers4.sure) { + if (exists) { + let question3 = [ + { + type: 'confirm', + name: 'continue', + message: 'Do you want to continue? This may overwrite existing files in the repo.', + default: false, + }, + ]; + + let question4 = [ + { + type: 'confirm', + name: 'sure', + message: 'Are you SURE? This may overwrite existing files in the repo.', + default: false, + }, + ]; + + const answers3 = await inquirer.prompt(question3); + if (answers3.continue) { + const answers4 = await inquirer.prompt(question4); + if (!answers4.sure) { + console.log(chalk.yellow('aborting...\n')); + exit(1); + } + } else { console.log(chalk.yellow('aborting...\n')); exit(1); } - } else { - console.log(chalk.yellow('aborting...\n')); - exit(1); - } - // new line - console.log(); + // new line + console.log(); + } } } - // newly create repo URLs - const repoUrlSSH = `git@github.com:${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}.git`; - const repoUrlHTTP = `https://${user.login}@github.com/${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}`; + if (useGitHubRepo && isLoggedIn && !options.dev) { + // newly create repo URLs + const creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; + const repoUrlSSH = `git@github.com:${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}.git`; + const repoUrlHTTP = `https://${user.login}@github.com/${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}`; - if (!options.dev) { try { console.log(`Cloning repository via SSH...`); await cloneAndCopyRepo(repoUrlSSH, dir, false); @@ -363,9 +428,9 @@ export const init = async (options) => { } const scaffoldVariables = { - 'snapfu.name': answers.name, + 'snapfu.name': answers.name || path.basename(dir), 'snapfu.siteId': answers.siteId, - 'snapfu.author': user.name, + 'snapfu.author': user?.name || 'Unknown', 'snapfu.framework': answers.framework, }; @@ -393,25 +458,43 @@ export const init = async (options) => { await wait(1000); // save secretKey mapping to creds.json - await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); - await setRepoSecret(options, { - siteId: answers.siteId, - secretKey: answers.secretKey, - organization: answers.organization, - name: answers.name, - dir, - }); + if (answers.secretKey) { + await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); + } - await setBranchProtection(options, { organization: answers.organization, name: answers.name }); + // Set repository secret and branch protection + if (useGitHubRepo && isLoggedIn) { + if (answers.secretKey) { + await setRepoSecret(options, { + siteId: answers.siteId, + secretKey: answers.secretKey, + organization: answers.organization, + name: answers.name, + dir, + }); + } + + await setBranchProtection(options, { organization: answers.organization, name: answers.name }); + } if (dir != cwd()) { console.log(`The ${chalk.blue(folderName)} directory has been created and initialized from ${chalk.blue(`${answers.scaffold}`)}.`); - console.log(`Get started by installing package dependencies and creating a branch:`); - console.log(chalk.grey(`\n\tcd ${folderName} && npm install && git checkout -b development\n`)); + if (useGitHubRepo) { + console.log(`Get started by installing package dependencies and creating a branch:`); + console.log(chalk.grey(`\n\tcd ${folderName} && npm install && git checkout -b development\n`)); + } else { + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tcd ${folderName} && npm install\n`)); + } } else { console.log(`Current working directory has been initialized from ${chalk.blue(`${answers.scaffold}`)}.`); - console.log(`Get started by installing package dependencies and creating a branch:`); - console.log(chalk.grey(`\n\tnpm install && git checkout -b development\n`)); + if (useGitHubRepo) { + console.log(`Get started by installing package dependencies and creating a branch:`); + console.log(chalk.grey(`\n\tnpm install && git checkout -b development\n`)); + } else { + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tnpm install\n`)); + } } } } catch (err) { diff --git a/src/recs.js b/src/recs.js index a93e8f6..c00629e 100644 --- a/src/recs.js +++ b/src/recs.js @@ -267,7 +267,7 @@ export async function listTemplates(options) { console.log(` ${chalk.white(`${type.charAt(0).toUpperCase() + type.slice(1)} Templates`)}`); // loop through each template and log details typeTemplates.map((template) => { - console.log(` ${chalk.green(template.details.name)} ${chalk.blue(`[${repository.branch}]`)}`); + console.log(` ${chalk.green(template.details.name)} ${repository.branch ? chalk.blue(`[${repository.branch}]`) : ''}`); }); } }); @@ -284,6 +284,7 @@ export async function listTemplates(options) { const list = async (secretKey, siteId = '', name = '') => { if (!secretKey) { console.log(chalk.red('Unable to list remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const remoteTemplates = await new ConfigApi(secretKey, options).getTemplates({ siteId }); @@ -362,6 +363,7 @@ export async function removeTemplate(options) { if (!secretKey) { console.log(chalk.red('Unable to archive remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } await new ConfigApi(secretKey, options).archiveTemplate({ payload, siteId }); @@ -463,6 +465,7 @@ ${invalidParam} const sync = async (template, secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const payload = buildTemplatePayload(template.details, { branch: branchName, framework: searchspring.framework }); From 209d3999a3746d5e2c9c8d795f4c7c36de630c4e Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Wed, 22 Oct 2025 11:13:25 -0400 Subject: [PATCH 3/7] feat: add prompt for Snap Templates vs Snap --- src/init.js | 733 ++++++++++++++++++++++++++-------------------------- 1 file changed, 361 insertions(+), 372 deletions(-) diff --git a/src/init.js b/src/init.js index 5858768..5c40256 100644 --- a/src/init.js +++ b/src/init.js @@ -14,6 +14,7 @@ import { ConfigApi } from './services/ConfigApi.js'; import YAML from 'yaml'; export const DEFAULT_BRANCH = 'production'; +const TEMPLATES_SCAFFOLD_NAME_IDENTIFIER = '-templates'; export const createDir = (dir) => { return new Promise((resolutionFunc, rejectionFunc) => { @@ -46,455 +47,445 @@ export const init = async (options) => { console.log(chalk.yellow(`A parameter was not provided to the init command. The current working directory will be initialized.`)); } - let octokit; - let orgs = []; - let snapfuScaffoldRepos = []; + const octokit = new Octokit({ + auth: user.token, + request: { + fetch: fetch, + }, + }); + let orgs = []; if (isLoggedIn) { - octokit = new Octokit({ - auth: user.token, - request: { - fetch: fetch, - }, - }); - orgs = await octokit.orgs.listForAuthenticatedUser().then(({ data }) => { return data.map((org) => { return org.login; }); }); + } - const fetchScaffoldRepos = async () => { - // using search modifiers - https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories - const searchOrgs = orgs - .concat(user.login) - .concat('searchspring') - .map((org) => `org:${org}`); - - let page = 0; - let per_page = 100; - let repos = []; - let response; - do { - page++; - response = await octokit.rest.search.repos({ - q: `snapfu-scaffold-+archived:false+${searchOrgs.join('+')}`, - per_page, - page, - }); + const fetchScaffoldRepos = async () => { + // using search modifiers - https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories + const searchOrgs = orgs + .concat(user.login) + .concat('searchspring') + .map((org) => `org:${org}`); + + let page = 0; + let per_page = 100; + let repos = []; + let response; + do { + page++; + response = await octokit.rest.search.repos({ + q: `snapfu-scaffold-+archived:false+${searchOrgs.join('+')}`, + per_page, + page, + }); - response.data?.items?.map((repo) => { - repos.push(repo); - }); - } while (response.data?.items.length == per_page); - return repos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-`)); - }; + response.data?.items?.map((repo) => { + repos.push(repo); + }); + } while (response.data?.items.length == per_page); + return repos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-`)); + }; + + const snapfuScaffoldRepos = await fetchScaffoldRepos(); - snapfuScaffoldRepos = await fetchScaffoldRepos(); + if (!snapfuScaffoldRepos?.length) { + console.log(chalk.red('Failed to fetch scaffolds. Please try again later or contact Athos support for assistance.')); + exit(1); } - if (isLoggedIn && !snapfuScaffoldRepos?.length) { - console.log(chalk.red('failed to fetch scaffolds...')); - } else { - let questions1 = [ - { - type: 'list', - name: 'framework', - message: "Please choose the framework you'd like to use:", - choices: ['preact'], - default: 'preact', + let questions1 = [ + { + type: 'list', + name: 'framework', + message: "Please choose the framework you'd like to use:", + choices: ['preact'], + default: 'preact', + }, + { + type: 'list', + name: 'distribution', + message: 'Would you like to use Snap or Snap Templates?', + when: () => { + // TODO: remove when templates scaffolds exist + return snapfuScaffoldRepos.some((repo) => repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); }, - ]; + choices: ['Snap', 'Snap Templates'], + default: 'Snap', + }, + ]; - const answers1 = await inquirer.prompt(questions1); + const answers1 = await inquirer.prompt(questions1); - // Step 2: Choose scaffold - let scaffoldRepos = []; - let scaffolds = {}; + // filter scaffolds based on framework and distribution + let availableScaffolds = snapfuScaffoldRepos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)); + if (answers1.distribution === 'Snap Templates') { + availableScaffolds = snapfuScaffoldRepos.filter((repo) => repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); + } else { + availableScaffolds = snapfuScaffoldRepos.filter((repo) => !repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); + } + + const scaffolds = availableScaffolds + .map((repo) => repo.full_name) + .sort() + .reduce((acc, fullName) => { + const [owner, repo] = fullName.split('/'); + acc[repo] = { + repo, + owner, + ssh: `git@github.com:${fullName}.git`, + http: `https://github.com/${fullName}`, + }; + return acc; + }, {}); + + // add additional custom scaffolds (snapfu.json advanced user) + if (isLoggedIn && user?.settings?.scaffolds?.repositories?.length) { + // add separator for clear delimiting + const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); - if (isLoggedIn && snapfuScaffoldRepos?.length) { - // filter out repos that apply to the framework - scaffoldRepos = snapfuScaffoldRepos - .filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)) - .map((repo) => repo.full_name) - .sort(); + availableScaffolds.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); + availableScaffolds.push(new inquirer.Separator('Custom Scaffolds')); - // map repos - scaffoldRepos.forEach((repository) => { - const [owner, repo] = repository.split('/'); + // loop through custom repos and add to scaffoldRepos list and scaffolds mapping + user.settings.scaffolds.repositories.forEach((url) => { + // supporting HTTP only list for now + const split = url.split('/'); + if (split.length > 2) { + const repo = split[split.length - 1]; + const owner = split[split.length - 2]; + const repository = `${owner}/${repo}`; + + availableScaffolds.push(repository); scaffolds[repository] = { repo, owner, ssh: `git@github.com:${repository}.git`, - http: `https://github.com/${repository}`, + http: url, }; - }); - - if (user?.settings?.scaffolds?.repositories?.length) { - // add separator for clear delimiting - const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); - - scaffoldRepos.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); - scaffoldRepos.push(new inquirer.Separator('Custom Scaffolds')); - - // loop through custom repos and add to scaffoldRepos list and scaffolds mapping - user.settings.scaffolds.repositories.forEach((url) => { - // supporting HTTP only list for now - const split = url.split('/'); - - if (split.length > 2) { - const repo = split[split.length - 1]; - const owner = split[split.length - 2]; - const repository = `${owner}/${repo}`; - - scaffoldRepos.push(repository); - - scaffolds[repository] = { - repo, - owner, - ssh: `git@github.com:${repository}.git`, - http: url, - }; - } - }); } - } else { - // Fallback for when not logged in - use default scaffold - scaffoldRepos = [`snapfu-scaffold-${answers1.framework}`]; - scaffolds[`snapfu-scaffold-${answers1.framework}`] = { - repo: `snapfu-scaffold-${answers1.framework}`, - owner: 'searchspring', - ssh: `git@github.com:searchspring/snapfu-scaffold-${answers1.framework}.git`, - http: `https://github.com/searchspring/snapfu-scaffold-${answers1.framework}`, - }; - } + }); + } - const questions2 = [ - { - type: 'list', - name: 'scaffold', - message: "Please choose the scaffold you'd like to use:", - choices: scaffoldRepos, - default: `snapfu-scaffold-${answers1.framework}`, - }, - ]; + const questions2 = [ + { + type: 'list', + name: 'scaffold', + message: "Please choose the scaffold you'd like to use:", + choices: availableScaffolds, + default: `snapfu-scaffold-${answers1.framework}`, + }, + ]; + + const answers2 = await inquirer.prompt(questions2); + + const scaffold = scaffolds[answers2.scaffold]; + if (!scaffold) { + console.log(chalk.red(`Failed to find the selected scaffold ${answers2.scaffold}...\n Please report this issue to Athos support.`)); + exit(1); + } - const answers2 = await inquirer.prompt(questions2); + // fetch scaffold configuration if available + try { + const contentResponse = await octokit.rest.repos.getContent({ + owner: scaffold.owner, + repo: scaffold.repo, + path: 'snapfu.config.yml', + }); - // scaffold reference - const scaffold = scaffolds[answers2.scaffold]; - if (!scaffold) { - console.log(chalk.red(`Failed to find the selected scaffold ${answers2.scaffold}...\n`)); + try { + const buffer = new Buffer.from(contentResponse.data.content, 'base64'); + const fileContents = buffer.toString('ascii'); + scaffold.advanced = YAML.parse(fileContents); + } catch (err) { + console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n Please report this issue to Athos support.`)); exit(1); } + } catch { + // do nothing - no advanced configuration available + } - // Fetch scaffold configuration if logged in - if (isLoggedIn && octokit) { - try { - const contentResponse = await octokit.rest.repos.getContent({ - owner: scaffold.owner, - repo: scaffold.repo, - path: 'snapfu.config.yml', - }); - - try { - const buffer = new Buffer.from(contentResponse.data.content, 'base64'); - const fileContents = buffer.toString('ascii'); - scaffold.advanced = YAML.parse(fileContents); - } catch (err) { - console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n`)); - exit(1); - } - } catch (err) { - if (err.status !== 404) { - console.log(chalk.red(`Failed to fetch snapfu.config.yml...\n`)); - exit(1); - } + // ask follow up scaffold questions (if they exist) + if (scaffold.advanced?.variables?.length) { + let advancedQuestions = []; + scaffold.advanced.variables.forEach((variable) => { + if (variable.name && variable.type && variable.message) { + // remove the value + delete variable.value; + advancedQuestions.push(variable); } - } + }); - // Step 3: Ask follow up scaffold questions (if they exist) - if (scaffold.advanced?.variables?.length) { - let advancedQuestions = []; - scaffold.advanced.variables.forEach((variable) => { - if (variable.name && variable.type && variable.message) { - // remove the value - delete variable.value; - advancedQuestions.push(variable); - } - }); + scaffold.answers = await inquirer.prompt(advancedQuestions); + } - scaffold.answers = await inquirer.prompt(advancedQuestions); - } + // ask for siteId (as this is used as a snapfu variable when copying over scaffold) + const questions3 = [ + { + type: 'input', + name: 'siteId', + message: 'Please enter the siteId as found in the SMC console (a1b2c3):', + validate: (input) => { + return input && input.length > 0 && /^[0-9a-z]{6}$/.test(input); + }, + }, + ]; + const answers3 = await inquirer.prompt(questions3); + + let useGitHubRepo = false; + let repositoryAnswers = {}; + + const githubQuestions = [ + { + type: 'confirm', + name: 'useGitHubRepo', + message: 'Would you like to create a GitHub repository? (requires login)', + when: () => { + return isLoggedIn; + }, + default: useGitHubRepo, + }, + ]; - // Step 4: Ask for siteId (as this is used as a snapfu variable when copying over scaffold) - const questions3 = [ + const githubAnswers = await inquirer.prompt(githubQuestions); + useGitHubRepo = githubAnswers.useGitHubRepo; + + if (useGitHubRepo) { + const repoQuestions = [ { type: 'input', - name: 'siteId', - message: 'Please enter the siteId as found in the SMC console (a1b2c3):', + name: 'name', validate: (input) => { - return input && input.length > 0 && /^[0-9a-z]{6}$/.test(input); + return input && input.length > 0; }, + message: 'Please choose the name of this repository:', + default: path.basename(dir), }, - ]; - const answers3 = await inquirer.prompt(questions3); - - let useGitHubRepo = false; - let repositoryAnswers = {}; - - const githubQuestions = [ { - type: 'confirm', - name: 'useGitHubRepo', - message: 'Would you like to create a GitHub repository? (requires login)', - default: false, + type: 'list', + name: 'organization', + message: 'Please choose which github organization to create this repository in:', + choices: orgs.concat(user.login), + default: 'searchspring-implementations', + when: () => { + return orgs && orgs.length > 0; + }, }, ]; - const githubAnswers = await inquirer.prompt(githubQuestions); - useGitHubRepo = githubAnswers.useGitHubRepo; + repositoryAnswers = await inquirer.prompt(repoQuestions); - if (useGitHubRepo) { - if (!isLoggedIn) { - console.log(chalk.yellow('You must be logged in to create a GitHub repository. Please run "snapfu login" first.')); - useGitHubRepo = false; - } else { - const repoQuestions = [ - { - type: 'input', - name: 'name', - validate: (input) => { - return input && input.length > 0; - }, - message: 'Please choose the name of this repository:', - default: path.basename(dir), - }, - { - type: 'list', - name: 'organization', - message: 'Please choose which github organization to create this repository in:', - choices: orgs.concat(user.login), - default: 'searchspring-implementations', - when: () => { - return orgs && orgs.length > 0; - }, + // Ask for secretKey if org is implementations + if (repositoryAnswers.organization === 'searchspring-implementations') { + const secretQuestions = [ + { + type: 'input', + name: 'secretKey', + message: 'Please enter the secretKey as found in the SMC console (32 characters):', + validate: (input) => { + return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); }, - ]; - - repositoryAnswers = await inquirer.prompt(repoQuestions); - - // Step 5c: Ask for secretKey if org is implementations - if (repositoryAnswers.organization === 'searchspring-implementations') { - const secretQuestions = [ - { - type: 'input', - name: 'secretKey', - message: 'Please enter the secretKey as found in the SMC console (32 characters):', - validate: (input) => { - return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); - }, - }, - ]; - const secretAnswers = await inquirer.prompt(secretQuestions); - repositoryAnswers.secretKey = secretAnswers.secretKey; - } - } + }, + ]; + const secretAnswers = await inquirer.prompt(secretQuestions); + repositoryAnswers.secretKey = secretAnswers.secretKey; } + } - // combined answers - const answers = { ...answers1, ...answers2, ...answers3, ...repositoryAnswers }; + // combined answers + const answers = { ...answers1, ...answers2, ...answers3, ...repositoryAnswers }; - // validate siteId and secretKey - if (answers.secretKey) { - try { - await new ConfigApi(answers.secretKey, options).validateSite({ siteId: answers.siteId }); - } catch (err) { - console.log(chalk.red('\nSite verification failed.')); - console.log(chalk.red(err)); - exit(1); - } + // validate siteId and secretKey + if (answers.secretKey) { + try { + await new ConfigApi(answers.secretKey, options).validateSite({ siteId: answers.siteId }); + } catch (err) { + console.log(chalk.red('\nSite verification failed.')); + console.log(chalk.red(err)); + exit(1); } + } - // create local directory - let folderName = await createDir(dir); + // create local directory + let folderName = await createDir(dir); - if (useGitHubRepo && octokit) { - // set organization to user.login when answer is undefined (question never asked) - answers.organization = answers.organization || user.login; + if (useGitHubRepo) { + // set organization to user.login when answer is undefined (question never asked) + answers.organization = answers.organization || user.login; - // determine if using org or userspace - let creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; + // determine if using org or userspace + let creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; - if (options.dev) { - console.log(chalk.blueBright('\nSkipping new repo creation...')); - } else { - // create the remote repo - console.log(`\nCreating repository...`); - let exists = false; + if (options.dev) { + console.log(chalk.blueBright('\nSkipping new repo creation...')); + } else { + // create the remote repo + console.log(`\nCreating repository...`); + let exists = false; + + await octokit.repos[creationMethod]({ + org: answers.organization, + name: answers.name, + private: true, + auto_init: true, + }) + .then(() => console.log(chalk.cyan(`${answers.organization}/${answers.name}\n`))) + .then(async () => { + // giving github some time + await wait(1000); + + // getting default branch name + const response = await octokit.repos.get({ + owner: answers.organization, + repo: answers.name, + }); - await octokit.repos[creationMethod]({ - org: answers.organization, - name: answers.name, - private: true, - auto_init: true, - }) - .then(() => console.log(chalk.cyan(`${answers.organization}/${answers.name}\n`))) - .then(async () => { - // giving github some time - await wait(1000); + const { default_branch } = response.data; - // getting default branch name - const response = await octokit.repos.get({ + if (default_branch !== DEFAULT_BRANCH) { + console.log(`Renaming default branch ${chalk.cyan(default_branch)} to ${chalk.cyan(DEFAULT_BRANCH)}\n`); + await octokit.repos.renameBranch({ owner: answers.organization, repo: answers.name, + branch: default_branch, + new_name: DEFAULT_BRANCH, }); + } + }) + .catch((err) => { + if (!err.message.includes('already exists')) { + console.log(chalk.red(err.message)); + exit(1); + } else { + console.log(chalk.yellow('*** WARNING *** repository already exists\n')); + exists = true; + } + }); - const { default_branch } = response.data; - - if (default_branch !== DEFAULT_BRANCH) { - console.log(`Renaming default branch ${chalk.cyan(default_branch)} to ${chalk.cyan(DEFAULT_BRANCH)}\n`); - const response2 = await octokit.repos.renameBranch({ - owner: answers.organization, - repo: answers.name, - branch: default_branch, - new_name: DEFAULT_BRANCH, - }); - } - }) - .catch((err) => { - if (!err.message.includes('already exists')) { - console.log(chalk.red(err.message)); - exit(1); - } else { - console.log(chalk.yellow('*** WARNING *** repository already exists\n')); - exists = true; - } - }); + if (exists) { + let question3 = [ + { + type: 'confirm', + name: 'continue', + message: 'Do you want to continue? This may overwrite existing files in the repo.', + default: false, + }, + ]; - if (exists) { - let question3 = [ - { - type: 'confirm', - name: 'continue', - message: 'Do you want to continue? This may overwrite existing files in the repo.', - default: false, - }, - ]; - - let question4 = [ - { - type: 'confirm', - name: 'sure', - message: 'Are you SURE? This may overwrite existing files in the repo.', - default: false, - }, - ]; - - const answers3 = await inquirer.prompt(question3); - if (answers3.continue) { - const answers4 = await inquirer.prompt(question4); - if (!answers4.sure) { - console.log(chalk.yellow('aborting...\n')); - exit(1); - } - } else { + let question4 = [ + { + type: 'confirm', + name: 'sure', + message: 'Are you SURE? This may overwrite existing files in the repo.', + default: false, + }, + ]; + + const answers3 = await inquirer.prompt(question3); + if (answers3.continue) { + const answers4 = await inquirer.prompt(question4); + if (!answers4.sure) { console.log(chalk.yellow('aborting...\n')); exit(1); } - - // new line - console.log(); + } else { + console.log(chalk.yellow('aborting...\n')); + exit(1); } - } - } - if (useGitHubRepo && isLoggedIn && !options.dev) { - // newly create repo URLs - const creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; - const repoUrlSSH = `git@github.com:${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}.git`; - const repoUrlHTTP = `https://${user.login}@github.com/${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}`; - - try { - console.log(`Cloning repository via SSH...`); - await cloneAndCopyRepo(repoUrlSSH, dir, false); - console.log(`${chalk.cyan(repoUrlSSH)}\n`); - } catch (err) { - console.log(`SSH authentication failed. Cloning repository via HTTPS...`); - await cloneAndCopyRepo(repoUrlHTTP, dir, false); - console.log(`${chalk.cyan(repoUrlHTTP)}\n`); + // new line + console.log(); } } + } - const scaffoldVariables = { - 'snapfu.name': answers.name || path.basename(dir), - 'snapfu.siteId': answers.siteId, - 'snapfu.author': user?.name || 'Unknown', - 'snapfu.framework': answers.framework, - }; - - // add advanced scaffold variables - if (scaffold.answers) { - Object.keys(scaffold.answers).forEach((key) => { - const value = scaffold.answers[key]; - scaffoldVariables[`snapfu.variables.${key}`] = value; - }); - } + if (useGitHubRepo && isLoggedIn && !options.dev) { + // newly create repo URLs + const creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; + const repoUrlSSH = `git@github.com:${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}.git`; + const repoUrlHTTP = `https://${user.login}@github.com/${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}`; try { - console.log(`Cloning scaffolding into ${dir} via SSH...`); - await cloneAndCopyRepo(scaffold.ssh, dir, true, scaffoldVariables); - console.log(`${chalk.cyan(scaffold.ssh)}\n`); + console.log(`Cloning repository via SSH...`); + await cloneAndCopyRepo(repoUrlSSH, dir, false); + console.log(`${chalk.cyan(repoUrlSSH)}\n`); } catch (err) { - console.log(`SSH authentication failed. Cloning scaffolding into ${dir} via HTTPS...`); - await cloneAndCopyRepo(scaffold.http, dir, true, scaffoldVariables); - console.log(`${chalk.cyan(scaffold.http)}\n`); + console.log(`SSH authentication failed. Cloning repository via HTTPS...`); + await cloneAndCopyRepo(repoUrlHTTP, dir, false); + console.log(`${chalk.cyan(repoUrlHTTP)}\n`); } + } + + const scaffoldVariables = { + 'snapfu.name': answers.name, + 'snapfu.siteId': answers.siteId, + 'snapfu.author': user?.name || 'Unknown', + 'snapfu.framework': answers.framework, + }; - // waiting here due to copyPromise function resolving before scaffold is actually copied - // TODO: look into why ncp does not like our filtering (does not resolve promise in callback) - // wait... - await wait(1000); + // add advanced scaffold variables + if (scaffold.answers) { + Object.keys(scaffold.answers).forEach((key) => { + const value = scaffold.answers[key]; + scaffoldVariables[`snapfu.variables.${key}`] = value; + }); + } - // save secretKey mapping to creds.json + try { + console.log(`Cloning scaffolding into ${dir} via SSH...`); + await cloneAndCopyRepo(scaffold.ssh, dir, true, scaffoldVariables); + console.log(`${chalk.cyan(scaffold.ssh)}\n`); + } catch (err) { + console.log(`SSH authentication failed. Cloning scaffolding into ${dir} via HTTPS...`); + await cloneAndCopyRepo(scaffold.http, dir, true, scaffoldVariables); + console.log(`${chalk.cyan(scaffold.http)}\n`); + } + + // waiting here due to copyPromise function resolving before scaffold is actually copied + // TODO: look into why ncp does not like our filtering (does not resolve promise in callback) + // wait... + await wait(1000); + + // save secretKey mapping to creds.json + if (answers.secretKey) { + await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); + } + + // Set repository secret and branch protection + if (useGitHubRepo && isLoggedIn) { if (answers.secretKey) { - await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); + await setRepoSecret(options, { + siteId: answers.siteId, + secretKey: answers.secretKey, + organization: answers.organization, + name: answers.name, + dir, + }); } - // Set repository secret and branch protection - if (useGitHubRepo && isLoggedIn) { - if (answers.secretKey) { - await setRepoSecret(options, { - siteId: answers.siteId, - secretKey: answers.secretKey, - organization: answers.organization, - name: answers.name, - dir, - }); - } + await setBranchProtection(options, { organization: answers.organization, name: answers.name }); + } - await setBranchProtection(options, { organization: answers.organization, name: answers.name }); + if (dir != cwd()) { + console.log(`The ${chalk.blue(folderName)} directory has been created and initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + if (useGitHubRepo) { + console.log(`Get started by installing package dependencies and creating a branch:`); + console.log(chalk.grey(`\n\tcd ${folderName} && npm install && git checkout -b development\n`)); + } else { + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tcd ${folderName} && npm install\n`)); } - - if (dir != cwd()) { - console.log(`The ${chalk.blue(folderName)} directory has been created and initialized from ${chalk.blue(`${answers.scaffold}`)}.`); - if (useGitHubRepo) { - console.log(`Get started by installing package dependencies and creating a branch:`); - console.log(chalk.grey(`\n\tcd ${folderName} && npm install && git checkout -b development\n`)); - } else { - console.log(`Get started by installing package dependencies:`); - console.log(chalk.grey(`\n\tcd ${folderName} && npm install\n`)); - } + } else { + console.log(`Current working directory has been initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + if (useGitHubRepo) { + console.log(`Get started by installing package dependencies and creating a branch:`); + console.log(chalk.grey(`\n\tnpm install && git checkout -b development\n`)); } else { - console.log(`Current working directory has been initialized from ${chalk.blue(`${answers.scaffold}`)}.`); - if (useGitHubRepo) { - console.log(`Get started by installing package dependencies and creating a branch:`); - console.log(chalk.grey(`\n\tnpm install && git checkout -b development\n`)); - } else { - console.log(`Get started by installing package dependencies:`); - console.log(chalk.grey(`\n\tnpm install\n`)); - } + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tnpm install\n`)); } } } catch (err) { @@ -515,7 +506,7 @@ export const setBranchProtection = async function (options, details) { const { organization, name } = details; - if (!options.dev && organization && name) { + if (!options.dev && organization === 'searchspring-implementations' && name) { console.log(`Setting branch protection for ${DEFAULT_BRANCH} in ${organization}/${name}...`); try { @@ -579,8 +570,6 @@ export const setBranchProtection = async function (options, details) { console.log(chalk.red(`failed to create branch protection rule`)); console.log(chalk.red(err)); } - } else { - console.log(chalk.yellow('skipping creation of branch protection')); } console.log(); // new line spacing }; From 833c680dd78299ca56cf3324ea68a0b90ed7478a Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Thu, 23 Oct 2025 10:53:32 -0400 Subject: [PATCH 4/7] refactor: pr feedback --- src/help.js | 2 +- src/init.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/help.js b/src/help.js index ba89ecc..580c18a 100644 --- a/src/help.js +++ b/src/help.js @@ -12,7 +12,7 @@ These are the snapfu commands used in various situations ${chalk.whiteBright('recs')} Recommendation template management ${chalk.whiteBright('secrets')} Project secret management ${chalk.whiteBright('patch')} Apply patches to update project - ${chalk.whiteBright('login')} Oauths with github to retrieve additional templates and create repositories when using the init command + ${chalk.whiteBright('login')} Oauths with github to retrieve additional scaffolds and create repositories when using the init command ${chalk.whiteBright('logout')} Removes login credentials ${chalk.whiteBright('org-access')} Review and change organization access for the tool ${chalk.whiteBright('whoami')} Shows the current user diff --git a/src/init.js b/src/init.js index 5c40256..0c1fe40 100644 --- a/src/init.js +++ b/src/init.js @@ -1,6 +1,6 @@ import os from 'os'; import { exit, cwd } from 'process'; -import { readdirSync, readFileSync, existsSync, mkdirSync, promises as fs } from 'fs'; +import { readdirSync, existsSync, mkdirSync, promises as fs } from 'fs'; import path from 'path'; import chalk from 'chalk'; import { Octokit } from '@octokit/rest'; @@ -242,11 +242,11 @@ export const init = async (options) => { { type: 'confirm', name: 'useGitHubRepo', - message: 'Would you like to create a GitHub repository? (requires login)', + message: 'Would you like to create a GitHub repository?', when: () => { return isLoggedIn; }, - default: useGitHubRepo, + default: true, }, ]; @@ -420,10 +420,10 @@ export const init = async (options) => { } const scaffoldVariables = { - 'snapfu.name': answers.name, + 'snapfu.name': answers.name || path.basename(dir), 'snapfu.siteId': answers.siteId, 'snapfu.author': user?.name || 'Unknown', - 'snapfu.framework': answers.framework, + 'snapfu.framework': answers.framework + answers.distribution === 'Snap Templates' ? TEMPLATES_SCAFFOLD_NAME_IDENTIFIER : '', }; // add advanced scaffold variables From 7f461f11c76b52a74176a617ac6d0f2debccf328 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Thu, 23 Oct 2025 12:07:30 -0400 Subject: [PATCH 5/7] undo distribution in snapfu.framework variable --- src/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.js b/src/init.js index 0c1fe40..e581395 100644 --- a/src/init.js +++ b/src/init.js @@ -423,7 +423,7 @@ export const init = async (options) => { 'snapfu.name': answers.name || path.basename(dir), 'snapfu.siteId': answers.siteId, 'snapfu.author': user?.name || 'Unknown', - 'snapfu.framework': answers.framework + answers.distribution === 'Snap Templates' ? TEMPLATES_SCAFFOLD_NAME_IDENTIFIER : '', + 'snapfu.framework': answers.framework, }; // add advanced scaffold variables From 03cefbbbf374d4dc528c08a92114697259676cb8 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Thu, 23 Oct 2025 15:00:11 -0400 Subject: [PATCH 6/7] update README.md --- README.md | 149 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8e303e8..03cd149 100644 --- a/README.md +++ b/README.md @@ -12,45 +12,151 @@ Snapfu is the scaffolding command line tool for the Searchspring Snap SDK. This npm install -g snapfu ``` -## Login +## Usage -Login to access your Github organizations - the following command will open a browser window -to give snapfu access to your Github organizations and to be able to create repositories in subsequent steps. +```bash +snapfu [--options] +``` + +## Commands + +### `init` - Create a new snap project +Creates a new snap project (optional directory) + +```bash +snapfu init +``` + +This command will: +- Download scaffolding files +- Create and initialize a repository in the Github organization you selected +- Populate a Github secret with the provided `secretKey` + +You will need your `siteId` and `secretKey` from the SMC before you run this command. + +### `badges` - Badge template management +Manage badge templates for your project + +```bash +snapfu badges [--options] +``` + +**Subcommands:** +- `init` - Initialize badge template in current project +- `list [local | remote]` - Display list of badge templates (local or remote) +- `archive ` - Remove remote badge template + - `--secret-key ` - Secret key for authentication +- `sync [ | locations.json]` - Synchronize badge template and parameters with remote + - `--secret-key ` - Secret key for authentication + +### `recs` - Recommendation template management +Manage recommendation templates for your project + +```bash +snapfu recs [--options] +``` + +**Subcommands:** +- `init` - Initialize recommendation template in current project +- `list [local | remote]` - Display list of recommendation templates (local or remote) +- `archive ` - Remove remote recommendation template (optional branch) + - `--secret-key ` - Secret key for authentication +- `sync ` - Synchronize recommendation template and parameters with remote (optional branch) + - `--secret-key ` - Secret key for authentication + +### `secrets` - Project secret management +Manage secrets in your snap project + +```bash +snapfu secrets [--options] +``` + +**Subcommands:** +- `add` - Adds secrets to snap project +- `update` - Update secrets in snap project +- `verify` - Verify secrets in snap project + +### `patch` - Apply patches to update project +Apply patches to update your project + +```bash +snapfu patch [--options] +``` + +**Subcommands:** +- `apply` - Apply patch version (version or latest) +- `list` - List available versions for project +- `fetch` - Fetch latest versions of patches + +### `login` - OAuth with GitHub +OAuths with GitHub to retrieve additional scaffolds and create repositories when using the init command ```bash snapfu login ``` -## Init +### `logout` - Remove login credentials +Removes login credentials -Create your website with the init command. Init will gather some information about the kind -of Snap project you wish to create. You will need your `siteId` and `secretKey` from the SMC before you run this command. This command will: +```bash +snapfu logout +``` -- download scaffolding files -- create and initialize a repository in the Github organization you selected -- populate a Github secret with the provided `secretKey` +### `org-access` - Review organization access +Review and change organization access for the tool ```bash -snapfu init my-awesome-website +snapfu org-access ``` - +### `whoami` - Show current user +Shows the current user -## Run it +```bash +snapfu whoami +``` -Now you can run the project with your standard `npm` tooling. +### `about` - Show versioning +Shows versioning information ```bash -cd my-awesome-website -npm install -npm run dev +snapfu about ``` -See the `package.json` for other npm commands. +### `help` - Display help text +Display help text (optional command) + +```bash +snapfu help [] +``` + +## Getting Started + +1. **Install snapfu globally:** + ```bash + npm install -g snapfu + ``` + +2. **Login (optional):** + ```bash + snapfu login + ``` + +3. **Create a new project:** + ```bash + snapfu init my-awesome-website + ``` + +4. **Run the project:** + ```bash + cd my-awesome-website + npm install + npm run dev + ``` ## Deployment -This tool integrates with the Searchspring build and deploy process. In order to take advantage of this you must select searchspring-implementations as your organizaiton during init. +This tool integrates with the Searchspring build and deploy process. In order to take advantage of this you must have access to the `searchspring-implementations` Github organization and select it during init command. (Requires login & invitation to the organization upon request). The tool uses Github actions to copy files to our AWS S3 backed CDN (Cloudfront). @@ -69,17 +175,14 @@ https://snapui.searchspring.io//my-branch/bundle.js ## Deploying to other places -You can modify the file `deploy.yml` in your generated project under `my-awesome-website/.github/workflows/deploy.yml` -to complete different actions if you don't want to use the Searchspring build process or don't have access to it. +You can modify the file `deploy.yml` in your generated project under `my-awesome-website/.github/workflows/deploy.yml` to complete different actions if you don't want to use the Searchspring build process or don't have access to it. ### SCP - Deploy the built artifacts using `scp`. [https://github.com/marketplace/actions/scp-command-to-transfer-files](https://github.com/marketplace/actions/scp-command-to-transfer-files) ### Google Cloud - Deploy to GCP using `gcloud`. [https://github.com/marketplace/actions/setup-gcloud-environment](https://github.com/marketplace/actions/setup-gcloud-environment) ### SFTP - Deploy a built artifacts through SFTP. [https://github.com/marketplace/actions/sftp-deploy](https://github.com/marketplace/actions/sftp-deploy) + From 8ecc5968e8edf24aae3edfdcd4db1d90164a8dea Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Thu, 23 Oct 2025 16:19:29 -0400 Subject: [PATCH 7/7] docs: update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 03cd149..e4572aa 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,6 @@ Creates a new snap project (optional directory) snapfu init ``` -This command will: -- Download scaffolding files -- Create and initialize a repository in the Github organization you selected -- Populate a Github secret with the provided `secretKey` - -You will need your `siteId` and `secretKey` from the SMC before you run this command. ### `badges` - Badge template management Manage badge templates for your project