@skill-map/cli 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1182 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +64 -0
- package/dist/kernel/index.js +1 -1
- package/dist/kernel/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -409,11 +409,11 @@ function buildIgnoreFilter(opts = {}) {
|
|
|
409
409
|
ig.add(opts.ignoreFileText);
|
|
410
410
|
}
|
|
411
411
|
return {
|
|
412
|
-
ignores(
|
|
413
|
-
if (
|
|
412
|
+
ignores(relativePath2) {
|
|
413
|
+
if (relativePath2 === "" || relativePath2 === "." || relativePath2 === "./") {
|
|
414
414
|
return false;
|
|
415
415
|
}
|
|
416
|
-
const normalised =
|
|
416
|
+
const normalised = relativePath2.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
417
417
|
if (normalised === "") return false;
|
|
418
418
|
return ig.ignores(normalised);
|
|
419
419
|
}
|
|
@@ -624,31 +624,79 @@ var claudeProvider = {
|
|
|
624
624
|
// pairs the relative manifest-style schema path (mirrors what the
|
|
625
625
|
// spec's provider.schema.json validates) with the loaded JSON Schema
|
|
626
626
|
// (`schemaJson`) the kernel registers with AJV at scan boot.
|
|
627
|
+
// Step 14.5.d: each kind declares its UI presentation (label, color,
|
|
628
|
+
// dark variant, icon). The UI consumes this registry via the
|
|
629
|
+
// `kindRegistry` field embedded in REST envelopes; it derives bg/fg
|
|
630
|
+
// tints from `color` per theme via a deterministic helper, so the
|
|
631
|
+
// Provider only declares intent (one base color per theme) instead of
|
|
632
|
+
// four hex values. Colors and SVG paths transplanted verbatim from
|
|
633
|
+
// the previous static UI catalog (`ui/src/styles.css` for hex,
|
|
634
|
+
// `ui/src/app/components/kind-icon/kind-icon.html` for SVG path data,
|
|
635
|
+
// `ui/src/i18n/kinds.texts.ts` for labels).
|
|
627
636
|
kinds: {
|
|
628
637
|
agent: {
|
|
629
638
|
schema: "./schemas/agent.schema.json",
|
|
630
639
|
schemaJson: agent_schema_default,
|
|
631
|
-
defaultRefreshAction: "claude/summarize-agent"
|
|
640
|
+
defaultRefreshAction: "claude/summarize-agent",
|
|
641
|
+
ui: {
|
|
642
|
+
label: "Agents",
|
|
643
|
+
color: "#3b82f6",
|
|
644
|
+
colorDark: "#60a5fa",
|
|
645
|
+
icon: { kind: "pi", id: "pi-user" }
|
|
646
|
+
}
|
|
632
647
|
},
|
|
633
648
|
command: {
|
|
634
649
|
schema: "./schemas/command.schema.json",
|
|
635
650
|
schemaJson: command_schema_default,
|
|
636
|
-
defaultRefreshAction: "claude/summarize-command"
|
|
651
|
+
defaultRefreshAction: "claude/summarize-command",
|
|
652
|
+
ui: {
|
|
653
|
+
label: "Commands",
|
|
654
|
+
color: "#f59e0b",
|
|
655
|
+
colorDark: "#fbbf24",
|
|
656
|
+
icon: {
|
|
657
|
+
kind: "svg",
|
|
658
|
+
path: "M4 17 L10 11 L4 5 M12 19 L20 19"
|
|
659
|
+
}
|
|
660
|
+
}
|
|
637
661
|
},
|
|
638
662
|
hook: {
|
|
639
663
|
schema: "./schemas/hook.schema.json",
|
|
640
664
|
schemaJson: hook_schema_default,
|
|
641
|
-
defaultRefreshAction: "claude/summarize-hook"
|
|
665
|
+
defaultRefreshAction: "claude/summarize-hook",
|
|
666
|
+
ui: {
|
|
667
|
+
label: "Hooks",
|
|
668
|
+
color: "#8b5cf6",
|
|
669
|
+
colorDark: "#a78bfa",
|
|
670
|
+
icon: {
|
|
671
|
+
kind: "svg",
|
|
672
|
+
path: "M12 2 a3 3 0 1 0 0 6 a3 3 0 1 0 0 -6 M12 8 L12 22 M5 12 H2 a10 10 0 0 0 20 0 H19"
|
|
673
|
+
}
|
|
674
|
+
}
|
|
642
675
|
},
|
|
643
676
|
skill: {
|
|
644
677
|
schema: "./schemas/skill.schema.json",
|
|
645
678
|
schemaJson: skill_schema_default,
|
|
646
|
-
defaultRefreshAction: "claude/summarize-skill"
|
|
679
|
+
defaultRefreshAction: "claude/summarize-skill",
|
|
680
|
+
ui: {
|
|
681
|
+
label: "Skills",
|
|
682
|
+
color: "#10b981",
|
|
683
|
+
colorDark: "#34d399",
|
|
684
|
+
icon: { kind: "pi", id: "pi-bolt" }
|
|
685
|
+
}
|
|
647
686
|
},
|
|
648
687
|
note: {
|
|
649
688
|
schema: "./schemas/note.schema.json",
|
|
650
689
|
schemaJson: note_schema_default,
|
|
651
|
-
defaultRefreshAction: "claude/summarize-note"
|
|
690
|
+
defaultRefreshAction: "claude/summarize-note",
|
|
691
|
+
ui: {
|
|
692
|
+
label: "Notes",
|
|
693
|
+
color: "#5b908c",
|
|
694
|
+
colorDark: "#9bbcb8",
|
|
695
|
+
icon: {
|
|
696
|
+
kind: "svg",
|
|
697
|
+
path: "M14 2 H6 a2 2 0 0 0 -2 2 V20 a2 2 0 0 0 2 2 H18 a2 2 0 0 0 2 -2 V8 L14 2 M14 2 V8 H20 M16 13 H8 M16 17 H8 M10 9 H8"
|
|
698
|
+
}
|
|
699
|
+
}
|
|
652
700
|
}
|
|
653
701
|
},
|
|
654
702
|
async *walk(roots, options = {}) {
|
|
@@ -6980,7 +7028,7 @@ import { Command as Command8, Option as Option8 } from "clipanion";
|
|
|
6980
7028
|
// package.json
|
|
6981
7029
|
var package_default = {
|
|
6982
7030
|
name: "@skill-map/cli",
|
|
6983
|
-
version: "0.
|
|
7031
|
+
version: "0.11.0",
|
|
6984
7032
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
6985
7033
|
license: "MIT",
|
|
6986
7034
|
type: "module",
|
|
@@ -9208,8 +9256,8 @@ function renderTable(rows) {
|
|
|
9208
9256
|
HISTORY_TEXTS.tableHeaderTokens,
|
|
9209
9257
|
HISTORY_TEXTS.tableHeaderNodes
|
|
9210
9258
|
);
|
|
9211
|
-
const
|
|
9212
|
-
const lines = [header,
|
|
9259
|
+
const sep5 = "-".repeat(header.length);
|
|
9260
|
+
const lines = [header, sep5];
|
|
9213
9261
|
for (const r of rows) {
|
|
9214
9262
|
const tokens = `${r.tokensIn ?? 0}/${r.tokensOut ?? 0}`;
|
|
9215
9263
|
const duration = r.durationMs === null || r.durationMs === void 0 ? "-" : formatElapsed(r.durationMs);
|
|
@@ -9618,8 +9666,8 @@ function renderTable2(nodes, issuesByNode) {
|
|
|
9618
9666
|
LIST_TEXTS.tableHeaderIssues,
|
|
9619
9667
|
LIST_TEXTS.tableHeaderBytes
|
|
9620
9668
|
);
|
|
9621
|
-
const
|
|
9622
|
-
const lines = [header,
|
|
9669
|
+
const sep5 = "-".repeat(header.length);
|
|
9670
|
+
const lines = [header, sep5];
|
|
9623
9671
|
for (const node of nodes) {
|
|
9624
9672
|
lines.push(
|
|
9625
9673
|
formatRow2(
|
|
@@ -11813,8 +11861,178 @@ import { WebSocketServer } from "ws";
|
|
|
11813
11861
|
|
|
11814
11862
|
// server/app.ts
|
|
11815
11863
|
import { Hono } from "hono";
|
|
11864
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
11865
|
+
|
|
11866
|
+
// server/routes/config.ts
|
|
11816
11867
|
import { HTTPException } from "hono/http-exception";
|
|
11817
11868
|
|
|
11869
|
+
// server/envelope.ts
|
|
11870
|
+
var REST_ENVELOPE_SCHEMA_VERSION = "1";
|
|
11871
|
+
function buildListEnvelope(opts) {
|
|
11872
|
+
const counts = {
|
|
11873
|
+
total: opts.total,
|
|
11874
|
+
returned: opts.items.length
|
|
11875
|
+
};
|
|
11876
|
+
if (opts.page) counts.page = opts.page;
|
|
11877
|
+
return {
|
|
11878
|
+
schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
|
|
11879
|
+
kind: opts.kind,
|
|
11880
|
+
items: opts.items,
|
|
11881
|
+
filters: opts.filters,
|
|
11882
|
+
counts,
|
|
11883
|
+
kindRegistry: opts.kindRegistry
|
|
11884
|
+
};
|
|
11885
|
+
}
|
|
11886
|
+
function buildValueEnvelope(kind, value, kindRegistry) {
|
|
11887
|
+
return {
|
|
11888
|
+
schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
|
|
11889
|
+
kind,
|
|
11890
|
+
value,
|
|
11891
|
+
kindRegistry
|
|
11892
|
+
};
|
|
11893
|
+
}
|
|
11894
|
+
|
|
11895
|
+
// server/routes/config.ts
|
|
11896
|
+
function registerConfigRoute(app, deps) {
|
|
11897
|
+
app.get("/api/config", (c) => {
|
|
11898
|
+
let loaded;
|
|
11899
|
+
try {
|
|
11900
|
+
loaded = loadConfig({
|
|
11901
|
+
scope: deps.options.scope,
|
|
11902
|
+
cwd: deps.runtimeContext.cwd,
|
|
11903
|
+
homedir: deps.runtimeContext.homedir
|
|
11904
|
+
});
|
|
11905
|
+
} catch (err) {
|
|
11906
|
+
throw new HTTPException(500, { message: formatErrorMessage(err) });
|
|
11907
|
+
}
|
|
11908
|
+
for (const warn of loaded.warnings) {
|
|
11909
|
+
process.stderr.write(`${warn}
|
|
11910
|
+
`);
|
|
11911
|
+
}
|
|
11912
|
+
return c.json(buildValueEnvelope("config", loaded.effective, deps.kindRegistry));
|
|
11913
|
+
});
|
|
11914
|
+
}
|
|
11915
|
+
|
|
11916
|
+
// server/routes/graph.ts
|
|
11917
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
11918
|
+
|
|
11919
|
+
// server/i18n/server.texts.ts
|
|
11920
|
+
var SERVER_TEXTS = {
|
|
11921
|
+
// Boot banner — printed by the server itself when it begins to listen.
|
|
11922
|
+
// The CLI verb `sm serve` formats its own boot banner separately
|
|
11923
|
+
// (SERVE_TEXTS.boot) so the two surfaces can diverge if needed.
|
|
11924
|
+
listening: "skill-map server listening on http://{{host}}:{{port}}\n",
|
|
11925
|
+
// UI bundle missing — non-fatal when the path was auto-resolved (the
|
|
11926
|
+
// server keeps running with an inline placeholder at `/`). Becomes
|
|
11927
|
+
// ExitCode.Error when `--ui-dist <path>` was explicit.
|
|
11928
|
+
uiBundleMissing: 'skill-map server: UI bundle not found at {{path}} \u2014 serving inline placeholder at "/" (run "npm run build --workspace=ui" to populate).\n',
|
|
11929
|
+
// Loopback-only deprecation hint — Decision #119. Logged once at boot
|
|
11930
|
+
// when `--host` resolves to a non-loopback address. Multi-host serve
|
|
11931
|
+
// re-opens post-v0.6.0.
|
|
11932
|
+
hostNonLoopbackHint: "skill-map server: --host {{host}} is non-loopback \u2014 through v0.6.0 the BFF assumes loopback-only (no auth). See Decision #119 in ROADMAP.\n",
|
|
11933
|
+
// Shutdown trace — printed by the close path so test runs that bring
|
|
11934
|
+
// the server up and down have a clear marker.
|
|
11935
|
+
closed: "skill-map server: closed.\n",
|
|
11936
|
+
// ---- error envelope messages (Step 14.2) ---------------------------------
|
|
11937
|
+
// Persisted scan absent and the route can't degrade to an empty result.
|
|
11938
|
+
// Hint nudges the user toward `sm scan` so the SPA can call it via the
|
|
11939
|
+
// CLI side-by-side with the server.
|
|
11940
|
+
dbMissingHint: "No persisted scan available at {{path}}. Run `sm scan` to populate the DB.",
|
|
11941
|
+
// `?fresh=1` was requested but the server was booted with --no-built-ins
|
|
11942
|
+
// or --no-plugins. A fresh scan with neither pipeline yields an empty /
|
|
11943
|
+
// partial result that would surprise the SPA. Reject up front.
|
|
11944
|
+
freshScanRequiresPipeline: "?fresh=1 cannot run while the server was started with --no-built-ins or --no-plugins (would yield empty / partial results).",
|
|
11945
|
+
// Unknown formatter on /api/graph — the user asked for a `format` value
|
|
11946
|
+
// that no registered formatter advertises. Mirrors `sm graph`'s message.
|
|
11947
|
+
graphUnknownFormat: 'Unknown graph format "{{format}}". Available: {{available}}.',
|
|
11948
|
+
// Pagination caps on /api/nodes.
|
|
11949
|
+
paginationLimitTooLarge: "limit={{value}} exceeds the maximum of {{max}}.",
|
|
11950
|
+
paginationInvalidInteger: "{{name}}={{value}} is not a non-negative integer.",
|
|
11951
|
+
// Node lookup miss on /api/nodes/:pathB64. Both the missing-node and
|
|
11952
|
+
// the malformed-pathB64 cases funnel here — the client experience is
|
|
11953
|
+
// the same (the resource isn't there).
|
|
11954
|
+
nodeNotFound: 'No node with path "{{path}}".',
|
|
11955
|
+
pathB64Malformed: "Malformed pathB64 \u2014 not a valid base64url-encoded node.path.",
|
|
11956
|
+
// ---- WS broadcaster + watcher (Step 14.4.a) ------------------------------
|
|
11957
|
+
// Logged once on watcher boot after chokidar's initial walk completes.
|
|
11958
|
+
// Marks the broadcaster as armed and the live event stream as flowing.
|
|
11959
|
+
watcherReady: 'skill-map server: watcher ready (roots="{{roots}}", debounceMs={{debounceMs}}).\n',
|
|
11960
|
+
// Watcher boot failure inside `createServer`. Non-fatal — the REST
|
|
11961
|
+
// surface stays alive so the operator can fix the underlying issue
|
|
11962
|
+
// (config, plugin, FS permission) and restart.
|
|
11963
|
+
watcherBootFailed: "skill-map server: watcher boot failed \u2014 {{message}}. /api/* still serving; pass --no-watcher to silence this on the next boot.\n",
|
|
11964
|
+
// Per-batch failure inside the watcher's scan+persist pipeline. The
|
|
11965
|
+
// watcher loop continues — a transient FS error must not kill the
|
|
11966
|
+
// broadcaster.
|
|
11967
|
+
watcherBatchFailed: "skill-map server: watcher batch failed \u2014 {{message}}.\n",
|
|
11968
|
+
// chokidar surfaced an error. The watcher stays open per IFsWatcher's
|
|
11969
|
+
// contract; the BFF also broadcasts a `watcher.error` advisory so the
|
|
11970
|
+
// SPA can surface it in the live event log.
|
|
11971
|
+
watcherError: "skill-map server: watcher error \u2014 {{message}}.\n",
|
|
11972
|
+
// chokidar.close() rejected during graceful shutdown. Logged but not
|
|
11973
|
+
// surfaced — close() is best-effort and idempotent.
|
|
11974
|
+
watcherCloseFailed: "skill-map server: watcher close failed \u2014 {{message}}.\n",
|
|
11975
|
+
// A connected client's outbound buffer exceeded the backpressure
|
|
11976
|
+
// threshold. The broadcaster closes the client with code 1009 and
|
|
11977
|
+
// unregisters it. Logged so operators can spot a wedged consumer.
|
|
11978
|
+
wsBackpressureEvicted: "skill-map server: ws client evicted (bufferedAmount={{buffered}} > threshold={{threshold}}).\n",
|
|
11979
|
+
// `WebSocket.send()` threw on a registered client. The client is
|
|
11980
|
+
// unregistered; the broadcast continues with the remaining clients.
|
|
11981
|
+
wsClientSendFailed: "skill-map server: ws send failed \u2014 {{message}}.\n",
|
|
11982
|
+
// `JSON.stringify(envelope)` threw inside `broadcast()`. The event is
|
|
11983
|
+
// dropped. Per spec/job-events.md §Error handling, the right shape
|
|
11984
|
+
// is a synthetic `emitter.error` event; v14.4.a does not yet route
|
|
11985
|
+
// it through the broadcaster (would re-enter the same stringify
|
|
11986
|
+
// path), so we degrade to a logged warning.
|
|
11987
|
+
wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped \u2014 failed to serialize event: {{message}}.\n"
|
|
11988
|
+
};
|
|
11989
|
+
|
|
11990
|
+
// server/routes/graph.ts
|
|
11991
|
+
var DEFAULT_FORMAT2 = "ascii";
|
|
11992
|
+
function registerGraphRoute(app, deps) {
|
|
11993
|
+
app.get("/api/graph", async (c) => {
|
|
11994
|
+
const format = c.req.query("format") ?? DEFAULT_FORMAT2;
|
|
11995
|
+
const pluginRuntime = deps.options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: deps.options.scope });
|
|
11996
|
+
for (const warn of pluginRuntime.warnings) {
|
|
11997
|
+
process.stderr.write(`${warn}
|
|
11998
|
+
`);
|
|
11999
|
+
}
|
|
12000
|
+
const formatters = composeFormatters({
|
|
12001
|
+
noBuiltIns: deps.options.noBuiltIns,
|
|
12002
|
+
pluginRuntime
|
|
12003
|
+
});
|
|
12004
|
+
const formatter = formatters.find((f) => f.formatId === format);
|
|
12005
|
+
if (!formatter) {
|
|
12006
|
+
const available = formatters.map((f) => f.formatId).sort().join(", ");
|
|
12007
|
+
throw new HTTPException2(400, {
|
|
12008
|
+
message: tx(SERVER_TEXTS.graphUnknownFormat, {
|
|
12009
|
+
format,
|
|
12010
|
+
available: available || "(none)"
|
|
12011
|
+
})
|
|
12012
|
+
});
|
|
12013
|
+
}
|
|
12014
|
+
const loaded = await tryWithSqlite(
|
|
12015
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12016
|
+
(adapter) => adapter.scans.load()
|
|
12017
|
+
);
|
|
12018
|
+
const scan = loaded ?? { nodes: [], links: [], issues: [] };
|
|
12019
|
+
const text = formatter.format({
|
|
12020
|
+
nodes: scan.nodes,
|
|
12021
|
+
links: scan.links,
|
|
12022
|
+
issues: scan.issues
|
|
12023
|
+
});
|
|
12024
|
+
const body = text.endsWith("\n") ? text : text + "\n";
|
|
12025
|
+
return c.body(body, 200, { "content-type": contentTypeFor(format) });
|
|
12026
|
+
});
|
|
12027
|
+
}
|
|
12028
|
+
function contentTypeFor(format) {
|
|
12029
|
+
if (format === "json") return "application/json; charset=utf-8";
|
|
12030
|
+
if (format === "md" || format === "markdown" || format === "mermaid") {
|
|
12031
|
+
return "text/markdown; charset=utf-8";
|
|
12032
|
+
}
|
|
12033
|
+
return "text/plain; charset=utf-8";
|
|
12034
|
+
}
|
|
12035
|
+
|
|
11818
12036
|
// server/health.ts
|
|
11819
12037
|
import { existsSync as existsSync15 } from "fs";
|
|
11820
12038
|
var FALLBACK_SCHEMA_VERSION = "1";
|
|
@@ -11838,9 +12056,435 @@ async function resolveSpecVersion2() {
|
|
|
11838
12056
|
}
|
|
11839
12057
|
}
|
|
11840
12058
|
|
|
12059
|
+
// server/routes/health.ts
|
|
12060
|
+
function registerHealthRoute(app, deps) {
|
|
12061
|
+
app.get("/api/health", (c) => {
|
|
12062
|
+
const payload = buildHealth({
|
|
12063
|
+
dbPath: deps.options.dbPath,
|
|
12064
|
+
scope: deps.options.scope,
|
|
12065
|
+
specVersion: deps.specVersion
|
|
12066
|
+
});
|
|
12067
|
+
return c.json(payload);
|
|
12068
|
+
});
|
|
12069
|
+
}
|
|
12070
|
+
|
|
12071
|
+
// server/routes/issues.ts
|
|
12072
|
+
function registerIssuesRoute(app, deps) {
|
|
12073
|
+
app.get("/api/issues", async (c) => {
|
|
12074
|
+
const params = new URL(c.req.url).searchParams;
|
|
12075
|
+
const severityFilter = parseCsv(params.get("severity"));
|
|
12076
|
+
const ruleFilter = parseRulesFilter(params.get("ruleId"));
|
|
12077
|
+
const nodePath = params.get("node");
|
|
12078
|
+
const loaded = await tryWithSqlite(
|
|
12079
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12080
|
+
(adapter) => adapter.issues.listAll()
|
|
12081
|
+
);
|
|
12082
|
+
const allIssues = loaded ?? [];
|
|
12083
|
+
const filtered = allIssues.filter((issue) => {
|
|
12084
|
+
if (severityFilter && !severityFilter.includes(issue.severity)) return false;
|
|
12085
|
+
if (ruleFilter && !matchesRuleFilter2(issue.ruleId, ruleFilter)) return false;
|
|
12086
|
+
if (nodePath !== null && !issue.nodeIds.includes(nodePath)) return false;
|
|
12087
|
+
return true;
|
|
12088
|
+
});
|
|
12089
|
+
return c.json(
|
|
12090
|
+
buildListEnvelope({
|
|
12091
|
+
kind: "issues",
|
|
12092
|
+
items: filtered,
|
|
12093
|
+
filters: {
|
|
12094
|
+
severity: severityFilter ?? null,
|
|
12095
|
+
ruleId: ruleFilter ? [...ruleFilter] : null,
|
|
12096
|
+
node: nodePath ?? null
|
|
12097
|
+
},
|
|
12098
|
+
total: filtered.length,
|
|
12099
|
+
kindRegistry: deps.kindRegistry
|
|
12100
|
+
})
|
|
12101
|
+
);
|
|
12102
|
+
});
|
|
12103
|
+
}
|
|
12104
|
+
function parseCsv(raw) {
|
|
12105
|
+
if (raw === null) return null;
|
|
12106
|
+
const list = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12107
|
+
return list.length > 0 ? list : null;
|
|
12108
|
+
}
|
|
12109
|
+
function parseRulesFilter(raw) {
|
|
12110
|
+
const list = parseCsv(raw);
|
|
12111
|
+
return list ? new Set(list) : null;
|
|
12112
|
+
}
|
|
12113
|
+
function matchesRuleFilter2(ruleId, filter) {
|
|
12114
|
+
if (filter.has(ruleId)) return true;
|
|
12115
|
+
const slashIdx = ruleId.indexOf("/");
|
|
12116
|
+
if (slashIdx >= 0) {
|
|
12117
|
+
const short = ruleId.slice(slashIdx + 1);
|
|
12118
|
+
if (filter.has(short)) return true;
|
|
12119
|
+
}
|
|
12120
|
+
return false;
|
|
12121
|
+
}
|
|
12122
|
+
|
|
12123
|
+
// server/routes/links.ts
|
|
12124
|
+
function registerLinksRoute(app, deps) {
|
|
12125
|
+
app.get("/api/links", async (c) => {
|
|
12126
|
+
const params = new URL(c.req.url).searchParams;
|
|
12127
|
+
const kindFilter = parseCsv2(params.get("kind"));
|
|
12128
|
+
const from = params.get("from");
|
|
12129
|
+
const to = params.get("to");
|
|
12130
|
+
const loaded = await tryWithSqlite(
|
|
12131
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12132
|
+
(adapter) => adapter.scans.load()
|
|
12133
|
+
);
|
|
12134
|
+
const allLinks = loaded?.links ?? [];
|
|
12135
|
+
const filtered = allLinks.filter((link2) => {
|
|
12136
|
+
if (kindFilter && !kindFilter.includes(link2.kind)) return false;
|
|
12137
|
+
if (from !== null && link2.source !== from) return false;
|
|
12138
|
+
if (to !== null && link2.target !== to) return false;
|
|
12139
|
+
return true;
|
|
12140
|
+
});
|
|
12141
|
+
return c.json(
|
|
12142
|
+
buildListEnvelope({
|
|
12143
|
+
kind: "links",
|
|
12144
|
+
items: filtered,
|
|
12145
|
+
filters: {
|
|
12146
|
+
kind: kindFilter ?? null,
|
|
12147
|
+
from: from ?? null,
|
|
12148
|
+
to: to ?? null
|
|
12149
|
+
},
|
|
12150
|
+
total: filtered.length,
|
|
12151
|
+
kindRegistry: deps.kindRegistry
|
|
12152
|
+
})
|
|
12153
|
+
);
|
|
12154
|
+
});
|
|
12155
|
+
}
|
|
12156
|
+
function parseCsv2(raw) {
|
|
12157
|
+
if (raw === null) return null;
|
|
12158
|
+
const list = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12159
|
+
return list.length > 0 ? list : null;
|
|
12160
|
+
}
|
|
12161
|
+
|
|
12162
|
+
// server/routes/nodes.ts
|
|
12163
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
12164
|
+
|
|
12165
|
+
// server/node-body.ts
|
|
12166
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
12167
|
+
import { isAbsolute as isAbsolute4, resolve as resolvePath, relative as relativePath, sep as sep4 } from "path";
|
|
12168
|
+
async function readNodeBody(cwd, relPath) {
|
|
12169
|
+
if (isAbsolute4(relPath)) return null;
|
|
12170
|
+
const absRoot = resolvePath(cwd);
|
|
12171
|
+
const absFile = resolvePath(absRoot, relPath);
|
|
12172
|
+
const rel = relativePath(absRoot, absFile);
|
|
12173
|
+
if (rel.startsWith("..") || rel.startsWith(sep4) || rel.length === 0) {
|
|
12174
|
+
return null;
|
|
12175
|
+
}
|
|
12176
|
+
let raw;
|
|
12177
|
+
try {
|
|
12178
|
+
raw = await readFile4(absFile, "utf-8");
|
|
12179
|
+
} catch (err) {
|
|
12180
|
+
if (isExpectedFsError(err)) return null;
|
|
12181
|
+
throw err;
|
|
12182
|
+
}
|
|
12183
|
+
return stripFrontmatter(raw);
|
|
12184
|
+
}
|
|
12185
|
+
function stripFrontmatter(raw) {
|
|
12186
|
+
if (!raw.startsWith("---")) return raw;
|
|
12187
|
+
const match = raw.match(/^---\r?\n[\s\S]*?^---\r?\n?/m);
|
|
12188
|
+
if (!match) return raw;
|
|
12189
|
+
return raw.slice(match[0].length);
|
|
12190
|
+
}
|
|
12191
|
+
var EXPECTED_FS_ERROR_CODES = /* @__PURE__ */ new Set(["ENOENT", "EACCES", "EISDIR", "ENOTDIR"]);
|
|
12192
|
+
function isExpectedFsError(err) {
|
|
12193
|
+
if (err === null || typeof err !== "object") return false;
|
|
12194
|
+
const code = err.code;
|
|
12195
|
+
return typeof code === "string" && EXPECTED_FS_ERROR_CODES.has(code);
|
|
12196
|
+
}
|
|
12197
|
+
|
|
12198
|
+
// server/path-codec.ts
|
|
12199
|
+
var PathCodecError = class extends Error {
|
|
12200
|
+
constructor(message) {
|
|
12201
|
+
super(message);
|
|
12202
|
+
this.name = "PathCodecError";
|
|
12203
|
+
}
|
|
12204
|
+
};
|
|
12205
|
+
function encodeNodePath(path) {
|
|
12206
|
+
return Buffer.from(path, "utf8").toString("base64url");
|
|
12207
|
+
}
|
|
12208
|
+
function decodeNodePath(encoded) {
|
|
12209
|
+
if (encoded.length === 0) {
|
|
12210
|
+
throw new PathCodecError("empty pathB64");
|
|
12211
|
+
}
|
|
12212
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
12213
|
+
throw new PathCodecError("pathB64 contains characters outside the base64url alphabet");
|
|
12214
|
+
}
|
|
12215
|
+
const decoded = Buffer.from(encoded, "base64url").toString("utf8");
|
|
12216
|
+
if (encodeNodePath(decoded) !== encoded) {
|
|
12217
|
+
throw new PathCodecError("pathB64 did not round-trip cleanly through base64url");
|
|
12218
|
+
}
|
|
12219
|
+
return decoded;
|
|
12220
|
+
}
|
|
12221
|
+
|
|
12222
|
+
// server/query-adapter.ts
|
|
12223
|
+
function urlParamsToExportQuery(params) {
|
|
12224
|
+
const filters = {};
|
|
12225
|
+
const tokens = [];
|
|
12226
|
+
const kindRaw = params.get("kind");
|
|
12227
|
+
if (kindRaw !== null) {
|
|
12228
|
+
const kinds = splitCsv(kindRaw);
|
|
12229
|
+
if (kinds.length === 0) {
|
|
12230
|
+
throw new ExportQueryError("kind: empty value list");
|
|
12231
|
+
}
|
|
12232
|
+
filters.kinds = kinds;
|
|
12233
|
+
tokens.push(`kind=${kinds.join(",")}`);
|
|
12234
|
+
}
|
|
12235
|
+
const hasIssuesRaw = params.get("hasIssues");
|
|
12236
|
+
if (hasIssuesRaw !== null) {
|
|
12237
|
+
const lower = hasIssuesRaw.toLowerCase();
|
|
12238
|
+
if (lower === "true") {
|
|
12239
|
+
filters.hasIssues = true;
|
|
12240
|
+
tokens.push("has=issues");
|
|
12241
|
+
} else if (lower === "false") {
|
|
12242
|
+
filters.hasIssues = false;
|
|
12243
|
+
} else {
|
|
12244
|
+
throw new ExportQueryError(`hasIssues: expected "true" or "false", got "${hasIssuesRaw}"`);
|
|
12245
|
+
}
|
|
12246
|
+
}
|
|
12247
|
+
const pathRaw = params.get("path");
|
|
12248
|
+
if (pathRaw !== null) {
|
|
12249
|
+
const globs = splitCsv(pathRaw);
|
|
12250
|
+
if (globs.length === 0) {
|
|
12251
|
+
throw new ExportQueryError("path: empty value list");
|
|
12252
|
+
}
|
|
12253
|
+
filters.pathGlobs = globs;
|
|
12254
|
+
tokens.push(`path=${globs.join(",")}`);
|
|
12255
|
+
}
|
|
12256
|
+
const raw = tokens.join(" ");
|
|
12257
|
+
const query = parseExportQuery(raw);
|
|
12258
|
+
return { query, filters };
|
|
12259
|
+
}
|
|
12260
|
+
function filterNodesWithoutIssues(nodes, issues) {
|
|
12261
|
+
if (issues.length === 0) return nodes;
|
|
12262
|
+
const nodesWithIssues = /* @__PURE__ */ new Set();
|
|
12263
|
+
for (const issue of issues) {
|
|
12264
|
+
for (const id of issue.nodeIds) nodesWithIssues.add(id);
|
|
12265
|
+
}
|
|
12266
|
+
return nodes.filter((n) => !nodesWithIssues.has(n.path));
|
|
12267
|
+
}
|
|
12268
|
+
function splitCsv(raw) {
|
|
12269
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12270
|
+
}
|
|
12271
|
+
|
|
12272
|
+
// server/routes/nodes.ts
|
|
12273
|
+
var DEFAULT_LIMIT = 100;
|
|
12274
|
+
var MAX_LIMIT = 1e3;
|
|
12275
|
+
function registerNodesRoutes(app, deps) {
|
|
12276
|
+
app.get("/api/nodes/:pathB64", async (c) => {
|
|
12277
|
+
const pathB64 = c.req.param("pathB64");
|
|
12278
|
+
let nodePath;
|
|
12279
|
+
try {
|
|
12280
|
+
nodePath = decodeNodePath(pathB64);
|
|
12281
|
+
} catch (err) {
|
|
12282
|
+
if (err instanceof PathCodecError) {
|
|
12283
|
+
throw new HTTPException3(404, { message: SERVER_TEXTS.pathB64Malformed });
|
|
12284
|
+
}
|
|
12285
|
+
throw err;
|
|
12286
|
+
}
|
|
12287
|
+
const bundle = await tryWithSqlite(
|
|
12288
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12289
|
+
(adapter) => adapter.scans.findNode(nodePath)
|
|
12290
|
+
);
|
|
12291
|
+
if (!bundle) {
|
|
12292
|
+
throw new HTTPException3(404, {
|
|
12293
|
+
message: tx(SERVER_TEXTS.nodeNotFound, { path: nodePath })
|
|
12294
|
+
});
|
|
12295
|
+
}
|
|
12296
|
+
const includes = parseIncludes(new URL(c.req.url).searchParams.get("include"));
|
|
12297
|
+
const item = includes.has("body") ? { ...bundle.node, body: await readNodeBody(deps.runtimeContext.cwd, nodePath) } : bundle.node;
|
|
12298
|
+
return c.json({
|
|
12299
|
+
schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
|
|
12300
|
+
kind: "node",
|
|
12301
|
+
item,
|
|
12302
|
+
links: { incoming: bundle.linksIn, outgoing: bundle.linksOut },
|
|
12303
|
+
issues: bundle.issues,
|
|
12304
|
+
kindRegistry: deps.kindRegistry
|
|
12305
|
+
});
|
|
12306
|
+
});
|
|
12307
|
+
app.get("/api/nodes", async (c) => {
|
|
12308
|
+
const params = new URL(c.req.url).searchParams;
|
|
12309
|
+
const { query, filters } = urlParamsToExportQuery(params);
|
|
12310
|
+
const { offset, limit } = parsePagination(params);
|
|
12311
|
+
const loaded = await tryWithSqlite(
|
|
12312
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12313
|
+
(adapter) => adapter.scans.load()
|
|
12314
|
+
);
|
|
12315
|
+
const scan = loaded ?? { nodes: [], links: [], issues: [] };
|
|
12316
|
+
const subset = applyExportQuery(scan, query);
|
|
12317
|
+
let nodes = subset.nodes;
|
|
12318
|
+
if (filters.hasIssues === false) {
|
|
12319
|
+
nodes = filterNodesWithoutIssues(nodes, scan.issues);
|
|
12320
|
+
}
|
|
12321
|
+
const total = nodes.length;
|
|
12322
|
+
const items = nodes.slice(offset, offset + limit);
|
|
12323
|
+
return c.json(
|
|
12324
|
+
buildListEnvelope({
|
|
12325
|
+
kind: "nodes",
|
|
12326
|
+
items,
|
|
12327
|
+
filters: {
|
|
12328
|
+
kind: filters.kinds ?? null,
|
|
12329
|
+
hasIssues: filters.hasIssues ?? null,
|
|
12330
|
+
path: filters.pathGlobs ?? null
|
|
12331
|
+
},
|
|
12332
|
+
total,
|
|
12333
|
+
page: { offset, limit },
|
|
12334
|
+
kindRegistry: deps.kindRegistry
|
|
12335
|
+
})
|
|
12336
|
+
);
|
|
12337
|
+
});
|
|
12338
|
+
}
|
|
12339
|
+
function parsePagination(params) {
|
|
12340
|
+
const offset = parseNonNegativeInt(params.get("offset"), "offset", 0);
|
|
12341
|
+
const limit = parseNonNegativeInt(params.get("limit"), "limit", DEFAULT_LIMIT);
|
|
12342
|
+
if (limit > MAX_LIMIT) {
|
|
12343
|
+
throw new HTTPException3(400, {
|
|
12344
|
+
message: tx(SERVER_TEXTS.paginationLimitTooLarge, { value: limit, max: MAX_LIMIT })
|
|
12345
|
+
});
|
|
12346
|
+
}
|
|
12347
|
+
return { offset, limit };
|
|
12348
|
+
}
|
|
12349
|
+
function parseNonNegativeInt(raw, name, fallback) {
|
|
12350
|
+
if (raw === null || raw.length === 0) return fallback;
|
|
12351
|
+
const trimmed = raw.trim();
|
|
12352
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
12353
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
12354
|
+
throw new HTTPException3(400, {
|
|
12355
|
+
message: tx(SERVER_TEXTS.paginationInvalidInteger, { name, value: raw })
|
|
12356
|
+
});
|
|
12357
|
+
}
|
|
12358
|
+
return parsed;
|
|
12359
|
+
}
|
|
12360
|
+
function parseIncludes(raw) {
|
|
12361
|
+
if (raw === null || raw.length === 0) return /* @__PURE__ */ new Set();
|
|
12362
|
+
return new Set(raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0));
|
|
12363
|
+
}
|
|
12364
|
+
|
|
12365
|
+
// server/routes/plugins.ts
|
|
12366
|
+
function registerPluginsRoute(app, deps) {
|
|
12367
|
+
app.get("/api/plugins", async (c) => {
|
|
12368
|
+
const pluginRuntime = deps.options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: deps.options.scope });
|
|
12369
|
+
for (const warn of pluginRuntime.warnings) {
|
|
12370
|
+
process.stderr.write(`${warn}
|
|
12371
|
+
`);
|
|
12372
|
+
}
|
|
12373
|
+
const items = [
|
|
12374
|
+
...deps.options.noBuiltIns ? [] : buildBuiltInItems(pluginRuntime.resolveEnabled),
|
|
12375
|
+
...buildDiscoveredItems(pluginRuntime.discovered, deps)
|
|
12376
|
+
];
|
|
12377
|
+
return c.json(
|
|
12378
|
+
buildListEnvelope({
|
|
12379
|
+
kind: "plugins",
|
|
12380
|
+
items,
|
|
12381
|
+
filters: {},
|
|
12382
|
+
total: items.length,
|
|
12383
|
+
kindRegistry: deps.kindRegistry
|
|
12384
|
+
})
|
|
12385
|
+
);
|
|
12386
|
+
});
|
|
12387
|
+
}
|
|
12388
|
+
function buildBuiltInItems(resolveEnabled) {
|
|
12389
|
+
return builtInBundles.map((bundle) => ({
|
|
12390
|
+
id: bundle.id,
|
|
12391
|
+
version: firstVersion(bundle.extensions),
|
|
12392
|
+
kinds: uniqueKinds(bundle.extensions.map((e) => e.kind)),
|
|
12393
|
+
status: resolveEnabled(bundle.id) ? "enabled" : "disabled",
|
|
12394
|
+
reason: null,
|
|
12395
|
+
source: "built-in"
|
|
12396
|
+
}));
|
|
12397
|
+
}
|
|
12398
|
+
function buildDiscoveredItems(discovered, deps) {
|
|
12399
|
+
return discovered.map((plugin) => ({
|
|
12400
|
+
id: plugin.id,
|
|
12401
|
+
version: plugin.manifest?.version ?? null,
|
|
12402
|
+
kinds: uniqueKinds(plugin.extensions?.map((e) => e.kind) ?? []),
|
|
12403
|
+
status: plugin.status,
|
|
12404
|
+
reason: plugin.reason ?? null,
|
|
12405
|
+
source: classifyPluginSource(plugin.path, deps)
|
|
12406
|
+
}));
|
|
12407
|
+
}
|
|
12408
|
+
function uniqueKinds(kinds) {
|
|
12409
|
+
return [...new Set(kinds)].sort();
|
|
12410
|
+
}
|
|
12411
|
+
function firstVersion(extensions) {
|
|
12412
|
+
for (const ext of extensions) {
|
|
12413
|
+
if (ext.version) return ext.version;
|
|
12414
|
+
}
|
|
12415
|
+
return null;
|
|
12416
|
+
}
|
|
12417
|
+
function classifyPluginSource(pluginPath, deps) {
|
|
12418
|
+
const projectDir = defaultProjectPluginsDir(deps.runtimeContext);
|
|
12419
|
+
return pluginPath.startsWith(projectDir) ? "project" : "global";
|
|
12420
|
+
}
|
|
12421
|
+
|
|
12422
|
+
// server/routes/scan.ts
|
|
12423
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
12424
|
+
function registerScanRoute(app, deps) {
|
|
12425
|
+
app.get("/api/scan", async (c) => {
|
|
12426
|
+
const fresh = c.req.query("fresh");
|
|
12427
|
+
if (fresh === "1" || fresh === "true") {
|
|
12428
|
+
return c.json(await runFreshScan(deps));
|
|
12429
|
+
}
|
|
12430
|
+
return c.json(await loadPersistedScan(deps));
|
|
12431
|
+
});
|
|
12432
|
+
}
|
|
12433
|
+
async function loadPersistedScan(deps) {
|
|
12434
|
+
const loaded = await tryWithSqlite(
|
|
12435
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
12436
|
+
(adapter) => adapter.scans.load()
|
|
12437
|
+
);
|
|
12438
|
+
if (loaded !== null) return loaded;
|
|
12439
|
+
return emptyScanResult();
|
|
12440
|
+
}
|
|
12441
|
+
async function runFreshScan(deps) {
|
|
12442
|
+
if (deps.options.noBuiltIns || deps.options.noPlugins) {
|
|
12443
|
+
throw new HTTPException4(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
|
|
12444
|
+
}
|
|
12445
|
+
const outcome = await runScanForCommand({
|
|
12446
|
+
roots: [deps.runtimeContext.cwd],
|
|
12447
|
+
noBuiltIns: false,
|
|
12448
|
+
noPlugins: false,
|
|
12449
|
+
noTokens: false,
|
|
12450
|
+
dryRun: true,
|
|
12451
|
+
changed: false,
|
|
12452
|
+
allowEmpty: true,
|
|
12453
|
+
strict: false,
|
|
12454
|
+
stderr: process.stderr,
|
|
12455
|
+
ctx: deps.runtimeContext
|
|
12456
|
+
});
|
|
12457
|
+
if (outcome.kind !== "ok") {
|
|
12458
|
+
throw new HTTPException4(500, {
|
|
12459
|
+
message: outcome.kind === "guard-trip" ? `fresh scan refused (existing rows: ${outcome.existing})` : outcome.message
|
|
12460
|
+
});
|
|
12461
|
+
}
|
|
12462
|
+
return outcome.result;
|
|
12463
|
+
}
|
|
12464
|
+
function emptyScanResult() {
|
|
12465
|
+
return {
|
|
12466
|
+
schemaVersion: 1,
|
|
12467
|
+
scannedAt: Date.now(),
|
|
12468
|
+
scope: "project",
|
|
12469
|
+
roots: ["."],
|
|
12470
|
+
providers: [],
|
|
12471
|
+
nodes: [],
|
|
12472
|
+
links: [],
|
|
12473
|
+
issues: [],
|
|
12474
|
+
stats: {
|
|
12475
|
+
filesWalked: 0,
|
|
12476
|
+
filesSkipped: 0,
|
|
12477
|
+
nodesCount: 0,
|
|
12478
|
+
linksCount: 0,
|
|
12479
|
+
issuesCount: 0,
|
|
12480
|
+
durationMs: 0
|
|
12481
|
+
}
|
|
12482
|
+
};
|
|
12483
|
+
}
|
|
12484
|
+
|
|
11841
12485
|
// server/static.ts
|
|
11842
12486
|
import { existsSync as existsSync16 } from "fs";
|
|
11843
|
-
import { readFile as
|
|
12487
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
11844
12488
|
import { extname, join as join13 } from "path";
|
|
11845
12489
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
11846
12490
|
var INDEX_HTML = "index.html";
|
|
@@ -11914,10 +12558,48 @@ function htmlResponse(c, html) {
|
|
|
11914
12558
|
return c.body(html, 200, { "content-type": "text/html; charset=UTF-8" });
|
|
11915
12559
|
}
|
|
11916
12560
|
async function fileResponse(c, absPath) {
|
|
11917
|
-
const buf = await
|
|
12561
|
+
const buf = await readFile5(absPath);
|
|
11918
12562
|
return c.body(buf, 200, { "content-type": mimeFor(absPath) });
|
|
11919
12563
|
}
|
|
11920
12564
|
|
|
12565
|
+
// server/ws.ts
|
|
12566
|
+
import { upgradeWebSocket } from "@hono/node-server";
|
|
12567
|
+
var WS_PATH = "/ws";
|
|
12568
|
+
function attachBroadcasterRoute(app, broadcaster) {
|
|
12569
|
+
app.get(
|
|
12570
|
+
WS_PATH,
|
|
12571
|
+
upgradeWebSocket(() => ({
|
|
12572
|
+
onOpen(_event, ws) {
|
|
12573
|
+
const raw = ws.raw;
|
|
12574
|
+
if (!raw) {
|
|
12575
|
+
broadcaster.register({
|
|
12576
|
+
send: (data) => ws.send(data),
|
|
12577
|
+
close: (code, reason) => ws.close(code, reason),
|
|
12578
|
+
bufferedAmount: 0,
|
|
12579
|
+
readyState: ws.readyState
|
|
12580
|
+
});
|
|
12581
|
+
return;
|
|
12582
|
+
}
|
|
12583
|
+
broadcaster.register(raw);
|
|
12584
|
+
},
|
|
12585
|
+
onClose(_event, ws) {
|
|
12586
|
+
const raw = ws.raw;
|
|
12587
|
+
if (raw) broadcaster.unregister(raw);
|
|
12588
|
+
},
|
|
12589
|
+
onError(event, ws) {
|
|
12590
|
+
const raw = ws.raw;
|
|
12591
|
+
if (raw) broadcaster.unregister(raw);
|
|
12592
|
+
const message = event?.message ?? "unknown";
|
|
12593
|
+
log.warn(
|
|
12594
|
+
tx(SERVER_TEXTS.wsClientSendFailed, {
|
|
12595
|
+
message: sanitizeForTerminal(message)
|
|
12596
|
+
})
|
|
12597
|
+
);
|
|
12598
|
+
}
|
|
12599
|
+
}))
|
|
12600
|
+
);
|
|
12601
|
+
}
|
|
12602
|
+
|
|
11921
12603
|
// server/app.ts
|
|
11922
12604
|
function createApp(deps) {
|
|
11923
12605
|
const app = new Hono();
|
|
@@ -11930,22 +12612,27 @@ function createApp(deps) {
|
|
|
11930
12612
|
});
|
|
11931
12613
|
app.options("*", (c) => c.body(null, 204));
|
|
11932
12614
|
}
|
|
11933
|
-
app.
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
12615
|
+
registerHealthRoute(app, { options: deps.options, specVersion: deps.specVersion });
|
|
12616
|
+
const routeDeps = {
|
|
12617
|
+
options: deps.options,
|
|
12618
|
+
runtimeContext: deps.runtimeContext,
|
|
12619
|
+
kindRegistry: deps.kindRegistry
|
|
12620
|
+
};
|
|
12621
|
+
registerScanRoute(app, routeDeps);
|
|
12622
|
+
registerNodesRoutes(app, routeDeps);
|
|
12623
|
+
registerLinksRoute(app, routeDeps);
|
|
12624
|
+
registerIssuesRoute(app, routeDeps);
|
|
12625
|
+
registerGraphRoute(app, routeDeps);
|
|
12626
|
+
registerConfigRoute(app, routeDeps);
|
|
12627
|
+
registerPluginsRoute(app, routeDeps);
|
|
11941
12628
|
app.all("/api/*", (c) => {
|
|
11942
|
-
throw new
|
|
12629
|
+
throw new HTTPException5(404, { message: `Unknown API endpoint: ${c.req.path}` });
|
|
11943
12630
|
});
|
|
11944
|
-
deps.
|
|
12631
|
+
attachBroadcasterRoute(app, deps.broadcaster);
|
|
11945
12632
|
app.use("*", createStaticHandler(deps.options.uiDist));
|
|
11946
12633
|
app.get("*", createSpaFallback(deps.options.uiDist));
|
|
11947
12634
|
app.notFound((c) => {
|
|
11948
|
-
throw new
|
|
12635
|
+
throw new HTTPException5(404, { message: `Not found: ${c.req.path}` });
|
|
11949
12636
|
});
|
|
11950
12637
|
app.onError((err, c) => {
|
|
11951
12638
|
return formatError2(err, c);
|
|
@@ -11958,7 +12645,7 @@ function codeForStatus(status) {
|
|
|
11958
12645
|
return "internal";
|
|
11959
12646
|
}
|
|
11960
12647
|
function formatError2(err, c) {
|
|
11961
|
-
if (err instanceof
|
|
12648
|
+
if (err instanceof HTTPException5) {
|
|
11962
12649
|
const status = err.status;
|
|
11963
12650
|
const envelope2 = {
|
|
11964
12651
|
ok: false,
|
|
@@ -11970,6 +12657,17 @@ function formatError2(err, c) {
|
|
|
11970
12657
|
};
|
|
11971
12658
|
return c.json(envelope2, status);
|
|
11972
12659
|
}
|
|
12660
|
+
if (err instanceof ExportQueryError) {
|
|
12661
|
+
const envelope2 = {
|
|
12662
|
+
ok: false,
|
|
12663
|
+
error: {
|
|
12664
|
+
code: "bad-query",
|
|
12665
|
+
message: err.message,
|
|
12666
|
+
details: null
|
|
12667
|
+
}
|
|
12668
|
+
};
|
|
12669
|
+
return c.json(envelope2, 400);
|
|
12670
|
+
}
|
|
11973
12671
|
const envelope = {
|
|
11974
12672
|
ok: false,
|
|
11975
12673
|
error: {
|
|
@@ -11981,19 +12679,328 @@ function formatError2(err, c) {
|
|
|
11981
12679
|
return c.json(envelope, 500);
|
|
11982
12680
|
}
|
|
11983
12681
|
|
|
11984
|
-
// server/
|
|
11985
|
-
|
|
11986
|
-
var
|
|
11987
|
-
var
|
|
11988
|
-
var
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
12682
|
+
// server/broadcaster.ts
|
|
12683
|
+
var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
12684
|
+
var CLOSE_CODE_GOING_AWAY = 1001;
|
|
12685
|
+
var CLOSE_CODE_MESSAGE_TOO_BIG = 1009;
|
|
12686
|
+
var READY_STATE_OPEN = 1;
|
|
12687
|
+
var WsBroadcaster = class {
|
|
12688
|
+
#clients = /* @__PURE__ */ new Set();
|
|
12689
|
+
#shutDown = false;
|
|
12690
|
+
/** Number of currently-registered clients. Read-only — for tests / `/api/health`. */
|
|
12691
|
+
get clientCount() {
|
|
12692
|
+
return this.#clients.size;
|
|
12693
|
+
}
|
|
12694
|
+
/**
|
|
12695
|
+
* Register a client. Called from the `/ws` `onOpen` handler with the
|
|
12696
|
+
* raw `WebSocket` instance. After shutdown the broadcaster refuses
|
|
12697
|
+
* new registrations and immediately closes the offered socket so a
|
|
12698
|
+
* late upgrade doesn't leak.
|
|
12699
|
+
*/
|
|
12700
|
+
register(ws) {
|
|
12701
|
+
if (this.#shutDown) {
|
|
12702
|
+
try {
|
|
12703
|
+
ws.close(CLOSE_CODE_GOING_AWAY, "server shutdown");
|
|
12704
|
+
} catch {
|
|
11995
12705
|
}
|
|
11996
|
-
|
|
12706
|
+
return;
|
|
12707
|
+
}
|
|
12708
|
+
this.#clients.add(ws);
|
|
12709
|
+
}
|
|
12710
|
+
/**
|
|
12711
|
+
* Unregister a client. Called from the `/ws` `onClose` / `onError`
|
|
12712
|
+
* handlers and from the backpressure path. Idempotent — calling on a
|
|
12713
|
+
* client that was never registered (or was already removed) is a no-op.
|
|
12714
|
+
*/
|
|
12715
|
+
unregister(ws) {
|
|
12716
|
+
this.#clients.delete(ws);
|
|
12717
|
+
}
|
|
12718
|
+
/**
|
|
12719
|
+
* Serialize the envelope once and fan out to every open client.
|
|
12720
|
+
* Closed / closing clients are silently skipped (the `onClose` handler
|
|
12721
|
+
* has already removed them, but we double-check `readyState` because a
|
|
12722
|
+
* close in the middle of the loop is observable as a transient
|
|
12723
|
+
* `OPEN → CLOSING` flip).
|
|
12724
|
+
*
|
|
12725
|
+
* Per-client `send()` failures are caught: one rogue socket cannot
|
|
12726
|
+
* stop the rest from receiving the event. A failing socket is closed
|
|
12727
|
+
* + unregistered so the next broadcast doesn't waste cycles on it.
|
|
12728
|
+
*
|
|
12729
|
+
* Backpressure check (per AGENTS.md §Watcher integration): if a
|
|
12730
|
+
* client's `bufferedAmount` exceeds `MAX_BUFFERED_BYTES`, it's evicted
|
|
12731
|
+
* with close code 1009. The check runs BEFORE `send` so the threshold
|
|
12732
|
+
* acts as an admission gate, not a post-mortem.
|
|
12733
|
+
*/
|
|
12734
|
+
broadcast(envelope) {
|
|
12735
|
+
if (this.#shutDown) return;
|
|
12736
|
+
let payload;
|
|
12737
|
+
try {
|
|
12738
|
+
payload = JSON.stringify(envelope);
|
|
12739
|
+
} catch (err) {
|
|
12740
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12741
|
+
log.warn(
|
|
12742
|
+
tx(SERVER_TEXTS.wsBroadcastSerializeFailed, {
|
|
12743
|
+
message: sanitizeForTerminal(message)
|
|
12744
|
+
})
|
|
12745
|
+
);
|
|
12746
|
+
return;
|
|
12747
|
+
}
|
|
12748
|
+
const snapshot = Array.from(this.#clients);
|
|
12749
|
+
for (const client of snapshot) {
|
|
12750
|
+
this.#deliver(client, payload);
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
/**
|
|
12754
|
+
* Drain every connected socket with code 1001 ('going away') + reason
|
|
12755
|
+
* `'server shutdown'`. Idempotent — a second call after the first
|
|
12756
|
+
* `shutdown()` is a no-op. After shutdown, `register()` immediately
|
|
12757
|
+
* closes any new client offered.
|
|
12758
|
+
*/
|
|
12759
|
+
shutdown() {
|
|
12760
|
+
if (this.#shutDown) return;
|
|
12761
|
+
this.#shutDown = true;
|
|
12762
|
+
const snapshot = Array.from(this.#clients);
|
|
12763
|
+
this.#clients.clear();
|
|
12764
|
+
for (const client of snapshot) {
|
|
12765
|
+
try {
|
|
12766
|
+
client.close(CLOSE_CODE_GOING_AWAY, "server shutdown");
|
|
12767
|
+
} catch {
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
}
|
|
12771
|
+
/**
|
|
12772
|
+
* Per-client delivery: backpressure check, then `send()`. Eviction +
|
|
12773
|
+
* unregistration on either failure mode.
|
|
12774
|
+
*/
|
|
12775
|
+
#deliver(client, payload) {
|
|
12776
|
+
if (client.readyState !== READY_STATE_OPEN) {
|
|
12777
|
+
this.#clients.delete(client);
|
|
12778
|
+
return;
|
|
12779
|
+
}
|
|
12780
|
+
if (client.bufferedAmount > MAX_BUFFERED_BYTES) {
|
|
12781
|
+
this.#clients.delete(client);
|
|
12782
|
+
try {
|
|
12783
|
+
client.close(CLOSE_CODE_MESSAGE_TOO_BIG, "backpressure exceeded");
|
|
12784
|
+
} catch {
|
|
12785
|
+
}
|
|
12786
|
+
log.warn(
|
|
12787
|
+
tx(SERVER_TEXTS.wsBackpressureEvicted, {
|
|
12788
|
+
buffered: String(client.bufferedAmount),
|
|
12789
|
+
threshold: String(MAX_BUFFERED_BYTES)
|
|
12790
|
+
})
|
|
12791
|
+
);
|
|
12792
|
+
return;
|
|
12793
|
+
}
|
|
12794
|
+
try {
|
|
12795
|
+
client.send(payload);
|
|
12796
|
+
} catch (err) {
|
|
12797
|
+
this.#clients.delete(client);
|
|
12798
|
+
try {
|
|
12799
|
+
client.close();
|
|
12800
|
+
} catch {
|
|
12801
|
+
}
|
|
12802
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12803
|
+
log.warn(
|
|
12804
|
+
tx(SERVER_TEXTS.wsClientSendFailed, {
|
|
12805
|
+
message: sanitizeForTerminal(message)
|
|
12806
|
+
})
|
|
12807
|
+
);
|
|
12808
|
+
}
|
|
12809
|
+
}
|
|
12810
|
+
};
|
|
12811
|
+
|
|
12812
|
+
// server/kind-registry.ts
|
|
12813
|
+
function buildKindRegistry(providers) {
|
|
12814
|
+
const registry = {};
|
|
12815
|
+
for (const provider of providers) {
|
|
12816
|
+
for (const [kindName, kindEntry] of Object.entries(provider.kinds)) {
|
|
12817
|
+
if (registry[kindName]) continue;
|
|
12818
|
+
const ui = kindEntry.ui;
|
|
12819
|
+
const entry = {
|
|
12820
|
+
providerId: provider.id,
|
|
12821
|
+
label: ui.label,
|
|
12822
|
+
color: ui.color
|
|
12823
|
+
};
|
|
12824
|
+
if (ui.colorDark !== void 0) entry.colorDark = ui.colorDark;
|
|
12825
|
+
if (ui.emoji !== void 0) entry.emoji = ui.emoji;
|
|
12826
|
+
if (ui.icon !== void 0) entry.icon = ui.icon;
|
|
12827
|
+
registry[kindName] = entry;
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12830
|
+
return registry;
|
|
12831
|
+
}
|
|
12832
|
+
|
|
12833
|
+
// server/events.ts
|
|
12834
|
+
function buildWatcherStartedEvent(data) {
|
|
12835
|
+
return {
|
|
12836
|
+
type: "watcher.started",
|
|
12837
|
+
timestamp: Date.now(),
|
|
12838
|
+
jobId: null,
|
|
12839
|
+
data
|
|
12840
|
+
};
|
|
12841
|
+
}
|
|
12842
|
+
function buildWatcherErrorEvent(data) {
|
|
12843
|
+
return {
|
|
12844
|
+
type: "watcher.error",
|
|
12845
|
+
timestamp: Date.now(),
|
|
12846
|
+
jobId: null,
|
|
12847
|
+
data
|
|
12848
|
+
};
|
|
12849
|
+
}
|
|
12850
|
+
|
|
12851
|
+
// server/watcher.ts
|
|
12852
|
+
var WATCH_ROOT = ".";
|
|
12853
|
+
function createWatcherService(opts) {
|
|
12854
|
+
let chokidarHandle = null;
|
|
12855
|
+
let stopped = false;
|
|
12856
|
+
const start = async () => {
|
|
12857
|
+
const cfg = loadConfig({
|
|
12858
|
+
scope: opts.options.scope,
|
|
12859
|
+
cwd: opts.runtimeContext.cwd,
|
|
12860
|
+
homedir: opts.runtimeContext.homedir
|
|
12861
|
+
}).effective;
|
|
12862
|
+
const ignoreFileText = readIgnoreFileText(opts.runtimeContext.cwd);
|
|
12863
|
+
const ignoreFilterOpts = {};
|
|
12864
|
+
if (cfg.ignore.length > 0) ignoreFilterOpts.configIgnore = cfg.ignore;
|
|
12865
|
+
if (ignoreFileText !== void 0) ignoreFilterOpts.ignoreFileText = ignoreFileText;
|
|
12866
|
+
const ignoreFilter = buildIgnoreFilter(ignoreFilterOpts);
|
|
12867
|
+
const debounceMs = opts.debounceMsOverride ?? cfg.scan.watch.debounceMs;
|
|
12868
|
+
const pluginRuntime = opts.options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: opts.options.scope });
|
|
12869
|
+
for (const warn of pluginRuntime.warnings) {
|
|
12870
|
+
log.warn(sanitizeForTerminal(warn));
|
|
12871
|
+
}
|
|
12872
|
+
const runOneBatch = async () => {
|
|
12873
|
+
const kernel = createKernel();
|
|
12874
|
+
registerKernelExtensions(kernel, pluginRuntime, opts.options.noBuiltIns);
|
|
12875
|
+
const emitter = buildBroadcasterEmitter(opts.broadcaster);
|
|
12876
|
+
const priorState = await loadPriorState(opts.options.dbPath);
|
|
12877
|
+
const composed = composeScanExtensions({
|
|
12878
|
+
noBuiltIns: opts.options.noBuiltIns,
|
|
12879
|
+
pluginRuntime
|
|
12880
|
+
});
|
|
12881
|
+
const runOptions = assembleRunOptions({
|
|
12882
|
+
scope: opts.options.scope,
|
|
12883
|
+
tokenize: cfg.scan.tokenize !== false,
|
|
12884
|
+
strict: cfg.scan.strict === true,
|
|
12885
|
+
ignoreFilter,
|
|
12886
|
+
emitter,
|
|
12887
|
+
composed,
|
|
12888
|
+
priorState
|
|
12889
|
+
});
|
|
12890
|
+
const ran = await runScanWithRenames(kernel, runOptions);
|
|
12891
|
+
await persistOutcome(opts.options.dbPath, ran);
|
|
12892
|
+
};
|
|
12893
|
+
chokidarHandle = createChokidarWatcher({
|
|
12894
|
+
roots: [WATCH_ROOT],
|
|
12895
|
+
cwd: opts.runtimeContext.cwd,
|
|
12896
|
+
debounceMs,
|
|
12897
|
+
ignoreFilter,
|
|
12898
|
+
onBatch: async () => {
|
|
12899
|
+
if (stopped) return;
|
|
12900
|
+
try {
|
|
12901
|
+
await runOneBatch();
|
|
12902
|
+
} catch (err) {
|
|
12903
|
+
const message = formatErrorMessage(err);
|
|
12904
|
+
log.warn(
|
|
12905
|
+
tx(SERVER_TEXTS.watcherBatchFailed, {
|
|
12906
|
+
message: sanitizeForTerminal(message)
|
|
12907
|
+
})
|
|
12908
|
+
);
|
|
12909
|
+
}
|
|
12910
|
+
},
|
|
12911
|
+
onError: (err) => {
|
|
12912
|
+
const message = err.message;
|
|
12913
|
+
log.warn(
|
|
12914
|
+
tx(SERVER_TEXTS.watcherError, {
|
|
12915
|
+
message: sanitizeForTerminal(message)
|
|
12916
|
+
})
|
|
12917
|
+
);
|
|
12918
|
+
opts.broadcaster.broadcast(buildWatcherErrorEvent({ message }));
|
|
12919
|
+
}
|
|
12920
|
+
});
|
|
12921
|
+
if ("ready" in chokidarHandle && chokidarHandle.ready instanceof Promise) {
|
|
12922
|
+
await chokidarHandle.ready;
|
|
12923
|
+
}
|
|
12924
|
+
opts.broadcaster.broadcast(
|
|
12925
|
+
buildWatcherStartedEvent({ roots: [WATCH_ROOT], debounceMs })
|
|
12926
|
+
);
|
|
12927
|
+
log.info(
|
|
12928
|
+
tx(SERVER_TEXTS.watcherReady, {
|
|
12929
|
+
roots: WATCH_ROOT,
|
|
12930
|
+
debounceMs: String(debounceMs)
|
|
12931
|
+
})
|
|
12932
|
+
);
|
|
12933
|
+
};
|
|
12934
|
+
const stop = async () => {
|
|
12935
|
+
if (stopped) return;
|
|
12936
|
+
stopped = true;
|
|
12937
|
+
if (chokidarHandle) {
|
|
12938
|
+
try {
|
|
12939
|
+
await chokidarHandle.close();
|
|
12940
|
+
} catch (err) {
|
|
12941
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12942
|
+
log.warn(
|
|
12943
|
+
tx(SERVER_TEXTS.watcherCloseFailed, {
|
|
12944
|
+
message: sanitizeForTerminal(message)
|
|
12945
|
+
})
|
|
12946
|
+
);
|
|
12947
|
+
}
|
|
12948
|
+
chokidarHandle = null;
|
|
12949
|
+
}
|
|
12950
|
+
};
|
|
12951
|
+
return { start, stop };
|
|
12952
|
+
}
|
|
12953
|
+
function registerKernelExtensions(kernel, pluginRuntime, noBuiltIns) {
|
|
12954
|
+
if (!noBuiltIns) {
|
|
12955
|
+
const enabledBuiltIns = filterBuiltInManifests(
|
|
12956
|
+
listBuiltIns(),
|
|
12957
|
+
pluginRuntime.resolveEnabled
|
|
12958
|
+
);
|
|
12959
|
+
for (const manifest of enabledBuiltIns) kernel.registry.register(manifest);
|
|
12960
|
+
}
|
|
12961
|
+
for (const manifest of pluginRuntime.manifests) kernel.registry.register(manifest);
|
|
12962
|
+
}
|
|
12963
|
+
function buildBroadcasterEmitter(broadcaster) {
|
|
12964
|
+
return {
|
|
12965
|
+
emit(event) {
|
|
12966
|
+
broadcaster.broadcast(event);
|
|
12967
|
+
},
|
|
12968
|
+
subscribe() {
|
|
12969
|
+
return () => {
|
|
12970
|
+
};
|
|
12971
|
+
}
|
|
12972
|
+
};
|
|
12973
|
+
}
|
|
12974
|
+
async function loadPriorState(dbPath) {
|
|
12975
|
+
return tryWithSqlite({ databasePath: dbPath, autoBackup: false }, async (reader) => {
|
|
12976
|
+
const loaded = await reader.scans.load();
|
|
12977
|
+
if (loaded.nodes.length === 0) return null;
|
|
12978
|
+
const extractorRuns = await reader.scans.loadExtractorRuns();
|
|
12979
|
+
return { snapshot: loaded, extractorRuns };
|
|
12980
|
+
});
|
|
12981
|
+
}
|
|
12982
|
+
function assembleRunOptions(args2) {
|
|
12983
|
+
const runOptions = {
|
|
12984
|
+
roots: [WATCH_ROOT],
|
|
12985
|
+
scope: args2.scope,
|
|
12986
|
+
tokenize: args2.tokenize,
|
|
12987
|
+
ignoreFilter: args2.ignoreFilter,
|
|
12988
|
+
strict: args2.strict,
|
|
12989
|
+
emitter: args2.emitter
|
|
12990
|
+
};
|
|
12991
|
+
if (args2.composed) runOptions.extensions = args2.composed;
|
|
12992
|
+
if (args2.priorState) {
|
|
12993
|
+
runOptions.priorSnapshot = args2.priorState.snapshot;
|
|
12994
|
+
runOptions.enableCache = true;
|
|
12995
|
+
runOptions.priorExtractorRuns = args2.priorState.extractorRuns;
|
|
12996
|
+
}
|
|
12997
|
+
return runOptions;
|
|
12998
|
+
}
|
|
12999
|
+
async function persistOutcome(dbPath, ran) {
|
|
13000
|
+
const { result, renameOps, extractorRuns, enrichments } = ran;
|
|
13001
|
+
await withSqlite(
|
|
13002
|
+
{ databasePath: dbPath },
|
|
13003
|
+
(writer) => writer.scans.persist(result, { renameOps, extractorRuns, enrichments })
|
|
11997
13004
|
);
|
|
11998
13005
|
}
|
|
11999
13006
|
|
|
@@ -12013,20 +13020,26 @@ function validateServerOptions(input) {
|
|
|
12013
13020
|
if (scopeError !== null) return { ok: false, error: scopeError };
|
|
12014
13021
|
const hostError = validateHost(filled.host, filled.devCors);
|
|
12015
13022
|
if (hostError !== null) return { ok: false, error: hostError };
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
13023
|
+
const watcherError = validateWatcher(filled.noWatcher, filled.noBuiltIns, filled.noPlugins);
|
|
13024
|
+
if (watcherError !== null) return { ok: false, error: watcherError };
|
|
13025
|
+
const debounceError = validateWatcherDebounce(input.watcherDebounceMs);
|
|
13026
|
+
if (debounceError !== null) return { ok: false, error: debounceError };
|
|
13027
|
+
const options = {
|
|
13028
|
+
port: filled.port,
|
|
13029
|
+
host: filled.host,
|
|
13030
|
+
scope: filled.scope,
|
|
13031
|
+
dbPath: input.dbPath,
|
|
13032
|
+
uiDist: filled.uiDist,
|
|
13033
|
+
noBuiltIns: filled.noBuiltIns,
|
|
13034
|
+
noPlugins: filled.noPlugins,
|
|
13035
|
+
open: filled.open,
|
|
13036
|
+
devCors: filled.devCors,
|
|
13037
|
+
noWatcher: filled.noWatcher
|
|
12029
13038
|
};
|
|
13039
|
+
if (input.watcherDebounceMs !== void 0) {
|
|
13040
|
+
options.watcherDebounceMs = input.watcherDebounceMs;
|
|
13041
|
+
}
|
|
13042
|
+
return { ok: true, options };
|
|
12030
13043
|
}
|
|
12031
13044
|
function applyDefaults(input) {
|
|
12032
13045
|
return {
|
|
@@ -12037,7 +13050,8 @@ function applyDefaults(input) {
|
|
|
12037
13050
|
noBuiltIns: input.noBuiltIns ?? false,
|
|
12038
13051
|
noPlugins: input.noPlugins ?? false,
|
|
12039
13052
|
open: input.open ?? true,
|
|
12040
|
-
devCors: input.devCors ?? false
|
|
13053
|
+
devCors: input.devCors ?? false,
|
|
13054
|
+
noWatcher: input.noWatcher ?? false
|
|
12041
13055
|
};
|
|
12042
13056
|
}
|
|
12043
13057
|
function validatePort(port) {
|
|
@@ -12069,10 +13083,32 @@ function validateHost(host, devCors) {
|
|
|
12069
13083
|
}
|
|
12070
13084
|
return null;
|
|
12071
13085
|
}
|
|
13086
|
+
function validateWatcher(noWatcher, noBuiltIns, _noPlugins) {
|
|
13087
|
+
if (noWatcher) return null;
|
|
13088
|
+
if (noBuiltIns) {
|
|
13089
|
+
return {
|
|
13090
|
+
code: "watcher-requires-pipeline",
|
|
13091
|
+
message: "the watcher cannot run with --no-built-ins (would persist empty scans on every batch). Pass --no-watcher to opt out, or drop --no-built-ins.",
|
|
13092
|
+
value: "no-built-ins"
|
|
13093
|
+
};
|
|
13094
|
+
}
|
|
13095
|
+
return null;
|
|
13096
|
+
}
|
|
13097
|
+
function validateWatcherDebounce(value) {
|
|
13098
|
+
if (value === void 0) return null;
|
|
13099
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
13100
|
+
return {
|
|
13101
|
+
code: "watcher-debounce-invalid",
|
|
13102
|
+
message: `--watcher-debounce-ms must be a non-negative integer (got ${value})`,
|
|
13103
|
+
value: String(value)
|
|
13104
|
+
};
|
|
13105
|
+
}
|
|
13106
|
+
return null;
|
|
13107
|
+
}
|
|
12072
13108
|
|
|
12073
13109
|
// server/paths.ts
|
|
12074
13110
|
import { existsSync as existsSync17, statSync as statSync5 } from "fs";
|
|
12075
|
-
import { dirname as dirname9, isAbsolute as
|
|
13111
|
+
import { dirname as dirname9, isAbsolute as isAbsolute5, join as join14, resolve as resolve19 } from "path";
|
|
12076
13112
|
var DEFAULT_UI_REL = join14("ui", "dist", "browser");
|
|
12077
13113
|
var INDEX_HTML2 = "index.html";
|
|
12078
13114
|
function resolveDefaultUiDist(ctx) {
|
|
@@ -12087,7 +13123,7 @@ function resolveDefaultUiDist(ctx) {
|
|
|
12087
13123
|
return null;
|
|
12088
13124
|
}
|
|
12089
13125
|
function resolveExplicitUiDist(ctx, raw) {
|
|
12090
|
-
return
|
|
13126
|
+
return isAbsolute5(raw) ? raw : resolve19(ctx.cwd, raw);
|
|
12091
13127
|
}
|
|
12092
13128
|
function isUiBundleDir(path) {
|
|
12093
13129
|
if (!existsSync17(path)) return false;
|
|
@@ -12100,21 +13136,74 @@ function isUiBundleDir(path) {
|
|
|
12100
13136
|
}
|
|
12101
13137
|
|
|
12102
13138
|
// server/index.ts
|
|
12103
|
-
async function createServer(options) {
|
|
13139
|
+
async function createServer(options, extra = {}) {
|
|
12104
13140
|
const specVersion = await resolveSpecVersion2();
|
|
12105
|
-
const
|
|
13141
|
+
const runtimeContext = extra.runtimeContext ?? defaultRuntimeContext();
|
|
13142
|
+
const broadcaster = new WsBroadcaster();
|
|
13143
|
+
const kindRegistry = await assembleKindRegistry(options);
|
|
13144
|
+
const app = createApp({
|
|
13145
|
+
options,
|
|
13146
|
+
specVersion,
|
|
13147
|
+
broadcaster,
|
|
13148
|
+
runtimeContext,
|
|
13149
|
+
kindRegistry
|
|
13150
|
+
});
|
|
12106
13151
|
const wss = new WebSocketServer({ noServer: true });
|
|
12107
13152
|
const server = await listenAsync(app.fetch, wss, options.host, options.port);
|
|
12108
13153
|
const addr = server.address();
|
|
12109
13154
|
const address = normalizeAddress(addr, options.host, options.port);
|
|
13155
|
+
let watcherService = null;
|
|
13156
|
+
if (!options.noWatcher) {
|
|
13157
|
+
const debounce = options.watcherDebounceMs;
|
|
13158
|
+
const svcOpts = {
|
|
13159
|
+
options,
|
|
13160
|
+
runtimeContext,
|
|
13161
|
+
broadcaster
|
|
13162
|
+
};
|
|
13163
|
+
if (debounce !== void 0) svcOpts.debounceMsOverride = debounce;
|
|
13164
|
+
const candidate = createWatcherService(svcOpts);
|
|
13165
|
+
try {
|
|
13166
|
+
await candidate.start();
|
|
13167
|
+
watcherService = candidate;
|
|
13168
|
+
} catch (err) {
|
|
13169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13170
|
+
log.warn(
|
|
13171
|
+
tx(SERVER_TEXTS.watcherBootFailed, {
|
|
13172
|
+
message: sanitizeForTerminal(message)
|
|
13173
|
+
})
|
|
13174
|
+
);
|
|
13175
|
+
try {
|
|
13176
|
+
await candidate.stop();
|
|
13177
|
+
} catch {
|
|
13178
|
+
}
|
|
13179
|
+
}
|
|
13180
|
+
}
|
|
12110
13181
|
let closed = false;
|
|
12111
13182
|
const close = async () => {
|
|
12112
13183
|
if (closed) return;
|
|
12113
13184
|
closed = true;
|
|
13185
|
+
if (watcherService) {
|
|
13186
|
+
try {
|
|
13187
|
+
await watcherService.stop();
|
|
13188
|
+
} catch {
|
|
13189
|
+
}
|
|
13190
|
+
}
|
|
13191
|
+
broadcaster.shutdown();
|
|
12114
13192
|
await closeServer(server);
|
|
12115
13193
|
wss.close();
|
|
12116
13194
|
};
|
|
12117
|
-
return { address, close };
|
|
13195
|
+
return { address, close, broadcaster };
|
|
13196
|
+
}
|
|
13197
|
+
async function assembleKindRegistry(options) {
|
|
13198
|
+
const pluginRuntime = options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: options.scope });
|
|
13199
|
+
for (const warn of pluginRuntime.warnings) {
|
|
13200
|
+
log.warn(sanitizeForTerminal(warn));
|
|
13201
|
+
}
|
|
13202
|
+
const composed = composeScanExtensions({
|
|
13203
|
+
noBuiltIns: options.noBuiltIns,
|
|
13204
|
+
pluginRuntime
|
|
13205
|
+
});
|
|
13206
|
+
return buildKindRegistry(composed?.providers ?? []);
|
|
12118
13207
|
}
|
|
12119
13208
|
function listenAsync(fetchCallback, wss, host, port) {
|
|
12120
13209
|
return new Promise((resolveListen, rejectListen) => {
|
|
@@ -12183,6 +13272,9 @@ var SERVE_TEXTS = {
|
|
|
12183
13272
|
portOutOfRange: "sm serve: --port must be an integer in [0, 65535] (got {{value}}).\n",
|
|
12184
13273
|
portInvalid: "sm serve: --port must be a non-negative integer (got {{value}}).\n",
|
|
12185
13274
|
scopeInvalid: 'sm serve: --scope must be "project" or "global" (got {{value}}).\n',
|
|
13275
|
+
// Watcher option failures — ExitCode.Error.
|
|
13276
|
+
watcherRequiresPipeline: "sm serve: --no-built-ins is incompatible with the watcher (would persist empty scans on every batch). Pass --no-watcher to opt out, or drop --no-built-ins.\n",
|
|
13277
|
+
watcherDebounceInvalid: "sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n",
|
|
12186
13278
|
// Generic operational error — surfaced when the server itself throws
|
|
12187
13279
|
// before the listener binds (e.g. UI bundle missing under explicit
|
|
12188
13280
|
// --ui-dist).
|
|
@@ -12257,6 +13349,13 @@ var ServeCommand = class extends SmCommand {
|
|
|
12257
13349
|
// need it). Clipanion still exposes it on the parser; the Usage
|
|
12258
13350
|
// omission is the "hidden" contract per the 14.1 brief.
|
|
12259
13351
|
uiDist = Option19.String("--ui-dist", { required: false, hidden: true });
|
|
13352
|
+
noWatcher = Option19.Boolean("--no-watcher", false, {
|
|
13353
|
+
description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
|
|
13354
|
+
});
|
|
13355
|
+
// `--watcher-debounce-ms` is undocumented sugar for advanced users
|
|
13356
|
+
// who want to tighten / relax the watcher's batching window without
|
|
13357
|
+
// editing settings.json. Hidden flag — the Usage block omits it.
|
|
13358
|
+
watcherDebounceMs = Option19.String("--watcher-debounce-ms", { required: false, hidden: true });
|
|
12260
13359
|
// Long-running daemon — `done in <…>` after a graceful shutdown is
|
|
12261
13360
|
// noise. Mirrors `sm watch`'s opt-out.
|
|
12262
13361
|
emitElapsed = false;
|
|
@@ -12297,6 +13396,15 @@ var ServeCommand = class extends SmCommand {
|
|
|
12297
13396
|
);
|
|
12298
13397
|
return ExitCode.Error;
|
|
12299
13398
|
}
|
|
13399
|
+
const debounceResult = parseDebounce(this.watcherDebounceMs);
|
|
13400
|
+
if (!debounceResult.ok) {
|
|
13401
|
+
this.context.stderr.write(
|
|
13402
|
+
tx(SERVE_TEXTS.watcherDebounceInvalid, {
|
|
13403
|
+
value: sanitizeForTerminal(debounceResult.value)
|
|
13404
|
+
})
|
|
13405
|
+
);
|
|
13406
|
+
return ExitCode.Error;
|
|
13407
|
+
}
|
|
12300
13408
|
const input = {
|
|
12301
13409
|
dbPath,
|
|
12302
13410
|
scope,
|
|
@@ -12304,10 +13412,12 @@ var ServeCommand = class extends SmCommand {
|
|
|
12304
13412
|
noBuiltIns: this.noBuiltIns,
|
|
12305
13413
|
noPlugins: this.noPlugins,
|
|
12306
13414
|
open: this.open,
|
|
12307
|
-
devCors: this.devCors
|
|
13415
|
+
devCors: this.devCors,
|
|
13416
|
+
noWatcher: this.noWatcher
|
|
12308
13417
|
};
|
|
12309
13418
|
if (portResult.port !== void 0) input.port = portResult.port;
|
|
12310
13419
|
if (this.host !== void 0) input.host = this.host;
|
|
13420
|
+
if (debounceResult.value !== void 0) input.watcherDebounceMs = debounceResult.value;
|
|
12311
13421
|
const validation = validateServerOptions(input);
|
|
12312
13422
|
if (!validation.ok) {
|
|
12313
13423
|
this.context.stderr.write(formatValidationError(validation.error));
|
|
@@ -12360,6 +13470,15 @@ function parsePort(raw) {
|
|
|
12360
13470
|
}
|
|
12361
13471
|
return { ok: true, port: parsed };
|
|
12362
13472
|
}
|
|
13473
|
+
function parseDebounce(raw) {
|
|
13474
|
+
if (raw === void 0) return { ok: true, value: void 0 };
|
|
13475
|
+
const trimmed = raw.trim();
|
|
13476
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
13477
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
13478
|
+
return { ok: false, value: raw };
|
|
13479
|
+
}
|
|
13480
|
+
return { ok: true, value: parsed };
|
|
13481
|
+
}
|
|
12363
13482
|
function resolveScope(rawScope, global) {
|
|
12364
13483
|
if (rawScope === void 0) return { ok: true, scope: global ? "global" : "project" };
|
|
12365
13484
|
if (rawScope === "project" || rawScope === "global") {
|
|
@@ -12390,6 +13509,10 @@ function formatValidationError(err) {
|
|
|
12390
13509
|
return tx(SERVE_TEXTS.portInvalid, { value: sanitizeForTerminal(err.value) });
|
|
12391
13510
|
case "scope-invalid":
|
|
12392
13511
|
return tx(SERVE_TEXTS.scopeInvalid, { value: sanitizeForTerminal(err.value) });
|
|
13512
|
+
case "watcher-requires-pipeline":
|
|
13513
|
+
return tx(SERVE_TEXTS.watcherRequiresPipeline, { value: sanitizeForTerminal(err.value) });
|
|
13514
|
+
case "watcher-debounce-invalid":
|
|
13515
|
+
return tx(SERVE_TEXTS.watcherDebounceInvalid, { value: sanitizeForTerminal(err.value) });
|
|
12393
13516
|
default:
|
|
12394
13517
|
return tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(err.message) });
|
|
12395
13518
|
}
|