diff --git a/packages/claude-code-plugin/.gitignore b/packages/claude-code-plugin/.gitignore index a5c9331d..7379af83 100644 --- a/packages/claude-code-plugin/.gitignore +++ b/packages/claude-code-plugin/.gitignore @@ -1,6 +1,7 @@ # Build artifacts /dist/ *.tsbuildinfo +namespace-manifest.json # Dependencies node_modules/ diff --git a/packages/claude-code-plugin/commands/buddy.md b/packages/claude-code-plugin/commands/buddy.md index c55f684c..908ebe25 100644 --- a/packages/claude-code-plugin/commands/buddy.md +++ b/packages/claude-code-plugin/commands/buddy.md @@ -101,12 +101,12 @@ Based on the project state, suggest 2-3 concrete next actions: ``` ## ⚡ Quick Actions -- `/plan` — Start planning a new task -- `/act` — Execute current plan -- `/eval` — Evaluate recent implementation -- `/auto` — Autonomous development cycle -- `/checklist [domain]` — Generate quality checklist -- `/buddy` — Show this status again +- `/codingbuddy:plan` — Start planning a new task +- `/codingbuddy:act` — Execute current plan +- `/codingbuddy:eval` — Evaluate recent implementation +- `/codingbuddy:auto` — Autonomous development cycle +- `/codingbuddy:checklist [domain]` — Generate quality checklist +- `/codingbuddy:buddy` — Show this status again ``` ## MCP Integration diff --git a/packages/claude-code-plugin/scripts/build.spec.ts b/packages/claude-code-plugin/scripts/build.spec.ts index 61c288df..bc27c0e3 100644 --- a/packages/claude-code-plugin/scripts/build.spec.ts +++ b/packages/claude-code-plugin/scripts/build.spec.ts @@ -1,15 +1,22 @@ /** * Unit Tests for Build Script Orchestration * - * Tests the build script's utility functions and argument parsing. - * Note: Build script now only syncs version and generates README. - * Agents, commands, and skills are provided by MCP server (single source of truth). + * Tests the build script's utility functions, argument parsing, + * and namespace manifest generation for the codingbuddy:* command mapping. + * + * Note: Build script syncs version, generates README, and produces + * namespace-manifest.json. Agents, commands, and skills are provided + * by MCP server (single source of truth). */ -import { describe, it, expect } from 'vitest'; +import * as path from 'path'; +import * as fs from 'fs'; +import { describe, it, expect, beforeAll } from 'vitest'; // Import utilities that are testable in isolation import { getErrorMessage, type BuildMode } from '../src/utils'; +import { buildNamespaceManifest, type NamespaceManifest } from './build'; +import { PLUGIN_NAMESPACE, NAMESPACE_SEPARATOR, LEGACY_ALLOWLIST } from './validate-commands'; describe('build script orchestration', () => { // ============================================================================ @@ -130,4 +137,88 @@ describe('build script orchestration', () => { expect(buildResult.errors).toContain('Failed to write README.md'); }); }); + + // ============================================================================ + // Namespace Manifest Generation + // ============================================================================ + describe('namespace manifest generation', () => { + let manifest: NamespaceManifest; + + // Build once — the manifest is deterministic for a given commands/ directory + beforeAll(() => { + manifest = buildNamespaceManifest(); + }); + + it('uses the codingbuddy plugin namespace', () => { + expect(manifest.pluginName).toBe(PLUGIN_NAMESPACE); + expect(manifest.namespace).toBe(`${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}`); + expect(manifest.separator).toBe(NAMESPACE_SEPARATOR); + }); + + it('includes all current command files', () => { + const bareNames = manifest.commands.map(c => c.bare); + for (const cmd of LEGACY_ALLOWLIST) { + expect(bareNames).toContain(cmd); + } + }); + + it('maps every command to its namespaced form', () => { + for (const cmd of manifest.commands) { + expect(cmd.namespaced).toBe(`${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}${cmd.bare}`); + } + }); + + it('references valid file paths in commands/', () => { + for (const cmd of manifest.commands) { + expect(cmd.file).toBe(`commands/${cmd.bare}.md`); + } + }); + + it('includes a generatedAt ISO timestamp', () => { + expect(manifest.generatedAt).toBeTruthy(); + // ISO 8601 rough check + expect(new Date(manifest.generatedAt).toISOString()).toBe(manifest.generatedAt); + }); + + it('produces at least 6 command entries', () => { + expect(manifest.commands.length).toBeGreaterThanOrEqual(6); + }); + }); +}); + +// ============================================================================ +// Packaging Integration — namespaced command assets +// ============================================================================ +describe('packaging integration', () => { + const pluginRoot = path.resolve(__dirname, '..'); + const commandsDir = path.join(pluginRoot, 'commands'); + + it('commands/ directory exists and contains .md files', () => { + expect(fs.existsSync(commandsDir)).toBe(true); + const files: string[] = fs.readdirSync(commandsDir).filter((f: string) => f.endsWith('.md')); + expect(files.length).toBeGreaterThanOrEqual(6); + }); + + it('every command file maps to a codingbuddy:* namespaced slug', () => { + const files: string[] = fs.readdirSync(commandsDir).filter((f: string) => f.endsWith('.md')); + for (const file of files) { + const bare = path.basename(file, '.md'); + const namespaced = `${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}${bare}`; + expect(namespaced).toMatch(/^codingbuddy:.+$/); + } + }); + + it('plugin.json name matches the namespace prefix', () => { + const pluginJsonPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json'); + const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8')); + expect(pluginJson.name).toBe(PLUGIN_NAMESPACE); + }); + + it('buddy.md references namespaced commands in Quick Actions', () => { + const buddyPath = path.join(commandsDir, 'buddy.md'); + const content = fs.readFileSync(buddyPath, 'utf8'); + expect(content).toContain('/codingbuddy:plan'); + expect(content).toContain('/codingbuddy:act'); + expect(content).toContain('/codingbuddy:buddy'); + }); }); diff --git a/packages/claude-code-plugin/scripts/build.ts b/packages/claude-code-plugin/scripts/build.ts index 58d7dc20..9a392c2c 100644 --- a/packages/claude-code-plugin/scripts/build.ts +++ b/packages/claude-code-plugin/scripts/build.ts @@ -20,9 +20,15 @@ import * as fs from 'fs'; // Import utilities import { getErrorMessage } from '../src/utils'; +import { + PLUGIN_NAMESPACE, + NAMESPACE_SEPARATOR, + extractCommandsFromDirectory, +} from './validate-commands'; // Paths const ROOT_DIR = path.resolve(__dirname, '..'); +const COMMANDS_DIR = path.join(ROOT_DIR, 'commands'); interface BuildResult { step: string; @@ -71,7 +77,10 @@ claude plugin add codingbuddy - **EVAL**: Evaluate code quality and suggest improvements - **AUTO**: Autonomous PLAN → ACT → EVAL cycle -### Commands +### Commands (codingbuddy:* namespace) + +All commands use the \`codingbuddy:\` namespace to avoid collisions with Claude Code built-ins. + - \`/codingbuddy:plan\` - Enter PLAN mode - \`/codingbuddy:act\` - Enter ACT mode - \`/codingbuddy:eval\` - Enter EVAL mode @@ -150,6 +159,64 @@ MIT return result; } +/** + * Namespace manifest shape exported for testing. + */ +export interface NamespaceManifest { + pluginName: string; + namespace: string; + separator: string; + commands: Array<{ + file: string; + bare: string; + namespaced: string; + }>; + generatedAt: string; +} + +/** + * Builds a namespace manifest object from the commands/ directory. + * Exported for testing — the build step serialises this to disk. + */ +export function buildNamespaceManifest(): NamespaceManifest { + const commands = extractCommandsFromDirectory(COMMANDS_DIR); + return { + pluginName: PLUGIN_NAMESPACE, + namespace: `${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}`, + separator: NAMESPACE_SEPARATOR, + commands: commands.map(cmd => ({ + file: `commands/${cmd}.md`, + bare: cmd, + namespaced: `${PLUGIN_NAMESPACE}${NAMESPACE_SEPARATOR}${cmd}`, + })), + generatedAt: new Date().toISOString(), + }; +} + +function createNamespaceManifest(): BuildResult { + const result: BuildResult = { + step: 'Namespace Manifest', + success: true, + details: [], + errors: [], + }; + + try { + const manifest = buildNamespaceManifest(); + const outPath = path.join(ROOT_DIR, 'namespace-manifest.json'); + fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n'); + result.details.push(`Generated namespace-manifest.json (${manifest.commands.length} commands)`); + for (const cmd of manifest.commands) { + result.details.push(` ${cmd.bare} → ${cmd.namespaced}`); + } + } catch (err: unknown) { + result.success = false; + result.errors.push(`Failed to create namespace-manifest.json: ${getErrorMessage(err)}`); + } + + return result; +} + function createMcpJson(): BuildResult { const result: BuildResult = { step: 'MCP Configuration', @@ -194,8 +261,12 @@ async function main(): Promise { console.log('📖 Step 1: Generating README...'); results.push(createReadme()); - // Step 2: Generate .mcp.json - console.log('🔧 Step 2: Generating .mcp.json...'); + // Step 2: Generate namespace manifest + console.log('📛 Step 2: Generating namespace manifest...'); + results.push(createNamespaceManifest()); + + // Step 3: Generate .mcp.json + console.log('🔧 Step 3: Generating .mcp.json...'); results.push(createMcpJson()); // Summary @@ -224,9 +295,10 @@ async function main(): Promise { if (allSuccess) { console.log('✨ Build completed successfully!'); console.log(`\nOutput directory: ${ROOT_DIR}`); - console.log(' ├── .claude-plugin/ (plugin manifest)'); - console.log(' ├── .mcp.json (MCP server configuration)'); - console.log(' └── README.md (plugin documentation)'); + console.log(' ├── .claude-plugin/ (plugin manifest)'); + console.log(' ├── .mcp.json (MCP server configuration)'); + console.log(' ├── namespace-manifest.json (codingbuddy:* command mapping)'); + console.log(' └── README.md (plugin documentation)'); console.log('\nNote: Agents, commands, and skills are provided by MCP server'); console.log(' from packages/rules/.ai-rules/ (single source of truth)'); } else {