Skip to content

Commit 61e201f

Browse files
committed
improve(plugin): prove end-to-end /codingbuddy:* command migration (#1343)
Add integration tests proving the namespace-manifest runtime path: - plugin.json name → commands/ filenames → manifest → validator consistency - Bidirectional manifest ↔ commands/ coverage - Cross-artifact namespace coherence check Clean up validator legacy terminology: - Rename LEGACY_ALLOWLIST → KNOWN_BARE_COMMANDS (deprecated alias kept) - Update comments and CLI output to reflect canonical model Align migration guide messaging: - Status column: "migration target" → "complete" - Timeline section reflects proven implementation state
1 parent fd59efc commit 61e201f

4 files changed

Lines changed: 151 additions & 29 deletions

File tree

packages/claude-code-plugin/docs/migration-guide.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,20 @@ Keywords also support localized variants: Korean (`계획`/`실행`/`평가`/`
3232

3333
If you prefer slash commands, use the namespaced form:
3434

35-
| Legacy (bare) | Namespaced (new) | Status |
36-
| -------------- | -------------------------- | ---------------- |
37-
| `/plan` | `/codingbuddy:plan` | migration target |
38-
| `/act` | `/codingbuddy:act` | migration target |
39-
| `/eval` | `/codingbuddy:eval` | migration target |
40-
| `/auto` | `/codingbuddy:auto` | migration target |
41-
| `/buddy` | `/codingbuddy:buddy` | migration target |
42-
| `/checklist` | `/codingbuddy:checklist` | migration target |
35+
| Legacy (bare) | Namespaced (canonical) | Status |
36+
| -------------- | -------------------------- | -------- |
37+
| `/plan` | `/codingbuddy:plan` | complete |
38+
| `/act` | `/codingbuddy:act` | complete |
39+
| `/eval` | `/codingbuddy:eval` | complete |
40+
| `/auto` | `/codingbuddy:auto` | complete |
41+
| `/buddy` | `/codingbuddy:buddy` | complete |
42+
| `/checklist` | `/codingbuddy:checklist` | complete |
4343

4444
## Timeline
4545

46-
1. **Now**: Both bare and namespaced commands work. Keywords are the recommended entry point.
47-
2. **Transition**: Bare commands are deprecated but functional. New commands use `codingbuddy:*` only.
48-
3. **Future**: Once Claude Code fully supports `plugin:command` namespace resolution, bare aliases will be removed.
46+
1. **Complete**: All commands are namespaced as `codingbuddy:*`. Claude Code resolves them via `plugin.json` name + `commands/` filenames. Keywords remain the recommended entry point.
47+
2. **Bare aliases**: Legacy bare commands (`/plan`, `/act`, etc.) continue to work. New commands use `codingbuddy:*` only.
48+
3. **Future**: Once Claude Code drops bare-alias resolution, legacy bare filenames may be removed.
4949

5050
## What Changed and Why
5151

packages/claude-code-plugin/scripts/build.spec.ts

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import { describe, it, expect, beforeAll } from 'vitest';
1616
// Import utilities that are testable in isolation
1717
import { getErrorMessage, type BuildMode } from '../src/utils';
1818
import { buildNamespaceManifest, type NamespaceManifest } from './build';
19-
import { PLUGIN_NAMESPACE, NAMESPACE_SEPARATOR, LEGACY_ALLOWLIST } from './validate-commands';
19+
import {
20+
PLUGIN_NAMESPACE,
21+
NAMESPACE_SEPARATOR,
22+
KNOWN_BARE_COMMANDS,
23+
validateCommands,
24+
} from './validate-commands';
2025

2126
describe('build script orchestration', () => {
2227
// ============================================================================
@@ -157,7 +162,7 @@ describe('build script orchestration', () => {
157162

158163
it('includes all current command files', () => {
159164
const bareNames = manifest.commands.map(c => c.bare);
160-
for (const cmd of LEGACY_ALLOWLIST) {
165+
for (const cmd of KNOWN_BARE_COMMANDS) {
161166
expect(bareNames).toContain(cmd);
162167
}
163168
});
@@ -269,3 +274,107 @@ describe('packaging integration', () => {
269274
expect(manifest.commands.length).toBeGreaterThanOrEqual(6);
270275
});
271276
});
277+
278+
// ============================================================================
279+
// End-to-End Namespace Consumption — proves the runtime path is consistent
280+
// ============================================================================
281+
describe('end-to-end namespace consumption', () => {
282+
const pluginRoot = path.resolve(__dirname, '..');
283+
const commandsDir = path.join(pluginRoot, 'commands');
284+
const manifestPath = path.join(pluginRoot, 'namespace-manifest.json');
285+
const pluginJsonPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
286+
const pkgPath = path.join(pluginRoot, 'package.json');
287+
288+
/**
289+
* Claude Code resolves plugin commands as `{plugin.json.name}:{command-filename}`.
290+
* This test suite proves the full chain:
291+
* plugin.json.name → commands/*.md filenames → namespace-manifest.json → validator
292+
* are all consistent and produce the expected `codingbuddy:*` namespace.
293+
*/
294+
295+
// --- Artifact existence ---
296+
297+
it('all required artifacts exist on disk', () => {
298+
expect(fs.existsSync(pluginJsonPath)).toBe(true);
299+
expect(fs.existsSync(commandsDir)).toBe(true);
300+
expect(fs.existsSync(manifestPath)).toBe(true);
301+
expect(fs.existsSync(pkgPath)).toBe(true);
302+
});
303+
304+
// --- plugin.json → namespace ---
305+
306+
it('plugin.json name establishes the namespace prefix', () => {
307+
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
308+
expect(pluginJson.name).toBe(PLUGIN_NAMESPACE);
309+
// Claude Code resolves: /{plugin.name}:{command}
310+
expect(`${pluginJson.name}${NAMESPACE_SEPARATOR}`).toBe(`${PLUGIN_NAMESPACE}:`);
311+
});
312+
313+
// --- commands/ ↔ manifest bidirectional consistency ---
314+
315+
it('every commands/*.md file has a corresponding manifest entry', () => {
316+
const files: string[] = fs.readdirSync(commandsDir).filter((f: string) => f.endsWith('.md'));
317+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
318+
const manifestBareNames: string[] = manifest.commands.map((c: { bare: string }) => c.bare);
319+
320+
for (const file of files) {
321+
const bare = path.basename(file, '.md');
322+
expect(manifestBareNames).toContain(bare);
323+
}
324+
});
325+
326+
it('every manifest entry has a corresponding commands/*.md file', () => {
327+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
328+
for (const cmd of manifest.commands) {
329+
const filePath = path.join(pluginRoot, cmd.file);
330+
expect(fs.existsSync(filePath)).toBe(true);
331+
}
332+
});
333+
334+
it('manifest count matches commands/ file count exactly', () => {
335+
const files: string[] = fs.readdirSync(commandsDir).filter((f: string) => f.endsWith('.md'));
336+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
337+
expect(manifest.commands.length).toBe(files.length);
338+
});
339+
340+
// --- Namespaced form correctness ---
341+
342+
it('manifest namespaced fields match plugin.json name + bare name', () => {
343+
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
344+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
345+
346+
for (const cmd of manifest.commands) {
347+
const expected = `${pluginJson.name}${NAMESPACE_SEPARATOR}${cmd.bare}`;
348+
expect(cmd.namespaced).toBe(expected);
349+
}
350+
});
351+
352+
// --- Validator agreement ---
353+
354+
it('validator passes for all commands in the manifest', () => {
355+
const result = validateCommands(commandsDir);
356+
expect(result.valid).toBe(true);
357+
expect(result.collisions).toHaveLength(0);
358+
expect(result.namespaceViolations).toHaveLength(0);
359+
});
360+
361+
// --- Package distribution ---
362+
363+
it('package.json files array ships all namespace artifacts', () => {
364+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
365+
expect(pkg.files).toContain('namespace-manifest.json');
366+
expect(pkg.files).toContain('commands/');
367+
expect(pkg.files).toContain('.claude-plugin');
368+
});
369+
370+
// --- Cross-artifact namespace coherence ---
371+
372+
it('plugin.json name, manifest pluginName, and PLUGIN_NAMESPACE constant all agree', () => {
373+
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
374+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
375+
376+
expect(pluginJson.name).toBe(PLUGIN_NAMESPACE);
377+
expect(manifest.pluginName).toBe(PLUGIN_NAMESPACE);
378+
expect(manifest.namespace).toBe(`${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}`);
379+
});
380+
});

packages/claude-code-plugin/scripts/validate-commands.spec.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as path from 'path';
1616

1717
import {
1818
RESERVED_COMMANDS,
19+
KNOWN_BARE_COMMANDS,
1920
LEGACY_ALLOWLIST,
2021
extractCommandsFromDirectory,
2122
getBaseCommandName,
@@ -71,22 +72,26 @@ describe('reserved command denylist', () => {
7172
});
7273

7374
// ============================================================================
74-
// Legacy Allowlist
75+
// Known Bare Commands
7576
// ============================================================================
7677

77-
describe('legacy allowlist', () => {
78-
it('contains current bare commands', () => {
78+
describe('known bare commands', () => {
79+
it('contains current bare command filenames', () => {
7980
const expected = ['plan', 'act', 'eval', 'auto', 'buddy', 'checklist'];
8081
for (const cmd of expected) {
81-
expect(LEGACY_ALLOWLIST.has(cmd)).toBe(true);
82+
expect(KNOWN_BARE_COMMANDS.has(cmd)).toBe(true);
8283
}
8384
});
8485

8586
it('does not overlap with reserved commands', () => {
86-
for (const cmd of LEGACY_ALLOWLIST) {
87+
for (const cmd of KNOWN_BARE_COMMANDS) {
8788
expect(RESERVED_COMMANDS.has(cmd)).toBe(false);
8889
}
8990
});
91+
92+
it('LEGACY_ALLOWLIST alias points to the same set', () => {
93+
expect(LEGACY_ALLOWLIST).toBe(KNOWN_BARE_COMMANDS);
94+
});
9095
});
9196

9297
// ============================================================================
@@ -218,7 +223,7 @@ describe('validateCommands', () => {
218223
});
219224

220225
it('passes with current legacy commands', () => {
221-
for (const cmd of LEGACY_ALLOWLIST) {
226+
for (const cmd of KNOWN_BARE_COMMANDS) {
222227
fs.writeFileSync(path.join(tmpDir, `${cmd}.md`), `# ${cmd}`);
223228
}
224229

@@ -236,7 +241,7 @@ describe('validateCommands', () => {
236241
expect(result.collisions).toContain('help');
237242
});
238243

239-
it('fails when a bare command is not in legacy allowlist', () => {
244+
it('fails when a bare command is not in known bare commands', () => {
240245
fs.writeFileSync(path.join(tmpDir, 'my-new-command.md'), '# New');
241246

242247
const result = validateCommands(tmpDir);

packages/claude-code-plugin/scripts/validate-commands.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,20 @@ export const RESERVED_COMMANDS: ReadonlySet<string> = new Set([
8484
]);
8585

8686
// ============================================================================
87-
// Legacy Allowlist
87+
// Known Bare Commands
8888
// ============================================================================
8989

9090
/**
91-
* Known legacy bare commands that are intentionally kept without namespace prefix.
92-
* These existed before the codingbuddy:* namespace convention.
91+
* Bare command filenames shipped in commands/.
92+
*
93+
* Claude Code resolves these as `{plugin.json.name}:{filename}` at runtime,
94+
* so the files are stored without a namespace prefix.
9395
* New commands MUST use the codingbuddy:* namespace.
96+
*
97+
* @deprecated Use `KNOWN_BARE_COMMANDS`. The `LEGACY_ALLOWLIST` alias is
98+
* retained only for backward compatibility with existing test imports.
9499
*/
95-
export const LEGACY_ALLOWLIST: ReadonlySet<string> = new Set([
100+
export const KNOWN_BARE_COMMANDS: ReadonlySet<string> = new Set([
96101
'plan',
97102
'act',
98103
'eval',
@@ -101,6 +106,9 @@ export const LEGACY_ALLOWLIST: ReadonlySet<string> = new Set([
101106
'checklist',
102107
]);
103108

109+
/** @deprecated Alias — prefer {@link KNOWN_BARE_COMMANDS}. */
110+
export const LEGACY_ALLOWLIST = KNOWN_BARE_COMMANDS;
111+
104112
// ============================================================================
105113
// Plugin Namespace
106114
// ============================================================================
@@ -182,7 +190,7 @@ export function isReservedCommand(command: string): boolean {
182190
*
183191
* Rules:
184192
* 1. No command may collide with a reserved Claude Code built-in
185-
* 2. Bare (non-namespaced) commands must be in the legacy allowlist
193+
* 2. Bare (non-namespaced) filenames must be in KNOWN_BARE_COMMANDS
186194
* 3. New commands must use the codingbuddy:* namespace
187195
*/
188196
export function validateCommands(commandsDir: string): ValidationResult {
@@ -205,8 +213,8 @@ export function validateCommands(commandsDir: string): ValidationResult {
205213
result.valid = false;
206214
}
207215

208-
// Check namespace compliance: bare commands must be in legacy allowlist
209-
if (!isNamespaced(cmd) && !LEGACY_ALLOWLIST.has(cmd)) {
216+
// Check namespace compliance: bare filenames must be in KNOWN_BARE_COMMANDS
217+
if (!isNamespaced(cmd) && !KNOWN_BARE_COMMANDS.has(cmd)) {
210218
result.namespaceViolations.push(cmd);
211219
result.valid = false;
212220
}
@@ -233,7 +241,7 @@ function main(): void {
233241

234242
console.log(`Commands found: ${result.commands.join(', ') || '(none)'}`);
235243
console.log(`Reserved denylist size: ${RESERVED_COMMANDS.size}`);
236-
console.log(`Legacy allowlist: ${[...LEGACY_ALLOWLIST].join(', ')}`);
244+
console.log(`Known bare commands: ${[...KNOWN_BARE_COMMANDS].join(', ')}`);
237245
console.log('');
238246

239247
if (result.collisions.length > 0) {
@@ -248,7 +256,7 @@ function main(): void {
248256
console.error('❌ NAMESPACE VIOLATION:');
249257
for (const cmd of result.namespaceViolations) {
250258
console.error(
251-
` "${cmd}" is a bare command not in the legacy allowlist. Use "${PLUGIN_NAMESPACE}:${cmd}" instead.`,
259+
` "${cmd}" is not a known bare command. Use "${PLUGIN_NAMESPACE}:${cmd}" instead.`,
252260
);
253261
}
254262
console.error('');

0 commit comments

Comments
 (0)