@skill-map/cli 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/sm.js +3 -3
- package/dist/cli/tutorial/sm-tutorial.md +22 -4
- package/dist/cli.js +1450 -686
- package/dist/cli.js.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.js.map +1 -1
- package/dist/index.js +155 -60
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +383 -247
- package/dist/kernel/index.js +155 -60
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/{chunk-GXRWH2VL.js → chunk-2TPMJJYQ.js} +1 -1
- package/dist/ui/chunk-3RAME7PF.js +251 -0
- package/dist/ui/chunk-4BVLXZO3.js +61 -0
- package/dist/ui/{chunk-MPMBTIUR.js → chunk-BMAKIDAV.js} +30 -30
- package/dist/ui/chunk-C7PRRCVD.js +123 -0
- package/dist/ui/{chunk-VVOEPDQD.js → chunk-GJJZ5QH6.js} +1 -1
- package/dist/ui/chunk-I7EELB7M.js +1 -0
- package/dist/ui/{chunk-OPPQMCMQ.js → chunk-KRNW54CI.js} +1 -1
- package/dist/ui/chunk-NJ4PSNK3.js +965 -0
- package/dist/ui/{chunk-W2EFGI3J.js → chunk-OU26UMVW.js} +1 -1
- package/dist/ui/chunk-SCSYN7U2.js +1 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/main-WQA6J5V5.js +2 -0
- package/dist/ui/{styles-M2FETVAG.css → styles-ALBMEXCF.css} +1 -1
- package/package.json +4 -4
- package/dist/ui/chunk-25AWRVIC.js +0 -965
- package/dist/ui/chunk-GETTEQ3S.js +0 -123
- package/dist/ui/chunk-HC6PNQMW.js +0 -251
- package/dist/ui/chunk-HJHWJTFH.js +0 -1
- package/dist/ui/chunk-MF2M6GYF.js +0 -1
- package/dist/ui/chunk-V3SZQETX.js +0 -61
- package/dist/ui/main-Q2WC254P.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -606,7 +606,7 @@ var geminiProvider = {
|
|
|
606
606
|
// registry entries (they ship later under the Gemini bundle), but
|
|
607
607
|
// the qualified form is the contract.
|
|
608
608
|
//
|
|
609
|
-
// UI presentation: kind visuals are normalised across Providers
|
|
609
|
+
// UI presentation: kind visuals are normalised across Providers, every
|
|
610
610
|
// Provider that contributes `agent` declares the same color + icon as
|
|
611
611
|
// Claude, every Provider that contributes `skill` declares the same
|
|
612
612
|
// color + icon as Claude, etc. The declaration STAYS per-Provider (the
|
|
@@ -665,7 +665,7 @@ var agentSkillsProvider = {
|
|
|
665
665
|
pluginId: "agent-skills",
|
|
666
666
|
kind: "provider",
|
|
667
667
|
version: "1.0.0",
|
|
668
|
-
description: "
|
|
668
|
+
description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
|
|
669
669
|
stability: "stable",
|
|
670
670
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
671
671
|
kinds: {
|
|
@@ -706,7 +706,7 @@ var coreMarkdownProvider = {
|
|
|
706
706
|
pluginId: "core",
|
|
707
707
|
kind: "provider",
|
|
708
708
|
version: "1.0.0",
|
|
709
|
-
description: "Universal `.md` fallback
|
|
709
|
+
description: "Universal `.md` fallback. Claims any markdown file no vendor-specific Provider classifies.",
|
|
710
710
|
stability: "stable",
|
|
711
711
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
712
712
|
// Per spec § A.6, defaultRefreshAction values MUST be qualified
|
|
@@ -825,7 +825,7 @@ var slashExtractor = {
|
|
|
825
825
|
pluginId: "core",
|
|
826
826
|
kind: "extractor",
|
|
827
827
|
version: "1.0.0",
|
|
828
|
-
description: "Detects `/command` invocations in a node's body and turns each one into an arrow
|
|
828
|
+
description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
|
|
829
829
|
stability: "stable",
|
|
830
830
|
emitsLinkKinds: ["invokes"],
|
|
831
831
|
defaultConfidence: "medium",
|
|
@@ -860,7 +860,7 @@ var atDirectiveExtractor = {
|
|
|
860
860
|
pluginId: "core",
|
|
861
861
|
kind: "extractor",
|
|
862
862
|
version: "1.0.0",
|
|
863
|
-
description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow
|
|
863
|
+
description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow between nodes in the graph.",
|
|
864
864
|
stability: "stable",
|
|
865
865
|
emitsLinkKinds: ["mentions"],
|
|
866
866
|
defaultConfidence: "medium",
|
|
@@ -896,24 +896,24 @@ var externalUrlCounterExtractor = {
|
|
|
896
896
|
pluginId: "core",
|
|
897
897
|
kind: "extractor",
|
|
898
898
|
version: "1.0.0",
|
|
899
|
-
description: "Counts the distinct external URLs
|
|
899
|
+
description: "Counts the distinct external URLs in a node's body and shows the total on the card.",
|
|
900
900
|
stability: "stable",
|
|
901
901
|
emitsLinkKinds: ["references"],
|
|
902
902
|
defaultConfidence: "low",
|
|
903
903
|
scope: "body",
|
|
904
904
|
/**
|
|
905
|
-
* Phase 6 / View contribution system
|
|
905
|
+
* Phase 6 / View contribution system, surface the distinct-URL
|
|
906
906
|
* count as a card-footer-left chip alongside the in/out link
|
|
907
907
|
* counters and the tools-count wrench. The chip is silent when
|
|
908
908
|
* zero URLs were emitted (`emitWhenEmpty: false`), so unrelated
|
|
909
909
|
* nodes do not gain a `link 0` decoration. The counter rides on
|
|
910
910
|
* exactly the same data the orchestrator was already going to
|
|
911
|
-
* count
|
|
911
|
+
* count, there is no second pass.
|
|
912
912
|
*
|
|
913
913
|
* Icon is the PrimeIcons `pi-link` glyph (declared as the bare
|
|
914
914
|
* `'link'` per `IconString` rules in `view-slots.schema.json`).
|
|
915
915
|
* Mirrors the look of the legacy hardcoded `pi pi-link` chip in
|
|
916
|
-
* `node-card.html` it replaced
|
|
916
|
+
* `node-card.html` it replaced, same icon font, same sizing
|
|
917
917
|
* inherited from the footer `.sm-gnode__stat` styles cloned by
|
|
918
918
|
* the `NodeCounter` renderer.
|
|
919
919
|
*/
|
|
@@ -997,7 +997,7 @@ var markdownLinkExtractor = {
|
|
|
997
997
|
pluginId: "core",
|
|
998
998
|
kind: "extractor",
|
|
999
999
|
version: "1.0.0",
|
|
1000
|
-
description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow
|
|
1000
|
+
description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow between nodes in the graph.",
|
|
1001
1001
|
stability: "stable",
|
|
1002
1002
|
emitsLinkKinds: ["references"],
|
|
1003
1003
|
defaultConfidence: "high",
|
|
@@ -1059,14 +1059,14 @@ function lineFor2(lineStarts, offset) {
|
|
|
1059
1059
|
|
|
1060
1060
|
// built-in-plugins/analyzers/stability/index.ts
|
|
1061
1061
|
var ID6 = "stability";
|
|
1062
|
-
var EXPERIMENTAL_TOOLTIP = "Experimental
|
|
1063
|
-
var DEPRECATED_TOOLTIP = "Deprecated
|
|
1062
|
+
var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
|
|
1063
|
+
var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
|
|
1064
1064
|
var stabilityAnalyzer = {
|
|
1065
1065
|
id: ID6,
|
|
1066
1066
|
pluginId: "core",
|
|
1067
1067
|
kind: "analyzer",
|
|
1068
1068
|
version: "1.0.0",
|
|
1069
|
-
description: "
|
|
1069
|
+
description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
|
|
1070
1070
|
stability: "stable",
|
|
1071
1071
|
mode: "deterministic",
|
|
1072
1072
|
viewContributions: {
|
|
@@ -1098,7 +1098,7 @@ var stabilityAnalyzer = {
|
|
|
1098
1098
|
analyzerId: ID6,
|
|
1099
1099
|
severity: "info",
|
|
1100
1100
|
nodeIds: [node.path],
|
|
1101
|
-
message: `Node '${node.path}' is marked experimental
|
|
1101
|
+
message: `Node '${node.path}' is marked experimental: API may change.`,
|
|
1102
1102
|
data: { stability }
|
|
1103
1103
|
});
|
|
1104
1104
|
} else if (stability === "deprecated") {
|
|
@@ -1111,7 +1111,7 @@ var stabilityAnalyzer = {
|
|
|
1111
1111
|
analyzerId: ID6,
|
|
1112
1112
|
severity: "warn",
|
|
1113
1113
|
nodeIds: [node.path],
|
|
1114
|
-
message: `Node '${node.path}' is marked deprecated
|
|
1114
|
+
message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
|
|
1115
1115
|
data: { stability }
|
|
1116
1116
|
});
|
|
1117
1117
|
}
|
|
@@ -1143,7 +1143,7 @@ var toolsCountExtractor = {
|
|
|
1143
1143
|
pluginId: "core",
|
|
1144
1144
|
kind: "extractor",
|
|
1145
1145
|
version: "1.0.0",
|
|
1146
|
-
description: "Counts the tools an agent declares in its frontmatter and shows the total
|
|
1146
|
+
description: "Counts the tools an agent declares in its frontmatter and shows the total on the agent card.",
|
|
1147
1147
|
stability: "stable",
|
|
1148
1148
|
emitsLinkKinds: [],
|
|
1149
1149
|
defaultConfidence: "high",
|
|
@@ -1193,9 +1193,9 @@ var TRIGGER_COLLISION_TEXTS = {
|
|
|
1193
1193
|
* (e.g. `'; y '` in Spanish) without touching the rule code.
|
|
1194
1194
|
*/
|
|
1195
1195
|
messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
|
|
1196
|
-
/** `<n> nodes advertise it: <list>` part
|
|
1196
|
+
/** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
|
|
1197
1197
|
partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
|
|
1198
|
-
/** `<n> distinct invocation forms: <list>` part
|
|
1198
|
+
/** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
|
|
1199
1199
|
partInvocations: "{{count}} distinct invocation forms: {{forms}}",
|
|
1200
1200
|
/** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
|
|
1201
1201
|
partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
|
|
@@ -1216,7 +1216,7 @@ var triggerCollisionAnalyzer = {
|
|
|
1216
1216
|
kind: "analyzer",
|
|
1217
1217
|
mode: "deterministic",
|
|
1218
1218
|
version: "1.0.0",
|
|
1219
|
-
description: "
|
|
1219
|
+
description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
|
|
1220
1220
|
stability: "stable",
|
|
1221
1221
|
// Two claim-collection passes (advertisement + invocation) feeding
|
|
1222
1222
|
// the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
|
|
@@ -1343,7 +1343,7 @@ var brokenRefAnalyzer = {
|
|
|
1343
1343
|
pluginId: "core",
|
|
1344
1344
|
kind: "analyzer",
|
|
1345
1345
|
version: "1.0.0",
|
|
1346
|
-
description: "
|
|
1346
|
+
description: "Detects and flags arrows pointing at a node not part of the current scan.",
|
|
1347
1347
|
stability: "stable",
|
|
1348
1348
|
mode: "deterministic",
|
|
1349
1349
|
viewContributions: {
|
|
@@ -1354,7 +1354,7 @@ var brokenRefAnalyzer = {
|
|
|
1354
1354
|
icon: "fa-solid fa-circle-xmark",
|
|
1355
1355
|
emitWhenEmpty: false
|
|
1356
1356
|
},
|
|
1357
|
-
// Footer chip on the card. `_counter` shape
|
|
1357
|
+
// Footer chip on the card. `_counter` shape, `value` always shows,
|
|
1358
1358
|
// so the operator sees "how many" at a glance. Renders OUTLINED
|
|
1359
1359
|
// (`fa-regular`) so the corner alert (filled, attention-grabbing)
|
|
1360
1360
|
// and the footer chip (quieter, paired with a number) read as two
|
|
@@ -1462,7 +1462,7 @@ var supersededAnalyzer = {
|
|
|
1462
1462
|
pluginId: "core",
|
|
1463
1463
|
kind: "analyzer",
|
|
1464
1464
|
version: "1.0.0",
|
|
1465
|
-
description: "
|
|
1465
|
+
description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
|
|
1466
1466
|
stability: "stable",
|
|
1467
1467
|
mode: "deterministic",
|
|
1468
1468
|
evaluate(ctx) {
|
|
@@ -1507,7 +1507,7 @@ var linkConflictAnalyzer = {
|
|
|
1507
1507
|
pluginId: "core",
|
|
1508
1508
|
kind: "analyzer",
|
|
1509
1509
|
version: "1.0.0",
|
|
1510
|
-
description: '
|
|
1510
|
+
description: 'Detects and flags conflicting arrow meanings between extractors (e.g. "references" vs "invokes").',
|
|
1511
1511
|
stability: "stable",
|
|
1512
1512
|
mode: "deterministic",
|
|
1513
1513
|
// Bucket links by (source, target), then per-bucket detect distinct
|
|
@@ -1586,9 +1586,9 @@ var ANNOTATION_STALE_TEXTS = {
|
|
|
1586
1586
|
/** both body and frontmatter changed */
|
|
1587
1587
|
bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
|
|
1588
1588
|
// Tooltips for the `card.footer.right` clock chip emitted alongside
|
|
1589
|
-
// the issue. Lists only the drifted face(s)
|
|
1589
|
+
// the issue. Lists only the drifted face(s), in-sync faces are
|
|
1590
1590
|
// omitted so the operator immediately sees what's modified without
|
|
1591
|
-
// scanning prose. No `{{path}}` placeholder
|
|
1591
|
+
// scanning prose. No `{{path}}` placeholder, the chip already sits
|
|
1592
1592
|
// on the affected node. The hint `sm bump <path>` keeps `<path>` as
|
|
1593
1593
|
// a literal placeholder the operator substitutes.
|
|
1594
1594
|
bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
|
|
@@ -1603,14 +1603,19 @@ var annotationStaleAnalyzer = {
|
|
|
1603
1603
|
pluginId: "core",
|
|
1604
1604
|
kind: "analyzer",
|
|
1605
1605
|
version: "1.0.0",
|
|
1606
|
-
description: "
|
|
1606
|
+
description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
|
|
1607
1607
|
stability: "stable",
|
|
1608
1608
|
mode: "deterministic",
|
|
1609
|
+
// The natural fix is to bump the node: refreshes `for` hashes,
|
|
1610
|
+
// increments `annotations.version`, and stamps the audit block. The
|
|
1611
|
+
// UI surfaces `core/bump` in the node inspector under "Recommended
|
|
1612
|
+
// for issues" whenever this analyzer fires.
|
|
1613
|
+
recommendedActions: ["core/bump"],
|
|
1609
1614
|
viewContributions: {
|
|
1610
1615
|
// A `pi-clock` chip in the footer-right cluster so the operator
|
|
1611
1616
|
// spots drift in the list / inspector view (and on the graph card
|
|
1612
1617
|
// body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
|
|
1613
|
-
// renderer treats it as icon-only
|
|
1618
|
+
// renderer treats it as icon-only, drift severity is binary at
|
|
1614
1619
|
// this surface (the tooltip carries the per-face detail body /
|
|
1615
1620
|
// frontmatter / both). The corner badge on `graph.node.alert` was
|
|
1616
1621
|
// dropped on purpose: a tooltip on the footer chip is enough, and
|
|
@@ -1670,7 +1675,7 @@ var annotationOrphanAnalyzer = {
|
|
|
1670
1675
|
pluginId: "core",
|
|
1671
1676
|
kind: "analyzer",
|
|
1672
1677
|
version: "1.0.0",
|
|
1673
|
-
description: "
|
|
1678
|
+
description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
|
|
1674
1679
|
stability: "stable",
|
|
1675
1680
|
mode: "deterministic",
|
|
1676
1681
|
evaluate(ctx) {
|
|
@@ -1713,7 +1718,7 @@ var jobOrphanFileAnalyzer = {
|
|
|
1713
1718
|
pluginId: "core",
|
|
1714
1719
|
kind: "analyzer",
|
|
1715
1720
|
version: "1.0.0",
|
|
1716
|
-
description: "
|
|
1721
|
+
description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
|
|
1717
1722
|
stability: "stable",
|
|
1718
1723
|
mode: "deterministic",
|
|
1719
1724
|
evaluate(ctx) {
|
|
@@ -1751,9 +1756,9 @@ var UNKNOWN_FIELD_TEXTS = {
|
|
|
1751
1756
|
/** Key inside `annotations:` is not in the curated catalog. */
|
|
1752
1757
|
unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
|
|
1753
1758
|
/** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
|
|
1754
|
-
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'
|
|
1759
|
+
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
|
|
1755
1760
|
/** Value under a registered plugin namespace fails the contributed schema. */
|
|
1756
|
-
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}'
|
|
1761
|
+
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
|
|
1757
1762
|
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
1758
1763
|
// plural keeps the count grammar correct without a sub-template.
|
|
1759
1764
|
alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
|
|
@@ -1768,7 +1773,7 @@ var unknownFieldAnalyzer = {
|
|
|
1768
1773
|
pluginId: "core",
|
|
1769
1774
|
kind: "analyzer",
|
|
1770
1775
|
version: "1.0.0",
|
|
1771
|
-
description: "
|
|
1776
|
+
description: "Detects and flags typos or unrecognized keys in sidecars (`.sm`).",
|
|
1772
1777
|
stability: "stable",
|
|
1773
1778
|
mode: "deterministic",
|
|
1774
1779
|
viewContributions: {
|
|
@@ -1776,13 +1781,13 @@ var unknownFieldAnalyzer = {
|
|
|
1776
1781
|
// single unknown field (avoids a noisy "icon + 1" chip).
|
|
1777
1782
|
alert: {
|
|
1778
1783
|
slot: "graph.node.alert",
|
|
1779
|
-
// Filled warning triangle on the corner
|
|
1784
|
+
// Filled warning triangle on the corner, matches the broken-ref
|
|
1780
1785
|
// alert's "attention-grabbing solid" pattern; the footer chip
|
|
1781
1786
|
// below stays outlined for the quieter counter pairing.
|
|
1782
1787
|
icon: "fa-solid fa-triangle-exclamation",
|
|
1783
1788
|
emitWhenEmpty: false
|
|
1784
1789
|
},
|
|
1785
|
-
// Footer chip on the card
|
|
1790
|
+
// Footer chip on the card, `_counter` shape but rendered icon-only
|
|
1786
1791
|
// (the analyzer emits `value: 0` so NodeCounter hides the number
|
|
1787
1792
|
// and only the glyph shows). PrimeIcons `pi-question-circle` so the
|
|
1788
1793
|
// visual weight matches `annotation-stale`'s `pi-clock` chip
|
|
@@ -1796,6 +1801,11 @@ var unknownFieldAnalyzer = {
|
|
|
1796
1801
|
priority: 30
|
|
1797
1802
|
}
|
|
1798
1803
|
},
|
|
1804
|
+
// Analyzer body iterates every sidecar root and classifies each
|
|
1805
|
+
// key against three buckets (catalog / plugin namespace / unknown
|
|
1806
|
+
// root). The per-key branching IS the classification table; factoring
|
|
1807
|
+
// it out would rebuild the discriminator elsewhere. Per
|
|
1808
|
+
// `context/lint.md` category 7 (recursive type-discriminator walkers).
|
|
1799
1809
|
// eslint-disable-next-line complexity
|
|
1800
1810
|
evaluate(ctx) {
|
|
1801
1811
|
const sidecarRoots = ctx.sidecarRoots;
|
|
@@ -1932,64 +1942,14 @@ function collectPluginIds(contributions) {
|
|
|
1932
1942
|
return out;
|
|
1933
1943
|
}
|
|
1934
1944
|
|
|
1935
|
-
// built-in-plugins/analyzers/unknown-slot/index.ts
|
|
1936
|
-
var ID16 = "unknown-slot";
|
|
1937
|
-
var KNOWN_SLOTS = /* @__PURE__ */ new Set([
|
|
1938
|
-
"card.title.right",
|
|
1939
|
-
"card.subtitle.left",
|
|
1940
|
-
"card.footer.left",
|
|
1941
|
-
"card.footer.right",
|
|
1942
|
-
"graph.node.alert",
|
|
1943
|
-
"inspector.header.badge.counter",
|
|
1944
|
-
"inspector.header.badge.tag",
|
|
1945
|
-
"inspector.body.panel.breakdown",
|
|
1946
|
-
"inspector.body.panel.records",
|
|
1947
|
-
"inspector.body.panel.tree",
|
|
1948
|
-
"inspector.body.panel.key-values",
|
|
1949
|
-
"inspector.body.panel.link-list",
|
|
1950
|
-
"inspector.body.panel.markdown",
|
|
1951
|
-
"topbar.nav.start"
|
|
1952
|
-
]);
|
|
1953
|
-
var unknownSlotAnalyzer = {
|
|
1954
|
-
id: ID16,
|
|
1955
|
-
pluginId: "core",
|
|
1956
|
-
kind: "analyzer",
|
|
1957
|
-
version: "1.0.0",
|
|
1958
|
-
description: "Warns when a plugin tries to render in a UI position that does not exist (typo or removed in a newer skill-map version).",
|
|
1959
|
-
stability: "stable",
|
|
1960
|
-
mode: "deterministic",
|
|
1961
|
-
evaluate(ctx) {
|
|
1962
|
-
const contributions = ctx.viewContributions;
|
|
1963
|
-
if (!contributions || contributions.length === 0) return [];
|
|
1964
|
-
const issues = [];
|
|
1965
|
-
for (const c of contributions) {
|
|
1966
|
-
if (KNOWN_SLOTS.has(c.slot)) continue;
|
|
1967
|
-
const qualified = `${c.pluginId}/${c.extensionId}/${c.contributionId}`;
|
|
1968
|
-
issues.push({
|
|
1969
|
-
analyzerId: ID16,
|
|
1970
|
-
severity: "warn",
|
|
1971
|
-
nodeIds: [],
|
|
1972
|
-
message: `Plugin ${qualified} declares unknown slot '${c.slot}'. Run \`sm plugins upgrade ${c.pluginId}\` or update the plugin to a slot in the current catalog (\`sm plugins slots list\`).`,
|
|
1973
|
-
data: {
|
|
1974
|
-
pluginId: c.pluginId,
|
|
1975
|
-
extensionId: c.extensionId,
|
|
1976
|
-
contributionId: c.contributionId,
|
|
1977
|
-
slot: c.slot
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
return issues;
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
|
|
1985
1945
|
// built-in-plugins/analyzers/contribution-orphan/index.ts
|
|
1986
|
-
var
|
|
1946
|
+
var ID16 = "contribution-orphan";
|
|
1987
1947
|
var contributionOrphanAnalyzer = {
|
|
1988
|
-
id:
|
|
1948
|
+
id: ID16,
|
|
1989
1949
|
pluginId: "core",
|
|
1990
1950
|
kind: "analyzer",
|
|
1991
|
-
version: "
|
|
1992
|
-
description: "
|
|
1951
|
+
version: "0.0.0",
|
|
1952
|
+
description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
|
|
1993
1953
|
stability: "experimental",
|
|
1994
1954
|
mode: "deterministic",
|
|
1995
1955
|
evaluate(_ctx) {
|
|
@@ -2006,14 +1966,14 @@ function sanitizeForTerminal(text) {
|
|
|
2006
1966
|
|
|
2007
1967
|
// built-in-plugins/i18n/ascii.texts.ts
|
|
2008
1968
|
var ASCII_FORMATTER_TEXTS = {
|
|
2009
|
-
/** Header line: `skill-map graph
|
|
2010
|
-
header: "skill-map graph
|
|
1969
|
+
/** Header line: `skill-map graph: N nodes, M links, K issues`. */
|
|
1970
|
+
header: "skill-map graph: {{nodes}} nodes, {{links}} links, {{issues}} issues",
|
|
2011
1971
|
/** Per-node-kind section header: `## <kind> (<count>)`. */
|
|
2012
1972
|
kindSectionHeader: "## {{kind}} ({{count}})",
|
|
2013
1973
|
/** Plain node bullet: `- <path>`. */
|
|
2014
1974
|
nodeBullet: "- {{path}}",
|
|
2015
|
-
/** Node bullet with title suffix: `- <path
|
|
2016
|
-
nodeBulletWithTitle: '- {{path}}
|
|
1975
|
+
/** Node bullet with title suffix: `- <path>: "<title>"`. */
|
|
1976
|
+
nodeBulletWithTitle: '- {{path}}: "{{title}}"',
|
|
2017
1977
|
/** `## links (<count>)` section header. */
|
|
2018
1978
|
linksSectionHeader: "## links ({{count}})",
|
|
2019
1979
|
/** Link bullet: `- <source> --<kind>--> <target> [<confidence>]`. */
|
|
@@ -2025,17 +1985,17 @@ var ASCII_FORMATTER_TEXTS = {
|
|
|
2025
1985
|
};
|
|
2026
1986
|
|
|
2027
1987
|
// built-in-plugins/formatters/ascii/index.ts
|
|
2028
|
-
var
|
|
1988
|
+
var ID17 = "ascii";
|
|
2029
1989
|
var KIND_ORDER = ["agent", "command", "skill", "markdown"];
|
|
2030
1990
|
var asciiFormatter = {
|
|
2031
|
-
id:
|
|
1991
|
+
id: ID17,
|
|
2032
1992
|
pluginId: "core",
|
|
2033
1993
|
kind: "formatter",
|
|
2034
1994
|
version: "1.0.0",
|
|
2035
|
-
description: "
|
|
1995
|
+
description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
|
|
2036
1996
|
stability: "stable",
|
|
2037
1997
|
formatId: "ascii",
|
|
2038
|
-
// ASCII tree formatter
|
|
1998
|
+
// ASCII tree formatter, header + per-kind sections + per-issue
|
|
2039
1999
|
// section. Each section iterates and renders; splitting per section
|
|
2040
2000
|
// would multiply the for-loop boilerplate.
|
|
2041
2001
|
// eslint-disable-next-line complexity
|
|
@@ -2126,11 +2086,54 @@ function renderSection(out, kind, group) {
|
|
|
2126
2086
|
out.push("");
|
|
2127
2087
|
}
|
|
2128
2088
|
|
|
2089
|
+
// built-in-plugins/formatters/json/index.ts
|
|
2090
|
+
var ID18 = "json";
|
|
2091
|
+
var jsonFormatter = {
|
|
2092
|
+
id: ID18,
|
|
2093
|
+
pluginId: "core",
|
|
2094
|
+
kind: "formatter",
|
|
2095
|
+
version: "1.0.0",
|
|
2096
|
+
description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
|
|
2097
|
+
stability: "stable",
|
|
2098
|
+
formatId: ID18,
|
|
2099
|
+
format(ctx) {
|
|
2100
|
+
if (ctx.scanResult !== void 0) {
|
|
2101
|
+
return JSON.stringify(ctx.scanResult);
|
|
2102
|
+
}
|
|
2103
|
+
return JSON.stringify({
|
|
2104
|
+
nodes: ctx.nodes,
|
|
2105
|
+
links: ctx.links,
|
|
2106
|
+
issues: ctx.issues
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2129
2111
|
// kernel/adapters/schema-validators.ts
|
|
2130
2112
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2131
2113
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
2132
2114
|
import { createRequire as createRequire2 } from "module";
|
|
2133
2115
|
import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
|
|
2116
|
+
|
|
2117
|
+
// kernel/types/view-catalog.ts
|
|
2118
|
+
var ALL_SLOT_NAMES = [
|
|
2119
|
+
"card.title.right",
|
|
2120
|
+
"card.subtitle.left",
|
|
2121
|
+
"card.footer.left",
|
|
2122
|
+
"card.footer.right",
|
|
2123
|
+
"graph.node.alert",
|
|
2124
|
+
"inspector.header.badge.counter",
|
|
2125
|
+
"inspector.header.badge.tag",
|
|
2126
|
+
"inspector.body.panel.breakdown",
|
|
2127
|
+
"inspector.body.panel.records",
|
|
2128
|
+
"inspector.body.panel.tree",
|
|
2129
|
+
"inspector.body.panel.key-values",
|
|
2130
|
+
"inspector.body.panel.link-list",
|
|
2131
|
+
"inspector.body.panel.markdown",
|
|
2132
|
+
"topbar.nav.start"
|
|
2133
|
+
];
|
|
2134
|
+
var KNOWN_SLOT_NAMES = new Set(ALL_SLOT_NAMES);
|
|
2135
|
+
|
|
2136
|
+
// kernel/adapters/schema-validators.ts
|
|
2134
2137
|
var SCHEMA_FILES = {
|
|
2135
2138
|
node: "schemas/node.schema.json",
|
|
2136
2139
|
link: "schemas/link.schema.json",
|
|
@@ -2198,24 +2201,8 @@ function buildSchemaValidators() {
|
|
|
2198
2201
|
});
|
|
2199
2202
|
const contributionValidators = /* @__PURE__ */ new Map();
|
|
2200
2203
|
const VIEW_SLOTS_ID = "https://skill-map.dev/spec/v0/view-slots.schema.json";
|
|
2201
|
-
const KNOWN_SLOTS2 = /* @__PURE__ */ new Set([
|
|
2202
|
-
"card.title.right",
|
|
2203
|
-
"card.subtitle.left",
|
|
2204
|
-
"card.footer.left",
|
|
2205
|
-
"card.footer.right",
|
|
2206
|
-
"graph.node.alert",
|
|
2207
|
-
"inspector.header.badge.counter",
|
|
2208
|
-
"inspector.header.badge.tag",
|
|
2209
|
-
"inspector.body.panel.breakdown",
|
|
2210
|
-
"inspector.body.panel.records",
|
|
2211
|
-
"inspector.body.panel.tree",
|
|
2212
|
-
"inspector.body.panel.key-values",
|
|
2213
|
-
"inspector.body.panel.link-list",
|
|
2214
|
-
"inspector.body.panel.markdown",
|
|
2215
|
-
"topbar.nav.start"
|
|
2216
|
-
]);
|
|
2217
2204
|
function getContributionValidator(slot) {
|
|
2218
|
-
if (!
|
|
2205
|
+
if (!KNOWN_SLOT_NAMES.has(slot)) return null;
|
|
2219
2206
|
const existing = contributionValidators.get(slot);
|
|
2220
2207
|
if (existing) return existing;
|
|
2221
2208
|
const ref = `${VIEW_SLOTS_ID}#/$defs/payloads/${slot}`;
|
|
@@ -2313,7 +2300,7 @@ function resolveSpecRoot() {
|
|
|
2313
2300
|
return dirname2(indexPath);
|
|
2314
2301
|
} catch {
|
|
2315
2302
|
throw new Error(
|
|
2316
|
-
"@skill-map/spec not resolvable
|
|
2303
|
+
"@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
|
|
2317
2304
|
);
|
|
2318
2305
|
}
|
|
2319
2306
|
}
|
|
@@ -2341,7 +2328,7 @@ var validateAllAnalyzer = {
|
|
|
2341
2328
|
pluginId: "core",
|
|
2342
2329
|
kind: "analyzer",
|
|
2343
2330
|
version: "1.0.0",
|
|
2344
|
-
description: "
|
|
2331
|
+
description: "Detects and flags nodes or links violating the project schemas.",
|
|
2345
2332
|
stability: "stable",
|
|
2346
2333
|
mode: "deterministic",
|
|
2347
2334
|
evaluate(ctx) {
|
|
@@ -2421,7 +2408,7 @@ var linkCountsAnalyzer = {
|
|
|
2421
2408
|
pluginId: "core",
|
|
2422
2409
|
kind: "analyzer",
|
|
2423
2410
|
version: "1.0.0",
|
|
2424
|
-
description: "Counts incoming and outgoing links per node
|
|
2411
|
+
description: "Counts incoming and outgoing links per node.",
|
|
2425
2412
|
stability: "stable",
|
|
2426
2413
|
mode: "deterministic",
|
|
2427
2414
|
viewContributions: {
|
|
@@ -2485,6 +2472,29 @@ import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
|
2485
2472
|
import { createRequire as createRequire3 } from "module";
|
|
2486
2473
|
import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
|
|
2487
2474
|
import yaml from "js-yaml";
|
|
2475
|
+
|
|
2476
|
+
// kernel/util/strip-prototype-pollution.ts
|
|
2477
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
2478
|
+
"__proto__",
|
|
2479
|
+
"constructor",
|
|
2480
|
+
"prototype"
|
|
2481
|
+
]);
|
|
2482
|
+
function stripPrototypePollution(value) {
|
|
2483
|
+
return strip(value);
|
|
2484
|
+
}
|
|
2485
|
+
function strip(value) {
|
|
2486
|
+
if (value === null || value === void 0) return value;
|
|
2487
|
+
if (typeof value !== "object") return value;
|
|
2488
|
+
if (Array.isArray(value)) return value.map(strip);
|
|
2489
|
+
const out = {};
|
|
2490
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2491
|
+
if (FORBIDDEN_KEYS.has(k)) continue;
|
|
2492
|
+
out[k] = strip(v);
|
|
2493
|
+
}
|
|
2494
|
+
return out;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// kernel/sidecar/parse.ts
|
|
2488
2498
|
function readSidecarFor(mdAbsolutePath) {
|
|
2489
2499
|
const sidecarPath = sidecarPathFor(mdAbsolutePath);
|
|
2490
2500
|
if (!existsSync(sidecarPath)) {
|
|
@@ -2510,6 +2520,7 @@ function readSidecarFor(mdAbsolutePath) {
|
|
|
2510
2520
|
issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
|
|
2511
2521
|
};
|
|
2512
2522
|
}
|
|
2523
|
+
parsedYaml = stripPrototypePollution(parsedYaml);
|
|
2513
2524
|
if (!isPlainObject(parsedYaml)) {
|
|
2514
2525
|
return {
|
|
2515
2526
|
parsed: null,
|
|
@@ -2575,7 +2586,7 @@ function resolveSpecRoot2() {
|
|
|
2575
2586
|
return dirname3(indexPath);
|
|
2576
2587
|
} catch {
|
|
2577
2588
|
throw new Error(
|
|
2578
|
-
"@skill-map/spec not resolvable
|
|
2589
|
+
"@skill-map/spec not resolvable: sidecar reader cannot load schemas."
|
|
2579
2590
|
);
|
|
2580
2591
|
}
|
|
2581
2592
|
}
|
|
@@ -2588,7 +2599,7 @@ var bumpAction = {
|
|
|
2588
2599
|
pluginId: PLUGIN_ID,
|
|
2589
2600
|
kind: "action",
|
|
2590
2601
|
version: "1.0.0",
|
|
2591
|
-
description: "Marks a node as updated
|
|
2602
|
+
description: "Marks a node as updated: bumps version, refreshes sidecar hashes, records the timestamp.",
|
|
2592
2603
|
stability: "stable",
|
|
2593
2604
|
mode: "deterministic",
|
|
2594
2605
|
reportSchemaRef: "https://skill-map.dev/spec/v0/bump-report.schema.json",
|
|
@@ -2644,6 +2655,24 @@ function pickCurrentVersion(overlay) {
|
|
|
2644
2655
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2645
2656
|
}
|
|
2646
2657
|
|
|
2658
|
+
// built-in-plugins/actions/mark-superseded/index.ts
|
|
2659
|
+
var ID22 = "mark-superseded";
|
|
2660
|
+
var PLUGIN_ID2 = "core";
|
|
2661
|
+
var markSupersededAction = {
|
|
2662
|
+
id: ID22,
|
|
2663
|
+
pluginId: PLUGIN_ID2,
|
|
2664
|
+
kind: "action",
|
|
2665
|
+
version: "0.0.0",
|
|
2666
|
+
description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar). Paired with the `core/superseded` analyzer.",
|
|
2667
|
+
stability: "experimental",
|
|
2668
|
+
mode: "deterministic",
|
|
2669
|
+
reportSchemaRef: "https://skill-map.dev/spec/v0/report-base-deterministic.schema.json",
|
|
2670
|
+
invoke(_input, _ctx) {
|
|
2671
|
+
const report = { ok: true, noop: true };
|
|
2672
|
+
return { report };
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2647
2676
|
// cli/util/update-check-banner.ts
|
|
2648
2677
|
import { existsSync as existsSync7 } from "fs";
|
|
2649
2678
|
|
|
@@ -3406,10 +3435,10 @@ function scanCheckedLiteral(sql4, start, closer, label) {
|
|
|
3406
3435
|
}
|
|
3407
3436
|
function findCommentMarker(ch, next, label) {
|
|
3408
3437
|
if (ch === "-" && next === "-") {
|
|
3409
|
-
return `${label} contains '--' (line comment marker). Reject
|
|
3438
|
+
return `${label} contains '--' (line comment marker). Reject: validator and engine would disagree on statement boundaries.`;
|
|
3410
3439
|
}
|
|
3411
3440
|
if (ch === "/" && next === "*") {
|
|
3412
|
-
return `${label} contains '/*' (block comment marker). Reject
|
|
3441
|
+
return `${label} contains '/*' (block comment marker). Reject: validator and engine would disagree on statement boundaries.`;
|
|
3413
3442
|
}
|
|
3414
3443
|
return null;
|
|
3415
3444
|
}
|
|
@@ -3975,7 +4004,7 @@ function rowToNode(row) {
|
|
|
3975
4004
|
linksInCount: row.linksInCount,
|
|
3976
4005
|
externalRefsCount: row.externalRefsCount,
|
|
3977
4006
|
frontmatter: parseJsonObject(row.frontmatterJson),
|
|
3978
|
-
// Step 9.6.2
|
|
4007
|
+
// Step 9.6.2, reconstitute the sidecar overlay from the
|
|
3979
4008
|
// denormalised columns. Status is trusted as-stored (the kernel
|
|
3980
4009
|
// wrote it from `computeDriftStatus`); annotations re-parse from
|
|
3981
4010
|
// the JSON column.
|
|
@@ -3983,7 +4012,7 @@ function rowToNode(row) {
|
|
|
3983
4012
|
present: row.sidecarPresent === 1,
|
|
3984
4013
|
status: row.sidecarStatus,
|
|
3985
4014
|
annotations: row.annotationsJson === null ? null : parseJsonObject(row.annotationsJson),
|
|
3986
|
-
// R15 closure (2026-05-07)
|
|
4015
|
+
// R15 closure (2026-05-07), rehydrate the full parsed root from
|
|
3987
4016
|
// the sibling JSON column. NULL when no sidecar is present, or
|
|
3988
4017
|
// when the sidecar failed to parse on the scanning side.
|
|
3989
4018
|
root: row.sidecarRootJson === null ? null : parseJsonObject(row.sidecarRootJson)
|
|
@@ -4076,7 +4105,13 @@ async function loadNodeEnrichments(db, nodePath) {
|
|
|
4076
4105
|
nodePath: row.nodePath,
|
|
4077
4106
|
extractorId: row.extractorId,
|
|
4078
4107
|
bodyHashAtEnrichment: row.bodyHashAtEnrichment,
|
|
4079
|
-
|
|
4108
|
+
// Audit M3: deep-strip `__proto__` / `constructor` / `prototype`
|
|
4109
|
+
// keys at every depth before the value flows into the read-time
|
|
4110
|
+
// merge in `mergeNodeWithEnrichments`. AJV at emit time does not
|
|
4111
|
+
// forbid these names; without the strip a hostile (or buggy)
|
|
4112
|
+
// extractor could persist a nested forbidden key that survived
|
|
4113
|
+
// the JSON round-trip and exploited a future deep merge.
|
|
4114
|
+
value: stripPrototypePollution(parseJsonObject(row.valueJson)),
|
|
4080
4115
|
stale: row.stale === 1,
|
|
4081
4116
|
enrichedAt: row.enrichedAt,
|
|
4082
4117
|
isProbabilistic: row.isProbabilistic === 1
|
|
@@ -4214,7 +4249,7 @@ async function purgeContributionsByPlugin(db, pluginId, extensionId) {
|
|
|
4214
4249
|
function rowToContribution(row) {
|
|
4215
4250
|
let payload;
|
|
4216
4251
|
try {
|
|
4217
|
-
payload = JSON.parse(row.payloadJson);
|
|
4252
|
+
payload = stripPrototypePollution(JSON.parse(row.payloadJson));
|
|
4218
4253
|
} catch {
|
|
4219
4254
|
payload = {};
|
|
4220
4255
|
}
|
|
@@ -4664,7 +4699,7 @@ var SqliteStorageAdapter = class {
|
|
|
4664
4699
|
/**
|
|
4665
4700
|
* Access the underlying Kysely instance.
|
|
4666
4701
|
*
|
|
4667
|
-
* Test-only escape hatch (per AGENTS.md § Kernel boundaries
|
|
4702
|
+
* Test-only escape hatch (per AGENTS.md § Kernel boundaries, tests
|
|
4668
4703
|
* are the documented exception). CLI commands MUST consume the
|
|
4669
4704
|
* adapter through the namespaced port surfaces (`port.<namespace>.*`
|
|
4670
4705
|
* or `port.transaction(...)`); reaching for this getter from a
|
|
@@ -4701,6 +4736,7 @@ var SqliteStorageAdapter = class {
|
|
|
4701
4736
|
};
|
|
4702
4737
|
this.issues = {
|
|
4703
4738
|
listAll: () => listAllIssues(this.db),
|
|
4739
|
+
list: (filter) => listIssues(this.db, filter),
|
|
4704
4740
|
findActive: (predicate) => findActiveIssues(this.db, predicate)
|
|
4705
4741
|
};
|
|
4706
4742
|
this.history = {
|
|
@@ -4855,6 +4891,50 @@ async function listAllIssues(db) {
|
|
|
4855
4891
|
const rows = await db.selectFrom("scan_issues").selectAll().execute();
|
|
4856
4892
|
return rows.map(rowToIssue);
|
|
4857
4893
|
}
|
|
4894
|
+
async function listIssues(db, filter) {
|
|
4895
|
+
const baseQuery = applyIssueFilters(
|
|
4896
|
+
db.selectFrom("scan_issues"),
|
|
4897
|
+
filter
|
|
4898
|
+
);
|
|
4899
|
+
const countRow = await baseQuery.select(({ fn }) => fn.countAll().as("c")).executeTakeFirst();
|
|
4900
|
+
const total = Number(countRow?.c ?? 0);
|
|
4901
|
+
const rows = await applyIssueFilters(
|
|
4902
|
+
db.selectFrom("scan_issues"),
|
|
4903
|
+
filter
|
|
4904
|
+
).selectAll().orderBy("id", "asc").offset(filter.offset).limit(filter.limit).execute();
|
|
4905
|
+
return { items: rows.map(rowToIssue), total };
|
|
4906
|
+
}
|
|
4907
|
+
function applyIssueFilters(query, filter) {
|
|
4908
|
+
let q = query;
|
|
4909
|
+
if (filter.severities && filter.severities.length > 0) {
|
|
4910
|
+
q = q.where("severity", "in", [...filter.severities]);
|
|
4911
|
+
}
|
|
4912
|
+
if (filter.analyzerIds && filter.analyzerIds.length > 0) {
|
|
4913
|
+
const tokens = filter.analyzerIds;
|
|
4914
|
+
q = q.where(
|
|
4915
|
+
({ eb, or }) => or(
|
|
4916
|
+
tokens.flatMap((token) => [
|
|
4917
|
+
eb("analyzerId", "=", token),
|
|
4918
|
+
// `'%/' || ?` keeps the LIKE pattern's `%` literal in the
|
|
4919
|
+
// template and binds `token` separately, no interpolation of
|
|
4920
|
+
// user input into the SQL string.
|
|
4921
|
+
eb("analyzerId", "like", `%/${token}`)
|
|
4922
|
+
])
|
|
4923
|
+
)
|
|
4924
|
+
);
|
|
4925
|
+
}
|
|
4926
|
+
if (filter.nodePath !== void 0 && filter.nodePath !== null) {
|
|
4927
|
+
const target = filter.nodePath;
|
|
4928
|
+
q = q.where(
|
|
4929
|
+
({ exists, selectFrom }) => exists(
|
|
4930
|
+
selectFrom(
|
|
4931
|
+
sql3`json_each(scan_issues.node_ids_json)`.as("je")
|
|
4932
|
+
).select(sql3`1`.as("one")).where(sql3.ref("je.value"), "=", target)
|
|
4933
|
+
)
|
|
4934
|
+
);
|
|
4935
|
+
}
|
|
4936
|
+
return q;
|
|
4937
|
+
}
|
|
4858
4938
|
async function findActiveIssues(db, predicate) {
|
|
4859
4939
|
const rows = await db.selectFrom("scan_issues").selectAll().execute();
|
|
4860
4940
|
const out = [];
|
|
@@ -5126,14 +5206,15 @@ function loadConfig(opts) {
|
|
|
5126
5206
|
const partial = readJsonSafe(path, layer, warnings, strict);
|
|
5127
5207
|
if (partial === null) continue;
|
|
5128
5208
|
const cleaned = validateAndStrip(validators, partial, layer, warnings, strict);
|
|
5129
|
-
if (layer
|
|
5130
|
-
stripProjectLocalOnlyKeys(cleaned, warnings, strict);
|
|
5209
|
+
if (layer !== "project-local") {
|
|
5210
|
+
stripProjectLocalOnlyKeys(cleaned, layer, warnings, strict);
|
|
5131
5211
|
}
|
|
5132
5212
|
effective = deepMerge(effective, cleaned);
|
|
5133
5213
|
recordSources("", cleaned, sources, layer);
|
|
5134
5214
|
}
|
|
5135
5215
|
if (opts.overrides && Object.keys(opts.overrides).length > 0) {
|
|
5136
5216
|
const cleaned = validateAndStrip(validators, opts.overrides, "override", warnings, strict);
|
|
5217
|
+
stripProjectLocalOnlyKeys(cleaned, "override", warnings, strict);
|
|
5137
5218
|
effective = deepMerge(effective, cleaned);
|
|
5138
5219
|
recordSources("", cleaned, sources, "override");
|
|
5139
5220
|
}
|
|
@@ -5208,7 +5289,6 @@ function describeJsonType(v) {
|
|
|
5208
5289
|
if (Array.isArray(v)) return "array";
|
|
5209
5290
|
return typeof v;
|
|
5210
5291
|
}
|
|
5211
|
-
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5212
5292
|
function deleteAtPath(root, parentPath, key) {
|
|
5213
5293
|
if (containsForbidden(parentPath, key)) return;
|
|
5214
5294
|
const segments = parentPath.split("/").filter(Boolean);
|
|
@@ -5219,7 +5299,7 @@ function deleteAtPath(root, parentPath, key) {
|
|
|
5219
5299
|
}
|
|
5220
5300
|
if (isPlainObject2(cur)) delete cur[key];
|
|
5221
5301
|
}
|
|
5222
|
-
function stripProjectLocalOnlyKeys(cloned, warnings, strict) {
|
|
5302
|
+
function stripProjectLocalOnlyKeys(cloned, layer, warnings, strict) {
|
|
5223
5303
|
for (const dotKey of PROJECT_LOCAL_ONLY_KEYS) {
|
|
5224
5304
|
const segments = dotKey.split(".").filter(Boolean);
|
|
5225
5305
|
if (segments.length === 0) continue;
|
|
@@ -5228,7 +5308,7 @@ function stripProjectLocalOnlyKeys(cloned, warnings, strict) {
|
|
|
5228
5308
|
const parentPath = "/" + segments.join("/");
|
|
5229
5309
|
deleteAtPath(cloned, parentPath, leaf);
|
|
5230
5310
|
const msg = tx(CONFIG_LOADER_TEXTS.projectLocalOnlyStripped, {
|
|
5231
|
-
layer
|
|
5311
|
+
layer,
|
|
5232
5312
|
key: dotKey
|
|
5233
5313
|
});
|
|
5234
5314
|
if (strict) throw new Error(msg);
|
|
@@ -5387,13 +5467,17 @@ function enumerateConfigPaths(obj, prefix = "") {
|
|
|
5387
5467
|
|
|
5388
5468
|
// core/config/atomic-write.ts
|
|
5389
5469
|
import {
|
|
5470
|
+
closeSync,
|
|
5471
|
+
constants as fsConstants,
|
|
5390
5472
|
existsSync as existsSync6,
|
|
5391
5473
|
mkdirSync as mkdirSync3,
|
|
5474
|
+
openSync,
|
|
5392
5475
|
readFileSync as readFileSync7,
|
|
5393
5476
|
renameSync,
|
|
5394
5477
|
unlinkSync,
|
|
5395
|
-
|
|
5478
|
+
writeSync
|
|
5396
5479
|
} from "fs";
|
|
5480
|
+
import { randomBytes } from "crypto";
|
|
5397
5481
|
import { dirname as dirname6 } from "path";
|
|
5398
5482
|
function readJsonObjectOrEmpty(path) {
|
|
5399
5483
|
if (!existsSync6(path)) return {};
|
|
@@ -5406,13 +5490,26 @@ function readJsonObjectOrEmpty(path) {
|
|
|
5406
5490
|
}
|
|
5407
5491
|
return {};
|
|
5408
5492
|
}
|
|
5409
|
-
function
|
|
5410
|
-
|
|
5411
|
-
|
|
5493
|
+
function writeFileAtomicExclusive(path, content) {
|
|
5494
|
+
const tmp = `${path}.tmp.${process.pid}.${randomBytes(8).toString("hex")}`;
|
|
5495
|
+
let fd = null;
|
|
5412
5496
|
try {
|
|
5413
|
-
|
|
5497
|
+
fd = openSync(
|
|
5498
|
+
tmp,
|
|
5499
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
5500
|
+
384
|
|
5501
|
+
);
|
|
5502
|
+
writeSync(fd, content);
|
|
5503
|
+
closeSync(fd);
|
|
5504
|
+
fd = null;
|
|
5414
5505
|
renameSync(tmp, path);
|
|
5415
5506
|
} catch (err) {
|
|
5507
|
+
if (fd !== null) {
|
|
5508
|
+
try {
|
|
5509
|
+
closeSync(fd);
|
|
5510
|
+
} catch {
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5416
5513
|
try {
|
|
5417
5514
|
unlinkSync(tmp);
|
|
5418
5515
|
} catch {
|
|
@@ -5420,6 +5517,10 @@ function writeJsonAtomic(path, content) {
|
|
|
5420
5517
|
throw err;
|
|
5421
5518
|
}
|
|
5422
5519
|
}
|
|
5520
|
+
function writeJsonAtomic(path, content) {
|
|
5521
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
5522
|
+
writeFileAtomicExclusive(path, JSON.stringify(content, null, 2) + "\n");
|
|
5523
|
+
}
|
|
5423
5524
|
|
|
5424
5525
|
// core/config/helper.ts
|
|
5425
5526
|
var USER_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -5549,6 +5650,7 @@ function isUnderProject(absPath, cwd) {
|
|
|
5549
5650
|
}
|
|
5550
5651
|
|
|
5551
5652
|
// core/update-check/index.ts
|
|
5653
|
+
var SEMVER_SHAPE_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
5552
5654
|
async function fetchLatestVersion(pkg, opts) {
|
|
5553
5655
|
const controller = new AbortController();
|
|
5554
5656
|
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
|
|
@@ -5565,6 +5667,9 @@ async function fetchLatestVersion(pkg, opts) {
|
|
|
5565
5667
|
if (typeof payload.version !== "string" || payload.version.length === 0) {
|
|
5566
5668
|
throw new Error("registry payload missing string `version`");
|
|
5567
5669
|
}
|
|
5670
|
+
if (!SEMVER_SHAPE_RE.test(payload.version)) {
|
|
5671
|
+
throw new Error("registry payload `version` is not a semver-shaped string");
|
|
5672
|
+
}
|
|
5568
5673
|
return payload.version;
|
|
5569
5674
|
} finally {
|
|
5570
5675
|
clearTimeout(timer);
|
|
@@ -5633,14 +5738,16 @@ function comparePrerelease(a, b) {
|
|
|
5633
5738
|
|
|
5634
5739
|
// cli/i18n/update-check.texts.ts
|
|
5635
5740
|
var UPDATE_CHECK_TEXTS = {
|
|
5636
|
-
|
|
5741
|
+
/** Label rendered inside the top border, between corner and fill. */
|
|
5742
|
+
availableHeader: "Update available",
|
|
5743
|
+
/** Actionable hint shown on the second body line, in dim ANSI. */
|
|
5637
5744
|
availableHint: "Run `npm i -g @skill-map/cli@latest` to update."
|
|
5638
5745
|
};
|
|
5639
5746
|
|
|
5640
5747
|
// package.json
|
|
5641
5748
|
var package_default = {
|
|
5642
5749
|
name: "@skill-map/cli",
|
|
5643
|
-
version: "0.
|
|
5750
|
+
version: "0.23.1",
|
|
5644
5751
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
5645
5752
|
license: "MIT",
|
|
5646
5753
|
type: "module",
|
|
@@ -5707,16 +5814,16 @@ var package_default = {
|
|
|
5707
5814
|
},
|
|
5708
5815
|
dependencies: {
|
|
5709
5816
|
"@hono/node-server": "2.0.1",
|
|
5710
|
-
"@skill-map/spec": "0.
|
|
5817
|
+
"@skill-map/spec": "0.23.0",
|
|
5711
5818
|
ajv: "8.18.0",
|
|
5712
5819
|
"ajv-formats": "3.0.1",
|
|
5713
5820
|
chokidar: "5.0.0",
|
|
5714
5821
|
clipanion: "4.0.0-rc.4",
|
|
5715
|
-
hono: "4.12.
|
|
5822
|
+
hono: "4.12.18",
|
|
5716
5823
|
ignore: "7.0.5",
|
|
5717
5824
|
"js-tiktoken": "1.0.21",
|
|
5718
5825
|
"js-yaml": "4.1.1",
|
|
5719
|
-
kysely: "0.28.
|
|
5826
|
+
kysely: "0.28.17",
|
|
5720
5827
|
semver: "7.7.4",
|
|
5721
5828
|
typanion: "3.14.0",
|
|
5722
5829
|
ws: "8.20.0"
|
|
@@ -5873,18 +5980,23 @@ async function runWithAdapter(adapter, opts) {
|
|
|
5873
5980
|
} catch {
|
|
5874
5981
|
}
|
|
5875
5982
|
}
|
|
5983
|
+
var BANNER_WIDTH = 60;
|
|
5876
5984
|
function writeBanner(opts, latestVersion) {
|
|
5877
5985
|
const ansi = ansiFor({
|
|
5878
5986
|
isTTY: opts.stderr.isTTY === true,
|
|
5879
5987
|
noColorFlag: opts.noColorFlag
|
|
5880
5988
|
});
|
|
5881
|
-
const
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
opts.stderr.write(
|
|
5989
|
+
const labelRaw = ` \u2B06 ${UPDATE_CHECK_TEXTS.availableHeader} `;
|
|
5990
|
+
const fillCount = Math.max(0, BANNER_WIDTH - 2 - labelRaw.length);
|
|
5991
|
+
const header = ansi.cyan("\u250C\u2500") + ansi.bold(ansi.cyan(labelRaw)) + ansi.cyan("\u2500".repeat(fillCount));
|
|
5992
|
+
const versionLine = `${ansi.cyan("\u2502")} ${VERSION} \u2192 ${latestVersion}`;
|
|
5993
|
+
const hintLine = `${ansi.cyan("\u2502")} ${ansi.dim(UPDATE_CHECK_TEXTS.availableHint)}`;
|
|
5994
|
+
const footer = ansi.cyan("\u2514" + "\u2500".repeat(BANNER_WIDTH - 1));
|
|
5995
|
+
opts.stderr.write(`${header}
|
|
5996
|
+
${versionLine}
|
|
5997
|
+
${hintLine}
|
|
5998
|
+
${footer}
|
|
5999
|
+
`);
|
|
5888
6000
|
}
|
|
5889
6001
|
|
|
5890
6002
|
// built-in-plugins/hooks/update-check/index.ts
|
|
@@ -5893,7 +6005,7 @@ var updateCheckHook = {
|
|
|
5893
6005
|
pluginId: "core",
|
|
5894
6006
|
kind: "hook",
|
|
5895
6007
|
version: "1.0.0",
|
|
5896
|
-
description: "Checks
|
|
6008
|
+
description: "Checks daily for a newer skill-map version on npm. Shows an `update available` banner when one is found.",
|
|
5897
6009
|
stability: "stable",
|
|
5898
6010
|
mode: "deterministic",
|
|
5899
6011
|
triggers: ["boot"],
|
|
@@ -5933,7 +6045,7 @@ var builtInBundles = [
|
|
|
5933
6045
|
{
|
|
5934
6046
|
id: "agent-skills",
|
|
5935
6047
|
granularity: "bundle",
|
|
5936
|
-
description: "
|
|
6048
|
+
description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
|
|
5937
6049
|
extensions: [
|
|
5938
6050
|
agentSkillsProvider
|
|
5939
6051
|
]
|
|
@@ -5941,7 +6053,7 @@ var builtInBundles = [
|
|
|
5941
6053
|
{
|
|
5942
6054
|
id: "core",
|
|
5943
6055
|
granularity: "extension",
|
|
5944
|
-
description: "Core extensions shared across providers
|
|
6056
|
+
description: "Core extensions shared across providers: extractors, analyzers, formatters, the bump action, and the universal `.md` fallback Provider.",
|
|
5945
6057
|
extensions: [
|
|
5946
6058
|
// Provider FIRST within the core bundle so the kindRegistry
|
|
5947
6059
|
// composer picks it up alongside other providers; orchestration
|
|
@@ -5949,7 +6061,7 @@ var builtInBundles = [
|
|
|
5949
6061
|
// enforced by the bundle list above (claude / gemini /
|
|
5950
6062
|
// agent-skills precede core). Within the core bundle, the
|
|
5951
6063
|
// provider's slot among extractors / analyzers / formatter is
|
|
5952
|
-
// irrelevant
|
|
6064
|
+
// irrelevant, the orchestrator buckets by kind before
|
|
5953
6065
|
// iterating, so this list defines registration order, not
|
|
5954
6066
|
// execution order.
|
|
5955
6067
|
coreMarkdownProvider,
|
|
@@ -5968,12 +6080,13 @@ var builtInBundles = [
|
|
|
5968
6080
|
jobOrphanFileAnalyzer,
|
|
5969
6081
|
stabilityAnalyzer,
|
|
5970
6082
|
unknownFieldAnalyzer,
|
|
5971
|
-
unknownSlotAnalyzer,
|
|
5972
6083
|
contributionOrphanAnalyzer,
|
|
5973
6084
|
asciiFormatter,
|
|
6085
|
+
jsonFormatter,
|
|
5974
6086
|
validateAllAnalyzer,
|
|
5975
6087
|
linkCountsAnalyzer,
|
|
5976
6088
|
bumpAction,
|
|
6089
|
+
markSupersededAction,
|
|
5977
6090
|
updateCheckHook
|
|
5978
6091
|
]
|
|
5979
6092
|
}
|
|
@@ -6183,9 +6296,9 @@ var UTIL_TEXTS = {
|
|
|
6183
6296
|
// Every verb's body is expected to end on a content line (with or
|
|
6184
6297
|
// without its own trailing \n); the blank line here is universal.
|
|
6185
6298
|
doneIn: "\ndone in {{elapsed}}\n",
|
|
6186
|
-
// confirm.ts (default-no prompt suffix
|
|
6299
|
+
// confirm.ts (default-no prompt suffix, destructive verbs)
|
|
6187
6300
|
confirmPromptSuffix: " [y/N] ",
|
|
6188
|
-
// confirm.ts (default-yes prompt suffix
|
|
6301
|
+
// confirm.ts (default-yes prompt suffix, consent-style verbs where the
|
|
6189
6302
|
// user already triggered the action and is just acknowledging it,
|
|
6190
6303
|
// e.g. the .sm write consent gate).
|
|
6191
6304
|
confirmPromptSuffixDefaultYes: " [Y/n] ",
|
|
@@ -6441,7 +6554,7 @@ function ensureSidecarWritesAllowed(opts) {
|
|
|
6441
6554
|
}
|
|
6442
6555
|
|
|
6443
6556
|
// kernel/sidecar/store.ts
|
|
6444
|
-
import { existsSync as existsSync9, readFileSync as readFileSync8
|
|
6557
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
6445
6558
|
import { dirname as dirname7, resolve as resolve10 } from "path";
|
|
6446
6559
|
import { createRequire as createRequire4 } from "module";
|
|
6447
6560
|
import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
|
|
@@ -6501,6 +6614,7 @@ var FilesystemSidecarStore = class {
|
|
|
6501
6614
|
function deepMerge2(base, patch) {
|
|
6502
6615
|
const out = { ...base };
|
|
6503
6616
|
for (const key of Object.keys(patch)) {
|
|
6617
|
+
if (FORBIDDEN_KEYS.has(key)) continue;
|
|
6504
6618
|
const a = out[key];
|
|
6505
6619
|
const b = patch[key];
|
|
6506
6620
|
if (b === null) {
|
|
@@ -6529,20 +6643,10 @@ function readSidecarObject(sidecarAbsPath) {
|
|
|
6529
6643
|
`sidecar at ${sidecarAbsPath} is not a YAML mapping; refusing to patch`
|
|
6530
6644
|
);
|
|
6531
6645
|
}
|
|
6532
|
-
return parsed;
|
|
6646
|
+
return stripPrototypePollution(parsed);
|
|
6533
6647
|
}
|
|
6534
6648
|
function atomicWriteFile(targetPath, content) {
|
|
6535
|
-
|
|
6536
|
-
try {
|
|
6537
|
-
writeFileSync2(tmpPath, content, { encoding: "utf8" });
|
|
6538
|
-
renameSync2(tmpPath, targetPath);
|
|
6539
|
-
} catch (err) {
|
|
6540
|
-
try {
|
|
6541
|
-
if (existsSync9(tmpPath)) unlinkSync2(tmpPath);
|
|
6542
|
-
} catch {
|
|
6543
|
-
}
|
|
6544
|
-
throw err;
|
|
6545
|
-
}
|
|
6649
|
+
writeFileAtomicExclusive(targetPath, content);
|
|
6546
6650
|
}
|
|
6547
6651
|
var cachedValidator = null;
|
|
6548
6652
|
function getSidecarValidator2() {
|
|
@@ -6566,7 +6670,7 @@ function resolveSpecRoot3() {
|
|
|
6566
6670
|
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
6567
6671
|
return dirname7(indexPath);
|
|
6568
6672
|
} catch {
|
|
6569
|
-
throw new Error("@skill-map/spec not resolvable
|
|
6673
|
+
throw new Error("@skill-map/spec not resolvable: sidecar store cannot load schemas.");
|
|
6570
6674
|
}
|
|
6571
6675
|
}
|
|
6572
6676
|
|
|
@@ -6588,7 +6692,7 @@ var BUMP_TEXTS = {
|
|
|
6588
6692
|
pendingNone: "sm bump --pending: no stale sidecars in the persisted scan. Nothing to do.\n",
|
|
6589
6693
|
pendingSummary: "sm bump --pending: bumped {{bumped}}, refused {{refused}}, skipped {{skipped}}, errors {{errors}}.\n",
|
|
6590
6694
|
bumpedItem: " bumped {{nodePath}} -> v{{version}}{{createdSuffix}}\n",
|
|
6591
|
-
refusedItem: " refused {{nodePath}} (fresh
|
|
6695
|
+
refusedItem: " refused {{nodePath}} (fresh, would need --force)\n",
|
|
6592
6696
|
skippedItem: " skipped {{nodePath}} ({{reason}})\n",
|
|
6593
6697
|
errorItem: " error {{nodePath}}: {{message}}\n",
|
|
6594
6698
|
// --- staged-mode (--staged) ---------------------------------------------
|
|
@@ -6604,12 +6708,12 @@ var BUMP_TEXTS = {
|
|
|
6604
6708
|
/**
|
|
6605
6709
|
* Pre-prompt context shown before the interactive `confirm()` so the
|
|
6606
6710
|
* operator sees what they are about to opt into. `.skill-map/settings.local.json`
|
|
6607
|
-
* is gitignored
|
|
6711
|
+
* is gitignored, the choice is saved per-checkout, never travels via the repo.
|
|
6608
6712
|
*/
|
|
6609
6713
|
consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
|
|
6610
6714
|
consentAborted: "{{glyph}} sm bump: aborted by user. No .sm sidecar files were written.\n",
|
|
6611
6715
|
consentRequiredNonTty: "{{glyph}} sm bump: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
|
|
6612
|
-
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json
|
|
6716
|
+
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
|
|
6613
6717
|
};
|
|
6614
6718
|
|
|
6615
6719
|
// cli/util/confirm.ts
|
|
@@ -6743,7 +6847,7 @@ var SmCommand = class extends Command {
|
|
|
6743
6847
|
db = Option.String("--db", { required: false, description: "Override the database file location (escape hatch)." });
|
|
6744
6848
|
/**
|
|
6745
6849
|
* Subclasses set this to `false` to opt out of the trailing
|
|
6746
|
-
* `done in <…>` line
|
|
6850
|
+
* `done in <…>` line, appropriate for interactive verbs (`db shell`),
|
|
6747
6851
|
* watcher loops (`watch`), and meta verbs that report a fixed
|
|
6748
6852
|
* version (`version`, `help`).
|
|
6749
6853
|
*/
|
|
@@ -6886,7 +6990,7 @@ var BumpCommand = class extends SmCommand {
|
|
|
6886
6990
|
static paths = [["bump"]];
|
|
6887
6991
|
static usage = Command2.Usage({
|
|
6888
6992
|
category: "Actions",
|
|
6889
|
-
description: "Bump a node's sidecar (`<basename>.sm`)
|
|
6993
|
+
description: "Bump a node's sidecar (`<basename>.sm`): increment annotations.version, refresh hashes, stamp audit.",
|
|
6890
6994
|
details: `
|
|
6891
6995
|
Wraps the built-in deterministic \`core/bump\` Action. Single-node
|
|
6892
6996
|
mode bumps one path; \`--pending\` walks every node whose sidecar
|
|
@@ -6901,7 +7005,7 @@ var BumpCommand = class extends SmCommand {
|
|
|
6901
7005
|
\`--staged\` (only valid with \`--pending\`) runs \`git add\` on
|
|
6902
7006
|
each successfully-bumped \`.sm\` file so the new content lands in
|
|
6903
7007
|
the same commit. Requires a git binary on PATH and a parent
|
|
6904
|
-
\`.git
|
|
7008
|
+
\`.git/\`: missing repo exits 5, missing binary exits 2.
|
|
6905
7009
|
`,
|
|
6906
7010
|
examples: [
|
|
6907
7011
|
["Bump a single node", "$0 bump .claude/agents/architect.md"],
|
|
@@ -7036,7 +7140,7 @@ var BumpCommand = class extends SmCommand {
|
|
|
7036
7140
|
* Handle the three non-`bumped` outcomes for single-node mode
|
|
7037
7141
|
* (`error`, `refused`, `skipped`). Returns the verb's exit code.
|
|
7038
7142
|
* The caller pre-narrows on `item.status !== 'bumped'` so this
|
|
7039
|
-
* method's union is exhaustive
|
|
7143
|
+
* method's union is exhaustive, the `skipped` branch is the only
|
|
7040
7144
|
* one that exits with `Ok` (silent no-op for fresh + --force).
|
|
7041
7145
|
*/
|
|
7042
7146
|
#renderTerminalSingle(item, node, ansi) {
|
|
@@ -7336,8 +7440,8 @@ function matchesAnalyzerFilter(analyzerId, filter) {
|
|
|
7336
7440
|
// cli/i18n/check.texts.ts
|
|
7337
7441
|
var CHECK_TEXTS = {
|
|
7338
7442
|
noIssues: "{{glyph}} No issues.\n",
|
|
7339
|
-
/** Header summary line: `sm check
|
|
7340
|
-
summaryHeader: "sm check
|
|
7443
|
+
/** Header summary line: `sm check: 10 warnings · 0 errors`. */
|
|
7444
|
+
summaryHeader: "sm check: {{summary}}\n\n",
|
|
7341
7445
|
/** Section heading: one per file with at least one issue. */
|
|
7342
7446
|
fileSection: " {{file}}\n",
|
|
7343
7447
|
/**
|
|
@@ -7349,8 +7453,8 @@ var CHECK_TEXTS = {
|
|
|
7349
7453
|
/** Footer hint, separated from the body by a blank line. */
|
|
7350
7454
|
tipLine: "\nTip: `sm refresh <node>` to revalidate a file after fixes.\n",
|
|
7351
7455
|
// --- prob stub advisory ---------------------------------------------------
|
|
7352
|
-
probStubAdvisory: "sm check --include-prob: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s)
|
|
7353
|
-
probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s)
|
|
7456
|
+
probStubAdvisory: "sm check --include-prob: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. Deterministic analyzers ran as usual; full dispatch lands when the job subsystem ships.\n",
|
|
7457
|
+
probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. The --async flag is reserved for future encoding (returns job ids without waiting once jobs land); today it is a no-op. Deterministic analyzers ran as usual.\n"
|
|
7354
7458
|
};
|
|
7355
7459
|
|
|
7356
7460
|
// cli/util/conformance-env.ts
|
|
@@ -7372,25 +7476,25 @@ var PLUGIN_LOADER_TEXTS = {
|
|
|
7372
7476
|
invalidSpecCompat: 'specCompat "{{specCompat}}" is not a valid semver range. Use a range like "^1.0.0".',
|
|
7373
7477
|
incompatibleSpec: `@skill-map/spec {{installedSpecVersion}} does not satisfy specCompat "{{specCompat}}". Either update the plugin's specCompat (and re-test) or pin sm to a compatible spec version.`,
|
|
7374
7478
|
loadErrorFileNotFound: "extension file not found: {{relEntry}} (resolved to {{abs}}). Check plugin.json#/extensions paths.",
|
|
7375
|
-
loadErrorImportFailed: "{{relEntry}}: import failed
|
|
7479
|
+
loadErrorImportFailed: "{{relEntry}}: import failed: {{errDescription}}",
|
|
7376
7480
|
loadErrorMissingKind: "{{relEntry}}: default export missing a string `kind` field. Expected one of: {{knownKindsList}}.",
|
|
7377
7481
|
loadErrorUnknownKind: '{{relEntry}}: unknown extension kind "{{kindReceived}}". Expected one of: {{knownKindsList}}.',
|
|
7378
|
-
invalidManifestExtensionShape: "{{relEntry}}: {{kind}} manifest invalid
|
|
7379
|
-
importExceededTimeout: "import exceeded {{timeoutMs}}ms
|
|
7482
|
+
invalidManifestExtensionShape: "{{relEntry}}: {{kind}} manifest invalid: {{errors}}. See spec/schemas/extensions/{{kind}}.schema.json.",
|
|
7483
|
+
importExceededTimeout: "import exceeded {{timeoutMs}}ms; likely a top-level side effect (network call, infinite loop, large blocking work). Move side effects into the runtime methods (`detect` / `evaluate` / `render` / etc.).",
|
|
7380
7484
|
disabledByConfig: "disabled by config_plugins or settings.json",
|
|
7381
7485
|
invalidManifestDirMismatch: "directory name '{{dirName}}' does not match manifest id '{{manifestId}}'. Rename the directory to match the id, or update the manifest id to match the directory.",
|
|
7382
7486
|
idCollision: "Plugin '{{id}}' at {{pathA}} collides with the plugin at {{pathB}}. Rename one and rerun.",
|
|
7383
|
-
loadErrorPluginIdMismatch: "{{relEntry}}: extension declares pluginId '{{declared}}' but its plugin.json declares id '{{manifestId}}'. Remove the explicit pluginId from the extension
|
|
7384
|
-
loadErrorStorageSchemaRead: "plugin '{{pluginId}}' failed to load schema for table '{{table}}': {{schemaPath}}
|
|
7385
|
-
loadErrorStorageSchemaCompile: "plugin '{{pluginId}}' failed to compile schema for table '{{table}}': {{schemaPath}}
|
|
7386
|
-
loadErrorStorageKvSchemaRead: "plugin '{{pluginId}}' failed to load KV schema: {{schemaPath}}
|
|
7387
|
-
loadErrorStorageKvSchemaCompile: "plugin '{{pluginId}}' failed to compile KV schema: {{schemaPath}}
|
|
7487
|
+
loadErrorPluginIdMismatch: "{{relEntry}}: extension declares pluginId '{{declared}}' but its plugin.json declares id '{{manifestId}}'. Remove the explicit pluginId from the extension; the loader injects it from plugin.json#/id.",
|
|
7488
|
+
loadErrorStorageSchemaRead: "plugin '{{pluginId}}' failed to load schema for table '{{table}}': {{schemaPath}}: {{errDescription}}",
|
|
7489
|
+
loadErrorStorageSchemaCompile: "plugin '{{pluginId}}' failed to compile schema for table '{{table}}': {{schemaPath}}: {{errDescription}}",
|
|
7490
|
+
loadErrorStorageKvSchemaRead: "plugin '{{pluginId}}' failed to load KV schema: {{schemaPath}}: {{errDescription}}",
|
|
7491
|
+
loadErrorStorageKvSchemaCompile: "plugin '{{pluginId}}' failed to compile KV schema: {{schemaPath}}: {{errDescription}}",
|
|
7388
7492
|
invalidManifestHookUnknownTrigger: "Hook '{{hookId}}' declares unknown trigger '{{trigger}}'. Hookable triggers: {{hookableList}}.",
|
|
7389
7493
|
invalidManifestHookEmptyTriggers: "Hook '{{hookId}}' declares no triggers. At least one entry from the curated set is required.",
|
|
7390
7494
|
loadErrorPathEscapesPlugin: "extension entry '{{relEntry}}' resolves outside the plugin directory ({{pluginPath}}). Plugin entries must be relative paths inside the plugin tree.",
|
|
7391
7495
|
loadErrorSchemaPathEscapesPlugin: "schema path '{{relPath}}' resolves outside the plugin directory ({{pluginPath}}). Plugin schemas must be relative paths inside the plugin tree.",
|
|
7392
|
-
invalidManifestRootSharedAnnotation: "{{relEntry}}: annotationContributions['{{key}}'] declares location: 'root' with ownership: '{{ownership}}'
|
|
7393
|
-
invalidManifestAnnotationSchemaCompile: "{{relEntry}}: annotationContributions['{{key}}'].schema is not a valid JSON Schema
|
|
7496
|
+
invalidManifestRootSharedAnnotation: "{{relEntry}}: annotationContributions['{{key}}'] declares location: 'root' with ownership: '{{ownership}}'; root keys MUST be 'exclusive' (a top-level reserved key cannot be silently shared between plugins).",
|
|
7497
|
+
invalidManifestAnnotationSchemaCompile: "{{relEntry}}: annotationContributions['{{key}}'].schema is not a valid JSON Schema: {{errDescription}}",
|
|
7394
7498
|
fatalAnnotationRootCollision: "Annotation root-key collision: '{{key}}' is claimed with ownership: 'exclusive' by multiple plugins ({{plugins}}). The kernel cannot boot with this configuration. Rename or merge the contributions and rerun."
|
|
7395
7499
|
};
|
|
7396
7500
|
|
|
@@ -7756,10 +7860,10 @@ var PluginLoader = class {
|
|
|
7756
7860
|
return out;
|
|
7757
7861
|
}
|
|
7758
7862
|
/**
|
|
7759
|
-
* Full pass
|
|
7863
|
+
* Full pass, discover every plugin, attempt to load each, then apply
|
|
7760
7864
|
* the cross-root id-collision pass over the results. Two plugins that
|
|
7761
7865
|
* survived their individual load with the same `manifest.id` both get
|
|
7762
|
-
* downgraded to status `id-collision` (no precedence
|
|
7866
|
+
* downgraded to status `id-collision` (no precedence, the spec is
|
|
7763
7867
|
* explicit that "no extension is privileged"). Plugins that already
|
|
7764
7868
|
* failed their individual load (`invalid-manifest` /
|
|
7765
7869
|
* `incompatible-spec` / `load-error`) keep their original status:
|
|
@@ -7776,7 +7880,7 @@ var PluginLoader = class {
|
|
|
7776
7880
|
return applyIdCollisions(out);
|
|
7777
7881
|
}
|
|
7778
7882
|
/**
|
|
7779
|
-
* Load a single plugin from its directory. Never throws
|
|
7883
|
+
* Load a single plugin from its directory. Never throws, a failure is
|
|
7780
7884
|
* reported via the returned status.
|
|
7781
7885
|
*/
|
|
7782
7886
|
// eslint-disable-next-line complexity
|
|
@@ -7818,7 +7922,7 @@ var PluginLoader = class {
|
|
|
7818
7922
|
};
|
|
7819
7923
|
}
|
|
7820
7924
|
/**
|
|
7821
|
-
* Phase 1 of `loadOne
|
|
7925
|
+
* Phase 1 of `loadOne`, read `plugin.json`, AJV-validate the manifest,
|
|
7822
7926
|
* enforce the directory-name == manifest.id structural rule, and check
|
|
7823
7927
|
* specCompat (range syntax + satisfies the installed spec version).
|
|
7824
7928
|
* Returns either the validated manifest or an `IDiscoveredPlugin` with
|
|
@@ -7895,7 +7999,7 @@ var PluginLoader = class {
|
|
|
7895
7999
|
return { ok: true, manifest };
|
|
7896
8000
|
}
|
|
7897
8001
|
/**
|
|
7898
|
-
* Phase 3 of `loadOne
|
|
8002
|
+
* Phase 3 of `loadOne`, load and validate one extension entry. Six
|
|
7899
8003
|
* sub-checks (file exists, dynamic import, has kind, kind known,
|
|
7900
8004
|
* pluginId match, kind-specific manifest validation including hook
|
|
7901
8005
|
* trigger pre-check). On success returns the `ILoadedExtension` with
|
|
@@ -8046,20 +8150,37 @@ var LOCKED_PLUGIN_IDS = /* @__PURE__ */ new Set([
|
|
|
8046
8150
|
// `core/markdown` is the universal `.md` fallback Provider (see
|
|
8047
8151
|
// spec/architecture.md §"core/markdown is the universal fallback for
|
|
8048
8152
|
// unclaimed `.md` files"). Disabling it makes every orphan markdown
|
|
8049
|
-
// silently invisible
|
|
8153
|
+
// silently invisible, a foot-gun the host product does not want to
|
|
8050
8154
|
// expose. Lock it in the enabled state.
|
|
8051
8155
|
"core/markdown",
|
|
8052
8156
|
// `core/annotations` turns the `supersedes` / `supersededBy` /
|
|
8053
8157
|
// `requires` / `related` / `conflictsWith` entries of the sidecar
|
|
8054
8158
|
// `annotations:` block into the arrows the graph draws between nodes.
|
|
8055
8159
|
// It does NOT own the rest of the block (`version`, `stability`,
|
|
8056
|
-
// `tags`, `description
|
|
8160
|
+
// `tags`, `description`, those live on the node bundle directly and
|
|
8057
8161
|
// keep rendering with the plugin off). Disabling it produces a
|
|
8058
8162
|
// confusing "edges disappear but the sidecar metadata stays" split
|
|
8059
8163
|
// that no operator actually wants; the lock makes the asymmetry
|
|
8060
8164
|
// unreachable from CLI / BFF / UI. Re-evaluate if a third-party ever
|
|
8061
8165
|
// ships a competing supersession extractor.
|
|
8062
|
-
"core/annotations"
|
|
8166
|
+
"core/annotations",
|
|
8167
|
+
// `core/validate-all` validates every scanned Node against
|
|
8168
|
+
// `node.schema.json` and every Link against `link.schema.json` (the
|
|
8169
|
+
// authoritative @skill-map/spec). Disabling it makes the system
|
|
8170
|
+
// persist non-conformant content silently, breaking the spec
|
|
8171
|
+
// invariant "what reaches the DB conforms to the spec". The check is
|
|
8172
|
+
// foundational, not advisory; lock it on so the guarantee holds
|
|
8173
|
+
// regardless of user / DB / settings hand-edits.
|
|
8174
|
+
"core/validate-all",
|
|
8175
|
+
// `core/ascii` is the only built-in Formatter today and the default
|
|
8176
|
+
// for `sm graph` (`--format ascii`). Disabling it breaks the verb
|
|
8177
|
+
// entirely (`composeFormatters` returns the empty list, the CLI
|
|
8178
|
+
// prints "no formatter registered for 'ascii'" and exits with an
|
|
8179
|
+
// error) with no useful fallback. Lock it on until additional
|
|
8180
|
+
// formatters land (mermaid / dot / json, deferred in ROADMAP § Built-in
|
|
8181
|
+
// graph formatters); revisit the lock once `sm graph` has a real
|
|
8182
|
+
// catalog to choose from.
|
|
8183
|
+
"core/ascii"
|
|
8063
8184
|
]);
|
|
8064
8185
|
function isPluginLocked(idOrQualified) {
|
|
8065
8186
|
return LOCKED_PLUGIN_IDS.has(idOrQualified);
|
|
@@ -8117,7 +8238,7 @@ async function buildEnabledResolver(scope, ctx) {
|
|
|
8117
8238
|
}
|
|
8118
8239
|
|
|
8119
8240
|
// kernel/scan/walk-content.ts
|
|
8120
|
-
import { readFile, readdir,
|
|
8241
|
+
import { readFile, readdir, lstat } from "fs/promises";
|
|
8121
8242
|
import { join as join6, relative as relative2, sep as sep2 } from "path";
|
|
8122
8243
|
|
|
8123
8244
|
// kernel/scan/ignore.ts
|
|
@@ -8200,7 +8321,6 @@ function readDefaultsFromDisk() {
|
|
|
8200
8321
|
// built-in-plugins/parsers/frontmatter-yaml/index.ts
|
|
8201
8322
|
import yaml3 from "js-yaml";
|
|
8202
8323
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
8203
|
-
var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
8204
8324
|
var frontmatterYamlParser = {
|
|
8205
8325
|
id: "frontmatter-yaml",
|
|
8206
8326
|
parse(raw, _path) {
|
|
@@ -8208,20 +8328,30 @@ var frontmatterYamlParser = {
|
|
|
8208
8328
|
if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
|
|
8209
8329
|
const frontmatterRaw = match[1];
|
|
8210
8330
|
const body = match[2];
|
|
8211
|
-
|
|
8331
|
+
let parsed = {};
|
|
8332
|
+
const issues = [];
|
|
8212
8333
|
try {
|
|
8213
8334
|
const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
|
|
8214
8335
|
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
|
|
8215
|
-
|
|
8216
|
-
if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
|
|
8217
|
-
parsed[k] = v;
|
|
8218
|
-
}
|
|
8336
|
+
parsed = stripPrototypePollution(doc);
|
|
8219
8337
|
}
|
|
8220
|
-
} catch {
|
|
8338
|
+
} catch (err) {
|
|
8339
|
+
issues.push({
|
|
8340
|
+
code: "frontmatter-parse-error",
|
|
8341
|
+
message: sanitiseParseErrorMessage(err)
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
const out = { frontmatterRaw, frontmatter: parsed, body };
|
|
8345
|
+
if (issues.length > 0) {
|
|
8346
|
+
return { ...out, issues };
|
|
8221
8347
|
}
|
|
8222
|
-
return
|
|
8348
|
+
return out;
|
|
8223
8349
|
}
|
|
8224
8350
|
};
|
|
8351
|
+
function sanitiseParseErrorMessage(err) {
|
|
8352
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
8353
|
+
return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
8354
|
+
}
|
|
8225
8355
|
|
|
8226
8356
|
// built-in-plugins/parsers/plain/index.ts
|
|
8227
8357
|
var plainParser = {
|
|
@@ -8267,7 +8397,12 @@ async function* walkContent(roots, options) {
|
|
|
8267
8397
|
path: relPath,
|
|
8268
8398
|
body: parsed.body,
|
|
8269
8399
|
frontmatterRaw: parsed.frontmatterRaw,
|
|
8270
|
-
frontmatter: parsed.frontmatter
|
|
8400
|
+
frontmatter: parsed.frontmatter,
|
|
8401
|
+
// Audit L1: forward parser diagnostics (e.g. malformed YAML)
|
|
8402
|
+
// through the IRawNode surface so the orchestrator can
|
|
8403
|
+
// convert them into warn-level kernel `Issue` rows. Omitted
|
|
8404
|
+
// when the parser reported no issues (happy path).
|
|
8405
|
+
...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
|
|
8271
8406
|
};
|
|
8272
8407
|
}
|
|
8273
8408
|
}
|
|
@@ -8289,7 +8424,7 @@ async function* walkRoot(root, current, filter, extensions) {
|
|
|
8289
8424
|
yield* walkRoot(root, full, filter, extensions);
|
|
8290
8425
|
} else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
|
|
8291
8426
|
try {
|
|
8292
|
-
const s = await
|
|
8427
|
+
const s = await lstat(full);
|
|
8293
8428
|
if (s.isFile()) yield full;
|
|
8294
8429
|
} catch {
|
|
8295
8430
|
}
|
|
@@ -8364,7 +8499,7 @@ function bucketLoaded(loaded, bundle) {
|
|
|
8364
8499
|
analyzer: bundle.extensions.analyzers,
|
|
8365
8500
|
formatter: bundle.extensions.formatters,
|
|
8366
8501
|
hook: bundle.extensions.hooks
|
|
8367
|
-
// `action` intentionally absent
|
|
8502
|
+
// `action` intentionally absent, see docstring.
|
|
8368
8503
|
});
|
|
8369
8504
|
bundle.manifests.push({
|
|
8370
8505
|
id: ext.id,
|
|
@@ -8420,7 +8555,7 @@ var PLUGIN_RUNTIME_TEXTS = {
|
|
|
8420
8555
|
* status word and the reason scannable so a user can grep
|
|
8421
8556
|
* `incompatible-spec` / `invalid-manifest` / `load-error`.
|
|
8422
8557
|
*/
|
|
8423
|
-
warningRow: "plugin {{id}}: {{status}}
|
|
8558
|
+
warningRow: "plugin {{id}}: {{status}}, {{reason}}",
|
|
8424
8559
|
/** Placeholder when a non-loaded plugin record carries no `reason`. */
|
|
8425
8560
|
warningReasonMissing: "(no reason recorded)"
|
|
8426
8561
|
};
|
|
@@ -8670,7 +8805,7 @@ function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
|
|
|
8670
8805
|
// catalog row carries `pluginId`, not `extensionId`), so the
|
|
8671
8806
|
// bundle-level toggle gates the entire row. Extension
|
|
8672
8807
|
// granularity falls through to the manifest-level filter above
|
|
8673
|
-
//
|
|
8808
|
+
// this surface is bundle-scoped by design.
|
|
8674
8809
|
resolveEnabled(entry.pluginId)
|
|
8675
8810
|
)
|
|
8676
8811
|
);
|
|
@@ -8710,7 +8845,7 @@ var CheckCommand = class extends SmCommand {
|
|
|
8710
8845
|
Run \`sm scan\` first to populate the DB.
|
|
8711
8846
|
|
|
8712
8847
|
\`--include-prob\` is an opt-in flag for probabilistic Analyzer
|
|
8713
|
-
dispatch (spec \xA7 A.7). Default is deterministic-only
|
|
8848
|
+
dispatch (spec \xA7 A.7). Default is deterministic-only: same
|
|
8714
8849
|
CI-safe behaviour as before. With the flag, registered prob
|
|
8715
8850
|
rules are detected and named in a stderr advisory; full
|
|
8716
8851
|
dispatch lands when the job subsystem ships at Step 10.
|
|
@@ -8928,7 +9063,7 @@ var CONFIG_TEXTS = {
|
|
|
8928
9063
|
/**
|
|
8929
9064
|
* Surfaced when a PROJECT_LOCAL_ONLY key (`allowEditSmFiles` /
|
|
8930
9065
|
* `scan.extraFolders` / `scan.referencePaths`) reaches the writer
|
|
8931
|
-
* with `target: 'project'
|
|
9066
|
+
* with `target: 'project'`, defensive only, the CLI auto-routes to
|
|
8932
9067
|
* `project-local`, but the helper enforces the rule for any other
|
|
8933
9068
|
* caller too.
|
|
8934
9069
|
*/
|
|
@@ -8959,8 +9094,8 @@ var CONFIG_TEXTS = {
|
|
|
8959
9094
|
* indented under the section heading.
|
|
8960
9095
|
*/
|
|
8961
9096
|
listRow: " {{key}} {{value}}\n",
|
|
8962
|
-
/** Placeholder for null / empty array / empty object
|
|
8963
|
-
listEmptyValue: "
|
|
9097
|
+
/** Placeholder for null / empty array / empty object, printed dim. */
|
|
9098
|
+
listEmptyValue: "-",
|
|
8964
9099
|
/** Section titles. */
|
|
8965
9100
|
listSectionGeneral: "General",
|
|
8966
9101
|
listSectionScan: "Scan",
|
|
@@ -9435,7 +9570,7 @@ var ConfigResetCommand = class extends SmCommand {
|
|
|
9435
9570
|
description: "Remove a config key from the target file (project default; -g for user).",
|
|
9436
9571
|
details: `
|
|
9437
9572
|
Strips the key from the target settings.json (lower layers still apply).
|
|
9438
|
-
Idempotent
|
|
9573
|
+
Idempotent: running twice is safe; absent key prints an info note and exits 0.
|
|
9439
9574
|
`
|
|
9440
9575
|
});
|
|
9441
9576
|
key = Option4.String({ required: true });
|
|
@@ -9901,7 +10036,7 @@ function resolveSpecRoot4() {
|
|
|
9901
10036
|
return dirname10(indexPath);
|
|
9902
10037
|
} catch {
|
|
9903
10038
|
throw new Error(
|
|
9904
|
-
"@skill-map/spec not resolvable
|
|
10039
|
+
"@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
|
|
9905
10040
|
);
|
|
9906
10041
|
}
|
|
9907
10042
|
}
|
|
@@ -9918,7 +10053,7 @@ function resolveCliWorkspaceRoot() {
|
|
|
9918
10053
|
cursor = parent;
|
|
9919
10054
|
}
|
|
9920
10055
|
throw new Error(
|
|
9921
|
-
`sm conformance: built-in Provider conformance assets not found (expected a 'built-in-plugins/providers/' directory above ${here}). The bundled CLI may not yet copy the assets
|
|
10056
|
+
`sm conformance: built-in Provider conformance assets not found (expected a 'built-in-plugins/providers/' directory above ${here}). The bundled CLI may not yet copy the assets; run from the source workspace, or rebuild after enabling the asset-copy step.`
|
|
9922
10057
|
);
|
|
9923
10058
|
}
|
|
9924
10059
|
function collectProviderScopes(specRoot) {
|
|
@@ -10007,7 +10142,7 @@ var ConformanceRunCommand = class extends SmCommand {
|
|
|
10007
10142
|
static paths = [["conformance", "run"]];
|
|
10008
10143
|
static usage = Command5.Usage({
|
|
10009
10144
|
category: "Introspection",
|
|
10010
|
-
description: "Run the conformance suite
|
|
10145
|
+
description: "Run the conformance suite: spec-owned cases plus every built-in Provider.",
|
|
10011
10146
|
details: `
|
|
10012
10147
|
Drives the conformance runner shipped at
|
|
10013
10148
|
\`@skill-map/cli/conformance\` against the cases bundled with
|
|
@@ -10055,11 +10190,22 @@ var ConformanceRunCommand = class extends SmCommand {
|
|
|
10055
10190
|
scopes = selectConformanceScopes(this.scope);
|
|
10056
10191
|
} catch (err) {
|
|
10057
10192
|
const message = formatErrorMessage(err);
|
|
10193
|
+
if (this.json) {
|
|
10194
|
+
this.#emitJsonError("bad-query", message);
|
|
10195
|
+
return ExitCode.Error;
|
|
10196
|
+
}
|
|
10058
10197
|
this.printer.error(tx(CONFORMANCE_TEXTS.unknownScope, { glyph: errGlyph, message }));
|
|
10059
10198
|
return ExitCode.Error;
|
|
10060
10199
|
}
|
|
10061
10200
|
const binary = resolveBinary();
|
|
10062
10201
|
if (!existsSync16(binary)) {
|
|
10202
|
+
if (this.json) {
|
|
10203
|
+
this.#emitJsonError(
|
|
10204
|
+
"internal",
|
|
10205
|
+
`cannot locate the sm binary at ${binary}`
|
|
10206
|
+
);
|
|
10207
|
+
return ExitCode.Error;
|
|
10208
|
+
}
|
|
10063
10209
|
this.printer.error(
|
|
10064
10210
|
tx(CONFORMANCE_TEXTS.noBinary, {
|
|
10065
10211
|
glyph: errGlyph,
|
|
@@ -10072,21 +10218,28 @@ var ConformanceRunCommand = class extends SmCommand {
|
|
|
10072
10218
|
let totalPass = 0;
|
|
10073
10219
|
let totalCases = 0;
|
|
10074
10220
|
let anyFailure = false;
|
|
10221
|
+
const scopeReports = [];
|
|
10075
10222
|
for (const scope of scopes) {
|
|
10076
10223
|
const cases = listCaseFiles(scope);
|
|
10077
10224
|
if (cases.length === 0) {
|
|
10225
|
+
if (!this.json) {
|
|
10226
|
+
this.printer.data(
|
|
10227
|
+
tx(CONFORMANCE_TEXTS.scopeEmpty, { label: scope.label })
|
|
10228
|
+
);
|
|
10229
|
+
}
|
|
10230
|
+
scopeReports.push({ label: scope.label, passCount: 0, caseCount: 0, cases: [] });
|
|
10231
|
+
continue;
|
|
10232
|
+
}
|
|
10233
|
+
if (!this.json) {
|
|
10078
10234
|
this.printer.data(
|
|
10079
|
-
tx(CONFORMANCE_TEXTS.
|
|
10235
|
+
tx(CONFORMANCE_TEXTS.scopeHeader, {
|
|
10236
|
+
label: scope.label,
|
|
10237
|
+
caseCount: cases.length
|
|
10238
|
+
})
|
|
10080
10239
|
);
|
|
10081
|
-
continue;
|
|
10082
10240
|
}
|
|
10083
|
-
this.printer.data(
|
|
10084
|
-
tx(CONFORMANCE_TEXTS.scopeHeader, {
|
|
10085
|
-
label: scope.label,
|
|
10086
|
-
caseCount: cases.length
|
|
10087
|
-
})
|
|
10088
|
-
);
|
|
10089
10241
|
let scopePass = 0;
|
|
10242
|
+
const caseReports = [];
|
|
10090
10243
|
for (const casePath of cases) {
|
|
10091
10244
|
const caseId = readCaseId(casePath);
|
|
10092
10245
|
try {
|
|
@@ -10097,51 +10250,92 @@ var ConformanceRunCommand = class extends SmCommand {
|
|
|
10097
10250
|
fixturesRoot: scope.fixturesDir
|
|
10098
10251
|
});
|
|
10099
10252
|
if (result.passed) {
|
|
10100
|
-
this.
|
|
10101
|
-
|
|
10102
|
-
|
|
10253
|
+
if (!this.json) {
|
|
10254
|
+
this.printer.data(
|
|
10255
|
+
tx(CONFORMANCE_TEXTS.caseOk, { caseId: result.caseId })
|
|
10256
|
+
);
|
|
10257
|
+
}
|
|
10258
|
+
caseReports.push({ id: result.caseId, status: "pass", failures: [] });
|
|
10103
10259
|
scopePass += 1;
|
|
10104
10260
|
} else {
|
|
10105
10261
|
anyFailure = true;
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10262
|
+
const failures = projectAssertionFailures(result.assertions);
|
|
10263
|
+
if (!this.json) {
|
|
10264
|
+
this.printer.data(
|
|
10265
|
+
tx(CONFORMANCE_TEXTS.caseFail, { caseId: result.caseId })
|
|
10266
|
+
);
|
|
10267
|
+
for (const a of result.assertions) {
|
|
10268
|
+
if (a.ok) continue;
|
|
10269
|
+
this.printer.info(
|
|
10270
|
+
formatAssertionFailureDetail(a.type, a.reason)
|
|
10271
|
+
);
|
|
10272
|
+
}
|
|
10273
|
+
writeStreamSnippet(
|
|
10274
|
+
this.context.stderr,
|
|
10275
|
+
CONFORMANCE_TEXTS.caseFailureStdoutHeader,
|
|
10276
|
+
result.stdout
|
|
10277
|
+
);
|
|
10278
|
+
writeStreamSnippet(
|
|
10279
|
+
this.context.stderr,
|
|
10280
|
+
CONFORMANCE_TEXTS.caseFailureStderrHeader,
|
|
10281
|
+
result.stderr
|
|
10113
10282
|
);
|
|
10114
10283
|
}
|
|
10115
|
-
|
|
10116
|
-
this.context.stderr,
|
|
10117
|
-
CONFORMANCE_TEXTS.caseFailureStdoutHeader,
|
|
10118
|
-
result.stdout
|
|
10119
|
-
);
|
|
10120
|
-
writeStreamSnippet(
|
|
10121
|
-
this.context.stderr,
|
|
10122
|
-
CONFORMANCE_TEXTS.caseFailureStderrHeader,
|
|
10123
|
-
result.stderr
|
|
10124
|
-
);
|
|
10284
|
+
caseReports.push({ id: result.caseId, status: "fail", failures });
|
|
10125
10285
|
}
|
|
10126
10286
|
} catch (err) {
|
|
10127
10287
|
anyFailure = true;
|
|
10128
10288
|
const message = formatErrorMessage(err);
|
|
10129
|
-
this.
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10289
|
+
if (!this.json) {
|
|
10290
|
+
this.printer.error(
|
|
10291
|
+
tx(CONFORMANCE_TEXTS.runtimeError, { glyph: errGlyph, message })
|
|
10292
|
+
);
|
|
10293
|
+
this.printer.data(tx(CONFORMANCE_TEXTS.caseFail, { caseId }));
|
|
10294
|
+
}
|
|
10295
|
+
caseReports.push({
|
|
10296
|
+
id: caseId,
|
|
10297
|
+
status: "fail",
|
|
10298
|
+
failures: [{
|
|
10299
|
+
type: "runtime-error",
|
|
10300
|
+
reason: sanitizeForTerminal(truncateHead(message, ASSERTION_REASON_DISPLAY_CAP))
|
|
10301
|
+
}]
|
|
10302
|
+
});
|
|
10133
10303
|
}
|
|
10134
10304
|
}
|
|
10135
|
-
this.
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10305
|
+
if (!this.json) {
|
|
10306
|
+
this.printer.data(
|
|
10307
|
+
tx(CONFORMANCE_TEXTS.scopeSummary, {
|
|
10308
|
+
label: scope.label,
|
|
10309
|
+
passCount: scopePass,
|
|
10310
|
+
caseCount: cases.length
|
|
10311
|
+
})
|
|
10312
|
+
);
|
|
10313
|
+
}
|
|
10314
|
+
scopeReports.push({
|
|
10315
|
+
label: scope.label,
|
|
10316
|
+
passCount: scopePass,
|
|
10317
|
+
caseCount: cases.length,
|
|
10318
|
+
cases: caseReports
|
|
10319
|
+
});
|
|
10142
10320
|
totalPass += scopePass;
|
|
10143
10321
|
totalCases += cases.length;
|
|
10144
10322
|
}
|
|
10323
|
+
if (this.json) {
|
|
10324
|
+
const envelope = {
|
|
10325
|
+
ok: true,
|
|
10326
|
+
kind: "conformance.result",
|
|
10327
|
+
totals: {
|
|
10328
|
+
scopes: scopes.length,
|
|
10329
|
+
cases: totalCases,
|
|
10330
|
+
passCount: totalPass,
|
|
10331
|
+
failCount: totalCases - totalPass
|
|
10332
|
+
},
|
|
10333
|
+
scopes: scopeReports,
|
|
10334
|
+
elapsedMs: this.elapsed.ms()
|
|
10335
|
+
};
|
|
10336
|
+
this.printer.data(JSON.stringify(envelope) + "\n");
|
|
10337
|
+
return anyFailure ? ExitCode.Issues : ExitCode.Ok;
|
|
10338
|
+
}
|
|
10145
10339
|
this.printer.data(
|
|
10146
10340
|
tx(CONFORMANCE_TEXTS.totalSummary, {
|
|
10147
10341
|
passCount: totalPass,
|
|
@@ -10152,7 +10346,30 @@ var ConformanceRunCommand = class extends SmCommand {
|
|
|
10152
10346
|
if (anyFailure) return ExitCode.Issues;
|
|
10153
10347
|
return ExitCode.Ok;
|
|
10154
10348
|
}
|
|
10349
|
+
/**
|
|
10350
|
+
* Emit the canonical `--json` error envelope on stdout. Mirrors the
|
|
10351
|
+
* shape from `cli-contract.md` §Error envelope. Suppresses the
|
|
10352
|
+
* human-facing glyph + hint output that the non-JSON branches still
|
|
10353
|
+
* render.
|
|
10354
|
+
*/
|
|
10355
|
+
#emitJsonError(code, message) {
|
|
10356
|
+
const payload = { ok: false, error: { code, message } };
|
|
10357
|
+
this.printer.data(JSON.stringify(payload) + "\n");
|
|
10358
|
+
}
|
|
10155
10359
|
};
|
|
10360
|
+
function projectAssertionFailures(assertions) {
|
|
10361
|
+
const out = [];
|
|
10362
|
+
for (const a of assertions) {
|
|
10363
|
+
if (a.ok) continue;
|
|
10364
|
+
out.push({
|
|
10365
|
+
type: a.type,
|
|
10366
|
+
reason: sanitizeForTerminal(
|
|
10367
|
+
truncateHead(a.reason ?? "", ASSERTION_REASON_DISPLAY_CAP)
|
|
10368
|
+
)
|
|
10369
|
+
});
|
|
10370
|
+
}
|
|
10371
|
+
return out;
|
|
10372
|
+
}
|
|
10156
10373
|
function readCaseId(casePath) {
|
|
10157
10374
|
try {
|
|
10158
10375
|
const raw = readFileSync13(casePath, "utf8");
|
|
@@ -10226,13 +10443,13 @@ var DB_TEXTS = {
|
|
|
10226
10443
|
pluginMigrateApplied: "{{glyph}} plugin {{pluginId}} \xB7 Applied {{count}} migration(s)\n",
|
|
10227
10444
|
pluginMigrateIntrusion: "plugin {{pluginId}} \xB7 catalog intrusion detected: {{intrusions}}\n",
|
|
10228
10445
|
// --- dry-run previews ------------------------------------------------
|
|
10229
|
-
dryRunHeader: "(dry-run
|
|
10230
|
-
dryRunResetWouldClearNone: "would clear 0 table(s): (none
|
|
10446
|
+
dryRunHeader: "(dry-run, no DB writes, no file unlinks)\n",
|
|
10447
|
+
dryRunResetWouldClearNone: "would clear 0 table(s): (none, DB schema is empty)\n",
|
|
10231
10448
|
// The `lines` arg is a pre-built multi-line block, one " - name: N row(s)"
|
|
10232
10449
|
// per table, joined with `\n`.
|
|
10233
10450
|
dryRunResetWouldClearWithRowCounts: "would clear {{tableCount}} table(s) ({{totalRows}} total row(s)):\n{{lines}}\n",
|
|
10234
10451
|
dryRunResetHardWouldDelete: "would delete {{path}} ({{sizeBytes}} bytes)\n",
|
|
10235
|
-
dryRunResetHardWouldDeleteMissing: "would delete {{path}} (file does not exist
|
|
10452
|
+
dryRunResetHardWouldDeleteMissing: "would delete {{path}} (file does not exist, no-op)\n",
|
|
10236
10453
|
// The `targetClause` arg is one of two pre-built strings:
|
|
10237
10454
|
// "(exists, would be overwritten)" / "(does not exist, would be created)".
|
|
10238
10455
|
dryRunRestoreWouldOverwrite: "would copy {{sourcePath}} ({{sourceBytes}} bytes) \u2192 {{target}} {{targetClause}}\nwould delete {{target}}-wal and {{target}}-shm sidecars if present\n",
|
|
@@ -10249,7 +10466,7 @@ var DbBackupCommand = class extends SmCommand {
|
|
|
10249
10466
|
details: `
|
|
10250
10467
|
Default output: <db-dir>/backups/<timestamp>.db. Use --out to override.
|
|
10251
10468
|
scan_* is regenerated on demand and is NOT excluded from the raw file
|
|
10252
|
-
copy, but restoring a backup over a live DB is the expected use
|
|
10469
|
+
copy, but restoring a backup over a live DB is the expected use;
|
|
10253
10470
|
running sm scan afterwards refreshes scan_*.
|
|
10254
10471
|
`
|
|
10255
10472
|
});
|
|
@@ -10281,10 +10498,10 @@ import { dirname as dirname13, resolve as resolve23 } from "path";
|
|
|
10281
10498
|
import { Command as Command7, Option as Option7 } from "clipanion";
|
|
10282
10499
|
|
|
10283
10500
|
// cli/util/fs.ts
|
|
10284
|
-
import { stat
|
|
10501
|
+
import { stat } from "fs/promises";
|
|
10285
10502
|
async function pathExists(path) {
|
|
10286
10503
|
try {
|
|
10287
|
-
await
|
|
10504
|
+
await stat(path);
|
|
10288
10505
|
return true;
|
|
10289
10506
|
} catch (err) {
|
|
10290
10507
|
if (err.code === "ENOENT") return false;
|
|
@@ -10293,7 +10510,7 @@ async function pathExists(path) {
|
|
|
10293
10510
|
}
|
|
10294
10511
|
async function statOrNull(path) {
|
|
10295
10512
|
try {
|
|
10296
|
-
return await
|
|
10513
|
+
return await stat(path);
|
|
10297
10514
|
} catch (err) {
|
|
10298
10515
|
if (err.code === "ENOENT") return null;
|
|
10299
10516
|
throw err;
|
|
@@ -10401,10 +10618,10 @@ var DbResetCommand = class extends SmCommand {
|
|
|
10401
10618
|
category: "Database",
|
|
10402
10619
|
description: "Drop scan_* (default), optionally state_*, or delete the DB entirely.",
|
|
10403
10620
|
details: `
|
|
10404
|
-
Without flags: drops scan_* tables only. Non-destructive
|
|
10405
|
-
With --state: also drops state_* tables. Destructive
|
|
10621
|
+
Without flags: drops scan_* tables only. Non-destructive, no prompt.
|
|
10622
|
+
With --state: also drops state_* tables. Destructive, requires
|
|
10406
10623
|
confirmation unless --yes / --force.
|
|
10407
|
-
With --hard: deletes the DB file entirely. Destructive
|
|
10624
|
+
With --hard: deletes the DB file entirely. Destructive, requires
|
|
10408
10625
|
confirmation unless --yes / --force.
|
|
10409
10626
|
With --dry-run: previews what would be cleared / deleted without
|
|
10410
10627
|
touching the DB. Bypasses the confirmation prompt entirely (the
|
|
@@ -10544,7 +10761,7 @@ var DbShellCommand = class extends SmCommand {
|
|
|
10544
10761
|
`
|
|
10545
10762
|
});
|
|
10546
10763
|
// Interactive shell: the spawned `sqlite3` owns the terminal. No
|
|
10547
|
-
// `done in <…>` line
|
|
10764
|
+
// `done in <…>` line, the user expects to see the shell's own
|
|
10548
10765
|
// prompt + farewell, not a follow-up trailer once they exit.
|
|
10549
10766
|
emitElapsed = false;
|
|
10550
10767
|
async run() {
|
|
@@ -10595,7 +10812,7 @@ var DbBrowserCommand = class extends SmCommand {
|
|
|
10595
10812
|
]
|
|
10596
10813
|
});
|
|
10597
10814
|
// GUI launch: the spawned process is detached and unref'd; we exit
|
|
10598
|
-
// immediately. No `done in <…>` line
|
|
10815
|
+
// immediately. No `done in <…>` line, the user expects to see the
|
|
10599
10816
|
// GUI window, not a follow-up trailer in the terminal.
|
|
10600
10817
|
emitElapsed = false;
|
|
10601
10818
|
rw = Option9.Boolean("--rw", false, {
|
|
@@ -10637,7 +10854,7 @@ var DbDumpCommand = class extends SmCommand {
|
|
|
10637
10854
|
static usage = Command11.Usage({
|
|
10638
10855
|
category: "Database",
|
|
10639
10856
|
description: "SQL dump to stdout.",
|
|
10640
|
-
details: "Read-only. Pure node:sqlite
|
|
10857
|
+
details: "Read-only. Pure node:sqlite; no external `sqlite3` binary required. Use --tables <names...> to limit the dump to specific tables."
|
|
10641
10858
|
});
|
|
10642
10859
|
tables = Option10.Array("--tables", { required: false });
|
|
10643
10860
|
async run() {
|
|
@@ -11128,17 +11345,17 @@ var EXPORT_TEXTS = {
|
|
|
11128
11345
|
/** Echo of the user's query string (or the empty placeholder). */
|
|
11129
11346
|
mdQueryLine: "Query: `{{query}}`",
|
|
11130
11347
|
/** Placeholder used when the user's query is empty. */
|
|
11131
|
-
mdQueryEmpty: "(empty
|
|
11348
|
+
mdQueryEmpty: "(empty, all nodes)",
|
|
11132
11349
|
/** Counts summary line under the query. */
|
|
11133
11350
|
mdCounts: "Counts: {{nodes}} nodes, {{links}} links, {{issues}} issues.",
|
|
11134
11351
|
/** Section header for a single node-kind group. */
|
|
11135
11352
|
mdKindSectionHeader: "## {{kind}} ({{count}})",
|
|
11136
11353
|
/** Bullet template for a node row. `{{title}}` and `{{issues}}` are pre-rendered (empty when absent). */
|
|
11137
11354
|
mdNodeBullet: "- `{{path}}`{{title}}{{issues}}",
|
|
11138
|
-
/**
|
|
11139
|
-
mdNodeTitleSuffix: '
|
|
11140
|
-
/** `
|
|
11141
|
-
mdNodeIssueSuffix: "
|
|
11355
|
+
/** `: "<title>"` segment when the node has a title. */
|
|
11356
|
+
mdNodeTitleSuffix: ': "{{title}}"',
|
|
11357
|
+
/** ` (N issue(s))` segment when the node has any associated issues. */
|
|
11358
|
+
mdNodeIssueSuffix: " ({{count}} {{label}})",
|
|
11142
11359
|
mdNodeIssueLabelSingular: "issue",
|
|
11143
11360
|
mdNodeIssueLabelPlural: "issues",
|
|
11144
11361
|
/** Section header for the links block. */
|
|
@@ -11169,10 +11386,10 @@ var ExportCommand = class extends SmCommand {
|
|
|
11169
11386
|
Query syntax (v0.5.0): whitespace-separated key=value tokens; AND
|
|
11170
11387
|
across keys, OR within comma-separated values. Keys: \`kind\`
|
|
11171
11388
|
(skill / agent / command / note), \`has\` (issues), \`path\`
|
|
11172
|
-
(POSIX glob
|
|
11389
|
+
(POSIX glob: \`*\` matches a single segment, \`**\` matches across
|
|
11173
11390
|
segments).
|
|
11174
11391
|
|
|
11175
|
-
Pass an empty query (\`""\`)
|
|
11392
|
+
Pass an empty query (\`""\`), or omit the argument entirely, to
|
|
11176
11393
|
export every node.
|
|
11177
11394
|
|
|
11178
11395
|
Run \`sm scan\` first to populate the DB.
|
|
@@ -11431,7 +11648,12 @@ var GraphCommand = class extends SmCommand {
|
|
|
11431
11648
|
const text = formatter.format({
|
|
11432
11649
|
nodes: scan.nodes,
|
|
11433
11650
|
links: scan.links,
|
|
11434
|
-
issues: scan.issues
|
|
11651
|
+
issues: scan.issues,
|
|
11652
|
+
// Pass the full persisted scan so format-specific renderers
|
|
11653
|
+
// that mirror a `ScanResult` envelope (today: built-in `json`)
|
|
11654
|
+
// can emit it verbatim without re-deriving fields like
|
|
11655
|
+
// `schemaVersion` or `stats` from the three primary arrays.
|
|
11656
|
+
scanResult: scan
|
|
11435
11657
|
});
|
|
11436
11658
|
this.printer.data(text.endsWith("\n") ? text : text + "\n");
|
|
11437
11659
|
return ExitCode.Ok;
|
|
@@ -11457,7 +11679,7 @@ var HELP_TEXTS = {
|
|
|
11457
11679
|
mdSpecVersionLine: "- Spec version: `{{version}}`",
|
|
11458
11680
|
// --- global flags section ------------------------------------------------
|
|
11459
11681
|
mdHeaderGlobalFlags: "## Global flags",
|
|
11460
|
-
mdGlobalFlagBullet: "- `{{name}}
|
|
11682
|
+
mdGlobalFlagBullet: "- `{{name}}`: {{description}}",
|
|
11461
11683
|
/** Description copy for the `--help` global flag in the JSON / md output. */
|
|
11462
11684
|
globalFlagHelpDescription: "Print usage and exit.",
|
|
11463
11685
|
// --- per-category / per-verb (md) ----------------------------------------
|
|
@@ -11468,12 +11690,12 @@ var HELP_TEXTS = {
|
|
|
11468
11690
|
mdFlagBullet: "- {{names}} `{{type}}`{{required}}{{description}}",
|
|
11469
11691
|
/** Trailing fragment for `mdFlagBullet`'s `{{required}}` slot. */
|
|
11470
11692
|
mdFlagBulletRequiredFragment: " (required)",
|
|
11471
|
-
/** Trailing fragment for `mdFlagBullet`'s `{{description}}` slot (with leading
|
|
11472
|
-
mdFlagBulletDescriptionFragment: "
|
|
11693
|
+
/** Trailing fragment for `mdFlagBullet`'s `{{description}}` slot (with leading colon). */
|
|
11694
|
+
mdFlagBulletDescriptionFragment: ": {{description}}",
|
|
11473
11695
|
mdExampleBullet: "- {{title}}",
|
|
11474
11696
|
// --- human single-verb renderer ------------------------------------------
|
|
11475
11697
|
/** Header line for `sm help <verb>` and `sm <verb> --help`. */
|
|
11476
|
-
humanVerbHeader: "sm {{name}}
|
|
11698
|
+
humanVerbHeader: "sm {{name}}: {{description}}",
|
|
11477
11699
|
humanDescriptionHeading: "DESCRIPTION",
|
|
11478
11700
|
humanUsageHeading: "USAGE",
|
|
11479
11701
|
/**
|
|
@@ -11491,9 +11713,9 @@ var HELP_TEXTS = {
|
|
|
11491
11713
|
// --- human compact overview (sm / sm --help / sm help, no verb) ---------
|
|
11492
11714
|
/**
|
|
11493
11715
|
* Compact-overview header. Replaces the Clipanion default ANSI banner.
|
|
11494
|
-
* Tagline mirrors README.md "In a sentence"
|
|
11716
|
+
* Tagline mirrors README.md "In a sentence", keep them in sync.
|
|
11495
11717
|
*/
|
|
11496
|
-
compactHeader: "{{binary}} {{version}}
|
|
11718
|
+
compactHeader: "{{binary}} {{version}}: the missing map for Markdown-based generative-AI ecosystems",
|
|
11497
11719
|
compactUsageHeading: "USAGE",
|
|
11498
11720
|
compactUsageLine: " sm <command> [options]",
|
|
11499
11721
|
compactExamplesHeading: "EXAMPLES",
|
|
@@ -11503,7 +11725,7 @@ var HELP_TEXTS = {
|
|
|
11503
11725
|
/**
|
|
11504
11726
|
* Marker prepended to the description column for not-yet-implemented
|
|
11505
11727
|
* verbs (those whose registered description carries `(planned)`).
|
|
11506
|
-
* Trailing space is intentional
|
|
11728
|
+
* Trailing space is intentional, the marker is concatenated before
|
|
11507
11729
|
* the rest of the description.
|
|
11508
11730
|
*/
|
|
11509
11731
|
compactStubMarker: "[stub] ",
|
|
@@ -11530,10 +11752,10 @@ var HelpCommand = class extends Command15 {
|
|
|
11530
11752
|
With a verb: the detail view for that single command.
|
|
11531
11753
|
|
|
11532
11754
|
Formats:
|
|
11533
|
-
human (default)
|
|
11534
|
-
md
|
|
11535
|
-
|
|
11536
|
-
json
|
|
11755
|
+
human (default): pretty terminal output.
|
|
11756
|
+
md : canonical markdown. context/cli-reference.md is
|
|
11757
|
+
regenerated from this and CI fails on drift.
|
|
11758
|
+
json : structured surface dump per spec/cli-contract.md.
|
|
11537
11759
|
`
|
|
11538
11760
|
});
|
|
11539
11761
|
verbParts = Option14.Rest({ required: 0 });
|
|
@@ -11917,7 +12139,7 @@ import {
|
|
|
11917
12139
|
mkdirSync as mkdirSync4,
|
|
11918
12140
|
readFileSync as readFileSync15,
|
|
11919
12141
|
statSync as statSync3,
|
|
11920
|
-
writeFileSync
|
|
12142
|
+
writeFileSync
|
|
11921
12143
|
} from "fs";
|
|
11922
12144
|
import { dirname as dirname15, resolve as resolve26 } from "path";
|
|
11923
12145
|
import { Command as Command16, Option as Option15 } from "clipanion";
|
|
@@ -11971,7 +12193,7 @@ var HooksInstallCommand = class extends SmCommand {
|
|
|
11971
12193
|
replacing it.
|
|
11972
12194
|
|
|
11973
12195
|
Requires a parent \`.git/\` (exit 5 otherwise). Writes nothing
|
|
11974
|
-
under \`--dry-run
|
|
12196
|
+
under \`--dry-run\`; instead prints the planned content with
|
|
11975
12197
|
\`--- target: <path> ---\` markers.
|
|
11976
12198
|
`,
|
|
11977
12199
|
examples: [
|
|
@@ -11981,7 +12203,7 @@ var HooksInstallCommand = class extends SmCommand {
|
|
|
11981
12203
|
});
|
|
11982
12204
|
flavour = Option15.String({ required: true });
|
|
11983
12205
|
dryRun = Option15.Boolean("-n,--dry-run", false);
|
|
11984
|
-
// The remaining cyclomatic count is from CLI ergonomics
|
|
12206
|
+
// The remaining cyclomatic count is from CLI ergonomics, flavour
|
|
11985
12207
|
// guard, repo lookup, marker detection, dry-run / json / chained /
|
|
11986
12208
|
// fresh branches each contributing a guard. Inner work already lives
|
|
11987
12209
|
// in `computePlannedHookContent` and `findGitRepoRoot`.
|
|
@@ -12040,7 +12262,7 @@ var HooksInstallCommand = class extends SmCommand {
|
|
|
12040
12262
|
}
|
|
12041
12263
|
try {
|
|
12042
12264
|
if (!existsSync17(hooksDir)) mkdirSync4(hooksDir, { recursive: true });
|
|
12043
|
-
|
|
12265
|
+
writeFileSync(hookPath, planned2.content, { encoding: "utf8" });
|
|
12044
12266
|
ensureExecutableBit(hookPath);
|
|
12045
12267
|
} catch (err) {
|
|
12046
12268
|
this.printer.error(
|
|
@@ -12103,13 +12325,14 @@ import cl100k_base from "js-tiktoken/ranks/cl100k_base";
|
|
|
12103
12325
|
// kernel/i18n/orchestrator.texts.ts
|
|
12104
12326
|
var ORCHESTRATOR_TEXTS = {
|
|
12105
12327
|
frontmatterInvalid: "Frontmatter for {{path}} ({{kind}}) failed schema validation: {{errors}}",
|
|
12106
|
-
frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only
|
|
12328
|
+
frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only; the metadata block was silently lost. Move the `---` lines to the start of the line.",
|
|
12107
12329
|
frontmatterMalformedByteOrderMark: "Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block was silently lost.",
|
|
12108
|
-
frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes
|
|
12330
|
+
frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes (no matching `---` line at column 0 was found). The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
|
|
12109
12331
|
extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
|
|
12110
12332
|
extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
|
|
12111
12333
|
extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
|
|
12112
12334
|
extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{slot}}" schema: {{errors}}. Contribution dropped.',
|
|
12335
|
+
extensionErrorRecommendedActionMissing: 'Analyzer "{{analyzerId}}" declares recommendedAction "{{actionId}}" but no Action is registered under that qualified id. The analyzer stays registered; the recommendation will not surface in the inspector.',
|
|
12113
12336
|
runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
|
|
12114
12337
|
runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
|
|
12115
12338
|
};
|
|
@@ -12298,10 +12521,11 @@ function isExternalUrlLink(link2) {
|
|
|
12298
12521
|
}
|
|
12299
12522
|
|
|
12300
12523
|
// kernel/orchestrator/analyzers.ts
|
|
12301
|
-
async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, emitter, hookDispatcher) {
|
|
12524
|
+
async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher) {
|
|
12302
12525
|
const issues = [];
|
|
12303
12526
|
const contributions = [];
|
|
12304
12527
|
const validators = loadSchemaValidators();
|
|
12528
|
+
validateRecommendedActions(analyzers, registeredActionIds, emitter);
|
|
12305
12529
|
const analyzerOrphans = orphanSidecars.map((o) => ({
|
|
12306
12530
|
relativePath: o.relativePath,
|
|
12307
12531
|
expectedMdPath: o.expectedMdPath
|
|
@@ -12373,6 +12597,27 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
|
|
|
12373
12597
|
}
|
|
12374
12598
|
return { issues, contributions };
|
|
12375
12599
|
}
|
|
12600
|
+
function validateRecommendedActions(analyzers, registeredActionIds, emitter) {
|
|
12601
|
+
for (const analyzer of analyzers) {
|
|
12602
|
+
const refs = analyzer.recommendedActions;
|
|
12603
|
+
if (refs === void 0 || refs.length === 0) continue;
|
|
12604
|
+
const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
|
|
12605
|
+
for (const actionId of refs) {
|
|
12606
|
+
if (registeredActionIds.has(actionId)) continue;
|
|
12607
|
+
emitter.emit(
|
|
12608
|
+
makeEvent("extension.error", {
|
|
12609
|
+
kind: "recommended-action-missing",
|
|
12610
|
+
extensionId: analyzerId,
|
|
12611
|
+
actionId,
|
|
12612
|
+
message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {
|
|
12613
|
+
analyzerId,
|
|
12614
|
+
actionId
|
|
12615
|
+
})
|
|
12616
|
+
})
|
|
12617
|
+
);
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
}
|
|
12376
12621
|
function validateIssue(analyzer, issue, emitter) {
|
|
12377
12622
|
const severity = issue.severity;
|
|
12378
12623
|
if (severity !== "error" && severity !== "warn" && severity !== "info") {
|
|
@@ -12422,9 +12667,20 @@ function indexPriorLinks(links, priorNodePaths, byOriginating) {
|
|
|
12422
12667
|
else byOriginating.set(key, [link2]);
|
|
12423
12668
|
}
|
|
12424
12669
|
}
|
|
12670
|
+
var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
|
|
12671
|
+
"frontmatter-invalid",
|
|
12672
|
+
"frontmatter-malformed",
|
|
12673
|
+
// Audit L1: parser parse-error is emitted by
|
|
12674
|
+
// `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The
|
|
12675
|
+
// raw.parseIssues only flows through the non-cache path; a cached
|
|
12676
|
+
// node skips the rebuild, so the prior issue MUST survive the
|
|
12677
|
+
// incremental scan or the warning silently disappears on a clean
|
|
12678
|
+
// re-scan of an unchanged file.
|
|
12679
|
+
"frontmatter-parse-error"
|
|
12680
|
+
]);
|
|
12425
12681
|
function indexPriorFrontmatterIssues(issues, byNode) {
|
|
12426
12682
|
for (const issue of issues) {
|
|
12427
|
-
if (
|
|
12683
|
+
if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
|
|
12428
12684
|
if (issue.nodeIds.length !== 1) continue;
|
|
12429
12685
|
const path = issue.nodeIds[0];
|
|
12430
12686
|
const list = byNode.get(path);
|
|
@@ -12623,7 +12879,7 @@ function flagAmbiguousRenames(opts) {
|
|
|
12623
12879
|
analyzerId: "auto-rename-ambiguous",
|
|
12624
12880
|
severity: "warn",
|
|
12625
12881
|
nodeIds: [toPath],
|
|
12626
|
-
message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters
|
|
12882
|
+
message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters; pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
|
|
12627
12883
|
data: { to: toPath, candidates: remaining }
|
|
12628
12884
|
});
|
|
12629
12885
|
}
|
|
@@ -12878,11 +13134,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
|
|
|
12878
13134
|
liveFrontmatterHash
|
|
12879
13135
|
});
|
|
12880
13136
|
return {
|
|
12881
|
-
// R15 closure (2026-05-07)
|
|
13137
|
+
// R15 closure (2026-05-07), surface the full parsed root on the
|
|
12882
13138
|
// overlay so BFF consumers (UI inspector audit / plugin-contributions
|
|
12883
13139
|
// / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
|
|
12884
13140
|
// plugin-namespaced sub-keys without re-reading the file. The
|
|
12885
|
-
// `annotations` field above stays
|
|
13141
|
+
// `annotations` field above stays, it duplicates `root.annotations`
|
|
12886
13142
|
// by design so existing consumers keep working unchanged.
|
|
12887
13143
|
overlay: {
|
|
12888
13144
|
present: true,
|
|
@@ -12926,6 +13182,16 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
|
|
|
12926
13182
|
encoder: opts.encoder
|
|
12927
13183
|
});
|
|
12928
13184
|
const frontmatterIssues = [];
|
|
13185
|
+
if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
|
|
13186
|
+
for (const pi of opts.raw.parseIssues) {
|
|
13187
|
+
frontmatterIssues.push({
|
|
13188
|
+
analyzerId: pi.code,
|
|
13189
|
+
severity: opts.strict ? "error" : "warn",
|
|
13190
|
+
nodeIds: [opts.raw.path],
|
|
13191
|
+
message: pi.message
|
|
13192
|
+
});
|
|
13193
|
+
}
|
|
13194
|
+
}
|
|
12929
13195
|
if (opts.raw.frontmatterRaw.length > 0) {
|
|
12930
13196
|
const fmIssue = validateFrontmatter(
|
|
12931
13197
|
opts.providerFrontmatter,
|
|
@@ -13217,6 +13483,9 @@ async function runScanInternal(_kernel, options) {
|
|
|
13217
13483
|
recomputeLinkCounts(walked.nodes, walked.internalLinks);
|
|
13218
13484
|
recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
|
|
13219
13485
|
await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
|
|
13486
|
+
const registeredActionIds = new Set(
|
|
13487
|
+
_kernel.registry.all("action").map((a) => qualifiedExtensionId(a.pluginId, a.id))
|
|
13488
|
+
);
|
|
13220
13489
|
const analyzerResult = await runAnalyzers(
|
|
13221
13490
|
exts.analyzers,
|
|
13222
13491
|
walked.nodes,
|
|
@@ -13228,6 +13497,7 @@ async function runScanInternal(_kernel, options) {
|
|
|
13228
13497
|
options.orphanJobFiles ?? [],
|
|
13229
13498
|
options.referenceablePaths,
|
|
13230
13499
|
options.cwd,
|
|
13500
|
+
registeredActionIds,
|
|
13231
13501
|
emitter,
|
|
13232
13502
|
hookDispatcher
|
|
13233
13503
|
);
|
|
@@ -13349,7 +13619,8 @@ function createChokidarWatcher(opts) {
|
|
|
13349
13619
|
const watcher = chokidar.watch(absRoots, {
|
|
13350
13620
|
ignoreInitial: true,
|
|
13351
13621
|
persistent: true,
|
|
13352
|
-
...ignored ? { ignored } : {}
|
|
13622
|
+
...ignored ? { ignored } : {},
|
|
13623
|
+
...opts.depth !== void 0 ? { depth: opts.depth } : {}
|
|
13353
13624
|
});
|
|
13354
13625
|
let pending = [];
|
|
13355
13626
|
let timer = null;
|
|
@@ -13558,16 +13829,19 @@ import { join as join10, resolve as resolve28 } from "path";
|
|
|
13558
13829
|
function findOrphanJobFiles(jobsDir, referencedPaths) {
|
|
13559
13830
|
let entries;
|
|
13560
13831
|
try {
|
|
13561
|
-
const
|
|
13562
|
-
if (!
|
|
13832
|
+
const stat2 = statSync6(jobsDir);
|
|
13833
|
+
if (!stat2.isDirectory()) {
|
|
13563
13834
|
return { orphanFilePaths: [], referencedCount: referencedPaths.size };
|
|
13564
13835
|
}
|
|
13565
|
-
entries = readdirSync7(jobsDir);
|
|
13836
|
+
entries = readdirSync7(jobsDir, { withFileTypes: true });
|
|
13566
13837
|
} catch {
|
|
13567
13838
|
return { orphanFilePaths: [], referencedCount: referencedPaths.size };
|
|
13568
13839
|
}
|
|
13569
13840
|
const orphans = [];
|
|
13570
|
-
for (const
|
|
13841
|
+
for (const entry of entries) {
|
|
13842
|
+
if (entry.isSymbolicLink()) continue;
|
|
13843
|
+
if (!entry.isFile()) continue;
|
|
13844
|
+
const name = entry.name;
|
|
13571
13845
|
if (!name.endsWith(".md")) continue;
|
|
13572
13846
|
const abs = resolve28(join10(jobsDir, name));
|
|
13573
13847
|
if (!referencedPaths.has(abs)) orphans.push(abs);
|
|
@@ -13649,7 +13923,7 @@ var REFERENCE_WALK_MAX_FILES = 5e4;
|
|
|
13649
13923
|
var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
13650
13924
|
"node_modules",
|
|
13651
13925
|
".git",
|
|
13652
|
-
|
|
13926
|
+
SKILL_MAP_DIR
|
|
13653
13927
|
]);
|
|
13654
13928
|
function resolveScanPath(raw, cwd, homedir4) {
|
|
13655
13929
|
if (raw.startsWith("~/")) return resolve29(join11(homedir4, raw.slice(2)));
|
|
@@ -13664,8 +13938,8 @@ function walkReferencePaths(rawRoots, cwd, homedir4) {
|
|
|
13664
13938
|
for (const raw of rawRoots) {
|
|
13665
13939
|
if (truncated) break;
|
|
13666
13940
|
const root = resolveScanPath(raw, cwd, homedir4);
|
|
13667
|
-
const
|
|
13668
|
-
if (!
|
|
13941
|
+
const stat2 = safeStat(root);
|
|
13942
|
+
if (!stat2 || !stat2.isDirectory()) {
|
|
13669
13943
|
missingRoots.push(root);
|
|
13670
13944
|
continue;
|
|
13671
13945
|
}
|
|
@@ -13865,7 +14139,7 @@ function buildRunScanOptions(args2) {
|
|
|
13865
14139
|
emitter: opts.emitterFactory ? opts.emitterFactory() : createStderrProgressEmitter(opts.stderr, {
|
|
13866
14140
|
colorEnabled: opts.colorEnabled === true
|
|
13867
14141
|
}),
|
|
13868
|
-
// Orphan job-file detection
|
|
14142
|
+
// Orphan job-file detection, empty list means "no orphans
|
|
13869
14143
|
// visible from this caller" (legacy behaviour). The orchestrator
|
|
13870
14144
|
// defaults to `[]` when the field is absent; we always pass the
|
|
13871
14145
|
// array (possibly empty) to keep the wiring uniform.
|
|
@@ -13958,7 +14232,7 @@ var INIT_TEXTS = {
|
|
|
13958
14232
|
scanFailed: "{{glyph}} sm init: scan failed: {{message}}\n",
|
|
13959
14233
|
firstScanSummary: "{{glyph}} First scan: {{nodes}} node{{nodesPlural}}, {{links}} link{{linksPlural}}, {{issues}} issue{{issuesPlural}}.\n",
|
|
13960
14234
|
// --- dry-run previews --------------------------------------------------
|
|
13961
|
-
dryRunHeader: "(dry-run
|
|
14235
|
+
dryRunHeader: "(dry-run, no files written, no DB provisioned)\n",
|
|
13962
14236
|
dryRunWouldCreateDir: "would create {{path}}/\n",
|
|
13963
14237
|
dryRunWouldWriteFile: "would write {{path}}\n",
|
|
13964
14238
|
dryRunWouldOverwriteFile: "would overwrite {{path}}\n",
|
|
@@ -14007,7 +14281,7 @@ var InitCommand = class extends SmCommand {
|
|
|
14007
14281
|
description: "Strict mode: fail on any layered-loader warning AND promote frontmatter warnings to errors during the first scan. Same flag as sm scan / sm config."
|
|
14008
14282
|
});
|
|
14009
14283
|
dryRun = Option16.Boolean("-n,--dry-run", false, {
|
|
14010
|
-
description: "Preview the scope provisioning without touching the filesystem or the DB. Honours --force for the would-overwrite preview. Skips the first scan unconditionally
|
|
14284
|
+
description: "Preview the scope provisioning without touching the filesystem or the DB. Honours --force for the would-overwrite preview. Skips the first scan unconditionally; dry-run never persists."
|
|
14011
14285
|
});
|
|
14012
14286
|
// CLI orchestrator: paths setup + dry-run branch (delegated to
|
|
14013
14287
|
// `writeDryRunPlan`) + real provision (mkdir + 4 file writes +
|
|
@@ -14137,7 +14411,7 @@ async function runFirstScan(scopeRoot, homedir4, strict, printer, stderr, ansi)
|
|
|
14137
14411
|
dryRun: false,
|
|
14138
14412
|
changed: false,
|
|
14139
14413
|
// Init's first scan always persists, even when the scope is
|
|
14140
|
-
// empty
|
|
14414
|
+
// empty, the historic behaviour was to seed the DB regardless of
|
|
14141
14415
|
// node count. `runScanForCommand`'s guard refuses to wipe a
|
|
14142
14416
|
// populated DB with a zero-result scan; init's DB is freshly
|
|
14143
14417
|
// provisioned (zero rows), so the guard is dormant. Pass
|
|
@@ -14224,11 +14498,11 @@ var HISTORY_TEXTS = {
|
|
|
14224
14498
|
statusInvalidHint: "Allowed: {{allowed}}.",
|
|
14225
14499
|
periodInvalid: '{{glyph}} --period: invalid value "{{value}}".\n {{hint}}\n',
|
|
14226
14500
|
periodInvalidHint: "Allowed: {{allowed}}.",
|
|
14227
|
-
schemaValidationFailed: "{{glyph}} internal: history-stats output failed schema validation
|
|
14501
|
+
schemaValidationFailed: "{{glyph}} internal: history-stats output failed schema validation: {{errors}}\n",
|
|
14228
14502
|
// --- renderStats: sectioned layout (matches `sm plugins doctor`) -----
|
|
14229
14503
|
statsAllTimeWindow: "(all time)",
|
|
14230
|
-
/** One-line dense header: `sm history stats
|
|
14231
|
-
statsHeader: "sm history stats
|
|
14504
|
+
/** One-line dense header: `sm history stats: N executions · M.M% error rate`. */
|
|
14505
|
+
statsHeader: "sm history stats: {{summary}}\n\n",
|
|
14232
14506
|
/** Section heading rendered before each indented block. */
|
|
14233
14507
|
statsSectionHeader: " {{title}}\n",
|
|
14234
14508
|
/** Two-column field row inside a section, label padded by the renderer. */
|
|
@@ -14243,7 +14517,7 @@ var HISTORY_TEXTS = {
|
|
|
14243
14517
|
statsLabelExecutions: "Executions",
|
|
14244
14518
|
statsLabelTokens: "Tokens",
|
|
14245
14519
|
statsLabelDuration: "Duration",
|
|
14246
|
-
/** `N (X ok · Y failed · Z cancelled)
|
|
14520
|
+
/** `N (X ok · Y failed · Z cancelled)`, only the populated buckets render. */
|
|
14247
14521
|
statsExecutionsCount: "{{count}}{{breakdown}}",
|
|
14248
14522
|
statsTokensSplit: "{{in}} in / {{out}} out",
|
|
14249
14523
|
/** Per-action row: `<id>@<version> N runs · T_in/T_out`. */
|
|
@@ -14273,7 +14547,7 @@ var HISTORY_TEXTS = {
|
|
|
14273
14547
|
tableFooterCount: "{{count}} {{noun}}\n",
|
|
14274
14548
|
tableFooterNounSingular: "execution",
|
|
14275
14549
|
tableFooterNounPlural: "executions",
|
|
14276
|
-
/** Footer tip
|
|
14550
|
+
/** Footer tip, printed dim under the count. */
|
|
14277
14551
|
tableFooterTip: "Tip: `sm history stats` for aggregated counts and top actions.\n"
|
|
14278
14552
|
};
|
|
14279
14553
|
|
|
@@ -14519,7 +14793,7 @@ function toHistoryRow(r) {
|
|
|
14519
14793
|
const status = reason.length > 0 ? tx(HISTORY_TEXTS.statusWithReason, { status: r.status, reason }) : r.status;
|
|
14520
14794
|
return {
|
|
14521
14795
|
id: truncateHead(sanitizeForTerminal(r.id), COL_ID_MAX),
|
|
14522
|
-
// ISO timestamp with the `T` swapped for a space
|
|
14796
|
+
// ISO timestamp with the `T` swapped for a space, keeps the column
|
|
14523
14797
|
// narrow and human-readable without losing the `Z` UTC marker.
|
|
14524
14798
|
started: new Date(r.startedAt).toISOString().slice(0, 19).replace("T", " ") + "Z",
|
|
14525
14799
|
action: truncateHead(sanitizeForTerminal(r.extensionId), COL_ACTION_MAX),
|
|
@@ -14720,6 +14994,7 @@ function trimMs(iso) {
|
|
|
14720
14994
|
|
|
14721
14995
|
// cli/commands/jobs.ts
|
|
14722
14996
|
import { unlink } from "fs/promises";
|
|
14997
|
+
import { relative as relative6 } from "path";
|
|
14723
14998
|
import { Command as Command19, Option as Option18 } from "clipanion";
|
|
14724
14999
|
|
|
14725
15000
|
// cli/i18n/jobs.texts.ts
|
|
@@ -14811,18 +15086,30 @@ var JobPruneCommand = class extends SmCommand {
|
|
|
14811
15086
|
const cutoff = now - completedPolicy * 1e3;
|
|
14812
15087
|
const result = await this.pruneOrPreview("completed", cutoff, adapter, this.dryRun);
|
|
14813
15088
|
out.retention.completed.deleted = result.deletedCount;
|
|
14814
|
-
out.retention.completed.files = await this.unlinkFiles(
|
|
15089
|
+
out.retention.completed.files = await this.unlinkFiles(
|
|
15090
|
+
result.filePaths,
|
|
15091
|
+
jobsDir,
|
|
15092
|
+
this.dryRun
|
|
15093
|
+
);
|
|
14815
15094
|
}
|
|
14816
15095
|
if (failedPolicy !== null) {
|
|
14817
15096
|
const cutoff = now - failedPolicy * 1e3;
|
|
14818
15097
|
const result = await this.pruneOrPreview("failed", cutoff, adapter, this.dryRun);
|
|
14819
15098
|
out.retention.failed.deleted = result.deletedCount;
|
|
14820
|
-
out.retention.failed.files = await this.unlinkFiles(
|
|
15099
|
+
out.retention.failed.files = await this.unlinkFiles(
|
|
15100
|
+
result.filePaths,
|
|
15101
|
+
jobsDir,
|
|
15102
|
+
this.dryRun
|
|
15103
|
+
);
|
|
14821
15104
|
}
|
|
14822
15105
|
if (this.orphanFiles && out.orphanFiles.scanned) {
|
|
14823
15106
|
const referenced = await adapter.jobs.listReferencedFilePaths();
|
|
14824
15107
|
const orphans = findOrphanJobFiles(jobsDir, referenced);
|
|
14825
|
-
const removed = await this.unlinkFiles(
|
|
15108
|
+
const removed = await this.unlinkFiles(
|
|
15109
|
+
orphans.orphanFilePaths,
|
|
15110
|
+
jobsDir,
|
|
15111
|
+
this.dryRun
|
|
15112
|
+
);
|
|
14826
15113
|
out.orphanFiles = { scanned: true, deleted: removed };
|
|
14827
15114
|
}
|
|
14828
15115
|
});
|
|
@@ -14841,10 +15128,15 @@ var JobPruneCommand = class extends SmCommand {
|
|
|
14841
15128
|
async pruneOrPreview(status, cutoffMs, adapter, dryRun) {
|
|
14842
15129
|
return dryRun ? adapter.jobs.listTerminalCandidates(status, cutoffMs) : adapter.jobs.pruneTerminal(status, cutoffMs);
|
|
14843
15130
|
}
|
|
14844
|
-
async unlinkFiles(paths, dryRun) {
|
|
15131
|
+
async unlinkFiles(paths, jobsDir, dryRun) {
|
|
14845
15132
|
if (dryRun) return paths.length;
|
|
14846
15133
|
let removed = 0;
|
|
14847
15134
|
for (const p of paths) {
|
|
15135
|
+
try {
|
|
15136
|
+
assertContained(jobsDir, relative6(jobsDir, p));
|
|
15137
|
+
} catch {
|
|
15138
|
+
continue;
|
|
15139
|
+
}
|
|
14848
15140
|
try {
|
|
14849
15141
|
await unlink(p);
|
|
14850
15142
|
removed += 1;
|
|
@@ -14917,7 +15209,7 @@ var LIST_TEXTS = {
|
|
|
14917
15209
|
tableFooterCount: "{{count}} {{noun}}\n",
|
|
14918
15210
|
tableFooterNounSingular: "node",
|
|
14919
15211
|
tableFooterNounPlural: "nodes",
|
|
14920
|
-
/** Footer tip
|
|
15212
|
+
/** Footer tip, printed dim under the count. */
|
|
14921
15213
|
tableFooterTip: "Tip: `sm show <path>` for details, `sm check` for issues.\n"
|
|
14922
15214
|
};
|
|
14923
15215
|
|
|
@@ -15183,7 +15475,7 @@ var ORPHANS_TEXTS = {
|
|
|
15183
15475
|
reconcileDryRunHead: "{{glyph}} Would reconcile {{from}} \u2192 {{to}}{{dryTag}}\n",
|
|
15184
15476
|
/** Breakdown line composed at the call site from non-zero counts only. */
|
|
15185
15477
|
reconcileBreakdown: "{{rows}} rows \xB7 jobs {{jobs}} \xB7 execs {{execs}} \xB7 summaries {{summaries}} \xB7 enrichments {{enrichments}} \xB7 kv {{kv}} \xB7 favorites {{favorites}}",
|
|
15186
|
-
reconcileCollisionsNote: "{{glyph}} {{count}} composite-PK collision{{plural}}
|
|
15478
|
+
reconcileCollisionsNote: "{{glyph}} {{count}} composite-PK collision{{plural}}; destination rows preserved.\n",
|
|
15187
15479
|
reconcileCollisionsNoteDryRun: "{{glyph}} {{count}} composite-PK collision{{plural}} would be skipped; destination rows preserved.\n",
|
|
15188
15480
|
// --- undo-rename -------------------------------------------------------
|
|
15189
15481
|
undoNoActiveIssue: '{{glyph}} sm orphans undo-rename: no active auto-rename issue targets "{{path}}".\n {{hint}}\n',
|
|
@@ -15208,7 +15500,7 @@ var ORPHANS_TEXTS = {
|
|
|
15208
15500
|
* emitted after `sm orphans undo-rename`. The string lands in DB rows
|
|
15209
15501
|
* and travels through `--json`, `sm check`, and downstream consumers,
|
|
15210
15502
|
* so localising it requires a kernel-side template (not just a CLI
|
|
15211
|
-
* catalog)
|
|
15503
|
+
* catalog), kept here for now so the wording lives in one greppable
|
|
15212
15504
|
* place even if the layering is imperfect.
|
|
15213
15505
|
*/
|
|
15214
15506
|
undoRenameOrphanMessage: "Orphan history: {{toPath}} (was reverted from auto-rename to {{newPath}}).",
|
|
@@ -15217,7 +15509,7 @@ var ORPHANS_TEXTS = {
|
|
|
15217
15509
|
invalidKindHint: "Allowed: orphan, medium, ambiguous.",
|
|
15218
15510
|
// --- renderOrphans (pretty listing) ------------------------------------
|
|
15219
15511
|
/** Header line for the active orphan / auto-rename issues block. */
|
|
15220
|
-
listHeader: "sm orphans
|
|
15512
|
+
listHeader: "sm orphans: {{count}} {{noun}}\n\n",
|
|
15221
15513
|
listNounSingular: "issue",
|
|
15222
15514
|
listNounPlural: "issues",
|
|
15223
15515
|
/**
|
|
@@ -15437,7 +15729,7 @@ var OrphansUndoRenameCommand = class extends SmCommand {
|
|
|
15437
15729
|
turned out to be unrelated.
|
|
15438
15730
|
|
|
15439
15731
|
For an active auto-rename-medium issue on <new.path>, the prior
|
|
15440
|
-
path is read from issue.data.from
|
|
15732
|
+
path is read from issue.data.from; omit --from. For an active
|
|
15441
15733
|
auto-rename-ambiguous issue, --from <old.path> is REQUIRED to
|
|
15442
15734
|
pick a candidate from data.candidates.
|
|
15443
15735
|
|
|
@@ -15684,7 +15976,7 @@ import { Command as Command22, Option as Option21 } from "clipanion";
|
|
|
15684
15976
|
// cli/i18n/plugins.texts.ts
|
|
15685
15977
|
var PLUGINS_TEXTS = {
|
|
15686
15978
|
// --- enable / disable error guidance --------------------------------
|
|
15687
|
-
// Spec § A.7
|
|
15979
|
+
// Spec § A.7, granularity validation. The CLI rejects mismatched ids
|
|
15688
15980
|
// up front (instead of silently writing a config_plugins row that the
|
|
15689
15981
|
// runtime would later ignore) so the user learns the model immediately.
|
|
15690
15982
|
/**
|
|
@@ -15707,7 +15999,7 @@ var PLUGINS_TEXTS = {
|
|
|
15707
15999
|
qualifiedIdNotFoundHint: "Run `sm plugins list` to see what each bundle ships.",
|
|
15708
16000
|
qualifiedIdUnknownBundle: "{{glyph}} Qualified extension id references unknown bundle: {{bundleId}}\n {{hint}}\n",
|
|
15709
16001
|
qualifiedIdUnknownBundleHint: "Run `sm plugins list` for known bundle ids.",
|
|
15710
|
-
// Spec § A.10
|
|
16002
|
+
// Spec § A.10, `applicableKinds` filter on Extractors. When an extractor
|
|
15711
16003
|
// declares a kind that no installed Provider emits, the load succeeds
|
|
15712
16004
|
// (the Provider may arrive later) but `sm plugins doctor` surfaces a
|
|
15713
16005
|
// non-blocking warning so the author sees the typo / missing dependency.
|
|
@@ -15715,11 +16007,22 @@ var PLUGINS_TEXTS = {
|
|
|
15715
16007
|
// The id is rendered as the entry header (`⚠ <id>`); the body skips
|
|
15716
16008
|
// re-stating it so the message reads cleanly under the entry.
|
|
15717
16009
|
doctorApplicableKindUnknown: "Declares applicableKinds including '{{unknownKind}}', but no installed Provider declares that kind. The extractor is loaded but will never fire on that kind.",
|
|
16010
|
+
// Phase 7 / View contribution system, defence-in-depth slot drift
|
|
16011
|
+
// check. AJV at manifest load already rejects unknown slots as
|
|
16012
|
+
// `invalid-manifest`, but a plugin authored against an older catalog
|
|
16013
|
+
// whose `catalogCompat` satisfies the current major syntactically can
|
|
16014
|
+
// still ship a slot id that was renamed / removed. The doctor pass
|
|
16015
|
+
// surfaces those so the user runs `sm plugins upgrade` to migrate.
|
|
16016
|
+
// Exit code is NOT promoted by this warning.
|
|
16017
|
+
// The id is rendered as the entry header
|
|
16018
|
+
// (`⚠ <pluginId>/<extensionId>/<contributionId>`); the body skips
|
|
16019
|
+
// re-stating it so the message reads cleanly under the entry.
|
|
16020
|
+
doctorUnknownSlot: "Contribution '{{contributionId}}' targets unknown slot '{{slot}}'. Run `sm plugins upgrade {{pluginId}}` or update the plugin to a slot in the current catalog (`sm plugins slots list`).",
|
|
15718
16021
|
// --- list verb -------------------------------------------------------
|
|
15719
16022
|
listEmpty: "No plugins discovered.\n",
|
|
15720
16023
|
// --- doctor verb -----------------------------------------------------
|
|
15721
16024
|
/** One-line summary that opens the human doctor output. */
|
|
15722
|
-
doctorSummary: "plugins doctor
|
|
16025
|
+
doctorSummary: "plugins doctor: {{enabled}} enabled \xB7 {{issues}} issue{{issuesPlural}} \xB7 {{warnings}} warning{{warningsPlural}}\n\n",
|
|
15723
16026
|
/** Source breakdown row (built-in vs user). Indented 4 to match the status rows. */
|
|
15724
16027
|
doctorSourceRow: " {{label}} {{count}}\n",
|
|
15725
16028
|
/** Status breakdown table heading. */
|
|
@@ -15769,12 +16072,12 @@ var PLUGINS_TEXTS = {
|
|
|
15769
16072
|
*/
|
|
15770
16073
|
bundleSubIndent: " ",
|
|
15771
16074
|
listTipShow: "\nTip: `sm plugins show <id>` for kinds, versions, and per-extension status.\n",
|
|
15772
|
-
/** Show command
|
|
16075
|
+
/** Show command, built-in header (no version row, no path). */
|
|
15773
16076
|
detailHeaderBuiltIn: " {{glyph}} {{id}} {{source}} {{count}} extension{{plural}}\n",
|
|
15774
16077
|
/**
|
|
15775
|
-
* Show command
|
|
16078
|
+
* Show command, user-plugin header. Version always present (defaults
|
|
15776
16079
|
* to `?` when the manifest omits it). Source labelled `user`; disabled
|
|
15777
|
-
* / failed states surface via the glyph (✕) only
|
|
16080
|
+
* / failed states surface via the glyph (✕) only, the source label
|
|
15778
16081
|
* stays the same so users learn that the plugin _is_ a user one
|
|
15779
16082
|
* regardless of its load state.
|
|
15780
16083
|
*/
|
|
@@ -15801,7 +16104,26 @@ var PLUGINS_TEXTS = {
|
|
|
15801
16104
|
*/
|
|
15802
16105
|
detailExtensionRowBare: " {{kind}} {{name}} v{{version}}\n",
|
|
15803
16106
|
detailVersionUnknown: "?",
|
|
15804
|
-
detailCompatUnknown: "?"
|
|
16107
|
+
detailCompatUnknown: "?",
|
|
16108
|
+
// --- create verb -----------------------------------------------------
|
|
16109
|
+
/** Rejected when `<plugin-id>` fails the kebab-case lowercase regex. */
|
|
16110
|
+
createInvalidId: "{{glyph}} Plugin id must be kebab-case lowercase (got: {{id}})\n",
|
|
16111
|
+
/** Target directory exists and `--force` was not passed. */
|
|
16112
|
+
createRefuseOverwrite: "{{glyph}} Refusing to overwrite {{targetDir}}. Pass --force to overwrite.\n",
|
|
16113
|
+
/**
|
|
16114
|
+
* Success block printed after scaffolding. Follows the no-em-dash rule
|
|
16115
|
+
* across every line.
|
|
16116
|
+
*/
|
|
16117
|
+
createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{pluginId}}/extensions/extractor.js (the extract() body)\n - Run sm scan to see the contribution surface\n - sm plugins slots list: browse other slots\n",
|
|
16118
|
+
// --- slots list verb -------------------------------------------------
|
|
16119
|
+
/** Section header for the view-slots catalogue. */
|
|
16120
|
+
slotsListHeaderViewSlots: " View slots ({{count}})\n",
|
|
16121
|
+
/** Section header for the input-types catalogue (leading blank line). */
|
|
16122
|
+
slotsListHeaderInputTypes: "\n Input types ({{count}})\n",
|
|
16123
|
+
/** Trailing tip; the `{{tip}}` is the dim-wrapped tip text. */
|
|
16124
|
+
slotsListTipFooter: "\n{{tip}}\n",
|
|
16125
|
+
/** Tip body, dim-wrapped by the caller. */
|
|
16126
|
+
slotsListTipText: "Tip: full spec at spec/view-slots.md and spec/input-types.md."
|
|
15805
16127
|
};
|
|
15806
16128
|
|
|
15807
16129
|
// cli/commands/plugins/shared.ts
|
|
@@ -16232,15 +16554,28 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
16232
16554
|
const counts = countByStatus(builtIns2, plugins);
|
|
16233
16555
|
const knownKinds = collectKnownKinds(plugins);
|
|
16234
16556
|
const applicableKindWarnings = collectApplicableKindWarnings(plugins, knownKinds);
|
|
16557
|
+
const unknownSlotWarnings = collectUnknownSlotWarnings(plugins, KNOWN_SLOT_NAMES);
|
|
16235
16558
|
const bad = plugins.filter((p) => p.status !== "enabled" && p.status !== "disabled");
|
|
16236
|
-
const totalWarnings = applicableKindWarnings.length;
|
|
16559
|
+
const totalWarnings = applicableKindWarnings.length + unknownSlotWarnings.length;
|
|
16560
|
+
if (this.json) {
|
|
16561
|
+
const envelope = buildDoctorJsonEnvelope({
|
|
16562
|
+
counts,
|
|
16563
|
+
bad,
|
|
16564
|
+
applicableKindWarnings,
|
|
16565
|
+
unknownSlotWarnings,
|
|
16566
|
+
totalWarnings,
|
|
16567
|
+
elapsedMs: this.elapsed.ms()
|
|
16568
|
+
});
|
|
16569
|
+
this.printer.data(JSON.stringify(envelope) + "\n");
|
|
16570
|
+
return bad.length > 0 ? ExitCode.Issues : ExitCode.Ok;
|
|
16571
|
+
}
|
|
16237
16572
|
const stdout = this.context.stdout;
|
|
16238
16573
|
const ansi = ansiFor({ isTTY: stdout.isTTY === true, noColorFlag: this.noColor });
|
|
16239
16574
|
this.#renderSummaryHeader(counts.enabled, bad.length, totalWarnings);
|
|
16240
16575
|
this.#renderSourceBreakdown(builtIns2.length, plugins.length);
|
|
16241
16576
|
this.#renderStatusBreakdown(counts, ansi);
|
|
16242
16577
|
if (totalWarnings > 0) {
|
|
16243
|
-
this.#renderWarnings(applicableKindWarnings, totalWarnings, ansi);
|
|
16578
|
+
this.#renderWarnings(applicableKindWarnings, unknownSlotWarnings, totalWarnings, ansi);
|
|
16244
16579
|
}
|
|
16245
16580
|
if (bad.length > 0) {
|
|
16246
16581
|
this.#renderIssues(bad, ansi);
|
|
@@ -16294,7 +16629,7 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
16294
16629
|
);
|
|
16295
16630
|
}
|
|
16296
16631
|
}
|
|
16297
|
-
#renderWarnings(applicableKindWarnings, totalWarnings, ansi) {
|
|
16632
|
+
#renderWarnings(applicableKindWarnings, unknownSlotWarnings, totalWarnings, ansi) {
|
|
16298
16633
|
this.printer.data(tx(PLUGINS_TEXTS.doctorWarningsHeader, { count: totalWarnings }));
|
|
16299
16634
|
const warnGlyph = ansi.yellow("\u26A0");
|
|
16300
16635
|
for (const w of applicableKindWarnings) {
|
|
@@ -16307,6 +16642,20 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
16307
16642
|
ansi
|
|
16308
16643
|
);
|
|
16309
16644
|
}
|
|
16645
|
+
for (const w of unknownSlotWarnings) {
|
|
16646
|
+
const slash = w.extensionQualifiedId.indexOf("/");
|
|
16647
|
+
const pluginId = slash >= 0 ? w.extensionQualifiedId.slice(0, slash) : w.extensionQualifiedId;
|
|
16648
|
+
this.#emitWarningEntry(
|
|
16649
|
+
warnGlyph,
|
|
16650
|
+
sanitizeForTerminal(`${w.extensionQualifiedId}/${w.contributionId}`),
|
|
16651
|
+
tx(PLUGINS_TEXTS.doctorUnknownSlot, {
|
|
16652
|
+
contributionId: sanitizeForTerminal(w.contributionId),
|
|
16653
|
+
slot: sanitizeForTerminal(w.slot),
|
|
16654
|
+
pluginId: sanitizeForTerminal(pluginId)
|
|
16655
|
+
}),
|
|
16656
|
+
ansi
|
|
16657
|
+
);
|
|
16658
|
+
}
|
|
16310
16659
|
}
|
|
16311
16660
|
#emitWarningEntry(glyph, id, message, ansi) {
|
|
16312
16661
|
this.printer.data(tx(PLUGINS_TEXTS.doctorWarningEntry, { glyph, id }));
|
|
@@ -16446,29 +16795,116 @@ function appendUnknownKindWarnings(out, extractorQualifiedId, applicableKinds, k
|
|
|
16446
16795
|
if (!knownKinds.has(k)) out.push({ extractorQualifiedId, unknownKind: k });
|
|
16447
16796
|
}
|
|
16448
16797
|
}
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
16452
|
-
|
|
16453
|
-
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
const
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16798
|
+
function collectUnknownSlotWarnings(plugins, knownSlots) {
|
|
16799
|
+
const out = [];
|
|
16800
|
+
collectBuiltInUnknownSlotWarnings(out, knownSlots);
|
|
16801
|
+
collectUserUnknownSlotWarnings(out, plugins, knownSlots);
|
|
16802
|
+
return out;
|
|
16803
|
+
}
|
|
16804
|
+
function collectBuiltInUnknownSlotWarnings(out, knownSlots) {
|
|
16805
|
+
for (const bundle of builtInBundles) {
|
|
16806
|
+
for (const ext of bundle.extensions) {
|
|
16807
|
+
const vc = ext.viewContributions;
|
|
16808
|
+
if (!vc) continue;
|
|
16809
|
+
appendUnknownSlotWarnings(out, qualifiedExtensionId(bundle.id, ext.id), vc, knownSlots);
|
|
16810
|
+
}
|
|
16811
|
+
}
|
|
16812
|
+
}
|
|
16813
|
+
function collectUserUnknownSlotWarnings(out, plugins, knownSlots) {
|
|
16814
|
+
for (const p of plugins) {
|
|
16815
|
+
if (p.status !== "enabled" || !p.extensions) continue;
|
|
16816
|
+
for (const ext of p.extensions) {
|
|
16817
|
+
const inst = extensionInstance(ext);
|
|
16818
|
+
if (!inst) continue;
|
|
16819
|
+
const vc = inst["viewContributions"];
|
|
16820
|
+
if (vc === null || typeof vc !== "object") continue;
|
|
16821
|
+
appendUnknownSlotWarnings(
|
|
16822
|
+
out,
|
|
16823
|
+
qualifiedExtensionId(ext.pluginId, ext.id),
|
|
16824
|
+
vc,
|
|
16825
|
+
knownSlots
|
|
16826
|
+
);
|
|
16827
|
+
}
|
|
16828
|
+
}
|
|
16829
|
+
}
|
|
16830
|
+
function appendUnknownSlotWarnings(out, extensionQualifiedId, viewContributions, knownSlots) {
|
|
16831
|
+
for (const [contributionId, raw] of Object.entries(viewContributions)) {
|
|
16832
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
16833
|
+
const slot = raw.slot;
|
|
16834
|
+
if (typeof slot !== "string") continue;
|
|
16835
|
+
if (knownSlots.has(slot)) continue;
|
|
16836
|
+
out.push({ extensionQualifiedId, contributionId, slot });
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
function buildDoctorJsonEnvelope(args2) {
|
|
16840
|
+
const issues = args2.bad.map((p) => ({
|
|
16841
|
+
id: sanitizeForTerminal(p.id),
|
|
16842
|
+
status: p.status,
|
|
16843
|
+
reason: sanitizeForTerminal(p.reason ?? "")
|
|
16844
|
+
}));
|
|
16845
|
+
const warnings = [];
|
|
16846
|
+
for (const w of args2.applicableKindWarnings) {
|
|
16847
|
+
warnings.push({
|
|
16848
|
+
id: sanitizeForTerminal(w.extractorQualifiedId),
|
|
16849
|
+
kind: "applicable-kind-unknown",
|
|
16850
|
+
message: tx(PLUGINS_TEXTS.doctorApplicableKindUnknown, {
|
|
16851
|
+
unknownKind: sanitizeForTerminal(w.unknownKind)
|
|
16852
|
+
})
|
|
16853
|
+
});
|
|
16854
|
+
}
|
|
16855
|
+
for (const w of args2.unknownSlotWarnings) {
|
|
16856
|
+
const slash = w.extensionQualifiedId.indexOf("/");
|
|
16857
|
+
const pluginId = slash >= 0 ? w.extensionQualifiedId.slice(0, slash) : w.extensionQualifiedId;
|
|
16858
|
+
warnings.push({
|
|
16859
|
+
id: sanitizeForTerminal(`${w.extensionQualifiedId}/${w.contributionId}`),
|
|
16860
|
+
kind: "unknown-slot",
|
|
16861
|
+
message: tx(PLUGINS_TEXTS.doctorUnknownSlot, {
|
|
16862
|
+
contributionId: sanitizeForTerminal(w.contributionId),
|
|
16863
|
+
slot: sanitizeForTerminal(w.slot),
|
|
16864
|
+
pluginId: sanitizeForTerminal(pluginId)
|
|
16865
|
+
})
|
|
16866
|
+
});
|
|
16867
|
+
}
|
|
16868
|
+
return {
|
|
16869
|
+
ok: true,
|
|
16870
|
+
kind: "plugins.doctor",
|
|
16871
|
+
counts: {
|
|
16872
|
+
enabled: args2.counts.enabled,
|
|
16873
|
+
disabled: args2.counts.disabled,
|
|
16874
|
+
loaded: args2.counts.enabled,
|
|
16875
|
+
incompatible: args2.counts["incompatible-spec"] + args2.counts["incompatible-catalog"],
|
|
16876
|
+
invalid: args2.counts["invalid-manifest"],
|
|
16877
|
+
loadError: args2.counts["load-error"] + args2.counts["id-collision"],
|
|
16878
|
+
warnings: args2.totalWarnings
|
|
16879
|
+
},
|
|
16880
|
+
issues,
|
|
16881
|
+
warnings,
|
|
16882
|
+
elapsedMs: args2.elapsedMs
|
|
16883
|
+
};
|
|
16884
|
+
}
|
|
16885
|
+
|
|
16886
|
+
// cli/commands/plugins/toggle.ts
|
|
16887
|
+
import { Command as Command25, Option as Option24 } from "clipanion";
|
|
16888
|
+
var TogglePluginsBase = class extends SmCommand {
|
|
16889
|
+
all = Option24.Boolean("--all", false);
|
|
16890
|
+
id = Option24.String({ required: false });
|
|
16891
|
+
async toggle(enabled) {
|
|
16892
|
+
const verb = enabled ? "enable" : "disable";
|
|
16893
|
+
const stderr = this.context.stderr;
|
|
16894
|
+
const stderrAnsi = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
16895
|
+
const argError = this.#validateArgs(stderrAnsi);
|
|
16896
|
+
if (argError !== null) return argError;
|
|
16897
|
+
const plugins = await loadAll({ global: this.global, pluginDir: void 0 });
|
|
16898
|
+
const catalogue = bundleCatalogue(plugins);
|
|
16899
|
+
const targetsResult = this.#pickTargets(catalogue, verb, stderrAnsi);
|
|
16900
|
+
if (typeof targetsResult === "number") return targetsResult;
|
|
16901
|
+
let targets = targetsResult;
|
|
16902
|
+
const lockError = this.#applyLockGate(targets, stderrAnsi);
|
|
16903
|
+
if (typeof lockError === "number") return lockError;
|
|
16904
|
+
targets = lockError;
|
|
16905
|
+
await this.#persistTargets(targets, enabled);
|
|
16906
|
+
this.#renderSuccess(targets, enabled);
|
|
16907
|
+
return ExitCode.Ok;
|
|
16472
16908
|
}
|
|
16473
16909
|
/**
|
|
16474
16910
|
* `--all` vs `<id>` mutex check. The two are mutually exclusive and
|
|
@@ -16493,7 +16929,7 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
16493
16929
|
*
|
|
16494
16930
|
* `--all` is a macro on bundle ids: every plugin / bundle the user
|
|
16495
16931
|
* can see. We deliberately do NOT expand to qualified
|
|
16496
|
-
* <bundle>/<ext> keys
|
|
16932
|
+
* <bundle>/<ext> keys, that would silently flip a granularity
|
|
16497
16933
|
* policy. For granularity=extension bundles the user already hits
|
|
16498
16934
|
* the directed error message when they try the bundle id directly,
|
|
16499
16935
|
* so `--all` skips them here too and the real "disable every core
|
|
@@ -16511,7 +16947,7 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
16511
16947
|
return [resolved.key];
|
|
16512
16948
|
}
|
|
16513
16949
|
/**
|
|
16514
|
-
* Host lock
|
|
16950
|
+
* Host lock, see `src/kernel/config/locked-plugins.ts`. `--all`
|
|
16515
16951
|
* silently skips locked targets so the user can still toggle the
|
|
16516
16952
|
* rest. Single-id mode surfaces a directed exit-5 message.
|
|
16517
16953
|
*/
|
|
@@ -16531,7 +16967,7 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
16531
16967
|
/**
|
|
16532
16968
|
* Persist the toggle in `config_plugins`. On disable, also purge
|
|
16533
16969
|
* the plugin's `scan_contributions` rows immediately (matches the
|
|
16534
|
-
* BFF route
|
|
16970
|
+
* BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
|
|
16535
16971
|
* `targets` carries either a bare bundle id (e.g. `claude`) or a
|
|
16536
16972
|
* qualified `<bundle>/<ext>` (e.g. `core/slash`); the split mirrors
|
|
16537
16973
|
* how the catalog sweep groups rows.
|
|
@@ -16597,8 +17033,8 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
|
|
|
16597
17033
|
description: "Disable a plugin (or --all). Persists in config_plugins; does not delete files.",
|
|
16598
17034
|
details: `
|
|
16599
17035
|
Writes a row to config_plugins with enabled=0. Discovery still
|
|
16600
|
-
surfaces the plugin in sm plugins list, but with status=disabled
|
|
16601
|
-
|
|
17036
|
+
surfaces the plugin in sm plugins list, but with status=disabled;
|
|
17037
|
+
its extensions are not imported and the kernel will not run
|
|
16602
17038
|
them.
|
|
16603
17039
|
|
|
16604
17040
|
Granularity: a bundle-granularity plugin (default for user plugins,
|
|
@@ -16705,7 +17141,7 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
|
|
|
16705
17141
|
}
|
|
16706
17142
|
|
|
16707
17143
|
// cli/commands/plugins/create.ts
|
|
16708
|
-
import { existsSync as existsSync21, mkdirSync as mkdirSync5, writeFileSync as
|
|
17144
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
16709
17145
|
import { join as join13, resolve as resolve31 } from "path";
|
|
16710
17146
|
import { Command as Command26, Option as Option25 } from "clipanion";
|
|
16711
17147
|
var PluginsCreateCommand = class extends SmCommand {
|
|
@@ -16719,18 +17155,27 @@ var PluginsCreateCommand = class extends SmCommand {
|
|
|
16719
17155
|
at = Option25.String("--at", { required: false });
|
|
16720
17156
|
force = Option25.Boolean("--force", false);
|
|
16721
17157
|
async run() {
|
|
17158
|
+
const stderr = this.context.stderr;
|
|
17159
|
+
const ansi = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
17160
|
+
const errGlyph = ansi.red("\u2715");
|
|
16722
17161
|
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(this.pluginId)) {
|
|
16723
17162
|
this.printer.error(
|
|
16724
|
-
|
|
16725
|
-
|
|
17163
|
+
tx(PLUGINS_TEXTS.createInvalidId, {
|
|
17164
|
+
glyph: errGlyph,
|
|
17165
|
+
id: sanitizeForTerminal(this.pluginId)
|
|
17166
|
+
})
|
|
16726
17167
|
);
|
|
16727
17168
|
return ExitCode.Error;
|
|
16728
17169
|
}
|
|
16729
|
-
const
|
|
17170
|
+
const ctx = defaultRuntimeContext();
|
|
17171
|
+
const baseDir = this.global ? defaultUserPluginsDir(ctx) : defaultProjectPluginsDir(ctx);
|
|
17172
|
+
const targetDir = this.at ? resolve31(this.at) : join13(baseDir, this.pluginId);
|
|
16730
17173
|
if (existsSync21(targetDir) && !this.force) {
|
|
16731
17174
|
this.printer.error(
|
|
16732
|
-
|
|
16733
|
-
|
|
17175
|
+
tx(PLUGINS_TEXTS.createRefuseOverwrite, {
|
|
17176
|
+
glyph: errGlyph,
|
|
17177
|
+
targetDir: sanitizeForTerminal(targetDir)
|
|
17178
|
+
})
|
|
16734
17179
|
);
|
|
16735
17180
|
return ExitCode.Error;
|
|
16736
17181
|
}
|
|
@@ -16753,22 +17198,20 @@ var PluginsCreateCommand = class extends SmCommand {
|
|
|
16753
17198
|
}
|
|
16754
17199
|
}
|
|
16755
17200
|
};
|
|
16756
|
-
|
|
17201
|
+
writeFileSync2(
|
|
16757
17202
|
join13(targetDir, "plugin.json"),
|
|
16758
17203
|
JSON.stringify(manifest, null, 2) + "\n"
|
|
16759
17204
|
);
|
|
16760
|
-
|
|
17205
|
+
writeFileSync2(
|
|
16761
17206
|
join13(targetDir, "extensions", "extractor.js"),
|
|
16762
17207
|
scaffolderExtractorStub(this.pluginId)
|
|
16763
17208
|
);
|
|
16764
|
-
|
|
17209
|
+
writeFileSync2(join13(targetDir, "README.md"), scaffolderReadme(this.pluginId));
|
|
16765
17210
|
this.printer.data(
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
- sm plugins slots list \u2014 browse other slots
|
|
16771
|
-
`
|
|
17211
|
+
tx(PLUGINS_TEXTS.createSuccess, {
|
|
17212
|
+
targetDir: sanitizeForTerminal(targetDir),
|
|
17213
|
+
pluginId: this.pluginId
|
|
17214
|
+
})
|
|
16772
17215
|
);
|
|
16773
17216
|
return ExitCode.Ok;
|
|
16774
17217
|
}
|
|
@@ -16834,16 +17277,16 @@ Generated by \`sm plugins create\`. Edit \`extensions/extractor.js\` to taste.
|
|
|
16834
17277
|
|
|
16835
17278
|
## Verbs
|
|
16836
17279
|
|
|
16837
|
-
- \`sm plugins show ${pluginId}
|
|
16838
|
-
- \`sm plugins doctor
|
|
16839
|
-
- \`sm scan
|
|
17280
|
+
- \`sm plugins show ${pluginId}\`: manifest + load status
|
|
17281
|
+
- \`sm plugins doctor\`: full plugin diagnostic
|
|
17282
|
+
- \`sm scan\`: re-emit contributions
|
|
16840
17283
|
|
|
16841
17284
|
## Resources
|
|
16842
17285
|
|
|
16843
17286
|
- \`spec/plugin-author-guide.md\` \xA7View contributions
|
|
16844
|
-
- \`spec/view-slots.md
|
|
16845
|
-
- \`spec/input-types.md
|
|
16846
|
-
- \`sm plugins slots list
|
|
17287
|
+
- \`spec/view-slots.md\`: the closed catalog of slots
|
|
17288
|
+
- \`spec/input-types.md\`: the closed catalog of input-types for settings
|
|
17289
|
+
- \`sm plugins slots list\`: browse the catalog from the CLI
|
|
16847
17290
|
`;
|
|
16848
17291
|
}
|
|
16849
17292
|
|
|
@@ -16852,11 +17295,11 @@ import { Command as Command27 } from "clipanion";
|
|
|
16852
17295
|
|
|
16853
17296
|
// cli/commands/plugins/slots-catalog.ts
|
|
16854
17297
|
var VIEW_SLOTS_CATALOG = [
|
|
16855
|
-
{ id: "card.title.right", summary: "Small icon marker next to the card title
|
|
17298
|
+
{ id: "card.title.right", summary: "Small icon marker next to the card title (language flag, platform glyph)." },
|
|
16856
17299
|
{ id: "card.subtitle.left", summary: "Single non-negative integer in the card subtitle row." },
|
|
16857
17300
|
{ id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
|
|
16858
17301
|
{ id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
|
|
16859
|
-
{ id: "graph.node.alert", summary: "Corner badge decoration on the graph node
|
|
17302
|
+
{ id: "graph.node.alert", summary: "Corner badge decoration on the graph node (alert / status)." },
|
|
16860
17303
|
{ id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
|
|
16861
17304
|
{ id: "inspector.header.badge.tag", summary: "Qualitative tag chip in the inspector header badge cluster." },
|
|
16862
17305
|
{ id: "inspector.body.panel.breakdown", summary: "Top-N labeled values rendered as a bar chart in the inspector body." },
|
|
@@ -16905,17 +17348,18 @@ var PluginsSlotsListCommand = class extends SmCommand {
|
|
|
16905
17348
|
...VIEW_SLOTS_CATALOG.map((c) => c.id.length),
|
|
16906
17349
|
...INPUT_TYPES_CATALOG.map((t) => t.id.length)
|
|
16907
17350
|
);
|
|
16908
|
-
this.printer.data(
|
|
16909
|
-
|
|
17351
|
+
this.printer.data(
|
|
17352
|
+
tx(PLUGINS_TEXTS.slotsListHeaderViewSlots, { count: VIEW_SLOTS_CATALOG.length })
|
|
17353
|
+
);
|
|
16910
17354
|
for (const c of VIEW_SLOTS_CATALOG) {
|
|
16911
17355
|
this.printer.data(
|
|
16912
17356
|
` ${c.id.padEnd(idWidth)} ${ansi.dim(c.summary)}
|
|
16913
17357
|
`
|
|
16914
17358
|
);
|
|
16915
17359
|
}
|
|
16916
|
-
this.printer.data(
|
|
16917
|
-
|
|
16918
|
-
|
|
17360
|
+
this.printer.data(
|
|
17361
|
+
tx(PLUGINS_TEXTS.slotsListHeaderInputTypes, { count: INPUT_TYPES_CATALOG.length })
|
|
17362
|
+
);
|
|
16919
17363
|
for (const t of INPUT_TYPES_CATALOG) {
|
|
16920
17364
|
this.printer.data(
|
|
16921
17365
|
` ${t.id.padEnd(idWidth)} ${ansi.dim(t.summary)}
|
|
@@ -16923,9 +17367,9 @@ var PluginsSlotsListCommand = class extends SmCommand {
|
|
|
16923
17367
|
);
|
|
16924
17368
|
}
|
|
16925
17369
|
this.printer.data(
|
|
16926
|
-
|
|
16927
|
-
|
|
16928
|
-
|
|
17370
|
+
tx(PLUGINS_TEXTS.slotsListTipFooter, {
|
|
17371
|
+
tip: ansi.dim(PLUGINS_TEXTS.slotsListTipText)
|
|
17372
|
+
})
|
|
16929
17373
|
);
|
|
16930
17374
|
return ExitCode.Ok;
|
|
16931
17375
|
}
|
|
@@ -16938,12 +17382,12 @@ var PluginsUpgradeCommand = class extends SmCommand {
|
|
|
16938
17382
|
static usage = Command28.Usage({
|
|
16939
17383
|
category: "Plugins",
|
|
16940
17384
|
description: "Apply catalog migrations to plugin manifests.",
|
|
16941
|
-
details: "No migrations registered against catalog v1.0.0 yet
|
|
17385
|
+
details: "No migrations registered against catalog v1.0.0 yet; this verb is a no-op today. The structure exists so future slot renames / deprecations land without spec churn."
|
|
16942
17386
|
});
|
|
16943
17387
|
pluginId = Option26.String({ required: false, name: "plugin-id" });
|
|
16944
17388
|
async run() {
|
|
16945
17389
|
this.printer.data(
|
|
16946
|
-
"sm plugins upgrade
|
|
17390
|
+
"sm plugins upgrade: no migrations registered for catalog v1.0.0.\n All loaded plugins are catalog-current.\n Run `sm plugins doctor` to surface any incompatible-catalog status.\n"
|
|
16947
17391
|
);
|
|
16948
17392
|
return ExitCode.Ok;
|
|
16949
17393
|
}
|
|
@@ -16991,6 +17435,13 @@ var REFRESH_TEXTS = {
|
|
|
16991
17435
|
refreshNodeNounPlural: "nodes",
|
|
16992
17436
|
// --- failures -------------------------------------------------------------
|
|
16993
17437
|
refreshFailed: "{{glyph}} sm refresh: {{message}}\n",
|
|
17438
|
+
/**
|
|
17439
|
+
* Error-envelope `message` body for `--json` failures. Used as the
|
|
17440
|
+
* `error.message` value when the verb cannot locate the project DB
|
|
17441
|
+
* (the `--json` consumer cannot rely on the human glyph + hint).
|
|
17442
|
+
*/
|
|
17443
|
+
jsonErrorDbMissing: "Project database not found. Run `sm init` before `sm refresh`.",
|
|
17444
|
+
jsonErrorNodeNotFound: "Node not found: {{nodePath}}",
|
|
16994
17445
|
/**
|
|
16995
17446
|
* Sub-detail composed inside `refreshFailed` when the failure is a
|
|
16996
17447
|
* filesystem read on a specific node body. Catalogued separately so the
|
|
@@ -17009,7 +17460,7 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17009
17460
|
details: `
|
|
17010
17461
|
Re-runs Extractors against the node(s) and upserts their outputs into
|
|
17011
17462
|
the universal enrichment layer (\`node_enrichments\`). Extractors are
|
|
17012
|
-
deterministic-only
|
|
17463
|
+
deterministic-only: they always run for real and persist.
|
|
17013
17464
|
|
|
17014
17465
|
Layer separation: enrichments live separately from the author's
|
|
17015
17466
|
frontmatter, which is immutable from any Extractor.
|
|
@@ -17071,6 +17522,10 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17071
17522
|
}
|
|
17072
17523
|
);
|
|
17073
17524
|
if (!persisted) {
|
|
17525
|
+
if (this.json) {
|
|
17526
|
+
this.#emitJsonError("db-missing", tx(REFRESH_TEXTS.jsonErrorDbMissing));
|
|
17527
|
+
return ExitCode.NotFound;
|
|
17528
|
+
}
|
|
17074
17529
|
this.printer.info(
|
|
17075
17530
|
tx(REFRESH_TEXTS.nodeNotFound, {
|
|
17076
17531
|
glyph: ansi.red("\u2715"),
|
|
@@ -17083,14 +17538,19 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17083
17538
|
const targetResult = this.#resolveTargetNodes(persisted, ansi);
|
|
17084
17539
|
if (!targetResult.ok) return targetResult.exitCode;
|
|
17085
17540
|
const targetNodes = targetResult.nodes;
|
|
17086
|
-
let
|
|
17541
|
+
let freshEnrichmentsByNode;
|
|
17087
17542
|
try {
|
|
17088
|
-
|
|
17543
|
+
freshEnrichmentsByNode = await this.#runExtractorsAcrossNodes(targetNodes, allExtractors, ctx.cwd);
|
|
17089
17544
|
} catch (err) {
|
|
17090
17545
|
const message = formatErrorMessage(err);
|
|
17546
|
+
if (this.json) {
|
|
17547
|
+
this.#emitJsonError("internal", message);
|
|
17548
|
+
return ExitCode.Error;
|
|
17549
|
+
}
|
|
17091
17550
|
this.printer.info(tx(REFRESH_TEXTS.refreshFailed, { glyph: errGlyph, message }));
|
|
17092
17551
|
return ExitCode.Error;
|
|
17093
17552
|
}
|
|
17553
|
+
const freshEnrichments = freshEnrichmentsByNode.flatMap((n) => n.enrichments);
|
|
17094
17554
|
if (freshEnrichments.length > 0) {
|
|
17095
17555
|
try {
|
|
17096
17556
|
await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
@@ -17100,10 +17560,25 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17100
17560
|
});
|
|
17101
17561
|
} catch (err) {
|
|
17102
17562
|
const message = formatErrorMessage(err);
|
|
17563
|
+
if (this.json) {
|
|
17564
|
+
this.#emitJsonError("internal", message);
|
|
17565
|
+
return ExitCode.Error;
|
|
17566
|
+
}
|
|
17103
17567
|
this.printer.info(tx(REFRESH_TEXTS.refreshFailed, { message }));
|
|
17104
17568
|
return ExitCode.Error;
|
|
17105
17569
|
}
|
|
17106
17570
|
}
|
|
17571
|
+
if (this.json) {
|
|
17572
|
+
const envelope = {
|
|
17573
|
+
ok: true,
|
|
17574
|
+
kind: "refresh.report",
|
|
17575
|
+
refreshed: freshEnrichments.length,
|
|
17576
|
+
nodes: freshEnrichmentsByNode.map((n) => ({ path: n.path, enrichments: n.enrichments.length })),
|
|
17577
|
+
elapsedMs: this.elapsed.ms()
|
|
17578
|
+
};
|
|
17579
|
+
this.printer.data(JSON.stringify(envelope) + "\n");
|
|
17580
|
+
return ExitCode.Ok;
|
|
17581
|
+
}
|
|
17107
17582
|
const glyph = ansi.green("\u2713");
|
|
17108
17583
|
const count = freshEnrichments.length;
|
|
17109
17584
|
const noun = count === 1 ? REFRESH_TEXTS.refreshNounSingular : REFRESH_TEXTS.refreshNounPlural;
|
|
@@ -17131,18 +17606,45 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17131
17606
|
}
|
|
17132
17607
|
return ExitCode.Ok;
|
|
17133
17608
|
}
|
|
17609
|
+
/**
|
|
17610
|
+
* Emit the canonical `--json` error envelope on stdout. Mirrors the
|
|
17611
|
+
* shape from `cli-contract.md` §Error envelope. Suppresses the
|
|
17612
|
+
* human-facing glyph + hint output that the non-JSON branches still
|
|
17613
|
+
* render.
|
|
17614
|
+
*/
|
|
17615
|
+
#emitJsonError(code, message) {
|
|
17616
|
+
const payload = { ok: false, error: { code, message } };
|
|
17617
|
+
this.printer.data(JSON.stringify(payload) + "\n");
|
|
17618
|
+
}
|
|
17134
17619
|
/**
|
|
17135
17620
|
* Decide which nodes the verb should refresh based on `--stale` /
|
|
17136
17621
|
* `<nodePath>`. Writes the per-target advisory to stdout (or the
|
|
17137
17622
|
* not-found / nothing-to-do message). Returns either the target list
|
|
17138
17623
|
* or the exit code the caller should use.
|
|
17624
|
+
*
|
|
17625
|
+
* Complexity is from the two-axis branch (stale-vs-single x
|
|
17626
|
+
* json-vs-human) plus the two terminal branches inside each axis.
|
|
17627
|
+
* Further extraction would split the method per axis but lose the
|
|
17628
|
+
* tight `nodesByPath.get(...)` reuse that drives both paths.
|
|
17139
17629
|
*/
|
|
17630
|
+
// eslint-disable-next-line complexity
|
|
17140
17631
|
#resolveTargetNodes(persisted, ansi) {
|
|
17141
17632
|
const nodesByPath = /* @__PURE__ */ new Map();
|
|
17142
17633
|
for (const node2 of persisted.result.nodes) nodesByPath.set(node2.path, node2);
|
|
17143
17634
|
if (this.stale) {
|
|
17144
17635
|
const staleEnrichments = persisted.enrichments.filter((e) => e.stale);
|
|
17145
17636
|
if (staleEnrichments.length === 0) {
|
|
17637
|
+
if (this.json) {
|
|
17638
|
+
const envelope = {
|
|
17639
|
+
ok: true,
|
|
17640
|
+
kind: "refresh.report",
|
|
17641
|
+
refreshed: 0,
|
|
17642
|
+
nodes: [],
|
|
17643
|
+
elapsedMs: this.elapsed.ms()
|
|
17644
|
+
};
|
|
17645
|
+
this.printer.data(JSON.stringify(envelope) + "\n");
|
|
17646
|
+
return { ok: false, exitCode: ExitCode.Ok };
|
|
17647
|
+
}
|
|
17146
17648
|
this.printer.data(
|
|
17147
17649
|
tx(REFRESH_TEXTS.refreshSuccessNoStale, { glyph: ansi.green("\u2713") })
|
|
17148
17650
|
);
|
|
@@ -17158,6 +17660,13 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17158
17660
|
}
|
|
17159
17661
|
const node = nodesByPath.get(this.nodePath);
|
|
17160
17662
|
if (!node) {
|
|
17663
|
+
if (this.json) {
|
|
17664
|
+
this.#emitJsonError(
|
|
17665
|
+
"not-found",
|
|
17666
|
+
tx(REFRESH_TEXTS.jsonErrorNodeNotFound, { nodePath: this.nodePath })
|
|
17667
|
+
);
|
|
17668
|
+
return { ok: false, exitCode: ExitCode.NotFound };
|
|
17669
|
+
}
|
|
17161
17670
|
this.printer.info(
|
|
17162
17671
|
tx(REFRESH_TEXTS.nodeNotFound, {
|
|
17163
17672
|
glyph: ansi.red("\u2715"),
|
|
@@ -17172,28 +17681,35 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17172
17681
|
/**
|
|
17173
17682
|
* For each target node: read its body off disk, run every applicable
|
|
17174
17683
|
* Extractor (deterministic-only by spec), and collect the enrichment
|
|
17175
|
-
* records they produce.
|
|
17684
|
+
* records they produce. Returns one entry per node (in iteration
|
|
17685
|
+
* order) so the verb's `--json` envelope can report a per-node
|
|
17686
|
+
* breakdown; consumers that only care about the flat list flatten
|
|
17687
|
+
* the result.
|
|
17176
17688
|
*/
|
|
17177
17689
|
async #runExtractorsAcrossNodes(targetNodes, allExtractors, cwd) {
|
|
17178
|
-
const
|
|
17690
|
+
const perNode = [];
|
|
17179
17691
|
for (const node of targetNodes) {
|
|
17692
|
+
const nodeEnrichments = [];
|
|
17180
17693
|
let body;
|
|
17181
17694
|
try {
|
|
17182
17695
|
assertContained(cwd, node.path);
|
|
17183
17696
|
const raw = await readFile3(resolve32(cwd, node.path), "utf8");
|
|
17184
17697
|
body = stripFrontmatterFence(raw);
|
|
17185
17698
|
} catch (err) {
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17192
|
-
|
|
17193
|
-
|
|
17699
|
+
if (!this.json) {
|
|
17700
|
+
const stderr = this.context.stderr;
|
|
17701
|
+
const ansi = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
17702
|
+
this.printer.info(
|
|
17703
|
+
tx(REFRESH_TEXTS.refreshFailed, {
|
|
17704
|
+
glyph: ansi.red("\u2715"),
|
|
17705
|
+
message: tx(REFRESH_TEXTS.readFailedDetail, {
|
|
17706
|
+
path: node.path,
|
|
17707
|
+
message: formatErrorMessage(err)
|
|
17708
|
+
})
|
|
17194
17709
|
})
|
|
17195
|
-
|
|
17196
|
-
|
|
17710
|
+
);
|
|
17711
|
+
}
|
|
17712
|
+
perNode.push({ path: node.path, enrichments: nodeEnrichments });
|
|
17197
17713
|
continue;
|
|
17198
17714
|
}
|
|
17199
17715
|
const fm = node.frontmatter ?? {};
|
|
@@ -17202,10 +17718,11 @@ var RefreshCommand = class extends SmCommand {
|
|
|
17202
17718
|
);
|
|
17203
17719
|
for (const extractor of applicable) {
|
|
17204
17720
|
const records = await runExtractorForEnrichment(extractor, node, body, fm);
|
|
17205
|
-
for (const record of records)
|
|
17721
|
+
for (const record of records) nodeEnrichments.push(record);
|
|
17206
17722
|
}
|
|
17723
|
+
perNode.push({ path: node.path, enrichments: nodeEnrichments });
|
|
17207
17724
|
}
|
|
17208
|
-
return
|
|
17725
|
+
return perNode;
|
|
17209
17726
|
}
|
|
17210
17727
|
};
|
|
17211
17728
|
async function runExtractorForEnrichment(extractor, node, body, frontmatter) {
|
|
@@ -17244,7 +17761,7 @@ var SCAN_TEXTS = {
|
|
|
17244
17761
|
scanFailure: "{{glyph}} sm scan: {{message}}\n",
|
|
17245
17762
|
guardWipeRefused: "{{glyph}} Refusing to wipe a populated DB ({{existing}} rows in scan_*) with a zero-result scan.\n {{hint}}\n",
|
|
17246
17763
|
guardWipeRefusedHint: "Pass --allow-empty to override. If this is unexpected, double-check the root paths.",
|
|
17247
|
-
jsonSelfValidationFailed: "{{glyph}} sm scan: internal
|
|
17764
|
+
jsonSelfValidationFailed: "{{glyph}} sm scan: internal: scan-result failed self-validation: {{errors}}\n",
|
|
17248
17765
|
/**
|
|
17249
17766
|
* Header summary line. `glyph` is ✓ (green) on success or ✕ (red)
|
|
17250
17767
|
* when error-severity issues fired; `counts` is the comma-separated
|
|
@@ -17254,9 +17771,9 @@ var SCAN_TEXTS = {
|
|
|
17254
17771
|
* ` (<N> roots)` for multi-root scans.
|
|
17255
17772
|
*/
|
|
17256
17773
|
scannedSummary: " {{glyph}} {{counts}} {{duration}}{{rootsSuffix}}\n",
|
|
17257
|
-
/** Body line directly under the header
|
|
17774
|
+
/** Body line directly under the header, final DB path (dim). */
|
|
17258
17775
|
persistedTo: " {{dbPath}}\n",
|
|
17259
|
-
/** Body line for dry-run mode
|
|
17776
|
+
/** Body line for dry-run mode, same indent, marker tail. */
|
|
17260
17777
|
wouldPersist: " would persist to {{dbPath}} (dry-run)\n",
|
|
17261
17778
|
// --- scan compare-with sub-verb --------------------------------------
|
|
17262
17779
|
compareErrorPrefix: "sm scan compare-with: {{message}}\n",
|
|
@@ -17272,7 +17789,7 @@ var SCAN_TEXTS = {
|
|
|
17272
17789
|
*/
|
|
17273
17790
|
compareDeltaSummary: "{{glyph}} Delta {{comparedTag}}\n {{nodesLine}}\n {{linksLine}}\n {{issuesLine}}",
|
|
17274
17791
|
compareDeltaComparedTag: "vs {{comparedWith}}",
|
|
17275
|
-
/** Per-row breakdown templates
|
|
17792
|
+
/** Per-row breakdown templates, composed at the call site with mid-dot separators. */
|
|
17276
17793
|
compareDeltaNodesLine: "nodes: {{added}} added \xB7 {{removed}} removed \xB7 {{changed}} changed",
|
|
17277
17794
|
compareDeltaLinksLine: "links: {{added}} added \xB7 {{removed}} removed",
|
|
17278
17795
|
compareDeltaIssuesLine: "issues: {{added}} added \xB7 {{removed}} removed",
|
|
@@ -17280,25 +17797,28 @@ var SCAN_TEXTS = {
|
|
|
17280
17797
|
compareDeltaNodesHeader: "## nodes",
|
|
17281
17798
|
compareDeltaLinksHeader: "## links",
|
|
17282
17799
|
compareDeltaIssuesHeader: "## issues",
|
|
17283
|
-
/** `+ <path> (<kind>)
|
|
17800
|
+
/** `+ <path> (<kind>)`, added node row. */
|
|
17284
17801
|
compareDeltaNodeAdded: "+ {{path}} ({{kind}})",
|
|
17285
|
-
/** `- <path> (<kind>)
|
|
17802
|
+
/** `- <path> (<kind>)`, removed node row. */
|
|
17286
17803
|
compareDeltaNodeRemoved: "- {{path}} ({{kind}})",
|
|
17287
|
-
/** `~ <path> (<reason> changed)
|
|
17804
|
+
/** `~ <path> (<reason> changed)`, changed node row. */
|
|
17288
17805
|
compareDeltaNodeChanged: "~ {{path}} ({{reason}} changed)",
|
|
17289
|
-
/** `+ <source> --<kind>--> <target
|
|
17806
|
+
/** `+ <source> --<kind>--> <target>`, added link row. */
|
|
17290
17807
|
compareDeltaLinkAdded: "+ {{source}} --{{kind}}--> {{target}}",
|
|
17291
|
-
/** `- <source> --<kind>--> <target
|
|
17808
|
+
/** `- <source> --<kind>--> <target>`, removed link row. */
|
|
17292
17809
|
compareDeltaLinkRemoved: "- {{source}} --{{kind}}--> {{target}}",
|
|
17293
|
-
/** `+ [<severity>] <analyzerId>: <message
|
|
17810
|
+
/** `+ [<severity>] <analyzerId>: <message>`, added issue row. */
|
|
17294
17811
|
compareDeltaIssueAdded: "+ [{{severity}}] {{analyzerId}}: {{message}}",
|
|
17295
|
-
/** `- [<severity>] <analyzerId>: <message
|
|
17812
|
+
/** `- [<severity>] <analyzerId>: <message>`, removed issue row. */
|
|
17296
17813
|
compareDeltaIssueRemoved: "- [{{severity}}] {{analyzerId}}: {{message}}"
|
|
17297
17814
|
};
|
|
17298
17815
|
|
|
17299
17816
|
// cli/commands/watch.ts
|
|
17300
17817
|
import { Command as Command30, Option as Option28 } from "clipanion";
|
|
17301
17818
|
|
|
17819
|
+
// core/watcher/runtime.ts
|
|
17820
|
+
import { dirname as dirname16 } from "path";
|
|
17821
|
+
|
|
17302
17822
|
// core/runtime/fresh-resolver.ts
|
|
17303
17823
|
async function buildFreshResolver(deps) {
|
|
17304
17824
|
const overrides = await tryWithSqlite(
|
|
@@ -17504,16 +18024,37 @@ function createWatcherRuntime(opts) {
|
|
|
17504
18024
|
});
|
|
17505
18025
|
};
|
|
17506
18026
|
const subscribeMeta = () => {
|
|
18027
|
+
const ignorePath = defaultIgnoreFilePath(cwd);
|
|
18028
|
+
const settingsPath = defaultSettingsPath(cwd);
|
|
18029
|
+
const metaTargets = /* @__PURE__ */ new Set([ignorePath, settingsPath]);
|
|
17507
18030
|
metaHandle = createChokidarWatcher({
|
|
18031
|
+
// Watch the PARENT directories with `depth: 0`, not the
|
|
18032
|
+
// individual files. Why: chokidar single-file watching on
|
|
18033
|
+
// macOS + FSEvents loses the watch when an editor performs an
|
|
18034
|
+
// atomic save (write to a tempfile, rename over the target).
|
|
18035
|
+
// The original inode the watcher attached to is gone and the
|
|
18036
|
+
// newly-renamed file is unobserved, so a `.skillmapignore`
|
|
18037
|
+
// edit silently fails to reach this hook and stale nodes
|
|
18038
|
+
// remain in the graph until the user touches some `.md` file
|
|
18039
|
+
// to force a per-file re-evaluation. Watching the parent
|
|
18040
|
+
// directory tracks the target by name (chokidar maps
|
|
18041
|
+
// directory-level events to filename), so atomic saves
|
|
18042
|
+
// surface as a normal `change` event regardless of inode
|
|
18043
|
+
// churn. The `metaTargets` filter above strips events for any
|
|
18044
|
+
// other file the parent directories happen to contain.
|
|
17508
18045
|
roots: [
|
|
17509
|
-
|
|
17510
|
-
|
|
18046
|
+
cwd,
|
|
18047
|
+
// parent of `.skillmapignore`
|
|
18048
|
+
dirname16(settingsPath)
|
|
18049
|
+
// parent of `.skill-map/settings.json`
|
|
17511
18050
|
],
|
|
17512
18051
|
cwd,
|
|
17513
18052
|
debounceMs,
|
|
17514
|
-
|
|
18053
|
+
depth: 0,
|
|
18054
|
+
// No ignore filter, these specific paths must always be
|
|
17515
18055
|
// observed regardless of any user pattern.
|
|
17516
|
-
onBatch: async () => {
|
|
18056
|
+
onBatch: async ({ paths }) => {
|
|
18057
|
+
if (!paths.some((p) => metaTargets.has(p))) return;
|
|
17517
18058
|
if (stopped) return;
|
|
17518
18059
|
try {
|
|
17519
18060
|
cfg = loadEffectiveConfig();
|
|
@@ -17583,10 +18124,10 @@ function createWatcherRuntime(opts) {
|
|
|
17583
18124
|
// cli/i18n/watch.texts.ts
|
|
17584
18125
|
var WATCH_TEXTS = {
|
|
17585
18126
|
configLoadFailure: "{{glyph}} sm watch: {{message}}\n",
|
|
17586
|
-
initialScanFailed: "{{glyph}} sm watch: initial scan failed
|
|
17587
|
-
batchFailed: "{{glyph}} sm watch: batch failed
|
|
17588
|
-
scanFailed: "{{glyph}} sm watch: scan failed
|
|
17589
|
-
watcherError: "{{glyph}} sm watch: watcher error
|
|
18127
|
+
initialScanFailed: "{{glyph}} sm watch: initial scan failed: {{message}}\n",
|
|
18128
|
+
batchFailed: "{{glyph}} sm watch: batch failed: {{message}}\n",
|
|
18129
|
+
scanFailed: "{{glyph}} sm watch: scan failed: {{message}}\n",
|
|
18130
|
+
watcherError: "{{glyph}} sm watch: watcher error: {{message}}\n",
|
|
17590
18131
|
starting: "sm watch: starting on {{rootsCount}} root(s), debounce {{debounceMs}}ms\n",
|
|
17591
18132
|
ready: "sm watch: ready. Press Ctrl+C to stop.\n",
|
|
17592
18133
|
stopped: "sm watch: stopped after {{batchCount}} batch(es).\n",
|
|
@@ -17599,7 +18140,7 @@ var WATCH_TEXTS = {
|
|
|
17599
18140
|
scannedNounIssuePlural: "issues",
|
|
17600
18141
|
scannedDurationTag: "in {{ms}}ms",
|
|
17601
18142
|
priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}",
|
|
17602
|
-
breakerTripped: "{{glyph}} sm watch: {{count}} consecutive batch failures
|
|
18143
|
+
breakerTripped: "{{glyph}} sm watch: {{count}} consecutive batch failures, shutting down.\n {{hint}}\n",
|
|
17603
18144
|
breakerTrippedHint: "Last error: {{message}}",
|
|
17604
18145
|
maxConsecutiveFailuresInvalid: "{{glyph}} sm watch: --max-consecutive-failures must be a non-negative integer (got {{raw}}).\n"
|
|
17605
18146
|
};
|
|
@@ -17655,7 +18196,7 @@ async function runWatchLoop(opts) {
|
|
|
17655
18196
|
emitterFactory: () => createStderrProgressEmitter(context.stderr),
|
|
17656
18197
|
runInitialBatch: true,
|
|
17657
18198
|
// CLI ordering: initial scan first, then subscribe. Matches the
|
|
17658
|
-
// historic `runWatchLoop` shape
|
|
18199
|
+
// historic `runWatchLoop` shape, events arriving during the
|
|
17659
18200
|
// initial scan are intentionally lost (the next user save covers
|
|
17660
18201
|
// any race).
|
|
17661
18202
|
subscribeBeforeInitial: false,
|
|
@@ -17778,7 +18319,7 @@ var WatchCommand = class extends SmCommand {
|
|
|
17778
18319
|
required: false,
|
|
17779
18320
|
description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
|
|
17780
18321
|
});
|
|
17781
|
-
// Long-running verb
|
|
18322
|
+
// Long-running verb, the watcher prints its own "stopped" line on
|
|
17782
18323
|
// SIGINT / SIGTERM. Adding `done in <…>` after that would be noise.
|
|
17783
18324
|
emitElapsed = false;
|
|
17784
18325
|
async run() {
|
|
@@ -17835,10 +18376,10 @@ var ScanCommand = class extends SmCommand {
|
|
|
17835
18376
|
the prior snapshot from the DB, reuse unchanged nodes, and only
|
|
17836
18377
|
reprocess new / modified files.
|
|
17837
18378
|
|
|
17838
|
-
Scans honour scan.extraFolders (append extra dirs verbatim
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
18379
|
+
Scans honour scan.extraFolders (append extra dirs verbatim, the
|
|
18380
|
+
only way to extend the scan beyond cwd) and scan.referencePaths
|
|
18381
|
+
(walk the configured dirs for link-validation only; files there
|
|
18382
|
+
are not indexed). Both are
|
|
17842
18383
|
privacy-sensitive; see "sm config set --help" for the --yes
|
|
17843
18384
|
gate.
|
|
17844
18385
|
`,
|
|
@@ -17921,7 +18462,7 @@ var ScanCommand = class extends SmCommand {
|
|
|
17921
18462
|
}
|
|
17922
18463
|
/**
|
|
17923
18464
|
* `--watch` is a thin alias for the `sm watch` verb. Combining
|
|
17924
|
-
* `--watch` with one-shot-only flags is incoherent
|
|
18465
|
+
* `--watch` with one-shot-only flags is incoherent, the watcher
|
|
17925
18466
|
* always persists incrementally over the prior snapshot.
|
|
17926
18467
|
*/
|
|
17927
18468
|
async runWatchAlias() {
|
|
@@ -18055,7 +18596,7 @@ var ScanCompareCommand = class extends SmCommand {
|
|
|
18055
18596
|
(default: current directory) using the same pipeline as 'sm scan'
|
|
18056
18597
|
(built-ins + plugin runtime + layered config + ignore filter),
|
|
18057
18598
|
and emits the delta between the dump and the fresh scan. The DB
|
|
18058
|
-
is NEVER touched
|
|
18599
|
+
is NEVER touched; this verb is read-only.
|
|
18059
18600
|
|
|
18060
18601
|
Exit 0 on empty delta (state matches the dump), exit 1 on any
|
|
18061
18602
|
drift (added / removed / changed nodes, links, or issues), exit
|
|
@@ -18289,13 +18830,26 @@ import { spawn as spawn2 } from "child_process";
|
|
|
18289
18830
|
import { existsSync as existsSync26 } from "fs";
|
|
18290
18831
|
import { Command as Command33, Option as Option31 } from "clipanion";
|
|
18291
18832
|
|
|
18833
|
+
// cli/util/browser-launch.ts
|
|
18834
|
+
function validateBrowserUrl(url) {
|
|
18835
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
18836
|
+
const FORBIDDEN_META = /["&|^<>%]/;
|
|
18837
|
+
if (FORBIDDEN_META.test(url)) return false;
|
|
18838
|
+
for (let i = 0; i < url.length; i++) {
|
|
18839
|
+
const code = url.charCodeAt(i);
|
|
18840
|
+
if (code <= 31 || code === 127) return false;
|
|
18841
|
+
}
|
|
18842
|
+
return true;
|
|
18843
|
+
}
|
|
18844
|
+
|
|
18292
18845
|
// server/index.ts
|
|
18293
18846
|
import { serve } from "@hono/node-server";
|
|
18294
18847
|
import { WebSocketServer } from "ws";
|
|
18295
18848
|
|
|
18296
18849
|
// server/app.ts
|
|
18297
18850
|
import { Hono } from "hono";
|
|
18298
|
-
import {
|
|
18851
|
+
import { bodyLimit } from "hono/body-limit";
|
|
18852
|
+
import { HTTPException as HTTPException13 } from "hono/http-exception";
|
|
18299
18853
|
|
|
18300
18854
|
// core/config/service.ts
|
|
18301
18855
|
var ConfigService = class {
|
|
@@ -18306,7 +18860,7 @@ var ConfigService = class {
|
|
|
18306
18860
|
}
|
|
18307
18861
|
/**
|
|
18308
18862
|
* Return the cached `ILoadedConfig` (loading on first call).
|
|
18309
|
-
* Subsequent calls return the same object reference
|
|
18863
|
+
* Subsequent calls return the same object reference, callers
|
|
18310
18864
|
* MUST treat it as read-only.
|
|
18311
18865
|
*/
|
|
18312
18866
|
get() {
|
|
@@ -18321,7 +18875,7 @@ var ConfigService = class {
|
|
|
18321
18875
|
return this.#cache;
|
|
18322
18876
|
}
|
|
18323
18877
|
/**
|
|
18324
|
-
* Sugar for `this.get().effective
|
|
18878
|
+
* Sugar for `this.get().effective`, the most common consumer pattern
|
|
18325
18879
|
* (the `sources` / `warnings` slots are only relevant to the
|
|
18326
18880
|
* `GET /api/config` and `sm config show` paths).
|
|
18327
18881
|
*/
|
|
@@ -18341,19 +18895,19 @@ var ConfigService = class {
|
|
|
18341
18895
|
|
|
18342
18896
|
// server/i18n/server.texts.ts
|
|
18343
18897
|
var SERVER_TEXTS = {
|
|
18344
|
-
// Boot banner
|
|
18898
|
+
// Boot banner, printed by the server itself when it begins to listen.
|
|
18345
18899
|
// The CLI verb `sm serve` formats its own boot banner separately
|
|
18346
18900
|
// (SERVE_TEXTS.boot) so the two surfaces can diverge if needed.
|
|
18347
18901
|
listening: "skill-map server listening on http://{{host}}:{{port}}\n",
|
|
18348
|
-
// UI bundle missing
|
|
18902
|
+
// UI bundle missing, non-fatal when the path was auto-resolved (the
|
|
18349
18903
|
// server keeps running with an inline placeholder at `/`). Becomes
|
|
18350
18904
|
// ExitCode.Error when `--ui-dist <path>` was explicit.
|
|
18351
|
-
uiBundleMissing: 'skill-map server: UI bundle not found at {{path}}
|
|
18352
|
-
// Loopback-only deprecation hint
|
|
18905
|
+
uiBundleMissing: 'skill-map server: UI bundle not found at {{path}} (serving inline placeholder at "/", run "npm run build --workspace=ui" to populate).\n',
|
|
18906
|
+
// Loopback-only deprecation hint, Decision #119. Logged once at boot
|
|
18353
18907
|
// when `--host` resolves to a non-loopback address. Multi-host serve
|
|
18354
18908
|
// re-opens post-v0.6.0.
|
|
18355
|
-
hostNonLoopbackHint: "skill-map server: --host {{host}} is non-loopback
|
|
18356
|
-
// Shutdown trace
|
|
18909
|
+
hostNonLoopbackHint: "skill-map server: --host {{host}} is non-loopback (through v0.6.0 the BFF assumes loopback-only, no auth). See Decision #119 in ROADMAP.\n",
|
|
18910
|
+
// Shutdown trace, printed by the close path so test runs that bring
|
|
18357
18911
|
// the server up and down have a clear marker.
|
|
18358
18912
|
closed: "skill-map server: closed.\n",
|
|
18359
18913
|
// ---- error envelope messages (Step 14.2) ---------------------------------
|
|
@@ -18361,51 +18915,97 @@ var SERVER_TEXTS = {
|
|
|
18361
18915
|
// Hint nudges the user toward `sm scan` so the SPA can call it via the
|
|
18362
18916
|
// CLI side-by-side with the server.
|
|
18363
18917
|
dbMissingHint: "No persisted scan available at {{path}}. Run `sm scan` to populate the DB.",
|
|
18918
|
+
// First-stage loopback gate (DNS rebinding + cross-origin defence). The
|
|
18919
|
+
// messages are pre-baked, terse, and shared across every probe so the
|
|
18920
|
+
// response stays opaque (no per-request state leaks). The discriminator
|
|
18921
|
+
// travels on `error.code`; the message is informational only.
|
|
18922
|
+
hostNotAllowed: "Request rejected: Host header is not loopback.",
|
|
18923
|
+
originNotAllowed: "Request rejected: Origin header is not loopback.",
|
|
18364
18924
|
// `?fresh=1` was requested but the server was booted with --no-built-ins
|
|
18365
18925
|
// or --no-plugins. A fresh scan with neither pipeline yields an empty /
|
|
18366
18926
|
// partial result that would surprise the SPA. Reject up front.
|
|
18367
18927
|
freshScanRequiresPipeline: "?fresh=1 cannot run while the server was started with --no-built-ins or --no-plugins (would yield empty / partial results).",
|
|
18368
|
-
// Unknown formatter on /api/graph
|
|
18928
|
+
// Unknown formatter on /api/graph, the user asked for a `format` value
|
|
18369
18929
|
// that no registered formatter advertises. Mirrors `sm graph`'s message.
|
|
18370
18930
|
graphUnknownFormat: 'Unknown graph format "{{format}}". Available: {{available}}.',
|
|
18371
18931
|
// Pagination caps on /api/nodes.
|
|
18372
18932
|
paginationLimitTooLarge: "limit={{value}} exceeds the maximum of {{max}}.",
|
|
18373
18933
|
paginationInvalidInteger: "{{name}}={{value}} is not a non-negative integer.",
|
|
18934
|
+
// Required-query-param miss (used by `parseRequiredString`). The
|
|
18935
|
+
// route names the offending parameter so the operator gets a useful
|
|
18936
|
+
// 400 instead of a generic "missing input".
|
|
18937
|
+
queryRequiredString: "Required query parameter: {{name}}.",
|
|
18938
|
+
// Malformed URL-path segment on a route whose params follow the
|
|
18939
|
+
// qualified-id alphabet (`[A-Za-z0-9._-]`). Surfaces on the
|
|
18940
|
+
// contributions lookup route (`/api/contributions/:pluginId/:extensionId/:contributionId`)
|
|
18941
|
+
// so a request with a slash, space, or control char in any segment
|
|
18942
|
+
// returns 400 before the kernel lookup.
|
|
18943
|
+
qualifiedIdMalformed: '{{name}}="{{value}}" is not a valid qualified-id segment ([A-Za-z0-9._-]+).',
|
|
18944
|
+
// 404 envelope for `/api/contributions/:pluginId/:extensionId/:contributionId`
|
|
18945
|
+
// when the catalog has no matching entry. Interpolates the full
|
|
18946
|
+
// triple so the SPA / operator can see which qualified id missed.
|
|
18947
|
+
contributionUnknown: "No registered contribution: {{pluginId}}/{{extensionId}}/{{contributionId}}.",
|
|
18948
|
+
// 400 envelope on /api/graph when `?format=` arrives with an invalid
|
|
18949
|
+
// shape (too long, or characters outside the formatter-id alphabet).
|
|
18950
|
+
// Caught BEFORE the registry lookup so a hostile value never reaches
|
|
18951
|
+
// the formatter table.
|
|
18952
|
+
graphFormatMalformed: 'format="{{value}}" is not a valid formatter id (lowercase a-z, 0-9, hyphen, max 32 chars).',
|
|
18953
|
+
// POST /api/scan + GET /api/scan?fresh=1, the runner returned a
|
|
18954
|
+
// `guard-trip` outcome (an idempotency / safety latch in the kernel).
|
|
18955
|
+
// Surfaced as a 500 with the offending row-count.
|
|
18956
|
+
scanGuardTrip: "scan refused (existing rows: {{existing}})",
|
|
18957
|
+
freshScanGuardTrip: "fresh scan refused (existing rows: {{existing}})",
|
|
18374
18958
|
// Node lookup miss on /api/nodes/:pathB64. Both the missing-node and
|
|
18375
|
-
// the malformed-pathB64 cases funnel here
|
|
18959
|
+
// the malformed-pathB64 cases funnel here, the client experience is
|
|
18376
18960
|
// the same (the resource isn't there).
|
|
18377
18961
|
nodeNotFound: 'No node with path "{{path}}".',
|
|
18378
|
-
pathB64Malformed: "Malformed pathB64
|
|
18962
|
+
pathB64Malformed: "Malformed pathB64, not a valid base64url-encoded node.path.",
|
|
18379
18963
|
// ---- WS broadcaster + watcher (Step 14.4.a) ------------------------------
|
|
18380
18964
|
// Logged once on watcher boot after chokidar's initial walk completes.
|
|
18381
18965
|
// Marks the broadcaster as armed and the live event stream as flowing.
|
|
18382
18966
|
watcherReady: 'skill-map server: watcher ready (roots="{{roots}}", debounceMs={{debounceMs}}).\n',
|
|
18383
|
-
// Watcher boot failure inside `createServer`. Non-fatal
|
|
18967
|
+
// Watcher boot failure inside `createServer`. Non-fatal, the REST
|
|
18384
18968
|
// surface stays alive so the operator can fix the underlying issue
|
|
18385
18969
|
// (config, plugin, FS permission) and restart.
|
|
18386
|
-
watcherBootFailed: "skill-map server: watcher boot failed
|
|
18970
|
+
watcherBootFailed: "skill-map server: watcher boot failed ({{message}}). /api/* still serving; pass --no-watcher to silence this on the next boot.\n",
|
|
18387
18971
|
// Per-batch failure inside the watcher's scan+persist pipeline. The
|
|
18388
|
-
// watcher loop continues
|
|
18972
|
+
// watcher loop continues, a transient FS error must not kill the
|
|
18389
18973
|
// broadcaster.
|
|
18390
|
-
watcherBatchFailed: "skill-map server: watcher batch failed
|
|
18974
|
+
watcherBatchFailed: "skill-map server: watcher batch failed ({{message}}).\n",
|
|
18391
18975
|
// chokidar surfaced an error. The watcher stays open per IFsWatcher's
|
|
18392
18976
|
// contract; the BFF also broadcasts a `watcher.error` advisory so the
|
|
18393
18977
|
// SPA can surface it in the live event log.
|
|
18394
|
-
watcherError: "skill-map server: watcher error
|
|
18978
|
+
watcherError: "skill-map server: watcher error ({{message}}).\n",
|
|
18395
18979
|
// chokidar.close() rejected during graceful shutdown. Logged but not
|
|
18396
|
-
// surfaced
|
|
18397
|
-
watcherCloseFailed: "skill-map server: watcher close failed
|
|
18980
|
+
// surfaced, close() is best-effort and idempotent.
|
|
18981
|
+
watcherCloseFailed: "skill-map server: watcher close failed ({{message}}).\n",
|
|
18982
|
+
// ---- body-limit middleware (app.ts, audit M4) ---------------------------
|
|
18983
|
+
// 413 envelope when a request body exceeds the global `BODY_LIMIT_BYTES`
|
|
18984
|
+
// cap. The discriminator travels on `error.code` (`payload-too-large`);
|
|
18985
|
+
// the message is informational only and names the byte cap so the
|
|
18986
|
+
// operator / SPA log can correlate without re-reading the source.
|
|
18987
|
+
bodyTooLarge: "Request body exceeds the {{maxBytes}}-byte limit.",
|
|
18988
|
+
// ---- onError fall-through (app.ts, audit L3) ----------------------------
|
|
18989
|
+
// 500 envelope for any throw that doesn't match a known mapped subclass
|
|
18990
|
+
// (DbMissingError, BulkValidationError, LoopbackGateError, HTTPException,
|
|
18991
|
+
// ExportQueryError, EConsentRequiredError). The raw err.message often
|
|
18992
|
+
// carries kernel detail (absolute paths, registry-probe hostnames),
|
|
18993
|
+
// so we redact the human-readable text to a generic constant and route
|
|
18994
|
+
// the real detail to log.warn instead. The envelope `code` stays
|
|
18995
|
+
// `internal`; `details` stays `null`. Operators see the full message
|
|
18996
|
+
// on stderr / log file via the BFF's logger.
|
|
18997
|
+
internalError: "internal error",
|
|
18398
18998
|
// ---- catch-all 404 envelopes (app.ts) ------------------------------------
|
|
18399
|
-
// `/api/*` catch-all
|
|
18999
|
+
// `/api/*` catch-all, request hit the API namespace but no route
|
|
18400
19000
|
// matched. The path is interpolated so the operator (and the SPA)
|
|
18401
19001
|
// can see exactly which endpoint was queried.
|
|
18402
19002
|
unknownApiEndpoint: "Unknown API endpoint: {{path}}.",
|
|
18403
|
-
// Hono's `app.notFound` fallback
|
|
19003
|
+
// Hono's `app.notFound` fallback, every other unmatched path funnels
|
|
18404
19004
|
// here (after static + SPA fallback have had their turn).
|
|
18405
19005
|
unknownPath: "Not found: {{path}}.",
|
|
18406
19006
|
// ---- sidecar bump route (routes/sidecar.ts) ------------------------------
|
|
18407
19007
|
// 409 refusal when a fresh node is bumped without `force`. The
|
|
18408
|
-
// `sidecar-fresh:` prefix is load-bearing
|
|
19008
|
+
// `sidecar-fresh:` prefix is load-bearing, the UI pattern-matches
|
|
18409
19009
|
// it (the global `app.onError` already maps HTTP 409 to the
|
|
18410
19010
|
// `sidecar-fresh` envelope `code`, so the prefix is for log-grep
|
|
18411
19011
|
// affinity with the CLI's bump verb).
|
|
@@ -18425,41 +19025,41 @@ var SERVER_TEXTS = {
|
|
|
18425
19025
|
* `ConfirmationService` dialog explaining `.sm` writes; on accept
|
|
18426
19026
|
* it retries with `confirm: true` in the body.
|
|
18427
19027
|
*/
|
|
18428
|
-
sidecarConsentRequired: "consent required to write .sm sidecar files in this project. Retry with `confirm: true` to grant (writes to .skill-map/settings.local.json
|
|
19028
|
+
sidecarConsentRequired: "consent required to write .sm sidecar files in this project. Retry with `confirm: true` to grant (writes to .skill-map/settings.local.json, gitignored).",
|
|
18429
19029
|
// 500 envelope when the built-in bump action ships without an
|
|
18430
|
-
// `invoke()
|
|
19030
|
+
// `invoke()`, should be impossible in production but the route
|
|
18431
19031
|
// throws a typed envelope rather than a bare `Error` so the global
|
|
18432
19032
|
// `app.onError` can format it.
|
|
18433
19033
|
sidecarBumpInvokeMissing: "built-in bump action is missing its invoke().",
|
|
18434
19034
|
// ---- POST /api/scan (manual refresh) ------------------------------------
|
|
18435
|
-
// 400
|
|
19035
|
+
// 400, runtime cannot persist a meaningful scan because the boot
|
|
18436
19036
|
// dropped half the pipeline. Same gate the `?fresh=1` GET applies.
|
|
18437
19037
|
scanPostRequiresFullPipeline: "POST /api/scan cannot run while the server was started with --no-built-ins or --no-plugins (would persist a partial DB).",
|
|
18438
|
-
// 409
|
|
19038
|
+
// 409, another scan (watcher batch or another POST) is in flight.
|
|
18439
19039
|
// The `scan-busy:` prefix is load-bearing: HTTP 409 maps to
|
|
18440
19040
|
// `scan-busy` in `app.onError`'s `codeForStatus`, but the prefix
|
|
18441
19041
|
// keeps log-grep affinity with the CLI's `sm scan` verb.
|
|
18442
19042
|
scanPostBusy: "scan-busy: Another scan is already in flight; retry once it finishes.",
|
|
18443
|
-
// 500
|
|
19043
|
+
// 500, DB missing on a write path. Read paths degrade to empty
|
|
18444
19044
|
// shapes; mutations cannot persist without a DB so they fail fast.
|
|
18445
19045
|
scanPostDbMissing: "Cannot persist scan: project DB not found. Run `sm scan` once or pass --db <path>.",
|
|
18446
19046
|
// ---- plugins toggle route (routes/plugins.ts) ---------------------------
|
|
18447
|
-
// 400 envelopes from `parsePluginPatchBody
|
|
19047
|
+
// 400 envelopes from `parsePluginPatchBody`, every branch keeps its
|
|
18448
19048
|
// own key so the UI can disambiguate without regex on the message.
|
|
18449
19049
|
pluginsBodyNotJson: "Request body must be valid JSON.",
|
|
18450
19050
|
pluginsBodyNotObject: "Request body must be a JSON object.",
|
|
18451
19051
|
pluginsEnabledRequired: "`enabled` is required and must be a boolean.",
|
|
18452
|
-
// 400
|
|
19052
|
+
// 400, granularity mismatch. Two flavours so the message is useful
|
|
18453
19053
|
// when the operator hits the wrong route by hand.
|
|
18454
19054
|
pluginsGranularityExtensionExpected: 'Plugin "{{id}}" has granularity:"extension"; toggle individual extensions via PATCH /api/plugins/{{id}}/extensions/<extensionId>.',
|
|
18455
19055
|
pluginsGranularityBundleExpected: 'Plugin "{{id}}" has granularity:"bundle"; toggle the whole bundle via PATCH /api/plugins/{{id}}.',
|
|
18456
|
-
// 404
|
|
19056
|
+
// 404, unknown plugin / extension.
|
|
18457
19057
|
pluginsUnknown: 'No plugin with id "{{id}}".',
|
|
18458
19058
|
pluginsExtensionUnknown: 'Plugin "{{bundleId}}" has no extension named "{{extensionId}}".',
|
|
18459
|
-
// 500
|
|
19059
|
+
// 500, DB missing on a write path. Read paths degrade to empty
|
|
18460
19060
|
// shapes, but mutations cannot persist without a DB so they fail fast.
|
|
18461
19061
|
pluginsDbMissing: "Cannot persist plugin override: project DB not found at {{path}}. Run `sm scan` first or pass --db <path>.",
|
|
18462
|
-
// 403
|
|
19062
|
+
// 403, host-enforced lock from `src/server/locked-plugins.ts`. The
|
|
18463
19063
|
// bundle (or qualified extension) is in the hardcoded lock-list and
|
|
18464
19064
|
// its enabled state is fixed; the UI mirrors the same rule by
|
|
18465
19065
|
// disabling the toggle.
|
|
@@ -18475,7 +19075,7 @@ var SERVER_TEXTS = {
|
|
|
18475
19075
|
//
|
|
18476
19076
|
// GET / PATCH /api/preferences. The PATCH body is shaped
|
|
18477
19077
|
// `{ updateCheck?: { enabled?: boolean } }`
|
|
18478
|
-
//
|
|
19078
|
+
// additive: future user-only preferences (locale, theme) extend the
|
|
18479
19079
|
// shape under their own sub-key. Each error keeps its own message
|
|
18480
19080
|
// key so the UI can disambiguate without regex on the body.
|
|
18481
19081
|
preferencesBodyNotJson: "Request body must be valid JSON.",
|
|
@@ -18506,15 +19106,69 @@ var SERVER_TEXTS = {
|
|
|
18506
19106
|
wsBackpressureEvicted: "skill-map server: ws client evicted (bufferedAmount={{buffered}} > threshold={{threshold}}).\n",
|
|
18507
19107
|
// `WebSocket.send()` threw on a registered client. The client is
|
|
18508
19108
|
// unregistered; the broadcast continues with the remaining clients.
|
|
18509
|
-
wsClientSendFailed: "skill-map server: ws send failed
|
|
19109
|
+
wsClientSendFailed: "skill-map server: ws send failed ({{message}}).\n",
|
|
18510
19110
|
// `JSON.stringify(envelope)` threw inside `broadcast()`. The event is
|
|
18511
19111
|
// dropped. Per spec/job-events.md §Error handling, the right shape
|
|
18512
19112
|
// is a synthetic `emitter.error` event; v14.4.a does not yet route
|
|
18513
19113
|
// it through the broadcaster (would re-enter the same stringify
|
|
18514
19114
|
// path), so we degrade to a logged warning.
|
|
18515
|
-
wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped
|
|
19115
|
+
wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n"
|
|
18516
19116
|
};
|
|
18517
19117
|
|
|
19118
|
+
// server/loopback-gate.ts
|
|
19119
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
19120
|
+
"127.0.0.1",
|
|
19121
|
+
"::1",
|
|
19122
|
+
"localhost"
|
|
19123
|
+
]);
|
|
19124
|
+
function createLoopbackGate(_opts) {
|
|
19125
|
+
return async function loopbackGate(c, next) {
|
|
19126
|
+
if (!hostAllowed(c.req.header("host"))) {
|
|
19127
|
+
throw new LoopbackGateError({
|
|
19128
|
+
code: "host-not-allowed",
|
|
19129
|
+
message: SERVER_TEXTS.hostNotAllowed
|
|
19130
|
+
});
|
|
19131
|
+
}
|
|
19132
|
+
if (originGuarded(c.req.path) && !originAllowed(c.req.header("origin"))) {
|
|
19133
|
+
throw new LoopbackGateError({
|
|
19134
|
+
code: "origin-not-allowed",
|
|
19135
|
+
message: SERVER_TEXTS.originNotAllowed
|
|
19136
|
+
});
|
|
19137
|
+
}
|
|
19138
|
+
await next();
|
|
19139
|
+
};
|
|
19140
|
+
}
|
|
19141
|
+
function hostAllowed(host) {
|
|
19142
|
+
if (host === void 0 || host === "") return true;
|
|
19143
|
+
return LOOPBACK_HOSTNAMES.has(hostnameOf(host));
|
|
19144
|
+
}
|
|
19145
|
+
function originAllowed(origin) {
|
|
19146
|
+
if (origin === void 0 || origin === "") return true;
|
|
19147
|
+
if (origin.toLowerCase() === "null") return true;
|
|
19148
|
+
try {
|
|
19149
|
+
const url = new URL(origin);
|
|
19150
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
19151
|
+
return LOOPBACK_HOSTNAMES.has(url.hostname.toLowerCase());
|
|
19152
|
+
} catch {
|
|
19153
|
+
return false;
|
|
19154
|
+
}
|
|
19155
|
+
}
|
|
19156
|
+
function hostnameOf(host) {
|
|
19157
|
+
const lower = host.toLowerCase();
|
|
19158
|
+
if (lower.startsWith("[")) {
|
|
19159
|
+
const close = lower.indexOf("]");
|
|
19160
|
+
if (close < 0) return lower;
|
|
19161
|
+
return lower.slice(1, close);
|
|
19162
|
+
}
|
|
19163
|
+
const colon = lower.indexOf(":");
|
|
19164
|
+
return colon < 0 ? lower : lower.slice(0, colon);
|
|
19165
|
+
}
|
|
19166
|
+
function originGuarded(path) {
|
|
19167
|
+
if (path === "/ws") return true;
|
|
19168
|
+
if (path.startsWith("/api/")) return true;
|
|
19169
|
+
return false;
|
|
19170
|
+
}
|
|
19171
|
+
|
|
18518
19172
|
// server/routes/annotations.ts
|
|
18519
19173
|
var ENVELOPE_KIND = "annotations.registered";
|
|
18520
19174
|
function registerAnnotationsRoute(app, deps) {
|
|
@@ -18531,6 +19185,63 @@ function registerAnnotationsRoute(app, deps) {
|
|
|
18531
19185
|
}
|
|
18532
19186
|
|
|
18533
19187
|
// server/routes/contributions.ts
|
|
19188
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
19189
|
+
|
|
19190
|
+
// server/util/parse-query.ts
|
|
19191
|
+
import { HTTPException } from "hono/http-exception";
|
|
19192
|
+
function parseCsv(value) {
|
|
19193
|
+
if (value === void 0) return [];
|
|
19194
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
19195
|
+
}
|
|
19196
|
+
function parsePagination(query, defaults) {
|
|
19197
|
+
const offset = parseNonNegativeInt(query.offset, "offset", 0);
|
|
19198
|
+
const limit = parseNonNegativeInt(query.limit, "limit", defaults.limit);
|
|
19199
|
+
if (limit > defaults.max) {
|
|
19200
|
+
throw new HTTPException(400, {
|
|
19201
|
+
message: tx(SERVER_TEXTS.paginationLimitTooLarge, {
|
|
19202
|
+
value: limit,
|
|
19203
|
+
max: defaults.max
|
|
19204
|
+
})
|
|
19205
|
+
});
|
|
19206
|
+
}
|
|
19207
|
+
return { offset, limit };
|
|
19208
|
+
}
|
|
19209
|
+
function parseBooleanFlag(value) {
|
|
19210
|
+
return value === "1" || value === "true";
|
|
19211
|
+
}
|
|
19212
|
+
function parseRequiredString(value, name) {
|
|
19213
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
19214
|
+
throw new HTTPException(400, {
|
|
19215
|
+
message: tx(SERVER_TEXTS.queryRequiredString, { name })
|
|
19216
|
+
});
|
|
19217
|
+
}
|
|
19218
|
+
return value;
|
|
19219
|
+
}
|
|
19220
|
+
function parseNonNegativeInt(raw, name, fallback) {
|
|
19221
|
+
if (raw === void 0 || raw.length === 0) return fallback;
|
|
19222
|
+
const trimmed = raw.trim();
|
|
19223
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
19224
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
19225
|
+
throw new HTTPException(400, {
|
|
19226
|
+
message: tx(SERVER_TEXTS.paginationInvalidInteger, { name, value: raw })
|
|
19227
|
+
});
|
|
19228
|
+
}
|
|
19229
|
+
return parsed;
|
|
19230
|
+
}
|
|
19231
|
+
|
|
19232
|
+
// server/routes/contributions.ts
|
|
19233
|
+
var QUALIFIED_ID_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
19234
|
+
function parseQualifiedIdSegment(value, name) {
|
|
19235
|
+
if (!QUALIFIED_ID_SEGMENT.test(value)) {
|
|
19236
|
+
throw new HTTPException2(400, {
|
|
19237
|
+
message: tx(SERVER_TEXTS.qualifiedIdMalformed, {
|
|
19238
|
+
name,
|
|
19239
|
+
value: sanitizeForTerminal(value)
|
|
19240
|
+
})
|
|
19241
|
+
});
|
|
19242
|
+
}
|
|
19243
|
+
return value;
|
|
19244
|
+
}
|
|
18534
19245
|
var REGISTERED_ENVELOPE_KIND = "contributions.registered";
|
|
18535
19246
|
function registerContributionsRoutes(app, deps) {
|
|
18536
19247
|
app.get("/api/contributions/registered", (c) => {
|
|
@@ -18544,27 +19255,21 @@ function registerContributionsRoutes(app, deps) {
|
|
|
18544
19255
|
return c.json(envelope);
|
|
18545
19256
|
});
|
|
18546
19257
|
app.get("/api/contributions/:pluginId/:extensionId/:contributionId", async (c) => {
|
|
18547
|
-
const pluginId = c.req.param("pluginId");
|
|
18548
|
-
const extensionId = c.req.param("extensionId");
|
|
18549
|
-
const contributionId = c.req.param("contributionId");
|
|
18550
|
-
const nodePath = c.req.query("path");
|
|
18551
|
-
if (typeof nodePath !== "string" || nodePath.length === 0) {
|
|
18552
|
-
return c.json(
|
|
18553
|
-
{ error: "missing-path", message: "Required query parameter: path" },
|
|
18554
|
-
400
|
|
18555
|
-
);
|
|
18556
|
-
}
|
|
19258
|
+
const pluginId = parseQualifiedIdSegment(c.req.param("pluginId"), "pluginId");
|
|
19259
|
+
const extensionId = parseQualifiedIdSegment(c.req.param("extensionId"), "extensionId");
|
|
19260
|
+
const contributionId = parseQualifiedIdSegment(c.req.param("contributionId"), "contributionId");
|
|
19261
|
+
const nodePath = parseRequiredString(c.req.query("path"), "path");
|
|
18557
19262
|
const catalogEntry = deps.kernel.getRegisteredViewContributions().find(
|
|
18558
19263
|
(e) => e.pluginId === pluginId && e.extensionId === extensionId && e.contributionId === contributionId
|
|
18559
19264
|
);
|
|
18560
19265
|
if (!catalogEntry) {
|
|
18561
|
-
|
|
18562
|
-
{
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
);
|
|
19266
|
+
throw new HTTPException2(404, {
|
|
19267
|
+
message: tx(SERVER_TEXTS.contributionUnknown, {
|
|
19268
|
+
pluginId: sanitizeForTerminal(pluginId),
|
|
19269
|
+
extensionId: sanitizeForTerminal(extensionId),
|
|
19270
|
+
contributionId: sanitizeForTerminal(contributionId)
|
|
19271
|
+
})
|
|
19272
|
+
});
|
|
18568
19273
|
}
|
|
18569
19274
|
const rows = await tryWithSqlite(
|
|
18570
19275
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
@@ -18589,7 +19294,7 @@ function registerContributionsRoutes(app, deps) {
|
|
|
18589
19294
|
}
|
|
18590
19295
|
|
|
18591
19296
|
// server/routes/config.ts
|
|
18592
|
-
import { HTTPException } from "hono/http-exception";
|
|
19297
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
18593
19298
|
|
|
18594
19299
|
// server/envelope.ts
|
|
18595
19300
|
var REST_ENVELOPE_SCHEMA_VERSION = "1";
|
|
@@ -18626,7 +19331,7 @@ function registerConfigRoute(app, deps) {
|
|
|
18626
19331
|
try {
|
|
18627
19332
|
loaded = deps.configService.get();
|
|
18628
19333
|
} catch (err) {
|
|
18629
|
-
throw new
|
|
19334
|
+
throw new HTTPException3(500, { message: formatErrorMessage(err) });
|
|
18630
19335
|
}
|
|
18631
19336
|
for (const warn of loaded.warnings) {
|
|
18632
19337
|
log.warn(sanitizeForTerminal(warn));
|
|
@@ -18636,7 +19341,7 @@ function registerConfigRoute(app, deps) {
|
|
|
18636
19341
|
}
|
|
18637
19342
|
|
|
18638
19343
|
// server/routes/favorites.ts
|
|
18639
|
-
import { HTTPException as
|
|
19344
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
18640
19345
|
|
|
18641
19346
|
// server/path-codec.ts
|
|
18642
19347
|
var PathCodecError = class extends Error {
|
|
@@ -18676,7 +19381,7 @@ function registerFavoritesRoutes(app, deps) {
|
|
|
18676
19381
|
}
|
|
18677
19382
|
);
|
|
18678
19383
|
if (!result || !result.found) {
|
|
18679
|
-
throw new
|
|
19384
|
+
throw new HTTPException4(404, {
|
|
18680
19385
|
message: tx(SERVER_TEXTS.nodeNotFound, { path: nodePath })
|
|
18681
19386
|
});
|
|
18682
19387
|
}
|
|
@@ -18696,18 +19401,30 @@ function decodePath(pathB64) {
|
|
|
18696
19401
|
return decodeNodePath(pathB64);
|
|
18697
19402
|
} catch (err) {
|
|
18698
19403
|
if (err instanceof PathCodecError) {
|
|
18699
|
-
throw new
|
|
19404
|
+
throw new HTTPException4(404, { message: SERVER_TEXTS.pathB64Malformed });
|
|
18700
19405
|
}
|
|
18701
19406
|
throw err;
|
|
18702
19407
|
}
|
|
18703
19408
|
}
|
|
18704
19409
|
|
|
18705
19410
|
// server/routes/graph.ts
|
|
18706
|
-
import { HTTPException as
|
|
19411
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
18707
19412
|
var DEFAULT_FORMAT2 = "ascii";
|
|
19413
|
+
var FORMAT_ID_PATTERN = /^[a-z0-9-]+$/;
|
|
19414
|
+
var FORMAT_ID_MAX = 32;
|
|
18708
19415
|
function registerGraphRoute(app, deps) {
|
|
18709
19416
|
app.get("/api/graph", async (c) => {
|
|
18710
19417
|
const format = c.req.query("format") ?? DEFAULT_FORMAT2;
|
|
19418
|
+
if (format.length > FORMAT_ID_MAX || !FORMAT_ID_PATTERN.test(format)) {
|
|
19419
|
+
throw new HTTPException5(400, {
|
|
19420
|
+
// Sanitize defensively, the regex above already rejects ANSI
|
|
19421
|
+
// and control bytes, but the message interpolates user input
|
|
19422
|
+
// and the BFF mirrors error envelopes into the server log.
|
|
19423
|
+
message: tx(SERVER_TEXTS.graphFormatMalformed, {
|
|
19424
|
+
value: sanitizeForTerminal(format)
|
|
19425
|
+
})
|
|
19426
|
+
});
|
|
19427
|
+
}
|
|
18711
19428
|
const formatters = composeFormatters({
|
|
18712
19429
|
noBuiltIns: deps.options.noBuiltIns,
|
|
18713
19430
|
pluginRuntime: deps.pluginRuntime
|
|
@@ -18715,7 +19432,7 @@ function registerGraphRoute(app, deps) {
|
|
|
18715
19432
|
const formatter = formatters.find((f) => f.formatId === format);
|
|
18716
19433
|
if (!formatter) {
|
|
18717
19434
|
const available = formatters.map((f) => f.formatId).sort().join(", ");
|
|
18718
|
-
throw new
|
|
19435
|
+
throw new HTTPException5(400, {
|
|
18719
19436
|
message: tx(SERVER_TEXTS.graphUnknownFormat, {
|
|
18720
19437
|
format,
|
|
18721
19438
|
available: available || "(none)"
|
|
@@ -18726,16 +19443,23 @@ function registerGraphRoute(app, deps) {
|
|
|
18726
19443
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
18727
19444
|
(adapter) => adapter.scans.load()
|
|
18728
19445
|
);
|
|
18729
|
-
const
|
|
18730
|
-
const text = formatter.format({
|
|
18731
|
-
nodes: scan.nodes,
|
|
18732
|
-
links: scan.links,
|
|
18733
|
-
issues: scan.issues
|
|
18734
|
-
});
|
|
19446
|
+
const text = renderGraphPayload(formatter, loaded);
|
|
18735
19447
|
const body = text.endsWith("\n") ? text : text + "\n";
|
|
18736
19448
|
return c.body(body, 200, { "content-type": contentTypeFor(format) });
|
|
18737
19449
|
});
|
|
18738
19450
|
}
|
|
19451
|
+
function renderGraphPayload(formatter, loaded) {
|
|
19452
|
+
const scan = loaded ?? { nodes: [], links: [], issues: [] };
|
|
19453
|
+
if (loaded === null) {
|
|
19454
|
+
return formatter.format({ nodes: scan.nodes, links: scan.links, issues: scan.issues });
|
|
19455
|
+
}
|
|
19456
|
+
return formatter.format({
|
|
19457
|
+
nodes: scan.nodes,
|
|
19458
|
+
links: scan.links,
|
|
19459
|
+
issues: scan.issues,
|
|
19460
|
+
scanResult: loaded
|
|
19461
|
+
});
|
|
19462
|
+
}
|
|
18739
19463
|
function contentTypeFor(format) {
|
|
18740
19464
|
if (format === "json") return "application/json; charset=utf-8";
|
|
18741
19465
|
if (format === "md" || format === "markdown" || format === "mermaid") {
|
|
@@ -18801,67 +19525,41 @@ function registerHealthRoute(app, deps) {
|
|
|
18801
19525
|
});
|
|
18802
19526
|
}
|
|
18803
19527
|
|
|
18804
|
-
// server/util/parse-query.ts
|
|
18805
|
-
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
18806
|
-
function parseCsv(value) {
|
|
18807
|
-
if (value === void 0) return [];
|
|
18808
|
-
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18809
|
-
}
|
|
18810
|
-
function parsePagination(query, defaults) {
|
|
18811
|
-
const offset = parseNonNegativeInt(query.offset, "offset", 0);
|
|
18812
|
-
const limit = parseNonNegativeInt(query.limit, "limit", defaults.limit);
|
|
18813
|
-
if (limit > defaults.max) {
|
|
18814
|
-
throw new HTTPException4(400, {
|
|
18815
|
-
message: tx(SERVER_TEXTS.paginationLimitTooLarge, {
|
|
18816
|
-
value: limit,
|
|
18817
|
-
max: defaults.max
|
|
18818
|
-
})
|
|
18819
|
-
});
|
|
18820
|
-
}
|
|
18821
|
-
return { offset, limit };
|
|
18822
|
-
}
|
|
18823
|
-
function parseBooleanFlag(value) {
|
|
18824
|
-
return value === "1" || value === "true";
|
|
18825
|
-
}
|
|
18826
|
-
function parseNonNegativeInt(raw, name, fallback) {
|
|
18827
|
-
if (raw === void 0 || raw.length === 0) return fallback;
|
|
18828
|
-
const trimmed = raw.trim();
|
|
18829
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
18830
|
-
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
18831
|
-
throw new HTTPException4(400, {
|
|
18832
|
-
message: tx(SERVER_TEXTS.paginationInvalidInteger, { name, value: raw })
|
|
18833
|
-
});
|
|
18834
|
-
}
|
|
18835
|
-
return parsed;
|
|
18836
|
-
}
|
|
18837
|
-
|
|
18838
19528
|
// server/routes/issues.ts
|
|
19529
|
+
var DEFAULT_LIMIT = 100;
|
|
19530
|
+
var MAX_LIMIT = 1e3;
|
|
18839
19531
|
function registerIssuesRoute(app, deps) {
|
|
18840
19532
|
app.get("/api/issues", async (c) => {
|
|
18841
19533
|
const severityFilter = parseCsv(c.req.query("severity"));
|
|
18842
19534
|
const analyzerFilter = parseCsv(c.req.query("analyzerId"));
|
|
18843
19535
|
const nodePath = c.req.query("node") ?? null;
|
|
18844
|
-
const
|
|
19536
|
+
const { offset, limit } = parsePagination(c.req.query(), {
|
|
19537
|
+
limit: DEFAULT_LIMIT,
|
|
19538
|
+
max: MAX_LIMIT
|
|
19539
|
+
});
|
|
19540
|
+
const result = await tryWithSqlite(
|
|
18845
19541
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
18846
|
-
(adapter) => adapter.issues.
|
|
19542
|
+
(adapter) => adapter.issues.list({
|
|
19543
|
+
severities: severityFilter,
|
|
19544
|
+
analyzerIds: analyzerFilter,
|
|
19545
|
+
nodePath,
|
|
19546
|
+
offset,
|
|
19547
|
+
limit
|
|
19548
|
+
})
|
|
18847
19549
|
);
|
|
18848
|
-
const
|
|
18849
|
-
const
|
|
18850
|
-
if (severityFilter.length > 0 && !severityFilter.includes(issue.severity)) return false;
|
|
18851
|
-
if (analyzerFilter.length > 0 && !matchesAnalyzerFilter(issue.analyzerId, analyzerFilter)) return false;
|
|
18852
|
-
if (nodePath !== null && !issue.nodeIds.includes(nodePath)) return false;
|
|
18853
|
-
return true;
|
|
18854
|
-
});
|
|
19550
|
+
const items = result?.items ?? [];
|
|
19551
|
+
const total = result?.total ?? 0;
|
|
18855
19552
|
return c.json(
|
|
18856
19553
|
buildListEnvelope({
|
|
18857
19554
|
kind: "issues",
|
|
18858
|
-
items
|
|
19555
|
+
items,
|
|
18859
19556
|
filters: {
|
|
18860
19557
|
severity: severityFilter.length > 0 ? severityFilter : null,
|
|
18861
19558
|
analyzerId: analyzerFilter.length > 0 ? analyzerFilter : null,
|
|
18862
19559
|
node: nodePath
|
|
18863
19560
|
},
|
|
18864
|
-
total
|
|
19561
|
+
total,
|
|
19562
|
+
page: { offset, limit },
|
|
18865
19563
|
kindRegistry: deps.kindRegistry,
|
|
18866
19564
|
contributionsRegistry: deps.contributionsRegistry
|
|
18867
19565
|
})
|
|
@@ -18904,7 +19602,7 @@ function registerLinksRoute(app, deps) {
|
|
|
18904
19602
|
}
|
|
18905
19603
|
|
|
18906
19604
|
// server/routes/nodes.ts
|
|
18907
|
-
import { HTTPException as
|
|
19605
|
+
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
18908
19606
|
|
|
18909
19607
|
// server/node-body.ts
|
|
18910
19608
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -18990,8 +19688,8 @@ function splitCsv(raw) {
|
|
|
18990
19688
|
}
|
|
18991
19689
|
|
|
18992
19690
|
// server/routes/nodes.ts
|
|
18993
|
-
var
|
|
18994
|
-
var
|
|
19691
|
+
var DEFAULT_LIMIT2 = 100;
|
|
19692
|
+
var MAX_LIMIT2 = 1e3;
|
|
18995
19693
|
var BFF_MAX_BULK_CONTRIBUTIONS = 200;
|
|
18996
19694
|
function registerNodesRoutes(app, deps) {
|
|
18997
19695
|
app.get("/api/nodes/:pathB64", async (c) => {
|
|
@@ -19001,7 +19699,7 @@ function registerNodesRoutes(app, deps) {
|
|
|
19001
19699
|
nodePath = decodeNodePath(pathB64);
|
|
19002
19700
|
} catch (err) {
|
|
19003
19701
|
if (err instanceof PathCodecError) {
|
|
19004
|
-
throw new
|
|
19702
|
+
throw new HTTPException6(404, { message: SERVER_TEXTS.pathB64Malformed });
|
|
19005
19703
|
}
|
|
19006
19704
|
throw err;
|
|
19007
19705
|
}
|
|
@@ -19033,7 +19731,7 @@ function registerNodesRoutes(app, deps) {
|
|
|
19033
19731
|
const contributions = result?.contributions ?? [];
|
|
19034
19732
|
const tags = result?.tags ?? { byAuthor: [], byUser: [] };
|
|
19035
19733
|
if (!bundle) {
|
|
19036
|
-
throw new
|
|
19734
|
+
throw new HTTPException6(404, {
|
|
19037
19735
|
message: tx(SERVER_TEXTS.nodeNotFound, { path: nodePath })
|
|
19038
19736
|
});
|
|
19039
19737
|
}
|
|
@@ -19054,8 +19752,8 @@ function registerNodesRoutes(app, deps) {
|
|
|
19054
19752
|
const params = new URL(c.req.url).searchParams;
|
|
19055
19753
|
const { query, filters } = urlParamsToExportQuery(params);
|
|
19056
19754
|
const { offset, limit } = parsePagination(c.req.query(), {
|
|
19057
|
-
limit:
|
|
19058
|
-
max:
|
|
19755
|
+
limit: DEFAULT_LIMIT2,
|
|
19756
|
+
max: MAX_LIMIT2
|
|
19059
19757
|
});
|
|
19060
19758
|
const opened = await tryWithSqlite(
|
|
19061
19759
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
@@ -19150,11 +19848,11 @@ async function groupContributionsByPath(rows) {
|
|
|
19150
19848
|
}
|
|
19151
19849
|
|
|
19152
19850
|
// server/routes/plugins.ts
|
|
19153
|
-
import { HTTPException as
|
|
19851
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
19154
19852
|
|
|
19155
19853
|
// server/util/parse-body.ts
|
|
19156
19854
|
import { Ajv2020 as Ajv20207 } from "ajv/dist/2020.js";
|
|
19157
|
-
import { HTTPException as
|
|
19855
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
19158
19856
|
function makeBodyValidator(schema, messages) {
|
|
19159
19857
|
const ajv = new Ajv20207({ strict: false, allErrors: false });
|
|
19160
19858
|
const validate = ajv.compile(schema);
|
|
@@ -19163,16 +19861,16 @@ function makeBodyValidator(schema, messages) {
|
|
|
19163
19861
|
try {
|
|
19164
19862
|
raw = await req.json();
|
|
19165
19863
|
} catch {
|
|
19166
|
-
throw new
|
|
19864
|
+
throw new HTTPException7(400, { message: messages.notJson });
|
|
19167
19865
|
}
|
|
19168
19866
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
19169
|
-
throw new
|
|
19867
|
+
throw new HTTPException7(400, { message: messages.notObject });
|
|
19170
19868
|
}
|
|
19171
19869
|
if (validate(raw)) {
|
|
19172
19870
|
return raw;
|
|
19173
19871
|
}
|
|
19174
19872
|
const message = resolveErrorMessage(validate.errors, messages);
|
|
19175
|
-
throw new
|
|
19873
|
+
throw new HTTPException7(400, { message });
|
|
19176
19874
|
};
|
|
19177
19875
|
}
|
|
19178
19876
|
function resolveErrorMessage(errors, messages) {
|
|
@@ -19272,23 +19970,23 @@ function registerPluginsRoute(app, deps) {
|
|
|
19272
19970
|
app.patch("/api/plugins/:id", async (c) => {
|
|
19273
19971
|
const id = c.req.param("id");
|
|
19274
19972
|
if (id.includes("/")) {
|
|
19275
|
-
throw new
|
|
19973
|
+
throw new HTTPException8(400, {
|
|
19276
19974
|
message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id })
|
|
19277
19975
|
});
|
|
19278
19976
|
}
|
|
19279
19977
|
const handle = findHandle(id, deps);
|
|
19280
19978
|
if (!handle) {
|
|
19281
|
-
throw new
|
|
19979
|
+
throw new HTTPException8(404, {
|
|
19282
19980
|
message: tx(SERVER_TEXTS.pluginsUnknown, { id })
|
|
19283
19981
|
});
|
|
19284
19982
|
}
|
|
19285
19983
|
if (granularityOf(handle) !== "bundle") {
|
|
19286
|
-
throw new
|
|
19984
|
+
throw new HTTPException8(400, {
|
|
19287
19985
|
message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id })
|
|
19288
19986
|
});
|
|
19289
19987
|
}
|
|
19290
19988
|
if (isPluginLocked(id)) {
|
|
19291
|
-
throw new
|
|
19989
|
+
throw new HTTPException8(403, {
|
|
19292
19990
|
message: tx(SERVER_TEXTS.pluginsLocked, { id })
|
|
19293
19991
|
});
|
|
19294
19992
|
}
|
|
@@ -19300,23 +19998,23 @@ function registerPluginsRoute(app, deps) {
|
|
|
19300
19998
|
const extensionId = c.req.param("extensionId");
|
|
19301
19999
|
const handle = findHandle(bundleId, deps);
|
|
19302
20000
|
if (!handle) {
|
|
19303
|
-
throw new
|
|
20001
|
+
throw new HTTPException8(404, {
|
|
19304
20002
|
message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
|
|
19305
20003
|
});
|
|
19306
20004
|
}
|
|
19307
20005
|
if (granularityOf(handle) !== "extension") {
|
|
19308
|
-
throw new
|
|
20006
|
+
throw new HTTPException8(400, {
|
|
19309
20007
|
message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
|
|
19310
20008
|
});
|
|
19311
20009
|
}
|
|
19312
20010
|
if (!hasExtension(handle, extensionId)) {
|
|
19313
|
-
throw new
|
|
20011
|
+
throw new HTTPException8(404, {
|
|
19314
20012
|
message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { bundleId, extensionId })
|
|
19315
20013
|
});
|
|
19316
20014
|
}
|
|
19317
20015
|
const qualified = qualifiedExtensionId(bundleId, extensionId);
|
|
19318
20016
|
if (isPluginLocked(qualified) || isPluginLocked(bundleId)) {
|
|
19319
|
-
throw new
|
|
20017
|
+
throw new HTTPException8(403, {
|
|
19320
20018
|
message: tx(SERVER_TEXTS.pluginsExtensionLocked, { bundleId, extensionId })
|
|
19321
20019
|
});
|
|
19322
20020
|
}
|
|
@@ -19328,17 +20026,12 @@ function registerPluginsRoute(app, deps) {
|
|
|
19328
20026
|
for (const change of changes) {
|
|
19329
20027
|
const failure = validateBulkChange(change, deps);
|
|
19330
20028
|
if (failure !== null) {
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
details: { id: change.id }
|
|
19338
|
-
}
|
|
19339
|
-
},
|
|
19340
|
-
failure.status
|
|
19341
|
-
);
|
|
20029
|
+
throw new BulkValidationError({
|
|
20030
|
+
status: failure.status,
|
|
20031
|
+
code: failure.code,
|
|
20032
|
+
message: failure.message,
|
|
20033
|
+
id: change.id
|
|
20034
|
+
});
|
|
19342
20035
|
}
|
|
19343
20036
|
}
|
|
19344
20037
|
return await persistBulkAndProject(c, deps, changes);
|
|
@@ -19473,16 +20166,8 @@ async function applyChangeToAdapter(adapter, configKey, enabled) {
|
|
|
19473
20166
|
}
|
|
19474
20167
|
function projectListResponse(c, deps, overrides) {
|
|
19475
20168
|
if (overrides === null) {
|
|
19476
|
-
|
|
19477
|
-
{
|
|
19478
|
-
ok: false,
|
|
19479
|
-
error: {
|
|
19480
|
-
code: "db-missing",
|
|
19481
|
-
message: tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath }),
|
|
19482
|
-
details: null
|
|
19483
|
-
}
|
|
19484
|
-
},
|
|
19485
|
-
500
|
|
20169
|
+
throw new DbMissingError(
|
|
20170
|
+
tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
|
|
19486
20171
|
);
|
|
19487
20172
|
}
|
|
19488
20173
|
const freshResolver = composeResolver2(deps, overrides);
|
|
@@ -19598,7 +20283,7 @@ function hasExtension(handle, extensionId) {
|
|
|
19598
20283
|
}
|
|
19599
20284
|
|
|
19600
20285
|
// server/routes/preferences.ts
|
|
19601
|
-
import { HTTPException as
|
|
20286
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
19602
20287
|
function registerPreferencesRoute(app, deps) {
|
|
19603
20288
|
app.get("/api/preferences", (c) => {
|
|
19604
20289
|
return c.json(buildEnvelope(deps));
|
|
@@ -19631,7 +20316,7 @@ function applyPatch(deps, body) {
|
|
|
19631
20316
|
});
|
|
19632
20317
|
wrote = true;
|
|
19633
20318
|
} catch (err) {
|
|
19634
|
-
throw new
|
|
20319
|
+
throw new HTTPException9(400, {
|
|
19635
20320
|
message: tx(SERVER_TEXTS.preferencesPersistFailed, {
|
|
19636
20321
|
message: formatErrorMessage(err)
|
|
19637
20322
|
})
|
|
@@ -19666,7 +20351,7 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
|
|
|
19666
20351
|
});
|
|
19667
20352
|
|
|
19668
20353
|
// server/routes/project-preferences.ts
|
|
19669
|
-
import { HTTPException as
|
|
20354
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
19670
20355
|
function registerProjectPreferencesRoute(app, deps) {
|
|
19671
20356
|
app.get("/api/project-preferences", (c) => {
|
|
19672
20357
|
return c.json(buildEnvelope2(deps));
|
|
@@ -19705,7 +20390,7 @@ function applyPatch2(deps, body) {
|
|
|
19705
20390
|
const exposures = writes.map((w) => projectPathExposure({ key: w.key, value: w.value, cwd, homedir: homedir4 })).filter((e) => e.expandsSurface);
|
|
19706
20391
|
if (exposures.length > 0 && body.confirm !== true) {
|
|
19707
20392
|
const exposed = exposures.flatMap((e) => e.exposedPaths);
|
|
19708
|
-
throw new
|
|
20393
|
+
throw new HTTPException10(412, {
|
|
19709
20394
|
message: tx(SERVER_TEXTS.projectPrefsConfirmRequired, {
|
|
19710
20395
|
paths: exposed.join(", ")
|
|
19711
20396
|
})
|
|
@@ -19716,7 +20401,7 @@ function applyPatch2(deps, body) {
|
|
|
19716
20401
|
writeConfigValue(w.key, w.value, { target: "project-local", cwd, homedir: homedir4 });
|
|
19717
20402
|
} catch (err) {
|
|
19718
20403
|
const status = err instanceof ConfigValidationError ? 400 : 400;
|
|
19719
|
-
throw new
|
|
20404
|
+
throw new HTTPException10(status, {
|
|
19720
20405
|
message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
|
|
19721
20406
|
key: w.key,
|
|
19722
20407
|
message: formatErrorMessage(err)
|
|
@@ -19771,7 +20456,7 @@ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
|
|
|
19771
20456
|
});
|
|
19772
20457
|
|
|
19773
20458
|
// server/routes/scan.ts
|
|
19774
|
-
import { HTTPException as
|
|
20459
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
19775
20460
|
|
|
19776
20461
|
// server/scan-mutex.ts
|
|
19777
20462
|
var inFlight = null;
|
|
@@ -19898,24 +20583,14 @@ function registerScanRoute(app, deps) {
|
|
|
19898
20583
|
}
|
|
19899
20584
|
async function runPersistedScan(c, deps) {
|
|
19900
20585
|
if (deps.options.noBuiltIns || deps.options.noPlugins) {
|
|
19901
|
-
throw new
|
|
20586
|
+
throw new HTTPException11(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
|
|
19902
20587
|
}
|
|
19903
20588
|
const dbExists = await tryWithSqlite(
|
|
19904
20589
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
19905
20590
|
async () => true
|
|
19906
20591
|
);
|
|
19907
20592
|
if (dbExists !== true) {
|
|
19908
|
-
|
|
19909
|
-
{
|
|
19910
|
-
ok: false,
|
|
19911
|
-
error: {
|
|
19912
|
-
code: "db-missing",
|
|
19913
|
-
message: SERVER_TEXTS.scanPostDbMissing,
|
|
19914
|
-
details: null
|
|
19915
|
-
}
|
|
19916
|
-
},
|
|
19917
|
-
500
|
|
19918
|
-
);
|
|
20593
|
+
throw new DbMissingError(SERVER_TEXTS.scanPostDbMissing);
|
|
19919
20594
|
}
|
|
19920
20595
|
try {
|
|
19921
20596
|
return await withScanMutex(async () => {
|
|
@@ -19937,15 +20612,15 @@ async function runPersistedScan(c, deps) {
|
|
|
19937
20612
|
emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
|
|
19938
20613
|
});
|
|
19939
20614
|
if (outcome.kind !== "ok") {
|
|
19940
|
-
throw new
|
|
19941
|
-
message: outcome.kind === "guard-trip" ?
|
|
20615
|
+
throw new HTTPException11(500, {
|
|
20616
|
+
message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
|
|
19942
20617
|
});
|
|
19943
20618
|
}
|
|
19944
20619
|
return c.json(outcome.result);
|
|
19945
20620
|
});
|
|
19946
20621
|
} catch (err) {
|
|
19947
20622
|
if (err instanceof ScanBusyError) {
|
|
19948
|
-
throw new
|
|
20623
|
+
throw new HTTPException11(409, { message: SERVER_TEXTS.scanPostBusy });
|
|
19949
20624
|
}
|
|
19950
20625
|
throw err;
|
|
19951
20626
|
}
|
|
@@ -20009,7 +20684,7 @@ function groupTagsBySource2(rows) {
|
|
|
20009
20684
|
}
|
|
20010
20685
|
async function runFreshScan(deps) {
|
|
20011
20686
|
if (deps.options.noBuiltIns || deps.options.noPlugins) {
|
|
20012
|
-
throw new
|
|
20687
|
+
throw new HTTPException11(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
|
|
20013
20688
|
}
|
|
20014
20689
|
const resolveEnabledOverride = await buildBffResolverOverride(deps);
|
|
20015
20690
|
const outcome = await runScanForCommand({
|
|
@@ -20026,7 +20701,7 @@ async function runFreshScan(deps) {
|
|
|
20026
20701
|
// M3: reuse the boot-cached pluginRuntime so a fresh scan over
|
|
20027
20702
|
// the BFF doesn't re-walk `.skill-map/plugins/` per request. A
|
|
20028
20703
|
// freshly-installed plugin needs an `sm serve` restart (the rest
|
|
20029
|
-
// of the BFF already classified against the boot snapshot
|
|
20704
|
+
// of the BFF already classified against the boot snapshot,
|
|
20030
20705
|
// discovering new plugins here would surface them in scan output
|
|
20031
20706
|
// but not in `/api/plugins` or the kindRegistry).
|
|
20032
20707
|
pluginRuntime: deps.pluginRuntime,
|
|
@@ -20038,8 +20713,8 @@ async function runFreshScan(deps) {
|
|
|
20038
20713
|
printer: bffScanRunnerPrinter
|
|
20039
20714
|
});
|
|
20040
20715
|
if (outcome.kind !== "ok") {
|
|
20041
|
-
throw new
|
|
20042
|
-
message: outcome.kind === "guard-trip" ?
|
|
20716
|
+
throw new HTTPException11(500, {
|
|
20717
|
+
message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
|
|
20043
20718
|
});
|
|
20044
20719
|
}
|
|
20045
20720
|
return outcome.result;
|
|
@@ -20073,7 +20748,7 @@ function emptyScanResult() {
|
|
|
20073
20748
|
}
|
|
20074
20749
|
|
|
20075
20750
|
// server/routes/sidecar.ts
|
|
20076
|
-
import { HTTPException as
|
|
20751
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
20077
20752
|
import { resolve as resolve33 } from "path";
|
|
20078
20753
|
var STATUS_FRESH = "fresh";
|
|
20079
20754
|
var ENVELOPE_KIND2 = "sidecar.bumped";
|
|
@@ -20110,11 +20785,11 @@ function registerSidecarRoutes(app, deps) {
|
|
|
20110
20785
|
assertContained(deps.runtimeContext.cwd, node.path);
|
|
20111
20786
|
absPath = resolve33(deps.runtimeContext.cwd, node.path);
|
|
20112
20787
|
} catch (err) {
|
|
20113
|
-
throw new
|
|
20788
|
+
throw new HTTPException12(500, { message: formatErrorMessage(err) });
|
|
20114
20789
|
}
|
|
20115
20790
|
const result = invokeBump2(node, absPath, body);
|
|
20116
20791
|
if (result.report.ok === false && result.report.reason === "fresh") {
|
|
20117
|
-
throw new
|
|
20792
|
+
throw new HTTPException12(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
|
|
20118
20793
|
}
|
|
20119
20794
|
if (result.report.ok === true && result.report.noop === true) {
|
|
20120
20795
|
const envelope2 = {
|
|
@@ -20142,7 +20817,7 @@ function registerSidecarRoutes(app, deps) {
|
|
|
20142
20817
|
}
|
|
20143
20818
|
} catch (err) {
|
|
20144
20819
|
if (err instanceof EConsentRequiredError) throw err;
|
|
20145
|
-
throw new
|
|
20820
|
+
throw new HTTPException12(500, { message: formatErrorMessage(err) });
|
|
20146
20821
|
}
|
|
20147
20822
|
if (body.confirm === true) {
|
|
20148
20823
|
deps.configService.reload();
|
|
@@ -20179,7 +20854,7 @@ async function loadNode(deps, nodePath) {
|
|
|
20179
20854
|
);
|
|
20180
20855
|
const node = persisted?.nodes.find((n) => n.path === nodePath);
|
|
20181
20856
|
if (!node) {
|
|
20182
|
-
throw new
|
|
20857
|
+
throw new HTTPException12(404, {
|
|
20183
20858
|
message: tx(SERVER_TEXTS.nodeNotFound, { path: nodePath })
|
|
20184
20859
|
});
|
|
20185
20860
|
}
|
|
@@ -20187,7 +20862,7 @@ async function loadNode(deps, nodePath) {
|
|
|
20187
20862
|
}
|
|
20188
20863
|
function invokeBump2(node, absPath, body) {
|
|
20189
20864
|
if (!bumpAction.invoke) {
|
|
20190
|
-
throw new
|
|
20865
|
+
throw new HTTPException12(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
|
|
20191
20866
|
}
|
|
20192
20867
|
const input = {};
|
|
20193
20868
|
if (body.force === true) input.force = true;
|
|
@@ -20252,7 +20927,7 @@ var PLACEHOLDER_HTML = `<!doctype html>
|
|
|
20252
20927
|
</head>
|
|
20253
20928
|
<body>
|
|
20254
20929
|
<h1>skill-map server is running</h1>
|
|
20255
|
-
<p>The UI bundle was not found. If you installed <code>@skill-map/cli</code> from npm, this is a packaging bug
|
|
20930
|
+
<p>The UI bundle was not found. If you installed <code>@skill-map/cli</code> from npm, this is a packaging bug; please report it. If you're developing in the monorepo, run <code>npm run build --workspace=ui</code> from the repo root and restart <code>sm serve</code> (or pass <code>--ui-dist <path></code> to point at a custom build).</p>
|
|
20256
20931
|
<p>The REST API is available at <code>/api/health</code>.</p>
|
|
20257
20932
|
</body>
|
|
20258
20933
|
</html>
|
|
@@ -20271,7 +20946,7 @@ var DEV_PLACEHOLDER_HTML = `<!doctype html>
|
|
|
20271
20946
|
</style>
|
|
20272
20947
|
</head>
|
|
20273
20948
|
<body>
|
|
20274
|
-
<h1>skill-map BFF in dev mode
|
|
20949
|
+
<h1>skill-map BFF in dev mode (UI disabled)</h1>
|
|
20275
20950
|
<p>Run <code>npm run ui:dev</code> in another terminal and visit <a href="http://localhost:4200/">http://localhost:4200/</a> for the Angular SPA.</p>
|
|
20276
20951
|
<p>The REST API on this port is reachable at <code>/api/health</code>.</p>
|
|
20277
20952
|
</body>
|
|
@@ -20372,6 +21047,31 @@ function attachBroadcasterRoute(app, broadcaster) {
|
|
|
20372
21047
|
}
|
|
20373
21048
|
|
|
20374
21049
|
// server/app.ts
|
|
21050
|
+
var BODY_LIMIT_BYTES = 1024 * 1024;
|
|
21051
|
+
var DbMissingError = class extends HTTPException13 {
|
|
21052
|
+
constructor(message) {
|
|
21053
|
+
super(500, { message });
|
|
21054
|
+
this.name = "DbMissingError";
|
|
21055
|
+
}
|
|
21056
|
+
};
|
|
21057
|
+
var BulkValidationError = class extends HTTPException13 {
|
|
21058
|
+
id;
|
|
21059
|
+
code;
|
|
21060
|
+
constructor(init) {
|
|
21061
|
+
super(init.status, { message: init.message });
|
|
21062
|
+
this.name = "BulkValidationError";
|
|
21063
|
+
this.id = init.id;
|
|
21064
|
+
this.code = init.code;
|
|
21065
|
+
}
|
|
21066
|
+
};
|
|
21067
|
+
var LoopbackGateError = class extends HTTPException13 {
|
|
21068
|
+
code;
|
|
21069
|
+
constructor(init) {
|
|
21070
|
+
super(403, { message: init.message });
|
|
21071
|
+
this.name = "LoopbackGateError";
|
|
21072
|
+
this.code = init.code;
|
|
21073
|
+
}
|
|
21074
|
+
};
|
|
20375
21075
|
function createApp(deps) {
|
|
20376
21076
|
const app = new Hono();
|
|
20377
21077
|
const configService = new ConfigService({
|
|
@@ -20379,6 +21079,16 @@ function createApp(deps) {
|
|
|
20379
21079
|
cwd: deps.runtimeContext.cwd,
|
|
20380
21080
|
homedir: deps.runtimeContext.homedir
|
|
20381
21081
|
});
|
|
21082
|
+
app.use("*", createLoopbackGate({ port: deps.options.port }));
|
|
21083
|
+
app.use(
|
|
21084
|
+
"/api/*",
|
|
21085
|
+
bodyLimit({
|
|
21086
|
+
maxSize: BODY_LIMIT_BYTES,
|
|
21087
|
+
onError: () => {
|
|
21088
|
+
throw new HTTPException13(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
|
|
21089
|
+
}
|
|
21090
|
+
})
|
|
21091
|
+
);
|
|
20382
21092
|
if (deps.options.devCors) {
|
|
20383
21093
|
app.use("*", async (c, next) => {
|
|
20384
21094
|
await next();
|
|
@@ -20416,16 +21126,16 @@ function createApp(deps) {
|
|
|
20416
21126
|
registerPreferencesRoute(app, routeDeps);
|
|
20417
21127
|
registerProjectPreferencesRoute(app, routeDeps);
|
|
20418
21128
|
app.all("/api/*", (c) => {
|
|
20419
|
-
throw new
|
|
20420
|
-
message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: c.req.path })
|
|
21129
|
+
throw new HTTPException13(404, {
|
|
21130
|
+
message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
|
|
20421
21131
|
});
|
|
20422
21132
|
});
|
|
20423
21133
|
attachBroadcasterRoute(app, deps.broadcaster);
|
|
20424
21134
|
app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
|
|
20425
21135
|
app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
|
|
20426
21136
|
app.notFound((c) => {
|
|
20427
|
-
throw new
|
|
20428
|
-
message: tx(SERVER_TEXTS.unknownPath, { path: c.req.path })
|
|
21137
|
+
throw new HTTPException13(404, {
|
|
21138
|
+
message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
|
|
20429
21139
|
});
|
|
20430
21140
|
});
|
|
20431
21141
|
app.onError((err, c) => {
|
|
@@ -20438,6 +21148,7 @@ function codeForStatus(status, message) {
|
|
|
20438
21148
|
if (status === 400) return "bad-query";
|
|
20439
21149
|
if (status === 403) return "locked";
|
|
20440
21150
|
if (status === 412) return "confirm-required";
|
|
21151
|
+
if (status === 413) return "payload-too-large";
|
|
20441
21152
|
if (status === 409) {
|
|
20442
21153
|
if (message.startsWith("scan-busy:")) return "scan-busy";
|
|
20443
21154
|
return "sidecar-fresh";
|
|
@@ -20445,9 +21156,42 @@ function codeForStatus(status, message) {
|
|
|
20445
21156
|
return "internal";
|
|
20446
21157
|
}
|
|
20447
21158
|
function formatError2(err, c) {
|
|
20448
|
-
if (err instanceof
|
|
21159
|
+
if (err instanceof DbMissingError) {
|
|
21160
|
+
const envelope = {
|
|
21161
|
+
ok: false,
|
|
21162
|
+
error: {
|
|
21163
|
+
code: "db-missing",
|
|
21164
|
+
message: err.message,
|
|
21165
|
+
details: null
|
|
21166
|
+
}
|
|
21167
|
+
};
|
|
21168
|
+
return c.json(envelope, 500);
|
|
21169
|
+
}
|
|
21170
|
+
if (err instanceof BulkValidationError) {
|
|
21171
|
+
const envelope = {
|
|
21172
|
+
ok: false,
|
|
21173
|
+
error: {
|
|
21174
|
+
code: err.code,
|
|
21175
|
+
message: err.message,
|
|
21176
|
+
details: { id: err.id }
|
|
21177
|
+
}
|
|
21178
|
+
};
|
|
21179
|
+
return c.json(envelope, err.status);
|
|
21180
|
+
}
|
|
21181
|
+
if (err instanceof LoopbackGateError) {
|
|
21182
|
+
const envelope = {
|
|
21183
|
+
ok: false,
|
|
21184
|
+
error: {
|
|
21185
|
+
code: err.code,
|
|
21186
|
+
message: err.message,
|
|
21187
|
+
details: null
|
|
21188
|
+
}
|
|
21189
|
+
};
|
|
21190
|
+
return c.json(envelope, 403);
|
|
21191
|
+
}
|
|
21192
|
+
if (err instanceof HTTPException13) {
|
|
20449
21193
|
const status = err.status;
|
|
20450
|
-
const
|
|
21194
|
+
const envelope = {
|
|
20451
21195
|
ok: false,
|
|
20452
21196
|
error: {
|
|
20453
21197
|
code: codeForStatus(status, err.message),
|
|
@@ -20455,10 +21199,10 @@ function formatError2(err, c) {
|
|
|
20455
21199
|
details: null
|
|
20456
21200
|
}
|
|
20457
21201
|
};
|
|
20458
|
-
return c.json(
|
|
21202
|
+
return c.json(envelope, status);
|
|
20459
21203
|
}
|
|
20460
21204
|
if (err instanceof ExportQueryError) {
|
|
20461
|
-
const
|
|
21205
|
+
const envelope = {
|
|
20462
21206
|
ok: false,
|
|
20463
21207
|
error: {
|
|
20464
21208
|
code: "bad-query",
|
|
@@ -20466,10 +21210,10 @@ function formatError2(err, c) {
|
|
|
20466
21210
|
details: null
|
|
20467
21211
|
}
|
|
20468
21212
|
};
|
|
20469
|
-
return c.json(
|
|
21213
|
+
return c.json(envelope, 400);
|
|
20470
21214
|
}
|
|
20471
21215
|
if (err instanceof EConsentRequiredError) {
|
|
20472
|
-
const
|
|
21216
|
+
const envelope = {
|
|
20473
21217
|
ok: false,
|
|
20474
21218
|
error: {
|
|
20475
21219
|
code: "confirm-required",
|
|
@@ -20477,13 +21221,20 @@ function formatError2(err, c) {
|
|
|
20477
21221
|
details: { key: err.key }
|
|
20478
21222
|
}
|
|
20479
21223
|
};
|
|
20480
|
-
return c.json(
|
|
21224
|
+
return c.json(envelope, 412);
|
|
20481
21225
|
}
|
|
21226
|
+
return formatInternalErrorFallThrough(err, c);
|
|
21227
|
+
}
|
|
21228
|
+
function formatInternalErrorFallThrough(err, c) {
|
|
21229
|
+
const detail = formatErrorMessage(err);
|
|
21230
|
+
const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : void 0;
|
|
21231
|
+
const context = stack !== void 0 ? { stack } : void 0;
|
|
21232
|
+
log.warn(`onError fall-through: ${detail}`, context);
|
|
20482
21233
|
const envelope = {
|
|
20483
21234
|
ok: false,
|
|
20484
21235
|
error: {
|
|
20485
21236
|
code: "internal",
|
|
20486
|
-
message:
|
|
21237
|
+
message: SERVER_TEXTS.internalError,
|
|
20487
21238
|
details: null
|
|
20488
21239
|
}
|
|
20489
21240
|
};
|
|
@@ -20498,7 +21249,7 @@ var READY_STATE_OPEN = 1;
|
|
|
20498
21249
|
var WsBroadcaster = class {
|
|
20499
21250
|
#clients = /* @__PURE__ */ new Set();
|
|
20500
21251
|
#shutDown = false;
|
|
20501
|
-
/** Number of currently-registered clients. Read-only
|
|
21252
|
+
/** Number of currently-registered clients. Read-only, for tests / `/api/health`. */
|
|
20502
21253
|
get clientCount() {
|
|
20503
21254
|
return this.#clients.size;
|
|
20504
21255
|
}
|
|
@@ -20520,7 +21271,7 @@ var WsBroadcaster = class {
|
|
|
20520
21271
|
}
|
|
20521
21272
|
/**
|
|
20522
21273
|
* Unregister a client. Called from the `/ws` `onClose` / `onError`
|
|
20523
|
-
* handlers and from the backpressure path. Idempotent
|
|
21274
|
+
* handlers and from the backpressure path. Idempotent, calling on a
|
|
20524
21275
|
* client that was never registered (or was already removed) is a no-op.
|
|
20525
21276
|
*/
|
|
20526
21277
|
unregister(ws) {
|
|
@@ -20563,7 +21314,7 @@ var WsBroadcaster = class {
|
|
|
20563
21314
|
}
|
|
20564
21315
|
/**
|
|
20565
21316
|
* Drain every connected socket with code 1001 ('going away') + reason
|
|
20566
|
-
* `'server shutdown'`. Idempotent
|
|
21317
|
+
* `'server shutdown'`. Idempotent, a second call after the first
|
|
20567
21318
|
* `shutdown()` is a no-op. After shutdown, `register()` immediately
|
|
20568
21319
|
* closes any new client offered.
|
|
20569
21320
|
*/
|
|
@@ -20793,7 +21544,7 @@ function validateNoUi(noUi, uiDist) {
|
|
|
20793
21544
|
|
|
20794
21545
|
// server/paths.ts
|
|
20795
21546
|
import { existsSync as existsSync25, statSync as statSync8 } from "fs";
|
|
20796
|
-
import { dirname as
|
|
21547
|
+
import { dirname as dirname17, isAbsolute as isAbsolute9, join as join16, resolve as resolve34 } from "path";
|
|
20797
21548
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
20798
21549
|
var DEFAULT_UI_REL = join16("ui", "dist", "ui", "browser");
|
|
20799
21550
|
var PACKAGE_UI_REL = "ui";
|
|
@@ -20818,7 +21569,7 @@ function isUiBundleDir(path) {
|
|
|
20818
21569
|
function resolvePackageBundledUi() {
|
|
20819
21570
|
let here;
|
|
20820
21571
|
try {
|
|
20821
|
-
here =
|
|
21572
|
+
here = dirname17(fileURLToPath5(import.meta.url));
|
|
20822
21573
|
} catch {
|
|
20823
21574
|
return null;
|
|
20824
21575
|
}
|
|
@@ -20831,7 +21582,7 @@ function resolvePackageBundledUiFrom(here) {
|
|
|
20831
21582
|
if (isUiBundleDir(candidate)) return candidate;
|
|
20832
21583
|
const distHere = join16(current, "dist", PACKAGE_UI_REL);
|
|
20833
21584
|
if (isUiBundleDir(distHere)) return distHere;
|
|
20834
|
-
const parent =
|
|
21585
|
+
const parent = dirname17(current);
|
|
20835
21586
|
if (parent === current) return null;
|
|
20836
21587
|
current = parent;
|
|
20837
21588
|
}
|
|
@@ -20842,7 +21593,7 @@ function walkUpForUi(startDir) {
|
|
|
20842
21593
|
for (let i = 0; i < 64; i++) {
|
|
20843
21594
|
const candidate = join16(current, DEFAULT_UI_REL);
|
|
20844
21595
|
if (isUiBundleDir(candidate)) return candidate;
|
|
20845
|
-
const parent =
|
|
21596
|
+
const parent = dirname17(current);
|
|
20846
21597
|
if (parent === current) return null;
|
|
20847
21598
|
current = parent;
|
|
20848
21599
|
}
|
|
@@ -21004,44 +21755,44 @@ function normalizeAddress(addr, fallbackHost, fallbackPort) {
|
|
|
21004
21755
|
// cli/i18n/serve.texts.ts
|
|
21005
21756
|
var SERVE_TEXTS = {
|
|
21006
21757
|
// The boot banner (TTY box / flat-line fallback) is rendered by
|
|
21007
|
-
// `cli/util/serve-banner.ts` rather than templated through `tx
|
|
21758
|
+
// `cli/util/serve-banner.ts` rather than templated through `tx`,
|
|
21008
21759
|
// ANSI escapes + box-drawing aren't a good fit for the flat
|
|
21009
21760
|
// `{{name}}` interpolation surface. The flat-mode strings live in
|
|
21010
21761
|
// that helper and stay byte-equivalent to the pre-banner format so
|
|
21011
21762
|
// existing pipes / redirects ('listening on <url>' scrapers) don't
|
|
21012
21763
|
// break.
|
|
21013
|
-
// Browser-open failure. Non-fatal
|
|
21764
|
+
// Browser-open failure. Non-fatal, the URL is already printed; the
|
|
21014
21765
|
// user can open it manually.
|
|
21015
21766
|
openFailed: "sm serve: could not auto-open browser ({{message}}). Visit {{url}} manually.\n",
|
|
21016
21767
|
// Bind failure (port in use, EACCES, etc.) → ExitCode.Error.
|
|
21017
|
-
bindFailed: "sm serve: failed to bind {{host}}:{{port}}
|
|
21018
|
-
// Flag-validation failures
|
|
21768
|
+
bindFailed: "sm serve: failed to bind {{host}}:{{port}}: {{message}}\n",
|
|
21769
|
+
// Flag-validation failures, ExitCode.Error.
|
|
21019
21770
|
hostDevCorsRejected: "sm serve: --dev-cors requires a loopback --host (got {{host}}). Refusing per Decision #119.\n",
|
|
21020
21771
|
portOutOfRange: "sm serve: --port must be an integer in [0, 65535] (got {{value}}).\n",
|
|
21021
21772
|
portInvalid: "sm serve: --port must be a non-negative integer (got {{value}}).\n",
|
|
21022
21773
|
scopeInvalid: 'sm serve: --scope must be "project" or "global" (got {{value}}).\n',
|
|
21023
|
-
// Watcher option failures
|
|
21774
|
+
// Watcher option failures, ExitCode.Error.
|
|
21024
21775
|
watcherRequiresPipeline: "sm serve: --no-built-ins is incompatible with the watcher (would persist empty scans on every batch). Pass --no-watcher to opt out, or drop --no-built-ins.\n",
|
|
21025
21776
|
watcherDebounceInvalid: "sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n",
|
|
21026
|
-
// --no-ui flag-validation failures
|
|
21777
|
+
// --no-ui flag-validation failures, ExitCode.Error.
|
|
21027
21778
|
noUiConflictsUiDist: "sm serve: --no-ui and --ui-dist {{path}} are mutually exclusive (drop one).\n",
|
|
21028
|
-
// --no-ui + --open is harmless but worth flagging
|
|
21779
|
+
// --no-ui + --open is harmless but worth flagging, non-fatal stderr note.
|
|
21029
21780
|
noUiOpenWarning: "sm serve: warning: --open with --no-ui will open the placeholder, not the live UI; pass --no-open if running alongside `ui:dev`.\n",
|
|
21030
|
-
// Generic operational error
|
|
21781
|
+
// Generic operational error, surfaced when the server itself throws
|
|
21031
21782
|
// before the listener binds (e.g. UI bundle missing under explicit
|
|
21032
21783
|
// --ui-dist).
|
|
21033
|
-
startupFailed: "sm serve: startup failed
|
|
21784
|
+
startupFailed: "sm serve: startup failed: {{message}}\n",
|
|
21034
21785
|
// DB-not-found (--db <path> doesn't exist) → ExitCode.NotFound.
|
|
21035
21786
|
dbNotFound: "sm serve: --db {{path}} does not exist.\n",
|
|
21036
21787
|
// --ui-dist override points at a missing / non-bundle directory.
|
|
21037
21788
|
uiDistInvalid: "--ui-dist {{path}} does not exist or is not a directory containing index.html",
|
|
21038
|
-
// Shutdown trace
|
|
21789
|
+
// Shutdown trace, printed once the listener has closed.
|
|
21039
21790
|
shutdown: "sm serve: shutdown complete.\n"
|
|
21040
21791
|
};
|
|
21041
21792
|
|
|
21042
21793
|
// cli/util/serve-banner.ts
|
|
21043
21794
|
import { homedir as homedir2 } from "os";
|
|
21044
|
-
import { relative as
|
|
21795
|
+
import { relative as relative7, isAbsolute as isAbsolute10 } from "path";
|
|
21045
21796
|
var ESC2 = {
|
|
21046
21797
|
reset: "\x1B[0m",
|
|
21047
21798
|
bold: "\x1B[1m",
|
|
@@ -21166,7 +21917,7 @@ function resolveAnsi(colorEnabled) {
|
|
|
21166
21917
|
function formatDbPath(dbPath, cwd) {
|
|
21167
21918
|
const safe = sanitizeForTerminal(dbPath);
|
|
21168
21919
|
if (!isAbsolute10(safe)) return safe;
|
|
21169
|
-
const rel =
|
|
21920
|
+
const rel = relative7(cwd, safe);
|
|
21170
21921
|
if (rel === "" || rel.startsWith("..") || isAbsolute10(rel)) {
|
|
21171
21922
|
return safe;
|
|
21172
21923
|
}
|
|
@@ -21190,10 +21941,10 @@ var ServeCommand = class extends SmCommand {
|
|
|
21190
21941
|
details: `
|
|
21191
21942
|
Boots the skill-map Web UI's backing server. One Node process
|
|
21192
21943
|
serves the Angular SPA, the REST API under /api/*, and the
|
|
21193
|
-
WebSocket at /ws
|
|
21944
|
+
WebSocket at /ws (single-port mandate, no proxy).
|
|
21194
21945
|
|
|
21195
21946
|
Default port is 4242, default host is 127.0.0.1. The server boots
|
|
21196
|
-
even when the project DB is missing
|
|
21947
|
+
even when the project DB is missing; /api/health reports
|
|
21197
21948
|
'db: missing' so the SPA renders an empty-state CTA instead of
|
|
21198
21949
|
failing the connection.
|
|
21199
21950
|
|
|
@@ -21227,7 +21978,7 @@ var ServeCommand = class extends SmCommand {
|
|
|
21227
21978
|
noPlugins = Option31.Boolean("--no-plugins", false, {
|
|
21228
21979
|
description: "Skip drop-in plugin discovery."
|
|
21229
21980
|
});
|
|
21230
|
-
// `Option.Boolean('--open', true)
|
|
21981
|
+
// `Option.Boolean('--open', true)`, Clipanion's parser auto-derives
|
|
21231
21982
|
// the `--no-open` inverse for every boolean flag (search for
|
|
21232
21983
|
// `--no-${name.slice(2)}` in clipanion's core), so the explicit
|
|
21233
21984
|
// `--no-open` descriptor must NOT be declared here or the parser sees
|
|
@@ -21253,12 +22004,12 @@ var ServeCommand = class extends SmCommand {
|
|
|
21253
22004
|
});
|
|
21254
22005
|
// `--watcher-debounce-ms` is undocumented sugar for advanced users
|
|
21255
22006
|
// who want to tighten / relax the watcher's batching window without
|
|
21256
|
-
// editing settings.json. Hidden flag
|
|
22007
|
+
// editing settings.json. Hidden flag, the Usage block omits it.
|
|
21257
22008
|
watcherDebounceMs = Option31.String("--watcher-debounce-ms", { required: false, hidden: true });
|
|
21258
|
-
// Long-running daemon
|
|
22009
|
+
// Long-running daemon, `done in <…>` after a graceful shutdown is
|
|
21259
22010
|
// noise. Mirrors `sm watch`'s opt-out.
|
|
21260
22011
|
emitElapsed = false;
|
|
21261
|
-
// CLI orchestrator with multi-flag handling
|
|
22012
|
+
// CLI orchestrator with multi-flag handling, each `if (this.flag)`
|
|
21262
22013
|
// branch is one cyclomatic point. Splitting per branch scatters the
|
|
21263
22014
|
// validation away from the flag it gates. Per AGENTS.md §Linting
|
|
21264
22015
|
// category 1 ("CLI orchestrators with multi-flag handling").
|
|
@@ -21447,6 +22198,15 @@ function waitForShutdown() {
|
|
|
21447
22198
|
}
|
|
21448
22199
|
function tryOpenBrowser(url, stderr) {
|
|
21449
22200
|
try {
|
|
22201
|
+
if (!validateBrowserUrl(url)) {
|
|
22202
|
+
stderr.write(
|
|
22203
|
+
tx(SERVE_TEXTS.openFailed, {
|
|
22204
|
+
message: sanitizeForTerminal("refused to launch browser: unsafe URL"),
|
|
22205
|
+
url: sanitizeForTerminal(url)
|
|
22206
|
+
})
|
|
22207
|
+
);
|
|
22208
|
+
return;
|
|
22209
|
+
}
|
|
21450
22210
|
const platform = process.platform;
|
|
21451
22211
|
let command;
|
|
21452
22212
|
let args2;
|
|
@@ -21455,7 +22215,7 @@ function tryOpenBrowser(url, stderr) {
|
|
|
21455
22215
|
args2 = [url];
|
|
21456
22216
|
} else if (platform === "win32") {
|
|
21457
22217
|
command = "cmd";
|
|
21458
|
-
args2 = ["/c", "start",
|
|
22218
|
+
args2 = ["/c", "start", "", url];
|
|
21459
22219
|
} else {
|
|
21460
22220
|
command = "xdg-open";
|
|
21461
22221
|
args2 = [url];
|
|
@@ -21792,7 +22552,7 @@ function rankConfidenceForGrouping(c) {
|
|
|
21792
22552
|
}
|
|
21793
22553
|
|
|
21794
22554
|
// cli/commands/sidecar.ts
|
|
21795
|
-
import { existsSync as existsSync27, unlinkSync as
|
|
22555
|
+
import { existsSync as existsSync27, unlinkSync as unlinkSync2 } from "fs";
|
|
21796
22556
|
import { resolve as resolve35 } from "path";
|
|
21797
22557
|
import { Command as Command35, Option as Option33 } from "clipanion";
|
|
21798
22558
|
|
|
@@ -21827,12 +22587,12 @@ var SIDECAR_TEXTS = {
|
|
|
21827
22587
|
/**
|
|
21828
22588
|
* Pre-prompt context shown before the interactive `confirm()` so the
|
|
21829
22589
|
* operator sees what they are about to opt into. `.skill-map/settings.local.json`
|
|
21830
|
-
* is gitignored
|
|
22590
|
+
* is gitignored, the choice is saved per-checkout, never travels via the repo.
|
|
21831
22591
|
*/
|
|
21832
22592
|
consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
|
|
21833
22593
|
consentAborted: "{{glyph}} sm sidecar: aborted by user. No .sm sidecar files were written.\n",
|
|
21834
22594
|
consentRequiredNonTty: "{{glyph}} sm sidecar: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
|
|
21835
|
-
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json
|
|
22595
|
+
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
|
|
21836
22596
|
};
|
|
21837
22597
|
|
|
21838
22598
|
// cli/commands/sidecar.ts
|
|
@@ -21876,11 +22636,11 @@ var SidecarRefreshCommand = class extends SmCommand {
|
|
|
21876
22636
|
Useful when the user knows a body change is editorial-only and
|
|
21877
22637
|
doesn't want to spend a \`annotations.version\` increment.
|
|
21878
22638
|
Distinct from \`sm refresh\` (the enrichment-layer verb at Step
|
|
21879
|
-
A.8)
|
|
22639
|
+
A.8); different storage, different concept.
|
|
21880
22640
|
|
|
21881
22641
|
Refuses if the node has no sidecar (run \`sm sidecar annotate\`
|
|
21882
22642
|
first, or \`sm bump\` to create one through the Action). No-ops
|
|
21883
|
-
on a fresh node
|
|
22643
|
+
on a fresh node, there's nothing to refresh.
|
|
21884
22644
|
`,
|
|
21885
22645
|
examples: [
|
|
21886
22646
|
["Refresh a node's sidecar hashes", "$0 sidecar refresh .claude/agents/architect.md"]
|
|
@@ -21912,7 +22672,7 @@ var SidecarRefreshCommand = class extends SmCommand {
|
|
|
21912
22672
|
() => this.#runOnce(ctx, dbPath, okGlyph, errGlyph, ansi)
|
|
21913
22673
|
);
|
|
21914
22674
|
}
|
|
21915
|
-
// Inner dispatch
|
|
22675
|
+
// Inner dispatch, single attempt. The outer `run()` wraps every
|
|
21916
22676
|
// call in `runWithSidecarConsent` so an `EConsentRequiredError`
|
|
21917
22677
|
// surfaces as an interactive prompt (TTY) or a directed exit
|
|
21918
22678
|
// (non-TTY).
|
|
@@ -22020,7 +22780,7 @@ var SidecarPruneCommand = class extends SmCommand {
|
|
|
22020
22780
|
convention). \`--yes\` (alias \`--force\`) bypasses the prompt
|
|
22021
22781
|
for non-interactive use (CI, scripts, the pre-commit hook).
|
|
22022
22782
|
|
|
22023
|
-
Different domain from \`sm orphans
|
|
22783
|
+
Different domain from \`sm orphans\`: that verb operates on the
|
|
22024
22784
|
node graph (rename heuristic). This one operates on the
|
|
22025
22785
|
filesystem layer.
|
|
22026
22786
|
`,
|
|
@@ -22034,7 +22794,7 @@ var SidecarPruneCommand = class extends SmCommand {
|
|
|
22034
22794
|
yes = Option33.Boolean("--yes,--force", false, {
|
|
22035
22795
|
description: "Skip the interactive confirmation prompt. Required for non-interactive callers (CI, pre-commit hooks)."
|
|
22036
22796
|
});
|
|
22037
|
-
// Complexity is from per-orphan handling
|
|
22797
|
+
// Complexity is from per-orphan handling, empty-set / dry-run /
|
|
22038
22798
|
// delete / error capture / json-vs-pretty branches each contributing
|
|
22039
22799
|
// a guard. The unlink loop itself is linear.
|
|
22040
22800
|
// eslint-disable-next-line complexity
|
|
@@ -22080,7 +22840,7 @@ var SidecarPruneCommand = class extends SmCommand {
|
|
|
22080
22840
|
continue;
|
|
22081
22841
|
}
|
|
22082
22842
|
try {
|
|
22083
|
-
|
|
22843
|
+
unlinkSync2(orphan.sidecarPath);
|
|
22084
22844
|
items.push({
|
|
22085
22845
|
sidecarPath: orphan.sidecarPath,
|
|
22086
22846
|
expectedMd: orphan.expectedMdPath,
|
|
@@ -22160,7 +22920,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
22160
22920
|
block. After editing, run \`sm bump <node>\` to commit the
|
|
22161
22921
|
version through the Action.
|
|
22162
22922
|
|
|
22163
|
-
Refuses if the file already exists
|
|
22923
|
+
Refuses if the file already exists; pass \`--force\` to
|
|
22164
22924
|
overwrite. Per Decision A4 the \`--from-frontmatter\` migration
|
|
22165
22925
|
helper is deferred (no released consumer demands it).
|
|
22166
22926
|
`,
|
|
@@ -22195,6 +22955,10 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
22195
22955
|
() => this.#runOnce(ctx, dbPath, errGlyph, ansi)
|
|
22196
22956
|
);
|
|
22197
22957
|
}
|
|
22958
|
+
// CLI orchestrator: argument-validation guards + dry-run branch +
|
|
22959
|
+
// interactive confirm + collect/delete loop. Each branch is one
|
|
22960
|
+
// cyclomatic point; splitting would scatter the validations away
|
|
22961
|
+
// from the flag they gate. Per `context/lint.md` category 1.
|
|
22198
22962
|
// eslint-disable-next-line complexity
|
|
22199
22963
|
async #runOnce(ctx, dbPath, errGlyph, ansi) {
|
|
22200
22964
|
const persisted = await tryWithSqlite(
|
|
@@ -22245,7 +23009,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
22245
23009
|
}
|
|
22246
23010
|
if (existsSync27(sidecarAbsPath) && this.force === true) {
|
|
22247
23011
|
try {
|
|
22248
|
-
|
|
23012
|
+
unlinkSync2(sidecarAbsPath);
|
|
22249
23013
|
} catch (err) {
|
|
22250
23014
|
this.printer.error(
|
|
22251
23015
|
tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
|
|
@@ -22476,13 +23240,13 @@ var STUB_COMMANDS = [
|
|
|
22476
23240
|
// cli/commands/tutorial.ts
|
|
22477
23241
|
import { existsSync as existsSync28, readFileSync as readFileSync17 } from "fs";
|
|
22478
23242
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
22479
|
-
import { dirname as
|
|
23243
|
+
import { dirname as dirname18, join as join17, resolve as resolve36 } from "path";
|
|
22480
23244
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
22481
23245
|
import { Command as Command37, Option as Option35 } from "clipanion";
|
|
22482
23246
|
|
|
22483
23247
|
// cli/i18n/tutorial.texts.ts
|
|
22484
23248
|
var TUTORIAL_TEXTS = {
|
|
22485
|
-
// Success
|
|
23249
|
+
// Success, written to stdout after `<cwd>/sm-tutorial.md` is created.
|
|
22486
23250
|
// Multi-line layout: the two trigger phrases (English / Spanish) are
|
|
22487
23251
|
// indented and labelled so they're the most visible part of the
|
|
22488
23252
|
// output. The reminder above them surfaces the SKILL's language
|
|
@@ -22491,13 +23255,13 @@ var TUTORIAL_TEXTS = {
|
|
|
22491
23255
|
/**
|
|
22492
23256
|
* Success body. `glyph` is wrapped green at the call site; `cwd`
|
|
22493
23257
|
* renders relative to the user's cwd when it sits underneath. The
|
|
22494
|
-
* `English` / `Español` labels print dim
|
|
23258
|
+
* `English` / `Español` labels print dim, the eye lands on the
|
|
22495
23259
|
* trigger phrases the user is going to copy / paste.
|
|
22496
23260
|
*/
|
|
22497
23261
|
written: " {{glyph}} sm-tutorial.md created at {{cwd}}\n\n Open Claude Code in this directory. Your first message sets\n the tutorial language for the rest of the session:\n\n {{enLabel}} run @sm-tutorial.md\n {{esLabel}} ejecut\xE1 @sm-tutorial.md\n",
|
|
22498
23262
|
writtenLabelEn: "English",
|
|
22499
23263
|
writtenLabelEs: "Espa\xF1ol",
|
|
22500
|
-
// Refusal
|
|
23264
|
+
// Refusal, `sm-tutorial.md` already exists and `--force` was not set.
|
|
22501
23265
|
// Goes to stderr, exit code 2 (operational error per spec § Exit codes).
|
|
22502
23266
|
// Mirrors the success body shape: glyph + headline, then a dim hint
|
|
22503
23267
|
// line spelling the fix.
|
|
@@ -22519,7 +23283,7 @@ var TutorialCommand = class extends SmCommand {
|
|
|
22519
23283
|
details: `
|
|
22520
23284
|
Drops the canonical SKILL.md content as ./sm-tutorial.md so a tester
|
|
22521
23285
|
can open Claude Code in the cwd and load the file as a skill by
|
|
22522
|
-
typing "ejecut\xE1 @sm-tutorial.md". Top-level only
|
|
23286
|
+
typing "ejecut\xE1 @sm-tutorial.md". Top-level only; no subdirectory
|
|
22523
23287
|
is created.
|
|
22524
23288
|
|
|
22525
23289
|
Does NOT require an initialized .skill-map/ project. Refuses to
|
|
@@ -22603,7 +23367,7 @@ function loadBundledTutorialText() {
|
|
|
22603
23367
|
return cachedTutorial;
|
|
22604
23368
|
}
|
|
22605
23369
|
function readTutorialFromDisk() {
|
|
22606
|
-
const here =
|
|
23370
|
+
const here = dirname18(fileURLToPath6(import.meta.url));
|
|
22607
23371
|
const candidates = [
|
|
22608
23372
|
// dev: src/cli/commands/ → repo-root .claude/skills/sm-tutorial/SKILL.md
|
|
22609
23373
|
resolve36(here, "../../../.claude/skills/sm-tutorial/SKILL.md"),
|
|
@@ -22640,7 +23404,7 @@ var VersionCommand = class extends SmCommand {
|
|
|
22640
23404
|
category: "Introspection",
|
|
22641
23405
|
description: "Print the CLI / kernel / spec / runtime / db-schema version matrix."
|
|
22642
23406
|
});
|
|
22643
|
-
// Informational verb
|
|
23407
|
+
// Informational verb, no `done in <…>` line; the version matrix is
|
|
22644
23408
|
// the entire output.
|
|
22645
23409
|
emitElapsed = false;
|
|
22646
23410
|
async run() {
|
|
@@ -22692,10 +23456,10 @@ async function resolveDbSchemaVersion() {
|
|
|
22692
23456
|
{ databasePath: dbPath, autoBackup: false },
|
|
22693
23457
|
async (port) => port.migrations.currentSchemaVersion()
|
|
22694
23458
|
);
|
|
22695
|
-
if (v === null || v === void 0) return "
|
|
23459
|
+
if (v === null || v === void 0) return "-";
|
|
22696
23460
|
return String(v);
|
|
22697
23461
|
} catch {
|
|
22698
|
-
return "
|
|
23462
|
+
return "-";
|
|
22699
23463
|
}
|
|
22700
23464
|
}
|
|
22701
23465
|
|