diff --git a/packages/twenty-apps/hello-world/.gitignore b/packages/twenty-apps/hello-world/.gitignore new file mode 100644 index 0000000000..1cfcb0ac00 --- /dev/null +++ b/packages/twenty-apps/hello-world/.gitignore @@ -0,0 +1,2 @@ +node_modules +.yarn/install-state.gz diff --git a/packages/twenty-apps/hello-world/twenty-app.jsonc b/packages/twenty-apps/hello-world/package.json similarity index 66% rename from packages/twenty-apps/hello-world/twenty-app.jsonc rename to packages/twenty-apps/hello-world/package.json index 2927182660..e46dd9fc3b 100644 --- a/packages/twenty-apps/hello-world/twenty-app.jsonc +++ b/packages/twenty-apps/hello-world/package.json @@ -1,11 +1,8 @@ { "$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json", - // Hello World Twenty App - // A friendly AI companion that greets users and helps them get started with Twenty CRM "standardId": "550e8400-e29b-41d4-a716-446655440000", "label": "Hello World 👋", "description": "A friendly AI companion that greets users and helps them get started with Twenty CRM", "icon": "🌟", "version": "1.0.0" - // Agents are automatically discovered from the agents/ folder } diff --git a/packages/twenty-apps/hello-world/yarn.lock b/packages/twenty-apps/hello-world/yarn.lock new file mode 100644 index 0000000000..11cbfd1572 --- /dev/null +++ b/packages/twenty-apps/hello-world/yarn.lock @@ -0,0 +1,12 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + languageName: unknown + linkType: soft diff --git a/packages/twenty-cli/schemas/README.md b/packages/twenty-cli/schemas/README.md deleted file mode 100644 index 1477f53aae..0000000000 --- a/packages/twenty-cli/schemas/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Twenty JSON Schemas - -This directory contains JSON Schema definitions for Twenty application manifests and agent configurations. These schemas provide validation, autocomplete, and documentation for developers building Twenty applications. - -## Schema URLs - -The schemas are published at the following URLs for public access: - -- **App Manifest**: `https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json` -- **Agent**: `https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json` - -## IDE Support - -### VS Code - -VS Code automatically provides IntelliSense, validation, and hover documentation when you include the `$schema` property in your JSONC files: - -```jsonc -{ - "$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json", - "standardId": "550e8400-e29b-41d4-a716-446655440000", - "label": "My App", - // ... rest of your manifest -} -``` - -### Other IDEs - -Most modern IDEs that support JSON Schema will work with these schemas: -- IntelliJ IDEA / WebStorm -- Sublime Text (with LSP) -- Vim/Neovim (with LSP) -- Emacs (with LSP) - -## Schema Features - -### Validation - -The schemas provide comprehensive validation including: - -- **Required fields**: Ensures all mandatory properties are present -- **Type checking**: Validates data types (string, number, object, array) -- **Format validation**: UUID patterns, version formats, naming conventions -- **Enum constraints**: Restricts values to allowed options (e.g., model IDs) -- **Length constraints**: Minimum/maximum lengths for strings and arrays - -### Documentation - -Each property includes: -- Human-readable descriptions -- Usage examples -- Validation rules -- Default values where applicable - - -## CLI Integration - -The Twenty CLI automatically: - -1. **Validates** all manifests and agent files against these schemas -2. **Generates** new files with proper `$schema` references -3. **Provides** helpful error messages when validation fails - -## Development Workflow - -1. **Create** your app with `twenty app init my-app` -2. **Edit** the generated JSONC files with full IDE support -3. **Validate** automatically when running CLI commands -4. **Deploy** with confidence knowing your configuration is valid - -## Schema Versioning - -Schemas are versioned alongside the Twenty CLI. Breaking changes will: -- Be documented in release notes -- Include migration guides -- Maintain backward compatibility when possible - -## Contributing - -When updating schemas: - -1. Update the schema files in this directory -2. Test with real manifests and agents -3. Update examples and documentation -4. Ensure backward compatibility or provide migration path - -The schemas are automatically published to GitHub raw URLs when merged to main. diff --git a/packages/twenty-cli/schemas/app-manifest.schema.json b/packages/twenty-cli/schemas/app-manifest.schema.json index 93c02ab380..d8649aa332 100644 --- a/packages/twenty-cli/schemas/app-manifest.schema.json +++ b/packages/twenty-cli/schemas/app-manifest.schema.json @@ -36,6 +36,16 @@ "description": "Semantic version of the application", "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?$" }, + "dependencies": { + "type": "object", + "title": "The extension's source dependencies", + "description": "Source dependencies following the npm package.json dependency format." + }, + "devDependencies": { + "type": "object", + "title": "The extension's source devDependencies", + "description": "Dev dependencies following the npm package.json dependency format." + }, "agents": { "type": "array", "description": "Optional inline agent definitions (agents are typically discovered from the agents/ folder)", @@ -46,7 +56,6 @@ } } }, - "additionalProperties": false, "examples": [ { "standardId": "550e8400-e29b-41d4-a716-446655440000", diff --git a/packages/twenty-cli/src/commands/app-init.command.ts b/packages/twenty-cli/src/commands/app-init.command.ts index b42f4fc422..be4a85f61f 100644 --- a/packages/twenty-cli/src/commands/app-init.command.ts +++ b/packages/twenty-cli/src/commands/app-init.command.ts @@ -4,7 +4,8 @@ import inquirer from 'inquirer'; import * as path from 'path'; import { createAgentManifest, - createManifest, + createBasePackageJson, + createGitignoreContent, createReadmeContent, } from '../utils/app-template'; import { writeJsoncFile } from '../utils/jsonc-parser'; @@ -92,12 +93,12 @@ export class AppInitCommand { const agentsDir = path.join(appDir, 'agents'); await fs.ensureDir(agentsDir); - // Create main manifest with agent references - const manifest = createManifest(appName); - const manifestPath = path.join(appDir, 'twenty-app.jsonc'); - await writeJsoncFile(manifestPath, manifest); + // Create main basePackageJson with agent references + const basePackageJson = createBasePackageJson(appName); + const basePackageJsonPath = path.join(appDir, 'package.json'); + await writeJsoncFile(basePackageJsonPath, basePackageJson); - // Create agent manifest file + // Create agent basePackageJson file const agentManifest = createAgentManifest(appName); const agentFileName = `${appName}-agent`; const agentPath = path.join(agentsDir, `${agentFileName}.jsonc`); @@ -106,6 +107,13 @@ export class AppInitCommand { // 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 { diff --git a/packages/twenty-cli/src/types/config.types.ts b/packages/twenty-cli/src/types/config.types.ts index a89bdcf6bc..fef56dff16 100644 --- a/packages/twenty-cli/src/types/config.types.ts +++ b/packages/twenty-cli/src/types/config.types.ts @@ -10,6 +10,8 @@ export interface AppManifest { description?: string; icon?: string; version: string; + dependencies?: object; + devDependencies?: object; agents: AgentManifest[]; } 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 8563eba99f..1c3cc6330c 100644 --- a/packages/twenty-cli/src/utils/__tests__/app-template.test.ts +++ b/packages/twenty-cli/src/utils/__tests__/app-template.test.ts @@ -1,6 +1,6 @@ import { createAgentManifest, - createManifest, + createBasePackageJson, createReadmeContent, } from '../app-template'; @@ -10,43 +10,42 @@ jest.mock('crypto', () => ({ })); describe('app-template', () => { - describe('createManifest', () => { - it('should create a valid app manifest with correct structure', () => { + describe('createBasePackageJson', () => { + it('should create a valid app package.json with correct structure', () => { const appName = 'my-test-app'; - const manifest = createManifest(appName); + const basePackageJson = createBasePackageJson(appName); - expect(manifest).toEqual({ + expect(basePackageJson).toEqual({ $schema: 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json', standardId: 'mocked-uuid-12345', label: 'My Test App', description: 'A Twenty application for my-test-app', - version: '1.0.0', - // agents will be discovered from the agents/ folder + version: '0.0.1', }); }); it('should handle single word app names', () => { const appName = 'calculator'; - const manifest = createManifest(appName); + const basePackageJson = createBasePackageJson(appName); - expect(manifest.label).toBe('Calculator'); - expect(manifest.standardId).toBe('mocked-uuid-12345'); + expect(basePackageJson.label).toBe('Calculator'); + expect(basePackageJson.standardId).toBe('mocked-uuid-12345'); }); it('should handle kebab-case app names correctly', () => { const appName = 'user-management-system'; - const manifest = createManifest(appName); + const basePackageJson = createBasePackageJson(appName); - expect(manifest.label).toBe('User Management System'); - expect(manifest.standardId).toBe('mocked-uuid-12345'); + expect(basePackageJson.label).toBe('User Management System'); + expect(basePackageJson.standardId).toBe('mocked-uuid-12345'); }); it('should generate unique standardIds', () => { - const manifest = createManifest('test-app'); + const basePackageJson = createBasePackageJson('test-app'); - expect(manifest.standardId).toBeDefined(); - expect(typeof manifest.standardId).toBe('string'); + expect(basePackageJson.standardId).toBeDefined(); + expect(typeof basePackageJson.standardId).toBe('string'); }); }); diff --git a/packages/twenty-cli/src/utils/app-discovery.ts b/packages/twenty-cli/src/utils/app-discovery.ts index 553349201f..f44c07ff93 100644 --- a/packages/twenty-cli/src/utils/app-discovery.ts +++ b/packages/twenty-cli/src/utils/app-discovery.ts @@ -53,12 +53,12 @@ export const findNearbyApps = async (startDir: string): Promise => { for (const item of items) { if (item.isDirectory()) { - const manifestPath = path.join( + const packageJsonPath = path.join( searchPath, item.name, - 'twenty-app.json', + 'package.json', ); - if (await fs.pathExists(manifestPath)) { + if (await fs.pathExists(packageJsonPath)) { apps.push(path.join(searchPath, item.name)); } } @@ -73,6 +73,5 @@ export const findNearbyApps = async (startDir: string): Promise => { }; export const isValidAppPath = async (appPath: string): Promise => { - const manifestPath = path.join(appPath, 'twenty-app.json'); - return fs.pathExists(manifestPath); + return fs.pathExists(path.join(appPath, 'package.json')); }; diff --git a/packages/twenty-cli/src/utils/app-manifest-loader.ts b/packages/twenty-cli/src/utils/app-manifest-loader.ts index 6823ad4d15..0ac598ed47 100644 --- a/packages/twenty-cli/src/utils/app-manifest-loader.ts +++ b/packages/twenty-cli/src/utils/app-manifest-loader.ts @@ -24,31 +24,23 @@ export class AppManifestLoader { } async loadManifest(): Promise { - const manifestPath = await this.findManifestFile(); - const rawManifest = await parseJsoncFile(manifestPath); + const packageJsonPath = await this.findPackageJsonFile(); + const rawPackageJson = await parseJsoncFile(packageJsonPath); // Validate the raw manifest structure - await schemaValidator.validateAppManifest(rawManifest, manifestPath); + await schemaValidator.validateAppManifest(rawPackageJson, packageJsonPath); - return this.discoverAndLoadAgents(rawManifest, manifestPath); + return this.discoverAndLoadAgents(rawPackageJson, packageJsonPath); } - private async findManifestFile(): Promise { - // Try JSONC first, then fall back to JSON for backward compatibility - const jsoncPath = path.join(this.appPath, 'twenty-app.jsonc'); - const jsonPath = path.join(this.appPath, 'twenty-app.json'); - - if (await fs.pathExists(jsoncPath)) { - return jsoncPath; - } + private async findPackageJsonFile(): Promise { + const jsonPath = path.join(this.appPath, 'package.json'); if (await fs.pathExists(jsonPath)) { return jsonPath; } - throw new Error( - `No manifest file found. Expected twenty-app.jsonc or twenty-app.json in ${this.appPath}`, - ); + throw new Error(`package.json not found in ${this.appPath}`); } private async discoverAndLoadAgents( @@ -79,11 +71,7 @@ export class AppManifestLoader { } return { - standardId: rawManifest.standardId, - label: rawManifest.label, - description: rawManifest.description, - icon: rawManifest.icon, - version: rawManifest.version, + ...rawManifest, agents, _meta: { agentFiles, @@ -91,59 +79,6 @@ export class AppManifestLoader { }, }; } - - // Utility method to split agents from an existing manifest - static async splitAgentsFromManifest( - appPath: string, - options: { - agentsDir?: string; - preserveOriginal?: boolean; - } = {}, - ): Promise { - const loader = new AppManifestLoader(appPath); - const manifest = await loader.loadManifest(); - - const agentsDir = options.agentsDir || 'agents'; - const agentsDirPath = path.join(appPath, agentsDir); - - // Create agents directory - await fs.ensureDir(agentsDirPath); - - // Extract agents to separate files - for (const agent of manifest.agents) { - const agentFileName = `${agent.name}.jsonc`; - const agentFilePath = path.join(agentsDirPath, agentFileName); - - // Write agent to separate file - await fs.writeFile(agentFilePath, JSON.stringify(agent, null, 2), 'utf8'); - } - - // Update main manifest (remove agents array since they're now discovered) - const updatedManifest = { - standardId: manifest.standardId, - label: manifest.label, - description: manifest.description, - icon: manifest.icon, - version: manifest.version, - // No agents array - they will be discovered from the agents/ folder - }; - - // Write updated manifest as JSONC - const newManifestPath = path.join(appPath, 'twenty-app.jsonc'); - await fs.writeFile( - newManifestPath, - JSON.stringify(updatedManifest, null, 2), - 'utf8', - ); - - // Optionally remove original JSON file - if (!options.preserveOriginal) { - const oldManifestPath = path.join(appPath, 'twenty-app.json'); - if (await fs.pathExists(oldManifestPath)) { - await fs.remove(oldManifestPath); - } - } - } } // Convenience function for backward compatibility diff --git a/packages/twenty-cli/src/utils/app-path-resolver.ts b/packages/twenty-cli/src/utils/app-path-resolver.ts index 546ae92198..d9bfc97067 100644 --- a/packages/twenty-cli/src/utils/app-path-resolver.ts +++ b/packages/twenty-cli/src/utils/app-path-resolver.ts @@ -34,7 +34,7 @@ const resolveRelativePath = async (providedPath: string): Promise => { } } - throw new Error(`Cannot find twenty-app.json at any of these locations: + throw new Error(`Cannot find package.json at any of these locations: - ${fromCwd} - ${projectRoot ? path.resolve(projectRoot, providedPath) : 'N/A (no project root found)'} @@ -60,7 +60,7 @@ const autoDetectAppPath = async (): Promise => { const suggestions = await findNearbyApps(process.cwd()); let errorMessage = - 'No twenty-app.json found in current directory or parent directories.'; + 'No package.json found in current directory or parent directories.'; if (suggestions.length > 0) { errorMessage += '\n\nFound Twenty applications nearby:'; @@ -77,14 +77,12 @@ const autoDetectAppPath = async (): Promise => { }; const validateAppPath = async (appPath: string): Promise => { - const jsoncManifestPath = path.join(appPath, 'twenty-app.jsonc'); - const jsonManifestPath = path.join(appPath, 'twenty-app.json'); + const hasPackageJson = await fs.pathExists( + path.join(appPath, 'package.json'), + ); - const hasJsoncManifest = await fs.pathExists(jsoncManifestPath); - const hasJsonManifest = await fs.pathExists(jsonManifestPath); - - if (!hasJsoncManifest && !hasJsonManifest) { - let errorMessage = `No manifest file found. Expected twenty-app.jsonc or twenty-app.json in: ${appPath}`; + if (!hasPackageJson) { + let errorMessage = `package.json not found in: ${appPath}`; if (await fs.pathExists(appPath)) { try { diff --git a/packages/twenty-cli/src/utils/app-template.ts b/packages/twenty-cli/src/utils/app-template.ts index 32e5ce3dba..efe655be72 100644 --- a/packages/twenty-cli/src/utils/app-template.ts +++ b/packages/twenty-cli/src/utils/app-template.ts @@ -11,7 +11,7 @@ export type AgentManifestTemplate = AgentManifest & { $schema?: string; }; -export const createManifest = (appName: string): AppManifestTemplate => { +export const createBasePackageJson = (appName: string): AppManifestTemplate => { const schemas = SchemaValidator.getSchemaUrls(); return { @@ -22,8 +22,7 @@ export const createManifest = (appName: string): AppManifestTemplate => { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '), description: `A Twenty application for ${appName}`, - version: '1.0.0', - // agents will be discovered from the agents/ folder + version: '0.0.1', }; }; @@ -47,6 +46,12 @@ export const createAgentManifest = (appName: string): AgentManifestTemplate => { }; }; +export const createGitignoreContent = () => { + return `node_modules +.yarn/install-state.gz +`; +}; + export const createReadmeContent = ( appName: string, appDir: string,