mirror of
https://github.com/twentyhq/twenty.git
synced 2026-04-20 23:14:20 -04:00
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}`,
|
||||
),
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user