diff --git a/.github/workflows/ci-create-app-e2e-minimal.yaml b/.github/workflows/ci-create-app-e2e-minimal.yaml index 781fcac2761..f8126a333e2 100644 --- a/.github/workflows/ci-create-app-e2e-minimal.yaml +++ b/.github/workflows/ci-create-app-e2e-minimal.yaml @@ -105,7 +105,7 @@ jobs: create-twenty-app --version mkdir -p /tmp/e2e-test-workspace cd /tmp/e2e-test-workspace - create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance + create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance --yes - name: Install scaffolded app dependencies run: | diff --git a/packages/create-twenty-app/src/create-app.command.ts b/packages/create-twenty-app/src/create-app.command.ts index bb49a086150..cf4e3345380 100644 --- a/packages/create-twenty-app/src/create-app.command.ts +++ b/packages/create-twenty-app/src/create-app.command.ts @@ -16,7 +16,6 @@ import { containerExists, detectLocalServer, serverStart, - type ServerStartResult, } from 'twenty-sdk/cli'; import { isDefined } from 'twenty-shared/utils'; @@ -33,6 +32,8 @@ type CreateAppOptions = { }; export class CreateAppCommand { + private static TOTAL_STEPS = 4; + async execute(options: CreateAppOptions = {}): Promise { const { appName, appDisplayName, appDirectory, appDescription } = await this.getAppInfos(options); @@ -40,9 +41,26 @@ export class CreateAppCommand { try { await this.validateDirectory(appDirectory); - this.logCreationInfo({ appDirectory, appName }); + const confirmed = await this.promptScaffoldConfirmation({ + appName, + appDisplayName, + appDescription, + appDirectory, + autoConfirm: options.yes, + }); + if (!confirmed) { + console.log(chalk.gray('\nScaffolding cancelled.')); + process.exit(0); + } + + console.log(''); + + this.logStep(1, 'Creating project directory'); await fs.ensureDir(appDirectory); + this.logDetail(appDirectory); + + this.logStep(2, 'Scaffolding project files'); if (options.example) { const exampleSucceeded = await this.tryDownloadExample( @@ -56,6 +74,7 @@ export class CreateAppCommand { appDisplayName, appDescription, appDirectory, + onProgress: (message) => this.logDetail(message), }); } } else { @@ -64,33 +83,59 @@ export class CreateAppCommand { appDisplayName, appDescription, appDirectory, + onProgress: (message) => this.logDetail(message), }); } - await install(appDirectory); + this.logStep(3, 'Installing dependencies'); + await install(appDirectory, (message) => this.logDetail(message)); - await tryGitInit(appDirectory); + this.logStep(4, 'Initializing Git repository'); + const gitInitialized = await tryGitInit(appDirectory); - let serverResult: ServerStartResult | undefined; + if (gitInitialized) { + this.logDetail('Initialized on branch main'); + this.logDetail('Created initial commit'); + } else { + this.logDetail( + 'Skipped (Git unavailable, initialization failed, or already in a repository)', + ); + } + + console.log(''); + + let hasLocalServer = false; + let authSucceeded = false; if (!options.skipLocalInstance) { - const shouldStartServer = await this.shouldStartServer(options.yes); + const existingServerUrl = await detectLocalServer(); - if (shouldStartServer) { - const startResult = await serverStart({ - onProgress: (message: string) => console.log(chalk.gray(message)), - }); + if (existingServerUrl) { + hasLocalServer = true; + authSucceeded = await this.promptConnectToLocal(existingServerUrl); + } else { + const shouldStart = await this.shouldStartServer(options.yes); - if (startResult.success) { - serverResult = startResult.data; - await this.promptConnectToLocal(serverResult.url); + if (shouldStart) { + const startResult = await serverStart({ + onProgress: (message: string) => console.log(chalk.gray(message)), + }); + + if (startResult.success) { + hasLocalServer = true; + authSucceeded = await this.promptConnectToLocal( + startResult.data.url, + ); + } else { + console.log(chalk.yellow(`\n${startResult.error.message}`)); + } } else { - console.log(chalk.yellow(`\n${startResult.error.message}`)); + this.logServerSkipped(); } } } - this.logSuccess(appDirectory, serverResult); + this.logSuccess(appDirectory, hasLocalServer, authSucceeded); } catch (error) { console.error( chalk.red('\nCreate application failed:'), @@ -213,25 +258,80 @@ export class CreateAppCommand { } } - private logCreationInfo({ - appDirectory, + private async promptScaffoldConfirmation({ appName, + appDisplayName, + appDescription, + appDirectory, + autoConfirm, }: { - appDirectory: string; appName: string; - }): void { + appDisplayName: string; + appDescription: string; + appDirectory: string; + autoConfirm?: boolean; + }): Promise { + console.log(chalk.blue('\nCreating Twenty Application\n')); + console.log(chalk.white(` Name: ${appName}`)); + console.log(chalk.white(` Display name: ${appDisplayName}`)); + + if (appDescription) { + console.log(chalk.white(` Description: ${appDescription}`)); + } + + console.log(chalk.white(` Directory: ${appDirectory}`)); + + console.log(chalk.white('\nThe following steps will be performed:\n')); + console.log(chalk.gray(' 1. Create project directory')); console.log( - chalk.blue('\n', 'Creating Twenty Application\n'), - chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`), + chalk.gray( + ' 2. Scaffold project files from base template\n' + + ' - Copy template files\n' + + ' - Configure dotfiles (.gitignore, .github)\n' + + ' - Generate unique application identifiers\n' + + ' - Update package.json with app name and SDK versions', + ), + ); + console.log(chalk.gray(' 3. Install dependencies (yarn)')); + console.log( + chalk.gray(' 4. Initialize Git repository with initial commit'), + ); + console.log(''); + + if (autoConfirm) { + return true; + } + + const { proceed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'proceed', + message: 'Proceed?', + default: true, + }, + ]); + + return proceed; + } + + private logStep(step: number, title: string): void { + console.log( + chalk.blue(`\n[${step}/${CreateAppCommand.TOTAL_STEPS}]`) + + chalk.white(` ${title}...`), ); } - private async shouldStartServer(autoConfirm?: boolean): Promise { - const existingServerUrl = await detectLocalServer(); + private logDetail(message: string): void { + console.log(chalk.gray(` → ${message}`)); + } - if (existingServerUrl) { - return true; - } + private async shouldStartServer(autoConfirm?: boolean): Promise { + console.log( + chalk.white( + '\n A local Twenty instance is required for app development.\n' + + ' It provides the API and schema your application connects to.\n', + ), + ); if (checkDockerRunning() && containerExists()) { if (autoConfirm) { @@ -268,12 +368,31 @@ export class CreateAppCommand { return startDocker; } - private async promptConnectToLocal(serverUrl: string): Promise { + private logServerSkipped(): void { + console.log( + chalk.gray( + '\n To start a Twenty instance later:\n' + + ' yarn twenty server start\n\n' + + ' To connect to a remote instance instead:\n' + + ' yarn twenty remote add\n', + ), + ); + } + + private async promptConnectToLocal(serverUrl: string): Promise { + console.log( + chalk.white( + '\n Authentication links your app to a Twenty instance so you can\n' + + ' sync custom objects, fields, and roles during development.\n' + + ' This will open a browser window to complete the OAuth flow.\n', + ), + ); + const { shouldAuthenticate } = await inquirer.prompt([ { type: 'confirm', name: 'shouldAuthenticate', - message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`, + message: `Authenticate to the local Twenty instance (${serverUrl})?`, default: true, }, ]); @@ -281,13 +400,22 @@ export class CreateAppCommand { if (!shouldAuthenticate) { console.log( chalk.gray( - 'Authentication skipped. Run `yarn twenty remote add --local` manually.', + '\n Authentication skipped. To authenticate later:\n' + + ` yarn twenty remote add --local\n`, ), ); - return; + return false; } + await inquirer.prompt([ + { + type: 'input', + name: 'confirm', + message: 'Press Enter to open the browser for authentication...', + }, + ]); + try { const result = await authLoginOAuth({ apiUrl: serverUrl, @@ -298,12 +426,16 @@ export class CreateAppCommand { const configService = new ConfigService(); await configService.setDefaultRemote('local'); + + return true; } else { console.log( chalk.yellow( 'Authentication failed. Run `yarn twenty remote add --local` manually.', ), ); + + return false; } } catch { console.log( @@ -311,28 +443,44 @@ export class CreateAppCommand { 'Authentication failed. Run `yarn twenty remote add` manually.', ), ); + + return false; } } private logSuccess( appDirectory: string, - serverResult?: ServerStartResult, + hasLocalServer: boolean, + authSucceeded: boolean, ): void { const dirName = basename(appDirectory); - console.log(chalk.blue('\nApplication created. Next steps:')); - console.log(chalk.gray(`- cd ${dirName}`)); + console.log(chalk.green('\nāœ” Application created successfully!\n')); + console.log(chalk.white(' Next steps:\n')); - if (!serverResult) { - console.log( - chalk.gray( - '- yarn twenty remote add # Authenticate with Twenty', - ), - ); + let stepNumber = 1; + + console.log(chalk.white(` ${stepNumber}. Navigate to your project`)); + console.log(chalk.cyan(` cd ${dirName}\n`)); + stepNumber++; + + if (!authSucceeded) { + const remoteCommand = hasLocalServer + ? 'yarn twenty remote add --local' + : 'yarn twenty remote add'; + + console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`)); + console.log(chalk.cyan(` ${remoteCommand}\n`)); + stepNumber++; } + console.log(chalk.white(` ${stepNumber}. Start developing`)); + console.log(chalk.cyan(' yarn twenty dev\n')); + console.log( - chalk.gray('- yarn twenty dev # Start dev mode'), + chalk.gray( + ' Documentation: https://docs.twenty.com/developers/extend/capabilities/apps', + ), ); } } diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index fe7ba74d1d9..f16309726f3 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -3,7 +3,6 @@ import { join } from 'path'; import { v4 } from 'uuid'; import createTwentyAppPackageJson from 'package.json'; -import chalk from 'chalk'; const SRC_FOLDER = 'src'; @@ -12,27 +11,33 @@ export const copyBaseApplicationProject = async ({ appDisplayName, appDescription, appDirectory, + onProgress, }: { appName: string; appDisplayName: string; appDescription: string; appDirectory: string; + onProgress?: (message: string) => void; }) => { - console.log(chalk.gray('Generating application project...')); + onProgress?.('Copying base template'); await fs.copy(join(__dirname, './constants/template'), appDirectory); + onProgress?.('Configuring dotfiles (.gitignore, .github)'); await renameDotfiles({ appDirectory }); + onProgress?.('Mirroring AGENTS.md to CLAUDE.md'); await mirrorAgentsToClaude({ appDirectory }); await addEmptyPublicDirectory({ appDirectory }); + onProgress?.('Generating unique application identifiers'); await generateUniversalIdentifiers({ appDisplayName, appDescription, appDirectory, }); + onProgress?.('Updating package.json'); await updatePackageJson({ appName, appDirectory }); }; diff --git a/packages/create-twenty-app/src/utils/install.ts b/packages/create-twenty-app/src/utils/install.ts index ee200794e38..cea60681ec0 100644 --- a/packages/create-twenty-app/src/utils/install.ts +++ b/packages/create-twenty-app/src/utils/install.ts @@ -4,14 +4,18 @@ import { exec } from 'child_process'; const execPromise = promisify(exec); -export const install = async (root: string) => { - console.log(chalk.gray('Installing yarn dependencies...')); +export const install = async ( + root: string, + onProgress?: (message: string) => void, +) => { + onProgress?.('Enabling corepack'); try { await execPromise('corepack enable', { cwd: root }); } catch (error: any) { - console.warn(chalk.yellow('corepack enabled failed:'), error.stderr); + console.warn(chalk.yellow('corepack enable failed:'), error.stderr); } + onProgress?.('Running yarn install'); try { await execPromise('yarn install', { cwd: root }); } catch (error: any) { diff --git a/packages/twenty-sdk/src/cli/operations/server-start.ts b/packages/twenty-sdk/src/cli/operations/server-start.ts index 2eb6f034fbb..d06feb5ffbb 100644 --- a/packages/twenty-sdk/src/cli/operations/server-start.ts +++ b/packages/twenty-sdk/src/cli/operations/server-start.ts @@ -210,7 +210,7 @@ const innerServerStart = async ( onProgress?.('Starting existing container...'); execSync(`docker start ${containerName}`, { stdio: 'ignore' }); } else { - onProgress?.('Starting Twenty container...'); + onProgress?.('Pulling Docker image and starting Twenty container...'); const runResult = spawnSync( 'docker',