diff --git a/.github/workflows/ci-create-app-e2e-hello-world.yaml b/.github/workflows/ci-create-app-e2e-hello-world.yaml index 8623c5404d..60ac77823e 100644 --- a/.github/workflows/ci-create-app-e2e-hello-world.yaml +++ b/.github/workflows/ci-create-app-e2e-hello-world.yaml @@ -109,7 +109,7 @@ jobs: create-twenty-app --version mkdir -p /tmp/e2e-test-workspace cd /tmp/e2e-test-workspace - create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance + create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --api-url http://localhost:3000 - name: Install scaffolded app dependencies run: | diff --git a/.github/workflows/ci-create-app-e2e-minimal.yaml b/.github/workflows/ci-create-app-e2e-minimal.yaml index 3a9bf1461e..d52dd552f0 100644 --- a/.github/workflows/ci-create-app-e2e-minimal.yaml +++ b/.github/workflows/ci-create-app-e2e-minimal.yaml @@ -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" --skip-local-instance --yes + create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --api-url http://localhost:3000 - name: Install scaffolded app dependencies run: | diff --git a/.github/workflows/ci-create-app-e2e-postcard.yaml b/.github/workflows/ci-create-app-e2e-postcard.yaml index 01b717387f..0e2b1f1021 100644 --- a/.github/workflows/ci-create-app-e2e-postcard.yaml +++ b/.github/workflows/ci-create-app-e2e-postcard.yaml @@ -107,7 +107,7 @@ jobs: create-twenty-app --version mkdir -p /tmp/e2e-test-workspace cd /tmp/e2e-test-workspace - create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance + create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --api-url http://localhost:3000 - name: Install scaffolded app dependencies run: | diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index 0bf3117cc3..fd1a89cecc 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -25,18 +25,19 @@ yarn twenty dev The scaffolder will: 1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI -2. Optionally start a local Twenty server (Docker) -3. Open the browser for OAuth authentication +2. Start a local Twenty server via Docker (pulls the latest image automatically) +3. Authenticate with the development API key ## Options -| Flag | Description | -| ------------------------------ | --------------------------------------- | -| `--example ` | Initialize from an example | -| `--name ` | Set the app name (skips the prompt) | -| `--display-name ` | Set the display name (skips the prompt) | -| `--description ` | Set the description (skips the prompt) | -| `--skip-local-instance` | Skip the local server setup prompt | +| Flag | Description | +| ---------------------------------- | --------------------------------------------------------------------- | +| `--example ` | Initialize from an example | +| `--name ` | Set the app name | +| `--display-name ` | Set the display name | +| `--description ` | Set the description | +| `--api-url ` | Twenty instance URL (default: `http://localhost:2020`) | +| `--authentication-method ` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) | By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example: @@ -57,7 +58,7 @@ 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: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`. +- 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 diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index 9a51d5082c..66fdaf6091 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "2.4.0", + "version": "2.4.1", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", @@ -32,7 +32,6 @@ "chalk": "^5.3.0", "commander": "^12.0.0", "fs-extra": "^11.2.0", - "inquirer": "^10.0.0", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.startcase": "^4.4.0", @@ -43,7 +42,6 @@ "@swc/core": "^1.15.11", "@swc/jest": "^0.2.39", "@types/fs-extra": "^11.0.0", - "@types/inquirer": "^9.0.0", "@types/jest": "^30.0.0", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.kebabcase": "^4.1.7", diff --git a/packages/create-twenty-app/src/cli.ts b/packages/create-twenty-app/src/cli.ts index f73e71deb5..024f20be9f 100644 --- a/packages/create-twenty-app/src/cli.ts +++ b/packages/create-twenty-app/src/cli.ts @@ -1,7 +1,10 @@ #!/usr/bin/env node import chalk from 'chalk'; import { Command, CommanderError } from 'commander'; -import { CreateAppCommand } from '@/create-app.command'; +import { + type AuthenticationMethod, + CreateAppCommand, +} from '@/create-app.command'; import packageJson from '../package.json'; const program = new Command(packageJson.name) @@ -13,20 +16,17 @@ const program = new Command(packageJson.name) ) .argument('[directory]') .option('--example ', 'Initialize from an example') - .option('-n, --name ', 'Application name (skips prompt)') + .option('-n, --name ', 'Application name') + .option('-d, --display-name ', 'Application display name') + .option('--description ', 'Application description') .option( - '-d, --display-name ', - 'Application display name (skips prompt)', + '--api-url ', + 'Twenty instance URL (default: http://localhost:2020)', ) .option( - '--description ', - 'Application description (skips prompt)', + '--authentication-method ', + 'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)', ) - .option( - '--skip-local-instance', - 'Skip the local Twenty instance setup prompt', - ) - .option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)') .helpOption('-h, --help', 'Display this help message.') .action( async ( @@ -36,8 +36,8 @@ const program = new Command(packageJson.name) name?: string; displayName?: string; description?: string; - skipLocalInstance?: boolean; - yes?: boolean; + apiUrl?: string; + authenticationMethod?: AuthenticationMethod; }, ) => { if (directory && !/^[a-z0-9-]+$/.test(directory)) { @@ -54,14 +54,26 @@ const program = new Command(packageJson.name) process.exit(1); } + if ( + options?.authenticationMethod && + !['oauth', 'apiKey'].includes(options.authenticationMethod) + ) { + console.error( + chalk.red( + 'Error: --authentication-method must be "oauth" or "apiKey".', + ), + ); + process.exit(1); + } + await new CreateAppCommand().execute({ directory, example: options?.example, name: options?.name, displayName: options?.displayName, description: options?.description, - skipLocalInstance: options?.skipLocalInstance, - yes: options?.yes, + apiUrl: options?.apiUrl, + authenticationMethod: options?.authenticationMethod, }); }, ); diff --git a/packages/create-twenty-app/src/constants/template/README.md b/packages/create-twenty-app/src/constants/template/README.md index 70b075e526..ffee43df9a 100644 --- a/packages/create-twenty-app/src/constants/template/README.md +++ b/packages/create-twenty-app/src/constants/template/README.md @@ -2,8 +2,19 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw ## Getting Started +This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020). + +Login with the default development credentials: `tim@apple.dev` / `tim@apple.dev`. + 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 test` - Run integration tests + ## Learn More - [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) diff --git a/packages/create-twenty-app/src/constants/template/github/workflows/cd.yml b/packages/create-twenty-app/src/constants/template/github/workflows/cd.yml index 61cbcfd7ab..0ec483fc57 100644 --- a/packages/create-twenty-app/src/constants/template/github/workflows/cd.yml +++ b/packages/create-twenty-app/src/constants/template/github/workflows/cd.yml @@ -11,7 +11,7 @@ permissions: contents: read env: - TWENTY_DEPLOY_URL: http://localhost:3000 + TWENTY_DEPLOY_URL: http://localhost:2020 concurrency: group: cd-${{ github.ref }} diff --git a/packages/create-twenty-app/src/create-app.command.ts b/packages/create-twenty-app/src/create-app.command.ts index cf4e334538..75d39401a7 100644 --- a/packages/create-twenty-app/src/create-app.command.ts +++ b/packages/create-twenty-app/src/create-app.command.ts @@ -5,21 +5,29 @@ import { install } from '@/utils/install'; import { tryGitInit } from '@/utils/try-git-init'; import chalk from 'chalk'; import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; import kebabCase from 'lodash.kebabcase'; import * as path from 'path'; import { basename } from 'path'; +import { spawn } from 'node:child_process'; import { + authLogin, authLoginOAuth, checkDockerRunning, ConfigService, - containerExists, - detectLocalServer, + DEV_API_KEY, + DEV_API_URL, serverStart, } from 'twenty-sdk/cli'; import { isDefined } from 'twenty-shared/utils'; +import { + getDockerInstallInstructions, + isDockerInstalled, +} from '@/utils/docker-install'; const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd(); +const IMAGE = 'twentycrm/twenty-app-dev:latest'; + +export type AuthenticationMethod = 'oauth' | 'apiKey'; type CreateAppOptions = { directory?: string; @@ -27,40 +35,62 @@ type CreateAppOptions = { name?: string; displayName?: string; description?: string; - skipLocalInstance?: boolean; - yes?: boolean; + apiUrl?: string; + authenticationMethod?: AuthenticationMethod; }; export class CreateAppCommand { - private static TOTAL_STEPS = 4; + private stepCounter = 0; + private totalSteps = 0; async execute(options: CreateAppOptions = {}): Promise { const { appName, appDisplayName, appDirectory, appDescription } = - await this.getAppInfos(options); + this.getAppInfos(options); + + const apiUrl = options.apiUrl ?? DEV_API_URL; + + const skipLocalInstance = apiUrl !== DEV_API_URL; + + if (!skipLocalInstance && !isDockerInstalled()) { + console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n')); + process.exit(1); + } + + if (skipLocalInstance && options.authenticationMethod === 'apiKey') { + console.log( + chalk.yellow( + 'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.', + ), + ); + } + + const authenticationMethod = skipLocalInstance + ? 'oauth' + : options.authenticationMethod; try { await this.validateDirectory(appDirectory); - const confirmed = await this.promptScaffoldConfirmation({ - appName, - appDisplayName, - appDescription, - appDirectory, - autoConfirm: options.yes, + this.totalSteps = this.computeTotalSteps({ + skipLocalInstance, }); - if (!confirmed) { - console.log(chalk.gray('\nScaffolding cancelled.')); - process.exit(0); - } + this.stepCounter = 0; - console.log(''); + const dockerPullPromise = + !skipLocalInstance && checkDockerRunning() + ? this.pullImageInBackground() + : Promise.resolve(false); + + this.logPlan({ appName, appDisplayName, appDescription, appDirectory }); + + this.logNextStep('Creating project directory'); - this.logStep(1, 'Creating project directory'); await fs.ensureDir(appDirectory); + this.logDetail(appDirectory); - this.logStep(2, 'Scaffolding project files'); + this.logNextStep('Scaffolding project files'); if (options.example) { const exampleSucceeded = await this.tryDownloadExample( @@ -87,10 +117,12 @@ export class CreateAppCommand { }); } - this.logStep(3, 'Installing dependencies'); + this.logNextStep('Installing dependencies'); + await install(appDirectory, (message) => this.logDetail(message)); - this.logStep(4, 'Initializing Git repository'); + this.logNextStep('Initializing Git repository'); + const gitInitialized = await tryGitInit(appDirectory); if (gitInitialized) { @@ -104,38 +136,29 @@ export class CreateAppCommand { console.log(''); - let hasLocalServer = false; let authSucceeded = false; + let resolvedApiUrl = apiUrl; + let serverReady = skipLocalInstance; - if (!options.skipLocalInstance) { - const existingServerUrl = await detectLocalServer(); + if (!skipLocalInstance) { + this.logNextStep('Starting Twenty server'); + const serverResult = await this.ensureDockerServer(dockerPullPromise); - if (existingServerUrl) { - hasLocalServer = true; - authSucceeded = await this.promptConnectToLocal(existingServerUrl); - } else { - const shouldStart = await this.shouldStartServer(options.yes); - - if (shouldStart) { - const startResult = await serverStart({ - onProgress: (message: string) => console.log(chalk.gray(message)), - }); - - if (startResult.success) { - hasLocalServer = true; - authSucceeded = await this.promptConnectToLocal( - startResult.data.url, - ); - } else { - console.log(chalk.yellow(`\n${startResult.error.message}`)); - } - } else { - this.logServerSkipped(); - } + if (isDefined(serverResult.url)) { + resolvedApiUrl = serverResult.url; + serverReady = true; } } - this.logSuccess(appDirectory, hasLocalServer, authSucceeded); + if (serverReady && authenticationMethod === 'oauth') { + this.logNextStep('Authenticating via OAuth'); + authSucceeded = await this.authenticateWithOAuth(resolvedApiUrl); + } else if (serverReady && authenticationMethod === 'apiKey') { + this.logNextStep('Authenticating via API key'); + authSucceeded = await this.authenticateWithDevKey(resolvedApiUrl); + } + + this.logSuccess(appDirectory, resolvedApiUrl, authSucceeded); } catch (error) { console.error( chalk.red('\nCreate application failed:'), @@ -145,66 +168,42 @@ export class CreateAppCommand { } } - private async getAppInfos(options: CreateAppOptions): Promise<{ + private computeTotalSteps({ + skipLocalInstance, + }: { + skipLocalInstance: boolean; + }): number { + let steps = 4; // directory, scaffold, install, git + + if (!skipLocalInstance) { + steps += 1; // start server + } + + steps += 1; // authenticate (oauth or apiKey) + + return steps; + } + + private getAppInfos(options: CreateAppOptions): { appName: string; appDisplayName: string; appDescription: string; appDirectory: string; - }> { - const { directory } = options; - - const hasName = isDefined(options.name) || isDefined(directory); - const hasDisplayName = isDefined(options.displayName); - const hasDescription = isDefined(options.description); - const hasExample = isDefined(options.example); - - const { name, displayName, description } = await inquirer.prompt([ - { - type: 'input', - name: 'name', - message: 'Application name:', - when: () => !hasName && !hasExample, - default: 'my-twenty-app', - validate: (input) => { - if (input.length === 0) return 'Application name is required'; - return true; - }, - }, - { - type: 'input', - name: 'displayName', - message: 'Application display name:', - when: () => !hasDisplayName && !hasExample, - default: (answers: { name?: string }) => { - return convertToLabel( - answers?.name ?? options.name ?? directory ?? '', - ); - }, - }, - { - type: 'input', - name: 'description', - message: 'Application description (optional):', - when: () => !hasDescription && !hasExample, - default: '', - }, - ]); - + } { const appName = ( options.name ?? - name ?? - directory ?? + options.directory ?? options.example ?? 'my-twenty-app' ).trim(); const appDisplayName = - (options.displayName ?? displayName)?.trim() || convertToLabel(appName); + options.displayName?.trim() || convertToLabel(appName); - const appDescription = (options.description ?? description ?? '').trim(); + const appDescription = (options.description ?? '').trim(); - const appDirectory = directory - ? path.join(CURRENT_EXECUTION_DIRECTORY, directory) + const appDirectory = options.directory + ? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory) : path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName)); return { appName, appDisplayName, appDirectory, appDescription }; @@ -232,45 +231,30 @@ export class CreateAppCommand { return true; } catch (error) { - console.error( - chalk.red( + console.log( + chalk.yellow( `\n${error instanceof Error ? error.message : 'Failed to download example.'}`, ), ); + this.logDetail('Falling back to default template...'); - const { useTemplate } = await inquirer.prompt([ - { - type: 'confirm', - name: 'useTemplate', - message: 'Would you like to create a default template app instead?', - default: true, - }, - ]); - - if (!useTemplate) { - process.exit(1); - } - - // Clean up any partial files from the failed download await fs.emptyDir(appDirectory); return false; } } - private async promptScaffoldConfirmation({ + private logPlan({ appName, appDisplayName, appDescription, appDirectory, - autoConfirm, }: { appName: string; appDisplayName: string; appDescription: string; appDirectory: string; - autoConfirm?: boolean; - }): Promise { + }): void { console.log(chalk.blue('\nCreating Twenty Application\n')); console.log(chalk.white(` Name: ${appName}`)); console.log(chalk.white(` Display name: ${appDisplayName}`)); @@ -280,43 +264,13 @@ export class CreateAppCommand { } console.log(chalk.white(` Directory: ${appDirectory}`)); - - console.log(chalk.white('\nThe following steps will be performed:\n')); - console.log(chalk.gray(' 1. Create project directory')); - console.log( - chalk.gray( - ' 2. Scaffold project files from base template\n' + - ' - Copy template files\n' + - ' - Configure dotfiles (.gitignore, .github)\n' + - ' - Generate unique application identifiers\n' + - ' - Update package.json with app name and SDK versions', - ), - ); - console.log(chalk.gray(' 3. Install dependencies (yarn)')); - console.log( - chalk.gray(' 4. Initialize Git repository with initial commit'), - ); console.log(''); - - if (autoConfirm) { - return true; - } - - const { proceed } = await inquirer.prompt([ - { - type: 'confirm', - name: 'proceed', - message: 'Proceed?', - default: true, - }, - ]); - - return proceed; } - private logStep(step: number, title: string): void { + private logNextStep(title: string): void { + this.stepCounter++; console.log( - chalk.blue(`\n[${step}/${CreateAppCommand.TOTAL_STEPS}]`) + + chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) + chalk.white(` ${title}...`), ); } @@ -325,100 +279,57 @@ export class CreateAppCommand { console.log(chalk.gray(` → ${message}`)); } - private async shouldStartServer(autoConfirm?: boolean): Promise { - console.log( - chalk.white( - '\n A local Twenty instance is required for app development.\n' + - ' It provides the API and schema your application connects to.\n', - ), - ); + private pullImageInBackground(): Promise { + return new Promise((resolve) => { + const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' }); - if (checkDockerRunning() && containerExists()) { - if (autoConfirm) { - return true; - } - - const { startExisting } = await inquirer.prompt([ - { - type: 'confirm', - name: 'startExisting', - message: - 'An existing Twenty server container was found. Would you like to start it?', - default: true, - }, - ]); - - return startExisting; - } - - if (autoConfirm) { - return true; - } - - const { startDocker } = await inquirer.prompt([ - { - type: 'confirm', - name: 'startDocker', - message: - 'No running Twenty instance found. Would you like to start one using Docker?', - default: true, - }, - ]); - - return startDocker; + child.on('close', (code) => resolve(code === 0)); + child.on('error', () => resolve(false)); + }); } - private logServerSkipped(): void { - console.log( - chalk.gray( - '\n To start a Twenty instance later:\n' + - ' yarn twenty server start\n\n' + - ' To connect to a remote instance instead:\n' + - ' yarn twenty remote add\n', - ), - ); - } - - private async promptConnectToLocal(serverUrl: string): Promise { - console.log( - chalk.white( - '\n Authentication links your app to a Twenty instance so you can\n' + - ' sync custom objects, fields, and roles during development.\n' + - ' This will open a browser window to complete the OAuth flow.\n', - ), - ); - - const { shouldAuthenticate } = await inquirer.prompt([ - { - type: 'confirm', - name: 'shouldAuthenticate', - message: `Authenticate to the local Twenty instance (${serverUrl})?`, - default: true, - }, - ]); - - if (!shouldAuthenticate) { + private async ensureDockerServer( + dockerPullPromise: Promise, + ): Promise<{ url?: string }> { + if (!checkDockerRunning()) { console.log( - chalk.gray( - '\n Authentication skipped. To authenticate later:\n' + - ` yarn twenty remote add --local\n`, + chalk.yellow( + '\n Docker is installed but not running.\n' + + ' Please start Docker and run this command again.\n', ), ); - return false; + return {}; } - await inquirer.prompt([ - { - type: 'input', - name: 'confirm', - message: 'Press Enter to open the browser for authentication...', - }, - ]); + this.logDetail('Ensuring latest Twenty server image...'); + const pullSucceeded = await dockerPullPromise; + + if (!pullSucceeded) { + this.logDetail( + 'Image pull failed, continuing with cached image if available...', + ); + } + + const startResult = await serverStart({ + onProgress: (message: string) => this.logDetail(message), + }); + + if (startResult.success) { + return { url: startResult.data.url }; + } + + console.log(chalk.yellow(`\n ${startResult.error.message}`)); + + return {}; + } + + private async authenticateWithDevKey(apiUrl: string): Promise { try { - const result = await authLoginOAuth({ - apiUrl: serverUrl, + const result = await authLogin({ + apiKey: DEV_API_KEY, + apiUrl, remote: 'local', }); @@ -426,21 +337,68 @@ export class CreateAppCommand { const configService = new ConfigService(); await configService.setDefaultRemote('local'); + this.logDetail('Authenticated as tim@apple.dev (development API key)'); return true; - } else { - console.log( - chalk.yellow( - 'Authentication failed. Run `yarn twenty remote add --local` manually.', - ), - ); - - return false; } + + console.log( + chalk.yellow( + ' Authentication failed. Run `yarn twenty remote add --local` manually.', + ), + ); + + return false; } catch { console.log( chalk.yellow( - 'Authentication failed. Run `yarn twenty remote add` manually.', + ' Authentication failed. Run `yarn twenty remote add --local` manually.', + ), + ); + + return false; + } + } + + private deriveRemoteName(url: string): string { + try { + return new URL(url).hostname.replace(/\./g, '-'); + } catch { + return 'remote'; + } + } + + private async authenticateWithOAuth(apiUrl: string): Promise { + try { + const remoteName = this.deriveRemoteName(apiUrl); + + ConfigService.setActiveRemote(remoteName); + + this.logDetail('Opening browser for OAuth...'); + + const result = await authLoginOAuth({ apiUrl }); + + if (result.success) { + const configService = new ConfigService(); + + await configService.setDefaultRemote(remoteName); + this.logDetail(`Authenticated via OAuth to ${apiUrl}`); + + return true; + } + + console.log( + chalk.yellow( + ` OAuth failed: ${result.error.message}\n` + + ` Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`, + ), + ); + + return false; + } catch { + console.log( + chalk.yellow( + ` Authentication failed. Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`, ), ); @@ -450,12 +408,13 @@ export class CreateAppCommand { private logSuccess( appDirectory: string, - hasLocalServer: boolean, + apiUrl: string, authSucceeded: boolean, ): void { const dirName = basename(appDirectory); console.log(chalk.green('\n✔ Application created successfully!\n')); + console.log(chalk.white(' Next steps:\n')); let stepNumber = 1; @@ -465,17 +424,21 @@ export class CreateAppCommand { stepNumber++; if (!authSucceeded) { - const remoteCommand = hasLocalServer - ? 'yarn twenty remote add --local' - : 'yarn twenty remote add'; - console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`)); - console.log(chalk.cyan(` ${remoteCommand}\n`)); + console.log( + chalk.cyan( + ' yarn twenty remote add --api-url \n', + ), + ); stepNumber++; } console.log(chalk.white(` ${stepNumber}. Start developing`)); console.log(chalk.cyan(' yarn twenty dev\n')); + stepNumber++; + + console.log(chalk.white(` ${stepNumber}. Open your twenty instance`)); + console.log(chalk.cyan(` ${apiUrl}\n`)); console.log( chalk.gray( diff --git a/packages/create-twenty-app/src/utils/docker-install.ts b/packages/create-twenty-app/src/utils/docker-install.ts new file mode 100644 index 0000000000..e6bd69f97a --- /dev/null +++ b/packages/create-twenty-app/src/utils/docker-install.ts @@ -0,0 +1,33 @@ +import { execSync } from 'node:child_process'; + +const DOCKER_DOWNLOAD_URLS: Record = { + darwin: 'https://docs.docker.com/desktop/setup/install/mac-install/', + win32: 'https://docs.docker.com/desktop/setup/install/windows-install/', + linux: 'https://docs.docker.com/engine/install/', +}; + +export const isDockerInstalled = (): boolean => { + try { + execSync('docker --version', { stdio: 'ignore' }); + + return true; + } catch { + return false; + } +}; + +export const getDockerInstallInstructions = (): string => { + const url = + DOCKER_DOWNLOAD_URLS[process.platform] ?? DOCKER_DOWNLOAD_URLS.linux; + + return [ + ' Docker is required but not installed.', + '', + ` Install Docker: ${url}`, + '', + ' Then run this command again.', + '', + ' Alternatively, connect to an existing Twenty instance:', + ' npx create-twenty-app@latest my-twenty-app --api-url ', + ].join('\n'); +}; diff --git a/packages/twenty-client-sdk/package.json b/packages/twenty-client-sdk/package.json index 5e0377764d..aa27f65584 100644 --- a/packages/twenty-client-sdk/package.json +++ b/packages/twenty-client-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-client-sdk", - "version": "2.4.0", + "version": "2.4.1", "sideEffects": false, "license": "AGPL-3.0", "scripts": { diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx index 312b1ce889..d100d57e61 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx @@ -87,7 +87,7 @@ cd my-twenty-app yarn twenty dev ``` -This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a few seconds the server reflects the change. You'll see a live status panel in your terminal. For more detailed output (build logs, sync requests, error traces), add `--verbose`. @@ -128,11 +128,15 @@ yarn twenty dev --once | `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | | `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | -Both modes need a server in development mode and an authenticated remote. +Both modes need an authenticated remote. - -Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/developers/extend/apps/operations/publishing). - +### Dev mode options + +| Flag | Description | +|------|-------------| +| `--once` | Build and sync once, then exit. | +| `--debounceMs ` | Set the file-change debounce delay in milliseconds (default: `2000`). | +| `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. | --- diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index f3f33e4245..5639529e9b 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "2.4.0", + "version": "2.4.1", "sideEffects": false, "bin": { "twenty": "dist/cli.cjs" diff --git a/packages/twenty-sdk/src/cli/commands/app-command.ts b/packages/twenty-sdk/src/cli/commands/app-command.ts index 61c45fc7d1..d0ce67e467 100644 --- a/packages/twenty-sdk/src/cli/commands/app-command.ts +++ b/packages/twenty-sdk/src/cli/commands/app-command.ts @@ -44,6 +44,7 @@ export const registerCommands = (program: Command): void => { '-o, --once', 'Build and sync once, then exit (useful for CI, scripts, and pre-commit hooks)', ) + .option('--debounceMs ', '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) => { @@ -59,6 +60,9 @@ export const registerCommands = (program: Command): void => { const commonOptions = { appPath: formatPath(appPath), verbose: options.verbose || options.debug, + debounceMs: options.debounceMs + ? parseInt(options.debounceMs, 10) + : undefined, }; if (options.once) { diff --git a/packages/twenty-sdk/src/cli/commands/dev.ts b/packages/twenty-sdk/src/cli/commands/dev.ts index 80ea9a5df9..2e95cff296 100644 --- a/packages/twenty-sdk/src/cli/commands/dev.ts +++ b/packages/twenty-sdk/src/cli/commands/dev.ts @@ -12,6 +12,7 @@ export type AppDevOptions = { appPath?: string; headless?: boolean; verbose?: boolean; + debounceMs?: number; }; export class AppDevCommand { @@ -60,6 +61,7 @@ export class AppDevCommand { this.orchestrator = new DevModeOrchestrator({ state: orchestratorState, verbose: options.verbose, + debounceMs: options.debounceMs, }); await this.orchestrator.start(); diff --git a/packages/twenty-sdk/src/cli/commands/remote.ts b/packages/twenty-sdk/src/cli/commands/remote.ts index fbf2c9fcdb..4ff009cc81 100644 --- a/packages/twenty-sdk/src/cli/commands/remote.ts +++ b/packages/twenty-sdk/src/cli/commands/remote.ts @@ -41,13 +41,7 @@ const authenticate = async ( const runOAuthWithApiKeyFallback = async ( apiUrl: string, ): Promise => { - await inquirer.prompt([ - { - type: 'input', - name: 'confirm', - message: 'Press Enter to open the browser for authentication...', - }, - ]); + console.log(chalk.gray('Opening browser for authentication...')); const oauthResult = await authLoginOAuth({ apiUrl }); diff --git a/packages/twenty-sdk/src/cli/constants/dev-api-key.ts b/packages/twenty-sdk/src/cli/constants/dev-api-key.ts new file mode 100644 index 0000000000..0c9c094618 --- /dev/null +++ b/packages/twenty-sdk/src/cli/constants/dev-api-key.ts @@ -0,0 +1,7 @@ +// Seeded development API key for the twenty-app-dev Docker image. +// Maps to API key ID 20202020-f401-4d8a-a731-64d007c27bad in tim@apple.dev's workspace. +// Only valid against the local dev Docker image — not a secret. +export const DEV_API_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; + +export const DEV_API_URL = 'http://localhost:2020'; diff --git a/packages/twenty-sdk/src/cli/operations/index.ts b/packages/twenty-sdk/src/cli/operations/index.ts index bf0acb4d4b..8b49784fd1 100644 --- a/packages/twenty-sdk/src/cli/operations/index.ts +++ b/packages/twenty-sdk/src/cli/operations/index.ts @@ -44,6 +44,9 @@ export { // Config export { ConfigService } from '@/cli/utilities/config/config-service'; +// Constants +export { DEV_API_KEY, DEV_API_URL } from '@/cli/constants/dev-api-key'; + // Shared types and error codes export { APP_ERROR_CODES, diff --git a/packages/twenty-sdk/src/cli/types.ts b/packages/twenty-sdk/src/cli/types.ts index e1d1f94792..2ad9acafbd 100644 --- a/packages/twenty-sdk/src/cli/types.ts +++ b/packages/twenty-sdk/src/cli/types.ts @@ -28,6 +28,7 @@ export const APP_ERROR_CODES = { } as const; export const SERVER_ERROR_CODES = { + DOCKER_NOT_INSTALLED: 'DOCKER_NOT_INSTALLED', DOCKER_NOT_RUNNING: 'DOCKER_NOT_RUNNING', CONTAINER_START_FAILED: 'CONTAINER_START_FAILED', HEALTH_TIMEOUT: 'HEALTH_TIMEOUT', diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts index 837335c8b0..d8c0e7855b 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts @@ -7,15 +7,15 @@ import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/st import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step'; import { RegisterAppOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step'; import { - StartWatchersOrchestratorStep, type FileBuiltEvent, + StartWatchersOrchestratorStep, } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step'; import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step'; import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; import { serializeError } from '@/cli/utilities/error/serialize-error'; import { emptyDir, ensureDir } from '@/cli/utilities/file/fs-utils'; import path from 'path'; -import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application'; +import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application'; export type DevModeOrchestratorOptions = { state: OrchestratorState; @@ -42,7 +42,7 @@ export class DevModeOrchestrator { private startWatchersStep: StartWatchersOrchestratorStep; constructor(options: DevModeOrchestratorOptions) { - this.debounceMs = options.debounceMs ?? 200; + this.debounceMs = options.debounceMs ?? 2_000; this.state = options.state; this.verbose = options.verbose ?? false; diff --git a/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts b/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts index 8a7df2d181..21b68436be 100644 --- a/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts +++ b/packages/twenty-sdk/src/cli/utilities/server/detect-local-server.ts @@ -1,4 +1,4 @@ -const LOCAL_PORTS = [2020, 3000]; +const LOCAL_PORTS = [2020]; export const checkServerHealth = async (port: number): Promise => { const controller = new AbortController(); diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index ae441dce57..640ffd546d 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -90,7 +90,7 @@ "axios": "^1.13.5", "axios-retry": "^4.5.0", "babel-plugin-module-resolver": "5.0.2", - "bcrypt": "5.1.1", + "bcrypt": "^6.0.0", "bullmq": "5.40.0", "bytes": "3.1.2", "cache-manager": "^5.4.0", diff --git a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts index 948a5dc18d..5e768891bd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts @@ -7,6 +7,7 @@ import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/applicat import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; +import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @@ -21,6 +22,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor TokenModule, FileStorageModule, PermissionsModule, + ThrottlerModule, ], providers: [ ApplicationDevelopmentResolver, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts index c1e88a1e7c..283ec3bc0d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts @@ -42,19 +42,21 @@ import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspac import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { DevelopmentGuard } from 'src/engine/guards/development.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; +const APP_DEV_RATE_LIMIT_MAX = 30; +const APP_DEV_RATE_LIMIT_WINDOW_MS = 30_000; + @UsePipes(ResolverValidationPipe) @MetadataResolver() @UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @UseFilters(ApplicationExceptionFilter) @UseGuards( WorkspaceAuthGuard, - DevelopmentGuard, SettingsPermissionGuard(PermissionFlagType.APPLICATIONS), ) export class ApplicationDevelopmentResolver { @@ -67,6 +69,7 @@ export class ApplicationDevelopmentResolver { private readonly fileStorageService: FileStorageService, private readonly sdkClientGenerationService: SdkClientGenerationService, private readonly twentyConfigService: TwentyConfigService, + private readonly throttlerService: ThrottlerService, ) {} @Mutation(() => DevelopmentApplicationDTO) @@ -74,6 +77,8 @@ export class ApplicationDevelopmentResolver { @Args() { universalIdentifier, name }: CreateDevelopmentApplicationInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { + await this.throttlePerApplication(universalIdentifier, workspaceId); + const applicationRegistrationId = await this.findApplicationRegistrationId(universalIdentifier); @@ -111,6 +116,8 @@ export class ApplicationDevelopmentResolver { @AuthUser({ allowUndefined: true }) user?: { id: string }, @AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string, ): Promise { + await this.throttlePerApplication(applicationId, workspaceId); + return this.applicationTokenService.generateApplicationTokenPair({ workspaceId, applicationId, @@ -124,6 +131,11 @@ export class ApplicationDevelopmentResolver { @Args() { manifest }: ApplicationInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { + await this.throttlePerApplication( + manifest.application.universalIdentifier, + workspaceId, + ); + const applicationRegistrationId = await this.findApplicationRegistrationId( manifest.application.universalIdentifier, ); @@ -187,6 +199,11 @@ export class ApplicationDevelopmentResolver { filePath, }: UploadApplicationFileInput, ): Promise { + await this.throttlePerApplication( + applicationUniversalIdentifier, + workspaceId, + ); + const allowedApplicationFileFolders: FileFolder[] = [ FileFolder.BuiltLogicFunction, FileFolder.BuiltFrontComponent, @@ -215,6 +232,18 @@ export class ApplicationDevelopmentResolver { }); } + private async throttlePerApplication( + applicationIdentifier: string, + workspaceId: string, + ): Promise { + await this.throttlerService.tokenBucketThrottleOrThrow( + `app-dev:${workspaceId}:${applicationIdentifier}`, + 1, + APP_DEV_RATE_LIMIT_MAX, + APP_DEV_RATE_LIMIT_WINDOW_MS, + ); + } + private async findApplicationRegistrationId( universalIdentifier: string, ): Promise { diff --git a/yarn.lock b/yarn.lock index 887aebcf5c..beb0b5c210 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9754,25 +9754,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/node-pre-gyp@npm:^1.0.11": - version: 1.0.11 - resolution: "@mapbox/node-pre-gyp@npm:1.0.11" - dependencies: - detect-libc: "npm:^2.0.0" - https-proxy-agent: "npm:^5.0.0" - make-dir: "npm:^3.1.0" - node-fetch: "npm:^2.6.7" - nopt: "npm:^5.0.0" - npmlog: "npm:^5.0.1" - rimraf: "npm:^3.0.2" - semver: "npm:^7.3.5" - tar: "npm:^6.1.11" - bin: - node-pre-gyp: bin/node-pre-gyp - checksum: 10c0/2b24b93c31beca1c91336fa3b3769fda98e202fb7f9771f0f4062588d36dcc30fcf8118c36aa747fa7f7610d8cf601872bdaaf62ce7822bb08b545d1bbe086cc - languageName: node - linkType: hard - "@mapbox/node-pre-gyp@npm:^2.0.0": version: 2.0.3 resolution: "@mapbox/node-pre-gyp@npm:2.0.3" @@ -26071,16 +26052,6 @@ __metadata: languageName: node linkType: hard -"are-we-there-yet@npm:^2.0.0": - version: 2.0.0 - resolution: "are-we-there-yet@npm:2.0.0" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^3.6.0" - checksum: 10c0/375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c - languageName: node - linkType: hard - "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -27147,13 +27118,14 @@ __metadata: languageName: node linkType: hard -"bcrypt@npm:5.1.1": - version: 5.1.1 - resolution: "bcrypt@npm:5.1.1" +"bcrypt@npm:^6.0.0": + version: 6.0.0 + resolution: "bcrypt@npm:6.0.0" dependencies: - "@mapbox/node-pre-gyp": "npm:^1.0.11" - node-addon-api: "npm:^5.0.0" - checksum: 10c0/743231158c866bddc46f25eb8e9617fe38bc1a6f5f3052aba35e361d349b7f8fb80e96b45c48a4c23c45c29967ccd11c81cf31166454fc0ab019801c336cab40 + node-addon-api: "npm:^8.3.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.8.4" + checksum: 10c0/006e69d4b3f0f021051fa9d998a768721a51dde28e7848e3e775ab608ba3476b25fa9262a7082ececbb0d493b0b6ca83673a6f3810b114fe90ec1bc6ae4b35b2 languageName: node linkType: hard @@ -29176,7 +29148,7 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.2, color-support@npm:^1.1.3": +"color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -29959,7 +29931,6 @@ __metadata: "@swc/core": "npm:^1.15.11" "@swc/jest": "npm:^0.2.39" "@types/fs-extra": "npm:^11.0.0" - "@types/inquirer": "npm:^9.0.0" "@types/jest": "npm:^30.0.0" "@types/lodash.camelcase": "npm:^4.3.7" "@types/lodash.kebabcase": "npm:^4.1.7" @@ -29968,7 +29939,6 @@ __metadata: chalk: "npm:^5.3.0" commander: "npm:^12.0.0" fs-extra: "npm:^11.2.0" - inquirer: "npm:^10.0.0" jest: "npm:29.7.0" jest-environment-node: "npm:^29.4.1" lodash.camelcase: "npm:^4.3.0" @@ -34581,23 +34551,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:^3.0.0": - version: 3.0.2 - resolution: "gauge@npm:3.0.2" - dependencies: - aproba: "npm:^1.0.3 || ^2.0.0" - color-support: "npm:^1.1.2" - console-control-strings: "npm:^1.0.0" - has-unicode: "npm:^2.0.1" - object-assign: "npm:^4.1.1" - signal-exit: "npm:^3.0.0" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wide-align: "npm:^1.1.2" - checksum: 10c0/75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913 - languageName: node - linkType: hard - "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -41309,7 +41262,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": +"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -43754,15 +43707,6 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^5.0.0": - version: 5.1.0 - resolution: "node-addon-api@npm:5.1.0" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d - languageName: node - linkType: hard - "node-addon-api@npm:^6.1.0": version: 6.1.0 resolution: "node-addon-api@npm:6.1.0" @@ -43781,6 +43725,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^8.3.0": + version: 8.7.0 + resolution: "node-addon-api@npm:8.7.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/31a03b00f6b0753ab08360952fdf80a1abb619dcf8125fa1ab07e3a414da050963440c3a86c77a0334c0be7a71acb5e242dc468b79201ee6151c7b943afe946d + languageName: node + linkType: hard + "node-api-version@npm:^0.2.0": version: 0.2.1 resolution: "node-api-version@npm:0.2.1" @@ -44328,18 +44281,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^5.0.1": - version: 5.0.1 - resolution: "npmlog@npm:5.0.1" - dependencies: - are-we-there-yet: "npm:^2.0.0" - console-control-strings: "npm:^1.1.0" - gauge: "npm:^3.0.0" - set-blocking: "npm:^2.0.0" - checksum: 10c0/489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa - languageName: node - linkType: hard - "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -54241,7 +54182,7 @@ __metadata: axios: "npm:^1.13.5" axios-retry: "npm:^4.5.0" babel-plugin-module-resolver: "npm:5.0.2" - bcrypt: "npm:5.1.1" + bcrypt: "npm:^6.0.0" bullmq: "npm:5.40.0" bytes: "npm:3.1.2" cache-manager: "npm:^5.4.0" @@ -57117,7 +57058,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.0, wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.0, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: