@skill-map/cli 0.29.0 → 0.31.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
@@ -775,6 +775,53 @@ function link(source, target) {
775
775
  };
776
776
  }
777
777
 
778
+ // kernel/util/strip-code-blocks.ts
779
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
780
+ function stripCodeBlocks(input) {
781
+ if (!input) return input;
782
+ const fenceless = stripFences(input);
783
+ return stripInline(fenceless);
784
+ }
785
+ function stripFences(input) {
786
+ const out = [];
787
+ const lines = input.split("\n");
788
+ let openFence = null;
789
+ for (const line of lines) {
790
+ if (openFence) {
791
+ const closer = matchClosingFence(line, openFence);
792
+ if (closer) {
793
+ out.push(blank(line));
794
+ openFence = null;
795
+ } else {
796
+ out.push(blank(line));
797
+ }
798
+ continue;
799
+ }
800
+ const open = FENCE_RE.exec(line);
801
+ if (open?.groups) {
802
+ openFence = open.groups["fence"];
803
+ out.push(blank(line));
804
+ continue;
805
+ }
806
+ out.push(line);
807
+ }
808
+ return out.join("\n");
809
+ }
810
+ function matchClosingFence(line, openFence) {
811
+ const m = FENCE_RE.exec(line);
812
+ if (!m?.groups) return false;
813
+ const fence = m.groups["fence"];
814
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
815
+ }
816
+ function stripInline(input) {
817
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
818
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
819
+ });
820
+ }
821
+ function blank(s) {
822
+ return s.replace(/[^\s]/g, " ");
823
+ }
824
+
778
825
  // kernel/trigger-normalize.ts
