@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.
- package/DEV.md +58 -0
- package/README.md +30 -0
- package/dist/commands/add.d.ts +13 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +111 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +40 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +53 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/remove.d.ts +13 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +129 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/show-dev.d.ts +20 -0
- package/dist/commands/show-dev.d.ts.map +1 -0
- package/dist/commands/show-dev.js +195 -0
- package/dist/commands/show-dev.js.map +1 -0
- package/dist/commands/show.d.ts +11 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +175 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/core/agent-detect.d.ts +22 -0
- package/dist/core/agent-detect.d.ts.map +1 -0
- package/dist/core/agent-detect.js +107 -0
- package/dist/core/agent-detect.js.map +1 -0
- package/dist/core/agents.d.ts +8 -0
- package/dist/core/agents.d.ts.map +1 -0
- package/dist/core/agents.js +34 -0
- package/dist/core/agents.js.map +1 -0
- package/dist/core/config-manager.d.ts +9 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +47 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/skill-hash.d.ts +12 -0
- package/dist/core/skill-hash.d.ts.map +1 -0
- package/dist/core/skill-hash.js +53 -0
- package/dist/core/skill-hash.js.map +1 -0
- package/dist/core/skill-info.d.ts +34 -0
- package/dist/core/skill-info.d.ts.map +1 -0
- package/dist/core/skill-info.js +213 -0
- package/dist/core/skill-info.js.map +1 -0
- package/dist/core/skill-install.d.ts +24 -0
- package/dist/core/skill-install.d.ts.map +1 -0
- package/dist/core/skill-install.js +123 -0
- package/dist/core/skill-install.js.map +1 -0
- package/dist/core/skill-source.d.ts +29 -0
- package/dist/core/skill-source.d.ts.map +1 -0
- package/dist/core/skill-source.js +111 -0
- package/dist/core/skill-source.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +57 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/fuzzy-match.d.ts +16 -0
- package/dist/utils/fuzzy-match.d.ts.map +1 -0
- package/dist/utils/fuzzy-match.js +37 -0
- package/dist/utils/fuzzy-match.js.map +1 -0
- package/dist/utils/prompts.d.ts +16 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +46 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/registry.d.ts +6 -0
- package/dist/utils/registry.d.ts.map +1 -0
- package/dist/utils/registry.js +14 -0
- package/dist/utils/registry.js.map +1 -0
- package/package.json +42 -0
- package/src/commands/add.ts +136 -0
- package/src/commands/config.ts +47 -0
- package/src/commands/list.ts +68 -0
- package/src/commands/remove.ts +154 -0
- package/src/commands/show-dev.ts +223 -0
- package/src/commands/show.ts +203 -0
- package/src/core/agent-detect.ts +125 -0
- package/src/core/agents.ts +40 -0
- package/src/core/config-manager.ts +55 -0
- package/src/core/skill-hash.ts +61 -0
- package/src/core/skill-info.ts +248 -0
- package/src/core/skill-install.ts +165 -0
- package/src/core/skill-source.ts +125 -0
- package/src/index.ts +116 -0
- package/src/types/index.ts +64 -0
- package/src/utils/fuzzy-match.ts +48 -0
- package/src/utils/prompts.ts +54 -0
- package/src/utils/registry.ts +16 -0
- package/test/README.md +123 -0
- package/test/fixtures/multi-skills/skill-one/SKILL.md +8 -0
- package/test/fixtures/multi-skills/skill-two/SKILL.md +8 -0
- package/test/fixtures/sample-skill/SKILL.md +8 -0
- package/test/logs/add-remove.log +108 -0
- package/test/logs/config.log +72 -0
- package/test/logs/fuzzy-match.log +64 -0
- package/test/logs/show.log +110 -0
- package/test/run-all.sh +83 -0
- package/test/test-add-remove.sh +245 -0
- package/test/test-config.sh +208 -0
- package/test/test-fuzzy-match.sh +166 -0
- package/test/test-show.sh +179 -0
- package/tsconfig.json +20 -0
- 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,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
|
+
============================================
|