@orderful/droid 0.26.0 → 0.27.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +7 -7
  2. package/AGENTS.md +36 -32
  3. package/CHANGELOG.md +46 -0
  4. package/dist/bin/droid.js +140 -24
  5. package/dist/index.js +97 -25
  6. package/dist/lib/migrations.d.ts +8 -0
  7. package/dist/lib/migrations.d.ts.map +1 -1
  8. package/dist/lib/skills.d.ts.map +1 -1
  9. package/dist/lib/tools.d.ts.map +1 -1
  10. package/dist/lib/types.d.ts +10 -2
  11. package/dist/lib/types.d.ts.map +1 -1
  12. package/dist/tools/brain/.claude-plugin/plugin.json +1 -1
  13. package/dist/tools/brain/TOOL.yaml +7 -5
  14. package/dist/tools/brain/commands/brain.md +17 -49
  15. package/dist/tools/brain/commands/scratchpad.md +13 -50
  16. package/{src/tools/brain/skills/droid-brain → dist/tools/brain/skills/brain}/SKILL.md +5 -5
  17. package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/SKILL.md +8 -8
  18. package/dist/tools/coach/.claude-plugin/plugin.json +1 -1
  19. package/dist/tools/coach/TOOL.yaml +4 -3
  20. package/dist/tools/coach/commands/coach.md +14 -54
  21. package/{src/tools/coach/skills/droid-coach → dist/tools/coach/skills/coach}/SKILL.md +4 -3
  22. package/dist/tools/code-review/.claude-plugin/plugin.json +1 -1
  23. package/dist/tools/code-review/TOOL.yaml +4 -3
  24. package/dist/tools/code-review/commands/code-review.md +18 -102
  25. package/dist/tools/code-review/skills/code-review/SKILL.md +154 -0
  26. package/dist/tools/codex/.claude-plugin/plugin.json +1 -1
  27. package/dist/tools/codex/TOOL.yaml +4 -3
  28. package/dist/tools/codex/commands/codex.md +18 -65
  29. package/dist/tools/codex/skills/{droid-codex → codex}/SKILL.md +64 -45
  30. package/{src/tools/codex/skills/droid-codex → dist/tools/codex/skills/codex}/references/loading.md +94 -55
  31. package/dist/tools/codex/skills/codex/scripts/git-finish-write.d.ts.map +1 -0
  32. package/dist/tools/codex/skills/codex/scripts/git-preamble.d.ts.map +1 -0
  33. package/dist/tools/codex/skills/codex/scripts/git-start-write.d.ts.map +1 -0
  34. package/dist/tools/comments/.claude-plugin/plugin.json +1 -1
  35. package/dist/tools/comments/TOOL.yaml +4 -3
  36. package/dist/tools/comments/commands/comments.md +12 -14
  37. package/{src/tools/comments/skills/droid-comments → dist/tools/comments/skills/comments}/SKILL.md +3 -1
  38. package/dist/tools/project/.claude-plugin/plugin.json +1 -1
  39. package/dist/tools/project/TOOL.yaml +4 -3
  40. package/dist/tools/project/commands/project.md +12 -27
  41. package/dist/tools/project/skills/{droid-project → project}/SKILL.md +12 -11
  42. package/dist/tools/tech-design/.claude-plugin/plugin.json +1 -1
  43. package/dist/tools/tech-design/TOOL.yaml +4 -3
  44. package/dist/tools/tech-design/commands/tech-design.md +18 -80
  45. package/{src/tools/tech-design/skills/droid-tech-design → dist/tools/tech-design/skills/tech-design}/SKILL.md +1 -1
  46. package/package.json +1 -1
  47. package/src/commands/tui/components/Badge.test.tsx +10 -4
  48. package/src/commands/tui.tsx +4 -4
  49. package/src/lib/migrations.ts +154 -4
  50. package/src/lib/skills.test.ts +199 -74
  51. package/src/lib/skills.ts +55 -54
  52. package/src/lib/tools.ts +19 -12
  53. package/src/lib/types.ts +20 -5
  54. package/src/tools/brain/.claude-plugin/plugin.json +1 -1
  55. package/src/tools/brain/TOOL.yaml +7 -5
  56. package/src/tools/brain/commands/brain.md +17 -49
  57. package/src/tools/brain/commands/scratchpad.md +13 -50
  58. package/{dist/tools/brain/skills/droid-brain → src/tools/brain/skills/brain}/SKILL.md +5 -5
  59. package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/SKILL.md +8 -8
  60. package/src/tools/coach/.claude-plugin/plugin.json +1 -1
  61. package/src/tools/coach/TOOL.yaml +4 -3
  62. package/src/tools/coach/commands/coach.md +14 -54
  63. package/{dist/tools/coach/skills/droid-coach → src/tools/coach/skills/coach}/SKILL.md +4 -3
  64. package/src/tools/code-review/.claude-plugin/plugin.json +1 -1
  65. package/src/tools/code-review/TOOL.yaml +4 -3
  66. package/src/tools/code-review/commands/code-review.md +18 -102
  67. package/src/tools/code-review/skills/code-review/SKILL.md +154 -0
  68. package/src/tools/codex/.claude-plugin/plugin.json +1 -1
  69. package/src/tools/codex/TOOL.yaml +4 -3
  70. package/src/tools/codex/commands/codex.md +18 -65
  71. package/src/tools/codex/skills/{droid-codex → codex}/SKILL.md +64 -45
  72. package/{dist/tools/codex/skills/droid-codex → src/tools/codex/skills/codex}/references/loading.md +94 -55
  73. package/src/tools/comments/.claude-plugin/plugin.json +1 -1
  74. package/src/tools/comments/TOOL.yaml +4 -3
  75. package/src/tools/comments/commands/comments.md +12 -14
  76. package/{dist/tools/comments/skills/droid-comments → src/tools/comments/skills/comments}/SKILL.md +3 -1
  77. package/src/tools/project/.claude-plugin/plugin.json +1 -1
  78. package/src/tools/project/TOOL.yaml +4 -3
  79. package/src/tools/project/commands/project.md +12 -27
  80. package/src/tools/project/skills/{droid-project → project}/SKILL.md +12 -11
  81. package/src/tools/tech-design/.claude-plugin/plugin.json +1 -1
  82. package/src/tools/tech-design/TOOL.yaml +4 -3
  83. package/src/tools/tech-design/commands/tech-design.md +18 -80
  84. package/{dist/tools/tech-design/skills/droid-tech-design → src/tools/tech-design/skills/tech-design}/SKILL.md +1 -1
  85. package/dist/tools/code-review/skills/droid-code-review/SKILL.md +0 -55
  86. package/dist/tools/codex/skills/droid-codex/scripts/git-finish-write.d.ts.map +0 -1
  87. package/dist/tools/codex/skills/droid-codex/scripts/git-preamble.d.ts.map +0 -1
  88. package/dist/tools/codex/skills/droid-codex/scripts/git-start-write.d.ts.map +0 -1
  89. package/src/tools/code-review/skills/droid-code-review/SKILL.md +0 -55
  90. /package/dist/tools/brain/skills/{droid-brain → brain}/references/metadata.md +0 -0
  91. /package/dist/tools/brain/skills/{droid-brain → brain}/references/naming.md +0 -0
  92. /package/dist/tools/brain/skills/{droid-brain → brain}/references/templates.md +0 -0
  93. /package/dist/tools/brain/skills/{droid-brain → brain}/references/workflows.md +0 -0
  94. /package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/templates.md +0 -0
  95. /package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/workflows.md +0 -0
  96. /package/dist/tools/codex/skills/{droid-codex → codex}/references/creating.md +0 -0
  97. /package/dist/tools/codex/skills/{droid-codex → codex}/references/decisions.md +0 -0
  98. /package/dist/tools/codex/skills/{droid-codex → codex}/references/topics.md +0 -0
  99. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.d.ts +0 -0
  100. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.ts +0 -0
  101. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.d.ts +0 -0
  102. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.ts +0 -0
  103. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-scripts.test.ts +0 -0
  104. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.d.ts +0 -0
  105. /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.ts +0 -0
  106. /package/dist/tools/project/skills/{droid-project → project}/references/changelog.md +0 -0
  107. /package/dist/tools/project/skills/{droid-project → project}/references/creating.md +0 -0
  108. /package/dist/tools/project/skills/{droid-project → project}/references/loading.md +0 -0
  109. /package/dist/tools/project/skills/{droid-project → project}/references/templates.md +0 -0
  110. /package/dist/tools/project/skills/{droid-project → project}/references/updating.md +0 -0
  111. /package/dist/tools/project/skills/{droid-project → project}/references/versioning.md +0 -0
  112. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/draft.md +0 -0
  113. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/gaps.md +0 -0
  114. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/publish.md +0 -0
  115. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/research-doc-template.md +0 -0
  116. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/rollup-template.md +0 -0
  117. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/start.md +0 -0
  118. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/think.md +0 -0
  119. /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/thought-doc-template.md +0 -0
  120. /package/src/tools/brain/skills/{droid-brain → brain}/references/metadata.md +0 -0
  121. /package/src/tools/brain/skills/{droid-brain → brain}/references/naming.md +0 -0
  122. /package/src/tools/brain/skills/{droid-brain → brain}/references/templates.md +0 -0
  123. /package/src/tools/brain/skills/{droid-brain → brain}/references/workflows.md +0 -0
  124. /package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/templates.md +0 -0
  125. /package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/workflows.md +0 -0
  126. /package/src/tools/codex/skills/{droid-codex → codex}/references/creating.md +0 -0
  127. /package/src/tools/codex/skills/{droid-codex → codex}/references/decisions.md +0 -0
  128. /package/src/tools/codex/skills/{droid-codex → codex}/references/topics.md +0 -0
  129. /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.ts +0 -0
  130. /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.ts +0 -0
  131. /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-scripts.test.ts +0 -0
  132. /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.ts +0 -0
  133. /package/src/tools/project/skills/{droid-project → project}/references/changelog.md +0 -0
  134. /package/src/tools/project/skills/{droid-project → project}/references/creating.md +0 -0
  135. /package/src/tools/project/skills/{droid-project → project}/references/loading.md +0 -0
  136. /package/src/tools/project/skills/{droid-project → project}/references/templates.md +0 -0
  137. /package/src/tools/project/skills/{droid-project → project}/references/updating.md +0 -0
  138. /package/src/tools/project/skills/{droid-project → project}/references/versioning.md +0 -0
  139. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/draft.md +0 -0
  140. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/gaps.md +0 -0
  141. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/publish.md +0 -0
  142. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/research-doc-template.md +0 -0
  143. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/rollup-template.md +0 -0
  144. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/start.md +0 -0
  145. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/think.md +0 -0
  146. /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/thought-doc-template.md +0 -0
