mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-11 09:26:53 -04:00
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:
2
.github/actions/deploy-twenty-app/action.yml
vendored
2
.github/actions/deploy-twenty-app/action.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ."
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Stub — overwritten by `twenty build` or `twenty dev`
|
||||
// Stub — overwritten by `twenty dev:build` or `twenty dev`
|
||||
export type CoreSchema = {};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -28,7 +28,7 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
availableApplicationId: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
const publishCommands = ['yarn twenty publish'];
|
||||
const publishCommands = ['yarn twenty app:publish'];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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`).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-sdk",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.0",
|
||||
"sideEffects": false,
|
||||
"bin": {
|
||||
"twenty": "dist/cli.cjs"
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -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`');
|
||||
}
|
||||
}
|
||||
56
packages/twenty-sdk/src/cli/commands/app/index.ts
Normal file
56
packages/twenty-sdk/src/cli/commands/app/index.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
200
packages/twenty-sdk/src/cli/commands/deprecated.ts
Normal file
200
packages/twenty-sdk/src/cli/commands/deprecated.ts
Normal 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 });
|
||||
});
|
||||
};
|
||||
@@ -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(
|
||||
86
packages/twenty-sdk/src/cli/commands/dev/function/index.ts
Normal file
86
packages/twenty-sdk/src/cli/commands/dev/function/index.ts
Normal 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),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
97
packages/twenty-sdk/src/cli/commands/dev/index.ts
Normal file
97
packages/twenty-sdk/src/cli/commands/dev/index.ts
Normal 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);
|
||||
};
|
||||
288
packages/twenty-sdk/src/cli/commands/docker/index.ts
Normal file
288
packages/twenty-sdk/src/cli/commands/docker/index.ts
Normal 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 });
|
||||
});
|
||||
};
|
||||
14
packages/twenty-sdk/src/cli/commands/index.ts
Normal file
14
packages/twenty-sdk/src/cli/commands/index.ts
Normal 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);
|
||||
};
|
||||
@@ -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.`));
|
||||
});
|
||||
};
|
||||
365
packages/twenty-sdk/src/cli/commands/remote/index.ts
Normal file
365
packages/twenty-sdk/src/cli/commands/remote/index.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
@@ -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 });
|
||||
});
|
||||
};
|
||||
@@ -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.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user