@skill-map/cli 0.54.0 → 0.55.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.
Files changed (32) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +22 -24
  2. package/dist/cli/tutorial/sm-tutorial/references/_core.md +8 -7
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +21 -42
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +15 -7
  5. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +2 -2
  6. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  7. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +9 -10
  8. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +563 -0
  9. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +5 -1
  10. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +7 -7
  11. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +24 -12
  12. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  13. package/dist/cli.js +594 -485
  14. package/dist/index.js +127 -13
  15. package/dist/kernel/index.d.ts +187 -94
  16. package/dist/kernel/index.js +127 -13
  17. package/dist/migrations/001_initial.sql +5 -0
  18. package/dist/ui/chunk-CN6IOM67.js +2 -0
  19. package/dist/ui/chunk-HPKRDGLH.js +123 -0
  20. package/dist/ui/{chunk-CXTU4HQV.js → chunk-LREXXX7T.js} +1 -1
  21. package/dist/ui/{chunk-BUNPMGDX.js → chunk-RGB5FBZL.js} +28 -28
  22. package/dist/ui/{chunk-4CXAL43H.js → chunk-XAM6VKXM.js} +1 -1
  23. package/dist/ui/index.html +2 -2
  24. package/dist/ui/{main-HP3MOLI2.js → main-7YXBWYHE.js} +3 -3
  25. package/dist/ui/{styles-4SNVM34O.css → styles-HRJG67XW.css} +1 -1
  26. package/migrations/001_initial.sql +5 -0
  27. package/package.json +2 -2
  28. package/dist/cli/tutorial/sm-tutorial/references/part-live-site.md +0 -155
  29. package/dist/cli/tutorial/sm-tutorial/references/part-maintain.md +0 -284
  30. package/dist/cli/tutorial/sm-tutorial/references/part-run-harness.md +0 -181
  31. package/dist/ui/chunk-DSNBKMYU.js +0 -2
  32. package/dist/ui/chunk-MVRQGDZJ.js +0 -123
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3118bb11-aad2-50b2-b70d-ed15c2f72a97")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e21852cd-d92e-5bdf-abc7-02fffaa89502")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -246,7 +246,7 @@ function bucketByKind(kind, instance, bag) {
246
246
  // package.json
247
247
  var package_default = {
248
248
  name: "@skill-map/cli",
249
- version: "0.54.0",
249
+ version: "0.55.0",
250
250
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
251
  license: "MIT",
252
252
  type: "module",
@@ -883,7 +883,7 @@ var atDirectiveExtractor = {
883
883
  id: ID,
884
884
  pluginId: CLAUDE_PLUGIN_ID,
885
885
  kind: "extractor",
886
- description: "Detects `@<token>` directives in a node's body using Claude Code rules. 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.",
886
+ description: "Detects `@<token>` directives in a node's body using Claude Code rules, choosing the link kind by token shape. Example: a bare handle `@team` becomes a `mentions` link, while a file-flavoured token `@docs/api.md` becomes a `references` link.",
887
887
  scope: "body",
888
888
  precondition: { provider: ["claude"] },
889
889
  // eslint-disable-next-line complexity
@@ -971,7 +971,7 @@ var slashCommandExtractor = {
971
971
  id: ID2,
972
972
  pluginId: CLAUDE_PLUGIN_ID,
973
973
  kind: "extractor",
974
- description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules.",
974
+ description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
975
975
  scope: "body",
976
976
  precondition: { provider: ["claude"] },
977
977
  extract(ctx) {
@@ -1029,7 +1029,7 @@ var toolsCounterExtractor = {
1029
1029
  id: ID3,
1030
1030
  pluginId: CLAUDE_PLUGIN_ID,
1031
1031
  kind: "extractor",
1032
- description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1032
+ description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card. Example: an agent with `tools: [Bash, Read, Grep]` shows a count of 3.",
1033
1033
  scope: "frontmatter",
1034
1034
  precondition: { kind: ["claude/agent"] },
1035
1035
  ui: { count },
@@ -1444,7 +1444,7 @@ var annotationsExtractor = {
1444
1444
  id: ID4,
1445
1445
  pluginId: CORE_PLUGIN_ID,
1446
1446
  kind: "extractor",
1447
- description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
1447
+ description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph. Example: `supersededBy: v1-skill.md` in a `.sm` sidecar draws an arrow to `v1-skill.md`.",
1448
1448
  scope: "frontmatter",
1449
1449
  extract(ctx) {
1450
1450
  const sourcePath = ctx.node.path;
@@ -1500,12 +1500,12 @@ function stringArray(value) {
1500
1500
  // plugins/core/extractors/backtick-path/index.ts
1501
1501
  import { posix as pathPosix2 } from "path";
1502
1502
  var ID5 = "backtick-path";
1503
- var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w.-]+(?:\/[\w.-]+)+\.md\b(?![\w/])/g;
1503
+ var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1504
1504
  var backtickPathExtractor = {
1505
1505
  id: ID5,
1506
1506
  pluginId: CORE_PLUGIN_ID,
1507
1507
  kind: "extractor",
1508
- description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph.",
1508
+ description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
1509
1509
  scope: "body",
1510
1510
  extract(ctx) {
1511
1511
  const seen = /* @__PURE__ */ new Set();
@@ -1570,7 +1570,7 @@ var externalUrlCounterExtractor = {
1570
1570
  id: ID6,
1571
1571
  pluginId: CORE_PLUGIN_ID,
1572
1572
  kind: "extractor",
1573
- description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
1573
+ description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
1574
1574
  scope: "body",
1575
1575
  /**
1576
1576
  * Phase 6 / View contribution system, surface the distinct-URL
@@ -1650,7 +1650,7 @@ var markdownLinkExtractor = {
1650
1650
  id: ID7,
1651
1651
  pluginId: CORE_PLUGIN_ID,
1652
1652
  kind: "extractor",
1653
- description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
1653
+ description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
1654
1654
  scope: "body",
1655
1655
  extract(ctx) {
1656
1656
  const seen = /* @__PURE__ */ new Set();
@@ -1675,13 +1675,15 @@ var markdownLinkExtractor = {
1675
1675
  extractorId: ID7,
1676
1676
  kind: "references",
1677
1677
  target: resolved,
1678
- // 1.0: the `[text](path)` syntax is unambiguous. Markdown
1679
- // explicitly designates an out-link via the brackets +
1680
- // parentheses pair; there is no inference left to discount.
1681
- // Whether the path resolves to a real node is a separate
1682
- // concern (the `core/reference-broken` analyzer flags unresolved
1683
- // targets), not a confidence question.
1684
- confidence: 1,
1678
+ // 0.95: the `[text](path)` syntax is unambiguous (the spec's
1679
+ // "unambiguous syntax" tier), but NOT 1.0. `1.0` is reserved
1680
+ // for structured input and is earned post-walk by the
1681
+ // confidence-lift transform when `target` resolves to a real
1682
+ // node; an unresolved (broken) target is downgraded to the
1683
+ // broken floor (0.5) by the same transform. Emitting 1.0 here
1684
+ // would short-circuit the lift and make a broken link
1685
+ // indistinguishable from a resolved one.
1686
+ confidence: 0.95,
1685
1687
  rationale: "unambiguous markdown link syntax",
1686
1688
  trigger: {
1687
1689
  originalTrigger: original,
@@ -1711,7 +1713,7 @@ var mcpToolsExtractor = {
1711
1713
  id: ID8,
1712
1714
  pluginId: CORE_PLUGIN_ID,
1713
1715
  kind: "extractor",
1714
- description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one.",
1716
+ description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
1715
1717
  // Claude-convention pattern only; per-vendor flavours and the
1716
1718
  // config-side MCP declaration (Phase 5b) are still pending, so the
1717
1719
  // extractor ships flagged as experimental in list / show / Settings.
@@ -2004,11 +2006,7 @@ var ANNOTATION_STALE_TEXTS = {
2004
2006
  // a literal placeholder the operator substitutes.
2005
2007
  bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
2006
2008
  frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
2007
- bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
2008
- /** Label of the inspector action button that dispatches a bump. */
2009
- bumpLabel: "Bump",
2010
- /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
2011
- bumpDisabledReason: "No drift to bump."
2009
+ bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
2012
2010
  };
2013
2011
 
2014
2012
  // plugins/core/analyzers/annotation-stale/index.ts
@@ -2024,10 +2022,6 @@ var staleBadge = {
2024
2022
  emitWhenEmpty: false,
2025
2023
  priority: 20
2026
2024
  };
2027
- var bumpButton = {
2028
- slot: "inspector.action.button",
2029
- priority: 10
2030
- };
2031
2025
  var annotationStaleAnalyzer = {
2032
2026
  id: ID11,
2033
2027
  pluginId: CORE_PLUGIN_ID,
@@ -2036,15 +2030,13 @@ var annotationStaleAnalyzer = {
2036
2030
  mode: "deterministic",
2037
2031
  // The natural fix is to bump the node: refreshes the sidecar hashes,
2038
2032
  // increments `annotations.version`, and stamps the audit block. The
2039
- // inspector surfaces `core/node-bump` as the `bumpButton` contribution.
2040
- ui: { staleIcon, staleBadge, bumpButton },
2033
+ // inspector surfaces that affordance via the `core/node-bump` action's
2034
+ // own scan-time `project()` self-projection, not from this analyzer.
2035
+ ui: { staleIcon, staleBadge },
2041
2036
  evaluate(ctx) {
2042
2037
  const issues = [];
2043
2038
  for (const node of ctx.nodes) {
2044
2039
  const status = staleStatus(node.sidecar);
2045
- if (node.sidecar?.present === true) {
2046
- emitBumpButton(ctx, node.path, status !== null);
2047
- }
2048
2040
  if (status === null) continue;
2049
2041
  issues.push({
2050
2042
  analyzerId: ID11,
@@ -2080,15 +2072,6 @@ function messageFor(status, path) {
2080
2072
  return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2081
2073
  }
2082
2074
  }
2083
- function emitBumpButton(ctx, nodePath, enabled) {
2084
- ctx.emitContribution(nodePath, bumpButton, {
2085
- actionId: "core/node-bump",
2086
- label: ANNOTATION_STALE_TEXTS.bumpLabel,
2087
- icon: "pi-arrow-up-right",
2088
- enabled,
2089
- ...enabled ? {} : { disabledReason: ANNOTATION_STALE_TEXTS.bumpDisabledReason }
2090
- });
2091
- }
2092
2075
  function tooltipFor(status) {
2093
2076
  switch (status) {
2094
2077
  case "stale-body":
@@ -2539,16 +2522,25 @@ function readDirname(node) {
2539
2522
 
2540
2523
  // kernel/orchestrator/lift-resolved-link-confidence.ts
2541
2524
  var RESERVED_TARGET_CONFIDENCE = 0.1;
2525
+ var BROKEN_TARGET_CONFIDENCE = 0.5;
2542
2526
  function liftResolvedLinkConfidence(links, nodes, ctx) {
2543
2527
  if (!links.some((l) => l.confidence < 1)) return;
2544
2528
  const indexes = buildIndexes(nodes, ctx);
2545
2529
  for (const link of links) {
2546
- if (link.confidence >= 1) continue;
2547
- const resolution = resolve2(link, indexes, ctx);
2548
- if (resolution === "none") continue;
2549
- link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2550
- link.resolvedTarget = resolution;
2530
+ if (link.confidence < 1) applyResolution(link, indexes, ctx);
2531
+ }
2532
+ }
2533
+ function applyResolution(link, indexes, ctx) {
2534
+ const resolution = resolve2(link, indexes, ctx);
2535
+ if (resolution === "none") {
2536
+ if (isGenuinelyBroken(link, indexes)) {
2537
+ link.confidence = Math.min(link.confidence, BROKEN_TARGET_CONFIDENCE);
2538
+ }
2539
+ return;
2551
2540
  }
2541
+ link.resolvedTarget = resolution;
2542
+ if (indexes.nodeByPath.get(resolution)?.virtual) return;
2543
+ link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2552
2544
  }
2553
2545
  function buildIndexes(nodes, ctx) {
2554
2546
  const byPath3 = /* @__PURE__ */ new Set();
@@ -2565,6 +2557,12 @@ function resolve2(link, indexes, ctx) {
2565
2557
  if (indexes.byPath.has(link.target)) return link.target;
2566
2558
  return resolveByName(link, indexes, ctx);
2567
2559
  }
2560
+ function isGenuinelyBroken(link, indexes) {
2561
+ if (indexes.byPath.has(link.target)) return false;
2562
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2563
+ if (stripped !== null && indexes.byName.has(stripped)) return false;
2564
+ return true;
2565
+ }
2568
2566
  function resolveByName(link, indexes, ctx) {
2569
2567
  const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2570
2568
  if (stripped === null) return "none";
@@ -2856,6 +2854,12 @@ var nodeSupersededAnalyzer = {
2856
2854
  pluginId: CORE_PLUGIN_ID,
2857
2855
  kind: "analyzer",
2858
2856
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
2857
+ // Part of the experimental supersession feature: ships disabled by
2858
+ // default alongside the declarer (`core/supersede` button +
2859
+ // `core/node-supersede` action). With the declarer off by default a
2860
+ // user rarely produces `supersededBy` data, so surfacing it stays
2861
+ // experimental too; the operator opts the whole family in together.
2862
+ stability: "experimental",
2859
2863
  mode: "deterministic",
2860
2864
  evaluate(ctx) {
2861
2865
  const issues = [];
@@ -3760,122 +3764,6 @@ function makeIssue(signal) {
3760
3764
  return null;
3761
3765
  }
3762
3766
 
3763
- // plugins/core/analyzers/supersede/text.ts
3764
- var SUPERSEDE_TEXTS = {
3765
- /** Label of the inspector action button that declares supersession. */
3766
- supersedeLabel: "Supersede",
3767
- /** Tooltip shown when the supersede button is disabled (already superseded). */
3768
- supersedeDisabledReason: "Already superseded.",
3769
- /** Tooltip shown when there is no other node to supersede this one. */
3770
- supersedeNoTargetsReason: "No other node to supersede this one.",
3771
- /** Prompt label for the target node-picker (enum-pick over the live node set). */
3772
- supersedePromptLabel: "Superseded by"
3773
- };
3774
-
3775
- // plugins/core/analyzers/supersede/index.ts
3776
- var ID25 = "supersede";
3777
- var supersedeButton = {
3778
- slot: "inspector.action.button",
3779
- priority: 10
3780
- };
3781
- var supersedeAnalyzer = {
3782
- id: ID25,
3783
- pluginId: CORE_PLUGIN_ID,
3784
- kind: "analyzer",
3785
- description: 'Projects the inspector "Supersede" button (declares a node replaced by another).',
3786
- mode: "deterministic",
3787
- ui: { supersedeButton },
3788
- evaluate(ctx) {
3789
- const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
3790
- for (const node of ctx.nodes) {
3791
- if (node.virtual === true) continue;
3792
- const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
3793
- emitSupersedeButton(ctx, node, options);
3794
- }
3795
- return [];
3796
- }
3797
- };
3798
- function emitSupersedeButton(ctx, node, options) {
3799
- const disabledReason = resolveDisabledReason(node, options.length);
3800
- ctx.emitContribution(node.path, supersedeButton, {
3801
- actionId: "core/node-supersede",
3802
- label: SUPERSEDE_TEXTS.supersedeLabel,
3803
- icon: "pi-arrow-right-arrow-left",
3804
- enabled: disabledReason === void 0,
3805
- ...disabledReason === void 0 ? {} : { disabledReason },
3806
- prompt: {
3807
- inputType: "enum-pick",
3808
- paramKey: "supersededBy",
3809
- label: SUPERSEDE_TEXTS.supersedePromptLabel,
3810
- options
3811
- }
3812
- });
3813
- }
3814
- function resolveDisabledReason(node, optionCount) {
3815
- if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
3816
- if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
3817
- return void 0;
3818
- }
3819
- function alreadySuperseded(node) {
3820
- const sidecar = node.sidecar;
3821
- if (!sidecar || sidecar.present !== true) return false;
3822
- const ann = sidecar.annotations;
3823
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
3824
- const value = ann["supersededBy"];
3825
- return typeof value === "string" && value.length > 0;
3826
- }
3827
-
3828
- // plugins/core/analyzers/tags/text.ts
3829
- var TAGS_TEXTS = {
3830
- /** Label of the inspector action button that edits the node's tags. */
3831
- editLabel: "Edit tags",
3832
- /** Prompt label for the string-list tags input. */
3833
- promptLabel: "Tags"
3834
- };
3835
-
3836
- // plugins/core/analyzers/tags/index.ts
3837
- var ID26 = "tags";
3838
- var setTagsButton = {
3839
- slot: "inspector.action.button",
3840
- priority: 15
3841
- };
3842
- var tagsAnalyzer = {
3843
- id: ID26,
3844
- pluginId: CORE_PLUGIN_ID,
3845
- kind: "analyzer",
3846
- description: `Projects the inspector "Edit tags" button (edits a node's taxonomy tags).`,
3847
- mode: "deterministic",
3848
- ui: { setTagsButton },
3849
- evaluate(ctx) {
3850
- for (const node of ctx.nodes) {
3851
- if (node.sidecar?.present !== true) continue;
3852
- emitSetTagsButton(ctx, node);
3853
- }
3854
- return [];
3855
- }
3856
- };
3857
- function emitSetTagsButton(ctx, node) {
3858
- ctx.emitContribution(node.path, setTagsButton, {
3859
- actionId: "core/node-set-tags",
3860
- label: TAGS_TEXTS.editLabel,
3861
- icon: "pi-tags",
3862
- enabled: true,
3863
- prompt: {
3864
- inputType: "string-list",
3865
- paramKey: "tags",
3866
- label: TAGS_TEXTS.promptLabel,
3867
- defaultValue: currentTags(node)
3868
- }
3869
- });
3870
- }
3871
- function currentTags(node) {
3872
- const ann = node.sidecar?.annotations;
3873
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
3874
- const value = ann["tags"];
3875
- if (!Array.isArray(value)) return [];
3876
- return value.filter((t) => typeof t === "string");
3877
- }
3878
-
3879
3767
  // plugins/core/analyzers/trigger-collision/text.ts
3880
3768
  var TRIGGER_COLLISION_TEXTS = {
3881
3769
  /**
@@ -3902,14 +3790,14 @@ var TRIGGER_COLLISION_TEXTS = {
3902
3790
  };
3903
3791
 
3904
3792
  // plugins/core/analyzers/trigger-collision/index.ts
3905
- var ID27 = "trigger-collision";
3793
+ var ID25 = "trigger-collision";
3906
3794
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3907
3795
  "command",
3908
3796
  "skill",
3909
3797
  "agent"
3910
3798
  ]);
3911
3799
  var triggerCollisionAnalyzer = {
3912
- id: ID27,
3800
+ id: ID25,
3913
3801
  pluginId: CORE_PLUGIN_ID,
3914
3802
  kind: "analyzer",
3915
3803
  mode: "deterministic",
@@ -4007,7 +3895,7 @@ function analyzeTriggerBucket(normalized, claims) {
4007
3895
  part: parts[0]
4008
3896
  });
4009
3897
  return {
4010
- analyzerId: ID27,
3898
+ analyzerId: ID25,
4011
3899
  severity: "error",
4012
3900
  nodeIds,
4013
3901
  message,
@@ -4047,13 +3935,13 @@ var ASCII_FORMATTER_TEXTS = {
4047
3935
  };
4048
3936
 
4049
3937
  // plugins/core/formatters/ascii/index.ts
4050
- var ID28 = "ascii";
3938
+ var ID26 = "ascii";
4051
3939
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
4052
3940
  var asciiFormatter = {
4053
- id: ID28,
3941
+ id: ID26,
4054
3942
  pluginId: CORE_PLUGIN_ID,
4055
3943
  kind: "formatter",
4056
- formatId: ID28,
3944
+ formatId: ID26,
4057
3945
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
4058
3946
  // ASCII tree formatter, header + per-kind sections + per-issue
4059
3947
  // section. Each section iterates and renders; splitting per section
@@ -4147,13 +4035,13 @@ function renderSection(out, kind, group) {
4147
4035
  }
4148
4036
 
4149
4037
  // plugins/core/formatters/json/index.ts
4150
- var ID29 = "json";
4038
+ var ID27 = "json";
4151
4039
  var jsonFormatter = {
4152
- id: ID29,
4040
+ id: ID27,
4153
4041
  pluginId: CORE_PLUGIN_ID,
4154
4042
  kind: "formatter",
4155
4043
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
4156
- formatId: ID29,
4044
+ formatId: ID27,
4157
4045
  format(ctx) {
4158
4046
  if (ctx.scanResult !== void 0) {
4159
4047
  return JSON.stringify(ctx.scanResult);
@@ -4291,14 +4179,33 @@ function resolveSpecRoot2() {
4291
4179
  }
4292
4180
  }
4293
4181
 
4182
+ // plugins/core/actions/node-bump/text.ts
4183
+ var BUMP_TEXTS = {
4184
+ /** Label of the inspector action button that dispatches a bump. */
4185
+ bumpLabel: "Bump",
4186
+ /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
4187
+ bumpDisabledReason: "No drift to bump."
4188
+ };
4189
+
4294
4190
  // plugins/core/actions/node-bump/index.ts
4295
- var ID30 = "node-bump";
4191
+ var ID28 = "node-bump";
4192
+ var bumpButton = {
4193
+ slot: "inspector.action.button",
4194
+ priority: 10
4195
+ };
4296
4196
  var nodeBumpAction = {
4297
- id: ID30,
4197
+ id: ID28,
4298
4198
  pluginId: CORE_PLUGIN_ID,
4299
4199
  kind: "action",
4300
4200
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
4301
4201
  mode: "deterministic",
4202
+ ui: { bumpButton },
4203
+ project(ctx) {
4204
+ for (const node of ctx.nodes) {
4205
+ if (node.sidecar?.present !== true) continue;
4206
+ emitBumpButton(ctx, node.path, staleStatus2(node.sidecar) !== null);
4207
+ }
4208
+ },
4302
4209
  // The runtime contract uses generic <TInput, TReport>; bump narrows
4303
4210
  // both. The cast is the standard pattern for built-ins that want
4304
4211
  // typed local I/O while staying compatible with the open generic.
@@ -4307,6 +4214,20 @@ var nodeBumpAction = {
4307
4214
  return invokeBump(input, ctx);
4308
4215
  }
4309
4216
  };
4217
+ function emitBumpButton(ctx, nodePath, enabled) {
4218
+ ctx.emitContribution(nodePath, bumpButton, {
4219
+ actionId: "core/node-bump",
4220
+ label: BUMP_TEXTS.bumpLabel,
4221
+ icon: "pi-arrow-up-right",
4222
+ enabled,
4223
+ ...enabled ? {} : { disabledReason: BUMP_TEXTS.bumpDisabledReason }
4224
+ });
4225
+ }
4226
+ function staleStatus2(overlay) {
4227
+ const status = overlay?.status;
4228
+ if (status === void 0 || status === null || status === "fresh") return null;
4229
+ return status;
4230
+ }
4310
4231
  function invokeBump(input, ctx) {
4311
4232
  const overlay = ctx.node.sidecar ?? null;
4312
4233
  const isFresh = overlay?.present === true && overlay.status === "fresh";
@@ -4353,9 +4274,9 @@ function pickCurrentVersion(overlay) {
4353
4274
 
4354
4275
  // plugins/core/actions/node-set-stability/index.ts
4355
4276
  var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4356
- var ID31 = "node-set-stability";
4277
+ var ID29 = "node-set-stability";
4357
4278
  var nodeSetStabilityAction = {
4358
- id: ID31,
4279
+ id: ID29,
4359
4280
  pluginId: CORE_PLUGIN_ID,
4360
4281
  kind: "action",
4361
4282
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -4394,14 +4315,33 @@ function invokeSetStability(input, ctx) {
4394
4315
  return { report, writes: [write] };
4395
4316
  }
4396
4317
 
4318
+ // plugins/core/actions/node-set-tags/text.ts
4319
+ var TAGS_TEXTS = {
4320
+ /** Label of the inspector action button that edits the node's tags. */
4321
+ editLabel: "Edit tags",
4322
+ /** Prompt label for the string-list tags input. */
4323
+ promptLabel: "Tags"
4324
+ };
4325
+
4397
4326
  // plugins/core/actions/node-set-tags/index.ts
4398
- var ID32 = "node-set-tags";
4327
+ var ID30 = "node-set-tags";
4328
+ var setTagsButton = {
4329
+ slot: "inspector.action.button",
4330
+ priority: 15
4331
+ };
4399
4332
  var nodeSetTagsAction = {
4400
- id: ID32,
4333
+ id: ID30,
4401
4334
  pluginId: CORE_PLUGIN_ID,
4402
4335
  kind: "action",
4403
4336
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
4404
4337
  mode: "deterministic",
4338
+ ui: { setTagsButton },
4339
+ project(ctx) {
4340
+ for (const node of ctx.nodes) {
4341
+ if (node.sidecar?.present !== true) continue;
4342
+ emitSetTagsButton(ctx, node);
4343
+ }
4344
+ },
4405
4345
  // The runtime contract uses generic <TInput, TReport>; this narrows
4406
4346
  // both. The cast is the standard pattern for built-ins that want
4407
4347
  // typed local I/O while staying compatible with the open generic.
@@ -4410,6 +4350,27 @@ var nodeSetTagsAction = {
4410
4350
  return invokeSetTags(input, ctx);
4411
4351
  }
4412
4352
  };
4353
+ function emitSetTagsButton(ctx, node) {
4354
+ ctx.emitContribution(node.path, setTagsButton, {
4355
+ actionId: "core/node-set-tags",
4356
+ label: TAGS_TEXTS.editLabel,
4357
+ icon: "pi-tags",
4358
+ enabled: true,
4359
+ prompt: {
4360
+ inputType: "string-list",
4361
+ paramKey: "tags",
4362
+ label: TAGS_TEXTS.promptLabel,
4363
+ defaultValue: currentTags(node)
4364
+ }
4365
+ });
4366
+ }
4367
+ function currentTags(node) {
4368
+ const ann = node.sidecar?.annotations;
4369
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
4370
+ const value = ann["tags"];
4371
+ if (!Array.isArray(value)) return [];
4372
+ return value.filter((t) => typeof t === "string");
4373
+ }
4413
4374
  function invokeSetTags(input, ctx) {
4414
4375
  const tags = Array.isArray(input.tags) ? input.tags : [];
4415
4376
  const timestamp = ctx.now().toISOString();
@@ -4433,14 +4394,44 @@ function invokeSetTags(input, ctx) {
4433
4394
  return { report, writes: [write] };
4434
4395
  }
4435
4396
 
4397
+ // plugins/core/actions/node-supersede/text.ts
4398
+ var SUPERSEDE_TEXTS = {
4399
+ /** Label of the inspector action button that declares supersession. */
4400
+ supersedeLabel: "Supersede",
4401
+ /** Tooltip shown when the supersede button is disabled (already superseded). */
4402
+ supersedeDisabledReason: "Already superseded.",
4403
+ /** Tooltip shown when there is no other node to supersede this one. */
4404
+ supersedeNoTargetsReason: "No other node to supersede this one.",
4405
+ /** Prompt label for the target node-picker (enum-pick over the live node set). */
4406
+ supersedePromptLabel: "Superseded by"
4407
+ };
4408
+
4436
4409
  // plugins/core/actions/node-supersede/index.ts
4437
- var ID33 = "node-supersede";
4410
+ var ID31 = "node-supersede";
4411
+ var supersedeButton = {
4412
+ slot: "inspector.action.button",
4413
+ priority: 10
4414
+ };
4438
4415
  var nodeSupersedeAction = {
4439
- id: ID33,
4416
+ id: ID31,
4440
4417
  pluginId: CORE_PLUGIN_ID,
4441
4418
  kind: "action",
4442
4419
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
4420
+ // Ships disabled by default (the declarer feature is still settling its
4421
+ // node-picker UX). The button self-projection gates as a unit with the
4422
+ // invoke executor: an enabled button pointing at a disabled action
4423
+ // would error on click, so the whole action stays experimental.
4424
+ stability: "experimental",
4443
4425
  mode: "deterministic",
4426
+ ui: { supersedeButton },
4427
+ project(ctx) {
4428
+ const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
4429
+ for (const node of ctx.nodes) {
4430
+ if (node.virtual === true) continue;
4431
+ const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
4432
+ emitSupersedeButton(ctx, node, options);
4433
+ }
4434
+ },
4444
4435
  // The runtime contract uses generic <TInput, TReport>; supersede
4445
4436
  // narrows both. The cast is the standard pattern for built-ins that
4446
4437
  // want typed local I/O while staying compatible with the open generic.
@@ -4449,6 +4440,35 @@ var nodeSupersedeAction = {
4449
4440
  return invokeSupersede(input, ctx);
4450
4441
  }
4451
4442
  };
4443
+ function emitSupersedeButton(ctx, node, options) {
4444
+ const disabledReason = resolveDisabledReason(node, options.length);
4445
+ ctx.emitContribution(node.path, supersedeButton, {
4446
+ actionId: "core/node-supersede",
4447
+ label: SUPERSEDE_TEXTS.supersedeLabel,
4448
+ icon: "pi-arrow-right-arrow-left",
4449
+ enabled: disabledReason === void 0,
4450
+ ...disabledReason === void 0 ? {} : { disabledReason },
4451
+ prompt: {
4452
+ inputType: "enum-pick",
4453
+ paramKey: "supersededBy",
4454
+ label: SUPERSEDE_TEXTS.supersedePromptLabel,
4455
+ options
4456
+ }
4457
+ });
4458
+ }
4459
+ function resolveDisabledReason(node, optionCount) {
4460
+ if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
4461
+ if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
4462
+ return void 0;
4463
+ }
4464
+ function alreadySuperseded(node) {
4465
+ const sidecar = node.sidecar;
4466
+ if (!sidecar || sidecar.present !== true) return false;
4467
+ const ann = sidecar.annotations;
4468
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
4469
+ const value = ann["supersededBy"];
4470
+ return typeof value === "string" && value.length > 0;
4471
+ }
4452
4472
  function invokeSupersede(input, ctx) {
4453
4473
  const supersededBy = input.supersededBy;
4454
4474
  if (supersededBy === ctx.node.path) {
@@ -4980,8 +5000,6 @@ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", v
4980
5000
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4981
5001
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4982
5002
  var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4983
- var supersedeAnalyzer2 = { ...supersedeAnalyzer, pluginId: "core", version: VERSION };
4984
- var tagsAnalyzer2 = { ...tagsAnalyzer, pluginId: "core", version: VERSION };
4985
5003
  var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4986
5004
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4987
5005
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
@@ -5048,8 +5066,6 @@ var builtInPlugins = [
5048
5066
  referenceRedundantAnalyzer2,
5049
5067
  schemaViolationAnalyzer2,
5050
5068
  signalCollisionAnalyzer2,
5051
- supersedeAnalyzer2,
5052
- tagsAnalyzer2,
5053
5069
  triggerCollisionAnalyzer2,
5054
5070
  asciiFormatter2,
5055
5071
  jsonFormatter2,
@@ -6458,7 +6474,7 @@ function resolveSpecRoot3() {
6458
6474
  }
6459
6475
 
6460
6476
  // cli/i18n/bump.texts.ts
6461
- var BUMP_TEXTS = {
6477
+ var BUMP_TEXTS2 = {
6462
6478
  // --- argument validation --------------------------------------------------
6463
6479
  /**
6464
6480
  * §3.1b two-line block. Mutex between the positional <node.path> and
@@ -8149,6 +8165,7 @@ function rowToNode(row) {
8149
8165
  const parsed = JSON.parse(row.externalRefsJson);
8150
8166
  if (Array.isArray(parsed) && parsed.length > 0) node.externalRefs = parsed;
8151
8167
  }
8168
+ if (row.modifiedAtMs !== null) node.modifiedAtMs = row.modifiedAtMs;
8152
8169
  return node;
8153
8170
  }
8154
8171
  function rowToLink(row) {
@@ -8665,7 +8682,10 @@ function nodeToRow(node, scannedAt) {
8665
8682
  // the column stays sparse for nodes whose bodies have no http(s)
8666
8683
  // URLs at all. Round-tripped by `rowToNode` on load.
8667
8684
  externalRefsJson: node.externalRefs && node.externalRefs.length > 0 ? JSON.stringify(node.externalRefs) : null,
8668
- scannedAt
8685
+ scannedAt,
8686
+ // File mtime (Unix ms) from the walker; NULL for virtual / derived
8687
+ // nodes that carry no backing file. Round-tripped by `rowToNode`.
8688
+ modifiedAtMs: node.modifiedAtMs ?? null
8669
8689
  };
8670
8690
  }
8671
8691
  function projectAnnotationColumns(node) {
@@ -9666,10 +9686,10 @@ var BumpCommand = class extends SmCommand {
9666
9686
  );
9667
9687
  if (!persisted) {
9668
9688
  this.printer.error(
9669
- tx(BUMP_TEXTS.nodeNotFound, {
9689
+ tx(BUMP_TEXTS2.nodeNotFound, {
9670
9690
  glyph: ansi.red("\u2715"),
9671
9691
  nodePath: this.nodePath ?? "<pending>",
9672
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9692
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9673
9693
  })
9674
9694
  );
9675
9695
  return ExitCode.NotFound;
@@ -9689,27 +9709,27 @@ var BumpCommand = class extends SmCommand {
9689
9709
  const errGlyph = ansi.red("\u2715");
9690
9710
  if (this.pending && this.nodePath !== void 0) {
9691
9711
  this.printer.error(
9692
- tx(BUMP_TEXTS.nodeAndPendingMutex, {
9712
+ tx(BUMP_TEXTS2.nodeAndPendingMutex, {
9693
9713
  glyph: errGlyph,
9694
- hint: ansi.dim(BUMP_TEXTS.nodeAndPendingMutexHint)
9714
+ hint: ansi.dim(BUMP_TEXTS2.nodeAndPendingMutexHint)
9695
9715
  })
9696
9716
  );
9697
9717
  return ExitCode.Error;
9698
9718
  }
9699
9719
  if (!this.pending && this.nodePath === void 0) {
9700
9720
  this.printer.error(
9701
- tx(BUMP_TEXTS.noTargetSpecified, {
9721
+ tx(BUMP_TEXTS2.noTargetSpecified, {
9702
9722
  glyph: errGlyph,
9703
- hint: ansi.dim(BUMP_TEXTS.noTargetSpecifiedHint)
9723
+ hint: ansi.dim(BUMP_TEXTS2.noTargetSpecifiedHint)
9704
9724
  })
9705
9725
  );
9706
9726
  return ExitCode.Error;
9707
9727
  }
9708
9728
  if (this.staged && !this.pending) {
9709
9729
  this.printer.error(
9710
- tx(BUMP_TEXTS.stagedRequiresPending, {
9730
+ tx(BUMP_TEXTS2.stagedRequiresPending, {
9711
9731
  glyph: errGlyph,
9712
- hint: ansi.dim(BUMP_TEXTS.stagedRequiresPendingHint)
9732
+ hint: ansi.dim(BUMP_TEXTS2.stagedRequiresPendingHint)
9713
9733
  })
9714
9734
  );
9715
9735
  return ExitCode.Error;
@@ -9768,10 +9788,10 @@ var BumpCommand = class extends SmCommand {
9768
9788
  const node = nodes.find((n) => n.path === this.nodePath);
9769
9789
  if (!node) {
9770
9790
  this.printer.error(
9771
- tx(BUMP_TEXTS.nodeNotFound, {
9791
+ tx(BUMP_TEXTS2.nodeNotFound, {
9772
9792
  glyph: ansi.red("\u2715"),
9773
9793
  nodePath: this.nodePath,
9774
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9794
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9775
9795
  })
9776
9796
  );
9777
9797
  return ExitCode.NotFound;
@@ -9793,9 +9813,9 @@ var BumpCommand = class extends SmCommand {
9793
9813
  const errGlyph = ansi.red("\u2715");
9794
9814
  if (item.status === "error") {
9795
9815
  this.printer.error(
9796
- tx(BUMP_TEXTS.bumpFailed, {
9816
+ tx(BUMP_TEXTS2.bumpFailed, {
9797
9817
  glyph: errGlyph,
9798
- message: tx(BUMP_TEXTS.resolveAbsPathFailed, {
9818
+ message: tx(BUMP_TEXTS2.resolveAbsPathFailed, {
9799
9819
  nodePath: node.path,
9800
9820
  message: item.message
9801
9821
  })
@@ -9805,10 +9825,10 @@ var BumpCommand = class extends SmCommand {
9805
9825
  }
9806
9826
  if (item.status === "refused") {
9807
9827
  this.printer.error(
9808
- tx(BUMP_TEXTS.refusedFresh, {
9828
+ tx(BUMP_TEXTS2.refusedFresh, {
9809
9829
  glyph: errGlyph,
9810
9830
  nodePath: node.path,
9811
- hint: ansi.dim(BUMP_TEXTS.refusedFreshHint)
9831
+ hint: ansi.dim(BUMP_TEXTS2.refusedFreshHint)
9812
9832
  })
9813
9833
  );
9814
9834
  return ExitCode.Error;
@@ -9835,9 +9855,9 @@ var BumpCommand = class extends SmCommand {
9835
9855
  if (applied.error !== void 0) {
9836
9856
  if (applied.error instanceof EConsentRequiredError) throw applied.error;
9837
9857
  this.printer.error(
9838
- tx(BUMP_TEXTS.bumpFailed, {
9858
+ tx(BUMP_TEXTS2.bumpFailed, {
9839
9859
  glyph: ansi.red("\u2715"),
9840
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9860
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9841
9861
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9842
9862
  message: formatErrorMessage(applied.error)
9843
9863
  })
@@ -9854,11 +9874,11 @@ var BumpCommand = class extends SmCommand {
9854
9874
  const version = item.report.version ?? 1;
9855
9875
  if (item.report.createdSidecar === true) {
9856
9876
  this.printer.data(
9857
- tx(BUMP_TEXTS.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9877
+ tx(BUMP_TEXTS2.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9858
9878
  );
9859
9879
  } else {
9860
9880
  this.printer.data(
9861
- tx(BUMP_TEXTS.bumped, { glyph: okGlyph, nodePath: node.path, version })
9881
+ tx(BUMP_TEXTS2.bumped, { glyph: okGlyph, nodePath: node.path, version })
9862
9882
  );
9863
9883
  }
9864
9884
  return ExitCode.Ok;
@@ -9870,7 +9890,7 @@ var BumpCommand = class extends SmCommand {
9870
9890
  const stale = nodes.filter((n) => n.sidecar?.present === true && n.sidecar.status !== null && n.sidecar.status !== "fresh").sort((a, b) => a.path.localeCompare(b.path));
9871
9891
  if (stale.length === 0) return this.#renderEmptyPending();
9872
9892
  if (!this.json) {
9873
- this.printer.info(tx(BUMP_TEXTS.pendingBanner, { count: stale.length }));
9893
+ this.printer.info(tx(BUMP_TEXTS2.pendingBanner, { count: stale.length }));
9874
9894
  }
9875
9895
  const plan = computeBumpPlan(stale, { cwd, force: this.force });
9876
9896
  const outcomes = await this.#executePending(plan, cwd, ansi);
@@ -9888,19 +9908,19 @@ var BumpCommand = class extends SmCommand {
9888
9908
  const gitOk = ensureGitForStaged(cwd);
9889
9909
  if (gitOk === "no-repo") {
9890
9910
  this.printer.error(
9891
- tx(BUMP_TEXTS.notInGitRepo, {
9911
+ tx(BUMP_TEXTS2.notInGitRepo, {
9892
9912
  glyph: errGlyph,
9893
9913
  cwd,
9894
- hint: ansi.dim(BUMP_TEXTS.notInGitRepoHint)
9914
+ hint: ansi.dim(BUMP_TEXTS2.notInGitRepoHint)
9895
9915
  })
9896
9916
  );
9897
9917
  return ExitCode.NotFound;
9898
9918
  }
9899
9919
  if (gitOk === "no-binary") {
9900
9920
  this.printer.error(
9901
- tx(BUMP_TEXTS.gitBinaryMissing, {
9921
+ tx(BUMP_TEXTS2.gitBinaryMissing, {
9902
9922
  glyph: errGlyph,
9903
- hint: ansi.dim(BUMP_TEXTS.gitBinaryMissingHint)
9923
+ hint: ansi.dim(BUMP_TEXTS2.gitBinaryMissingHint)
9904
9924
  })
9905
9925
  );
9906
9926
  return ExitCode.Error;
@@ -9938,7 +9958,7 @@ var BumpCommand = class extends SmCommand {
9938
9958
  return {
9939
9959
  nodePath: item.nodePath,
9940
9960
  status: "error",
9941
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9961
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9942
9962
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9943
9963
  message: formatErrorMessage(applied.error)
9944
9964
  })
@@ -9958,11 +9978,11 @@ var BumpCommand = class extends SmCommand {
9958
9978
  const addErr = stageSidecar(cwd, sidecarPath);
9959
9979
  if (addErr === null || this.json) return;
9960
9980
  this.printer.warn(
9961
- tx(BUMP_TEXTS.gitAddFailed, {
9981
+ tx(BUMP_TEXTS2.gitAddFailed, {
9962
9982
  glyph: ansi.yellow("\u26A0"),
9963
9983
  path: sidecarPath,
9964
9984
  message: addErr,
9965
- hint: ansi.dim(tx(BUMP_TEXTS.gitAddFailedHint, { path: sidecarPath }))
9985
+ hint: ansi.dim(tx(BUMP_TEXTS2.gitAddFailedHint, { path: sidecarPath }))
9966
9986
  })
9967
9987
  );
9968
9988
  }
@@ -9982,7 +10002,7 @@ var BumpCommand = class extends SmCommand {
9982
10002
  this.printer.data(JSON.stringify(empty) + "\n");
9983
10003
  return ExitCode.Ok;
9984
10004
  }
9985
- this.printer.data(BUMP_TEXTS.pendingNone);
10005
+ this.printer.data(BUMP_TEXTS2.pendingNone);
9986
10006
  return ExitCode.Ok;
9987
10007
  }
9988
10008
  // Complexity is from per-status rendering (4 status values) plus
@@ -10013,24 +10033,24 @@ var BumpCommand = class extends SmCommand {
10013
10033
  for (const o of outcomes) {
10014
10034
  if (o.status === "bumped") {
10015
10035
  this.printer.data(
10016
- tx(BUMP_TEXTS.bumpedItem, {
10036
+ tx(BUMP_TEXTS2.bumpedItem, {
10017
10037
  nodePath: o.nodePath,
10018
10038
  version: o.version ?? 0,
10019
10039
  createdSuffix: o.createdSidecar === true ? " (new sidecar)" : ""
10020
10040
  })
10021
10041
  );
10022
10042
  } else if (o.status === "refused") {
10023
- this.printer.data(tx(BUMP_TEXTS.refusedItem, { nodePath: o.nodePath }));
10043
+ this.printer.data(tx(BUMP_TEXTS2.refusedItem, { nodePath: o.nodePath }));
10024
10044
  } else if (o.status === "skipped") {
10025
10045
  this.printer.data(
10026
- tx(BUMP_TEXTS.skippedItem, {
10046
+ tx(BUMP_TEXTS2.skippedItem, {
10027
10047
  nodePath: o.nodePath,
10028
10048
  reason: o.reason ?? "unknown"
10029
10049
  })
10030
10050
  );
10031
10051
  } else {
10032
10052
  this.printer.data(
10033
- tx(BUMP_TEXTS.errorItem, {
10053
+ tx(BUMP_TEXTS2.errorItem, {
10034
10054
  nodePath: o.nodePath,
10035
10055
  message: o.message ?? ""
10036
10056
  })
@@ -10038,7 +10058,7 @@ var BumpCommand = class extends SmCommand {
10038
10058
  }
10039
10059
  }
10040
10060
  this.printer.info(
10041
- tx(BUMP_TEXTS.pendingSummary, {
10061
+ tx(BUMP_TEXTS2.pendingSummary, {
10042
10062
  bumped: counts.bumped,
10043
10063
  refused: counts.refused,
10044
10064
  skipped: counts.skipped,
@@ -11091,29 +11111,39 @@ function isPluginLocked(idOrQualified) {
11091
11111
  }
11092
11112
 
11093
11113
  // kernel/config/plugin-resolver.ts
11094
- function resolvePluginEnabled(pluginId, cfg, dbOverrides) {
11114
+ var SHIPS_DISABLED = /* @__PURE__ */ new Set([
11115
+ "experimental",
11116
+ "deprecated"
11117
+ ]);
11118
+ function installedDefaultEnabled(stability) {
11119
+ return stability === void 0 || !SHIPS_DISABLED.has(stability);
11120
+ }
11121
+ function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = true) {
11095
11122
  if (isPluginLocked(pluginId)) return true;
11096
11123
  if (dbOverrides.has(pluginId)) return dbOverrides.get(pluginId) === true;
11097
11124
  const settingsEntry = cfg.plugins[pluginId];
11098
11125
  if (settingsEntry?.enabled !== void 0) return settingsEntry.enabled;
11099
- return true;
11126
+ return installedDefault;
11100
11127
  }
11101
11128
  function makeEnabledResolver(cfg, dbOverrides) {
11102
- return (pluginId) => resolvePluginEnabled(pluginId, cfg, dbOverrides);
11129
+ return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
11103
11130
  }
11104
11131
 
11105
11132
  // core/runtime/plugin-runtime/resolver.ts
11106
- function defaultResolveEnabled(_id) {
11107
- return true;
11133
+ function defaultResolveEnabled(_id, installedDefault = true) {
11134
+ return installedDefault;
11108
11135
  }
11109
11136
  function isBuiltInExtensionEnabled(plugin, ext, resolveEnabled) {
11110
- return isPluginEntryEnabled(plugin, ext.id, resolveEnabled);
11137
+ return isPluginEntryEnabled(plugin, ext.id, resolveEnabled, ext.stability);
11111
11138
  }
11112
- function isPluginEntryEnabled(plugin, extId, resolveEnabled) {
11113
- return resolveEnabled(qualifiedExtensionId(plugin.id, extId));
11139
+ function isPluginEntryEnabled(plugin, extId, resolveEnabled, stability) {
11140
+ return resolveEnabled(qualifiedExtensionId(plugin.id, extId), installedDefaultEnabled(stability));
11114
11141
  }
11115
11142
  function isPluginExtensionEnabled(ext, resolveEnabled) {
11116
- return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
11143
+ return resolveEnabled(
11144
+ qualifiedExtensionId(ext.pluginId, ext.id),
11145
+ installedDefaultEnabled(ext.stability)
11146
+ );
11117
11147
  }
11118
11148
  async function buildEnabledResolver(ctx) {
11119
11149
  const { effective: cfg } = loadConfig({ ...ctx });
@@ -11311,11 +11341,11 @@ async function* walkContent(roots, options) {
11311
11341
  const extensions = options.extensions;
11312
11342
  const sizeLimit = buildSizeLimit(options);
11313
11343
  for (const root of roots) {
11314
- for await (const file of walkRoot(root, root, filter, extensions, sizeLimit)) {
11315
- const relPath = relative2(root, file).split(sep3).join("/");
11344
+ for await (const entry of walkRoot(root, root, filter, extensions, sizeLimit)) {
11345
+ const relPath = relative2(root, entry.full).split(sep3).join("/");
11316
11346
  let raw;
11317
11347
  try {
11318
- raw = await readFile(file, "utf8");
11348
+ raw = await readFile(entry.full, "utf8");
11319
11349
  } catch {
11320
11350
  continue;
11321
11351
  }
@@ -11325,6 +11355,9 @@ async function* walkContent(roots, options) {
11325
11355
  body: parsed.body,
11326
11356
  frontmatterRaw: parsed.frontmatterRaw,
11327
11357
  frontmatter: parsed.frontmatter,
11358
+ // File mtime from the TOCTOU `lstat` (zero extra syscalls).
11359
+ // Threaded onto the persisted `Node` as `modifiedAtMs`.
11360
+ modifiedAtMs: entry.modifiedAtMs,
11328
11361
  // Audit L1: forward parser diagnostics (e.g. malformed YAML)
11329
11362
  // through the IRawNode surface so the orchestrator can
11330
11363
  // convert them into warn-level kernel `Issue` rows. Omitted
@@ -11365,7 +11398,7 @@ async function* walkRoot(root, current, filter, extensions, sizeLimit) {
11365
11398
  sizeLimit.onOversizedFile?.({ path: rel, bytes: s.size });
11366
11399
  continue;
11367
11400
  }
11368
- yield full;
11401
+ yield { full, modifiedAtMs: Math.round(s.mtimeMs) };
11369
11402
  } catch {
11370
11403
  }
11371
11404
  }
@@ -11447,8 +11480,8 @@ function bucketLoaded(loaded, runtime, pluginOrder) {
11447
11480
  extractor: runtime.extensions.extractors,
11448
11481
  analyzer: runtime.extensions.analyzers,
11449
11482
  formatter: runtime.extensions.formatters,
11450
- hook: runtime.extensions.hooks
11451
- // `action` intentionally absent, see docstring.
11483
+ hook: runtime.extensions.hooks,
11484
+ action: runtime.extensions.actions
11452
11485
  });
11453
11486
  runtime.manifests.push({
11454
11487
  id: ext.id,
@@ -11563,7 +11596,7 @@ async function loadPluginRuntime(opts = {}) {
11563
11596
  const loader = createPluginLoader(loaderOpts);
11564
11597
  const discovered = await loader.discoverAndLoadAll();
11565
11598
  const runtime = {
11566
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11599
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11567
11600
  annotationContributions: [],
11568
11601
  viewContributions: [],
11569
11602
  manifests: [],
@@ -11618,7 +11651,7 @@ function enforceRootExclusivity(catalog) {
11618
11651
  }
11619
11652
  function emptyPluginRuntime() {
11620
11653
  const runtime = {
11621
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11654
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11622
11655
  annotationContributions: [],
11623
11656
  viewContributions: [],
11624
11657
  manifests: [],
@@ -11636,23 +11669,26 @@ function emptyPluginRuntime() {
11636
11669
  function collectRegisteredContributionKeys(composed) {
11637
11670
  const keys = /* @__PURE__ */ new Set();
11638
11671
  if (!composed) return keys;
11639
- for (const ext of [...composed.extractors, ...composed.analyzers]) {
11640
- const raw = ext.ui;
11641
- if (typeof raw !== "object" || raw === null) continue;
11642
- for (const [contributionId, value] of Object.entries(raw)) {
11643
- if (typeof value !== "object" || value === null) continue;
11644
- keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11645
- }
11672
+ for (const ext of [...composed.extractors, ...composed.analyzers, ...composed.actions ?? []]) {
11673
+ addContributionKeysForExtension(ext, keys);
11646
11674
  }
11647
11675
  return keys;
11648
11676
  }
11677
+ function addContributionKeysForExtension(ext, keys) {
11678
+ const raw = ext.ui;
11679
+ if (typeof raw !== "object" || raw === null) return;
11680
+ for (const [contributionId, value] of Object.entries(raw)) {
11681
+ if (typeof value !== "object" || value === null) continue;
11682
+ keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11683
+ }
11684
+ }
11649
11685
  function filterBuiltInManifests(manifests, resolveEnabled) {
11650
11686
  const pluginById = /* @__PURE__ */ new Map();
11651
11687
  for (const plugin of builtInPlugins) pluginById.set(plugin.id, plugin);
11652
11688
  return manifests.filter((m) => {
11653
11689
  const plugin = pluginById.get(m.pluginId);
11654
11690
  if (!plugin) return true;
11655
- return isPluginEntryEnabled(plugin, m.id, resolveEnabled);
11691
+ return isPluginEntryEnabled(plugin, m.id, resolveEnabled, m.stability);
11656
11692
  });
11657
11693
  }
11658
11694
 
@@ -11663,9 +11699,10 @@ function composeScanExtensions(opts) {
11663
11699
  const extractors = [];
11664
11700
  const analyzers = [];
11665
11701
  const hooks = [];
11702
+ const actions = [];
11666
11703
  if (!opts.noBuiltIns) {
11667
11704
  accumulateBuiltInScanExtensions(
11668
- { providers, extractors, analyzers, hooks },
11705
+ { providers, extractors, analyzers, hooks, actions },
11669
11706
  resolveEnabled
11670
11707
  );
11671
11708
  }
@@ -11681,6 +11718,9 @@ function composeScanExtensions(opts) {
11681
11718
  for (const ext of opts.pluginRuntime.extensions.hooks) {
11682
11719
  if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
11683
11720
  }
11721
+ for (const ext of opts.pluginRuntime.extensions.actions) {
11722
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) actions.push(ext);
11723
+ }
11684
11724
  const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
11685
11725
  const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
11686
11726
  const finalAnalyzers = opts.killSwitches?.analyzers === true ? [] : analyzers;
@@ -11691,7 +11731,8 @@ function composeScanExtensions(opts) {
11691
11731
  providers: finalProviders,
11692
11732
  extractors: finalExtractors,
11693
11733
  analyzers: finalAnalyzers,
11694
- hooks
11734
+ hooks,
11735
+ actions
11695
11736
  };
11696
11737
  }
11697
11738
  function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
@@ -11712,6 +11753,7 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11712
11753
  buckets.hooks.push(ext);
11713
11754
  break;
11714
11755
  case "action":
11756
+ buckets.actions.push(ext);
11715
11757
  break;
11716
11758
  case "formatter":
11717
11759
  break;
@@ -16155,12 +16197,87 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
16155
16197
  });
16156
16198
  source.externalRefs = refs;
16157
16199
  }
16158
- }
16159
- var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
16160
- var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
16161
- function isExternalUrlLink(link) {
16162
- if (VIRTUAL_NODE_SCHEME_RE.test(link.target)) return false;
16163
- return EXTERNAL_URL_SCHEME_RE.test(link.target);
16200
+ }
16201
+ var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
16202
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
16203
+ function isExternalUrlLink(link) {
16204
+ if (VIRTUAL_NODE_SCHEME_RE.test(link.target)) return false;
16205
+ return EXTERNAL_URL_SCHEME_RE.test(link.target);
16206
+ }
16207
+
16208
+ // kernel/orchestrator/action-projections.ts
16209
+ function runActionProjections(actions, nodes, links, emitter) {
16210
+ const contributions = [];
16211
+ const contributionErrors = [];
16212
+ const validators = loadSchemaValidators();
16213
+ for (const action of actions) {
16214
+ if (typeof action.project !== "function") continue;
16215
+ const qualifiedId2 = qualifiedExtensionId(action.pluginId, action.id);
16216
+ const declaredContributions = readDeclaredContributionRefs(action);
16217
+ const emitContribution = (nodePath, ref, payload) => {
16218
+ const declared = typeof ref === "object" && ref !== null ? declaredContributions.get(ref) : void 0;
16219
+ if (!declared) {
16220
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
16221
+ extractorId: qualifiedId2,
16222
+ nodePath
16223
+ });
16224
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16225
+ phase: "emitContribution",
16226
+ reason: "undeclared-contribution-ref",
16227
+ message
16228
+ });
16229
+ contributionErrors.push({
16230
+ pluginId: action.pluginId,
16231
+ extensionId: action.id,
16232
+ nodePath,
16233
+ reason: "undeclared-contribution-ref",
16234
+ message,
16235
+ emittedAt: Date.now()
16236
+ });
16237
+ return;
16238
+ }
16239
+ const result = validators.validateContributionPayload(declared.slot, payload);
16240
+ if (!result.ok) {
16241
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
16242
+ extractorId: qualifiedId2,
16243
+ contributionId: declared.id,
16244
+ nodePath,
16245
+ slot: declared.slot,
16246
+ errors: result.errors
16247
+ });
16248
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16249
+ phase: "emitContribution",
16250
+ contributionId: declared.id,
16251
+ slot: declared.slot,
16252
+ reason: result.errors,
16253
+ message
16254
+ });
16255
+ contributionErrors.push({
16256
+ pluginId: action.pluginId,
16257
+ extensionId: action.id,
16258
+ nodePath,
16259
+ reason: result.errors,
16260
+ message,
16261
+ contributionId: declared.id,
16262
+ slot: declared.slot,
16263
+ emittedAt: Date.now()
16264
+ });
16265
+ return;
16266
+ }
16267
+ contributions.push({
16268
+ pluginId: action.pluginId,
16269
+ extensionId: action.id,
16270
+ nodePath,
16271
+ contributionId: declared.id,
16272
+ slot: declared.slot,
16273
+ payload,
16274
+ emittedAt: Date.now()
16275
+ });
16276
+ };
16277
+ const ctx = { nodes, links, emitContribution };
16278
+ action.project(ctx);
16279
+ }
16280
+ return { contributions, contributionErrors };
16164
16281
  }
16165
16282
 
16166
16283
  // kernel/orchestrator/analyzers.ts
@@ -16952,6 +17069,7 @@ function buildNode(args2) {
16952
17069
  externalRefsCount: 0,
16953
17070
  frontmatter: args2.frontmatter
16954
17071
  };
17072
+ if (args2.modifiedAtMs !== void 0) node.modifiedAtMs = args2.modifiedAtMs;
16955
17073
  if (args2.encoder) {
16956
17074
  node.tokens = countTokens(args2.encoder, args2.frontmatterRaw, args2.body);
16957
17075
  }
@@ -17067,7 +17185,10 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
17067
17185
  frontmatter: opts.raw.frontmatter,
17068
17186
  bodyHash: opts.bodyHash,
17069
17187
  frontmatterHash: opts.frontmatterHash,
17070
- encoder: opts.encoder
17188
+ encoder: opts.encoder,
17189
+ // Thread the walker's mtime through; `buildNode` only attaches it
17190
+ // when present, so virtual / walk()-without-stat sources stay absent.
17191
+ modifiedAtMs: opts.raw.modifiedAtMs
17071
17192
  });
17072
17193
  const frontmatterIssues = [];
17073
17194
  if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
@@ -17501,6 +17622,13 @@ async function runScanInternal(_kernel, options) {
17501
17622
  walked.frontmatterIssues
17502
17623
  );
17503
17624
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
17625
+ const projectionResult = runActionProjections(
17626
+ exts.actions ?? [],
17627
+ walked.nodes,
17628
+ walked.internalLinks,
17629
+ emitter
17630
+ );
17631
+ mergeActionProjections(walked, projectionResult, exts.actions);
17504
17632
  const issues = analyzerResult.issues;
17505
17633
  const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
17506
17634
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
@@ -17609,6 +17737,16 @@ function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
17609
17737
  }
17610
17738
  }
17611
17739
  }
17740
+ function mergeActionProjections(walked, projectionResult, actions) {
17741
+ for (const c of projectionResult.contributions) walked.contributions.push(c);
17742
+ for (const e of projectionResult.contributionErrors) walked.contributionErrors.push(e);
17743
+ for (const action of actions ?? []) {
17744
+ if (action.ui === void 0 || typeof action.project !== "function") continue;
17745
+ for (const node of walked.nodes) {
17746
+ walked.freshlyRunTuples.add(`${action.pluginId}\0${action.id}\0${node.path}`);
17747
+ }
17748
+ }
17749
+ }
17612
17750
  function buildScanStats(walked, issues, start) {
17613
17751
  return {
17614
17752
  // `filesSkipped` is "files walked but not classified by any
@@ -20513,6 +20651,17 @@ var PLUGINS_TEXTS = {
20513
20651
  qualifiedIdNotFoundHint: "Run `sm plugins list` to see what each plugin ships.",
20514
20652
  qualifiedIdUnknownPlugin: "{{glyph}} Qualified extension id references unknown plugin: {{pluginId}}\n {{hint}}\n",
20515
20653
  qualifiedIdUnknownPluginHint: "Run `sm plugins list` for known plugin ids.",
20654
+ // --- verb-shape redirects (show is extension-only; list is plugin-only) ---
20655
+ // `sm plugins show` takes a qualified `<plugin>/<ext>` id and renders a
20656
+ // single extension. A bare plugin id is the wrong granularity, redirect
20657
+ // to `sm plugins list <id>`, which renders the whole plugin.
20658
+ showBareId: '{{glyph}} `sm plugins show` needs a qualified `<plugin>/<ext>` id; "{{id}}" is a plugin.\n {{hint}}\n',
20659
+ showBareIdHint: "Run `sm plugins list {{id}}` for the plugin and its extensions, then `sm plugins show {{id}}/<ext>` for one.",
20660
+ // `sm plugins list <id>` takes a bare plugin id. A qualified
20661
+ // `<plugin>/<ext>` id targets a single extension, redirect to
20662
+ // `sm plugins show`.
20663
+ listQualifiedId: "{{glyph}} `sm plugins list` takes a plugin id, not a qualified `<plugin>/<ext>` id: {{id}}\n {{hint}}\n",
20664
+ listQualifiedIdHint: "Run `sm plugins show {{id}}` for that extension, or `sm plugins list {{pluginId}}` for the whole plugin.",
20516
20665
  // Spec § A.10, `applicableKinds` filter on Extractors. When an extractor
20517
20666
  // declares a kind that no installed Provider emits, the load succeeds
20518
20667
  // (the Provider may arrive later) but `sm plugins doctor` surfaces a
@@ -20653,7 +20802,7 @@ var PLUGINS_TEXTS = {
20653
20802
  * (declared or defaulted) renders no tag.
20654
20803
  */
20655
20804
  stabilityTag: " ({{stability}})",
20656
- listTipShow: "\nTip: `sm plugins show <id>` for kinds, versions, and per-extension status.\n",
20805
+ listTipShow: "\nTip: `sm plugins list <id>` for a plugin's extensions (kinds, versions, per-extension status), `sm plugins show <plugin>/<ext>` for one extension.\n",
20657
20806
  /** Show command, built-in header (no version row, no path). */
20658
20807
  detailHeaderBuiltIn: " {{glyph}} {{id}} {{source}} {{count}} extension{{plural}}\n",
20659
20808
  /**
@@ -20805,7 +20954,7 @@ function extensionRowFromBuiltIn(ext, plugin, resolveEnabled) {
20805
20954
  id: ext.id,
20806
20955
  kind: ext.kind,
20807
20956
  version: ext.version,
20808
- enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id)),
20957
+ enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id), installedDefaultEnabled(ext.stability)),
20809
20958
  description: ext.description ?? ""
20810
20959
  };
20811
20960
  if (ext.stability !== void 0) row.stability = ext.stability;
@@ -20822,6 +20971,43 @@ function omitModule(key, value) {
20822
20971
  const tag = value[Symbol.toStringTag];
20823
20972
  return tag === "Module" ? void 0 : value;
20824
20973
  }
20974
+ function pluginCatalogue(plugins) {
20975
+ const out = [];
20976
+ for (const plugin of builtInPlugins) {
20977
+ out.push({ id: plugin.id, extensionIds: plugin.extensions.map((e) => e.id) });
20978
+ }
20979
+ for (const p of plugins) {
20980
+ out.push({ id: p.id, extensionIds: p.extensions?.map((e) => e.id) ?? [] });
20981
+ }
20982
+ return out;
20983
+ }
20984
+ function parseQualifiedExtensionId(id, catalogue) {
20985
+ const [pluginId, extId, ...rest] = id.split("/");
20986
+ if (!pluginId || !extId || rest.length > 0) return { ok: false, reason: "malformed" };
20987
+ const plugin = catalogue.find((p) => p.id === pluginId);
20988
+ if (!plugin) return { ok: false, reason: "unknown-plugin", pluginId };
20989
+ if (!plugin.extensionIds.includes(extId)) {
20990
+ return { ok: false, reason: "unknown-extension", pluginId, extId };
20991
+ }
20992
+ return { ok: true, pluginId, extId };
20993
+ }
20994
+ function renderQualifiedIdError(result, rawId, ansi) {
20995
+ const glyph = ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20996
+ if (result.reason === "unknown-extension") {
20997
+ return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
20998
+ glyph,
20999
+ id: sanitizeForTerminal(rawId),
21000
+ pluginId: sanitizeForTerminal(result.pluginId ?? ""),
21001
+ extId: sanitizeForTerminal(result.extId ?? ""),
21002
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21003
+ });
21004
+ }
21005
+ return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21006
+ glyph,
21007
+ pluginId: sanitizeForTerminal(result.reason === "unknown-plugin" ? result.pluginId ?? rawId : rawId),
21008
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21009
+ });
21010
+ }
20825
21011
  function wrapText(text, maxWidth) {
20826
21012
  const words = text.split(/\s+/).filter((w) => w.length > 0);
20827
21013
  if (words.length === 0) return [];
@@ -20845,14 +21031,24 @@ var PluginsListCommand = class extends SmCommand {
20845
21031
  static paths = [["plugins", "list"]];
20846
21032
  static usage = Command22.Usage({
20847
21033
  category: "Plugins",
20848
- description: "List discovered plugins and their load status.",
20849
- details: "Scans <cwd>/.skill-map/plugins (or --plugin-dir <path>). Built-in plugins (claude, core) are listed alongside user plugins."
21034
+ description: "List discovered plugins, or one plugin's extensions.",
21035
+ details: `
21036
+ No id: scans <cwd>/.skill-map/plugins (or --plugin-dir <path>) and
21037
+ lists every plugin (built-in + user) with status, one row each.
21038
+ With a bare plugin id: renders that plugin's manifest and its
21039
+ extensions (kind / version / per-extension status). A qualified
21040
+ \`<plugin>/<ext>\` id is rejected with a redirect to \`sm plugins show\`.
21041
+ `
20850
21042
  });
21043
+ id = Option21.String({ required: false });
20851
21044
  pluginDir = Option21.String("--plugin-dir", { required: false });
20852
21045
  async run() {
20853
21046
  const plugins = await loadAll({ pluginDir: this.pluginDir });
20854
21047
  const resolveEnabled = await buildResolver();
20855
21048
  const builtIns2 = builtInRows(resolveEnabled);
21049
+ if (this.id !== void 0) {
21050
+ return this.renderPluginDetailById(this.id, builtIns2, plugins);
21051
+ }
20856
21052
  if (this.json) {
20857
21053
  this.printer.data(
20858
21054
  JSON.stringify({ builtIns: builtIns2, plugins }, omitModule, 2) + "\n"
@@ -20864,24 +21060,68 @@ var PluginsListCommand = class extends SmCommand {
20864
21060
  return ExitCode.Ok;
20865
21061
  }
20866
21062
  const ansi = this.ansiFor("stdout");
20867
- this.printer.data(renderListHuman(builtIns2, plugins, resolveEnabled, ansi));
21063
+ this.printer.data(renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi));
21064
+ return ExitCode.Ok;
21065
+ }
21066
+ /**
21067
+ * `sm plugins list <id>`, render one plugin's full detail. A qualified
21068
+ * `<plugin>/<ext>` id is the wrong granularity for `list` (it targets a
21069
+ * single extension), redirect to `sm plugins show`. A bare id that
21070
+ * matches no plugin is a NotFound.
21071
+ */
21072
+ renderPluginDetailById(id, builtIns2, plugins) {
21073
+ const stderrAnsi = this.ansiFor("stderr");
21074
+ if (id.includes("/")) {
21075
+ const pluginId = id.split("/")[0] ?? id;
21076
+ this.printer.error(
21077
+ tx(PLUGINS_TEXTS.listQualifiedId, {
21078
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21079
+ id: sanitizeForTerminal(id),
21080
+ hint: stderrAnsi.dim(
21081
+ tx(PLUGINS_TEXTS.listQualifiedIdHint, {
21082
+ id: sanitizeForTerminal(id),
21083
+ pluginId: sanitizeForTerminal(pluginId)
21084
+ })
21085
+ )
21086
+ })
21087
+ );
21088
+ return ExitCode.Error;
21089
+ }
21090
+ const builtIn = builtIns2.find((b) => b.id === id);
21091
+ const match = plugins.find((p) => p.id === id);
21092
+ if (!builtIn && !match) {
21093
+ this.printer.error(
21094
+ tx(PLUGINS_TEXTS.pluginNotFound, {
21095
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21096
+ id: sanitizeForTerminal(id),
21097
+ hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
21098
+ })
21099
+ );
21100
+ return ExitCode.NotFound;
21101
+ }
21102
+ if (this.json) {
21103
+ const payload = builtIn ?? match;
21104
+ this.printer.data(JSON.stringify(payload, omitModule, 2) + "\n");
21105
+ return ExitCode.Ok;
21106
+ }
21107
+ const ansi = this.ansiFor("stdout");
21108
+ const text = builtIn ? renderBuiltInDetail(builtIn, ansi) : renderPluginDetail(match, ansi);
21109
+ this.printer.data(text);
20868
21110
  return ExitCode.Ok;
20869
21111
  }
20870
21112
  };
20871
- function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
21113
+ function renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi) {
20872
21114
  const rows = [
20873
- ...builtIns2.map(builtInToListRow),
20874
- ...plugins.map((p) => pluginToListRow(p, resolveEnabled))
21115
+ ...builtIns2.map(builtInToIndexRow),
21116
+ ...plugins.map((p) => pluginToIndexRow(p, resolveEnabled))
20875
21117
  ];
20876
21118
  const idWidth = Math.max(...rows.map((r) => r.id.length));
20877
- const countWidth = Math.max(
20878
- ...rows.map((r) => String(r.names.length).length)
20879
- );
21119
+ const countWidth = Math.max(...rows.map((r) => String(r.extCount).length));
20880
21120
  const lines = [];
20881
21121
  for (const row of rows) {
20882
21122
  const glyph = row.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20883
21123
  const idCol = row.id.padEnd(idWidth);
20884
- const countCol = String(row.names.length).padStart(countWidth);
21124
+ const countCol = String(row.extCount).padStart(countWidth);
20885
21125
  lines.push(
20886
21126
  tx(PLUGINS_TEXTS.pluginRow, {
20887
21127
  glyph,
@@ -20890,196 +21130,34 @@ function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
20890
21130
  source: ansi.dim(row.source)
20891
21131
  })
20892
21132
  );
20893
- const indent = PLUGINS_TEXTS.pluginSubIndent;
20894
21133
  if (row.reason) {
20895
- lines.push(`${indent}${ansi.dim(row.reason)}`);
20896
- } else if (row.names.length > 0) {
20897
- for (const wrapped of wrapNames(row.names, indent, 76)) {
20898
- lines.push(`${indent}${ansi.dim(wrapped)}`);
20899
- }
21134
+ lines.push(`${PLUGINS_TEXTS.pluginSubIndent}${ansi.dim(row.reason)}`);
20900
21135
  }
20901
21136
  }
20902
21137
  return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
20903
21138
  }
20904
- function builtInToListRow(b) {
20905
- const names = b.extensions.map((e) => {
20906
- const name = withStabilityTag(e.id, e.stability);
20907
- return e.enabled ? name : `${PLUGINS_TEXTS.rowGlyphOff} ${name}`;
20908
- });
21139
+ function builtInToIndexRow(b) {
20909
21140
  return {
20910
21141
  id: b.id,
20911
21142
  enabled: b.enabled,
20912
21143
  source: PLUGINS_TEXTS.sourceBuiltIn,
20913
- names
21144
+ extCount: b.extensions.length
20914
21145
  };
20915
21146
  }
20916
- function pluginToListRow(p, resolveEnabled) {
21147
+ function pluginToIndexRow(p, resolveEnabled) {
20917
21148
  const isLoaded = p.status === "enabled";
20918
21149
  const extensions = p.extensions ?? [];
20919
- const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
20920
- const names = extensions.map((e) => {
20921
- const safeId = withStabilityTag(sanitizeForTerminal(e.id), e.stability);
20922
- return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
20923
- });
21150
+ const extEnabled = (e) => resolveEnabled(qualifiedExtensionId(p.id, e.id), installedDefaultEnabled(e.stability));
21151
+ const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => extEnabled(e)) : false;
20924
21152
  const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
20925
21153
  return {
20926
21154
  id: sanitizeForTerminal(p.id),
20927
21155
  enabled,
20928
21156
  source: PLUGINS_TEXTS.sourceUser,
20929
- names,
21157
+ extCount: extensions.length,
20930
21158
  reason
20931
21159
  };
20932
21160
  }
20933
- function wrapNames(names, indent, maxWidth) {
20934
- const out = [];
20935
- const sep8 = ", ";
20936
- let current = "";
20937
- for (const name of names) {
20938
- const candidate = current === "" ? name : `${current}${sep8}${name}`;
20939
- if (indent.length + candidate.length > maxWidth && current !== "") {
20940
- out.push(`${current},`);
20941
- current = name;
20942
- } else {
20943
- current = candidate;
20944
- }
20945
- }
20946
- if (current !== "") out.push(current);
20947
- return out;
20948
- }
20949
-
20950
- // cli/commands/plugins/show.ts
20951
- import { Command as Command23, Option as Option22 } from "clipanion";
20952
- var PluginsShowCommand = class extends SmCommand {
20953
- static paths = [["plugins", "show"]];
20954
- static usage = Command23.Usage({
20955
- category: "Plugins",
20956
- description: "Show a single plugin's manifest + loaded extensions.",
20957
- details: `
20958
- Accepts a plugin id (\`core\`, \`claude\`, \`my-plugin\`)
20959
- or a qualified extension id (\`core/<ext-id>\`,
20960
- \`<plugin>/<ext-id>\`). When given a qualified id, validates the
20961
- extension exists and renders a single-extension detail block.
20962
- The bare form renders the parent plugin's detail with per-extension
20963
- status. The same id shapes \`sm plugins enable\` and
20964
- \`sm plugins disable\` accept resolve cleanly here too.
20965
- `
20966
- });
20967
- id = Option22.String({ required: true });
20968
- pluginDir = Option22.String("--plugin-dir", { required: false });
20969
- async run() {
20970
- const plugins = await loadAll({ pluginDir: this.pluginDir });
20971
- const resolveEnabled = await buildResolver();
20972
- const builtIns2 = builtInRows(resolveEnabled);
20973
- const stderrAnsi = this.ansiFor("stderr");
20974
- const lookupResult = resolveShowLookupId(this.id, builtIns2, plugins, stderrAnsi);
20975
- if ("error" in lookupResult) {
20976
- this.printer.error(lookupResult.error);
20977
- return ExitCode.NotFound;
20978
- }
20979
- const { pluginId, extId } = lookupResult;
20980
- const builtIn = builtIns2.find((b) => b.id === pluginId);
20981
- const match = plugins.find((p) => p.id === pluginId);
20982
- if (!builtIn && !match) {
20983
- this.printer.error(
20984
- tx(PLUGINS_TEXTS.pluginNotFound, {
20985
- glyph: stderrAnsi.red("\u2715"),
20986
- id: sanitizeForTerminal(this.id),
20987
- hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
20988
- })
20989
- );
20990
- return ExitCode.NotFound;
20991
- }
20992
- if (extId !== void 0) {
20993
- return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
20994
- }
20995
- if (this.json) {
20996
- const payload = builtIn ?? match;
20997
- this.printer.data(JSON.stringify(payload, omitModule, 2) + "\n");
20998
- return ExitCode.Ok;
20999
- }
21000
- const ansi = this.ansiFor("stdout");
21001
- const text = builtIn ? renderBuiltInDetail(builtIn, ansi) : renderPluginDetail(match, ansi);
21002
- this.printer.data(text);
21003
- return ExitCode.Ok;
21004
- }
21005
- /**
21006
- * Render the single-extension detail block, the path taken when the
21007
- * user supplies a qualified `<plugin>/<ext>` id. `--json` emits the
21008
- * single extension row (no surrounding plugin envelope) so tooling
21009
- * can pipe straight into `jq`; human mode renders a focused header
21010
- * plus a Kind / Version / Stability / Description / Preconditions /
21011
- * Entry field block.
21012
- */
21013
- renderExtensionDetail(args2) {
21014
- const { extId, pluginId, builtIn, match } = args2;
21015
- const ansi = this.ansiFor("stdout");
21016
- if (builtIn) {
21017
- const ext = builtIn.extensions.find((e) => e.id === extId);
21018
- if (!ext) return ExitCode.NotFound;
21019
- if (this.json) {
21020
- this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
21021
- return ExitCode.Ok;
21022
- }
21023
- this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
21024
- return ExitCode.Ok;
21025
- }
21026
- const userExt = match?.extensions?.find((e) => e.id === extId);
21027
- if (!userExt) return ExitCode.NotFound;
21028
- if (this.json) {
21029
- this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
21030
- return ExitCode.Ok;
21031
- }
21032
- this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
21033
- return ExitCode.Ok;
21034
- }
21035
- };
21036
- function resolveShowLookupId(id, builtIns2, plugins, ansi) {
21037
- if (!id.includes("/")) return { pluginId: id };
21038
- const parsed = parseQualifiedId(id);
21039
- if ("error" in parsed) return { error: malformedQualifiedError(id, ansi) };
21040
- const { pluginId, extId } = parsed;
21041
- const knownExts = collectKnownExtensions(pluginId, builtIns2, plugins);
21042
- if (knownExts === null) return { error: unknownPluginError(pluginId, ansi) };
21043
- if (!knownExts.includes(extId)) {
21044
- return { error: unknownExtensionError(id, pluginId, extId, ansi) };
21045
- }
21046
- return { pluginId, extId };
21047
- }
21048
- function parseQualifiedId(id) {
21049
- const [pluginId, extId, ...rest] = id.split("/");
21050
- if (!pluginId || !extId || rest.length > 0) return { error: true };
21051
- return { pluginId, extId };
21052
- }
21053
- function collectKnownExtensions(pluginId, builtIns2, plugins) {
21054
- const builtIn = builtIns2.find((b) => b.id === pluginId);
21055
- if (builtIn) return builtIn.extensions.map((e) => e.id);
21056
- const userPlugin = plugins.find((p) => p.id === pluginId);
21057
- if (userPlugin) return userPlugin.extensions?.map((e) => e.id) ?? [];
21058
- return null;
21059
- }
21060
- function malformedQualifiedError(id, ansi) {
21061
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21062
- glyph: ansi.red("\u2715"),
21063
- pluginId: sanitizeForTerminal(id),
21064
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21065
- });
21066
- }
21067
- function unknownPluginError(pluginId, ansi) {
21068
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21069
- glyph: ansi.red("\u2715"),
21070
- pluginId: sanitizeForTerminal(pluginId),
21071
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21072
- });
21073
- }
21074
- function unknownExtensionError(id, pluginId, extId, ansi) {
21075
- return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
21076
- glyph: ansi.red("\u2715"),
21077
- id: sanitizeForTerminal(id),
21078
- pluginId: sanitizeForTerminal(pluginId),
21079
- extId: sanitizeForTerminal(extId),
21080
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21081
- });
21082
- }
21083
21161
  function kindIndex(kind) {
21084
21162
  const idx = EXTENSION_KINDS.indexOf(kind);
21085
21163
  return idx === -1 ? EXTENSION_KINDS.length : idx;
@@ -21203,6 +21281,83 @@ function renderExtensionItems(items) {
21203
21281
  }
21204
21282
  return out.join("");
21205
21283
  }
21284
+
21285
+ // cli/commands/plugins/show.ts
21286
+ import { Command as Command23, Option as Option22 } from "clipanion";
21287
+ var PluginsShowCommand = class extends SmCommand {
21288
+ static paths = [["plugins", "show"]];
21289
+ static usage = Command23.Usage({
21290
+ category: "Plugins",
21291
+ description: "Show a single extension's detail.",
21292
+ details: `
21293
+ Accepts a qualified extension id (\`core/<ext-id>\`,
21294
+ \`<plugin>/<ext-id>\`) and renders a single-extension detail block
21295
+ (Kind / Version / Stability / Description / Preconditions / Entry).
21296
+ A bare plugin id is rejected with a redirect to
21297
+ \`sm plugins list <id>\`, which renders the whole plugin. The same
21298
+ qualified id shape \`sm plugins enable\` and \`sm plugins disable\`
21299
+ accept resolves cleanly here too.
21300
+ `
21301
+ });
21302
+ id = Option22.String({ required: true });
21303
+ pluginDir = Option22.String("--plugin-dir", { required: false });
21304
+ async run() {
21305
+ const plugins = await loadAll({ pluginDir: this.pluginDir });
21306
+ const resolveEnabled = await buildResolver();
21307
+ const builtIns2 = builtInRows(resolveEnabled);
21308
+ const stderrAnsi = this.ansiFor("stderr");
21309
+ if (!this.id.includes("/")) {
21310
+ this.printer.error(
21311
+ tx(PLUGINS_TEXTS.showBareId, {
21312
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21313
+ id: sanitizeForTerminal(this.id),
21314
+ hint: stderrAnsi.dim(
21315
+ tx(PLUGINS_TEXTS.showBareIdHint, { id: sanitizeForTerminal(this.id) })
21316
+ )
21317
+ })
21318
+ );
21319
+ return ExitCode.Error;
21320
+ }
21321
+ const parsed = parseQualifiedExtensionId(this.id, pluginCatalogue(plugins));
21322
+ if (!parsed.ok) {
21323
+ this.printer.error(renderQualifiedIdError(parsed, this.id, stderrAnsi));
21324
+ return ExitCode.NotFound;
21325
+ }
21326
+ const { pluginId, extId } = parsed;
21327
+ const builtIn = builtIns2.find((b) => b.id === pluginId);
21328
+ const match = plugins.find((p) => p.id === pluginId);
21329
+ return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
21330
+ }
21331
+ /**
21332
+ * Render the single-extension detail block. `--json` emits the single
21333
+ * extension row (no surrounding plugin envelope) so tooling can pipe
21334
+ * straight into `jq`; human mode renders a focused header plus a
21335
+ * Kind / Version / Stability / Description / Preconditions / Entry
21336
+ * field block.
21337
+ */
21338
+ renderExtensionDetail(args2) {
21339
+ const { extId, pluginId, builtIn, match } = args2;
21340
+ const ansi = this.ansiFor("stdout");
21341
+ if (builtIn) {
21342
+ const ext = builtIn.extensions.find((e) => e.id === extId);
21343
+ if (!ext) return ExitCode.NotFound;
21344
+ if (this.json) {
21345
+ this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
21346
+ return ExitCode.Ok;
21347
+ }
21348
+ this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
21349
+ return ExitCode.Ok;
21350
+ }
21351
+ const userExt = match?.extensions?.find((e) => e.id === extId);
21352
+ if (!userExt) return ExitCode.NotFound;
21353
+ if (this.json) {
21354
+ this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
21355
+ return ExitCode.Ok;
21356
+ }
21357
+ this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
21358
+ return ExitCode.Ok;
21359
+ }
21360
+ };
21206
21361
  function renderBuiltInExtensionDetail(pluginId, ext, ansi) {
21207
21362
  const glyph = ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21208
21363
  const header = tx(PLUGINS_TEXTS.detailHeaderExtensionBuiltIn, {
@@ -22039,61 +22194,15 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
22039
22194
  return this.toggle(false);
22040
22195
  }
22041
22196
  };
22042
- function pluginCatalogue(plugins) {
22043
- const out = [];
22044
- for (const plugin of builtInPlugins) {
22045
- out.push({
22046
- id: plugin.id,
22047
- extensionIds: plugin.extensions.map((e) => e.id)
22048
- });
22049
- }
22050
- for (const p of plugins) {
22051
- out.push({
22052
- id: p.id,
22053
- extensionIds: p.extensions?.map((e) => e.id) ?? []
22054
- });
22055
- }
22056
- return out;
22057
- }
22058
22197
  function resolveToggleTarget(id, catalogue, ansi) {
22059
22198
  return id.includes("/") ? resolveQualifiedToggle(id, catalogue, ansi) : resolveBareToggle(id, catalogue);
22060
22199
  }
22061
22200
  function resolveQualifiedToggle(id, catalogue, ansi) {
22062
- const errGlyph = ansi.red("\u2715");
22063
- const [pluginId, extId, ...rest] = id.split("/");
22064
- if (!pluginId || !extId || rest.length > 0) {
22065
- return {
22066
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
22067
- glyph: errGlyph,
22068
- pluginId: sanitizeForTerminal(id),
22069
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
22070
- })
22071
- };
22072
- }
22073
- const plugin = catalogue.find((b) => b.id === pluginId);
22074
- if (!plugin) {
22075
- return {
22076
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
22077
- glyph: errGlyph,
22078
- pluginId: sanitizeForTerminal(pluginId),
22079
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
22080
- })
22081
- };
22082
- }
22083
- if (!plugin.extensionIds.includes(extId)) {
22084
- return {
22085
- error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
22086
- glyph: errGlyph,
22087
- id: sanitizeForTerminal(id),
22088
- pluginId: sanitizeForTerminal(pluginId),
22089
- extId: sanitizeForTerminal(extId),
22090
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
22091
- })
22092
- };
22093
- }
22201
+ const parsed = parseQualifiedExtensionId(id, catalogue);
22202
+ if (!parsed.ok) return { error: renderQualifiedIdError(parsed, id, ansi) };
22094
22203
  return {
22095
22204
  origin: "qualified",
22096
- keys: [qualifiedExtensionId(pluginId, extId)]
22205
+ keys: [qualifiedExtensionId(parsed.pluginId, parsed.extId)]
22097
22206
  };
22098
22207
  }
22099
22208
  function resolveBareToggle(id, catalogue) {
@@ -22473,7 +22582,7 @@ Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\`
22473
22582
 
22474
22583
  ## Verbs
22475
22584
 
22476
- - \`sm plugins show ${pluginId}\`: manifest + load status
22585
+ - \`sm plugins list ${pluginId}\`: manifest + extensions + load status
22477
22586
  - \`sm plugins doctor\`: full plugin diagnostic
22478
22587
  - \`sm scan\`: re-emit contributions / re-run analysis
22479
22588
 
@@ -25925,7 +26034,7 @@ function buildBuiltInItems(resolveEnabled) {
25925
26034
  id: ext.id,
25926
26035
  kind: ext.kind,
25927
26036
  version: ext.version,
25928
- enabled: resolveEnabled(qualified),
26037
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25929
26038
  ...ext.description ? { description: ext.description } : {},
25930
26039
  ...ext.stability ? { stability: ext.stability } : {},
25931
26040
  ...extLocked ? { locked: true } : {}
@@ -25981,7 +26090,7 @@ function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
25981
26090
  id: ext.id,
25982
26091
  kind: ext.kind,
25983
26092
  version: ext.version,
25984
- enabled: resolveEnabled(qualified),
26093
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25985
26094
  ...description ? { description } : {},
25986
26095
  ...ext.stability ? { stability: ext.stability } : {},
25987
26096
  ...extLocked ? { locked: true } : {}
@@ -30469,4 +30578,4 @@ function resolveBareDefault() {
30469
30578
  process.exit(ExitCode.Error);
30470
30579
  }
30471
30580
  //# sourceMappingURL=cli.js.map
30472
- //# debugId=3118bb11-aad2-50b2-b70d-ed15c2f72a97
30581
+ //# debugId=e21852cd-d92e-5bdf-abc7-02fffaa89502