@orderful/droid 0.29.1 → 0.30.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @orderful/droid
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#178](https://github.com/Orderful/droid/pull/178) [`77bc5f2`](https://github.com/Orderful/droid/commit/77bc5f24f4340a123cfe9648da134d68895ffefd) Thanks [@frytyler](https://github.com/frytyler)! - Unify skills installation to ~/.claude/skills/ for all platforms. This is the Agent Skills standard location supported by Claude Code, OpenCode, and Cursor. Existing OpenCode skills are automatically migrated to the unified location.
8
+
9
+ ## 0.29.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#176](https://github.com/Orderful/droid/pull/176) [`8451100`](https://github.com/Orderful/droid/commit/845110083082198213b68f34579beebd845b608f) Thanks [@frytyler](https://github.com/frytyler)! - Add migration to remove stale opencode-skills plugin from opencode.json. This plugin was required before OpenCode got native skills support, but now causes errors because it looks for `skills/` (plural) while native OpenCode looks for `skill/` (singular).
14
+
3
15
  ## 0.29.1
4
16
 
5
17
  ### Patch Changes
package/dist/bin/droid.js CHANGED
@@ -7,9 +7,9 @@ import { program } from "commander";
7
7
  import inquirer from "inquirer";
8
8
  import chalk2 from "chalk";
9
9
  import { execSync as execSync2 } from "child_process";
10
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
10
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
11
11
  import { join as join8 } from "path";
12
- import { homedir as homedir3 } from "os";
12
+ import { homedir as homedir4 } from "os";
13
13
 
