Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/claude-code-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Build artifacts
/dist/
*.tsbuildinfo
namespace-manifest.json

# Dependencies
node_modules/
Expand Down
12 changes: 6 additions & 6 deletions packages/claude-code-plugin/commands/buddy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 95 additions & 4 deletions packages/claude-code-plugin/scripts/build.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
// ============================================================================
Expand Down Expand Up @@ -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');
});
});
84 changes: 78 additions & 6 deletions packages/claude-code-plugin/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -194,8 +261,12 @@ async function main(): Promise<void> {
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
Expand Down Expand Up @@ -224,9 +295,10 @@ async function main(): Promise<void> {
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 {
Expand Down
Loading