@orderful/droid 0.26.0 → 0.27.0

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 +29 -0
  4. package/dist/bin/droid.js +102 -24
  5. package/dist/index.js +59 -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 +95 -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
@@ -29,7 +29,9 @@ describe('Badge', () => {
29
29
  const types = ['skill', 'command', 'agent'] as const;
30
30
  for (const type of types) {
31
31
  const { lastFrame } = render(<Badge type={type} />);
32
- expect(lastFrame()).toContain(type.charAt(0).toUpperCase() + type.slice(1));
32
+ expect(lastFrame()).toContain(
33
+ type.charAt(0).toUpperCase() + type.slice(1),
34
+ );
33
35
  }
34
36
  });
35
37
  });
@@ -44,7 +46,7 @@ describe('ComponentBadges', () => {
44
46
  { name: 'skill1', required: true },
45
47
  { name: 'skill2', required: false },
46
48
  ],
47
- commands: ['cmd1'],
49
+ commands: [{ name: 'cmd1', is_alias: false }],
48
50
  agents: [],
49
51
  },
50
52
  };
@@ -63,7 +65,11 @@ describe('ComponentBadges', () => {
63
65
  it('renders singular form for single item', () => {
64
66
  const singleSkillTool: ToolManifest = {
65
67
  ...mockTool,
66
- includes: { skills: [{ name: 'skill1', required: true }], commands: [], agents: [] },
68
+ includes: {
69
+ skills: [{ name: 'skill1', required: true }],
70
+ commands: [],
71
+ agents: [],
72
+ },
67
73
  };
68
74
  const { lastFrame } = render(<ComponentBadges tool={singleSkillTool} />);
69
75
  expect(lastFrame()).toContain('1 skill');
@@ -82,7 +88,7 @@ describe('ComponentBadges', () => {
82
88
  ...mockTool,
83
89
  includes: {
84
90
  skills: [{ name: 's1', required: true }],
85
- commands: ['c1'],
91
+ commands: [{ name: 'c1', is_alias: false }],
86
92
  agents: ['a1'],
87
93
  },
88
94
  };
@@ -14,7 +14,7 @@ import {
14
14
  import { configExists, loadConfig, getAutoUpdateConfig } from '../lib/config';
15
15
  import { Platform, type ConfigOption, type ToolManifest } from '../lib/types';
16
16
  import { getVersion } from '../lib/version';
17
- import { runToolMigrations } from '../lib/migrations';
17
+ import { runPackageMigrations } from '../lib/migrations';
18
18
  import { type Tab, type View } from './tui/types';
19
19
  import { colors, MAX_VISIBLE_ITEMS } from './tui/constants';
20
20
  import { TabBar } from './tui/components/TabBar';
@@ -72,10 +72,10 @@ function App() {
72
72
  },
73
73
  });
74
74
 
75
- // Run droid meta-tool migration on startup (sync installed tools across platforms)
75
+ // Run droid package migrations on startup (sync installed tools across platforms)
76
76
  useEffect(() => {
77
- const droidVersion = getVersion();
78
- runToolMigrations('droid', droidVersion);
77
+ const packageVersion = getVersion();
78
+ runPackageMigrations(packageVersion);
79
79
  }, []);
80
80
 
81
81
  // Auto-update app if enabled and update available
@@ -153,6 +153,14 @@ function createPlatformSyncMigration(version: string): Migration {
153
153
  };
154
154
  }
155
155
 
156
+ /**
157
+ * Registry of package-level migrations
158
+ * These run when the @orderful/droid npm package updates
159
+ * Triggered by package version from package.json (e.g., 0.26.0)
160
+ * Run at TUI startup
161
+ */
162
+ const PACKAGE_MIGRATIONS: Migration[] = [createPlatformSyncMigration('0.25.0')];
163
+
156
164
  /**
157
165
  * Registry of all tool migrations
158
166
  * Key: tool name (e.g., "brain", "comments")
@@ -169,8 +177,6 @@ const TOOL_MIGRATIONS: Record<string, Migration[]> = {
169
177
  comments: [createConfigDirMigration('droid-comments', '0.2.6')],
170
178
  project: [createConfigDirMigration('droid-project', '0.1.5')],
171
179
  coach: [createConfigDirMigration('droid-coach', '0.1.3')],
172
- // Global migration for the droid meta-tool
173
- droid: [createPlatformSyncMigration('0.25.0')],
174
180
  };
175
181
 
176
182
  /**
@@ -185,7 +191,18 @@ export function getToolMigrations(toolName: string): Migration[] {
185
191
  */
186
192
  export function getLastMigratedVersion(toolName: string): string {
187
193
  const config = loadConfig();
188
- return config.migrations?.[toolName] ?? '0.0.0';
194
+
195
+ // Check new location first
196
+ if (config.migrations?.tools?.[toolName]) {
197
+ return config.migrations.tools[toolName];
198
+ }
199
+
200
+ // Fall back to old location (backward compatibility)
201
+ if (config.migrations && typeof config.migrations[toolName] === 'string') {
202
+ return config.migrations[toolName] as string;
203
+ }
204
+
205
+ return '0.0.0';
189
206
  }
190
207
 
191
208
  /**
@@ -196,10 +213,22 @@ export function setLastMigratedVersion(
196
213
  version: string,
197
214
  ): void {
198
215
  const config = loadConfig();
216
+
217
+ // Ensure new structure exists
199
218
  if (!config.migrations) {
200
219
  config.migrations = {};
201
220
  }
202
- config.migrations[toolName] = version;
221
+ if (!config.migrations.tools) {
222
+ config.migrations.tools = {};
223
+ }
224
+
225
+ config.migrations.tools[toolName] = version;
226
+
227
+ // Clean up old location if it exists
228
+ if (typeof config.migrations[toolName] === 'string') {
229
+ delete config.migrations[toolName];
230
+ }
231
+
203
232
  saveConfig(config);
204
233
  }
205
234
 
@@ -273,3 +302,65 @@ export function runToolMigrations(
273
302
 
274
303
  return runMigrations(toolName, lastMigrated, installedVersion);
275
304
  }
305
+
306
+ /**
307
+ * Run package-level migrations
308
+ * Call this on TUI startup with package version from package.json
309
+ */
310
+ export function runPackageMigrations(packageVersion: string): {
311
+ success: boolean;
312
+ error?: string;
313
+ } {
314
+ const config = loadConfig();
315
+ const lastMigrated = config.migrations?.package ?? '0.0.0';
316
+
317
+ // Only run if the package version is newer than last migrated
318
+ if (compareSemver(packageVersion, lastMigrated) <= 0) {
319
+ return { success: true };
320
+ }
321
+
322
+ // Filter migrations that need to run
323
+ const pendingMigrations = PACKAGE_MIGRATIONS.filter((m) => {
324
+ const afterFrom = compareSemver(m.version, lastMigrated) > 0;
325
+ const beforeOrAtTo = compareSemver(m.version, packageVersion) <= 0;
326
+ return afterFrom && beforeOrAtTo;
327
+ });
328
+
329
+ if (pendingMigrations.length === 0) {
330
+ // No migrations to run, but still update the version marker
331
+ config.migrations = config.migrations || {};
332
+ config.migrations.package = packageVersion;
333
+ saveConfig(config);
334
+ return { success: true };
335
+ }
336
+
337
+ const configDir = getConfigDir();
338
+
339
+ for (const migration of pendingMigrations) {
340
+ try {
341
+ migration.up(configDir);
342
+ logMigration('package', lastMigrated, migration.version, 'OK');
343
+ } catch (error) {
344
+ const errorMessage =
345
+ error instanceof Error ? error.message : String(error);
346
+ logMigration(
347
+ 'package',
348
+ lastMigrated,
349
+ migration.version,
350
+ 'FAILED',
351
+ errorMessage,
352
+ );
353
+ // Don't update version marker on failure - will retry next time
354
+ return {
355
+ success: false,
356
+ error: `Package migration ${migration.version} failed: ${errorMessage}`,
357
+ };
358
+ }
359
+ }
360
+
361
+ // All migrations succeeded, update version marker
362
+ config.migrations = config.migrations || {};
363
+ config.migrations.package = packageVersion;
364
+ saveConfig(config);
365
+ return { success: true };
366
+ }
@@ -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
  });