Update twenty sdk commands (#20735)

Performs twenty-sdk cli command migration:

Summary

``` ┌─────┬──────────────────────────┬────────────────────────────┬───────────────────────┐
 │  #  │       Old command        │        New command         │        Status         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 1   │ twenty dev [appPath]     │ twenty dev [appPath]       │ Unchanged (now also   │
 │     │                          │                            │ DEFAULT)              │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 2   │ twenty dev --once        │ twenty dev --once          │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 3   │ twenty dev --watch       │ twenty dev [appPath]       │ --watch flag removed  │
 │     │ [appPath]                │                            │ (was default)         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 4   │ twenty dev --verbose     │ twenty dev --verbose       │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 5   │ twenty dev --debug       │ twenty dev --debug         │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 6   │ twenty dev --debounceMs  │ twenty dev --debounceMs    │ Unchanged             │
 │     │ <ms> [appPath]           │ <ms> [appPath]             │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 7   │ twenty build [appPath]   │ twenty dev:build [appPath] │ Deprecated → colon    │
 │     │                          │                            │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 8   │ twenty build --tarball   │ twenty dev:build --tarball │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 9   │ twenty typecheck         │ twenty dev:typecheck       │ Deprecated → colon    │
 │     │ [appPath]                │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 10  │ twenty logs [appPath]    │ twenty dev:fn-logs         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 11  │ twenty logs -n <name>    │ twenty dev:fn-logs -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 12  │ twenty logs -u <id>      │ twenty dev:fn-logs -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 13  │ twenty exec [appPath]    │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 14  │ twenty exec -n <name>    │ twenty dev:fn-exec -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 15  │ twenty exec -u <id>      │ twenty dev:fn-exec -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 16  │ twenty exec -p <json>    │ twenty dev:fn-exec -p      │ Deprecated → colon    │
 │     │ [appPath]                │ <json> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 17  │ twenty exec              │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │ --postInstall [appPath]  │ --postInstall [appPath]    │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 18  │ twenty exec --preInstall │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │  [appPath]               │ --preInstall [appPath]     │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 19  │ twenty add [entityType]  │ twenty dev:add             │ Deprecated → colon    │
 │     │                          │ [entityType]               │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 20  │ twenty add --path <path> │ twenty dev:add --path      │ Deprecated → colon    │
 │     │  [entityType]            │ <path> [entityType]        │ command               │
 └─────┴──────────────────────────┴────────────────────────────┴───────────────────────┘

 App lifecycle commands

 ┌─────┬────────────────────────┬────────────────────────────┬─────────────────────────┐
 │  #  │      Old command       │        New command         │         Status          │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 21  │ twenty publish         │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 22  │ twenty publish --tag   │ twenty app:publish --tag   │ Deprecated → colon      │
 │     │ <tag> [appPath]        │ <tag> [appPath]            │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 23  │ twenty deploy          │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ --private [appPath]        │ command + --private     │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 24  │ twenty install         │ twenty app:install         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 25  │ twenty uninstall       │ twenty app:uninstall       │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 26  │ twenty uninstall -y    │ twenty app:uninstall -y    │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 └─────┴────────────────────────┴────────────────────────────┴─────────────────────────┘

 Server commands

 ┌─────┬─────────────────────────┬─────────────────────────────┬──────────────────────┐
 │  #  │       Old command       │         New command         │        Status        │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 27  │ twenty server start     │ twenty docker:start         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 28  │ twenty server start -p  │ twenty docker:start -p      │ Deprecated → colon   │
 │     │ <port>                  │ <port>                      │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 29  │ twenty server start     │ twenty docker:start --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 30  │ twenty server stop      │ twenty docker:stop          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 31  │ twenty server stop      │ twenty docker:stop --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 32  │ twenty server status    │ twenty docker:status        │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 33  │ twenty server status    │ twenty docker:status --test │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 34  │ twenty server logs      │ twenty docker:logs          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 35  │ twenty server logs -n   │ twenty docker:logs -n       │ Deprecated → colon   │
 │     │ <lines>                 │ <lines>                     │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 36  │ twenty server logs      │ twenty docker:logs --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 37  │ twenty server reset     │ twenty docker:reset         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 38  │ twenty server reset     │ twenty docker:reset --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 39  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ [version]               │ [version]                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 40  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ --test [version]        │ --test [version]            │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 41  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync            │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 42  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync -r <name>  │ -r <name>                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 43  │ twenty catalog-sync     │ (removed)                   │ Removed (was already │
 │     │                         │                             │  deprecated)         │
 └─────┴─────────────────────────┴─────────────────────────────┴──────────────────────┘

 Remote commands

 ┌─────┬────────────────────────┬──────────────────────────┬──────────────────────────┐
 │  #  │      Old command       │       New command        │          Status          │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 44  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 45  │ twenty remote add --as │ twenty remote:add --as   │ Deprecated → colon       │
 │     │  <name>                │ <name>                   │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 46  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-key <key>        │ --api-key <key>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 47  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-url <url>        │ --api-url <url>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 48  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --local                │ --local                  │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 49  │ twenty remote add      │ twenty remote:add --test │ Deprecated → colon       │
 │     │ --test                 │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 50  │ twenty remote list     │ twenty remote:list       │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 51  │ twenty remote switch   │ twenty remote:use [name] │ Deprecated → colon       │
 │     │ [name]                 │                          │ syntax + renamed         │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 52  │ twenty remote status   │ twenty remote:status     │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 53  │ twenty remote remove   │ twenty remote:remove     │ Deprecated → colon       │
 │     │ <name>                 │ <name>                   │ syntax                   │
 └─────┴────────────────────────┴──────────────────────────┴──────────────────────────┘
```

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-05-20 17:12:39 +02:00
committed by GitHub
parent 6e5e7963b5
commit 237a943947
73 changed files with 1364 additions and 1002 deletions

View File

@@ -50,4 +50,4 @@ runs:
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty deploy --remote target
run: yarn twenty app:publish --private --remote target

View File

@@ -50,4 +50,4 @@ runs:
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty install --remote target
run: yarn twenty app:install --remote target

View File

@@ -105,7 +105,7 @@ jobs:
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --workspace-url http://localhost:3000
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
- name: Install scaffolded app dependencies
run: |
@@ -153,7 +153,7 @@ jobs:
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |

View File

@@ -63,7 +63,7 @@ export default defineObject({
Then ship it to your workspace:
```bash
npx twenty deploy
npx twenty app:publish --private
```
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.

View File

@@ -35,7 +35,7 @@ The scaffolder will:
| `--name <name>` | Set the app name |
| `--display-name <displayName>` | Set the display name |
| `--description <description>` | Set the description |
| `--workspace-url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
## Documentation
@@ -48,8 +48,8 @@ Full documentation is available at **[docs.twenty.com/developers/extend/apps](ht
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: run `yarn twenty remote add --local` to re-authenticate.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty docker:logs`.
- Auth not working: run `yarn twenty remote:add --local` to re-authenticate.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing

View File

@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.6.0",
"version": "2.7.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",

View File

@@ -18,11 +18,8 @@ const program = new Command(packageJson.name)
.option('-n, --name <name>', 'Application name')
.option('-d, --display-name <displayName>', 'Application display name')
.option('--description <description>', 'Application description')
.option(
'--workspace-url <workspaceUrl>',
'Twenty workspace URL (default: http://localhost:2020)',
)
.option('--api-url <apiUrl>', '[deprecated: use --workspace-url]')
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option(
'--authentication-method <method>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
@@ -35,7 +32,7 @@ const program = new Command(packageJson.name)
name?: string;
displayName?: string;
description?: string;
workspaceUrl?: string;
url?: string;
apiUrl?: string;
authenticationMethod?: AuthenticationMethod;
},
@@ -68,23 +65,18 @@ const program = new Command(packageJson.name)
if (options?.apiUrl) {
console.warn(
chalk.yellow(
'Warning: --api-url is deprecated. Use --workspace-url instead.',
),
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
);
}
const workspaceUrl = (options?.workspaceUrl ?? options?.apiUrl)?.replace(
/\/+$/,
'',
);
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
await new CreateAppCommand().execute({
directory,
name: options?.name,
displayName: options?.displayName,
description: options?.description,
workspaceUrl,
serverUrl,
authenticationMethod: options?.authenticationMethod,
});
},

View File

@@ -49,19 +49,19 @@
## Best practice
It's highly recommended to create new app entities using `yarn twenty add`. These are the options:
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
| Entity type | Command | Generated file |
| -------------------- | ------------------------------------ | ------------------------------------- |
| Object | `yarn twenty add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
This helps automatically generate required IDs etc.

View File

@@ -11,8 +11,8 @@ Run `yarn twenty help` to list all available commands.
## Useful Commands
- `yarn twenty dev` - Start the development server and sync your app
- `yarn twenty server status` - Check the local Twenty server status
- `yarn twenty server start` - Start the local Twenty server
- `yarn twenty docker:status` - Check the local Twenty server status
- `yarn twenty docker:start` - Start the local Twenty server
- `yarn test` - Run integration tests
## Learn More

View File

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}

View File

@@ -33,7 +33,7 @@ type CreateAppOptions = {
name?: string;
displayName?: string;
description?: string;
workspaceUrl?: string;
serverUrl?: string;
authenticationMethod?: AuthenticationMethod;
};
@@ -45,9 +45,9 @@ export class CreateAppCommand {
const { appName, appDisplayName, appDirectory, appDescription } =
this.getAppInfos(options);
const workspaceUrl = options.workspaceUrl ?? DEV_API_URL;
const serverUrl = options.serverUrl ?? DEV_API_URL;
const skipLocalInstance = workspaceUrl !== DEV_API_URL;
const skipLocalInstance = serverUrl !== DEV_API_URL;
if (!skipLocalInstance && !isDockerInstalled()) {
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
@@ -118,7 +118,7 @@ export class CreateAppCommand {
console.log('');
let authSucceeded = false;
let resolvedWorkspaceUrl = workspaceUrl;
let resolvedServerUrl = serverUrl;
let serverReady = skipLocalInstance;
if (!skipLocalInstance) {
@@ -126,7 +126,7 @@ export class CreateAppCommand {
const serverResult = await this.ensureDockerServer(dockerPullPromise);
if (isDefined(serverResult.url)) {
resolvedWorkspaceUrl = serverResult.url;
resolvedServerUrl = serverResult.url;
serverReady = true;
}
}
@@ -134,18 +134,16 @@ export class CreateAppCommand {
if (serverReady) {
this.logNextStep('Authenticating');
authSucceeded = await this.tryExistingAuth(resolvedWorkspaceUrl);
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
if (authSucceeded) {
this.logDetail('Reusing existing credentials');
} else if (authenticationMethod === 'oauth') {
this.logDetail('Starting OAuth flow');
authSucceeded =
await this.authenticateWithOAuth(resolvedWorkspaceUrl);
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
} else {
this.logDetail('Using development API key');
authSucceeded =
await this.authenticateWithDevKey(resolvedWorkspaceUrl);
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
}
}
@@ -165,10 +163,10 @@ export class CreateAppCommand {
}
if (syncSucceeded) {
await this.openMainPage(appDirectory, resolvedWorkspaceUrl);
await this.openMainPage(appDirectory, resolvedServerUrl);
}
this.logSuccess(appDirectory, resolvedWorkspaceUrl, authSucceeded);
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
} catch (error) {
console.error(
chalk.red('\nCreate application failed:'),
@@ -315,7 +313,7 @@ export class CreateAppCommand {
private async openMainPage(
appDirectory: string,
workspaceUrl: string,
serverUrl: string,
): Promise<void> {
try {
const configService = new ConfigService();
@@ -328,7 +326,7 @@ export class CreateAppCommand {
const [universalIdentifier, frontUrl] = await Promise.all([
this.readMainPageLayoutUniversalIdentifier(appDirectory),
this.resolveWorkspaceFrontUrl(workspaceUrl, token),
this.resolveWorkspaceFrontUrl(serverUrl, token),
]);
if (!universalIdentifier || !frontUrl) {
@@ -336,7 +334,7 @@ export class CreateAppCommand {
}
const pageLayoutId = await this.resolvePageLayoutId(
workspaceUrl,
serverUrl,
universalIdentifier,
token,
);
@@ -355,12 +353,12 @@ export class CreateAppCommand {
}
private async resolveWorkspaceFrontUrl(
workspaceUrl: string,
serverUrl: string,
token: string,
): Promise<string | null> {
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
const response = await fetch(`${workspaceUrl}/metadata`, {
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -410,13 +408,13 @@ export class CreateAppCommand {
}
private async resolvePageLayoutId(
workspaceUrl: string,
serverUrl: string,
universalIdentifier: string,
token: string,
): Promise<string | null> {
const query = `{ getPageLayouts { id universalIdentifier } }`;
const response = await fetch(`${workspaceUrl}/metadata`, {
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -503,7 +501,7 @@ export class CreateAppCommand {
});
}
private async tryExistingAuth(workspaceUrl: string): Promise<boolean> {
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
try {
const configService = new ConfigService();
const remoteNames = await configService.getRemotes();
@@ -511,7 +509,7 @@ export class CreateAppCommand {
for (const remoteName of remoteNames) {
const remoteConfig = await configService.getConfigForRemote(remoteName);
if (remoteConfig.apiUrl !== workspaceUrl) {
if (remoteConfig.apiUrl !== serverUrl) {
continue;
}
@@ -521,7 +519,7 @@ export class CreateAppCommand {
continue;
}
const response = await fetch(`${workspaceUrl}/metadata`, {
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -555,11 +553,11 @@ export class CreateAppCommand {
}
}
private async authenticateWithDevKey(workspaceUrl: string): Promise<boolean> {
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
try {
const result = await authLogin({
apiKey: DEV_API_KEY,
apiUrl: workspaceUrl,
apiUrl: serverUrl,
remote: 'local',
});
@@ -574,7 +572,7 @@ export class CreateAppCommand {
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote add --local` manually.',
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
),
);
@@ -582,7 +580,7 @@ export class CreateAppCommand {
} catch {
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote add --local` manually.',
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
),
);
@@ -598,21 +596,21 @@ export class CreateAppCommand {
}
}
private async authenticateWithOAuth(workspaceUrl: string): Promise<boolean> {
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
try {
const remoteName = this.deriveRemoteName(workspaceUrl);
const remoteName = this.deriveRemoteName(serverUrl);
ConfigService.setActiveRemote(remoteName);
this.logDetail('Opening browser for OAuth...');
const result = await authLoginOAuth({ apiUrl: workspaceUrl });
const result = await authLoginOAuth({ apiUrl: serverUrl });
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote(remoteName);
this.logDetail(`Authenticated via OAuth to ${workspaceUrl}`);
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
return true;
}
@@ -620,7 +618,7 @@ export class CreateAppCommand {
console.log(
chalk.yellow(
` OAuth failed: ${result.error.message}\n` +
` Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
@@ -628,7 +626,7 @@ export class CreateAppCommand {
} catch {
console.log(
chalk.yellow(
` Authentication failed. Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
@@ -638,7 +636,7 @@ export class CreateAppCommand {
private logSuccess(
appDirectory: string,
workspaceUrl: string,
serverUrl: string,
authSucceeded: boolean,
): void {
const dirName = basename(appDirectory);
@@ -656,9 +654,7 @@ export class CreateAppCommand {
if (!authSucceeded) {
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
console.log(
chalk.cyan(
' yarn twenty remote add --api-url <your-instance-url>\n',
),
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
);
stepNumber++;
}
@@ -668,7 +664,7 @@ export class CreateAppCommand {
stepNumber++;
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
console.log(chalk.cyan(` ${workspaceUrl}\n`));
console.log(chalk.cyan(` ${serverUrl}\n`));
console.log(
chalk.gray(

View File

@@ -28,6 +28,6 @@ export const getDockerInstallInstructions = (): string => {
' Then run this command again.',
'',
' Alternatively, connect to an existing Twenty instance:',
' npx create-twenty-app@latest my-twenty-app --workspace-url <your-instance-url>',
' npx create-twenty-app@latest my-twenty-app --url <your-instance-url>',
].join('\n');
};

View File

@@ -93,7 +93,7 @@ yarn install
# Register your local Twenty server as a remote (interactive prompt).
# When asked for the URL use http://localhost:2021 and paste an API key
# from Settings -> Developers in the Twenty UI.
yarn twenty remote add
yarn twenty remote:add
# Build, install, and watch for changes.
yarn twenty dev
@@ -107,8 +107,8 @@ watching `src/`. Edit any file and the change is re-synced within seconds.
```bash
cd packages/twenty-apps/community/github-connector
yarn install
yarn twenty remote add # same prompts as above
yarn twenty install # builds and installs once
yarn twenty remote:add # same prompts as above
yarn twenty app:install # builds and installs once
```
## Configure authentication

View File

@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote add --api-url http://localhost:2020 --as local
yarn twenty remote:add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Integration Tests

View File

@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}

View File

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}

View File

@@ -10,8 +10,8 @@
"packageManager": "yarn@4.9.2",
"scripts": {
"dev": "twenty dev",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"exec": "twenty dev:fn-exec",
"uninstall": "twenty app:uninstall",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},

View File

@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote add --api-url http://localhost:2020 --as local
yarn twenty remote:add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## LLMs instructions

View File

@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}

View File

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}

View File

@@ -64,15 +64,15 @@ cd packages/twenty-apps/internal/twenty-linear
# For day-to-day development (publish + install + watch in one):
yarn twenty dev
# Manual publish flow (deploy registers the app, install activates it):
yarn twenty deploy
yarn twenty install
# Manual publish flow (publish registers the app, install activates it):
yarn twenty app:publish --private
yarn twenty app:install
```
`twenty dev` is recommended for iteration — it publishes, installs, and
watches for changes in one command. Use `twenty deploy` + `twenty install`
when you want to control each step separately (e.g. deploying to a
production server without auto-installing).
watches for changes in one command. Use `twenty app:publish --private` +
`twenty app:install` when you want to control each step separately (e.g.
deploying to a production server without auto-installing).
This serves as the reference implementation for Twenty's
`defineConnectionProvider({ type: 'oauth' })` flow — useful as a template

View File

@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "2.6.0",
"version": "2.7.0",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {

View File

@@ -1,2 +1,2 @@
// Stub — overwritten by `twenty build` or `twenty dev`
// Stub — overwritten by `twenty dev:build` or `twenty dev`
export type CoreSchema = {};

View File

@@ -45,7 +45,7 @@ export default definePostInstallLogicFunction({
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --postInstall
yarn twenty dev:function:exec --postInstall
```
Key points:
@@ -60,7 +60,7 @@ Key points:
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/developers/extend/apps/config/application).
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty dev:function:exec --postInstall` to trigger it manually against a running workspace.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Runs before the workspace metadata migration is applied">
@@ -87,7 +87,7 @@ export default definePreInstallLogicFunction({
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
yarn twenty dev:function:exec --preInstall
```
Key points:
@@ -96,7 +96,7 @@ Key points:
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty dev:function:exec --preInstall` to trigger it manually.
</Accordion>
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">

View File

@@ -13,7 +13,7 @@ Files placed in `public/` are:
- **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic.
- **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published.
- **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed.
- **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output.
- **Included in builds** — `yarn twenty dev:build` bundles all public assets into the distribution output.
## Accessing public assets with `getPublicAssetUrl`

View File

@@ -80,7 +80,7 @@ export default defineObject({
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
- The `fields` array is optional — you can define objects without custom fields.
- Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/developers/extend/apps/data/extending-objects) to add fields to objects you don't own.
- You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding).
- You can scaffold new objects with `yarn twenty dev:add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding).
<Note>
**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea.

View File

@@ -65,7 +65,7 @@ your-app/
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty build → yarn twenty deploy
│ yarn twenty dev:build → yarn twenty app:publish
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
@@ -77,7 +77,7 @@ your-app/
```
- **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes.
- **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest.
- **`yarn twenty dev:build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest.
- **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/developers/extend/apps/config/install-hooks) for details.
## Next steps

View File

@@ -6,44 +6,44 @@ icon: "server"
## Managing the local server
Use `yarn twenty server` to control the local Twenty container:
Use `yarn twenty docker:*` to control the local Twenty container:
| Command | What it does |
|---------|--------------|
| `yarn twenty server start` | Start the server (pulls the image if needed) |
| `yarn twenty server start --port 3030` | Start on a custom port |
| `yarn twenty server stop` | Stop the server (preserves data) |
| `yarn twenty server status` | Show URL, version, and login credentials |
| `yarn twenty server logs` | Stream server logs |
| `yarn twenty server reset` | Wipe data and start fresh |
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image |
| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version |
| `yarn twenty docker:start` | Start the server (pulls the image if needed) |
| `yarn twenty docker:start --port 3030` | Start on a custom port |
| `yarn twenty docker:stop` | Stop the server (preserves data) |
| `yarn twenty docker:status` | Show URL, version, and login credentials |
| `yarn twenty docker:logs` | Stream server logs |
| `yarn twenty docker:reset` | Wipe data and start fresh |
| `yarn twenty docker:upgrade` | Pull the latest `twenty-app-dev` image |
| `yarn twenty docker:upgrade 2.2.0` | Upgrade to a specific version |
Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything.
## Upgrading the server image
`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy.
`yarn twenty docker:upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty docker:start` afterward to wait for it to become healthy.
```bash filename="Terminal"
yarn twenty server upgrade # Latest
yarn twenty server upgrade 2.2.0 # Specific version
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container).
Verify the running version with `yarn twenty docker:status` (it shows the `APP_VERSION` baked into the container).
## Running a parallel test instance
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data:
Pass `--test` to any `docker:*` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data:
| Command | What it does |
|---------|--------------|
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty server stop --test` | Stop it |
| `yarn twenty server status --test` | Show its status |
| `yarn twenty server logs --test` | Stream its logs |
| `yarn twenty server reset --test` | Wipe its data |
| `yarn twenty server upgrade --test` | Upgrade its image |
| `yarn twenty docker:start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty docker:stop --test` | Stop it |
| `yarn twenty docker:status --test` | Show its status |
| `yarn twenty docker:logs --test` | Stream its logs |
| `yarn twenty docker:reset --test` | Wipe its data |
| `yarn twenty docker:upgrade --test` | Upgrade its image |
The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021.
@@ -65,7 +65,7 @@ Add the script to `package.json`:
}
```
You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest.
You can now run `yarn twenty dev`, `yarn twenty docker:start`, and the rest.
<Note>
Don't install `twenty-sdk` globally — pin it per project so each app uses its own version.

View File

@@ -43,7 +43,7 @@ The scaffolder offers to start one for you:
> **Would you like to set up a local Twenty instance?**
- **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first.
- **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`.
- **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote:add`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
@@ -73,7 +73,7 @@ Your terminal will confirm everything is set up.
**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it.
<Note>
If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold.
If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty docker:start` — no need to re-scaffold.
</Note>
---

View File

@@ -1,13 +1,13 @@
---
title: Scaffolding
description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more.
description: Generate entity files interactively with yarn twenty dev:add — objects, fields, views, logic functions, and more.
icon: "wand-magic-sparkles"
---
Instead of creating entity files by hand, use the interactive scaffolder:
```bash filename="Terminal"
yarn twenty add
yarn twenty dev:add
```
It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call.
@@ -15,29 +15,29 @@ It prompts you to pick an entity type and walks you through the required fields,
You can also pass the entity type directly to skip the first prompt:
```bash filename="Terminal"
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Available entity types
| Entity type | Command | Generated file |
|-------------|---------|----------------|
| Object | `yarn twenty add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
## What the scaffolder generates
Each entity type has its own template. For example, `yarn twenty add object` asks for:
Each entity type has its own template. For example, `yarn twenty dev:add object` asks for:
1. **Name (singular)** — e.g., `invoice`
2. **Name (plural)** — e.g., `invoices`
@@ -54,5 +54,5 @@ The `field` entity type is more detailed: it asks for the field name, label, typ
Use the `--path` flag to place the generated file in a custom location:
```bash filename="Terminal"
yarn twenty add logicFunction --path src/custom-folder
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -4,7 +4,7 @@ description: Common first-run issues — Docker, Node version, Yarn, dependencie
icon: "wrench"
---
- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty server start`. The error message will show the right start command for your OS.
- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty docker:start`. The error message will show the right start command for your OS.
- **Wrong Node version** — Need 24+. Check with `node -v`.
- **Yarn 4 missing** — Run `corepack enable`.
- **Dependencies broken** — `rm -rf node_modules && yarn install`.

View File

@@ -61,17 +61,17 @@ Available trigger types:
You can also manually execute a function using the CLI:
```bash filename="Terminal"
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
You can watch logs with:
```bash filename="Terminal"
yarn twenty logs
yarn twenty dev:function:logs
```
</Note>
@@ -341,7 +341,7 @@ The `twenty-client-sdk` package provides two typed GraphQL clients for interacti
<AccordionGroup>
<Accordion title="CoreApiClient" description="Query and mutate workspace data (records, objects)">
`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields.
`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty dev:build`, so it is fully typed to match your objects and fields.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
@@ -381,7 +381,7 @@ const { createCompany } = await client.mutation({
The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
<Note>
**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`.
**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty dev:build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`.
</Note>
#### Using CoreSchema for type annotations

View File

@@ -4,54 +4,54 @@ description: yarn twenty commands for executing functions, streaming logs, manag
icon: "terminal"
---
Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations.
Beyond `dev`, `dev:build`, `dev:add`, and `dev:typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations.
## Executing functions (`yarn twenty exec`)
## Executing functions (`yarn twenty dev:function:exec`)
Run a logic function manually without triggering it via HTTP, cron, or database event:
```bash filename="Terminal"
# Execute by function name
yarn twenty exec -n create-new-post-card
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty exec --postInstall
yarn twenty dev:function:exec --postInstall
```
## Viewing function logs (`yarn twenty logs`)
## Viewing function logs (`yarn twenty dev:function:logs`)
Stream execution logs for your app's logic functions:
```bash filename="Terminal"
# Stream all function logs
yarn twenty logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty logs -n create-new-post-card
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server.
This is different from `yarn twenty docker:logs`, which shows the Docker container logs. `yarn twenty dev:function:logs` shows your app's function execution logs from the Twenty server.
</Note>
## Uninstalling an app (`yarn twenty uninstall`)
## Uninstalling an app (`yarn twenty app:uninstall`)
Remove your app from the active workspace:
```bash filename="Terminal"
yarn twenty uninstall
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty uninstall --yes
yarn twenty app:uninstall --yes
```
## Managing remotes
@@ -60,19 +60,19 @@ A **remote** is a Twenty server that your app connects to. During setup, the sca
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote list
yarn twenty remote:list
# Switch the active remote
yarn twenty remote switch <name>
# Set the active remote
yarn twenty remote:use <name>
```
Your credentials are stored in `~/.twenty/config.json`.

View File

@@ -9,9 +9,9 @@ The **operations layer** is everything you do *to* your app rather than *with* i
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty deploy (tarball → one server)
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev build yarn twenty publish (npm → marketplace)
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## In this section

View File

@@ -18,10 +18,10 @@ Both paths start from the same **build** step.
Run the build command to compile your app and generate a distribution-ready `manifest.json`:
```bash filename="Terminal"
yarn twenty build
yarn twenty dev:build
```
This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command.
This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the publish command.
## Deploying to a server (tarball)
@@ -34,7 +34,7 @@ Before deploying, you need a configured remote pointing to the target server. Re
Add a remote:
```bash filename="Terminal"
yarn twenty remote add --api-url https://your-twenty-server.com --as production
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Deploying
@@ -42,9 +42,9 @@ yarn twenty remote add --api-url https://your-twenty-server.com --as production
Build and upload your app to the server in one step:
```bash filename="Terminal"
yarn twenty deploy
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty deploy --remote production
# yarn twenty app:publish --private --remote production
```
### Sharing a deployed app
@@ -68,7 +68,7 @@ When updating an already deployed tarball app, the server requires the `version`
To release an update:
1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
2. Run `yarn twenty app:publish --private` (or `yarn twenty app:publish --private --remote production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
<Note>
@@ -121,7 +121,7 @@ Runs integration tests on every push to `main` and every pull request.
**What it does:**
1. Checks out your app's source.
2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`).
2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty docker:start --test`).
3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`.
4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server.
@@ -139,7 +139,7 @@ Deploys your app to a configured Twenty server on every push to `main`, and opti
**What it does:**
1. Checks out the PR head (for labeled PRs) or the pushed commit.
2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`.
2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty app:publish --private`.
3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace.
**Required configuration:**
@@ -208,13 +208,13 @@ Screenshots of any aspect ratio are displayed in full and are never cropped, but
### Publish
```bash filename="Terminal"
yarn twenty publish
yarn twenty app:publish
```
To publish under a specific dist-tag (e.g., `beta` or `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
yarn twenty app:publish --tag beta
```
### How marketplace discovery works
@@ -224,9 +224,9 @@ The Twenty server syncs its marketplace catalog from the npm registry **every ho
You can trigger the sync immediately instead of waiting:
```bash filename="Terminal"
yarn twenty server catalog-sync
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty server catalog-sync --remote production
# yarn twenty dev:catalog-sync --remote production
```
The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
@@ -259,12 +259,12 @@ jobs:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty build
- run: npx twenty dev:build
- run: npm publish --provenance --access public
working-directory: .twenty/output
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`.
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty dev:build`, then `npm publish` from `.twenty/output`.
<Note>
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
@@ -281,7 +281,7 @@ Go to the **Settings > Applications** page in Twenty, where both marketplace and
You can also install apps from the command line:
```bash filename="Terminal"
yarn twenty install
yarn twenty app:install
```
<Note>
@@ -290,5 +290,5 @@ The server enforces semver versioning on install, mirroring the rules on deploy:
- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
To install a newer version, deploy or publish it first, then re-run `yarn twenty install`.
To install a newer version, deploy or publish it first, then re-run `yarn twenty app:install`.
</Note>

View File

@@ -235,7 +235,7 @@ yarn test:watch
You can also run type checking on your app without running tests:
```bash filename="Terminal"
yarn twenty typecheck
yarn twenty dev:typecheck
```
This runs `tsc --noEmit` and reports any type errors.

View File

@@ -35,7 +35,7 @@ Everything is detected via AST analysis at build time — no config files, no re
## The developer experience
You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to a production server, or `yarn twenty publish` lists your app on npm and the Twenty marketplace.
You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty app:publish --private` pushes to a production server, or `yarn twenty app:publish` lists your app on npm and the Twenty marketplace.
<Card title="Build your first app" icon="arrow-right" href="/developers/extend/apps/getting-started">
Three-phase walkthrough — scaffold, run a local server, sync your changes.

View File

@@ -28,7 +28,7 @@ export const SettingsApplicationRegistrationDistributionTab = ({
availableApplicationId: registration.universalIdentifier,
});
const publishCommands = ['yarn twenty publish'];
const publishCommands = ['yarn twenty app:publish'];
return (
<>

View File

@@ -54,11 +54,11 @@ Run `yarn twenty help` to see all available commands.
## Configuration
The CLI stores credentials per remote in `~/.twenty/config.json`. Run `yarn twenty remote add` to configure a remote, or `yarn twenty remote list` to see existing ones.
The CLI stores credentials per remote in `~/.twenty/config.json`. Run `yarn twenty remote:add` to configure a remote, or `yarn twenty remote:list` to see existing ones.
## Troubleshooting
- Auth errors: run `yarn twenty remote add` to re-authenticate.
- Auth errors: run `yarn twenty remote:add` to re-authenticate.
- Typings out of date: restart `yarn twenty dev` to refresh the client and types.
- Not seeing changes in dev: make sure dev mode is running (`yarn twenty dev`).

View File

@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "2.6.0",
"version": "2.7.0",
"sideEffects": false,
"bin": {
"twenty": "dist/cli.cjs"

View File

@@ -14,7 +14,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Run: twenty server start --test\n' +
'Run: twenty docker:start --test\n' +
'Or set them in vitest env config.',
);
}
@@ -24,7 +24,7 @@ beforeAll(async () => {
if (!response?.ok) {
throw new Error(
`Twenty server not reachable at ${apiUrl}. ` +
'Run: twenty server start --test',
'Run: twenty docker:start --test',
);
}

View File

@@ -1,7 +1,7 @@
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { AppDevCommand } from '@/cli/commands/dev';
import { AppDevCommand } from '@/cli/commands/dev/dev';
import { pathExists } from '@/cli/utilities/file/fs-utils';
export type RunAppDevResult = {

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { registerCommands } from '@/cli/commands/app-command';
import { registerCommands } from '@/cli/commands';
import { ConfigService } from '@/cli/utilities/config/config-service';
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
@@ -17,7 +17,7 @@ program
program.option(
'-r, --remote <name>',
'Use a specific remote (overrides the default set by remote switch)',
'Use a specific remote (overrides the default set by remote:use)',
);
program.hook('preAction', async (thisCommand) => {

View File

@@ -1,252 +0,0 @@
import { formatPath } from '@/cli/utilities/file/file-path';
import chalk from 'chalk';
import type { Command } from 'commander';
import { SyncableEntity } from 'twenty-shared/application';
import { EntityAddCommand } from './add';
import { AppBuildCommand } from './build';
import { CatalogSyncCommand } from './catalog-sync';
import { DeployCommand } from './deploy';
import { AppDevCommand } from './dev';
import { LogicFunctionExecuteCommand } from './exec';
import { AppInstallCommand } from './install';
import { LogicFunctionLogsCommand } from './logs';
import { AppPublishCommand } from './publish';
import { registerRemoteCommands } from './remote';
import { registerServerCommands } from './server';
import { AppDevOnceCommand } from './dev-once';
import { AppTypecheckCommand } from './typecheck';
import { AppUninstallCommand } from './uninstall';
export const registerCommands = (program: Command): void => {
const buildCommand = new AppBuildCommand();
const devCommand = new AppDevCommand();
const devOnceCommand = new AppDevOnceCommand();
const installCommand = new AppInstallCommand();
const publishCommand = new AppPublishCommand();
const typecheckCommand = new AppTypecheckCommand();
const uninstallCommand = new AppUninstallCommand();
const catalogSyncCommand = new CatalogSyncCommand();
const deployCommand = new DeployCommand();
const addCommand = new EntityAddCommand();
const logsCommand = new LogicFunctionLogsCommand();
const executeCommand = new LogicFunctionExecuteCommand();
program
.command('dev [appPath]')
.description(
'Build and sync local application changes (watches by default; use --once for a one-shot sync)',
)
.option(
'-w, --watch',
'Watch source files and re-sync on every change (default behavior)',
)
.option(
'-o, --once',
'Build and sync once, then exit (useful for CI, scripts, and pre-commit hooks)',
)
.option('--debounceMs <ms>', 'Debounce in ms (default: 2 000)')
.option('-v, --verbose', 'Show detailed logs')
.option('-d, --debug', 'Show detailed logs (alias for --verbose)')
.action(async (appPath, options) => {
if (options.once && options.watch) {
console.error(
chalk.red(
'Error: --once and --watch are mutually exclusive. Watch mode is the default.',
),
);
process.exit(1);
}
const commonOptions = {
appPath: formatPath(appPath),
verbose: options.verbose || options.debug,
debounceMs: options.debounceMs
? parseInt(options.debounceMs, 10)
: undefined,
};
if (options.once) {
await devOnceCommand.execute(commonOptions);
return;
}
await devCommand.execute(commonOptions);
});
program
.command('build [appPath]')
.description('Build, sync, and generate API client into .twenty/output/')
.option('--tarball', 'Also pack into a .tgz tarball')
.action(async (appPath, options) => {
await buildCommand.execute({
appPath: formatPath(appPath),
tarball: options.tarball,
});
});
program
.command('install [appPath]')
.description('Install a deployed application on the connected server')
.option('-r, --remote <name>', 'Install on a specific remote')
.action(async (appPath, options) => {
await installCommand.execute({
appPath: formatPath(appPath),
remote: options.remote,
});
});
program
.command('deploy [appPath]')
.description("Publish a new version to a Twenty server's registry")
.option('-r, --remote <name>', 'Deploy to a specific remote')
.action(async (appPath, options) => {
await deployCommand.execute({
appPath: formatPath(appPath),
remote: options.remote,
});
});
program
.command('publish [appPath]')
.description('Build and publish to npm')
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
.action(async (appPath, options) => {
await publishCommand.execute({
appPath: formatPath(appPath),
tag: options.tag,
});
});
program
.command('catalog-sync')
.description(
'[Deprecated] Moved under server. Use `yarn twenty server catalog-sync`.',
)
.option('-r, --remote <name>', 'Sync on a specific remote')
.action(async (options) => {
console.warn(
chalk.yellow(
'`yarn twenty catalog-sync` is deprecated and will be removed in a future release.\n' +
'Use `yarn twenty server catalog-sync` instead.\n',
),
);
await catalogSyncCommand.execute({
remote: options.remote,
});
});
program
.command('typecheck [appPath]')
.description('Run TypeScript type checking on the application')
.action(async (appPath) => {
await typecheckCommand.execute({
appPath: formatPath(appPath),
});
});
program
.command('uninstall [appPath]')
.description('Uninstall application from Twenty')
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (appPath?: string, options?: { yes?: boolean }) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: !options?.yes,
});
process.exit(result.success ? 0 : 1);
} catch {
process.exit(1);
}
});
registerRemoteCommands(program);
registerServerCommands(program);
program
.command('add [entityType]')
.option('--path <path>', 'Path in which the entity should be created.')
.description(
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
)
.action(async (entityType?: string, options?: { path?: string }) => {
await addCommand.execute(entityType as SyncableEntity, options?.path);
});
program
.command('exec [appPath]')
.option('--postInstall', 'Execute post-install logic function if defined')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
'{}',
)
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Universal ID of the function to execute',
)
.option(
'-n, --functionName <functionName>',
'Name of the function to execute',
)
.description('Execute a logic function with a JSON payload')
.action(
async (
appPath?: string,
options?: {
postInstall?: boolean;
preInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (
!options?.postInstall &&
!options?.preInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --postInstall, --preInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
}
await executeCommand.execute({
...options,
payload: options?.payload ?? '{}',
appPath: formatPath(appPath),
});
},
);
program
.command('logs [appPath]')
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Only show logs for the function with this universal ID',
)
.option(
'-n, --functionName <functionName>',
'Only show logs for the function with this name',
)
.description('Watch application function logs')
.action(
async (
appPath?: string,
options?: {
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
await logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
},
);
};

View File

@@ -2,6 +2,7 @@ import path from 'path';
import { appBuild } from '@/cli/operations/build';
import { appDeploy } from '@/cli/operations/deploy';
import { appPublish } from '@/cli/operations/publish';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { readJson } from '@/cli/utilities/file/fs-utils';
@@ -11,10 +12,42 @@ import chalk from 'chalk';
export type DeployCommandOptions = {
appPath?: string;
remote?: string;
private?: boolean;
tag?: string;
};
export class DeployCommand {
async execute(options: DeployCommandOptions): Promise<void> {
if (options.private) {
await this.executePrivate(options);
} else {
await this.executeNpm(options);
}
}
private async executeNpm(options: DeployCommandOptions): Promise<void> {
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
await checkSdkVersionCompatibility(appPath);
console.log(chalk.blue('Publishing to npm...'));
console.log(chalk.gray(`App path: ${appPath}\n`));
const result = await appPublish({
appPath,
npmTag: options.tag,
onProgress: (message) => console.log(chalk.gray(message)),
});
if (!result.success) {
console.error(chalk.red(result.error.message));
process.exit(1);
}
console.log(chalk.green('✓ Published to npm successfully'));
}
private async executePrivate(options: DeployCommandOptions): Promise<void> {
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
await checkSdkVersionCompatibility(appPath);
@@ -57,6 +90,6 @@ export class DeployCommand {
console.log(
chalk.green(`\n✓ Published ${appName} v${appVersion} to ${remoteName}\n`),
);
console.log(' To install deployed application: `yarn twenty install`');
console.log(' To install deployed application: `yarn twenty app:install`');
}
}

View File

@@ -0,0 +1,56 @@
import { formatPath } from '@/cli/utilities/file/file-path';
import type { Command } from 'commander';
import { DeployCommand } from './deploy';
import { AppInstallCommand } from './install';
import { AppUninstallCommand } from './uninstall';
export const registerAppCommands = (program: Command): void => {
const deployCommand = new DeployCommand();
const installCommand = new AppInstallCommand();
const uninstallCommand = new AppUninstallCommand();
program
.command('app:publish [appPath]')
.description('Build and publish to npm (default) or server registry')
.option('--private', "Push to a Twenty server's registry instead of npm")
.option(
'-r, --remote <name>',
'Publish to a specific remote (with --private)',
)
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
.action(async (appPath, options) => {
await deployCommand.execute({
appPath: formatPath(appPath),
private: options.private,
remote: options.remote,
tag: options.tag,
});
});
program
.command('app:install [appPath]')
.description('Install a deployed app on the server')
.option('-r, --remote <name>', 'Install on a specific remote')
.action(async (appPath, options) => {
await installCommand.execute({
appPath: formatPath(appPath),
remote: options.remote,
});
});
program
.command('app:uninstall [appPath]')
.description('Uninstall app from server')
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (appPath?: string, options?: { yes?: boolean }) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: !options?.yes,
});
process.exit(result.success ? 0 : 1);
} catch {
process.exit(1);
}
});
};

View File

@@ -0,0 +1,200 @@
import { formatPath } from '@/cli/utilities/file/file-path';
import chalk from 'chalk';
import type { Command } from 'commander';
import type { SyncableEntity } from 'twenty-shared/application';
import { EntityAddCommand } from './dev/add';
import { AppBuildCommand } from './dev/build';
import { DeployCommand } from './app/deploy';
import { LogicFunctionExecuteCommand } from './dev/exec';
import { AppInstallCommand } from './app/install';
import { LogicFunctionLogsCommand } from './dev/logs';
import { AppTypecheckCommand } from './dev/typecheck';
import { AppUninstallCommand } from './app/uninstall';
const deprecate = (oldCmd: string, newCmd: string) =>
console.warn(
chalk.yellow(
`\`twenty ${oldCmd}\` is deprecated. Use \`twenty ${newCmd}\` instead.`,
),
);
export const registerDeprecatedCommands = (program: Command): void => {
const buildCommand = new AppBuildCommand();
const typecheckCommand = new AppTypecheckCommand();
const logsCommand = new LogicFunctionLogsCommand();
const executeCommand = new LogicFunctionExecuteCommand();
const addCommand = new EntityAddCommand();
const deployCommand = new DeployCommand();
const installCommand = new AppInstallCommand();
const uninstallCommand = new AppUninstallCommand();
program
.command('build [appPath]', { hidden: true })
.option('--tarball', 'Also pack into a .tgz tarball')
.action(
async (appPath: string | undefined, options: { tarball?: boolean }) => {
deprecate('build', 'dev:build');
await buildCommand.execute({
appPath: formatPath(appPath),
tarball: options.tarball,
});
},
);
program
.command('typecheck [appPath]', { hidden: true })
.action(async (appPath: string | undefined) => {
deprecate('typecheck', 'dev:typecheck');
await typecheckCommand.execute({
appPath: formatPath(appPath),
});
});
program
.command('logs [appPath]', { hidden: true })
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Only show logs for the function with this universal ID',
)
.option(
'-n, --functionName <functionName>',
'Only show logs for the function with this name',
)
.action(
async (
appPath?: string,
options?: {
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
deprecate('logs', 'dev:function:logs');
await logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
},
);
program
.command('exec [appPath]', { hidden: true })
.option('--postInstall', 'Execute post-install logic function if defined')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
'{}',
)
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Universal ID of the function to execute',
)
.option(
'-n, --functionName <functionName>',
'Name of the function to execute',
)
.action(
async (
appPath?: string,
options?: {
postInstall?: boolean;
preInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
deprecate('exec', 'dev:function:exec');
if (
!options?.postInstall &&
!options?.preInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --postInstall, --preInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
}
await executeCommand.execute({
...options,
payload: options?.payload ?? '{}',
appPath: formatPath(appPath),
});
},
);
program
.command('add [entityType]', { hidden: true })
.option('--path <path>', 'Path in which the entity should be created.')
.action(async (entityType?: string, options?: { path?: string }) => {
deprecate('add', 'dev:add');
await addCommand.execute(entityType as SyncableEntity, options?.path);
});
program
.command('publish [appPath]', { hidden: true })
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
.action(async (appPath: string | undefined, options: { tag?: string }) => {
deprecate('publish', 'app:publish');
await deployCommand.execute({
appPath: formatPath(appPath),
tag: options.tag,
});
});
program
.command('deploy [appPath]', { hidden: true })
.option('-r, --remote <name>', 'Deploy to a specific remote')
.action(
async (appPath: string | undefined, options: { remote?: string }) => {
deprecate('deploy', 'app:publish --private');
await deployCommand.execute({
appPath: formatPath(appPath),
private: true,
remote: options.remote,
});
},
);
program
.command('install [appPath]', { hidden: true })
.option('-r, --remote <name>', 'Install on a specific remote')
.action(
async (appPath: string | undefined, options: { remote?: string }) => {
deprecate('install', 'app:install');
await installCommand.execute({
appPath: formatPath(appPath),
remote: options.remote,
});
},
);
program
.command('uninstall [appPath]', { hidden: true })
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (appPath?: string, options?: { yes?: boolean }) => {
deprecate('uninstall', 'app:uninstall');
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: !options?.yes,
});
process.exit(result.success ? 0 : 1);
} catch {
process.exit(1);
}
});
program
.command('catalog-sync', { hidden: true })
.option('-r, --remote <name>', 'Sync on a specific remote')
.action(async (options: { remote?: string }) => {
deprecate('catalog-sync', 'dev:catalog-sync');
const { CatalogSyncCommand } = await import('./dev/catalog-sync');
const cmd = new CatalogSyncCommand();
await cmd.execute({ remote: options.remote });
});
};

