mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Refactor dependency graph for SDK, client-sdk and create-app (#18963)
## Summary
### Externalize `twenty-client-sdk` from `twenty-sdk`
Previously, `twenty-client-sdk` was listed as a `devDependency` of
`twenty-sdk`, which caused Vite to bundle it inline into the dist
output. This meant end-user apps had two copies of `twenty-client-sdk`:
one hidden inside `twenty-sdk`'s bundle, and one installed explicitly in
their `node_modules`. These copies could drift apart since they weren't
guaranteed to be the same version.
**Change:** Moved `twenty-client-sdk` from `devDependencies` to
`dependencies` in `twenty-sdk/package.json`. Vite's `external` function
now recognizes it and keeps it as an external `require`/`import` in the
dist output. End users get a single deduplicated copy resolved by their
package manager.
### Externalize `twenty-sdk` from `create-twenty-app`
Similarly, `create-twenty-app` had `twenty-sdk` as a `devDependency`
(bundled inline). After refactoring `create-twenty-app` to
programmatically import operations from `twenty-sdk` (instead of
shelling out via `execSync`), it became a proper runtime dependency.
**Change:** Moved `twenty-sdk` from `devDependencies` to `dependencies`
in `create-twenty-app/package.json`.
### Switch E2E CI to `yarn npm publish`
The `workspace:*` protocol in `dependencies` is a Yarn-specific feature.
`npm publish` publishes it as-is (which breaks for consumers), while
`yarn npm publish` automatically replaces `workspace:*` with the
resolved version at publish time (e.g., `workspace:*` becomes `=1.2.3`).
**Change:** Replaced `npm publish` with `yarn npm publish` in
`.github/workflows/ci-create-app-e2e.yaml`.
### Replace `execSync` with programmatic SDK calls in
`create-twenty-app`
`create-twenty-app` was shelling out to `yarn twenty remote add` and
`yarn twenty server start` via `execSync`, which assumed the `twenty`
binary was already installed in the scaffolded app. This was fragile and
created an implicit circular dependency.
**Changes:**
- Replaced `execSync('yarn twenty remote add ...')` with a direct call
to `authLoginOAuth()` from `twenty-sdk/cli`
- Replaced `execSync('yarn twenty server start')` with a direct call to
`serverStart()` from `twenty-sdk/cli`
- Deleted the duplicated `setup-local-instance.ts` from
`create-twenty-app`
### Centralize `serverStart` as a dedicated operation
The Docker server start logic was previously inline in the `server
start` CLI command handler (`server.ts`), and `setup-local-instance.ts`
was shelling out to `yarn twenty server start` to invoke it -- meaning
`twenty-sdk` was calling itself via a child process.
**Changes:**
- Extracted the Docker container management logic into a new
`serverStart` operation (`cli/operations/server-start.ts`)
- Merged the detect-or-start flow from `setup-local-instance.ts` into
`serverStart` (detect across multiple ports, start Docker if needed,
poll for health)
- Deleted `setup-local-instance.ts` from `twenty-sdk`
- Added `onProgress` callback (consistent with other operations like
`appBuild`) instead of direct `console.log` calls
- Both the `server start` CLI command and `create-twenty-app` now call
`serverStart()` programmatically
related to https://github.com/twentyhq/twenty-infra/pull/525
This commit is contained in:
parent
b651a74b1f
commit
052aecccc7
26 changed files with 1276 additions and 1415 deletions
18
.github/workflows/ci-create-app-e2e.yaml
vendored
18
.github/workflows/ci-create-app-e2e.yaml
vendored
|
|
@ -53,6 +53,8 @@ jobs:
|
||||||
image: redis
|
image: redis
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
env:
|
||||||
|
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch custom Github Actions and base branch history
|
- name: Fetch custom Github Actions and base branch history
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -66,13 +68,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
CI_VERSION="0.0.0-ci.$(date +%s)"
|
CI_VERSION="0.0.0-ci.$(date +%s)"
|
||||||
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
||||||
npx nx run-many -t set-local-version -p twenty-sdk twenty-client-sdk create-twenty-app --releaseVersion=$CI_VERSION
|
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: |
|
run: |
|
||||||
npx nx build twenty-sdk
|
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||||
npx nx build twenty-client-sdk
|
npx nx build $pkg
|
||||||
npx nx build create-twenty-app
|
done
|
||||||
|
|
||||||
- name: Install and start Verdaccio
|
- name: Install and start Verdaccio
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -89,11 +91,13 @@ jobs:
|
||||||
|
|
||||||
- name: Publish packages to local registry
|
- name: Publish packages to local registry
|
||||||
run: |
|
run: |
|
||||||
npm set //localhost:4873/:_authToken "ci-auth-token"
|
yarn config set npmRegistryServer http://localhost:4873
|
||||||
|
yarn config set unsafeHttpWhitelist --json '["localhost"]'
|
||||||
|
yarn config set npmAuthToken ci-auth-token
|
||||||
|
|
||||||
for pkg in twenty-sdk twenty-client-sdk create-twenty-app; do
|
for pkg in $PUBLISHABLE_PACKAGES; do
|
||||||
cd packages/$pkg
|
cd packages/$pkg
|
||||||
npm publish --registry http://localhost:4873 --tag ci
|
yarn npm publish --tag ci
|
||||||
cd ../..
|
cd ../..
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
||||||
940
.yarn/releases/yarn-4.13.0.cjs
vendored
Executable file
940
.yarn/releases/yarn-4.13.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
942
.yarn/releases/yarn-4.9.2.cjs
vendored
942
.yarn/releases/yarn-4.9.2.cjs
vendored
File diff suppressed because one or more lines are too long
|
|
@ -6,4 +6,4 @@ enableInlineHunks: true
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"name": "twenty",
|
"name": "twenty",
|
||||||
"packageManager": "yarn@4.9.2",
|
"packageManager": "yarn@4.13.0",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"graphql": "16.8.1",
|
"graphql": "16.8.1",
|
||||||
"type-fest": "4.10.1",
|
"type-fest": "4.10.1",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ cd my-twenty-app
|
||||||
|
|
||||||
# Or do it manually:
|
# Or do it manually:
|
||||||
yarn twenty server start # Start local Twenty server
|
yarn twenty server start # Start local Twenty server
|
||||||
yarn twenty remote add --local # Authenticate via OAuth
|
yarn twenty remote add http://localhost:2020 --as local # Authenticate via OAuth
|
||||||
|
|
||||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built — both available via `twenty-client-sdk`)
|
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built — both available via `twenty-client-sdk`)
|
||||||
|
|
@ -122,18 +122,10 @@ 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`).
|
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
|
## Next steps
|
||||||
|
|
||||||
- Run `yarn twenty help` to see all available commands.
|
- Run `yarn twenty help` to see all available commands.
|
||||||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
|
- Use `yarn twenty remote add <url>` to authenticate with your Twenty workspace via OAuth.
|
||||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||||
- `CoreApiClient` is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient } from 'twenty-client-sdk/core'` and `import { MetadataApiClient } from 'twenty-client-sdk/metadata'`.
|
- `CoreApiClient` is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient } from 'twenty-client-sdk/core'` and `import { MetadataApiClient } from 'twenty-client-sdk/metadata'`.
|
||||||
|
|
@ -177,7 +169,7 @@ Our team reviews contributions for quality, security, and reusability before mer
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
||||||
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
|
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add <url>`.
|
||||||
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "create-twenty-app",
|
"name": "create-twenty-app",
|
||||||
"version": "0.8.0-canary.2",
|
"version": "0.8.0-canary.3",
|
||||||
"description": "Command-line interface to create Twenty application",
|
"description": "Command-line interface to create Twenty application",
|
||||||
"main": "dist/cli.cjs",
|
"main": "dist/cli.cjs",
|
||||||
"bin": "dist/cli.cjs",
|
"bin": "dist/cli.cjs",
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
"lodash.kebabcase": "^4.1.1",
|
"lodash.kebabcase": "^4.1.1",
|
||||||
"lodash.startcase": "^4.4.0",
|
"lodash.startcase": "^4.4.0",
|
||||||
|
"twenty-sdk": "workspace:*",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -45,7 +46,6 @@
|
||||||
"@types/lodash.kebabcase": "^4.1.7",
|
"@types/lodash.kebabcase": "^4.1.7",
|
||||||
"@types/lodash.startcase": "^4",
|
"@types/lodash.startcase": "^4",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"twenty-sdk": "workspace:*",
|
|
||||||
"twenty-shared": "workspace:*",
|
"twenty-shared": "workspace:*",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,6 @@ const program = new Command(packageJson.name)
|
||||||
'--skip-local-instance',
|
'--skip-local-instance',
|
||||||
'Skip the local Twenty instance setup prompt',
|
'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.')
|
.helpOption('-h, --help', 'Display this help message.')
|
||||||
.action(
|
.action(
|
||||||
async (
|
async (
|
||||||
|
|
@ -46,7 +42,6 @@ const program = new Command(packageJson.name)
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
skipLocalInstance?: boolean;
|
skipLocalInstance?: boolean;
|
||||||
port?: string;
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||||
|
|
@ -76,8 +71,6 @@ const program = new Command(packageJson.name)
|
||||||
|
|
||||||
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
||||||
|
|
||||||
const port = options?.port ? parseInt(options.port, 10) : undefined;
|
|
||||||
|
|
||||||
await new CreateAppCommand().execute({
|
await new CreateAppCommand().execute({
|
||||||
directory,
|
directory,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -85,7 +78,6 @@ const program = new Command(packageJson.name)
|
||||||
displayName: options?.displayName,
|
displayName: options?.displayName,
|
||||||
description: options?.description,
|
description: options?.description,
|
||||||
skipLocalInstance: options?.skipLocalInstance,
|
skipLocalInstance: options?.skipLocalInstance,
|
||||||
port,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { basename } from 'path';
|
|
||||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||||
import { convertToLabel } from '@/utils/convert-to-label';
|
import { convertToLabel } from '@/utils/convert-to-label';
|
||||||
import { install } from '@/utils/install';
|
import { install } from '@/utils/install';
|
||||||
import {
|
|
||||||
type LocalInstanceResult,
|
|
||||||
setupLocalInstance,
|
|
||||||
} from '@/utils/setup-local-instance';
|
|
||||||
import { tryGitInit } from '@/utils/try-git-init';
|
import { tryGitInit } from '@/utils/try-git-init';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import kebabCase from 'lodash.kebabcase';
|
import kebabCase from 'lodash.kebabcase';
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { basename } from 'path';
|
||||||
|
import {
|
||||||
|
authLoginOAuth,
|
||||||
|
serverStart,
|
||||||
|
type ServerStartResult,
|
||||||
|
} from 'twenty-sdk/cli';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,7 +29,6 @@ type CreateAppOptions = {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
skipLocalInstance?: boolean;
|
skipLocalInstance?: boolean;
|
||||||
port?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CreateAppCommand {
|
export class CreateAppCommand {
|
||||||
|
|
@ -60,17 +59,22 @@ export class CreateAppCommand {
|
||||||
|
|
||||||
await tryGitInit(appDirectory);
|
await tryGitInit(appDirectory);
|
||||||
|
|
||||||
let localResult: LocalInstanceResult = { running: false };
|
let serverResult: ServerStartResult | undefined;
|
||||||
|
|
||||||
if (!options.skipLocalInstance) {
|
if (!options.skipLocalInstance) {
|
||||||
localResult = await setupLocalInstance(appDirectory, options.port);
|
const startResult = await serverStart({
|
||||||
|
onProgress: (message: string) => console.log(chalk.gray(message)),
|
||||||
|
});
|
||||||
|
|
||||||
if (localResult.running && localResult.serverUrl) {
|
if (startResult.success) {
|
||||||
await this.connectToLocal(appDirectory, localResult.serverUrl);
|
serverResult = startResult.data;
|
||||||
|
await this.connectToLocal(serverResult.url);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.yellow(`\n${startResult.error.message}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logSuccess(appDirectory, localResult);
|
this.logSuccess(appDirectory, serverResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red('\nCreate application failed:'),
|
chalk.red('\nCreate application failed:'),
|
||||||
|
|
@ -197,15 +201,20 @@ export class CreateAppCommand {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connectToLocal(
|
private async connectToLocal(serverUrl: string): Promise<void> {
|
||||||
appDirectory: string,
|
|
||||||
serverUrl: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
execSync(`yarn twenty remote add ${serverUrl} --as local`, {
|
const result = await authLoginOAuth({
|
||||||
cwd: appDirectory,
|
apiUrl: serverUrl,
|
||||||
stdio: 'inherit',
|
remote: 'local',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
|
|
@ -217,14 +226,14 @@ export class CreateAppCommand {
|
||||||
|
|
||||||
private logSuccess(
|
private logSuccess(
|
||||||
appDirectory: string,
|
appDirectory: string,
|
||||||
localResult: LocalInstanceResult,
|
serverResult?: ServerStartResult,
|
||||||
): void {
|
): void {
|
||||||
const dirName = basename(appDirectory);
|
const dirName = basename(appDirectory);
|
||||||
|
|
||||||
console.log(chalk.blue('\nApplication created. Next steps:'));
|
console.log(chalk.blue('\nApplication created. Next steps:'));
|
||||||
console.log(chalk.gray(`- cd ${dirName}`));
|
console.log(chalk.gray(`- cd ${dirName}`));
|
||||||
|
|
||||||
if (!localResult.running) {
|
if (!serverResult) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.gray(
|
chalk.gray(
|
||||||
'- yarn twenty remote add --local # Authenticate with Twenty',
|
'- yarn twenty remote add --local # Authenticate with Twenty',
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
|
|
||||||
const LOCAL_PORTS = [2020, 3000];
|
|
||||||
|
|
||||||
// Minimal health check — the full implementation lives in twenty-sdk
|
|
||||||
const isServerReady = async (port: number): Promise<boolean> => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:${port}/healthz`, {
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await response.json();
|
|
||||||
|
|
||||||
return body.status === 'ok';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupLocalInstance = async (
|
|
||||||
appDirectory: string,
|
|
||||||
preferredPort?: number,
|
|
||||||
): Promise<LocalInstanceResult> => {
|
|
||||||
const detectedPort = await detectRunningServer(preferredPort);
|
|
||||||
|
|
||||||
if (detectedPort) {
|
|
||||||
const serverUrl = `http://localhost:${detectedPort}`;
|
|
||||||
|
|
||||||
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 isServerReady(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 };
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||||
First, authenticate to your workspace:
|
First, authenticate to your workspace:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn twenty remote add --local
|
yarn twenty remote add http://localhost:2020 --as local
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, start development mode to sync your app and watch for changes:
|
Then, start development mode to sync your app and watch for changes:
|
||||||
|
|
@ -22,7 +22,7 @@ Run `yarn twenty help` to list all available commands. Common commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remotes & Authentication
|
# Remotes & Authentication
|
||||||
yarn twenty remote add --local # Authenticate with Twenty
|
yarn twenty remote add http://localhost:2020 --as local # Authenticate with Twenty
|
||||||
yarn twenty remote status # Check auth status
|
yarn twenty remote status # Check auth status
|
||||||
yarn twenty remote switch # Switch default remote
|
yarn twenty remote switch # Switch default remote
|
||||||
yarn twenty remote list # List all configured remotes
|
yarn twenty remote list # List all configured remotes
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||||
First, authenticate to your workspace:
|
First, authenticate to your workspace:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn twenty remote add --local
|
yarn twenty remote add http://localhost:2020 --as local
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, start development mode to sync your app and watch for changes:
|
Then, start development mode to sync your app and watch for changes:
|
||||||
|
|
@ -22,7 +22,7 @@ Run `yarn twenty help` to list all available commands. Common commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remotes & Authentication
|
# Remotes & Authentication
|
||||||
yarn twenty remote add --local # Authenticate with Twenty
|
yarn twenty remote add http://localhost:2020 --as local # Authenticate with Twenty
|
||||||
yarn twenty remote status # Check auth status
|
yarn twenty remote status # Check auth status
|
||||||
yarn twenty remote switch # Switch default remote
|
yarn twenty remote switch # Switch default remote
|
||||||
yarn twenty remote list # List all configured remotes
|
yarn twenty remote list # List all configured remotes
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||||
First, authenticate to your workspace:
|
First, authenticate to your workspace:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn twenty remote add --local
|
yarn twenty remote add http://localhost:2020 --as local
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, start development mode to sync your app and watch for changes:
|
Then, start development mode to sync your app and watch for changes:
|
||||||
|
|
@ -22,7 +22,7 @@ Run `yarn twenty help` to list all available commands. Common commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remotes & Authentication
|
# Remotes & Authentication
|
||||||
yarn twenty remote add --local # Authenticate with Twenty
|
yarn twenty remote add http://localhost:2020 --as local # Authenticate with Twenty
|
||||||
yarn twenty remote status # Check auth status
|
yarn twenty remote status # Check auth status
|
||||||
yarn twenty remote switch # Switch default remote
|
yarn twenty remote switch # Switch default remote
|
||||||
yarn twenty remote list # List all configured remotes
|
yarn twenty remote list # List all configured remotes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "twenty-client-sdk",
|
"name": "twenty-client-sdk",
|
||||||
"version": "0.7.0-canary.0",
|
"version": "0.8.0-canary.3",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,6 @@ Manage remote server connections and authentication.
|
||||||
- `--token <token>`: API key for non-interactive auth.
|
- `--token <token>`: API key for non-interactive auth.
|
||||||
- `--url <url>`: Server URL (alternative to positional arg).
|
- `--url <url>`: Server URL (alternative to positional arg).
|
||||||
- `--as <name>`: Name for this remote (otherwise derived from URL hostname).
|
- `--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).
|
- 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.
|
- `twenty remote remove <name>` — Remove a remote and its credentials.
|
||||||
|
|
@ -147,9 +145,6 @@ twenty remote add
|
||||||
# Provide values in flags (non-interactive, for CI)
|
# Provide values in flags (non-interactive, for CI)
|
||||||
twenty remote add https://api.twenty.com --token $TWENTY_API_KEY
|
twenty remote add https://api.twenty.com --token $TWENTY_API_KEY
|
||||||
|
|
||||||
# Add a local development remote
|
|
||||||
twenty remote add --local
|
|
||||||
|
|
||||||
# Name a remote explicitly
|
# Name a remote explicitly
|
||||||
twenty remote add https://api.twenty.com --as production
|
twenty remote add https://api.twenty.com --as production
|
||||||
|
|
||||||
|
|
@ -339,14 +334,10 @@ Notes:
|
||||||
|
|
||||||
## How to use a local Twenty instance
|
## 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`):
|
If you're already running a local Twenty instance, you can connect to it instead of using Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# During scaffolding
|
twenty remote add http://localhost:3000 --as local
|
||||||
npx create-twenty-app@latest my-app --port 3000
|
|
||||||
|
|
||||||
# Or after scaffolding
|
|
||||||
twenty remote add --local --port 3000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "twenty-sdk",
|
"name": "twenty-sdk",
|
||||||
"version": "0.8.0-canary.2",
|
"version": "0.8.0-canary.3",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"types": "dist/sdk/index.d.ts",
|
"types": "dist/sdk/index.d.ts",
|
||||||
|
|
@ -75,6 +75,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
|
"twenty-client-sdk": "workspace:*",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
|
|
@ -96,7 +97,6 @@
|
||||||
"storybook": "^10.2.13",
|
"storybook": "^10.2.13",
|
||||||
"ts-morph": "^25.0.0",
|
"ts-morph": "^25.0.0",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"twenty-client-sdk": "workspace:*",
|
|
||||||
"twenty-shared": "workspace:*",
|
"twenty-shared": "workspace:*",
|
||||||
"twenty-ui": "workspace:*",
|
"twenty-ui": "workspace:*",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,6 @@ export const registerRemoteCommands = (program: Command): void => {
|
||||||
.command('add [nameOrUrl]')
|
.command('add [nameOrUrl]')
|
||||||
.description('Add a new remote or re-authenticate an existing one')
|
.description('Add a new remote or re-authenticate an existing one')
|
||||||
.option('--as <name>', 'Name for this remote')
|
.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('--token <token>', 'API key for non-interactive auth')
|
||||||
.option('--url <url>', 'Server URL (alternative to positional arg)')
|
.option('--url <url>', 'Server URL (alternative to positional arg)')
|
||||||
.action(
|
.action(
|
||||||
|
|
@ -78,8 +76,6 @@ export const registerRemoteCommands = (program: Command): void => {
|
||||||
nameOrUrl: string | undefined,
|
nameOrUrl: string | undefined,
|
||||||
options: {
|
options: {
|
||||||
as?: string;
|
as?: string;
|
||||||
local?: boolean;
|
|
||||||
port?: string;
|
|
||||||
token?: string;
|
token?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
},
|
},
|
||||||
|
|
@ -87,32 +83,6 @@ export const registerRemoteCommands = (program: Command): void => {
|
||||||
const configService = new ConfigService();
|
const configService = new ConfigService();
|
||||||
const existingRemotes = await configService.getRemotes();
|
const existingRemotes = await configService.getRemotes();
|
||||||
|
|
||||||
if (options.local) {
|
|
||||||
const remoteName = options.as ?? 'local';
|
|
||||||
const preferredPort = options.port
|
|
||||||
? parseInt(options.port, 10)
|
|
||||||
: undefined;
|
|
||||||
const localUrl = preferredPort
|
|
||||||
? `http://localhost:${preferredPort}`
|
|
||||||
: await detectLocalServer();
|
|
||||||
|
|
||||||
if (!localUrl) {
|
|
||||||
console.error(
|
|
||||||
chalk.red(
|
|
||||||
'No local Twenty server found on ports 2020 or 3000.\n' +
|
|
||||||
'Start one with: yarn twenty server start',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.gray(`Found server at ${localUrl}`));
|
|
||||||
ConfigService.setActiveRemote(remoteName);
|
|
||||||
await authenticate(localUrl, options.token);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-authenticate an existing remote by name
|
// Re-authenticate an existing remote by name
|
||||||
const isExistingRemote =
|
const isExistingRemote =
|
||||||
nameOrUrl !== undefined && existingRemotes.includes(nameOrUrl);
|
nameOrUrl !== undefined && existingRemotes.includes(nameOrUrl);
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,16 @@
|
||||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
import { serverStart } from '@/cli/operations/server-start';
|
||||||
|
import {
|
||||||
|
CONTAINER_NAME,
|
||||||
|
containerExists,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
getContainerPort,
|
||||||
|
isContainerRunning,
|
||||||
|
} from '@/cli/utilities/server/docker-container';
|
||||||
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
|
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
import { execSync, spawnSync } from 'node:child_process';
|
import { execSync, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
const CONTAINER_NAME = 'twenty-app-dev';
|
|
||||||
const IMAGE = 'twentycrm/twenty-app-dev:latest';
|
|
||||||
const DEFAULT_PORT = 2020;
|
|
||||||
|
|
||||||
const isContainerRunning = (): boolean => {
|
|
||||||
try {
|
|
||||||
const result = execSync(
|
|
||||||
`docker inspect -f '{{.State.Running}}' ${CONTAINER_NAME}`,
|
|
||||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
return result === 'true';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContainerPort = (): number => {
|
|
||||||
try {
|
|
||||||
const result = execSync(
|
|
||||||
`docker inspect -f '{{(index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort}}' ${CONTAINER_NAME}`,
|
|
||||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
return parseInt(result, 10) || DEFAULT_PORT;
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_PORT;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerExists = (): boolean => {
|
|
||||||
try {
|
|
||||||
execSync(`docker inspect ${CONTAINER_NAME}`, {
|
|
||||||
stdio: ['pipe', 'pipe', 'ignore'],
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (isNaN(port) || port < 1 || port > 65535) {
|
|
||||||
console.error(chalk.red('Invalid port number.'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return port;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerServerCommands = (program: Command): void => {
|
export const registerServerCommands = (program: Command): void => {
|
||||||
const server = program
|
const server = program
|
||||||
.command('server')
|
.command('server')
|
||||||
|
|
@ -81,81 +21,26 @@ export const registerServerCommands = (program: Command): void => {
|
||||||
.description('Start a local Twenty server')
|
.description('Start a local Twenty server')
|
||||||
.option('-p, --port <port>', 'HTTP port', String(DEFAULT_PORT))
|
.option('-p, --port <port>', 'HTTP port', String(DEFAULT_PORT))
|
||||||
.action(async (options: { port: string }) => {
|
.action(async (options: { port: string }) => {
|
||||||
let port = validatePort(options.port);
|
const port = parseInt(options.port, 10);
|
||||||
|
|
||||||
if (await checkServerHealth(port)) {
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
const localUrl = `http://localhost:${port}`;
|
console.error(chalk.red('Invalid port number.'));
|
||||||
const configService = new ConfigService();
|
|
||||||
|
|
||||||
ConfigService.setActiveRemote('local');
|
|
||||||
await configService.setConfig({ apiUrl: localUrl });
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.green(`Twenty server is already running on localhost:${port}.`),
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkDockerRunning()) {
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContainerRunning()) {
|
const result = await serverStart({
|
||||||
console.log(chalk.gray('Container is running but not healthy yet.'));
|
port,
|
||||||
|
onProgress: (message) => console.log(chalk.gray(message)),
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
if (!result.success) {
|
||||||
|
console.error(chalk.red(result.error.message));
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containerExists()) {
|
console.log(
|
||||||
const existingPort = getContainerPort();
|
chalk.green(`\nLocal remote configured → ${result.data.url}`),
|
||||||
|
);
|
||||||
if (existingPort !== port) {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset' first to change ports.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
port = existingPort;
|
|
||||||
|
|
||||||
console.log(chalk.gray('Starting existing container...'));
|
|
||||||
execSync(`docker start ${CONTAINER_NAME}`, { stdio: 'ignore' });
|
|
||||||
} else {
|
|
||||||
console.log(chalk.gray('Starting Twenty container...'));
|
|
||||||
|
|
||||||
const runResult = spawnSync(
|
|
||||||
'docker',
|
|
||||||
[
|
|
||||||
'run',
|
|
||||||
'-d',
|
|
||||||
'--name',
|
|
||||||
CONTAINER_NAME,
|
|
||||||
'-p',
|
|
||||||
`${port}:3000`,
|
|
||||||
'-v',
|
|
||||||
'twenty-app-dev-data:/data/postgres',
|
|
||||||
'-v',
|
|
||||||
'twenty-app-dev-storage:/app/.local-storage',
|
|
||||||
IMAGE,
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (runResult.status !== 0) {
|
|
||||||
console.error(chalk.red('\nFailed to start Twenty container.'));
|
|
||||||
process.exit(runResult.status ?? 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
server
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,16 @@ export type { AppUninstallOptions } from './uninstall';
|
||||||
export { functionExecute } from './execute';
|
export { functionExecute } from './execute';
|
||||||
export type { FunctionExecuteOptions } from './execute';
|
export type { FunctionExecuteOptions } from './execute';
|
||||||
|
|
||||||
|
// Server
|
||||||
|
export { serverStart } from './server-start';
|
||||||
|
export type { ServerStartOptions, ServerStartResult } from './server-start';
|
||||||
|
|
||||||
// Shared types and error codes
|
// Shared types and error codes
|
||||||
export {
|
export {
|
||||||
APP_ERROR_CODES,
|
APP_ERROR_CODES,
|
||||||
AUTH_ERROR_CODES,
|
AUTH_ERROR_CODES,
|
||||||
FUNCTION_ERROR_CODES,
|
FUNCTION_ERROR_CODES,
|
||||||
|
SERVER_ERROR_CODES,
|
||||||
} from '@/cli/types';
|
} from '@/cli/types';
|
||||||
export type {
|
export type {
|
||||||
AuthListRemote,
|
AuthListRemote,
|
||||||
|
|
|
||||||
191
packages/twenty-sdk/src/cli/operations/server-start.ts
Normal file
191
packages/twenty-sdk/src/cli/operations/server-start.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { SERVER_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||||
|
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||||
|
import { runSafe } from '@/cli/utilities/run-safe';
|
||||||
|
import {
|
||||||
|
checkDockerRunning,
|
||||||
|
CONTAINER_NAME,
|
||||||
|
containerExists,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
getContainerPort,
|
||||||
|
IMAGE,
|
||||||
|
isContainerRunning,
|
||||||
|
} from '@/cli/utilities/server/docker-container';
|
||||||
|
import {
|
||||||
|
checkServerHealth,
|
||||||
|
detectLocalServer,
|
||||||
|
} from '@/cli/utilities/server/detect-local-server';
|
||||||
|
import { execSync, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const HEALTH_POLL_INTERVAL_MS = 2000;
|
||||||
|
const HEALTH_TIMEOUT_MS = 180 * 1000;
|
||||||
|
|
||||||
|
const waitForHealthy = async (port: number): Promise<boolean> => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {
|
||||||
|
if (await checkServerHealth(port)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, HEALTH_POLL_INTERVAL_MS),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerStartOptions = {
|
||||||
|
port?: number;
|
||||||
|
onProgress?: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerStartResult = {
|
||||||
|
port: number;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const innerServerStart = async (
|
||||||
|
options: ServerStartOptions = {},
|
||||||
|
): Promise<CommandResult<ServerStartResult>> => {
|
||||||
|
const { onProgress } = options;
|
||||||
|
|
||||||
|
const existingUrl = await detectLocalServer(options.port);
|
||||||
|
|
||||||
|
if (existingUrl) {
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
ConfigService.setActiveRemote('local');
|
||||||
|
await configService.setConfig({ apiUrl: existingUrl });
|
||||||
|
|
||||||
|
const port = new URL(existingUrl).port;
|
||||||
|
|
||||||
|
onProgress?.(`Twenty server detected on ${existingUrl}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { port: parseInt(port, 10), url: existingUrl },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkDockerRunning()) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: SERVER_ERROR_CODES.DOCKER_NOT_RUNNING,
|
||||||
|
message: 'Docker is not running. Please start Docker and try again.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContainerRunning()) {
|
||||||
|
const port = getContainerPort();
|
||||||
|
|
||||||
|
onProgress?.('Container is running, waiting for it to become healthy...');
|
||||||
|
|
||||||
|
const healthy = await waitForHealthy(port);
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: SERVER_ERROR_CODES.HEALTH_TIMEOUT,
|
||||||
|
message:
|
||||||
|
'Twenty server did not become healthy in time.\n' +
|
||||||
|
"Check: 'yarn twenty server logs'",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
ConfigService.setActiveRemote('local');
|
||||||
|
await configService.setConfig({ apiUrl: url });
|
||||||
|
|
||||||
|
onProgress?.(`Server running on ${url}`);
|
||||||
|
|
||||||
|
return { success: true, data: { port, url } };
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = options.port ?? DEFAULT_PORT;
|
||||||
|
|
||||||
|
if (containerExists()) {
|
||||||
|
const existingPort = getContainerPort();
|
||||||
|
|
||||||
|
if (existingPort !== port) {
|
||||||
|
onProgress?.(
|
||||||
|
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset' first to change ports.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
port = existingPort;
|
||||||
|
|
||||||
|
onProgress?.('Starting existing container...');
|
||||||
|
execSync(`docker start ${CONTAINER_NAME}`, { stdio: 'ignore' });
|
||||||
|
} else {
|
||||||
|
onProgress?.('Starting Twenty container...');
|
||||||
|
|
||||||
|
const runResult = spawnSync(
|
||||||
|
'docker',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
'-d',
|
||||||
|
'--name',
|
||||||
|
CONTAINER_NAME,
|
||||||
|
'-p',
|
||||||
|
`${port}:3000`,
|
||||||
|
'-v',
|
||||||
|
'twenty-app-dev-data:/data/postgres',
|
||||||
|
'-v',
|
||||||
|
'twenty-app-dev-storage:/app/.local-storage',
|
||||||
|
IMAGE,
|
||||||
|
],
|
||||||
|
{ stdio: 'inherit' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runResult.status !== 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: SERVER_ERROR_CODES.CONTAINER_START_FAILED,
|
||||||
|
message: 'Failed to start Twenty container.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress?.('Waiting for Twenty to be ready...');
|
||||||
|
|
||||||
|
const healthy = await waitForHealthy(port);
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: SERVER_ERROR_CODES.HEALTH_TIMEOUT,
|
||||||
|
message:
|
||||||
|
'Twenty server did not become healthy in time.\n' +
|
||||||
|
"Check: 'yarn twenty server logs'",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
ConfigService.setActiveRemote('local');
|
||||||
|
await configService.setConfig({ apiUrl: url });
|
||||||
|
|
||||||
|
onProgress?.(`Server running on ${url}`);
|
||||||
|
|
||||||
|
return { success: true, data: { port, url } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serverStart = (
|
||||||
|
options?: ServerStartOptions,
|
||||||
|
): Promise<CommandResult<ServerStartResult>> =>
|
||||||
|
runSafe(
|
||||||
|
() => innerServerStart(options),
|
||||||
|
SERVER_ERROR_CODES.CONTAINER_START_FAILED,
|
||||||
|
);
|
||||||
|
|
@ -27,6 +27,12 @@ export const APP_ERROR_CODES = {
|
||||||
DEPLOY_FAILED: 'DEPLOY_FAILED',
|
DEPLOY_FAILED: 'DEPLOY_FAILED',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SERVER_ERROR_CODES = {
|
||||||
|
DOCKER_NOT_RUNNING: 'DOCKER_NOT_RUNNING',
|
||||||
|
CONTAINER_START_FAILED: 'CONTAINER_START_FAILED',
|
||||||
|
HEALTH_TIMEOUT: 'HEALTH_TIMEOUT',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const FUNCTION_ERROR_CODES = {
|
export const FUNCTION_ERROR_CODES = {
|
||||||
FETCH_FUNCTIONS_FAILED: 'FETCH_FUNCTIONS_FAILED',
|
FETCH_FUNCTIONS_FAILED: 'FETCH_FUNCTIONS_FAILED',
|
||||||
FUNCTION_NOT_FOUND: 'FUNCTION_NOT_FOUND',
|
FUNCTION_NOT_FOUND: 'FUNCTION_NOT_FOUND',
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class CheckServerOrchestratorStep {
|
||||||
this.state.applyStepEvents([
|
this.state.applyStepEvents([
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
'Authentication failed. Run `twenty remote add --local` to authenticate.',
|
'Authentication failed. Run `twenty remote add <url>` to authenticate.',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
|
||||||
|
export const CONTAINER_NAME = 'twenty-app-dev';
|
||||||
|
export const IMAGE = 'twentycrm/twenty-app-dev:latest';
|
||||||
|
export const DEFAULT_PORT = 2020;
|
||||||
|
|
||||||
|
export const isContainerRunning = (): boolean => {
|
||||||
|
try {
|
||||||
|
const result = execSync(
|
||||||
|
`docker inspect -f '{{.State.Running}}' ${CONTAINER_NAME}`,
|
||||||
|
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
return result === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getContainerPort = (): number => {
|
||||||
|
try {
|
||||||
|
const result = execSync(
|
||||||
|
`docker inspect -f '{{(index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort}}' ${CONTAINER_NAME}`,
|
||||||
|
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
return parseInt(result, 10) || DEFAULT_PORT;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const containerExists = (): boolean => {
|
||||||
|
try {
|
||||||
|
execSync(`docker inspect ${CONTAINER_NAME}`, {
|
||||||
|
stdio: ['pipe', 'pipe', 'ignore'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkDockerRunning = (): boolean => {
|
||||||
|
try {
|
||||||
|
execSync('docker info', { stdio: 'ignore' });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
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 };
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { readFileSync, existsSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
const verifyBuildPackagesBundleTwentyDependencies = () => {
|
|
||||||
const distPath = resolve('dist/cli.cjs');
|
|
||||||
|
|
||||||
if (!existsSync(distPath)) {
|
|
||||||
console.error(`Build check failed: ${distPath} does not exist`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = readFileSync(distPath, 'utf8');
|
|
||||||
|
|
||||||
const filePath = resolve('package.json');
|
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
||||||
|
|
||||||
if (/require\("twenty/.test(content)) {
|
|
||||||
console.error(
|
|
||||||
`Build check failed: ${pkg.name}/dist/cli.cjs contains a require("twenty...) import. Workspace packages should be bundled, not externalized.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log(`${pkg.name}/dist/cli.cjs: OK`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
verifyBuildPackagesBundleTwentyDependencies();
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
const packages = ['twenty-sdk', 'create-twenty-app', 'twenty-client-sdk'];
|
|
||||||
|
|
||||||
const verifyUniquePackageVersion = () => {
|
|
||||||
const packageVersions = packages.map((pkg) => {
|
|
||||||
const packageJsonPath = resolve('packages', pkg, 'package.json');
|
|
||||||
|
|
||||||
const packageJsonFile = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
||||||
|
|
||||||
return packageJsonFile.version as string;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (new Set(packageVersions).size !== 1) {
|
|
||||||
console.error(
|
|
||||||
`Build check failed: "${packages.join('", "')}" should have the same package.json version. Got ${packageVersions.join(', ')}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`"${packages.join('", "')}" share the same version ${packageVersions[0]}: OK`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
verifyUniquePackageVersion();
|
|
||||||
Loading…
Reference in a new issue