@skill-map/cli 0.24.5 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -748,19 +748,19 @@ var annotationsExtractor = {
748
748
  pluginId: "core",
749
749
  kind: "extractor",
750
750
  version: "1.0.0",
751
- description: "Turns the `supersedes`, `requires`, `related`, `conflictsWith`, and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
751
+ description: "Turns the `supersedes` and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
752
752
  stability: "stable",
753
- emitsLinkKinds: ["supersedes", "references"],
753
+ emitsLinkKinds: ["supersedes"],
754
754
  defaultConfidence: "high",
755
755
  scope: "frontmatter",
756
756
  extract(ctx) {
757
757
  const sourcePath = ctx.node.path;
758
758
  const seen = /* @__PURE__ */ new Set();
759
- function emit(source, target, kind) {
760
- const key = `${source} ${target} ${kind}`;
759
+ function emit(source, target) {
760
+ const key = `${source} ${target}`;
761
761
  if (seen.has(key)) return;
762
762
  seen.add(key);
763
- ctx.emitLink(link(source, target, kind));
763
+ ctx.emitLink(link(source, target));
764
764
  }
765
765
  const ann = pickAnnotations(ctx.node);
766
766
  if (ann) processBlock(ann, sourcePath, emit);
@@ -768,20 +768,11 @@ var annotationsExtractor = {
768
768
  };
769
769
  function processBlock(block, sourcePath, emit) {
770
770
  for (const target of stringArray(block["supersedes"])) {
771
- emit(sourcePath, target, "supersedes");
771
+ emit(sourcePath, target);
772
772
  }
773
773
  const supersededBy = block["supersededBy"];
774
774
  if (typeof supersededBy === "string" && supersededBy.length > 0) {
775
- emit(supersededBy, sourcePath, "supersedes");
776
- }
777
- for (const target of stringArray(block["requires"])) {
778
- emit(sourcePath, target, "references");
779
- }
780
- for (const target of stringArray(block["related"])) {
781
- emit(sourcePath, target, "references");
782
- }
783
- for (const target of stringArray(block["conflictsWith"])) {
784
- emit(sourcePath, target, "references");
775
+ emit(supersededBy, sourcePath);
785
776
  }
786
777
  }
787
778
  function pickAnnotations(node) {
@@ -797,11 +788,11 @@ function stringArray(value) {
797
788
  if (!Array.isArray(value)) return [];
798
789
  return value.filter((v) => typeof v === "string" && v.length > 0);
799
790
  }
800
- function link(source, target, kind) {
791
+ function link(source, target) {
801
792
  return {
802
793
  source,
803
794
  target,
804
- kind,
795
+ kind: "supersedes",
805
796
  confidence: "high",
806
797
  sources: [ID]
807
798
  };
@@ -2318,7 +2309,13 @@ var VALIDATE_ALL_TEXTS = {
2318
2309
  /** `Node <path> failed schema validation: <errors>` */
2319
2310
  nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
2320
2311
  /** `Link <source> → <target> failed schema validation: <errors>` */
2321
- linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}"
2312
+ linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}",
2313
+ /** `Node <path> is missing required frontmatter fields: <missing>` */
2314
+ frontmatterBaseFailure: "Node {{path}} is missing required frontmatter fields: {{missing}}.",
2315
+ /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
2316
+ alertTooltipSingle: "Frontmatter or schema validation failed.",
2317
+ /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
2318
+ alertTooltipMany: "{{count}} schema validation issues on this node."
2322
2319
  };
2323
2320
 
2324
2321
  // built-in-plugins/analyzers/validate-all/index.ts
@@ -2331,15 +2328,56 @@ var validateAllAnalyzer = {
2331
2328
  description: "Detects and flags nodes or links violating the project schemas.",
2332
2329
  stability: "stable",
2333
2330
  mode: "deterministic",
2331
+ viewContributions: {
2332
+ // Corner badge on the graph card; surfaces when the node body /
2333
+ // frontmatter fails schema validation (parse error, missing
2334
+ // `name`/`description`, malformed YAML, etc.). Same visual
2335
+ // chassis as `core/broken-ref`, danger severity.
2336
+ alert: {
2337
+ slot: "graph.node.alert",
2338
+ icon: "fa-solid fa-triangle-exclamation",
2339
+ emitWhenEmpty: false
2340
+ },
2341
+ // Footer chip that mirrors the corner alert with the actual
2342
+ // count so the operator can scan the cards and prioritise.
2343
+ // Outlined (vs the filled corner alert) per the broken-ref
2344
+ // pattern: two beats of the same signal.
2345
+ chip: {
2346
+ slot: "card.footer.right",
2347
+ icon: "fa-regular fa-triangle-exclamation",
2348
+ emitWhenEmpty: false,
2349
+ priority: 35
2350
+ }
2351
+ },
2334
2352
  evaluate(ctx) {
2335
2353
  const validators = loadSchemaValidators();
2336
2354
  const findings = [];
2355
+ const perNode = /* @__PURE__ */ new Map();
2337
2356
  for (const node of ctx.nodes) {
2357
+ const before = findings.length;
2338
2358
  collectNodeFindings(validators, node, findings);
2359
+ collectFrontmatterBaseFindings(node, findings);
2360
+ if (findings.length > before) {
2361
+ perNode.set(node.path, (perNode.get(node.path) ?? 0) + (findings.length - before));
2362
+ }
2339
2363
  }
2340
2364
  for (const link2 of ctx.links) {
2341
2365
  collectLinkFindings(validators, link2, findings);
2342
2366
  }
2367
+ for (const [nodePath, count] of perNode) {
2368
+ const tooltip = count === 1 ? VALIDATE_ALL_TEXTS.alertTooltipSingle : tx(VALIDATE_ALL_TEXTS.alertTooltipMany, { count });
2369
+ const capped = Math.min(count, 99);
2370
+ ctx.emitContribution(nodePath, "alert", {
2371
+ icon: "fa-solid fa-triangle-exclamation",
2372
+ severity: "danger",
2373
+ tooltip
2374
+ });
2375
+ ctx.emitContribution(nodePath, "chip", {
2376
+ value: capped,
2377
+ severity: "danger",
2378
+ tooltip
2379
+ });
2380
+ }
2343
2381
  return findings;
2344
2382
  }
2345
2383
  };
@@ -2357,6 +2395,33 @@ function collectNodeFindings(v, node, out) {
2357
2395
  data: { target: "node", path: node.path }
2358
2396
  });
2359
2397
  }
2398
+ function collectFrontmatterBaseFindings(node, out) {
2399
+ if (node.provider === "markdown") return;
2400
+ if (node.bytes.frontmatter === 0) return;
2401
+ const fm = node.frontmatter ?? {};
2402
+ const missing = [];
2403
+ if (isMissingStringField(fm, "name")) missing.push("name");
2404
+ if (isMissingStringField(fm, "description")) missing.push("description");
2405
+ if (missing.length === 0) return;
2406
+ out.push({
2407
+ analyzerId: ID19,
2408
+ // `warn` (not `error`) so the default `sm scan` exit code stays
2409
+ // 0 even when nodes are missing frontmatter base fields. Strict
2410
+ // mode (`sm scan --strict`) still escalates to exit 1. Matches
2411
+ // the `frontmatter-invalid` severity policy of the orchestrator.
2412
+ severity: "warn",
2413
+ nodeIds: [node.path],
2414
+ message: tx(VALIDATE_ALL_TEXTS.frontmatterBaseFailure, {
2415
+ path: node.path,
2416
+ missing: missing.join(", ")
2417
+ }),
2418
+ data: { target: "frontmatter", path: node.path, missing }
2419
+ });
2420
+ }
2421
+ function isMissingStringField(fm, field) {
2422
+ const v = fm[field];
2423
+ return typeof v !== "string" || v.length === 0;
2424
+ }
2360
2425
  function collectLinkFindings(v, link2, out) {
2361
2426
  const result = v.validate("link", toLinkForSchema(link2));
2362
2427
  if (result.ok) return;
@@ -2401,6 +2466,54 @@ function toLinkForSchema(link2) {
2401
2466
  };
2402
2467
  }
2403
2468
 
2469
+ // kernel/util/trigger-resolve.ts
2470
+ function buildNameIndex(nodes) {
2471
+ const out = /* @__PURE__ */ new Map();
2472
+ indexByCanonicalName(nodes, out);
2473
+ fillIndexWithPathBasename(nodes, out);
2474
+ return out;
2475
+ }
2476
+ function indexByCanonicalName(nodes, out) {
2477
+ for (const node of nodes) {
2478
+ const raw = canonicalName(node);
2479
+ if (raw === null) continue;
2480
+ const key = normalizeTrigger(raw);
2481
+ if (!out.has(key)) out.set(key, node.path);
2482
+ }
2483
+ }
2484
+ function fillIndexWithPathBasename(nodes, out) {
2485
+ for (const node of nodes) {
2486
+ if (canonicalName(node) !== null) continue;
2487
+ const derived = pathBasenameForLink(node.path);
2488
+ if (derived.length === 0) continue;
2489
+ const key = normalizeTrigger(derived);
2490
+ if (!out.has(key)) out.set(key, node.path);
2491
+ }
2492
+ }
2493
+ function canonicalName(node) {
2494
+ const raw = node.frontmatter?.["name"];
2495
+ if (typeof raw !== "string" || raw.length === 0) return null;
2496
+ return raw;
2497
+ }
2498
+ function pathBasenameForLink(path) {
2499
+ const segments = path.split("/").filter((s) => s.length > 0);
2500
+ if (segments.length === 0) return path;
2501
+ const last = segments[segments.length - 1];
2502
+ if (last === "SKILL.md" && segments.length >= 2) {
2503
+ return segments[segments.length - 2];
2504
+ }
2505
+ return last.replace(/\.md$/, "");
2506
+ }
2507
+ function resolveLinkTargetToPath(link2, nameIndex) {
2508
+ const raw = link2.target;
2509
+ const sigil = raw.charAt(0);
2510
+ if (sigil !== "/" && sigil !== "@") return raw;
2511
+ const normalizedTrigger = link2.trigger?.normalizedTrigger;
2512
+ const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
2513
+ const resolved = nameIndex.get(normalized);
2514
+ return resolved ?? raw;
2515
+ }
2516
+
2404
2517
  // built-in-plugins/analyzers/link-counts/index.ts
2405
2518
  var ID20 = "link-counts";
2406
2519
  var linkCountsAnalyzer = {
@@ -2428,10 +2541,12 @@ var linkCountsAnalyzer = {
2428
2541
  }
2429
2542
  },
2430
2543
  evaluate(ctx) {
2544
+ const nameIndex = buildNameIndex(ctx.nodes);
2431
2545
  const perTarget = /* @__PURE__ */ new Map();
2432
2546
  const perSource = /* @__PURE__ */ new Map();
2433
2547
  for (const link2 of ctx.links) {
2434
- bump(perTarget, link2.target, link2.kind);
2548
+ const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
2549
+ bump(perTarget, resolvedTarget, link2.kind);
2435
2550
  bump(perSource, link2.source, link2.kind);
2436
2551
  }
2437
2552
  for (const node of ctx.nodes) {
@@ -4614,8 +4729,8 @@ async function saveUpdateCheckCache(db, cache) {
4614
4729
  var SORT_BY_COLUMNS = /* @__PURE__ */ new Set([
4615
4730
  "path",
4616
4731
  "kind",
4617
- "bytes_total",
4618
- "bytesTotal",
4732
+ "tokens_total",
4733
+ "tokensTotal",
4619
4734
  "links_out_count",
4620
4735
  "linksOutCount",
4621
4736
  "links_in_count",
@@ -4626,7 +4741,7 @@ var SORT_BY_COLUMNS = /* @__PURE__ */ new Set([
4626
4741
  var SORT_BY_DEFAULT_DIRECTION = {
4627
4742
  path: "asc",
4628
4743
  kind: "asc",
4629
- bytesTotal: "desc",
4744
+ tokensTotal: "desc",
4630
4745
  linksOutCount: "desc",
4631
4746
  linksInCount: "desc",
4632
4747
  externalRefsCount: "desc"
@@ -5747,7 +5862,7 @@ var UPDATE_CHECK_TEXTS = {
5747
5862
  // package.json
5748
5863
  var package_default = {
5749
5864
  name: "@skill-map/cli",
5750
- version: "0.24.5",
5865
+ version: "0.26.0",
5751
5866
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
5752
5867
  license: "MIT",
5753
5868
  type: "module",
@@ -6705,16 +6820,25 @@ var BUMP_TEXTS = {
6705
6820
  // --- failures -------------------------------------------------------------
6706
6821
  bumpFailed: "{{glyph}} sm bump: {{message}}\n",
6707
6822
  storeFailedDetail: "sidecar write failed for {{path}}: {{message}}",
6708
- resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}",
6823
+ resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}"
6709
6824
  // --- .sm consent gate ---------------------------------------------------
6825
+ // The shared strings live in `consent.texts.ts` (CONSENT_TEXTS); they
6826
+ // are used by every verb that writes a sidecar (`sm bump`,
6827
+ // `sm sidecar refresh`, `sm sidecar annotate`) with a `{{verb}}`
6828
+ // placeholder for the directed prefix.
6829
+ };
6830
+
6831
+ // cli/i18n/consent.texts.ts
6832
+ var CONSENT_TEXTS = {
6710
6833
  /**
6711
6834
  * Pre-prompt context shown before the interactive `confirm()` so the
6712
- * operator sees what they are about to opt into. `.skill-map/settings.local.json`
6713
- * is gitignored, the choice is saved per-checkout, never travels via the repo.
6835
+ * operator sees what they are about to opt into.
6836
+ * `.skill-map/settings.local.json` is gitignored, the choice is saved
6837
+ * per-checkout, never travels via the repo.
6714
6838
  */
6715
- consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
6716
- consentAborted: "{{glyph}} sm bump: aborted by user. No .sm sidecar files were written.\n",
6717
- consentRequiredNonTty: "{{glyph}} sm bump: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
6839
+ consentPrompt: "{{glyph}} skill-map needs consent to create .sm sidecar files next to your\n .md sources. Your choice is saved to .skill-map/settings.local.json\n (gitignored) and this prompt will not appear again.\n\nAllow .sm sidecar writes in this project?",
6840
+ consentAborted: "{{glyph}} {{verb}}: aborted by user. No .sm sidecar files were written.\n",
6841
+ consentRequiredNonTty: "{{glyph}} {{verb}}: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
6718
6842
  consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
6719
6843
  };
6720
6844
 
@@ -7112,21 +7236,25 @@ var BumpCommand = class extends SmCommand {
7112
7236
  const isTTY = stdin.isTTY === true;
7113
7237
  if (!isTTY || this.yes) {
7114
7238
  this.printer.error(
7115
- tx(BUMP_TEXTS.consentRequiredNonTty, {
7239
+ tx(CONSENT_TEXTS.consentRequiredNonTty, {
7116
7240
  glyph: ansi.red("\u2715"),
7117
- hint: ansi.dim(BUMP_TEXTS.consentRequiredNonTtyHint)
7241
+ verb: "sm bump",
7242
+ hint: ansi.dim(CONSENT_TEXTS.consentRequiredNonTtyHint)
7118
7243
  })
7119
7244
  );
7120
7245
  return ExitCode.Error;
7121
7246
  }
7122
7247
  const ok = await confirm(
7123
- BUMP_TEXTS.consentPrompt,
7248
+ tx(CONSENT_TEXTS.consentPrompt, { glyph: ansi.cyan("\u2139") }),
7124
7249
  { stdin, stderr },
7125
7250
  { defaultAnswer: "yes" }
7126
7251
  );
7127
7252
  if (!ok) {
7128
7253
  this.printer.info(
7129
- tx(BUMP_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
7254
+ tx(CONSENT_TEXTS.consentAborted, {
7255
+ glyph: ansi.cyan("\u2139"),
7256
+ verb: "sm bump"
7257
+ })
7130
7258
  );
7131
7259
  return ExitCode.Error;
7132
7260
  }
@@ -12502,6 +12630,26 @@ function validateLink(extractor, link2, emitter) {
12502
12630
  const confidence = link2.confidence ?? extractor.defaultConfidence;
12503
12631
  return { ...link2, confidence };
12504
12632
  }
12633
+ function dedupeLinks(links) {
12634
+ const out = /* @__PURE__ */ new Map();
12635
+ for (const link2 of links) {
12636
+ const trigger = link2.trigger?.normalizedTrigger ?? "";
12637
+ const key = `${link2.source}\0${link2.target}\0${link2.kind}\0${trigger}`;
12638
+ const existing = out.get(key);
12639
+ if (existing) {
12640
+ const seen = new Set(existing.sources);
12641
+ for (const src of link2.sources) {
12642
+ if (!seen.has(src)) {
12643
+ seen.add(src);
12644
+ existing.sources = [...existing.sources, src];
12645
+ }
12646
+ }
12647
+ continue;
12648
+ }
12649
+ out.set(key, link2);
12650
+ }
12651
+ return [...out.values()];
12652
+ }
12505
12653
  function recomputeLinkCounts(nodes, links) {
12506
12654
  const byPath3 = /* @__PURE__ */ new Map();
12507
12655
  for (const node of nodes) {
@@ -13492,6 +13640,7 @@ async function runScanInternal(_kernel, options) {
13492
13640
  providerFrontmatter: setup.providerFrontmatter,
13493
13641
  pluginStores: options.pluginStores
13494
13642
  });
13643
+ walked.internalLinks = dedupeLinks(walked.internalLinks);
13495
13644
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
13496
13645
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
13497
13646
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -15209,7 +15358,7 @@ var LIST_TEXTS = {
15209
15358
  tableHeaderIn: "IN",
15210
15359
  tableHeaderExt: "EXT",
15211
15360
  tableHeaderIssues: "ISSUES",
15212
- tableHeaderBytes: "BYTES",
15361
+ tableHeaderTokens: "TOKENS",
15213
15362
  /** Footer line: count of rendered nodes (`3 nodes` / `1 node`). */
15214
15363
  tableFooterCount: "{{count}} {{noun}}\n",
15215
15364
  tableFooterNounSingular: "node",
@@ -15222,7 +15371,7 @@ var LIST_TEXTS = {
15222
15371
  var SORT_BY = {
15223
15372
  path: { column: "path", direction: "asc" },
15224
15373
  kind: { column: "kind", direction: "asc" },
15225
- bytes_total: { column: "bytesTotal", direction: "desc" },
15374
+ tokens_total: { column: "tokensTotal", direction: "desc" },
15226
15375
  links_out_count: { column: "linksOutCount", direction: "desc" },
15227
15376
  links_in_count: { column: "linksInCount", direction: "desc" },
15228
15377
  external_refs_count: { column: "externalRefsCount", direction: "desc" }
@@ -15241,7 +15390,7 @@ var ListCommand = class extends SmCommand {
15241
15390
  and sidecar.annotations.tags by default; --tag-source author|user
15242
15391
  narrows to one side).
15243
15392
 
15244
- --sort-by accepts: path, kind, bytes_total, links_out_count,
15393
+ --sort-by accepts: path, kind, tokens_total, links_out_count,
15245
15394
  links_in_count, external_refs_count. Default: path. --limit N caps
15246
15395
  the result; default is no limit.
15247
15396
 
@@ -15250,7 +15399,7 @@ var ListCommand = class extends SmCommand {
15250
15399
  examples: [
15251
15400
  ["List every node", "$0 list"],
15252
15401
  ["List only agents", "$0 list --kind agent"],
15253
- ["Top 5 by total bytes", "$0 list --sort-by bytes_total --limit 5"],
15402
+ ["Top 5 by total tokens", "$0 list --sort-by tokens_total --limit 5"],
15254
15403
  ["Only nodes with issues, machine-readable", "$0 list --issue --json"],
15255
15404
  ["Filter by tag (author or user surfaces)", "$0 list --tag urgent"],
15256
15405
  ["Filter by user-only tag", "$0 list --tag wip --tag-source user"]
@@ -15393,7 +15542,7 @@ function renderTable2(nodes, issuesByNode, ansi) {
15393
15542
  in: n.linksInCount,
15394
15543
  ext: n.externalRefsCount,
15395
15544
  issues: issuesByNode.get(n.path) ?? 0,
15396
- bytes: n.bytes.total
15545
+ tokens: n.tokens?.total ?? null
15397
15546
  }));
15398
15547
  const widths = computeWidths(rows);
15399
15548
  const lines = [];
@@ -15421,9 +15570,12 @@ function computeWidths(rows) {
15421
15570
  in: Math.max(headerLen(LIST_TEXTS.tableHeaderIn), ...rows.map((r) => String(r.in).length)),
15422
15571
  ext: Math.max(headerLen(LIST_TEXTS.tableHeaderExt), ...rows.map((r) => String(r.ext).length)),
15423
15572
  issues: Math.max(headerLen(LIST_TEXTS.tableHeaderIssues), ...rows.map((r) => String(r.issues).length)),
15424
- bytes: Math.max(headerLen(LIST_TEXTS.tableHeaderBytes), ...rows.map((r) => String(r.bytes).length))
15573
+ tokens: Math.max(headerLen(LIST_TEXTS.tableHeaderTokens), ...rows.map((r) => formatTokens(r.tokens).length))
15425
15574
  };
15426
15575
  }
15576
+ function formatTokens(value) {
15577
+ return value === null ? "-" : String(value);
15578
+ }
15427
15579
  function clampMax(value, max) {
15428
15580
  return value > max ? max : value;
15429
15581
  }
@@ -15436,7 +15588,7 @@ function formatHeaderRow(w, ansi) {
15436
15588
  ansi.dim(LIST_TEXTS.tableHeaderIn.padStart(w.in)),
15437
15589
  ansi.dim(LIST_TEXTS.tableHeaderExt.padStart(w.ext)),
15438
15590
  ansi.dim(LIST_TEXTS.tableHeaderIssues.padStart(w.issues)),
15439
- ansi.dim(LIST_TEXTS.tableHeaderBytes.padStart(w.bytes))
15591
+ ansi.dim(LIST_TEXTS.tableHeaderTokens.padStart(w.tokens))
15440
15592
  ].join(" ");
15441
15593
  }
15442
15594
  function formatDataRow(r, w, ansi) {
@@ -15449,7 +15601,7 @@ function formatDataRow(r, w, ansi) {
15449
15601
  String(r.in).padStart(w.in),
15450
15602
  String(r.ext).padStart(w.ext),
15451
15603
  issuesCol,
15452
- String(r.bytes).padStart(w.bytes)
15604
+ formatTokens(r.tokens).padStart(w.tokens)
15453
15605
  ].join(" ");
15454
15606
  }
15455
15607
 
@@ -22308,7 +22460,7 @@ var SHOW_TEXTS = {
22308
22460
  /** Tail appended to `nodeHeader` when provider differs from kind. */
22309
22461
  providerSuffix: " {{label}}",
22310
22462
  providerSuffixLabel: "provider: {{provider}}",
22311
- // --- field block (Title / Description / Bytes / Tokens / …) ---------
22463
+ // --- field block (Title / Description / Tokens / …) -----------------
22312
22464
  /** Field row, label padded by the renderer to align values. */
22313
22465
  fieldRow: " {{label}} {{value}}\n",
22314
22466
  /** Continuation indent for multi-line values (description, etc.). */
@@ -22317,7 +22469,6 @@ var SHOW_TEXTS = {
22317
22469
  fieldLabelDescription: "Description",
22318
22470
  fieldLabelStability: "Stability",
22319
22471
  fieldLabelVersion: "Version",
22320
- fieldLabelBytes: "Bytes",
22321
22472
  fieldLabelTokens: "Tokens",
22322
22473
  fieldLabelExternalRefs: "External refs",
22323
22474
  /** `{{total}} total · {{frontmatter}} frontmatter · {{body}} body`. */
@@ -22431,23 +22582,13 @@ function collectNodeFields(node) {
22431
22582
  fields.push({ label: SHOW_TEXTS.fieldLabelVersion, value: sanitizeForTerminal(String(projected.version)) });
22432
22583
  }
22433
22584
  fields.push({
22434
- label: SHOW_TEXTS.fieldLabelBytes,
22435
- value: tx(SHOW_TEXTS.weightSplit, {
22436
- total: node.bytes.total,
22437
- frontmatter: node.bytes.frontmatter,
22438
- body: node.bytes.body
22439
- })
22585
+ label: SHOW_TEXTS.fieldLabelTokens,
22586
+ value: node.tokens ? tx(SHOW_TEXTS.weightSplit, {
22587
+ total: node.tokens.total,
22588
+ frontmatter: node.tokens.frontmatter,
22589
+ body: node.tokens.body
22590
+ }) : "-"
22440
22591
  });
22441
- if (node.tokens) {
22442
- fields.push({
22443
- label: SHOW_TEXTS.fieldLabelTokens,
22444
- value: tx(SHOW_TEXTS.weightSplit, {
22445
- total: node.tokens.total,
22446
- frontmatter: node.tokens.frontmatter,
22447
- body: node.tokens.body
22448
- })
22449
- });
22450
- }
22451
22592
  fields.push({ label: SHOW_TEXTS.fieldLabelExternalRefs, value: String(node.externalRefsCount) });
22452
22593
  return fields;
22453
22594
  }
@@ -22631,17 +22772,12 @@ var SIDECAR_TEXTS = {
22631
22772
  annotateCreated: "{{glyph}} Created {{sidecarPath}}. Edit it, then run `sm bump {{nodePath}}` to commit the version.\n",
22632
22773
  /** Trailing dim tag for sidecar prune dry-run (matches the orphans pattern). */
22633
22774
  sidecarDryRunTag: " (no changes made)",
22634
- annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n",
22635
- // --- .sm consent gate (shared across refresh + annotate) -----------------
22636
- /**
22637
- * Pre-prompt context shown before the interactive `confirm()` so the
22638
- * operator sees what they are about to opt into. `.skill-map/settings.local.json`
22639
- * is gitignored, the choice is saved per-checkout, never travels via the repo.
22640
- */
22641
- consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
22642
- consentAborted: "{{glyph}} sm sidecar: aborted by user. No .sm sidecar files were written.\n",
22643
- consentRequiredNonTty: "{{glyph}} sm sidecar: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
22644
- consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
22775
+ annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n"
22776
+ // --- .sm consent gate ---------------------------------------------------
22777
+ // The shared strings live in `consent.texts.ts` (CONSENT_TEXTS); they
22778
+ // are used by every verb that writes a sidecar (`sm bump`,
22779
+ // `sm sidecar refresh`, `sm sidecar annotate`) with a `{{verb}}`
22780
+ // placeholder for the directed prefix.
22645
22781
  };
22646
22782
 
22647
22783
  // cli/commands/sidecar.ts
@@ -22654,21 +22790,25 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
22654
22790
  if (!isTTY || bag.yes) {
22655
22791
  const errGlyph = ansi.red("\u2715");
22656
22792
  bag.printError(
22657
- tx(SIDECAR_TEXTS.consentRequiredNonTty, {
22793
+ tx(CONSENT_TEXTS.consentRequiredNonTty, {
22658
22794
  glyph: errGlyph,
22659
- hint: ansi.dim(SIDECAR_TEXTS.consentRequiredNonTtyHint)
22795
+ verb: "sm sidecar",
22796
+ hint: ansi.dim(CONSENT_TEXTS.consentRequiredNonTtyHint)
22660
22797
  })
22661
22798
  );
22662
22799
  return ExitCode.Error;
22663
22800
  }
22664
22801
  const ok = await confirm(
22665
- SIDECAR_TEXTS.consentPrompt,
22802
+ tx(CONSENT_TEXTS.consentPrompt, { glyph: ansi.cyan("\u2139") }),
22666
22803
  { stdin: bag.stdin, stderr: bag.stderr },
22667
22804
  { defaultAnswer: "yes" }
22668
22805
  );
22669
22806
  if (!ok) {
22670
22807
  bag.printInfo(
22671
- tx(SIDECAR_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
22808
+ tx(CONSENT_TEXTS.consentAborted, {
22809
+ glyph: ansi.cyan("\u2139"),
22810
+ verb: "sm sidecar"
22811
+ })
22672
22812
  );
22673
22813
  return ExitCode.Error;
22674
22814
  }
@@ -23291,7 +23431,7 @@ import { Command as Command37, Option as Option35 } from "clipanion";
23291
23431
 
23292
23432
  // cli/i18n/tutorial.texts.ts
23293
23433
  var TUTORIAL_TEXTS = {
23294
- // Success, written to stdout after `<cwd>/sm-tutorial.md` is created.
23434
+ // Success, written to stdout after `<cwd>/{{filename}}` is created.
23295
23435
  // Multi-line layout: the two trigger phrases (English / Spanish) are
23296
23436
  // indented and labelled so they're the most visible part of the
23297
23437
  // output. The reminder above them surfaces the SKILL's language
@@ -23303,55 +23443,91 @@ var TUTORIAL_TEXTS = {
23303
23443
  * `English` / `Español` labels print dim, the eye lands on the
23304
23444
  * trigger phrases the user is going to copy / paste.
23305
23445
  */
23306
- written: " {{glyph}} sm-tutorial.md created at {{cwd}}\n\n Open Claude Code in this directory. Your first message sets\n the tutorial language for the rest of the session:\n\n {{enLabel}} run @sm-tutorial.md\n {{esLabel}} ejecut\xE1 @sm-tutorial.md\n",
23446
+ written: " {{glyph}} {{filename}} created at {{cwd}}\n\n Open Claude Code in this directory. Your first message sets\n the tutorial language for the rest of the session:\n\n {{enLabel}} run @{{filename}}\n {{esLabel}} ejecut\xE1 @{{filename}}\n",
23307
23447
  writtenLabelEn: "English",
23308
23448
  writtenLabelEs: "Espa\xF1ol",
23309
- // Refusal, `sm-tutorial.md` already exists and `--force` was not set.
23449
+ // Refusal, `{{filename}}` already exists and `--force` was not set.
23310
23450
  // Goes to stderr, exit code 2 (operational error per spec § Exit codes).
23311
23451
  // Mirrors the success body shape: glyph + headline, then a dim hint
23312
23452
  // line spelling the fix.
23313
- alreadyExists: "{{glyph}} sm-tutorial.md already exists at {{cwd}}\n {{hint}}\n",
23453
+ alreadyExists: "{{glyph}} {{filename}} already exists at {{cwd}}\n {{hint}}\n",
23314
23454
  alreadyExistsHint: "Pass `--force` to overwrite.",
23455
+ // Invalid `variant` positional argument. Goes to stderr, exit code 2.
23456
+ // Mirrors `alreadyExists`: glyph + headline + dim hint enumerating the
23457
+ // valid values.
23458
+ invalidVariant: "{{glyph}} sm tutorial: unknown variant '{{variant}}'\n {{hint}}\n",
23459
+ invalidVariantHint: "Valid values: tutorial (default), master.",
23315
23460
  // I/O failure on write or on reading the bundled SKILL source.
23316
- writeFailed: "{{glyph}} sm tutorial: failed to write sm-tutorial.md: {{message}}\n",
23317
- sourceMissing: "{{glyph}} sm tutorial: could not read the bundled tutorial (SKILL.md) from the install.\n {{hint}}\n",
23461
+ writeFailed: "{{glyph}} sm tutorial: failed to write {{filename}}: {{message}}\n",
23462
+ sourceMissing: "{{glyph}} sm tutorial: could not read the bundled tutorial ({{filename}}) from the install.\n {{hint}}\n",
23318
23463
  sourceMissingHint: "Reinstall @skill-map/cli or report the bug."
23319
23464
  };
23320
23465
 
23321
23466
  // cli/commands/tutorial.ts
23322
- var SM_TUTORIAL_FILENAME = "sm-tutorial.md";
23467
+ var VALID_VARIANTS = ["tutorial", "master"];
23468
+ var DEFAULT_VARIANT = "tutorial";
23469
+ var VARIANT_SPECS = {
23470
+ tutorial: {
23471
+ filename: "sm-tutorial.md",
23472
+ sourcePath: ".claude/skills/sm-tutorial/SKILL.md",
23473
+ bundledName: "sm-tutorial.md"
23474
+ },
23475
+ master: {
23476
+ filename: "sm-master.md",
23477
+ sourcePath: ".claude/skills/sm-master/SKILL.md",
23478
+ bundledName: "sm-master.md"
23479
+ }
23480
+ };
23323
23481
  var TutorialCommand = class extends SmCommand {
23324
23482
  static paths = [["tutorial"]];
23325
23483
  static usage = Command37.Usage({
23326
23484
  category: "Setup",
23327
- description: "Materialize the interactive tester tutorial (sm-tutorial.md) in the current directory.",
23485
+ description: "Materialize an interactive tester tutorial (sm-tutorial.md or sm-master.md) in the current directory.",
23328
23486
  details: `
23329
- Drops the canonical SKILL.md content as ./sm-tutorial.md so a tester
23330
- can open Claude Code in the cwd and load the file as a skill by
23331
- typing "ejecut\xE1 @sm-tutorial.md". Top-level only; no subdirectory
23332
- is created.
23487
+ Drops the canonical SKILL.md content as ./sm-tutorial.md (default)
23488
+ or ./sm-master.md (when invoked as \`sm tutorial master\`) so a
23489
+ tester can open Claude Code in the cwd and load the file as a
23490
+ skill by typing "ejecut\xE1 @sm-tutorial.md" (or "@sm-master.md").
23491
+ Top-level only; no subdirectory is created.
23333
23492
 
23334
23493
  Does NOT require an initialized .skill-map/ project. Refuses to
23335
- overwrite an existing sm-tutorial.md unless --force is passed.
23494
+ overwrite the target file unless --force is passed. Valid values
23495
+ for the positional argument are: tutorial (default), master.
23336
23496
  `,
23337
23497
  examples: [
23338
- ["Materialize the tutorial in the cwd", "$0 tutorial"],
23339
- ["Overwrite an existing sm-tutorial.md", "$0 tutorial --force"]
23498
+ ["Materialize the basic tutorial in the cwd", "$0 tutorial"],
23499
+ ["Materialize the advanced tutorial in the cwd", "$0 tutorial master"],
23500
+ ["Overwrite an existing target file", "$0 tutorial --force"]
23340
23501
  ]
23341
23502
  });
23503
+ variant = Option35.String({ required: false });
23342
23504
  force = Option35.Boolean("--force", false, {
23343
- description: "Overwrite an existing sm-tutorial.md without prompting."
23505
+ description: "Overwrite an existing target file without prompting."
23344
23506
  });
23345
23507
  async run() {
23346
23508
  const ctx = defaultRuntimeContext();
23347
- const target = join17(ctx.cwd, SM_TUTORIAL_FILENAME);
23348
23509
  const stderr = this.context.stderr;
23349
23510
  const stderrAnsi = this.ansiFor("stderr");
23350
23511
  const errGlyph = stderrAnsi.red("\u2715");
23512
+ const rawVariant = this.variant;
23513
+ if (rawVariant !== void 0 && !isTutorialVariant(rawVariant)) {
23514
+ this.printer.error(
23515
+ tx(TUTORIAL_TEXTS.invalidVariant, {
23516
+ glyph: errGlyph,
23517
+ variant: rawVariant,
23518
+ hint: stderrAnsi.dim(TUTORIAL_TEXTS.invalidVariantHint)
23519
+ })
23520
+ );
23521
+ return ExitCode.Error;
23522
+ }
23523
+ const variant = rawVariant ?? DEFAULT_VARIANT;
23524
+ const spec = VARIANT_SPECS[variant];
23525
+ const target = join17(ctx.cwd, spec.filename);
23351
23526
  if (await pathExists(target) && !this.force) {
23352
23527
  this.printer.error(
23353
23528
  tx(TUTORIAL_TEXTS.alreadyExists, {
23354
23529
  glyph: errGlyph,
23530
+ filename: spec.filename,
23355
23531
  cwd: stderrAnsi.dim(displayCwd(ctx.cwd)),
23356
23532
  hint: stderrAnsi.dim(TUTORIAL_TEXTS.alreadyExistsHint)
23357
23533
  })
@@ -23360,11 +23536,12 @@ var TutorialCommand = class extends SmCommand {
23360
23536
  }
23361
23537
  let body;
23362
23538
  try {
23363
- body = loadBundledTutorialText();
23539
+ body = loadBundledTutorialText(variant);
23364
23540
  } catch {
23365
23541
  this.printer.error(
23366
23542
  tx(TUTORIAL_TEXTS.sourceMissing, {
23367
23543
  glyph: errGlyph,
23544
+ filename: spec.filename,
23368
23545
  hint: stderrAnsi.dim(TUTORIAL_TEXTS.sourceMissingHint)
23369
23546
  })
23370
23547
  );
@@ -23376,6 +23553,7 @@ var TutorialCommand = class extends SmCommand {
23376
23553
  this.printer.error(
23377
23554
  tx(TUTORIAL_TEXTS.writeFailed, {
23378
23555
  glyph: errGlyph,
23556
+ filename: spec.filename,
23379
23557
  message: formatErrorMessage(err)
23380
23558
  })
23381
23559
  );
@@ -23391,6 +23569,7 @@ var TutorialCommand = class extends SmCommand {
23391
23569
  this.printer.data(
23392
23570
  tx(TUTORIAL_TEXTS.written, {
23393
23571
  glyph: ansi.green("\u2713"),
23572
+ filename: spec.filename,
23394
23573
  cwd: ansi.dim(displayCwd(ctx.cwd)),
23395
23574
  enLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEn),
23396
23575
  esLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEs)
@@ -23399,26 +23578,32 @@ var TutorialCommand = class extends SmCommand {
23399
23578
  return ExitCode.Ok;
23400
23579
  }
23401
23580
  };
23581
+ function isTutorialVariant(value) {
23582
+ return VALID_VARIANTS.includes(value);
23583
+ }
23402
23584
  function displayCwd(cwd) {
23403
23585
  const segments = cwd.split("/").filter((s) => s.length > 0);
23404
23586
  if (segments.length === 0) return "./";
23405
23587
  return `./${segments[segments.length - 1]}/`;
23406
23588
  }
23407
- var cachedTutorial = null;
23408
- function loadBundledTutorialText() {
23409
- if (cachedTutorial !== null) return cachedTutorial;
23410
- cachedTutorial = readTutorialFromDisk();
23411
- return cachedTutorial;
23589
+ var cachedTutorials = /* @__PURE__ */ new Map();
23590
+ function loadBundledTutorialText(variant) {
23591
+ const cached = cachedTutorials.get(variant);
23592
+ if (cached !== void 0) return cached;
23593
+ const body = readTutorialFromDisk(variant);
23594
+ cachedTutorials.set(variant, body);
23595
+ return body;
23412
23596
  }
23413
- function readTutorialFromDisk() {
23597
+ function readTutorialFromDisk(variant) {
23598
+ const spec = VARIANT_SPECS[variant];
23414
23599
  const here = dirname18(fileURLToPath6(import.meta.url));
23415
23600
  const candidates = [
23416
- // dev: src/cli/commands/ → repo-root .claude/skills/sm-tutorial/SKILL.md
23417
- resolve36(here, "../../../.claude/skills/sm-tutorial/SKILL.md"),
23418
- // bundled: dist/cli.js → dist/cli/tutorial/sm-tutorial.md (sibling)
23419
- resolve36(here, "cli/tutorial/sm-tutorial.md"),
23420
- // bundled fallback: any-depth → cli/tutorial/sm-tutorial.md
23421
- resolve36(here, "../cli/tutorial/sm-tutorial.md")
23601
+ // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/SKILL.md
23602
+ resolve36(here, "../../..", spec.sourcePath),
23603
+ // bundled: dist/cli.js → dist/cli/tutorial/<filename> (sibling)
23604
+ resolve36(here, "cli/tutorial", spec.bundledName),
23605
+ // bundled fallback: any-depth → cli/tutorial/<filename>
23606
+ resolve36(here, "../cli/tutorial", spec.bundledName)
23422
23607
  ];
23423
23608
  for (const candidate of candidates) {
23424
23609
  if (existsSync28(candidate)) {