diff --git a/packages/twenty-cli/src/commands/app-add.command.ts b/packages/twenty-cli/src/commands/app-add.command.ts index 979d2b814c..236a6f1147 100644 --- a/packages/twenty-cli/src/commands/app-add.command.ts +++ b/packages/twenty-cli/src/commands/app-add.command.ts @@ -1,11 +1,11 @@ import chalk from 'chalk'; -import { resolveAppPath } from '../utils/app-path-resolver'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; -import { v4 } from 'uuid'; import path from 'path'; -import { getSchemaUrls } from '../utils/schema-validator'; +import { v4 } from 'uuid'; +import { resolveAppPath } from '../utils/app-path-resolver'; import { writeJsoncFile } from '../utils/jsonc-parser'; +import { getSchemaUrls } from '../utils/schema-validator'; type SyncableEntity = 'agent' | 'object'; @@ -22,11 +22,11 @@ const getFolderName = (entity: SyncableEntity) => { export class AppAddCommand { async execute(options: { path?: string }): Promise { - const entity = await this.getEntity(); - try { const appPath = await resolveAppPath(options.path); + const entity = await this.getEntity(); + const appExists = await fs.pathExists(appPath); if (!appExists) { @@ -49,7 +49,7 @@ export class AppAddCommand { await writeJsoncFile(entityPath, entityData); } catch (error) { console.error( - chalk.red(`Add new ${entity} failed:`), + chalk.red(`Add new entity failed:`), error instanceof Error ? error.message : error, ); process.exit(1); @@ -62,7 +62,6 @@ export class AppAddCommand { type: 'select', name: 'entity', message: `What entity do you want to create?`, - default: '', choices: ['agent', 'object'], }, ]); diff --git a/packages/twenty-cli/src/constants/schemas.ts b/packages/twenty-cli/src/constants/schemas.ts new file mode 100644 index 0000000000..b6d0c4a0f5 --- /dev/null +++ b/packages/twenty-cli/src/constants/schemas.ts @@ -0,0 +1,6 @@ +const SCHEMA_BASE_URL = + 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas'; + +export const APP_MANIFEST_SCHEMA_URL = `${SCHEMA_BASE_URL}/app-manifest.schema.json`; +export const AGENT_SCHEMA_URL = `${SCHEMA_BASE_URL}/agent.schema.json`; +export const OBJECT_SCHEMA_URL = `${SCHEMA_BASE_URL}/object.schema.json`; diff --git a/packages/twenty-cli/src/utils/__tests__/app-template.test.ts b/packages/twenty-cli/src/utils/__tests__/app-template.test.ts index e09943dacb..320d28aa42 100644 --- a/packages/twenty-cli/src/utils/__tests__/app-template.test.ts +++ b/packages/twenty-cli/src/utils/__tests__/app-template.test.ts @@ -3,6 +3,10 @@ import { 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', () => ({ @@ -17,8 +21,7 @@ describe('app-template', () => { const basePackageJson = createBasePackageJson(appName, description); expect(basePackageJson).toEqual({ - $schema: - 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json', + $schema: APP_MANIFEST_SCHEMA_URL, standardId: 'mocked-uuid-12345', label: 'My Test App', description: 'A Twenty application for my-test-app', @@ -62,8 +65,7 @@ describe('app-template', () => { const agent = createAgentManifest(appName); expect(agent).toEqual({ - $schema: - 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json', + $schema: AGENT_SCHEMA_URL, standardId: 'mocked-uuid-12345', name: 'myTestAppAgent', label: 'My Test App Agent', diff --git a/packages/twenty-cli/src/utils/app-discovery.ts b/packages/twenty-cli/src/utils/app-discovery.ts index f44c07ff93..763378d0d2 100644 --- a/packages/twenty-cli/src/utils/app-discovery.ts +++ b/packages/twenty-cli/src/utils/app-discovery.ts @@ -1,5 +1,6 @@ import * as fs from 'fs-extra'; import * as path from 'path'; +import { APP_MANIFEST_SCHEMA_URL } from '../constants/schemas'; export const findProjectRoot = async (): Promise => { let currentDir = process.cwd(); @@ -53,13 +54,9 @@ export const findNearbyApps = async (startDir: string): Promise => { for (const item of items) { if (item.isDirectory()) { - const packageJsonPath = path.join( - searchPath, - item.name, - 'package.json', - ); - if (await fs.pathExists(packageJsonPath)) { - apps.push(path.join(searchPath, item.name)); + const itemPath = path.join(searchPath, item.name); + if (await isValidAppPath(itemPath)) { + apps.push(itemPath); } } } @@ -73,5 +70,18 @@ export const findNearbyApps = async (startDir: string): Promise => { }; export const isValidAppPath = async (appPath: string): Promise => { - return fs.pathExists(path.join(appPath, 'package.json')); + const packageJsonPath = path.join(appPath, 'package.json'); + + if (!(await fs.pathExists(packageJsonPath))) { + return false; + } + + try { + const packageJson = await fs.readJson(packageJsonPath); + + // Check if this is a Twenty app by looking for the exact $schema URL + return packageJson.$schema === APP_MANIFEST_SCHEMA_URL; + } catch { + return false; + } }; diff --git a/packages/twenty-cli/src/utils/app-path-resolver.ts b/packages/twenty-cli/src/utils/app-path-resolver.ts index d9bfc97067..b226877d3d 100644 --- a/packages/twenty-cli/src/utils/app-path-resolver.ts +++ b/packages/twenty-cli/src/utils/app-path-resolver.ts @@ -34,10 +34,10 @@ const resolveRelativePath = async (providedPath: string): Promise => { } } - throw new Error(`Cannot find package.json at any of these locations: + throw new Error(`Cannot find Twenty app package.json at any of these locations: - ${fromCwd} - ${projectRoot ? path.resolve(projectRoot, providedPath) : 'N/A (no project root found)'} - + Please check the path or run from the correct directory.`); }; @@ -60,11 +60,11 @@ const autoDetectAppPath = async (): Promise => { const suggestions = await findNearbyApps(process.cwd()); let errorMessage = - 'No package.json found in current directory or parent directories.'; + 'No Twenty app found in current directory or parent directories.'; if (suggestions.length > 0) { errorMessage += '\n\nFound Twenty applications nearby:'; - suggestions.forEach((suggestion, i) => { + suggestions.forEach((suggestion: string, i: number) => { errorMessage += `\n ${i + 1}. ${suggestion}`; }); errorMessage += @@ -77,24 +77,23 @@ const autoDetectAppPath = async (): Promise => { }; const validateAppPath = async (appPath: string): Promise => { - const hasPackageJson = await fs.pathExists( - path.join(appPath, 'package.json'), - ); + if (!(await fs.pathExists(appPath))) { + throw new Error(`Directory does not exist: ${appPath}`); + } - if (!hasPackageJson) { - let errorMessage = `package.json not found in: ${appPath}`; + if (!(await isValidAppPath(appPath))) { + let errorMessage = `Not a valid Twenty app: ${appPath}`; - if (await fs.pathExists(appPath)) { - try { - const files = await fs.readdir(appPath); - errorMessage += `\n\nFiles in directory: ${files.join(', ')}`; - } catch { - errorMessage += '\n\nCould not read directory contents.'; - } + const packageJsonPath = path.join(appPath, 'package.json'); + if (await fs.pathExists(packageJsonPath)) { + errorMessage += + '\n\npackage.json found but missing required $schema field for Twenty apps.'; } else { - errorMessage += '\n\nDirectory does not exist.'; + errorMessage += '\n\npackage.json not found.'; } + errorMessage += '\n\nRun `twenty app init` to create a new application.'; + throw new Error(errorMessage); } diff --git a/packages/twenty-cli/src/utils/schema-validator.ts b/packages/twenty-cli/src/utils/schema-validator.ts index ef870931e5..042ddd3648 100644 --- a/packages/twenty-cli/src/utils/schema-validator.ts +++ b/packages/twenty-cli/src/utils/schema-validator.ts @@ -1,6 +1,11 @@ import Ajv from 'ajv'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { + AGENT_SCHEMA_URL, + APP_MANIFEST_SCHEMA_URL, + OBJECT_SCHEMA_URL, +} from '../constants/schemas'; export class SchemaValidationError extends Error { constructor( @@ -66,11 +71,8 @@ export const validateSchema = async ( export const getSchemaUrls = () => { return { - agent: - 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json', - object: - 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/object.schema.json', - appManifest: - 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json', + agent: AGENT_SCHEMA_URL, + object: OBJECT_SCHEMA_URL, + appManifest: APP_MANIFEST_SCHEMA_URL, }; }; diff --git a/packages/twenty-cli/tsconfig.json b/packages/twenty-cli/tsconfig.json index 9f0828f25c..18450eba6f 100644 --- a/packages/twenty-cli/tsconfig.json +++ b/packages/twenty-cli/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "./dist", "rootDir": "./src", - "module": "CommonJS", + "module": "commonjs", "target": "ES2022", "moduleResolution": "node", "esModuleInterop": true,