1574 extensibility twenty cli use workspace migration v2 to synchronize serverless function triggers (#14830)

twenty-cli serverless triggers follow up. Fixes:
- eventName don't support wildcard
- universalIdentifier not used to create or update trigger : update does
not work properly (does deletion then creation)
- add a base project in twenty-cli that is copied when creating a new
app
This commit is contained in:
martmull
2025-10-02 14:06:56 +02:00
committed by GitHub
parent 326d2a99ed
commit 0368d2a094
24 changed files with 1143 additions and 407 deletions
@@ -1,134 +0,0 @@
import {
createAgentManifest,
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', () => ({
randomUUID: jest.fn(() => 'mocked-uuid-12345'),
}));
describe('app-template', () => {
describe('createBasePackageJson', () => {
it('should create a valid app package.json with correct structure', () => {
const appName = 'my-test-app';
const description = 'A Twenty application for my-test-app';
const basePackageJson = createBasePackageJson(appName, description);
expect(basePackageJson).toEqual({
$schema: APP_MANIFEST_SCHEMA_URL,
universalIdentifier: 'mocked-uuid-12345',
label: 'My Test App',
description: 'A Twenty application for my-test-app',
version: '0.0.1',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
license: 'MIT',
});
});
it('should handle single word app names', () => {
const appName = 'calculator';
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('Calculator');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should handle kebab-case app names correctly', () => {
const appName = 'user-management-system';
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('User Management System');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should generate unique universalIdentifiers', () => {
const basePackageJson = createBasePackageJson('test-app', '');
expect(basePackageJson.universalIdentifier).toBeDefined();
expect(typeof basePackageJson.universalIdentifier).toBe('string');
});
});
describe('createAgentManifest', () => {
it('should create a valid agent manifest with correct structure', () => {
const appName = 'my-test-app';
const agent = createAgentManifest(appName);
expect(agent).toEqual({
$schema: AGENT_SCHEMA_URL,
standardId: 'mocked-uuid-12345',
name: 'myTestAppAgent',
label: 'My Test App Agent',
description: 'AI agent for my-test-app',
prompt:
'You are an AI agent for my-test-app. Help users with their tasks and provide assistance with Twenty CRM features.',
modelId: 'auto',
responseFormat: {
type: 'text',
},
});
});
it('should handle single word app names', () => {
const appName = 'calculator';
const agent = createAgentManifest(appName);
expect(agent.name).toBe('calculatorAgent');
expect(agent.label).toBe('Calculator Agent');
});
it('should handle kebab-case app names correctly', () => {
const appName = 'user-management-system';
const agent = createAgentManifest(appName);
expect(agent.name).toBe('userManagementSystemAgent');
expect(agent.label).toBe('User Management System Agent');
});
});
describe('createReadmeContent', () => {
it('should generate correct README content', () => {
const appName = 'my-awesome-app';
const appDir = '/path/to/my-awesome-app';
const readmeContent = createReadmeContent(appName, appDir);
expect(readmeContent).toContain('# my-awesome-app');
expect(readmeContent).toContain('A Twenty application.');
expect(readmeContent).toContain(
'twenty app dev --path /path/to/my-awesome-app',
);
expect(readmeContent).toContain('cd /path/to/my-awesome-app');
expect(readmeContent).toContain(
'twenty app deploy --path /path/to/my-awesome-app',
);
});
it('should include development and deployment sections', () => {
const readmeContent = createReadmeContent('test-app', '/test/path');
expect(readmeContent).toContain('## Development');
expect(readmeContent).toContain('## Deployment');
expect(readmeContent).toContain('To start development mode:');
expect(readmeContent).toContain('To deploy the application:');
});
it('should handle different app directories', () => {
const appName = 'sample-app';
const appDir = '/custom/directory/sample-app';
const readmeContent = createReadmeContent(appName, appDir);
expect(readmeContent).toContain('/custom/directory/sample-app');
});
});
});
+71 -76
View File
@@ -1,87 +1,82 @@
import { randomUUID } from 'crypto';
import { AgentManifest, PackageJson } from '../types/config.types';
import { getSchemaUrls } from './schema-validator';
import * as fs from 'fs-extra';
import { BASE_APPLICATION_PROJECT_PATH } from '../constants/base-application-project-path';
import { writeJsoncFile } from '../utils/jsonc-parser';
import { join } from 'path';
import path from 'path';
export const copyBaseApplicationProject = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
await fs.copy(BASE_APPLICATION_PROJECT_PATH, appDirectory);
await createBasePackageJson({
appName,
appDescription,
appDirectory,
});
await createReadmeContent({
appName,
appDescription,
appDirectory,
});
};
const createBasePackageJson = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
const base = JSON.parse(await readBaseApplicationProjectFile('package.json'));
export const createBasePackageJson = (
appName: string,
description: string,
): PackageJson => {
const schemas = getSchemaUrls();
return {
$schema: schemas.appManifest,
universalIdentifier: randomUUID(),
label: appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '),
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
description,
license: 'MIT',
version: '0.0.1',
};
base['$schema'] = schemas.appManifest;
base['universalIdentifier'] = randomUUID();
base['name'] = appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
base['description'] = appDescription;
await writeJsoncFile(join(appDirectory, 'package.json'), base);
};
export const createAgentManifest = (appName: string): AgentManifest => {
const schemas = getSchemaUrls();
const createReadmeContent = async ({
appName,
appDescription,
appDirectory,
}: {
appName: string;
appDescription: string;
appDirectory: string;
}) => {
let readmeContent = await readBaseApplicationProjectFile('README.md');
return {
$schema: schemas.agent,
standardId: randomUUID(),
name: `${appName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Agent`,
label: `${appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')} Agent`,
description: `AI agent for ${appName}`,
prompt: `You are an AI agent for ${appName}. Help users with their tasks and provide assistance with Twenty CRM features.`,
modelId: 'auto',
responseFormat: {
type: 'text',
},
};
readmeContent = readmeContent.replace(/\{title}/g, appName);
readmeContent = readmeContent.replace(/\{description}/g, appDescription);
readmeContent = readmeContent.replace(/\{appDir}/g, appDirectory);
await fs.writeFile(path.join(appDirectory, 'README.md'), readmeContent);
};
export const createGitignoreContent = () => {
return `node_modules
.yarn/install-state.gz
`;
};
export const createReadmeContent = (
appName: string,
appDir: string,
): string => {
return `# ${appName}
A Twenty application.
## Development
To start development mode:
\`\`\`bash
twenty app dev --path ${appDir}
\`\`\`
Or from the app directory:
\`\`\`bash
cd ${appDir}
twenty app dev
\`\`\`
## Deployment
To deploy the application:
\`\`\`bash
twenty app deploy --path ${appDir}
\`\`\`
`;
const readBaseApplicationProjectFile = async (fileName: string) => {
return await fs.readFile(
join(BASE_APPLICATION_PROJECT_PATH, fileName),
'utf-8',
);
};