Skip to content

Commit 4631b8e

Browse files
leex279claudecrownpeak-thomasritter
authored
feat(cli): add archon skill install command (#1445)
* feat(cli): add `archon skill install` command Adds a standalone `archon skill install [path]` subcommand that copies the bundled Archon skill files into `<target>/.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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Thomas Ritter <thomas.ritter@crownpeak.com>
1 parent 1820a35 commit 4631b8e

9 files changed

Lines changed: 201 additions & 30 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ bun run cli serve
253253
bun run cli serve --port 4000
254254
bun run cli serve --download-only # Download without starting
255255

256+
# Install the bundled Archon skill into a project
257+
bun run cli skill install
258+
bun run cli skill install /path/to/project
259+
256260
# Show version
257261
bun run cli version
258262
```

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"scripts": {
1010
"cli": "bun src/cli.ts",
11-
"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",
11+
"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",
1212
"type-check": "bun x tsc --noEmit"
1313
},
1414
"dependencies": {

packages/cli/src/cli.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
import { continueCommand } from './commands/continue';
6363
import { chatCommand } from './commands/chat';
6464
import { setupCommand } from './commands/setup';
65+
import { skillInstallCommand } from './commands/skill';
6566
import { validateWorkflowsCommand, validateCommandsCommand } from './commands/validate';
6667
import { serveCommand } from './commands/serve';
6768
import { closeDatabase } from '@archon/core';
@@ -104,6 +105,7 @@ Commands:
104105
continue <branch> [msg] Continue work on an existing worktree with prior context
105106
complete <branch> [...] Complete branch lifecycle (remove worktree + branches)
106107
serve Start the web UI server (downloads web UI on first run)
108+
skill install [path] Install the bundled Archon skill into .claude/skills/archon
107109
validate workflows [name] Validate workflow definitions and their references
108110
validate commands [name] Validate command files
109111
version Show version info
@@ -132,6 +134,8 @@ Examples:
132134
archon workflow run implement --branch feature-auth "Implement auth"
133135
archon workflow run quick-fix --no-worktree "Fix typo"
134136
archon continue fix/issue-42 --workflow archon-smart-pr-review "Review the changes"
137+
archon skill install
138+
archon skill install /path/to/project
135139
`);
136140
}
137141

@@ -236,7 +240,7 @@ async function main(): Promise<number> {
236240
const subcommand = positionals[1];
237241

238242
// Commands that don't require git repo validation
239-
const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue', 'serve'];
243+
const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue', 'serve', 'skill'];
240244
const requiresGitRepo = !noGitCommands.includes(command ?? '');
241245

242246
try {
@@ -569,6 +573,26 @@ async function main(): Promise<number> {
569573
return await serveCommand({ port: servePort, downloadOnly });
570574
}
571575

576+
case 'skill': {
577+
switch (subcommand) {
578+
case 'install': {
579+
// Optional positional path; otherwise install into the resolved cwd.
580+
const targetArg = positionals[2];
581+
const targetPath = targetArg ? resolve(targetArg) : cwd;
582+
return await skillInstallCommand(targetPath);
583+
}
584+
585+
default:
586+
if (subcommand === undefined) {
587+
console.error('Missing skill subcommand');
588+
} else {
589+
console.error(`Unknown skill subcommand: ${subcommand}`);
590+
}
591+
console.error('Available: install');
592+
return 1;
593+
}
594+
}
595+
572596
default:
573597
if (command === undefined) {
574598
console.error('Missing command');

packages/cli/src/commands/setup.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import {
1010
generateEnvContent,
1111
generateWebhookSecret,
1212
spawnTerminalWithSetup,
13-
copyArchonSkill,
1413
detectClaudeExecutablePath,
1514
writeScopedEnv,
1615
serializeEnv,
1716
resolveScopedEnvPath,
1817
} from './setup';
1918
import * as setupModule from './setup';
19+
import { copyArchonSkill } from './skill';
2020
import { parse as parseDotenv } from 'dotenv';
2121

2222
// Test directory for file operations

packages/cli/src/commands/setup.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'fs';
3636
import { parse as parseDotenv } from 'dotenv';
3737
import { join, dirname } from 'path';
38+
import { copyArchonSkill } from './skill';
3839
import { homedir } from 'os';
3940
import { randomBytes } from 'crypto';
4041
import { spawn, execSync, type ChildProcess } from 'child_process';
@@ -1443,33 +1444,6 @@ export function writeScopedEnv(
14431444
return { targetPath, backupPath, preservedKeys, forced: options.force && exists };
14441445
}
14451446

1446-
/**
1447-
* Copy the bundled Archon skill files to <targetPath>/.claude/skills/archon/
1448-
*
1449-
* Always overwrites existing files to ensure the latest skill version is installed.
1450-
*
1451-
* The `bundled-skill` module is dynamically imported here so that its 18 top-level
1452-
* `import … with { type: 'text' }` statements only execute when this function is
1453-
* actually called. Compiled binaries (`bun build --compile`) still statically
1454-
* analyze the literal-string `import()` and embed the chunk; linked-source
1455-
* installs (`bun link`) don't touch the source skill files unless the user runs
1456-
* `archon setup`. Without this indirection, every `archon` invocation —
1457-
* including `archon --help` — fails at module load when the source skill files
1458-
* are missing from disk.
1459-
*/
1460-
export async function copyArchonSkill(targetPath: string): Promise<void> {
1461-
const { BUNDLED_SKILL_FILES } = await import('../bundled-skill');
1462-
const skillRoot = join(targetPath, '.claude', 'skills', 'archon');
1463-
for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) {
1464-
const dest = join(skillRoot, relativePath);
1465-
const destDir = dirname(dest);
1466-
if (!existsSync(destDir)) {
1467-
mkdirSync(destDir, { recursive: true });
1468-
}
1469-
writeFileSync(dest, content);
1470-
}
1471-
}
1472-
14731447
// =============================================================================
14741448
// Terminal Spawning
14751449
// =============================================================================
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Tests for skill install command
3+
*/
4+
import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test';
5+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
6+
import { tmpdir } from 'os';
7+
import { join } from 'path';
8+
import { BUNDLED_SKILL_FILES } from '../bundled-skill';
9+
import { copyArchonSkill, skillInstallCommand } from './skill';
10+
11+
describe('copyArchonSkill', () => {
12+
let tempDir: string;
13+
14+
beforeEach(() => {
15+
tempDir = mkdtempSync(join(tmpdir(), 'archon-skill-test-'));
16+
});
17+
18+
afterEach(() => {
19+
rmSync(tempDir, { recursive: true, force: true });
20+
});
21+
22+
it('writes every bundled skill file under .claude/skills/archon/', async () => {
23+
await copyArchonSkill(tempDir);
24+
25+
const skillRoot = join(tempDir, '.claude', 'skills', 'archon');
26+
for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) {
27+
const dest = join(skillRoot, relativePath);
28+
expect(existsSync(dest)).toBe(true);
29+
expect(readFileSync(dest, 'utf-8')).toBe(content);
30+
}
31+
});
32+
33+
it('overwrites pre-existing skill files with bundled content', async () => {
34+
const skillRoot = join(tempDir, '.claude', 'skills', 'archon');
35+
const skillMdPath = join(skillRoot, 'SKILL.md');
36+
37+
// Pre-seed with stale content; copyArchonSkill must overwrite it.
38+
await copyArchonSkill(tempDir);
39+
writeFileSync(skillMdPath, 'STALE');
40+
expect(readFileSync(skillMdPath, 'utf-8')).toBe('STALE');
41+
42+
await copyArchonSkill(tempDir);
43+
expect(readFileSync(skillMdPath, 'utf-8')).toBe(BUNDLED_SKILL_FILES['SKILL.md']);
44+
});
45+
});
46+
47+
describe('skillInstallCommand', () => {
48+
let tempDir: string;
49+
let logSpy: ReturnType<typeof spyOn>;
50+
let errSpy: ReturnType<typeof spyOn>;
51+
52+
beforeEach(() => {
53+
tempDir = mkdtempSync(join(tmpdir(), 'archon-skill-cmd-test-'));
54+
logSpy = spyOn(console, 'log').mockImplementation(() => {});
55+
errSpy = spyOn(console, 'error').mockImplementation(() => {});
56+
});
57+
58+
afterEach(() => {
59+
rmSync(tempDir, { recursive: true, force: true });
60+
logSpy.mockRestore();
61+
errSpy.mockRestore();
62+
});
63+
64+
it('returns 0 and installs the skill into the target directory', async () => {
65+
const exitCode = await skillInstallCommand(tempDir);
66+
67+
expect(exitCode).toBe(0);
68+
expect(existsSync(join(tempDir, '.claude', 'skills', 'archon', 'SKILL.md'))).toBe(true);
69+
// Final log line should mention restarting Claude Code
70+
const lastLog = logSpy.mock.calls.at(-1)?.[0] as string | undefined;
71+
expect(lastLog).toContain('Restart Claude Code');
72+
});
73+
74+
it('returns 1 and prints an error when the target directory does not exist', async () => {
75+
const missing = join(tempDir, 'does-not-exist');
76+
const exitCode = await skillInstallCommand(missing);
77+
78+
expect(exitCode).toBe(1);
79+
expect(errSpy).toHaveBeenCalled();
80+
const firstError = errSpy.mock.calls[0][0] as string;
81+
expect(firstError).toContain('Directory does not exist');
82+
// Nothing should have been written
83+
expect(existsSync(join(missing, '.claude'))).toBe(false);
84+
});
85+
});

packages/cli/src/commands/skill.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Skill command - Install bundled Archon skill files into a project
3+
*
4+
* Writes the bundled SKILL.md, guides, references and examples into
5+
* <targetPath>/.claude/skills/archon/ so Claude Code picks up the skill
6+
* the next time the project is opened.
7+
*
8+
* Always overwrites existing files to ensure the latest skill version
9+
* shipped with the current Archon binary is installed.
10+
*/
11+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
12+
import { dirname, join, resolve } from 'path';
13+
14+
/**
15+
* Copy the bundled Archon skill files to <targetPath>/.claude/skills/archon/
16+
*
17+
* Pure file-system helper used by both the standalone `skill install` CLI
18+
* command and the interactive setup wizard.
19+
*
20+
* The `bundled-skill` module is dynamically imported here so that its 18 top-level
21+
* `import … with { type: 'text' }` statements only execute when this function is
22+
* actually called. Compiled binaries (`bun build --compile`) still statically
23+
* analyze the literal-string `import()` and embed the chunk; linked-source
24+
* installs (`bun link`) don't touch the source skill files unless the user runs
25+
* `archon setup` or `archon skill install`. Without this indirection, every
26+
* `archon` invocation — including `archon --help` — fails at module load when
27+
* the source skill files are missing from disk.
28+
*/
29+
export async function copyArchonSkill(targetPath: string): Promise<void> {
30+
const { BUNDLED_SKILL_FILES } = await import('../bundled-skill');
31+
const skillRoot = join(targetPath, '.claude', 'skills', 'archon');
32+
for (const [relativePath, content] of Object.entries(BUNDLED_SKILL_FILES)) {
33+
const dest = join(skillRoot, relativePath);
34+
const destDir = dirname(dest);
35+
if (!existsSync(destDir)) {
36+
mkdirSync(destDir, { recursive: true });
37+
}
38+
writeFileSync(dest, content);
39+
}
40+
}
41+
42+
/**
43+
* Install the bundled Archon skill into a project directory.
44+
*
45+
* Returns an exit code: 0 on success, 1 on failure.
46+
*/
47+
export async function skillInstallCommand(targetPath: string): Promise<number> {
48+
const absoluteTarget = resolve(targetPath);
49+
50+
if (!existsSync(absoluteTarget)) {
51+
console.error(`Error: Directory does not exist: ${absoluteTarget}`);
52+
return 1;
53+
}
54+
55+
const skillRoot = join(absoluteTarget, '.claude', 'skills', 'archon');
56+
try {
57+
const { BUNDLED_SKILL_FILES } = await import('../bundled-skill');
58+
const fileCount = Object.keys(BUNDLED_SKILL_FILES).length;
59+
console.log(`Installing Archon skill (${fileCount} files) into ${skillRoot}`);
60+
61+
await copyArchonSkill(absoluteTarget);
62+
console.log('Done. Restart Claude Code to load the skill.');
63+
return 0;
64+
} catch (error) {
65+
const err = error as NodeJS.ErrnoException;
66+
console.error(`Error: Failed to install skill: ${err.message}`);
67+
return 1;
68+
}
69+
}

packages/docs-web/src/content/docs/guides/skills.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ smaller box with a tastefully curated set of tools."
166166

167167
| Skill | Install | What It Teaches |
168168
|-------|---------|----------------|
169+
| `archon` (bundled) | `archon skill install` | Archon workflows, commands, and project conventions |
169170
| `remotion-best-practices` | `npx skills add remotion-dev/skills` | Remotion animation patterns, API usage, gotchas (35 rules) |
170171
| `skill-creator` | `npx skills add anthropics/skills` | How to create new SKILL.md files |
171172
| Community skills | Browse [skills.sh](https://skills.sh) | Search 500K+ skills for any domain |

packages/docs-web/src/content/docs/reference/cli.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,20 @@ archon serve --download-only
335335

336336
The cached web UI is stored at `~/.archon/web-dist/<version>/`. Each version is cached independently, so upgrading the binary automatically downloads the matching web UI.
337337

338+
### `skill install [path]`
339+
340+
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.
341+
342+
```bash
343+
# Install into the current directory
344+
archon skill install
345+
346+
# Install into a specific project
347+
archon skill install /path/to/project
348+
```
349+
350+
The Archon skill teaches Claude Code how to work with Archon workflows, commands, and project conventions. It is also installed automatically during `archon setup`.
351+
338352
### `version`
339353

340354
Show version, build type, and database info.

0 commit comments

Comments
 (0)