-
Notifications
You must be signed in to change notification settings - Fork 0
feat: sync-router.mjs — generate SKILL.md routing tables from references/index.json #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // Shared rendering for the SKILL.md router: the frontmatter description and the | ||
| // three routing tables are derived from references/index.json so humans only | ||
| // maintain index.json and the reference files. Used by split-from-cli.mjs | ||
| // (one-time migration) and sync-router.mjs (ongoing). | ||
|
|
||
| export const PRODUCTS = { | ||
| a6: { name: 'Apache APISIX', short: 'APISIX', audience: 'the open-source Apache APISIX gateway', versionKey: 'apisix_version' }, | ||
| a7: { name: 'API7 Enterprise Edition', short: 'API7 EE', audience: 'API7 Enterprise Edition (API7 Gateway)', versionKey: 'apisix_version' }, | ||
| }; | ||
|
|
||
| export const TABLE_START = '<!-- routing-tables:start (generated from references/index.json by scripts/sync-router.mjs; do not edit by hand) -->'; | ||
| export const TABLE_END = '<!-- routing-tables:end -->'; | ||
|
|
||
| export function renderDescription(cli, entries) { | ||
| const p = PRODUCTS[cli]; | ||
| const plugins = entries.filter((e) => e.category === 'plugin').map((e) => e.id).join(', '); | ||
| const recipes = entries.filter((e) => e.category === 'recipe').map((e) => e.id).join(', '); | ||
| const d = | ||
| `Configure and operate ${p.audience} through the ${cli} CLI. Use whenever the user wants to ` + | ||
| `create, inspect, change, or delete ${p.short} routes, services, upstreams, consumers, credentials, SSL certificates, ` + | ||
| `global rules, or plugins (${plugins}), or run a workflow such as ${recipes}. ` + | ||
| `Includes developer and platform-operator personas and the ${cli} command conventions.`; | ||
| if (d.length > 1024) throw new Error(`${cli}: description is ${d.length} chars (> 1024)`); | ||
| return d; | ||
| } | ||
|
|
||
| export const foldDescription = (d) => d.match(/.{1,96}(\s|$)/g).map((l) => l.trim()).join('\n '); | ||
|
|
||
| const short = (d, n = 110) => (d.length > n ? d.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : d); | ||
|
|
||
| function stripPrefix(cli, d) { | ||
| const p = PRODUCTS[cli]; | ||
| const prod = p.name.replace(/[()]/g, '\\$&'); | ||
| d = d | ||
| .replace(new RegExp(`^(Skill|Recipe skill|Persona skill|Core skill) for (configuring|implementing|working with|setting up|managing)?\\s*(the )?(${prod}|Apache APISIX|APISIX|API7 Enterprise Edition \\(API7 EE\\)|API7 EE)?\\s*`, 'i'), '') | ||
| .replace(new RegExp(`\\s*(via|using|with) the ${cli} CLI[^.]*\\.\\s*`, 'i'), '. ') | ||
| .replace(/^\s*(plugin|recipe|persona)?\s*/i, '') | ||
| .replace(/^\w/, (c) => c.toUpperCase()); | ||
| d = d.replace(new RegExp(`\\s*(using|on|with|for) (the )?(${prod}( \\(API7 EE\\))?|Apache APISIX|APISIX|API7 EE)( and the ${cli} CLI)?`, 'g'), ''); | ||
| d = d.replace(/^[^.]{0,60}?\b(plugin|recipe|persona|releases?|workflows?|strategies|patterns)\.\s*/i, ''); | ||
| return short(d.replace(/^\w/, (c) => c.toUpperCase())); | ||
| } | ||
|
|
||
| const row = (cli, e) => `| \`${e.category === 'plugin' ? (e.plugin_name || e.id) : e.id}\` | [${e.path.replace('references/', '')}](${e.path}) | ${stripPrefix(cli, e.description)} |`; | ||
|
|
||
| export function renderTables(cli, entries) { | ||
| const plugins = entries.filter((e) => e.category === 'plugin'); | ||
| const recipes = entries.filter((e) => e.category === 'recipe'); | ||
| const personas = entries.filter((e) => e.category === 'persona'); | ||
| return `${TABLE_START} | ||
|
|
||
| ### Plugins (${plugins.length}) | ||
|
|
||
| | Plugin | Reference | Covers | | ||
| |---|---|---| | ||
| ${plugins.map((e) => row(cli, e)).join('\n')} | ||
|
|
||
| ### Recipes — multi-step workflows (${recipes.length}) | ||
|
|
||
| | Workflow | Reference | Covers | | ||
| |---|---|---| | ||
| ${recipes.map((e) => row(cli, e)).join('\n')} | ||
|
|
||
| ### Personas — role-based guidance (${personas.length}) | ||
|
|
||
| | Role | Reference | Covers | | ||
| |---|---|---| | ||
| ${personas.map((e) => row(cli, e)).join('\n')} | ||
|
|
||
| ${TABLE_END}`; | ||
| } | ||
|
|
||
| /** Apply description + tables to an existing SKILL.md; returns the new text. */ | ||
| export function applyRouter(cli, skillMd, entries) { | ||
| const desc = foldDescription(renderDescription(cli, entries)); | ||
| let out = skillMd.replace(/^description: >-\n(?: .*\n)+/m, `description: >-\n ${desc}\n`); | ||
| const start = out.indexOf(TABLE_START); | ||
| const end = out.indexOf(TABLE_END); | ||
| if (start === -1 || end === -1) throw new Error(`${cli}: SKILL.md is missing the routing-tables markers`); | ||
| out = out.slice(0, start) + renderTables(cli, entries) + out.slice(end + TABLE_END.length); | ||
| return out; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Regenerate the generated parts of each skills/<cli>/SKILL.md from | ||
| * references/index.json: the frontmatter `description` (must list every plugin | ||
| * and workflow) and the three routing tables between the markers. | ||
| * | ||
| * node scripts/sync-router.mjs # rewrite SKILL.md files in place | ||
| * node scripts/sync-router.mjs --check # exit 1 if any SKILL.md is stale (CI) | ||
| * | ||
| * Everything outside the markers (intro, operating rules, example) is hand-written. | ||
| */ | ||
| import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; | ||
| import { join } from 'node:path'; | ||
| import { applyRouter, PRODUCTS } from './lib/router.mjs'; | ||
|
|
||
| const ROOT = new URL('..', import.meta.url).pathname; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- scripts/sync-router.mjs ---'
cat -n scripts/sync-router.mjs
printf '%s\n' '--- related path handling ---'
rg -n -C 3 'ROOT|readdirSync|readFileSync|writeFileSync|existsSync|join\(' scripts/sync-router.mjs scripts/libRepository: api7/agent-skills Length of output: 5593 🤖 get_repo_knowledge executed:
Length of output: 322 🏁 Script executed: #!/bin/bash
set -eu
cat -n scripts/sync-router.mjsRepository: api7/agent-skills Length of output: 1832 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- scripts/sync-router.mjs ---'
cat -n scripts/sync-router.mjs
printf '%s\n' '--- direct ROOT consumers ---'
rg -n -C 3 'ROOT|readdirSync|readFileSync|writeFileSync|existsSync|join\(' scripts/sync-router.mjs scripts/libRepository: api7/agent-skills Length of output: 5593 Convert the file URL to a filesystem path.
Proposed fix import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
import { applyRouter, PRODUCTS } from './lib/router.mjs';
-const ROOT = new URL('..', import.meta.url).pathname;
+const ROOT = fileURLToPath(new URL('..', import.meta.url));🤖 Prompt for AI Agents |
||
| const check = process.argv.includes('--check'); | ||
| let stale = 0; | ||
| for (const cli of readdirSync(join(ROOT, 'skills'))) { | ||
| if (!PRODUCTS[cli]) continue; | ||
| const dir = join(ROOT, 'skills', cli); | ||
| const index = JSON.parse(readFileSync(join(dir, 'references', 'index.json'), 'utf8')); | ||
| const file = join(dir, 'SKILL.md'); | ||
| const current = readFileSync(file, 'utf8'); | ||
| const next = applyRouter(cli, current, index.entries); | ||
| if (next === current) { console.log(`${cli}: up to date`); continue; } | ||
| stale++; | ||
| if (check) console.error(`${cli}: SKILL.md is out of date with references/index.json — run node scripts/sync-router.mjs`); | ||
| else { writeFileSync(file, next); console.log(`${cli}: SKILL.md updated`); } | ||
| } | ||
| process.exit(check && stale ? 1 : 0); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve long plugin and recipe identifiers when folding the generated description.
references/index.jsonentries have no identifier length limit, andscripts/validate.mjsonly validates their paths. Therefore an identifier longer than 96 non-whitespace characters can reachrenderDescription().foldDescription()then skips the identifier prefix while searching for a later whitespace boundary.sync-router.mjswrites the incomplete name to frontmatter, and validation still passes when the result is non-empty and under 1024 characters.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents