@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 +12 -0
- package/dist/bin/droid.js +127 -28
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/index.js +118 -19
- package/dist/lib/migrations.d.ts.map +1 -1
- package/dist/lib/platforms.d.ts +3 -0
- package/dist/lib/platforms.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/setup.test.ts +62 -54
- package/src/commands/setup.ts +4 -3
- package/src/lib/migrations.ts +149 -0
- package/src/lib/platforms.ts +11 -2
- package/src/lib/skills.test.ts +3 -2
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
|
|
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
|
|
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
|
|
185
|
+
readFileSync as readFileSync6,
|
|
186
186
|
mkdirSync as mkdirSync4,
|
|
187
|
-
writeFileSync as
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1165
|
-
|
|
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 =
|
|
1180
|
-
|
|
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 =
|
|
1196
|
-
|
|
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 =
|
|
1217
|
-
|
|
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(
|
|
1338
|
-
const claudeDir = join8(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
193
|
+
readFileSync as readFileSync6,
|
|
194
194
|
mkdirSync as mkdirSync4,
|
|
195
|
-
writeFileSync as
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1113
|
-
|
|
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 =
|
|
1128
|
-
|
|
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 =
|
|
1144
|
-
|
|
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 =
|
|
1165
|
-
|
|
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 =
|
|
1309
|
-
|
|
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":"
|
|
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"}
|
package/dist/lib/platforms.d.ts
CHANGED
|
@@ -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;
|
|
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,16 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach
|
|
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
|
|
27
|
-
it('should
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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: ['
|
|
58
|
+
plugin: ['opencode-skills'],
|
|
53
59
|
theme: 'dark',
|
|
54
60
|
};
|
|
55
61
|
writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
|
|
56
62
|
|
|
57
|
-
//
|
|
63
|
+
// Simulate migration logic
|
|
58
64
|
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
59
|
-
|
|
60
|
-
|
|
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).
|
|
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
|
|
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: ['
|
|
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
|
|
83
|
-
|
|
84
|
-
expect(
|
|
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 (
|
|
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
|
-
|
|
108
|
-
expect(
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
// Simulate migration logic - should not throw
|
|
146
|
+
let processed = false;
|
|
135
147
|
try {
|
|
136
|
-
|
|
148
|
+
JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
149
|
+
processed = true;
|
|
137
150
|
} catch {
|
|
138
|
-
// Invalid JSON
|
|
139
|
-
|
|
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(
|
|
155
|
+
expect(processed).toBe(false);
|
|
148
156
|
});
|
|
149
157
|
});
|
|
150
158
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -107,8 +107,9 @@ export function configurePlatformPermissions(platform: Platform): { added: strin
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (platform === Platform.OpenCode) {
|
|
110
|
-
//
|
|
111
|
-
//
|
|
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('
|
|
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.'));
|
package/src/lib/migrations.ts
CHANGED
|
@@ -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
|
/**
|
package/src/lib/platforms.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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'),
|
package/src/lib/skills.test.ts
CHANGED
|
@@ -42,9 +42,10 @@ describe("getSkillsInstallPath", () => {
|
|
|
42
42
|
expect(path).toBe(join(homedir(), ".claude", "skills"));
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("should return
|
|
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(), ".
|
|
48
|
+
expect(path).toBe(join(homedir(), ".claude", "skills"));
|
|
48
49
|
});
|
|
49
50
|
});
|
|
50
51
|
|