UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

245 lines 8.12 kB
import inquirer from 'inquirer'; import composer from '../auth/composer.js'; import replaceVars from './replaceVars.js'; import createArtifactBucket from './createArtifactBucket.js'; import createSecrets from './createSecrets.js'; import createTaskQueue from './createTaskQueue.js'; import addDefaultRoles from './addDefaultRoles.js'; import createLocalEnvFile from './createLocalEnvFile.js'; import createRealtimeDatabase from './createRealtimeDatabase.js'; import createFirestoreDatabase from './createFirestoreDatabase.js'; import { logger } from '../../utils/logger.js'; const DEFAULT_INIT_SUCCESS_MESSAGE = 'Atlas initialized successfully.'; const INIT_STEP_DEFINITIONS = { intro: { dependencyKey: 'intro', label: 'Confirm prerequisites' }, replaceVars: { dependencyKey: 'replaceVars', label: 'Replace template variables' }, composer: { dependencyKey: 'composer', label: 'Authenticate Composer access' }, addDefaultRoles: { dependencyKey: 'addDefaultRoles', label: 'Create default roles' }, createSecrets: { dependencyKey: 'createSecrets', label: 'Create required secrets' }, createArtifactBucket: { dependencyKey: 'createArtifactBucket', label: 'Create the shared artifact bucket' }, createTaskQueue: { dependencyKey: 'createTaskQueue', label: 'Create required task queues' }, createLocalEnvFile: { dependencyKey: 'createLocalEnvFile', label: 'Create the local env file' }, createFirestoreDatabase: { dependencyKey: 'createFirestoreDatabase', label: 'Create Firestore databases' }, createRealtimeDatabase: { dependencyKey: 'createRealtimeDatabase', label: 'Create the Realtime Database instance' }, outro: { dependencyKey: 'outro', label: 'Review follow-up steps' } }; const DEFAULT_INIT_STEP_KEYS = ['intro', 'replaceVars', 'composer', 'addDefaultRoles', 'createSecrets', 'createArtifactBucket', 'createTaskQueue', 'createLocalEnvFile', 'createFirestoreDatabase', 'createRealtimeDatabase', 'outro']; const PARTIAL_INIT_STEP_OPTIONS = [{ optionKey: 'secrets', stepKeys: ['createSecrets'] }, { optionKey: 'localEnv', stepKeys: ['createLocalEnvFile'] }, { optionKey: 'database', stepKeys: ['createFirestoreDatabase', 'createRealtimeDatabase'] }, { optionKey: 'taskQueue', stepKeys: ['createTaskQueue'] }, { optionKey: 'artifactBucket', stepKeys: ['createArtifactBucket'] }]; const PRE_FLIGHT_CHECKLIST = ['Create a new repository from atlas-project-template: https://github.com/new?template_name=atlas-project-template&template_owner=limebooth', 'Run this command from the root of your project.', 'Install the latest version of the Atlas CLI.', 'Prepare production and development Google Cloud projects with billing enabled.', 'Install and configure the Google Cloud SDK: https://cloud.google.com/sdk/docs/install', 'Confirm you can create resources in the Google Cloud projects.', 'Create production and development Firebase projects: https://console.firebase.google.com/', 'Create Firestore databases for production and development.', 'Create a Packagist vendor for Composer packages: https://packagist.com/orgs/puls/customers']; const NEXT_STEPS = ['Create a new Firebase App for the production and development environments.', 'Create a new user in Identity Platform: https://console.cloud.google.com/customer-identity/providers', 'Validate that all required secrets exist in Google Cloud Secret Manager.', 'Run atlas auth print-developer-token your-email@puls.be to generate a developer token.', 'Run atlas i app to install the required packages.', 'Run atlas start to start the development server.']; const createNumberedEntries = entries => entries.map((entry, index) => `${index + 1}. ${entry}`); const getInitStepLabel = stepKey => INIT_STEP_DEFINITIONS[stepKey]?.label ?? stepKey; export const resolveInitStepKeys = (options = {}) => { const hasSelectedPartialSteps = PARTIAL_INIT_STEP_OPTIONS.some(({ optionKey }) => options[optionKey]); if (!hasSelectedPartialSteps) { return [...DEFAULT_INIT_STEP_KEYS]; } return PARTIAL_INIT_STEP_OPTIONS.flatMap(({ optionKey, stepKeys }) => options[optionKey] ? stepKeys : []); }; export const createIntroStep = (dependencies = {}) => { const { loggerImpl = logger, promptImpl = inquirer.prompt } = dependencies; return async () => { loggerImpl.break(); loggerImpl.banner('Puls Atlas Project Initialization'); loggerImpl.break(); loggerImpl.section('Pre-flight checklist', createNumberedEntries(PRE_FLIGHT_CHECKLIST), { bullet: ' ', severity: 'info' }); loggerImpl.break(); const { shouldContinue } = await promptImpl([{ type: 'confirm', name: 'shouldContinue', message: 'Continue with project initialization?', default: true }]); if (!shouldContinue) { loggerImpl.warning('Initialization cancelled by the user.'); return false; } return true; }; }; export const createOutroStep = (dependencies = {}) => { const { loggerImpl = logger } = dependencies; return () => { loggerImpl.break(); loggerImpl.section('Next steps', createNumberedEntries(NEXT_STEPS), { bullet: ' ', severity: 'info' }); loggerImpl.break(); }; }; const outputInitPlanSummary = ({ loggerImpl = logger, stepKeys }) => { const isFullInitialization = stepKeys.includes('intro'); loggerImpl.summary('Initialization plan', [{ label: 'Mode', value: isFullInitialization ? 'Full project bootstrap' : 'Selected resources only', tone: 'accent' }, { label: 'Step count', value: String(stepKeys.length), tone: 'warning' }], { spacing: 'after' }); loggerImpl.section('Selected steps', stepKeys.map(getInitStepLabel), { severity: 'info' }); loggerImpl.break(); }; const outputInitOutcomeSummary = ({ loggerImpl = logger, stepKeys }) => { loggerImpl.summary('Initialization outcome', [{ label: 'Status', value: 'completed', tone: 'success' }, { label: 'Executed steps', value: String(stepKeys.length), tone: 'warning' }], { spacing: 'after' }); }; export const resolveInitSteps = (options = {}, dependencies = {}) => { const resolvedDependencies = { addDefaultRoles, composer, createArtifactBucket, createFirestoreDatabase, createLocalEnvFile, createRealtimeDatabase, createSecrets, createTaskQueue, replaceVars, ...dependencies }; return resolveInitStepKeys(options).map(stepKey => { const dependencyKey = INIT_STEP_DEFINITIONS[stepKey]?.dependencyKey; const step = resolvedDependencies[dependencyKey]; if (typeof step !== 'function') { throw new Error(`Init step ${stepKey} is not configured.`); } return step; }); }; export const runInitSteps = async steps => { for (const step of steps) { const shouldContinue = await step(); if (shouldContinue === false) { return false; } } return true; }; export default async (options = {}, dependencies = {}) => { const { logger: loggerImpl = logger, prompt: promptImpl = inquirer.prompt } = dependencies; const resolvedDependencies = { ...dependencies, intro: dependencies.intro ?? createIntroStep({ loggerImpl, promptImpl }), logger: loggerImpl, outro: dependencies.outro ?? createOutroStep({ loggerImpl }) }; const stepKeys = resolveInitStepKeys(options); const steps = resolveInitSteps(options, resolvedDependencies); outputInitPlanSummary({ loggerImpl, stepKeys }); try { const isCompleted = await runInitSteps(steps); if (!isCompleted) { return false; } } catch (error) { if (error instanceof Error && error.name === 'ExitPromptError') { loggerImpl.error('Initialization cancelled by the user.', { exit: true, exitCode: 130 }); return false; } throw error; } outputInitOutcomeSummary({ loggerImpl, stepKeys }); loggerImpl.success(DEFAULT_INIT_SUCCESS_MESSAGE); return true; };