@orderful/droid 0.10.5 → 0.11.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 (167) hide show
  1. package/.claude/CLAUDE.md +13 -6
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +23 -24
  4. package/dist/bin/droid.js +9 -9
  5. package/dist/bin/droid.js.map +1 -1
  6. package/dist/commands/install.d.ts +1 -1
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +24 -23
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/setup.d.ts +3 -3
  11. package/dist/commands/setup.d.ts.map +1 -1
  12. package/dist/commands/setup.js +32 -28
  13. package/dist/commands/setup.js.map +1 -1
  14. package/dist/commands/skills.d.ts.map +1 -1
  15. package/dist/commands/skills.js +60 -53
  16. package/dist/commands/skills.js.map +1 -1
  17. package/dist/commands/tui.d.ts.map +1 -1
  18. package/dist/commands/tui.js +213 -319
  19. package/dist/commands/tui.js.map +1 -1
  20. package/dist/commands/uninstall.d.ts +1 -1
  21. package/dist/commands/uninstall.d.ts.map +1 -1
  22. package/dist/commands/uninstall.js +15 -6
  23. package/dist/commands/uninstall.js.map +1 -1
  24. package/dist/commands/update.d.ts +2 -2
  25. package/dist/commands/update.d.ts.map +1 -1
  26. package/dist/commands/update.js +9 -9
  27. package/dist/commands/update.js.map +1 -1
  28. package/dist/lib/agents.d.ts +11 -10
  29. package/dist/lib/agents.d.ts.map +1 -1
  30. package/dist/lib/agents.js +52 -54
  31. package/dist/lib/agents.js.map +1 -1
  32. package/dist/lib/config.d.ts +1 -0
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +42 -5
  35. package/dist/lib/config.js.map +1 -1
  36. package/dist/lib/platforms.d.ts +41 -0
  37. package/dist/lib/platforms.d.ts.map +1 -0
  38. package/dist/lib/platforms.js +52 -0
  39. package/dist/lib/platforms.js.map +1 -0
  40. package/dist/lib/skills.d.ts +19 -11
  41. package/dist/lib/skills.d.ts.map +1 -1
  42. package/dist/lib/skills.js +125 -99
  43. package/dist/lib/skills.js.map +1 -1
  44. package/dist/lib/tools.d.ts +30 -0
  45. package/dist/lib/tools.d.ts.map +1 -0
  46. package/dist/lib/tools.js +116 -0
  47. package/dist/lib/tools.js.map +1 -0
  48. package/dist/lib/types.d.ts +45 -2
  49. package/dist/lib/types.d.ts.map +1 -1
  50. package/dist/lib/types.js +24 -5
  51. package/dist/lib/types.js.map +1 -1
  52. package/dist/tools/brain/TOOL.yaml +27 -0
  53. package/dist/tools/coach/TOOL.yaml +21 -0
  54. package/dist/tools/code-review/TOOL.yaml +18 -0
  55. package/dist/tools/comments/TOOL.yaml +27 -0
  56. package/dist/{skills → tools/comments/skills}/comments/SKILL.md +12 -3
  57. package/dist/{skills → tools/comments/skills}/comments/SKILL.yaml +1 -1
  58. package/dist/tools/project/TOOL.yaml +26 -0
  59. package/package.json +2 -2
  60. package/src/bin/droid.ts +9 -9
  61. package/src/commands/install.ts +24 -23
  62. package/src/commands/setup.test.ts +2 -2
  63. package/src/commands/setup.ts +33 -29
  64. package/src/commands/skills.ts +63 -64
  65. package/src/commands/tui.tsx +432 -578
  66. package/src/commands/uninstall.ts +17 -6
  67. package/src/commands/update.ts +10 -10
  68. package/src/lib/agents.ts +58 -58
  69. package/src/lib/config.test.ts +0 -10
  70. package/src/lib/config.ts +47 -5
  71. package/src/lib/platforms.ts +59 -0
  72. package/src/lib/skills.test.ts +53 -28
  73. package/src/lib/skills.ts +134 -101
  74. package/src/lib/tools.ts +140 -0
  75. package/src/lib/types.test.ts +15 -7
  76. package/src/lib/types.ts +63 -2
  77. package/src/tools/brain/TOOL.yaml +27 -0
  78. package/src/tools/coach/TOOL.yaml +21 -0
  79. package/src/tools/code-review/TOOL.yaml +18 -0
  80. package/src/tools/comments/TOOL.yaml +27 -0
  81. package/src/{skills → tools/comments/skills}/comments/SKILL.md +12 -3
  82. package/src/{skills → tools/comments/skills}/comments/SKILL.yaml +1 -1
  83. package/src/tools/project/TOOL.yaml +26 -0
  84. package/dist/agents/README.md +0 -137
  85. package/src/agents/README.md +0 -137
  86. /package/dist/{skills → tools}/README.md +0 -0
  87. /package/dist/{skills → tools}/brain/commands/README.md +0 -0
  88. /package/dist/{skills → tools}/brain/commands/brain.md +0 -0
  89. /package/dist/{skills → tools}/brain/commands/scratchpad.md +0 -0
  90. /package/dist/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  91. /package/dist/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  92. /package/dist/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  93. /package/dist/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  94. /package/dist/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  95. /package/dist/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  96. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  97. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  98. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  99. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  100. /package/dist/{skills → tools}/coach/commands/README.md +0 -0
  101. /package/dist/{skills → tools}/coach/commands/coach.md +0 -0
  102. /package/dist/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  103. /package/dist/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  104. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  105. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  106. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  107. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  108. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  109. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  110. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  111. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  112. /package/dist/{skills → tools}/code-review/commands/code-review.md +0 -0
  113. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  114. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  115. /package/dist/{skills → tools}/comments/commands/README.md +0 -0
  116. /package/dist/{skills → tools}/comments/commands/comments.md +0 -0
  117. /package/dist/{skills → tools}/project/commands/README.md +0 -0
  118. /package/dist/{skills → tools}/project/commands/project.md +0 -0
  119. /package/dist/{skills → tools/project/skills}/project/SKILL.md +0 -0
  120. /package/dist/{skills → tools/project/skills}/project/SKILL.yaml +0 -0
  121. /package/dist/{skills → tools/project/skills}/project/references/changelog.md +0 -0
  122. /package/dist/{skills → tools/project/skills}/project/references/creating.md +0 -0
  123. /package/dist/{skills → tools/project/skills}/project/references/loading.md +0 -0
  124. /package/dist/{skills → tools/project/skills}/project/references/templates.md +0 -0
  125. /package/dist/{skills → tools/project/skills}/project/references/updating.md +0 -0
  126. /package/dist/{skills → tools/project/skills}/project/references/versioning.md +0 -0
  127. /package/src/{skills → tools}/README.md +0 -0
  128. /package/src/{skills → tools}/brain/commands/README.md +0 -0
  129. /package/src/{skills → tools}/brain/commands/brain.md +0 -0
  130. /package/src/{skills → tools}/brain/commands/scratchpad.md +0 -0
  131. /package/src/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  132. /package/src/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  133. /package/src/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  134. /package/src/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  135. /package/src/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  136. /package/src/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  137. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  138. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  139. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  140. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  141. /package/src/{skills → tools}/coach/commands/README.md +0 -0
  142. /package/src/{skills → tools}/coach/commands/coach.md +0 -0
  143. /package/src/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  144. /package/src/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  145. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  146. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  147. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  148. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  149. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  150. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  151. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  152. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  153. /package/src/{skills → tools}/code-review/commands/code-review.md +0 -0
  154. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  155. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  156. /package/src/{skills → tools}/comments/commands/README.md +0 -0
  157. /package/src/{skills → tools}/comments/commands/comments.md +0 -0
  158. /package/src/{skills → tools}/project/commands/README.md +0 -0
  159. /package/src/{skills → tools}/project/commands/project.md +0 -0
  160. /package/src/{skills → tools/project/skills}/project/SKILL.md +0 -0
  161. /package/src/{skills → tools/project/skills}/project/SKILL.yaml +0 -0
  162. /package/src/{skills → tools/project/skills}/project/references/changelog.md +0 -0
  163. /package/src/{skills → tools/project/skills}/project/references/creating.md +0 -0
  164. /package/src/{skills → tools/project/skills}/project/references/loading.md +0 -0
  165. /package/src/{skills → tools/project/skills}/project/references/templates.md +0 -0
  166. /package/src/{skills → tools/project/skills}/project/references/updating.md +0 -0
  167. /package/src/{skills → tools/project/skills}/project/references/versioning.md +0 -0
