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
@@ -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,