@light-merlin-dark/skill-sync 0.1.3 → 0.1.5

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.1.5
6
+ - Make `make -n release` a true dry run by avoiding recursive `make` inside the release recipe
7
+
8
+ ## 0.1.4
9
+ - Make bare `skill-sync` / `ss` show a high-signal landing/help view instead of mutating
10
+ - Add `doctor` as the high-signal diagnostic command and `execute` as the explicit mutating command; keep `check` and `sync` as compatibility aliases
11
+ - Discover harness-installed skills as fallback sources, while keeping project-root sources authoritative for the same slug
12
+ - Add frontmatter-based install scoping so harness-native skills can stay local-only or target specific harness ids instead of syncing everywhere
13
+ - Tighten symlink-first behavior by repairing matching copied installs instead of treating them as fully healthy
14
+ - Align CLI `--version` output with the package version
15
+ - Harden `make release` so patch releases can auto-bump version/changelog, publish, commit, tag, push, and refresh the GitHub release in one path
16
+
3
17
  ## 0.1.3
4
18
  - Makefile release: robust GitHub release notes generation via `--notes-file` and correct escaping of `awk $0`.
5
19
 
package/README.md CHANGED
@@ -29,6 +29,7 @@ Sync local repo-backed agent skills across Codex, Claude Code, Cursor, Gemini, H
29
29
  - top-level `SKILL.md`
30
30
  - nested `skills/*/SKILL.md`
31
31
  - Detects installed harness skill roots
32
+ - Supports scoped installs for harness-local skills
32
33
  - Plans drift without changing anything
33
34
  - Syncs symlinked installs into harnesses
34
35
  - Warns when the same skill slug appears multiple times in your configured project roots
@@ -64,19 +65,23 @@ skill-sync sources
64
65
  Check drift:
65
66
 
66
67
  ```bash
67
- skill-sync check
68
+ skill-sync doctor
69
+ skill-sync doctor --verbose
68
70
  ```
69
71
 
70
- Sync:
72
+ Execute:
71
73
 
72
74
  ```bash
73
- skill-sync
75
+ skill-sync execute
76
+ skill-sync sync
74
77
  ```
75
78
 
76
79
  Shortcut:
77
80
 
78
81
  ```bash
79
82
  ss
83
+ ss doctor
84
+ ss execute
80
85
  ```
81
86
 
82
87
  ## Default Model
@@ -94,8 +99,9 @@ It complements `npx skills`; it does not replace it.
94
99
  Core:
95
100
 
96
101
  ```bash
102
+ skill-sync doctor
97
103
  skill-sync check
98
- skill-sync
104
+ skill-sync execute
99
105
  skill-sync sync
100
106
  skill-sync sources
101
107
  skill-sync harnesses
@@ -124,13 +130,15 @@ skill-sync harness remove my-tool
124
130
  Agent-friendly options:
125
131
 
126
132
  ```bash
127
- skill-sync check --json
128
- skill-sync sync --dry-run --json
133
+ skill-sync doctor --json
134
+ skill-sync execute --dry-run --json
129
135
  skill-sync backup restore <id> --dry-run
130
- skill-sync check --projects-root /path/to/projects --harness codex
131
- skill-sync check --home /tmp/fake-home
136
+ skill-sync doctor --projects-root /path/to/projects --harness codex
137
+ skill-sync doctor --home /tmp/fake-home
132
138
  ```
133
139
 
140
+ Bare `skill-sync` / `ss` prints a high-signal landing/help view. Human output defaults to a concise summary. Use `--verbose` when you want the full per-entry plan, including orphan install details during `doctor`.
141
+
134
142
  ## Supported Harness Model
135
143
 
136
144
  Built-in harness roots currently include:
@@ -156,10 +164,12 @@ skill-sync harness add codex-beta ~/.codex-beta/skills
156
164
 
157
165
  ## Safety Rules
158
166
 
159
- - `check` never mutates
160
- - `sync` only replaces entries the tool owns or missing entries
167
+ - `doctor` never mutates
168
+ - `execute` / `sync` only replace entries the tool owns or missing entries
161
169
  - unmanaged conflicts are reported, not overwritten
162
170
  - duplicate `_dev` slugs are surfaced before harness-level sync planning
171
+ - harness-installed skills can be promoted as fallback sources when no project-root source exists for the same slug
172
+ - harness-native skills can stay local-only via frontmatter instead of being fanned out everywhere
163
173
  - backups snapshot harness `skills` directories before or after risky changes
164
174
  - restore can recreate symlinks when the original source still exists
165
175
  - restore falls back to minimal backed-up `SKILL.md` content when the source no longer exists
@@ -210,7 +220,7 @@ Each backup includes:
210
220
  Example:
211
221
 
212
222
  ```bash
