@skill-map/cli 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/tutorial/sm-tutorial.md +93 -14
- package/dist/cli.js +1332 -339
- package/dist/cli.js.map +1 -1
- package/dist/index.js +300 -238
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +91 -11
- package/dist/kernel/index.js +300 -238
- package/dist/kernel/index.js.map +1 -1
- package/dist/migrations/001_initial.sql +13 -0
- package/dist/ui/chunk-25AWRVIC.js +965 -0
- package/dist/ui/chunk-6FTVUS57.js +123 -0
- package/dist/ui/{chunk-LQTUSDHD.js → chunk-GXRWH2VL.js} +1 -1
- package/dist/ui/chunk-MF2M6GYF.js +1 -0
- package/dist/ui/{chunk-2W62S3FU.js → chunk-MPMBTIUR.js} +2 -2
- package/dist/ui/chunk-N366HMME.js +1 -0
- package/dist/ui/{chunk-QICH7GU2.js → chunk-OPPQMCMQ.js} +1 -1
- package/dist/ui/chunk-V3SZQETX.js +61 -0
- package/dist/ui/{chunk-HJSRMZTK.js → chunk-VVOEPDQD.js} +1 -1
- package/dist/ui/{chunk-DLT5AP43.js → chunk-W2EFGI3J.js} +1 -1
- package/dist/ui/chunk-W62WVNU4.js +251 -0
- package/dist/ui/index.html +2 -10
- package/dist/ui/main-NIYE2VFS.js +2 -0
- package/dist/ui/media/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/ui/media/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/ui/media/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/ui/media/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/ui/styles-M2FETVAG.css +1 -0
- package/migrations/001_initial.sql +13 -0
- package/package.json +2 -2
- package/dist/ui/chunk-C7QWBAYP.js +0 -247
- package/dist/ui/chunk-HOBQ4G4O.js +0 -125
- package/dist/ui/chunk-IBUV6OG2.js +0 -1
- package/dist/ui/chunk-UJRROL5X.js +0 -1
- package/dist/ui/chunk-VLNLJAUB.js +0 -61
- package/dist/ui/chunk-W3JLG7BI.js +0 -965
- package/dist/ui/main-QHE47BCM.js +0 -1
- package/dist/ui/styles-VJ5Q6D2X.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -614,20 +614,23 @@ var geminiProvider = {
|
|
|
614
614
|
// registry entries (they ship later under the Gemini bundle), but
|
|
615
615
|
// the qualified form is the contract.
|
|
616
616
|
//
|
|
617
|
-
// UI presentation:
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
617
|
+
// UI presentation: kind visuals are normalised across Providers — every
|
|
618
|
+
// Provider that contributes `agent` declares the same color + icon as
|
|
619
|
+
// Claude, every Provider that contributes `skill` declares the same
|
|
620
|
+
// color + icon as Claude, etc. The declaration STAYS per-Provider (the
|
|
621
|
+
// shape allows divergence the day a Provider wants its own identity for
|
|
622
|
+
// a kind), but today the values mirror Claude so the visual vocabulary
|
|
623
|
+
// is uniform regardless of where a node was sourced from.
|
|
621
624
|
kinds: {
|
|
622
625
|
agent: {
|
|
623
626
|
schema: "./schemas/agent.schema.json",
|
|
624
627
|
schemaJson: agent_schema_default2,
|
|
625
628
|
defaultRefreshAction: "gemini/summarize-agent",
|
|
626
629
|
ui: {
|
|
627
|
-
label: "
|
|
628
|
-
color: "#
|
|
629
|
-
colorDark: "#
|
|
630
|
-
icon: { kind: "pi", id: "pi-
|
|
630
|
+
label: "Agents",
|
|
631
|
+
color: "#3b82f6",
|
|
632
|
+
colorDark: "#60a5fa",
|
|
633
|
+
icon: { kind: "pi", id: "pi-user" }
|
|
631
634
|
}
|
|
632
635
|
},
|
|
633
636
|
skill: {
|
|
@@ -635,9 +638,9 @@ var geminiProvider = {
|
|
|
635
638
|
schemaJson: skill_schema_default2,
|
|
636
639
|
defaultRefreshAction: "gemini/summarize-skill",
|
|
637
640
|
ui: {
|
|
638
|
-
label: "
|
|
639
|
-
color: "#
|
|
640
|
-
colorDark: "#
|
|
641
|
+
label: "Skills",
|
|
642
|
+
color: "#10b981",
|
|
643
|
+
colorDark: "#34d399",
|
|
641
644
|
icon: { kind: "pi", id: "pi-bolt" }
|
|
642
645
|
}
|
|
643
646
|
}
|
|
@@ -685,12 +688,9 @@ var agentSkillsProvider = {
|
|
|
685
688
|
schemaJson: skill_schema_default3,
|
|
686
689
|
defaultRefreshAction: "agent-skills/summarize-skill",
|
|
687
690
|
ui: {
|
|
688
|
-
label: "
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
// "vendor-agnostic open-standard" at a glance.
|
|
692
|
-
color: "#64748b",
|
|
693
|
-
colorDark: "#94a3b8",
|
|
691
|
+
label: "Skills",
|
|
692
|
+
color: "#10b981",
|
|
693
|
+
colorDark: "#34d399",
|
|
694
694
|
icon: { kind: "pi", id: "pi-bolt" }
|
|
695
695
|
}
|
|
696
696
|
}
|
|
@@ -766,7 +766,7 @@ var annotationsExtractor = {
|
|
|
766
766
|
pluginId: "core",
|
|
767
767
|
kind: "extractor",
|
|
768
768
|
version: "1.0.0",
|
|
769
|
-
description: "
|
|
769
|
+
description: "Turns the `supersedes`, `requires`, `related`, `conflictsWith`, and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
|
|
770
770
|
stability: "stable",
|
|
771
771
|
emitsLinkKinds: ["supersedes", "references"],
|
|
772
772
|
defaultConfidence: "high",
|
|
@@ -843,25 +843,11 @@ var slashExtractor = {
|
|
|
843
843
|
pluginId: "core",
|
|
844
844
|
kind: "extractor",
|
|
845
845
|
version: "1.0.0",
|
|
846
|
-
description: "Detects
|
|
846
|
+
description: "Detects `/command` invocations in a node's body and turns each one into an arrow (edge) between nodes in the graph.",
|
|
847
847
|
stability: "stable",
|
|
848
848
|
emitsLinkKinds: ["invokes"],
|
|
849
849
|
defaultConfidence: "medium",
|
|
850
850
|
scope: "body",
|
|
851
|
-
/**
|
|
852
|
-
* View contribution — surface the distinct-invocation count as a
|
|
853
|
-
* counter chip in `card.footer.left.counter`, alongside the
|
|
854
|
-
* at-directive and markdown-link counters. `emitWhenEmpty: false`
|
|
855
|
-
* keeps unrelated nodes free of a `/ 0` decoration.
|
|
856
|
-
*/
|
|
857
|
-
viewContributions: {
|
|
858
|
-
count: {
|
|
859
|
-
slot: "card.footer.left.counter",
|
|
860
|
-
icon: "/",
|
|
861
|
-
label: "commands",
|
|
862
|
-
emitWhenEmpty: false
|
|
863
|
-
}
|
|
864
|
-
},
|
|
865
851
|
extract(ctx) {
|
|
866
852
|
const seen = /* @__PURE__ */ new Set();
|
|
867
853
|
for (const match of ctx.body.matchAll(SLASH_RE)) {
|
|
@@ -881,9 +867,6 @@ var slashExtractor = {
|
|
|
881
867
|
}
|
|
882
868
|
});
|
|
883
869
|
}
|
|
884
|
-
if (seen.size > 0) {
|
|
885
|
-
ctx.emitContribution("count", { value: seen.size });
|
|
886
|
-
}
|
|
887
870
|
}
|
|
888
871
|
};
|
|
889
872
|
|
|
@@ -895,25 +878,11 @@ var atDirectiveExtractor = {
|
|
|
895
878
|
pluginId: "core",
|
|
896
879
|
kind: "extractor",
|
|
897
880
|
version: "1.0.0",
|
|
898
|
-
description: "Detects
|
|
881
|
+
description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow (edge) between nodes in the graph.",
|
|
899
882
|
stability: "stable",
|
|
900
883
|
emitsLinkKinds: ["mentions"],
|
|
901
884
|
defaultConfidence: "medium",
|
|
902
885
|
scope: "body",
|
|
903
|
-
/**
|
|
904
|
-
* View contribution — surface the distinct-mention count as a
|
|
905
|
-
* counter chip in `card.footer.left.counter`. `emitWhenEmpty: false`
|
|
906
|
-
* keeps unrelated nodes (no @-handles in the body) free of a `@ 0`
|
|
907
|
-
* decoration.
|
|
908
|
-
*/
|
|
909
|
-
viewContributions: {
|
|
910
|
-
count: {
|
|
911
|
-
slot: "card.footer.left.counter",
|
|
912
|
-
icon: "@",
|
|
913
|
-
label: "mentions",
|
|
914
|
-
emitWhenEmpty: false
|
|
915
|
-
}
|
|
916
|
-
},
|
|
917
886
|
extract(ctx) {
|
|
918
887
|
const seen = /* @__PURE__ */ new Set();
|
|
919
888
|
for (const match of ctx.body.matchAll(AT_RE)) {
|
|
@@ -933,9 +902,6 @@ var atDirectiveExtractor = {
|
|
|
933
902
|
}
|
|
934
903
|
});
|
|
935
904
|
}
|
|
936
|
-
if (seen.size > 0) {
|
|
937
|
-
ctx.emitContribution("count", { value: seen.size });
|
|
938
|
-
}
|
|
939
905
|
}
|
|
940
906
|
};
|
|
941
907
|
|
|
@@ -948,7 +914,7 @@ var externalUrlCounterExtractor = {
|
|
|
948
914
|
pluginId: "core",
|
|
949
915
|
kind: "extractor",
|
|
950
916
|
version: "1.0.0",
|
|
951
|
-
description: "Counts distinct external http
|
|
917
|
+
description: "Counts the distinct external URLs (`http://` / `https://`) in a node's body and shows the total as a chip on the card.",
|
|
952
918
|
stability: "stable",
|
|
953
919
|
emitsLinkKinds: ["references"],
|
|
954
920
|
defaultConfidence: "low",
|
|
@@ -957,14 +923,21 @@ var externalUrlCounterExtractor = {
|
|
|
957
923
|
* Phase 6 / View contribution system — surface the distinct-URL
|
|
958
924
|
* count as a card-footer-right chip. The chip is silent when zero
|
|
959
925
|
* URLs were emitted (`emitWhenEmpty: false`), so unrelated nodes
|
|
960
|
-
* do not gain a
|
|
926
|
+
* do not gain a `link 0` decoration. The counter rides on exactly
|
|
961
927
|
* the same data the orchestrator was already going to count — there
|
|
962
928
|
* is no second pass.
|
|
929
|
+
*
|
|
930
|
+
* Icon is the PrimeIcons `pi-link` glyph (declared as the bare
|
|
931
|
+
* `'link'` per `IconString` rules in `view-slots.schema.json`).
|
|
932
|
+
* Mirrors the look of the legacy hardcoded `pi pi-link` chip in
|
|
933
|
+
* `node-card.html` it is poised to replace — same icon font, same
|
|
934
|
+
* sizing inherited from the footer `.sm-gnode__stat` styles cloned
|
|
935
|
+
* by the `NodeCounter` renderer.
|
|
963
936
|
*/
|
|
964
937
|
viewContributions: {
|
|
965
938
|
count: {
|
|
966
939
|
slot: "card.footer.right",
|
|
967
|
-
icon: "
|
|
940
|
+
icon: "pi-link",
|
|
968
941
|
label: "urls",
|
|
969
942
|
emitWhenEmpty: false
|
|
970
943
|
}
|
|
@@ -1040,25 +1013,11 @@ var markdownLinkExtractor = {
|
|
|
1040
1013
|
pluginId: "core",
|
|
1041
1014
|
kind: "extractor",
|
|
1042
1015
|
version: "1.0.0",
|
|
1043
|
-
description: "Detects [text](path)
|
|
1016
|
+
description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow (edge) between nodes in the graph.",
|
|
1044
1017
|
stability: "stable",
|
|
1045
1018
|
emitsLinkKinds: ["references"],
|
|
1046
1019
|
defaultConfidence: "high",
|
|
1047
1020
|
scope: "body",
|
|
1048
|
-
/**
|
|
1049
|
-
* View contribution — surface the distinct-link count as a counter
|
|
1050
|
-
* chip in `card.footer.left.counter`, alongside the at-directive
|
|
1051
|
-
* (`@`) and slash (`/`) counters. `emitWhenEmpty: false` keeps
|
|
1052
|
-
* unrelated nodes (no markdown links) free of a `📎 0` decoration.
|
|
1053
|
-
*/
|
|
1054
|
-
viewContributions: {
|
|
1055
|
-
count: {
|
|
1056
|
-
slot: "card.footer.left.counter",
|
|
1057
|
-
icon: "\u{1F4CE}",
|
|
1058
|
-
label: "links",
|
|
1059
|
-
emitWhenEmpty: false
|
|
1060
|
-
}
|
|
1061
|
-
},
|
|
1062
1021
|
extract(ctx) {
|
|
1063
1022
|
const seen = /* @__PURE__ */ new Set();
|
|
1064
1023
|
const lineStarts = computeLineStarts2(ctx.body);
|
|
@@ -1084,9 +1043,6 @@ var markdownLinkExtractor = {
|
|
|
1084
1043
|
};
|
|
1085
1044
|
ctx.emitLink(link2);
|
|
1086
1045
|
}
|
|
1087
|
-
if (seen.size > 0) {
|
|
1088
|
-
ctx.emitContribution("count", { value: seen.size });
|
|
1089
|
-
}
|
|
1090
1046
|
}
|
|
1091
1047
|
};
|
|
1092
1048
|
function resolveTarget(sourceDir, raw) {
|
|
@@ -1117,6 +1073,107 @@ function lineFor2(lineStarts, offset) {
|
|
|
1117
1073
|
return lo + 1;
|
|
1118
1074
|
}
|
|
1119
1075
|
|
|
1076
|
+
// built-in-plugins/extractors/stability/index.ts
|
|
1077
|
+
var ID6 = "stability";
|
|
1078
|
+
var EXPERIMENTAL_TOOLTIP = "Experimental \u2014 API may change";
|
|
1079
|
+
var DEPRECATED_TOOLTIP = "Deprecated \u2014 avoid in new code";
|
|
1080
|
+
var stabilityExtractor = {
|
|
1081
|
+
id: ID6,
|
|
1082
|
+
pluginId: "core",
|
|
1083
|
+
kind: "extractor",
|
|
1084
|
+
version: "1.0.0",
|
|
1085
|
+
description: "Shows an icon chip on the card footer when the node is marked `stability: experimental` or `stability: deprecated` (read from the sidecar `annotations:` block, with legacy `metadata:` frontmatter as fallback).",
|
|
1086
|
+
stability: "stable",
|
|
1087
|
+
emitsLinkKinds: [],
|
|
1088
|
+
defaultConfidence: "high",
|
|
1089
|
+
scope: "frontmatter",
|
|
1090
|
+
viewContributions: {
|
|
1091
|
+
experimental: {
|
|
1092
|
+
slot: "card.footer.right",
|
|
1093
|
+
icon: "fa-solid fa-flask",
|
|
1094
|
+
label: "experimental",
|
|
1095
|
+
emitWhenEmpty: false
|
|
1096
|
+
},
|
|
1097
|
+
deprecated: {
|
|
1098
|
+
slot: "card.footer.right",
|
|
1099
|
+
icon: "pi-ban",
|
|
1100
|
+
label: "deprecated",
|
|
1101
|
+
emitWhenEmpty: false
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
extract(ctx) {
|
|
1105
|
+
const stability = readStability(ctx);
|
|
1106
|
+
if (stability === "experimental") {
|
|
1107
|
+
ctx.emitContribution("experimental", {
|
|
1108
|
+
value: 0,
|
|
1109
|
+
tooltip: EXPERIMENTAL_TOOLTIP
|
|
1110
|
+
});
|
|
1111
|
+
} else if (stability === "deprecated") {
|
|
1112
|
+
ctx.emitContribution("deprecated", {
|
|
1113
|
+
value: 0,
|
|
1114
|
+
tooltip: DEPRECATED_TOOLTIP,
|
|
1115
|
+
severity: "warn"
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
function readStability(ctx) {
|
|
1121
|
+
const ann = ctx.node.sidecar?.annotations;
|
|
1122
|
+
const fromAnn = ann?.["stability"];
|
|
1123
|
+
if (isStability(fromAnn)) return fromAnn;
|
|
1124
|
+
const meta = ctx.frontmatter["metadata"];
|
|
1125
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
1126
|
+
const legacy = meta["stability"];
|
|
1127
|
+
if (isStability(legacy)) return legacy;
|
|
1128
|
+
}
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
function isStability(value) {
|
|
1132
|
+
return value === "experimental" || value === "deprecated" || value === "stable";
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// built-in-plugins/extractors/tools-count/index.ts
|
|
1136
|
+
var ID7 = "tools-count";
|
|
1137
|
+
var TOOLTIP_MAX = 255;
|
|
1138
|
+
var toolsCountExtractor = {
|
|
1139
|
+
id: ID7,
|
|
1140
|
+
pluginId: "core",
|
|
1141
|
+
kind: "extractor",
|
|
1142
|
+
version: "1.0.0",
|
|
1143
|
+
description: "Counts the tools an agent declares in its frontmatter and shows the total as a wrench chip on the agent card.",
|
|
1144
|
+
stability: "stable",
|
|
1145
|
+
emitsLinkKinds: [],
|
|
1146
|
+
defaultConfidence: "high",
|
|
1147
|
+
scope: "frontmatter",
|
|
1148
|
+
applicableKinds: ["agent"],
|
|
1149
|
+
viewContributions: {
|
|
1150
|
+
count: {
|
|
1151
|
+
slot: "card.footer.left",
|
|
1152
|
+
icon: "pi-wrench",
|
|
1153
|
+
label: "tools",
|
|
1154
|
+
emitWhenEmpty: false
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
extract(ctx) {
|
|
1158
|
+
const raw = ctx.frontmatter["tools"];
|
|
1159
|
+
if (!Array.isArray(raw)) return;
|
|
1160
|
+
const names = [];
|
|
1161
|
+
for (const t of raw) {
|
|
1162
|
+
if (typeof t === "string" && t.length > 0) names.push(t);
|
|
1163
|
+
}
|
|
1164
|
+
if (names.length === 0) return;
|
|
1165
|
+
ctx.emitContribution("count", {
|
|
1166
|
+
value: names.length,
|
|
1167
|
+
tooltip: buildTooltip(names)
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
function buildTooltip(names) {
|
|
1172
|
+
const joined = names.join(" \xB7 ");
|
|
1173
|
+
if (joined.length <= TOOLTIP_MAX) return joined;
|
|
1174
|
+
return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1120
1177
|
// built-in-plugins/i18n/trigger-collision.texts.ts
|
|
1121
1178
|
var TRIGGER_COLLISION_TEXTS = {
|
|
1122
1179
|
/**
|
|
@@ -1143,19 +1200,19 @@ var TRIGGER_COLLISION_TEXTS = {
|
|
|
1143
1200
|
};
|
|
1144
1201
|
|
|
1145
1202
|
// built-in-plugins/analyzers/trigger-collision/index.ts
|
|
1146
|
-
var
|
|
1203
|
+
var ID8 = "trigger-collision";
|
|
1147
1204
|
var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
|
|
1148
1205
|
"command",
|
|
1149
1206
|
"skill",
|
|
1150
1207
|
"agent"
|
|
1151
1208
|
]);
|
|
1152
1209
|
var triggerCollisionAnalyzer = {
|
|
1153
|
-
id:
|
|
1210
|
+
id: ID8,
|
|
1154
1211
|
pluginId: "core",
|
|
1155
1212
|
kind: "analyzer",
|
|
1156
1213
|
mode: "deterministic",
|
|
1157
1214
|
version: "1.0.0",
|
|
1158
|
-
description: "Flags
|
|
1215
|
+
description: "Flags when two or more nodes claim the same `/command` or `@agent` name \u2014 either by their `name` field or by how they are invoked elsewhere.",
|
|
1159
1216
|
stability: "stable",
|
|
1160
1217
|
// Two claim-collection passes (advertisement + invocation) feeding
|
|
1161
1218
|
// the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
|
|
@@ -1250,7 +1307,7 @@ function analyzeTriggerBucket(normalized, claims) {
|
|
|
1250
1307
|
part: parts[0]
|
|
1251
1308
|
});
|
|
1252
1309
|
return {
|
|
1253
|
-
analyzerId:
|
|
1310
|
+
analyzerId: ID8,
|
|
1254
1311
|
severity: "error",
|
|
1255
1312
|
nodeIds,
|
|
1256
1313
|
message,
|
|
@@ -1268,35 +1325,79 @@ import { resolve } from "path";
|
|
|
1268
1325
|
// built-in-plugins/i18n/broken-ref.texts.ts
|
|
1269
1326
|
var BROKEN_REF_TEXTS = {
|
|
1270
1327
|
/** `Broken <kind> reference from <source> → <target>` */
|
|
1271
|
-
message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}"
|
|
1328
|
+
message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
|
|
1329
|
+
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
1330
|
+
// plural keeps the count grammar correct without a sub-template.
|
|
1331
|
+
alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
|
|
1332
|
+
alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
|
|
1272
1333
|
};
|
|
1273
1334
|
|
|
1274
1335
|
// built-in-plugins/analyzers/broken-ref/index.ts
|
|
1275
|
-
var
|
|
1336
|
+
var ID9 = "broken-ref";
|
|
1276
1337
|
var brokenRefAnalyzer = {
|
|
1277
|
-
id:
|
|
1338
|
+
id: ID9,
|
|
1278
1339
|
pluginId: "core",
|
|
1279
1340
|
kind: "analyzer",
|
|
1280
1341
|
version: "1.0.0",
|
|
1281
|
-
description: "Flags
|
|
1342
|
+
description: "Flags arrows pointing at a node that is not part of the current scan (broken link).",
|
|
1282
1343
|
stability: "stable",
|
|
1283
1344
|
mode: "deterministic",
|
|
1345
|
+
viewContributions: {
|
|
1346
|
+
// Corner badge on the graph card; count omitted when there is a
|
|
1347
|
+
// single broken ref (avoids a noisy "icon + 1" chip).
|
|
1348
|
+
alert: {
|
|
1349
|
+
slot: "graph.node.alert",
|
|
1350
|
+
icon: "fa-solid fa-circle-xmark",
|
|
1351
|
+
emitWhenEmpty: false
|
|
1352
|
+
},
|
|
1353
|
+
// Footer chip on the card. `_counter` shape — `value` always shows,
|
|
1354
|
+
// so the operator sees "how many" at a glance. Renders OUTLINED
|
|
1355
|
+
// (`fa-regular`) so the corner alert (filled, attention-grabbing)
|
|
1356
|
+
// and the footer chip (quieter, paired with a number) read as two
|
|
1357
|
+
// beats of the same signal rather than two identical glyphs.
|
|
1358
|
+
chip: {
|
|
1359
|
+
slot: "card.footer.right",
|
|
1360
|
+
icon: "fa-regular fa-circle-xmark",
|
|
1361
|
+
emitWhenEmpty: false
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
// The resolver, the reference-paths escape hatch, the per-source
|
|
1365
|
+
// aggregation, and the dual-slot emit (with single/plural tooltip and
|
|
1366
|
+
// optional count) all live in one flow because they share the per-link
|
|
1367
|
+
// loop. Splitting them would re-walk `ctx.links` three times.
|
|
1368
|
+
// eslint-disable-next-line complexity
|
|
1284
1369
|
evaluate(ctx) {
|
|
1285
1370
|
const byPath3 = new Set(ctx.nodes.map((n) => n.path));
|
|
1286
1371
|
const byNormalizedName = indexByNormalizedName(ctx.nodes);
|
|
1287
1372
|
const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
|
|
1288
1373
|
const issues = [];
|
|
1374
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
1289
1375
|
for (const link2 of ctx.links) {
|
|
1290
1376
|
if (isResolved(link2, byPath3, byNormalizedName)) continue;
|
|
1291
1377
|
if (refIndex && resolvesViaReferencePaths(link2, refIndex)) continue;
|
|
1292
1378
|
issues.push(buildIssue(link2));
|
|
1379
|
+
perNode.set(link2.source, (perNode.get(link2.source) ?? 0) + 1);
|
|
1380
|
+
}
|
|
1381
|
+
for (const [nodePath, count] of perNode) {
|
|
1382
|
+
const tooltip = count === 1 ? BROKEN_REF_TEXTS.alertTooltipSingle : tx(BROKEN_REF_TEXTS.alertTooltipMany, { count });
|
|
1383
|
+
const capped = Math.min(count, 99);
|
|
1384
|
+
ctx.emitContribution(nodePath, "alert", {
|
|
1385
|
+
icon: "fa-solid fa-circle-xmark",
|
|
1386
|
+
severity: "danger",
|
|
1387
|
+
tooltip
|
|
1388
|
+
});
|
|
1389
|
+
ctx.emitContribution(nodePath, "chip", {
|
|
1390
|
+
value: capped,
|
|
1391
|
+
severity: "danger",
|
|
1392
|
+
tooltip
|
|
1393
|
+
});
|
|
1293
1394
|
}
|
|
1294
1395
|
return issues;
|
|
1295
1396
|
}
|
|
1296
1397
|
};
|
|
1297
1398
|
function buildIssue(link2) {
|
|
1298
1399
|
return {
|
|
1299
|
-
analyzerId:
|
|
1400
|
+
analyzerId: ID9,
|
|
1300
1401
|
severity: "warn",
|
|
1301
1402
|
nodeIds: [link2.source],
|
|
1302
1403
|
message: tx(BROKEN_REF_TEXTS.message, {
|
|
@@ -1350,13 +1451,13 @@ var SUPERSEDED_TEXTS = {
|
|
|
1350
1451
|
};
|
|
1351
1452
|
|
|
1352
1453
|
// built-in-plugins/analyzers/superseded/index.ts
|
|
1353
|
-
var
|
|
1454
|
+
var ID10 = "superseded";
|
|
1354
1455
|
var supersededAnalyzer = {
|
|
1355
|
-
id:
|
|
1456
|
+
id: ID10,
|
|
1356
1457
|
pluginId: "core",
|
|
1357
1458
|
kind: "analyzer",
|
|
1358
1459
|
version: "1.0.0",
|
|
1359
|
-
description: "
|
|
1460
|
+
description: "Marks nodes that have been replaced by a newer one (the sidecar declares `supersededBy`).",
|
|
1360
1461
|
stability: "stable",
|
|
1361
1462
|
mode: "deterministic",
|
|
1362
1463
|
evaluate(ctx) {
|
|
@@ -1365,7 +1466,7 @@ var supersededAnalyzer = {
|
|
|
1365
1466
|
const supersededBy = pickSupersededBy(node);
|
|
1366
1467
|
if (supersededBy === null) continue;
|
|
1367
1468
|
issues.push({
|
|
1368
|
-
analyzerId:
|
|
1469
|
+
analyzerId: ID10,
|
|
1369
1470
|
severity: "info",
|
|
1370
1471
|
nodeIds: [node.path],
|
|
1371
1472
|
message: tx(SUPERSEDED_TEXTS.message, {
|
|
@@ -1395,13 +1496,13 @@ var LINK_CONFLICT_TEXTS = {
|
|
|
1395
1496
|
};
|
|
1396
1497
|
|
|
1397
1498
|
// built-in-plugins/analyzers/link-conflict/index.ts
|
|
1398
|
-
var
|
|
1499
|
+
var ID11 = "link-conflict";
|
|
1399
1500
|
var linkConflictAnalyzer = {
|
|
1400
|
-
id:
|
|
1501
|
+
id: ID11,
|
|
1401
1502
|
pluginId: "core",
|
|
1402
1503
|
kind: "analyzer",
|
|
1403
1504
|
version: "1.0.0",
|
|
1404
|
-
description:
|
|
1505
|
+
description: 'Flags when two extractors disagree on the meaning of the same arrow (e.g. one says "references", the other says "invokes").',
|
|
1405
1506
|
stability: "stable",
|
|
1406
1507
|
mode: "deterministic",
|
|
1407
1508
|
// Bucket links by (source, target), then per-bucket detect distinct
|
|
@@ -1446,7 +1547,7 @@ var linkConflictAnalyzer = {
|
|
|
1446
1547
|
const [source, target] = key.split("\0");
|
|
1447
1548
|
const kindList = variants.map((v) => v.kind).join(" / ");
|
|
1448
1549
|
issues.push({
|
|
1449
|
-
analyzerId:
|
|
1550
|
+
analyzerId: ID11,
|
|
1450
1551
|
severity: "warn",
|
|
1451
1552
|
nodeIds: [source, target],
|
|
1452
1553
|
message: tx(LINK_CONFLICT_TEXTS.message, {
|
|
@@ -1478,19 +1579,44 @@ var ANNOTATION_STALE_TEXTS = {
|
|
|
1478
1579
|
/** frontmatter changed since last bump */
|
|
1479
1580
|
frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
|
|
1480
1581
|
/** both body and frontmatter changed */
|
|
1481
|
-
bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump)."
|
|
1582
|
+
bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
|
|
1583
|
+
// Tooltips for the `card.footer.right` clock chip emitted alongside
|
|
1584
|
+
// the issue. Lists only the drifted face(s) — in-sync faces are
|
|
1585
|
+
// omitted so the operator immediately sees what's modified without
|
|
1586
|
+
// scanning prose. No `{{path}}` placeholder — the chip already sits
|
|
1587
|
+
// on the affected node. The hint `sm bump <path>` keeps `<path>` as
|
|
1588
|
+
// a literal placeholder the operator substitutes.
|
|
1589
|
+
bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
|
|
1590
|
+
frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
|
|
1591
|
+
bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
|
|
1482
1592
|
};
|
|
1483
1593
|
|
|
1484
1594
|
// built-in-plugins/analyzers/annotation-stale/index.ts
|
|
1485
|
-
var
|
|
1595
|
+
var ID12 = "annotation-stale";
|
|
1486
1596
|
var annotationStaleAnalyzer = {
|
|
1487
|
-
id:
|
|
1597
|
+
id: ID12,
|
|
1488
1598
|
pluginId: "core",
|
|
1489
1599
|
kind: "analyzer",
|
|
1490
1600
|
version: "1.0.0",
|
|
1491
|
-
description: "
|
|
1601
|
+
description: "Marks nodes whose `.sm` sidecar is out of date \u2014 the `.md` content changed since the last sidecar bump. Surfaces an Issue (panel) plus a `pi-clock` chip in the card footer.",
|
|
1492
1602
|
stability: "stable",
|
|
1493
1603
|
mode: "deterministic",
|
|
1604
|
+
viewContributions: {
|
|
1605
|
+
// A `pi-clock` chip in the footer-right cluster so the operator
|
|
1606
|
+
// spots drift in the list / inspector view (and on the graph card
|
|
1607
|
+
// body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
|
|
1608
|
+
// renderer treats it as icon-only — drift severity is binary at
|
|
1609
|
+
// this surface (the tooltip carries the per-face detail body /
|
|
1610
|
+
// frontmatter / both). The corner badge on `graph.node.alert` was
|
|
1611
|
+
// dropped on purpose: a tooltip on the footer chip is enough, and
|
|
1612
|
+
// the corner badge stacked on top of broken-ref / unknown-field
|
|
1613
|
+
// alerts produced visual noise.
|
|
1614
|
+
staleIcon: {
|
|
1615
|
+
slot: "card.footer.right",
|
|
1616
|
+
icon: "pi-clock",
|
|
1617
|
+
emitWhenEmpty: true
|
|
1618
|
+
}
|
|
1619
|
+
},
|
|
1494
1620
|
evaluate(ctx) {
|
|
1495
1621
|
const issues = [];
|
|
1496
1622
|
for (const node of ctx.nodes) {
|
|
@@ -1499,16 +1625,31 @@ var annotationStaleAnalyzer = {
|
|
|
1499
1625
|
if (status === "fresh") continue;
|
|
1500
1626
|
const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
|
|
1501
1627
|
issues.push({
|
|
1502
|
-
analyzerId:
|
|
1628
|
+
analyzerId: ID12,
|
|
1503
1629
|
severity: "warn",
|
|
1504
1630
|
nodeIds: [node.path],
|
|
1505
1631
|
message,
|
|
1506
1632
|
data: { status }
|
|
1507
1633
|
});
|
|
1634
|
+
ctx.emitContribution(node.path, "staleIcon", {
|
|
1635
|
+
value: 0,
|
|
1636
|
+
severity: "warn",
|
|
1637
|
+
tooltip: tooltipFor(status)
|
|
1638
|
+
});
|
|
1508
1639
|
}
|
|
1509
1640
|
return issues;
|
|
1510
1641
|
}
|
|
1511
1642
|
};
|
|
1643
|
+
function tooltipFor(status) {
|
|
1644
|
+
switch (status) {
|
|
1645
|
+
case "stale-body":
|
|
1646
|
+
return ANNOTATION_STALE_TEXTS.bodyTooltip;
|
|
1647
|
+
case "stale-frontmatter":
|
|
1648
|
+
return ANNOTATION_STALE_TEXTS.frontmatterTooltip;
|
|
1649
|
+
case "stale-both":
|
|
1650
|
+
return ANNOTATION_STALE_TEXTS.bothTooltip;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1512
1653
|
|
|
1513
1654
|
// built-in-plugins/i18n/annotation-orphan.texts.ts
|
|
1514
1655
|
var ANNOTATION_ORPHAN_TEXTS = {
|
|
@@ -1517,13 +1658,13 @@ var ANNOTATION_ORPHAN_TEXTS = {
|
|
|
1517
1658
|
};
|
|
1518
1659
|
|
|
1519
1660
|
// built-in-plugins/analyzers/annotation-orphan/index.ts
|
|
1520
|
-
var
|
|
1661
|
+
var ID13 = "annotation-orphan";
|
|
1521
1662
|
var annotationOrphanAnalyzer = {
|
|
1522
|
-
id:
|
|
1663
|
+
id: ID13,
|
|
1523
1664
|
pluginId: "core",
|
|
1524
1665
|
kind: "analyzer",
|
|
1525
1666
|
version: "1.0.0",
|
|
1526
|
-
description: "Flags
|
|
1667
|
+
description: "Flags `.sm` sidecars whose matching `.md` file no longer exists on disk.",
|
|
1527
1668
|
stability: "stable",
|
|
1528
1669
|
mode: "deterministic",
|
|
1529
1670
|
evaluate(ctx) {
|
|
@@ -1533,7 +1674,7 @@ var annotationOrphanAnalyzer = {
|
|
|
1533
1674
|
for (const orphan of orphans) {
|
|
1534
1675
|
const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
|
|
1535
1676
|
issues.push({
|
|
1536
|
-
analyzerId:
|
|
1677
|
+
analyzerId: ID13,
|
|
1537
1678
|
severity: "warn",
|
|
1538
1679
|
nodeIds: [expectedMdRelative],
|
|
1539
1680
|
message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
|
|
@@ -1560,13 +1701,13 @@ var JOB_ORPHAN_FILE_TEXTS = {
|
|
|
1560
1701
|
};
|
|
1561
1702
|
|
|
1562
1703
|
// built-in-plugins/analyzers/job-orphan-file/index.ts
|
|
1563
|
-
var
|
|
1704
|
+
var ID14 = "job-orphan-file";
|
|
1564
1705
|
var jobOrphanFileAnalyzer = {
|
|
1565
|
-
id:
|
|
1706
|
+
id: ID14,
|
|
1566
1707
|
pluginId: "core",
|
|
1567
1708
|
kind: "analyzer",
|
|
1568
1709
|
version: "1.0.0",
|
|
1569
|
-
description: "Flags
|
|
1710
|
+
description: "Flags leftover job result files in `.skill-map/jobs/` that no live job references. Cleanup via `sm job prune --orphan-files`.",
|
|
1570
1711
|
stability: "stable",
|
|
1571
1712
|
mode: "deterministic",
|
|
1572
1713
|
evaluate(ctx) {
|
|
@@ -1575,7 +1716,7 @@ var jobOrphanFileAnalyzer = {
|
|
|
1575
1716
|
const issues = [];
|
|
1576
1717
|
for (const filePath of orphans) {
|
|
1577
1718
|
issues.push({
|
|
1578
|
-
analyzerId:
|
|
1719
|
+
analyzerId: ID14,
|
|
1579
1720
|
severity: "warn",
|
|
1580
1721
|
nodeIds: [filePath],
|
|
1581
1722
|
message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
|
|
@@ -1606,20 +1747,48 @@ var UNKNOWN_FIELD_TEXTS = {
|
|
|
1606
1747
|
/** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
|
|
1607
1748
|
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}' \u2014 not a reserved block, not a registered plugin namespace, not a registered root contribution.",
|
|
1608
1749
|
/** Value under a registered plugin namespace fails the contributed schema. */
|
|
1609
|
-
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}' \u2014 {{errors}}."
|
|
1750
|
+
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}' \u2014 {{errors}}.",
|
|
1751
|
+
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
1752
|
+
// plural keeps the count grammar correct without a sub-template.
|
|
1753
|
+
alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
|
|
1754
|
+
alertTooltipMany: "This node has {{count}} unknown fields in its sidecar. Open the inspector for details."
|
|
1610
1755
|
};
|
|
1611
1756
|
|
|
1612
1757
|
// built-in-plugins/analyzers/unknown-field/index.ts
|
|
1613
|
-
var
|
|
1758
|
+
var ID15 = "unknown-field";
|
|
1614
1759
|
var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
|
|
1615
1760
|
var unknownFieldAnalyzer = {
|
|
1616
|
-
id:
|
|
1761
|
+
id: ID15,
|
|
1617
1762
|
pluginId: "core",
|
|
1618
1763
|
kind: "analyzer",
|
|
1619
1764
|
version: "1.0.0",
|
|
1620
|
-
description: "
|
|
1765
|
+
description: "Catches typos and unrecognized keys inside `.sm` sidecars, including plugin-contributed annotation fields that fail their own schema.",
|
|
1621
1766
|
stability: "stable",
|
|
1622
1767
|
mode: "deterministic",
|
|
1768
|
+
viewContributions: {
|
|
1769
|
+
// Corner badge on the graph card; count omitted when there is a
|
|
1770
|
+
// single unknown field (avoids a noisy "icon + 1" chip).
|
|
1771
|
+
alert: {
|
|
1772
|
+
slot: "graph.node.alert",
|
|
1773
|
+
// Filled warning triangle on the corner — matches the broken-ref
|
|
1774
|
+
// alert's "attention-grabbing solid" pattern; the footer chip
|
|
1775
|
+
// below stays outlined for the quieter counter pairing.
|
|
1776
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
1777
|
+
emitWhenEmpty: false
|
|
1778
|
+
},
|
|
1779
|
+
// Footer chip on the card — `_counter` shape but rendered icon-only
|
|
1780
|
+
// (the analyzer emits `value: 0` so NodeCounter hides the number
|
|
1781
|
+
// and only the glyph shows). PrimeIcons `pi-question-circle` so the
|
|
1782
|
+
// visual weight matches `annotation-stale`'s `pi-clock` chip
|
|
1783
|
+
// sitting next to it on the same footer row. `emitWhenEmpty: true`
|
|
1784
|
+
// is required: with `value: 0` the slot treats the payload as
|
|
1785
|
+
// empty, so the manifest has to opt in to keep the emission.
|
|
1786
|
+
chip: {
|
|
1787
|
+
slot: "card.footer.right",
|
|
1788
|
+
icon: "pi-question-circle",
|
|
1789
|
+
emitWhenEmpty: true
|
|
1790
|
+
}
|
|
1791
|
+
},
|
|
1623
1792
|
// eslint-disable-next-line complexity
|
|
1624
1793
|
evaluate(ctx) {
|
|
1625
1794
|
const sidecarRoots = ctx.sidecarRoots;
|
|
@@ -1630,6 +1799,10 @@ var unknownFieldAnalyzer = {
|
|
|
1630
1799
|
const rootKeys = indexRootContributions(contributions);
|
|
1631
1800
|
const knownPluginIds = collectPluginIds(contributions);
|
|
1632
1801
|
const issues = [];
|
|
1802
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
1803
|
+
const bump2 = (nodePath) => {
|
|
1804
|
+
perNode.set(nodePath, (perNode.get(nodePath) ?? 0) + 1);
|
|
1805
|
+
};
|
|
1633
1806
|
for (const node of ctx.nodes) {
|
|
1634
1807
|
const root = sidecarRoots.get(node.path);
|
|
1635
1808
|
if (!root) continue;
|
|
@@ -1638,7 +1811,7 @@ var unknownFieldAnalyzer = {
|
|
|
1638
1811
|
for (const key of Object.keys(annotations)) {
|
|
1639
1812
|
if (!knownAnnotationKeys.has(key)) {
|
|
1640
1813
|
issues.push({
|
|
1641
|
-
analyzerId:
|
|
1814
|
+
analyzerId: ID15,
|
|
1642
1815
|
severity: "warn",
|
|
1643
1816
|
nodeIds: [node.path],
|
|
1644
1817
|
message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
|
|
@@ -1647,6 +1820,7 @@ var unknownFieldAnalyzer = {
|
|
|
1647
1820
|
}),
|
|
1648
1821
|
data: { surface: "annotations", key }
|
|
1649
1822
|
});
|
|
1823
|
+
bump2(node.path);
|
|
1650
1824
|
}
|
|
1651
1825
|
}
|
|
1652
1826
|
}
|
|
@@ -1664,7 +1838,7 @@ var unknownFieldAnalyzer = {
|
|
|
1664
1838
|
if (validator(value)) continue;
|
|
1665
1839
|
const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
|
|
1666
1840
|
issues.push({
|
|
1667
|
-
analyzerId:
|
|
1841
|
+
analyzerId: ID15,
|
|
1668
1842
|
severity: "warn",
|
|
1669
1843
|
nodeIds: [node.path],
|
|
1670
1844
|
message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
|
|
@@ -1675,11 +1849,12 @@ var unknownFieldAnalyzer = {
|
|
|
1675
1849
|
}),
|
|
1676
1850
|
data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
|
|
1677
1851
|
});
|
|
1852
|
+
bump2(node.path);
|
|
1678
1853
|
}
|
|
1679
1854
|
continue;
|
|
1680
1855
|
}
|
|
1681
1856
|
issues.push({
|
|
1682
|
-
analyzerId:
|
|
1857
|
+
analyzerId: ID15,
|
|
1683
1858
|
severity: "warn",
|
|
1684
1859
|
nodeIds: [node.path],
|
|
1685
1860
|
message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
|
|
@@ -1688,8 +1863,22 @@ var unknownFieldAnalyzer = {
|
|
|
1688
1863
|
}),
|
|
1689
1864
|
data: { surface: "root", key }
|
|
1690
1865
|
});
|
|
1866
|
+
bump2(node.path);
|
|
1691
1867
|
}
|
|
1692
1868
|
}
|
|
1869
|
+
for (const [nodePath, count] of perNode) {
|
|
1870
|
+
const tooltip = count === 1 ? UNKNOWN_FIELD_TEXTS.alertTooltipSingle : tx(UNKNOWN_FIELD_TEXTS.alertTooltipMany, { count });
|
|
1871
|
+
ctx.emitContribution(nodePath, "alert", {
|
|
1872
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
1873
|
+
severity: "warn",
|
|
1874
|
+
tooltip
|
|
1875
|
+
});
|
|
1876
|
+
ctx.emitContribution(nodePath, "chip", {
|
|
1877
|
+
value: 0,
|
|
1878
|
+
severity: "warn",
|
|
1879
|
+
tooltip
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1693
1882
|
return issues;
|
|
1694
1883
|
}
|
|
1695
1884
|
};
|
|
@@ -1737,11 +1926,11 @@ function collectPluginIds(contributions) {
|
|
|
1737
1926
|
}
|
|
1738
1927
|
|
|
1739
1928
|
// built-in-plugins/analyzers/unknown-slot/index.ts
|
|
1740
|
-
var
|
|
1929
|
+
var ID16 = "unknown-slot";
|
|
1741
1930
|
var KNOWN_SLOTS = /* @__PURE__ */ new Set([
|
|
1742
1931
|
"card.title.right",
|
|
1743
1932
|
"card.subtitle.left",
|
|
1744
|
-
"card.footer.left
|
|
1933
|
+
"card.footer.left",
|
|
1745
1934
|
"card.footer.right",
|
|
1746
1935
|
"graph.node.alert",
|
|
1747
1936
|
"inspector.header.badge.counter",
|
|
@@ -1752,14 +1941,14 @@ var KNOWN_SLOTS = /* @__PURE__ */ new Set([
|
|
|
1752
1941
|
"inspector.body.panel.key-values",
|
|
1753
1942
|
"inspector.body.panel.link-list",
|
|
1754
1943
|
"inspector.body.panel.markdown",
|
|
1755
|
-
"topbar.
|
|
1944
|
+
"topbar.nav.start"
|
|
1756
1945
|
]);
|
|
1757
1946
|
var unknownSlotAnalyzer = {
|
|
1758
|
-
id:
|
|
1947
|
+
id: ID16,
|
|
1759
1948
|
pluginId: "core",
|
|
1760
1949
|
kind: "analyzer",
|
|
1761
1950
|
version: "1.0.0",
|
|
1762
|
-
description: "Warns
|
|
1951
|
+
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).",
|
|
1763
1952
|
stability: "stable",
|
|
1764
1953
|
mode: "deterministic",
|
|
1765
1954
|
evaluate(ctx) {
|
|
@@ -1770,7 +1959,7 @@ var unknownSlotAnalyzer = {
|
|
|
1770
1959
|
if (KNOWN_SLOTS.has(c.slot)) continue;
|
|
1771
1960
|
const qualified = `${c.pluginId}/${c.extensionId}/${c.contributionId}`;
|
|
1772
1961
|
issues.push({
|
|
1773
|
-
analyzerId:
|
|
1962
|
+
analyzerId: ID16,
|
|
1774
1963
|
severity: "warn",
|
|
1775
1964
|
nodeIds: [],
|
|
1776
1965
|
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\`).`,
|
|
@@ -1787,13 +1976,13 @@ var unknownSlotAnalyzer = {
|
|
|
1787
1976
|
};
|
|
1788
1977
|
|
|
1789
1978
|
// built-in-plugins/analyzers/contribution-orphan/index.ts
|
|
1790
|
-
var
|
|
1979
|
+
var ID17 = "contribution-orphan";
|
|
1791
1980
|
var contributionOrphanAnalyzer = {
|
|
1792
|
-
id:
|
|
1981
|
+
id: ID17,
|
|
1793
1982
|
pluginId: "core",
|
|
1794
1983
|
kind: "analyzer",
|
|
1795
1984
|
version: "1.0.0",
|
|
1796
|
-
description: "Warns when
|
|
1985
|
+
description: "Warns when a plugin's per-node chips reference a node that was renamed or deleted in the latest scan.",
|
|
1797
1986
|
stability: "experimental",
|
|
1798
1987
|
mode: "deterministic",
|
|
1799
1988
|
evaluate(_ctx) {
|
|
@@ -1829,14 +2018,14 @@ var ASCII_FORMATTER_TEXTS = {
|
|
|
1829
2018
|
};
|
|
1830
2019
|
|
|
1831
2020
|
// built-in-plugins/formatters/ascii/index.ts
|
|
1832
|
-
var
|
|
2021
|
+
var ID18 = "ascii";
|
|
1833
2022
|
var KIND_ORDER = ["agent", "command", "skill", "markdown"];
|
|
1834
2023
|
var asciiFormatter = {
|
|
1835
|
-
id:
|
|
2024
|
+
id: ID18,
|
|
1836
2025
|
pluginId: "core",
|
|
1837
2026
|
kind: "formatter",
|
|
1838
2027
|
version: "1.0.0",
|
|
1839
|
-
description: "Plain-text
|
|
2028
|
+
description: "Plain-text dump of the scan grouped by kind, then arrows, then issues. Used by `sm scan --format=ascii`.",
|
|
1840
2029
|
stability: "stable",
|
|
1841
2030
|
formatId: "ascii",
|
|
1842
2031
|
// ASCII tree formatter — header + per-kind sections + per-issue
|
|
@@ -2005,7 +2194,7 @@ function buildSchemaValidators() {
|
|
|
2005
2194
|
const KNOWN_SLOTS2 = /* @__PURE__ */ new Set([
|
|
2006
2195
|
"card.title.right",
|
|
2007
2196
|
"card.subtitle.left",
|
|
2008
|
-
"card.footer.left
|
|
2197
|
+
"card.footer.left",
|
|
2009
2198
|
"card.footer.right",
|
|
2010
2199
|
"graph.node.alert",
|
|
2011
2200
|
"inspector.header.badge.counter",
|
|
@@ -2016,7 +2205,7 @@ function buildSchemaValidators() {
|
|
|
2016
2205
|
"inspector.body.panel.key-values",
|
|
2017
2206
|
"inspector.body.panel.link-list",
|
|
2018
2207
|
"inspector.body.panel.markdown",
|
|
2019
|
-
"topbar.
|
|
2208
|
+
"topbar.nav.start"
|
|
2020
2209
|
]);
|
|
2021
2210
|
function getContributionValidator(slot) {
|
|
2022
2211
|
if (!KNOWN_SLOTS2.has(slot)) return null;
|
|
@@ -2139,9 +2328,9 @@ var VALIDATE_ALL_TEXTS = {
|
|
|
2139
2328
|
};
|
|
2140
2329
|
|
|
2141
2330
|
// built-in-plugins/analyzers/validate-all/index.ts
|
|
2142
|
-
var
|
|
2331
|
+
var ID19 = "validate-all";
|
|
2143
2332
|
var validateAllAnalyzer = {
|
|
2144
|
-
id:
|
|
2333
|
+
id: ID19,
|
|
2145
2334
|
pluginId: "core",
|
|
2146
2335
|
kind: "analyzer",
|
|
2147
2336
|
version: "1.0.0",
|
|
@@ -2164,7 +2353,7 @@ function collectNodeFindings(v, node, out) {
|
|
|
2164
2353
|
const result = v.validate("node", toNodeForSchema(node));
|
|
2165
2354
|
if (result.ok) return;
|
|
2166
2355
|
out.push({
|
|
2167
|
-
analyzerId:
|
|
2356
|
+
analyzerId: ID19,
|
|
2168
2357
|
severity: "error",
|
|
2169
2358
|
nodeIds: [node.path],
|
|
2170
2359
|
message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
|
|
@@ -2178,7 +2367,7 @@ function collectLinkFindings(v, link2, out) {
|
|
|
2178
2367
|
const result = v.validate("link", toLinkForSchema(link2));
|
|
2179
2368
|
if (result.ok) return;
|
|
2180
2369
|
out.push({
|
|
2181
|
-
analyzerId:
|
|
2370
|
+
analyzerId: ID19,
|
|
2182
2371
|
severity: "error",
|
|
2183
2372
|
nodeIds: [link2.source],
|
|
2184
2373
|
message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
|
|
@@ -2219,19 +2408,67 @@ function toLinkForSchema(link2) {
|
|
|
2219
2408
|
}
|
|
2220
2409
|
|
|
2221
2410
|
// built-in-plugins/analyzers/link-counts/index.ts
|
|
2222
|
-
var
|
|
2411
|
+
var ID20 = "link-counts";
|
|
2223
2412
|
var linkCountsAnalyzer = {
|
|
2224
|
-
id:
|
|
2413
|
+
id: ID20,
|
|
2225
2414
|
pluginId: "core",
|
|
2226
2415
|
kind: "analyzer",
|
|
2227
2416
|
version: "1.0.0",
|
|
2228
|
-
description: "
|
|
2417
|
+
description: "Counts incoming and outgoing links per node and surfaces them as paired footer chips.",
|
|
2229
2418
|
stability: "stable",
|
|
2230
2419
|
mode: "deterministic",
|
|
2231
|
-
|
|
2420
|
+
viewContributions: {
|
|
2421
|
+
linksIn: {
|
|
2422
|
+
slot: "card.footer.left",
|
|
2423
|
+
icon: "pi-arrow-up",
|
|
2424
|
+
label: "incoming links",
|
|
2425
|
+
emitWhenEmpty: false
|
|
2426
|
+
},
|
|
2427
|
+
linksOut: {
|
|
2428
|
+
slot: "card.footer.left",
|
|
2429
|
+
icon: "pi-arrow-down",
|
|
2430
|
+
label: "outgoing links",
|
|
2431
|
+
emitWhenEmpty: false
|
|
2432
|
+
}
|
|
2433
|
+
},
|
|
2434
|
+
evaluate(ctx) {
|
|
2435
|
+
const perTarget = /* @__PURE__ */ new Map();
|
|
2436
|
+
const perSource = /* @__PURE__ */ new Map();
|
|
2437
|
+
for (const link2 of ctx.links) {
|
|
2438
|
+
bump(perTarget, link2.target, link2.kind);
|
|
2439
|
+
bump(perSource, link2.source, link2.kind);
|
|
2440
|
+
}
|
|
2441
|
+
for (const node of ctx.nodes) {
|
|
2442
|
+
emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
|
|
2443
|
+
emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
|
|
2444
|
+
}
|
|
2232
2445
|
return [];
|
|
2233
2446
|
}
|
|
2234
2447
|
};
|
|
2448
|
+
function bump(map, key, kind) {
|
|
2449
|
+
let byKind = map.get(key);
|
|
2450
|
+
if (!byKind) {
|
|
2451
|
+
byKind = /* @__PURE__ */ new Map();
|
|
2452
|
+
map.set(key, byKind);
|
|
2453
|
+
}
|
|
2454
|
+
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
|
|
2455
|
+
}
|
|
2456
|
+
function emitChip(ctx, nodePath, contributionId, byKind) {
|
|
2457
|
+
if (!byKind) return;
|
|
2458
|
+
let total = 0;
|
|
2459
|
+
for (const n of byKind.values()) total += n;
|
|
2460
|
+
if (total === 0) return;
|
|
2461
|
+
const capped = Math.min(total, 99);
|
|
2462
|
+
const direction = contributionId === "linksIn" ? "in" : "out";
|
|
2463
|
+
ctx.emitContribution(nodePath, contributionId, {
|
|
2464
|
+
value: capped,
|
|
2465
|
+
tooltip: formatBreakdown(byKind, direction)
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function formatBreakdown(byKind, direction) {
|
|
2469
|
+
const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
|
|
2470
|
+
return [direction, ...lines].join("\n");
|
|
2471
|
+
}
|
|
2235
2472
|
|
|
2236
2473
|
// kernel/sidecar/parse.ts
|
|
2237
2474
|
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
@@ -2335,14 +2572,14 @@ function resolveSpecRoot2() {
|
|
|
2335
2572
|
}
|
|
2336
2573
|
|
|
2337
2574
|
// built-in-plugins/actions/bump/index.ts
|
|
2338
|
-
var
|
|
2575
|
+
var ID21 = "bump";
|
|
2339
2576
|
var PLUGIN_ID = "core";
|
|
2340
2577
|
var bumpAction = {
|
|
2341
|
-
id:
|
|
2578
|
+
id: ID21,
|
|
2342
2579
|
pluginId: PLUGIN_ID,
|
|
2343
2580
|
kind: "action",
|
|
2344
2581
|
version: "1.0.0",
|
|
2345
|
-
description: "
|
|
2582
|
+
description: "Marks a node as updated \u2014 bumps its version, refreshes the sidecar hashes, and records the timestamp. Refuses on a fresh node unless `force: true` is passed.",
|
|
2346
2583
|
stability: "stable",
|
|
2347
2584
|
mode: "deterministic",
|
|
2348
2585
|
reportSchemaRef: "https://skill-map.dev/spec/v0/bump-report.schema.json",
|
|
@@ -3762,7 +3999,7 @@ function rowToIssue(row) {
|
|
|
3762
3999
|
return issue;
|
|
3763
4000
|
}
|
|
3764
4001
|
async function loadExtractorRuns(db) {
|
|
3765
|
-
const rows = await db.selectFrom("scan_extractor_runs").select(["nodePath", "extractorId", "bodyHashAtRun"]).execute();
|
|
4002
|
+
const rows = await db.selectFrom("scan_extractor_runs").select(["nodePath", "extractorId", "bodyHashAtRun", "sidecarAnnotationsHashAtRun"]).execute();
|
|
3766
4003
|
const result = /* @__PURE__ */ new Map();
|
|
3767
4004
|
for (const row of rows) {
|
|
3768
4005
|
let perNode = result.get(row.nodePath);
|
|
@@ -3770,7 +4007,10 @@ async function loadExtractorRuns(db) {
|
|
|
3770
4007
|
perNode = /* @__PURE__ */ new Map();
|
|
3771
4008
|
result.set(row.nodePath, perNode);
|
|
3772
4009
|
}
|
|
3773
|
-
perNode.set(row.extractorId,
|
|
4010
|
+
perNode.set(row.extractorId, {
|
|
4011
|
+
bodyHash: row.bodyHashAtRun,
|
|
4012
|
+
sidecarAnnotationsHash: row.sidecarAnnotationsHashAtRun
|
|
4013
|
+
});
|
|
3774
4014
|
}
|
|
3775
4015
|
return result;
|
|
3776
4016
|
}
|
|
@@ -3837,14 +4077,14 @@ async function replaceAllScanContributions(trx, contributions, livePaths = /* @_
|
|
|
3837
4077
|
if (freshlyRunTuples.size > 0) {
|
|
3838
4078
|
const bufferKeys = /* @__PURE__ */ new Set();
|
|
3839
4079
|
for (const c of contributions) {
|
|
3840
|
-
bufferKeys.add(`${c.pluginId}
|
|
4080
|
+
bufferKeys.add(`${c.pluginId}\0${c.extensionId}\0${c.nodePath}\0${c.contributionId}`);
|
|
3841
4081
|
}
|
|
3842
4082
|
const tuplesByPluginExt = /* @__PURE__ */ new Map();
|
|
3843
4083
|
for (const tuple of freshlyRunTuples) {
|
|
3844
|
-
const
|
|
3845
|
-
if (
|
|
3846
|
-
const
|
|
3847
|
-
const
|
|
4084
|
+
const parts = tuple.split("\0");
|
|
4085
|
+
if (parts.length !== 3) continue;
|
|
4086
|
+
const [pluginId, extensionId, node] = parts;
|
|
4087
|
+
const pe = `${pluginId}\0${extensionId}`;
|
|
3848
4088
|
let nodes = tuplesByPluginExt.get(pe);
|
|
3849
4089
|
if (!nodes) {
|
|
3850
4090
|
nodes = /* @__PURE__ */ new Set();
|
|
@@ -3853,15 +4093,15 @@ async function replaceAllScanContributions(trx, contributions, livePaths = /* @_
|
|
|
3853
4093
|
nodes.add(node);
|
|
3854
4094
|
}
|
|
3855
4095
|
for (const [pe, nodes] of tuplesByPluginExt) {
|
|
3856
|
-
const
|
|
3857
|
-
if (
|
|
3858
|
-
const pluginId = pe.slice(0,
|
|
3859
|
-
const extensionId = pe.slice(
|
|
4096
|
+
const sep6 = pe.indexOf("\0");
|
|
4097
|
+
if (sep6 < 0) continue;
|
|
4098
|
+
const pluginId = pe.slice(0, sep6);
|
|
4099
|
+
const extensionId = pe.slice(sep6 + 1);
|
|
3860
4100
|
const nodeArr = [...nodes];
|
|
3861
4101
|
const candidates = await trx.selectFrom("scan_contributions").select(["nodePath", "contributionId"]).where("pluginId", "=", pluginId).where("extensionId", "=", extensionId).where("nodePath", "in", nodeArr).execute();
|
|
3862
4102
|
const stale = [];
|
|
3863
4103
|
for (const row of candidates) {
|
|
3864
|
-
const key = `${pluginId}
|
|
4104
|
+
const key = `${pluginId}\0${extensionId}\0${row.nodePath}\0${row.contributionId}`;
|
|
3865
4105
|
if (!bufferKeys.has(key)) stale.push(row);
|
|
3866
4106
|
}
|
|
3867
4107
|
for (const s of stale) {
|
|
@@ -3908,6 +4148,14 @@ async function loadContributionLookup(db, pluginId, contributionId, nodePath, ex
|
|
|
3908
4148
|
const rows = await query.orderBy("extensionId", "asc").execute();
|
|
3909
4149
|
return rows.map(rowToContribution);
|
|
3910
4150
|
}
|
|
4151
|
+
async function purgeContributionsByPlugin(db, pluginId, extensionId) {
|
|
4152
|
+
let query = db.deleteFrom("scan_contributions").where("pluginId", "=", pluginId);
|
|
4153
|
+
if (extensionId !== void 0) {
|
|
4154
|
+
query = query.where("extensionId", "=", extensionId);
|
|
4155
|
+
}
|
|
4156
|
+
const result = await query.executeTakeFirst();
|
|
4157
|
+
return Number(result.numDeletedRows ?? 0n);
|
|
4158
|
+
}
|
|
3911
4159
|
function rowToContribution(row) {
|
|
3912
4160
|
let payload;
|
|
3913
4161
|
try {
|
|
@@ -4174,7 +4422,8 @@ function extractorRunToRow(record) {
|
|
|
4174
4422
|
nodePath: record.nodePath,
|
|
4175
4423
|
extractorId: record.extractorId,
|
|
4176
4424
|
bodyHashAtRun: record.bodyHashAtRun,
|
|
4177
|
-
ranAt: record.ranAt
|
|
4425
|
+
ranAt: record.ranAt,
|
|
4426
|
+
sidecarAnnotationsHashAtRun: record.sidecarAnnotationsHashAtRun
|
|
4178
4427
|
};
|
|
4179
4428
|
}
|
|
4180
4429
|
function enrichmentToRow(record) {
|
|
@@ -4349,7 +4598,8 @@ var SqliteStorageAdapter = class {
|
|
|
4349
4598
|
this.contributions = {
|
|
4350
4599
|
listForNode: (nodePath) => loadContributionsForNode(this.db, nodePath),
|
|
4351
4600
|
listForPaths: (paths) => loadContributionsForPaths(this.db, paths),
|
|
4352
|
-
lookup: (pluginId, contributionId, nodePath, extensionId) => loadContributionLookup(this.db, pluginId, contributionId, nodePath, extensionId)
|
|
4601
|
+
lookup: (pluginId, contributionId, nodePath, extensionId) => loadContributionLookup(this.db, pluginId, contributionId, nodePath, extensionId),
|
|
4602
|
+
purgeByPlugin: (pluginId, extensionId) => purgeContributionsByPlugin(this.db, pluginId, extensionId)
|
|
4353
4603
|
};
|
|
4354
4604
|
this.tags = {
|
|
4355
4605
|
listForNode: (nodePath) => loadTagsForNode(this.db, nodePath),
|
|
@@ -4636,7 +4886,8 @@ var CONFIG_LOADER_TEXTS = {
|
|
|
4636
4886
|
invalidJson: "[config:{{layer}}] invalid JSON in {{path}}: {{message}}",
|
|
4637
4887
|
expectedObject: "[config:{{layer}}] expected a JSON object, got {{type}}; ignored",
|
|
4638
4888
|
unknownKey: "[config:{{layer}}] unknown key {{key}} ignored",
|
|
4639
|
-
invalidValue: "[config:{{layer}}] invalid value at {{path}}: {{message}}"
|
|
4889
|
+
invalidValue: "[config:{{layer}}] invalid value at {{path}}: {{message}}",
|
|
4890
|
+
projectLocalOnlyStripped: "[config:{{layer}}] key {{key}} is project-local only; stripped from the committed project layer. Move it to .skill-map/settings.local.json (gitignored, per-checkout)."
|
|
4640
4891
|
};
|
|
4641
4892
|
|
|
4642
4893
|
// kernel/util/skill-map-paths.ts
|
|
@@ -4701,6 +4952,7 @@ function kernelLocalSettingsPath(scopeRoot) {
|
|
|
4701
4952
|
var defaults_default = {
|
|
4702
4953
|
schemaVersion: 1,
|
|
4703
4954
|
autoMigrate: true,
|
|
4955
|
+
allowEditSmFiles: false,
|
|
4704
4956
|
tokenizer: "cl100k_base",
|
|
4705
4957
|
providers: [],
|
|
4706
4958
|
roots: [],
|
|
@@ -4738,6 +4990,12 @@ var defaults_default = {
|
|
|
4738
4990
|
};
|
|
4739
4991
|
|
|
4740
4992
|
// kernel/config/loader.ts
|
|
4993
|
+
var PROJECT_LOCAL_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
4994
|
+
"allowEditSmFiles",
|
|
4995
|
+
"scan.includeHome",
|
|
4996
|
+
"scan.extraRoots",
|
|
4997
|
+
"scan.referencePaths"
|
|
4998
|
+
]);
|
|
4741
4999
|
var DEFAULTS = defaults_default;
|
|
4742
5000
|
function loadConfig(opts) {
|
|
4743
5001
|
const cwd = opts.cwd;
|
|
@@ -4763,6 +5021,9 @@ function loadConfig(opts) {
|
|
|
4763
5021
|
const partial = readJsonSafe(path, layer, warnings, strict);
|
|
4764
5022
|
if (partial === null) continue;
|
|
4765
5023
|
const cleaned = validateAndStrip(validators, partial, layer, warnings, strict);
|
|
5024
|
+
if (layer === "project") {
|
|
5025
|
+
stripProjectLocalOnlyKeys(cleaned, warnings, strict);
|
|
5026
|
+
}
|
|
4766
5027
|
effective = deepMerge(effective, cleaned);
|
|
4767
5028
|
recordSources("", cleaned, sources, layer);
|
|
4768
5029
|
}
|
|
@@ -4853,6 +5114,30 @@ function deleteAtPath(root, parentPath, key) {
|
|
|
4853
5114
|
}
|
|
4854
5115
|
if (isPlainObject2(cur)) delete cur[key];
|
|
4855
5116
|
}
|
|
5117
|
+
function stripProjectLocalOnlyKeys(cloned, warnings, strict) {
|
|
5118
|
+
for (const dotKey of PROJECT_LOCAL_ONLY_KEYS) {
|
|
5119
|
+
const segments = dotKey.split(".").filter(Boolean);
|
|
5120
|
+
if (segments.length === 0) continue;
|
|
5121
|
+
const leaf = segments.pop();
|
|
5122
|
+
if (!keyPresentAtPath(cloned, segments, leaf)) continue;
|
|
5123
|
+
const parentPath = "/" + segments.join("/");
|
|
5124
|
+
deleteAtPath(cloned, parentPath, leaf);
|
|
5125
|
+
const msg = tx(CONFIG_LOADER_TEXTS.projectLocalOnlyStripped, {
|
|
5126
|
+
layer: "project",
|
|
5127
|
+
key: dotKey
|
|
5128
|
+
});
|
|
5129
|
+
if (strict) throw new Error(msg);
|
|
5130
|
+
warnings.push(msg);
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
function keyPresentAtPath(root, parentSegments, leaf) {
|
|
5134
|
+
let cur = root;
|
|
5135
|
+
for (const seg of parentSegments) {
|
|
5136
|
+
if (!isPlainObject2(cur)) return false;
|
|
5137
|
+
cur = cur[seg];
|
|
5138
|
+
}
|
|
5139
|
+
return isPlainObject2(cur) && Object.prototype.hasOwnProperty.call(cur, leaf);
|
|
5140
|
+
}
|
|
4856
5141
|
function isPlainObject2(v) {
|
|
4857
5142
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4858
5143
|
}
|
|
@@ -5050,6 +5335,16 @@ var UserOnlyKeyError = class extends Error {
|
|
|
5050
5335
|
}
|
|
5051
5336
|
key;
|
|
5052
5337
|
};
|
|
5338
|
+
var ProjectLocalOnlyKeyError = class extends Error {
|
|
5339
|
+
constructor(key) {
|
|
5340
|
+
super(
|
|
5341
|
+
`Config key '${key}' is project-local only. Pass { target: 'project-local' } to write it to .skill-map/settings.local.json (gitignored), or use -g for the user / user-local scope.`
|
|
5342
|
+
);
|
|
5343
|
+
this.key = key;
|
|
5344
|
+
this.name = "ProjectLocalOnlyKeyError";
|
|
5345
|
+
}
|
|
5346
|
+
key;
|
|
5347
|
+
};
|
|
5053
5348
|
function readConfigValue(key, opts) {
|
|
5054
5349
|
const scope = USER_ONLY_KEYS.has(key) ? "global" : opts.scope;
|
|
5055
5350
|
const loaded = loadConfigForScope(scope, opts);
|
|
@@ -5061,6 +5356,9 @@ function writeConfigValue(key, value, opts) {
|
|
|
5061
5356
|
if (USER_ONLY_KEYS.has(key) && opts.target === "project") {
|
|
5062
5357
|
throw new UserOnlyKeyError(key);
|
|
5063
5358
|
}
|
|
5359
|
+
if (PROJECT_LOCAL_ONLY_KEYS.has(key) && opts.target === "project") {
|
|
5360
|
+
throw new ProjectLocalOnlyKeyError(key);
|
|
5361
|
+
}
|
|
5064
5362
|
const path = targetSettingsPath(opts.target, opts.cwd, opts.homedir);
|
|
5065
5363
|
const merged = readJsonObjectOrEmpty(path);
|
|
5066
5364
|
setAtPath(merged, key, value);
|
|
@@ -5071,6 +5369,9 @@ function removeConfigValue(key, opts) {
|
|
|
5071
5369
|
if (USER_ONLY_KEYS.has(key) && opts.target === "project") {
|
|
5072
5370
|
throw new UserOnlyKeyError(key);
|
|
5073
5371
|
}
|
|
5372
|
+
if (PROJECT_LOCAL_ONLY_KEYS.has(key) && opts.target === "project") {
|
|
5373
|
+
throw new ProjectLocalOnlyKeyError(key);
|
|
5374
|
+
}
|
|
5074
5375
|
const path = targetSettingsPath(opts.target, opts.cwd, opts.homedir);
|
|
5075
5376
|
const merged = readJsonObjectOrEmpty(path);
|
|
5076
5377
|
const removed = deleteAtPath2(merged, key);
|
|
@@ -5088,8 +5389,16 @@ function loadConfigForScope(scope, opts) {
|
|
|
5088
5389
|
});
|
|
5089
5390
|
}
|
|
5090
5391
|
function targetSettingsPath(target, cwd, home) {
|
|
5091
|
-
|
|
5092
|
-
|
|
5392
|
+
switch (target) {
|
|
5393
|
+
case "user":
|
|
5394
|
+
return defaultSettingsPath(home);
|
|
5395
|
+
case "user-local":
|
|
5396
|
+
return defaultLocalSettingsPath(home);
|
|
5397
|
+
case "project":
|
|
5398
|
+
return defaultSettingsPath(cwd);
|
|
5399
|
+
case "project-local":
|
|
5400
|
+
return defaultLocalSettingsPath(cwd);
|
|
5401
|
+
}
|
|
5093
5402
|
}
|
|
5094
5403
|
function validateOrThrow(content) {
|
|
5095
5404
|
const validators = loadSchemaValidators();
|
|
@@ -5243,7 +5552,7 @@ var UPDATE_CHECK_TEXTS = {
|
|
|
5243
5552
|
// package.json
|
|
5244
5553
|
var package_default = {
|
|
5245
5554
|
name: "@skill-map/cli",
|
|
5246
|
-
version: "0.
|
|
5555
|
+
version: "0.21.0",
|
|
5247
5556
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
5248
5557
|
license: "MIT",
|
|
5249
5558
|
type: "module",
|
|
@@ -5309,7 +5618,7 @@ var package_default = {
|
|
|
5309
5618
|
},
|
|
5310
5619
|
dependencies: {
|
|
5311
5620
|
"@hono/node-server": "2.0.1",
|
|
5312
|
-
"@skill-map/spec": "0.
|
|
5621
|
+
"@skill-map/spec": "0.21.0",
|
|
5313
5622
|
ajv: "8.18.0",
|
|
5314
5623
|
"ajv-formats": "3.0.1",
|
|
5315
5624
|
chokidar: "5.0.0",
|
|
@@ -5433,10 +5742,14 @@ function isUpdateCheckEnabled(opts) {
|
|
|
5433
5742
|
async function runWithAdapter(adapter, opts) {
|
|
5434
5743
|
const cache = await adapter.preferences.loadUpdateCheckCache();
|
|
5435
5744
|
const now = Date.now();
|
|
5745
|
+
let lastShownAt = cache?.shownAt ?? null;
|
|
5746
|
+
let didShowThisRun = false;
|
|
5436
5747
|
if (cache && isOutdated(VERSION, cache.latestVersion)) {
|
|
5437
|
-
const dueToShow =
|
|
5748
|
+
const dueToShow = lastShownAt === null || now - lastShownAt > ONE_DAY_MS;
|
|
5438
5749
|
if (dueToShow) {
|
|
5439
5750
|
writeBanner(opts, cache.latestVersion);
|
|
5751
|
+
didShowThisRun = true;
|
|
5752
|
+
lastShownAt = now;
|
|
5440
5753
|
try {
|
|
5441
5754
|
await adapter.preferences.saveUpdateCheckCache({
|
|
5442
5755
|
latestVersion: cache.latestVersion,
|
|
@@ -5455,11 +5768,18 @@ async function runWithAdapter(adapter, opts) {
|
|
|
5455
5768
|
} catch {
|
|
5456
5769
|
return;
|
|
5457
5770
|
}
|
|
5771
|
+
if (!didShowThisRun && isOutdated(VERSION, latest)) {
|
|
5772
|
+
const dueToShow = lastShownAt === null || now - lastShownAt > ONE_DAY_MS;
|
|
5773
|
+
if (dueToShow) {
|
|
5774
|
+
writeBanner(opts, latest);
|
|
5775
|
+
lastShownAt = now;
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5458
5778
|
try {
|
|
5459
5779
|
await adapter.preferences.saveUpdateCheckCache({
|
|
5460
5780
|
latestVersion: latest,
|
|
5461
5781
|
checkedAt: now,
|
|
5462
|
-
shownAt:
|
|
5782
|
+
shownAt: lastShownAt
|
|
5463
5783
|
});
|
|
5464
5784
|
} catch {
|
|
5465
5785
|
}
|
|
@@ -5484,7 +5804,7 @@ var updateCheckHook = {
|
|
|
5484
5804
|
pluginId: "core",
|
|
5485
5805
|
kind: "hook",
|
|
5486
5806
|
version: "1.0.0",
|
|
5487
|
-
description:
|
|
5807
|
+
description: "Checks once a day for a newer version of skill-map on npm and shows the `update available` banner when one exists.",
|
|
5488
5808
|
stability: "stable",
|
|
5489
5809
|
mode: "deterministic",
|
|
5490
5810
|
triggers: ["boot"],
|
|
@@ -5549,6 +5869,8 @@ var builtInBundles = [
|
|
|
5549
5869
|
externalUrlCounterExtractor,
|
|
5550
5870
|
markdownLinkExtractor,
|
|
5551
5871
|
slashExtractor,
|
|
5872
|
+
stabilityExtractor,
|
|
5873
|
+
toolsCountExtractor,
|
|
5552
5874
|
triggerCollisionAnalyzer,
|
|
5553
5875
|
brokenRefAnalyzer,
|
|
5554
5876
|
supersededAnalyzer,
|
|
@@ -5772,15 +6094,20 @@ var UTIL_TEXTS = {
|
|
|
5772
6094
|
// Every verb's body is expected to end on a content line (with or
|
|
5773
6095
|
// without its own trailing \n); the blank line here is universal.
|
|
5774
6096
|
doneIn: "\ndone in {{elapsed}}\n",
|
|
5775
|
-
// confirm.ts (default-no prompt suffix)
|
|
6097
|
+
// confirm.ts (default-no prompt suffix — destructive verbs)
|
|
5776
6098
|
confirmPromptSuffix: " [y/N] ",
|
|
6099
|
+
// confirm.ts (default-yes prompt suffix — consent-style verbs where the
|
|
6100
|
+
// user already triggered the action and is just acknowledging it,
|
|
6101
|
+
// e.g. the .sm write consent gate).
|
|
6102
|
+
confirmPromptSuffixDefaultYes: " [Y/n] ",
|
|
5777
6103
|
/**
|
|
5778
|
-
* Regex source matching affirmative answers in `confirm()`.
|
|
5779
|
-
* with the `i` flag in the helper. Pre-i18n today the
|
|
5780
|
-
* English-only; when a non-English locale lands
|
|
5781
|
-
* alternations (e.g. `^(y(es)?|s(í|i)?)$`).
|
|
6104
|
+
* Regex source matching affirmative / negative answers in `confirm()`.
|
|
6105
|
+
* Compiled with the `i` flag in the helper. Pre-i18n today the
|
|
6106
|
+
* patterns are English-only; when a non-English locale lands each
|
|
6107
|
+
* catalog entry grows alternations (e.g. `^(y(es)?|s(í|i)?)$`).
|
|
5782
6108
|
*/
|
|
5783
|
-
confirmYesPatternSource: "^y(es)?$"
|
|
6109
|
+
confirmYesPatternSource: "^y(es)?$",
|
|
6110
|
+
confirmNoPatternSource: "^no?$"
|
|
5784
6111
|
};
|
|
5785
6112
|
|
|
5786
6113
|
// cli/util/exit-codes.ts
|
|
@@ -5992,6 +6319,41 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
5992
6319
|
import { dirname as dirname8, resolve as resolve12 } from "path";
|
|
5993
6320
|
import { Command as Command2, Option as Option2 } from "clipanion";
|
|
5994
6321
|
|
|
6322
|
+
// core/config/sidecar-consent.ts
|
|
6323
|
+
var EConsentRequiredError = class extends Error {
|
|
6324
|
+
key;
|
|
6325
|
+
hintTarget;
|
|
6326
|
+
constructor(init) {
|
|
6327
|
+
super(
|
|
6328
|
+
`Skill-map needs your consent to create .sm sidecars in this project. Set '${init.key}' to true in .skill-map/settings.local.json (gitignored), or pass --yes / { confirm: true } to grant on the fly.`
|
|
6329
|
+
);
|
|
6330
|
+
this.name = "EConsentRequiredError";
|
|
6331
|
+
this.key = init.key;
|
|
6332
|
+
this.hintTarget = init.hintTarget;
|
|
6333
|
+
}
|
|
6334
|
+
};
|
|
6335
|
+
function ensureSidecarWritesAllowed(opts) {
|
|
6336
|
+
const allowed = readConfigValue("allowEditSmFiles", {
|
|
6337
|
+
scope: "project",
|
|
6338
|
+
cwd: opts.cwd,
|
|
6339
|
+
homedir: opts.homedir,
|
|
6340
|
+
default: false
|
|
6341
|
+
});
|
|
6342
|
+
if (allowed === true) return;
|
|
6343
|
+
if (opts.confirm === true) {
|
|
6344
|
+
writeConfigValue("allowEditSmFiles", true, {
|
|
6345
|
+
target: "project-local",
|
|
6346
|
+
cwd: opts.cwd,
|
|
6347
|
+
homedir: opts.homedir
|
|
6348
|
+
});
|
|
6349
|
+
return;
|
|
6350
|
+
}
|
|
6351
|
+
throw new EConsentRequiredError({
|
|
6352
|
+
key: "allowEditSmFiles",
|
|
6353
|
+
hintTarget: "project-local"
|
|
6354
|
+
});
|
|
6355
|
+
}
|
|
6356
|
+
|
|
5995
6357
|
// kernel/sidecar/store.ts
|
|
5996
6358
|
import { existsSync as existsSync9, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
5997
6359
|
import { dirname as dirname7, resolve as resolve10 } from "path";
|
|
@@ -6008,7 +6370,12 @@ var FilesystemSidecarStore = class {
|
|
|
6008
6370
|
* files in the repo and entries are tiny).
|
|
6009
6371
|
*/
|
|
6010
6372
|
#locks = /* @__PURE__ */ new Map();
|
|
6011
|
-
async applyPatch(sidecarAbsPath, changes) {
|
|
6373
|
+
async applyPatch(sidecarAbsPath, changes, consent) {
|
|
6374
|
+
ensureSidecarWritesAllowed({
|
|
6375
|
+
confirm: consent.confirm,
|
|
6376
|
+
cwd: consent.cwd,
|
|
6377
|
+
homedir: consent.homedir
|
|
6378
|
+
});
|
|
6012
6379
|
const prev = this.#locks.get(sidecarAbsPath) ?? Promise.resolve();
|
|
6013
6380
|
let release;
|
|
6014
6381
|
const settled = new Promise((res) => {
|
|
@@ -6146,9 +6513,41 @@ var BUMP_TEXTS = {
|
|
|
6146
6513
|
// --- failures -------------------------------------------------------------
|
|
6147
6514
|
bumpFailed: "{{glyph}} sm bump: {{message}}\n",
|
|
6148
6515
|
storeFailedDetail: "sidecar write failed for {{path}}: {{message}}",
|
|
6149
|
-
resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}"
|
|
6516
|
+
resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}",
|
|
6517
|
+
// --- .sm consent gate ---------------------------------------------------
|
|
6518
|
+
/**
|
|
6519
|
+
* Pre-prompt context shown before the interactive `confirm()` so the
|
|
6520
|
+
* operator sees what they are about to opt into. `.skill-map/settings.local.json`
|
|
6521
|
+
* is gitignored — the choice is saved per-checkout, never travels via the repo.
|
|
6522
|
+
*/
|
|
6523
|
+
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?",
|
|
6524
|
+
consentAborted: "{{glyph}} sm bump: aborted by user. No .sm sidecar files were written.\n",
|
|
6525
|
+
consentRequiredNonTty: "{{glyph}} sm bump: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
|
|
6526
|
+
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json \u2014 gitignored)."
|
|
6150
6527
|
};
|
|
6151
6528
|
|
|
6529
|
+
// cli/util/confirm.ts
|
|
6530
|
+
import { createInterface } from "readline";
|
|
6531
|
+
var YES_PATTERN = new RegExp(UTIL_TEXTS.confirmYesPatternSource, "i");
|
|
6532
|
+
var NO_PATTERN = new RegExp(UTIL_TEXTS.confirmNoPatternSource, "i");
|
|
6533
|
+
async function confirm(question, streams, opts) {
|
|
6534
|
+
const defaultAnswer = opts?.defaultAnswer ?? "no";
|
|
6535
|
+
const suffix = defaultAnswer === "yes" ? UTIL_TEXTS.confirmPromptSuffixDefaultYes : UTIL_TEXTS.confirmPromptSuffix;
|
|
6536
|
+
const rl = createInterface({ input: streams.stdin, output: streams.stderr });
|
|
6537
|
+
try {
|
|
6538
|
+
const answer = await new Promise(
|
|
6539
|
+
(resolveP) => rl.question(`${question}${suffix}`, resolveP)
|
|
6540
|
+
);
|
|
6541
|
+
const trimmed = answer.trim();
|
|
6542
|
+
if (trimmed === "") return defaultAnswer === "yes";
|
|
6543
|
+
if (YES_PATTERN.test(trimmed)) return true;
|
|
6544
|
+
if (NO_PATTERN.test(trimmed)) return false;
|
|
6545
|
+
return defaultAnswer === "yes";
|
|
6546
|
+
} finally {
|
|
6547
|
+
rl.close();
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6152
6551
|
// cli/util/path-guard.ts
|
|
6153
6552
|
import { isAbsolute as isAbsolute2, resolve as resolve11, sep } from "path";
|
|
6154
6553
|
function assertContained(cwd, rel) {
|
|
@@ -6337,6 +6736,9 @@ var BumpCommand = class extends SmCommand {
|
|
|
6337
6736
|
force = Option2.Boolean("--force", false, {
|
|
6338
6737
|
description: "Single-node: bump even when the node is fresh. Batch: turn fresh-node refusals into silent no-ops."
|
|
6339
6738
|
});
|
|
6739
|
+
yes = Option2.Boolean("--yes", false, {
|
|
6740
|
+
description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
|
|
6741
|
+
});
|
|
6340
6742
|
// The remaining cyclomatic count is from CLI ergonomics — argument
|
|
6341
6743
|
// validation guards (3) + dispatch (1) + JSON-vs-pretty branch.
|
|
6342
6744
|
// eslint-disable-next-line complexity
|
|
@@ -6372,10 +6774,53 @@ var BumpCommand = class extends SmCommand {
|
|
|
6372
6774
|
);
|
|
6373
6775
|
return ExitCode.NotFound;
|
|
6374
6776
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
6777
|
+
return this.#runWithConsent(
|
|
6778
|
+
ansi,
|
|
6779
|
+
() => this.pending ? this.#runPending(persisted.nodes, ctx.cwd, ansi) : this.#runSingle(persisted.nodes, ctx.cwd, ansi)
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
/**
|
|
6783
|
+
* Wrap `dispatch` with the `.sm` consent gate: on the first
|
|
6784
|
+
* `EConsentRequiredError` thrown by `FilesystemSidecarStore.applyPatch`
|
|
6785
|
+
* (via `ensureSidecarWritesAllowed`), prompt the operator if stdin is
|
|
6786
|
+
* a TTY and `--yes` was not passed. On accept, flip `this.yes` to
|
|
6787
|
+
* true and re-run `dispatch` (the second pass passes `confirm: true`
|
|
6788
|
+
* to the store and the gate persists the flag to project-local).
|
|
6789
|
+
* On decline or non-TTY without `--yes`, print a directed message
|
|
6790
|
+
* and return `ExitCode.Error`.
|
|
6791
|
+
*/
|
|
6792
|
+
async #runWithConsent(ansi, dispatch) {
|
|
6793
|
+
try {
|
|
6794
|
+
return await dispatch();
|
|
6795
|
+
} catch (err) {
|
|
6796
|
+
if (!(err instanceof EConsentRequiredError)) throw err;
|
|
6797
|
+
const stdin = this.context.stdin;
|
|
6798
|
+
const stderr = this.context.stderr;
|
|
6799
|
+
const isTTY = stdin.isTTY === true;
|
|
6800
|
+
if (!isTTY || this.yes) {
|
|
6801
|
+
const errGlyph = ansi.red("\u2715");
|
|
6802
|
+
this.printer.error(
|
|
6803
|
+
tx(BUMP_TEXTS.consentRequiredNonTty, {
|
|
6804
|
+
glyph: errGlyph,
|
|
6805
|
+
hint: ansi.dim(BUMP_TEXTS.consentRequiredNonTtyHint)
|
|
6806
|
+
})
|
|
6807
|
+
);
|
|
6808
|
+
return ExitCode.Error;
|
|
6809
|
+
}
|
|
6810
|
+
const ok = await confirm(
|
|
6811
|
+
BUMP_TEXTS.consentPrompt,
|
|
6812
|
+
{ stdin, stderr },
|
|
6813
|
+
{ defaultAnswer: "yes" }
|
|
6814
|
+
);
|
|
6815
|
+
if (!ok) {
|
|
6816
|
+
this.printer.info(
|
|
6817
|
+
tx(BUMP_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
|
|
6818
|
+
);
|
|
6819
|
+
return ExitCode.Error;
|
|
6820
|
+
}
|
|
6821
|
+
this.yes = true;
|
|
6822
|
+
return await dispatch();
|
|
6377
6823
|
}
|
|
6378
|
-
return this.#runSingle(persisted.nodes, ctx.cwd, ansi);
|
|
6379
6824
|
}
|
|
6380
6825
|
// --- single-node --------------------------------------------------------
|
|
6381
6826
|
// Complexity is from CLI ergonomics: not-found / abs-path-resolve /
|
|
@@ -6427,15 +6872,21 @@ var BumpCommand = class extends SmCommand {
|
|
|
6427
6872
|
return ExitCode.Ok;
|
|
6428
6873
|
}
|
|
6429
6874
|
const store = new FilesystemSidecarStore();
|
|
6875
|
+
const ctx = defaultRuntimeContext();
|
|
6430
6876
|
let sidecarPath;
|
|
6431
6877
|
try {
|
|
6432
6878
|
for (const w of result.writes ?? []) {
|
|
6433
6879
|
if (w.kind === "sidecar") {
|
|
6434
|
-
await store.applyPatch(w.path, w.changes
|
|
6880
|
+
await store.applyPatch(w.path, w.changes, {
|
|
6881
|
+
confirm: this.yes,
|
|
6882
|
+
cwd: ctx.cwd,
|
|
6883
|
+
homedir: ctx.homedir
|
|
6884
|
+
});
|
|
6435
6885
|
sidecarPath = w.path;
|
|
6436
6886
|
}
|
|
6437
6887
|
}
|
|
6438
6888
|
} catch (err) {
|
|
6889
|
+
if (err instanceof EConsentRequiredError) throw err;
|
|
6439
6890
|
this.printer.error(
|
|
6440
6891
|
tx(BUMP_TEXTS.bumpFailed, {
|
|
6441
6892
|
glyph: errGlyph,
|
|
@@ -6516,9 +6967,11 @@ var BumpCommand = class extends SmCommand {
|
|
|
6516
6967
|
this.printer.info(tx(BUMP_TEXTS.pendingBanner, { count: stale.length }));
|
|
6517
6968
|
}
|
|
6518
6969
|
const store = new FilesystemSidecarStore();
|
|
6970
|
+
const ctx = defaultRuntimeContext();
|
|
6971
|
+
const consent = { confirm: this.yes, cwd: ctx.cwd, homedir: ctx.homedir };
|
|
6519
6972
|
const outcomes = [];
|
|
6520
6973
|
for (const node of stale) {
|
|
6521
|
-
const outcome = await bumpOnePending(node, cwd, this.force, store);
|
|
6974
|
+
const outcome = await bumpOnePending(node, cwd, this.force, store, consent);
|
|
6522
6975
|
outcomes.push(outcome);
|
|
6523
6976
|
if (outcome.status === "bumped" && this.staged && outcome.sidecarPath !== void 0) {
|
|
6524
6977
|
const addErr = stageSidecar(cwd, outcome.sidecarPath);
|
|
@@ -6611,7 +7064,7 @@ function invokeBumpFor(node, absPath, force) {
|
|
|
6611
7064
|
now: () => /* @__PURE__ */ new Date()
|
|
6612
7065
|
});
|
|
6613
7066
|
}
|
|
6614
|
-
async function bumpOnePending(node, cwd, force, store) {
|
|
7067
|
+
async function bumpOnePending(node, cwd, force, store, consent) {
|
|
6615
7068
|
let absPath;
|
|
6616
7069
|
try {
|
|
6617
7070
|
assertContained(cwd, node.path);
|
|
@@ -6643,11 +7096,12 @@ async function bumpOnePending(node, cwd, force, store) {
|
|
|
6643
7096
|
try {
|
|
6644
7097
|
for (const w of result.writes ?? []) {
|
|
6645
7098
|
if (w.kind === "sidecar") {
|
|
6646
|
-
await store.applyPatch(w.path, w.changes);
|
|
7099
|
+
await store.applyPatch(w.path, w.changes, consent);
|
|
6647
7100
|
sidecarPath = w.path;
|
|
6648
7101
|
}
|
|
6649
7102
|
}
|
|
6650
7103
|
} catch (err) {
|
|
7104
|
+
if (err instanceof EConsentRequiredError) throw err;
|
|
6651
7105
|
return {
|
|
6652
7106
|
nodePath: node.path,
|
|
6653
7107
|
status: "error",
|
|
@@ -7410,7 +7864,18 @@ var LOCKED_PLUGIN_IDS = /* @__PURE__ */ new Set([
|
|
|
7410
7864
|
// unclaimed `.md` files"). Disabling it makes every orphan markdown
|
|
7411
7865
|
// silently invisible — a foot-gun the host product does not want to
|
|
7412
7866
|
// expose. Lock it in the enabled state.
|
|
7413
|
-
"core/markdown"
|
|
7867
|
+
"core/markdown",
|
|
7868
|
+
// `core/annotations` turns the `supersedes` / `supersededBy` /
|
|
7869
|
+
// `requires` / `related` / `conflictsWith` entries of the sidecar
|
|
7870
|
+
// `annotations:` block into the arrows the graph draws between nodes.
|
|
7871
|
+
// It does NOT own the rest of the block (`version`, `stability`,
|
|
7872
|
+
// `tags`, `description` — those live on the node bundle directly and
|
|
7873
|
+
// keep rendering with the plugin off). Disabling it produces a
|
|
7874
|
+
// confusing "edges disappear but the sidecar metadata stays" split
|
|
7875
|
+
// that no operator actually wants; the lock makes the asymmetry
|
|
7876
|
+
// unreachable from CLI / BFF / UI. Re-evaluate if a third-party ever
|
|
7877
|
+
// ships a competing supersession extractor.
|
|
7878
|
+
"core/annotations"
|
|
7414
7879
|
]);
|
|
7415
7880
|
function isPluginLocked(idOrQualified) {
|
|
7416
7881
|
return LOCKED_PLUGIN_IDS.has(idOrQualified);
|
|
@@ -7557,7 +8022,21 @@ function isBundleEntryEnabled(bundle, extId, resolveEnabled) {
|
|
|
7557
8022
|
}
|
|
7558
8023
|
return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
|
|
7559
8024
|
}
|
|
8025
|
+
function buildGranularityMap(discovered) {
|
|
8026
|
+
const out = /* @__PURE__ */ new Map();
|
|
8027
|
+
for (const plugin of discovered) {
|
|
8028
|
+
out.set(plugin.id, plugin.granularity ?? "bundle");
|
|
8029
|
+
}
|
|
8030
|
+
return out;
|
|
8031
|
+
}
|
|
8032
|
+
function isPluginExtensionEnabled(ext, granularityMap, resolveEnabled) {
|
|
8033
|
+
const granularity = granularityMap.get(ext.pluginId) ?? "bundle";
|
|
8034
|
+
if (granularity === "bundle") return resolveEnabled(ext.pluginId);
|
|
8035
|
+
return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
|
|
8036
|
+
}
|
|
7560
8037
|
function composeScanExtensions(opts) {
|
|
8038
|
+
const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
|
|
8039
|
+
const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
|
|
7561
8040
|
const providers = [];
|
|
7562
8041
|
const extractors = [];
|
|
7563
8042
|
const analyzers = [];
|
|
@@ -7565,13 +8044,21 @@ function composeScanExtensions(opts) {
|
|
|
7565
8044
|
if (!opts.noBuiltIns) {
|
|
7566
8045
|
accumulateBuiltInScanExtensions(
|
|
7567
8046
|
{ providers, extractors, analyzers, hooks },
|
|
7568
|
-
|
|
8047
|
+
resolveEnabled
|
|
7569
8048
|
);
|
|
7570
8049
|
}
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
8050
|
+
for (const ext of opts.pluginRuntime.extensions.providers) {
|
|
8051
|
+
if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) providers.push(ext);
|
|
8052
|
+
}
|
|
8053
|
+
for (const ext of opts.pluginRuntime.extensions.extractors) {
|
|
8054
|
+
if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) extractors.push(ext);
|
|
8055
|
+
}
|
|
8056
|
+
for (const ext of opts.pluginRuntime.extensions.analyzers) {
|
|
8057
|
+
if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) analyzers.push(ext);
|
|
8058
|
+
}
|
|
8059
|
+
for (const ext of opts.pluginRuntime.extensions.hooks) {
|
|
8060
|
+
if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) hooks.push(ext);
|
|
8061
|
+
}
|
|
7575
8062
|
const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
|
|
7576
8063
|
const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
|
|
7577
8064
|
const finalAnalyzers = opts.killSwitches?.analyzers === true ? [] : analyzers;
|
|
@@ -7616,38 +8103,61 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
|
|
|
7616
8103
|
}
|
|
7617
8104
|
function composeFormatters(opts) {
|
|
7618
8105
|
const noBuiltIns = opts.noBuiltIns ?? false;
|
|
8106
|
+
const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
|
|
8107
|
+
const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
|
|
7619
8108
|
const out = [];
|
|
7620
8109
|
if (!noBuiltIns) {
|
|
7621
8110
|
for (const bundle of builtInBundles) {
|
|
7622
8111
|
for (const ext of bundle.extensions) {
|
|
7623
8112
|
if (ext.kind !== "formatter") continue;
|
|
7624
|
-
if (!isBuiltInExtensionEnabled(bundle, ext,
|
|
8113
|
+
if (!isBuiltInExtensionEnabled(bundle, ext, resolveEnabled)) continue;
|
|
7625
8114
|
out.push(ext);
|
|
7626
8115
|
}
|
|
7627
8116
|
}
|
|
7628
8117
|
}
|
|
7629
|
-
|
|
8118
|
+
for (const ext of opts.pluginRuntime.extensions.formatters) {
|
|
8119
|
+
if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) out.push(ext);
|
|
8120
|
+
}
|
|
7630
8121
|
return out;
|
|
7631
8122
|
}
|
|
7632
8123
|
function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
|
|
7633
8124
|
const noBuiltIns = options.noBuiltIns === true;
|
|
8125
|
+
const resolveEnabled = options.resolveEnabled ?? pluginRuntime.resolveEnabled;
|
|
8126
|
+
const granularityMap = buildGranularityMap(pluginRuntime.discovered);
|
|
7634
8127
|
if (!noBuiltIns) {
|
|
7635
|
-
const enabledBuiltIns = filterBuiltInManifests(
|
|
7636
|
-
listBuiltIns(),
|
|
7637
|
-
pluginRuntime.resolveEnabled
|
|
7638
|
-
);
|
|
8128
|
+
const enabledBuiltIns = filterBuiltInManifests(listBuiltIns(), resolveEnabled);
|
|
7639
8129
|
for (const manifest of enabledBuiltIns) kernel.registry.register(manifest);
|
|
7640
8130
|
}
|
|
7641
|
-
for (const manifest of pluginRuntime.manifests)
|
|
8131
|
+
for (const manifest of pluginRuntime.manifests) {
|
|
8132
|
+
if (!isPluginExtensionEnabled(manifest, granularityMap, resolveEnabled)) continue;
|
|
8133
|
+
kernel.registry.register(manifest);
|
|
8134
|
+
}
|
|
7642
8135
|
if (kernel.setRegisteredAnnotationKeys) {
|
|
7643
|
-
|
|
8136
|
+
const filteredAnnotations = pluginRuntime.annotationContributions.filter(
|
|
8137
|
+
(entry) => (
|
|
8138
|
+
// Annotation contributions live at plugin-id granularity (the
|
|
8139
|
+
// catalog row carries `pluginId`, not `extensionId`), so the
|
|
8140
|
+
// bundle-level toggle gates the entire row. Extension
|
|
8141
|
+
// granularity falls through to the manifest-level filter above
|
|
8142
|
+
// — this surface is bundle-scoped by design.
|
|
8143
|
+
resolveEnabled(entry.pluginId)
|
|
8144
|
+
)
|
|
8145
|
+
);
|
|
8146
|
+
kernel.setRegisteredAnnotationKeys(filteredAnnotations);
|
|
7644
8147
|
}
|
|
7645
8148
|
if (kernel.setRegisteredViewContributions) {
|
|
7646
|
-
const
|
|
8149
|
+
const userContribs = pluginRuntime.viewContributions.filter(
|
|
8150
|
+
(entry) => isPluginExtensionEnabled(
|
|
8151
|
+
{ pluginId: entry.pluginId, id: entry.extensionId },
|
|
8152
|
+
granularityMap,
|
|
8153
|
+
resolveEnabled
|
|
8154
|
+
)
|
|
8155
|
+
);
|
|
8156
|
+
const merged = [...userContribs];
|
|
7647
8157
|
if (!noBuiltIns) {
|
|
7648
8158
|
for (const bundle of builtInBundles) {
|
|
7649
8159
|
for (const ext of bundle.extensions) {
|
|
7650
|
-
if (!isBundleEntryEnabled(bundle, ext.id,
|
|
8160
|
+
if (!isBundleEntryEnabled(bundle, ext.id, resolveEnabled)) continue;
|
|
7651
8161
|
collectViewContributions(ext.pluginId, ext.id, ext, merged);
|
|
7652
8162
|
}
|
|
7653
8163
|
}
|
|
@@ -8002,6 +8512,15 @@ var CONFIG_TEXTS = {
|
|
|
8002
8512
|
*/
|
|
8003
8513
|
userOnlyKeyRejection: '{{glyph}} sm config: "{{key}}" is a user-scope key.\n {{hint}}\n',
|
|
8004
8514
|
userOnlyKeyRejectionHint: "Rerun with -g to write to ~/.skill-map/settings.json.",
|
|
8515
|
+
/**
|
|
8516
|
+
* Surfaced when a PROJECT_LOCAL_ONLY key (`allowEditSmFiles` /
|
|
8517
|
+
* `scan.includeHome` / `scan.extraRoots` / `scan.referencePaths`)
|
|
8518
|
+
* reaches the writer with `target: 'project'` — defensive only, the
|
|
8519
|
+
* CLI auto-routes to `project-local`, but the helper enforces the
|
|
8520
|
+
* rule for any other caller too.
|
|
8521
|
+
*/
|
|
8522
|
+
projectLocalOnlyKeyRejection: '{{glyph}} sm config: "{{key}}" is project-local only and cannot live in committed settings.json.\n {{hint}}\n',
|
|
8523
|
+
projectLocalOnlyKeyRejectionHint: "Writes to .skill-map/settings.local.json (gitignored), or -g for user scope.",
|
|
8005
8524
|
/**
|
|
8006
8525
|
* Surfaced when `sm config set` is invoked on a privacy-sensitive
|
|
8007
8526
|
* key (`scan.includeHome` / `scan.extraRoots` /
|
|
@@ -8040,8 +8559,14 @@ var CONFIG_TEXTS = {
|
|
|
8040
8559
|
|
|
8041
8560
|
// cli/commands/config.ts
|
|
8042
8561
|
function targetSettingsPath2(target, cwd, home) {
|
|
8043
|
-
const root = target === "user" ? home : cwd;
|
|
8044
|
-
return defaultSettingsPath(root);
|
|
8562
|
+
const root = target === "user" || target === "user-local" ? home : cwd;
|
|
8563
|
+
return target === "project-local" || target === "user-local" ? defaultLocalSettingsPath(root) : defaultSettingsPath(root);
|
|
8564
|
+
}
|
|
8565
|
+
function resolveWriteTarget(key, global) {
|
|
8566
|
+
if (PROJECT_LOCAL_ONLY_KEYS.has(key)) {
|
|
8567
|
+
return global ? "user" : "project-local";
|
|
8568
|
+
}
|
|
8569
|
+
return global ? "user" : "project";
|
|
8045
8570
|
}
|
|
8046
8571
|
function suggestConfigKey(effective, typed, ansi) {
|
|
8047
8572
|
const candidates = enumerateConfigPaths(effective);
|
|
@@ -8393,7 +8918,7 @@ var ConfigSetCommand = class extends SmCommand {
|
|
|
8393
8918
|
// eslint-disable-next-line complexity
|
|
8394
8919
|
async run() {
|
|
8395
8920
|
const ctx = defaultRuntimeContext();
|
|
8396
|
-
const target = this.global
|
|
8921
|
+
const target = resolveWriteTarget(this.key, this.global);
|
|
8397
8922
|
const path = targetSettingsPath2(target, ctx.cwd, ctx.homedir);
|
|
8398
8923
|
const stderr = this.context.stderr;
|
|
8399
8924
|
const stderrAnsi = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
@@ -8455,6 +8980,16 @@ var ConfigSetCommand = class extends SmCommand {
|
|
|
8455
8980
|
);
|
|
8456
8981
|
return ExitCode.Error;
|
|
8457
8982
|
}
|
|
8983
|
+
if (err instanceof ProjectLocalOnlyKeyError) {
|
|
8984
|
+
this.printer.info(
|
|
8985
|
+
tx(CONFIG_TEXTS.projectLocalOnlyKeyRejection, {
|
|
8986
|
+
glyph: errGlyph,
|
|
8987
|
+
key: err.key,
|
|
8988
|
+
hint: stderrAnsi.dim(CONFIG_TEXTS.projectLocalOnlyKeyRejectionHint)
|
|
8989
|
+
})
|
|
8990
|
+
);
|
|
8991
|
+
return ExitCode.Error;
|
|
8992
|
+
}
|
|
8458
8993
|
if (err instanceof ConfigValidationError) {
|
|
8459
8994
|
this.printer.info(
|
|
8460
8995
|
tx(CONFIG_TEXTS.invalidAfterSet, { glyph: errGlyph, errors: err.errors })
|
|
@@ -8497,7 +9032,7 @@ var ConfigResetCommand = class extends SmCommand {
|
|
|
8497
9032
|
// the value they gate.
|
|
8498
9033
|
async run() {
|
|
8499
9034
|
const ctx = defaultRuntimeContext();
|
|
8500
|
-
const target = this.global
|
|
9035
|
+
const target = resolveWriteTarget(this.key, this.global);
|
|
8501
9036
|
const path = targetSettingsPath2(target, ctx.cwd, ctx.homedir);
|
|
8502
9037
|
const stdout = this.context.stdout;
|
|
8503
9038
|
const ansi = ansiFor({ isTTY: stdout.isTTY === true, noColorFlag: this.noColor });
|
|
@@ -8541,6 +9076,16 @@ var ConfigResetCommand = class extends SmCommand {
|
|
|
8541
9076
|
);
|
|
8542
9077
|
return ExitCode.Error;
|
|
8543
9078
|
}
|
|
9079
|
+
if (err instanceof ProjectLocalOnlyKeyError) {
|
|
9080
|
+
this.printer.info(
|
|
9081
|
+
tx(CONFIG_TEXTS.projectLocalOnlyKeyRejection, {
|
|
9082
|
+
glyph: ansi.red("\u2715"),
|
|
9083
|
+
key: err.key,
|
|
9084
|
+
hint: ansi.dim(CONFIG_TEXTS.projectLocalOnlyKeyRejectionHint)
|
|
9085
|
+
})
|
|
9086
|
+
);
|
|
9087
|
+
return ExitCode.Error;
|
|
9088
|
+
}
|
|
8544
9089
|
if (err instanceof ConfigValidationError) {
|
|
8545
9090
|
this.printer.info(
|
|
8546
9091
|
tx(CONFIG_TEXTS.invalidAfterSet, { glyph: ansi.red("\u2715"), errors: err.errors })
|
|
@@ -9220,21 +9765,6 @@ import { chmod, copyFile, mkdir, rm } from "fs/promises";
|
|
|
9220
9765
|
import { dirname as dirname11, join as join7, resolve as resolve18 } from "path";
|
|
9221
9766
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
9222
9767
|
|
|
9223
|
-
// cli/util/confirm.ts
|
|
9224
|
-
import { createInterface } from "readline";
|
|
9225
|
-
var YES_PATTERN = new RegExp(UTIL_TEXTS.confirmYesPatternSource, "i");
|
|
9226
|
-
async function confirm(question, streams) {
|
|
9227
|
-
const rl = createInterface({ input: streams.stdin, output: streams.stderr });
|
|
9228
|
-
try {
|
|
9229
|
-
const answer = await new Promise(
|
|
9230
|
-
(resolveP) => rl.question(`${question}${UTIL_TEXTS.confirmPromptSuffix}`, resolveP)
|
|
9231
|
-
);
|
|
9232
|
-
return YES_PATTERN.test(answer.trim());
|
|
9233
|
-
} finally {
|
|
9234
|
-
rl.close();
|
|
9235
|
-
}
|
|
9236
|
-
}
|
|
9237
|
-
|
|
9238
9768
|
// cli/i18n/db.texts.ts
|
|
9239
9769
|
var DB_TEXTS = {
|
|
9240
9770
|
// --- reset -----------------------------------------------------------
|
|
@@ -11481,7 +12011,7 @@ async function runScanInternal(_kernel, options) {
|
|
|
11481
12011
|
for (const analyzer of exts.analyzers ?? []) {
|
|
11482
12012
|
if (analyzer.viewContributions === void 0) continue;
|
|
11483
12013
|
for (const node of walked.nodes) {
|
|
11484
|
-
walked.freshlyRunTuples.add(`${analyzer.pluginId}
|
|
12014
|
+
walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
|
|
11485
12015
|
}
|
|
11486
12016
|
}
|
|
11487
12017
|
for (const issue of walked.frontmatterIssues) issues.push(issue);
|
|
@@ -11697,8 +12227,10 @@ function computeCacheDecision(opts) {
|
|
|
11697
12227
|
const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? /* @__PURE__ */ new Map();
|
|
11698
12228
|
for (const ex of applicableExtractors) {
|
|
11699
12229
|
const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
|
|
11700
|
-
const
|
|
11701
|
-
|
|
12230
|
+
const prior = priorRunsForNode.get(qualified);
|
|
12231
|
+
const bodyMatch = prior !== void 0 && prior.bodyHash === opts.bodyHash;
|
|
12232
|
+
const sidecarOk = prior !== void 0 && prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash;
|
|
12233
|
+
if (opts.nodeHashCacheEligible && bodyMatch && sidecarOk) {
|
|
11702
12234
|
cachedQualifiedIds.add(qualified);
|
|
11703
12235
|
} else {
|
|
11704
12236
|
missingExtractors.push(ex);
|
|
@@ -11743,7 +12275,8 @@ function reusePriorNode(opts) {
|
|
|
11743
12275
|
nodePath: opts.priorNode.path,
|
|
11744
12276
|
extractorId: qualified,
|
|
11745
12277
|
bodyHashAtRun: opts.bodyHash,
|
|
11746
|
-
ranAt
|
|
12278
|
+
ranAt,
|
|
12279
|
+
sidecarAnnotationsHashAtRun: opts.sidecarAnnotationsHash
|
|
11747
12280
|
});
|
|
11748
12281
|
}
|
|
11749
12282
|
return { ...base, extractorRuns };
|
|
@@ -11829,11 +12362,22 @@ async function walkAndExtract(opts) {
|
|
|
11829
12362
|
}
|
|
11830
12363
|
claimedPaths.add(raw.path);
|
|
11831
12364
|
index += 1;
|
|
12365
|
+
const sidecarResolution = resolveSidecarOverlay(
|
|
12366
|
+
raw.path,
|
|
12367
|
+
raw.path,
|
|
12368
|
+
roots,
|
|
12369
|
+
bodyHash,
|
|
12370
|
+
frontmatterHash
|
|
12371
|
+
);
|
|
12372
|
+
const sidecarAnnotationsHash = sha256(
|
|
12373
|
+
canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
|
|
12374
|
+
);
|
|
11832
12375
|
const cacheDecision = computeCacheDecision({
|
|
11833
12376
|
extractors,
|
|
11834
12377
|
kind,
|
|
11835
12378
|
nodePath: raw.path,
|
|
11836
12379
|
bodyHash,
|
|
12380
|
+
sidecarAnnotationsHash,
|
|
11837
12381
|
nodeHashCacheEligible,
|
|
11838
12382
|
priorExtractorRuns
|
|
11839
12383
|
});
|
|
@@ -11844,10 +12388,20 @@ async function walkAndExtract(opts) {
|
|
|
11844
12388
|
missingExtractors,
|
|
11845
12389
|
fullCacheHit
|
|
11846
12390
|
} = cacheDecision;
|
|
12391
|
+
const attachSidecar = (node2) => {
|
|
12392
|
+
node2.sidecar = sidecarResolution.overlay;
|
|
12393
|
+
if (sidecarResolution.parsedRoot !== null) {
|
|
12394
|
+
sidecarRoots.set(node2.path, sidecarResolution.parsedRoot);
|
|
12395
|
+
}
|
|
12396
|
+
return sidecarResolution.issues.map(
|
|
12397
|
+
(i) => i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node2.path] }
|
|
12398
|
+
);
|
|
12399
|
+
};
|
|
11847
12400
|
if (fullCacheHit && priorNode) {
|
|
11848
12401
|
const reused = reusePriorNode({
|
|
11849
12402
|
priorNode,
|
|
11850
12403
|
bodyHash,
|
|
12404
|
+
sidecarAnnotationsHash,
|
|
11851
12405
|
strict,
|
|
11852
12406
|
cachedQualifiedIds,
|
|
11853
12407
|
applicableQualifiedIds,
|
|
@@ -11855,14 +12409,7 @@ async function walkAndExtract(opts) {
|
|
|
11855
12409
|
priorLinksByOriginating,
|
|
11856
12410
|
priorFrontmatterIssuesByNode
|
|
11857
12411
|
});
|
|
11858
|
-
const reusedSidecarIssues =
|
|
11859
|
-
reused.node,
|
|
11860
|
-
raw.path,
|
|
11861
|
-
roots,
|
|
11862
|
-
bodyHash,
|
|
11863
|
-
frontmatterHash,
|
|
11864
|
-
sidecarRoots
|
|
11865
|
-
);
|
|
12412
|
+
const reusedSidecarIssues = attachSidecar(reused.node);
|
|
11866
12413
|
nodes.push(reused.node);
|
|
11867
12414
|
cachedPaths.add(reused.node.path);
|
|
11868
12415
|
for (const link2 of reused.internalLinks) internalLinks.push(link2);
|
|
@@ -11903,14 +12450,7 @@ async function walkAndExtract(opts) {
|
|
|
11903
12450
|
nodes.push(node);
|
|
11904
12451
|
for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
|
|
11905
12452
|
}
|
|
11906
|
-
const sidecarIssues =
|
|
11907
|
-
node,
|
|
11908
|
-
raw.path,
|
|
11909
|
-
roots,
|
|
11910
|
-
bodyHash,
|
|
11911
|
-
frontmatterHash,
|
|
11912
|
-
sidecarRoots
|
|
11913
|
-
);
|
|
12453
|
+
const sidecarIssues = attachSidecar(node);
|
|
11914
12454
|
for (const issue of sidecarIssues) frontmatterIssues.push(issue);
|
|
11915
12455
|
emitter.emit(makeEvent("scan.progress", {
|
|
11916
12456
|
index,
|
|
@@ -11921,7 +12461,7 @@ async function walkAndExtract(opts) {
|
|
|
11921
12461
|
}));
|
|
11922
12462
|
const extractorsToRun = partialCacheHit ? missingExtractors : applicableExtractors;
|
|
11923
12463
|
for (const ex of extractorsToRun) {
|
|
11924
|
-
freshlyRunTuples.add(`${ex.pluginId}
|
|
12464
|
+
freshlyRunTuples.add(`${ex.pluginId}\0${ex.id}\0${node.path}`);
|
|
11925
12465
|
}
|
|
11926
12466
|
const extractResult = await runExtractorsForNode({
|
|
11927
12467
|
extractors: extractorsToRun,
|
|
@@ -11945,7 +12485,8 @@ async function walkAndExtract(opts) {
|
|
|
11945
12485
|
nodePath: node.path,
|
|
11946
12486
|
extractorId: qualified,
|
|
11947
12487
|
bodyHashAtRun: bodyHash,
|
|
11948
|
-
ranAt
|
|
12488
|
+
ranAt,
|
|
12489
|
+
sidecarAnnotationsHashAtRun: sidecarAnnotationsHash
|
|
11949
12490
|
});
|
|
11950
12491
|
}
|
|
11951
12492
|
}
|
|
@@ -12246,30 +12787,42 @@ function canonicalFrontmatter(parsed, raw) {
|
|
|
12246
12787
|
noCompatMode: true
|
|
12247
12788
|
});
|
|
12248
12789
|
}
|
|
12249
|
-
function
|
|
12790
|
+
function canonicalSidecarAnnotations(annotations) {
|
|
12791
|
+
if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
|
|
12792
|
+
return yaml4.dump({}, { sortKeys: true, lineWidth: -1, noRefs: true, noCompatMode: true });
|
|
12793
|
+
}
|
|
12794
|
+
return yaml4.dump(annotations, {
|
|
12795
|
+
sortKeys: true,
|
|
12796
|
+
lineWidth: -1,
|
|
12797
|
+
noRefs: true,
|
|
12798
|
+
noCompatMode: true
|
|
12799
|
+
});
|
|
12800
|
+
}
|
|
12801
|
+
function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyHash, liveFrontmatterHash) {
|
|
12250
12802
|
const issues = [];
|
|
12251
12803
|
const mdAbs = resolveAbsoluteMdPath(relativePath2, roots);
|
|
12252
12804
|
if (mdAbs === null) {
|
|
12253
|
-
|
|
12254
|
-
return issues;
|
|
12805
|
+
return { overlay: { present: false }, issues, parsedRoot: null };
|
|
12255
12806
|
}
|
|
12256
12807
|
const result = readSidecarFor(mdAbs);
|
|
12257
12808
|
if (!result.present) {
|
|
12258
|
-
|
|
12259
|
-
return issues;
|
|
12809
|
+
return { overlay: { present: false }, issues, parsedRoot: null };
|
|
12260
12810
|
}
|
|
12261
12811
|
if (result.parsed === null) {
|
|
12262
|
-
node.sidecar = { present: true, status: null, annotations: null, root: null };
|
|
12263
12812
|
for (const parseIssue of result.issues) {
|
|
12264
12813
|
issues.push({
|
|
12265
12814
|
analyzerId: "invalid-sidecar",
|
|
12266
12815
|
severity: "warn",
|
|
12267
|
-
nodeIds: [
|
|
12816
|
+
nodeIds: [nodePathForIssue],
|
|
12268
12817
|
message: parseIssue.message,
|
|
12269
12818
|
data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
|
|
12270
12819
|
});
|
|
12271
12820
|
}
|
|
12272
|
-
return
|
|
12821
|
+
return {
|
|
12822
|
+
overlay: { present: true, status: null, annotations: null, root: null },
|
|
12823
|
+
issues,
|
|
12824
|
+
parsedRoot: null
|
|
12825
|
+
};
|
|
12273
12826
|
}
|
|
12274
12827
|
const status = computeDriftStatus({
|
|
12275
12828
|
storedBodyHash: result.parsed.identityBodyHash,
|
|
@@ -12277,14 +12830,22 @@ function resolveAndApplySidecar(node, relativePath2, roots, liveBodyHash, liveFr
|
|
|
12277
12830
|
liveBodyHash,
|
|
12278
12831
|
liveFrontmatterHash
|
|
12279
12832
|
});
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12833
|
+
return {
|
|
12834
|
+
// R15 closure (2026-05-07) — surface the full parsed root on the
|
|
12835
|
+
// overlay so BFF consumers (UI inspector audit / plugin-contributions
|
|
12836
|
+
// / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
|
|
12837
|
+
// plugin-namespaced sub-keys without re-reading the file. The
|
|
12838
|
+
// `annotations` field above stays — it duplicates `root.annotations`
|
|
12839
|
+
// by design so existing consumers keep working unchanged.
|
|
12840
|
+
overlay: {
|
|
12841
|
+
present: true,
|
|
12842
|
+
status,
|
|
12843
|
+
annotations: result.parsed.annotations,
|
|
12844
|
+
root: result.parsed.raw
|
|
12845
|
+
},
|
|
12846
|
+
issues,
|
|
12847
|
+
parsedRoot: result.parsed.raw
|
|
12285
12848
|
};
|
|
12286
|
-
sidecarRoots.set(node.path, result.parsed.raw);
|
|
12287
|
-
return issues;
|
|
12288
12849
|
}
|
|
12289
12850
|
function resolveAbsoluteMdPath(relativePath2, roots) {
|
|
12290
12851
|
if (isAbsolute6(relativePath2)) {
|
|
@@ -12951,8 +13512,13 @@ function registerExtensions(kernel, pluginRuntime, opts) {
|
|
|
12951
13512
|
pluginRuntime
|
|
12952
13513
|
};
|
|
12953
13514
|
if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
|
|
13515
|
+
if (opts.resolveEnabledOverride) composeOpts.resolveEnabled = opts.resolveEnabledOverride;
|
|
12954
13516
|
const extensions = composeScanExtensions(composeOpts);
|
|
12955
|
-
|
|
13517
|
+
const registerOpts = {
|
|
13518
|
+
noBuiltIns: opts.noBuiltIns
|
|
13519
|
+
};
|
|
13520
|
+
if (opts.resolveEnabledOverride) registerOpts.resolveEnabled = opts.resolveEnabledOverride;
|
|
13521
|
+
registerEnabledExtensions(kernel, pluginRuntime, registerOpts);
|
|
12956
13522
|
return extensions;
|
|
12957
13523
|
}
|
|
12958
13524
|
function buildScanIgnoreFilter(cfg, cwd) {
|
|
@@ -15076,11 +15642,14 @@ function renderListHuman(builtIns2, plugins, ansi) {
|
|
|
15076
15642
|
return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
|
|
15077
15643
|
}
|
|
15078
15644
|
function builtInToListRow(b) {
|
|
15645
|
+
const names = b.granularity === "extension" ? b.extensions.map(
|
|
15646
|
+
(e) => e.enabled ? e.id : `${PLUGINS_TEXTS.rowGlyphOff} ${e.id}`
|
|
15647
|
+
) : b.extensions.map((e) => e.id);
|
|
15079
15648
|
return {
|
|
15080
15649
|
id: b.id,
|
|
15081
15650
|
enabled: b.enabled,
|
|
15082
15651
|
source: PLUGINS_TEXTS.sourceBuiltIn,
|
|
15083
|
-
names
|
|
15652
|
+
names
|
|
15084
15653
|
};
|
|
15085
15654
|
}
|
|
15086
15655
|
function pluginToListRow(p) {
|
|
@@ -15132,7 +15701,17 @@ var PluginsShowCommand = class extends SmCommand {
|
|
|
15132
15701
|
static paths = [["plugins", "show"]];
|
|
15133
15702
|
static usage = Command16.Usage({
|
|
15134
15703
|
category: "Plugins",
|
|
15135
|
-
description: "Show a single plugin's manifest + loaded extensions."
|
|
15704
|
+
description: "Show a single plugin's manifest + loaded extensions.",
|
|
15705
|
+
details: `
|
|
15706
|
+
Accepts a bundle / plugin id (\`core\`, \`claude\`, \`my-plugin\`)
|
|
15707
|
+
or a qualified extension id (\`core/<ext-id>\`,
|
|
15708
|
+
\`<plugin>/<ext-id>\`). When given a qualified id, validates the
|
|
15709
|
+
extension exists and renders the parent bundle's detail (which
|
|
15710
|
+
lists every extension with per-extension status for
|
|
15711
|
+
granularity=extension bundles like \`core\`). The same id shapes
|
|
15712
|
+
\`sm plugins enable\` and \`sm plugins disable\` accept resolve
|
|
15713
|
+
cleanly here too.
|
|
15714
|
+
`
|
|
15136
15715
|
});
|
|
15137
15716
|
id = Option16.String({ required: true });
|
|
15138
15717
|
pluginDir = Option16.String("--plugin-dir", { required: false });
|
|
@@ -15140,16 +15719,22 @@ var PluginsShowCommand = class extends SmCommand {
|
|
|
15140
15719
|
const plugins = await loadAll({ global: this.global, pluginDir: this.pluginDir });
|
|
15141
15720
|
const resolveEnabled = await buildResolver(this.global);
|
|
15142
15721
|
const builtIns2 = builtInRows(resolveEnabled);
|
|
15143
|
-
const
|
|
15144
|
-
const
|
|
15722
|
+
const stderr = this.context.stderr;
|
|
15723
|
+
const stderrAnsi = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
15724
|
+
const lookupResult = resolveShowLookupId(this.id, builtIns2, plugins, stderrAnsi);
|
|
15725
|
+
if ("error" in lookupResult) {
|
|
15726
|
+
this.printer.error(lookupResult.error);
|
|
15727
|
+
return ExitCode.NotFound;
|
|
15728
|
+
}
|
|
15729
|
+
const lookupId = lookupResult.bundleId;
|
|
15730
|
+
const builtIn = builtIns2.find((b) => b.id === lookupId);
|
|
15731
|
+
const match = plugins.find((p) => p.id === lookupId);
|
|
15145
15732
|
if (!builtIn && !match) {
|
|
15146
|
-
const stderr = this.context.stderr;
|
|
15147
|
-
const ansi2 = ansiFor({ isTTY: stderr.isTTY === true, noColorFlag: this.noColor });
|
|
15148
15733
|
this.printer.error(
|
|
15149
15734
|
tx(PLUGINS_TEXTS.pluginNotFound, {
|
|
15150
|
-
glyph:
|
|
15735
|
+
glyph: stderrAnsi.red("\u2715"),
|
|
15151
15736
|
id: sanitizeForTerminal(this.id),
|
|
15152
|
-
hint:
|
|
15737
|
+
hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
|
|
15153
15738
|
})
|
|
15154
15739
|
);
|
|
15155
15740
|
return ExitCode.NotFound;
|
|
@@ -15166,6 +15751,44 @@ var PluginsShowCommand = class extends SmCommand {
|
|
|
15166
15751
|
return ExitCode.Ok;
|
|
15167
15752
|
}
|
|
15168
15753
|
};
|
|
15754
|
+
function resolveShowLookupId(id, builtIns2, plugins, ansi) {
|
|
15755
|
+
if (!id.includes("/")) return { bundleId: id };
|
|
15756
|
+
const errGlyph = ansi.red("\u2715");
|
|
15757
|
+
const [bundleId, extId, ...rest] = id.split("/");
|
|
15758
|
+
if (!bundleId || !extId || rest.length > 0) {
|
|
15759
|
+
return {
|
|
15760
|
+
error: tx(PLUGINS_TEXTS.qualifiedIdUnknownBundle, {
|
|
15761
|
+
glyph: errGlyph,
|
|
15762
|
+
bundleId: sanitizeForTerminal(id),
|
|
15763
|
+
hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownBundleHint)
|
|
15764
|
+
})
|
|
15765
|
+
};
|
|
15766
|
+
}
|
|
15767
|
+
const builtIn = builtIns2.find((b) => b.id === bundleId);
|
|
15768
|
+
const userPlugin = plugins.find((p) => p.id === bundleId);
|
|
15769
|
+
if (!builtIn && !userPlugin) {
|
|
15770
|
+
return {
|
|
15771
|
+
error: tx(PLUGINS_TEXTS.qualifiedIdUnknownBundle, {
|
|
15772
|
+
glyph: errGlyph,
|
|
15773
|
+
bundleId: sanitizeForTerminal(bundleId),
|
|
15774
|
+
hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownBundleHint)
|
|
15775
|
+
})
|
|
15776
|
+
};
|
|
15777
|
+
}
|
|
15778
|
+
const knownExts = builtIn ? builtIn.extensions.map((e) => e.id) : userPlugin?.extensions?.map((e) => e.id) ?? [];
|
|
15779
|
+
if (!knownExts.includes(extId)) {
|
|
15780
|
+
return {
|
|
15781
|
+
error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
|
|
15782
|
+
glyph: errGlyph,
|
|
15783
|
+
id: sanitizeForTerminal(id),
|
|
15784
|
+
bundleId: sanitizeForTerminal(bundleId),
|
|
15785
|
+
extId: sanitizeForTerminal(extId),
|
|
15786
|
+
hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
|
|
15787
|
+
})
|
|
15788
|
+
};
|
|
15789
|
+
}
|
|
15790
|
+
return { bundleId };
|
|
15791
|
+
}
|
|
15169
15792
|
function renderBuiltInDetail(b, ansi) {
|
|
15170
15793
|
const enabled = b.enabled;
|
|
15171
15794
|
const glyph = enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
|
|
@@ -15677,6 +16300,14 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
15677
16300
|
await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
15678
16301
|
for (const id of targets) {
|
|
15679
16302
|
await adapter.pluginConfig.set(id, enabled);
|
|
16303
|
+
if (!enabled) {
|
|
16304
|
+
const slash = id.indexOf("/");
|
|
16305
|
+
if (slash < 0) {
|
|
16306
|
+
await adapter.contributions.purgeByPlugin(id);
|
|
16307
|
+
} else {
|
|
16308
|
+
await adapter.contributions.purgeByPlugin(id.slice(0, slash), id.slice(slash + 1));
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
15680
16311
|
}
|
|
15681
16312
|
});
|
|
15682
16313
|
const verbPast = enabled ? "enabled" : "disabled";
|
|
@@ -15746,7 +16377,7 @@ function omitModule(key, value) {
|
|
|
15746
16377
|
var VIEW_SLOTS_CATALOG = [
|
|
15747
16378
|
{ id: "card.title.right", summary: "Small icon marker next to the card title \u2014 language flag, platform glyph." },
|
|
15748
16379
|
{ id: "card.subtitle.left", summary: "Single non-negative integer in the card subtitle row." },
|
|
15749
|
-
{ id: "card.footer.left
|
|
16380
|
+
{ id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
|
|
15750
16381
|
{ id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
|
|
15751
16382
|
{ id: "graph.node.alert", summary: "Corner badge decoration on the graph node \u2014 alert / status." },
|
|
15752
16383
|
{ id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
|
|
@@ -15757,7 +16388,7 @@ var VIEW_SLOTS_CATALOG = [
|
|
|
15757
16388
|
{ id: "inspector.body.panel.key-values", summary: "Flat key/value pairs (\u2264 50) in the inspector body." },
|
|
15758
16389
|
{ id: "inspector.body.panel.link-list", summary: "Clickable scope-relative paths (\u2264 100) in the inspector body." },
|
|
15759
16390
|
{ id: "inspector.body.panel.markdown", summary: "Sanitized markdown text (\u2264 4096 chars) in the inspector body." },
|
|
15760
|
-
{ id: "topbar.
|
|
16391
|
+
{ id: "topbar.nav.start", summary: "Scope-wide indicator chip at the start of the topbar nav (before the view-switcher links)." }
|
|
15761
16392
|
];
|
|
15762
16393
|
var INPUT_TYPES_CATALOG = [
|
|
15763
16394
|
{ id: "string-list", summary: "Array of free-form strings." },
|
|
@@ -15776,7 +16407,7 @@ var PluginsCreateCommand = class extends SmCommand {
|
|
|
15776
16407
|
static usage = Command16.Usage({
|
|
15777
16408
|
category: "Plugins",
|
|
15778
16409
|
description: "Scaffold a new plugin directory.",
|
|
15779
|
-
details: "Emits plugin.json + extension stub + README. Pre-filled with one view contribution (slot `card.footer.left
|
|
16410
|
+
details: "Emits plugin.json + extension stub + README. Pre-filled with one view contribution (slot `card.footer.left`) and one setting (`string-list`); edit to taste. Use `sm plugins slots list` to see other options."
|
|
15780
16411
|
});
|
|
15781
16412
|
pluginId = Option16.String({ required: true, name: "plugin-id" });
|
|
15782
16413
|
at = Option16.String("--at", { required: false });
|
|
@@ -15845,7 +16476,7 @@ function scaffolderExtractorStub(pluginId) {
|
|
|
15845
16476
|
* export missing a string \\\`kind\\\` field\`.
|
|
15846
16477
|
*
|
|
15847
16478
|
* Declared view contributions (in plugin.json):
|
|
15848
|
-
* - 'count' \u2192 slot \`card.footer.left
|
|
16479
|
+
* - 'count' \u2192 slot \`card.footer.left\` (renders as a chip
|
|
15849
16480
|
* in the left footer of the node card)
|
|
15850
16481
|
*
|
|
15851
16482
|
* Declared settings:
|
|
@@ -15868,7 +16499,7 @@ export default {
|
|
|
15868
16499
|
|
|
15869
16500
|
viewContributions: {
|
|
15870
16501
|
count: {
|
|
15871
|
-
slot: 'card.footer.left
|
|
16502
|
+
slot: 'card.footer.left',
|
|
15872
16503
|
icon: '\u{1F50D}',
|
|
15873
16504
|
label: 'kw',
|
|
15874
16505
|
emitWhenEmpty: false,
|
|
@@ -16321,6 +16952,19 @@ var SCAN_TEXTS = {
|
|
|
16321
16952
|
// cli/commands/watch.ts
|
|
16322
16953
|
import { Command as Command18, Option as Option18 } from "clipanion";
|
|
16323
16954
|
|
|
16955
|
+
// core/runtime/fresh-resolver.ts
|
|
16956
|
+
async function buildFreshResolver(deps) {
|
|
16957
|
+
const overrides = await tryWithSqlite(
|
|
16958
|
+
{ databasePath: deps.databasePath, autoBackup: false },
|
|
16959
|
+
async (adapter) => adapter.pluginConfig.loadOverrideMap()
|
|
16960
|
+
);
|
|
16961
|
+
if (overrides === null) return deps.fallbackResolver;
|
|
16962
|
+
return makeEnabledResolver(deps.effectiveConfig(), overrides);
|
|
16963
|
+
}
|
|
16964
|
+
function composeResolver(effectiveConfig, overrides) {
|
|
16965
|
+
return makeEnabledResolver(effectiveConfig, overrides);
|
|
16966
|
+
}
|
|
16967
|
+
|
|
16324
16968
|
// core/watcher/i18n/runtime.texts.ts
|
|
16325
16969
|
var RUNTIME_TEXTS = {
|
|
16326
16970
|
/**
|
|
@@ -16388,8 +17032,16 @@ function createWatcherRuntime(opts) {
|
|
|
16388
17032
|
events.onPluginWarning?.(warn);
|
|
16389
17033
|
}
|
|
16390
17034
|
const runOnePass = async () => {
|
|
17035
|
+
const resolveEnabledOverride = await buildFreshResolver({
|
|
17036
|
+
databasePath: opts.dbPath,
|
|
17037
|
+
effectiveConfig: () => cfg,
|
|
17038
|
+
fallbackResolver: pluginRuntime.resolveEnabled
|
|
17039
|
+
});
|
|
16391
17040
|
const kernel = createKernel();
|
|
16392
|
-
registerEnabledExtensions(kernel, pluginRuntime, {
|
|
17041
|
+
registerEnabledExtensions(kernel, pluginRuntime, {
|
|
17042
|
+
noBuiltIns: opts.noBuiltIns,
|
|
17043
|
+
resolveEnabled: resolveEnabledOverride
|
|
17044
|
+
});
|
|
16393
17045
|
const emitter = opts.emitterFactory();
|
|
16394
17046
|
const priorState = await tryWithSqlite(
|
|
16395
17047
|
{ databasePath: opts.dbPath, autoBackup: false },
|
|
@@ -16411,7 +17063,8 @@ function createWatcherRuntime(opts) {
|
|
|
16411
17063
|
);
|
|
16412
17064
|
const composeOpts = {
|
|
16413
17065
|
noBuiltIns: opts.noBuiltIns,
|
|
16414
|
-
pluginRuntime
|
|
17066
|
+
pluginRuntime,
|
|
17067
|
+
resolveEnabled: resolveEnabledOverride
|
|
16415
17068
|
};
|
|
16416
17069
|
if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
|
|
16417
17070
|
const composed = composeScanExtensions(composeOpts);
|
|
@@ -17305,6 +17958,48 @@ import { WebSocketServer } from "ws";
|
|
|
17305
17958
|
import { Hono } from "hono";
|
|
17306
17959
|
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
17307
17960
|
|
|
17961
|
+
// core/config/service.ts
|
|
17962
|
+
var ConfigService = class {
|
|
17963
|
+
#opts;
|
|
17964
|
+
#cache = null;
|
|
17965
|
+
constructor(opts) {
|
|
17966
|
+
this.#opts = opts;
|
|
17967
|
+
}
|
|
17968
|
+
/**
|
|
17969
|
+
* Return the cached `ILoadedConfig` (loading on first call).
|
|
17970
|
+
* Subsequent calls return the same object reference — callers
|
|
17971
|
+
* MUST treat it as read-only.
|
|
17972
|
+
*/
|
|
17973
|
+
get() {
|
|
17974
|
+
if (this.#cache === null) {
|
|
17975
|
+
this.#cache = loadConfig({
|
|
17976
|
+
scope: this.#opts.scope,
|
|
17977
|
+
cwd: this.#opts.cwd,
|
|
17978
|
+
homedir: this.#opts.homedir,
|
|
17979
|
+
...this.#opts.strict ? { strict: true } : {}
|
|
17980
|
+
});
|
|
17981
|
+
}
|
|
17982
|
+
return this.#cache;
|
|
17983
|
+
}
|
|
17984
|
+
/**
|
|
17985
|
+
* Sugar for `this.get().effective` — the most common consumer pattern
|
|
17986
|
+
* (the `sources` / `warnings` slots are only relevant to the
|
|
17987
|
+
* `GET /api/config` and `sm config show` paths).
|
|
17988
|
+
*/
|
|
17989
|
+
effective() {
|
|
17990
|
+
return this.get().effective;
|
|
17991
|
+
}
|
|
17992
|
+
/**
|
|
17993
|
+
* Drop the cached `ILoadedConfig`. Next `get()` re-reads every layer
|
|
17994
|
+
* from disk. Called by routes after a successful `writeConfigValue`
|
|
17995
|
+
* (PATCH preferences / project-preferences) and by the sidecar
|
|
17996
|
+
* consent gate after it flips `allowEditSmFiles` to `true`.
|
|
17997
|
+
*/
|
|
17998
|
+
reload() {
|
|
17999
|
+
this.#cache = null;
|
|
18000
|
+
}
|
|
18001
|
+
};
|
|
18002
|
+
|
|
17308
18003
|
// server/i18n/server.texts.ts
|
|
17309
18004
|
var SERVER_TEXTS = {
|
|
17310
18005
|
// Boot banner — printed by the server itself when it begins to listen.
|
|
@@ -17383,6 +18078,15 @@ var SERVER_TEXTS = {
|
|
|
17383
18078
|
sidecarBodyNotObject: "Request body must be a JSON object.",
|
|
17384
18079
|
sidecarNodePathRequired: "`nodePath` is required and must be a non-empty string.",
|
|
17385
18080
|
sidecarForceMustBeBoolean: "`force` must be a boolean when present.",
|
|
18081
|
+
sidecarConfirmMustBeBoolean: "`confirm` must be a boolean when present.",
|
|
18082
|
+
/**
|
|
18083
|
+
* 412 envelope when `POST /api/sidecar/bump` would create a `.sm`
|
|
18084
|
+
* file but `allowEditSmFiles` is still false. The UI's bump
|
|
18085
|
+
* call-path catches `code: 'confirm-required'` and opens a
|
|
18086
|
+
* `ConfirmationService` dialog explaining `.sm` writes; on accept
|
|
18087
|
+
* it retries with `confirm: true` in the body.
|
|
18088
|
+
*/
|
|
18089
|
+
sidecarConsentRequired: "consent required to write .sm sidecar files in this project. Retry with `confirm: true` to grant (writes to .skill-map/settings.local.json \u2014 gitignored).",
|
|
17386
18090
|
// 500 envelope when the built-in bump action ships without an
|
|
17387
18091
|
// `invoke()` — should be impossible in production but the route
|
|
17388
18092
|
// throws a typed envelope rather than a bare `Error` so the global
|
|
@@ -17422,6 +18126,12 @@ var SERVER_TEXTS = {
|
|
|
17422
18126
|
// disabling the toggle.
|
|
17423
18127
|
pluginsLocked: 'Plugin "{{id}}" is locked by the host and cannot be toggled.',
|
|
17424
18128
|
pluginsExtensionLocked: 'Extension "{{bundleId}}/{{extensionId}}" is locked by the host and cannot be toggled.',
|
|
18129
|
+
// 400 envelopes specific to the bulk `PATCH /api/plugins` endpoint.
|
|
18130
|
+
// The single-id variants above still apply for per-entry validation
|
|
18131
|
+
// (unknown id, granularity mismatch, lock); these cover the
|
|
18132
|
+
// body-shape level.
|
|
18133
|
+
pluginsChangesRequired: "Request body must include a `changes` array of `{ id, enabled }` entries.",
|
|
18134
|
+
pluginsChangeMalformed: "Each entry in `changes` must have a string `id` and a boolean `enabled`.",
|
|
17425
18135
|
// ---- preferences route (routes/preferences.ts) --------------------------
|
|
17426
18136
|
//
|
|
17427
18137
|
// GET / PATCH /api/preferences. The PATCH body is shaped
|
|
@@ -17576,11 +18286,7 @@ function registerConfigRoute(app, deps) {
|
|
|
17576
18286
|
app.get("/api/config", (c) => {
|
|
17577
18287
|
let loaded;
|
|
17578
18288
|
try {
|
|
17579
|
-
loaded =
|
|
17580
|
-
scope: deps.options.scope,
|
|
17581
|
-
cwd: deps.runtimeContext.cwd,
|
|
17582
|
-
homedir: deps.runtimeContext.homedir
|
|
17583
|
-
});
|
|
18289
|
+
loaded = deps.configService.get();
|
|
17584
18290
|
} catch (err) {
|
|
17585
18291
|
throw new HTTPException(500, { message: formatErrorMessage(err) });
|
|
17586
18292
|
}
|
|
@@ -18109,7 +18815,7 @@ async function groupContributionsByPath(rows) {
|
|
|
18109
18815
|
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
18110
18816
|
function registerPluginsRoute(app, deps) {
|
|
18111
18817
|
app.get("/api/plugins", async (c) => {
|
|
18112
|
-
const resolveEnabled = await
|
|
18818
|
+
const resolveEnabled = await buildFreshResolver2(deps);
|
|
18113
18819
|
const items = listItems(deps, resolveEnabled);
|
|
18114
18820
|
return c.json(
|
|
18115
18821
|
buildListEnvelope({
|
|
@@ -18176,6 +18882,26 @@ function registerPluginsRoute(app, deps) {
|
|
|
18176
18882
|
const body = await parsePatchBody(c.req.raw);
|
|
18177
18883
|
return await persistAndProject(c, deps, qualified, body.enabled);
|
|
18178
18884
|
});
|
|
18885
|
+
app.patch("/api/plugins", async (c) => {
|
|
18886
|
+
const changes = await parseBulkBody(c.req.raw);
|
|
18887
|
+
for (const change of changes) {
|
|
18888
|
+
const failure = validateBulkChange(change, deps);
|
|
18889
|
+
if (failure !== null) {
|
|
18890
|
+
return c.json(
|
|
18891
|
+
{
|
|
18892
|
+
ok: false,
|
|
18893
|
+
error: {
|
|
18894
|
+
code: failure.code,
|
|
18895
|
+
message: failure.message,
|
|
18896
|
+
details: { id: change.id }
|
|
18897
|
+
}
|
|
18898
|
+
},
|
|
18899
|
+
failure.status
|
|
18900
|
+
);
|
|
18901
|
+
}
|
|
18902
|
+
}
|
|
18903
|
+
return await persistBulkAndProject(c, deps, changes);
|
|
18904
|
+
});
|
|
18179
18905
|
}
|
|
18180
18906
|
function listItems(deps, resolveEnabled) {
|
|
18181
18907
|
return [
|
|
@@ -18230,7 +18956,8 @@ function buildDiscoveredItem(plugin, deps, resolveEnabled) {
|
|
|
18230
18956
|
source: classifyPluginSource(plugin.path, deps),
|
|
18231
18957
|
granularity,
|
|
18232
18958
|
...optional,
|
|
18233
|
-
...bundleLocked ? { locked: true } : {}
|
|
18959
|
+
...bundleLocked ? { locked: true } : {},
|
|
18960
|
+
...plugin.status === "disabled" ? { startsAsDisabled: true } : {}
|
|
18234
18961
|
};
|
|
18235
18962
|
}
|
|
18236
18963
|
function optionalDiscoveredFields(plugin, extensions) {
|
|
@@ -18300,10 +19027,26 @@ async function persistAndProject(c, deps, configKey, enabled) {
|
|
|
18300
19027
|
const overrides = await tryWithSqlite(
|
|
18301
19028
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
18302
19029
|
async (adapter) => {
|
|
18303
|
-
await adapter
|
|
19030
|
+
await applyChangeToAdapter(adapter, configKey, enabled);
|
|
18304
19031
|
return await adapter.pluginConfig.loadOverrideMap();
|
|
18305
19032
|
}
|
|
18306
19033
|
);
|
|
19034
|
+
return projectListResponse(c, deps, overrides);
|
|
19035
|
+
}
|
|
19036
|
+
async function applyChangeToAdapter(adapter, configKey, enabled) {
|
|
19037
|
+
await adapter.pluginConfig.set(configKey, enabled);
|
|
19038
|
+
if (enabled) return;
|
|
19039
|
+
const slash = configKey.indexOf("/");
|
|
19040
|
+
if (slash < 0) {
|
|
19041
|
+
await adapter.contributions.purgeByPlugin(configKey);
|
|
19042
|
+
return;
|
|
19043
|
+
}
|
|
19044
|
+
await adapter.contributions.purgeByPlugin(
|
|
19045
|
+
configKey.slice(0, slash),
|
|
19046
|
+
configKey.slice(slash + 1)
|
|
19047
|
+
);
|
|
19048
|
+
}
|
|
19049
|
+
function projectListResponse(c, deps, overrides) {
|
|
18307
19050
|
if (overrides === null) {
|
|
18308
19051
|
return c.json(
|
|
18309
19052
|
{
|
|
@@ -18317,7 +19060,7 @@ async function persistAndProject(c, deps, configKey, enabled) {
|
|
|
18317
19060
|
500
|
|
18318
19061
|
);
|
|
18319
19062
|
}
|
|
18320
|
-
const freshResolver =
|
|
19063
|
+
const freshResolver = composeResolver2(deps, overrides);
|
|
18321
19064
|
const items = listItems(deps, freshResolver);
|
|
18322
19065
|
return c.json(
|
|
18323
19066
|
buildListEnvelope({
|
|
@@ -18330,21 +19073,119 @@ async function persistAndProject(c, deps, configKey, enabled) {
|
|
|
18330
19073
|
})
|
|
18331
19074
|
);
|
|
18332
19075
|
}
|
|
18333
|
-
async function
|
|
19076
|
+
async function parseBulkBody(req) {
|
|
19077
|
+
let raw;
|
|
19078
|
+
try {
|
|
19079
|
+
raw = await req.json();
|
|
19080
|
+
} catch {
|
|
19081
|
+
throw new HTTPException6(400, { message: SERVER_TEXTS.pluginsBodyNotJson });
|
|
19082
|
+
}
|
|
19083
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
19084
|
+
throw new HTTPException6(400, { message: SERVER_TEXTS.pluginsBodyNotObject });
|
|
19085
|
+
}
|
|
19086
|
+
const obj = raw;
|
|
19087
|
+
const changes = obj["changes"];
|
|
19088
|
+
if (!Array.isArray(changes)) {
|
|
19089
|
+
throw new HTTPException6(400, { message: SERVER_TEXTS.pluginsChangesRequired });
|
|
19090
|
+
}
|
|
19091
|
+
const out = [];
|
|
19092
|
+
for (const entry of changes) {
|
|
19093
|
+
if (!isWellShapedBulkEntry(entry)) {
|
|
19094
|
+
throw new HTTPException6(400, { message: SERVER_TEXTS.pluginsChangeMalformed });
|
|
19095
|
+
}
|
|
19096
|
+
out.push({
|
|
19097
|
+
id: entry.id,
|
|
19098
|
+
enabled: entry.enabled
|
|
19099
|
+
});
|
|
19100
|
+
}
|
|
19101
|
+
return out;
|
|
19102
|
+
}
|
|
19103
|
+
function isWellShapedBulkEntry(entry) {
|
|
19104
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
19105
|
+
const obj = entry;
|
|
19106
|
+
return typeof obj["id"] === "string" && typeof obj["enabled"] === "boolean";
|
|
19107
|
+
}
|
|
19108
|
+
function validateBulkChange(change, deps) {
|
|
19109
|
+
const slash = change.id.indexOf("/");
|
|
19110
|
+
if (slash < 0) {
|
|
19111
|
+
const handle2 = findHandle(change.id, deps);
|
|
19112
|
+
if (!handle2) {
|
|
19113
|
+
return {
|
|
19114
|
+
status: 404,
|
|
19115
|
+
code: "not-found",
|
|
19116
|
+
message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
|
|
19117
|
+
};
|
|
19118
|
+
}
|
|
19119
|
+
if (granularityOf(handle2) !== "bundle") {
|
|
19120
|
+
return {
|
|
19121
|
+
status: 400,
|
|
19122
|
+
code: "bad-query",
|
|
19123
|
+
message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id: change.id })
|
|
19124
|
+
};
|
|
19125
|
+
}
|
|
19126
|
+
if (isPluginLocked(change.id)) {
|
|
19127
|
+
return {
|
|
19128
|
+
status: 403,
|
|
19129
|
+
code: "locked",
|
|
19130
|
+
message: tx(SERVER_TEXTS.pluginsLocked, { id: change.id })
|
|
19131
|
+
};
|
|
19132
|
+
}
|
|
19133
|
+
return null;
|
|
19134
|
+
}
|
|
19135
|
+
const bundleId = change.id.slice(0, slash);
|
|
19136
|
+
const extensionId = change.id.slice(slash + 1);
|
|
19137
|
+
const handle = findHandle(bundleId, deps);
|
|
19138
|
+
if (!handle) {
|
|
19139
|
+
return {
|
|
19140
|
+
status: 404,
|
|
19141
|
+
code: "not-found",
|
|
19142
|
+
message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
|
|
19143
|
+
};
|
|
19144
|
+
}
|
|
19145
|
+
if (granularityOf(handle) !== "extension") {
|
|
19146
|
+
return {
|
|
19147
|
+
status: 400,
|
|
19148
|
+
code: "bad-query",
|
|
19149
|
+
message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
|
|
19150
|
+
};
|
|
19151
|
+
}
|
|
19152
|
+
if (!hasExtension(handle, extensionId)) {
|
|
19153
|
+
return {
|
|
19154
|
+
status: 404,
|
|
19155
|
+
code: "not-found",
|
|
19156
|
+
message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { bundleId, extensionId })
|
|
19157
|
+
};
|
|
19158
|
+
}
|
|
19159
|
+
if (isPluginLocked(change.id) || isPluginLocked(bundleId)) {
|
|
19160
|
+
return {
|
|
19161
|
+
status: 403,
|
|
19162
|
+
code: "locked",
|
|
19163
|
+
message: tx(SERVER_TEXTS.pluginsExtensionLocked, { bundleId, extensionId })
|
|
19164
|
+
};
|
|
19165
|
+
}
|
|
19166
|
+
return null;
|
|
19167
|
+
}
|
|
19168
|
+
async function persistBulkAndProject(c, deps, changes) {
|
|
18334
19169
|
const overrides = await tryWithSqlite(
|
|
18335
19170
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
18336
|
-
async (adapter) =>
|
|
19171
|
+
async (adapter) => {
|
|
19172
|
+
for (const change of changes) {
|
|
19173
|
+
await applyChangeToAdapter(adapter, change.id, change.enabled);
|
|
19174
|
+
}
|
|
19175
|
+
return await adapter.pluginConfig.loadOverrideMap();
|
|
19176
|
+
}
|
|
18337
19177
|
);
|
|
18338
|
-
|
|
18339
|
-
return composeResolver(deps, overrides);
|
|
19178
|
+
return projectListResponse(c, deps, overrides);
|
|
18340
19179
|
}
|
|
18341
|
-
function
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
19180
|
+
async function buildFreshResolver2(deps) {
|
|
19181
|
+
return buildFreshResolver({
|
|
19182
|
+
databasePath: deps.options.dbPath,
|
|
19183
|
+
effectiveConfig: () => deps.configService.effective(),
|
|
19184
|
+
fallbackResolver: deps.pluginRuntime.resolveEnabled
|
|
18346
19185
|
});
|
|
18347
|
-
|
|
19186
|
+
}
|
|
19187
|
+
function composeResolver2(deps, overrides) {
|
|
19188
|
+
return composeResolver(deps.configService.effective(), overrides);
|
|
18348
19189
|
}
|
|
18349
19190
|
function findHandle(id, deps) {
|
|
18350
19191
|
const builtIn = builtInBundles.find((b) => b.id === id);
|
|
@@ -18387,6 +19228,7 @@ function buildEnvelope(deps) {
|
|
|
18387
19228
|
};
|
|
18388
19229
|
}
|
|
18389
19230
|
function applyPatch(deps, body) {
|
|
19231
|
+
let wrote = false;
|
|
18390
19232
|
if (body.updateCheck && typeof body.updateCheck.enabled === "boolean") {
|
|
18391
19233
|
try {
|
|
18392
19234
|
writeConfigValue("updateCheck.enabled", body.updateCheck.enabled, {
|
|
@@ -18394,6 +19236,7 @@ function applyPatch(deps, body) {
|
|
|
18394
19236
|
cwd: deps.runtimeContext.cwd,
|
|
18395
19237
|
homedir: deps.runtimeContext.homedir
|
|
18396
19238
|
});
|
|
19239
|
+
wrote = true;
|
|
18397
19240
|
} catch (err) {
|
|
18398
19241
|
throw new HTTPException7(400, {
|
|
18399
19242
|
message: tx(SERVER_TEXTS.preferencesPersistFailed, {
|
|
@@ -18402,6 +19245,7 @@ function applyPatch(deps, body) {
|
|
|
18402
19245
|
});
|
|
18403
19246
|
}
|
|
18404
19247
|
}
|
|
19248
|
+
if (wrote) deps.configService.reload();
|
|
18405
19249
|
}
|
|
18406
19250
|
async function parsePatchBody2(req) {
|
|
18407
19251
|
const obj = await readJsonObject(req);
|
|
@@ -18496,7 +19340,7 @@ function applyPatch2(deps, body) {
|
|
|
18496
19340
|
}
|
|
18497
19341
|
for (const w of writes) {
|
|
18498
19342
|
try {
|
|
18499
|
-
writeConfigValue(w.key, w.value, { target: "project", cwd, homedir: homedir4 });
|
|
19343
|
+
writeConfigValue(w.key, w.value, { target: "project-local", cwd, homedir: homedir4 });
|
|
18500
19344
|
} catch (err) {
|
|
18501
19345
|
const status = err instanceof ConfigValidationError ? 400 : 400;
|
|
18502
19346
|
throw new HTTPException8(status, {
|
|
@@ -18507,6 +19351,7 @@ function applyPatch2(deps, body) {
|
|
|
18507
19351
|
});
|
|
18508
19352
|
}
|
|
18509
19353
|
}
|
|
19354
|
+
deps.configService.reload();
|
|
18510
19355
|
}
|
|
18511
19356
|
function collectWrites(body) {
|
|
18512
19357
|
if (!body.scan) return [];
|
|
@@ -18732,6 +19577,7 @@ async function runPersistedScan(c, deps) {
|
|
|
18732
19577
|
}
|
|
18733
19578
|
try {
|
|
18734
19579
|
return await withScanMutex(async () => {
|
|
19580
|
+
const resolveEnabledOverride = await buildBffResolverOverride(deps);
|
|
18735
19581
|
const outcome = await runScanForCommand({
|
|
18736
19582
|
roots: [deps.runtimeContext.cwd],
|
|
18737
19583
|
noBuiltIns: deps.options.noBuiltIns,
|
|
@@ -18744,6 +19590,7 @@ async function runPersistedScan(c, deps) {
|
|
|
18744
19590
|
stderr: process.stderr,
|
|
18745
19591
|
ctx: deps.runtimeContext,
|
|
18746
19592
|
pluginRuntime: deps.pluginRuntime,
|
|
19593
|
+
resolveEnabledOverride,
|
|
18747
19594
|
printer: bffScanRunnerPrinter,
|
|
18748
19595
|
emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
|
|
18749
19596
|
});
|
|
@@ -18761,6 +19608,13 @@ async function runPersistedScan(c, deps) {
|
|
|
18761
19608
|
throw err;
|
|
18762
19609
|
}
|
|
18763
19610
|
}
|
|
19611
|
+
async function buildBffResolverOverride(deps) {
|
|
19612
|
+
return buildFreshResolver({
|
|
19613
|
+
databasePath: deps.options.dbPath,
|
|
19614
|
+
effectiveConfig: () => deps.configService.effective(),
|
|
19615
|
+
fallbackResolver: deps.pluginRuntime.resolveEnabled
|
|
19616
|
+
});
|
|
19617
|
+
}
|
|
18764
19618
|
async function loadPersistedScan(deps) {
|
|
18765
19619
|
const opened = await tryWithSqlite(
|
|
18766
19620
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
@@ -18815,6 +19669,7 @@ async function runFreshScan(deps) {
|
|
|
18815
19669
|
if (deps.options.noBuiltIns || deps.options.noPlugins) {
|
|
18816
19670
|
throw new HTTPException9(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
|
|
18817
19671
|
}
|
|
19672
|
+
const resolveEnabledOverride = await buildBffResolverOverride(deps);
|
|
18818
19673
|
const outcome = await runScanForCommand({
|
|
18819
19674
|
roots: [deps.runtimeContext.cwd],
|
|
18820
19675
|
noBuiltIns: deps.options.noBuiltIns,
|
|
@@ -18833,6 +19688,7 @@ async function runFreshScan(deps) {
|
|
|
18833
19688
|
// discovering new plugins here would surface them in scan output
|
|
18834
19689
|
// but not in `/api/plugins` or the kindRegistry).
|
|
18835
19690
|
pluginRuntime: deps.pluginRuntime,
|
|
19691
|
+
resolveEnabledOverride,
|
|
18836
19692
|
// M8: explicit printer instead of the runner's old stdout=stderr
|
|
18837
19693
|
// fallback. The fresh-scan response body IS the ScanResult JSON,
|
|
18838
19694
|
// so `data` is never used here; warn/info/error route through
|
|
@@ -18912,12 +19768,20 @@ function registerSidecarRoutes(app, deps) {
|
|
|
18912
19768
|
try {
|
|
18913
19769
|
for (const w of result.writes ?? []) {
|
|
18914
19770
|
if (w.kind === "sidecar") {
|
|
18915
|
-
await store.applyPatch(w.path, w.changes
|
|
19771
|
+
await store.applyPatch(w.path, w.changes, {
|
|
19772
|
+
confirm: body.confirm,
|
|
19773
|
+
cwd: deps.runtimeContext.cwd,
|
|
19774
|
+
homedir: deps.runtimeContext.homedir
|
|
19775
|
+
});
|
|
18916
19776
|
}
|
|
18917
19777
|
}
|
|
18918
19778
|
} catch (err) {
|
|
19779
|
+
if (err instanceof EConsentRequiredError) throw err;
|
|
18919
19780
|
throw new HTTPException10(500, { message: formatErrorMessage(err) });
|
|
18920
19781
|
}
|
|
19782
|
+
if (body.confirm === true) {
|
|
19783
|
+
deps.configService.reload();
|
|
19784
|
+
}
|
|
18921
19785
|
const newVersion = result.report.version ?? null;
|
|
18922
19786
|
const eventData = {
|
|
18923
19787
|
nodePath: node.path,
|
|
@@ -18962,9 +19826,14 @@ async function parseBody(req) {
|
|
|
18962
19826
|
if (forceRaw !== void 0 && typeof forceRaw !== "boolean") {
|
|
18963
19827
|
throw new HTTPException10(400, { message: SERVER_TEXTS.sidecarForceMustBeBoolean });
|
|
18964
19828
|
}
|
|
19829
|
+
const confirmRaw = obj["confirm"];
|
|
19830
|
+
if (confirmRaw !== void 0 && typeof confirmRaw !== "boolean") {
|
|
19831
|
+
throw new HTTPException10(400, { message: SERVER_TEXTS.sidecarConfirmMustBeBoolean });
|
|
19832
|
+
}
|
|
18965
19833
|
return {
|
|
18966
19834
|
nodePath: nodePathRaw,
|
|
18967
|
-
force: forceRaw === true
|
|
19835
|
+
force: forceRaw === true,
|
|
19836
|
+
confirm: confirmRaw === true
|
|
18968
19837
|
};
|
|
18969
19838
|
}
|
|
18970
19839
|
async function loadNode(deps, nodePath) {
|
|
@@ -19169,6 +20038,11 @@ function attachBroadcasterRoute(app, broadcaster) {
|
|
|
19169
20038
|
// server/app.ts
|
|
19170
20039
|
function createApp(deps) {
|
|
19171
20040
|
const app = new Hono();
|
|
20041
|
+
const configService = new ConfigService({
|
|
20042
|
+
scope: deps.options.scope,
|
|
20043
|
+
cwd: deps.runtimeContext.cwd,
|
|
20044
|
+
homedir: deps.runtimeContext.homedir
|
|
20045
|
+
});
|
|
19172
20046
|
if (deps.options.devCors) {
|
|
19173
20047
|
app.use("*", async (c, next) => {
|
|
19174
20048
|
await next();
|
|
@@ -19188,7 +20062,8 @@ function createApp(deps) {
|
|
|
19188
20062
|
runtimeContext: deps.runtimeContext,
|
|
19189
20063
|
kindRegistry: deps.kindRegistry,
|
|
19190
20064
|
contributionsRegistry: deps.contributionsRegistry,
|
|
19191
|
-
pluginRuntime: deps.pluginRuntime
|
|
20065
|
+
pluginRuntime: deps.pluginRuntime,
|
|
20066
|
+
configService
|
|
19192
20067
|
};
|
|
19193
20068
|
registerScanRoute(app, { ...routeDeps, broadcaster: deps.broadcaster });
|
|
19194
20069
|
registerNodesRoutes(app, routeDeps);
|
|
@@ -19257,6 +20132,17 @@ function formatError2(err, c) {
|
|
|
19257
20132
|
};
|
|
19258
20133
|
return c.json(envelope2, 400);
|
|
19259
20134
|
}
|
|
20135
|
+
if (err instanceof EConsentRequiredError) {
|
|
20136
|
+
const envelope2 = {
|
|
20137
|
+
ok: false,
|
|
20138
|
+
error: {
|
|
20139
|
+
code: "confirm-required",
|
|
20140
|
+
message: err.message,
|
|
20141
|
+
details: { key: err.key }
|
|
20142
|
+
}
|
|
20143
|
+
};
|
|
20144
|
+
return c.json(envelope2, 412);
|
|
20145
|
+
}
|
|
19260
20146
|
const envelope = {
|
|
19261
20147
|
ok: false,
|
|
19262
20148
|
error: {
|
|
@@ -19696,50 +20582,64 @@ async function assembleBootBundle(options, runtimeContext) {
|
|
|
19696
20582
|
for (const warn of pluginRuntime.warnings) {
|
|
19697
20583
|
log.warn(sanitizeForTerminal(warn));
|
|
19698
20584
|
}
|
|
19699
|
-
const
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
|
|
20585
|
+
const builtInProviders = options.noBuiltIns ? [] : collectBuiltInProviders();
|
|
20586
|
+
const kindRegistry = buildKindRegistry([
|
|
20587
|
+
...builtInProviders,
|
|
20588
|
+
...pluginRuntime.extensions.providers
|
|
20589
|
+
]);
|
|
19704
20590
|
const kernel = createKernel();
|
|
19705
20591
|
kernel.setRegisteredAnnotationKeys(pluginRuntime.annotationContributions);
|
|
19706
20592
|
const mergedViewContributions = mergeBuiltInViewContributions(
|
|
19707
20593
|
pluginRuntime.viewContributions,
|
|
19708
|
-
|
|
20594
|
+
options.noBuiltIns
|
|
19709
20595
|
);
|
|
19710
20596
|
kernel.setRegisteredViewContributions(mergedViewContributions);
|
|
19711
20597
|
const contributionsRegistry = buildContributionsRegistry(kernel);
|
|
19712
20598
|
return { pluginRuntime, kindRegistry, contributionsRegistry, kernel };
|
|
19713
20599
|
}
|
|
19714
|
-
function
|
|
20600
|
+
function collectBuiltInProviders() {
|
|
20601
|
+
const out = [];
|
|
20602
|
+
for (const bundle of builtInBundles) {
|
|
20603
|
+
for (const ext of bundle.extensions) {
|
|
20604
|
+
if (ext.kind === "provider") {
|
|
20605
|
+
out.push(ext);
|
|
20606
|
+
}
|
|
20607
|
+
}
|
|
20608
|
+
}
|
|
20609
|
+
return out;
|
|
20610
|
+
}
|
|
20611
|
+
function mergeBuiltInViewContributions(userPluginContributions, noBuiltIns) {
|
|
19715
20612
|
const merged = [...userPluginContributions];
|
|
19716
|
-
if (
|
|
20613
|
+
if (noBuiltIns) return merged;
|
|
19717
20614
|
const userKey = new Set(
|
|
19718
20615
|
userPluginContributions.map(
|
|
19719
20616
|
(c) => `${c.pluginId}/${c.extensionId}/${c.contributionId}`
|
|
19720
20617
|
)
|
|
19721
20618
|
);
|
|
19722
|
-
for (const
|
|
19723
|
-
const
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
if (typeof
|
|
19727
|
-
const
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
|
|
19740
|
-
|
|
19741
|
-
|
|
19742
|
-
|
|
20619
|
+
for (const bundle of builtInBundles) {
|
|
20620
|
+
for (const ext of bundle.extensions) {
|
|
20621
|
+
if (ext.kind !== "extractor" && ext.kind !== "analyzer") continue;
|
|
20622
|
+
const raw = ext.viewContributions;
|
|
20623
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
20624
|
+
for (const [contributionId, value] of Object.entries(raw)) {
|
|
20625
|
+
if (typeof value !== "object" || value === null) continue;
|
|
20626
|
+
const v = value;
|
|
20627
|
+
if (typeof v.slot !== "string") continue;
|
|
20628
|
+
const qualified = `${ext.pluginId}/${ext.id}/${contributionId}`;
|
|
20629
|
+
if (userKey.has(qualified)) continue;
|
|
20630
|
+
const entry = {
|
|
20631
|
+
pluginId: ext.pluginId,
|
|
20632
|
+
extensionId: ext.id,
|
|
20633
|
+
contributionId,
|
|
20634
|
+
slot: v.slot,
|
|
20635
|
+
emitWhenEmpty: v.emitWhenEmpty === true
|
|
20636
|
+
};
|
|
20637
|
+
if (typeof v.label === "string") entry.label = v.label;
|
|
20638
|
+
if (typeof v.tooltip === "string") entry.tooltip = v.tooltip;
|
|
20639
|
+
if (typeof v.icon === "string") entry.icon = v.icon;
|
|
20640
|
+
if (typeof v.emptyText === "string") entry.emptyText = v.emptyText;
|
|
20641
|
+
merged.push(entry);
|
|
20642
|
+
}
|
|
19743
20643
|
}
|
|
19744
20644
|
}
|
|
19745
20645
|
return merged;
|
|
@@ -20579,10 +21479,9 @@ function rankConfidenceForGrouping(c) {
|
|
|
20579
21479
|
}
|
|
20580
21480
|
|
|
20581
21481
|
// cli/commands/sidecar.ts
|
|
20582
|
-
import { existsSync as existsSync26, unlinkSync as unlinkSync3
|
|
21482
|
+
import { existsSync as existsSync26, unlinkSync as unlinkSync3 } from "fs";
|
|
20583
21483
|
import { resolve as resolve30 } from "path";
|
|
20584
21484
|
import { Command as Command23, Option as Option23 } from "clipanion";
|
|
20585
|
-
import yaml5 from "js-yaml";
|
|
20586
21485
|
|
|
20587
21486
|
// cli/i18n/sidecar.texts.ts
|
|
20588
21487
|
var SIDECAR_TEXTS = {
|
|
@@ -20610,10 +21509,51 @@ var SIDECAR_TEXTS = {
|
|
|
20610
21509
|
annotateCreated: "{{glyph}} Created {{sidecarPath}}. Edit it, then run `sm bump {{nodePath}}` to commit the version.\n",
|
|
20611
21510
|
/** Trailing dim tag for sidecar prune dry-run (matches the orphans pattern). */
|
|
20612
21511
|
sidecarDryRunTag: " (no changes made)",
|
|
20613
|
-
annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n"
|
|
21512
|
+
annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n",
|
|
21513
|
+
// --- .sm consent gate (shared across refresh + annotate) -----------------
|
|
21514
|
+
/**
|
|
21515
|
+
* Pre-prompt context shown before the interactive `confirm()` so the
|
|
21516
|
+
* operator sees what they are about to opt into. `.skill-map/settings.local.json`
|
|
21517
|
+
* is gitignored — the choice is saved per-checkout, never travels via the repo.
|
|
21518
|
+
*/
|
|
21519
|
+
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?",
|
|
21520
|
+
consentAborted: "{{glyph}} sm sidecar: aborted by user. No .sm sidecar files were written.\n",
|
|
21521
|
+
consentRequiredNonTty: "{{glyph}} sm sidecar: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
|
|
21522
|
+
consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json \u2014 gitignored)."
|
|
20614
21523
|
};
|
|
20615
21524
|
|
|
20616
21525
|
// cli/commands/sidecar.ts
|
|
21526
|
+
async function runWithSidecarConsent(bag, ansi, dispatch) {
|
|
21527
|
+
try {
|
|
21528
|
+
return await dispatch();
|
|
21529
|
+
} catch (err) {
|
|
21530
|
+
if (!(err instanceof EConsentRequiredError)) throw err;
|
|
21531
|
+
const isTTY = bag.stdin.isTTY === true;
|
|
21532
|
+
if (!isTTY || bag.yes) {
|
|
21533
|
+
const errGlyph = ansi.red("\u2715");
|
|
21534
|
+
bag.printError(
|
|
21535
|
+
tx(SIDECAR_TEXTS.consentRequiredNonTty, {
|
|
21536
|
+
glyph: errGlyph,
|
|
21537
|
+
hint: ansi.dim(SIDECAR_TEXTS.consentRequiredNonTtyHint)
|
|
21538
|
+
})
|
|
21539
|
+
);
|
|
21540
|
+
return ExitCode.Error;
|
|
21541
|
+
}
|
|
21542
|
+
const ok = await confirm(
|
|
21543
|
+
SIDECAR_TEXTS.consentPrompt,
|
|
21544
|
+
{ stdin: bag.stdin, stderr: bag.stderr },
|
|
21545
|
+
{ defaultAnswer: "yes" }
|
|
21546
|
+
);
|
|
21547
|
+
if (!ok) {
|
|
21548
|
+
bag.printInfo(
|
|
21549
|
+
tx(SIDECAR_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
|
|
21550
|
+
);
|
|
21551
|
+
return ExitCode.Error;
|
|
21552
|
+
}
|
|
21553
|
+
bag.onAccept();
|
|
21554
|
+
return await dispatch();
|
|
21555
|
+
}
|
|
21556
|
+
}
|
|
20617
21557
|
var SidecarRefreshCommand = class extends SmCommand {
|
|
20618
21558
|
static paths = [["sidecar", "refresh"]];
|
|
20619
21559
|
static usage = Command23.Usage({
|
|
@@ -20634,9 +21574,9 @@ var SidecarRefreshCommand = class extends SmCommand {
|
|
|
20634
21574
|
]
|
|
20635
21575
|
});
|
|
20636
21576
|
nodePath = Option23.String({ required: true });
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
|
|
21577
|
+
yes = Option23.Boolean("--yes", false, {
|
|
21578
|
+
description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
|
|
21579
|
+
});
|
|
20640
21580
|
async run() {
|
|
20641
21581
|
const ctx = defaultRuntimeContext();
|
|
20642
21582
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...ctx });
|
|
@@ -20644,6 +21584,27 @@ var SidecarRefreshCommand = class extends SmCommand {
|
|
|
20644
21584
|
const ansi = ansiFor({ isTTY: stdout.isTTY === true, noColorFlag: this.noColor });
|
|
20645
21585
|
const okGlyph = ansi.green("\u2713");
|
|
20646
21586
|
const errGlyph = ansi.red("\u2715");
|
|
21587
|
+
return runWithSidecarConsent(
|
|
21588
|
+
{
|
|
21589
|
+
stdin: this.context.stdin,
|
|
21590
|
+
stderr: this.context.stderr,
|
|
21591
|
+
yes: this.yes,
|
|
21592
|
+
onAccept: () => {
|
|
21593
|
+
this.yes = true;
|
|
21594
|
+
},
|
|
21595
|
+
printInfo: (s) => this.printer.info(s),
|
|
21596
|
+
printError: (s) => this.printer.error(s)
|
|
21597
|
+
},
|
|
21598
|
+
ansi,
|
|
21599
|
+
() => this.#runOnce(ctx, dbPath, okGlyph, errGlyph, ansi)
|
|
21600
|
+
);
|
|
21601
|
+
}
|
|
21602
|
+
// Inner dispatch — single attempt. The outer `run()` wraps every
|
|
21603
|
+
// call in `runWithSidecarConsent` so an `EConsentRequiredError`
|
|
21604
|
+
// surfaces as an interactive prompt (TTY) or a directed exit
|
|
21605
|
+
// (non-TTY).
|
|
21606
|
+
// eslint-disable-next-line complexity
|
|
21607
|
+
async #runOnce(ctx, dbPath, okGlyph, errGlyph, ansi) {
|
|
20647
21608
|
const persisted = await tryWithSqlite(
|
|
20648
21609
|
{ databasePath: dbPath, autoBackup: false },
|
|
20649
21610
|
async (adapter) => adapter.scans.load()
|
|
@@ -20698,14 +21659,19 @@ var SidecarRefreshCommand = class extends SmCommand {
|
|
|
20698
21659
|
}
|
|
20699
21660
|
const store = new FilesystemSidecarStore();
|
|
20700
21661
|
try {
|
|
20701
|
-
await store.applyPatch(
|
|
20702
|
-
|
|
20703
|
-
|
|
20704
|
-
|
|
20705
|
-
|
|
20706
|
-
|
|
20707
|
-
|
|
21662
|
+
await store.applyPatch(
|
|
21663
|
+
sidecarAbsPath,
|
|
21664
|
+
{
|
|
21665
|
+
identity: {
|
|
21666
|
+
path: node.path,
|
|
21667
|
+
bodyHash: node.bodyHash,
|
|
21668
|
+
frontmatterHash: node.frontmatterHash
|
|
21669
|
+
}
|
|
21670
|
+
},
|
|
21671
|
+
{ confirm: this.yes, cwd: ctx.cwd, homedir: ctx.homedir }
|
|
21672
|
+
);
|
|
20708
21673
|
} catch (err) {
|
|
21674
|
+
if (err instanceof EConsentRequiredError) throw err;
|
|
20709
21675
|
this.printer.error(
|
|
20710
21676
|
tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
|
|
20711
21677
|
);
|
|
@@ -20892,12 +21858,32 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
20892
21858
|
});
|
|
20893
21859
|
nodePath = Option23.String({ required: true });
|
|
20894
21860
|
force = Option23.Boolean("--force", false);
|
|
21861
|
+
yes = Option23.Boolean("--yes", false, {
|
|
21862
|
+
description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
|
|
21863
|
+
});
|
|
20895
21864
|
async run() {
|
|
20896
21865
|
const ctx = defaultRuntimeContext();
|
|
20897
21866
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...ctx });
|
|
20898
21867
|
const stdout = this.context.stdout;
|
|
20899
21868
|
const ansi = ansiFor({ isTTY: stdout.isTTY === true, noColorFlag: this.noColor });
|
|
20900
21869
|
const errGlyph = ansi.red("\u2715");
|
|
21870
|
+
return runWithSidecarConsent(
|
|
21871
|
+
{
|
|
21872
|
+
stdin: this.context.stdin,
|
|
21873
|
+
stderr: this.context.stderr,
|
|
21874
|
+
yes: this.yes,
|
|
21875
|
+
onAccept: () => {
|
|
21876
|
+
this.yes = true;
|
|
21877
|
+
},
|
|
21878
|
+
printInfo: (s) => this.printer.info(s),
|
|
21879
|
+
printError: (s) => this.printer.error(s)
|
|
21880
|
+
},
|
|
21881
|
+
ansi,
|
|
21882
|
+
() => this.#runOnce(ctx, dbPath, errGlyph, ansi)
|
|
21883
|
+
);
|
|
21884
|
+
}
|
|
21885
|
+
// eslint-disable-next-line complexity
|
|
21886
|
+
async #runOnce(ctx, dbPath, errGlyph, ansi) {
|
|
20901
21887
|
const persisted = await tryWithSqlite(
|
|
20902
21888
|
{ databasePath: dbPath, autoBackup: false },
|
|
20903
21889
|
async (adapter) => adapter.scans.load()
|
|
@@ -20944,10 +21930,25 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
20944
21930
|
);
|
|
20945
21931
|
return ExitCode.Error;
|
|
20946
21932
|
}
|
|
20947
|
-
|
|
21933
|
+
if (existsSync26(sidecarAbsPath) && this.force === true) {
|
|
21934
|
+
try {
|
|
21935
|
+
unlinkSync3(sidecarAbsPath);
|
|
21936
|
+
} catch (err) {
|
|
21937
|
+
this.printer.error(
|
|
21938
|
+
tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
|
|
21939
|
+
);
|
|
21940
|
+
return ExitCode.Error;
|
|
21941
|
+
}
|
|
21942
|
+
}
|
|
21943
|
+
const store = new FilesystemSidecarStore();
|
|
20948
21944
|
try {
|
|
20949
|
-
|
|
21945
|
+
await store.applyPatch(
|
|
21946
|
+
sidecarAbsPath,
|
|
21947
|
+
scaffoldSidecarObject(node),
|
|
21948
|
+
{ confirm: this.yes, cwd: ctx.cwd, homedir: ctx.homedir }
|
|
21949
|
+
);
|
|
20950
21950
|
} catch (err) {
|
|
21951
|
+
if (err instanceof EConsentRequiredError) throw err;
|
|
20951
21952
|
this.printer.error(
|
|
20952
21953
|
tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
|
|
20953
21954
|
);
|
|
@@ -20973,8 +21974,8 @@ var SidecarAnnotateCommand = class extends SmCommand {
|
|
|
20973
21974
|
return ExitCode.Ok;
|
|
20974
21975
|
}
|
|
20975
21976
|
};
|
|
20976
|
-
function
|
|
20977
|
-
|
|
21977
|
+
function scaffoldSidecarObject(node) {
|
|
21978
|
+
return {
|
|
20978
21979
|
identity: {
|
|
20979
21980
|
bodyHash: node.bodyHash,
|
|
20980
21981
|
frontmatterHash: node.frontmatterHash,
|
|
@@ -20982,14 +21983,6 @@ function scaffoldSidecar(node) {
|
|
|
20982
21983
|
},
|
|
20983
21984
|
annotations: {}
|
|
20984
21985
|
};
|
|
20985
|
-
const body = yaml5.dump(root, {
|
|
20986
|
-
sortKeys: true,
|
|
20987
|
-
lineWidth: -1,
|
|
20988
|
-
noRefs: true,
|
|
20989
|
-
noCompatMode: true
|
|
20990
|
-
});
|
|
20991
|
-
const banner = "# Skill-map sidecar \u2014 managed artifact.\n# Comments in .sm are NOT preserved across `sm bump` (the bump action\n# re-serialises the file). Narrative / docs \u2192 the .md body, which is\n# never touched. See spec/cli-contract.md \xA7Sidecar bump for the\n# round-trip contract.\n\n";
|
|
20992
|
-
return banner + body;
|
|
20993
21986
|
}
|
|
20994
21987
|
var SIDECAR_COMMANDS = [
|
|
20995
21988
|
SidecarRefreshCommand,
|