@@ -1,16 +1,27 @@
1
1
  import chalk from 'chalk';
2
- import { uninstallSkill, isSkillInstalled } from '../lib/skills.js';
2
+ import { uninstallSkill } from '../lib/skills.js';
3
+ import { getBundledTools, isToolInstalled } from '../lib/tools.js';
3
4
 
4
- export async function uninstallCommand(skillName: string): Promise<void> {
5
- if (!isSkillInstalled(skillName)) {
6
- console.error(chalk.red(`\n✗ Skill '${skillName}' is not installed`));
5
+ export async function uninstallCommand(toolName: string): Promise<void> {
6
+ const tools = getBundledTools();
7
+ const tool = tools.find((t) => t.name === toolName);
8
+
9
+ if (!tool) {
10
+ console.error(chalk.red(`\n✗ Tool '${toolName}' not found`));
11
+ process.exit(1);
12
+ }
13
+
14
+ if (!isToolInstalled(toolName)) {
15
+ console.error(chalk.red(`\n✗ Tool '${toolName}' is not installed`));
7
16
  process.exit(1);
8
17
  }
9
18
 
10
- const result = uninstallSkill(skillName);
19
+ // Uninstall via primary skill
20
+ const primarySkill = tool.includes.skills.find(s => s.required)?.name || toolName;
21
+ const result = uninstallSkill(primarySkill);
11
22
 
12
23
  if (result.success) {
13
- console.log(chalk.green(`\n✓ ${result.message}`));
24
+ console.log(chalk.green(`\n✓ Uninstalled ${toolName}`));
14
25
  } else {
15
26
  console.error(chalk.red(`\n✗ ${result.message}`));
16
27
  process.exit(1);
@@ -3,22 +3,22 @@ import { execSync } from 'child_process';
3
3
  import { getVersion } from '../lib/version.js';
4
4
 
5
5
  interface UpdateOptions {
6
- skills?: boolean;
6
+ tools?: boolean;
7
7
  cli?: boolean;
8
8
  }
9
9
 
10
- export async function updateCommand(skill?: string, options?: UpdateOptions): Promise<void> {
11
- // If specific skill specified, update just that skill
12
- if (skill) {
13
- console.log(chalk.yellow('\n⚠ Per-skill updates not implemented yet'));
14
- console.log(chalk.gray('Skills are bundled with the CLI - run `droid update` to update all.'));
10
+ export async function updateCommand(tool?: string, options?: UpdateOptions): Promise<void> {
11
+ // If specific tool specified, update just that tool
12
+ if (tool) {
13
+ console.log(chalk.yellow('\n⚠ Per-tool updates not implemented yet'));
14
+ console.log(chalk.gray('Tools are bundled with the CLI - run `droid update` to update all.'));
15
15
  return;
16
16
  }
17
17
 
18
- // If --skills flag, update skills only
19
- if (options?.skills) {
20
- console.log(chalk.yellow('\n⚠ Skill-only updates not implemented yet'));
21
- console.log(chalk.gray('Skills are bundled with the CLI - run `droid update` to update all.'));
18
+ // If --tools flag, update tools only
19
+ if (options?.tools) {
20
+ console.log(chalk.yellow('\n⚠ Tool-only updates not implemented yet'));
21
+ console.log(chalk.gray('Tools are bundled with the CLI - run `droid update` to update all.'));
22
22
  return;
23
23
  }
24
24
 
package/src/lib/agents.ts CHANGED
@@ -1,24 +1,19 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { homedir } from 'os';
5
4
  import YAML from 'yaml';
6
5
  import { loadConfig } from './config.js';
7
- import { AITool } from './types.js';
6
+ import { Platform } from './types.js';
7
+ import { getAgentsPath } from './platforms.js';
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const BUNDLED_AGENTS_DIR = join(__dirname, '../agents');
10
+ const BUNDLED_TOOLS_DIR = join(__dirname, '../tools');
11
11
 
12
12
  /**
13
- * Get the installation path for agents based on AI tool
13
+ * Get the installation path for agents based on platform
14
14
  */
15
- export function getAgentsInstallPath(aiTool: AITool): string {
16
- switch (aiTool) {
17
- case AITool.ClaudeCode:
18
- return join(homedir(), '.claude', 'agents');
19
- case AITool.OpenCode:
20
- return join(homedir(), '.config', 'opencode', 'agent');
21
- }
15
+ export function getAgentsInstallPath(platform: Platform): string {
16
+ return getAgentsPath(platform);
22
17
  }
23
18
 
24
19
  /**
@@ -37,13 +32,6 @@ export interface AgentManifest {
37
32
  persona?: string;
38
33
  }
39
34
 
40
- /**
41
- * Get the path to bundled agents directory
42
- */
43
- export function getBundledAgentsDir(): string {
44
- return BUNDLED_AGENTS_DIR;
45
- }
46
-
47
35
  /**
48
36
  * Load an agent manifest from an agent directory
49
37
  */
@@ -63,20 +51,31 @@ export function loadAgentManifest(agentDir: string): AgentManifest | null {
63
51
  }
64
52
 
65
53
  /**
66
- * Get all bundled agents (standalone + skill-bundled)
54
+ * Get all bundled agents from tools
67
55
  */
68
56
  export function getBundledAgents(): AgentManifest[] {
69
57
  const agents: AgentManifest[] = [];
70
58
  const seenNames = new Set<string>();
71
59
 
72
- // Get standalone agents from src/agents/
73
- if (existsSync(BUNDLED_AGENTS_DIR)) {
74
- const agentDirs = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true })
60
+ if (!existsSync(BUNDLED_TOOLS_DIR)) {
61
+ return agents;
62
+ }
63
+
64
+ // Get agents from tools/*/agents/
65
+ const toolDirs = readdirSync(BUNDLED_TOOLS_DIR, { withFileTypes: true })
66
+ .filter((dirent) => dirent.isDirectory())
67
+ .map((dirent) => dirent.name);
68
+
69
+ for (const toolName of toolDirs) {
70
+ const toolAgentsDir = join(BUNDLED_TOOLS_DIR, toolName, 'agents');
71
+ if (!existsSync(toolAgentsDir)) continue;
72
+
73
+ const agentDirs = readdirSync(toolAgentsDir, { withFileTypes: true })
75
74
  .filter((dirent) => dirent.isDirectory())
76
75
  .map((dirent) => dirent.name);
77
76
 
78
77
  for (const agentDir of agentDirs) {
79
- const manifest = loadAgentManifest(join(BUNDLED_AGENTS_DIR, agentDir));
78
+ const manifest = loadAgentManifest(join(toolAgentsDir, agentDir));
80
79
  if (manifest && !seenNames.has(manifest.name)) {
81
80
  agents.push(manifest);
82
81
  seenNames.add(manifest.name);
@@ -84,36 +83,12 @@ export function getBundledAgents(): AgentManifest[] {
84
83
  }
85
84
  }
86
85
 
87
- // Get skill-bundled agents from src/skills/*/agents/
88
- const skillsDir = join(__dirname, '../skills');
89
- if (existsSync(skillsDir)) {
90
- const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
91
- .filter((dirent) => dirent.isDirectory())
92
- .map((dirent) => dirent.name);
93
-
94
- for (const skillName of skillDirs) {
95
- const skillAgentsDir = join(skillsDir, skillName, 'agents');
96
- if (!existsSync(skillAgentsDir)) continue;
97
-
98
- const agentDirs = readdirSync(skillAgentsDir, { withFileTypes: true })
99
- .filter((dirent) => dirent.isDirectory())
100
- .map((dirent) => dirent.name);
101
-
102
- for (const agentDir of agentDirs) {
103
- const manifest = loadAgentManifest(join(skillAgentsDir, agentDir));
104
- if (manifest && !seenNames.has(manifest.name)) {
105
- agents.push(manifest);
106
- seenNames.add(manifest.name);
107
- }
108
- }
109
- }
110
- }
111
-
112
86
  return agents;
113
87
  }
114
88
 
115
89
  /**
116
90
  * Get agent status display string
91
+ * Note: Not currently used, kept for future TUI agent status display
117
92
  */
118
93
  export function getAgentStatusDisplay(status?: string): string {
119
94
  switch (status) {
@@ -128,11 +103,11 @@ export function getAgentStatusDisplay(status?: string): string {
128
103
  }
129
104
 
130
105
  /**
131
- * Get installed agents directory for the configured AI tool
106
+ * Get installed agents directory for the configured platform
132
107
  */
133
108
  export function getInstalledAgentsDir(): string {
134
109
  const config = loadConfig();
135
- return getAgentsInstallPath(config.ai_tool);
110
+ return getAgentsInstallPath(config.platform);
136
111
  }
137
112
 
138
113
  /**
@@ -140,7 +115,7 @@ export function getInstalledAgentsDir(): string {
140
115
  */
141
116
  export function isAgentInstalled(agentName: string): boolean {
142
117
  const config = loadConfig();
143
- const agentsDir = getAgentsInstallPath(config.ai_tool);
118
+ const agentsDir = getAgentsInstallPath(config.platform);
144
119
  const agentPath = join(agentsDir, `${agentName}.md`);
145
120
  return existsSync(agentPath);
146
121
  }
@@ -221,13 +196,13 @@ export function installAgentFromPath(agentDir: string, agentName: string): { suc
221
196
  agentContent = manifest.persona;
222
197
  }
223
198
 
224
- // Generate format based on AI tool
225
- const installedContent = config.ai_tool === AITool.ClaudeCode
199
+ // Generate format based on platform
200
+ const installedContent = config.platform === Platform.ClaudeCode
226
201
  ? generateClaudeCodeAgent(manifest, agentContent)
227
202
  : generateOpenCodeAgent(manifest, agentContent);
228
203
 
229
204
  // Ensure agents directory exists
230
- const agentsDir = getAgentsInstallPath(config.ai_tool);
205
+ const agentsDir = getAgentsInstallPath(config.platform);
231
206
  if (!existsSync(agentsDir)) {
232
207
  mkdirSync(agentsDir, { recursive: true });
233
208
  }
@@ -236,7 +211,7 @@ export function installAgentFromPath(agentDir: string, agentName: string): { suc
236
211
  const outputPath = join(agentsDir, `${agentName}.md`);
237
212
  writeFileSync(outputPath, installedContent);
238
213
 
239
- const targetDir = config.ai_tool === AITool.ClaudeCode
214
+ const targetDir = config.platform === Platform.ClaudeCode
240
215
  ? '~/.claude/agents/'
241
216
  : '~/.config/opencode/agent/';
242
217
 
@@ -247,11 +222,36 @@ export function installAgentFromPath(agentDir: string, agentName: string): { suc
247
222
  }
248
223
 
249
224
  /**
250
- * Install an agent from the bundled agents directory
225
+ * Find the path to an agent within the tools directory structure
226
+ */
227
+ export function findAgentPath(agentName: string): string | null {
228
+ if (!existsSync(BUNDLED_TOOLS_DIR)) {
229
+ return null;
230
+ }
231
+
232
+ const toolDirs = readdirSync(BUNDLED_TOOLS_DIR, { withFileTypes: true })
233
+ .filter((dirent) => dirent.isDirectory())
234
+ .map((dirent) => dirent.name);
235
+
236
+ for (const toolName of toolDirs) {
237
+ const agentDir = join(BUNDLED_TOOLS_DIR, toolName, 'agents', agentName);
238
+ if (existsSync(agentDir) && existsSync(join(agentDir, 'AGENT.yaml'))) {
239
+ return agentDir;
240
+ }
241
+ }
242
+
243
+ return null;
244
+ }
245
+
246
+ /**
247
+ * Install an agent from the tools directory
251
248
  * Combines AGENT.yaml metadata with AGENT.md content into a single .md file
252
249
  */
253
250
  export function installAgent(agentName: string): { success: boolean; message: string } {
254
- const agentDir = join(BUNDLED_AGENTS_DIR, agentName);
251
+ const agentDir = findAgentPath(agentName);
252
+ if (!agentDir) {
253
+ return { success: false, message: `Agent '${agentName}' not found` };
254
+ }
255
255
  return installAgentFromPath(agentDir, agentName);
256
256
  }
257
257
 
@@ -260,7 +260,7 @@ export function installAgent(agentName: string): { success: boolean; message: st
260
260
  */
261
261
  export function uninstallAgent(agentName: string): { success: boolean; message: string } {
262
262
  const config = loadConfig();
263
- const agentsDir = getAgentsInstallPath(config.ai_tool);
263
+ const agentsDir = getAgentsInstallPath(config.platform);
264
264
  const agentPath = join(agentsDir, `${agentName}.md`);
265
265
 
266
266
  if (!existsSync(agentPath)) {
@@ -3,16 +3,6 @@ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import YAML from 'yaml';
6
- import { AITool, BuiltInOutput } from './types.js';
7
-
8
- describe('config types', () => {
9
- it('should have correct enum values', () => {
10
- expect(AITool.ClaudeCode).toBe('claude-code');
11
- expect(AITool.OpenCode).toBe('opencode');
12
- expect(BuiltInOutput.Terminal).toBe('terminal');
13
- expect(BuiltInOutput.Editor).toBe('editor');
14
- });
15
- });
16
6
 
17
7
  describe('config value parsing', () => {
18
8
  it('should parse dot notation keys correctly', () => {
package/src/lib/config.ts CHANGED
@@ -2,19 +2,49 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  import YAML from 'yaml';
5
- import { AITool, BuiltInOutput, type DroidConfig, type SkillOverrides } from './types.js';
5
+ import { Platform, BuiltInOutput, type DroidConfig, type LegacyDroidConfig, type SkillOverrides } from './types.js';
6
6
 
7
7
  const CONFIG_DIR = join(homedir(), '.droid');
8
8
  const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
9
9
 
10
10
  const DEFAULT_CONFIG: DroidConfig = {
11
- ai_tool: AITool.ClaudeCode,
11
+ platform: Platform.ClaudeCode,
12
12
  user_mention: '@user',
13
13
  output_preference: BuiltInOutput.Terminal,
14
14
  git_username: '',
15
- skills: {},
15
+ platforms: {},
16
16
  };
17
17
 
18
+ /**
19
+ * Migrate legacy config (v1) to new config format (v2)
20
+ * v1: ai_tool + skills flat
21
+ * v2: platform + platforms map with per-platform tools
22
+ */
23
+ function migrateConfig(config: Partial<DroidConfig> & Partial<LegacyDroidConfig>): DroidConfig {
24
+ // Check if this is a legacy config (has ai_tool, no platform)
25
+ if ('ai_tool' in config && !('platform' in config)) {
26
+ const legacyConfig = config as LegacyDroidConfig;
27
+ return {
28
+ platform: legacyConfig.ai_tool,
29
+ user_mention: legacyConfig.user_mention ?? DEFAULT_CONFIG.user_mention,
30
+ output_preference: legacyConfig.output_preference ?? DEFAULT_CONFIG.output_preference,
31
+ git_username: legacyConfig.git_username ?? DEFAULT_CONFIG.git_username,
32
+ platforms: {
33
+ [legacyConfig.ai_tool]: {
34
+ tools: legacyConfig.skills ?? {},
35
+ },
36
+ },
37
+ };
38
+ }
39
+
40
+ // Already new format, just ensure defaults
41
+ return {
42
+ ...DEFAULT_CONFIG,
43
+ ...config,
44
+ platforms: config.platforms ?? {},
45
+ };
46
+ }
47
+
18
48
  /**
19
49
  * Ensure the config directory exists
20
50
  */
@@ -33,6 +63,7 @@ export function configExists(): boolean {
33
63
 
34
64
  /**
35
65
  * Load the global config, or return defaults if none exists
66
+ * Automatically migrates legacy config format to new format
36
67
  */
37
68
  export function loadConfig(): DroidConfig {
38
69
  ensureConfigDir();
@@ -43,8 +74,19 @@ export function loadConfig(): DroidConfig {
43
74
 
44
75
  try {
45
76
  const content = readFileSync(CONFIG_FILE, 'utf-8');
46
- const config = YAML.parse(content) as Partial<DroidConfig>;
47
- return { ...DEFAULT_CONFIG, ...config };
77
+ const rawConfig = YAML.parse(content) as Partial<DroidConfig> & Partial<LegacyDroidConfig>;
78
+
79
+ // Check if migration is needed
80
+ // TODO: Remove after v0.14.0 (target: late Jan 2025)
81
+ const needsMigration = 'ai_tool' in rawConfig && !('platform' in rawConfig);
82
+ const config = migrateConfig(rawConfig);
83
+
84
+ // Save migrated config if it was migrated
85
+ if (needsMigration) {
86
+ saveConfig(config);
87
+ }
88
+
89
+ return config;
48
90
  } catch (error) {
49
91
  const message = error instanceof Error ? error.message : 'Unknown error';
50
92
  console.error(`Error reading config: ${message}`);
@@ -0,0 +1,59 @@
1
+ import { join } from 'path';
2
+ import { homedir } from 'os';
3
+ import { Platform } from './types.js';
4
+
5
+ /**
6
+ * Platform-specific paths configuration
7
+ * Single source of truth for all platform-specific directories
8
+ */
9
+ export const PLATFORM_PATHS = {
10
+ [Platform.ClaudeCode]: {
11
+ skills: join(homedir(), '.claude', 'skills'),
12
+ commands: join(homedir(), '.claude', 'commands'),
13
+ agents: join(homedir(), '.claude', 'agents'),
14
+ config: join(homedir(), '.claude', 'CLAUDE.md'),
15
+ },
16
+ [Platform.OpenCode]: {
17
+ skills: join(homedir(), '.config', 'opencode', 'skills'),
18
+ commands: join(homedir(), '.config', 'opencode', 'command'),
19
+ agents: join(homedir(), '.config', 'opencode', 'agent'),
20
+ config: join(homedir(), '.config', 'opencode', 'AGENTS.md'),
21
+ },
22
+ } as const;
23
+
24
+ export type PlatformPaths = typeof PLATFORM_PATHS[Platform];
25
+
26
+ /**
27
+ * Get all paths for a platform
28
+ */
29
+ export function getPlatformPaths(platform: Platform): PlatformPaths {
30
+ return PLATFORM_PATHS[platform];
31
+ }
32
+
33
+ /**
34
+ * Get skills install path for a platform
35
+ */
36
+ export function getSkillsPath(platform: Platform): string {
37
+ return PLATFORM_PATHS[platform].skills;
38
+ }
39
+
40
+ /**
41
+ * Get commands install path for a platform
42
+ */
43
+ export function getCommandsPath(platform: Platform): string {
44
+ return PLATFORM_PATHS[platform].commands;
45
+ }
46
+
47
+ /**
48
+ * Get agents install path for a platform
49
+ */
50
+ export function getAgentsPath(platform: Platform): string {
51
+ return PLATFORM_PATHS[platform].agents;
52
+ }
53
+
54
+ /**
55
+ * Get platform config file path (CLAUDE.md or AGENTS.md)
56
+ */
57
+ export function getConfigPath(platform: Platform): string {
58
+ return PLATFORM_PATHS[platform].config;
59
+ }
@@ -4,11 +4,11 @@ import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { homedir } from 'os';
6
6
  import YAML from 'yaml';
7
- import { AITool, SkillStatus } from './types.js';
7
+ import { Platform, SkillStatus } from './types.js';
8
8
  import {
9
9
  getSkillsInstallPath,
10
10
  getCommandsInstallPath,
11
- getAIConfigPath,
11
+ getPlatformConfigPath,
12
12
  getSkillStatusDisplay,
13
13
  getBundledSkillsDir,
14
14
  } from './skills.js';
@@ -28,44 +28,44 @@ function parseFrontmatter(content: string): Record<string, unknown> | null {
28
28
 
29
29
  describe('getSkillsInstallPath', () => {
30
30
  it('should return Claude Code path', () => {
31
- const path = getSkillsInstallPath(AITool.ClaudeCode);
31
+ const path = getSkillsInstallPath(Platform.ClaudeCode);
32
32
  expect(path).toBe(join(homedir(), '.claude', 'skills'));
33
33
  });
34
34
 
35
35
  it('should return OpenCode path', () => {
36
- const path = getSkillsInstallPath(AITool.OpenCode);
36
+ const path = getSkillsInstallPath(Platform.OpenCode);
37
37
  expect(path).toBe(join(homedir(), '.config', 'opencode', 'skills'));
38
38
  });
39
39
  });
40
40
 
41
41
  describe('getCommandsInstallPath', () => {
42
42
  it('should return Claude Code commands path', () => {
43
- const path = getCommandsInstallPath(AITool.ClaudeCode);
43
+ const path = getCommandsInstallPath(Platform.ClaudeCode);
44
44
  expect(path).toBe(join(homedir(), '.claude', 'commands'));
45
45
  });
46
46
 
47
47
  it('should return OpenCode commands path', () => {
48
- const path = getCommandsInstallPath(AITool.OpenCode);
48
+ const path = getCommandsInstallPath(Platform.OpenCode);
49
49
  expect(path).toBe(join(homedir(), '.config', 'opencode', 'command'));
50
50
  });
51
51
  });
52
52
 
53
- describe('getAIConfigPath', () => {
53
+ describe('getPlatformConfigPath', () => {
54
54
  it('should return Claude Code CLAUDE.md path', () => {
55
- const path = getAIConfigPath(AITool.ClaudeCode);
55
+ const path = getPlatformConfigPath(Platform.ClaudeCode);
56
56
  expect(path).toBe(join(homedir(), '.claude', 'CLAUDE.md'));
57
57
  });
58
58
 
59
59
  it('should return OpenCode AGENTS.md path', () => {
60
- const path = getAIConfigPath(AITool.OpenCode);
60
+ const path = getPlatformConfigPath(Platform.OpenCode);
61
61
  expect(path).toBe(join(homedir(), '.config', 'opencode', 'AGENTS.md'));
62
62
  });
63
63
  });
64
64
 
65
65
  describe('getBundledSkillsDir', () => {
66
- it('should return a path ending in skills', () => {
66
+ it('should return a path ending in tools', () => {
67
67
  const dir = getBundledSkillsDir();
68
- expect(dir.endsWith('skills')).toBe(true);
68
+ expect(dir.endsWith('tools')).toBe(true);
69
69
  });
70
70
 
71
71
  it('should return an existing directory', () => {
@@ -150,15 +150,44 @@ describe('skill manifest parsing', () => {
150
150
  });
151
151
  });
152
152
 
153
- describe('bundled skills validation', () => {
154
- it('all skills should have SKILL.md with valid frontmatter', () => {
155
- const skillsDir = getBundledSkillsDir();
153
+ /**
154
+ * Helper to get all skill directories in the new tools/{tool}/skills/{skill} structure
155
+ */
156
+ function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir: string }> {
157
+ const skillPaths: Array<{ skillName: string; skillDir: string }> = [];
158
+
159
+ const toolDirs = readdirSync(toolsDir, { withFileTypes: true })
160
+ .filter((d) => d.isDirectory())
161
+ .map((d) => d.name);
162
+
163
+ for (const toolName of toolDirs) {
164
+ const skillsDir = join(toolsDir, toolName, 'skills');
165
+ if (!existsSync(skillsDir)) continue;
166
+
156
167
  const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
157
168
  .filter((d) => d.isDirectory())
158
169
  .map((d) => d.name);
159
170
 
160
171
  for (const skillName of skillDirs) {
161
- const skillMdPath = join(skillsDir, skillName, 'SKILL.md');
172
+ skillPaths.push({
173
+ skillName,
174
+ skillDir: join(skillsDir, skillName),
175
+ });
176
+ }
177
+ }
178
+
179
+ return skillPaths;
180
+ }
181
+
182
+ describe('bundled skills validation', () => {
183
+ it('all skills should have SKILL.md with valid frontmatter', () => {
184
+ const toolsDir = getBundledSkillsDir();
185
+ const skillPaths = getAllSkillPaths(toolsDir);
186
+
187
+ expect(skillPaths.length).toBeGreaterThan(0);
188
+
189
+ for (const { skillName, skillDir } of skillPaths) {
190
+ const skillMdPath = join(skillDir, 'SKILL.md');
162
191
  expect(existsSync(skillMdPath)).toBe(true);
163
192
 
164
193
  const content = readFileSync(skillMdPath, 'utf-8');
@@ -171,13 +200,11 @@ describe('bundled skills validation', () => {
171
200
  });
172
201
 
173
202
  it('all skills should have SKILL.yaml manifest', () => {
174
- const skillsDir = getBundledSkillsDir();
175
- const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
176
- .filter((d) => d.isDirectory())
177
- .map((d) => d.name);
203
+ const toolsDir = getBundledSkillsDir();
204
+ const skillPaths = getAllSkillPaths(toolsDir);
178
205
 
179
- for (const skillName of skillDirs) {
180
- const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
206
+ for (const { skillName, skillDir } of skillPaths) {
207
+ const yamlPath = join(skillDir, 'SKILL.yaml');
181
208
  expect(existsSync(yamlPath)).toBe(true);
182
209
 
183
210
  const content = readFileSync(yamlPath, 'utf-8');
@@ -190,14 +217,12 @@ describe('bundled skills validation', () => {
190
217
  });
191
218
 
192
219
  it('SKILL.md frontmatter should match SKILL.yaml', () => {
193
- const skillsDir = getBundledSkillsDir();
194
- const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
195
- .filter((d) => d.isDirectory())
196
- .map((d) => d.name);
220
+ const toolsDir = getBundledSkillsDir();
221
+ const skillPaths = getAllSkillPaths(toolsDir);
197
222
 
198
- for (const skillName of skillDirs) {
199
- const mdPath = join(skillsDir, skillName, 'SKILL.md');
200
- const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
223
+ for (const { skillDir } of skillPaths) {
224
+ const mdPath = join(skillDir, 'SKILL.md');
225
+ const yamlPath = join(skillDir, 'SKILL.yaml');
201
226
 
202
227
  const mdContent = readFileSync(mdPath, 'utf-8');
203
228
  const yamlContent = readFileSync(yamlPath, 'utf-8');