diff --git a/package.json b/package.json index 8d6821c..4e832ef 100644 --- a/package.json +++ b/package.json @@ -57,5 +57,10 @@ }, "keywords": [], "author": "", - "license": "ISC" + "license": "ISC", + "pnpm": { + "patchedDependencies": { + "ansi-colors@4.1.3": "patches/ansi-colors@4.1.3.patch" + } + } } diff --git a/packages/cli/command-prompts/getProjectNamePrompt.ts b/packages/cli/command-prompts/getProjectNamePrompt.ts index 63a28fa..b5fd376 100644 --- a/packages/cli/command-prompts/getProjectNamePrompt.ts +++ b/packages/cli/command-prompts/getProjectNamePrompt.ts @@ -1,13 +1,19 @@ -import inquirer from 'inquirer'; +import chalk from 'chalk'; +import Enquirer from 'enquirer'; +import { CHECK_MARK_COLOR, LEFT_PADDING } from 'stplr-utils'; -export const getProjectNamePrompt = async (): Promise => - ( - await inquirer.prompt([ - { - type: 'input', - name: 'name', - message: 'What is your project named?', - default: 'my-stapled-app', - }, - ]) - ).name; +export const getProjectNamePrompt = async (): Promise => { + const enquirer = new Enquirer(); + const response = (await enquirer.prompt({ + type: 'input', + name: 'name', + message: chalk.whiteBright('What is your project named?'), + initial: 'my-stapled-app', + prefix: LEFT_PADDING, + format(value) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; + }, + })) as { name: string }; + + return response.name; +}; diff --git a/packages/cli/command-prompts/overwriteDirectoryPrompt.ts b/packages/cli/command-prompts/overwriteDirectoryPrompt.ts index 4530cb6..a7f9791 100644 --- a/packages/cli/command-prompts/overwriteDirectoryPrompt.ts +++ b/packages/cli/command-prompts/overwriteDirectoryPrompt.ts @@ -1,4 +1,6 @@ -import inquirer from 'inquirer'; +import chalk from 'chalk'; +import Enquirer from 'enquirer'; +import { CHECK_MARK_COLOR, LEFT_PADDING } from 'stplr-utils'; /** * Prompts the user to confirm whether they want to overwrite an existing project directory. @@ -8,12 +10,18 @@ import inquirer from 'inquirer'; * **/ -export const overwriteDirectoryPrompt = async (projectName: string): Promise<{ overwrite: boolean }> => - await inquirer.prompt([ - { - type: 'confirm', - name: 'overwrite', - message: `The directory "${projectName}" already exists. Do you want to overwrite it?`, - default: false, +export const overwriteDirectoryPrompt = async (projectName: string): Promise<{ overwrite: boolean }> => { + const enquirer = new Enquirer(); + const response = (await enquirer.prompt({ + type: 'confirm', + name: 'overwrite', + message: chalk.whiteBright(`The directory "${projectName}" already exists. Do you want to overwrite it?`), + initial: false, + prefix: LEFT_PADDING, + format(value) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; }, - ]); + })) as { overwrite: boolean }; + + return response; +}; diff --git a/packages/cli/command-prompts/shouldUsePayloadPrompt.ts b/packages/cli/command-prompts/shouldUsePayloadPrompt.ts index 4d3e9e8..aae8c5d 100644 --- a/packages/cli/command-prompts/shouldUsePayloadPrompt.ts +++ b/packages/cli/command-prompts/shouldUsePayloadPrompt.ts @@ -1,4 +1,6 @@ -import inquirer from 'inquirer'; +import chalk from 'chalk'; +import Enquirer from 'enquirer'; +import { CHECK_MARK_COLOR, LEFT_PADDING } from 'stplr-utils'; /** * Prompts the user to confirm whether they want to overwrite an existing project directory. @@ -8,12 +10,18 @@ import inquirer from 'inquirer'; * **/ -export const shouldUsePayloadPrompt = async (): Promise<{ usePayload: boolean }> => - await inquirer.prompt([ - { - type: 'confirm', - name: 'usePayload', - message: 'Would you like to add Payload to your app?', - default: true, +export const shouldUsePayloadPrompt = async (): Promise<{ usePayload: boolean }> => { + const payloadEnquirer = new Enquirer(); + const response = (await payloadEnquirer.prompt({ + type: 'confirm', + name: 'usePayload', + message: chalk.whiteBright('Would you like to use Payload?'), + initial: true, // Default value + prefix: LEFT_PADDING, // Removes the default '?' prefix + format(value) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; }, - ]); + })) as { usePayload: boolean }; + + return response; +}; diff --git a/packages/cli/command-prompts/unfinishedProjectsChoicePrompt.ts b/packages/cli/command-prompts/unfinishedProjectsChoicePrompt.ts index 1a74cda..105697b 100644 --- a/packages/cli/command-prompts/unfinishedProjectsChoicePrompt.ts +++ b/packages/cli/command-prompts/unfinishedProjectsChoicePrompt.ts @@ -1,7 +1,15 @@ -import inquirer from 'inquirer'; +import Enquirer from 'enquirer'; + +import chalk from 'chalk'; +import { CHECK_MARK_COLOR, LEFT_PADDING, QUESTION_MARK } from 'stplr-utils'; import { ProjectChoice, UnfinishedProject } from '../utils/findUnfinishedProjects'; export type UnfinishedProjectsChoiceAnswers = { + resume: boolean; + unfinishedSelectedProject: string; +}; + +export type UnfinishedProjectsChoiceResponse = { resume: boolean; unfinishedSelectedProject: UnfinishedProject; }; @@ -18,21 +26,67 @@ export type UnfinishedProjectsChoiceAnswers = { export const unfinishedProjectsChoice = async ( unfinishedProjects: UnfinishedProject[], projectChoices: ProjectChoice[], -): Promise => - await inquirer.prompt([ - { - type: 'confirm', - name: 'resume', - message: `We found the following unfinished project(s):\n${unfinishedProjects - .map((p) => `- ${p.projectName}`) - .join('\n')}\nWould you like to resume one of them?`, - default: true, +): Promise => { + const enquirer = new Enquirer(); + const formattedProjectChoices = projectChoices.map((choice) => { + return { + name: choice.name, + value: choice.name, + message: chalk.whiteBright(choice.name), + }; + }); + + // we might need to create custom prompt format like in: https://www.npmjs.com/package/enquirer#-custom-prompts + const shouldResume = (await enquirer.prompt({ + type: 'confirm', + name: 'resume', + message: chalk.whiteBright( + `We found the following unfinished project(s):\n${unfinishedProjects + .map((p) => `${LEFT_PADDING} - ${p.projectName}`) + .join('\n')}\n ${LEFT_PADDING}${QUESTION_MARK} Would you like to resume one of them?`, + ), + initial: true, + prefix: LEFT_PADDING, + format(value) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; + }, + })) as { resume: boolean }; + + if (!shouldResume.resume) { + return { + resume: false, + unfinishedSelectedProject: unfinishedProjects[0], + }; + } + + const selectProjectAnswer = (await enquirer.prompt({ + type: 'select', + name: 'unfinishedSelectedProject', + message: chalk.whiteBright('Select a project to resume:'), + choices: formattedProjectChoices, + prefix: ' ' + LEFT_PADDING + QUESTION_MARK, + // use it only when we want to resume a project + skip: (state: unknown) => { + if (typeof state === 'object' && state !== null && 'resume' in state) { + console.log('state', state); + return !(state as UnfinishedProjectsChoiceAnswers).resume; + } + console.log('state', state); + return false; // default fallback if not sure }, - { - type: 'list', - name: 'unfinishedSelectedProject', - message: 'Select a project to resume:', - choices: projectChoices, - when: (answers) => answers.resume && unfinishedProjects.length > 1, + format(value) { + return chalk.hex(CHECK_MARK_COLOR)(value); }, - ]); + })) as UnfinishedProjectsChoiceAnswers; + + const selectedProject = unfinishedProjects.find( + (project) => project.projectName === selectProjectAnswer.unfinishedSelectedProject, + ); + + const response = { + resume: selectProjectAnswer.resume, + unfinishedSelectedProject: selectedProject, + } as UnfinishedProjectsChoiceResponse; + + return response; +}; diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 9d2dbfe..c76708c 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -1,22 +1,16 @@ #!/usr/bin/env node -import fs from 'fs'; import chalk from 'chalk'; import { Command } from 'commander'; +import fs from 'fs'; import { createProject } from 'stplr-core'; -import { - checkAuthentication, - checkTools, - displayHeader, - findUnfinishedProjects, - getProjectChoices, - UnfinishedProject, -} from './utils'; +import { logger } from 'stplr-utils'; import { getProjectNamePrompt, overwriteDirectoryPrompt, shouldUsePayloadPrompt, unfinishedProjectsChoice, } from './command-prompts'; +import { checkAuthentication, checkTools, findUnfinishedProjects, getProjectChoices, UnfinishedProject } from './utils'; interface Flags { deploy?: boolean; @@ -33,7 +27,7 @@ program ) .version('0.1.0') .hook('preAction', () => { - displayHeader(); + logger.displayHeader(); }) .option('-n, --name ', 'Set the name of the project') .option( @@ -53,6 +47,7 @@ const createAction = async (options: Flags) => { const unfinishedProjects: UnfinishedProject[] = findUnfinishedProjects(currentDir); + logger.withLabel('dir', 'Your project name and location'); // If no project name is provided, and there are unfinished projects, we prompt the user to resume one of them if (!options.name && unfinishedProjects.length > 0) { const projectChoices = getProjectChoices(unfinishedProjects); @@ -104,15 +99,18 @@ const createAction = async (options: Flags) => { // Clear the directory if overwrite is confirmed fs.rmSync(projectDir, { recursive: true, force: true }); - console.log(chalk.yellow(`The directory "${projectName}" has been cleared.`)); + logger.log(`The directory "${projectName}" has been cleared.`); } // Skip Payload if specified by the flag + logger.withLabel('cms', 'CMS setup'); const payloadAnswer = options.skipPayload ? { usePayload: false } : await shouldUsePayloadPrompt(); const finalOptions = { name: projectName, shouldDeploy, ...payloadAnswer }; if (shouldDeploy) { + logger.withLabel('auth', 'Authentication status'); await checkAuthentication(); + logger.withLabel('stapler', 'Tooling status'); await checkTools(); } diff --git a/packages/cli/package.json b/packages/cli/package.json index b088715..797c856 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -33,16 +33,18 @@ "dev": "tsc -w" }, "dependencies": { + "ansi-colors": "4.1.3", "chalk": "^5.3.0", "commander": "^12.1.0", + "enquirer": "^2.4.1", "gradient-string": "^3.0.0", "inquirer": "^10.2.2" }, "devDependencies": { - "stplr-core": "workspace:*", - "stplr-utils": "workspace:*", "@types/inquirer": "^9.0.7", "@types/node": "^22.5.4", + "stplr-core": "workspace:*", + "stplr-utils": "workspace:*", "tsup": "^8.2.4", "typescript": "^5.6.2" } diff --git a/packages/core/installMachine/index.ts b/packages/core/installMachine/index.ts index 64872c9..7bfee0d 100644 --- a/packages/core/installMachine/index.ts +++ b/packages/core/installMachine/index.ts @@ -17,6 +17,7 @@ import { updateVercelProjectSettings, } from './installSteps/vercel'; import { shouldDeploy } from './installSteps/shouldDeploy'; +import { logger } from 'stplr-utils'; const isStepCompleted = (stepName: keyof StepsCompleted) => { return ({ context }: { context: InstallMachineContext; event: AnyEventObject }) => { @@ -349,6 +350,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { createTurboActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('turborepo', 'Creating Turbo project'); await createTurbo(input.stateData.options.name); process.chdir(input.projectDir); input.stateData.stepsCompleted.createTurbo = true; @@ -362,6 +364,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { modifyGitignoreActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('git', 'Modifying .gitignore'); await modifyGitignore('.initializeRcFile'); input.stateData.stepsCompleted.modifyGitignore = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -374,6 +377,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { installTailwindActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('tailwind', 'Installing Tailwind CSS'); const currentDir = process.cwd(); await installTailwind(currentDir); input.stateData.stepsCompleted.installTailwind = true; @@ -399,6 +403,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { installSupabaseActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('supabase', 'Installing Supabase'); const currentDir = process.cwd(); await installSupabase(currentDir); input.stateData.stepsCompleted.installSupabase = true; @@ -412,6 +417,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { installPayloadActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('payload', 'Preparing Payload'); await preparePayload(); input.stateData.stepsCompleted.installPayload = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -424,6 +430,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { createDocFilesActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('stapler', 'Creating documentation files'); await createDocFiles(); input.stateData.stepsCompleted.createDocFiles = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -436,6 +443,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { prettifyCodeActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('prettier', 'Prettifying code'); await prettify(); input.stateData.stepsCompleted.prettifyCode = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -459,6 +467,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { initializeRepositoryActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('github', 'Initializing GitHub repository'); await initializeRepository({ projectName: input.stateData.options.name, stateData: input.stateData, @@ -486,6 +495,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { createSupabaseProjectActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('supabase', 'Creating Supabase project'); await createSupabaseProject(input.stateData.githubCandidateName); input.stateData.stepsCompleted.createSupabaseProject = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -498,6 +508,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { chooseVercelTeamActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('vercel', 'Managing your Vercel'); await chooseVercelTeam(); input.stateData.stepsCompleted.chooseVercelTeam = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -534,6 +545,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { connectSupabaseProjectActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('supabase', 'Connecting Supabase project'); const currentDir = process.cwd(); await connectSupabaseProject(input.stateData.githubCandidateName, currentDir); input.stateData.stepsCompleted.connectSupabaseProject = true; @@ -547,6 +559,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { deployVercelProjectActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('vercel', 'Deploying Vercel project'); await deployVercelProject(input.stateData); input.stateData.stepsCompleted.deployVercelProject = true; saveStateToRcFile(input.stateData, input.projectDir); @@ -559,6 +572,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { prepareDrinkActor: createStepMachine( fromPromise(async ({ input }) => { try { + logger.withLabel('stapler', 'Preparing your drink'); const { projectName, prettyDeploymentUrl, diff --git a/packages/core/installMachine/installSteps/github/checkGitHubCLI.ts b/packages/core/installMachine/installSteps/github/checkGitHubCLI.ts index 8000210..fdb6dd7 100644 --- a/packages/core/installMachine/installSteps/github/checkGitHubCLI.ts +++ b/packages/core/installMachine/installSteps/github/checkGitHubCLI.ts @@ -1,19 +1,21 @@ -import inquirer from 'inquirer'; -import { logger } from 'stplr-utils'; +import Enquirer from 'enquirer'; +import { LEFT_PADDING, logger } from 'stplr-utils'; import { isGitHubCLIInstalled, installGitHubCLI } from './ghInstaller'; export const checkGitHubCLI = async () => { - await logger.withSpinner('github', 'Checking if GitHub CLI is installed...', async (spinner) => { + await logger.withSpinner('Checking if GitHub CLI is installed...', async (spinner) => { if (!isGitHubCLIInstalled()) { - logger.log('github', 'GitHub CLI is not installed.'); - const { shouldInstallGitHubCLI } = await inquirer.prompt([ + logger.log('GitHub CLI is not installed.'); + const enquirer = new Enquirer(); + const { shouldInstallGitHubCLI } = (await enquirer.prompt([ { type: 'confirm', name: 'shouldInstallGitHubCLI', message: 'Would you like us to install GitHub CLI?', - default: true, + initial: true, + prefix: LEFT_PADDING, }, - ]); + ])) as { shouldInstallGitHubCLI: boolean }; if (shouldInstallGitHubCLI) { const installed = await installGitHubCLI(); diff --git a/packages/core/installMachine/installSteps/github/ensureGitHubAuthentication.ts b/packages/core/installMachine/installSteps/github/ensureGitHubAuthentication.ts index edd770b..f26f7dd 100644 --- a/packages/core/installMachine/installSteps/github/ensureGitHubAuthentication.ts +++ b/packages/core/installMachine/installSteps/github/ensureGitHubAuthentication.ts @@ -2,7 +2,7 @@ import { logger } from 'stplr-utils'; import { authenticateGitHub, isGitHubAuthenticated } from './repositoryManager'; export const ensureGitHubAuthentication = async () => { - await logger.withSpinner('github', 'Checking authentication status...', async (spinner) => { + await logger.withSpinner('Checking authentication status...', async (spinner) => { if (isGitHubAuthenticated()) { spinner.succeed('Logged in'); return; diff --git a/packages/core/installMachine/installSteps/github/fetchOrganizations.ts b/packages/core/installMachine/installSteps/github/fetchOrganizations.ts index 34b023f..a96c6c8 100644 --- a/packages/core/installMachine/installSteps/github/fetchOrganizations.ts +++ b/packages/core/installMachine/installSteps/github/fetchOrganizations.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; export const fetchOrganizations = async (): Promise<{ name: string; writable: boolean }[]> => { - return await logger.withSpinner('github', 'Fetching organizations you belong to...', async (spinner) => { + return await logger.withSpinner('Fetching organizations you belong to...', async (spinner) => { try { // Fetch all organizations the user belongs to const orgsOutput = await execAsync(`gh api user/orgs --jq '[.[] | {name: .login, repos_url: .repos_url}]'`); diff --git a/packages/core/installMachine/installSteps/github/ghInstaller.ts b/packages/core/installMachine/installSteps/github/ghInstaller.ts index d1a9a40..45b72c6 100644 --- a/packages/core/installMachine/installSteps/github/ghInstaller.ts +++ b/packages/core/installMachine/installSteps/github/ghInstaller.ts @@ -23,7 +23,7 @@ export const installGitHubCLI = async (): Promise => { case 'linux': // Linux const linuxDistro = await getLinuxDistro(); if (linuxDistro === 'unknown') { - logger.log('github', 'Automatic installation is not supported for your Linux distribution.'); + logger.log('Automatic installation is not supported for your Linux distribution.'); } if (linuxDistro === 'ubuntu' || linuxDistro === 'debian') { installCommand = @@ -32,7 +32,7 @@ export const installGitHubCLI = async (): Promise => { installCommand = 'sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo && sudo dnf install gh'; } else { - logger.log('github', [ + logger.log([ 'Automatic installation is not supported for your Linux distribution.', '\n Please visit https://github.com/cli/cli#installation for installation instructions.', ]); @@ -43,20 +43,20 @@ export const installGitHubCLI = async (): Promise => { installCommand = 'winget install --id GitHub.cli'; break; default: - logger.log('github', [ + logger.log([ 'Automatic installation is not supported for your operating system.', '\nPlease visit https://github.com/cli/cli#installation for installation instructions.', ]); return false; } - logger.log('github', 'Installing GitHub CLI...'); + logger.log('Installing GitHub CLI...'); try { await execAsync(installCommand); return true; } catch (error) { console.error('Failed to install GitHub CLI.'); - logger.log('github', 'Please install it manually from: https://github.com/cli/cli#installation'); + logger.log('Please install it manually from: https://github.com/cli/cli#installation'); return false; } }; diff --git a/packages/core/installMachine/installSteps/github/repositoryManager.ts b/packages/core/installMachine/installSteps/github/repositoryManager.ts index 5b8d330..d3a15f1 100644 --- a/packages/core/installMachine/installSteps/github/repositoryManager.ts +++ b/packages/core/installMachine/installSteps/github/repositoryManager.ts @@ -1,31 +1,37 @@ -import { execSync, spawnSync } from 'child_process'; -import inquirer from 'inquirer'; import chalk from 'chalk'; -import { logger } from 'stplr-utils'; -import { execAsync } from '../../../utils/execAsync'; +import { execSync, spawnSync } from 'child_process'; +import Enquirer from 'enquirer'; +import { CHECK_MARK_COLOR, LABEL_SECONDARY_TEXT_COLOR, LEFT_PADDING, logger, QUESTION_MARK } from 'stplr-utils'; import { InstallMachineContext } from '../../../types'; +import { execAsync } from '../../../utils/execAsync'; import { fetchOrganizations } from './fetchOrganizations'; +export interface ProjectChoice { + name: string; + value: string; +} + const generateUniqueRepoName = async (baseName: string): Promise => { const cleanBaseName = baseName.replace(/-\d+$/, ''); // Clean base name - try { - await execAsync(`gh repo view ${cleanBaseName}`); - logger.log('github', `Repository "${cleanBaseName}" already exists.`); - let counter = 2; - while (true) { - const candidateName = `${cleanBaseName}-v${counter}`; - try { - await execAsync(`gh repo view ${candidateName}`); - logger.log('github', `Repository "${candidateName}" already exists.`); - counter++; - } catch { - return candidateName; + const uniqueRepoName = await logger.withSpinner('Generating unique repo name...', async (spinner) => { + try { + await execAsync(`gh repo view ${cleanBaseName}`); + let counter = 2; + while (true) { + const candidateName = `${cleanBaseName}-v${counter}`; + try { + await execAsync(`gh repo view ${candidateName}`); + counter++; + } catch { + return candidateName; + } } + } catch (error) { + return cleanBaseName; } - } catch { - return cleanBaseName; - } + }); + return uniqueRepoName; }; export const isGitHubAuthenticated = (): boolean => { @@ -38,7 +44,7 @@ export const isGitHubAuthenticated = (): boolean => { }; export const authenticateGitHub = async () => { - await logger.withSpinner('github', 'Attempting to authenticate...', async (spinner) => { + await logger.withSpinner('Attempting to authenticate...', async (spinner) => { try { spinner.start('Authenticating...'); const isAuthenticated = isGitHubAuthenticated(); @@ -91,67 +97,88 @@ export const createGitHubRepository = async ( // Fetch organizations and build choices for the prompt const organizations = await fetchOrganizations(); const accountChoices = [ - { name: `${username} (personal account)`, value: username }, + { name: username, value: username, message: chalk.whiteBright(username + 'personal account') }, ...organizations.map((org: { writable: any; name: any }) => ({ - name: org.writable ? org.name : chalk.gray(`${org.name} (read-only)`), - value: org.name, + name: org.name, + value: chalk.hex(CHECK_MARK_COLOR)(LEFT_PADDING + org.name), + message: org.writable ? chalk.whiteBright(org.name) : chalk.gray(`${org.name} (read-only)`), disabled: org.writable ? false : 'No write access', })), ]; // Prompt the user to select an account or organization - const { selectedAccount } = await inquirer.prompt([ + const enquirer = new Enquirer(); + const { selectedAccount } = (await enquirer.prompt([ { - type: 'list', + type: 'select', name: 'selectedAccount', - message: 'Select the account or organization to create the repository under:', + message: chalk.whiteBright('Select the account or organization to create the repository under:'), choices: accountChoices, + prefix: ' ' + LEFT_PADDING + QUESTION_MARK, + format(value) { + return chalk.hex(CHECK_MARK_COLOR)(value); + }, }, - ]); + ])) as { selectedAccount: string }; + stateData.selectedAccount = selectedAccount; // Update state with selected account - await logger.withSpinner('github', 'Checking if repository already exists...', async (spinner) => { + const repoExists = await logger.withSpinner('Checking repository...', async (spinner) => { try { const repoNameJSON = await execAsync(`echo "$(gh repo view ${selectedAccount}/${projectName} --json name)"`); const repoExists = repoNameJSON.stdout.trim().includes(`{"name":"${projectName}"}`); - - if (repoExists) { - spinner.stop(); - const newRepoName = await generateUniqueRepoName(projectName); - const { confirmedName } = await inquirer.prompt([ - { - type: 'input', - name: 'confirmedName', - message: 'The repository already exists. Please confirm or modify the repository name:', - default: newRepoName, - }, - ]); - repoName = confirmedName; - stateData.githubCandidateName = confirmedName; // Update state with confirmed name - } - spinner.stop(); + return repoExists; } catch (error) { - spinner.fail('Error checking repository existence.'); - console.error(error); + spinner.fail('Failed to update project settings.'); + console.error('Error during Vercel project settings update:', error); } }); - await logger.withSpinner('github', `Creating repository: ${selectedAccount}/${repoName}...`, async (spinner) => { - try { - spinner.stop(); - const { repositoryVisibility } = await inquirer.prompt([ - { - type: 'list', - name: 'repositoryVisibility', - message: 'Choose the repository visibility:', - choices: ['public', 'private'], - default: 'public', + if (repoExists) { + const newRepoName = await generateUniqueRepoName(projectName); + const enquirer = new Enquirer(); + const { confirmedName } = (await enquirer.prompt([ + { + type: 'input', + name: 'confirmedName', + message: chalk.whiteBright('The repository already exists. Please confirm or modify the repository name:'), + initial: newRepoName, + prefix: ' ' + LEFT_PADDING + QUESTION_MARK, + format(value) { + return chalk.hex(CHECK_MARK_COLOR)(value); }, - ]); + }, + ])) as { confirmedName: string }; + repoName = confirmedName; + stateData.githubCandidateName = confirmedName; // Update state with confirmed name + } + const questions = [ + { + type: 'select' as const, + name: 'repositoryVisibility', + message: chalk.whiteBright('Choose the repository visibility:'), + prefix: ' ' + LEFT_PADDING + QUESTION_MARK, + choices: [ + { name: 'public', value: 'public', message: chalk.whiteBright('public') }, + { name: 'private', value: 'private', message: chalk.whiteBright('private') }, + ], + initial: 'public', + format(value: string) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; + }, + }, + ]; + + const response = (await enquirer.prompt(questions)) as { repositoryVisibility: string }; + + const { repositoryVisibility } = response; + + await logger.withSpinner(`Creating repository: ${selectedAccount}/${repoName}...`, async (spinner) => { + try { const visibilityFlag = repositoryVisibility === 'public' ? '--public' : '--private'; const command = `gh repo create ${selectedAccount}/${repoName} ${visibilityFlag}`; await execAsync(command); - spinner.succeed(`Repository created: ${chalk.cyan(repoName)}`); + spinner.succeed(`Repository created: ${chalk.hex(LABEL_SECONDARY_TEXT_COLOR)(repoName)}`); return repoName; } catch (error) { spinner.fail('Failed to create repository.'); @@ -175,7 +202,7 @@ const executeCommands = async (commands: string[]) => { }; export const setupGitRepository = async () => { - await logger.withSpinner('github', `Setting up Git for the repository...`, async (spinner) => { + await logger.withSpinner(`Setting up Git for the repository...`, async (spinner) => { const commands = [`git init`, `git add .`]; await executeCommands(commands); spinner.succeed('Git setup complete.'); @@ -183,7 +210,7 @@ export const setupGitRepository = async () => { }; export const pushToGitHub = async (selectedAccount: string, githubCandidateName: string) => { - await logger.withSpinner('github', 'Pushing changes...', async (spinner) => { + await logger.withSpinner('Pushing changes...', async (spinner) => { const commands = [ `git add .`, `git branch -M main`, diff --git a/packages/core/installMachine/installSteps/homepage/__tests__/install.test.ts b/packages/core/installMachine/installSteps/homepage/__tests__/install.test.ts index 037da7e..d693a4e 100644 --- a/packages/core/installMachine/installSteps/homepage/__tests__/install.test.ts +++ b/packages/core/installMachine/installSteps/homepage/__tests__/install.test.ts @@ -9,7 +9,7 @@ vi.mock('../../../../utils/generator/generator'); vi.mock('../../../../utils/getTemplateDirectory'); vi.mock('../../../../utils/logger', () => ({ logger: { - withSpinner: vi.fn((_, __, callback) => callback({ succeed: vi.fn(), fail: vi.fn() })), + withSpinner: vi.fn((__, callback) => callback({ succeed: vi.fn(), fail: vi.fn() })), }, })); diff --git a/packages/core/installMachine/installSteps/homepage/install.ts b/packages/core/installMachine/installSteps/homepage/install.ts index 09938ae..ea4dd6f 100644 --- a/packages/core/installMachine/installSteps/homepage/install.ts +++ b/packages/core/installMachine/installSteps/homepage/install.ts @@ -4,7 +4,7 @@ import { getTemplateDirectory } from '../../../utils/getTemplateDirectory'; import { logger } from 'stplr-utils'; export const modifyHomepage = async (destinationDirectory: string) => { - await logger.withSpinner('tailwind', 'Setting up your welcome homepage...', async (spinner) => { + await logger.withSpinner('Setting up your welcome homepage...', async (spinner) => { try { const templateDirectory = getTemplateDirectory(`/templates/homepage/files/`); templateGenerator(homepageFiles, templateDirectory, destinationDirectory); diff --git a/packages/core/installMachine/installSteps/payload/moveFilesToAppDir.ts b/packages/core/installMachine/installSteps/payload/moveFilesToAppDir.ts index e3e8af4..2731a8f 100644 --- a/packages/core/installMachine/installSteps/payload/moveFilesToAppDir.ts +++ b/packages/core/installMachine/installSteps/payload/moveFilesToAppDir.ts @@ -2,7 +2,7 @@ import { logger } from 'stplr-utils'; import { execAsync } from '../../../utils/execAsync'; export const moveFilesToAppDir = async () => { - await logger.withSpinner('payload', 'Moving files to (app) directory...', async (spinner) => { + await logger.withSpinner('Moving files to (app) directory...', async (spinner) => { try { await execAsync( `mkdir -p ./app/\\(app\\) && find ./app -maxdepth 1 ! -path './app' ! -path './app/\\(app\\)' -exec mv {} ./app/\\(app\\)/ \\;`, diff --git a/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts b/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts index 9f04fbb..1fc26d4 100644 --- a/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts +++ b/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts @@ -11,7 +11,7 @@ export const preparePayloadConfig = async () => { return; } - await logger.withSpinner('payload', 'Preparing config...', async (spinner) => { + await logger.withSpinner('Preparing config...', async (spinner) => { try { // Read the payload.config.ts file const data = await fs.readFile(payloadConfigPath, 'utf8'); diff --git a/packages/core/installMachine/installSteps/payload/prepareTsConfig.ts b/packages/core/installMachine/installSteps/payload/prepareTsConfig.ts index d902223..2eecf76 100644 --- a/packages/core/installMachine/installSteps/payload/prepareTsConfig.ts +++ b/packages/core/installMachine/installSteps/payload/prepareTsConfig.ts @@ -7,7 +7,7 @@ const readFileAsync = promisify(fs.readFile); const writeFileAsync = promisify(fs.writeFile); export const prepareTsConfig = async () => { - await logger.withSpinner('payload', 'Preparing TypeScript config..', async (spinner) => { + await logger.withSpinner('Preparing TypeScript config..', async (spinner) => { const tsconfigPath = path.join(process.cwd(), 'tsconfig.json'); try { diff --git a/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts b/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts index 28df30c..bb73aa0 100644 --- a/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts +++ b/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts @@ -7,7 +7,7 @@ const readFileAsync = promisify(fs.readFile); const writeFileAsync = promisify(fs.writeFile); export const removeTurboFlag = async () => { - await logger.withSpinner('payload', 'Removing --turbo flag from dev script...', async (spinner) => { + await logger.withSpinner('Removing --turbo flag from dev script...', async (spinner) => { const packageJsonPath = path.join(process.cwd(), 'package.json'); try { diff --git a/packages/core/installMachine/installSteps/payload/runInstallCommand.ts b/packages/core/installMachine/installSteps/payload/runInstallCommand.ts index c8abdaf..988a7aa 100644 --- a/packages/core/installMachine/installSteps/payload/runInstallCommand.ts +++ b/packages/core/installMachine/installSteps/payload/runInstallCommand.ts @@ -4,7 +4,7 @@ import { loadEnvFile } from './utils/loadEnvFile'; export const runInstallCommand = async () => { loadEnvFile('../../supabase/.env'); - await logger.withSpinner('payload', 'Installing to Next.js...', async (spinner) => { + await logger.withSpinner('Installing to Next.js...', async (spinner) => { try { await execAsync(`echo y | npx create-payload-app --db postgres --db-connection-string ${process.env.DB_URL}`); diff --git a/packages/core/installMachine/installSteps/payload/updatePackages.ts b/packages/core/installMachine/installSteps/payload/updatePackages.ts index 2e9ed1b..d0ef1d5 100644 --- a/packages/core/installMachine/installSteps/payload/updatePackages.ts +++ b/packages/core/installMachine/installSteps/payload/updatePackages.ts @@ -2,7 +2,7 @@ import { logger } from 'stplr-utils'; import { execAsync } from '../../../utils/execAsync'; export const updatePackages = async () => { - await logger.withSpinner('payload', `Updating React to version 19...`, async (spinner) => { + await logger.withSpinner(`Updating React to version 19...`, async (spinner) => { try { await execAsync(`pnpm up react@19 react-dom@19 --reporter silent`); spinner.succeed(`Updated React to version 19.`); @@ -12,7 +12,7 @@ export const updatePackages = async () => { } }); - await logger.withSpinner('payload', 'Installing necessary packages...', async (spinner) => { + await logger.withSpinner('Installing necessary packages...', async (spinner) => { try { await execAsync(`pnpm i pg sharp --reporter silent`); spinner.succeed('Installed necessary packages.'); diff --git a/packages/core/installMachine/installSteps/prettier/prettify.ts b/packages/core/installMachine/installSteps/prettier/prettify.ts index c3ffbe1..619cfbd 100644 --- a/packages/core/installMachine/installSteps/prettier/prettify.ts +++ b/packages/core/installMachine/installSteps/prettier/prettify.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; import { logger } from 'stplr-utils'; export const prettify = async () => { - await logger.withSpinner('prettier', 'Prettifying...', async (spinner) => { + await logger.withSpinner('Prettifying...', async (spinner) => { try { const ignorePatterns = [ 'node_modules/', diff --git a/packages/core/installMachine/installSteps/shouldDeploy/shouldDeploy.ts b/packages/core/installMachine/installSteps/shouldDeploy/shouldDeploy.ts index 2f6065d..9752413 100644 --- a/packages/core/installMachine/installSteps/shouldDeploy/shouldDeploy.ts +++ b/packages/core/installMachine/installSteps/shouldDeploy/shouldDeploy.ts @@ -1,33 +1,37 @@ -import inquirer from 'inquirer'; -import { logger } from 'stplr-utils'; +import chalk from 'chalk'; +import Enquirer from 'enquirer'; +import { CHECK_MARK_COLOR, LEFT_PADDING, logger, QUESTION_MARK } from 'stplr-utils'; export const shouldDeploy = async (shouldContinue: boolean): Promise => { - return await logger.withSpinner('deployment', 'Deciding next steps...', async (spinner) => { - if (!shouldContinue) { - spinner.succeed('Local deployment completed'); - return false; - } + if (!shouldContinue) { + logger.log('Local deployment completed'); + return false; + } - try { - spinner.stop(); - const answers = (await inquirer.prompt([ - { - type: 'confirm', - name: 'continue', - message: - 'Local installation completed. Would you like to continue with remote setup (GitHub, Supabase, Vercel)?', - default: true, + try { + const enquirer = new Enquirer(); + const answers = (await enquirer.prompt([ + { + type: 'confirm', + name: 'continue', + message: chalk.whiteBright( + 'Local installation completed. Would you like to continue with remote setup (GitHub, Supabase, Vercel)?', + ), + initial: true, + prefix: ' ' + LEFT_PADDING + QUESTION_MARK, + format(value) { + return `${chalk.hex(CHECK_MARK_COLOR)(value)}`; }, - ])) as { continue: boolean }; - spinner.start(); - const spinnerMessage = answers.continue ? 'Continuing with remote setup...' : 'Local deployment completed'; - spinner.succeed(spinnerMessage); + }, + ])) as { continue: boolean }; - return answers.continue; - } catch (error) { - spinner.fail('Local deployment failed'); - console.error('Error during local deployment:', error); - return false; - } - }); + const spinnerMessage = answers.continue ? 'Continuing with remote setup...' : 'Local deployment completed'; + logger.log(spinnerMessage); + + return answers.continue; + } catch (error) { + logger.log('Local deployment failed'); + console.error('Error during local deployment:', error); + return false; + } }; diff --git a/packages/core/installMachine/installSteps/stapler/createDocFiles.ts b/packages/core/installMachine/installSteps/stapler/createDocFiles.ts index 5bf0b91..2872bdc 100644 --- a/packages/core/installMachine/installSteps/stapler/createDocFiles.ts +++ b/packages/core/installMachine/installSteps/stapler/createDocFiles.ts @@ -4,7 +4,7 @@ import { logger } from 'stplr-utils'; import { getTemplateDirectory } from '../../../utils/getTemplateDirectory'; export const createDocFiles = async () => { - await logger.withSpinner('stapler', 'Writing docs...', async (spinner) => { + await logger.withSpinner('Writing docs...', async (spinner) => { try { const templateDirectory = getTemplateDirectory(`/templates/docs/files`); const destinationDirectory = process.cwd(); diff --git a/packages/core/installMachine/installSteps/stapler/initializeStapler.ts b/packages/core/installMachine/installSteps/stapler/initializeStapler.ts index db6de5a..80182dc 100644 --- a/packages/core/installMachine/installSteps/stapler/initializeStapler.ts +++ b/packages/core/installMachine/installSteps/stapler/initializeStapler.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; export const initializeStapler = async (name: string) => { - await logger.withSpinner('stapler', 'Initializing...', async (spinner) => { + await logger.withSpinner('Initializing...', async (spinner) => { try { await execAsync(`mkdir ${name}`); spinner.succeed('Initialized.'); diff --git a/packages/core/installMachine/installSteps/stapler/modifyGitignore.ts b/packages/core/installMachine/installSteps/stapler/modifyGitignore.ts index 90dd592..fb18ad3 100644 --- a/packages/core/installMachine/installSteps/stapler/modifyGitignore.ts +++ b/packages/core/installMachine/installSteps/stapler/modifyGitignore.ts @@ -7,7 +7,7 @@ const readFileAsync = promisify(fs.readFile); const writeFileAsync = promisify(fs.writeFile); export const modifyGitignore = async (entry: string) => { - await logger.withSpinner('stapler', `Adding entries to .gitignore..`, async (spinner) => { + await logger.withSpinner(`Adding entries to .gitignore..`, async (spinner) => { const gitignorePath = path.join(process.cwd(), '.gitignore'); try { diff --git a/packages/core/installMachine/installSteps/stapler/prepareDrink.ts b/packages/core/installMachine/installSteps/stapler/prepareDrink.ts index d4af8b9..ff7e47d 100644 --- a/packages/core/installMachine/installSteps/stapler/prepareDrink.ts +++ b/packages/core/installMachine/installSteps/stapler/prepareDrink.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import { CHECK_MARK_COLOR, LABEL_WIDTH, SPACING } from 'stplr-utils'; import { delay } from '../../../utils/delay'; const getMessages = (name: string, prettyDeploymentUrl: string, shouldDeploy: boolean) => { @@ -7,12 +8,12 @@ const getMessages = (name: string, prettyDeploymentUrl: string, shouldDeploy: bo '🍸 Adding gin and lime juice...', `🍸 Topping with ${chalk.blue('Tonik')}...`, '🍸 Garnishing with lime wedge...', - `🍸 ${chalk.green(`Your Stapled ${name} is ready!`)}`, - `🍸 Ready to explore? Jump into your project with: ${chalk.cyan(`cd ${name} && pnpm dev`)}`, + `🍸 ${chalk.hex(CHECK_MARK_COLOR)(`Your Stapled ${chalk.hex(CHECK_MARK_COLOR)(name)} is ready!`)}`, + `🍸 Ready to explore? Jump into your project with: ${chalk.hex(CHECK_MARK_COLOR)(`cd ${name} && pnpm dev`)}`, ]; if (shouldDeploy) { - messages.push(`🍸 Prefer to see it online? Check it out here: ${chalk.cyan(prettyDeploymentUrl)}`); + messages.push(`🍸 Prefer to see it online? Check it out here: ${chalk.hex(CHECK_MARK_COLOR)(prettyDeploymentUrl)}`); } else { messages.push('🍸 Want to deploy your project? Run `stplr` within your project directory.'); } @@ -21,10 +22,11 @@ const getMessages = (name: string, prettyDeploymentUrl: string, shouldDeploy: bo }; export const prepareDrink = async (name: string, prettyDeploymentUrl: string, shouldDeploy: boolean) => { + const leftPadding = ' '.repeat(SPACING + LABEL_WIDTH); const messages = getMessages(name, prettyDeploymentUrl, shouldDeploy); for (const message of messages) { - console.log(message); + console.log(`${leftPadding}${message}`); await delay(1000); } }; diff --git a/packages/core/installMachine/installSteps/supabase/addTemplateFiles.ts b/packages/core/installMachine/installSteps/supabase/addTemplateFiles.ts index 5b9c16a..51008fb 100644 --- a/packages/core/installMachine/installSteps/supabase/addTemplateFiles.ts +++ b/packages/core/installMachine/installSteps/supabase/addTemplateFiles.ts @@ -6,7 +6,7 @@ import { getTemplateDirectory } from '../../../utils/getTemplateDirectory'; import { logger } from 'stplr-utils'; export const addTemplateFiles = async (destinationDirectory: string) => { - await logger.withSpinner('supabase', 'Adding files from template...', async (spinner) => { + await logger.withSpinner('Adding files from template...', async (spinner) => { const templateDirectory = getTemplateDirectory(`/templates/supabase/files/`); templateGenerator(supabaseFiles, templateDirectory, destinationDirectory); diff --git a/packages/core/installMachine/installSteps/supabase/authenticateSupabase.ts b/packages/core/installMachine/installSteps/supabase/authenticateSupabase.ts index 0b32a25..a1282ca 100644 --- a/packages/core/installMachine/installSteps/supabase/authenticateSupabase.ts +++ b/packages/core/installMachine/installSteps/supabase/authenticateSupabase.ts @@ -3,7 +3,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils/logger'; export const authenticateSupabase = async () => { - await logger.withSpinner('supabase', 'Checking Supabase authentication...', async (spinner) => { + await logger.withSpinner('Checking Supabase authentication...', async (spinner) => { try { await execAsync('npx supabase projects list'); spinner.succeed('Logged in'); diff --git a/packages/core/installMachine/installSteps/supabase/checkSupabaseCLI.ts b/packages/core/installMachine/installSteps/supabase/checkSupabaseCLI.ts index cb55f7a..3a19f0a 100644 --- a/packages/core/installMachine/installSteps/supabase/checkSupabaseCLI.ts +++ b/packages/core/installMachine/installSteps/supabase/checkSupabaseCLI.ts @@ -27,28 +27,28 @@ const installSupabaseCLI = async (): Promise => { 'scoop bucket add supabase https://github.com/supabase/scoop-bucket.git && scoop install supabase'; break; default: - logger.log('supabase', [ + logger.log([ 'Automatic installation is not supported for your operating system.', '\nPlease visit https://supabase.com/docs/guides/cli/getting-started for installation instructions.', ]); return false; } - logger.log('supabase', 'Installing Supabase CLI...'); + logger.log('Installing Supabase CLI...'); try { await execAsync(installCommand); return true; } catch (error) { console.error('Failed to install Supabase CLI.'); - logger.log('supabase', 'Please install it manually from: https://supabase.com/docs/guides/cli/getting-started'); + logger.log('Please install it manually from: https://supabase.com/docs/guides/cli/getting-started'); return false; } }; export const checkSupabaseCLI = async () => { - await logger.withSpinner('supabase', 'Checking if Supabase CLI is installed...', async (spinner) => { + await logger.withSpinner('Checking if Supabase CLI is installed...', async (spinner) => { if (!isSupabaseCLIInstalled()) { - logger.log('supabase', 'Supabase CLI is not installed.'); + logger.log('Supabase CLI is not installed.'); const installed = await installSupabaseCLI(); if (!installed) { diff --git a/packages/core/installMachine/installSteps/supabase/connectProject.ts b/packages/core/installMachine/installSteps/supabase/connectProject.ts index a05592f..2bda9c2 100644 --- a/packages/core/installMachine/installSteps/supabase/connectProject.ts +++ b/packages/core/installMachine/installSteps/supabase/connectProject.ts @@ -1,17 +1,17 @@ -import { execSync } from 'child_process'; +import boxen, { Options } from 'boxen'; import chalk from 'chalk'; -import boxen from 'boxen'; -import { getSupabaseKeys, parseProjectsList } from './utils'; -import { logger } from 'stplr-utils'; -import { getVercelTokenFromAuthFile } from '../../../utils/getVercelTokenFromAuthFile'; -import { getDataFromVercelConfig } from '../../../utils/getDataFromVercelConfig'; -import { execAsync } from '../../../utils/execAsync'; +import { execSync } from 'child_process'; +import { BOXEN_SETTINGS, CHECK_MARK_COLOR, LABEL_SECONDARY_TEXT_COLOR, logger } from 'stplr-utils'; import { delay } from '../../../utils/delay'; +import { execAsync } from '../../../utils/execAsync'; +import { getDataFromVercelConfig } from '../../../utils/getDataFromVercelConfig'; +import { getVercelTokenFromAuthFile } from '../../../utils/getVercelTokenFromAuthFile'; +import { getSupabaseKeys, parseProjectsList } from './utils'; export const connectSupabaseProject = async (projectName: string, currentDir: string) => { try { // Get project information - const newProject = await logger.withSpinner('supabase', 'Getting project information...', async (spinner) => { + const newProject = await logger.withSpinner('Getting project information...', async (spinner) => { const { stdout: projectsList } = await execAsync('npx supabase projects list'); const projects = parseProjectsList(projectsList); const project = projects.find((p) => p.name === projectName); @@ -28,27 +28,23 @@ export const connectSupabaseProject = async (projectName: string, currentDir: st }); // Get API keys - const { anonKey, serviceRoleKey } = await logger.withSpinner( - 'supabase', - 'Getting project API keys...', - async (spinner) => { - const { stdout: projectAPIKeys } = await execAsync( - `npx supabase projects api-keys --project-ref ${newProject.refId}`, - ); + const { anonKey, serviceRoleKey } = await logger.withSpinner('Getting project API keys...', async (spinner) => { + const { stdout: projectAPIKeys } = await execAsync( + `npx supabase projects api-keys --project-ref ${newProject.refId}`, + ); - const keys = getSupabaseKeys(projectAPIKeys); - if (!keys.anonKey || !keys.serviceRoleKey) { - spinner.fail('Failed to retrieve API keys'); - throw new Error('Failed to retrieve Supabase API keys. Please check your project configuration.'); - } + const keys = getSupabaseKeys(projectAPIKeys); + if (!keys.anonKey || !keys.serviceRoleKey) { + spinner.fail('Failed to retrieve API keys'); + throw new Error('Failed to retrieve Supabase API keys. Please check your project configuration.'); + } - spinner.succeed('API keys retrieved.'); - return keys; - }, - ); + spinner.succeed('API keys retrieved.'); + return keys; + }); // Link project - logger.log('supabase', 'Linking project...'); + logger.log('Linking project...'); execSync(`npx supabase link --project-ref ${newProject.refId}`, { stdio: 'inherit', }); @@ -57,24 +53,19 @@ export const connectSupabaseProject = async (projectName: string, currentDir: st console.log( boxen( chalk.bold('Supabase Integration Setup\n\n') + - chalk.hex('#259764')('1.') + + chalk.hex(LABEL_SECONDARY_TEXT_COLOR)('1.') + ' You will be redirected to your project dashboard\n' + - chalk.hex('#259764')('2.') + + chalk.hex(LABEL_SECONDARY_TEXT_COLOR)('2.') + ' Connect Vercel: "Add new project connection"\n' + - chalk.hex('#259764')('3.') + + chalk.hex(LABEL_SECONDARY_TEXT_COLOR)('3.') + ' (Optional) Connect GitHub: "Add new project connection"\n\n' + chalk.dim('Tip: Keep this terminal open to track the integration status'), - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: '#3ABC82', - }, + BOXEN_SETTINGS as Options, ), ); // Countdown and open dashboard - const spinner = logger.createSpinner('supabase', 'Preparing to open dashboard'); + const spinner = logger.createSpinner('Preparing to open dashboard'); spinner.start(); for (let i = 3; i > 0; i--) { @@ -84,10 +75,13 @@ export const connectSupabaseProject = async (projectName: string, currentDir: st spinner.text = 'Opening dashboard in your browser...'; await execAsync(`open https://supabase.com/dashboard/project/${newProject.refId}/settings/integrations`); - spinner.succeed('Dashboard opened.'); + spinner.stopAndPersist({ + text: 'Dashboard opened.', + symbol: chalk.hex(CHECK_MARK_COLOR)('✔'), + }); // Check Vercel integration - await logger.withSpinner('vercel', 'Checking integration...', async (spinner) => { + await logger.withSpinner('Checking integration...', async (spinner) => { const token = await getVercelTokenFromAuthFile(); const { projectId: vercelProjectId, orgId: vercelTeamId } = await getDataFromVercelConfig(); let attempts = 0; @@ -140,7 +134,7 @@ export const connectSupabaseProject = async (projectName: string, currentDir: st return false; }); } catch (error) { - logger.log('error', error instanceof Error ? error.message : 'An unknown error occurred'); + logger.log(error instanceof Error ? error.message : 'An unknown error occurred', false); throw error; } }; diff --git a/packages/core/installMachine/installSteps/supabase/createEnvFile.ts b/packages/core/installMachine/installSteps/supabase/createEnvFile.ts index 3437301..0e6e16f 100644 --- a/packages/core/installMachine/installSteps/supabase/createEnvFile.ts +++ b/packages/core/installMachine/installSteps/supabase/createEnvFile.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import { logger } from 'stplr-utils'; export const createEnvFile = async () => { - await logger.withSpinner('supabase', 'Writing local variables to .env file...', async (spinner) => { + await logger.withSpinner('Writing local variables to .env file...', async (spinner) => { const envData = `ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 API_URL=http://127.0.0.1:54321 DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres diff --git a/packages/core/installMachine/installSteps/supabase/createProject.ts b/packages/core/installMachine/installSteps/supabase/createProject.ts index ce9c1bd..4d476ae 100644 --- a/packages/core/installMachine/installSteps/supabase/createProject.ts +++ b/packages/core/installMachine/installSteps/supabase/createProject.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; import { logger } from 'stplr-utils'; export const createSupabaseProject = async (name: string) => { - logger.log('supabase', 'Creating Supabase project...'); + logger.log('Creating Supabase project...'); execSync(`npx supabase projects create ${name}`, { stdio: 'inherit', diff --git a/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts b/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts index 184edbf..3fdd836 100644 --- a/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts +++ b/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; export const initializeSupabaseProject = async () => { - await logger.withSpinner('supabase', 'Initializing project...', async (spinner) => { + await logger.withSpinner('Initializing project...', async (spinner) => { try { await execAsync(`npx supabase init`); spinner.succeed('Project initialized.'); diff --git a/packages/core/installMachine/installSteps/supabase/installDependencies.ts b/packages/core/installMachine/installSteps/supabase/installDependencies.ts index de1290d..e306305 100644 --- a/packages/core/installMachine/installSteps/supabase/installDependencies.ts +++ b/packages/core/installMachine/installSteps/supabase/installDependencies.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; export const installDependencies = async () => { - await logger.withSpinner('supabase', 'Installing dependencies...', async (spinner) => { + await logger.withSpinner('Installing dependencies...', async (spinner) => { await execAsync('pnpm i --reporter silent'); spinner.succeed('Dependencies installed.'); }); diff --git a/packages/core/installMachine/installSteps/supabase/utils/index.ts b/packages/core/installMachine/installSteps/supabase/utils/index.ts index 568a6ab..4fb6737 100644 --- a/packages/core/installMachine/installSteps/supabase/utils/index.ts +++ b/packages/core/installMachine/installSteps/supabase/utils/index.ts @@ -12,7 +12,7 @@ export const parseProjectsList = (output: string): SupabaseProjectInfo[] => { lines.splice(0, 2); return lines.map((line) => { - const [linked, org_id, refId, name, region, created_at] = line.split('│').map((item) => item.trim()); + const [linked, org_id, refId, name, region, created_at] = line.split('|').map((item) => item.trim()); return { linked: linked !== '', org_id, @@ -27,17 +27,19 @@ export const parseProjectsList = (output: string): SupabaseProjectInfo[] => { export const getSupabaseKeys = (input: string) => { const lines = input.split('\n'); - const anonKey = lines - .find((line) => line.replace(/\x1B\[[0-9;]*[JKmsu]/g, '').includes('anon')) - ?.split('│')[1] - .trim(); - const serviceRoleKey = lines - .find((line) => line.replace(/\x1B\[[0-9;]*[JKmsu]/g, '').includes('service_role')) - ?.split('│')[1] - .trim(); + const tokens: { anon: string; service_role: string } = { anon: '', service_role: '' }; + + lines.forEach((line) => { + if (line.includes('|')) { + const [key, value] = line.split('|').map((s) => s.trim()); + if (key === 'anon' || key === 'service_role') { + tokens[key] = value; + } + } + }); return { - anonKey, - serviceRoleKey, + anonKey: tokens.anon, + serviceRoleKey: tokens.service_role, }; }; diff --git a/packages/core/installMachine/installSteps/tailwind/install.ts b/packages/core/installMachine/installSteps/tailwind/install.ts index f8fcbd0..da108c4 100644 --- a/packages/core/installMachine/installSteps/tailwind/install.ts +++ b/packages/core/installMachine/installSteps/tailwind/install.ts @@ -23,7 +23,7 @@ const installTailwindPackage = async (currentDir: string) => { }; export const installTailwind = async (currentDir: string) => { - await logger.withSpinner('tailwind', 'Adding Tailwind...', async (spinner) => { + await logger.withSpinner('Adding Tailwind...', async (spinner) => { try { await installTailwindPackage(currentDir); copyTailwindFiles(currentDir); diff --git a/packages/core/installMachine/installSteps/turbo/install.ts b/packages/core/installMachine/installSteps/turbo/install.ts index 5435588..3afeb59 100644 --- a/packages/core/installMachine/installSteps/turbo/install.ts +++ b/packages/core/installMachine/installSteps/turbo/install.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; const checkPnpmVersion = async () => { - await logger.withSpinner('turborepo', 'Checking pnpm version...', async (spinner) => { + await logger.withSpinner('Checking pnpm version...', async (spinner) => { try { // Run the command to check the pnpm version const { stdout } = await execAsync('pnpm --version'); @@ -15,7 +15,7 @@ const checkPnpmVersion = async () => { }; export const createTurbo = async (name: string) => { - await logger.withSpinner('turborepo', 'Initializing...', async (spinner) => { + await logger.withSpinner('Initializing...', async (spinner) => { try { // Check the pnpm version await checkPnpmVersion(); diff --git a/packages/core/installMachine/installSteps/vercel/authenticateVercel.ts b/packages/core/installMachine/installSteps/vercel/authenticateVercel.ts index 9ea2c43..dc3cd0c 100644 --- a/packages/core/installMachine/installSteps/vercel/authenticateVercel.ts +++ b/packages/core/installMachine/installSteps/vercel/authenticateVercel.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; import { logger } from 'stplr-utils'; export const authenticateVercel = async () => { - await logger.withSpinner('vercel', 'Logging in to Vercel...', async (spinner) => { + await logger.withSpinner('Logging in to Vercel...', async (spinner) => { try { execSync('npx vercel whoami', { stdio: 'pipe' }); @@ -13,7 +13,7 @@ export const authenticateVercel = async () => { spinner.succeed('Logged in successfully.'); } catch { spinner.fail('Failed to log in.'); - logger.log('vercel', [ + logger.log([ 'Oops! Something went wrong while logging in to Vercel...', '\nYou might already be logged in with this email in another project.', '\nIn this case, select "Continue with Email" and enter the email you\'re already logged in with.\n', @@ -24,7 +24,7 @@ export const authenticateVercel = async () => { spinner.succeed('Logged in'); } catch { spinner.fail('Failed to log in.'); - logger.log('vercel', [ + logger.log([ 'Please check the error above and try again.', '\nAfter successfully logging in with "vercel login", please run stplr again.\n', ]); diff --git a/packages/core/installMachine/installSteps/vercel/checkVercelCLI.ts b/packages/core/installMachine/installSteps/vercel/checkVercelCLI.ts index 3b079ad..c87581d 100644 --- a/packages/core/installMachine/installSteps/vercel/checkVercelCLI.ts +++ b/packages/core/installMachine/installSteps/vercel/checkVercelCLI.ts @@ -11,21 +11,21 @@ const isVercelCLIInstalled = async (): Promise => { }; const installVercelCLI = async (): Promise => { - logger.log('vercel', 'Installing Vercel CLI...'); + logger.log('Installing Vercel CLI...'); try { await execAsync('npm i -g vercel@latest'); return true; } catch (error) { console.error('Failed to install Vercel CLI.'); - logger.log('vercel', 'Please install it manually from: https://vercel.com/docs/cli'); + logger.log('Please install it manually from: https://vercel.com/docs/cli'); return false; } }; export const checkVercelCLI = async () => { - await logger.withSpinner('vercel', 'Checking if Vercel CLI is installed...', async (spinner) => { + await logger.withSpinner('Checking if Vercel CLI is installed...', async (spinner) => { if (!isVercelCLIInstalled()) { - logger.log('vercel', 'Vercel CLI is not installed.'); + logger.log('Vercel CLI is not installed.'); const installed = await installVercelCLI(); if (!installed) { diff --git a/packages/core/installMachine/installSteps/vercel/chooseTeam.ts b/packages/core/installMachine/installSteps/vercel/chooseTeam.ts index c3f676d..f0f8542 100644 --- a/packages/core/installMachine/installSteps/vercel/chooseTeam.ts +++ b/packages/core/installMachine/installSteps/vercel/chooseTeam.ts @@ -1,18 +1,15 @@ -import { execSync } from 'child_process'; -import boxen from 'boxen'; +import boxen, { Options } from 'boxen'; import chalk from 'chalk'; +import { execSync } from 'child_process'; +import { BOXEN_SETTINGS } from 'stplr-utils'; export const chooseVercelTeam = async () => { console.log( boxen( chalk.bold('Choose a Vercel team to link your project to.\n\n') + - 'If you are not sure, you can skip this step by choosing "Cancel". This will link the project to the current logged-in user.', - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: '#FFFFFF', - }, + 'If you are not sure, you can skip this step by choosing "Cancel".\n' + + 'This will link the project to the current logged-in user.', + BOXEN_SETTINGS as Options, ), ); diff --git a/packages/core/installMachine/installSteps/vercel/deploy.ts b/packages/core/installMachine/installSteps/vercel/deploy.ts index 9aa08ef..e07beb1 100644 --- a/packages/core/installMachine/installSteps/vercel/deploy.ts +++ b/packages/core/installMachine/installSteps/vercel/deploy.ts @@ -5,7 +5,7 @@ import { getShortestVercelAlias } from './utils/getShortestVercelAlias'; import { type InstallMachineContext } from '../../../types'; export const deployVercelProject = async (stateData: InstallMachineContext['stateData']) => { - await logger.withSpinner('vercel', 'Connecting Vercel to Git...', async (spinner) => { + await logger.withSpinner('Connecting Vercel to Git...', async (spinner) => { try { // Execute 'vercel git connect' and capture the output await execAsync('npx vercel git connect'); @@ -17,7 +17,7 @@ export const deployVercelProject = async (stateData: InstallMachineContext['stat } }); - logger.log('vercel', 'Creating production deployment...'); + logger.log('Creating production deployment...'); const productionUrl = execSync('npx vercel --prod', { stdio: ['inherit', 'pipe', 'inherit'], @@ -26,7 +26,7 @@ export const deployVercelProject = async (stateData: InstallMachineContext['stat const shortestVercelAlias = await getShortestVercelAlias(productionUrl); - if (!productionUrl) logger.log('vercel', 'Failed to create production deployment.'); + if (!productionUrl) logger.log('Failed to create production deployment.'); stateData.prettyDeploymentUrl = productionUrl; diff --git a/packages/core/installMachine/installSteps/vercel/link.ts b/packages/core/installMachine/installSteps/vercel/link.ts index 834e93f..1ab1512 100644 --- a/packages/core/installMachine/installSteps/vercel/link.ts +++ b/packages/core/installMachine/installSteps/vercel/link.ts @@ -2,7 +2,7 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from 'stplr-utils'; export const linkVercelProject = async (projectName: string) => { - await logger.withSpinner('vercel', 'Linking project...', async (spinner) => { + await logger.withSpinner('Linking project...', async (spinner) => { try { await execAsync(`npx vercel link --yes --project ${projectName}`); spinner.succeed('Project linked successfully.'); diff --git a/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts b/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts index c817d8f..4ca3022 100644 --- a/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts +++ b/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts @@ -3,7 +3,7 @@ import { getDataFromVercelConfig } from '../../../utils/getDataFromVercelConfig' import { getVercelTokenFromAuthFile } from '../../../utils/getVercelTokenFromAuthFile'; export const updateVercelProjectSettings = async () => { - await logger.withSpinner('vercel', 'Changing project settings...', async (spinner) => { + await logger.withSpinner('Changing project settings...', async (spinner) => { try { const token = await getVercelTokenFromAuthFile(); if (!token) { diff --git a/packages/core/package.json b/packages/core/package.json index e2658d8..6c443a9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,15 +10,16 @@ "dependencies": { "boxen": "^8.0.1", "chalk": "^5.3.0", + "enquirer": "^2.4.1", "fs-extra": "^11.2.0", "gradient-string": "^3.0.0", "inquirer": "^10.2.2", "xstate": "^5.18.2" }, "devDependencies": { - "stplr-utils": "workspace:*", "@types/fs-extra": "^11.0.4", "@types/node": "^22.5.4", + "stplr-utils": "workspace:*", "tsup": "^8.3.0", "typescript": "^5.6.2" } diff --git a/packages/utils/logger.ts b/packages/utils/logger.ts deleted file mode 100644 index e69cce9..0000000 --- a/packages/utils/logger.ts +++ /dev/null @@ -1,129 +0,0 @@ -import chalk from 'chalk'; -import gradient from 'gradient-string'; -import ora, { Ora } from 'ora'; - -type Name = - | 'stapler' - | 'turborepo' - | 'supabase' - | 'tailwind' - | 'payload' - | 'github' - | 'prettier' - | 'deployment' - | 'vercel' - | 'docker' - | 'postgres' - | 'error'; - -type NameProps = { - name: Name; - prefix: string; - colors: string[]; -}; - -const names: NameProps[] = [ - { - name: 'stapler', - prefix: 'Stapler', - colors: ['#FAD400', '#FAD400'], - }, - { - name: 'turborepo', - prefix: 'Turbo', - colors: ['#0099F7', '#F11712'], - }, - { - name: 'supabase', - prefix: 'Supabase', - colors: ['#3ABC82', '#259764'], - }, - { - name: 'tailwind', - prefix: 'Tailwind', - colors: ['#38B2AC', '#0099F7'], - }, - { - name: 'payload', - prefix: 'Payload', - colors: ['#12324A', '#E5AA5F'], - }, - { - name: 'github', - prefix: 'GitHub', - colors: ['#3B8640', '#8256D0'], - }, - { - name: 'prettier', - prefix: 'Prettier', - colors: ['#F11D28', '#FFA12C'], - }, - { - name: 'deployment', - prefix: 'Deployment', - colors: ['#c83488', '#FAD400'], - }, - { - name: 'vercel', - prefix: 'Vercel', - colors: ['#FFF', '#FFF'], - }, - { - name: 'docker', - prefix: 'Docker', - colors: ['#0db7ed', '#0db7ed'], - }, - { - name: 'postgres', - prefix: 'PostgreSQL', - colors: ['#0064a5', '#008bb9'], - }, - { - name: 'error', - prefix: 'Error', - colors: ['#990000', '#FF0000'], - }, -]; - -const getPrefix = (name: Name): string => { - const color = names.find((color) => color.name === name); - if (!color) { - return chalk.red('[Error]'); - } - - const gradientColor = gradient(color.colors); - return name === 'vercel' ? chalk.bgBlack(gradientColor(`[▲ ${color.prefix}]`)) : gradientColor(`[${color.prefix}]`); -}; - -const log = (name: Name, messages: string[] | string): void => { - const prefix = getPrefix(name); - console.log(prefix, typeof messages === 'string' ? messages : messages.join(' ')); -}; - -const createSpinner = (name: Name, initialText?: string): Ora => { - const prefix = getPrefix(name); - return ora({ - prefixText: prefix, - text: initialText, - spinner: 'dots', - }); -}; - -const withSpinner = async (name: Name, initialText: string, action: (spinner: Ora) => Promise): Promise => { - const spinner = createSpinner(name, initialText); - try { - spinner.start(); - const result = await action(spinner); - return result; - } catch (error) { - spinner.fail(); - throw error; - } -}; - -// Example usage with named exports -export const logger = { - log, - createSpinner, - withSpinner, -}; diff --git a/packages/utils/logger/displayHeader.ts b/packages/utils/logger/displayHeader.ts new file mode 100644 index 0000000..24c09ab --- /dev/null +++ b/packages/utils/logger/displayHeader.ts @@ -0,0 +1,37 @@ +import chalk from 'chalk'; + +type DisplayHeaderType = (labelBgColor: string, labelTextColor: string, activeTextColor: string) => void; + +export const displayHeader: DisplayHeaderType = (labelBgColor, labelTextColor, activeTextColor) => { + const block = chalk.bgHex(labelBgColor).hex(labelBgColor)('█'); + const stplrText = chalk.bgHex(labelBgColor).hex(labelTextColor)('stplr'); + const stplrLeft = chalk.bgHex(labelBgColor).hex(labelTextColor)(String.fromCharCode(9484)); + const stplrMiddle = chalk.bgHex(labelBgColor).hex(labelTextColor)(String.fromCharCode(9472)); + const stplrRight = chalk.bgHex(labelBgColor).hex(labelTextColor)(String.fromCharCode(9488)); + const triangle = chalk.hex(labelBgColor)(String.fromCharCode(9701)); + + const logoLines = [ + `${block}${block}${block}${stplrLeft}${stplrMiddle}${stplrRight}${block}`, + `${block}${stplrText}${block}`, + ` ${triangle}`, + ` `, + ]; + + const title = 'Stapler setup initialized'; + const subText1 = ' '; + const subText2 = 'Everything is fine.'; + const subText3 = "I've done this like a million times."; + + const padding = 2; + + const output = [ + '', // Initial newline + ` ${logoLines[0]}${' '.repeat(padding)}${chalk.hex(activeTextColor)(title)}`, + ` ${logoLines[1]}${' '.repeat(padding)}${chalk.gray(subText1)}`, + ` ${logoLines[2]}${' '.repeat(padding)}${chalk.gray(subText2)}`, + ` ${logoLines[3]}${' '.repeat(padding)}${chalk.gray(subText3)}`, + '', + ].join('\n'); + + console.log(output); +}; diff --git a/packages/utils/logger/index.ts b/packages/utils/logger/index.ts new file mode 100644 index 0000000..1ff09ef --- /dev/null +++ b/packages/utils/logger/index.ts @@ -0,0 +1 @@ +export * from './logger'; diff --git a/packages/utils/logger/logger.ts b/packages/utils/logger/logger.ts new file mode 100644 index 0000000..1dc5599 --- /dev/null +++ b/packages/utils/logger/logger.ts @@ -0,0 +1,154 @@ +import chalk from 'chalk'; +import ora, { Ora } from 'ora'; +import { displayHeader } from './displayHeader'; + +export const SPACING = 2; +export const LABEL_WIDTH = 9; +export const LABEL_BG_COLOR = '#FAD400'; +export const LABEL_TEXT_COLOR = '#000000'; +export const LABEL_SECONDARY_TEXT_COLOR = '#FAD400'; +export const DIMMED_COLOR = '#5C4D00'; +export const CHECK_MARK_COLOR = '#FAD400'; +export const ACTIVE_TEXT_COLOR = '#FFFFFF'; +export const COMPLETED_TEXT_COLOR = '#666666'; +export const BORDER_COLOR = '#FAD400'; + +export const LEFT_PADDING = ' '.repeat(SPACING + LABEL_WIDTH - 1); +export const QUESTION_MARK = chalk.hex(CHECK_MARK_COLOR)('?'); +export const BOXEN_SETTINGS = { + padding: 1, + margin: { left: 11, right: 11, top: 1, bottom: 1 }, + borderStyle: 'round', + borderColor: BORDER_COLOR, +}; + +type Name = + | 'dir' + | 'cms' + | 'git' + | 'db' + | 'auth' + | 'stapler' + | 'turborepo' + | 'supabase' + | 'tailwind' + | 'payload' + | 'github' + | 'prettier' + | 'deployment' + | 'vercel' + | 'docker' + | 'postgres' + | 'error'; + +interface LabelConfig { + text: string; +} + +const labels: Record = { + dir: { text: 'dir' }, + cms: { text: 'cms' }, + git: { text: 'git' }, + db: { text: 'db' }, + auth: { text: 'auth' }, + stapler: { text: 'stplr' }, + turborepo: { text: 'turbo' }, + supabase: { text: 'supa' }, + tailwind: { text: 'tw' }, + payload: { text: 'cms' }, + github: { text: 'git' }, + prettier: { text: 'fmt' }, + deployment: { text: 'dep' }, + vercel: { text: 'vrcl' }, + docker: { text: 'dock' }, + postgres: { text: 'pg' }, + error: { text: 'error' }, +}; + +const gap = ' '.repeat(SPACING); +const leftPadding = ' '.repeat(LABEL_WIDTH); + +const formatLabel = (name: Name): string => { + const labelText = ` ${labels[name].text} `; + const label = chalk.bgHex(LABEL_BG_COLOR).hex(LABEL_TEXT_COLOR)(labelText); + const padding = ' '.repeat(LABEL_WIDTH - labelText.length); + + return `${padding}${label}`; +}; + +const formatCheckMark = (): string => { + return chalk.hex(CHECK_MARK_COLOR)('✓'); +}; + +const formatMessage = (message: string, isCompleted: boolean = false): string => { + return isCompleted ? chalk.hex(COMPLETED_TEXT_COLOR)(message) : chalk.hex(ACTIVE_TEXT_COLOR)(message); +}; + +const log = (messages: string[] | string, showCheck: boolean = true): void => { + // Print current message + const checkmark = showCheck ? formatCheckMark() + ' ' : ' '; + + messages = Array.isArray(messages) ? messages : [messages]; + messages.forEach((message) => { + console.log(`${leftPadding}${gap}${checkmark}${formatMessage(message)}`); + }); +}; + +const withLabel = (label: Name, message: string): void => { + const formattedLabel = formatLabel(label); + console.log(` `); + console.log(`${formattedLabel}${gap}${formatMessage(message)}`); + console.log(` `); +}; + +const createSpinner = (initialText?: string): Ora => { + const padding = ' '.repeat(SPACING); + + const spinner = { + frames: [ + `${chalk.hex(LABEL_BG_COLOR)('[')} ${chalk.hex(DIMMED_COLOR)(']')}`, + ` ${chalk.hex(LABEL_BG_COLOR)('[')}${chalk.hex(DIMMED_COLOR)(']')} `, + ` ${chalk.hex(DIMMED_COLOR)(']')}${chalk.hex(LABEL_BG_COLOR)('[')} `, + `${chalk.hex(DIMMED_COLOR)(']')} ${chalk.hex(LABEL_BG_COLOR)('[')}`, + ` ${chalk.hex(DIMMED_COLOR)(']')}${chalk.hex(LABEL_BG_COLOR)('[')} `, + ` ${chalk.hex(LABEL_BG_COLOR)('[')}${chalk.hex(DIMMED_COLOR)(']')} `, + ], + interval: 140, + }; + return ora({ + suffixText: `${padding}`, + text: initialText, + spinner: spinner, + color: 'yellow', + indent: LABEL_WIDTH + SPACING, + }); +}; + +const withSpinner = async (initialText: string, action: (spinner: Ora) => Promise): Promise => { + const spinner = createSpinner(initialText); + try { + spinner.start(); + + // overwrite success checkmark + spinner.succeed = (text: string) => { + return spinner.stopAndPersist({ + text, + symbol: chalk.hex(CHECK_MARK_COLOR)('✔'), + }); + }; + const result = await action(spinner); + spinner.succeed(); + return result; + } catch (error) { + spinner.fail(); + throw error; + } +}; + +export const logger = { + createSpinner, + log, + withSpinner, + withLabel, + displayHeader: () => displayHeader(LABEL_BG_COLOR, LABEL_TEXT_COLOR, ACTIVE_TEXT_COLOR), +}; diff --git a/patches/ansi-colors@4.1.3.patch b/patches/ansi-colors@4.1.3.patch new file mode 100644 index 0000000..b3c01ec --- /dev/null +++ b/patches/ansi-colors@4.1.3.patch @@ -0,0 +1,30 @@ +diff --git a/symbols.js b/symbols.js +index 02ab25799d8846be9861569f4edf5295dd6247ae..b8ed609981a538e940c63f0ded65ba2c527bc23c 100644 +--- a/symbols.js ++++ b/symbols.js +@@ -33,6 +33,7 @@ const common = { + upDownArrow: '↕' + }; + ++ + const windows = Object.assign({}, common, { + check: '√', + cross: '×', +@@ -47,6 +48,7 @@ const windows = Object.assign({}, common, { + warning: 'â€ŧ' + }); + ++const hex = (color) => `\x1b[38;2;${parseInt(color.slice(1, 3), 16)};${parseInt(color.slice(3, 5), 16)};${parseInt(color.slice(5, 7), 16)}m`; + const other = Object.assign({}, common, { + ballotCross: '✘', + check: '✔', +@@ -56,7 +58,8 @@ const other = Object.assign({}, common, { + info: 'ℹ', + questionFull: 'īŧŸ', + questionSmall: 'īš–', +- pointer: isLinux ? '▸' : '❯', ++ pointer: isLinux ? '▸' : ` ${hex('#FAD400')}❯ \x1b[0m` ++ , + pointerSmall: isLinux ? 'â€Ŗ' : 'â€ē', + radioOff: '◯', + radioOn: '◉', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc874ca..946125f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + ansi-colors@4.1.3: + hash: vgudubycctmxevfq2gz364c7qu + path: patches/ansi-colors@4.1.3.patch + importers: .: @@ -25,7 +30,7 @@ importers: version: 2.1.3(eslint@8.57.1) eslint-import-resolver-alias: specifier: ^1.1.2 - version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)) + version: 1.1.2(eslint-plugin-import@2.31.0) eslint-import-resolver-node: specifier: ^0.3.9 version: 0.3.9 @@ -34,7 +39,7 @@ importers: version: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-module-utils: specifier: ^2.8.0 - version: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + version: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-eslint-comments: specifier: ^3.2.0 version: 3.2.0(eslint@8.57.1) @@ -111,12 +116,21 @@ importers: packages/cli: dependencies: + ansi-colors: + specifier: 4.1.3 + version: 4.1.3(patch_hash=vgudubycctmxevfq2gz364c7qu) + boxen: + specifier: ^8.0.1 + version: 8.0.1 chalk: specifier: ^5.3.0 version: 5.3.0 commander: specifier: ^12.1.0 version: 12.1.0 + enquirer: + specifier: ^2.4.1 + version: 2.4.1 gradient-string: specifier: ^3.0.0 version: 3.0.0 @@ -151,6 +165,9 @@ importers: chalk: specifier: ^5.3.0 version: 5.3.0 + enquirer: + specifier: ^2.4.1 + version: 2.4.1 fs-extra: specifier: ^11.2.0 version: 11.2.0 @@ -3128,7 +3145,7 @@ snapshots: '@changesets/types': 6.0.0 '@changesets/write': 0.3.2 '@manypkg/get-packages': 1.1.3 - ansi-colors: 4.1.3 + ansi-colors: 4.1.3(patch_hash=vgudubycctmxevfq2gz364c7qu) ci-info: 3.9.0 enquirer: 2.4.1 external-editor: 3.1.0 @@ -3878,7 +3895,7 @@ snapshots: dependencies: string-width: 4.2.3 - ansi-colors@4.1.3: {} + ansi-colors@4.1.3(patch_hash=vgudubycctmxevfq2gz364c7qu): {} ansi-escapes@4.3.2: dependencies: @@ -4232,7 +4249,7 @@ snapshots: enquirer@2.4.1: dependencies: - ansi-colors: 4.1.3 + ansi-colors: 4.1.3(patch_hash=vgudubycctmxevfq2gz364c7qu) strip-ansi: 6.0.1 error-ex@1.3.2: @@ -4431,7 +4448,7 @@ snapshots: eslint: 8.57.1 eslint-plugin-turbo: 2.1.3(eslint@8.57.1) - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0): dependencies: eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) @@ -4449,7 +4466,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -4462,7 +4479,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -4490,7 +4507,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3