@orderful/droid 0.27.5 → 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/`).
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
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
+
3
31
  ## 0.27.5
4
32
 
5
33
  ### 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,9 +642,101 @@ function createConfigSkillNameMigration(version2) {
642
642
  }
643
643
  };
644
644
  }
645
+ function createOpenCodeSkillsPathMigration(version2) {
646
+ return {
647
+ version: version2,
648
+ description: "Move OpenCode skills from skills/ to skill/ directory",
649
+ up: () => {
650
+ const config = loadConfig();
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}`
665
+ );
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
+ );
680
+ }
681
+ }
682
+ }
683
+ try {
684
+ const remaining = readdirSync3(oldSkillsPath);
685
+ if (remaining.length === 0) {
686
+ rmSync(oldSkillsPath, { recursive: true });
687
+ }
688
+ } catch (error) {
689
+ console.warn(
690
+ `Warning: Could not remove old skills directory ${oldSkillsPath}: ${error}`
691
+ );
692
+ }
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
+ }
731
+ }
732
+ }
733
+ };
734
+ }
645
735
  var PACKAGE_MIGRATIONS = [
646
736
  createPlatformSyncMigration("0.25.0"),
647
- createConfigSkillNameMigration("0.27.2")
737
+ createConfigSkillNameMigration("0.27.2"),
738
+ createOpenCodeSkillsPathMigration("0.28.0"),
739
+ createClaudeCodeCommandCleanupMigration("0.28.0")
648
740
  ];
649
741
  var TOOL_MIGRATIONS = {
650
742
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -1200,7 +1292,6 @@ function detectGitUsername() {
1200
1292
  return "";
1201
1293
  }
1202
1294
  }
1203
- var OPENCODE_SKILLS_PLUGIN = "opencode-skills";
1204
1295
  function configurePlatformPermissions(platform) {
1205
1296
  const added = [];
1206
1297
  if (platform === "claude-code" /* ClaudeCode */) {
@@ -1242,35 +1333,10 @@ function configurePlatformPermissions(platform) {
1242
1333
  }
1243
1334
  if (platform === "opencode" /* OpenCode */) {
1244
1335
  const globalConfigDir = join8(homedir3(), ".config", "opencode");
1245
- const globalConfigPath = join8(globalConfigDir, "opencode.json");
1246
1336
  if (!existsSync6(globalConfigDir)) {
1247
1337
  mkdirSync5(globalConfigDir, { recursive: true });
1248
1338
  }
1249
- let config = {};
1250
- if (existsSync6(globalConfigPath)) {
1251
- try {
1252
- config = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
1253
- } catch {
1254
- console.warn(chalk2.yellow("\u26A0 OpenCode config appears corrupted, resetting"));
1255
- config = {};
1256
- }
1257
- }
1258
- if (!Array.isArray(config.plugin)) {
1259
- config.plugin = [];
1260
- }
1261
- if (!config.plugin.includes(OPENCODE_SKILLS_PLUGIN)) {
1262
- config.plugin.push(OPENCODE_SKILLS_PLUGIN);
1263
- added.push(OPENCODE_SKILLS_PLUGIN);
1264
- }
1265
- if (added.length > 0) {
1266
- try {
1267
- writeFileSync4(globalConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1268
- } catch (e) {
1269
- const message = e instanceof Error ? e.message : "Unknown error";
1270
- return { added: [], alreadyPresent: false, error: `Failed to update OpenCode config: ${message}` };
1271
- }
1272
- }
1273
- return { added, alreadyPresent: added.length === 0 };
1339
+ return { added: [], alreadyPresent: true };
1274
1340
  }
1275
1341
  return { added: [], alreadyPresent: true };
1276
1342
  }
@@ -1371,13 +1437,7 @@ async function setupCommand() {
1371
1437
  console.log(chalk2.gray(" Droid permissions already configured in Claude Code"));
1372
1438
  }
1373
1439
  } else if (answers.platform === "opencode" /* OpenCode */) {
1374
- if (added.length > 0) {
1375
- console.log(chalk2.green("\u2713 Added opencode-skills plugin to OpenCode config"));
1376
- console.log(chalk2.gray(" This enables Claude Code-style skills in OpenCode"));
1377
- console.log(chalk2.gray(" Restart OpenCode to activate the plugin"));
1378
- } else if (alreadyPresent) {
1379
- console.log(chalk2.gray(" opencode-skills plugin already configured in OpenCode"));
1380
- }
1440
+ console.log(chalk2.gray(" OpenCode has native skills support - no configuration needed"));
1381
1441
  }
1382
1442
  console.log(chalk2.gray("\nRun `droid skills` to browse and install skills."));
1383
1443
  }
