@orderful/droid 0.27.4 → 0.28.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.
package/AGENTS.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  Project instructions for AI coding assistants.
4
4
 
5
+ ## ⚠️ CRITICAL: Git Workflow
6
+
7
+ **NEVER PUSH DIRECTLY TO MAIN!**
8
+
9
+ All changes MUST go through Pull Requests:
10
+
11
+ 1. Create a feature branch: `git checkout -b fix/description` or `feat/description`
12
+ 2. Make your changes and commit
13
+ 3. Push the branch: `git push origin <branch-name>`
14
+ 4. Create a PR using `gh pr create` or the GitHub UI
15
+ 5. Wait for CI checks and review before merging
16
+
17
+ **No exceptions.** Even "small fixes" require PRs.
18
+
5
19
  ## Overview
6
20
 
7
21
  Droid is a TUI dashboard for managing AI tools. Each tool bundles related skills, commands, and agents. Installs to Claude Code (`~/.claude/`) and OpenCode (`~/.config/opencode/`).
@@ -71,6 +85,7 @@ Don't consolidate them. Using Ink for simple prompts would be overkill.
71
85
  ## Code Style
72
86
 
73
87
  - TypeScript strict mode
88
+ - **Use single quotes** for strings (enforced by ESLint)
74
89
  - Prefer `const` over `let`
75
90
  - Use Canadian/British spelling (behaviour, colour, favourite)
