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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -104,6 +105,7 @@ Commands:
continue <branch> [msg] Continue work on an existing worktree with prior context
complete <branch> [...] 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
Expand Down Expand Up @@ -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
`);
}

Expand Down Expand Up @@ -236,7 +240,7 @@ async function main(): Promise<number> {
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 {
Expand Down Expand Up @@ -569,6 +573,26 @@ async function main(): Promise<number> {
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');
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 1 addition & 27 deletions packages/cli/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'fs';
import { parse as parseDotenv } from 'dotenv';
import { join, dirname } from 'path';
import { copyArchonSkill } from './skill';
import { homedir } from 'os';
import { randomBytes } from 'crypto';
import { spawn, execSync, type ChildProcess } from 'child_process';
Expand Down Expand Up @@ -1443,33 +1444,6 @@ export function writeScopedEnv(
return { targetPath, backupPath, preservedKeys, forced: options.force && exists };
}

/**
* Copy the bundled Archon skill files to <targetPath>/.claude/skills/archon/
*
* Always overwrites existing files to ensure the latest skill version is installed.
*
* The `bundled-skill` module is dynamically imported here so that its 18 top-level
* `import … with { type: 'text' }` statements only execute when this function is
* actually called. Compiled binaries (`bun build --compile`) still statically
* analyze the literal-string `import()` and embed the chunk; linked-source
* installs (`bun link`) don't touch the source skill files unless the user runs
* `archon setup`. Without this indirection, every `archon` invocation —
* including `archon --help` — fails at module load when the source skill files
* are missing from disk.
*/
export async function copyArchonSkill(targetPath: string): Promise<void> {
const { BUNDLED_SKILL_FILES } = await import('../bundled-skill');
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
// =============================================================================
Expand Down
85 changes: 85 additions & 0 deletions packages/cli/src/commands/skill.test.ts
Original file line number Diff line number Diff line change
@@ -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/', async () => {
await 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', async () => {
const skillRoot = join(tempDir, '.claude', 'skills', 'archon');
const skillMdPath = join(skillRoot, 'SKILL.md');

// Pre-seed with stale content; copyArchonSkill must overwrite it.
await copyArchonSkill(tempDir);
writeFileSync(skillMdPath, 'STALE');
expect(readFileSync(skillMdPath, 'utf-8')).toBe('STALE');

await copyArchonSkill(tempDir);
expect(readFileSync(skillMdPath, 'utf-8')).toBe(BUNDLED_SKILL_FILES['SKILL.md']);
});
});

describe('skillInstallCommand', () => {
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errSpy: ReturnType<typeof spyOn>;

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);
});
});
69 changes: 69 additions & 0 deletions packages/cli/src/commands/skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Skill command - Install bundled Archon skill files into a project
*
* Writes the bundled SKILL.md, guides, references and examples into
* <targetPath>/.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';

/**
* Copy the bundled Archon skill files to <targetPath>/.claude/skills/archon/
*
* Pure file-system helper used by both the standalone `skill install` CLI
* command and the interactive setup wizard.
*
* The `bundled-skill` module is dynamically imported here so that its 18 top-level
* `import … with { type: 'text' }` statements only execute when this function is
* actually called. Compiled binaries (`bun build --compile`) still statically
* analyze the literal-string `import()` and embed the chunk; linked-source
* installs (`bun link`) don't touch the source skill files unless the user runs
* `archon setup` or `archon skill install`. Without this indirection, every
* `archon` invocation — including `archon --help` — fails at module load when
* the source skill files are missing from disk.
*/
export async function copyArchonSkill(targetPath: string): Promise<void> {
const { BUNDLED_SKILL_FILES } = await import('../bundled-skill');
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<number> {
const absoluteTarget = resolve(targetPath);

if (!existsSync(absoluteTarget)) {
console.error(`Error: Directory does not exist: ${absoluteTarget}`);
return 1;
}

const skillRoot = join(absoluteTarget, '.claude', 'skills', 'archon');
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions packages/docs-web/src/content/docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
14 changes: 14 additions & 0 deletions packages/docs-web/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ archon serve --download-only

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.

### `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.
Expand Down
Loading