@ichintansoni/skills-master 0.1.7 → 0.1.8
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/README.md +3 -0
- package/dist/bin.js +176 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,13 @@ npx @ichintansoni/skills-master list --class code # browse the catalo
|
|
|
14
14
|
npx @ichintansoni/skills-master search navigation
|
|
15
15
|
npx @ichintansoni/skills-master update # pull newer skill versions
|
|
16
16
|
npx @ichintansoni/skills-master remove swiftui-sheets
|
|
17
|
+
npx @ichintansoni/skills-master sync # re-emit to match the current config
|
|
17
18
|
npx @ichintansoni/skills-master status # what's installed, and has it drifted?
|
|
18
19
|
npx @ichintansoni/skills-master doctor # same detection, but fails on drift
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
`update` pulls newer skill content, but only ever re-emits to the targets a skill was *already* installed to. So adding a target to `skills-master.json` after the fact, or changing a `paths` override, leaves config and disk disagreeing with nothing to reconcile them. `sync` is that reconciliation — it treats the configured target set as the source of truth and makes disk match. Local edits are preserved unless you pass `--overwrite`. A target you *dropped* from config is reported but only deleted with `--prune`; a `paths` change is a move, so the old copy is cleaned up straight away rather than left for agents to load twice.
|
|
23
|
+
|
|
21
24
|
`status` and `doctor` share their drift detection but answer different questions. `status` is an inventory — it lists every installed skill with its version, targets, and whether the output has been edited or deleted, works entirely offline, and always exits 0, so it is safe to pipe (`status --json`, `status --problems`, `status <name>…`). `doctor` is the gate: same findings, but it exits non-zero, which is what you want in CI.
|
|
22
25
|
|
|
23
26
|
`add` writes, per detected tool:
|
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
|
-
var version = "0.1.
|
|
7
|
+
var version = "0.1.8";
|
|
8
8
|
|
|
9
9
|
// src/schema/projectConfig.ts
|
|
10
10
|
import { z } from "zod";
|
|
@@ -1037,13 +1037,31 @@ function ensureGitignored(root, entries) {
|
|
|
1037
1037
|
writeFileSync3(p, next, "utf8");
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
+
// src/util/targets.ts
|
|
1041
|
+
var VALID = new Set(ALL_TARGETS);
|
|
1042
|
+
function resolveTargets(cwd, configured, explicit) {
|
|
1043
|
+
if (explicit?.length) return explicit;
|
|
1044
|
+
if (configured.length) return configured;
|
|
1045
|
+
const detected = detectTargets(cwd);
|
|
1046
|
+
return detected.length ? detected : ALL_TARGETS;
|
|
1047
|
+
}
|
|
1048
|
+
function parseTargets(value) {
|
|
1049
|
+
if (!value) return void 0;
|
|
1050
|
+
if (value === "all") return ALL_TARGETS;
|
|
1051
|
+
const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1052
|
+
for (const id of ids) {
|
|
1053
|
+
if (!VALID.has(id)) {
|
|
1054
|
+
throw new Error(`Unknown target "${id}". Valid: ${[...VALID, "all"].join(", ")}.`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return ids;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1040
1060
|
// src/commands/add.ts
|
|
1041
1061
|
async function addCommand(opts) {
|
|
1042
1062
|
const cfg = loadConfigOrDefault(opts.cwd);
|
|
1043
1063
|
const hadConfig = loadConfig(opts.cwd) != null;
|
|
1044
|
-
|
|
1045
|
-
if (!targets.length) targets = detectTargets(opts.cwd);
|
|
1046
|
-
if (!targets.length) targets = ALL_TARGETS;
|
|
1064
|
+
const targets = resolveTargets(opts.cwd, cfg.targets, opts.targets);
|
|
1047
1065
|
const content = await resolveContent({
|
|
1048
1066
|
content: opts.content,
|
|
1049
1067
|
ref: opts.ref ?? cfg.contentRef,
|
|
@@ -1378,6 +1396,147 @@ function statusCommand(opts) {
|
|
|
1378
1396
|
return report;
|
|
1379
1397
|
}
|
|
1380
1398
|
|
|
1399
|
+
// src/commands/sync.ts
|
|
1400
|
+
async function syncCommand(opts) {
|
|
1401
|
+
const cfg = loadConfigOrDefault(opts.cwd);
|
|
1402
|
+
const lock = loadLockfile(opts.cwd);
|
|
1403
|
+
const targets = resolveTargets(opts.cwd, cfg.targets);
|
|
1404
|
+
const paths = resolvePaths(cfg);
|
|
1405
|
+
const prefix = opts.dryRun ? "[dry-run] " : "";
|
|
1406
|
+
const all = Object.keys(lock.skills).sort();
|
|
1407
|
+
const names = opts.names?.length ? all.filter((n) => opts.names.includes(n)) : all;
|
|
1408
|
+
const result = {
|
|
1409
|
+
synced: [],
|
|
1410
|
+
skipped: [],
|
|
1411
|
+
addedTargets: [],
|
|
1412
|
+
orphaned: [],
|
|
1413
|
+
stale: [],
|
|
1414
|
+
pruned: false
|
|
1415
|
+
};
|
|
1416
|
+
if (all.length === 0) {
|
|
1417
|
+
log.info("No skills installed \u2014 run `skills-master add <name>`.");
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1420
|
+
if (names.length === 0) {
|
|
1421
|
+
log.warn("No installed skills match.");
|
|
1422
|
+
return result;
|
|
1423
|
+
}
|
|
1424
|
+
const edited = new Map(
|
|
1425
|
+
diagnoseInstalled(opts.cwd, lock).map((d) => [d.name, d.targets.some((t) => t.edited)])
|
|
1426
|
+
);
|
|
1427
|
+
const content = await resolveContent({
|
|
1428
|
+
content: opts.content,
|
|
1429
|
+
ref: opts.ref ?? cfg.contentRef,
|
|
1430
|
+
cwd: opts.cwd
|
|
1431
|
+
});
|
|
1432
|
+
log.info(`Syncing ${names.length} skill(s) to targets: ${targets.join(", ")}`);
|
|
1433
|
+
const addedTargets = /* @__PURE__ */ new Set();
|
|
1434
|
+
const pruneNames = [];
|
|
1435
|
+
const pruneTargets = /* @__PURE__ */ new Set();
|
|
1436
|
+
for (const name of names) {
|
|
1437
|
+
const locked = lock.skills[name];
|
|
1438
|
+
const installedTo = Object.keys(locked.emitted);
|
|
1439
|
+
const orphans = installedTo.filter((t) => !targets.includes(t));
|
|
1440
|
+
if (orphans.length) {
|
|
1441
|
+
result.orphaned.push({ name, targets: orphans });
|
|
1442
|
+
pruneNames.push(name);
|
|
1443
|
+
for (const t of orphans) pruneTargets.add(t);
|
|
1444
|
+
}
|
|
1445
|
+
if (edited.get(name) && !opts.overwrite) {
|
|
1446
|
+
log.warn(`${prefix}"${name}" has local edits \u2014 skipping (use --overwrite to replace).`);
|
|
1447
|
+
result.skipped.push(name);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
let skill;
|
|
1451
|
+
try {
|
|
1452
|
+
skill = content.loadSkill(name);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (err instanceof SkillNotFoundError) {
|
|
1455
|
+
log.warn(`"${name}" no longer exists in the content library \u2014 leaving it in place.`);
|
|
1456
|
+
} else {
|
|
1457
|
+
log.error(`Failed to load "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1458
|
+
}
|
|
1459
|
+
result.skipped.push(name);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
for (const t of targets) if (!locked.emitted[t]) addedTargets.add(t);
|
|
1463
|
+
const prevFiles = /* @__PURE__ */ new Set();
|
|
1464
|
+
const prevBlocks = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const t of targets) {
|
|
1466
|
+
const e = locked.emitted[t];
|
|
1467
|
+
if (!e) continue;
|
|
1468
|
+
for (const f of e.files) prevFiles.add(f);
|
|
1469
|
+
if (e.block) prevBlocks.set(t, e.block);
|
|
1470
|
+
}
|
|
1471
|
+
const emitted = installSkill(opts.cwd, skill, targets, paths, {
|
|
1472
|
+
dryRun: opts.dryRun,
|
|
1473
|
+
overwrite: true
|
|
1474
|
+
// edits were already checked above
|
|
1475
|
+
});
|
|
1476
|
+
const nowFiles = /* @__PURE__ */ new Set();
|
|
1477
|
+
for (const e of Object.values(emitted.locked.emitted)) {
|
|
1478
|
+
for (const f of e.files) nowFiles.add(f);
|
|
1479
|
+
}
|
|
1480
|
+
const staleFiles = [...prevFiles].filter((f) => !nowFiles.has(f));
|
|
1481
|
+
const staleBlocks = [...prevBlocks].filter(([t, b]) => emitted.locked.emitted[t]?.block !== b).map(([, b]) => b);
|
|
1482
|
+
if (staleFiles.length || staleBlocks.length) {
|
|
1483
|
+
result.stale.push({ name, files: staleFiles, blocks: staleBlocks });
|
|
1484
|
+
}
|
|
1485
|
+
if (!opts.dryRun) {
|
|
1486
|
+
lock.skills[name] = {
|
|
1487
|
+
...emitted.locked,
|
|
1488
|
+
emitted: { ...locked.emitted, ...emitted.locked.emitted }
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
result.synced.push(name);
|
|
1492
|
+
for (const r of emitted.results) {
|
|
1493
|
+
if (r.action === "unchanged") continue;
|
|
1494
|
+
const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
|
|
1495
|
+
log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
result.addedTargets = [...addedTargets].sort();
|
|
1499
|
+
if (!opts.dryRun) saveLockfile(opts.cwd, lock);
|
|
1500
|
+
if (result.orphaned.length) {
|
|
1501
|
+
const list = [...pruneTargets].sort().join(", ");
|
|
1502
|
+
if (opts.prune) {
|
|
1503
|
+
removeCommand({
|
|
1504
|
+
cwd: opts.cwd,
|
|
1505
|
+
names: pruneNames,
|
|
1506
|
+
targets: [...pruneTargets],
|
|
1507
|
+
dryRun: opts.dryRun
|
|
1508
|
+
});
|
|
1509
|
+
result.pruned = true;
|
|
1510
|
+
} else {
|
|
1511
|
+
log.warn(
|
|
1512
|
+
`${result.orphaned.length} skill(s) still have output for target(s) the config no longer lists: ${list}. Re-run with --prune to remove.`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (result.stale.length) {
|
|
1517
|
+
const gone = [];
|
|
1518
|
+
for (const f of result.stale.flatMap((s) => s.files)) {
|
|
1519
|
+
if (removeWholeFile(opts.cwd, f, opts.dryRun)) {
|
|
1520
|
+
gone.push(f);
|
|
1521
|
+
log.info(`${prefix}moved-from ${f}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
pruneEmptyDirs(opts.cwd, gone, opts.dryRun);
|
|
1525
|
+
for (const s of result.stale) {
|
|
1526
|
+
for (const b of s.blocks) {
|
|
1527
|
+
if (removeBlockFromFile(opts.cwd, b, s.name, opts.dryRun)) {
|
|
1528
|
+
log.info(`${prefix}unblocked ${b} [${s.name}]`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const bits = [`${result.synced.length} synced`];
|
|
1534
|
+
if (result.addedTargets.length) bits.push(`new target(s): ${result.addedTargets.join(", ")}`);
|
|
1535
|
+
if (result.skipped.length) bits.push(`${result.skipped.length} skipped`);
|
|
1536
|
+
log.success(`${prefix}${bits.join(", ")}.`);
|
|
1537
|
+
return result;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1381
1540
|
// src/core/search-text.ts
|
|
1382
1541
|
function searchNormalize(value) {
|
|
1383
1542
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
@@ -1922,20 +2081,6 @@ async function newSkillCommand(opts) {
|
|
|
1922
2081
|
return skillMd;
|
|
1923
2082
|
}
|
|
1924
2083
|
|
|
1925
|
-
// src/util/targets.ts
|
|
1926
|
-
var VALID = new Set(ALL_TARGETS);
|
|
1927
|
-
function parseTargets(value) {
|
|
1928
|
-
if (!value) return void 0;
|
|
1929
|
-
if (value === "all") return ALL_TARGETS;
|
|
1930
|
-
const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1931
|
-
for (const id of ids) {
|
|
1932
|
-
if (!VALID.has(id)) {
|
|
1933
|
-
throw new Error(`Unknown target "${id}". Valid: ${[...VALID, "all"].join(", ")}.`);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
return ids;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
2084
|
// src/bin.ts
|
|
1940
2085
|
async function run(fn, exitOnFalse = false) {
|
|
1941
2086
|
try {
|
|
@@ -2023,6 +2168,19 @@ program.command("remove <names...>").description("Remove installed skills").opti
|
|
|
2023
2168
|
})
|
|
2024
2169
|
)
|
|
2025
2170
|
);
|
|
2171
|
+
program.command("sync").description("Re-emit installed skills to match the current config (new targets, moved paths)").argument("[names...]", "limit the sync to these installed skills").option("--content <dir>", "local skills directory").option("--ref <git-ref>", "content ref to read from").option("--overwrite", "replace locally edited outputs").option("--prune", "delete outputs for targets the config no longer lists").option("--dry-run", "show what would change without writing").action(
|
|
2172
|
+
(names, opts) => run(
|
|
2173
|
+
() => syncCommand({
|
|
2174
|
+
cwd: process.cwd(),
|
|
2175
|
+
names,
|
|
2176
|
+
content: opts.content,
|
|
2177
|
+
ref: opts.ref,
|
|
2178
|
+
overwrite: opts.overwrite,
|
|
2179
|
+
prune: opts.prune,
|
|
2180
|
+
dryRun: opts.dryRun
|
|
2181
|
+
})
|
|
2182
|
+
)
|
|
2183
|
+
);
|
|
2026
2184
|
program.command("status").description("Show installed skills: versions, targets, and local-edit state").argument("[names...]", "limit the report to these skills").option("--problems", "show only skills that are edited or missing files").option("--json", "machine-readable output").action(
|
|
2027
2185
|
(names, opts) => run(
|
|
2028
2186
|
() => statusCommand({
|
package/package.json
CHANGED