Fix twenty app dev image (#18852)

as title
This commit is contained in:
martmull
2026-03-24 10:31:05 +01:00
committed by GitHub
parent 493830204a
commit cc2be505c0
21 changed files with 255 additions and 127 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -31,6 +31,10 @@ const program = new Command(packageJson.name)
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option(
'-p, --port <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,
});
},
);

View File

@@ -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<void> {
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<void> {
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'),
);
}
}

View File

@@ -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);
}
};

View File

@@ -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<boolean> => {
@@ -24,6 +23,20 @@ const isServerReady = async (port: number): Promise<boolean> => {
}
};
const detectRunningServer = async (
preferredPort?: number,
): Promise<number | null> => {
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<LocalInstanceResult> => {
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",
),
);

View File

@@ -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 <command>`, 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.

View File

@@ -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"

View File

@@ -74,7 +74,7 @@ In a scaffolded project (via `create-twenty-app`), use `yarn twenty <command>` 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 <port>`: HTTP port (default: `2020`).
- `twenty server stop` — Stop the local server.
@@ -116,6 +116,7 @@ Manage remote server connections and authentication.
- `--url <url>`: Server URL (alternative to positional arg).
- `--as <name>`: Name for this remote (otherwise derived from URL hostname).
- `--local`: Connect to local development server (`http://localhost:2020`) via OAuth.
- `--port <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 <name>` — 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.

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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('');
}
}

View File

@@ -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,

View File

@@ -70,6 +70,7 @@ export const registerRemoteCommands = (program: Command): void => {
.description('Add a new remote or re-authenticate an existing one')
.option('--as <name>', 'Name for this remote')
.option('--local', 'Connect to local development server')
.option('--port <port>', 'Port for local server (use with --local)')
.option('--token <token>', 'API key for non-interactive auth')
.option('--url <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 <name>' to change default"),
);
});

View File

@@ -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

View File

@@ -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'}`,
),

View File

@@ -13,8 +13,7 @@ export class AppUninstallCommand {
askForConfirmation: boolean;
}): Promise<ApiResponse<any>> {
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'));

View File

@@ -19,8 +19,12 @@ export const checkServerHealth = async (port: number): Promise<boolean> => {
}
};
export const detectLocalServer = async (): Promise<string | null> => {
for (const port of LOCAL_PORTS) {
export const detectLocalServer = async (
preferredPort?: number,
): Promise<string | null> => {
const ports = preferredPort ? [preferredPort] : LOCAL_PORTS;
for (const port of ports) {
if (await checkServerHealth(port)) {
return `http://localhost:${port}`;
}

View File

@@ -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<LocalInstanceResult> => {
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 };
};