@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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/bin.js +176 -18
  3. 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";
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
- let targets = opts.targets?.length ? opts.targets : cfg.targets;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ichintansoni/skills-master",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Install tool-agnostic Apple & Android development skills into any AI coding tool (Claude Code, Cursor, Copilot, AGENTS.md).",
5
5
  "keywords": [
6
6
  "skills",