14
14
  // src/lib/config.ts
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -182,9 +182,9 @@ function setAutoUpdateConfig(updates) {
182
182
  import {
183
183
  existsSync as existsSync5,
184
184
  readdirSync as readdirSync4,
185
- readFileSync as readFileSync5,
185
+ readFileSync as readFileSync6,
186
186
  mkdirSync as mkdirSync4,
187
- writeFileSync as writeFileSync3,
187
+ writeFileSync as writeFileSync4,
188
188
  rmSync as rmSync2
189
189
  } from "fs";
190
190
  import { join as join7, dirname as dirname5, basename } from "path";
@@ -200,15 +200,16 @@ import YAML3 from "yaml";
200
200
  // src/lib/platforms.ts
201
201
  import { join as join2 } from "path";
202
202
  import { homedir as homedir2 } from "os";
203
+ var UNIFIED_SKILLS_PATH = join2(homedir2(), ".claude", "skills");
203
204
  var PLATFORM_PATHS = {
204
205
  ["claude-code" /* ClaudeCode */]: {
205
- skills: join2(homedir2(), ".claude", "skills"),
206
+ skills: UNIFIED_SKILLS_PATH,
206
207
  commands: join2(homedir2(), ".claude", "commands"),
207
208
  agents: join2(homedir2(), ".claude", "agents"),
208
209
  config: join2(homedir2(), ".claude", "CLAUDE.md")
209
210
  },
210
211
  ["opencode" /* OpenCode */]: {
211
- skills: join2(homedir2(), ".config", "opencode", "skill"),
212
+ skills: UNIFIED_SKILLS_PATH,
212
213
  commands: join2(homedir2(), ".config", "opencode", "command"),
213
214
  agents: join2(homedir2(), ".config", "opencode", "agent"),
214
215
  config: join2(homedir2(), ".config", "opencode", "AGENTS.md")
@@ -525,9 +526,12 @@ import {
525
526
  mkdirSync as mkdirSync3,
526
527
  renameSync,
527
528
  rmSync,
528
- readdirSync as readdirSync3
529
+ readdirSync as readdirSync3,
530
+ readFileSync as readFileSync5,
531
+ writeFileSync as writeFileSync3
529
532
  } from "fs";
530
533
  import { join as join6, dirname as dirname4 } from "path";
534
+ import { homedir as homedir3 } from "os";
531
535
  var MIGRATIONS_LOG_FILE = ".migrations.log";
532
536
  function getMigrationsLogPath() {
533
537
  return join6(getConfigDir(), MIGRATIONS_LOG_FILE);
@@ -728,13 +732,108 @@ function createClaudeCodeCommandCleanupMigration(version2) {
728
732
  }
729
733
  };
730
734
  }
735
+ function createOpenCodePluginCleanupMigration(version2) {
736
+ return {
737
+ version: version2,
738
+ description: "Remove opencode-skills plugin from opencode.json",
739
+ up: () => {
740
+ const opencodeConfigPath = join6(
741
+ homedir3(),
742
+ ".config",
743
+ "opencode",
744
+ "opencode.json"
745
+ );
746
+ if (!existsSync4(opencodeConfigPath)) {
747
+ return;
748
+ }
749
+ let config;
750
+ try {
751
+ config = JSON.parse(readFileSync5(opencodeConfigPath, "utf-8"));
752
+ } catch {
753
+ return;
754
+ }
755
+ if (!Array.isArray(config.plugin)) {
756
+ return;
757
+ }
758
+ const pluginIndex = config.plugin.indexOf("opencode-skills");
759
+ if (pluginIndex === -1) {
760
+ return;
761
+ }
762
+ config.plugin.splice(pluginIndex, 1);
763
+ if (config.plugin.length === 0) {
764
+ delete config.plugin;
765
+ }
766
+ try {
767
+ writeFileSync3(
768
+ opencodeConfigPath,
769
+ JSON.stringify(config, null, 2) + "\n",
770
+ "utf-8"
771
+ );
772
+ } catch (error) {
773
+ console.warn(`Warning: Could not update opencode.json: ${error}`);
774
+ }
775
+ }
776
+ };
777
+ }
778
+ function createUnifiedSkillsPathMigration(version2) {
779
+ return {
780
+ version: version2,
781
+ description: "Copy OpenCode skills to unified ~/.claude/skills/ location",
782
+ up: () => {
783
+ const oldOpenCodeSkillsPath = join6(
784
+ homedir3(),
785
+ ".config",
786
+ "opencode",
787
+ "skill"
788
+ );
789
+ const unifiedSkillsPath = join6(homedir3(), ".claude", "skills");
790
+ if (!existsSync4(oldOpenCodeSkillsPath)) {
791
+ return;
792
+ }
793
+ if (!existsSync4(unifiedSkillsPath)) {
794
+ mkdirSync3(unifiedSkillsPath, { recursive: true });
795
+ }
796
+ const skillDirs = readdirSync3(oldOpenCodeSkillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
797
+ for (const skillName of skillDirs) {
798
+ const sourcePath = join6(oldOpenCodeSkillsPath, skillName);
799
+ const destPath = join6(unifiedSkillsPath, skillName);
800
+ if (existsSync4(destPath)) {
801
+ continue;
802
+ }
803
+ try {
804
+ copyDirRecursive(sourcePath, destPath);
805
+ } catch (error) {
806
+ console.warn(
807
+ `Warning: Could not copy skill ${skillName} to unified location: ${error}`
808
+ );
809
+ }
810
+ }
811
+ }
812
+ };
813
+ }
814
+ function copyDirRecursive(src, dest) {
815
+ mkdirSync3(dest, { recursive: true });
816
+ const entries = readdirSync3(src, { withFileTypes: true });
817
+ for (const entry of entries) {
818
+ const srcPath = join6(src, entry.name);
819
+ const destPath = join6(dest, entry.name);
820
+ if (entry.isDirectory()) {
821
+ copyDirRecursive(srcPath, destPath);
822
+ } else {
823
+ const content = readFileSync5(srcPath);
824
+ writeFileSync3(destPath, content);
825
+ }
826
+ }
827
+ }
731
828
  var PACKAGE_MIGRATIONS = [
732
829
  createPlatformSyncMigration("0.25.0"),
733
830
  createConfigSkillNameMigration("0.27.2"),
734
831
  createOpenCodeSkillsPathMigration("0.28.0"),
735
832
  createClaudeCodeCommandCleanupMigration("0.28.0"),
736
833
  // Retry: 0.28.0 migration had platform check that prevented running after platform switch
737
- createClaudeCodeCommandCleanupMigration("0.28.1")
834
+ createClaudeCodeCommandCleanupMigration("0.28.1"),
835
+ createOpenCodePluginCleanupMigration("0.29.2"),
836
+ createUnifiedSkillsPathMigration("0.30.0")
738
837
  ];
739
838
  var TOOL_MIGRATIONS = {
740
839
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -873,7 +972,7 @@ function updatePlatformConfigSkills(platform, installedSkills) {
873
972
  const configPath = getPlatformConfigPath(platform);
874
973
  let content = "";
875
974
  if (existsSync5(configPath)) {
876
- content = readFileSync5(configPath, "utf-8");
975
+ content = readFileSync6(configPath, "utf-8");
877
976
  }
878
977
  const skillLines = installedSkills.map((name) => {
879
978
  const relativePath = `skills/${name}/SKILL.md`;
@@ -895,7 +994,7 @@ ${DROID_SKILLS_END}` : "";
895
994
  if (!existsSync5(configDir)) {
896
995
  mkdirSync4(configDir, { recursive: true });
897
996
  }
898
- writeFileSync3(configPath, content, "utf-8");
997
+ writeFileSync4(configPath, content, "utf-8");
899
998
  }
900
999
  function parseSkillFrontmatter(content) {
901
1000
  const trimmed = content.trimStart();
@@ -918,7 +1017,7 @@ function loadSkillManifest(skillDir) {
918
1017
  if (!existsSync5(skillMdPath)) {
919
1018
  return null;
920
1019
  }
921
- const content = readFileSync5(skillMdPath, "utf-8");
1020
+ const content = readFileSync6(skillMdPath, "utf-8");
922
1021
  const frontmatter = parseSkillFrontmatter(content);
923
1022
  if (!frontmatter || !frontmatter.name) {
924
1023
  return null;
@@ -1161,8 +1260,8 @@ function installSkill(skillName) {
1161
1260
  mkdirSync4(targetSkillDir, { recursive: true });
1162
1261
  }
1163
1262
  const skillMdTarget = join7(targetSkillDir, "SKILL.md");
1164
- const content = readFileSync5(skillMdSource, "utf-8");
1165
- writeFileSync3(skillMdTarget, content);
1263
+ const content = readFileSync6(skillMdSource, "utf-8");
1264
+ writeFileSync4(skillMdTarget, content);
1166
1265
  }
1167
1266
  const referencesSource = join7(skillDir, "references");
1168
1267
  if (existsSync5(referencesSource)) {
@@ -1176,8 +1275,8 @@ function installSkill(skillName) {
1176
1275
  for (const file of referenceFiles) {
1177
1276
  const sourcePath = join7(referencesSource, file);
1178
1277
  const targetPath = join7(targetReferencesDir, file);
1179
- const content = readFileSync5(sourcePath, "utf-8");
1180
- writeFileSync3(targetPath, content);
1278
+ const content = readFileSync6(sourcePath, "utf-8");
1279
+ writeFileSync4(targetPath, content);
1181
1280
  }
1182
1281
  }
1183
1282
  const scriptsSource = join7(skillDir, "scripts");
@@ -1192,8 +1291,8 @@ function installSkill(skillName) {
1192
1291
  for (const file of scriptFiles) {
1193
1292
  const sourcePath = join7(scriptsSource, file);
1194
1293
  const targetPath = join7(targetScriptsDir, file);
1195
- const content = readFileSync5(sourcePath, "utf-8");
1196
- writeFileSync3(targetPath, content);
1294
+ const content = readFileSync6(sourcePath, "utf-8");
1295
+ writeFileSync4(targetPath, content);
1197
1296
  }
1198
1297
  }
1199
1298
  if (existsSync5(commandsSource)) {
@@ -1213,8 +1312,8 @@ function installSkill(skillName) {
1213
1312
  if (shouldInstall) {
1214
1313
  const sourcePath = join7(commandsSource, file);
1215
1314
  const targetPath = join7(commandsPath, file);
1216
- const content = readFileSync5(sourcePath, "utf-8");
1217
- writeFileSync3(targetPath, content);
1315
+ const content = readFileSync6(sourcePath, "utf-8");
1316
+ writeFileSync4(targetPath, content);
1218
1317
  }
1219
1318
  }
1220
1319
  }
@@ -1334,15 +1433,15 @@ function detectGitUsername() {
1334
1433
  function configurePlatformPermissions(platform) {
1335
1434
  const added = [];
1336
1435
  if (platform === "claude-code" /* ClaudeCode */) {
1337
- const settingsPath = join8(homedir3(), ".claude", "settings.json");
1338
- const claudeDir = join8(homedir3(), ".claude");
1436
+ const settingsPath = join8(homedir4(), ".claude", "settings.json");
1437
+ const claudeDir = join8(homedir4(), ".claude");
1339
1438
  if (!existsSync6(claudeDir)) {
1340
1439
  mkdirSync5(claudeDir, { recursive: true });
1341
1440
  }
1342
1441
  let settings = {};
1343
1442
  if (existsSync6(settingsPath)) {
1344
1443
  try {
1345
- settings = JSON.parse(readFileSync6(settingsPath, "utf-8"));
1444
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
1346
1445
  } catch {
1347
1446
  console.warn(chalk2.yellow("\u26A0 Claude Code settings.json appears corrupted, resetting permissions"));
1348
1447
  settings = {};
@@ -1362,7 +1461,7 @@ function configurePlatformPermissions(platform) {
1362
1461
  }
1363
1462
  if (added.length > 0) {
1364
1463
  try {
1365
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1464
+ writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1366
1465
  } catch (e) {
1367
1466
  const message = e instanceof Error ? e.message : "Unknown error";
1368
1467
  return { added: [], alreadyPresent: false, error: `Failed to update Claude Code settings: ${message}` };
@@ -1371,7 +1470,7 @@ function configurePlatformPermissions(platform) {
1371
1470
  return { added, alreadyPresent: added.length === 0 };
1372
1471
  }
1373
1472
  if (platform === "opencode" /* OpenCode */) {
1374
- const globalConfigDir = join8(homedir3(), ".config", "opencode");
1473
+ const globalConfigDir = join8(homedir4(), ".config", "opencode");
1375
1474
  if (!existsSync6(globalConfigDir)) {
1376
1475
  mkdirSync5(globalConfigDir, { recursive: true });
1377
1476
  }
@@ -1476,7 +1575,7 @@ async function setupCommand() {
1476
1575
  console.log(chalk2.gray(" Droid permissions already configured in Claude Code"));
1477
1576
  }
1478
1577
  } else if (answers.platform === "opencode" /* OpenCode */) {
1479
- console.log(chalk2.gray(" OpenCode has native skills support - no configuration needed"));
1578
+ console.log(chalk2.gray(" Skills installed to ~/.claude/skills/ (works with Claude Code, OpenCode, and Cursor)"));
1480
1579
  }
1481
1580
  console.log(chalk2.gray("\nRun `droid skills` to browse and install skills."));
1482
1581
  }
@@ -2728,7 +2827,7 @@ function ReadmeViewer({ title, content, onClose }) {
2728
2827
  // src/commands/tui/views/ToolExplorer.tsx
2729
2828
  import { Box as Box10, Text as Text11, useInput as useInput5 } from "ink";
2730
2829
  import { useState as useState5, useMemo as useMemo3 } from "react";
2731
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
2830
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
2732
2831
  import { join as join9 } from "path";
2733
2832
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2734
2833
  function ToolExplorer({ tool, onViewSource, onClose }) {
@@ -2773,12 +2872,12 @@ function ToolExplorer({ tool, onViewSource, onClose }) {
2773
2872
  if (key.return && items.length > 0) {
2774
2873
  const item = items[selectedIndex];
2775
2874
  if (existsSync7(item.path)) {
2776
- const content = readFileSync7(item.path, "utf-8");
2875
+ const content = readFileSync8(item.path, "utf-8");
2777
2876
  onViewSource(`${tool.name} / ${item.name}`, content);
2778
2877
  } else {
2779
2878
  const yamlPath = item.path.replace(".md", ".yaml");
2780
2879
  if (existsSync7(yamlPath)) {
2781
- const content = readFileSync7(yamlPath, "utf-8");
2880
+ const content = readFileSync8(yamlPath, "utf-8");
2782
2881
  onViewSource(`${tool.name} / ${item.name}`, content);
2783
2882
  }
2784
2883
  }
@@ -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;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
+ {"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,CAiE7H;AAyBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAwGlD"}
package/dist/index.js CHANGED
@@ -190,9 +190,9 @@ function setAutoUpdateConfig(updates) {
190
190
  import {
191
191
  existsSync as existsSync5,
192
192
  readdirSync as readdirSync4,
193
- readFileSync as readFileSync5,
193
+ readFileSync as readFileSync6,
194
194
  mkdirSync as mkdirSync4,
195
- writeFileSync as writeFileSync3,
195
+ writeFileSync as writeFileSync4,
196
196
  rmSync as rmSync2
197
197
  } from "fs";
198
198
  import { join as join7, dirname as dirname5, basename } from "path";
@@ -208,15 +208,16 @@ import YAML3 from "yaml";
208
208
  // src/lib/platforms.ts
209
209
  import { join as join2 } from "path";
210
210
  import { homedir as homedir2 } from "os";
211
+ var UNIFIED_SKILLS_PATH = join2(homedir2(), ".claude", "skills");
211
212
  var PLATFORM_PATHS = {
212
213
  ["claude-code" /* ClaudeCode */]: {
213
- skills: join2(homedir2(), ".claude", "skills"),
214
+ skills: UNIFIED_SKILLS_PATH,
214
215
  commands: join2(homedir2(), ".claude", "commands"),
215
216
  agents: join2(homedir2(), ".claude", "agents"),
216
217
  config: join2(homedir2(), ".claude", "CLAUDE.md")
217
218
  },
218
219
  ["opencode" /* OpenCode */]: {
219
- skills: join2(homedir2(), ".config", "opencode", "skill"),
220
+ skills: UNIFIED_SKILLS_PATH,
220
221
  commands: join2(homedir2(), ".config", "opencode", "command"),
221
222
  agents: join2(homedir2(), ".config", "opencode", "agent"),
222
223
  config: join2(homedir2(), ".config", "opencode", "AGENTS.md")
@@ -498,9 +499,12 @@ import {
498
499
  mkdirSync as mkdirSync3,
499
500
  renameSync,
500
501
  rmSync,
501
- readdirSync as readdirSync3
502
+ readdirSync as readdirSync3,
503
+ readFileSync as readFileSync5,
504
+ writeFileSync as writeFileSync3
502
505
  } from "fs";
503
506
  import { join as join6, dirname as dirname4 } from "path";
507
+ import { homedir as homedir3 } from "os";
504
508
  var MIGRATIONS_LOG_FILE = ".migrations.log";
505
509
  function getMigrationsLogPath() {
506
510
  return join6(getConfigDir(), MIGRATIONS_LOG_FILE);
@@ -701,13 +705,108 @@ function createClaudeCodeCommandCleanupMigration(version) {
701
705
  }
702
706
  };
703
707
  }
708
+ function createOpenCodePluginCleanupMigration(version) {
709
+ return {
710
+ version,
711
+ description: "Remove opencode-skills plugin from opencode.json",
712
+ up: () => {
713
+ const opencodeConfigPath = join6(
714
+ homedir3(),
715
+ ".config",
716
+ "opencode",
717
+ "opencode.json"
718
+ );
719
+ if (!existsSync4(opencodeConfigPath)) {
720
+ return;
721
+ }
722
+ let config;
723
+ try {
724
+ config = JSON.parse(readFileSync5(opencodeConfigPath, "utf-8"));
725
+ } catch {
726
+ return;
727
+ }
728
+ if (!Array.isArray(config.plugin)) {
729
+ return;
730
+ }
731
+ const pluginIndex = config.plugin.indexOf("opencode-skills");
732
+ if (pluginIndex === -1) {
733
+ return;
734
+ }
735
+ config.plugin.splice(pluginIndex, 1);
736
+ if (config.plugin.length === 0) {
737
+ delete config.plugin;
738
+ }
739
+ try {
740
+ writeFileSync3(
741
+ opencodeConfigPath,
742
+ JSON.stringify(config, null, 2) + "\n",
743
+ "utf-8"
744
+ );
745
+ } catch (error) {
746
+ console.warn(`Warning: Could not update opencode.json: ${error}`);
747
+ }
748
+ }
749
+ };
750
+ }
751
+ function createUnifiedSkillsPathMigration(version) {
752
+ return {
753
+ version,
754
+ description: "Copy OpenCode skills to unified ~/.claude/skills/ location",
755
+ up: () => {
756
+ const oldOpenCodeSkillsPath = join6(
757
+ homedir3(),
758
+ ".config",
759
+ "opencode",
760
+ "skill"
761
+ );
762
+ const unifiedSkillsPath = join6(homedir3(), ".claude", "skills");
763
+ if (!existsSync4(oldOpenCodeSkillsPath)) {
764
+ return;
765
+ }
766
+ if (!existsSync4(unifiedSkillsPath)) {
767
+ mkdirSync3(unifiedSkillsPath, { recursive: true });
768
+ }
769
+ const skillDirs = readdirSync3(oldOpenCodeSkillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
770
+ for (const skillName of skillDirs) {
771
+ const sourcePath = join6(oldOpenCodeSkillsPath, skillName);
772
+ const destPath = join6(unifiedSkillsPath, skillName);
773
+ if (existsSync4(destPath)) {
774
+ continue;
775
+ }
776
+ try {
777
+ copyDirRecursive(sourcePath, destPath);
778
+ } catch (error) {
779
+ console.warn(
780
+ `Warning: Could not copy skill ${skillName} to unified location: ${error}`
781
+ );
782
+ }
783
+ }
784
+ }
785
+ };
786
+ }
787
+ function copyDirRecursive(src, dest) {
788
+ mkdirSync3(dest, { recursive: true });
789
+ const entries = readdirSync3(src, { withFileTypes: true });
790
+ for (const entry of entries) {
791
+ const srcPath = join6(src, entry.name);
792
+ const destPath = join6(dest, entry.name);
793
+ if (entry.isDirectory()) {
794
+ copyDirRecursive(srcPath, destPath);
795
+ } else {
796
+ const content = readFileSync5(srcPath);
797
+ writeFileSync3(destPath, content);
798
+ }
799
+ }
800
+ }
704
801
  var PACKAGE_MIGRATIONS = [
705
802
  createPlatformSyncMigration("0.25.0"),
706
803
  createConfigSkillNameMigration("0.27.2"),
707
804
  createOpenCodeSkillsPathMigration("0.28.0"),
708
805
  createClaudeCodeCommandCleanupMigration("0.28.0"),
709
806
  // Retry: 0.28.0 migration had platform check that prevented running after platform switch
710
- createClaudeCodeCommandCleanupMigration("0.28.1")
807
+ createClaudeCodeCommandCleanupMigration("0.28.1"),
808
+ createOpenCodePluginCleanupMigration("0.29.2"),
809
+ createUnifiedSkillsPathMigration("0.30.0")
711
810
  ];
712
811
  var TOOL_MIGRATIONS = {
713
812
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -805,7 +904,7 @@ function updatePlatformConfigSkills(platform, installedSkills) {
805
904
  const configPath = getPlatformConfigPath(platform);
806
905
  let content = "";
807
906
  if (existsSync5(configPath)) {
808
- content = readFileSync5(configPath, "utf-8");
907
+ content = readFileSync6(configPath, "utf-8");
809
908
  }
810
909
  const skillLines = installedSkills.map((name) => {
811
910
  const relativePath = `skills/${name}/SKILL.md`;
@@ -827,7 +926,7 @@ ${DROID_SKILLS_END}` : "";
827
926
  if (!existsSync5(configDir)) {
828
927
  mkdirSync4(configDir, { recursive: true });
829
928
  }
830
- writeFileSync3(configPath, content, "utf-8");
929
+ writeFileSync4(configPath, content, "utf-8");
831
930
  }
832
931
  function parseSkillFrontmatter(content) {
833
932
  const trimmed = content.trimStart();
@@ -850,7 +949,7 @@ function loadSkillManifest(skillDir) {
850
949
  if (!existsSync5(skillMdPath)) {
851
950
  return null;
852
951
  }
853
- const content = readFileSync5(skillMdPath, "utf-8");
952
+ const content = readFileSync6(skillMdPath, "utf-8");
854
953
  const frontmatter = parseSkillFrontmatter(content);
855
954
  if (!frontmatter || !frontmatter.name) {
856
955
  return null;
@@ -1109,8 +1208,8 @@ function installSkill(skillName) {
1109
1208
  mkdirSync4(targetSkillDir, { recursive: true });
1110
1209
  }
1111
1210
  const skillMdTarget = join7(targetSkillDir, "SKILL.md");
1112
- const content = readFileSync5(skillMdSource, "utf-8");
1113
- writeFileSync3(skillMdTarget, content);
1211
+ const content = readFileSync6(skillMdSource, "utf-8");
1212
+ writeFileSync4(skillMdTarget, content);
1114
1213
  }
1115
1214
  const referencesSource = join7(skillDir, "references");
1116
1215
  if (existsSync5(referencesSource)) {
@@ -1124,8 +1223,8 @@ function installSkill(skillName) {
1124
1223
  for (const file of referenceFiles) {
1125
1224
  const sourcePath = join7(referencesSource, file);
1126
1225
  const targetPath = join7(targetReferencesDir, file);
1127
- const content = readFileSync5(sourcePath, "utf-8");
1128
- writeFileSync3(targetPath, content);
1226
+ const content = readFileSync6(sourcePath, "utf-8");
1227
+ writeFileSync4(targetPath, content);
1129
1228
  }
1130
1229
  }
1131
1230
  const scriptsSource = join7(skillDir, "scripts");
@@ -1140,8 +1239,8 @@ function installSkill(skillName) {
1140
1239
  for (const file of scriptFiles) {
1141
1240
  const sourcePath = join7(scriptsSource, file);
1142
1241
  const targetPath = join7(targetScriptsDir, file);
1143
- const content = readFileSync5(sourcePath, "utf-8");
1144
- writeFileSync3(targetPath, content);
1242
+ const content = readFileSync6(sourcePath, "utf-8");
1243
+ writeFileSync4(targetPath, content);
1145
1244
  }
1146
1245
  }
1147
1246
  if (existsSync5(commandsSource)) {
@@ -1161,8 +1260,8 @@ function installSkill(skillName) {
1161
1260
  if (shouldInstall) {
1162
1261
  const sourcePath = join7(commandsSource, file);
1163
1262
  const targetPath = join7(commandsPath, file);
1164
- const content = readFileSync5(sourcePath, "utf-8");
1165
- writeFileSync3(targetPath, content);
1263
+ const content = readFileSync6(sourcePath, "utf-8");
1264
+ writeFileSync4(targetPath, content);
1166
1265
  }
1167
1266
  }
1168
1267
  }
@@ -1305,8 +1404,8 @@ function installCommand(commandName, skillName) {
1305
1404
  const actualSourcePath = join7(commandsDir, sourceFile);
1306
1405
  const targetPath = join7(commandsPath, sourceFile);
1307
1406
  try {
1308
- const content = readFileSync5(actualSourcePath, "utf-8");
1309
- writeFileSync3(targetPath, content);
1407
+ const content = readFileSync6(actualSourcePath, "utf-8");
1408
+ writeFileSync4(targetPath, content);
1310
1409
  return { success: true, message: `Installed /${commandName}` };
1311
1410
  } catch (error) {
1312
1411
  return { success: false, message: `Failed to install command: ${error}` };
@@ -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;AA4WjB;;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":"AAaA,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,SAAS,CAAC;AA8fjB;;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"}
@@ -2,6 +2,9 @@ import { Platform } from './types';
2
2
  /**
3
3
  * Platform-specific paths configuration
4
4
  * Single source of truth for all platform-specific directories
5
+ *
6
+ * Note: Skills are unified to ~/.claude/skills/ (all platforms read this location).
7
+ * Commands and agents remain platform-specific as there's no cross-platform compatibility.
5
8
  */
6
9
  export declare const PLATFORM_PATHS: {
7
10
  readonly "claude-code": {
@@ -1 +1 @@
1
- {"version":3,"file":"platforms.d.ts","sourceRoot":"","sources":["../../src/lib/platforms.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;CAajB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;AAE5D;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAElE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE1D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD"}
1
+ {"version":3,"file":"platforms.d.ts","sourceRoot":"","sources":["../../src/lib/platforms.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQnC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;CAajB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;AAE5D;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAElE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE1D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderful/droid",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "description": "AI workflow toolkit for sharing skills, commands, and agents across the team",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +1,12 @@
1
- import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
2
  import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- import { Platform } from '../lib/types';
6
5
 
7
6
  // We need to mock homedir() before importing the module
8
7
  // Create a test directory that will act as our fake home
9
8
  let testHomeDir: string;
10
9
 
11
- // Mock the os module's homedir function
12
- const originalHomedir = await import('os').then(m => m.homedir);
13
-
14
10
  describe('configurePlatformPermissions', () => {
15
11
  beforeEach(() => {
16
12
  testHomeDir = join(tmpdir(), `droid-setup-test-${Date.now()}`);
@@ -23,66 +19,80 @@ describe('configurePlatformPermissions', () => {
23
19
  }
24
20
  });
25
21
 
26
- describe('OpenCode plugin configuration', () => {
27
- it('should add opencode-skills plugin to empty config', () => {
22
+ describe('OpenCode plugin cleanup migration', () => {
23
+ it('should remove opencode-skills plugin from config', () => {
28
24
  const configDir = join(testHomeDir, '.config', 'opencode');
29
25
  const configPath = join(configDir, 'opencode.json');
30
26
 
31
27
  mkdirSync(configDir, { recursive: true });
32
28
 
33
- const config: { plugin?: string[] } = {};
34
- if (!Array.isArray(config.plugin)) {
35
- config.plugin = [];
36
- }
37
- config.plugin.push('opencode-skills');
29
+ // Config with the stale plugin
30
+ const existingConfig = {
31
+ plugin: ['opencode-skills', 'some-other-plugin'],
32
+ theme: 'dark',
33
+ };
34
+ writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
38
35
 
36
+ // Simulate migration logic
37
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
38
+ const pluginIndex = config.plugin.indexOf('opencode-skills');
39
+ if (pluginIndex !== -1) {
40
+ config.plugin.splice(pluginIndex, 1);
41
+ }
39
42
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
40
43
 
41
44
  const read = JSON.parse(readFileSync(configPath, 'utf-8'));
42
- expect(read.plugin).toContain('opencode-skills');
45
+ expect(read.plugin).not.toContain('opencode-skills');
46
+ expect(read.plugin).toContain('some-other-plugin');
47
+ expect(read.theme).toBe('dark');
43
48
  });
44
49
 
45
- it('should add opencode-skills plugin to config with existing plugins', () => {
50
+ it('should remove empty plugin array after cleanup', () => {
46
51
  const configDir = join(testHomeDir, '.config', 'opencode');
47
52
  const configPath = join(configDir, 'opencode.json');
48
53
 
49
54
  mkdirSync(configDir, { recursive: true });
50
55
 
56
+ // Config with only the stale plugin
51
57
  const existingConfig = {
52
- plugin: ['some-other-plugin'],
58
+ plugin: ['opencode-skills'],
53
59
  theme: 'dark',
54
60
  };
55
61
  writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
56
62
 
57
- // Read and modify (simulating what the function does)
63
+ // Simulate migration logic
58
64
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));
59
- if (!config.plugin.includes('opencode-skills')) {
60
- config.plugin.push('opencode-skills');
65
+ const pluginIndex = config.plugin.indexOf('opencode-skills');
66
+ if (pluginIndex !== -1) {
67
+ config.plugin.splice(pluginIndex, 1);
68
+ }
69
+ if (config.plugin.length === 0) {
70
+ delete config.plugin;
61
71
  }
62
72
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
63
73
 
64
74
  const read = JSON.parse(readFileSync(configPath, 'utf-8'));
65
- expect(read.plugin).toContain('opencode-skills');
66
- expect(read.plugin).toContain('some-other-plugin');
75
+ expect(read.plugin).toBeUndefined();
67
76
  expect(read.theme).toBe('dark');
68
77
  });
69
78
 
70
- it('should not duplicate opencode-skills if already present', () => {
79
+ it('should do nothing if plugin not present', () => {
71
80
  const configDir = join(testHomeDir, '.config', 'opencode');
72
81
  const configPath = join(configDir, 'opencode.json');
73
82
 
74
83
  mkdirSync(configDir, { recursive: true });
75
84
 
76
85
  const existingConfig = {
77
- plugin: ['opencode-skills'],
86
+ plugin: ['some-other-plugin'],
87
+ theme: 'dark',
78
88
  };
79
89
  writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
80
90
 
91
+ // Simulate migration logic
81
92
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));
82
- const alreadyPresent = config.plugin.includes('opencode-skills');
83
-
84
- expect(alreadyPresent).toBe(true);
85
- expect(config.plugin.length).toBe(1);
93
+ const pluginIndex = config.plugin.indexOf('opencode-skills');
94
+ expect(pluginIndex).toBe(-1);
95
+ expect(config.plugin).toHaveLength(1);
86
96
  });
87
97
 
88
98
  it('should handle config without plugin key', () => {
@@ -97,31 +107,32 @@ describe('configurePlatformPermissions', () => {
97
107
  };
98
108
  writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
99
109
 
110
+ // Simulate migration logic - should not throw
100
111
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));
101
- if (!Array.isArray(config.plugin)) {
102
- config.plugin = [];
112
+ if (Array.isArray(config.plugin)) {
113
+ const pluginIndex = config.plugin.indexOf('opencode-skills');
114
+ if (pluginIndex !== -1) {
115
+ config.plugin.splice(pluginIndex, 1);
116
+ }
103
117
  }
104
- config.plugin.push('opencode-skills');
105
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
106
118
 
107
- const read = JSON.parse(readFileSync(configPath, 'utf-8'));
108
- expect(read.plugin).toContain('opencode-skills');
109
- expect(read.theme).toBe('dark');
110
- expect(read.model).toBe('claude-sonnet');
119
+ expect(config.theme).toBe('dark');
120
+ expect(config.model).toBe('claude-sonnet');
111
121
  });
112
122
 
113
- it('should create config directory if it does not exist', () => {
114
- const nestedDir = join(testHomeDir, '.config', 'opencode');
115
- expect(existsSync(nestedDir)).toBe(false);
116
-
117
- mkdirSync(nestedDir, { recursive: true });
118
- expect(existsSync(nestedDir)).toBe(true);
123
+ it('should handle missing config file gracefully', () => {
124
+ const configDir = join(testHomeDir, '.config', 'opencode');
125
+ const configPath = join(configDir, 'opencode.json');
119
126
 
120
- const configPath = join(nestedDir, 'opencode.json');
121
- const config = { plugin: ['opencode-skills'] };
122
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
127
+ // Don't create the file - simulate missing config
128
+ expect(existsSync(configPath)).toBe(false);
123
129
 
124
- expect(existsSync(configPath)).toBe(true);
130
+ // Migration should not throw
131
+ if (existsSync(configPath)) {
132
+ // Would process file here
133
+ }
134
+ // No error thrown = success
135
+ expect(true).toBe(true);
125
136
  });
126
137
 
127
138
  it('should handle corrupted JSON gracefully', () => {
@@ -131,20 +142,17 @@ describe('configurePlatformPermissions', () => {
131
142
  mkdirSync(configDir, { recursive: true });
132
143
  writeFileSync(configPath, '{ invalid json }}}', 'utf-8');
133
144
 
134
- let config: { plugin?: string[] } = {};
145
+ // Simulate migration logic - should not throw
146
+ let processed = false;
135
147
  try {
136
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
148
+ JSON.parse(readFileSync(configPath, 'utf-8'));
149
+ processed = true;
137
150
  } catch {
138
- // Invalid JSON, start fresh
139
- config = {};
140
- }
141
-
142
- if (!Array.isArray(config.plugin)) {
143
- config.plugin = [];
151
+ // Invalid JSON - migration skips this file
152
+ processed = false;
144
153
  }
145
- config.plugin.push('opencode-skills');
146
154
 
147
- expect(config.plugin).toContain('opencode-skills');
155
+ expect(processed).toBe(false);
148
156
  });
149
157
  });
150
158
 
@@ -107,8 +107,9 @@ export function configurePlatformPermissions(platform: Platform): { added: strin
107
107
  }
108
108
 
109
109
  if (platform === Platform.OpenCode) {
110
- // OpenCode now has native skills support - no configuration needed
111
- // Ensure config directory exists for when skills are installed
110
+ // Skills are installed to ~/.claude/skills/ (unified location)
111
+ // OpenCode reads from this location via Agent Skills standard compatibility
112
+ // Ensure config directory exists for commands and agents
112
113
  const globalConfigDir = join(homedir(), '.config', 'opencode');
113
114
  if (!existsSync(globalConfigDir)) {
114
115
  mkdirSync(globalConfigDir, { recursive: true });
@@ -243,7 +244,7 @@ export async function setupCommand(): Promise<void> {
243
244
  console.log(chalk.gray(' Droid permissions already configured in Claude Code'));
244
245
  }
245
246
  } else if (answers.platform === Platform.OpenCode) {
246
- console.log(chalk.gray(' OpenCode has native skills support - no configuration needed'));
247
+ console.log(chalk.gray(' Skills installed to ~/.claude/skills/ (works with Claude Code, OpenCode, and Cursor)'));
247
248
  }
248
249
 
249
250
  console.log(chalk.gray('\nRun `droid skills` to browse and install skills.'));
@@ -5,8 +5,11 @@ import {
5
5
  renameSync,
6
6
  rmSync,
7
7
  readdirSync,
8
+ readFileSync,
9
+ writeFileSync,
8
10
  } from 'fs';
9
11
  import { join, dirname } from 'path';
12
+ import { homedir } from 'os';
10
13
  import { loadConfig, saveConfig, getConfigDir } from './config';
11
14
  import {
12
15
  type Migration,
@@ -344,6 +347,150 @@ function createClaudeCodeCommandCleanupMigration(version: string): Migration {
344
347
  };
345
348
  }
346
349
 
350
+ /**
351
+ * Migration: Remove opencode-skills plugin from opencode.json
352
+ *
353
+ * The opencode-skills plugin was required before OpenCode got native skills support.
354
+ * Now it causes errors because it looks for `skills/` (plural) while native OpenCode
355
+ * looks for `skill/` (singular). This migration removes the stale plugin entry.
356
+ */
357
+ function createOpenCodePluginCleanupMigration(version: string): Migration {
358
+ return {
359
+ version,
360
+ description: 'Remove opencode-skills plugin from opencode.json',
361
+ up: () => {
362
+ const opencodeConfigPath = join(
363
+ homedir(),
364
+ '.config',
365
+ 'opencode',
366
+ 'opencode.json',
367
+ );
368
+
369
+ if (!existsSync(opencodeConfigPath)) {
370
+ return;
371
+ }
372
+
373
+ let config: { plugin?: string[]; [key: string]: unknown };
374
+ try {
375
+ config = JSON.parse(readFileSync(opencodeConfigPath, 'utf-8'));
376
+ } catch {
377
+ // Invalid JSON or read error - skip
378
+ return;
379
+ }
380
+
381
+ if (!Array.isArray(config.plugin)) {
382
+ return;
383
+ }
384
+
385
+ const pluginIndex = config.plugin.indexOf('opencode-skills');
386
+ if (pluginIndex === -1) {
387
+ return;
388
+ }
389
+
390
+ // Remove the plugin
391
+ config.plugin.splice(pluginIndex, 1);
392
+
393
+ // Clean up empty plugin array
394
+ if (config.plugin.length === 0) {
395
+ delete config.plugin;
396
+ }
397
+
398
+ try {
399
+ writeFileSync(
400
+ opencodeConfigPath,
401
+ JSON.stringify(config, null, 2) + '\n',
402
+ 'utf-8',
403
+ );
404
+ } catch (error) {
405
+ // Non-fatal: Log warning but continue
406
+ console.warn(`Warning: Could not update opencode.json: ${error}`);
407
+ }
408
+ },
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Migration: Copy OpenCode skills to unified ~/.claude/skills/ location
414
+ *
415
+ * All platforms now read skills from ~/.claude/skills/ (Agent Skills standard).
416
+ * This migration copies existing OpenCode skills to the unified location.
417
+ *
418
+ * Strategy:
419
+ * - Additive only: copies skills, never deletes from source
420
+ * - Skip if exists: if skill already in ~/.claude/skills/, don't overwrite
421
+ */
422
+ function createUnifiedSkillsPathMigration(version: string): Migration {
423
+ return {
424
+ version,
425
+ description: 'Copy OpenCode skills to unified ~/.claude/skills/ location',
426
+ up: () => {
427
+ const oldOpenCodeSkillsPath = join(
428
+ homedir(),
429
+ '.config',
430
+ 'opencode',
431
+ 'skill',
432
+ );
433
+ const unifiedSkillsPath = join(homedir(), '.claude', 'skills');
434
+
435
+ // Skip if old OpenCode skills directory doesn't exist
436
+ if (!existsSync(oldOpenCodeSkillsPath)) {
437
+ return;
438
+ }
439
+
440
+ // Ensure unified skills directory exists
441
+ if (!existsSync(unifiedSkillsPath)) {
442
+ mkdirSync(unifiedSkillsPath, { recursive: true });
443
+ }
444
+
445
+ // Get all skill directories from old location
446
+ const skillDirs = readdirSync(oldOpenCodeSkillsPath, { withFileTypes: true })
447
+ .filter((dirent) => dirent.isDirectory())
448
+ .map((dirent) => dirent.name);
449
+
450
+ for (const skillName of skillDirs) {
451
+ const sourcePath = join(oldOpenCodeSkillsPath, skillName);
452
+ const destPath = join(unifiedSkillsPath, skillName);
453
+
454
+ // Skip if already exists in unified location (Claude version wins)
455
+ if (existsSync(destPath)) {
456
+ continue;
457
+ }
458
+
459
+ // Copy skill directory recursively
460
+ try {
461
+ copyDirRecursive(sourcePath, destPath);
462
+ } catch (error) {
463
+ // Non-fatal: Log warning but continue with other skills
464
+ console.warn(
465
+ `Warning: Could not copy skill ${skillName} to unified location: ${error}`,
466
+ );
467
+ }
468
+ }
469
+ },
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Recursively copy a directory
475
+ */
476
+ function copyDirRecursive(src: string, dest: string): void {
477
+ mkdirSync(dest, { recursive: true });
478
+
479
+ const entries = readdirSync(src, { withFileTypes: true });
480
+
481
+ for (const entry of entries) {
482
+ const srcPath = join(src, entry.name);
483
+ const destPath = join(dest, entry.name);
484
+
485
+ if (entry.isDirectory()) {
486
+ copyDirRecursive(srcPath, destPath);
487
+ } else {
488
+ const content = readFileSync(srcPath);
489
+ writeFileSync(destPath, content);
490
+ }
491
+ }
492
+ }
493
+
347
494
  /**
348
495
  * Registry of package-level migrations
349
496
  * These run when the @orderful/droid npm package updates
@@ -357,6 +504,8 @@ const PACKAGE_MIGRATIONS: Migration[] = [
357
504
  createClaudeCodeCommandCleanupMigration('0.28.0'),
358
505
  // Retry: 0.28.0 migration had platform check that prevented running after platform switch
359
506
  createClaudeCodeCommandCleanupMigration('0.28.1'),
507
+ createOpenCodePluginCleanupMigration('0.29.2'),
508
+ createUnifiedSkillsPathMigration('0.30.0'),
360
509
  ];
361
510
 
362
511
  /**
@@ -2,19 +2,28 @@ import { join } from 'path';
2
2
  import { homedir } from 'os';
3
3
  import { Platform } from './types';
4
4
 
5
+ /**
6
+ * Unified skills path - all platforms read from ~/.claude/skills/
7
+ * This is the Agent Skills standard location supported by Claude Code, OpenCode, and Cursor.
8
+ */
9
+ const UNIFIED_SKILLS_PATH = join(homedir(), '.claude', 'skills');
10
+
5
11
  /**
6
12
  * Platform-specific paths configuration
7
13
  * Single source of truth for all platform-specific directories
14
+ *
15
+ * Note: Skills are unified to ~/.claude/skills/ (all platforms read this location).
16
+ * Commands and agents remain platform-specific as there's no cross-platform compatibility.
8
17
  */
9
18
  export const PLATFORM_PATHS = {
10
19
  [Platform.ClaudeCode]: {
11
- skills: join(homedir(), '.claude', 'skills'),
20
+ skills: UNIFIED_SKILLS_PATH,
12
21
  commands: join(homedir(), '.claude', 'commands'),
13
22
  agents: join(homedir(), '.claude', 'agents'),
14
23
  config: join(homedir(), '.claude', 'CLAUDE.md'),
15
24
  },
16
25
  [Platform.OpenCode]: {
17
- skills: join(homedir(), '.config', 'opencode', 'skill'),
26
+ skills: UNIFIED_SKILLS_PATH,
18
27
  commands: join(homedir(), '.config', 'opencode', 'command'),
19
28
  agents: join(homedir(), '.config', 'opencode', 'agent'),
20
29
  config: join(homedir(), '.config', 'opencode', 'AGENTS.md'),
@@ -42,9 +42,10 @@ describe("getSkillsInstallPath", () => {
42
42
  expect(path).toBe(join(homedir(), ".claude", "skills"));
43
43
  });
44
44
 
45
- it("should return OpenCode path", () => {
45
+ it("should return unified skills path for OpenCode", () => {
46
+ // OpenCode now uses the unified ~/.claude/skills/ path (Agent Skills standard)
46
47
  const path = getSkillsInstallPath(Platform.OpenCode);
47
- expect(path).toBe(join(homedir(), ".config", "opencode", "skill"));
48
+ expect(path).toBe(join(homedir(), ".claude", "skills"));
48
49
  });
49
50
  });
50
51