Skip to content

Commit 549cfc5

Browse files
sarahxsandersclaude
andcommitted
refactor(cli): drop cli-manifest.json emit; cliEntries lives in skill-menu.json
The wizard now reads cliEntries from skill-menu.json at runtime — no published consumer reads cli-manifest.json. The standalone file was dead-on-arrival, so removing it now keeps the contract honest (one file, one consumer). generateCliManifest was producing a wrapped object solely for the file write. Renamed to generateCliEntries, returns the entries array directly. Caller embeds it under skill-menu.json's cliEntries. CLAUDE.md and CONTRIBUTING.md updated to point contributors at the new location. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 282c9a4 commit 549cfc5

4 files changed

Lines changed: 40 additions & 67 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ User-facing intro: [README.md](README.md). Contributor handbook:
1818
| Build pipeline | `scripts/lib/` (skill generator, build phases, change router) |
1919
| Build entrypoints | `scripts/build.js` (full) and `scripts/dev-server.js` (partial / watch) |
2020
| Tests | `scripts/lib/tests/` and `scripts/plugins/tests/` (vitest) |
21-
| Manifest output | `dist/skills/manifest.json`, `dist/skills/cli-manifest.json`, `dist/skills/skill-menu.json` |
21+
| Manifest output | `dist/skills/manifest.json`, `dist/skills/skill-menu.json` (CLI entries live under `cliEntries`) |
2222
| Per-skill ZIPs | `dist/skills/<id>.zip` |
2323

