Remove twenty-app.jsonc (#14662)
As title, use package.json instead of twenty-app.jsonc
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
-3
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user