Support Skill in manifest (#18092)

# Introduction
Support skill in manifest, pre-requisite for the twenty standard app
migration
This commit is contained in:
Paul Rastoin
2026-02-20 11:45:42 +01:00
committed by GitHub
parent 6ad581d178
commit 175df59c21
29 changed files with 402 additions and 11 deletions
@@ -18,6 +18,7 @@ export const EXPECTED_MANIFEST: Manifest = {
fileType: 'png',
},
],
skills: [],
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
@@ -13,6 +13,7 @@ export const EXPECTED_MANIFEST: Manifest = {
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
apiClientChecksum: null,
},
skills: [],
publicAssets: [],
fields: [],
objects: [
@@ -6,6 +6,7 @@ import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import { getSkillBaseFile } from '@/cli/utilities/entity/entity-skill-template';
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
@@ -116,6 +117,16 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.Skill: {
const name = await this.getEntityName(entity);
const file = getSkillBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.View: {
const entityData = await this.getViewData();
@@ -32,6 +32,7 @@ const validManifest: Manifest = {
fields: [],
logicFunctions: [],
roles: [],
skills: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
@@ -29,6 +29,7 @@ import {
type ObjectManifest,
type PageLayoutManifest,
type RoleManifest,
type SkillManifest,
type ViewManifest,
} from 'twenty-shared/application';
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
@@ -64,6 +65,7 @@ export const buildManifest = async (
const objects: ObjectManifest[] = [];
const fields: FieldManifest[] = [];
const roles: RoleManifest[] = [];
const skills: SkillManifest[] = [];
const logicFunctions: LogicFunctionManifest[] = [];
const frontComponents: FrontComponentManifest[] = [];
const publicAssets: AssetManifest[] = [];
@@ -75,6 +77,7 @@ export const buildManifest = async (
const objectsFilePaths: string[] = [];
const fieldsFilePaths: string[] = [];
const rolesFilePaths: string[] = [];
const skillsFilePaths: string[] = [];
const logicFunctionsFilePaths: string[] = [];
const frontComponentsFilePaths: string[] = [];
const publicAssetsFilePaths: string[] = [];
@@ -165,6 +168,16 @@ export const buildManifest = async (
rolesFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Skills: {
const extract = await extractManifestFromFile<SkillManifest>({
appPath,
filePath,
});
skills.push(extract.config);
errors.push(...extract.errors);
skillsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.LogicFunctions: {
const extract = await extractManifestFromFile<LogicFunctionConfig>({
appPath,
@@ -295,6 +308,7 @@ export const buildManifest = async (
objects,
fields,
roles,
skills,
logicFunctions,
frontComponents,
publicAssets,
@@ -308,6 +322,7 @@ export const buildManifest = async (
objects: objectsFilePaths,
fields: fieldsFilePaths,
roles: rolesFilePaths,
skills: skillsFilePaths,
logicFunctions: logicFunctionsFilePaths,
frontComponents: frontComponentsFilePaths,
publicAssets: publicAssetsFilePaths,
@@ -6,6 +6,7 @@ export enum TargetFunction {
DefineLogicFunction = 'defineLogicFunction',
DefineObject = 'defineObject',
DefineRole = 'defineRole',
DefineSkill = 'defineSkill',
DefineFrontComponent = 'defineFrontComponent',
DefineView = 'defineView',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
@@ -18,6 +19,7 @@ export enum ManifestEntityKey {
LogicFunctions = 'logicFunctions',
Objects = 'objects',
Roles = 'roles',
Skills = 'skills',
FrontComponents = 'frontComponents',
PublicAssets = 'publicAssets',
Views = 'views',
@@ -36,6 +38,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions,
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineNavigationMenuItem]:
@@ -69,6 +69,7 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
logicFunctions: SyncableEntity.LogicFunction,
frontComponents: SyncableEntity.FrontComponent,
roles: SyncableEntity.Role,
skills: SyncableEntity.Skill,
views: SyncableEntity.View,
navigationMenuItems: SyncableEntity.NavigationMenuItem,
pageLayouts: SyncableEntity.PageLayout,
@@ -97,6 +97,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.LogicFunction]: 'Logic functions',
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.Skill]: 'Skills',
[SyncableEntity.View]: 'Views',
[SyncableEntity.NavigationMenuItem]: 'Navigation menu items',
[SyncableEntity.PageLayout]: 'Page layouts',
@@ -0,0 +1,26 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getSkillBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineSkill } from 'twenty-sdk';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineSkill({
universalIdentifier: ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_SKILL_UNIVERSAL_IDENTIFIER,
name: '${kebabCaseName}',
label: '${name}',
description: 'Add a description for your skill',
content: 'Add the skill content here',
});
`;
};