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
+3 -1
View File
@@ -89,6 +89,7 @@ In interactive mode, you can pick from:
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
- **Example view** — a saved view for the example object (`views/example-view.ts`)
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
## What gets scaffolded
@@ -106,11 +107,12 @@ In interactive mode, you can pick from:
- `front-components/hello-world.tsx` — Example front component
- `views/example-view.ts` — Example saved view for the example object
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
- `skills/example-skill.ts` — Example AI agent skill definition
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items).
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Types are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
@@ -114,6 +114,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
};
}
@@ -125,6 +126,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
}
@@ -164,6 +166,11 @@ export class CreateAppCommand {
value: 'navigationMenuItem',
checked: true,
},
{
name: 'Example skill (AI agent skill definition)',
value: 'skill',
checked: true,
},
],
},
]);
@@ -189,6 +196,7 @@ export class CreateAppCommand {
includeExampleView: includeView,
includeExampleNavigationMenuItem:
selectedExamples.includes('navigationMenuItem'),
includeExampleSkill: selectedExamples.includes('skill'),
};
}
@@ -7,4 +7,5 @@ export type ExampleOptions = {
includeExampleFrontComponent: boolean;
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
};
@@ -1,8 +1,8 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'os';
import { copyBaseApplicationProject } from '@/utils/app-template';
import { type ExampleOptions } from '@/types/scaffolding-options';
import { copyBaseApplicationProject } from '@/utils/app-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
// Mock fs-extra's copy function to skip copying base template (not available during tests)
jest.mock('fs-extra', () => {
@@ -23,11 +23,13 @@ const ALL_EXAMPLES: ExampleOptions = {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
const NO_EXAMPLES: ExampleOptions = {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
@@ -437,6 +439,7 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: true,
includeExampleView: false,
@@ -472,6 +475,7 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
exampleOptions: {
includeExampleObject: false,
includeExampleSkill: false,
includeExampleField: false,
includeExampleLogicFunction: true,
includeExampleFrontComponent: false,
@@ -89,6 +89,14 @@ export const copyBaseApplicationProject = async ({
});
}
if (exampleOptions.includeExampleSkill) {
await createExampleSkill({
appDirectory: sourceFolderPath,
fileFolder: 'skills',
fileName: 'example-skill.ts',
});
}
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -424,6 +432,36 @@ export default defineNavigationMenuItem({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleSkill = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineSkill({
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -14,6 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
**What you can do today:**
- Define custom objects and fields as code (managed data model)
- Build logic functions with custom triggers
- Define skills for AI agents
- Deploy the same app across multiple workspaces
## Prerequisites
@@ -44,7 +45,7 @@ yarn twenty app:dev
The scaffolder supports three modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -117,8 +118,10 @@ my-twenty-app/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Example AI agent skill definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
@@ -147,6 +150,7 @@ The SDK detects entities by parsing your TypeScript files for **`export default
| `defineField()` | Field extensions for existing objects |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
@@ -167,7 +171,7 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -220,6 +224,7 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineField()` | Extend existing objects with additional fields |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
@@ -729,6 +734,40 @@ You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
### Skills
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Key points:
- `name` is a unique identifier string for the skill (kebab-case recommended).
- `label` is the human-readable display name shown in the UI.
- `content` contains the skill instructions — this is the text the AI agent uses.
- `icon` (optional) sets the icon displayed in the UI.
- `description` (optional) provides additional context about the skill's purpose.
You can create new skills in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed client
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
+5 -1
View File
@@ -130,7 +130,7 @@ Application development commands.
- `twenty entity:add [entityType]` — Add a new entity to your application.
- Arguments:
- `entityType`: one of `object`, `field`, `function`, `front-component`, `role`, `view`, or `navigation-menu-item`. If omitted, an interactive prompt is shown.
- `entityType`: one of `object`, `field`, `function`, `front-component`, `role`, `view`, `navigation-menu-item`, or `skill`. If omitted, an interactive prompt is shown.
- Options:
- `--path <path>`: The path where the entity file should be created (relative to the current directory).
- Behavior:
@@ -141,6 +141,7 @@ Application development commands.
- `role`: prompts for a name and scaffolds a `*.role.ts` role definition file.
- `view`: prompts for a name and target object, then creates a `*.view.ts` definition file.
- `navigation-menu-item`: prompts for a name and scaffolds a `*.navigation-menu-item.ts` file.
- `skill`: prompts for a name and scaffolds a `*.skill.ts` skill definition file.
### Function
@@ -180,6 +181,9 @@ twenty entity:add view
# Add a new navigation menu item
twenty entity:add navigation-menu-item
# Add a new skill
twenty entity:add skill
# Uninstall the app from the workspace
twenty app:uninstall
@@ -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',
});
`;
};
@@ -8,6 +8,7 @@ import {
type FieldManifest,
type NavigationMenuItemManifest,
type RoleManifest,
type SkillManifest,
} from 'twenty-shared/application';
export type ValidationResult<T> = {
@@ -23,6 +24,7 @@ export type DefinableEntity =
| FrontComponentConfig
| LogicFunctionConfig
| RoleManifest
| SkillManifest
| ViewConfig
| NavigationMenuItemManifest
| PageLayoutConfig;
+1
View File
@@ -59,6 +59,7 @@ export {
export type { PageLayoutWidgetUniversalConfiguration } from 'twenty-shared/types';
export { defineRole } from './roles/define-role';
export { PermissionFlag } from './roles/permission-flag-type';
export { defineSkill } from './skills/define-skill';
export { defineView } from './views/define-view';
export type { ViewConfig } from './views/view-config';
@@ -0,0 +1,25 @@
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type SkillManifest } from 'twenty-shared/application';
export const defineSkill: DefineEntity<SkillManifest> = (config) => {
const errors: string[] = [];
if (!config.universalIdentifier) {
errors.push('Skill must have a universalIdentifier');
}
if (!config.name) {
errors.push('Skill must have a name');
}
if (!config.label) {
errors.push('Skill must have a label');
}
if (!config.content) {
errors.push('Skill must have content');
}
return createValidationResult({ config, errors });
};
@@ -6,6 +6,7 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
'logicFunction',
'frontComponent',
'role',
'skill',
'view',
'viewField',
'viewFieldGroup',
@@ -11,6 +11,7 @@ import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/utils/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/utils/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/utils/from-skill-manifest-to-universal-flat-skill.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/utils/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/utils/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/utils/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
@@ -153,6 +154,20 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
);
}
for (const skillManifest of manifest.skills ?? []) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
metadataName: 'skill',
universalFlatEntity: fromSkillManifestToUniversalFlatSkill({
skillManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
},
);
}
for (const viewManifest of manifest.views ?? []) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
@@ -0,0 +1,27 @@
import { type SkillManifest } from 'twenty-shared/application';
import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type';
export const fromSkillManifestToUniversalFlatSkill = ({
skillManifest,
applicationUniversalIdentifier,
now,
}: {
skillManifest: SkillManifest;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatSkill => {
return {
universalIdentifier: skillManifest.universalIdentifier,
applicationUniversalIdentifier,
name: skillManifest.name,
label: skillManifest.label,
icon: skillManifest.icon ?? null,
description: skillManifest.description ?? null,
content: skillManifest.content,
isCustom: false,
isActive: true,
createdAt: now,
updatedAt: now,
};
};
@@ -348,3 +348,77 @@ exports[`syncApplication should return workspace migration actions on initial sy
},
}
`;
exports[`syncApplication should sync a skill then update it on second sync 1`] = `
{
"syncApplication": {
"actions": [
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A test role",
"icon": null,
"isEditable": true,
"label": "Test Role",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"content": "# Test Skill
This is a test skill.",
"createdAt": Any<String>,
"description": "A skill for testing",
"icon": "IconBrain",
"isActive": true,
"isCustom": false,
"label": "Test Skill",
"name": "test-skill",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "skill",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication should sync a skill then update it on second sync 2`] = `
{
"syncApplication": {
"actions": [
{
"metadataName": "skill",
"type": "update",
"universalIdentifier": Any<String>,
"update": {
"content": "# Test Skill
This is an updated test skill with more content.",
"description": "An updated skill for testing",
"label": "Test Skill Updated",
},
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
@@ -48,6 +48,7 @@ const buildBaseManifest = (
views: [],
navigationMenuItems: [],
pageLayouts: [],
skills: [],
...overrides,
});
@@ -11,6 +11,7 @@ const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_SECOND_ROLE_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_SKILL_ID = uuidv4();
const TEST_OBJECT = buildDefaultObjectManifest({
nameSingular: 'ticket',
@@ -35,7 +36,7 @@ describe('syncApplication', () => {
appCreated = true;
}, 60000);
afterAll(async () => {
afterEach(async () => {
if (!appCreated) {
return;
}
@@ -66,6 +67,7 @@ describe('syncApplication', () => {
description: 'A test role',
},
],
skills: [],
objects: [TEST_OBJECT],
fields: [
{
@@ -131,4 +133,78 @@ describe('syncApplication', () => {
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
it('should sync a skill then update it on second sync', async () => {
const initialManifest: Manifest = {
application: {
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test Application',
description: 'A test application for workspace migration',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
apiClientChecksum: null,
},
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill',
description: 'A skill for testing',
icon: 'IconBrain',
content: '# Test Skill\n\nThis is a test skill.',
},
],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const { data: firstSyncData } = await syncApplication({
manifest: initialManifest,
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const updatedManifest: Manifest = {
...initialManifest,
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill Updated',
description: 'An updated skill for testing',
icon: 'IconBrain',
content:
'# Test Skill\n\nThis is an updated test skill with more content.',
},
],
};
const { data: secondSyncData } = await syncApplication({
manifest: updatedManifest,
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
});
@@ -4,6 +4,7 @@ export enum SyncableEntity {
LogicFunction = 'logicFunction',
FrontComponent = 'frontComponent',
Role = 'role',
Skill = 'skill',
View = 'view',
NavigationMenuItem = 'navigationMenuItem',
PageLayout = 'pageLayout',
@@ -50,6 +50,7 @@ export type {
FieldPermissionManifest,
RoleManifest,
} from './roleManifestType';
export type { SkillManifest } from './skillManifestType';
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
export type {
ViewManifestFilterValue,
@@ -7,6 +7,7 @@ import { type NavigationMenuItemManifest } from './navigationMenuItemManifestTyp
import { type ObjectManifest } from './objectManifestType';
import { type PageLayoutManifest } from './pageLayoutManifestType';
import { type RoleManifest } from './roleManifestType';
import { type SkillManifest } from './skillManifestType';
import { type ViewManifest } from './viewManifestType';
export type Manifest = {
@@ -16,6 +17,7 @@ export type Manifest = {
logicFunctions: LogicFunctionManifest[];
frontComponents: FrontComponentManifest[];
roles: RoleManifest[];
skills: SkillManifest[];
publicAssets: AssetManifest[];
views: ViewManifest[];
navigationMenuItems: NavigationMenuItemManifest[];
@@ -0,0 +1,9 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type SkillManifest = SyncableEntityOptions & {
name: string;
label: string;
icon?: string;
description?: string;
content: string;
};