@sundial-ai/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/DEV.md +58 -0
  2. package/README.md +30 -0
  3. package/dist/commands/add.d.ts +13 -0
  4. package/dist/commands/add.d.ts.map +1 -0
  5. package/dist/commands/add.js +111 -0
  6. package/dist/commands/add.js.map +1 -0
  7. package/dist/commands/config.d.ts +5 -0
  8. package/dist/commands/config.d.ts.map +1 -0
  9. package/dist/commands/config.js +40 -0
  10. package/dist/commands/config.js.map +1 -0
  11. package/dist/commands/list.d.ts +5 -0
  12. package/dist/commands/list.d.ts.map +1 -0
  13. package/dist/commands/list.js +53 -0
  14. package/dist/commands/list.js.map +1 -0
  15. package/dist/commands/remove.d.ts +13 -0
  16. package/dist/commands/remove.d.ts.map +1 -0
  17. package/dist/commands/remove.js +129 -0
  18. package/dist/commands/remove.js.map +1 -0
  19. package/dist/commands/show-dev.d.ts +20 -0
  20. package/dist/commands/show-dev.d.ts.map +1 -0
  21. package/dist/commands/show-dev.js +195 -0
  22. package/dist/commands/show-dev.js.map +1 -0
  23. package/dist/commands/show.d.ts +11 -0
  24. package/dist/commands/show.d.ts.map +1 -0
  25. package/dist/commands/show.js +175 -0
  26. package/dist/commands/show.js.map +1 -0
  27. package/dist/core/agent-detect.d.ts +22 -0
  28. package/dist/core/agent-detect.d.ts.map +1 -0
  29. package/dist/core/agent-detect.js +107 -0
  30. package/dist/core/agent-detect.js.map +1 -0
  31. package/dist/core/agents.d.ts +8 -0
  32. package/dist/core/agents.d.ts.map +1 -0
  33. package/dist/core/agents.js +34 -0
  34. package/dist/core/agents.js.map +1 -0
  35. package/dist/core/config-manager.d.ts +9 -0
  36. package/dist/core/config-manager.d.ts.map +1 -0
  37. package/dist/core/config-manager.js +47 -0
  38. package/dist/core/config-manager.js.map +1 -0
  39. package/dist/core/skill-hash.d.ts +12 -0
  40. package/dist/core/skill-hash.d.ts.map +1 -0
  41. package/dist/core/skill-hash.js +53 -0
  42. package/dist/core/skill-hash.js.map +1 -0
  43. package/dist/core/skill-info.d.ts +34 -0
  44. package/dist/core/skill-info.d.ts.map +1 -0
  45. package/dist/core/skill-info.js +213 -0
  46. package/dist/core/skill-info.js.map +1 -0
  47. package/dist/core/skill-install.d.ts +24 -0
  48. package/dist/core/skill-install.d.ts.map +1 -0
  49. package/dist/core/skill-install.js +123 -0
  50. package/dist/core/skill-install.js.map +1 -0
  51. package/dist/core/skill-source.d.ts +29 -0
  52. package/dist/core/skill-source.d.ts.map +1 -0
  53. package/dist/core/skill-source.js +111 -0
  54. package/dist/core/skill-source.js.map +1 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +104 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/types/index.d.ts +57 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/index.js +2 -0
  62. package/dist/types/index.js.map +1 -0
  63. package/dist/utils/fuzzy-match.d.ts +16 -0
  64. package/dist/utils/fuzzy-match.d.ts.map +1 -0
  65. package/dist/utils/fuzzy-match.js +37 -0
  66. package/dist/utils/fuzzy-match.js.map +1 -0
  67. package/dist/utils/prompts.d.ts +16 -0
  68. package/dist/utils/prompts.d.ts.map +1 -0
  69. package/dist/utils/prompts.js +46 -0
  70. package/dist/utils/prompts.js.map +1 -0
  71. package/dist/utils/registry.d.ts +6 -0
  72. package/dist/utils/registry.d.ts.map +1 -0
  73. package/dist/utils/registry.js +14 -0
  74. package/dist/utils/registry.js.map +1 -0
  75. package/package.json +42 -0
  76. package/src/commands/add.ts +136 -0
  77. package/src/commands/config.ts +47 -0
  78. package/src/commands/list.ts +68 -0
  79. package/src/commands/remove.ts +154 -0
  80. package/src/commands/show-dev.ts +223 -0
  81. package/src/commands/show.ts +203 -0
  82. package/src/core/agent-detect.ts +125 -0
  83. package/src/core/agents.ts +40 -0
  84. package/src/core/config-manager.ts +55 -0
  85. package/src/core/skill-hash.ts +61 -0
  86. package/src/core/skill-info.ts +248 -0
  87. package/src/core/skill-install.ts +165 -0
  88. package/src/core/skill-source.ts +125 -0
  89. package/src/index.ts +116 -0
  90. package/src/types/index.ts +64 -0
  91. package/src/utils/fuzzy-match.ts +48 -0
  92. package/src/utils/prompts.ts +54 -0
  93. package/src/utils/registry.ts +16 -0
  94. package/test/README.md +123 -0
  95. package/test/fixtures/multi-skills/skill-one/SKILL.md +8 -0
  96. package/test/fixtures/multi-skills/skill-two/SKILL.md +8 -0
  97. package/test/fixtures/sample-skill/SKILL.md +8 -0
  98. package/test/logs/add-remove.log +108 -0
  99. package/test/logs/config.log +72 -0
  100. package/test/logs/fuzzy-match.log +64 -0
  101. package/test/logs/show.log +110 -0
  102. package/test/run-all.sh +83 -0
  103. package/test/test-add-remove.sh +245 -0
  104. package/test/test-config.sh +208 -0
  105. package/test/test-fuzzy-match.sh +166 -0
  106. package/test/test-show.sh +179 -0
  107. package/tsconfig.json +20 -0
  108. package/vitest.config.ts +15 -0