@@ -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"}
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,9 +615,101 @@ function createConfigSkillNameMigration(version) {
615
615
  }
616
616
  };
617
617
  }
618
+ function createOpenCodeSkillsPathMigration(version) {
619
+ return {
620
+ version,
621
+ description: "Move OpenCode skills from skills/ to skill/ directory",
622
+ up: () => {
623
+ const config = loadConfig();
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}`
638
+ );
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
+ );
653
+ }
654
+ }
655
+ }
656
+ try {
657
+ const remaining = readdirSync3(oldSkillsPath);
658
+ if (remaining.length === 0) {
659
+ rmSync(oldSkillsPath, { recursive: true });
660
+ }
661
+ } catch (error) {
662
+ console.warn(
663
+ `Warning: Could not remove old skills directory ${oldSkillsPath}: ${error}`
664
+ );
665
+ }
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
+ }
704
+ }
705
+ }
706
+ };
707
+ }
618
708
  var PACKAGE_MIGRATIONS = [
619
709
  createPlatformSyncMigration("0.25.0"),
620
- createConfigSkillNameMigration("0.27.2")
710
+ createConfigSkillNameMigration("0.27.2"),
711
+ createOpenCodeSkillsPathMigration("0.28.0"),
712
+ createClaudeCodeCommandCleanupMigration("0.28.0")
621
713
  ];
622
714
  var TOOL_MIGRATIONS = {
623
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;AAmOjB;;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.5",
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.'));
@@ -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';
@@ -211,6 +211,144 @@ function createConfigSkillNameMigration(version: string): Migration {
211
211
  };
212
212
  }
213
213
 
214
+ /**
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.
220
+ */
221
+ function createOpenCodeSkillsPathMigration(version: string): Migration {
222
+ return {
223
+ version,
224
+ description: 'Move OpenCode skills from skills/ to skill/ directory',
225
+ up: () => {
226
+ const config = loadConfig();
227
+
228
+ // Only run for OpenCode platform
229
+ if (config.platform !== Platform.OpenCode) {
230
+ return;
231
+ }
232
+
233
+ const oldSkillsPath = join(getSkillsPath(Platform.OpenCode), '..', 'skills');
234
+ const newSkillsPath = getSkillsPath(Platform.OpenCode);
235
+
236
+ // Check if old directory exists
237
+ if (!existsSync(oldSkillsPath)) {
238
+ return;
239
+ }
240
+
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
+ }
252
+
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
+ }
273
+
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
+ }
289
+
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();
303
+
304
+ // Only run for Claude Code platform
305
+ if (config.platform !== Platform.ClaudeCode) {
306
+ return;
307
+ }
308
+
309
+ const commandsPath = getCommandsPath(Platform.ClaudeCode);
310
+ if (!existsSync(commandsPath)) {
311
+ return;
312
+ }
313
+
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
+ }
324
+ }
325
+ }
326
+
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
+ }
347
+ }
348
+ },
349
+ };
350
+ }
351
+
214
352
  /**
215
353
  * Registry of package-level migrations
216
354
  * These run when the @orderful/droid npm package updates
@@ -220,6 +358,8 @@ function createConfigSkillNameMigration(version: string): Migration {
220
358
  const PACKAGE_MIGRATIONS: Migration[] = [
221
359
  createPlatformSyncMigration('0.25.0'),
222
360
  createConfigSkillNameMigration('0.27.2'),
361
+ createOpenCodeSkillsPathMigration('0.28.0'),
362
+ createClaudeCodeCommandCleanupMigration('0.28.0'),
223
363
  ];
224
364
 
225
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