-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(cli): add archon skill install command #1445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
83822ea
feat(cli): add `archon skill install` command
leex279 01df8ff
docs: add `skill install` to CLAUDE.md, CLI reference, and skills guide
leex279 12ce692
Merge branch 'dev' into feat/cli-skill-install
crownpeak-thomasritter bc46041
fix(cli): guard bundled-skill import inside skillInstallCommand try b…
crownpeak-thomasritter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.