213
- skill-sync check --json
223
+ skill-sync doctor --json
214
224
  ```
215
225
 
216
226
  Returns structured data for:
@@ -229,6 +239,13 @@ For every immediate child repo under each configured projects root, `skill-sync`
229
239
  - `<repo>/SKILL.md`
230
240
  - `<repo>/skills/*/SKILL.md`
231
241
 
242
+ It also inspects detected harness roots and can treat installed skills there as fallback sources. Project-root sources win over harness-installed sources when the same slug exists in both places.
243
+
244
+ Harness-root sources can also declare install scope in frontmatter:
245
+
246
+ - `skill-sync-scope: local-only` keeps that source on its owning harness only
247
+ - `skill-sync-install-on: [codex, hermes]` limits installs to specific harness ids
248
+
232
249
  Canonical install names default to:
233
250
 
234
251
  - `slugify(frontmatter name)` when `name:` exists
@@ -262,7 +279,7 @@ bun install
262
279
  Run the CLI directly:
263
280
 
264
281
  ```bash
265
- bun run src/index.ts check
282
+ bun run src/index.ts doctor
266
283
  ```
267
284
 
268
285
  Run tests:
@@ -277,6 +294,16 @@ Build:
277
294
  bun run build
278
295
  ```
279
296
 
297
+ ## Release
298
+
299
+ Patch release:
300
+
301
+ ```bash
302
+ make release
303
+ ```
304
+
305
+ This bumps the patch version, moves the `## Unreleased` notes into the new versioned changelog section, runs lint/test/build, publishes to npm, commits the release, pushes `main`, tags the release, and creates or updates the GitHub release.
306
+
280
307
  ## Positioning
281
308
 
282
309
  Use `npx skills` when:
@@ -293,3 +320,7 @@ Use `skill-sync` when:
293
320
  ## License
294
321
 
295
322
  MIT
323
+
324
+ ---
325
+
326
+ Built by [Robert E. Beckner III (Merlin)](https://rbeckner.com)
package/SKILL.md CHANGED
@@ -9,9 +9,9 @@ Use `skill-sync` as the default interface for local skill-harness maintenance.
9
9
 
10
10
  ## Core Workflow
11
11
  1. Inspect harness detection and discovered skill sources.
12
- 2. Run a dry check before making changes.
12
+ 2. Run a doctor pass before making changes.
13
13
  3. Create a backup before risky cleanup or restore work.
14
- 4. Sync or restore.
14
+ 4. Execute or restore.
15
15
  5. Verify the resulting symlinks or restored content.
16
16
 
17
17
  Start with:
@@ -19,19 +19,21 @@ Start with:
19
19
  ```bash
20
20
  skill-sync harnesses
21
21
  skill-sync sources
22
- skill-sync check
22
+ skill-sync doctor
23
+ skill-sync doctor --verbose
23
24
  ```
24
25
 
25
26
  Apply changes:
26
27
 
27
28
  ```bash
28
- skill-sync
29
+ skill-sync execute
30
+ skill-sync sync
29
31
  ```
30
32
 
31
33
  Or explicitly:
32
34
 
33
35
  ```bash
34
- skill-sync sync
36
+ skill-sync execute
35
37
  ```
36
38
 
37
39
  ## Backup Workflow
@@ -65,17 +67,22 @@ skill-sync backup restore <backup-id>
65
67
  Use JSON when the output will be consumed by another tool or agent:
66
68
 
67
69
  ```bash
68
- skill-sync check --json
70
+ skill-sync doctor --json
69
71
  skill-sync sources --json
70
72
  skill-sync harnesses --json
73
+ skill-sync execute --json
71
74
  ```
72
75
 
76
+ Bare `skill-sync` prints a high-signal landing/help view. Default human output is concise. Add `--verbose` when you need the full per-entry plan and orphan listing.
77
+
73
78
  ## Safety Rules
74
79
 
75
- - Prefer `check` before `sync`.
76
- - If `check` reports a `conflict` due to an existing *unmanaged* install (common case: a skill folder already exists in a harness root like `~/.hermes/skills/<skill>`), resolve by either:
77
- - removing the unmanaged directory/file and re-running `sync`, or
80
+ - Prefer `doctor` before `execute`.
81
+ - If `doctor` reports a `conflict` due to an existing *unmanaged* install (common case: a skill folder already exists in a harness root like `~/.hermes/skills/<skill>`), resolve by either:
82
+ - removing the unmanaged directory/file and re-running `execute`, or
78
83
  - restoring via `skill-sync backup restore <backup-id>`.
79
84
  Do not leave mixed symlink + real directories behind.
80
85
  - Use `--home` for isolated testing against a fake home directory.
81
86
  - Use `--projects-root` when you need to constrain discovery to a specific source tree.
87
+ - Project-root sources are authoritative. Harness-installed skills act as fallback sources when no project-root source exists for the same slug.
88
+ - If a harness-native skill should not fan out globally, add `skill-sync-scope: local-only` to its frontmatter. Use `skill-sync-install-on: [harness-a, harness-b]` for explicit multi-harness targeting.
package/dist/index.js CHANGED
@@ -666,18 +666,53 @@ function timestampId() {
666
666
  function slugify(input) {
667
667
  return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9.-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
668
668
  }
669
- function parseSkillFrontmatterName(skillFilePath) {
670
- const content = readFileSync(skillFilePath, "utf8");
671
- if (!content.startsWith("---")) {
672
- return;
673
- }
674
- const parts = content.split(`
675
- ---`);
676
- if (parts.length < 2) {
677
- return;
669
+ function parseSkillFrontmatterContent(content) {
670
+ const frontmatterLines = extractFrontmatterLines(content);
671
+ if (!frontmatterLines) {
672
+ return {};
673
+ }
674
+ const frontmatter = {};
675
+ for (let index = 0;index < frontmatterLines.length; index += 1) {
676
+ const line = frontmatterLines[index];
677
+ const nameMatch = line.match(/^name:\s*(.+)\s*$/);
678
+ if (nameMatch) {
679
+ frontmatter.name = stripYamlQuotes(nameMatch[1].trim());
680
+ continue;
681
+ }
682
+ const scopeMatch = line.match(/^skill-sync-scope:\s*(.+)\s*$/);
683
+ if (scopeMatch) {
684
+ const value = stripYamlQuotes(scopeMatch[1].trim()).toLowerCase();
685
+ if (value === "global" || value === "local-only") {
686
+ frontmatter.skillSyncScope = value;
687
+ }
688
+ continue;
689
+ }
690
+ const installOnInlineMatch = line.match(/^skill-sync-install-on:\s*(.+)\s*$/);
691
+ if (installOnInlineMatch) {
692
+ const parsed = parseFrontmatterListValue(installOnInlineMatch[1]);
693
+ if (parsed.length > 0) {
694
+ frontmatter.skillSyncInstallOn = parsed;
695
+ }
696
+ continue;
697
+ }
698
+ if (!/^skill-sync-install-on:\s*$/.test(line)) {
699
+ continue;
700
+ }
701
+ const values = [];
702
+ while (index + 1 < frontmatterLines.length) {
703
+ const nextLine = frontmatterLines[index + 1];
704
+ const itemMatch = nextLine.match(/^\s*-\s+(.+)\s*$/);
705
+ if (!itemMatch) {
706
+ break;
707
+ }
708
+ values.push(stripYamlQuotes(itemMatch[1].trim()));
709
+ index += 1;
710
+ }
711
+ if (values.length > 0) {
712
+ frontmatter.skillSyncInstallOn = values;
713
+ }
678
714
  }
679
- const match = parts[0].match(/^name:\s*(.+)\s*$/m);
680
- return match?.[1]?.trim();
715
+ return frontmatter;
681
716
  }
682
717
  function hashContent(content) {
683
718
  return createHash("sha1").update(content).digest("hex");
@@ -718,13 +753,45 @@ function readFileSyncLink(path) {
718
753
  return readlinkSync(path);
719
754
  }
720
755
  function pathOwnsEntry(rootPath, entryPath) {
721
- const normalizedRoot = resolve(rootPath);
722
- const normalizedEntry = resolve(entryPath);
756
+ const normalizedRoot = normalizeComparablePath(rootPath);
757
+ const normalizedEntry = normalizeComparablePath(entryPath);
723
758
  return normalizedEntry === normalizedRoot || normalizedEntry.startsWith(`${normalizedRoot}/`);
724
759
  }
725
760
  function removePath(path) {
726
761
  rmSync(path, { recursive: true, force: true });
727
762
  }
763
+ function extractFrontmatterLines(content) {
764
+ const lines = content.split(/\r?\n/);
765
+ if (lines[0] !== "---") {
766
+ return;
767
+ }
768
+ const closingIndex = lines.findIndex((line, index) => index > 0 && line === "---");
769
+ if (closingIndex === -1) {
770
+ return;
771
+ }
772
+ return lines.slice(1, closingIndex);
773
+ }
774
+ function stripYamlQuotes(value) {
775
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
776
+ return value.slice(1, -1);
777
+ }
778
+ return value;
779
+ }
780
+ function parseFrontmatterListValue(rawValue) {
781
+ const value = stripYamlQuotes(rawValue.trim());
782
+ if (!value) {
783
+ return [];
784
+ }
785
+ const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
786
+ return [...new Set(inner.split(",").map((item) => stripYamlQuotes(item.trim())).filter(Boolean))];
787
+ }
788
+ function normalizeComparablePath(path) {
789
+ try {
790
+ return realpathSync(path);
791
+ } catch {
792
+ return resolve(path);
793
+ }
794
+ }
728
795
 
729
796
  // src/core/backup.ts
730
797
  function createBackup(runtime, harnesses, state) {
@@ -950,7 +1017,8 @@ function getDefaultConfig(homeDir) {
950
1017
  projectsRoots: [join3(homeDir, "_dev")],
951
1018
  discovery: {
952
1019
  ignorePathPrefixes: [],
953
- preferPathPrefixes: []
1020
+ preferPathPrefixes: [],
1021
+ includeHarnessRoots: true
954
1022
  },
955
1023
  harnesses: {
956
1024
  custom: []
@@ -975,7 +1043,8 @@ function loadConfig(runtime) {
975
1043
  projectsRoots: (config.projectsRoots || []).map((root) => expandHomePath(root, runtime.homeDir)),
976
1044
  discovery: {
977
1045
  ignorePathPrefixes: (config.discovery?.ignorePathPrefixes || []).map((path) => expandHomePath(path, runtime.homeDir)),
978
- preferPathPrefixes: (config.discovery?.preferPathPrefixes || []).map((path) => expandHomePath(path, runtime.homeDir))
1046
+ preferPathPrefixes: (config.discovery?.preferPathPrefixes || []).map((path) => expandHomePath(path, runtime.homeDir)),
1047
+ includeHarnessRoots: config.discovery?.includeHarnessRoots !== false
979
1048
  },
980
1049
  harnesses: {
981
1050
  custom: config.harnesses?.custom || []
@@ -1089,9 +1158,9 @@ function filterHarnesses(harnesses, selectedIds) {
1089
1158
  }
1090
1159
 
1091
1160
  // src/core/sources.ts
1092
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
1161
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync as realpathSync3 } from "node:fs";
1093
1162
  import { basename as basename2, join as join4, relative as relative2, resolve as resolve4 } from "node:path";
1094
- function discoverSkillSet(config) {
1163
+ function discoverSkillSet(config, harnesses = []) {
1095
1164
  const discovered = [];
1096
1165
  const discovery = getDiscoveryConfig(config);
1097
1166
  for (const projectsRoot of config.projectsRoots) {
@@ -1110,6 +1179,9 @@ function discoverSkillSet(config) {
1110
1179
  }
1111
1180
  }
1112
1181
  }
1182
+ if (discovery.includeHarnessRoots) {
1183
+ discovered.push(...discoverHarnessSkills(harnesses));
1184
+ }
1113
1185
  const filtered = discovered.filter((skill) => !isIgnoredSource(skill.sourcePath, discovery.ignorePathPrefixes));
1114
1186
  const deduped = new Map;
1115
1187
  for (const skill of filtered) {
@@ -1119,7 +1191,7 @@ function discoverSkillSet(config) {
1119
1191
  deduped.set(key, skill);
1120
1192
  continue;
1121
1193
  }
1122
- if (existing.sourceType === "nested" && skill.sourceType === "repo-root") {
1194
+ if (compareEquivalentSourcePreference(skill, existing) < 0) {
1123
1195
  deduped.set(key, skill);
1124
1196
  }
1125
1197
  }
@@ -1129,27 +1201,39 @@ function discoverSkillSet(config) {
1129
1201
  sourceDiagnostics
1130
1202
  };
1131
1203
  }
1132
- function buildDiscoveredSkill(projectsRoot, repoPath, sourcePath, skillFilePath, sourceType) {
1133
- const metadataName = parseSkillFrontmatterName(skillFilePath);
1134
- const contentHash = hashContent(readFileSync3(skillFilePath, "utf8"));
1204
+ function buildDiscoveredSkill(projectsRoot, repoPath, sourcePath, skillFilePath, sourceType, harnessId) {
1205
+ const normalizedProjectsRoot = normalizeExistingPath(projectsRoot);
1206
+ const normalizedRepoPath = normalizeExistingPath(repoPath);
1207
+ const normalizedSourcePath = normalizeExistingPath(sourcePath);
1208
+ const normalizedSkillFilePath = normalizeExistingPath(skillFilePath);
1209
+ const skillContent = readFileSync3(normalizedSkillFilePath, "utf8");
1210
+ const frontmatter = parseSkillFrontmatterContent(skillContent);
1211
+ const metadataName = frontmatter.name;
1212
+ const contentHash = hashContent(skillContent);
1135
1213
  const fallbackName = sourceType === "repo-root" ? basename2(repoPath) : basename2(sourcePath);
1136
1214
  const canonicalSlug = slugify(metadataName || fallbackName);
1137
- const sourceKey = resolve4(sourcePath);
1215
+ const sourceKey = normalizedSourcePath;
1138
1216
  return {
1139
1217
  sourceKey,
1140
- sourcePath: resolve4(sourcePath),
1141
- skillFilePath: resolve4(skillFilePath),
1142
- repoPath: resolve4(repoPath),
1143
- projectsRoot: resolve4(projectsRoot),
1218
+ sourcePath: normalizedSourcePath,
1219
+ skillFilePath: normalizedSkillFilePath,
1220
+ repoPath: normalizedRepoPath,
1221
+ projectsRoot: normalizedProjectsRoot,
1144
1222
  sourceType,
1223
+ harnessId,
1145
1224
  metadataName,
1225
+ installHarnessIds: resolveInstallHarnessIds(sourceType, harnessId, frontmatter),
1146
1226
  canonicalSlug,
1147
1227
  contentHash
1148
1228
  };
1149
1229
  }
1150
1230
  function describeSkill(skill) {
1231
+ const scopeSuffix = describeInstallScope(skill);
1232
+ if (skill.sourceType === "harness-root" && skill.harnessId) {
1233
+ return `${skill.canonicalSlug} <= ${skill.harnessId}:${skill.sourcePath}${scopeSuffix}`;
1234
+ }
1151
1235
  const repoRelative = relative2(skill.projectsRoot, skill.sourcePath) || basename2(skill.sourcePath);
1152
- return `${skill.canonicalSlug} <= ${repoRelative}`;
1236
+ return `${skill.canonicalSlug} <= ${repoRelative}${scopeSuffix}`;
1153
1237
  }
1154
1238
  function isIgnoredSource(sourcePath, ignorePrefixes) {
1155
1239
  return ignorePrefixes.some((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
@@ -1165,29 +1249,41 @@ function resolveGlobalDuplicates(skills, preferPrefixes) {
1165
1249
  const warnings = [];
1166
1250
  const errors = [];
1167
1251
  for (const group of grouped.values()) {
1168
- if (group.length === 1) {
1169
- resolved.push(group[0]);
1252
+ const uniqueGroup = dedupeEquivalentSources(group);
1253
+ const projectBacked = uniqueGroup.filter((skill) => skill.sourceType !== "harness-root");
1254
+ const preferredGroup = projectBacked.length > 0 ? projectBacked : uniqueGroup;
1255
+ if (preferredGroup.length === 1) {
1256
+ resolved.push(preferredGroup[0]);
1257
+ if (uniqueGroup.length > preferredGroup.length) {
1258
+ warnings.push({
1259
+ slug: uniqueGroup[0].canonicalSlug,
1260
+ severity: "warning",
1261
+ resolution: "resolved-by-preference",
1262
+ chosenSourcePath: preferredGroup[0].sourcePath,
1263
+ sourcePaths: uniqueGroup.map((skill) => skill.sourcePath).sort()
1264
+ });
1265
+ }
1170
1266
  continue;
1171
1267
  }
1172
- const distinctHashes = new Set(group.map((skill) => skill.contentHash));
1268
+ const distinctHashes = new Set(preferredGroup.map((skill) => skill.contentHash));
1173
1269
  if (distinctHashes.size !== 1) {
1174
- resolved.push(...group);
1270
+ resolved.push(...preferredGroup);
1175
1271
  errors.push({
1176
- slug: group[0].canonicalSlug,
1272
+ slug: preferredGroup[0].canonicalSlug,
1177
1273
  severity: "error",
1178
1274
  resolution: "unresolved",
1179
- sourcePaths: group.map((skill) => skill.sourcePath).sort()
1275
+ sourcePaths: preferredGroup.map((skill) => skill.sourcePath).sort()
1180
1276
  });
1181
1277
  continue;
1182
1278
  }
1183
- const sorted = [...group].sort((a, b) => compareDiscoveredSkills(a, b, preferPrefixes));
1279
+ const sorted = [...preferredGroup].sort((a, b) => compareDiscoveredSkills(a, b, preferPrefixes));
1184
1280
  resolved.push(sorted[0]);
1185
1281
  warnings.push({
1186
- slug: group[0].canonicalSlug,
1282
+ slug: preferredGroup[0].canonicalSlug,
1187
1283
  severity: "warning",
1188
1284
  resolution: "resolved-by-preference",
1189
1285
  chosenSourcePath: sorted[0].sourcePath,
1190
- sourcePaths: group.map((skill) => skill.sourcePath).sort()
1286
+ sourcePaths: uniqueGroup.map((skill) => skill.sourcePath).sort()
1191
1287
  });
1192
1288
  }
1193
1289
  return {
@@ -1201,10 +1297,15 @@ function resolveGlobalDuplicates(skills, preferPrefixes) {
1201
1297
  function getDiscoveryConfig(config) {
1202
1298
  return {
1203
1299
  ignorePathPrefixes: config.discovery?.ignorePathPrefixes ?? [],
1204
- preferPathPrefixes: config.discovery?.preferPathPrefixes ?? []
1300
+ preferPathPrefixes: config.discovery?.preferPathPrefixes ?? [],
1301
+ includeHarnessRoots: config.discovery?.includeHarnessRoots !== false
1205
1302
  };
1206
1303
  }
1207
1304
  function compareDiscoveredSkills(a, b, preferPrefixes) {
1305
+ const typePriority = compareSourceTypePriority(a, b);
1306
+ if (typePriority !== 0) {
1307
+ return typePriority;
1308
+ }
1208
1309
  const rankA = getPreferenceRank(a.sourcePath, preferPrefixes);
1209
1310
  const rankB = getPreferenceRank(b.sourcePath, preferPrefixes);
1210
1311
  if (rankA !== rankB) {
@@ -1212,6 +1313,18 @@ function compareDiscoveredSkills(a, b, preferPrefixes) {
1212
1313
  }
1213
1314
  return a.sourcePath.localeCompare(b.sourcePath);
1214
1315
  }
1316
+ function compareSourceTypePriority(a, b) {
1317
+ return getSourceTypePriority(a.sourceType) - getSourceTypePriority(b.sourceType);
1318
+ }
1319
+ function getSourceTypePriority(sourceType) {
1320
+ if (sourceType === "repo-root") {
1321
+ return 0;
1322
+ }
1323
+ if (sourceType === "nested") {
1324
+ return 1;
1325
+ }
1326
+ return 2;
1327
+ }
1215
1328
  function getPreferenceRank(sourcePath, preferPrefixes) {
1216
1329
  const matchIndex = preferPrefixes.findIndex((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
1217
1330
  return matchIndex === -1 ? Number.MAX_SAFE_INTEGER : matchIndex;
@@ -1221,10 +1334,124 @@ function compareDiagnostics(a, b) {
1221
1334
  `).localeCompare(b.sourcePaths.join(`
1222
1335
  `));
1223
1336
  }
1337
+ function discoverHarnessSkills(harnesses) {
1338
+ const discovered = [];
1339
+ for (const harness of harnesses) {
1340
+ if (!harness.detected) {
1341
+ continue;
1342
+ }
1343
+ let children = [];
1344
+ try {
1345
+ children = readdirSync3(harness.rootPath);
1346
+ } catch {
1347
+ continue;
1348
+ }
1349
+ for (const child of children) {
1350
+ if (shouldIgnoreHarnessSkillName(child)) {
1351
+ continue;
1352
+ }
1353
+ const entryPath = join4(harness.rootPath, child);
1354
+ const resolved = resolveHarnessSkillSource(entryPath);
1355
+ if (!resolved) {
1356
+ continue;
1357
+ }
1358
+ const ownerHarnessId = resolveSourceHarnessId(resolved.sourcePath, harnesses) || harness.id;
1359
+ const ownerHarnessRoot = harnesses.find((candidate) => candidate.id === ownerHarnessId)?.rootPath || harness.rootPath;
1360
+ discovered.push(buildDiscoveredSkill(ownerHarnessRoot, resolved.sourcePath, resolved.sourcePath, resolved.skillFilePath, "harness-root", ownerHarnessId));
1361
+ }
1362
+ }
1363
+ return discovered;
1364
+ }
1365
+ function resolveHarnessSkillSource(entryPath) {
1366
+ const inspection = inspectEntry(entryPath);
1367
+ if (!inspection.exists) {
1368
+ return null;
1369
+ }
1370
+ if (inspection.type === "directory") {
1371
+ const skillFilePath = join4(entryPath, "SKILL.md");
1372
+ if (!existsSync5(skillFilePath)) {
1373
+ return null;
1374
+ }
1375
+ return {
1376
+ sourcePath: resolve4(entryPath),
1377
+ skillFilePath: resolve4(skillFilePath)
1378
+ };
1379
+ }
1380
+ if (inspection.type === "symlink" && inspection.resolvedTarget) {
1381
+ const skillFilePath = join4(inspection.resolvedTarget, "SKILL.md");
1382
+ if (!existsSync5(skillFilePath)) {
1383
+ return null;
1384
+ }
1385
+ return {
1386
+ sourcePath: resolve4(inspection.resolvedTarget),
1387
+ skillFilePath: resolve4(skillFilePath)
1388
+ };
1389
+ }
1390
+ return null;
1391
+ }
1392
+ function shouldIgnoreHarnessSkillName(name) {
1393
+ return name.startsWith(".") || name.includes(".backup-");
1394
+ }
1395
+ function normalizeExistingPath(path) {
1396
+ try {
1397
+ return realpathSync3(path);
1398
+ } catch {
1399
+ return resolve4(path);
1400
+ }
1401
+ }
1402
+ function dedupeEquivalentSources(skills) {
1403
+ const unique = new Map;
1404
+ for (const skill of skills) {
1405
+ const key = `${skill.canonicalSlug}::${skill.sourcePath}`;
1406
+ const existing = unique.get(key);
1407
+ if (!existing || compareEquivalentSourcePreference(skill, existing) < 0) {
1408
+ unique.set(key, skill);
1409
+ }
1410
+ }
1411
+ return [...unique.values()];
1412
+ }
1413
+ function resolveInstallHarnessIds(sourceType, harnessId, frontmatter) {
1414
+ if (frontmatter.skillSyncInstallOn && frontmatter.skillSyncInstallOn.length > 0) {
1415
+ return frontmatter.skillSyncInstallOn;
1416
+ }
1417
+ if (sourceType === "harness-root" && harnessId && frontmatter.skillSyncScope === "local-only") {
1418
+ return [harnessId];
1419
+ }
1420
+ return;
1421
+ }
1422
+ function describeInstallScope(skill) {
1423
+ if (!skill.installHarnessIds || skill.installHarnessIds.length === 0) {
1424
+ return "";
1425
+ }
1426
+ if (skill.sourceType === "harness-root" && skill.harnessId && skill.installHarnessIds.length === 1 && skill.installHarnessIds[0] === skill.harnessId) {
1427
+ return ` [local-only: ${skill.harnessId}]`;
1428
+ }
1429
+ return ` [install-on: ${skill.installHarnessIds.join(", ")}]`;
1430
+ }
1431
+ function resolveSourceHarnessId(sourcePath, harnesses) {
1432
+ return harnesses.filter((harness) => pathOwnsEntry(harness.rootPath, sourcePath)).sort((a, b) => b.rootPath.length - a.rootPath.length || a.id.localeCompare(b.id))[0]?.id;
1433
+ }
1434
+ function compareEquivalentSourcePreference(a, b) {
1435
+ const sourceTypePriority = compareSourceTypePriority(a, b);
1436
+ if (sourceTypePriority !== 0) {
1437
+ return sourceTypePriority;
1438
+ }
1439
+ const ownerPriorityA = a.harnessId ? 0 : 1;
1440
+ const ownerPriorityB = b.harnessId ? 0 : 1;
1441
+ if (ownerPriorityA !== ownerPriorityB) {
1442
+ return ownerPriorityA - ownerPriorityB;
1443
+ }
1444
+ const scopePriorityA = a.installHarnessIds ? 0 : 1;
1445
+ const scopePriorityB = b.installHarnessIds ? 0 : 1;
1446
+ if (scopePriorityA !== scopePriorityB) {
1447
+ return scopePriorityA - scopePriorityB;
1448
+ }
1449
+ return a.sourcePath.localeCompare(b.sourcePath);
1450
+ }
1224
1451
 
1225
1452
  // src/core/sync.ts
1226
1453
  import { join as join5, resolve as resolve5 } from "node:path";
1227
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, symlinkSync as symlinkSync2 } from "node:fs";
1454
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync4, symlinkSync as symlinkSync2 } from "node:fs";
1228
1455
  function buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics) {
1229
1456
  const harnessPlans = harnesses.map((harness) => ({
1230
1457
  harness,
@@ -1238,6 +1465,9 @@ function buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics) {
1238
1465
  desiredByHarness.set(harnessPlan.harness.id, new Set);
1239
1466
  const pathClaims = new Map;
1240
1467
  for (const skill of skills) {
1468
+ if (!shouldInstallOnHarness(skill, harnessPlan.harness.id)) {
1469
+ continue;
1470
+ }
1241
1471
  const installName = resolveInstallName(skill, harnessPlan.harness.id, config);
1242
1472
  const destinationPath = join5(harnessPlan.harness.rootPath, installName);
1243
1473
  const existingClaim = pathClaims.get(destinationPath);
@@ -1293,7 +1523,7 @@ function buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics) {
1293
1523
  const desiredSet = desiredByHarness.get(harnessPlan.harness.id) || new Set;
1294
1524
  let children = [];
1295
1525
  try {
1296
- children = readdirSync3(harnessPlan.harness.rootPath);
1526
+ children = readdirSync4(harnessPlan.harness.rootPath);
1297
1527
  } catch {
1298
1528
  continue;
1299
1529
  }
@@ -1339,7 +1569,7 @@ function buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics) {
1339
1569
  function buildPlannedEntry(skill, harness, installName, destinationPath, state, config) {
1340
1570
  const inspection = inspectEntry(destinationPath);
1341
1571
  const stateEntry = state.managedEntries[destinationPath];
1342
- const sameSource = inspection.type === "symlink" && inspection.resolvedTarget === resolve5(skill.sourcePath);
1572
+ const sameSource = normalizeComparablePath2(destinationPath) === normalizeComparablePath2(skill.sourcePath) || inspection.type === "symlink" && inspection.resolvedTarget === resolve5(skill.sourcePath);
1343
1573
  const compatibility = inspectCompatibility(destinationPath, skill);
1344
1574
  if (!inspection.exists) {
1345
1575
  return makePlannedEntry(skill, harness, installName, destinationPath, "create", "missing entry will be created");
@@ -1348,7 +1578,7 @@ function buildPlannedEntry(skill, harness, installName, destinationPath, state,
1348
1578
  return makePlannedEntry(skill, harness, installName, destinationPath, "ok", "already synced");
1349
1579
  }
1350
1580
  if (compatibility === "matching-skill") {
1351
- return makePlannedEntry(skill, harness, installName, destinationPath, "ok", "compatible existing install");
1581
+ return makePlannedEntry(skill, harness, installName, destinationPath, "repair", "matching install will be replaced with a symlink to the authoritative source");
1352
1582
  }
1353
1583
  if (stateEntry) {
1354
1584
  return makePlannedEntry(skill, harness, installName, destinationPath, "repair", "managed entry drift will be repaired");
@@ -1365,7 +1595,7 @@ function inspectCompatibility(destinationPath, skill) {
1365
1595
  }
1366
1596
  const sourceSkillText = readFileSync4(skill.skillFilePath, "utf8");
1367
1597
  if (inspection.type === "directory") {
1368
- if (readdirSync3(destinationPath).length === 0) {
1598
+ if (readdirSync4(destinationPath).length === 0) {
1369
1599
  return "empty-directory";
1370
1600
  }
1371
1601
  const installedSkillPath = join5(destinationPath, "SKILL.md");
@@ -1385,6 +1615,13 @@ function inspectCompatibility(destinationPath, skill) {
1385
1615
  }
1386
1616
  return "none";
1387
1617
  }
1618
+ function normalizeComparablePath2(path) {
1619
+ try {
1620
+ return realpathSync4(path);
1621
+ } catch {
1622
+ return resolve5(path);
1623
+ }
1624
+ }
1388
1625
  function makePlannedEntry(skill, harness, installName, destinationPath, action, message) {
1389
1626
  return {
1390
1627
  harnessId: harness.id,
@@ -1397,6 +1634,12 @@ function makePlannedEntry(skill, harness, installName, destinationPath, action,
1397
1634
  message
1398
1635
  };
1399
1636
  }
1637
+ function shouldInstallOnHarness(skill, harnessId) {
1638
+ if (!skill.installHarnessIds || skill.installHarnessIds.length === 0) {
1639
+ return true;
1640
+ }
1641
+ return skill.installHarnessIds.includes(harnessId);
1642
+ }
1400
1643
  function resolveInstallName(skill, harnessId, config) {
1401
1644
  const override = config.aliases[skill.sourceKey];
1402
1645
  if (override?.harnesses?.[harnessId]) {
@@ -1460,7 +1703,7 @@ function hasDrift(plan) {
1460
1703
 
1461
1704
  // src/index.ts
1462
1705
  var cli = cac("skill-sync");
1463
- var version = "0.1.1";
1706
+ var version = "0.1.3";
1464
1707
  function normalizeList(value) {
1465
1708
  if (!value) {
1466
1709
  return [];
@@ -1487,10 +1730,34 @@ function print(value, json) {
1487
1730
  }
1488
1731
  console.log(value);
1489
1732
  }
1490
- function renderPlan(plan) {
1733
+ function renderLandingHelp() {
1734
+ return [
1735
+ "skill-sync",
1736
+ "",
1737
+ "High-signal commands:",
1738
+ " skill-sync doctor Inspect sources, drift, and orphan installs",
1739
+ " skill-sync doctor --verbose Show the full per-entry plan",
1740
+ " skill-sync execute Apply symlink updates",
1741
+ " skill-sync sync Alias for execute",
1742
+ " skill-sync sources List discovered source skills",
1743
+ " skill-sync harnesses List detected harness roots",
1744
+ "",
1745
+ "Short alias:",
1746
+ " ss doctor",
1747
+ " ss execute",
1748
+ "",
1749
+ "Safety:",
1750
+ " skill-sync backup create",
1751
+ " skill-sync backup list",
1752
+ "",
1753
+ "Use --help for the full command reference."
1754
+ ].join(`
1755
+ `);
1756
+ }
1757
+ function renderDetailedPlan(plan, options) {
1491
1758
  const lines = [];
1492
1759
  appendSourceDiagnostics(lines, plan.sourceDiagnostics);
1493
- if (plan.orphanSkills && plan.orphanSkills.length > 0) {
1760
+ if (options?.includeOrphans !== false && plan.orphanSkills && plan.orphanSkills.length > 0) {
1494
1761
  if (lines.length > 0) {
1495
1762
  lines.push("");
1496
1763
  }
@@ -1522,6 +1789,98 @@ function renderPlan(plan) {
1522
1789
  return lines.join(`
1523
1790
  `);
1524
1791
  }
1792
+ function renderPlan(plan, options) {
1793
+ if (options.verbose || hasConflicts(plan)) {
1794
+ return renderDetailedPlan(plan, options);
1795
+ }
1796
+ const lines = [];
1797
+ appendSourceDiagnostics(lines, plan.sourceDiagnostics);
1798
+ if (options.includeOrphans !== false && plan.orphanSkills && plan.orphanSkills.length > 0) {
1799
+ if (lines.length > 0) {
1800
+ lines.push("");
1801
+ }
1802
+ const harnessCount = new Set(plan.orphanSkills.map((orphan) => orphan.harnessId)).size;
1803
+ lines.push(`Orphan installed skills: ${plan.orphanSkills.length} detected across ${harnessCount} harness(es)`);
1804
+ lines.push("Run `skill-sync doctor --verbose` to inspect orphan entries.");
1805
+ }
1806
+ const counts = countPlanActions(plan);
1807
+ if (lines.length > 0) {
1808
+ lines.push("");
1809
+ }
1810
+ lines.push(`Summary: ${plan.ok} ok, ${plan.changes} change(s), ${plan.conflicts} conflict(s)`);
1811
+ lines.push(`Actions: ${Object.entries(counts).map(([action, count]) => `${action}=${count}`).join(", ")}`);
1812
+ const harnessLines = summarizeHarnessPlans(plan);
1813
+ if (harnessLines.length > 0) {
1814
+ lines.push("");
1815
+ lines.push("Harness changes:");
1816
+ lines.push(...harnessLines);
1817
+ }
1818
+ return lines.join(`
1819
+ `);
1820
+ }
1821
+ function summarizeHarnessPlans(plan) {
1822
+ const lines = [];
1823
+ for (const harnessPlan of plan.harnesses) {
1824
+ const interestingEntries = harnessPlan.entries.filter((entry) => entry.action !== "ok");
1825
+ if (interestingEntries.length === 0) {
1826
+ continue;
1827
+ }
1828
+ const counts = {};
1829
+ for (const entry of interestingEntries) {
1830
+ counts[entry.action] = (counts[entry.action] || 0) + 1;
1831
+ }
1832
+ lines.push(`- ${harnessPlan.harness.id}: ${Object.entries(counts).map(([action, count]) => `${action}=${count}`).join(", ")}`);
1833
+ }
1834
+ return lines;
1835
+ }
1836
+ function renderDoctorReport(plan, state, skills, harnessCount, verbose) {
1837
+ if (verbose || hasConflicts(plan)) {
1838
+ return renderDetailedPlan(plan, { includeOrphans: true });
1839
+ }
1840
+ const totalExpectedInstalls = plan.harnesses.flatMap((harness) => harness.entries).filter((entry) => entry.action !== "remove-managed" && entry.action !== "prune-state").length;
1841
+ const trackedExpectedInstalls = plan.harnesses.flatMap((harness) => harness.entries).filter((entry) => entry.action !== "conflict" && Boolean(state.managedEntries[entry.destinationPath])).length;
1842
+ const okButUntracked = plan.harnesses.flatMap((harness) => harness.entries).filter((entry) => entry.action === "ok" && !state.managedEntries[entry.destinationPath]).length;
1843
+ const compatibleCopies = plan.harnesses.flatMap((harness) => harness.entries).filter((entry) => entry.message === "matching install will be replaced with a symlink to the authoritative source").length;
1844
+ const lines = [];
1845
+ appendSourceDiagnostics(lines, plan.sourceDiagnostics);
1846
+ if (lines.length > 0) {
1847
+ lines.push("");
1848
+ }
1849
+ lines.push("Doctor");
1850
+ lines.push(`Sources: ${skills.length} discovered skill source(s)`);
1851
+ const scopedSources = skills.filter((skill) => skill.installHarnessIds && skill.installHarnessIds.length > 0).length;
1852
+ if (scopedSources > 0) {
1853
+ lines.push(`Scope: ${skills.length - scopedSources} global, ${scopedSources} scoped`);
1854
+ }
1855
+ lines.push(`Harnesses: ${harnessCount} detected/enabled root(s)`);
1856
+ lines.push(`Expected installs: ${totalExpectedInstalls}`);
1857
+ lines.push(`State: ${trackedExpectedInstalls} tracked, ${okButUntracked} ok-but-untracked`);
1858
+ lines.push(`Sync: ${plan.changes} change(s), ${plan.conflicts} conflict(s), ${plan.ok} ok`);
1859
+ if (compatibleCopies > 0) {
1860
+ lines.push(`Copies: ${compatibleCopies} matching install(s) still need conversion from copied content to authoritative symlinks`);
1861
+ }
1862
+ if (plan.orphanSkills && plan.orphanSkills.length > 0) {
1863
+ const groupedOrphans = new Map;
1864
+ for (const orphan of plan.orphanSkills) {
1865
+ groupedOrphans.set(orphan.harnessId, (groupedOrphans.get(orphan.harnessId) || 0) + 1);
1866
+ }
1867
+ const topHarnesses = [...groupedOrphans.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([harnessId, count]) => `${harnessId}=${count}`).join(", ");
1868
+ lines.push(`Orphans: ${plan.orphanSkills.length} installed skill(s) exist outside the discovered source set`);
1869
+ lines.push(`Top orphan roots: ${topHarnesses}`);
1870
+ lines.push("Diagnosis: project-root skills are syncing correctly. The remaining orphans are typically slug mismatches, backup artifacts, or installed entries that do not yet map to a canonical source.");
1871
+ lines.push("Run `skill-sync doctor --verbose` to inspect orphan entries.");
1872
+ } else {
1873
+ lines.push("Orphans: 0");
1874
+ }
1875
+ const harnessLines = summarizeHarnessPlans(plan);
1876
+ if (harnessLines.length > 0) {
1877
+ lines.push("");
1878
+ lines.push("Harness changes:");
1879
+ lines.push(...harnessLines);
1880
+ }
1881
+ return lines.join(`
1882
+ `);
1883
+ }
1525
1884
  function appendSourceDiagnostics(lines, sourceDiagnostics) {
1526
1885
  if (sourceDiagnostics.errors.length === 0 && sourceDiagnostics.warnings.length === 0) {
1527
1886
  return;
@@ -1557,39 +1916,58 @@ function planSync(options) {
1557
1916
  return withRuntime(options, (runtime) => {
1558
1917
  const config = loadConfig(runtime);
1559
1918
  config.projectsRoots = resolveProjectsOverride(config.projectsRoots, options);
1560
- const harnesses = resolveSelectedHarnesses(resolveHarnesses(runtime.homeDir, config), options);
1561
- const { skills, sourceDiagnostics } = discoverSkillSet(config);
1919
+ const allHarnesses = resolveHarnesses(runtime.homeDir, config).filter((harness) => harness.enabled);
1920
+ const harnesses = resolveSelectedHarnesses(allHarnesses, options);
1921
+ const { skills, sourceDiagnostics } = discoverSkillSet(config, allHarnesses);
1562
1922
  const state = loadState(runtime);
1563
1923
  const plan = buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics);
1564
- return { runtime, plan, harnesses, skillsCount: skills.length };
1924
+ return { runtime, plan, harnesses, skills, state };
1565
1925
  });
1566
1926
  }
1567
- function printCheckResult(plan, json) {
1568
- print(json ? plan : renderPlan(plan), json);
1927
+ function printDoctorResult(plan, options, state, skills, harnessCount) {
1928
+ print(options.json ? {
1929
+ ...plan,
1930
+ summary: {
1931
+ sourcesDiscovered: skills.length,
1932
+ scopedSources: skills.filter((skill) => skill.installHarnessIds && skill.installHarnessIds.length > 0).length,
1933
+ harnessesDetected: harnessCount,
1934
+ expectedInstalls: plan.harnesses.flatMap((harness) => harness.entries).filter((entry) => entry.action !== "remove-managed" && entry.action !== "prune-state").length,
1935
+ changes: plan.changes,
1936
+ conflicts: plan.conflicts,
1937
+ ok: plan.ok,
1938
+ orphans: plan.orphanSkills?.length || 0
1939
+ }
1940
+ } : renderDoctorReport(plan, state, skills, harnessCount, options.verbose), Boolean(options.json));
1569
1941
  process.exit(hasConflicts(plan) ? 3 : hasDrift(plan) ? 2 : 0);
1570
1942
  }
1571
- cli.command("check", "Show drift without changing anything").option("--json", "Output JSON").option("--dry-run", "Accepted for parity; check is always read-only").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1572
- const { plan } = planSync(options);
1573
- printCheckResult(plan, Boolean(options.json));
1943
+ cli.command("doctor", "Inspect current sources, drift, and orphan installs").option("--json", "Output JSON").option("--dry-run", "Accepted for parity; check is always read-only").option("--verbose", "Show detailed plan output").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1944
+ const { plan, state, skills, harnesses } = planSync(options);
1945
+ printDoctorResult(plan, options, state, skills, harnesses.length);
1946
+ });
1947
+ cli.command("check", "Alias for doctor").option("--json", "Output JSON").option("--dry-run", "Accepted for parity; check is always read-only").option("--verbose", "Show detailed plan output").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1948
+ const { plan, state, skills, harnesses } = planSync(options);
1949
+ printDoctorResult(plan, options, state, skills, harnesses.length);
1574
1950
  });
1575
- cli.command("sync", "Apply the desired symlink state").option("--json", "Output JSON").option("--dry-run", "Show changes without mutating").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1576
- const { runtime, plan } = planSync(options);
1951
+ function runExecute(options) {
1952
+ const { runtime, plan, state } = planSync(options);
1577
1953
  if (hasConflicts(plan)) {
1578
- print(options.json ? plan : renderPlan(plan), Boolean(options.json));
1954
+ print(options.json ? plan : renderPlan(plan, { verbose: true, includeOrphans: true }), Boolean(options.json));
1579
1955
  process.exit(3);
1580
1956
  }
1581
- const state = loadState(runtime);
1582
1957
  const nextState = applySyncPlan(plan, state, Boolean(options.dryRun));
1583
1958
  if (!options.dryRun) {
1584
1959
  saveState(runtime, nextState);
1585
1960
  }
1586
- print(options.json ? plan : renderPlan(plan), Boolean(options.json));
1587
- });
1961
+ print(options.json ? plan : renderPlan(plan, { verbose: options.verbose, includeOrphans: false }), Boolean(options.json));
1962
+ }
1963
+ cli.command("execute", "Apply the desired symlink state").option("--json", "Output JSON").option("--dry-run", "Show changes without mutating").option("--verbose", "Show detailed plan output").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action(runExecute);
1964
+ cli.command("sync", "Alias for execute").option("--json", "Output JSON").option("--dry-run", "Show changes without mutating").option("--verbose", "Show detailed plan output").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action(runExecute);
1588
1965
  cli.command("sources", "List discovered source skills").option("--json", "Output JSON").option("--projects-root <path>", "Override configured projects root").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1589
1966
  withRuntime(options, (runtime) => {
1590
1967
  const config = loadConfig(runtime);
1591
1968
  config.projectsRoots = resolveProjectsOverride(config.projectsRoots, options);
1592
- const { skills, sourceDiagnostics } = discoverSkillSet(config);
1969
+ const harnesses = resolveHarnesses(runtime.homeDir, config).filter((harness) => harness.enabled);
1970
+ const { skills, sourceDiagnostics } = discoverSkillSet(config, harnesses);
1593
1971
  if (options.json) {
1594
1972
  print({ skills, sourceDiagnostics }, true);
1595
1973
  return;
@@ -1742,22 +2120,16 @@ cli.help();
1742
2120
  cli.version(version);
1743
2121
  cli.option("--json", "Output JSON");
1744
2122
  cli.option("--dry-run", "Show changes without mutating");
2123
+ cli.option("--verbose", "Show detailed plan output");
1745
2124
  cli.option("--projects-root <path>", "Override configured projects root");
1746
2125
  cli.option("--harness <id>", "Filter to one or more harness ids");
1747
2126
  cli.option("--home <path>", "Override HOME for skill-sync state and harness resolution");
1748
2127
  var rawArgs = process.argv.slice(2);
1749
2128
  cli.parse();
1750
- var shouldRunDefaultSync = !rawArgs.includes("--help") && !rawArgs.includes("-h") && !rawArgs.includes("--version") && !rawArgs.includes("-v") && !cli.matchedCommand;
2129
+ var shouldRunDefaultSync = rawArgs.length > 0 && !rawArgs.includes("--help") && !rawArgs.includes("-h") && !rawArgs.includes("--version") && !rawArgs.includes("-v") && !cli.matchedCommand;
1751
2130
  if (shouldRunDefaultSync) {
1752
- const options = cli.options;
1753
- const { runtime, plan } = planSync(options);
1754
- if (hasConflicts(plan)) {
1755
- console.log(renderPlan(plan));
1756
- process.exit(3);
1757
- }
1758
- const nextState = applySyncPlan(plan, loadState(runtime), Boolean(options.dryRun));
1759
- if (!options.dryRun) {
1760
- saveState(runtime, nextState);
1761
- }
1762
- console.log(renderPlan(plan));
2131
+ print(renderLandingHelp(), false);
2132
+ }
2133
+ if (rawArgs.length === 0) {
2134
+ print(renderLandingHelp(), false);
1763
2135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@light-merlin-dark/skill-sync",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Sync local repo-backed agent skills across Codex, Claude Code, Cursor, Gemini, Hermes, and other harnesses.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",