2424
## The `cli:` block (read [CONTRIBUTING.md](CONTRIBUTING.md) before editing)
@@ -62,10 +62,11 @@ abstraction. Restructure to a family when a second vendor lands. See
6262
1. Read [CONTRIBUTING.md § Promotion criterion for `role: command`](CONTRIBUTING.md#promotion-criterion-for-role-command).
6363
2. Run `npm test` — the parser's test suite (`scripts/lib/tests/cli-block.test.js`)
6464
covers every naming-convention case.
65-
3. Run `npm run build` — confirm the entry appears (or disappears) in
66-
`dist/skills/cli-manifest.json` with the values you expect.
67-
4. The wizard's next release picks up the change automatically. No wizard
68-
PR needed unless the change requires wizard-side hooks (custom outro,
65+
3. Run `npm run build` — confirm the entry appears (or disappears) under
66+
`cliEntries` inside `dist/skills/skill-menu.json` with the values you
67+
expect.
68+
4. The wizard resolves new entries at runtime, so no wizard release is
69+
required unless the change needs wizard-side hooks (custom outro,
6970
content blocks, abort cases).
7071
5. **Flag the wizard maintainer:** the wizard ships a committed
7172
`docs/cli.md` auto-generated from the manifest. When the wizard

CONTRIBUTING.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ manifest, see the [README](README.md).
99
Every skill ships with a `config.yaml`. An optional `cli:` block on that
1010
config tells the PostHog wizard whether and how this skill appears as a
1111
command. The block is parsed in `scripts/lib/skill-generator.js` and
12-
emitted into `dist/skills/cli-manifest.json` alongside the regular
13-
manifest. The wizard snapshots that file at build time and turns each
14-
entry into a registered command.
12+
emitted as `cliEntries` inside `dist/skills/skill-menu.json`. The wizard
13+
fetches `skill-menu.json` at runtime and registers each entry as a
14+
command, so adding a new skill-backed command is a context-mill release
15+
— no wizard release needed.
1516

1617
### The `cli:` block schema
1718

@@ -199,9 +200,10 @@ When you've decided your skill meets the `role: command` criterion:
199200
1. Add the `cli:` block to the skill's `config.yaml` with `role:
200201
command`, the right `parentCommand` (if it nests under an existing
201202
family), and `command`.
202-
2. Confirm `npm run build` emits the entry in
203-
`dist/skills/cli-manifest.json` with the right `parentCommand` /
204-
`command` values. The wizard's next release picks it up automatically.
203+
2. Confirm `npm run build` emits the entry under `cliEntries` inside
204+
`dist/skills/skill-menu.json` with the right `parentCommand` /
205+
`command` values. The wizard picks it up on its next invocation
206+
(no wizard release needed).
205207
3. No wizard PR is needed for skill-backed public commands. If you also
206208
need wizard-side hooks (custom outro, content blocks, abort cases),
207209
that's a wizard PR — but the CLI registration is handled by the
@@ -239,7 +241,7 @@ manifest is published before the wizard tries to consume it.
239241

240242
- Skill schema details: `scripts/lib/skill-generator.js`
241243
(`parseCliBlock`, `expandSkillGroups`, JSDoc typedef for the `cli:` block)
242-
- CLI manifest emit: `scripts/lib/build-phases.js` (`generateCliManifest`)
244+
- CLI entries emit: `scripts/lib/build-phases.js` (`generateCliEntries`)
243245
- Tests for the cli block parser: `scripts/lib/tests/cli-block.test.js`
244246
- The wizard's side of the contract: [PostHog/wizard CONTRIBUTING.md](https://github.com/PostHog/wizard/blob/main/CONTRIBUTING.md)
245247

scripts/lib/build-phases.js

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -195,27 +195,23 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers
195195
// The CLI entries are the lookup table the wizard's runtime resolver uses
196196
// (parentCommand + command -> skillId). They live inside skill-menu.json
197197
// so the wizard can reach them through the existing fetchSkillMenu path.
198-
// cli-manifest.json still ships with the same array under `entries` for
199-
// back-compat with the still-baked older wizard; it's slated for removal
200-
// once that wizard release ages out.
201-
const cliManifest = generateCliManifest({ allSkills, manifest });
198+
const cliEntries = generateCliEntries({ allSkills });
202199

203200
const skillMenu = {
204201
version: manifest.version,
205202
buildVersion: manifest.buildVersion,
206203
categories: skillsByCategory,
207-
cliEntries: cliManifest.entries,
204+
cliEntries,
208205
};
209206
fs.writeFileSync(path.join(skillsDir, 'skill-menu.json'), JSON.stringify(skillMenu, null, 2));
210207

211-
fs.writeFileSync(path.join(skillsDir, 'cli-manifest.json'), JSON.stringify(cliManifest, null, 2));
212-
213208
return manifest;
214209
}
215210

216211
/**
217-
* Build the CLI manifest object from the expanded skill list. Used by
218-
* `writeManifestAndMenu` and exercised directly by tests. Throws on an
212+
* Build the CLI entries array from the expanded skill list. Used by
213+
* `writeManifestAndMenu` (which embeds the result in `skill-menu.json`
214+
* under `cliEntries`) and exercised directly by tests. Throws on an
219215
* invalid `recommended:` arrangement (see `validateRecommended`) so the
220216
* build fails before bad data reaches the wizard.
221217
*
@@ -227,10 +223,10 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers
227223
* { skillId, role, command?, parentCommand?, recommended?, displayName, description }
228224
*
229225
* Entries are sorted by role (command first, then skill, then internal),
230-
* then by `parentCommand`/`command` so diffs in `cli-manifest.json` stay
226+
* then by `parentCommand`/`command` so diffs in `skill-menu.json` stay
231227
* reviewable.
232228
*/
233-
function generateCliManifest({ allSkills, manifest }) {
229+
function generateCliEntries({ allSkills }) {
234230
const roleOrder = { command: 0, skill: 1, internal: 2 };
235231
const entries = allSkills
236232
.filter(s => s.cli)
@@ -254,17 +250,7 @@ function generateCliManifest({ allSkills, manifest }) {
254250
return (a.command || '').localeCompare(b.command || '');
255251
});
256252
validateRecommended(entries);
257-
return {
258-
// `version` is shared with the main manifest (it's uri-schema.yaml's
259-
// `manifest_version`). There's no CLI-manifest-only version knob —
260-
// bumping it bumps both manifests' version. If the CLI manifest shape
261-
// ever needs to change independently of the main manifest, give it its
262-
// own version field rather than reusing this one.
263-
version: manifest.version,
264-
buildVersion: manifest.buildVersion,
265-
buildTimestamp: manifest.buildTimestamp,
266-
entries,
267-
};
253+
return entries;
268254
}
269255

270256
/**
@@ -379,7 +365,7 @@ export {
379365
zipSkillToBuffer,
380366
createBundledArchive,
381367
generateManifest,
382-
generateCliManifest,
368+
generateCliEntries,
383369
writeManifestAndMenu,
384370
reconcileOrphans,
385371
partialRebuild,

scripts/lib/tests/cli-block.test.js

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
expandSkillGroups,
1010
serializeSkill,
1111
} from '../skill-generator.js';
12-
import { generateCliManifest } from '../build-phases.js';
12+
import { generateCliEntries } from '../build-phases.js';
1313

1414
function createFixture(tree, baseDir) {
1515
for (const [name, content] of Object.entries(tree)) {
@@ -293,43 +293,31 @@ describe('expandSkillGroups with cli blocks', () => {
293293
});
294294
});
295295

296-
describe('generateCliManifest', () => {
297-
const baseManifest = {
298-
version: '1.0',
299-
buildVersion: 'test',
300-
buildTimestamp: '2026-06-08T00:00:00.000Z',
301-
};
302-
296+
describe('generateCliEntries', () => {
303297
it('emits only skills with a cli block', () => {
304298
const skills = [
305299
{ id: 'integration-django', displayName: 'Django', description: 'd' },
306300
{ id: 'audit-events', displayName: 'Audit events', description: 'a',
307301
cli: { role: 'command', parentCommand: 'audit', command: 'events' } },
308302
];
309-
const manifest = generateCliManifest({ allSkills: skills, manifest: baseManifest });
310-
expect(manifest.entries).toHaveLength(1);
311-
expect(manifest.entries[0].skillId).toBe('audit-events');
303+
const entries = generateCliEntries({ allSkills: skills });
304+
expect(entries).toHaveLength(1);
305+
expect(entries[0].skillId).toBe('audit-events');
312306
});
313307

314-
it('carries version + buildVersion + buildTimestamp through', () => {
315-
const manifest = generateCliManifest({ allSkills: [], manifest: baseManifest });
316-
expect(manifest).toMatchObject({
317-
version: '1.0',
318-
buildVersion: 'test',
319-
buildTimestamp: '2026-06-08T00:00:00.000Z',
320-
entries: [],
321-
});
308+
it('returns an empty array when no skills declare a cli block', () => {
309+
const entries = generateCliEntries({ allSkills: [] });
310+
expect(entries).toEqual([]);
322311
});
323312

324313
it('omits command and parentCommand when not set on the cli block', () => {
325-
const manifest = generateCliManifest({
314+
const entries = generateCliEntries({
326315
allSkills: [
327316
{ id: 'doctor', displayName: 'Doctor', description: 'd',
328317
cli: { role: 'skill' } },
329318
],
330-
manifest: baseManifest,
331319
});
332-
expect(manifest.entries[0]).toEqual({
320+
expect(entries[0]).toEqual({
333321
skillId: 'doctor',
334322
role: 'skill',
335323
displayName: 'Doctor',
@@ -338,7 +326,7 @@ describe('generateCliManifest', () => {
338326
});
339327

340328
it('sorts entries by role, then parentCommand, then command', () => {
341-
const manifest = generateCliManifest({
329+
const entries = generateCliEntries({
342330
allSkills: [
343331
{ id: 'b-skill', displayName: 'B', description: 'd', cli: { role: 'skill' } },
344332
{ id: 'a-int', displayName: 'A', description: 'd', cli: { role: 'internal' } },
@@ -349,22 +337,20 @@ describe('generateCliManifest', () => {
349337
{ id: 'revenue', displayName: 'R', description: 'd',
350338
cli: { role: 'command', command: 'revenue' } },
351339
],
352-
manifest: baseManifest,
353340
});
354-
const order = manifest.entries.map(e => e.skillId);
341+
const order = entries.map(e => e.skillId);
355342
// command flat (no parent) sorts before grouped 'audit', then skill, then internal
356343
expect(order).toEqual(['revenue', 'audit-all', 'audit-events', 'b-skill', 'a-int']);
357344
});
358345

359346
it('carries recommended:true through into the entry', () => {
360-
const manifest = generateCliManifest({
347+
const entries = generateCliEntries({
361348
allSkills: [
362349
{ id: 'audit-all', displayName: 'Audit', description: 'd',
363350
cli: { role: 'command', parentCommand: 'audit', command: 'all', recommended: true } },
364351
],
365-
manifest: baseManifest,
366352
});
367-
expect(manifest.entries[0]).toMatchObject({
353+
expect(entries[0]).toMatchObject({
368354
skillId: 'audit-all',
369355
parentCommand: 'audit',
370356
command: 'all',
@@ -374,26 +360,24 @@ describe('generateCliManifest', () => {
374360

375361
it('throws when a family has more than one recommended leaf', () => {
376362
expect(() =>
377-
generateCliManifest({
363+
generateCliEntries({
378364
allSkills: [
379365
{ id: 'audit-all', displayName: 'A', description: 'd',
380366
cli: { role: 'command', parentCommand: 'audit', command: 'all', recommended: true } },
381367
{ id: 'audit-events', displayName: 'AE', description: 'd',
382368
cli: { role: 'command', parentCommand: 'audit', command: 'events', recommended: true } },
383369
],
384-
manifest: baseManifest,
385370
}),
386371
).toThrow(/Family "audit" has more than one cli\.recommended leaf/);
387372
});
388373

389374
it('throws when recommended is set on a flat command with no parentCommand', () => {
390375
expect(() =>
391-
generateCliManifest({
376+
generateCliEntries({
392377
allSkills: [
393378
{ id: 'revenue', displayName: 'R', description: 'd',
394379
cli: { role: 'command', command: 'revenue', recommended: true } },
395380
],
396-
manifest: baseManifest,
397381
}),
398382
).toThrow(/only valid on a leaf inside a family/);
399383
});

0 commit comments

Comments
 (0)