Detail steps during create twenty app (#20374)

## Before
<img width="391" height="188" alt="image"
src="https://github.com/user-attachments/assets/78a0139f-d992-49cb-98ff-531bdbadc64b"
/>

## After
<img width="419" height="773" alt="image"
src="https://github.com/user-attachments/assets/da9b36a4-74e0-4445-b8c5-7a2329eb4f46"
/>
This commit is contained in:
martmull
2026-05-07 20:51:24 +00:00
committed by GitHub
parent 59f2b1c724
commit e294d74e07
5 changed files with 204 additions and 47 deletions
@@ -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: |
@@ -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<void> {
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<boolean> {
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<boolean> {
const existingServerUrl = await detectLocalServer();
private logDetail(message: string): void {
console.log(chalk.gray(`${message}`));
}
if (existingServerUrl) {
return true;
}
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
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<void> {
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<boolean> {
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('\nApplication 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',
),
);
}
}
@@ -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 });
};
@@ -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) {
@@ -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',