1574 extensibility twenty cli use workspace migration v2 to synchronize serverless function triggers (#14830)

twenty-cli serverless triggers follow up. Fixes:
- eventName don't support wildcard
- universalIdentifier not used to create or update trigger : update does
not work properly (does deletion then creation)
- add a base project in twenty-cli that is copied when creating a new
app
This commit is contained in:
martmull
2025-10-02 14:06:56 +02:00
committed by GitHub
parent 326d2a99ed
commit 0368d2a094
24 changed files with 1143 additions and 407 deletions
+4
View File
@@ -47,6 +47,10 @@ twenty auth logout
# Initialize a new application
twenty app init [name]
# Add a new core entity to your application
twenty app add [options]
-p, --path <path> Application directory path (default: current directory)
# Start development mode with file watching
twenty app dev [options]
-p, --path <path> Application directory path (default: current directory)
+1 -1
View File
@@ -32,7 +32,7 @@ const jestConfig = {
global: {
statements: 2,
lines: 1,
functions: 5,
functions: 1,
},
},
};
+3 -4
View File
@@ -7,7 +7,7 @@
"twenty": "dist/cli.js"
},
"scripts": {
"build": "tsc",
"build": "echo 'use npx nx build'",
"dev": "tsx src/cli.ts",
"start": "node dist/cli.js"
},
@@ -30,14 +30,13 @@
"fs-extra": "^11.2.0",
"inquirer": "^10.0.0",
"jsonc-parser": "^3.2.0",
"ora": "^8.0.0",
"yaml": "^2.4.0",
"zod": "^4.1.11"
"lodash.kebabcase": "^4.1.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^29.5.0",
"@types/lodash.kebabcase": "^4.1.7",
"@types/node": "^20.0.0",
"jest": "^29.5.0",
"tsx": "^4.7.0",
+12 -2
View File
@@ -4,17 +4,27 @@
"projectType": "application",
"tags": ["scope:cli"],
"targets": {
"build": {
"after-build": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"cwd": "packages/twenty-cli",
"commands": ["rimraf dist", "tsc"]
},
"dependsOn": ["^build"]
"dependsOn": ["^after-build"]
},
"build": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"cwd": "packages/twenty-cli",
"commands": ["cp -R src/constants/base-application-project dist/constants"]
},
"dependsOn": ["after-build"]
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-cli",
"command": "tsx src/cli.ts"
@@ -4,7 +4,7 @@
"title": "Twenty App Manifest",
"description": "Schema for Twenty application manifest files",
"type": "object",
"required": ["universalIdentifier", "label", "version", "license", "engines", "packageManager"],
"required": ["universalIdentifier", "name", "version", "license", "engines", "packageManager"],
"properties": {
"$schema": {
"type": "string",
@@ -15,7 +15,7 @@
"description": "Unique identifier (UUID format recommended)",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
},
"label": {
"name": {
"type": "string",
"description": "Human-readable display name for the application",
"minLength": 1,
@@ -90,7 +90,7 @@
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
"yarn": ">=4.9.2"
}
}
]
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import path from 'path';
import { v4 } from 'uuid';
import { randomUUID } from 'crypto';
import { resolveAppPath } from '../utils/app-path-resolver';
import { parseJsoncFile, writeJsoncFile } from '../utils/jsonc-parser';
import { getSchemaUrls } from '../utils/schema-validator';
@@ -141,7 +141,7 @@ export class AppAddCommand {
) {
const schemas = getSchemaUrls();
const uuid = v4();
const uuid = randomUUID();
const entityToCreateData: Record<string, string> = {
$schema: schemas[entity],
@@ -255,7 +255,7 @@ export class AppAddCommand {
}
private async createDatabaseEventTrigger() {
const uuid = v4();
const uuid = randomUUID();
const { eventName } = await inquirer.prompt([
{
@@ -266,8 +266,8 @@ export class AppAddCommand {
if (input.length === 0) {
return 'Event name is required';
}
if (!/^[a-zA-Z]+\.(created|updated|deleted)$/.test(input)) {
return 'Event name must be in format: objectName.(created|updated|deleted)';
if (!/^(?:[a-zA-Z]+|\*)\.(created|updated|deleted|\*)$/.test(input)) {
return 'Event name must be in format: (objectName|*).(created|updated|deleted|*)';
}
return true;
},
@@ -282,7 +282,7 @@ export class AppAddCommand {
}
private async createCronTrigger() {
const uuid = v4();
const uuid = randomUUID();
const { schedule } = await inquirer.prompt([
{
@@ -2,27 +2,28 @@ import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import * as path from 'path';
import {
createBasePackageJson,
createGitignoreContent,
createReadmeContent,
} from '../utils/app-template';
import { writeJsoncFile } from '../utils/jsonc-parser';
import { copyBaseApplicationProject } from '../utils/app-template';
import kebabCase from 'lodash.kebabcase';
export class AppInitCommand {
async execute(options: { path?: string; name?: string }): Promise<void> {
async execute(options: { path?: string }): Promise<void> {
try {
const { name, description } = await this.getAppInfos(options.name);
const { appName, appDirectory, appDescription } =
await this.getAppInfos(options);
const appDir = this.determineAppDirectory(name, options.path);
await this.validateDirectory(appDirectory);
await this.validateDirectory(appDir);
this.logCreationInfo({ appDirectory, appName });
this.logCreationInfo(appDir, name);
await fs.ensureDir(appDirectory);
await this.createAppStructure(appDir, name, description);
await copyBaseApplicationProject({
appName,
appDescription,
appDirectory,
});
this.logSuccess(appDir);
this.logSuccess(appDirectory);
} catch (error) {
console.error(
chalk.red('Initialization failed:'),
@@ -32,22 +33,18 @@ export class AppInitCommand {
}
}
private async getAppInfos(
providedName?: string,
): Promise<{ name: string; description: string }> {
if (providedName) {
return { name: providedName, description: '' };
}
return inquirer.prompt([
private async getAppInfos(options: { path?: string }): Promise<{
appName: string;
appDirectory: string;
appDescription: string;
}> {
const { name, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
message: 'Application name (eg: My awesome application):',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
if (!/^[a-z0-9-]+$/.test(input))
return 'Name must contain only lowercase letters, numbers, and hyphens';
return true;
},
},
@@ -58,66 +55,49 @@ export class AppInitCommand {
default: '',
},
]);
const appName = name.trim();
const appDescription = description.trim();
const appDirectory = options.path
? path.resolve(options.path, kebabCase(appName))
: path.join(process.cwd(), kebabCase(appName)!);
return { appName, appDirectory, appDescription };
}
private determineAppDirectory(
appName: string,
providedPath?: string,
): string {
if (providedPath) {
return path.resolve(providedPath, appName);
}
return path.join(process.cwd(), appName!);
}
private async validateDirectory(appDir: string): Promise<void> {
if (!(await fs.pathExists(appDir))) {
private async validateDirectory(appDirectory: string): Promise<void> {
if (!(await fs.pathExists(appDirectory))) {
return;
}
const files = await fs.readdir(appDir);
const files = await fs.readdir(appDirectory);
if (files.length > 0) {
throw new Error(`Directory ${appDir} already exists and is not empty`);
throw new Error(
`Directory ${appDirectory} already exists and is not empty`,
);
}
}
private logCreationInfo(appDir: string, appName: string): void {
private logCreationInfo({
appDirectory,
appName,
}: {
appDirectory: string;
appName: string;
}): void {
console.log(chalk.blue('🎯 Creating Twenty Application'));
console.log(chalk.gray(`📁 Directory: ${appDir}`));
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
console.log(chalk.gray(`📝 Name: ${appName}`));
console.log('');
}
private async createAppStructure(
appDir: string,
appName: string,
description: string,
): Promise<void> {
await fs.ensureDir(appDir);
// Create main basePackageJson with agent references
const basePackageJson = createBasePackageJson(appName, description);
const basePackageJsonPath = path.join(appDir, 'package.json');
await writeJsoncFile(basePackageJsonPath, basePackageJson);
// Create README
const readmeContent = createReadmeContent(appName, appDir);
await fs.writeFile(path.join(appDir, 'README.md'), readmeContent);
// Create empty yarn.lock
await fs.writeFile(path.join(appDir, 'yarn.lock'), '');
// Create .gitignore
const gitignoreContent = createGitignoreContent();
await fs.writeFile(path.join(appDir, '.gitignore'), gitignoreContent);
}
private logSuccess(appDir: string): void {
private logSuccess(appDirectory: string): void {
console.log(chalk.green('✅ Application created successfully!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(` cd ${appDir}`);
console.log(` cd ${appDirectory}`);
console.log(' twenty app dev');
}
}
@@ -41,7 +41,6 @@ export class AppCommand {
.command('init')
.description('Initialize a new Twenty application')
.option('-p, --path <path>', 'Directory to create the application in')
.option('-n, --name <name>', 'Application name')
.action(async (options) => {
await this.initCommand.execute(options);
});
@@ -0,0 +1,6 @@
import { join } from 'path';
export const BASE_APPLICATION_PROJECT_PATH = join(
__dirname,
'../constants/base-application-project',
);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
yarnPath: .yarn/releases/yarn-4.9.2.cjs
nodeLinker: node-modules
@@ -0,0 +1,15 @@
# {title}
{description}
## Development
```bash
twenty app dev --path {appDir}
```
### Deployment
```bash
twenty app sync --path {appDir}
```
@@ -0,0 +1,10 @@
{
"version": "0.0.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "[email protected]"
}
@@ -1,134 +0,0 @@
import {
createAgentManifest,
createBasePackageJson,
createReadmeContent,
} from '../app-template';
import {
AGENT_SCHEMA_URL,
APP_MANIFEST_SCHEMA_URL,
} from '../../constants/schemas';
// Mock crypto.randomUUID to make tests deterministic
jest.mock('crypto', () => ({
randomUUID: jest.fn(() => 'mocked-uuid-12345'),
}));
describe('app-template', () => {
describe('createBasePackageJson', () => {
it('should create a valid app package.json with correct structure', () => {
const appName = 'my-test-app';
const description = 'A Twenty application for my-test-app';
const basePackageJson = createBasePackageJson(appName, description);
expect(basePackageJson).toEqual({
$schema: APP_MANIFEST_SCHEMA_URL,
universalIdentifier: 'mocked-uuid-12345',
label: 'My Test App',
description: 'A Twenty application for my-test-app',
version: '0.0.1',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: '[email protected]',
license: 'MIT',
});
});
it('should handle single word app names', () => {
const appName = 'calculator';
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('Calculator');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should handle kebab-case app names correctly', () => {
const appName = 'user-management-system';
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('User Management System');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should generate unique universalIdentifiers', () => {
const basePackageJson = createBasePackageJson('test-app', '');
expect(basePackageJson.universalIdentifier).toBeDefined();
expect(typeof basePackageJson.universalIdentifier).toBe('string');
});
});
describe('createAgentManifest', () => {
it('should create a valid agent manifest with correct structure', () => {
const appName = 'my-test-app';
const agent = createAgentManifest(appName);
expect(agent).toEqual({
$schema: AGENT_SCHEMA_URL,
standardId: 'mocked-uuid-12345',
name: 'myTestAppAgent',
label: 'My Test App Agent',
description: 'AI agent for my-test-app',
prompt:
'You are an AI agent for my-test-app. Help users with their tasks and provide assistance with Twenty CRM features.',
modelId: 'auto',
responseFormat: {
type: 'text',
},
});
});
it('should handle single word app names', () => {
const appName = 'calculator';
const agent = createAgentManifest(appName);
expect(agent.name).toBe('calculatorAgent');
expect(agent.label).toBe('Calculator Agent');
});
it('should handle kebab-case app names correctly', () => {
const appName = 'user-management-system';
const agent = createAgentManifest(appName);
expect(agent.name).toBe('userManagementSystemAgent');
expect(agent.label).toBe('User Management System Agent');
});
});
describe('createReadmeContent', () => {
it('should generate correct README content', () => {
const appName = 'my-awesome-app';
const appDir = '/path/to/my-awesome-app';
const readmeContent = createReadmeContent(appName, appDir);
expect(readmeContent).toContain('# my-awesome-app');
expect(readmeContent).toContain('A Twenty application.');
expect(readmeContent).toContain(
'twenty app dev --path /path/to/my-awesome-app',
);
expect(readmeContent).toContain('cd /path/to/my-awesome-app');
expect(readmeContent).toContain(
'twenty app deploy --path /path/to/my-awesome-app',
);
});
it('should include development and deployment sections', () => {
const readmeContent = createReadmeContent('test-app', '/test/path');
expect(readmeContent).toContain('## Development');
expect(readmeContent).toContain('## Deployment');
expect(readmeContent).toContain('To start development mode:');
expect(readmeContent).toContain('To deploy the application:');
});
it('should handle different app directories', () => {
const appName = 'sample-app';
const appDir = '/custom/directory/sample-app';
const readmeContent = createReadmeContent(appName, appDir);
expect(readmeContent).toContain('/custom/directory/sample-app');
});
});
});
+71 -76
View File
@@ -1,87 +1,82 @@
import { randomUUID } from 'crypto';
import { AgentManifest, PackageJson } from '../types/config.types';
import { getSchemaUrls } from './schema-validator';
import * as fs from 'fs-extra';
import { BASE_APPLICATION_PROJECT_PATH } from '../constants/base-application-project-path';
import { writeJsoncFile } from '../utils/jsonc-parser';
import { join } from 'path';
import path from 'path';
export const copyBaseApplicationProject = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
await fs.copy(BASE_APPLICATION_PROJECT_PATH, appDirectory);
await createBasePackageJson({
appName,
appDescription,
appDirectory,
});
await createReadmeContent({
appName,
appDescription,
appDirectory,
});
};
const createBasePackageJson = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
const base = JSON.parse(await readBaseApplicationProjectFile('package.json'));
export const createBasePackageJson = (
appName: string,
description: string,
): PackageJson => {
const schemas = getSchemaUrls();
return {
$schema: schemas.appManifest,
universalIdentifier: randomUUID(),
label: appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '),
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: '[email protected]',
description,
license: 'MIT',
version: '0.0.1',
};
base['$schema'] = schemas.appManifest;
base['universalIdentifier'] = randomUUID();
base['name'] = appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
base['description'] = appDescription;
await writeJsoncFile(join(appDirectory, 'package.json'), base);
};
export const createAgentManifest = (appName: string): AgentManifest => {
const schemas = getSchemaUrls();
const createReadmeContent = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
let readmeContent = await readBaseApplicationProjectFile('README.md');
return {
$schema: schemas.agent,
standardId: randomUUID(),
name: `${appName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Agent`,
label: `${appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')} Agent`,
description: `AI agent for ${appName}`,
prompt: `You are an AI agent for ${appName}. Help users with their tasks and provide assistance with Twenty CRM features.`,
modelId: 'auto',
responseFormat: {
type: 'text',
},
};
readmeContent = readmeContent.replace(/\{title}/g, appName);
readmeContent = readmeContent.replace(/\{description}/g, appDescription);
readmeContent = readmeContent.replace(/\{appDir}/g, appDirectory);
await fs.writeFile(path.join(appDirectory, 'README.md'), readmeContent);
};
export const createGitignoreContent = () => {
return `node_modules
.yarn/install-state.gz
`;
};
export const createReadmeContent = (
appName: string,
appDir: string,
): string => {
return `# ${appName}
A Twenty application.
## Development
To start development mode:
\`\`\`bash
twenty app dev --path ${appDir}
\`\`\`
Or from the app directory:
\`\`\`bash
cd ${appDir}
twenty app dev
\`\`\`
## Deployment
To deploy the application:
\`\`\`bash
twenty app deploy --path ${appDir}
\`\`\`
`;
const readBaseApplicationProjectFile = async (fileName: string) => {
return await fs.readFile(
join(BASE_APPLICATION_PROJECT_PATH, fileName),
'utf-8',
);
};
@@ -524,6 +524,7 @@ export class ApplicationSyncService {
settings: {
eventName: triggerToCreate.eventName,
},
universalIdentifier: triggerToCreate.universalIdentifier,
serverlessFunctionId,
};
@@ -634,6 +635,7 @@ export class ApplicationSyncService {
settings: {
pattern: triggerToCreate.schedule,
},
universalIdentifier: triggerToCreate.universalIdentifier,
serverlessFunctionId,
};
@@ -1,4 +1,4 @@
import { Field, InputType } from '@nestjs/graphql';
import { Field, HideField, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
@@ -16,4 +16,7 @@ export class CreateCronTriggerInput {
@IsNotEmpty()
@Field()
serverlessFunctionId: string;
@HideField()
universalIdentifier?: string;
}
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
import { v4 } from 'uuid';
import { type CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/create-cron-trigger.input';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
@@ -13,8 +13,8 @@ export const fromCreateCronTriggerInputToFlatCronTrigger = ({
const now = new Date();
return {
id: uuidv4(),
universalIdentifier: uuidv4(),
id: v4(),
universalIdentifier: createCronTriggerInput.universalIdentifier ?? v4(),
settings: createCronTriggerInput.settings,
serverlessFunctionId: createCronTriggerInput.serverlessFunctionId,
workspaceId,
@@ -1,4 +1,4 @@
import { Field, InputType } from '@nestjs/graphql';
import { Field, HideField, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
@@ -16,4 +16,7 @@ export class CreateDatabaseEventTriggerInput {
@IsNotEmpty()
@Field()
serverlessFunctionId: string;
@HideField()
universalIdentifier?: string;
}
@@ -14,7 +14,8 @@ export const fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger = ({
return {
id: uuidV4(),
universalIdentifier: uuidV4(),
universalIdentifier:
createDatabaseEventTriggerInput.universalIdentifier ?? uuidV4(),
settings: createDatabaseEventTriggerInput.settings,
serverlessFunctionId: createDatabaseEventTriggerInput.serverlessFunctionId,
workspaceId,
+5 -107
View File
@@ -27840,15 +27840,6 @@ __metadata:
languageName: node
linkType: hard
"cli-cursor@npm:^5.0.0":
version: 5.0.0
resolution: "cli-cursor@npm:5.0.0"
dependencies:
restore-cursor: "npm:^5.0.0"
checksum: 10c0/7ec62f69b79f6734ab209a3e4dbdc8af7422d44d360a7cb1efa8a0887bbe466a6e625650c466fe4359aee44dbe2dc0b6994b583d40a05d0808a5cb193641d220
languageName: node
linkType: hard
"cli-highlight@npm:^2.1.11":
version: 2.1.11
resolution: "cli-highlight@npm:2.1.11"
@@ -27881,7 +27872,7 @@ __metadata:
languageName: node
linkType: hard
"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2":
"cli-spinners@npm:^2.5.0":
version: 2.9.2
resolution: "cli-spinners@npm:2.9.2"
checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3
@@ -34157,13 +34148,6 @@ __metadata:
languageName: node
linkType: hard
"get-east-asian-width@npm:^1.0.0":
version: 1.4.0
resolution: "get-east-asian-width@npm:1.4.0"
checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83
languageName: node
linkType: hard
"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2":
version: 2.0.2
resolution: "get-func-name@npm:2.0.2"
@@ -37083,13 +37067,6 @@ __metadata:
languageName: node
linkType: hard
"is-interactive@npm:^2.0.0":
version: 2.0.0
resolution: "is-interactive@npm:2.0.0"
checksum: 10c0/801c8f6064f85199dc6bf99b5dd98db3282e930c3bc197b32f2c5b89313bb578a07d1b8a01365c4348c2927229234f3681eb861b9c2c92bee72ff397390fa600
languageName: node
linkType: hard
"is-lambda@npm:^1.0.1":
version: 1.0.1
resolution: "is-lambda@npm:1.0.1"
@@ -37412,20 +37389,13 @@ __metadata:
languageName: node
linkType: hard
"is-unicode-supported@npm:^1.2.0, is-unicode-supported@npm:^1.3.0":
"is-unicode-supported@npm:^1.2.0":
version: 1.3.0
resolution: "is-unicode-supported@npm:1.3.0"
checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a
languageName: node
linkType: hard
"is-unicode-supported@npm:^2.0.0":
version: 2.1.0
resolution: "is-unicode-supported@npm:2.1.0"
checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5
languageName: node
linkType: hard
"is-upper-case@npm:^2.0.2":
version: 2.0.2
resolution: "is-upper-case@npm:2.0.2"
@@ -40214,16 +40184,6 @@ __metadata:
languageName: node
linkType: hard
"log-symbols@npm:^6.0.0":
version: 6.0.0
resolution: "log-symbols@npm:6.0.0"
dependencies:
chalk: "npm:^5.3.0"
is-unicode-supported: "npm:^1.3.0"
checksum: 10c0/36636cacedba8f067d2deb4aad44e91a89d9efb3ead27e1846e7b82c9a10ea2e3a7bd6ce28a7ca616bebc60954ff25c67b0f92d20a6a746bb3cc52c3701891f6
languageName: node
linkType: hard
"log-update@npm:^4.0.0":
version: 4.0.0
resolution: "log-update@npm:4.0.0"
@@ -42505,13 +42465,6 @@ __metadata:
languageName: node
linkType: hard
"mimic-function@npm:^5.0.0":
version: 5.0.1
resolution: "mimic-function@npm:5.0.1"
checksum: 10c0/f3d9464dd1816ecf6bdf2aec6ba32c0728022039d992f178237d8e289b48764fee4131319e72eedd4f7f094e22ded0af836c3187a7edc4595d28dd74368fd81d
languageName: node
linkType: hard
"mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1":
version: 1.0.1
resolution: "mimic-response@npm:1.0.1"
@@ -44419,15 +44372,6 @@ __metadata:
languageName: node
linkType: hard
"onetime@npm:^7.0.0":
version: 7.0.0
resolution: "onetime@npm:7.0.0"
dependencies:
mimic-function: "npm:^5.0.0"
checksum: 10c0/5cb9179d74b63f52a196a2e7037ba2b9a893245a5532d3f44360012005c9cadb60851d56716ebff18a6f47129dab7168022445df47c2aff3b276d92585ed1221
languageName: node
linkType: hard
"only@npm:~0.0.2":
version: 0.0.2
resolution: "only@npm:0.0.2"
@@ -44569,23 +44513,6 @@ __metadata:
languageName: node
linkType: hard
"ora@npm:^8.0.0":
version: 8.2.0
resolution: "ora@npm:8.2.0"
dependencies:
chalk: "npm:^5.3.0"
cli-cursor: "npm:^5.0.0"
cli-spinners: "npm:^2.9.2"
is-interactive: "npm:^2.0.0"
is-unicode-supported: "npm:^2.0.0"
log-symbols: "npm:^6.0.0"
stdin-discarder: "npm:^0.2.2"
string-width: "npm:^7.2.0"
strip-ansi: "npm:^7.1.0"
checksum: 10c0/7d9291255db22e293ea164f520b6042a3e906576ab06c9cf408bf9ef5664ba0a9f3bd258baa4ada058cfcc2163ef9b6696d51237a866682ce33295349ba02c3a
languageName: node
linkType: hard
"orderedmap@npm:^2.0.0":
version: 2.1.1
resolution: "orderedmap@npm:2.1.1"
@@ -48722,16 +48649,6 @@ __metadata:
languageName: node
linkType: hard
"restore-cursor@npm:^5.0.0":
version: 5.1.0
resolution: "restore-cursor@npm:5.1.0"
dependencies:
onetime: "npm:^7.0.0"
signal-exit: "npm:^4.1.0"
checksum: 10c0/c2ba89131eea791d1b25205bdfdc86699767e2b88dee2a590b1a6caa51737deac8bad0260a5ded2f7c074b7db2f3a626bcf1fcf3cdf35974cbeea5e2e6764f60
languageName: node
linkType: hard
"restructure@npm:^3.0.0":
version: 3.0.2
resolution: "restructure@npm:3.0.2"
@@ -50476,13 +50393,6 @@ __metadata:
languageName: node
linkType: hard
"stdin-discarder@npm:^0.2.2":
version: 0.2.2
resolution: "stdin-discarder@npm:0.2.2"
checksum: 10c0/c78375e82e956d7a64be6e63c809c7f058f5303efcaf62ea48350af072bacdb99c06cba39209b45a071c1acbd49116af30df1df9abb448df78a6005b72f10537
languageName: node
linkType: hard
"stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0":
version: 1.1.0
resolution: "stop-iteration-iterator@npm:1.1.0"
@@ -50737,17 +50647,6 @@ __metadata:
languageName: node
linkType: hard
"string-width@npm:^7.2.0":
version: 7.2.0
resolution: "string-width@npm:7.2.0"
dependencies:
emoji-regex: "npm:^10.3.0"
get-east-asian-width: "npm:^1.0.0"
strip-ansi: "npm:^7.1.0"
checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9
languageName: node
linkType: hard
"string.prototype.includes@npm:^2.0.1":
version: 2.0.1
resolution: "string.prototype.includes@npm:2.0.1"
@@ -52435,6 +52334,7 @@ __metadata:
"@types/fs-extra": "npm:^11.0.0"
"@types/inquirer": "npm:^9.0.0"
"@types/jest": "npm:^29.5.0"
"@types/lodash.kebabcase": "npm:^4.1.7"
"@types/node": "npm:^20.0.0"
ajv: "npm:^8.12.0"
ajv-formats: "npm:^2.1.1"
@@ -52447,11 +52347,9 @@ __metadata:
inquirer: "npm:^10.0.0"
jest: "npm:^29.5.0"
jsonc-parser: "npm:^3.2.0"
ora: "npm:^8.0.0"
lodash.kebabcase: "npm:^4.1.1"
tsx: "npm:^4.7.0"
typescript: "npm:^5.3.0"
yaml: "npm:^2.4.0"
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.js
languageName: unknown
@@ -55947,7 +55845,7 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.4.0, yaml@npm:^2.4.5, yaml@npm:^2.6.0":
"yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.4.5, yaml@npm:^2.6.0":
version: 2.8.1
resolution: "yaml@npm:2.8.1"
bin: