From cc2be505c0581feaa2a3bbd4e33740805b946cd2 Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 24 Mar 2026 10:31:05 +0100 Subject: [PATCH] Fix twenty app dev image (#18852) as title --- packages/create-twenty-app/README.md | 8 ++ packages/create-twenty-app/package.json | 2 +- packages/create-twenty-app/src/cli.ts | 8 ++ .../src/create-app.command.ts | 50 ++++++------- .../create-twenty-app/src/utils/install.ts | 3 +- .../src/utils/setup-local-instance.ts | 71 +++++++++--------- .../extend/apps/getting-started.mdx | 16 +++- .../developers/extend/capabilities/apps.mdx | 12 +++ packages/twenty-sdk/README.md | 15 +++- packages/twenty-sdk/package.json | 2 +- packages/twenty-sdk/src/cli/commands/build.ts | 3 +- .../twenty-sdk/src/cli/commands/deploy.ts | 3 +- packages/twenty-sdk/src/cli/commands/exec.ts | 14 +--- packages/twenty-sdk/src/cli/commands/logs.ts | 4 +- .../twenty-sdk/src/cli/commands/publish.ts | 3 +- .../twenty-sdk/src/cli/commands/remote.ts | 15 ++-- .../twenty-sdk/src/cli/commands/server.ts | 63 +++++++++------- .../twenty-sdk/src/cli/commands/typecheck.ts | 5 +- .../twenty-sdk/src/cli/commands/uninstall.ts | 3 +- .../utilities/server/detect-local-server.ts | 8 +- .../utilities/server/setup-local-instance.ts | 74 +++++++++++++++++++ 21 files changed, 255 insertions(+), 127 deletions(-) create mode 100644 packages/twenty-sdk/src/cli/utilities/server/setup-local-instance.ts diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index d367dc76e12..488618aefff 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -121,6 +121,14 @@ yarn twenty server reset # Wipe all data and start fresh The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`). +### How to use a local Twenty instance + +If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`): + +```bash +npx create-twenty-app@latest my-app --port 3000 +``` + ## Next steps - Run `yarn twenty help` to see all available commands. diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index aeea4cd7fa8..4cf12fb0f15 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "0.8.0-canary.1", + "version": "0.8.0-canary.2", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", diff --git a/packages/create-twenty-app/src/cli.ts b/packages/create-twenty-app/src/cli.ts index 9737149cead..e5f719374fa 100644 --- a/packages/create-twenty-app/src/cli.ts +++ b/packages/create-twenty-app/src/cli.ts @@ -31,6 +31,10 @@ const program = new Command(packageJson.name) '--skip-local-instance', 'Skip the local Twenty instance setup prompt', ) + .option( + '-p, --port ', + 'Port of an existing Twenty server (skips Docker setup)', + ) .helpOption('-h, --help', 'Display this help message.') .action( async ( @@ -42,6 +46,7 @@ const program = new Command(packageJson.name) displayName?: string; description?: string; skipLocalInstance?: boolean; + port?: string; }, ) => { const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean); @@ -71,6 +76,8 @@ const program = new Command(packageJson.name) const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive'; + const port = options?.port ? parseInt(options.port, 10) : undefined; + await new CreateAppCommand().execute({ directory, mode, @@ -78,6 +85,7 @@ const program = new Command(packageJson.name) displayName: options?.displayName, description: options?.description, skipLocalInstance: options?.skipLocalInstance, + port, }); }, ); diff --git a/packages/create-twenty-app/src/create-app.command.ts b/packages/create-twenty-app/src/create-app.command.ts index 200800ac5d6..16d0c270ee0 100644 --- a/packages/create-twenty-app/src/create-app.command.ts +++ b/packages/create-twenty-app/src/create-app.command.ts @@ -1,3 +1,4 @@ +import { basename } from 'path'; import { copyBaseApplicationProject } from '@/utils/app-template'; import { convertToLabel } from '@/utils/convert-to-label'; import { install } from '@/utils/install'; @@ -28,14 +29,15 @@ type CreateAppOptions = { displayName?: string; description?: string; skipLocalInstance?: boolean; + port?: number; }; export class CreateAppCommand { async execute(options: CreateAppOptions = {}): Promise { - try { - const { appName, appDisplayName, appDirectory, appDescription } = - await this.getAppInfos(options); + const { appName, appDisplayName, appDirectory, appDescription } = + await this.getAppInfos(options); + try { const exampleOptions = this.resolveExampleOptions( options.mode ?? 'exhaustive', ); @@ -46,7 +48,6 @@ export class CreateAppCommand { await fs.ensureDir(appDirectory); - console.log(chalk.gray(' Scaffolding project files...')); await copyBaseApplicationProject({ appName, appDisplayName, @@ -55,17 +56,14 @@ export class CreateAppCommand { exampleOptions, }); - console.log(chalk.gray(' Installing dependencies...')); await install(appDirectory); - console.log(chalk.gray(' Initializing git repository...')); await tryGitInit(appDirectory); let localResult: LocalInstanceResult = { running: false }; if (!options.skipLocalInstance) { - // Auto-detect a running server first - localResult = await setupLocalInstance(appDirectory); + localResult = await setupLocalInstance(appDirectory, options.port); if (localResult.running && localResult.serverUrl) { await this.connectToLocal(appDirectory, localResult.serverUrl); @@ -75,7 +73,7 @@ export class CreateAppCommand { this.logSuccess(appDirectory, localResult); } catch (error) { console.error( - chalk.red('Initialization failed:'), + chalk.red('\nCreate application failed:'), error instanceof Error ? error.message : error, ); process.exit(1); @@ -193,10 +191,10 @@ export class CreateAppCommand { appDirectory: string; appName: string; }): void { - console.log(chalk.blue('Creating Twenty Application')); - console.log(chalk.gray(` Directory: ${appDirectory}`)); - console.log(chalk.gray(` Name: ${appName}`)); - console.log(''); + console.log( + chalk.blue('\n', 'Creating Twenty Application\n'), + chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`), + ); } private async connectToLocal( @@ -204,18 +202,14 @@ export class CreateAppCommand { serverUrl: string, ): Promise { try { - execSync( - `npx nx run twenty-sdk:start -- remote add ${serverUrl} --as local`, - { - cwd: appDirectory, - stdio: 'inherit', - }, - ); - console.log(chalk.green('Authenticated with local Twenty instance.')); + execSync(`yarn twenty remote add ${serverUrl} --as local`, { + cwd: appDirectory, + stdio: 'inherit', + }); } catch { console.log( chalk.yellow( - 'Authentication skipped. Run `npx nx run twenty-sdk:start -- remote add --local` manually.', + 'Authentication skipped. Run `yarn twenty remote add --local` manually.', ), ); } @@ -225,23 +219,21 @@ export class CreateAppCommand { appDirectory: string, localResult: LocalInstanceResult, ): void { - const dirName = appDirectory.split('/').reverse()[0] ?? ''; + const dirName = basename(appDirectory); - console.log(chalk.green('Application created!')); - console.log(''); - console.log(chalk.blue('Next steps:')); - console.log(chalk.gray(` cd ${dirName}`)); + console.log(chalk.blue('\nApplication created. Next steps:')); + console.log(chalk.gray(`- cd ${dirName}`)); if (!localResult.running) { console.log( chalk.gray( - ' yarn twenty remote add --local # Authenticate with Twenty', + '- yarn twenty remote add --local # Authenticate with Twenty', ), ); } console.log( - chalk.gray(' yarn twenty dev # Start dev mode'), + chalk.gray('- yarn twenty dev # Start dev mode'), ); } } diff --git a/packages/create-twenty-app/src/utils/install.ts b/packages/create-twenty-app/src/utils/install.ts index 2fe67a52f21..ee200794e38 100644 --- a/packages/create-twenty-app/src/utils/install.ts +++ b/packages/create-twenty-app/src/utils/install.ts @@ -5,6 +5,7 @@ import { exec } from 'child_process'; const execPromise = promisify(exec); export const install = async (root: string) => { + console.log(chalk.gray('Installing yarn dependencies...')); try { await execPromise('corepack enable', { cwd: root }); } catch (error: any) { @@ -14,6 +15,6 @@ export const install = async (root: string) => { try { await execPromise('yarn install', { cwd: root }); } catch (error: any) { - console.error(chalk.red('yarn install failed:'), error.stdout); + console.warn(chalk.yellow('yarn install failed:'), error.stdout); } }; diff --git a/packages/create-twenty-app/src/utils/setup-local-instance.ts b/packages/create-twenty-app/src/utils/setup-local-instance.ts index 1e47928dfa0..be9cfda9d2d 100644 --- a/packages/create-twenty-app/src/utils/setup-local-instance.ts +++ b/packages/create-twenty-app/src/utils/setup-local-instance.ts @@ -1,8 +1,7 @@ import chalk from 'chalk'; import { execSync } from 'node:child_process'; -import { platform } from 'node:os'; -const DEFAULT_PORT = 2020; +const LOCAL_PORTS = [2020, 3000]; // Minimal health check — the full implementation lives in twenty-sdk const isServerReady = async (port: number): Promise => { @@ -24,6 +23,20 @@ const isServerReady = async (port: number): Promise => { } }; +const detectRunningServer = async ( + preferredPort?: number, +): Promise => { + const ports = preferredPort ? [preferredPort] : LOCAL_PORTS; + + for (const port of ports) { + if (await isServerReady(port)) { + return port; + } + } + + return null; +}; + export type LocalInstanceResult = { running: boolean; serverUrl?: string; @@ -31,20 +44,30 @@ export type LocalInstanceResult = { export const setupLocalInstance = async ( appDirectory: string, + preferredPort?: number, ): Promise => { - console.log(''); - console.log(chalk.blue('Setting up local Twenty instance...')); + const detectedPort = await detectRunningServer(preferredPort); - if (await isServerReady(DEFAULT_PORT)) { - const serverUrl = `http://localhost:${DEFAULT_PORT}`; + if (detectedPort) { + const serverUrl = `http://localhost:${detectedPort}`; - console.log(chalk.green(`Twenty server detected on ${serverUrl}.`)); + console.log(chalk.green(`Twenty server detected on ${serverUrl}.\n`)); return { running: true, serverUrl }; } - // Delegate to `twenty server start` from the scaffolded app - console.log(chalk.gray('Starting local Twenty server...')); + if (preferredPort) { + console.log( + chalk.yellow( + `No Twenty server found on port ${preferredPort}.\n` + + 'Start your server and run `yarn twenty remote add --local` manually.\n', + ), + ); + + return { running: false }; + } + + console.log(chalk.blue('Setting up local Twenty instance...\n')); try { execSync('yarn twenty server start', { @@ -52,38 +75,19 @@ export const setupLocalInstance = async ( stdio: 'inherit', }); } catch { - console.log( - chalk.yellow( - 'Failed to start Twenty server. Run `yarn twenty server start` manually.', - ), - ); - return { running: false }; } - console.log(chalk.gray('Waiting for Twenty to be ready...')); + console.log(chalk.gray('Waiting for Twenty to be ready...\n')); const startTime = Date.now(); const timeoutMs = 180 * 1000; while (Date.now() - startTime < timeoutMs) { - if (await isServerReady(DEFAULT_PORT)) { - const serverUrl = `http://localhost:${DEFAULT_PORT}`; + if (await isServerReady(LOCAL_PORTS[0])) { + const serverUrl = `http://localhost:${LOCAL_PORTS[0]}`; - console.log(chalk.green(`Twenty server is running on ${serverUrl}.`)); - console.log( - chalk.gray( - 'Workspace ready — login with tim@apple.dev / tim@apple.dev', - ), - ); - - const openCommand = platform() === 'darwin' ? 'open' : 'xdg-open'; - - try { - execSync(`${openCommand} ${serverUrl}`, { stdio: 'ignore' }); - } catch { - // Ignore if browser can't be opened - } + console.log(chalk.green(`Server running on '${serverUrl}'\n`)); return { running: true, serverUrl }; } @@ -93,7 +97,8 @@ export const setupLocalInstance = async ( console.log( chalk.yellow( - 'Twenty server did not become healthy in time. Check: yarn twenty server logs', + 'Twenty server did not become healthy in time.\n', + "Check: 'yarn twenty server logs'\n", ), ); diff --git a/packages/twenty-docs/developers/extend/apps/getting-started.mdx b/packages/twenty-docs/developers/extend/apps/getting-started.mdx index c654b5b9b40..8e8dc422747 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started.mdx @@ -19,7 +19,7 @@ Apps let you extend Twenty with custom objects, fields, logic functions, AI skil ## Prerequisites - Node.js 24+ and Yarn 4 -- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks) +- Docker (for the local Twenty dev server) ## Getting Started @@ -37,7 +37,7 @@ yarn twenty app:dev The scaffolder supports two modes for controlling which example files are included: ```bash filename="Terminal" -# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill) +# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent) npx create-twenty-app@latest my-app # Minimal: only core files (application-config.ts and default-role.ts) @@ -221,6 +221,18 @@ Then add a `twenty` script: Now you can run all commands via `yarn twenty `, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc. +## How to use a local Twenty instance + +If you're already running a Twenty instance locally (e.g. via `npx nx start twenty-server`), you can connect to it instead of using Docker: + +```bash filename="Terminal" +# During scaffolding — skip Docker, connect to your running instance +npx create-twenty-app@latest my-app --port 3000 + +# Or after scaffolding — add a remote pointing to your instance +yarn twenty remote add --local --port 3000 +``` + ## Troubleshooting - Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions. diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index 5d76e79945e..a1210cb69a4 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -77,6 +77,18 @@ npx create-twenty-app@latest my-app npx create-twenty-app@latest my-app --minimal ``` +### How to use a local Twenty instance + +If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`): + +```bash filename="Terminal" +# During scaffolding +npx create-twenty-app@latest my-app --port 3000 + +# Or after scaffolding +yarn twenty remote add --local --port 3000 +``` + From here you can: ```bash filename="Terminal" diff --git a/packages/twenty-sdk/README.md b/packages/twenty-sdk/README.md index 366c06bc3e8..cb68b501895 100644 --- a/packages/twenty-sdk/README.md +++ b/packages/twenty-sdk/README.md @@ -74,7 +74,7 @@ In a scaffolded project (via `create-twenty-app`), use `yarn twenty ` i Manage a local Twenty dev server (all-in-one Docker image). -- `twenty server start` — Start the local server (pulls image if needed). +- `twenty server start` — Start the local server (pulls image if needed). Automatically configures the `local` remote. - Options: - `-p, --port `: HTTP port (default: `2020`). - `twenty server stop` — Stop the local server. @@ -116,6 +116,7 @@ Manage remote server connections and authentication. - `--url `: Server URL (alternative to positional arg). - `--as `: Name for this remote (otherwise derived from URL hostname). - `--local`: Connect to local development server (`http://localhost:2020`) via OAuth. + - `--port `: Port for local server (use with `--local`). - Behavior: If `nameOrUrl` matches an existing remote name, re-authenticates it. Otherwise, creates a new remote and authenticates via OAuth (with API key fallback). - `twenty remote remove ` — Remove a remote and its credentials. @@ -327,6 +328,18 @@ Notes: - `twenty remote switch` sets the `defaultRemote` field, used when `-r` is not specified. - `twenty remote list` shows all configured remotes and their authentication status. +## How to use a local Twenty instance + +If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`): + +```bash +# During scaffolding +npx create-twenty-app@latest my-app --port 3000 + +# Or after scaffolding +twenty remote add --local --port 3000 +``` + ## Troubleshooting - Auth errors: run `twenty remote add` again (or add a new remote) and ensure the API key has the required permissions. diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 88b5ba3571c..23c2f558f8c 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "0.8.0-canary.1", + "version": "0.8.0-canary.2", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/sdk/index.d.ts", diff --git a/packages/twenty-sdk/src/cli/commands/build.ts b/packages/twenty-sdk/src/cli/commands/build.ts index 451b0fe4b16..26ca92dd9c0 100644 --- a/packages/twenty-sdk/src/cli/commands/build.ts +++ b/packages/twenty-sdk/src/cli/commands/build.ts @@ -15,8 +15,7 @@ export class AppBuildCommand { await checkSdkVersionCompatibility(appPath); console.log(chalk.blue('Building application...')); - console.log(chalk.gray(`App path: ${appPath}`)); - console.log(''); + console.log(chalk.gray(`App path: ${appPath}\n`)); const result = await appBuild({ appPath, diff --git a/packages/twenty-sdk/src/cli/commands/deploy.ts b/packages/twenty-sdk/src/cli/commands/deploy.ts index c5db2a7c41f..d77cb709e0a 100644 --- a/packages/twenty-sdk/src/cli/commands/deploy.ts +++ b/packages/twenty-sdk/src/cli/commands/deploy.ts @@ -36,8 +36,7 @@ export class DeployCommand { const remoteName = options.remote ?? ConfigService.getActiveRemote(); console.log(chalk.blue(`Deploying to ${remoteName} (${serverUrl})...`)); - console.log(chalk.gray(`App path: ${appPath}`)); - console.log(''); + console.log(chalk.gray(`App path: ${appPath}\n`)); const result = await appDeploy({ appPath, diff --git a/packages/twenty-sdk/src/cli/commands/exec.ts b/packages/twenty-sdk/src/cli/commands/exec.ts index ca284dbfe67..a68562bb708 100644 --- a/packages/twenty-sdk/src/cli/commands/exec.ts +++ b/packages/twenty-sdk/src/cli/commands/exec.ts @@ -33,8 +33,7 @@ export class LogicFunctionExecuteCommand { : (functionUniversalIdentifier ?? functionName); console.log(chalk.blue(`🚀 Executing function "${identifier}"...`)); - console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}`)); - console.log(''); + console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`)); const executeOptions = postInstall ? { appPath, postInstall: true as const, payload: parsedPayload } @@ -58,8 +57,7 @@ export class LogicFunctionExecuteCommand { break; } case FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND: { - console.error(chalk.red(result.error.message)); - console.log(''); + console.error(chalk.red(result.error.message), '\n'); const availableFunctions = (result.error.details ?.availableFunctions ?? []) as Array<{ @@ -106,30 +104,26 @@ export class LogicFunctionExecuteCommand { `${chalk.bold('Status:')} ${statusColor(executionResult.status)}`, ); - console.log(`${chalk.bold('Duration:')} ${executionResult.duration}ms`); + console.log(`${chalk.bold('Duration:')} ${executionResult.duration}ms\n`); if (isDefined(executionResult.data)) { - console.log(''); console.log(chalk.bold('Data:')); console.log(chalk.white(JSON.stringify(executionResult.data, null, 2))); } if (executionResult.error) { - console.log(''); console.log(chalk.bold.red('Error:')); console.log(chalk.red(` Type: ${executionResult.error.errorType}`)); console.log( - chalk.red(` Message: ${executionResult.error.errorMessage}`), + chalk.red(` Message: ${executionResult.error.errorMessage}\n`), ); if (executionResult.error.stackTrace) { - console.log(''); console.log(chalk.gray('Stack trace:')); console.log(chalk.gray(executionResult.error.stackTrace)); } } if (executionResult.logs) { - console.log(''); console.log(chalk.bold('Logs:')); console.log(chalk.gray(executionResult.logs)); } diff --git a/packages/twenty-sdk/src/cli/commands/logs.ts b/packages/twenty-sdk/src/cli/commands/logs.ts index 49e66cbe9b2..5b172be4b07 100644 --- a/packages/twenty-sdk/src/cli/commands/logs.ts +++ b/packages/twenty-sdk/src/cli/commands/logs.ts @@ -59,9 +59,7 @@ export class LogicFunctionLogsCommand { : 'functions'; console.log( - chalk.blue(`🚀 Watching ${appPath} ${functionIdentifier} logs:`), + chalk.blue(`🚀 Watching ${appPath} ${functionIdentifier} logs:\n`), ); - - console.log(''); } } diff --git a/packages/twenty-sdk/src/cli/commands/publish.ts b/packages/twenty-sdk/src/cli/commands/publish.ts index eeda764d0ea..bb6270d9a50 100644 --- a/packages/twenty-sdk/src/cli/commands/publish.ts +++ b/packages/twenty-sdk/src/cli/commands/publish.ts @@ -15,8 +15,7 @@ export class AppPublishCommand { await checkSdkVersionCompatibility(appPath); console.log(chalk.blue('Publishing to npm...')); - console.log(chalk.gray(`App path: ${appPath}`)); - console.log(''); + console.log(chalk.gray(`App path: ${appPath}\n`)); const result = await appPublish({ appPath, diff --git a/packages/twenty-sdk/src/cli/commands/remote.ts b/packages/twenty-sdk/src/cli/commands/remote.ts index 6d12453baeb..623b49b572d 100644 --- a/packages/twenty-sdk/src/cli/commands/remote.ts +++ b/packages/twenty-sdk/src/cli/commands/remote.ts @@ -70,6 +70,7 @@ export const registerRemoteCommands = (program: Command): void => { .description('Add a new remote or re-authenticate an existing one') .option('--as ', 'Name for this remote') .option('--local', 'Connect to local development server') + .option('--port ', 'Port for local server (use with --local)') .option('--token ', 'API key for non-interactive auth') .option('--url ', 'Server URL (alternative to positional arg)') .action( @@ -78,6 +79,7 @@ export const registerRemoteCommands = (program: Command): void => { options: { as?: string; local?: boolean; + port?: string; token?: string; url?: string; }, @@ -87,7 +89,12 @@ export const registerRemoteCommands = (program: Command): void => { if (options.local) { const remoteName = options.as ?? 'local'; - const localUrl = await detectLocalServer(); + const preferredPort = options.port + ? parseInt(options.port, 10) + : undefined; + const localUrl = preferredPort + ? `http://localhost:${preferredPort}` + : await detectLocalServer(); if (!localUrl) { console.error( @@ -102,7 +109,6 @@ export const registerRemoteCommands = (program: Command): void => { console.log(chalk.gray(`Found server at ${localUrl}`)); ConfigService.setActiveRemote(remoteName); await authenticate(localUrl, options.token); - console.log(chalk.green(`✓ Authenticated remote "${remoteName}".`)); return; } @@ -116,7 +122,6 @@ export const registerRemoteCommands = (program: Command): void => { ConfigService.setActiveRemote(nameOrUrl); await authenticate(config.apiUrl, options.token); - console.log(chalk.green(`✓ Re-authenticated remote "${nameOrUrl}".`)); return; } @@ -156,8 +161,6 @@ export const registerRemoteCommands = (program: Command): void => { if (defaultRemote === 'local') { await configService.setDefaultRemote(name); } - - console.log(chalk.green(`✓ Authenticated remote "${name}".`)); }, ); @@ -196,8 +199,8 @@ export const registerRemoteCommands = (program: Command): void => { ); } - console.log(''); console.log( + '\n', chalk.gray("Use 'twenty remote switch ' to change default"), ); }); diff --git a/packages/twenty-sdk/src/cli/commands/server.ts b/packages/twenty-sdk/src/cli/commands/server.ts index 902573fa728..e05874ae68f 100644 --- a/packages/twenty-sdk/src/cli/commands/server.ts +++ b/packages/twenty-sdk/src/cli/commands/server.ts @@ -1,3 +1,4 @@ +import { ConfigService } from '@/cli/utilities/config/config-service'; import { checkServerHealth } from '@/cli/utilities/server/detect-local-server'; import chalk from 'chalk'; import type { Command } from 'commander'; @@ -45,6 +46,20 @@ const containerExists = (): boolean => { } }; +const checkDockerRunning = (): boolean => { + try { + execSync('docker info', { stdio: 'ignore' }); + + return true; + } catch { + console.error( + chalk.red('Docker is not running. Please start Docker and try again.'), + ); + + return false; + } +}; + const validatePort = (value: string): number => { const port = parseInt(value, 10); @@ -69,6 +84,12 @@ export const registerServerCommands = (program: Command): void => { let port = validatePort(options.port); if (await checkServerHealth(port)) { + const localUrl = `http://localhost:${port}`; + const configService = new ConfigService(); + + ConfigService.setActiveRemote('local'); + await configService.setConfig({ apiUrl: localUrl }); + console.log( chalk.green(`Twenty server is already running on localhost:${port}.`), ); @@ -76,6 +97,10 @@ export const registerServerCommands = (program: Command): void => { return; } + if (!checkDockerRunning()) { + process.exit(1); + } + if (isContainerRunning()) { console.log(chalk.gray('Container is running but not healthy yet.')); @@ -93,30 +118,13 @@ export const registerServerCommands = (program: Command): void => { ); } + port = existingPort; + console.log(chalk.gray('Starting existing container...')); execSync(`docker start ${CONTAINER_NAME}`, { stdio: 'ignore' }); - port = existingPort; } else { - try { - execSync('docker info', { stdio: 'ignore' }); - } catch { - console.error( - chalk.red( - 'Docker is not running. Please start Docker and try again.', - ), - ); - process.exit(1); - } - - console.log(chalk.gray(`Pulling ${IMAGE}...`)); - - try { - execSync(`docker pull ${IMAGE}`, { stdio: 'inherit' }); - } catch { - console.log(chalk.gray('Pull failed, trying local image...')); - } - console.log(chalk.gray('Starting Twenty container...')); + const runResult = spawnSync( 'docker', [ @@ -136,17 +144,18 @@ export const registerServerCommands = (program: Command): void => { ); if (runResult.status !== 0) { - console.error(chalk.red('Failed to start Twenty container.')); + console.error(chalk.red('\nFailed to start Twenty container.')); process.exit(runResult.status ?? 1); } } - console.log( - chalk.green(`Twenty server starting on http://localhost:${port}`), - ); - console.log( - chalk.gray('Run `yarn twenty server logs` to follow startup progress.'), - ); + const localUrl = `http://localhost:${port}`; + const configService = new ConfigService(); + + ConfigService.setActiveRemote('local'); + await configService.setConfig({ apiUrl: localUrl }); + + console.log(chalk.green(`\nLocal remote configured → ${localUrl}`)); }); server diff --git a/packages/twenty-sdk/src/cli/commands/typecheck.ts b/packages/twenty-sdk/src/cli/commands/typecheck.ts index 2216d9f6be1..3e2ec79f662 100644 --- a/packages/twenty-sdk/src/cli/commands/typecheck.ts +++ b/packages/twenty-sdk/src/cli/commands/typecheck.ts @@ -18,8 +18,7 @@ export class AppTypecheckCommand { const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY; console.log(chalk.blue('Running type check...')); - console.log(chalk.gray(`App path: ${appPath}`)); - console.log(''); + console.log(chalk.gray(`App path: ${appPath}\n`)); const errors = await runTypecheck(appPath); @@ -32,8 +31,8 @@ export class AppTypecheckCommand { console.log(formatTypecheckError(error)); } - console.log(''); console.log( + '\n', chalk.red( `✗ Found ${errors.length} type error${errors.length === 1 ? '' : 's'}`, ), diff --git a/packages/twenty-sdk/src/cli/commands/uninstall.ts b/packages/twenty-sdk/src/cli/commands/uninstall.ts index 59cd170bbcc..1dc41aadfa1 100644 --- a/packages/twenty-sdk/src/cli/commands/uninstall.ts +++ b/packages/twenty-sdk/src/cli/commands/uninstall.ts @@ -13,8 +13,7 @@ export class AppUninstallCommand { askForConfirmation: boolean; }): Promise> { console.log(chalk.blue('🚀 Uninstall Twenty Application')); - console.log(chalk.gray(`📁 App Path: ${appPath}`)); - console.log(''); + console.log(chalk.gray(`📁 App Path: ${appPath}\n`)); if (askForConfirmation && !(await this.confirmationPrompt())) { console.error(chalk.red('⛔️ Aborting uninstall')); diff --git a/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts b/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts index cfe4c8597a1..8a7df2d181c 100644 --- a/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts +++ b/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts @@ -19,8 +19,12 @@ export const checkServerHealth = async (port: number): Promise => { } }; -export const detectLocalServer = async (): Promise => { - for (const port of LOCAL_PORTS) { +export const detectLocalServer = async ( + preferredPort?: number, +): Promise => { + const ports = preferredPort ? [preferredPort] : LOCAL_PORTS; + + for (const port of ports) { if (await checkServerHealth(port)) { return `http://localhost:${port}`; } diff --git a/packages/twenty-sdk/src/cli/utilities/server/setup-local-instance.ts b/packages/twenty-sdk/src/cli/utilities/server/setup-local-instance.ts new file mode 100644 index 00000000000..2b18172d767 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/server/setup-local-instance.ts @@ -0,0 +1,74 @@ +import { + checkServerHealth, + detectLocalServer, +} from '@/cli/utilities/server/detect-local-server'; +import chalk from 'chalk'; +import { execSync } from 'node:child_process'; + +const LOCAL_PORTS = [2020, 3000]; + +export type LocalInstanceResult = { + running: boolean; + serverUrl?: string; +}; + +export const setupLocalInstance = async ( + appDirectory: string, + preferredPort?: number, +): Promise => { + const serverUrl = await detectLocalServer(preferredPort); + + if (serverUrl) { + console.log(chalk.green(`Twenty server detected on ${serverUrl}.\n`)); + + return { running: true, serverUrl }; + } + + if (preferredPort) { + console.log( + chalk.yellow( + `No Twenty server found on port ${preferredPort}.\n` + + 'Start your server and run `yarn twenty remote add --local` manually.\n', + ), + ); + + return { running: false }; + } + + console.log(chalk.blue('Setting up local Twenty instance...\n')); + + try { + execSync('yarn twenty server start', { + cwd: appDirectory, + stdio: 'inherit', + }); + } catch { + return { running: false }; + } + + console.log(chalk.gray('Waiting for Twenty to be ready...\n')); + + const startTime = Date.now(); + const timeoutMs = 180 * 1000; + + while (Date.now() - startTime < timeoutMs) { + if (await checkServerHealth(LOCAL_PORTS[0])) { + const serverUrl = `http://localhost:${LOCAL_PORTS[0]}`; + + console.log(chalk.green(`Server running on '${serverUrl}'\n`)); + + return { running: true, serverUrl }; + } + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + console.log( + chalk.yellow( + 'Twenty server did not become healthy in time.\n', + "Check: 'yarn twenty server logs'\n", + ), + ); + + return { running: false }; +};