From fa9aeea4084c506066578cec6f569bf260eb17dc Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 12 Jun 2026 14:58:20 +0200 Subject: [PATCH] Add dev:generate-client command to sdk (#21489) ## after `yarn twenty dev:generate-client` image Review in cubic --- .../developers/extend/apps/operations/cli.mdx | 27 +++++++ .../src/cli/commands/dev/generate-client.ts | 71 +++++++++++++++++++ .../twenty-sdk/src/cli/commands/dev/index.ts | 13 ++++ .../cli/utilities/client/client-service.ts | 2 +- 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-sdk/src/cli/commands/dev/generate-client.ts diff --git a/packages/twenty-docs/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/developers/extend/apps/operations/cli.mdx index 5e326461aa..cb3c3ac6a5 100644 --- a/packages/twenty-docs/developers/extend/apps/operations/cli.mdx +++ b/packages/twenty-docs/developers/extend/apps/operations/cli.mdx @@ -43,6 +43,33 @@ yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf 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. +## Generating the typed client (`yarn twenty dev:generate-client`) + +Regenerate the typed API client (`twenty-client-sdk`) from the active remote's schema, without building or syncing an app. Use it to get a typed client in any project — like a backend service living in a separate repository — that talks to your Twenty instance: + +```bash filename="Terminal" +# In your project (no Twenty app definition required) +yarn add twenty-sdk twenty-client-sdk + +# Connect to the Twenty instance to generate the client from +yarn twenty remote:add + +# Generate the typed client into node_modules/twenty-client-sdk +yarn twenty dev:generate-client +``` + +Then import the client in your code: + +```typescript +import { CoreApiClient } from 'twenty-client-sdk/core'; +``` + +Re-run the command whenever your data model changes to refresh the generated types. + + +The client is generated inside `node_modules`, so it is not committed with your code. Run `yarn twenty dev:generate-client` after every install (for example in a `postinstall` script or in CI). + + ## Uninstalling an app (`yarn twenty app:uninstall`) Remove your app from the active workspace: diff --git a/packages/twenty-sdk/src/cli/commands/dev/generate-client.ts b/packages/twenty-sdk/src/cli/commands/dev/generate-client.ts new file mode 100644 index 0000000000..f0dcdac25b --- /dev/null +++ b/packages/twenty-sdk/src/cli/commands/dev/generate-client.ts @@ -0,0 +1,71 @@ +import { join } from 'path'; + +import { ApiService } from '@/cli/utilities/api/api-service'; +import { ClientService } from '@/cli/utilities/client/client-service'; +import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; +import { serializeError } from '@/cli/utilities/error/serialize-error'; +import { pathExists } from '@/cli/utilities/file/fs-utils'; +import chalk from 'chalk'; + +export type AppGenerateClientCommandOptions = { + appPath?: string; +}; + +export class AppGenerateClientCommand { + async execute(options: AppGenerateClientCommandOptions): Promise { + const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY; + const clientSdkPath = join(appPath, 'node_modules', 'twenty-client-sdk'); + + if (!(await pathExists(clientSdkPath))) { + console.error( + chalk.red( + `Cannot find twenty-client-sdk in ${appPath}.\n\n` + + ' Install it first:\n' + + ' yarn add twenty-client-sdk', + ), + ); + process.exit(1); + } + + const apiService = new ApiService({ disableInterceptors: true }); + const validateAuth = await apiService.validateAuth(); + + if (!validateAuth.serverUp) { + console.error( + chalk.red( + 'Cannot reach Twenty server.\n\n' + + ' Check your remotes:\n' + + ' yarn twenty remote:status', + ), + ); + process.exit(1); + } + + if (!validateAuth.authValid) { + console.error( + chalk.red( + 'Authentication failed. Run `yarn twenty remote:add` to authenticate.', + ), + ); + process.exit(1); + } + + console.log(chalk.blue('Generating API client...')); + + try { + const clientService = new ClientService({ skipAuth: false }); + + await clientService.generateCoreClient({ appPath }); + } catch (error) { + console.error( + chalk.red(`Failed to generate API client: ${serializeError(error)}`), + ); + process.exit(1); + } + + console.log(chalk.green('✓ API client generated')); + console.log( + chalk.gray(`Output: ${join(clientSdkPath, 'dist', 'core', 'generated')}`), + ); + } +} diff --git a/packages/twenty-sdk/src/cli/commands/dev/index.ts b/packages/twenty-sdk/src/cli/commands/dev/index.ts index b618398267..1754e07050 100644 --- a/packages/twenty-sdk/src/cli/commands/dev/index.ts +++ b/packages/twenty-sdk/src/cli/commands/dev/index.ts @@ -6,6 +6,7 @@ import { EntityAddCommand } from './add'; import { AppBuildCommand } from './build'; import { AppDevCommand } from './dev'; import { AppDevOnceCommand } from './dev-once'; +import { AppGenerateClientCommand } from './generate-client'; import { AppTypecheckCommand } from './typecheck'; import { registerDevFunctionCommands } from './function'; @@ -15,6 +16,7 @@ export const registerDevCommands = (program: Command): void => { const devOnceCommand = new AppDevOnceCommand(); const typecheckCommand = new AppTypecheckCommand(); const addCommand = new EntityAddCommand(); + const generateClientCommand = new AppGenerateClientCommand(); const devAction = async ( appPath: string | undefined, @@ -110,5 +112,16 @@ export const registerDevCommands = (program: Command): void => { await cmd.execute({ remote: options.remote }); }); + program + .command('dev:generate-client [appPath]') + .description( + 'Generate the typed API client from the active remote (no app definition required)', + ) + .action(async (appPath) => { + await generateClientCommand.execute({ + appPath: formatPath(appPath), + }); + }); + registerDevFunctionCommands(program); }; diff --git a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts index 811df733d5..075eb1b045 100644 --- a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts @@ -14,7 +14,7 @@ export class ClientService { this.apiService = new ApiService({ disableInterceptors: true, serverUrl: options?.serverUrl, - skipAuth: true, + skipAuth: options?.skipAuth ?? true, token: options?.token, }); }