76
91
  - Use "allow list/deny list" not "whitelist/blacklist"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @orderful/droid
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#160](https://github.com/Orderful/droid/pull/160) [`235c520`](https://github.com/Orderful/droid/commit/235c520de056f286fd2af04d6ec2606c51c7bbb2) Thanks [@frytyler](https://github.com/frytyler)! - Support OpenCode's native skills implementation
8
+
9
+ OpenCode now has native skills support and no longer requires the deprecated opencode-skills plugin. This update removes the plugin requirement and updates paths to match OpenCode's native directory structure.
10
+
11
+ **Breaking changes for OpenCode users:**
12
+ - Skills directory changed from `~/.config/opencode/skills/` to `~/.config/opencode/skill/` (singular)
13
+ - Automatic migration moves existing skills to the new location
14
+
15
+ **Changes:**
16
+ - Remove opencode-skills plugin requirement from setup
17
+ - Update OpenCode skills path to use singular `skill/` directory
18
+ - Add migration to move existing skills from old to new directory
19
+ - Update setup messaging to reflect native skills support
20
+ - Fix test expectations for new OpenCode paths
21
+
22
+ ### Patch Changes
23
+
24
+ - [#159](https://github.com/Orderful/droid/pull/159) [`4275602`](https://github.com/Orderful/droid/commit/4275602e1c05f6205e6442a2a6c121a508116bd8) Thanks [@frytyler](https://github.com/frytyler)! - Add migration to remove non-alias commands from Claude Code
25
+
26
+ Claude Code v2.1.3+ can invoke skills directly via /{skillName}, making non-alias commands redundant. This migration removes old non-alias commands from ~/.claude/commands/ while preserving aliases (e.g., /scratchpad for /brain).
27
+ - New package migration (v0.27.3): `createClaudeCodeCommandCleanupMigration`
28
+ - Only affects Claude Code platform
29
+ - Keeps alias commands, removes primary commands that are now redundant
30
+
31
+ ## 0.27.5
32
+
33
+ ### Patch Changes
34
+
35
+ - [`dd0e5f1`](https://github.com/Orderful/droid/commit/dd0e5f114f11ab2ff34c6e329265ea84117792e9) Thanks [@frytyler](https://github.com/frytyler)! - Fix TUI displaying commands as `/[object Object]` instead of actual command names. Commands are now displayed correctly (e.g., `/brain, /scratchpad`) after handling both string and object formats in TOOL.yaml.
36
+
3
37
  ## 0.27.4
4
38
 
5
39
  ### Patch Changes
package/dist/bin/droid.js CHANGED
@@ -208,7 +208,7 @@ var PLATFORM_PATHS = {
208
208
  config: join2(homedir2(), ".claude", "CLAUDE.md")
209
209
  },
210
210
  ["opencode" /* OpenCode */]: {
211
- skills: join2(homedir2(), ".config", "opencode", "skills"),
211
+ skills: join2(homedir2(), ".config", "opencode", "skill"),
212
212
  commands: join2(homedir2(), ".config", "opencode", "command"),
213
213
  agents: join2(homedir2(), ".config", "opencode", "agent"),
214
214
  config: join2(homedir2(), ".config", "opencode", "AGENTS.md")
@@ -642,52 +642,92 @@ function createConfigSkillNameMigration(version2) {
642
642
  }
643
643
  };
644
644
  }
645
- function createFilesystemSyncMigration(version2) {
645
+ function createOpenCodeSkillsPathMigration(version2) {
646
646
  return {
647
647
  version: version2,
648
- description: "Sync config with actually installed skills on filesystem",
648
+ description: "Move OpenCode skills from skills/ to skill/ directory",
649
649
  up: () => {
650
650
  const config = loadConfig();
651
- const originalPlatform = config.platform;
652
- let configChanged = false;
653
- for (const platformKey of ["claude-code" /* ClaudeCode */, "opencode" /* OpenCode */]) {
654
- if (!config.platforms[platformKey]) continue;
655
- const skillsPath = getSkillsPath(platformKey);
656
- if (!existsSync4(skillsPath)) continue;
657
- config.platform = platformKey;
658
- const trackedTools = getPlatformTools(config);
659
- let platformChanged = false;
660
- const bundledTools = getBundledTools();
661
- const allToolNames = new Set(
662
- bundledTools.flatMap(
663
- (tool) => tool.includes.skills.map((s) => s.name)
664
- )
665
- );
666
- const installedDirs = readdirSync3(skillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
667
- for (const dirName of installedDirs) {
668
- if (!allToolNames.has(dirName)) continue;
669
- if (trackedTools[dirName]) continue;
670
- let version3 = "0.0.0";
671
- const matchingTool = bundledTools.find(
672
- (tool) => tool.includes.skills.some((s) => s.name === dirName && s.required)
651
+ if (config.platform !== "opencode" /* OpenCode */) {
652
+ return;
653
+ }
654
+ const oldSkillsPath = join6(getSkillsPath("opencode" /* OpenCode */), "..", "skills");
655
+ const newSkillsPath = getSkillsPath("opencode" /* OpenCode */);
656
+ if (!existsSync4(oldSkillsPath)) {
657
+ return;
658
+ }
659
+ if (!existsSync4(newSkillsPath)) {
660
+ try {
661
+ renameSync(oldSkillsPath, newSkillsPath);
662
+ } catch (error) {
663
+ console.warn(
664
+ `Warning: Could not rename skills directory ${oldSkillsPath}: ${error}`
673
665
  );
674
- if (matchingTool) {
675
- version3 = matchingTool.version;
666
+ }
667
+ return;
668
+ }
669
+ const skillDirs = readdirSync3(oldSkillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
670
+ for (const skillName of skillDirs) {
671
+ const oldSkillDir = join6(oldSkillsPath, skillName);
672
+ const newSkillDir = join6(newSkillsPath, skillName);
673
+ if (!existsSync4(newSkillDir)) {
674
+ try {
675
+ renameSync(oldSkillDir, newSkillDir);
676
+ } catch (error) {
677
+ console.warn(
678
+ `Warning: Could not move skill ${skillName}: ${error}`
679
+ );
676
680
  }
677
- trackedTools[dirName] = {
678
- version: version3,
679
- installed_at: (/* @__PURE__ */ new Date()).toISOString()
680
- };
681
- platformChanged = true;
682
- configChanged = true;
683
681
  }
684
- if (platformChanged) {
685
- setPlatformTools(config, trackedTools);
682
+ }
683
+ try {
684
+ const remaining = readdirSync3(oldSkillsPath);
685
+ if (remaining.length === 0) {
686
+ rmSync(oldSkillsPath, { recursive: true });
686
687
  }
688
+ } catch (error) {
689
+ console.warn(
690
+ `Warning: Could not remove old skills directory ${oldSkillsPath}: ${error}`
691
+ );
687
692
  }
688
- config.platform = originalPlatform;
689
- if (configChanged) {
690
- saveConfig(config);
693
+ }
694
+ };
695
+ }
696
+ function createClaudeCodeCommandCleanupMigration(version2) {
697
+ return {
698
+ version: version2,
699
+ description: "Remove non-alias commands from Claude Code",
700
+ up: () => {
701
+ const config = loadConfig();
702
+ if (config.platform !== "claude-code" /* ClaudeCode */) {
703
+ return;
704
+ }
705
+ const commandsPath = getCommandsPath("claude-code" /* ClaudeCode */);
706
+ if (!existsSync4(commandsPath)) {
707
+ return;
708
+ }
709
+ const bundledTools = getBundledTools();
710
+ const aliasCommands = /* @__PURE__ */ new Set();
711
+ for (const tool of bundledTools) {
712
+ for (const cmd of tool.includes.commands) {
713
+ if (typeof cmd === "object" && cmd.is_alias) {
714
+ aliasCommands.add(cmd.name);
715
+ }
716
+ }
717
+ }
718
+ const commandFiles = readdirSync3(commandsPath, { withFileTypes: true }).filter((dirent) => dirent.isFile() && dirent.name.endsWith(".md")).map((dirent) => dirent.name);
719
+ for (const file of commandFiles) {
720
+ const commandName = file.replace(".md", "");
721
+ if (!aliasCommands.has(commandName)) {
722
+ const commandFilePath = join6(commandsPath, file);
723
+ try {
724
+ rmSync(commandFilePath);
725
+ } catch (error) {
726
+ console.warn(
727
+ `Warning: Could not remove command ${commandFilePath}: ${error}`
728
+ );
729
+ }
730
+ }
691
731
  }
692
732
  }
693
733
  };
@@ -695,7 +735,8 @@ function createFilesystemSyncMigration(version2) {
695
735
  var PACKAGE_MIGRATIONS = [
696
736
  createPlatformSyncMigration("0.25.0"),
697
737
  createConfigSkillNameMigration("0.27.2"),
698
- createFilesystemSyncMigration("0.27.4")
738
+ createOpenCodeSkillsPathMigration("0.28.0"),
739
+ createClaudeCodeCommandCleanupMigration("0.28.0")
699
740
  ];
700
741
  var TOOL_MIGRATIONS = {
701
742
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -1251,7 +1292,6 @@ function detectGitUsername() {
1251
1292
  return "";
1252
1293
  }
1253
1294
  }
1254
- var OPENCODE_SKILLS_PLUGIN = "opencode-skills";
1255
1295
  function configurePlatformPermissions(platform) {
1256
1296
  const added = [];
1257
1297
  if (platform === "claude-code" /* ClaudeCode */) {
@@ -1293,35 +1333,10 @@ function configurePlatformPermissions(platform) {
1293
1333
  }
1294
1334
  if (platform === "opencode" /* OpenCode */) {
1295
1335
  const globalConfigDir = join8(homedir3(), ".config", "opencode");
1296
- const globalConfigPath = join8(globalConfigDir, "opencode.json");
1297
1336
  if (!existsSync6(globalConfigDir)) {
1298
1337
  mkdirSync5(globalConfigDir, { recursive: true });
1299
1338
  }
1300
- let config = {};
1301
- if (existsSync6(globalConfigPath)) {
1302
- try {
1303
- config = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
1304
- } catch {
1305
- console.warn(chalk2.yellow("\u26A0 OpenCode config appears corrupted, resetting"));
1306
- config = {};
1307
- }
1308
- }
1309
- if (!Array.isArray(config.plugin)) {
1310
- config.plugin = [];
1311
- }
1312
- if (!config.plugin.includes(OPENCODE_SKILLS_PLUGIN)) {
1313
- config.plugin.push(OPENCODE_SKILLS_PLUGIN);
1314
- added.push(OPENCODE_SKILLS_PLUGIN);
1315
- }
1316
- if (added.length > 0) {
1317
- try {
1318
- writeFileSync4(globalConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1319
- } catch (e) {
1320
- const message = e instanceof Error ? e.message : "Unknown error";
1321
- return { added: [], alreadyPresent: false, error: `Failed to update OpenCode config: ${message}` };
1322
- }
1323
- }
1324
- return { added, alreadyPresent: added.length === 0 };
1339
+ return { added: [], alreadyPresent: true };
1325
1340
  }
1326
1341
  return { added: [], alreadyPresent: true };
1327
1342
  }
@@ -1422,13 +1437,7 @@ async function setupCommand() {
1422
1437
  console.log(chalk2.gray(" Droid permissions already configured in Claude Code"));
1423
1438
  }
1424
1439
  } else if (answers.platform === "opencode" /* OpenCode */) {
1425
- if (added.length > 0) {
1426
- console.log(chalk2.green("\u2713 Added opencode-skills plugin to OpenCode config"));
1427
- console.log(chalk2.gray(" This enables Claude Code-style skills in OpenCode"));
1428
- console.log(chalk2.gray(" Restart OpenCode to activate the plugin"));
1429
- } else if (alreadyPresent) {
1430
- console.log(chalk2.gray(" opencode-skills plugin already configured in OpenCode"));
1431
- }
1440
+ console.log(chalk2.gray(" OpenCode has native skills support - no configuration needed"));
1432
1441
  }
1433
1442
  console.log(chalk2.gray("\nRun `droid skills` to browse and install skills."));
1434
1443
  }
@@ -1974,7 +1983,13 @@ function ToolDetails({
1974
1983
  const isSystemTool = tool.system === true;
1975
1984
  const actions = installed ? [
1976
1985
  { id: "explore", label: "Explore", variant: "default" },
1977
- ...updateStatus.hasUpdate ? [{ id: "update", label: `Update (${updateStatus.bundledVersion})`, variant: "primary" }] : [],
1986
+ ...updateStatus.hasUpdate ? [
1987
+ {
1988
+ id: "update",
1989
+ label: `Update (${updateStatus.bundledVersion})`,
1990
+ variant: "primary"
1991
+ }
1992
+ ] : [],
1978
1993
  { id: "configure", label: "Configure", variant: "default" },
1979
1994
  // System tools can't be uninstalled
1980
1995
  ...!isSystemTool ? [{ id: "uninstall", label: "Uninstall", variant: "danger" }] : []
@@ -1990,7 +2005,8 @@ function ToolDetails({
1990
2005
  tool.status && ` \xB7 ${tool.status}`,
1991
2006
  installed && /* @__PURE__ */ jsx4(Text4, { color: colors.success, children: " \xB7 installed" }),
1992
2007
  updateStatus.hasUpdate && /* @__PURE__ */ jsxs4(Text4, { color: colors.primary, children: [
1993
- " \xB7 update (",
2008
+ " ",
2009
+ "\xB7 update (",
1994
2010
  installedVersion,
1995
2011
  " \u2192 ",
1996
2012
  updateStatus.bundledVersion,
@@ -2008,7 +2024,7 @@ function ToolDetails({
2008
2024
  ] }),
2009
2025
  tool.includes.commands.length > 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
2010
2026
  /* @__PURE__ */ jsx4(Text4, { color: colors.command, children: "Commands: " }),
2011
- /* @__PURE__ */ jsx4(Text4, { color: colors.textMuted, children: tool.includes.commands.map((c) => `/${c}`).join(", ") })
2027
+ /* @__PURE__ */ jsx4(Text4, { color: colors.textMuted, children: tool.includes.commands.map((c) => typeof c === "string" ? `/${c}` : `/${c.name}`).join(", ") })
2012
2028
  ] }),
2013
2029
  tool.includes.agents.length > 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
2014
2030
  /* @__PURE__ */ jsx4(Text4, { color: colors.agent, children: "Agents: " }),
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAA0D,MAAM,cAAc,CAAC;AAoDhG;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAmG7H;AAyBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA8GlD"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAA0D,MAAM,cAAc,CAAC;AA6ChG;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAgE7H;AAyBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAwGlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"ToolDetails.d.ts","sourceRoot":"","sources":["../../../../src/commands/tui/components/ToolDetails.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,SAAS,EACT,cAAc,GACf,EAAE,gBAAgB,2CAoGlB"}
1
+ {"version":3,"file":"ToolDetails.d.ts","sourceRoot":"","sources":["../../../../src/commands/tui/components/ToolDetails.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,SAAS,EACT,cAAc,GACf,EAAE,gBAAgB,2CA2HlB"}
package/dist/index.js CHANGED
@@ -216,7 +216,7 @@ var PLATFORM_PATHS = {
216
216
  config: join2(homedir2(), ".claude", "CLAUDE.md")
217
217
  },
218
218
  ["opencode" /* OpenCode */]: {
219
- skills: join2(homedir2(), ".config", "opencode", "skills"),
219
+ skills: join2(homedir2(), ".config", "opencode", "skill"),
220
220
  commands: join2(homedir2(), ".config", "opencode", "command"),
221
221
  agents: join2(homedir2(), ".config", "opencode", "agent"),
222
222
  config: join2(homedir2(), ".config", "opencode", "AGENTS.md")
@@ -615,52 +615,92 @@ function createConfigSkillNameMigration(version) {
615
615
  }
616
616
  };
617
617
  }
618
- function createFilesystemSyncMigration(version) {
618
+ function createOpenCodeSkillsPathMigration(version) {
619
619
  return {
620
620
  version,
621
- description: "Sync config with actually installed skills on filesystem",
621
+ description: "Move OpenCode skills from skills/ to skill/ directory",
622
622
  up: () => {
623
623
  const config = loadConfig();
624
- const originalPlatform = config.platform;
625
- let configChanged = false;
626
- for (const platformKey of ["claude-code" /* ClaudeCode */, "opencode" /* OpenCode */]) {
627
- if (!config.platforms[platformKey]) continue;
628
- const skillsPath = getSkillsPath(platformKey);
629
- if (!existsSync4(skillsPath)) continue;
630
- config.platform = platformKey;
631
- const trackedTools = getPlatformTools(config);
632
- let platformChanged = false;
633
- const bundledTools = getBundledTools();
634
- const allToolNames = new Set(
635
- bundledTools.flatMap(
636
- (tool) => tool.includes.skills.map((s) => s.name)
637
- )
638
- );
639
- const installedDirs = readdirSync3(skillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
640
- for (const dirName of installedDirs) {
641
- if (!allToolNames.has(dirName)) continue;
642
- if (trackedTools[dirName]) continue;
643
- let version2 = "0.0.0";
644
- const matchingTool = bundledTools.find(
645
- (tool) => tool.includes.skills.some((s) => s.name === dirName && s.required)
624
+ if (config.platform !== "opencode" /* OpenCode */) {
625
+ return;
626
+ }
627
+ const oldSkillsPath = join6(getSkillsPath("opencode" /* OpenCode */), "..", "skills");
628
+ const newSkillsPath = getSkillsPath("opencode" /* OpenCode */);
629
+ if (!existsSync4(oldSkillsPath)) {
630
+ return;
631
+ }
632
+ if (!existsSync4(newSkillsPath)) {
633
+ try {
634
+ renameSync(oldSkillsPath, newSkillsPath);
635
+ } catch (error) {
636
+ console.warn(
637
+ `Warning: Could not rename skills directory ${oldSkillsPath}: ${error}`
646
638
  );
647
- if (matchingTool) {
648
- version2 = matchingTool.version;
639
+ }
640
+ return;
641
+ }
642
+ const skillDirs = readdirSync3(oldSkillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
643
+ for (const skillName of skillDirs) {
644
+ const oldSkillDir = join6(oldSkillsPath, skillName);
645
+ const newSkillDir = join6(newSkillsPath, skillName);
646
+ if (!existsSync4(newSkillDir)) {
647
+ try {
648
+ renameSync(oldSkillDir, newSkillDir);
649
+ } catch (error) {
650
+ console.warn(
651
+ `Warning: Could not move skill ${skillName}: ${error}`
652
+ );
649
653
  }
650
- trackedTools[dirName] = {
651
- version: version2,
652
- installed_at: (/* @__PURE__ */ new Date()).toISOString()
653
- };
654
- platformChanged = true;
655
- configChanged = true;
656
654
  }
657
- if (platformChanged) {
658
- setPlatformTools(config, trackedTools);
655
+ }
656
+ try {
657
+ const remaining = readdirSync3(oldSkillsPath);
658
+ if (remaining.length === 0) {
659
+ rmSync(oldSkillsPath, { recursive: true });
659
660
  }
661
+ } catch (error) {
662
+ console.warn(
663
+ `Warning: Could not remove old skills directory ${oldSkillsPath}: ${error}`
664
+ );
660
665
  }
661
- config.platform = originalPlatform;
662
- if (configChanged) {
663
- saveConfig(config);
666
+ }
667
+ };
668
+ }
669
+ function createClaudeCodeCommandCleanupMigration(version) {
670
+ return {
671
+ version,
672
+ description: "Remove non-alias commands from Claude Code",
673
+ up: () => {
674
+ const config = loadConfig();
675
+ if (config.platform !== "claude-code" /* ClaudeCode */) {
676
+ return;
677
+ }
678
+ const commandsPath = getCommandsPath("claude-code" /* ClaudeCode */);
679
+ if (!existsSync4(commandsPath)) {
680
+ return;
681
+ }
682
+ const bundledTools = getBundledTools();
683
+ const aliasCommands = /* @__PURE__ */ new Set();
684
+ for (const tool of bundledTools) {
685
+ for (const cmd of tool.includes.commands) {
686
+ if (typeof cmd === "object" && cmd.is_alias) {
687
+ aliasCommands.add(cmd.name);
688
+ }
689
+ }
690
+ }
691
+ const commandFiles = readdirSync3(commandsPath, { withFileTypes: true }).filter((dirent) => dirent.isFile() && dirent.name.endsWith(".md")).map((dirent) => dirent.name);
692
+ for (const file of commandFiles) {
693
+ const commandName = file.replace(".md", "");
694
+ if (!aliasCommands.has(commandName)) {
695
+ const commandFilePath = join6(commandsPath, file);
696
+ try {
697
+ rmSync(commandFilePath);
698
+ } catch (error) {
699
+ console.warn(
700
+ `Warning: Could not remove command ${commandFilePath}: ${error}`
701
+ );
702
+ }
703
+ }
664
704
  }
665
705
  }
666
706
  };
@@ -668,7 +708,8 @@ function createFilesystemSyncMigration(version) {
668
708
  var PACKAGE_MIGRATIONS = [
669
709
  createPlatformSyncMigration("0.25.0"),
670
710
  createConfigSkillNameMigration("0.27.2"),
671
- createFilesystemSyncMigration("0.27.4")
711
+ createOpenCodeSkillsPathMigration("0.28.0"),
712
+ createClaudeCodeCommandCleanupMigration("0.28.0")
672
713
  ];
673
714
  var TOOL_MIGRATIONS = {
674
715
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/lib/migrations.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,SAAS,CAAC;AA8SjB;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA2CtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAStC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAyDA"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/lib/migrations.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,SAAS,CAAC;AA+WjB;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA2CtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAStC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAyDA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderful/droid",
3
- "version": "0.27.4",
3
+ "version": "0.28.0",
4
4
  "description": "AI workflow toolkit for sharing skills, commands, and agents across the team",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,13 +51,6 @@ function detectGitUsername(): string {
51
51
  }
52
52
  }
53
53
 
54
- /**
55
- * The opencode-skills plugin name for OpenCode
56
- * This plugin enables Claude Code-style skills in OpenCode
57
- * @see https://github.com/malhashemi/opencode-skills
58
- */
59
- const OPENCODE_SKILLS_PLUGIN = 'opencode-skills';
60
-
61
54
  /**
62
55
  * Configure platform permissions for droid
63
56
  */
@@ -114,49 +107,14 @@ export function configurePlatformPermissions(platform: Platform): { added: strin
114
107
  }
115
108
 
116
109
  if (platform === Platform.OpenCode) {
117
- // OpenCode uses opencode.json for config
118
- // Check global config location: ~/.config/opencode/opencode.json
110
+ // OpenCode now has native skills support - no configuration needed
111
+ // Ensure config directory exists for when skills are installed
119
112
  const globalConfigDir = join(homedir(), '.config', 'opencode');
120
- const globalConfigPath = join(globalConfigDir, 'opencode.json');
121
-
122
- // Ensure config directory exists
123
113
  if (!existsSync(globalConfigDir)) {
124
114
  mkdirSync(globalConfigDir, { recursive: true });
125
115
  }
126
116
 
127
- // Load or create config
128
- let config: { plugin?: string[] } = {};
129
- if (existsSync(globalConfigPath)) {
130
- try {
131
- config = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
132
- } catch {
133
- console.warn(chalk.yellow('⚠ OpenCode config appears corrupted, resetting'));
134
- config = {};
135
- }
136
- }
137
-
138
- // Ensure plugin array exists
139
- if (!Array.isArray(config.plugin)) {
140
- config.plugin = [];
141
- }
142
-
143
- // Add opencode-skills plugin if not present
144
- if (!config.plugin.includes(OPENCODE_SKILLS_PLUGIN)) {
145
- config.plugin.push(OPENCODE_SKILLS_PLUGIN);
146
- added.push(OPENCODE_SKILLS_PLUGIN);
147
- }
148
-
149
- // Save if we added anything
150
- if (added.length > 0) {
151
- try {
152
- writeFileSync(globalConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
153
- } catch (e) {
154
- const message = e instanceof Error ? e.message : 'Unknown error';
155
- return { added: [], alreadyPresent: false, error: `Failed to update OpenCode config: ${message}` };
156
- }
157
- }
158
-
159
- return { added, alreadyPresent: added.length === 0 };
117
+ return { added: [], alreadyPresent: true };
160
118
  }
161
119
 
162
120
  return { added: [], alreadyPresent: true };
@@ -285,13 +243,7 @@ export async function setupCommand(): Promise<void> {
285
243
  console.log(chalk.gray(' Droid permissions already configured in Claude Code'));
286
244
  }
287
245
  } else if (answers.platform === Platform.OpenCode) {
288
- if (added.length > 0) {
289
- console.log(chalk.green('✓ Added opencode-skills plugin to OpenCode config'));
290
- console.log(chalk.gray(' This enables Claude Code-style skills in OpenCode'));
291
- console.log(chalk.gray(' Restart OpenCode to activate the plugin'));
292
- } else if (alreadyPresent) {
293
- console.log(chalk.gray(' opencode-skills plugin already configured in OpenCode'));
294
- }
246
+ console.log(chalk.gray(' OpenCode has native skills support - no configuration needed'));
295
247
  }
296
248
 
297
249
  console.log(chalk.gray('\nRun `droid skills` to browse and install skills.'));
@@ -1,5 +1,9 @@
1
1
  import { Box, Text } from 'ink';
2
- import { isToolInstalled, getInstalledToolVersion, getToolUpdateStatus } from '../../../lib/tools';
2
+ import {
3
+ isToolInstalled,
4
+ getInstalledToolVersion,
5
+ getToolUpdateStatus,
6
+ } from '../../../lib/tools';
3
7
  import type { ToolManifest } from '../../../lib/types';
4
8
  import { colors } from '../constants';
5
9
  import { ComponentBadges } from './Badge';
@@ -26,17 +30,26 @@ export function ToolDetails({
26
30
  const installed = isToolInstalled(tool.name);
27
31
  const installedVersion = getInstalledToolVersion(tool.name);
28
32
  const updateStatus = getToolUpdateStatus(tool.name);
29
- const isSystemTool = (tool as ToolManifest & { system?: boolean }).system === true;
33
+ const isSystemTool =
34
+ (tool as ToolManifest & { system?: boolean }).system === true;
30
35
 
31
36
  const actions = installed
32
37
  ? [
33
38
  { id: 'explore', label: 'Explore', variant: 'default' },
34
39
  ...(updateStatus.hasUpdate
35
- ? [{ id: 'update', label: `Update (${updateStatus.bundledVersion})`, variant: 'primary' }]
40
+ ? [
41
+ {
42
+ id: 'update',
43
+ label: `Update (${updateStatus.bundledVersion})`,
44
+ variant: 'primary',
45
+ },
46
+ ]
36
47
  : []),
37
48
  { id: 'configure', label: 'Configure', variant: 'default' },
38
49
  // System tools can't be uninstalled
39
- ...(!isSystemTool ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }] : []),
50
+ ...(!isSystemTool
51
+ ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }]
52
+ : []),
40
53
  ]
41
54
  : [
42
55
  { id: 'explore', label: 'Explore', variant: 'default' },
@@ -46,7 +59,9 @@ export function ToolDetails({
46
59
 
47
60
  return (
48
61
  <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
49
- <Text color={colors.text} bold>{tool.name}</Text>
62
+ <Text color={colors.text} bold>
63
+ {tool.name}
64
+ </Text>
50
65
 
51
66
  <Box marginTop={1}>
52
67
  <Text color={colors.textDim}>
@@ -54,7 +69,10 @@ export function ToolDetails({
54
69
  {tool.status && ` · ${tool.status}`}
55
70
  {installed && <Text color={colors.success}> · installed</Text>}
56
71
  {updateStatus.hasUpdate && (
57
- <Text color={colors.primary}> · update ({installedVersion} → {updateStatus.bundledVersion})</Text>
72
+ <Text color={colors.primary}>
73
+ {' '}
74
+ · update ({installedVersion} → {updateStatus.bundledVersion})
75
+ </Text>
58
76
  )}
59
77
  </Text>
60
78
  </Box>
@@ -74,19 +92,27 @@ export function ToolDetails({
74
92
  {tool.includes.skills.length > 0 && (
75
93
  <Text>
76
94
  <Text color={colors.skill}>Skills: </Text>
77
- <Text color={colors.textMuted}>{tool.includes.skills.map(s => s.name).join(', ')}</Text>
95
+ <Text color={colors.textMuted}>
96
+ {tool.includes.skills.map((s) => s.name).join(', ')}
97
+ </Text>
78
98
  </Text>
79
99
  )}
80
100
  {tool.includes.commands.length > 0 && (
81
101
  <Text>
82
102
  <Text color={colors.command}>Commands: </Text>
83
- <Text color={colors.textMuted}>{tool.includes.commands.map(c => `/${c}`).join(', ')}</Text>
103
+ <Text color={colors.textMuted}>
104
+ {tool.includes.commands
105
+ .map((c) => (typeof c === 'string' ? `/${c}` : `/${c.name}`))
106
+ .join(', ')}
107
+ </Text>
84
108
  </Text>
85
109
  )}
86
110
  {tool.includes.agents.length > 0 && (
87
111
  <Text>
88
112
  <Text color={colors.agent}>Agents: </Text>
89
- <Text color={colors.textMuted}>{tool.includes.agents.join(', ')}</Text>
113
+ <Text color={colors.textMuted}>
114
+ {tool.includes.agents.join(', ')}
115
+ </Text>
90
116
  </Text>
91
117
  )}
92
118
  </Box>
@@ -107,7 +133,8 @@ export function ToolDetails({
107
133
  color={selectedAction === index ? '#ffffff' : colors.textMuted}
108
134
  bold={selectedAction === index}
109
135
  >
110
- {' '}{action.label}{' '}
136
+ {' '}
137
+ {action.label}{' '}
111
138
  </Text>
112
139
  ))}
113
140
  </Box>
@@ -15,7 +15,7 @@ import {
15
15
  setPlatformTools,
16
16
  } from './types';
17
17
  import { compareSemver } from './version';
18
- import { getSkillsPath } from './platforms';
18
+ import { getSkillsPath, getCommandsPath } from './platforms';
19
19
  import { getBundledTools } from './tools';
20
20
 
21
21
  const MIGRATIONS_LOG_FILE = '.migrations.log';
@@ -212,74 +212,138 @@ function createConfigSkillNameMigration(version: string): Migration {
212
212
  }
213
213
 
214
214
  /**
215
- * Sync config with filesystem - add any installed skills that aren't tracked
216
- * This is a one-time cleanup migration to fix config inconsistencies from v0.27.x
215
+ * Migration: Move OpenCode skills to native skill directory
216
+ *
217
+ * OpenCode now has native skills support and looks for skills in ~/.config/opencode/skill/
218
+ * (singular) instead of the old ~/.config/opencode/skills/ (plural). This migration
219
+ * moves existing skills to the correct location.
217
220
  */
218
- function createFilesystemSyncMigration(version: string): Migration {
221
+ function createOpenCodeSkillsPathMigration(version: string): Migration {
219
222
  return {
220
223
  version,
221
- description: 'Sync config with actually installed skills on filesystem',
224
+ description: 'Move OpenCode skills from skills/ to skill/ directory',
222
225
  up: () => {
223
226
  const config = loadConfig();
224
- const originalPlatform = config.platform;
225
- let configChanged = false;
226
227
 
227
- // Check both platforms
228
- for (const platformKey of [Platform.ClaudeCode, Platform.OpenCode]) {
229
- if (!config.platforms[platformKey]) continue;
228
+ // Only run for OpenCode platform
229
+ if (config.platform !== Platform.OpenCode) {
230
+ return;
231
+ }
230
232
 
231
- const skillsPath = getSkillsPath(platformKey);
232
- if (!existsSync(skillsPath)) continue;
233
+ const oldSkillsPath = join(getSkillsPath(Platform.OpenCode), '..', 'skills');
234
+ const newSkillsPath = getSkillsPath(Platform.OpenCode);
233
235
 
234
- config.platform = platformKey;
235
- const trackedTools = getPlatformTools(config);
236
- let platformChanged = false;
236
+ // Check if old directory exists
237
+ if (!existsSync(oldSkillsPath)) {
238
+ return;
239
+ }
237
240
 
238
- // Get all tool names from manifests
239
- const bundledTools = getBundledTools();
240
- const allToolNames = new Set(
241
- bundledTools.flatMap((tool) =>
242
- tool.includes.skills.map((s) => s.name),
243
- ),
244
- );
241
+ // If new directory doesn't exist, just rename the old one
242
+ if (!existsSync(newSkillsPath)) {
243
+ try {
244
+ renameSync(oldSkillsPath, newSkillsPath);
245
+ } catch (error) {
246
+ console.warn(
247
+ `Warning: Could not rename skills directory ${oldSkillsPath}: ${error}`,
248
+ );
249
+ }
250
+ return;
251
+ }
245
252
 
246
- // Scan filesystem for installed skills
247
- const installedDirs = readdirSync(skillsPath, { withFileTypes: true })
248
- .filter((dirent) => dirent.isDirectory())
249
- .map((dirent) => dirent.name);
253
+ // Both exist - move skills from old to new
254
+ const skillDirs = readdirSync(oldSkillsPath, { withFileTypes: true })
255
+ .filter((dirent) => dirent.isDirectory())
256
+ .map((dirent) => dirent.name);
257
+
258
+ for (const skillName of skillDirs) {
259
+ const oldSkillDir = join(oldSkillsPath, skillName);
260
+ const newSkillDir = join(newSkillsPath, skillName);
261
+
262
+ // Only move if destination doesn't exist
263
+ if (!existsSync(newSkillDir)) {
264
+ try {
265
+ renameSync(oldSkillDir, newSkillDir);
266
+ } catch (error) {
267
+ console.warn(
268
+ `Warning: Could not move skill ${skillName}: ${error}`,
269
+ );
270
+ }
271
+ }
272
+ }
250
273
 
251
- for (const dirName of installedDirs) {
252
- // Skip if not a known tool skill
253
- if (!allToolNames.has(dirName)) continue;
274
+ // Remove old directory if empty
275
+ try {
276
+ const remaining = readdirSync(oldSkillsPath);
277
+ if (remaining.length === 0) {
278
+ rmSync(oldSkillsPath, { recursive: true });
279
+ }
280
+ } catch (error) {
281
+ // Non-fatal: Log warning but continue
282
+ console.warn(
283
+ `Warning: Could not remove old skills directory ${oldSkillsPath}: ${error}`,
284
+ );
285
+ }
286
+ },
287
+ };
288
+ }
254
289
 
255
- // Skip if already tracked
256
- if (trackedTools[dirName]) continue;
290
+ /**
291
+ * Migration: Remove non-alias commands from Claude Code
292
+ *
293
+ * Claude Code v2.1.3+ can invoke skills directly via /{skillName}, so non-alias
294
+ * commands are redundant. This migration removes them from ~/.claude/commands/
295
+ * while keeping aliases (e.g., /scratchpad for /brain).
296
+ */
297
+ function createClaudeCodeCommandCleanupMigration(version: string): Migration {
298
+ return {
299
+ version,
300
+ description: 'Remove non-alias commands from Claude Code',
301
+ up: () => {
302
+ const config = loadConfig();
257
303
 
258
- // Try to get version from bundled tool
259
- let version = '0.0.0';
260
- const matchingTool = bundledTools.find((tool) =>
261
- tool.includes.skills.some((s) => s.name === dirName && s.required),
262
- );
263
- if (matchingTool) {
264
- version = matchingTool.version;
265
- }
304
+ // Only run for Claude Code platform
305
+ if (config.platform !== Platform.ClaudeCode) {
306
+ return;
307
+ }
266
308
 
267
- trackedTools[dirName] = {
268
- version,
269
- installed_at: new Date().toISOString(),
270
- };
271
- platformChanged = true;
272
- configChanged = true;
273
- }
309
+ const commandsPath = getCommandsPath(Platform.ClaudeCode);
310
+ if (!existsSync(commandsPath)) {
311
+ return;
312
+ }
274
313
 
275
- if (platformChanged) {
276
- setPlatformTools(config, trackedTools);
314
+ // Get all bundled tools to check which commands are aliases
315
+ const bundledTools = getBundledTools();
316
+ const aliasCommands = new Set<string>();
317
+
318
+ // Collect all alias command names across all tools
319
+ for (const tool of bundledTools) {
320
+ for (const cmd of tool.includes.commands) {
321
+ if (typeof cmd === 'object' && cmd.is_alias) {
322
+ aliasCommands.add(cmd.name);
323
+ }
277
324
  }
278
325
  }
279
326
 
280
- config.platform = originalPlatform;
281
- if (configChanged) {
282
- saveConfig(config);
327
+ // Check each command file and remove non-aliases
328
+ const commandFiles = readdirSync(commandsPath, { withFileTypes: true })
329
+ .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md'))
330
+ .map((dirent) => dirent.name);
331
+
332
+ for (const file of commandFiles) {
333
+ const commandName = file.replace('.md', '');
334
+
335
+ // Keep aliases, remove everything else
336
+ if (!aliasCommands.has(commandName)) {
337
+ const commandFilePath = join(commandsPath, file);
338
+ try {
339
+ rmSync(commandFilePath);
340
+ } catch (error) {
341
+ // Non-fatal: Log warning but continue
342
+ console.warn(
343
+ `Warning: Could not remove command ${commandFilePath}: ${error}`,
344
+ );
345
+ }
346
+ }
283
347
  }
284
348
  },
285
349
  };
@@ -294,7 +358,8 @@ function createFilesystemSyncMigration(version: string): Migration {
294
358
  const PACKAGE_MIGRATIONS: Migration[] = [
295
359
  createPlatformSyncMigration('0.25.0'),
296
360
  createConfigSkillNameMigration('0.27.2'),
297
- createFilesystemSyncMigration('0.27.4'),
361
+ createOpenCodeSkillsPathMigration('0.28.0'),
362
+ createClaudeCodeCommandCleanupMigration('0.28.0'),
298
363
  ];
299
364
 
300
365
  /**
@@ -14,7 +14,7 @@ export const PLATFORM_PATHS = {
14
14
  config: join(homedir(), '.claude', 'CLAUDE.md'),
15
15
  },
16
16
  [Platform.OpenCode]: {
17
- skills: join(homedir(), '.config', 'opencode', 'skills'),
17
+ skills: join(homedir(), '.config', 'opencode', 'skill'),
18
18
  commands: join(homedir(), '.config', 'opencode', 'command'),
19
19
  agents: join(homedir(), '.config', 'opencode', 'agent'),
20
20
  config: join(homedir(), '.config', 'opencode', 'AGENTS.md'),
@@ -44,7 +44,7 @@ describe("getSkillsInstallPath", () => {
44
44
 
45
45
  it("should return OpenCode path", () => {
46
46
  const path = getSkillsInstallPath(Platform.OpenCode);
47
- expect(path).toBe(join(homedir(), ".config", "opencode", "skills"));
47
+ expect(path).toBe(join(homedir(), ".config", "opencode", "skill"));
48
48
  });
49
49
  });
50
50