Remove twenty-app.jsonc (#14662)

As title, use package.json instead of twenty-app.jsonc
This commit is contained in:
martmull
2025-09-23 16:54:24 +02:00
committed by GitHub
parent bbc97f4ab3
commit 5105b4284e
12 changed files with 82 additions and 203 deletions
@@ -0,0 +1,2 @@
node_modules
.yarn/install-state.gz
@@ -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
}
@@ -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
-87
View File
@@ -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.
@@ -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",
@@ -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 {
@@ -10,6 +10,8 @@ export interface AppManifest {
description?: string;
icon?: string;
version: string;
dependencies?: object;
devDependencies?: object;
agents: AgentManifest[];
}
@@ -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');
});
});
@@ -53,12 +53,12 @@ export const findNearbyApps = async (startDir: string): Promise<string[]> => {
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<string[]> => {
};
export const isValidAppPath = async (appPath: string): Promise<boolean> => {
const manifestPath = path.join(appPath, 'twenty-app.json');
return fs.pathExists(manifestPath);
return fs.pathExists(path.join(appPath, 'package.json'));
};
@@ -24,31 +24,23 @@ export class AppManifestLoader {
}
async loadManifest(): Promise<AppManifestWithMeta> {
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<string> {
// 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<string> {
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<void> {
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
@@ -34,7 +34,7 @@ const resolveRelativePath = async (providedPath: string): Promise<string> => {
}
}
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<string> => {
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<string> => {
};
const validateAppPath = async (appPath: string): Promise<string> => {
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 {
@@ -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,