@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,47 @@
1
+ import chalk from 'chalk';
2
+ import { getSupportedAgentsMessage, SUPPORTED_AGENTS } from '../core/agents.js';
3
+ import { loadConfig, setDefaultAgents, getConfigPath } from '../core/config-manager.js';
4
+ import { promptAgentSelection } from '../utils/prompts.js';
5
+
6
+ /**
7
+ * Re-open agent selection dialog and update config.
8
+ */
9
+ export async function configCommand(): Promise<void> {
10
+ const config = await loadConfig();
11
+
12
+ console.log(chalk.cyan('Sundial CLI Configuration'));
13
+ console.log(chalk.gray(`Config file: ${getConfigPath()}`));
14
+ console.log();
15
+
16
+ // Show current defaults
17
+ if (config.defaultAgents.length > 0) {
18
+ console.log('Current default agents:');
19
+ for (const agentFlag of config.defaultAgents) {
20
+ const agent = SUPPORTED_AGENTS.find(a => a.flag === agentFlag);
21
+ if (agent) {
22
+ console.log(` - ${agent.name} ${chalk.gray(`(~/${agent.folderName}/ and ./${agent.folderName}/)`)}`);
23
+ }
24
+ }
25
+ console.log();
26
+ }
27
+
28
+ console.log(chalk.gray(getSupportedAgentsMessage()));
29
+ console.log();
30
+
31
+ // Show agent type selection dialog
32
+ const selectedAgents = await promptAgentSelection(config.defaultAgents);
33
+
34
+ await setDefaultAgents(selectedAgents);
35
+
36
+ console.log();
37
+ console.log(chalk.green('Configuration saved!'));
38
+
39
+ // Show new defaults
40
+ console.log('New default agents:');
41
+ for (const agentFlag of selectedAgents) {
42
+ const agent = SUPPORTED_AGENTS.find(a => a.flag === agentFlag);
43
+ if (agent) {
44
+ console.log(` - ${agent.name} ${chalk.gray(`(~/${agent.folderName}/ and ./${agent.folderName}/)`)}`);
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import { SUPPORTED_AGENTS, getSupportedAgentsMessage } from '../core/agents.js';
3
+ import { listSkillsForAgent } from '../core/skill-info.js';
4
+
5
+ interface AgentSkills {
6
+ agentName: string;
7
+ folderName: string;
8
+ isGlobal: boolean;
9
+ skills: string[];
10
+ }
11
+
12
+ /**
13
+ * List all installed skills for each agent.
14
+ */
15
+ export async function listCommand(): Promise<void> {
16
+ const allAgentSkills: AgentSkills[] = [];
17
+
18
+ // Check both local and global for each agent
19
+ for (const agent of SUPPORTED_AGENTS) {
20
+ // Local
21
+ const localSkills = await listSkillsForAgent(agent.folderName, false);
22
+ if (localSkills.length > 0) {
23
+ allAgentSkills.push({
24
+ agentName: agent.name,
25
+ folderName: agent.folderName,
26
+ isGlobal: false,
27
+ skills: localSkills
28
+ });
29
+ }
30
+
31
+ // Global
32
+ const globalSkills = await listSkillsForAgent(agent.folderName, true);
33
+ if (globalSkills.length > 0) {
34
+ allAgentSkills.push({
35
+ agentName: agent.name,
36
+ folderName: agent.folderName,
37
+ isGlobal: true,
38
+ skills: globalSkills
39
+ });
40
+ }
41
+ }
42
+
43
+ if (allAgentSkills.length === 0) {
44
+ console.log(chalk.yellow('No skills installed.'));
45
+ console.log();
46
+ console.log(chalk.gray(getSupportedAgentsMessage()));
47
+ console.log(chalk.gray('You can add skills with "sun add <skill>"'));
48
+ return;
49
+ }
50
+
51
+ // Print skills grouped by agent
52
+ for (const agentSkills of allAgentSkills) {
53
+ const location = agentSkills.isGlobal
54
+ ? `global ~/${agentSkills.folderName}/`
55
+ : `${agentSkills.folderName}/`;
56
+
57
+ console.log(chalk.cyan(`${agentSkills.agentName} (${location}):`));
58
+
59
+ for (const skill of agentSkills.skills) {
60
+ console.log(` - ${skill}`);
61
+ }
62
+
63
+ console.log();
64
+ }
65
+
66
+ console.log(chalk.gray(getSupportedAgentsMessage()));
67
+ console.log(chalk.gray('You can add skills with "sun add <skill>" or remove with "sun remove <skill>"'));
68
+ }
@@ -0,0 +1,154 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getAgentByFlag, SUPPORTED_AGENTS } from '../core/agents.js';
4
+ import { getDefaultAgents } from '../core/config-manager.js';
5
+ import { detectLocalAgents } from '../core/agent-detect.js';
6
+ import { removeSkill } from '../core/skill-install.js';
7
+ import type { AgentType, CommandFlags } from '../types/index.js';
8
+
9
+ /**
10
+ * Determine which agents to remove from and whether to target global installs.
11
+ * Uses same logic as add command (except no first-run prompt).
12
+ *
13
+ * Logic:
14
+ * 1. If --global flag is set, always target global
15
+ * 2. If agent flags (--claude, --codex, etc.) are set, use those agents
16
+ * 3. Otherwise use saved default agents (error if none configured)
17
+ *
18
+ * For local vs global:
19
+ * - If --global: always global
20
+ * - Otherwise: check if any configured agents have local folders
21
+ * - If yes: target local
22
+ * - If no local folders exist for configured agents: target global
23
+ */
24
+ async function resolveTargetAgents(flags: CommandFlags): Promise<{ agents: AgentType[]; isGlobal: boolean }> {
25
+ const forceGlobal = flags.global ?? false;
26
+
27
+ // Check if any agent flags were explicitly set
28
+ const explicitAgents: AgentType[] = [];
29
+ for (const agent of SUPPORTED_AGENTS) {
30
+ if (flags[agent.flag as keyof CommandFlags]) {
31
+ explicitAgents.push(agent.flag as AgentType);
32
+ }
33
+ }
34
+
35
+ let targetAgents: AgentType[];
36
+
37
+ // Determine which agents to target
38
+ if (explicitAgents.length > 0) {
39
+ targetAgents = explicitAgents;
40
+ } else {
41
+ // Use saved defaults (no first-run prompt for remove)
42
+ const defaultAgents = await getDefaultAgents();
43
+ if (defaultAgents.length === 0) {
44
+ throw new Error('No default agents configured. Run "sun add" first or use --claude/--codex/--gemini flags.');
45
+ }
46
+ targetAgents = defaultAgents;
47
+ }
48
+
49
+ // Determine if we should target global or local
50
+ if (forceGlobal) {
51
+ return { agents: targetAgents, isGlobal: true };
52
+ }
53
+
54
+ // Check if any of the target agents have local folders in current directory
55
+ const localAgents = await detectLocalAgents();
56
+ const localAgentFlags = new Set(localAgents.map(a => a.agent.flag));
57
+
58
+ const hasLocalFolders = targetAgents.some(agentFlag => localAgentFlags.has(agentFlag));
59
+
60
+ // If no local folders exist for any configured agent, target global
61
+ const isGlobal = !hasLocalFolders;
62
+
63
+ return { agents: targetAgents, isGlobal };
64
+ }
65
+
66
+ export interface RemoveResult {
67
+ skill: string;
68
+ removedFrom: string[];
69
+ notFoundIn: string[];
70
+ isGlobal: boolean;
71
+ error?: string;
72
+ }
73
+
74
+ /**
75
+ * Remove skill(s) from agent configuration(s).
76
+ */
77
+ export async function removeCommand(skills: string[], flags: CommandFlags): Promise<void> {
78
+ if (skills.length === 0) {
79
+ console.error(chalk.red('Error: No skills specified. Usage: sun remove <skill> [skill2] ...'));
80
+ process.exit(1);
81
+ }
82
+
83
+ // Resolve target agents
84
+ let agents: AgentType[];
85
+ let isGlobal: boolean;
86
+
87
+ try {
88
+ const resolved = await resolveTargetAgents(flags);
89
+ agents = resolved.agents;
90
+ isGlobal = resolved.isGlobal;
91
+ } catch (error) {
92
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
93
+ process.exit(1);
94
+ }
95
+
96
+ const results: RemoveResult[] = [];
97
+
98
+ for (const skill of skills) {
99
+ const spinner = ora(`Removing ${skill}...`).start();
100
+
101
+ const result: RemoveResult = {
102
+ skill,
103
+ removedFrom: [],
104
+ notFoundIn: [],
105
+ isGlobal
106
+ };
107
+
108
+ try {
109
+ // Remove from each target agent
110
+ for (const agentFlag of agents) {
111
+ const removed = await removeSkill(skill, agentFlag, isGlobal);
112
+ const agentName = getAgentByFlag(agentFlag)!.name;
113
+
114
+ if (removed) {
115
+ result.removedFrom.push(agentName);
116
+ } else {
117
+ result.notFoundIn.push(agentName);
118
+ }
119
+ }
120
+
121
+ if (result.removedFrom.length > 0) {
122
+ spinner.succeed(`Removed ${skill} from ${result.removedFrom.join(' and ')}`);
123
+ } else {
124
+ const pathPrefix = isGlobal ? '~/' : './';
125
+ const checkedPaths = agents.map(a => `${pathPrefix}${getAgentByFlag(a)!.folderName}/`);
126
+ spinner.warn(`${skill} not found in configured agents (${checkedPaths.join(', ')})`);
127
+ }
128
+
129
+ if (result.notFoundIn.length > 0 && result.removedFrom.length > 0) {
130
+ console.log(chalk.gray(` (not found in: ${result.notFoundIn.join(', ')})`));
131
+ }
132
+ } catch (error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ result.error = message;
135
+ spinner.fail(`Failed to remove ${skill}: ${message}`);
136
+ }
137
+
138
+ results.push(result);
139
+ }
140
+
141
+ // Print summary if multiple skills
142
+ if (skills.length > 1) {
143
+ const totalRemoved = results.filter(r => r.removedFrom.length > 0).length;
144
+ const totalFailed = results.filter(r => r.error || r.removedFrom.length === 0).length;
145
+
146
+ console.log();
147
+ if (totalRemoved > 0) {
148
+ console.log(chalk.green(`Removed ${totalRemoved} skill(s)`));
149
+ }
150
+ if (totalFailed > 0) {
151
+ console.log(chalk.yellow(`${totalFailed} skill(s) not found or failed`));
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Dev-only show commands for debugging and exploration.
3
+ * These search the entire file system starting from home directory.
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import fs from 'fs-extra';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ import { SUPPORTED_AGENTS } from '../core/agents.js';
13
+ import { readSkillMetadata } from '../core/skill-info.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ // Timeout for find commands (60 seconds)
18
+ const FIND_TIMEOUT = 60000;
19
+
20
+ /**
21
+ * List skills in a directory, returning name and description.
22
+ */
23
+ async function listSkillsInDir(skillsDir: string): Promise<Array<{ name: string; description: string }>> {
24
+ const skills: Array<{ name: string; description: string }> = [];
25
+
26
+ if (!await fs.pathExists(skillsDir)) {
27
+ return skills;
28
+ }
29
+
30
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
31
+
32
+ for (const entry of entries) {
33
+ if (entry.isDirectory()) {
34
+ const skillPath = path.join(skillsDir, entry.name);
35
+ const metadata = await readSkillMetadata(skillPath);
36
+ if (metadata) {
37
+ skills.push({
38
+ name: metadata.name,
39
+ description: metadata.description
40
+ });
41
+ }
42
+ }
43
+ }
44
+
45
+ return skills;
46
+ }
47
+
48
+ /**
49
+ * sun show all-agent-folders
50
+ * Find ALL agent folders (.claude, .codex, .gemini) starting from ~ and show skills in each.
51
+ */
52
+ export async function showAllAgentFolders(): Promise<void> {
53
+ const homeDir = os.homedir();
54
+
55
+ // Build find command: find ~ -type d \( -name ".claude" -o -name ".codex" -o -name ".gemini" \)
56
+ const namePatterns = SUPPORTED_AGENTS.map(a => `-name "${a.folderName}"`).join(' -o ');
57
+ const findCmd = `find "${homeDir}" -type d \\( ${namePatterns} \\)`;
58
+
59
+ console.log(chalk.cyan('All Agent Folders'));
60
+ console.log(chalk.gray('─'.repeat(40)));
61
+ console.log();
62
+
63
+ let stdout = '';
64
+ try {
65
+ const result = await execAsync(findCmd, { maxBuffer: 10 * 1024 * 1024, timeout: FIND_TIMEOUT });
66
+ stdout = result.stdout;
67
+ } catch (err: any) {
68
+ // find exits non-zero on permission errors but still produces output
69
+ if (err.stdout) {
70
+ stdout = err.stdout;
71
+ } else {
72
+ console.log(chalk.red('Failed to search for agent folders.'));
73
+ console.log(chalk.gray(String(err)));
74
+ return;
75
+ }
76
+ }
77
+
78
+ const agentPaths = stdout.trim().split('\n').filter(Boolean);
79
+
80
+ if (agentPaths.length === 0) {
81
+ console.log(chalk.yellow('No agent folders found.'));
82
+ return;
83
+ }
84
+
85
+ for (const agentPath of agentPaths) {
86
+ const folderName = path.basename(agentPath);
87
+ const agent = SUPPORTED_AGENTS.find(a => a.folderName === folderName);
88
+ if (!agent) continue;
89
+
90
+ console.log(chalk.white.bold(agent.name));
91
+ console.log(chalk.gray(` ${agentPath}`));
92
+
93
+ // List skills
94
+ const skillsDir = path.join(agentPath, 'skills');
95
+ const skills = await listSkillsInDir(skillsDir);
96
+
97
+ if (skills.length === 0) {
98
+ console.log(chalk.gray(' No skills installed'));
99
+ } else {
100
+ console.log(` Skills (${skills.length}):`);
101
+ for (const skill of skills) {
102
+ console.log(` - ${skill.name} ${chalk.gray(`- ${skill.description}`)}`);
103
+ }
104
+ }
105
+ console.log();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * sun show all-agent-skills-folders
111
+ * Find ALL <agent>/skills folders starting from ~ and show skills in each.
112
+ */
113
+ export async function showAllAgentSkillsFolders(): Promise<void> {
114
+ const homeDir = os.homedir();
115
+
116
+ // Build find command for skills directories inside agent folders
117
+ // e.g., find ~ -type d \( -path "*/.claude/skills" -o -path "*/.codex/skills" -o -path "*/.gemini/skills" \)
118
+ const pathPatterns = SUPPORTED_AGENTS.map(a => `-path "*/${a.folderName}/skills"`).join(' -o ');
119
+ const findCmd = `find "${homeDir}" -type d \\( ${pathPatterns} \\)`;
120
+
121
+ console.log(chalk.cyan('All Agent Skills Folders'));
122
+ console.log(chalk.gray('─'.repeat(40)));
123
+ console.log();
124
+
125
+ let stdout = '';
126
+ try {
127
+ const result = await execAsync(findCmd, { maxBuffer: 10 * 1024 * 1024, timeout: FIND_TIMEOUT });
128
+ stdout = result.stdout;
129
+ } catch (err: any) {
130
+ if (err.stdout) {
131
+ stdout = err.stdout;
132
+ } else {
133
+ console.log(chalk.red('Failed to search for skills folders.'));
134
+ console.log(chalk.gray(String(err)));
135
+ return;
136
+ }
137
+ }
138
+
139
+ const skillsDirs = stdout.trim().split('\n').filter(Boolean);
140
+
141
+ if (skillsDirs.length === 0) {
142
+ console.log(chalk.yellow('No agent skills folders found.'));
143
+ return;
144
+ }
145
+
146
+ for (const skillsDir of skillsDirs) {
147
+ // Get agent name from parent folder
148
+ const agentFolder = path.basename(path.dirname(skillsDir));
149
+ const agent = SUPPORTED_AGENTS.find(a => a.folderName === agentFolder);
150
+
151
+ console.log(chalk.white.bold(agent?.name ?? agentFolder));
152
+ console.log(chalk.gray(` ${skillsDir}`));
153
+
154
+ // List skills
155
+ const skills = await listSkillsInDir(skillsDir);
156
+
157
+ if (skills.length === 0) {
158
+ console.log(chalk.gray(' No skills installed'));
159
+ } else {
160
+ console.log(` Skills (${skills.length}):`);
161
+ for (const skill of skills) {
162
+ console.log(` - ${skill.name} ${chalk.gray(`- ${skill.description}`)}`);
163
+ }
164
+ }
165
+ console.log();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * sun show all-skill-folders
171
+ * Find ALL folders containing a valid SKILL.md (with name/description frontmatter).
172
+ */
173
+ export async function showAllSkillFolders(): Promise<void> {
174
+ const homeDir = os.homedir();
175
+
176
+ // Find all SKILL.md files
177
+ const findCmd = `find "${homeDir}" -name "SKILL.md" -type f`;
178
+
179
+ console.log(chalk.cyan('All Skill Folders'));
180
+ console.log(chalk.gray('─'.repeat(40)));
181
+ console.log();
182
+
183
+ let stdout = '';
184
+ try {
185
+ const result = await execAsync(findCmd, { maxBuffer: 10 * 1024 * 1024, timeout: FIND_TIMEOUT });
186
+ stdout = result.stdout;
187
+ } catch (err: any) {
188
+ if (err.stdout) {
189
+ stdout = err.stdout;
190
+ } else {
191
+ console.log(chalk.red('Failed to search for SKILL.md files.'));
192
+ console.log(chalk.gray(String(err)));
193
+ return;
194
+ }
195
+ }
196
+
197
+ const skillMdPaths = stdout.trim().split('\n').filter(Boolean);
198
+
199
+ if (skillMdPaths.length === 0) {
200
+ console.log(chalk.yellow('No SKILL.md files found.'));
201
+ return;
202
+ }
203
+
204
+ let validCount = 0;
205
+
206
+ for (const skillMdPath of skillMdPaths) {
207
+ const skillDir = path.dirname(skillMdPath);
208
+ const metadata = await readSkillMetadata(skillDir);
209
+
210
+ // Only show if it has valid name and description
211
+ if (metadata && metadata.name && metadata.description) {
212
+ validCount++;
213
+ console.log(chalk.white.bold(metadata.name));
214
+ console.log(chalk.gray(` ${skillDir}`));
215
+ console.log(` ${metadata.description}`);
216
+ console.log();
217
+ }
218
+ }
219
+
220
+ if (validCount === 0) {
221
+ console.log(chalk.yellow('No valid SKILL.md files found (missing name/description frontmatter).'));
222
+ }
223
+ }
@@ -0,0 +1,203 @@
1
+ import chalk from 'chalk';
2
+ import path from 'path';
3
+ import { findSkillInstallations, readSkillMetadata } from '../core/skill-info.js';
4
+ import { getAgentByFlag } from '../core/agents.js';
5
+ import { detectAllAgents, detectLocalAgents } from '../core/agent-detect.js';
6
+ import { getDefaultAgents } from '../core/config-manager.js';
7
+ import { showAllAgentFolders, showAllAgentSkillsFolders, showAllSkillFolders } from './show-dev.js';
8
+ import fs from 'fs-extra';
9
+
10
+ /**
11
+ * List all skills in a given skills directory path.
12
+ */
13
+ async function listSkillsInPath(skillsDir: string): Promise<Array<{ name: string; description: string }>> {
14
+ const skills: Array<{ name: string; description: string }> = [];
15
+
16
+ if (!await fs.pathExists(skillsDir)) {
17
+ return skills;
18
+ }
19
+
20
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
21
+
22
+ for (const entry of entries) {
23
+ if (entry.isDirectory()) {
24
+ const skillPath = path.join(skillsDir, entry.name);
25
+ const metadata = await readSkillMetadata(skillPath);
26
+ if (metadata) {
27
+ skills.push({
28
+ name: metadata.name,
29
+ description: metadata.description
30
+ });
31
+ }
32
+ }
33
+ }
34
+
35
+ return skills;
36
+ }
37
+
38
+ /**
39
+ * Show all agent folders and their installed skills.
40
+ */
41
+ async function showAllAgents(): Promise<void> {
42
+ const allAgents = await detectAllAgents();
43
+ const defaultAgents = await getDefaultAgents();
44
+ const defaultSet = new Set(defaultAgents);
45
+ const localAgents = await detectLocalAgents();
46
+ const localAgentFlags = new Set(localAgents.map(a => a.agent.flag));
47
+
48
+ if (allAgents.length === 0) {
49
+ console.log(chalk.yellow('No agent folders found.'));
50
+ console.log(chalk.gray('Agent folders are directories like .claude/, .codex/, .gemini/'));
51
+ return;
52
+ }
53
+
54
+ console.log(chalk.cyan('Agent Folders & Installed Skills'));
55
+ console.log(chalk.gray('─'.repeat(40)));
56
+ console.log();
57
+
58
+ for (const detected of allAgents) {
59
+ const { agent, path: agentPath, isGlobal } = detected;
60
+ const locationLabel = isGlobal ? chalk.gray('(global)') : chalk.gray('(local)');
61
+ const isDefault = defaultSet.has(agent.flag as any);
62
+ const hasLocalFolder = localAgentFlags.has(agent.flag);
63
+
64
+ // Show checkmark if this folder would be affected by sun add/remove:
65
+ // - Local folder: affected if agent is selected
66
+ // - Global folder: affected if agent is selected AND no local folder exists
67
+ const wouldBeAffected = isDefault && (isGlobal ? !hasLocalFolder : true);
68
+ const marker = wouldBeAffected ? chalk.green('✓') : chalk.gray('○');
69
+
70
+ console.log(`${marker} ${chalk.white.bold(agent.name)} ${locationLabel}`);
71
+ console.log(chalk.gray(` ${agentPath}`));
72
+
73
+ // List skills in this agent folder
74
+ const skillsDir = path.join(agentPath, 'skills');
75
+ const skills = await listSkillsInPath(skillsDir);
76
+
77
+ if (skills.length === 0) {
78
+ console.log(chalk.gray(' No skills installed'));
79
+ } else {
80
+ console.log(` Skills (${skills.length}):`);
81
+ for (const skill of skills) {
82
+ console.log(` - ${skill.name} ${chalk.gray(`- ${skill.description}`)}`);
83
+ }
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ console.log(`${chalk.green('✓')} = affected by sun add/remove in this directory`);
89
+ console.log('Run "sun config" to change selected agents.');
90
+ }
91
+
92
+ /**
93
+ * Show skill details and installation locations.
94
+ * If no skill specified, shows all agent folders and their packages.
95
+ *
96
+ * Secret commands (not in README, documented in DEV.md):
97
+ * - all-agent-folders: Find all agent folders on the system
98
+ * - all-agent-skills-folders: Find all <agent>/skills folders
99
+ * - all-skill-folders: Find all folders with valid SKILL.md
100
+ */
101
+ export async function showCommand(skillName?: string): Promise<void> {
102
+ // Handle secret dev commands
103
+ if (skillName === 'all-agent-folders') {
104
+ await showAllAgentFolders();
105
+ return;
106
+ }
107
+
108
+ if (skillName === 'all-agent-skills-folders') {
109
+ await showAllAgentSkillsFolders();
110
+ return;
111
+ }
112
+
113
+ if (skillName === 'all-skill-folders') {
114
+ await showAllSkillFolders();
115
+ return;
116
+ }
117
+
118
+ // If no skill specified, show all agents and their packages
119
+ if (!skillName) {
120
+ await showAllAgents();
121
+ return;
122
+ }
123
+
124
+ const installations = await findSkillInstallations(skillName);
125
+
126
+ if (installations.length === 0) {
127
+ console.error(chalk.yellow(`Skill "${skillName}" is not installed.`));
128
+ process.exit(1);
129
+ }
130
+
131
+ // Check if there are multiple versions (different content hashes)
132
+ const uniqueHashes = new Set(installations.map(i => i.contentHash));
133
+ const hasMultipleVersions = uniqueHashes.size > 1;
134
+
135
+ if (hasMultipleVersions) {
136
+ // Multiple versions detected
137
+ console.log(chalk.cyan(`Skill: ${skillName}`));
138
+ console.log(chalk.yellow('Warning: Multiple versions detected'));
139
+ console.log();
140
+
141
+ // Group by content hash
142
+ const byHash = new Map<string, typeof installations>();
143
+ for (const inst of installations) {
144
+ const existing = byHash.get(inst.contentHash) || [];
145
+ existing.push(inst);
146
+ byHash.set(inst.contentHash, existing);
147
+ }
148
+
149
+ let versionNum = 1;
150
+ for (const [hash, insts] of byHash) {
151
+ const firstInst = insts[0];
152
+ const locations = insts.map(i => {
153
+ const agent = getAgentByFlag(i.agent)!;
154
+ return i.isGlobal ? `~/${agent.folderName}/` : `${agent.folderName}/`;
155
+ });
156
+
157
+ console.log(chalk.white(`Version ${versionNum} (${locations.join(', ')}):`));
158
+ console.log(` Description: ${firstInst.metadata.description}`);
159
+
160
+ if (firstInst.metadata.metadata?.author) {
161
+ console.log(` Author: ${firstInst.metadata.metadata.author}`);
162
+ }
163
+ if (firstInst.metadata.metadata?.version) {
164
+ console.log(` Version: ${firstInst.metadata.metadata.version}`);
165
+ }
166
+ if (firstInst.metadata.license) {
167
+ console.log(` License: ${firstInst.metadata.license}`);
168
+ }
169
+
170
+ console.log(` Content hash: ${hash}`);
171
+ console.log();
172
+
173
+ versionNum++;
174
+ }
175
+ } else {
176
+ // Single version across all installations
177
+ const firstInst = installations[0];
178
+
179
+ console.log(chalk.cyan(`Skill: ${skillName}`));
180
+ console.log(`Description: ${firstInst.metadata.description}`);
181
+
182
+ if (firstInst.metadata.metadata?.author) {
183
+ console.log(`Author: ${firstInst.metadata.metadata.author}`);
184
+ }
185
+ if (firstInst.metadata.metadata?.version) {
186
+ console.log(`Version: ${firstInst.metadata.metadata.version}`);
187
+ }
188
+ if (firstInst.metadata.license) {
189
+ console.log(`License: ${firstInst.metadata.license}`);
190
+ }
191
+ if (firstInst.metadata.compatibility) {
192
+ console.log(`Compatibility: ${firstInst.metadata.compatibility}`);
193
+ }
194
+
195
+ console.log();
196
+ console.log('Installed in:');
197
+ for (const inst of installations) {
198
+ const agent = getAgentByFlag(inst.agent)!;
199
+ const location = inst.isGlobal ? '(global)' : '(local)';
200
+ console.log(` - ${agent.folderName}/ ${chalk.gray(location)}`);
201
+ }
202
+ }
203
+ }