View File

@@ -412,7 +412,7 @@ export class EntityAddCommand {
// Connection providers reference two serverVariables (`<NAME>_CLIENT_ID`
// and `<NAME>_CLIENT_SECRET`) that the dev needs to declare on
// `defineApplication.serverVariables`. Auto-append them so the dev
// doesn't have to remember the wiring after `twenty add connection-provider`.
// doesn't have to remember the wiring after `twenty dev:add connection-provider`.
// The util is best-effort: it handles the common file shapes and falls
// back to a printed snippet for anything it can't safely modify.
private async registerConnectionProviderServerVariables(

View File

@@ -0,0 +1,86 @@
import { formatPath } from '@/cli/utilities/file/file-path';
import chalk from 'chalk';
import type { Command } from 'commander';
import { LogicFunctionExecuteCommand } from '../exec';
import { LogicFunctionLogsCommand } from '../logs';
export const registerDevFunctionCommands = (program: Command): void => {
const logsCommand = new LogicFunctionLogsCommand();
const executeCommand = new LogicFunctionExecuteCommand();
program
.command('dev:function:logs [appPath]')
.description('Stream logic function logs')
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Only show logs for the function with this universal ID',
)
.option(
'-n, --functionName <functionName>',
'Only show logs for the function with this name',
)
.action(
async (
appPath?: string,
options?: {
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
await logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
},
);
program
.command('dev:function:exec [appPath]')
.description('Execute a logic function')
.option('--postInstall', 'Execute post-install logic function if defined')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
'{}',
)
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Universal ID of the function to execute',
)
.option(
'-n, --functionName <functionName>',
'Name of the function to execute',
)
.action(
async (
appPath?: string,
options?: {
postInstall?: boolean;
preInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (
!options?.postInstall &&
!options?.preInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --postInstall, --preInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
}
await executeCommand.execute({
...options,
payload: options?.payload ?? '{}',
appPath: formatPath(appPath),
});
},
);
};

View File

@@ -0,0 +1,97 @@
import { formatPath } from '@/cli/utilities/file/file-path';
import type { Command } from 'commander';
import { SyncableEntity } from 'twenty-shared/application';
import { EntityAddCommand } from './add';
import { AppBuildCommand } from './build';
import { AppDevCommand } from './dev';
import { AppDevOnceCommand } from './dev-once';
import { AppTypecheckCommand } from './typecheck';
import { registerDevFunctionCommands } from './function';
export const registerDevCommands = (program: Command): void => {
const buildCommand = new AppBuildCommand();
const devCommand = new AppDevCommand();
const devOnceCommand = new AppDevOnceCommand();
const typecheckCommand = new AppTypecheckCommand();
const addCommand = new EntityAddCommand();
const devAction = async (
appPath: string | undefined,
options: {
once?: boolean;
verbose?: boolean;
debug?: boolean;
debounceMs?: string;
},
) => {
const commonOptions = {
appPath: formatPath(appPath),
verbose: options.verbose || options.debug,
debounceMs: options.debounceMs
? parseInt(options.debounceMs, 10)
: undefined,
};
if (options.once) {
await devOnceCommand.execute(commonOptions);
return;
}
await devCommand.execute(commonOptions);
};
program
.command('dev [appPath]', { isDefault: true })
.description('Build and sync local changes (default command)')
.option(
'-o, --once',
'Build and sync once, then exit (useful for CI, scripts, and pre-commit hooks)',
)
.option('--debounceMs <ms>', 'Debounce in ms (default: 2 000)')
.option('-v, --verbose', 'Show detailed logs')
.option('-d, --debug', 'Show detailed logs (alias for --verbose)')
.action(devAction);
program
.command('dev:build [appPath]')
.description('Build and generate API client')
.option('--tarball', 'Also pack into a .tgz tarball')
.action(async (appPath, options) => {
await buildCommand.execute({
appPath: formatPath(appPath),
tarball: options.tarball,
});
});
program
.command('dev:typecheck [appPath]')
.description('Run TypeScript type checking')
.action(async (appPath) => {
await typecheckCommand.execute({
appPath: formatPath(appPath),
});
});
program
.command('dev:add [entityType]')
.description(
`Scaffold a new entity (${Object.values(SyncableEntity).join('|')})`,
)
.option('--path <path>', 'Path in which the entity should be created.')
.action(async (entityType?: string, options?: { path?: string }) => {
await addCommand.execute(entityType as SyncableEntity, options?.path);
});
program
.command('dev:catalog-sync')
.description('Trigger marketplace catalog sync')
.option('-r, --remote <name>', 'Sync on a specific remote')
.action(async (options: { remote?: string }) => {
const { CatalogSyncCommand } = await import('./catalog-sync');
const cmd = new CatalogSyncCommand();
await cmd.execute({ remote: options.remote });
});
registerDevFunctionCommands(program);
};

View File

@@ -0,0 +1,288 @@
import { serverStart } from '@/cli/operations/server-start';
import { serverUpgrade } from '@/cli/operations/server-upgrade';
import {
CONTAINER_NAME,
containerExists,
DEFAULT_PORT,
DEFAULT_TEST_PORT,
getContainerEnvVar,
getContainerPort,
isContainerRunning,
TEST_CONTAINER_NAME,
} from '@/cli/utilities/server/docker-container';
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import { execSync, spawnSync } from 'node:child_process';
const startAction = async (options: { port?: string; test?: boolean }) => {
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
const port = options.port ? parseInt(options.port, 10) : defaultPort;
if (isNaN(port) || port < 1 || port > 65535) {
console.error(chalk.red('Invalid port number.'));
process.exit(1);
}
const result = await serverStart({
port,
test: options.test,
onProgress: (message) => console.log(chalk.gray(message)),
});
if (!result.success) {
console.error(chalk.red(result.error.message));
process.exit(1);
}
};
const stopAction = (options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
if (!containerExists(containerName)) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
execSync(`docker stop ${containerName}`, { stdio: 'ignore' });
console.log(chalk.green('Twenty server stopped.'));
};
const logsAction = (options: { lines: string; test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
if (!containerExists(containerName)) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
try {
spawnSync(
'docker',
['logs', '-f', '--tail', options.lines, containerName],
{ stdio: 'inherit' },
);
} catch {
// User hit Ctrl-C
}
};
const statusAction = async (options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
if (!containerExists(containerName)) {
console.log(` Status: ${chalk.gray('not created')}`);
console.log(
chalk.gray(
` Run 'yarn twenty docker:start${options.test ? ' --test' : ''}' to create one.`,
),
);
return;
}
const running = isContainerRunning(containerName);
const port = running ? getContainerPort(containerName) : defaultPort;
const healthy = running ? await checkServerHealth(port) : false;
const statusText = healthy
? chalk.green('running (healthy)')
: running
? chalk.yellow('running (starting...)')
: chalk.gray('stopped');
const appVersion = getContainerEnvVar('APP_VERSION', containerName);
console.log(` Status: ${statusText}`);
console.log(` URL: http://localhost:${port}`);
if (appVersion) {
console.log(` Version: ${chalk.gray(appVersion)}`);
}
if (healthy) {
console.log(chalk.gray(' Login: tim@apple.dev / tim@apple.dev'));
}
};
const resetAction = (options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
const volumeData = options.test
? 'twenty-app-dev-test-data'
: 'twenty-app-dev-data';
const volumeStorage = options.test
? 'twenty-app-dev-test-storage'
: 'twenty-app-dev-storage';
if (containerExists(containerName)) {
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
}
try {
execSync(`docker volume rm ${volumeData} ${volumeStorage}`, {
stdio: 'ignore',
});
} catch {
// Volumes may not exist
}
console.log(chalk.green('Twenty server data reset.'));
console.log(
chalk.gray(
`Run 'yarn twenty docker:start${options.test ? ' --test' : ''}' to start a fresh instance.`,
),
);
};
const upgradeAction = async (
version: string | undefined,
options: { test?: boolean },
) => {
const result = await serverUpgrade({
version: version ?? 'latest',
test: options.test,
onProgress: (message) => console.log(chalk.gray(message)),
});
if (!result.success) {
console.error(chalk.red(result.error.message));
process.exit(1);
}
const { data } = result;
if (!data.imageUpdated) {
console.log(chalk.green(` Already up to date (${data.image}).`));
return;
}
console.log(chalk.green(` Upgraded to: ${data.image}`));
if (data.containerRecreated) {
console.log(
chalk.gray(
` Run 'yarn twenty docker:start${options.test ? ' --test' : ''}' to wait for the server to be ready.`,
),
);
}
};
export const registerServerCommands = (program: Command): void => {
program
.command('docker:start')
.description('Start the local Twenty container')
.option('-p, --port <port>', 'HTTP port')
.option('--test', 'Start a separate test instance (port 2021)')
.action(startAction);
program
.command('docker:stop')
.description('Stop the local Twenty container')
.option('--test', 'Stop the test instance')
.action(stopAction);
program
.command('docker:logs')
.description('Stream container logs')
.option('-n, --lines <lines>', 'Number of lines to show', '50')
.option('--test', 'Show logs for the test instance')
.action(logsAction);
program
.command('docker:status')
.description('Show container status')
.option('--test', 'Show status of the test instance')
.action(statusAction);
program
.command('docker:reset')
.description('Delete all data and start fresh')
.option('--test', 'Reset the test instance')
.action(resetAction);
program
.command('docker:upgrade [version]')
.description('Upgrade the Docker image')
.option('--test', 'Upgrade the test instance')
.action(upgradeAction);
// Deprecated: `server <subcommand>` forwarding to `docker:<subcommand>`
const server = program
.command('server', { hidden: true })
.description(
'Manage a Twenty server (local instance and server-side actions)',
);
const deprecate = (oldCmd: string, newCmd: string) =>
console.warn(
chalk.yellow(
`\`twenty server ${oldCmd}\` is deprecated. Use \`twenty ${newCmd}\` instead.`,
),
);
server
.command('start')
.option('-p, --port <port>', 'HTTP port')
.option('--test', 'Start a separate test instance (port 2021)')
.action(async (options: { port?: string; test?: boolean }) => {
deprecate('start', 'docker:start');
await startAction(options);
});
server
.command('stop')
.option('--test', 'Stop the test instance')
.action((options: { test?: boolean }) => {
deprecate('stop', 'docker:stop');
stopAction(options);
});
server
.command('logs')
.option('-n, --lines <lines>', 'Number of lines to show', '50')
.option('--test', 'Show logs for the test instance')
.action((options: { lines: string; test?: boolean }) => {
deprecate('logs', 'docker:logs');
logsAction(options);
});
server
.command('status')
.option('--test', 'Show status of the test instance')
.action(async (options: { test?: boolean }) => {
deprecate('status', 'docker:status');
await statusAction(options);
});
server
.command('reset')
.option('--test', 'Reset the test instance')
.action((options: { test?: boolean }) => {
deprecate('reset', 'docker:reset');
resetAction(options);
});
server
.command('upgrade [version]')
.option('--test', 'Upgrade the test instance')
.action(
async (version: string | undefined, options: { test?: boolean }) => {
deprecate('upgrade', 'docker:upgrade');
await upgradeAction(version, options);
},
);
server
.command('catalog-sync')
.option('-r, --remote <name>', 'Sync on a specific remote')
.action(async (options: { remote?: string }) => {
deprecate('catalog-sync', 'dev:catalog-sync');
const { CatalogSyncCommand } = await import('../dev/catalog-sync');
const cmd = new CatalogSyncCommand();
await cmd.execute({ remote: options.remote });
});
};

View File

@@ -0,0 +1,14 @@
import type { Command } from 'commander';
import { registerAppCommands } from './app';
import { registerDeprecatedCommands } from './deprecated';
import { registerDevCommands } from './dev';
import { registerServerCommands } from './docker';
import { registerRemoteCommands } from './remote';
export const registerCommands = (program: Command): void => {
registerDevCommands(program);
registerAppCommands(program);
registerServerCommands(program);
registerRemoteCommands(program);
registerDeprecatedCommands(program);
};

View File

@@ -1,298 +0,0 @@
import { authLogin } from '@/cli/operations/login';
import { authLoginOAuth } from '@/cli/operations/login-oauth';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
import { detectLocalServer } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import inquirer from 'inquirer';
import { normalizeUrl } from 'twenty-shared/utils';
const deriveRemoteName = (url: string): string => {
try {
const hostname = new URL(url).hostname;
return hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
};
type AuthMethod = 'OAuth' | 'API key';
const authenticate = async (
apiUrl: string,
apiKey?: string,
): Promise<AuthMethod> => {
if (apiKey) {
const result = await authLogin({ apiKey, apiUrl });
if (!result.success) {
console.error(chalk.red('✗ Authentication failed.'));
process.exit(1);
}
return 'API key';
}
return runOAuthWithApiKeyFallback(apiUrl);
};
const runOAuthWithApiKeyFallback = async (
apiUrl: string,
): Promise<AuthMethod> => {
console.log(chalk.gray('Opening browser for authentication...'));
const oauthResult = await authLoginOAuth({ apiUrl });
if (oauthResult.success) {
return 'OAuth';
}
console.log(chalk.yellow(oauthResult.error.message));
const keyAnswer = await inquirer.prompt([
{
type: 'password',
name: 'apiKey',
message: 'API Key:',
mask: '*',
validate: (input: string) => input.length > 0 || 'API key is required',
},
]);
const fallbackResult = await authLogin({
apiKey: keyAnswer.apiKey,
apiUrl,
});
if (!fallbackResult.success) {
console.error(chalk.red('✗ Authentication failed.'));
process.exit(1);
}
return 'API key';
};
export const registerRemoteCommands = (program: Command): void => {
const remote = program
.command('remote')
.description('Manage remote Twenty servers');
remote
.command('add')
.description('Add a new remote or re-authenticate an existing one')
.option('--as <name>', 'Name for this remote')
.option('--api-key <apiKey>', 'API key for non-interactive auth')
.option('--api-url <apiUrl>', 'Server URL')
.option('--local', 'Connect to a local Twenty server (auto-detect)')
.option('--test', 'Write to config.test.json (for integration tests)')
.action(
async (options: {
as?: string;
apiKey?: string;
apiUrl?: string;
local?: boolean;
test?: boolean;
}) => {
const configPath = options.test ? getConfigPath(true) : undefined;
const configService = new ConfigService(
configPath ? { configPath } : undefined,
);
const existingRemotes = await configService.getRemotes();
if (options.as !== undefined && existingRemotes.includes(options.as)) {
const config = await configService.getConfigForRemote(options.as);
ConfigService.setActiveRemote(options.as);
const method = await authenticate(config.apiUrl, options.apiKey);
console.log(
chalk.green(`✓ Re-authenticated "${options.as}" via ${method}.`),
);
await configService.setDefaultRemote(options.as);
console.log(chalk.green(`✓ Default remote set to "${options.as}".`));
return;
}
let apiUrl = options.apiUrl ? normalizeUrl(options.apiUrl) : undefined;
if (!apiUrl) {
const detectedUrl = await detectLocalServer();
if (options.local) {
if (!detectedUrl) {
console.error(
chalk.red(
'No local Twenty server found.\n' +
'Start one with: yarn twenty server start',
),
);
process.exit(1);
}
console.log(chalk.gray(`Found local server at ${detectedUrl}`));
apiUrl = detectedUrl;
} else {
apiUrl = normalizeUrl(
(
await inquirer.prompt<{ apiUrl: string }>([
{
type: 'input',
name: 'apiUrl',
message: 'Twenty server URL:',
validate: (input: string) => {
try {
new URL(input);
return true;
} catch {
return 'Please enter a valid URL';
}
},
},
])
).apiUrl,
);
}
}
const name = options.as ?? deriveRemoteName(apiUrl);
ConfigService.setActiveRemote(name);
const method = await authenticate(apiUrl, options.apiKey);
console.log(
chalk.green(`✓ Remote "${name}" added (${apiUrl}) via ${method}.`),
);
await configService.setDefaultRemote(name);
console.log(chalk.green(`✓ Default remote set to "${name}".`));
},
);
remote
.command('list')
.description('List all configured remotes')
.action(async () => {
const configService = new ConfigService();
const remotes = await configService.getRemotes();
const defaultRemote = await configService.getDefaultRemote();
if (remotes.length === 0) {
console.log('No remotes configured.');
console.log("Use 'twenty remote add' to add one.");
return;
}
console.log('');
for (const remoteName of remotes) {
const config = await configService.getConfigForRemote(remoteName);
const authMethod = config.twentyCLIAccessToken
? 'oauth'
: config.apiKey
? 'api-key'
: 'none';
const isDefault = remoteName === defaultRemote;
const marker = isDefault ? '* ' : ' ';
const nameText = isDefault ? chalk.bold(remoteName) : remoteName;
console.log(
`${marker}${nameText} ${chalk.gray(config.apiUrl)} [${authMethod}]`,
);
}
console.log(
'\n',
chalk.gray("Use 'twenty remote switch <name>' to change default"),
);
});
remote
.command('switch [name]')
.description('Set the default remote')
.action(async (nameArg?: string) => {
const configService = new ConfigService();
const remoteName =
nameArg ??
(
await inquirer.prompt<{ remote: string }>([
{
type: 'list',
name: 'remote',
message: 'Select default remote:',
choices: await configService.getRemotes(),
},
])
).remote;
const remotes = await configService.getRemotes();
if (!remotes.includes(remoteName)) {
console.error(chalk.red(`Remote "${remoteName}" not found.`));
process.exit(1);
}
await configService.setDefaultRemote(remoteName);
console.log(chalk.green(`✓ Default remote set to "${remoteName}".`));
});
remote
.command('status')
.description('Show active remote and authentication status')
.action(async () => {
const configService = new ConfigService();
const apiService = new ApiService();
const activeRemote = ConfigService.getActiveRemote();
const config = await configService.getConfig();
const authMethod = config.twentyCLIAccessToken
? 'oauth'
: config.apiKey
? 'api-key'
: 'none';
console.log(` Remote: ${chalk.bold(activeRemote)}`);
console.log(` Server: ${config.apiUrl}`);
if (authMethod === 'none') {
console.log(` Auth: ${chalk.yellow('not configured')}`);
return;
}
const { authValid } = await apiService.validateAuth();
const statusText = authValid
? chalk.green(`${authMethod} (valid)`)
: chalk.red(`${authMethod} (invalid)`);
console.log(` Auth: ${statusText}`);
});
remote
.command('remove <name>')
.description('Remove a remote')
.action(async (name: string) => {
const configService = new ConfigService();
const remotes = await configService.getRemotes();
if (!remotes.includes(name)) {
console.error(chalk.red(`Remote "${name}" not found.`));
process.exit(1);
}
ConfigService.setActiveRemote(name);
await configService.clearConfig();
console.log(chalk.green(`✓ Remote "${name}" removed.`));
});
};

View File

@@ -0,0 +1,365 @@
import { authLogin } from '@/cli/operations/login';
import { authLoginOAuth } from '@/cli/operations/login-oauth';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
import { detectLocalServer } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import inquirer from 'inquirer';
import { normalizeUrl } from 'twenty-shared/utils';
const deriveRemoteName = (url: string): string => {
try {
const hostname = new URL(url).hostname;
return hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
};
type AuthMethod = 'OAuth' | 'API key';
const authenticate = async (
apiUrl: string,
apiKey?: string,
): Promise<AuthMethod> => {
if (apiKey) {
const result = await authLogin({ apiKey, apiUrl });
if (!result.success) {
console.error(chalk.red('✗ Authentication failed.'));
process.exit(1);
}
return 'API key';
}
return runOAuthWithApiKeyFallback(apiUrl);
};
const runOAuthWithApiKeyFallback = async (
apiUrl: string,
): Promise<AuthMethod> => {
console.log(chalk.gray('Opening browser for authentication...'));
const oauthResult = await authLoginOAuth({ apiUrl });
if (oauthResult.success) {
return 'OAuth';
}
console.log(chalk.yellow(oauthResult.error.message));
const keyAnswer = await inquirer.prompt([
{
type: 'password',
name: 'apiKey',
message: 'API Key:',
mask: '*',
validate: (input: string) => input.length > 0 || 'API key is required',
},
]);
const fallbackResult = await authLogin({
apiKey: keyAnswer.apiKey,
apiUrl,
});
if (!fallbackResult.success) {
console.error(chalk.red('✗ Authentication failed.'));
process.exit(1);
}
return 'API key';
};
const addAction = async (options: {
as?: string;
apiKey?: string;
url?: string;
apiUrl?: string;
local?: boolean;
test?: boolean;
}) => {
if (options.apiUrl) {
console.warn(
chalk.yellow('⚠ --api-url is deprecated. Use --url instead.'),
);
}
const configPath = options.test ? getConfigPath(true) : undefined;
const configService = new ConfigService(
configPath ? { configPath } : undefined,
);
const existingRemotes = await configService.getRemotes();
if (options.as !== undefined && existingRemotes.includes(options.as)) {
const config = await configService.getConfigForRemote(options.as);
ConfigService.setActiveRemote(options.as);
const method = await authenticate(config.apiUrl, options.apiKey);
console.log(
chalk.green(`✓ Re-authenticated "${options.as}" via ${method}.`),
);
await configService.setDefaultRemote(options.as);
console.log(chalk.green(`✓ Default remote set to "${options.as}".`));
return;
}
let serverUrl = options.url ?? options.apiUrl;
if (serverUrl) {
serverUrl = normalizeUrl(serverUrl);
} else {
const detectedUrl = await detectLocalServer();
if (options.local) {
if (!detectedUrl) {
console.error(
chalk.red(
'No local Twenty server found.\n' +
'Start one with: yarn twenty docker:start',
),
);
process.exit(1);
}
console.log(chalk.gray(`Found local server at ${detectedUrl}`));
serverUrl = detectedUrl;
} else {
serverUrl = normalizeUrl(
(
await inquirer.prompt<{ serverUrl: string }>([
{
type: 'input',
name: 'serverUrl',
message: 'Twenty server URL:',
validate: (input: string) => {
try {
new URL(input);
return true;
} catch {
return 'Please enter a valid URL';
}
},
},
])
).serverUrl,
);
}
}
const name = options.as ?? deriveRemoteName(serverUrl);
ConfigService.setActiveRemote(name);
const method = await authenticate(serverUrl, options.apiKey);
console.log(
chalk.green(`✓ Remote "${name}" added (${serverUrl}) via ${method}.`),
);
await configService.setDefaultRemote(name);
console.log(chalk.green(`✓ Default remote set to "${name}".`));
};
const listAction = async () => {
const configService = new ConfigService();
const remotes = await configService.getRemotes();
const defaultRemote = await configService.getDefaultRemote();
if (remotes.length === 0) {
console.log('No remotes configured.');
console.log("Use 'twenty remote:add' to add one.");
return;
}
console.log('');
for (const remoteName of remotes) {
const config = await configService.getConfigForRemote(remoteName);
const authMethod = config.twentyCLIAccessToken
? 'oauth'
: config.apiKey
? 'api-key'
: 'none';
const isDefault = remoteName === defaultRemote;
const marker = isDefault ? '* ' : ' ';
const nameText = isDefault ? chalk.bold(remoteName) : remoteName;
console.log(
`${marker}${nameText} ${chalk.gray(config.apiUrl)} [${authMethod}]`,
);
}
console.log(
'\n',
chalk.gray("Use 'twenty remote:use <name>' to change default"),
);
};
const useAction = async (nameArg?: string) => {
const configService = new ConfigService();
const remoteName =
nameArg ??
(
await inquirer.prompt<{ remote: string }>([
{
type: 'list',
name: 'remote',
message: 'Select default remote:',
choices: await configService.getRemotes(),
},
])
).remote;
const remotes = await configService.getRemotes();
if (!remotes.includes(remoteName)) {
console.error(chalk.red(`Remote "${remoteName}" not found.`));
process.exit(1);
}
await configService.setDefaultRemote(remoteName);
console.log(chalk.green(`✓ Default remote set to "${remoteName}".`));
};
const statusAction = async () => {
const configService = new ConfigService();
const apiService = new ApiService();
const activeRemote = ConfigService.getActiveRemote();
const config = await configService.getConfig();
const authMethod = config.twentyCLIAccessToken
? 'oauth'
: config.apiKey
? 'api-key'
: 'none';
console.log(` Remote: ${chalk.bold(activeRemote)}`);
console.log(` Server: ${config.apiUrl}`);
if (authMethod === 'none') {
console.log(` Auth: ${chalk.yellow('not configured')}`);
return;
}
const { authValid } = await apiService.validateAuth();
const statusText = authValid
? chalk.green(`${authMethod} (valid)`)
: chalk.red(`${authMethod} (invalid)`);
console.log(` Auth: ${statusText}`);
};
const removeAction = async (name: string) => {
const configService = new ConfigService();
const remotes = await configService.getRemotes();
if (!remotes.includes(name)) {
console.error(chalk.red(`Remote "${name}" not found.`));
process.exit(1);
}
ConfigService.setActiveRemote(name);
await configService.clearConfig();
console.log(chalk.green(`✓ Remote "${name}" removed.`));
};
export const registerRemoteCommands = (program: Command): void => {
program
.command('remote:add')
.description('Add or re-authenticate a remote')
.option('--as <name>', 'Name for this remote')
.option('--api-key <apiKey>', 'API key for non-interactive auth')
.option('--url <url>', 'Server URL')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option('--local', 'Connect to a local Twenty server (auto-detect)')
.option('--test', 'Write to config.test.json (for integration tests)')
.action(addAction);
program
.command('remote:list')
.description('List all configured remotes')
.action(listAction);
program
.command('remote:use [name]')
.description('Set the default remote')
.action(useAction);
program
.command('remote:status')
.description('Show active remote auth status')
.action(statusAction);
program
.command('remote:remove <name>')
.description('Remove a remote')
.action(removeAction);
// Deprecated: `remote <subcommand>` forwarding to `remote:<subcommand>`
const remote = program
.command('remote', { hidden: true })
.description('Manage remote Twenty servers');
const deprecate = (oldCmd: string, newCmd: string) =>
console.warn(
chalk.yellow(
`\`twenty remote ${oldCmd}\` is deprecated. Use \`twenty ${newCmd}\` instead.`,
),
);
remote
.command('add')
.option('--as <name>', 'Name for this remote')
.option('--api-key <apiKey>', 'API key for non-interactive auth')
.option('--url <url>', 'Server URL')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option('--local', 'Connect to a local Twenty server (auto-detect)')
.option('--test', 'Write to config.test.json (for integration tests)')
.action(
async (options: {
as?: string;
apiKey?: string;
url?: string;
apiUrl?: string;
local?: boolean;
test?: boolean;
}) => {
deprecate('add', 'remote:add');
await addAction(options);
},
);
remote.command('list').action(async () => {
deprecate('list', 'remote:list');
await listAction();
});
remote.command('switch [name]').action(async (name?: string) => {
deprecate('switch', 'remote:use');
await useAction(name);
});
remote.command('status').action(async () => {
deprecate('status', 'remote:status');
await statusAction();
});
remote.command('remove <name>').action(async (name: string) => {
deprecate('remove', 'remote:remove');
await removeAction(name);
});
};

View File

@@ -1,215 +0,0 @@
import { serverStart } from '@/cli/operations/server-start';
import { serverUpgrade } from '@/cli/operations/server-upgrade';
import {
CONTAINER_NAME,
containerExists,
DEFAULT_PORT,
DEFAULT_TEST_PORT,
getContainerEnvVar,
getContainerPort,
isContainerRunning,
TEST_CONTAINER_NAME,
} from '@/cli/utilities/server/docker-container';
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import { execSync, spawnSync } from 'node:child_process';
import { CatalogSyncCommand } from './catalog-sync';
export const registerServerCommands = (program: Command): void => {
const server = program
.command('server')
.description(
'Manage a Twenty server (local instance and server-side actions)',
);
server
.command('start')
.description('Start a local Twenty server')
.option('-p, --port <port>', 'HTTP port')
.option('--test', 'Start a separate test instance (port 2021)')
.action(async (options: { port?: string; test?: boolean }) => {
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
const port = options.port ? parseInt(options.port, 10) : defaultPort;
if (isNaN(port) || port < 1 || port > 65535) {
console.error(chalk.red('Invalid port number.'));
process.exit(1);
}
const result = await serverStart({
port,
test: options.test,
onProgress: (message) => console.log(chalk.gray(message)),
});
if (!result.success) {
console.error(chalk.red(result.error.message));
process.exit(1);
}
});
server
.command('stop')
.description('Stop the local Twenty server')
.option('--test', 'Stop the test instance')
.action((options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
if (!containerExists(containerName)) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
execSync(`docker stop ${containerName}`, { stdio: 'ignore' });
console.log(chalk.green('Twenty server stopped.'));
});
server
.command('logs')
.description('Stream Twenty server logs')
.option('-n, --lines <lines>', 'Number of lines to show', '50')
.option('--test', 'Show logs for the test instance')
.action((options: { lines: string; test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
if (!containerExists(containerName)) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
try {
spawnSync(
'docker',
['logs', '-f', '--tail', options.lines, containerName],
{ stdio: 'inherit' },
);
} catch {
// User hit Ctrl-C
}
});
server
.command('status')
.description('Show Twenty server status')
.option('--test', 'Show status of the test instance')
.action(async (options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
if (!containerExists(containerName)) {
console.log(` Status: ${chalk.gray('not created')}`);
console.log(
chalk.gray(
` Run 'yarn twenty server start${options.test ? ' --test' : ''}' to create one.`,
),
);
return;
}
const running = isContainerRunning(containerName);
const port = running ? getContainerPort(containerName) : defaultPort;
const healthy = running ? await checkServerHealth(port) : false;
const statusText = healthy
? chalk.green('running (healthy)')
: running
? chalk.yellow('running (starting...)')
: chalk.gray('stopped');
const appVersion = getContainerEnvVar('APP_VERSION', containerName);
console.log(` Status: ${statusText}`);
console.log(` URL: http://localhost:${port}`);
if (appVersion) {
console.log(` Version: ${chalk.gray(appVersion)}`);
}
if (healthy) {
console.log(chalk.gray(' Login: tim@apple.dev / tim@apple.dev'));
}
});
server
.command('reset')
.description('Delete all data and start fresh')
.option('--test', 'Reset the test instance')
.action((options: { test?: boolean }) => {
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
const volumeData = options.test
? 'twenty-app-dev-test-data'
: 'twenty-app-dev-data';
const volumeStorage = options.test
? 'twenty-app-dev-test-storage'
: 'twenty-app-dev-storage';
if (containerExists(containerName)) {
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
}
try {
execSync(`docker volume rm ${volumeData} ${volumeStorage}`, {
stdio: 'ignore',
});
} catch {
// Volumes may not exist
}
console.log(chalk.green('Twenty server data reset.'));
console.log(
chalk.gray(
`Run 'yarn twenty server start${options.test ? ' --test' : ''}' to start a fresh instance.`,
),
);
});
server
.command('upgrade [version]')
.description('Upgrade the twenty-app-dev Docker image')
.option('--test', 'Upgrade the test instance')
.action(
async (version: string | undefined, options: { test?: boolean }) => {
const result = await serverUpgrade({
version: version ?? 'latest',
test: options.test,
onProgress: (message) => console.log(chalk.gray(message)),
});
if (!result.success) {
console.error(chalk.red(result.error.message));
process.exit(1);
}
const { data } = result;
if (!data.imageUpdated) {
console.log(chalk.green(` Already up to date (${data.image}).`));
return;
}
console.log(chalk.green(` Upgraded to: ${data.image}`));
if (data.containerRecreated) {
console.log(
chalk.gray(
` Run 'yarn twenty server start${options.test ? ' --test' : ''}' to wait for the server to be ready.`,
),
);
}
},
);
server
.command('catalog-sync')
.description('Trigger a marketplace catalog sync on the server')
.option('-r, --remote <name>', 'Sync on a specific remote')
.action(async (options: { remote?: string }) => {
const command = new CatalogSyncCommand();
await command.execute({ remote: options.remote });
});
};

View File

@@ -50,9 +50,9 @@ const innerAppDevOnce = async (
message:
'Cannot reach Twenty server.\n\n' +
' Start a local server:\n' +
' yarn twenty server start\n\n' +
' yarn twenty docker:start\n\n' +
' Check server status:\n' +
' yarn twenty server status',
' yarn twenty docker:status',
},
};
}
@@ -63,7 +63,7 @@ const innerAppDevOnce = async (
error: {
code: APP_ERROR_CODES.SYNC_FAILED,
message:
'Authentication failed. Run `yarn twenty remote add --local` to authenticate.',
'Authentication failed. Run `yarn twenty remote:add --local` to authenticate.',
},
};
}

View File

@@ -151,8 +151,8 @@ const innerServerStart = async (
if (!checkDockerRunning()) {
const retryCommand = isTest
? 'yarn twenty server start --test'
: 'yarn twenty server start';
? 'yarn twenty docker:start --test'
: 'yarn twenty docker:start';
return {
success: false,
@@ -177,7 +177,7 @@ const innerServerStart = async (
code: SERVER_ERROR_CODES.HEALTH_TIMEOUT,
message:
'Twenty server did not become healthy in time.\n' +
"Check: 'yarn twenty server logs'",
"Check: 'yarn twenty docker:logs'",
},
};
}
@@ -202,7 +202,7 @@ const innerServerStart = async (
if (existingPort !== port) {
onProgress?.(
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset${isTest ? ' --test' : ''}' first to change ports.`,
`Existing container uses port ${existingPort}. Run 'yarn twenty docker:reset${isTest ? ' --test' : ''}' first to change ports.`,
);
}
@@ -257,7 +257,7 @@ const innerServerStart = async (
code: SERVER_ERROR_CODES.HEALTH_TIMEOUT,
message:
'Twenty server did not become healthy in time.\n' +
"Check: 'yarn twenty server logs'",
"Check: 'yarn twenty docker:logs'",
},
};
}

View File

@@ -32,7 +32,7 @@ const innerServerUpgrade = async (
if (!checkDockerRunning()) {
const retryCommand = [
'yarn twenty server upgrade',
'yarn twenty docker:upgrade',
version !== 'latest' ? version : null,
isTest ? '--test' : null,
]

View File

@@ -51,7 +51,7 @@ export class ApiClient {
if (error.response?.status === 401) {
console.error(
chalk.red(
'Authentication failed. Run `yarn twenty remote add` to authenticate.',
'Authentication failed. Run `yarn twenty remote:add` to authenticate.',
),
);
} else if (error.response?.status === 403) {

View File

@@ -8,7 +8,7 @@ import { getConfigPath } from '@/cli/utilities/config/get-config-path';
export type RemoteConfig = {
apiUrl: string;
apiKey?: string;
// CLI OAuth app credentials (from `yarn twenty remote add`)
// CLI OAuth app credentials (from `yarn twenty remote:add`)
twentyCLIRegistrationId?: string;
twentyCLIRegistrationClientId?: string;
twentyCLIAccessToken?: string;

View File

@@ -53,9 +53,9 @@ export class CheckServerOrchestratorStep {
message:
'Cannot reach Twenty server.\n\n' +
' Start a local server:\n' +
' yarn twenty server start\n\n' +
' yarn twenty docker:start\n\n' +
' Check server status:\n' +
' yarn twenty server status\n\n' +
' yarn twenty docker:status\n\n' +
' Waiting for server...',
status: 'error',
},
@@ -73,7 +73,7 @@ export class CheckServerOrchestratorStep {
this.state.applyStepEvents([
{
message:
'Authentication failed. Run `yarn twenty remote add --local` to authenticate.',
'Authentication failed. Run `yarn twenty remote:add --local` to authenticate.',
status: 'error',
},
]);

View File

@@ -22,7 +22,7 @@ const DEFINE_APPLICATION_PATTERN = /defineApplication\s*\(\s*\{/;
// Auto-appends OAuth client_id / client_secret entries to the dev's
// `defineApplication({ serverVariables: { ... } })` block so they don't
// have to remember the wiring after `twenty add connection-provider`.
// have to remember the wiring after `twenty dev:add connection-provider`.
//
// Returns one of:
// - { status: 'appended', file } — wrote new entries to an existing block

View File

@@ -25,6 +25,6 @@ export const checkServerVersionCompatibility = async (
`⚠ Local Twenty server is v${info.localServerVersion} (${info.daysBehind} days behind v${info.latestServerVersion}).`,
),
);
console.warn(chalk.dim(' Update with: yarn twenty server upgrade'));
console.warn(chalk.dim(' Update with: yarn twenty docker:upgrade'));
console.warn('');
};