From 83822ea1809a6cada40c7aaa90accbd0ab4fa02a Mon Sep 17 00:00:00 2001 From: Leex Date: Mon, 27 Apr 2026 20:19:14 +0200 Subject: [PATCH 1/3] feat(cli): add `archon skill install` command Adds a standalone `archon skill install [path]` subcommand that copies the bundled Archon skill files into `/.claude/skills/archon/`, so users can install or refresh the skill outside the interactive setup wizard. Defaults to the current directory. Refactors `copyArchonSkill` out of `commands/setup.ts` into a new `commands/skill.ts` so the helper can be shared between the wizard and the new CLI command without pulling in `@clack/prompts`. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/package.json | 2 +- packages/cli/src/cli.ts | 26 +++++++- packages/cli/src/commands/setup.test.ts | 2 +- packages/cli/src/commands/setup.ts | 19 +----- packages/cli/src/commands/skill.test.ts | 85 +++++++++++++++++++++++++ packages/cli/src/commands/skill.ts | 61 ++++++++++++++++++ 6 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 packages/cli/src/commands/skill.test.ts create mode 100644 packages/cli/src/commands/skill.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index e21b7f2bf9..5a2fef065c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,7 +8,7 @@ }, "scripts": { "cli": "bun src/cli.ts", - "test": "bun test src/commands/version.test.ts src/commands/setup.test.ts && bun test src/commands/workflow.test.ts && bun test src/commands/isolation.test.ts && bun test src/commands/chat.test.ts && bun test src/commands/serve.test.ts", + "test": "bun test src/commands/version.test.ts src/commands/setup.test.ts src/commands/skill.test.ts && bun test src/commands/workflow.test.ts && bun test src/commands/isolation.test.ts && bun test src/commands/chat.test.ts && bun test src/commands/serve.test.ts", "type-check": "bun x tsc --noEmit" }, "dependencies": { diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 3ecd580178..34070f1d3c 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -62,6 +62,7 @@ import { import { continueCommand } from './commands/continue'; import { chatCommand } from './commands/chat'; import { setupCommand } from './commands/setup'; +import { skillInstallCommand } from './commands/skill'; import { validateWorkflowsCommand, validateCommandsCommand } from './commands/validate'; import { serveCommand } from './commands/serve'; import { closeDatabase } from '@archon/core'; @@ -104,6 +105,7 @@ Commands: continue [msg] Continue work on an existing worktree with prior context complete [...] Complete branch lifecycle (remove worktree + branches) serve Start the web UI server (downloads web UI on first run) + skill install [path] Install the bundled Archon skill into .claude/skills/archon validate workflows [name] Validate workflow definitions and their references validate commands [name] Validate command files version Show version info @@ -132,6 +134,8 @@ Examples: archon workflow run implement --branch feature-auth "Implement auth" archon workflow run quick-fix --no-worktree "Fix typo" archon continue fix/issue-42 --workflow archon-smart-pr-review "Review the changes" + archon skill install + archon skill install /path/to/project `); } @@ -236,7 +240,7 @@ async function main(): Promise { const subcommand = positionals[1]; // Commands that don't require git repo validation - const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue', 'serve']; + const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue', 'serve', 'skill']; const requiresGitRepo = !noGitCommands.includes(command ?? ''); try { @@ -569,6 +573,26 @@ async function main(): Promise { return await serveCommand({ port: servePort, downloadOnly }); } + case 'skill': { + switch (subcommand) { + case 'install': { + // Optional positional path; otherwise install into the resolved cwd. + const targetArg = positionals[2]; + const targetPath = targetArg ? resolve(targetArg) : cwd; + return await skillInstallCommand(targetPath); + } + + default: + if (subcommand === undefined) { + console.error('Missing skill subcommand'); + } else { + console.error(`Unknown skill subcommand: ${subcommand}`); + } + console.error('Available: install'); + return 1; + } + } + default: if (command === undefined) { console.error('Missing command'); diff --git a/packages/cli/src/commands/setup.test.ts b/packages/cli/src/commands/setup.test.ts index a0fa7373b5..0a7fc885d1 100644 --- a/packages/cli/src/commands/setup.test.ts +++ b/packages/cli/src/commands/setup.test.ts @@ -10,13 +10,13 @@ import { generateEnvContent, generateWebhookSecret, spawnTerminalWithSetup, - copyArchonSkill, detectClaudeExecutablePath, writeScopedEnv, serializeEnv, resolveScopedEnvPath, } from './setup'; import * as setupModule from './setup'; +import { copyArchonSkill } from './skill'; import { parse as parseDotenv } from 'dotenv'; // Test directory for file operations diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 2160a99d8a..3cb4d7898a 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -35,7 +35,7 @@ import { import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'fs'; import { parse as parseDotenv } from 'dotenv'; import { join, dirname } from 'path'; -import { BUNDLED_SKILL_FILES } from '../bundled-skill'; +import { copyArchonSkill } from './skill'; import { homedir } from 'os'; import { randomBytes } from 'crypto'; import { spawn, execSync, type ChildProcess } from 'child_process'; @@ -1444,23 +1444,6 @@ export function writeScopedEnv( return { targetPath, backupPath, preservedKeys, forced: options.force && exists }; } -/** - * Copy the bundled Archon skill files to /.claude/skills/archon/ - * - * Always overwrites existing files to ensure the latest skill version is installed. - */ -export function copyArchonSkill(targetPath: string): void { - const skillRoot = join(targetPath, '.claude', 'skills', 'archon'); - for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) { - const dest = join(skillRoot, relativePath); - const destDir = dirname(dest); - if (!existsSync(destDir)) { - mkdirSync(destDir, { recursive: true }); - } - writeFileSync(dest, content); - } -} - // ============================================================================= // Terminal Spawning // ============================================================================= diff --git a/packages/cli/src/commands/skill.test.ts b/packages/cli/src/commands/skill.test.ts new file mode 100644 index 0000000000..2583bf8f45 --- /dev/null +++ b/packages/cli/src/commands/skill.test.ts @@ -0,0 +1,85 @@ +/** + * Tests for skill install command + */ +import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { BUNDLED_SKILL_FILES } from '../bundled-skill'; +import { copyArchonSkill, skillInstallCommand } from './skill'; + +describe('copyArchonSkill', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'archon-skill-test-')); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('writes every bundled skill file under .claude/skills/archon/', () => { + copyArchonSkill(tempDir); + + const skillRoot = join(tempDir, '.claude', 'skills', 'archon'); + for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) { + const dest = join(skillRoot, relativePath); + expect(existsSync(dest)).toBe(true); + expect(readFileSync(dest, 'utf-8')).toBe(content); + } + }); + + it('overwrites pre-existing skill files with bundled content', () => { + const skillRoot = join(tempDir, '.claude', 'skills', 'archon'); + const skillMdPath = join(skillRoot, 'SKILL.md'); + + // Pre-seed with stale content; copyArchonSkill must overwrite it. + copyArchonSkill(tempDir); + writeFileSync(skillMdPath, 'STALE'); + expect(readFileSync(skillMdPath, 'utf-8')).toBe('STALE'); + + copyArchonSkill(tempDir); + expect(readFileSync(skillMdPath, 'utf-8')).toBe(BUNDLED_SKILL_FILES['SKILL.md']); + }); +}); + +describe('skillInstallCommand', () => { + let tempDir: string; + let logSpy: ReturnType; + let errSpy: ReturnType; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'archon-skill-cmd-test-')); + logSpy = spyOn(console, 'log').mockImplementation(() => {}); + errSpy = spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + logSpy.mockRestore(); + errSpy.mockRestore(); + }); + + it('returns 0 and installs the skill into the target directory', async () => { + const exitCode = await skillInstallCommand(tempDir); + + expect(exitCode).toBe(0); + expect(existsSync(join(tempDir, '.claude', 'skills', 'archon', 'SKILL.md'))).toBe(true); + // Final log line should mention restarting Claude Code + const lastLog = logSpy.mock.calls.at(-1)?.[0] as string | undefined; + expect(lastLog).toContain('Restart Claude Code'); + }); + + it('returns 1 and prints an error when the target directory does not exist', async () => { + const missing = join(tempDir, 'does-not-exist'); + const exitCode = await skillInstallCommand(missing); + + expect(exitCode).toBe(1); + expect(errSpy).toHaveBeenCalled(); + const firstError = errSpy.mock.calls[0][0] as string; + expect(firstError).toContain('Directory does not exist'); + // Nothing should have been written + expect(existsSync(join(missing, '.claude'))).toBe(false); + }); +}); diff --git a/packages/cli/src/commands/skill.ts b/packages/cli/src/commands/skill.ts new file mode 100644 index 0000000000..61ec425a72 --- /dev/null +++ b/packages/cli/src/commands/skill.ts @@ -0,0 +1,61 @@ +/** + * Skill command - Install bundled Archon skill files into a project + * + * Writes the bundled SKILL.md, guides, references and examples into + * /.claude/skills/archon/ so Claude Code picks up the skill + * the next time the project is opened. + * + * Always overwrites existing files to ensure the latest skill version + * shipped with the current Archon binary is installed. + */ +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { dirname, join, resolve } from 'path'; +import { BUNDLED_SKILL_FILES } from '../bundled-skill'; + +/** + * Copy the bundled Archon skill files to /.claude/skills/archon/ + * + * Pure file-system helper used by both the standalone `skill install` CLI + * command and the interactive setup wizard. + */ +export function copyArchonSkill(targetPath: string): void { + const skillRoot = join(targetPath, '.claude', 'skills', 'archon'); + for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) { + const dest = join(skillRoot, relativePath); + const destDir = dirname(dest); + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + writeFileSync(dest, content); + } +} + +/** + * Install the bundled Archon skill into a project directory. + * + * Returns an exit code: 0 on success, 1 on failure. + */ +export async function skillInstallCommand(targetPath: string): Promise { + const absoluteTarget = resolve(targetPath); + + if (!existsSync(absoluteTarget)) { + console.error(`Error: Directory does not exist: ${absoluteTarget}`); + return 1; + } + + const skillRoot = join(absoluteTarget, '.claude', 'skills', 'archon'); + const fileCount = Object.keys(BUNDLED_SKILL_FILES).length; + + console.log(`Installing Archon skill (${fileCount} files) into ${skillRoot}`); + + try { + copyArchonSkill(absoluteTarget); + } catch (error) { + const err = error as NodeJS.ErrnoException; + console.error(`Error: Failed to install skill: ${err.message}`); + return 1; + } + + console.log('Done. Restart Claude Code to load the skill.'); + return 0; +} From 01df8ffd2ef4553a25471b02a4b4d787db5e4773 Mon Sep 17 00:00:00 2001 From: Leex Date: Mon, 27 Apr 2026 20:32:30 +0200 Subject: [PATCH 2/3] docs: add `skill install` to CLAUDE.md, CLI reference, and skills guide - Add `skill install` command entries to CLAUDE.md CLI section - Add `skill install` section to docs-web CLI reference page - Add bundled Archon skill to Popular Skills table in skills guide Addresses HIGH findings from comprehensive PR review. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 4 ++++ .../docs-web/src/content/docs/guides/skills.md | 1 + .../docs-web/src/content/docs/reference/cli.md | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9988a4bc23..90c59b872e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -253,6 +253,10 @@ bun run cli serve bun run cli serve --port 4000 bun run cli serve --download-only # Download without starting +# Install the bundled Archon skill into a project +bun run cli skill install +bun run cli skill install /path/to/project + # Show version bun run cli version ``` diff --git a/packages/docs-web/src/content/docs/guides/skills.md b/packages/docs-web/src/content/docs/guides/skills.md index f64b6def3d..667f88562f 100644 --- a/packages/docs-web/src/content/docs/guides/skills.md +++ b/packages/docs-web/src/content/docs/guides/skills.md @@ -166,6 +166,7 @@ smaller box with a tastefully curated set of tools." | Skill | Install | What It Teaches | |-------|---------|----------------| +| `archon` (bundled) | `archon skill install` | Archon workflows, commands, and project conventions | | `remotion-best-practices` | `npx skills add remotion-dev/skills` | Remotion animation patterns, API usage, gotchas (35 rules) | | `skill-creator` | `npx skills add anthropics/skills` | How to create new SKILL.md files | | Community skills | Browse [skills.sh](https://skills.sh) | Search 500K+ skills for any domain | diff --git a/packages/docs-web/src/content/docs/reference/cli.md b/packages/docs-web/src/content/docs/reference/cli.md index adf0471c01..37790374cf 100644 --- a/packages/docs-web/src/content/docs/reference/cli.md +++ b/packages/docs-web/src/content/docs/reference/cli.md @@ -335,6 +335,20 @@ archon serve --download-only The cached web UI is stored at `~/.archon/web-dist//`. Each version is cached independently, so upgrading the binary automatically downloads the matching web UI. +### `skill install [path]` + +Install the bundled Archon skill files into a project's `.claude/skills/archon/` directory. Always overwrites existing files to ensure the latest version shipped with the current Archon binary is installed. + +```bash +# Install into the current directory +archon skill install + +# Install into a specific project +archon skill install /path/to/project +``` + +The Archon skill teaches Claude Code how to work with Archon workflows, commands, and project conventions. It is also installed automatically during `archon setup`. + ### `version` Show version, build type, and database info. From bc46041b29bbc81679d591cd67f9966edc0ff2d5 Mon Sep 17 00:00:00 2001 From: Thomas Ritter Date: Fri, 1 May 2026 12:05:11 +0200 Subject: [PATCH 3/3] fix(cli): guard bundled-skill import inside skillInstallCommand try block The dynamic `await import('../bundled-skill')` was outside the try/catch, so a load failure crashed uncaught instead of returning exit code 1. Move the import (and the success log + return) inside the try so import, copy, and post-copy errors all flow through the same controlled path. Addresses coderabbitai review on PR #1445. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/src/commands/skill.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/skill.ts b/packages/cli/src/commands/skill.ts index 2ebdae59e8..e759ab5a57 100644 --- a/packages/cli/src/commands/skill.ts +++ b/packages/cli/src/commands/skill.ts @@ -53,19 +53,17 @@ export async function skillInstallCommand(targetPath: string): Promise { } const skillRoot = join(absoluteTarget, '.claude', 'skills', 'archon'); - const { BUNDLED_SKILL_FILES } = await import('../bundled-skill'); - const fileCount = Object.keys(BUNDLED_SKILL_FILES).length; - - console.log(`Installing Archon skill (${fileCount} files) into ${skillRoot}`); - try { + const { BUNDLED_SKILL_FILES } = await import('../bundled-skill'); + const fileCount = Object.keys(BUNDLED_SKILL_FILES).length; + console.log(`Installing Archon skill (${fileCount} files) into ${skillRoot}`); + await copyArchonSkill(absoluteTarget); + console.log('Done. Restart Claude Code to load the skill.'); + return 0; } catch (error) { const err = error as NodeJS.ErrnoException; console.error(`Error: Failed to install skill: ${err.message}`); return 1; } - - console.log('Done. Restart Claude Code to load the skill.'); - return 0; }