@@ -0,0 +1,125 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { isShortcut, getShortcutUrl } from '../utils/registry.js';
4
+ import type { SkillSource } from '../types/index.js';
5
+
6
+ /**
7
+ * Check if input looks like a GitHub URL or reference.
8
+ * Matches: github.com/user/repo, https://github.com/..., etc.
9
+ */
10
+ export function isGithubUrl(input: string): boolean {
11
+ return input.includes('github.com');
12
+ }
13
+
14
+ /**
15
+ * Normalize a GitHub URL to degit format.
16
+ * Converts: https://github.com/user/repo/tree/branch/path -> user/repo/path#branch
17
+ */
18
+ function normalizeGithubUrl(url: string): string {
19
+ let location = url;
20
+
21
+ // Remove https:// or http:// prefix if present
22
+ location = location.replace(/^https?:\/\//, '');
23
+
24
+ // Handle github.com/user/repo/tree/branch/path format
25
+ // Convert to degit format: user/repo/path#branch
26
+ const treeMatch = location.match(/^github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
27
+ if (treeMatch) {
28
+ const [, user, repo, branch, subpath] = treeMatch;
29
+ location = `${user}/${repo}/${subpath}#${branch}`;
30
+ } else {
31
+ // Simple format: github.com/user/repo -> user/repo
32
+ location = location.replace(/^github\.com\//, '');
33
+ }
34
+
35
+ return location;
36
+ }
37
+
38
+ /**
39
+ * Check if input looks like a local file path.
40
+ * Matches: ./path, ../path, ~/path, /absolute/path
41
+ */
42
+ export function isLocalPath(input: string): boolean {
43
+ // Check if it starts with path indicators
44
+ if (input.startsWith('./') ||
45
+ input.startsWith('../') ||
46
+ input.startsWith('~/') ||
47
+ input.startsWith('/')) {
48
+ return true;
49
+ }
50
+
51
+ // Check if it's an existing path on disk
52
+ const resolved = path.resolve(input);
53
+ return fs.pathExistsSync(resolved);
54
+ }
55
+
56
+ /**
57
+ * Resolve a skill input to its source type and location.
58
+ *
59
+ * Resolution order:
60
+ * 1. Check if it's a registered shortcut (e.g., "tinker")
61
+ * 2. Check if it contains "github.com" (treat as GitHub URL)
62
+ * 3. Check if it's a valid local path
63
+ * 4. Otherwise, throw error
64
+ */
65
+ export function resolveSkillSource(input: string): SkillSource {
66
+ // 1. Check shortcuts first
67
+ if (isShortcut(input)) {
68
+ const url = getShortcutUrl(input)!;
69
+ return {
70
+ type: 'shortcut',
71
+ location: normalizeGithubUrl(url),
72
+ originalInput: input
73
+ };
74
+ }
75
+
76
+ // 2. Check if it's a GitHub URL
77
+ if (isGithubUrl(input)) {
78
+ return {
79
+ type: 'github',
80
+ location: normalizeGithubUrl(input),
81
+ originalInput: input
82
+ };
83
+ }
84
+
85
+ // 3. Check if it's a local path
86
+ if (isLocalPath(input)) {
87
+ // Resolve to absolute path, handling ~
88
+ let location = input;
89
+ if (location.startsWith('~/')) {
90
+ location = path.join(process.env.HOME || '', location.slice(2));
91
+ }
92
+ location = path.resolve(location);
93
+
94
+ return {
95
+ type: 'local',
96
+ location,
97
+ originalInput: input
98
+ };
99
+ }
100
+
101
+ // 4. Not found
102
+ throw new Error(`Skill not found: "${input}". Expected a shortcut name, GitHub URL, or local path.`);
103
+ }
104
+
105
+ /**
106
+ * Extract skill name from a source.
107
+ * For GitHub: last path segment
108
+ * For local: folder name
109
+ * For shortcut: the shortcut name itself
110
+ */
111
+ export function getSkillNameFromSource(source: SkillSource): string {
112
+ if (source.type === 'shortcut') {
113
+ return source.originalInput;
114
+ }
115
+
116
+ if (source.type === 'local') {
117
+ return path.basename(source.location);
118
+ }
119
+
120
+ // GitHub: extract from path (e.g., "user/repo/skills/tinker#main" -> "tinker")
121
+ const parts = source.location.split('/');
122
+ const lastPart = parts[parts.length - 1];
123
+ // Remove branch suffix if present (e.g., "tinker#main" -> "tinker")
124
+ return lastPart.split('#')[0];
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { SUPPORTED_AGENTS } from './core/agents.js';
6
+ import { addCommand } from './commands/add.js';
7
+ import { removeCommand } from './commands/remove.js';
8
+ import { listCommand } from './commands/list.js';
9
+ import { showCommand } from './commands/show.js';
10
+ import { configCommand } from './commands/config.js';
11
+ import { suggestCommand, getValidCommands } from './utils/fuzzy-match.js';
12
+ import type { CommandFlags } from './types/index.js';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('sun')
18
+ .description('Sundial CLI - Manage skills for your AI agents')
19
+ .version('0.1.0');
20
+
21
+ // Add command
22
+ const add = program
23
+ .command('add <skills...>')
24
+ .description('Add skill(s) to agent configuration(s)')
25
+ .option('--global', 'Install to global agent config (~/.claude/, ~/.codex/, etc.)');
26
+
27
+ // Add agent flags dynamically
28
+ for (const agent of SUPPORTED_AGENTS) {
29
+ add.option(`--${agent.flag}`, `Install to ${agent.name}`);
30
+ }
31
+
32
+ add.action(async (skills: string[], options: CommandFlags) => {
33
+ try {
34
+ await addCommand(skills, options);
35
+ } catch (error) {
36
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ // Remove command
42
+ const remove = program
43
+ .command('remove <skills...>')
44
+ .description('Remove skill(s) from agent configuration(s)')
45
+ .option('--global', 'Remove from global config');
46
+
47
+ for (const agent of SUPPORTED_AGENTS) {
48
+ remove.option(`--${agent.flag}`, `Remove from ${agent.name}`);
49
+ }
50
+
51
+ remove.action(async (skills: string[], options: CommandFlags) => {
52
+ try {
53
+ await removeCommand(skills, options);
54
+ } catch (error) {
55
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
56
+ process.exit(1);
57
+ }
58
+ });
59
+
60
+ // List command
61
+ program
62
+ .command('list')
63
+ .description('List all installed skills for each agent')
64
+ .action(async () => {
65
+ try {
66
+ await listCommand();
67
+ } catch (error) {
68
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+ // Show command
74
+ program
75
+ .command('show [skill]')
76
+ .description('Show all agent folders and packages, or details for a specific skill')
77
+ .action(async (skill?: string) => {
78
+ try {
79
+ await showCommand(skill);
80
+ } catch (error) {
81
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
82
+ process.exit(1);
83
+ }
84
+ });
85
+
86
+ // Config command
87
+ program
88
+ .command('config')
89
+ .description('Configure default agents')
90
+ .action(async () => {
91
+ try {
92
+ await configCommand();
93
+ } catch (error) {
94
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ // Handle unknown commands with fuzzy matching
100
+ program.on('command:*', (operands) => {
101
+ const unknownCommand = operands[0];
102
+ const suggestion = suggestCommand(unknownCommand);
103
+
104
+ console.error(chalk.red(`Error: Unknown command '${unknownCommand}'`));
105
+
106
+ if (suggestion) {
107
+ console.error(chalk.yellow(`Did you mean '${suggestion}'?`));
108
+ }
109
+
110
+ console.error();
111
+ console.error(`Valid commands: ${getValidCommands().join(', ')}`);
112
+ process.exit(1);
113
+ });
114
+
115
+ // Parse and execute
116
+ program.parse();
@@ -0,0 +1,64 @@
1
+ export type AgentType = 'claude' | 'codex' | 'gemini';
2
+
3
+ export interface AgentConfig {
4
+ name: string;
5
+ flag: string;
6
+ folderName: string;
7
+ }
8
+
9
+ export interface SunConfig {
10
+ defaultAgents: AgentType[];
11
+ firstRunComplete: boolean;
12
+ skillRegistryUrl?: string;
13
+ }
14
+
15
+ /**
16
+ * Skill metadata parsed from SKILL.md frontmatter.
17
+ * Per spec: https://agentskills.io/specification#skill-md-format
18
+ */
19
+ export interface SkillMetadata {
20
+ /** Required: Max 64 chars, lowercase letters, numbers, hyphens only */
21
+ name: string;
22
+ /** Required: Max 1024 chars, describes what the skill does */
23
+ description: string;
24
+ /** Optional: License name or reference to bundled license file */
25
+ license?: string;
26
+ /** Optional: Max 500 chars, environment requirements */
27
+ compatibility?: string;
28
+ /** Optional: Arbitrary key-value mapping (includes author, version, etc.) */
29
+ metadata?: Record<string, string>;
30
+ /** Optional: Space-delimited list of pre-approved tools (experimental) */
31
+ allowedTools?: string;
32
+ }
33
+
34
+ export type SkillSourceType = 'shortcut' | 'github' | 'local';
35
+
36
+ /** Used at install time to determine how to fetch a skill */
37
+ export interface SkillSource {
38
+ type: SkillSourceType;
39
+ /** The resolved location (URL for github/shortcut, path for local) */
40
+ location: string;
41
+ /** Original input string from user (e.g., "tinker" or "github.com/user/skill") */
42
+ originalInput: string;
43
+ }
44
+
45
+ export interface SkillInstallation {
46
+ agent: AgentType;
47
+ path: string;
48
+ isGlobal: boolean;
49
+ metadata: SkillMetadata;
50
+ contentHash: string;
51
+ }
52
+
53
+ export interface CommandFlags {
54
+ global?: boolean;
55
+ claude?: boolean;
56
+ codex?: boolean;
57
+ gemini?: boolean;
58
+ }
59
+
60
+ export interface DetectedAgent {
61
+ agent: AgentConfig;
62
+ path: string;
63
+ isGlobal: boolean;
64
+ }
@@ -0,0 +1,48 @@
1
+ import fuzzysort from 'fuzzysort';
2
+
3
+ const VALID_COMMANDS = ['add', 'remove', 'list', 'show', 'config'];
4
+
5
+ export interface FuzzyMatch {
6
+ command: string;
7
+ score: number;
8
+ }
9
+
10
+ /**
11
+ * Find the closest matching command using fuzzy search.
12
+ * fuzzysort scores: 0 = perfect match, negative = worse match
13
+ */
14
+ export function findClosestCommand(input: string): FuzzyMatch | null {
15
+ const results = fuzzysort.go(input, VALID_COMMANDS, {
16
+ // Allow any match (we'll filter by score later)
17
+ threshold: -Infinity
18
+ });
19
+
20
+ if (results.length === 0) {
21
+ return null;
22
+ }
23
+
24
+ const best = results[0];
25
+ return {
26
+ command: best.target,
27
+ score: best.score
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Suggest a command if the input is close enough to a valid command.
33
+ * Returns null if no good suggestion (avoids suggesting "add" for "xyz").
34
+ */
35
+ export function suggestCommand(input: string): string | null {
36
+ const match = findClosestCommand(input);
37
+
38
+ // Only suggest if score is reasonable (0=perfect, -100 is still decent for typos)
39
+ // Examples: "ad" -> "add" (good), "addd" -> "add" (good), "xyz" -> null (no suggestion)
40
+ if (match && match.score > -100) {
41
+ return match.command;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ export function getValidCommands(): string[] {
47
+ return [...VALID_COMMANDS];
48
+ }
@@ -0,0 +1,54 @@
1
+ import { checkbox, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { SUPPORTED_AGENTS } from '../core/agents.js';
4
+ import type { AgentType } from '../types/index.js';
5
+
6
+ /**
7
+ * Show an interactive checkbox UI for selecting default agents.
8
+ *
9
+ * @param currentDefaults - Previously saved defaults (empty on first run)
10
+ * @returns Array of selected agent flags (e.g., ['claude', 'codex'])
11
+ *
12
+ * Behavior:
13
+ * - Shows all supported agent TYPES from SUPPORTED_AGENTS
14
+ * - First run (currentDefaults empty): ALL agents are pre-selected
15
+ * - Subsequent runs: Only previously saved defaults are pre-selected
16
+ * - Must select at least one agent
17
+ */
18
+ export async function promptAgentSelection(
19
+ currentDefaults: AgentType[] = []
20
+ ): Promise<AgentType[]> {
21
+ const isFirstRun = currentDefaults.length === 0;
22
+
23
+ console.log(chalk.gray('Use space to toggle, enter to confirm:\n'));
24
+
25
+ // Build choices from SUPPORTED_AGENTS constants
26
+ // Show paths so users understand what each agent refers to
27
+ const choices = SUPPORTED_AGENTS.map(agent => ({
28
+ name: `${agent.name} (~/${agent.folderName}/ and ./${agent.folderName}/)`,
29
+ value: agent.flag as AgentType,
30
+ // First run: select ALL agents
31
+ // Otherwise: only select if it was in previous defaults
32
+ checked: isFirstRun ? true : currentDefaults.includes(agent.flag as AgentType)
33
+ }));
34
+
35
+ const selectedAgents = await checkbox({
36
+ message: 'Select default agents:',
37
+ choices,
38
+ required: true,
39
+ theme: {
40
+ style: {
41
+ keysHelpTip: () => undefined
42
+ }
43
+ }
44
+ });
45
+
46
+ return selectedAgents;
47
+ }
48
+
49
+ export async function confirmAction(message: string): Promise<boolean> {
50
+ return confirm({
51
+ message,
52
+ default: true
53
+ });
54
+ }
@@ -0,0 +1,16 @@
1
+ /** Skill shortcuts registry - maps short names to GitHub URLs */
2
+ export const SKILL_SHORTCUTS: Record<string, string> = {
3
+ tinker: 'https://github.com/sundial-org/skills/tree/main/skills/tinker'
4
+ };
5
+
6
+ export function isShortcut(skill: string): boolean {
7
+ return skill in SKILL_SHORTCUTS;
8
+ }
9
+
10
+ export function getShortcutUrl(skill: string): string | undefined {
11
+ return SKILL_SHORTCUTS[skill];
12
+ }
13
+
14
+ export function listShortcuts(): string[] {
15
+ return Object.keys(SKILL_SHORTCUTS);
16
+ }
package/test/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Sundial CLI Integration Tests
2
+
3
+ 🚧 Warning: these tests were auto-generated by Claude Code.
4
+
5
+ This directory contains bash-based integration tests for the Sundial CLI (`sun`).
6
+
7
+ ## Prerequisites
8
+
9
+ Before running tests, build the CLI:
10
+
11
+ ```bash
12
+ cd cli
13
+ npm run build
14
+ ```
15
+
16
+ ## Running Tests
17
+
18
+ Run all tests:
19
+
20
+ ```bash
21
+ ./test/run-all.sh
22
+ ```
23
+
24
+ Or run `npm test` from the `cli/` directory.
25
+
26
+ Run individual tests:
27
+
28
+ ```bash
29
+ ./test/test-add-remove.sh
30
+ ./test/test-config.sh
31
+ ./test/test-show.sh
32
+ ./test/test-fuzzy-match.sh
33
+ ```
34
+
35
+ ## Test Files
36
+
37
+ | File | Description |
38
+ |------|-------------|
39
+ | `run-all.sh` | Master test runner - runs all tests and reports results |
40
+ | `test-add-remove.sh` | Tests `sun add` and `sun remove` commands |
41
+ | `test-config.sh` | Tests configuration management |
42
+ | `test-show.sh` | Tests `sun show` command |
43
+ | `test-fuzzy-match.sh` | Tests typo suggestions for misspelled commands |
44
+
45
+ ## Test Output
46
+
47
+ All tests log their output to the `logs/` directory:
48
+
49
+ - `logs/add-remove.log`
50
+ - `logs/config.log`
51
+ - `logs/show.log`
52
+ - `logs/fuzzy-match.log`
53
+
54
+ These logs contain the full CLI output and PASS/FAIL assertions for each test case.
55
+
56
+ ## Test Structure
57
+
58
+ Each test script:
59
+
60
+ 1. Creates isolated temp directories for `HOME` and working directory
61
+ 2. Sets up any required config files
62
+ 3. Runs CLI commands and captures output
63
+ 4. Asserts on directory structure (folders exist/don't exist)
64
+ 5. Asserts on CLI output (contains expected strings)
65
+ 6. Cleans up temp directories on exit
66
+
67
+ ## What's Tested
68
+
69
+ ### test-add-remove.sh
70
+
71
+ - **Local folder**: Add skill from local path
72
+ - **Multi-skills**: Add multiple skills from parent directory
73
+ - **Git**: Add skills from git repository (network dependent)
74
+ - **Shortcuts**: Add skills via registry shortcuts (registry dependent)
75
+ - **Local vs Global**: `--global` flag behavior
76
+ - **Agent tags**: `--claude`, `--codex`, `--gemini` flags
77
+ - **Remove**: Remove from single and multiple agents
78
+
79
+ ### test-config.sh
80
+
81
+ - Config directory creation (`~/.sun/`)
82
+ - Config file parsing
83
+ - Default agents affect `add` command behavior
84
+ - Explicit flags override config defaults
85
+
86
+ ### test-show.sh
87
+
88
+ - Show with no agent folders
89
+ - Show with empty agent folder
90
+ - Show with installed skills
91
+ - Show specific skill details
92
+ - Show skill in multiple locations (local + global)
93
+ - Show non-existent skill error
94
+
95
+ ### test-fuzzy-match.sh
96
+
97
+ - Typo suggestions: `ad` → `add`, `remov` → `remove`, etc.
98
+ - "Did you mean" messages
99
+ - Valid commands list displayed
100
+ - Non-zero exit code for unknown commands
101
+
102
+ ## Fixtures
103
+
104
+ Test fixtures are in `fixtures/`:
105
+
106
+ ```
107
+ fixtures/
108
+ ├── sample-skill/
109
+ │ └── SKILL.md # Single test skill
110
+ └── multi-skills/
111
+ ├── skill-one/
112
+ │ └── SKILL.md # First skill in multi-skill dir
113
+ └── skill-two/
114
+ └── SKILL.md # Second skill in multi-skill dir
115
+ ```
116
+
117
+ ## Adding New Tests
118
+
119
+ 1. Create a new `test-*.sh` file
120
+ 2. Add header documentation following the existing pattern
121
+ 3. Use the helper functions: `log()`, `assert_dir_exists()`, `assert_output_contains()`
122
+ 4. Add cleanup trap: `trap cleanup EXIT`
123
+ 5. Add the test to the `TESTS` array in `run-all.sh`
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: skill-one
3
+ description: First skill in multi-skills directory
4
+ ---
5
+
6
+ # skill-one
7
+
8
+ This is the first test skill in a multi-skill directory.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: skill-two
3
+ description: Second skill in multi-skills directory
4
+ ---
5
+
6
+ # skill-two
7
+
8
+ This is the second test skill in a multi-skill directory.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sample-skill
3
+ description: A sample skill for testing the CLI
4
+ ---
5
+
6
+ # sample-skill
7
+
8
+ This is a test skill used by the CLI integration tests.
@@ -0,0 +1,108 @@
1
+ === Test run: Wed Jan 14 14:16:55 PST 2026 ===
2
+ TEMP_DIR: /var/folders/64/t_nm43xn2mx3jnxm5pfyxrd80000gn/T/tmp.IvMSm6iqd9
3
+ TEMP_HOME: /var/folders/64/t_nm43xn2mx3jnxm5pfyxrd80000gn/T/tmp.UUle90HZhY
4
+
5
+ TEST: Add single skill from local folder
6
+ ----------------------------------------
7
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
8
+ ✔ Added sample-skill
9
+
10
+ Added sample-skill to Claude Code (local)
11
+ (use --global to install globally)
12
+ PASS: Directory exists: .claude/skills/sample-skill
13
+ PASS: File exists: .claude/skills/sample-skill/SKILL.md
14
+
15
+ TEST: Add multiple skills from parent directory
16
+ ------------------------------------------------
17
+ - Adding /Users/bmo/skills/cli/test/fixtures/multi-skills...
18
+ ✔ Added skill-one, skill-two
19
+
20
+ Added skill-one, skill-two to Claude Code (local)
21
+ (use --global to install globally)
22
+ PASS: Directory exists: .claude/skills/skill-one
23
+ PASS: Directory exists: .claude/skills/skill-two
24
+ PASS: File exists: .claude/skills/skill-one/SKILL.md
25
+ PASS: File exists: .claude/skills/skill-two/SKILL.md
26
+
27
+ TEST: Add skill from git repository
28
+ ------------------------------------
29
+ - Adding github:sundial-org/skills...
30
+ ✖ Failed to add github:sundial-org/skills: Skill not found: "github:sundial-org/skills". Expected a shortcut name, GitHub URL, or local path.
31
+ SKIP: No skills installed (may need network)
32
+
33
+ TEST: Add skill via shortcut (registry)
34
+ ----------------------------------------
35
+ - Adding tinker-from-docs...
36
+ ✖ Failed to add tinker-from-docs: Skill not found: "tinker-from-docs". Expected a shortcut name, GitHub URL, or local path.
37
+ SKIP: Shortcut not available in registry
38
+
39
+ TEST: Local vs Global installation
40
+ -----------------------------------
41
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
42
+ ✔ Added sample-skill
43
+
44
+ Added sample-skill to Claude Code (local)
45
+ (use --global to install globally)
46
+ PASS: Directory exists: .claude/skills/sample-skill
47
+ Local installation verified
48
+ - Adding /Users/bmo/skills/cli/test/fixtures/multi-skills/skill-one...
49
+ ✔ Added skill-one
50
+
51
+ Added skill-one to Claude Code (global)
52
+ PASS: Directory exists: /var/folders/64/t_nm43xn2mx3jnxm5pfyxrd80000gn/T/tmp.UUle90HZhY/.claude/skills/skill-one
53
+ Global installation verified
54
+ PASS: Directory exists: .claude/skills/sample-skill
55
+ PASS: Directory does not exist: .claude/skills/skill-one
56
+ PASS: Directory exists: /var/folders/64/t_nm43xn2mx3jnxm5pfyxrd80000gn/T/tmp.UUle90HZhY/.claude/skills/skill-one
57
+ PASS: Directory does not exist: /var/folders/64/t_nm43xn2mx3jnxm5pfyxrd80000gn/T/tmp.UUle90HZhY/.claude/skills/sample-skill
58
+
59
+ TEST: Multi-agent installation (--claude --codex)
60
+ --------------------------------------------------
61
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
62
+ ✔ Added sample-skill
63
+
64
+ Added sample-skill to Claude Code and Codex (local)
65
+ (use --global to install globally)
66
+ PASS: Directory exists: .claude/skills/sample-skill
67
+ PASS: Directory exists: .codex/skills/sample-skill
68
+
69
+ TEST: Single agent flag (--gemini only)
70
+ ----------------------------------------
71
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
72
+ ✔ Added sample-skill
73
+
74
+ Added sample-skill to Gemini (local)
75
+ (use --global to install globally)
76
+ PASS: Directory exists: .gemini/skills/sample-skill
77
+ PASS: Directory does not exist: .claude/skills/sample-skill
78
+ PASS: Directory does not exist: .codex/skills/sample-skill
79
+
80
+ TEST: Remove skill from single agent
81
+ -------------------------------------
82
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
83
+ ✔ Added sample-skill
84
+
85
+ Added sample-skill to Claude Code (local)
86
+ (use --global to install globally)
87
+ PASS: Directory exists: .claude/skills/sample-skill
88
+ - Removing sample-skill...
89
+ ✔ Removed sample-skill from Claude Code
90
+ PASS: Directory does not exist: .claude/skills/sample-skill
91
+
92
+ TEST: Remove skill from multiple agents
93
+ ----------------------------------------
94
+ - Adding /Users/bmo/skills/cli/test/fixtures/sample-skill...
95
+ ✔ Added sample-skill
96
+
97
+ Added sample-skill to Claude Code and Codex (local)
98
+ (use --global to install globally)
99
+ PASS: Directory exists: .claude/skills/sample-skill
100
+ PASS: Directory exists: .codex/skills/sample-skill
101
+ - Removing sample-skill...
102
+ ✔ Removed sample-skill from Claude Code and Codex
103
+ PASS: Directory does not exist: .claude/skills/sample-skill
104
+ PASS: Directory does not exist: .codex/skills/sample-skill
105
+
106
+ ============================================
107
+ All add-remove tests passed!
108
+ ============================================