@@ -1,10 +1,17 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'fs';
3
- import { join } from 'path';
4
- import { tmpdir } from 'os';
5
- import { homedir } from 'os';
6
- import YAML from 'yaml';
7
- import { Platform, SkillStatus } from './types';
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ } from "fs";
10
+ import { join } from "path";
11
+ import { tmpdir } from "os";
12
+ import { homedir } from "os";
13
+ import YAML from "yaml";
14
+ import { Platform, SkillStatus } from "./types";
8
15
  import {
9
16
  getSkillsInstallPath,
10
17
  getCommandsInstallPath,
@@ -12,7 +19,9 @@ import {
12
19
  getSkillStatusDisplay,
13
20
  getBundledSkillsDir,
14
21
  loadSkillManifest,
15
- } from './skills';
22
+ installSkill,
23
+ } from "./skills";
24
+ import { loadConfig, saveConfig } from "./config";
16
25
 
17
26
  /**
18
27
  * Parse YAML frontmatter from markdown content
@@ -27,73 +36,73 @@ function parseFrontmatter(content: string): Record<string, unknown> | null {
27
36
  }
28
37
  }
29
38
 
30
- describe('getSkillsInstallPath', () => {
31
- it('should return Claude Code path', () => {
39
+ describe("getSkillsInstallPath", () => {
40
+ it("should return Claude Code path", () => {
32
41
  const path = getSkillsInstallPath(Platform.ClaudeCode);
33
- expect(path).toBe(join(homedir(), '.claude', 'skills'));
42
+ expect(path).toBe(join(homedir(), ".claude", "skills"));
34
43
  });
35
44
 
36
- it('should return OpenCode path', () => {
45
+ it("should return OpenCode path", () => {
37
46
  const path = getSkillsInstallPath(Platform.OpenCode);
38
- expect(path).toBe(join(homedir(), '.config', 'opencode', 'skills'));
47
+ expect(path).toBe(join(homedir(), ".config", "opencode", "skills"));
39
48
  });
40
49
  });
41
50
 
42
- describe('getCommandsInstallPath', () => {
43
- it('should return Claude Code commands path', () => {
51
+ describe("getCommandsInstallPath", () => {
52
+ it("should return Claude Code commands path", () => {
44
53
  const path = getCommandsInstallPath(Platform.ClaudeCode);
45
- expect(path).toBe(join(homedir(), '.claude', 'commands'));
54
+ expect(path).toBe(join(homedir(), ".claude", "commands"));
46
55
  });
47
56
 
48
- it('should return OpenCode commands path', () => {
57
+ it("should return OpenCode commands path", () => {
49
58
  const path = getCommandsInstallPath(Platform.OpenCode);
50
- expect(path).toBe(join(homedir(), '.config', 'opencode', 'command'));
59
+ expect(path).toBe(join(homedir(), ".config", "opencode", "command"));
51
60
  });
52
61
  });
53
62
 
54
- describe('getPlatformConfigPath', () => {
55
- it('should return Claude Code CLAUDE.md path', () => {
63
+ describe("getPlatformConfigPath", () => {
64
+ it("should return Claude Code CLAUDE.md path", () => {
56
65
  const path = getPlatformConfigPath(Platform.ClaudeCode);
57
- expect(path).toBe(join(homedir(), '.claude', 'CLAUDE.md'));
66
+ expect(path).toBe(join(homedir(), ".claude", "CLAUDE.md"));
58
67
  });
59
68
 
60
- it('should return OpenCode AGENTS.md path', () => {
69
+ it("should return OpenCode AGENTS.md path", () => {
61
70
  const path = getPlatformConfigPath(Platform.OpenCode);
62
- expect(path).toBe(join(homedir(), '.config', 'opencode', 'AGENTS.md'));
71
+ expect(path).toBe(join(homedir(), ".config", "opencode", "AGENTS.md"));
63
72
  });
64
73
  });
65
74
 
66
- describe('getBundledSkillsDir', () => {
67
- it('should return a path ending in tools', () => {
75
+ describe("getBundledSkillsDir", () => {
76
+ it("should return a path ending in tools", () => {
68
77
  const dir = getBundledSkillsDir();
69
- expect(dir.endsWith('tools')).toBe(true);
78
+ expect(dir.endsWith("tools")).toBe(true);
70
79
  });
71
80
 
72
- it('should return an existing directory', () => {
81
+ it("should return an existing directory", () => {
73
82
  const dir = getBundledSkillsDir();
74
83
  expect(existsSync(dir)).toBe(true);
75
84
  });
76
85
  });
77
86
 
78
- describe('getSkillStatusDisplay', () => {
79
- it('should return [alpha] for alpha status', () => {
80
- expect(getSkillStatusDisplay(SkillStatus.Alpha)).toBe('[alpha]');
87
+ describe("getSkillStatusDisplay", () => {
88
+ it("should return [alpha] for alpha status", () => {
89
+ expect(getSkillStatusDisplay(SkillStatus.Alpha)).toBe("[alpha]");
81
90
  });
82
91
 
83
- it('should return [beta] for beta status', () => {
84
- expect(getSkillStatusDisplay(SkillStatus.Beta)).toBe('[beta]');
92
+ it("should return [beta] for beta status", () => {
93
+ expect(getSkillStatusDisplay(SkillStatus.Beta)).toBe("[beta]");
85
94
  });
86
95
 
87
- it('should return empty string for stable status', () => {
88
- expect(getSkillStatusDisplay(SkillStatus.Stable)).toBe('');
96
+ it("should return empty string for stable status", () => {
97
+ expect(getSkillStatusDisplay(SkillStatus.Stable)).toBe("");
89
98
  });
90
99
 
91
- it('should return empty string for undefined status', () => {
92
- expect(getSkillStatusDisplay(undefined)).toBe('');
100
+ it("should return empty string for undefined status", () => {
101
+ expect(getSkillStatusDisplay(undefined)).toBe("");
93
102
  });
94
103
  });
95
104
 
96
- describe('skill manifest parsing', () => {
105
+ describe("skill manifest parsing", () => {
97
106
  let testDir: string;
98
107
 
99
108
  beforeEach(() => {
@@ -107,44 +116,44 @@ describe('skill manifest parsing', () => {
107
116
  }
108
117
  });
109
118
 
110
- it('should parse valid skill manifest', () => {
119
+ it("should parse valid skill manifest", () => {
111
120
  const manifest = {
112
- name: 'test-skill',
113
- description: 'A test skill',
114
- version: '1.0.0',
115
- status: 'beta',
116
- dependencies: ['comments'],
121
+ name: "test-skill",
122
+ description: "A test skill",
123
+ version: "1.0.0",
124
+ status: "beta",
125
+ dependencies: ["comments"],
117
126
  provides_output: true,
118
127
  };
119
128
 
120
- const manifestPath = join(testDir, 'SKILL.yaml');
121
- writeFileSync(manifestPath, YAML.stringify(manifest), 'utf-8');
129
+ const manifestPath = join(testDir, "SKILL.yaml");
130
+ writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
122
131
 
123
- const content = require('fs').readFileSync(manifestPath, 'utf-8');
132
+ const content = require("fs").readFileSync(manifestPath, "utf-8");
124
133
  const parsed = YAML.parse(content);
125
134
 
126
- expect(parsed.name).toBe('test-skill');
127
- expect(parsed.description).toBe('A test skill');
128
- expect(parsed.version).toBe('1.0.0');
129
- expect(parsed.status).toBe('beta');
130
- expect(parsed.dependencies).toEqual(['comments']);
135
+ expect(parsed.name).toBe("test-skill");
136
+ expect(parsed.description).toBe("A test skill");
137
+ expect(parsed.version).toBe("1.0.0");
138
+ expect(parsed.status).toBe("beta");
139
+ expect(parsed.dependencies).toEqual(["comments"]);
131
140
  expect(parsed.provides_output).toBe(true);
132
141
  });
133
142
 
134
- it('should handle manifest without optional fields', () => {
143
+ it("should handle manifest without optional fields", () => {
135
144
  const manifest = {
136
- name: 'minimal-skill',
137
- description: 'Minimal',
138
- version: '0.1.0',
145
+ name: "minimal-skill",
146
+ description: "Minimal",
147
+ version: "0.1.0",
139
148
  };
140
149
 
141
- const manifestPath = join(testDir, 'SKILL.yaml');
142
- writeFileSync(manifestPath, YAML.stringify(manifest), 'utf-8');
150
+ const manifestPath = join(testDir, "SKILL.yaml");
151
+ writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
143
152
 
144
- const content = require('fs').readFileSync(manifestPath, 'utf-8');
153
+ const content = require("fs").readFileSync(manifestPath, "utf-8");
145
154
  const parsed = YAML.parse(content);
146
155
 
147
- expect(parsed.name).toBe('minimal-skill');
156
+ expect(parsed.name).toBe("minimal-skill");
148
157
  expect(parsed.status).toBeUndefined();
149
158
  expect(parsed.dependencies).toBeUndefined();
150
159
  expect(parsed.provides_output).toBeUndefined();
@@ -154,7 +163,9 @@ describe('skill manifest parsing', () => {
154
163
  /**
155
164
  * Helper to get all skill directories in the new tools/{tool}/skills/{skill} structure
156
165
  */