779
826
  function normalizeTrigger(source) {
780
827
  let out = source.normalize("NFD");
@@ -787,21 +834,43 @@ function normalizeTrigger(source) {
787
834
 
788
835
  // plugins/core/extractors/at-directive/index.ts
789
836
  var ID2 = "at-directive";
790
- var AT_RE = /(?:^|[^A-Za-z0-9_@])(@[a-z0-9][a-z0-9_-]*(?:[/:][a-z0-9][a-z0-9_-]*)?)/gi;
837
+ var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
838
+ var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
791
839
  var atDirectiveExtractor = {
792
840
  id: ID2,
793
841
  pluginId: "core",
794
842
  kind: "extractor",
795
843
  version: "1.0.0",
796
- description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow between nodes in the graph.",
844
+ description: "Detects `@<token>` directives in a node's body. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link, matching how Claude Code / Gemini CLI / Cursor read the same syntax.",
797
845
  scope: "body",
798
846
  extract(ctx) {
799
- const seen = /* @__PURE__ */ new Set();
800
- for (const match of ctx.body.matchAll(AT_RE)) {
847
+ const seenMentions = /* @__PURE__ */ new Set();
848
+ const seenReferences = /* @__PURE__ */ new Set();
849
+ const body = stripCodeBlocks(ctx.body);
850
+ for (const match of body.matchAll(AT_RE)) {
801
851
  const original = match[1];
852
+ const bare = original.slice(1);
853
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
854
+ if (isReference) {
855
+ const target = bare.replace(/^\.\//, "");
856
+ if (seenReferences.has(target)) continue;
857
+ seenReferences.add(target);
858
+ ctx.emitLink({
859
+ source: ctx.node.path,
860
+ target,
861
+ kind: "references",
862
+ confidence: "medium",
863
+ sources: [ID2],
864
+ trigger: {
865
+ originalTrigger: original,
866
+ normalizedTrigger: target.toLowerCase()
867
+ }
868
+ });
869
+ continue;
870
+ }
802
871
  const normalized = normalizeTrigger(original);
803
- if (seen.has(normalized)) continue;
804
- seen.add(normalized);
872
+ if (seenMentions.has(normalized)) continue;
873
+ seenMentions.add(normalized);
805
874
  ctx.emitLink({
806
875
  source: ctx.node.path,
807
876
  target: original,
@@ -993,8 +1062,12 @@ var slashExtractor = {
993
1062
  scope: "body",
994
1063
  extract(ctx) {
995
1064
  const seen = /* @__PURE__ */ new Set();
996
- for (const match of ctx.body.matchAll(SLASH_RE)) {
1065
+ const body = stripCodeBlocks(ctx.body);
1066
+ for (const match of body.matchAll(SLASH_RE)) {
997
1067
  const original = match[1];
1068
+ const endIdx = (match.index ?? 0) + match[0].length;
1069
+ const nextChar = body[endIdx];
1070
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
998
1071
  const normalized = normalizeTrigger(original);
999
1072
  if (seen.has(normalized)) continue;
1000
1073
  seen.add(normalized);
@@ -1176,7 +1249,7 @@ function tooltipFor(status) {
1176
1249
  }
1177
1250
 
1178
1251
  // plugins/core/analyzers/broken-ref/index.ts
1179
- import { resolve } from "path";
1252
+ import { posix as pathPosix2, resolve } from "path";
1180
1253
 
1181
1254
  // plugins/core/analyzers/broken-ref/text.ts
1182
1255
  var BROKEN_REF_TEXTS = {
@@ -1185,7 +1258,13 @@ var BROKEN_REF_TEXTS = {
1185
1258
  // Tooltips for the per-node view-contribution badges. Singular vs
1186
1259
  // plural keeps the count grammar correct without a sub-template.
1187
1260
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
1188
- alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
1261
+ alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
1262
+ // Fix-summary copy when the broken trigger has a same-named file on
1263
+ // disk that does not advertise `name:` in its frontmatter. Two
1264
+ // variants for single vs multiple candidates; same template family
1265
+ // as the alert tooltips above.
1266
+ hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
1267
+ hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
1189
1268
  };
1190
1269
 
1191
1270
  // plugins/core/analyzers/broken-ref/index.ts
@@ -1225,13 +1304,15 @@ var brokenRefAnalyzer = {
1225
1304
  evaluate(ctx) {
1226
1305
  const byPath3 = new Set(ctx.nodes.map((n) => n.path));
1227
1306
  const byNormalizedName = indexByNormalizedName(ctx.nodes);
1307
+ const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
1228
1308
  const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
1229
1309
  const issues = [];
1230
1310
  const perNode = /* @__PURE__ */ new Map();
1231
1311
  for (const link2 of ctx.links) {
1232
1312
  if (isResolved(link2, byPath3, byNormalizedName)) continue;
1233
1313
  if (refIndex && resolvesViaReferencePaths(link2, refIndex)) continue;
1234
- issues.push(buildIssue(link2));
1314
+ const candidates = findHintCandidates(link2, byBasenameWithoutName);
1315
+ issues.push(buildIssue(link2, candidates));
1235
1316
  perNode.set(link2.source, (perNode.get(link2.source) ?? 0) + 1);
1236
1317
  }
1237
1318
  for (const [nodePath, count] of perNode) {
@@ -1251,8 +1332,13 @@ var brokenRefAnalyzer = {
1251
1332
  return issues;
1252
1333
  }
1253
1334
  };
1254
- function buildIssue(link2) {
1255
- return {
1335
+ function buildIssue(link2, hintCandidates = []) {
1336
+ const data = {
1337
+ target: link2.target,
1338
+ kind: link2.kind,
1339
+ trigger: link2.trigger?.normalizedTrigger ?? null
1340
+ };
1341
+ const issue = {
1256
1342
  analyzerId: ID9,
1257
1343
  severity: "warn",
1258
1344
  nodeIds: [link2.source],
@@ -1261,12 +1347,28 @@ function buildIssue(link2) {
1261
1347
  source: link2.source,
1262
1348
  target: link2.target
1263
1349
  }),
1264
- data: {
1265
- target: link2.target,
1266
- kind: link2.kind,
1267
- trigger: link2.trigger?.normalizedTrigger ?? null
1268
- }
1350
+ data
1269
1351
  };
1352
+ if (hintCandidates.length > 0) {
1353
+ const suggestedName = (link2.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
1354
+ const candidatePaths = hintCandidates.map((n) => n.path);
1355
+ data["hint"] = {
1356
+ kind: "missing-frontmatter-name",
1357
+ suggestedName,
1358
+ candidates: candidatePaths
1359
+ };
1360
+ issue.fix = {
1361
+ summary: candidatePaths.length === 1 ? tx(BROKEN_REF_TEXTS.hintSummarySingle, {
1362
+ name: suggestedName,
1363
+ candidate: candidatePaths[0]
1364
+ }) : tx(BROKEN_REF_TEXTS.hintSummaryMany, {
1365
+ name: suggestedName,
1366
+ candidates: candidatePaths.join(", ")
1367
+ }),
1368
+ autofixable: false
1369
+ };
1370
+ }
1371
+ return issue;
1270
1372
  }
1271
1373
  function resolvesViaReferencePaths(link2, refIndex) {
1272
1374
  if (!isPathStyleLink(link2)) return false;
@@ -1285,6 +1387,36 @@ function indexByNormalizedName(nodes) {
1285
1387
  }
1286
1388
  return out;
1287
1389
  }
1390
+ function basenameWithoutExt(path) {
1391
+ const base = pathPosix2.basename(path);
1392
+ const ext = pathPosix2.extname(base);
1393
+ return ext ? base.slice(0, -ext.length) : base;
1394
+ }
1395
+ function indexByBasenameWithoutName(nodes) {
1396
+ const out = /* @__PURE__ */ new Map();
1397
+ for (const node of nodes) {
1398
+ const raw = node.frontmatter?.["name"];
1399
+ const name = typeof raw === "string" ? raw : "";
1400
+ if (name) continue;
1401
+ const bare = basenameWithoutExt(node.path);
1402
+ if (!bare) continue;
1403
+ const key = normalizeTrigger(bare);
1404
+ if (!key) continue;
1405
+ const bucket = out.get(key) ?? [];
1406
+ bucket.push(node);
1407
+ out.set(key, bucket);
1408
+ }
1409
+ return out;
1410
+ }
1411
+ function findHintCandidates(link2, idx) {
1412
+ const normalized = link2.trigger?.normalizedTrigger;
1413
+ if (!normalized) return [];
1414
+ const sigil = normalized.charAt(0);
1415
+ if (sigil !== "/" && sigil !== "@") return [];
1416
+ const withoutSigil = normalized.slice(1).trim();
1417
+ if (!withoutSigil) return [];
1418
+ return idx.get(withoutSigil) ?? [];
1419
+ }
1288
1420
  function isResolved(link2, byPath3, byNormalizedName) {
1289
1421
  const normalized = link2.trigger?.normalizedTrigger;
1290
1422
  if (normalized) {
@@ -2831,7 +2963,7 @@ var UPDATE_CHECK_TEXTS = {
2831
2963
  // package.json
2832
2964
  var package_default = {
2833
2965
  name: "@skill-map/cli",
2834
- version: "0.29.0",
2966
+ version: "0.31.0",
2835
2967
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2836
2968
  license: "MIT",
2837
2969
  type: "module",
@@ -7949,9 +8081,9 @@ function providerKindFailure(opts, status, fileName, errDescription) {
7949
8081
  }
7950
8082
  };
7951
8083
  }
7952
- function isDirectorySafe(path, statSync11) {
8084
+ function isDirectorySafe(path, statSync12) {
7953
8085
  try {
7954
- return statSync11(path).isDirectory();
8086
+ return statSync12(path).isDirectory();
7955
8087
  } catch {
7956
8088
  return false;
7957
8089
  }
@@ -23616,43 +23748,37 @@ var STUB_COMMANDS = [
23616
23748
  ];
23617
23749
 
23618
23750
  // cli/commands/tutorial.ts
23619
- import { existsSync as existsSync29, readFileSync as readFileSync19 } from "fs";
23620
- import { writeFile as writeFile2 } from "fs/promises";
23751
+ import { cpSync as cpSync2, existsSync as existsSync29, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync11 } from "fs";
23621
23752
  import { dirname as dirname19, join as join19, resolve as resolve36 } from "path";
23622
23753
  import { fileURLToPath as fileURLToPath6 } from "url";
23623
23754
  import { Command as Command37, Option as Option35 } from "clipanion";
23624
23755
 
23625
23756
  // cli/i18n/tutorial.texts.ts
23626
23757
  var TUTORIAL_TEXTS = {
23627
- // Success, written to stdout after `<cwd>/{{filename}}` is created.
23628
- // Multi-line layout: the two trigger phrases (English / Spanish) are
23629
- // indented and labelled so they're the most visible part of the
23630
- // output. The reminder above them surfaces the SKILL's language
23631
- // policy: the first message the tester writes to Claude sets the
23632
- // tutorial language for the rest of the session.
23633
- /**
23634
- * Success body. `glyph` is wrapped green at the call site; `cwd`
23635
- * renders relative to the user's cwd when it sits underneath. The
23636
- * `English` / `Español` labels print dim, the eye lands on the
23637
- * trigger phrases the user is going to copy / paste.
23638
- */
23639
- 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",
23758
+ // Success, written to stdout after `<cwd>/{{target}}` is created.
23759
+ // The skill now lives at `.claude/skills/<slug>/`; Claude Code
23760
+ // auto-discovers it on the next boot, so the tester invokes by
23761
+ // speaking a trigger phrase rather than referencing the file path.
23762
+ // English / Spanish triggers are surfaced side by side and the
23763
+ // first phrase the tester types sets the tutorial language for the
23764
+ // rest of the session.
23765
+ written: " {{glyph}} Skill `{{slug}}` materialized at {{target}} (under {{cwd}})\n\n Open Claude Code in this directory. The skill is auto-\n discovered; invoke it with one of its trigger phrases. The\n first message you type sets the tutorial language for the\n rest of the session:\n\n {{enLabel}} {{enTrigger}}\n {{esLabel}} {{esTrigger}}\n",
23640
23766
  writtenLabelEn: "English",
23641
23767
  writtenLabelEs: "Espa\xF1ol",
23642
- // Refusal, `{{filename}}` already exists and `--force` was not set.
23768
+ // Refusal, `{{target}}` already exists and `--force` was not set.
23643
23769
  // Goes to stderr, exit code 2 (operational error per spec § Exit codes).
23644
23770
  // Mirrors the success body shape: glyph + headline, then a dim hint
23645
23771
  // line spelling the fix.
23646
- alreadyExists: "{{glyph}} {{filename}} already exists at {{cwd}}\n {{hint}}\n",
23647
- alreadyExistsHint: "Pass `--force` to overwrite.",
23772
+ alreadyExists: "{{glyph}} {{target}} already exists under {{cwd}}\n {{hint}}\n",
23773
+ alreadyExistsHint: "Pass `--force` to overwrite (deletes the existing folder first).",
23648
23774
  // Invalid `variant` positional argument. Goes to stderr, exit code 2.
23649
23775
  // Mirrors `alreadyExists`: glyph + headline + dim hint enumerating the
23650
23776
  // valid values.
23651
23777
  invalidVariant: "{{glyph}} sm tutorial: unknown variant '{{variant}}'\n {{hint}}\n",
23652
23778
  invalidVariantHint: "Valid values: tutorial (default), master.",
23653
- // I/O failure on write or on reading the bundled SKILL source.
23654
- writeFailed: "{{glyph}} sm tutorial: failed to write {{filename}}: {{message}}\n",
23655
- sourceMissing: "{{glyph}} sm tutorial: could not read the bundled tutorial ({{filename}}) from the install.\n {{hint}}\n",
23779
+ // I/O failure on write or on reading the bundled skill source.
23780
+ writeFailed: "{{glyph}} sm tutorial: failed to write {{target}}: {{message}}\n",
23781
+ sourceMissing: "{{glyph}} sm tutorial: could not read the bundled skill payload for {{target}} from the install.\n {{hint}}\n",
23656
23782
  sourceMissingHint: "Reinstall @skill-map/cli or report the bug."
23657
23783
  };
23658
23784
 
@@ -23661,41 +23787,45 @@ var VALID_VARIANTS = ["tutorial", "master"];
23661
23787
  var DEFAULT_VARIANT = "tutorial";
23662
23788
  var VARIANT_SPECS = {
23663
23789
  tutorial: {
23664
- filename: "sm-tutorial.md",
23665
- sourcePath: ".claude/skills/sm-tutorial/SKILL.md",
23666
- bundledName: "sm-tutorial.md"
23790
+ slug: "sm-tutorial",
23791
+ sourceDir: ".claude/skills/sm-tutorial",
23792
+ triggerEn: "start the tutorial",
23793
+ triggerEs: "arranquemos el tutorial"
23667
23794
  },
23668
23795
  master: {
23669
- filename: "sm-master.md",
23670
- sourcePath: ".claude/skills/sm-master/SKILL.md",
23671
- bundledName: "sm-master.md"
23796
+ slug: "sm-master",
23797
+ sourceDir: ".claude/skills/sm-master",
23798
+ triggerEn: "advanced tutorial",
23799
+ triggerEs: "tutorial maestro"
23672
23800
  }
23673
23801
  };
23674
23802
  var TutorialCommand = class extends SmCommand {
23675
23803
  static paths = [["tutorial"]];
23676
23804
  static usage = Command37.Usage({
23677
23805
  category: "Setup",
23678
- description: "Materialize an interactive tester tutorial (sm-tutorial.md or sm-master.md) in the current directory.",
23806
+ description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
23679
23807
  details: `
23680
- Drops the canonical SKILL.md content as ./sm-tutorial.md (default)
23681
- or ./sm-master.md (when invoked as \`sm tutorial master\`) so a
23682
- tester can open Claude Code in the cwd and load the file as a
23683
- skill by typing "ejecut\xE1 @sm-tutorial.md" (or "@sm-master.md").
23684
- Top-level only; no subdirectory is created.
23808
+ Drops the canonical skill directory (SKILL.md + any references/
23809
+ sub-folder) under \`<cwd>/.claude/skills/sm-tutorial/\` (default)
23810
+ or \`<cwd>/.claude/skills/sm-master/\` (when invoked as \`sm
23811
+ tutorial master\`). Claude Code auto-discovers the skill the
23812
+ next time it boots in this directory; the tester invokes it by
23813
+ speaking one of its trigger phrases.
23685
23814
 
23686
23815
  Does NOT require an initialized .skill-map/ project. Refuses to
23687
- overwrite the target file unless --force is passed. Valid values
23688
- for the positional argument are: tutorial (default), master.
23816
+ overwrite the target directory unless --force is passed. Valid
23817
+ values for the positional argument are: tutorial (default),
23818
+ master.
23689
23819
  `,
23690
23820
  examples: [
23691
- ["Materialize the basic tutorial in the cwd", "$0 tutorial"],
23692
- ["Materialize the advanced tutorial in the cwd", "$0 tutorial master"],
23693
- ["Overwrite an existing target file", "$0 tutorial --force"]
23821
+ ["Materialize the basic tutorial skill in the cwd", "$0 tutorial"],
23822
+ ["Materialize the advanced tutorial skill in the cwd", "$0 tutorial master"],
23823
+ ["Overwrite an existing target directory", "$0 tutorial --force"]
23694
23824
  ]
23695
23825
  });
23696
23826
  variant = Option35.String({ required: false });
23697
23827
  force = Option35.Boolean("--force", false, {
23698
- description: "Overwrite an existing target file without prompting."
23828
+ description: "Overwrite an existing target directory without prompting."
23699
23829
  });
23700
23830
  async run() {
23701
23831
  const ctx = defaultRuntimeContext();
@@ -23715,38 +23845,41 @@ var TutorialCommand = class extends SmCommand {
23715
23845
  }
23716
23846
  const variant = rawVariant ?? DEFAULT_VARIANT;
23717
23847
  const spec = VARIANT_SPECS[variant];
23718
- const target = join19(ctx.cwd, spec.filename);
23719
- if (await pathExists(target) && !this.force) {
23848
+ const targetDir = join19(ctx.cwd, ".claude", "skills", spec.slug);
23849
+ const targetDisplay = `.claude/skills/${spec.slug}/`;
23850
+ if (existsSync29(targetDir) && !this.force) {
23720
23851
  this.printer.error(
23721
23852
  tx(TUTORIAL_TEXTS.alreadyExists, {
23722
23853
  glyph: errGlyph,
23723
- filename: spec.filename,
23854
+ target: targetDisplay,
23724
23855
  cwd: stderrAnsi.dim(displayCwd(ctx.cwd)),
23725
23856
  hint: stderrAnsi.dim(TUTORIAL_TEXTS.alreadyExistsHint)
23726
23857
  })
23727
23858
  );
23728
23859
  return ExitCode.Error;
23729
23860
  }
23730
- let body;
23861
+ let sourceDir;
23731
23862
  try {
23732
- body = loadBundledTutorialText(variant);
23863
+ sourceDir = resolveSkillSourceDir(variant);
23733
23864
  } catch {
23734
23865
  this.printer.error(
23735
23866
  tx(TUTORIAL_TEXTS.sourceMissing, {
23736
23867
  glyph: errGlyph,
23737
- filename: spec.filename,
23868
+ target: targetDisplay,
23738
23869
  hint: stderrAnsi.dim(TUTORIAL_TEXTS.sourceMissingHint)
23739
23870
  })
23740
23871
  );
23741
23872
  return ExitCode.Error;
23742
23873
  }
23743
23874
  try {
23744
- await writeFile2(target, body);
23875
+ rmSync2(targetDir, { recursive: true, force: true });
23876
+ mkdirSync7(dirname19(targetDir), { recursive: true });
23877
+ cpSync2(sourceDir, targetDir, { recursive: true });
23745
23878
  } catch (err) {
23746
23879
  this.printer.error(
23747
23880
  tx(TUTORIAL_TEXTS.writeFailed, {
23748
23881
  glyph: errGlyph,
23749
- filename: spec.filename,
23882
+ target: targetDisplay,
23750
23883
  message: formatErrorMessage(err)
23751
23884
  })
23752
23885
  );
@@ -23762,10 +23895,13 @@ var TutorialCommand = class extends SmCommand {
23762
23895
  this.printer.data(
23763
23896
  tx(TUTORIAL_TEXTS.written, {
23764
23897
  glyph: ansi.green("\u2713"),
23765
- filename: spec.filename,
23898
+ slug: spec.slug,
23899
+ target: targetDisplay,
23766
23900
  cwd: ansi.dim(displayCwd(ctx.cwd)),
23767
23901
  enLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEn),
23768
- esLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEs)
23902
+ esLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEs),
23903
+ enTrigger: spec.triggerEn,
23904
+ esTrigger: spec.triggerEs
23769
23905
  })
23770
23906
  );
23771
23907
  return ExitCode.Ok;
@@ -23779,31 +23915,29 @@ function displayCwd(cwd) {
23779
23915
  if (segments.length === 0) return "./";
23780
23916
  return `./${segments[segments.length - 1]}/`;
23781
23917
  }
23782
- var cachedTutorials = /* @__PURE__ */ new Map();
23783
- function loadBundledTutorialText(variant) {
23784
- const cached = cachedTutorials.get(variant);
23918
+ var cachedSourceDirs = /* @__PURE__ */ new Map();
23919
+ function resolveSkillSourceDir(variant) {
23920
+ const cached = cachedSourceDirs.get(variant);
23785
23921
  if (cached !== void 0) return cached;
23786
- const body = readTutorialFromDisk(variant);
23787
- cachedTutorials.set(variant, body);
23788
- return body;
23789
- }
23790
- function readTutorialFromDisk(variant) {
23791
23922
  const spec = VARIANT_SPECS[variant];
23792
23923
  const here = dirname19(fileURLToPath6(import.meta.url));
23793
23924
  const candidates = [
23794
- // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/SKILL.md
23795
- resolve36(here, "../../..", spec.sourcePath),
23796
- // bundled: dist/cli.js → dist/cli/tutorial/<filename> (sibling)
23797
- resolve36(here, "cli/tutorial", spec.bundledName),
23798
- // bundled fallback: any-depth → cli/tutorial/<filename>
23799
- resolve36(here, "../cli/tutorial", spec.bundledName)
23925
+ // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
23926
+ resolve36(here, "../../..", spec.sourceDir),
23927
+ // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
23928
+ resolve36(here, "cli/tutorial", spec.slug),
23929
+ // bundled fallback: any-depth → cli/tutorial/<slug>
23930
+ resolve36(here, "../cli/tutorial", spec.slug)
23800
23931
  ];
23801
23932
  for (const candidate of candidates) {
23802
- if (existsSync29(candidate)) {
23803
- return readFileSync19(candidate, "utf8");
23933
+ if (existsSync29(candidate) && statSync11(candidate).isDirectory()) {
23934
+ cachedSourceDirs.set(variant, candidate);
23935
+ return candidate;
23804
23936
  }
23805
23937
  }
23806
- throw new Error(`SKILL.md not found in any candidate location (last tried: ${candidates[candidates.length - 1]})`);
23938
+ throw new Error(
23939
+ `skill source directory not found in any candidate location (last tried: ${candidates[candidates.length - 1]})`
23940
+ );
23807
23941
  }
23808
23942
 
23809
23943
  // cli/commands/version.ts