@orderful/droid 0.27.2 → 0.27.4

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,42 @@
1
1
  # @orderful/droid
2
2
 
3
+ ## 0.27.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`c5bd82e`](https://github.com/Orderful/droid/commit/c5bd82e96202bd13fa71e57e45b5b2039ad95e6b) Thanks [@frytyler](https://github.com/frytyler)! - Add filesystem sync migration to fix untracked installations
8
+
9
+ **New migration (v0.27.4):**
10
+
11
+ Scans installed skills on the filesystem and adds any that aren't tracked in config. This is a one-time cleanup to fix inconsistencies from the v0.27.x migration series.
12
+
13
+ **What it does:**
14
+ - Scans `~/.claude/skills/` and `~/.config/opencode/skills/`
15
+ - Identifies installed skills missing from config
16
+ - Adds them with correct versions from bundled tool manifests
17
+ - Only processes known droid tool skills (ignores custom directories)
18
+
19
+ **Impact:**
20
+
21
+ Tools that were physically installed but not tracked (like `brain` and `comments`) will now show as properly installed with checkmarks in the TUI.
22
+
23
+ ## 0.27.3
24
+
25
+ ### Patch Changes
26
+
27
+ - [#152](https://github.com/Orderful/droid/pull/152) [`8bd3a4f`](https://github.com/Orderful/droid/commit/8bd3a4f4c6ef1352ed683be584693124a202f35a) Thanks [@frytyler](https://github.com/frytyler)! - Fix config skill name migration actually persisting changes to disk
28
+
29
+ **Bug fixes:**
30
+ - **Per-platform change tracking**: Migration now uses a `platformChanged` flag to track changes independently for each platform, ensuring all platform configs are properly saved
31
+ - **Prevent config overwrites**: `runPackageMigrations` now reloads config before saving the version marker, preventing stale data from overwriting migration changes
32
+ - **Remove conflicting legacy code**: Deleted v0.18.0 migration fallback that was actively undoing the config renaming by deleting newly-migrated unprefixed entries
33
+
34
+ **Impact:**
35
+
36
+ The v0.27.2 config migration now successfully renames tool entries (e.g. `droid-codex` → `codex`) and persists changes to `~/.droid/config.yaml`. Tools will correctly show as installed in the TUI after upgrade.
37
+
38
+ This completes the fix for tools showing as "not installed" after upgrading to v0.27.x.
39
+
3
40
  ## 0.27.2
4
41
 
5
42
  ### Patch Changes
package/dist/bin/droid.js CHANGED
@@ -352,16 +352,6 @@ function getInstalledToolVersion(toolName) {
352
352
  if (installedTools[skillName]) {
353
353
  return installedTools[skillName].version;
354
354
  }
355
- if (skillName.startsWith("droid-")) {
356
- const oldSkillName = skillName.replace(/^droid-/, "");
357
- if (installedTools[oldSkillName]) {
358
- const version2 = installedTools[oldSkillName].version;
359
- delete installedTools[oldSkillName];
360
- setPlatformTools(config, installedTools);
361
- saveConfig(config);
362
- return version2;
363
- }
364
- }
365
355
  }
366
356
  return null;
367
357
  }