157
- function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir: string }> {
166
+ function getAllSkillPaths(
167
+ toolsDir: string,
168
+ ): Array<{ skillName: string; skillDir: string }> {
158
169
  const skillPaths: Array<{ skillName: string; skillDir: string }> = [];
159
170
 
160
171
  const toolDirs = readdirSync(toolsDir, { withFileTypes: true })
@@ -162,7 +173,7 @@ function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir
162
173
  .map((d) => d.name);
163
174
 
164
175
  for (const toolName of toolDirs) {
165
- const skillsDir = join(toolsDir, toolName, 'skills');
176
+ const skillsDir = join(toolsDir, toolName, "skills");
166
177
  if (!existsSync(skillsDir)) continue;
167
178
 
168
179
  const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
@@ -180,43 +191,43 @@ function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir
180
191
  return skillPaths;
181
192
  }
182
193
 
183
- describe('bundled skills validation', () => {
184
- it('all skills should have SKILL.md', () => {
194
+ describe("bundled skills validation", () => {
195
+ it("all skills should have SKILL.md", () => {
185
196
  const toolsDir = getBundledSkillsDir();
186
197
  const skillPaths = getAllSkillPaths(toolsDir);
187
198
 
188
199
  expect(skillPaths.length).toBeGreaterThan(0);
189
200
 
190
201
  for (const { skillDir } of skillPaths) {
191
- const skillMdPath = join(skillDir, 'SKILL.md');
202
+ const skillMdPath = join(skillDir, "SKILL.md");
192
203
  expect(existsSync(skillMdPath)).toBe(true);
193
204
  }
194
205
  });
195
206
 
196
- it('all skills should have metadata in TOOL.yaml', () => {
207
+ it("all skills should have metadata in TOOL.yaml", () => {
197
208
  const toolsDir = getBundledSkillsDir();
198
209
  const skillPaths = getAllSkillPaths(toolsDir);
199
210
 
200
211
  for (const { skillName, skillDir } of skillPaths) {
201
212
  // Get the parent tool directory
202
- const toolDir = join(skillDir, '..', '..');
203
- const toolYamlPath = join(toolDir, 'TOOL.yaml');
213
+ const toolDir = join(skillDir, "..", "..");
214
+ const toolYamlPath = join(toolDir, "TOOL.yaml");
204
215
  expect(existsSync(toolYamlPath)).toBe(true);
205
216
 
206
- const toolContent = readFileSync(toolYamlPath, 'utf-8');
217
+ const toolContent = readFileSync(toolYamlPath, "utf-8");
207
218
  const toolManifest = YAML.parse(toolContent);
208
219
 
209
220
  // Find the skill in includes.skills
210
221
  const skillInclude = toolManifest.includes?.skills?.find(
211
- (s: { name: string }) => s.name === skillName
222
+ (s: { name: string }) => s.name === skillName,
212
223
  );
213
224
  expect(skillInclude).toBeDefined();
214
- expect(typeof toolManifest.description).toBe('string');
215
- expect(typeof toolManifest.version).toBe('string');
225
+ expect(typeof toolManifest.description).toBe("string");
226
+ expect(typeof toolManifest.version).toBe("string");
216
227
  }
217
228
  });
218
229
 
219
- it('loadSkillManifest should return valid manifest from TOOL.yaml', () => {
230
+ it("loadSkillManifest should return valid manifest from TOOL.yaml", () => {
220
231
  const toolsDir = getBundledSkillsDir();
221
232
  const skillPaths = getAllSkillPaths(toolsDir);
222
233
 
@@ -225,8 +236,122 @@ describe('bundled skills validation', () => {
225
236
 
226
237
  expect(manifest).not.toBeNull();
227
238
  expect(manifest?.name).toBe(skillName);
228
- expect(typeof manifest?.description).toBe('string');
229
- expect(typeof manifest?.version).toBe('string');
239
+ expect(typeof manifest?.description).toBe("string");
240
+ expect(typeof manifest?.version).toBe("string");
241
+ }
242
+ });
243
+ });
244
+ describe('platform-specific command installation', () => {
245
+ let testConfigDir: string;
246
+ let testSkillsDir: string;
247
+ let testCommandsDir: string;
248
+ let originalConfig: any;
249
+
250
+ beforeEach(() => {
251
+ // Save original config
252
+ originalConfig = loadConfig();
253
+
254
+ // Create temporary directories for testing
255
+ testConfigDir = join(tmpdir(), `droid-test-config-${Date.now()}`);
256
+ testSkillsDir = join(tmpdir(), `droid-test-skills-${Date.now()}`);
257
+ testCommandsDir = join(tmpdir(), `droid-test-commands-${Date.now()}`);
258
+
259
+ mkdirSync(testConfigDir, { recursive: true });
260
+ mkdirSync(testSkillsDir, { recursive: true });
261
+ mkdirSync(testCommandsDir, { recursive: true });
262
+ });
263
+
264
+ afterEach(() => {
265
+ // Restore original config
266
+ saveConfig(originalConfig);
267
+
268
+ // Cleanup test directories
269
+ if (existsSync(testConfigDir)) {
270
+ rmSync(testConfigDir, { recursive: true });
271
+ }
272
+ if (existsSync(testSkillsDir)) {
273
+ rmSync(testSkillsDir, { recursive: true });
274
+ }
275
+ if (existsSync(testCommandsDir)) {
276
+ rmSync(testCommandsDir, { recursive: true });
277
+ }
278
+ });
279
+
280
+ it('should install all commands on OpenCode platform', () => {
281
+ // This is an integration test that would require mocking the install paths
282
+ // For now, we verify the logic exists by checking tool manifests
283
+ const toolsDir = getBundledSkillsDir();
284
+
285
+ // Find brain tool which has both primary and alias commands
286
+ const brainToolDir = join(toolsDir, 'brain');
287
+ const toolYamlPath = join(brainToolDir, 'TOOL.yaml');
288
+
289
+ if (!existsSync(toolYamlPath)) {
290
+ // Skip test if brain tool doesn't exist
291
+ return;
292
+ }
293
+
294
+ const toolContent = readFileSync(toolYamlPath, 'utf-8');
295
+ const toolManifest = YAML.parse(toolContent);
296
+
297
+ // Verify brain has both primary and alias commands
298
+ const commands = toolManifest.includes?.commands || [];
299
+ const brainCommand = commands.find((c: any) => c.name === 'brain');
300
+ const scratchpadCommand = commands.find((c: any) => c.name === 'scratchpad');
301
+
302
+ expect(brainCommand).toBeDefined();
303
+ expect(brainCommand.is_alias).toBe(false);
304
+
305
+ expect(scratchpadCommand).toBeDefined();
306
+ expect(scratchpadCommand.is_alias).toBe(true);
307
+ });
308
+
309
+ it('should identify alias commands correctly', () => {
310
+ const toolsDir = getBundledSkillsDir();
311
+ const brainToolDir = join(toolsDir, 'brain');
312
+ const toolYamlPath = join(brainToolDir, 'TOOL.yaml');
313
+
314
+ if (!existsSync(toolYamlPath)) {
315
+ return;
316
+ }
317
+
318
+ const toolContent = readFileSync(toolYamlPath, 'utf-8');
319
+ const toolManifest = YAML.parse(toolContent);
320
+ const commands = toolManifest.includes?.commands || [];
321
+
322
+ // All alias commands should have is_alias: true
323
+ const aliasCommands = commands.filter((c: any) => c.is_alias === true);
324
+ const primaryCommands = commands.filter((c: any) => c.is_alias === false);
325
+
326
+ // Should have at least one of each type in brain tool
327
+ expect(aliasCommands.length).toBeGreaterThan(0);
328
+ expect(primaryCommands.length).toBeGreaterThan(0);
329
+
330
+ // Verify scratchpad is marked as alias
331
+ const scratchpad = commands.find((c: any) => c.name === 'scratchpad');
332
+ if (scratchpad) {
333
+ expect(scratchpad.is_alias).toBe(true);
334
+ }
335
+ });
336
+
337
+ it('should have is_alias property on all command objects', () => {
338
+ const toolsDir = getBundledSkillsDir();
339
+ const toolDirs = readdirSync(toolsDir, { withFileTypes: true })
340
+ .filter(d => d.isDirectory())
341
+ .map(d => join(toolsDir, d.name));
342
+
343
+ for (const toolDir of toolDirs) {
344
+ const toolYamlPath = join(toolDir, 'TOOL.yaml');
345
+ if (!existsSync(toolYamlPath)) continue;
346
+
347
+ const toolContent = readFileSync(toolYamlPath, 'utf-8');
348
+ const toolManifest = YAML.parse(toolContent);
349
+ const commands = toolManifest.includes?.commands || [];
350
+
351
+ // All commands should have is_alias property (boolean)
352
+ for (const cmd of commands) {
353
+ expect(typeof cmd.is_alias).toBe('boolean');
354
+ }
230
355
  }
231
356
  });
232
357
  });
package/src/lib/skills.ts CHANGED
@@ -411,6 +411,15 @@ export function installSkill(skillName: string): {
411
411
  };
412
412
  }
413
413
 
414
+ // Load tool manifest to get command metadata (for is_alias flag)
415
+ const toolManifest = loadToolManifest(toolDir);
416
+ if (!toolManifest) {
417
+ return {
418
+ success: false,
419
+ message: `Invalid tool manifest for tool containing '${skillName}'`,
420
+ };
421
+ }
422
+
414
423
  // Check dependencies
415
424
  if (manifest.dependencies) {
416
425
  for (const dep of manifest.dependencies) {
@@ -428,49 +437,39 @@ export function installSkill(skillName: string): {
428
437
  const commandsPath = getCommandsInstallPath(config.platform);
429
438
  const tools = getPlatformTools(config);
430
439
 
431
- // Clean up old skill directory if this is a renamed skill (v0.18.0 workaround for Claude Code bug)
432
- // Renamed skills have 'droid-' prefix, so check for old directory without prefix
433
- // Bug: https://github.com/anthropics/claude-code/issues/14945
434
- //
435
- // TO REVERSE WHEN BUG IS FIXED:
436
- // 1. Rename directories: droid-comments/ → comments/, droid-brain/ → brain/, etc.
437
- // 2. Update TOOL.yaml manifests back to original skill names
438
- // 3. Update SKILL.md frontmatter back to original names
439
- // 4. Replace this cleanup logic with inverse:
440
- // const renamedSkills = ['comments', 'brain', 'project', 'coach', 'code-review', 'brain-obsidian'];
441
- // if (renamedSkills.includes(skillName)) {
442
- // const droidPrefixedName = `droid-${skillName}`;
443
- // const droidPrefixedDir = join(skillsPath, droidPrefixedName);
444
- // if (existsSync(droidPrefixedDir)) {
445
- // rmSync(droidPrefixedDir, { recursive: true });
446
- // }
447
- // if (tools[droidPrefixedName]) {
448
- // delete tools[droidPrefixedName];
449
- // }
450
- // }
451
- // 5. Bump tool versions and create changeset
452
- // Migration logic: Handle skills renamed from {name} to droid-{name} (v0.18.0 workaround)
453
- // This handles both fresh installs and updates where old directory/config entries exist
454
- if (skillName.startsWith('droid-')) {
455
- const oldSkillName = skillName.replace(/^droid-/, '');
456
- const oldSkillDir = join(skillsPath, oldSkillName);
457
-
458
- // Remove or migrate old skill directory
459
- if (existsSync(oldSkillDir)) {
440
+ // Clean up old droid- prefixed directories from v0.18.x (Claude Code bug workaround)
441
+ // CLEANUP: Remove after v0.22.0 or 2026-04-01 (3 months post-v0.21.0 release)
442
+ // Migration for droid- prefix removal (v0.18.x → v0.21.0)
443
+ // These skills were temporarily renamed with droid- prefix, now back to original names
444
+ const renamedSkills = [
445
+ 'comments',
446
+ 'brain',
447
+ 'project',
448
+ 'coach',
449
+ 'code-review',
450
+ 'codex',
451
+ 'tech-design',
452
+ 'brain-obsidian',
453
+ ];
454
+ if (renamedSkills.includes(skillName)) {
455
+ const droidPrefixedName = `droid-${skillName}`;
456
+ const droidPrefixedDir = join(skillsPath, droidPrefixedName);
457
+
458
+ // Remove old droid- prefixed directory
459
+ if (existsSync(droidPrefixedDir)) {
460
460
  try {
461
- rmSync(oldSkillDir, { recursive: true });
461
+ rmSync(droidPrefixedDir, { recursive: true });
462
462
  } catch (error) {
463
463
  // Non-fatal: Log warning but continue installation
464
464
  console.warn(
465
- `Warning: Could not remove old skill directory ${oldSkillDir}: ${error}`,
465
+ `Warning: Could not remove old skill directory ${droidPrefixedDir}: ${error}`,
466
466
  );
467
467
  }
468
468
  }
469
469
 
470
470
  // Migrate old config entry if it exists
471
- // This allows tools to show as "update available" rather than "not installed"
472
- if (tools[oldSkillName]) {
473
- delete tools[oldSkillName];
471
+ if (tools[droidPrefixedName]) {
472
+ delete tools[droidPrefixedName];
474
473
  // Save immediately to ensure cleanup is persisted (matches pattern in tools.ts)
475
474
  setPlatformTools(config, tools);
476
475
  saveConfig(config);
@@ -484,23 +483,10 @@ export function installSkill(skillName: string): {
484
483
  // Check for collisions BEFORE installing (only if not already installed by droid)
485
484
  // Note: If skill folder exists but skill isn't in config, we allow overwriting (stale state
486
485
  // from platform switch or manual cleanup). Command/agent collisions still checked.
487
- //
488
- // Special handling for droid- prefix migration (temporary workaround for Claude Code bug #14945):
489
- // If installing droid-comments, check if 'comments' is in config (old naming)
490
- const oldSkillName = skillName.startsWith('droid-')
491
- ? skillName.replace(/^droid-/, '')
492
- : null;
493
- const isAlreadyInstalled =
494
- tools[skillName] || (oldSkillName && tools[oldSkillName]);
486
+ const isAlreadyInstalled = tools[skillName];
495
487
 
496
488
  if (!isAlreadyInstalled) {
497
- // For collision detection, also check if this is the same tool (with/without droid- prefix)
498
- // E.g., installing droid-comments should be allowed to overwrite existing comments.md
499
- //
500
- // TO REVERSE WHEN BUG #14945 IS FIXED:
501
- // Remove the normalizedToolName logic - tools will have original names without droid- prefix
502
489
  const toolName = basename(toolDir);
503
- const normalizedToolName = toolName.replace(/^droid-/, '');
504
490
 
505
491
  // Check command file collisions (these could conflict with other skills)
506
492
  if (existsSync(commandsSource)) {
@@ -512,9 +498,9 @@ export function installSkill(skillName: string): {
512
498
  if (existsSync(targetCommandPath)) {
513
499
  const commandName = file.replace('.md', '');
514
500
 
515
- // Allow overwrite if the command name matches the tool name (with/without prefix)
501
+ // Allow overwrite if the command name matches the tool name
516
502
  // E.g., installing 'comments' tool can overwrite 'comments.md'
517
- if (commandName === toolName || commandName === normalizedToolName) {
503
+ if (commandName === toolName) {
518
504
  continue; // Same tool, allow overwrite
519
505
  }
520
506
 
@@ -595,6 +581,7 @@ export function installSkill(skillName: string): {
595
581
  }
596
582
 
597
583
  // Copy commands if present (from tool level)
584
+ // Platform-specific: Only install primary commands on OpenCode, always install aliases
598
585
  if (existsSync(commandsSource)) {
599
586
  if (!existsSync(commandsPath)) {
600
587
  mkdirSync(commandsPath, { recursive: true });
@@ -602,11 +589,25 @@ export function installSkill(skillName: string): {
602
589
  const commandFiles = readdirSync(commandsSource).filter(
603
590
  (f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
604
591
  );
592
+
605
593
  for (const file of commandFiles) {
606
- const sourcePath = join(commandsSource, file);
607
- const targetPath = join(commandsPath, file);
608
- const content = readFileSync(sourcePath, 'utf-8');
609
- writeFileSync(targetPath, content);
594
+ const commandName = file.replace('.md', '');
595
+
596
+ // Find this command in the tool manifest to check if it's an alias
597
+ const commandMeta = toolManifest.includes.commands.find(
598
+ (cmd) => (typeof cmd === 'string' ? cmd : cmd.name) === commandName,
599
+ );
600
+ const isAlias = typeof commandMeta === 'object' && commandMeta.is_alias;
601
+
602
+ // Install if: OpenCode (all commands) OR alias (both platforms)
603
+ const shouldInstall = config.platform === Platform.OpenCode || isAlias;
604
+
605
+ if (shouldInstall) {
606
+ const sourcePath = join(commandsSource, file);
607
+ const targetPath = join(commandsPath, file);
608
+ const content = readFileSync(sourcePath, 'utf-8');
609
+ writeFileSync(targetPath, content);
610
+ }
610
611
  }
611
612
  }
612
613
 
package/src/lib/tools.ts CHANGED
@@ -3,7 +3,12 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import YAML from 'yaml';
5
5
  import { loadConfig, saveConfig } from './config';
6
- import { type ToolManifest, type ToolIncludes, getPlatformTools, setPlatformTools } from './types';
6
+ import {
7
+ type ToolManifest,
8
+ type ToolIncludes,
9
+ getPlatformTools,
10
+ setPlatformTools,
11
+ } from './types';
7
12
  import { compareSemver } from './version';
8
13
 
9
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -78,15 +83,15 @@ export function isToolInstalled(toolName: string): boolean {
78
83
  const installedTools = getPlatformTools(config);
79
84
 
80
85
  // A tool is installed if any of its required skills are in the installed tools
81
- const tool = getBundledTools().find(t => t.name === toolName);
86
+ const tool = getBundledTools().find((t) => t.name === toolName);
82
87
  if (!tool) return false;
83
88
 
84
89
  const requiredSkills = tool.includes.skills
85
- .filter(s => s.required)
86
- .map(s => s.name);
90
+ .filter((s) => s.required)
91
+ .map((s) => s.name);
87
92
 
88
93
  // Tool is installed if at least one required skill is installed
89
- return requiredSkills.some(skillName => skillName in installedTools);
94
+ return requiredSkills.some((skillName) => skillName in installedTools);
90
95
  }
91
96
 
92
97
  /**
@@ -96,13 +101,13 @@ export function getInstalledToolVersion(toolName: string): string | null {
96
101
  const config = loadConfig();
97
102
  const installedTools = getPlatformTools(config);
98
103
 
99
- const tool = getBundledTools().find(t => t.name === toolName);
104
+ const tool = getBundledTools().find((t) => t.name === toolName);
100
105
  if (!tool) return null;
101
106
 
102
107
  // Get version from the first installed required skill
103
108
  const requiredSkills = tool.includes.skills
104
- .filter(s => s.required)
105
- .map(s => s.name);
109
+ .filter((s) => s.required)
110
+ .map((s) => s.name);
106
111
 
107
112
  for (const skillName of requiredSkills) {
108
113
  if (installedTools[skillName]) {
@@ -139,7 +144,7 @@ export interface ToolUpdateInfo {
139
144
  */
140
145
  export function getToolUpdateStatus(toolName: string): ToolUpdateInfo {
141
146
  const installedVersion = getInstalledToolVersion(toolName);
142
- const tool = getBundledTools().find(t => t.name === toolName);
147
+ const tool = getBundledTools().find((t) => t.name === toolName);
143
148
  const bundledVersion = tool?.version || null;
144
149
 
145
150
  if (!installedVersion || !bundledVersion) {
@@ -176,10 +181,12 @@ export function getToolsWithUpdates(): ToolUpdateInfo[] {
176
181
  for (const tool of bundledTools) {
177
182
  // Find if any of the tool's required skills are installed
178
183
  const requiredSkills = tool.includes.skills
179
- .filter(s => s.required)
180
- .map(s => s.name);
184
+ .filter((s) => s.required)
185
+ .map((s) => s.name);
181
186
 
182
- const isInstalled = requiredSkills.some(skillName => skillName in installedTools);
187
+ const isInstalled = requiredSkills.some(
188
+ (skillName) => skillName in installedTools,
189
+ );
183
190
 
184
191
  if (isInstalled) {
185
192
  const updateStatus = getToolUpdateStatus(tool.name);