Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
cb744b2eeb
commit
1642be86f5
@@ -0,0 +1,360 @@
|
||||
const path = require('node:path');
|
||||
|
||||
const { PLUGIN_ROOT, readText } = require('./lib');
|
||||
|
||||
const assertTwentyMcpFormattingContract = (fail) => {
|
||||
const skillPath = path.join(PLUGIN_ROOT, 'skills/use-twenty-mcp/SKILL.md');
|
||||
const resultFormattingPath = path.join(PLUGIN_ROOT, 'references/use-twenty-mcp/result-formatting.md');
|
||||
const skill = readText(skillPath);
|
||||
const formatting = readText(resultFormattingPath);
|
||||
|
||||
const requiredSkillFragments = [
|
||||
'# Output Contract',
|
||||
'If the tool output includes `recordReferences`',
|
||||
'MUST link each display name back to Twenty',
|
||||
'{workspaceOrigin}/object/{objectNameSingular}/{recordId}',
|
||||
'Never show unlinked record names',
|
||||
];
|
||||
|
||||
for (const fragment of requiredSkillFragments) {
|
||||
if (!skill.includes(fragment)) {
|
||||
fail(`use-twenty-mcp/SKILL.md is missing formatting contract fragment: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredFormattingFragments = [
|
||||
'## Workspace Origin',
|
||||
'derive the origin from it by removing the trailing `/mcp`',
|
||||
'If `recordReferences` and workspace origin are both available',
|
||||
'the first record-name column or record heading MUST link the display name',
|
||||
'For recent companies with `recordReferences`, link the company name',
|
||||
];
|
||||
|
||||
for (const fragment of requiredFormattingFragments) {
|
||||
if (!formatting.includes(fragment)) {
|
||||
fail(`result-formatting.md is missing record-link guidance fragment: ${fragment}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assertFrontComponentGuidance = (fail) => {
|
||||
const developSkillPath = path.join(PLUGIN_ROOT, 'skills/develop-app/SKILL.md');
|
||||
const layoutPath = path.join(PLUGIN_ROOT, 'references/develop-app/layout.md');
|
||||
const frontComponentsPath = path.join(PLUGIN_ROOT, 'references/develop-app/front-components.md');
|
||||
const standalonePagesPath = path.join(PLUGIN_ROOT, 'references/develop-app/standalone-pages.md');
|
||||
const appStructurePath = path.join(PLUGIN_ROOT, 'references/develop-app/app-structure.md');
|
||||
const frontComponentUiPath = path.join(PLUGIN_ROOT, 'references/design/front-component-ui.md');
|
||||
const developSkill = readText(developSkillPath);
|
||||
const layout = readText(layoutPath);
|
||||
const frontComponents = readText(frontComponentsPath);
|
||||
const standalonePages = readText(standalonePagesPath);
|
||||
const appStructure = readText(appStructurePath);
|
||||
const frontComponentUi = readText(frontComponentUiPath);
|
||||
|
||||
const requiredDevelopSkillFragments = [
|
||||
'references/develop-app/front-components.md',
|
||||
'references/develop-app/standalone-pages.md',
|
||||
'Twenty UI imports',
|
||||
'Use `layout.md` for placement, `standalone-pages.md` for full-page custom UI, and `front-component-ui.md` for visual design and Twenty UI component selection',
|
||||
];
|
||||
|
||||
for (const fragment of requiredDevelopSkillFragments) {
|
||||
if (!developSkill.includes(fragment)) {
|
||||
fail(`develop-app/SKILL.md is missing front component guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredLayoutFragments = [
|
||||
'## Front Component Widgets',
|
||||
'frontComponentUniversalIdentifier',
|
||||
'A `frontComponentId` is not the same value',
|
||||
'use `front-components.md`',
|
||||
];
|
||||
|
||||
for (const fragment of requiredLayoutFragments) {
|
||||
if (!layout.includes(fragment)) {
|
||||
fail(`layout.md is missing front component guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredFrontComponentFragments = [
|
||||
'# Front Components',
|
||||
'defineFrontComponent',
|
||||
'Use `twenty-sdk/front-component`',
|
||||
'Use `twenty-client-sdk/core` or `twenty-client-sdk/metadata`',
|
||||
'Use `twenty-sdk/ui` for Twenty UI components',
|
||||
'Do not import from `twenty-ui` directly',
|
||||
'ThemeProvider',
|
||||
'example-sources/twenty-ui-example.front-component.tsx',
|
||||
'themeCssVariables',
|
||||
'mocks `twenty-sdk/ui` during manifest extraction',
|
||||
'A clean typecheck and sync is not runtime verification',
|
||||
'without a `FrontComponent error`',
|
||||
'hard refresh',
|
||||
];
|
||||
|
||||
for (const fragment of requiredFrontComponentFragments) {
|
||||
if (!frontComponents.includes(fragment)) {
|
||||
fail(`front-components.md is missing runtime guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredStandalonePageFragments = [
|
||||
'# Standalone Pages',
|
||||
'custom page content should be rendered through a `FRONT_COMPONENT` widget',
|
||||
'There does not appear to be a separate public "page body component" API',
|
||||
"type: 'STANDALONE_PAGE'",
|
||||
'NavigationMenuItemType.PAGE_LAYOUT',
|
||||
'PageLayoutTabLayoutMode.CANVAS',
|
||||
'gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 }',
|
||||
'12 x 12 fill pattern',
|
||||
'Full-Page Layout Guidance',
|
||||
'black screen',
|
||||
'yarn twenty dev --once',
|
||||
];
|
||||
|
||||
for (const fragment of requiredStandalonePageFragments) {
|
||||
if (!standalonePages.includes(fragment)) {
|
||||
fail(`standalone-pages.md is missing standalone page guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredAppStructureFragments = [
|
||||
'yarn twenty dev:typecheck',
|
||||
'yarn lint',
|
||||
'yarn twenty dev --once',
|
||||
];
|
||||
|
||||
for (const fragment of requiredAppStructureFragments) {
|
||||
if (!appStructure.includes(fragment)) {
|
||||
fail(`app-structure.md is missing validation checklist command: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredUiDesignFragments = [
|
||||
'# Design Rules',
|
||||
'Do not use this reference for source files, registration, runtime imports, data access, CLI commands, or browser verification',
|
||||
'## Twenty UI Defaults',
|
||||
'Prefer Twenty UI primitives',
|
||||
'Use `Callout`',
|
||||
'Use `Button`',
|
||||
'Use `Tag`, `Status`, `Chip`, `Label`, and `Avatar`',
|
||||
'Use `themeCssVariables`',
|
||||
'Design the visible states',
|
||||
];
|
||||
|
||||
for (const fragment of requiredUiDesignFragments) {
|
||||
if (!frontComponentUi.includes(fragment)) {
|
||||
fail(`front-component-ui.md is missing design-only guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const forbiddenUiFragments = [
|
||||
'# Runtime Safety',
|
||||
'ReactCurrentDispatcher',
|
||||
'yarn twenty',
|
||||
'without a `FrontComponent error`',
|
||||
];
|
||||
|
||||
for (const fragment of forbiddenUiFragments) {
|
||||
if (frontComponentUi.includes(fragment)) {
|
||||
fail(`front-component-ui.md should stay design-only and not include: ${fragment}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assertCliGuidanceSplit = (fail) => {
|
||||
const developSkillPath = path.join(PLUGIN_ROOT, 'skills/develop-app/SKILL.md');
|
||||
const manageSkillPath = path.join(PLUGIN_ROOT, 'skills/manage-app/SKILL.md');
|
||||
const appStructurePath = path.join(PLUGIN_ROOT, 'references/develop-app/app-structure.md');
|
||||
const cliAndSyncPath = path.join(PLUGIN_ROOT, 'references/manage-app/cli-and-sync.md');
|
||||
const developSkill = readText(developSkillPath);
|
||||
const manageSkill = readText(manageSkillPath);
|
||||
const appStructure = readText(appStructurePath);
|
||||
const cliAndSync = readText(cliAndSyncPath);
|
||||
|
||||
const requiredDevelopFragments = [
|
||||
'references/develop-app/app-structure.md',
|
||||
'yarn twenty dev:add',
|
||||
'yarn twenty dev --once',
|
||||
'switch to `manage-app`',
|
||||
];
|
||||
|
||||
for (const fragment of requiredDevelopFragments) {
|
||||
if (!developSkill.includes(fragment)) {
|
||||
fail(`develop-app/SKILL.md is missing CLI split guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredManageFragments = [
|
||||
'references/manage-app/cli-and-sync.md',
|
||||
'validation command semantics',
|
||||
'sync modes',
|
||||
];
|
||||
|
||||
for (const fragment of requiredManageFragments) {
|
||||
if (!manageSkill.includes(fragment)) {
|
||||
fail(`manage-app/SKILL.md is missing CLI reference guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredAppStructureFragments = [
|
||||
'# App Structure',
|
||||
'../manage-app/cli-and-sync.md',
|
||||
'## Entity Creation',
|
||||
'## Validation Checklist',
|
||||
'run lint and typecheck once at the end (not after each individual edit)',
|
||||
'yarn twenty dev:typecheck',
|
||||
'yarn lint',
|
||||
'yarn twenty dev --once',
|
||||
];
|
||||
|
||||
for (const fragment of requiredAppStructureFragments) {
|
||||
if (!appStructure.includes(fragment)) {
|
||||
fail(`app-structure.md is missing develop-app structure guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const forbiddenAppStructureFragments = [
|
||||
'# App Structure And CLI',
|
||||
'Use watch mode only',
|
||||
'Use watch mode for interactive development',
|
||||
'Use one-shot mode for agents',
|
||||
'yarn twenty dev --once --verbose',
|
||||
'yarn twenty remote:list',
|
||||
'Do not run `yarn twenty dev:typecheck`',
|
||||
'run outside the sandbox',
|
||||
'incompatible Node and Yarn',
|
||||
];
|
||||
|
||||
for (const fragment of forbiddenAppStructureFragments) {
|
||||
if (appStructure.includes(fragment)) {
|
||||
fail(`app-structure.md should not own CLI semantics or forbid post-edit validation: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredDevelopValidationFragments = [
|
||||
'run lint and typecheck once at the end (not after each individual edit)',
|
||||
'yarn twenty dev:typecheck',
|
||||
'yarn lint',
|
||||
];
|
||||
|
||||
for (const fragment of requiredDevelopValidationFragments) {
|
||||
if (!developSkill.includes(fragment)) {
|
||||
fail(`develop-app/SKILL.md is missing post-edit validation guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const forbiddenDevelopFragments = [
|
||||
'Do not run `yarn twenty dev:typecheck`',
|
||||
'debug the toolchain',
|
||||
'run outside the sandbox',
|
||||
];
|
||||
|
||||
for (const fragment of forbiddenDevelopFragments) {
|
||||
if (developSkill.includes(fragment)) {
|
||||
fail(`develop-app/SKILL.md should not forbid post-edit validation or warn about the sandbox: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredCliFragments = [
|
||||
'# CLI And Sync',
|
||||
'yarn twenty dev:typecheck',
|
||||
'yarn lint',
|
||||
'yarn twenty dev --once',
|
||||
'Always use one-shot sync to synchronize app changes with the active remote',
|
||||
'Do not use bare `yarn twenty dev` (watch mode)',
|
||||
'yarn twenty dev --once --verbose',
|
||||
'yarn twenty remote:list',
|
||||
'yarn twenty dev:build',
|
||||
'yarn twenty app:publish',
|
||||
'yarn twenty dev:function:logs',
|
||||
];
|
||||
|
||||
for (const fragment of requiredCliFragments) {
|
||||
if (!cliAndSync.includes(fragment)) {
|
||||
fail(`cli-and-sync.md is missing command guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const forbiddenCliFragments = [
|
||||
'run outside the sandbox',
|
||||
'incompatible Node and Yarn',
|
||||
'operations/command-execution.md',
|
||||
];
|
||||
|
||||
for (const fragment of forbiddenCliFragments) {
|
||||
if (cliAndSync.includes(fragment)) {
|
||||
fail(`cli-and-sync.md should not warn about the sandbox or reference the removed command-execution.md: ${fragment}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assertTestingGuidance = (fail) => {
|
||||
const manageSkillPath = path.join(PLUGIN_ROOT, 'skills/manage-app/SKILL.md');
|
||||
const testsPath = path.join(PLUGIN_ROOT, 'references/develop-app/tests.md');
|
||||
const cliAndSyncPath = path.join(PLUGIN_ROOT, 'references/manage-app/cli-and-sync.md');
|
||||
const agentsPath = path.join(PLUGIN_ROOT, 'AGENTS.md');
|
||||
const manageSkill = readText(manageSkillPath);
|
||||
const tests = readText(testsPath);
|
||||
const cliAndSync = readText(cliAndSyncPath);
|
||||
const agents = readText(agentsPath);
|
||||
|
||||
const requiredManageFragments = [
|
||||
'run tests for my Twenty app',
|
||||
'references/develop-app/tests.md',
|
||||
'yarn twenty docker:start --test',
|
||||
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
||||
'Do not run integration tests against the dev instance on `http://localhost:2020`',
|
||||
];
|
||||
|
||||
for (const fragment of requiredManageFragments) {
|
||||
if (!manageSkill.includes(fragment)) {
|
||||
fail(`manage-app/SKILL.md is missing test execution guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredSharedFragments = [
|
||||
'isolated test instance',
|
||||
'port (`2021`)',
|
||||
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
||||
'Do not run integration tests against `http://localhost:2020`',
|
||||
];
|
||||
|
||||
for (const fragment of requiredSharedFragments) {
|
||||
if (!tests.includes(fragment)) {
|
||||
fail(`tests.md is missing isolated integration-test guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredCliFragments = [
|
||||
'Integration tests install and uninstall the app on their target server',
|
||||
'yarn twenty docker:start --test',
|
||||
'port `2021`',
|
||||
'../develop-app/tests.md',
|
||||
];
|
||||
|
||||
for (const fragment of requiredCliFragments) {
|
||||
if (!cliAndSync.includes(fragment)) {
|
||||
fail(`cli-and-sync.md is missing integration-test target guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredAgentsFragments = [
|
||||
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
||||
'Integration tests must target the isolated test instance on port `2021`',
|
||||
];
|
||||
|
||||
for (const fragment of requiredAgentsFragments) {
|
||||
if (!agents.includes(fragment)) {
|
||||
fail(`AGENTS.md is missing durable test target guidance: ${fragment}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
assertTwentyMcpFormattingContract,
|
||||
assertFrontComponentGuidance,
|
||||
assertCliGuidanceSplit,
|
||||
assertTestingGuidance,
|
||||
};
|
||||
Reference in New Issue
Block a user