@@ -628,6 +618,7 @@ function createConfigSkillNameMigration(version2) {
628
618
  config.platform = platformKey;
629
619
  const trackedTools = getPlatformTools(config);
630
620
  const skillNames = Object.keys(trackedTools);
621
+ let platformChanged = false;
631
622
  for (const skillName of skillNames) {
632
623
  if (!skillName.startsWith("droid-") || skillName === "droid") {
633
624
  continue;
@@ -636,10 +627,61 @@ function createConfigSkillNameMigration(version2) {
636
627
  if (!trackedTools[unprefixedName]) {
637
628
  trackedTools[unprefixedName] = trackedTools[skillName];
638
629
  delete trackedTools[skillName];
630
+ platformChanged = true;
639
631
  configChanged = true;
640
632
  }
641
633
  }
642
- if (configChanged) {
634
+ if (platformChanged) {
635
+ setPlatformTools(config, trackedTools);
636
+ }
637
+ }
638
+ config.platform = originalPlatform;
639
+ if (configChanged) {
640
+ saveConfig(config);
641
+ }
642
+ }
643
+ };
644
+ }
645
+ function createFilesystemSyncMigration(version2) {
646
+ return {
647
+ version: version2,
648
+ description: "Sync config with actually installed skills on filesystem",
649
+ up: () => {
650
+ const config = loadConfig();
651
+ const originalPlatform = config.platform;
652
+ let configChanged = false;
653
+ for (const platformKey of ["claude-code" /* ClaudeCode */, "opencode" /* OpenCode */]) {
654
+ if (!config.platforms[platformKey]) continue;
655
+ const skillsPath = getSkillsPath(platformKey);
656
+ if (!existsSync4(skillsPath)) continue;
657
+ config.platform = platformKey;
658
+ const trackedTools = getPlatformTools(config);
659
+ let platformChanged = false;
660
+ const bundledTools = getBundledTools();
661
+ const allToolNames = new Set(
662
+ bundledTools.flatMap(
663
+ (tool) => tool.includes.skills.map((s) => s.name)
664
+ )
665
+ );
666
+ const installedDirs = readdirSync3(skillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
667
+ for (const dirName of installedDirs) {
668
+ if (!allToolNames.has(dirName)) continue;
669
+ if (trackedTools[dirName]) continue;
670
+ let version3 = "0.0.0";
671
+ const matchingTool = bundledTools.find(
672
+ (tool) => tool.includes.skills.some((s) => s.name === dirName && s.required)
673
+ );
674
+ if (matchingTool) {
675
+ version3 = matchingTool.version;
676
+ }
677
+ trackedTools[dirName] = {
678
+ version: version3,
679
+ installed_at: (/* @__PURE__ */ new Date()).toISOString()
680
+ };
681
+ platformChanged = true;
682
+ configChanged = true;
683
+ }
684
+ if (platformChanged) {
643
685
  setPlatformTools(config, trackedTools);
644
686
  }
645
687
  }
@@ -652,7 +694,8 @@ function createConfigSkillNameMigration(version2) {
652
694
  }
653
695
  var PACKAGE_MIGRATIONS = [
654
696
  createPlatformSyncMigration("0.25.0"),
655
- createConfigSkillNameMigration("0.27.2")
697
+ createConfigSkillNameMigration("0.27.2"),
698
+ createFilesystemSyncMigration("0.27.4")
656
699
  ];
657
700
  var TOOL_MIGRATIONS = {
658
701
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -740,9 +783,10 @@ function runPackageMigrations(packageVersion) {
740
783
  return afterFrom && beforeOrAtTo;
741
784
  });
742
785
  if (pendingMigrations.length === 0) {
743
- config.migrations = config.migrations || {};
744
- config.migrations.package = packageVersion;
745
- saveConfig(config);
786
+ const freshConfig = loadConfig();
787
+ freshConfig.migrations = freshConfig.migrations || {};
788
+ freshConfig.migrations.package = packageVersion;
789
+ saveConfig(freshConfig);
746
790
  return { success: true };
747
791
  }
748
792
  const configDir = getConfigDir();
@@ -765,9 +809,10 @@ function runPackageMigrations(packageVersion) {
765
809
  };
766
810
  }
767
811
  }
768
- config.migrations = config.migrations || {};
769
- config.migrations.package = packageVersion;
770
- saveConfig(config);
812
+ const updatedConfig = loadConfig();
813
+ updatedConfig.migrations = updatedConfig.migrations || {};
814
+ updatedConfig.migrations.package = packageVersion;
815
+ saveConfig(updatedConfig);
771
816
  return { success: true };
772
817
  }
773
818
 
package/dist/index.js CHANGED
@@ -591,6 +591,7 @@ function createConfigSkillNameMigration(version) {
591
591
  config.platform = platformKey;
592
592
  const trackedTools = getPlatformTools(config);
593
593
  const skillNames = Object.keys(trackedTools);
594
+ let platformChanged = false;
594
595
  for (const skillName of skillNames) {
595
596
  if (!skillName.startsWith("droid-") || skillName === "droid") {
596
597
  continue;
@@ -599,10 +600,61 @@ function createConfigSkillNameMigration(version) {
599
600
  if (!trackedTools[unprefixedName]) {
600
601
  trackedTools[unprefixedName] = trackedTools[skillName];
601
602
  delete trackedTools[skillName];
603
+ platformChanged = true;
602
604
  configChanged = true;
603
605
  }
604
606
  }
605
- if (configChanged) {
607
+ if (platformChanged) {
608
+ setPlatformTools(config, trackedTools);
609
+ }
610
+ }
611
+ config.platform = originalPlatform;
612
+ if (configChanged) {
613
+ saveConfig(config);
614
+ }
615
+ }
616
+ };
617
+ }
618
+ function createFilesystemSyncMigration(version) {
619
+ return {
620
+ version,
621
+ description: "Sync config with actually installed skills on filesystem",
622
+ up: () => {
623
+ const config = loadConfig();
624
+ const originalPlatform = config.platform;
625
+ let configChanged = false;
626
+ for (const platformKey of ["claude-code" /* ClaudeCode */, "opencode" /* OpenCode */]) {
627
+ if (!config.platforms[platformKey]) continue;
628
+ const skillsPath = getSkillsPath(platformKey);
629
+ if (!existsSync4(skillsPath)) continue;
630
+ config.platform = platformKey;
631
+ const trackedTools = getPlatformTools(config);
632
+ let platformChanged = false;
633
+ const bundledTools = getBundledTools();
634
+ const allToolNames = new Set(
635
+ bundledTools.flatMap(
636
+ (tool) => tool.includes.skills.map((s) => s.name)
637
+ )
638
+ );
639
+ const installedDirs = readdirSync3(skillsPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
640
+ for (const dirName of installedDirs) {
641
+ if (!allToolNames.has(dirName)) continue;
642
+ if (trackedTools[dirName]) continue;
643
+ let version2 = "0.0.0";
644
+ const matchingTool = bundledTools.find(
645
+ (tool) => tool.includes.skills.some((s) => s.name === dirName && s.required)
646
+ );
647
+ if (matchingTool) {
648
+ version2 = matchingTool.version;
649
+ }
650
+ trackedTools[dirName] = {
651
+ version: version2,
652
+ installed_at: (/* @__PURE__ */ new Date()).toISOString()
653
+ };
654
+ platformChanged = true;
655
+ configChanged = true;
656
+ }
657
+ if (platformChanged) {
606
658
  setPlatformTools(config, trackedTools);
607
659
  }
608
660
  }
@@ -615,7 +667,8 @@ function createConfigSkillNameMigration(version) {
615
667
  }
616
668
  var PACKAGE_MIGRATIONS = [
617
669
  createPlatformSyncMigration("0.25.0"),
618
- createConfigSkillNameMigration("0.27.2")
670
+ createConfigSkillNameMigration("0.27.2"),
671
+ createFilesystemSyncMigration("0.27.4")
619
672
  ];
620
673
  var TOOL_MIGRATIONS = {
621
674
  brain: [createConfigDirMigration("droid-brain", "0.2.3")],
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,WAAW,EAA0B,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgDxI;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,WAAW,CA2BxC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAKpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAanD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAchE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAWD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAapE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,GAAG,IAAI,CAWrF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAQ5E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACtB,MAAM,SAAS,CAAC;AAmDjB;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,WAAW,CA4BxC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAKpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAanD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAiBhE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAWD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAapE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,cAAc,GACxB,IAAI,CAWN;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAQ5E"}
@@ -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;AAiOjB;;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,CAqDA"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/lib/migrations.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,SAAS,CAAC;AA8SjB;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA2CtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAStC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAyDA"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/lib/tools.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAyBrE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,YAAY,EAAE,CAmBhD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAczD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiCvE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAuBpE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CA2BtD"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/lib/tools.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAyBrE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,YAAY,EAAE,CAmBhD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAczD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBvE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAuBpE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CA2BtD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderful/droid",
3
- "version": "0.27.2",
3
+ "version": "0.27.4",
4
4
  "description": "AI workflow toolkit for sharing skills, commands, and agents across the team",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib/config.ts CHANGED
@@ -2,14 +2,21 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  import YAML from 'yaml';
5
- import { Platform, BuiltInOutput, type DroidConfig, type LegacyDroidConfig, type SkillOverrides, type AutoUpdateConfig } from './types';
5
+ import {
6
+ Platform,
7
+ BuiltInOutput,
8
+ type DroidConfig,
9
+ type LegacyDroidConfig,
10
+ type SkillOverrides,
11
+ type AutoUpdateConfig,
12
+ } from './types';
6
13
 
7
14
  const CONFIG_DIR = join(homedir(), '.droid');
8
15
  const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
9
16
 
10
17
  const DEFAULT_AUTO_UPDATE: AutoUpdateConfig = {
11
- app: false, // Opt-in: user must enable
12
- tools: true, // Opt-out: enabled by default (tools only update when app updates)
18
+ app: false, // Opt-in: user must enable
19
+ tools: true, // Opt-out: enabled by default (tools only update when app updates)
13
20
  };
14
21
 
15
22
  const DEFAULT_CONFIG: DroidConfig = {
@@ -25,14 +32,17 @@ const DEFAULT_CONFIG: DroidConfig = {
25
32
  * v1: ai_tool + skills flat
26
33
  * v2: platform + platforms map with per-platform tools
27
34
  */
28
- function migrateConfig(config: Partial<DroidConfig> & Partial<LegacyDroidConfig>): DroidConfig {
35
+ function migrateConfig(
36
+ config: Partial<DroidConfig> & Partial<LegacyDroidConfig>,
37
+ ): DroidConfig {
29
38
  // Check if this is a legacy config (has ai_tool, no platform)
30
39
  if ('ai_tool' in config && !('platform' in config)) {
31
40
  const legacyConfig = config as LegacyDroidConfig;
32
41
  return {
33
42
  platform: legacyConfig.ai_tool,
34
43
  user_mention: legacyConfig.user_mention ?? DEFAULT_CONFIG.user_mention,
35
- output_preference: legacyConfig.output_preference ?? DEFAULT_CONFIG.output_preference,
44
+ output_preference:
45
+ legacyConfig.output_preference ?? DEFAULT_CONFIG.output_preference,
36
46
  git_username: legacyConfig.git_username ?? DEFAULT_CONFIG.git_username,
37
47
  platforms: {
38
48
  [legacyConfig.ai_tool]: {
@@ -79,7 +89,8 @@ export function loadConfig(): DroidConfig {
79
89
 
80
90
  try {
81
91
  const content = readFileSync(CONFIG_FILE, 'utf-8');
82
- const rawConfig = YAML.parse(content) as Partial<DroidConfig> & Partial<LegacyDroidConfig>;
92
+ const rawConfig = YAML.parse(content) as Partial<DroidConfig> &
93
+ Partial<LegacyDroidConfig>;
83
94
 
84
95
  // Check if migration is needed
85
96
  // TODO: Remove after v0.14.0 (target: late Jan 2025)
@@ -133,7 +144,10 @@ export function getConfigValue(key: string): unknown {
133
144
  export function setConfigValue(key: string, value: unknown): void {
134
145
  const config = loadConfig();
135
146
  const keys = key.split('.');
136
- let current: Record<string, unknown> = config as unknown as Record<string, unknown>;
147
+ let current: Record<string, unknown> = config as unknown as Record<
148
+ string,
149
+ unknown
150
+ >;
137
151
 
138
152
  for (let i = 0; i < keys.length - 1; i++) {
139
153
  if (current[keys[i]] === undefined) {
@@ -198,7 +212,10 @@ export function loadSkillOverrides(skillName: string): SkillOverrides {
198
212
  /**
199
213
  * Save skill-specific overrides
200
214
  */
201
- export function saveSkillOverrides(skillName: string, overrides: SkillOverrides): void {
215
+ export function saveSkillOverrides(
216
+ skillName: string,
217
+ overrides: SkillOverrides,
218
+ ): void {
202
219
  const normalizedName = normalizeSkillNameForConfig(skillName);
203
220
  const overridesPath = getSkillOverridesPath(skillName);
204
221
  const skillDir = join(CONFIG_DIR, 'skills', normalizedName);
@@ -179,6 +179,7 @@ function createConfigSkillNameMigration(version: string): Migration {
179
179
  config.platform = platformKey;
180
180
  const trackedTools = getPlatformTools(config);
181
181
  const skillNames = Object.keys(trackedTools);
182
+ let platformChanged = false;
182
183
 
183
184
  for (const skillName of skillNames) {
184
185
  // Skip if already unprefixed or is the special 'droid' tool
@@ -192,11 +193,86 @@ function createConfigSkillNameMigration(version: string): Migration {
192
193
  if (!trackedTools[unprefixedName]) {
193
194
  trackedTools[unprefixedName] = trackedTools[skillName];
194
195
  delete trackedTools[skillName];
196
+ platformChanged = true;
195
197
  configChanged = true;
196
198
  }
197
199
  }
198
200
 
199
- if (configChanged) {
201
+ if (platformChanged) {
202
+ setPlatformTools(config, trackedTools);
203
+ }
204
+ }
205
+
206
+ config.platform = originalPlatform;
207
+ if (configChanged) {
208
+ saveConfig(config);
209
+ }
210
+ },
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Sync config with filesystem - add any installed skills that aren't tracked
216
+ * This is a one-time cleanup migration to fix config inconsistencies from v0.27.x
217
+ */
218
+ function createFilesystemSyncMigration(version: string): Migration {
219
+ return {
220
+ version,
221
+ description: 'Sync config with actually installed skills on filesystem',
222
+ up: () => {
223
+ const config = loadConfig();
224
+ const originalPlatform = config.platform;
225
+ let configChanged = false;
226
+
227
+ // Check both platforms
228
+ for (const platformKey of [Platform.ClaudeCode, Platform.OpenCode]) {
229
+ if (!config.platforms[platformKey]) continue;
230
+
231
+ const skillsPath = getSkillsPath(platformKey);
232
+ if (!existsSync(skillsPath)) continue;
233
+
234
+ config.platform = platformKey;
235
+ const trackedTools = getPlatformTools(config);
236
+ let platformChanged = false;
237
+
238
+ // Get all tool names from manifests
239
+ const bundledTools = getBundledTools();
240
+ const allToolNames = new Set(
241
+ bundledTools.flatMap((tool) =>
242
+ tool.includes.skills.map((s) => s.name),
243
+ ),
244
+ );
245
+
246
+ // Scan filesystem for installed skills
247
+ const installedDirs = readdirSync(skillsPath, { withFileTypes: true })
248
+ .filter((dirent) => dirent.isDirectory())
249
+ .map((dirent) => dirent.name);
250
+
251
+ for (const dirName of installedDirs) {
252
+ // Skip if not a known tool skill
253
+ if (!allToolNames.has(dirName)) continue;
254
+
255
+ // Skip if already tracked
256
+ if (trackedTools[dirName]) continue;
257
+
258
+ // Try to get version from bundled tool
259
+ let version = '0.0.0';
260
+ const matchingTool = bundledTools.find((tool) =>
261
+ tool.includes.skills.some((s) => s.name === dirName && s.required),
262
+ );
263
+ if (matchingTool) {
264
+ version = matchingTool.version;
265
+ }
266
+
267
+ trackedTools[dirName] = {
268
+ version,
269
+ installed_at: new Date().toISOString(),
270
+ };
271
+ platformChanged = true;
272
+ configChanged = true;
273
+ }
274
+
275
+ if (platformChanged) {
200
276
  setPlatformTools(config, trackedTools);
201
277
  }
202
278
  }
@@ -218,6 +294,7 @@ function createConfigSkillNameMigration(version: string): Migration {
218
294
  const PACKAGE_MIGRATIONS: Migration[] = [
219
295
  createPlatformSyncMigration('0.25.0'),
220
296
  createConfigSkillNameMigration('0.27.2'),
297
+ createFilesystemSyncMigration('0.27.4'),
221
298
  ];
222
299
 
223
300
  /**
@@ -387,9 +464,11 @@ export function runPackageMigrations(packageVersion: string): {
387
464
 
388
465
  if (pendingMigrations.length === 0) {
389
466
  // No migrations to run, but still update the version marker
390
- config.migrations = config.migrations || {};
391
- config.migrations.package = packageVersion;
392
- saveConfig(config);
467
+ // Reload config in case it was modified elsewhere
468
+ const freshConfig = loadConfig();
469
+ freshConfig.migrations = freshConfig.migrations || {};
470
+ freshConfig.migrations.package = packageVersion;
471
+ saveConfig(freshConfig);
393
472
  return { success: true };
394
473
  }
395
474
 
@@ -418,8 +497,10 @@ export function runPackageMigrations(packageVersion: string): {
418
497
  }
419
498
 
420
499
  // All migrations succeeded, update version marker
421
- config.migrations = config.migrations || {};
422
- config.migrations.package = packageVersion;
423
- saveConfig(config);
500
+ // Reload config in case migrations modified it
501
+ const updatedConfig = loadConfig();
502
+ updatedConfig.migrations = updatedConfig.migrations || {};
503
+ updatedConfig.migrations.package = packageVersion;
504
+ saveConfig(updatedConfig);
424
505
  return { success: true };
425
506
  }
package/src/lib/tools.ts CHANGED
@@ -2,12 +2,11 @@ import { existsSync, readdirSync, readFileSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import YAML from 'yaml';
5
- import { loadConfig, saveConfig } from './config';
5
+ import { loadConfig } from './config';
6
6
  import {
7
7
  type ToolManifest,
8
8
  type ToolIncludes,
9
9
  getPlatformTools,
10
- setPlatformTools,
11
10
  } from './types';
12
11
  import { compareSemver } from './version';
13
12
 
@@ -113,20 +112,6 @@ export function getInstalledToolVersion(toolName: string): string | null {
113
112
  if (installedTools[skillName]) {
114
113
  return installedTools[skillName].version;
115
114
  }
116
-
117
- // Migration fallback (v0.18.0): Check for old skill name without droid- prefix
118
- // This allows tools to show as "update available" rather than "not installed"
119
- if (skillName.startsWith('droid-')) {
120
- const oldSkillName = skillName.replace(/^droid-/, '');
121
- if (installedTools[oldSkillName]) {
122
- const version = installedTools[oldSkillName].version;
123
- // Clean up stale config entry now that we've detected it
124
- delete installedTools[oldSkillName];
125
- setPlatformTools(config, installedTools);
126
- saveConfig(config);
127
- return version;
128
- }
129
- }
130
115
  }
131
116
 
132
117
  return null;