@skill-map/cli 0.9.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 +2546 -718
- package/dist/cli.js.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +64 -0
- package/dist/kernel/index.js +15 -7
- package/dist/kernel/index.js.map +1 -1
- package/package.json +7 -2
package/dist/cli.js
CHANGED
|
@@ -180,7 +180,7 @@ function extractLogLevelFlag(argv) {
|
|
|
180
180
|
var LOGGER_ENV_VAR = ENV_VAR;
|
|
181
181
|
|
|
182
182
|
// cli/commands/check.ts
|
|
183
|
-
import { Command, Option } from "clipanion";
|
|
183
|
+
import { Command as Command2, Option as Option2 } from "clipanion";
|
|
184
184
|
|
|
185
185
|
// kernel/i18n/registry.texts.ts
|
|
186
186
|
var REGISTRY_TEXTS = {
|
|
@@ -297,7 +297,14 @@ var SKILL_MAP_DIR = ".skill-map";
|
|
|
297
297
|
var DB_FILENAME = "skill-map.db";
|
|
298
298
|
var JOBS_DIRNAME = "jobs";
|
|
299
299
|
var PLUGINS_DIRNAME = "plugins";
|
|
300
|
+
var SETTINGS_FILENAME = "settings.json";
|
|
301
|
+
var LOCAL_SETTINGS_FILENAME = "settings.local.json";
|
|
302
|
+
var IGNORE_FILENAME = ".skill-mapignore";
|
|
300
303
|
var DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;
|
|
304
|
+
var GITIGNORE_ENTRIES = [
|
|
305
|
+
`${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,
|
|
306
|
+
`${SKILL_MAP_DIR}/${DB_FILENAME}`
|
|
307
|
+
];
|
|
301
308
|
function resolveDbPath(options) {
|
|
302
309
|
if (options.db) return resolve(options.db);
|
|
303
310
|
if (options.global) return join(options.homedir, DEFAULT_DB_REL);
|
|
@@ -315,6 +322,18 @@ function defaultProjectPluginsDir(ctx) {
|
|
|
315
322
|
function defaultUserPluginsDir(ctx) {
|
|
316
323
|
return join(ctx.homedir, SKILL_MAP_DIR, PLUGINS_DIRNAME);
|
|
317
324
|
}
|
|
325
|
+
function defaultDbPath(scopeRoot) {
|
|
326
|
+
return join(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);
|
|
327
|
+
}
|
|
328
|
+
function defaultSettingsPath(scopeRoot) {
|
|
329
|
+
return join(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME);
|
|
330
|
+
}
|
|
331
|
+
function defaultLocalSettingsPath(scopeRoot) {
|
|
332
|
+
return join(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);
|
|
333
|
+
}
|
|
334
|
+
function defaultIgnoreFilePath(scopeRoot) {
|
|
335
|
+
return join(scopeRoot, IGNORE_FILENAME);
|
|
336
|
+
}
|
|
318
337
|
function assertDbExists(path, stderr) {
|
|
319
338
|
if (path === ":memory:" || existsSync(path)) return true;
|
|
320
339
|
stderr.write(tx(UTIL_TEXTS.dbNotFound, { path }));
|
|
@@ -390,11 +409,11 @@ function buildIgnoreFilter(opts = {}) {
|
|
|
390
409
|
ig.add(opts.ignoreFileText);
|
|
391
410
|
}
|
|
392
411
|
return {
|
|
393
|
-
ignores(
|
|
394
|
-
if (
|
|
412
|
+
ignores(relativePath2) {
|
|
413
|
+
if (relativePath2 === "" || relativePath2 === "." || relativePath2 === "./") {
|
|
395
414
|
return false;
|
|
396
415
|
}
|
|
397
|
-
const normalised =
|
|
416
|
+
const normalised = relativePath2.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
398
417
|
if (normalised === "") return false;
|
|
399
418
|
return ig.ignores(normalised);
|
|
400
419
|
}
|
|
@@ -605,31 +624,79 @@ var claudeProvider = {
|
|
|
605
624
|
// pairs the relative manifest-style schema path (mirrors what the
|
|
606
625
|
// spec's provider.schema.json validates) with the loaded JSON Schema
|
|
607
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).
|
|
608
636
|
kinds: {
|
|
609
637
|
agent: {
|
|
610
638
|
schema: "./schemas/agent.schema.json",
|
|
611
639
|
schemaJson: agent_schema_default,
|
|
612
|
-
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
|
+
}
|
|
613
647
|
},
|
|
614
648
|
command: {
|
|
615
649
|
schema: "./schemas/command.schema.json",
|
|
616
650
|
schemaJson: command_schema_default,
|
|
617
|
-
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
|
+
}
|
|
618
661
|
},
|
|
619
662
|
hook: {
|
|
620
663
|
schema: "./schemas/hook.schema.json",
|
|
621
664
|
schemaJson: hook_schema_default,
|
|
622
|
-
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
|
+
}
|
|
623
675
|
},
|
|
624
676
|
skill: {
|
|
625
677
|
schema: "./schemas/skill.schema.json",
|
|
626
678
|
schemaJson: skill_schema_default,
|
|
627
|
-
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
|
+
}
|
|
628
686
|
},
|
|
629
687
|
note: {
|
|
630
688
|
schema: "./schemas/note.schema.json",
|
|
631
689
|
schemaJson: note_schema_default,
|
|
632
|
-
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
|
+
}
|
|
633
700
|
}
|
|
634
701
|
},
|
|
635
702
|
async *walk(roots, options = {}) {
|
|
@@ -1367,8 +1434,15 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
1367
1434
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
1368
1435
|
import { createRequire } from "module";
|
|
1369
1436
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
1437
|
+
|
|
1438
|
+
// kernel/util/ajv-interop.ts
|
|
1370
1439
|
import addFormatsModule from "ajv-formats";
|
|
1371
1440
|
var addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
1441
|
+
function applyAjvFormats(ajv) {
|
|
1442
|
+
addFormats(ajv);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// kernel/adapters/schema-validators.ts
|
|
1372
1446
|
var SCHEMA_FILES = {
|
|
1373
1447
|
node: "schemas/node.schema.json",
|
|
1374
1448
|
link: "schemas/link.schema.json",
|
|
@@ -1407,7 +1481,7 @@ function buildSchemaValidators() {
|
|
|
1407
1481
|
allErrors: true,
|
|
1408
1482
|
allowUnionTypes: true
|
|
1409
1483
|
});
|
|
1410
|
-
|
|
1484
|
+
applyAjvFormats(ajv);
|
|
1411
1485
|
for (const rel of SUPPORTING_SCHEMAS) {
|
|
1412
1486
|
const file = resolve3(specRoot, rel);
|
|
1413
1487
|
if (!existsSyncSafe(file)) continue;
|
|
@@ -1462,7 +1536,7 @@ function buildProviderFrontmatterValidator(providers) {
|
|
|
1462
1536
|
allErrors: true,
|
|
1463
1537
|
allowUnionTypes: true
|
|
1464
1538
|
});
|
|
1465
|
-
|
|
1539
|
+
applyAjvFormats(ajv);
|
|
1466
1540
|
const baseFile = resolve3(specRoot, "schemas/frontmatter/base.schema.json");
|
|
1467
1541
|
const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
|
|
1468
1542
|
ajv.addSchema(baseSchema);
|
|
@@ -1683,7 +1757,6 @@ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync }
|
|
|
1683
1757
|
import { isAbsolute, join as join3, relative as relative2, resolve as resolve4 } from "path";
|
|
1684
1758
|
import { pathToFileURL } from "url";
|
|
1685
1759
|
import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
|
|
1686
|
-
import addFormatsModule2 from "ajv-formats";
|
|
1687
1760
|
import semver from "semver";
|
|
1688
1761
|
|
|
1689
1762
|
// kernel/i18n/plugin-loader.texts.ts
|
|
@@ -1728,7 +1801,6 @@ var HOOK_TRIGGERS = Object.freeze([
|
|
|
1728
1801
|
]);
|
|
1729
1802
|
|
|
1730
1803
|
// kernel/adapters/plugin-loader.ts
|
|
1731
|
-
var addFormats2 = addFormatsModule2.default ?? addFormatsModule2;
|
|
1732
1804
|
var DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS = 5e3;
|
|
1733
1805
|
function createPluginLoader(options) {
|
|
1734
1806
|
return new PluginLoader(options);
|
|
@@ -2243,7 +2315,7 @@ function compilePluginSchema(pluginPath, relPath) {
|
|
|
2243
2315
|
}
|
|
2244
2316
|
try {
|
|
2245
2317
|
const ajv = new Ajv20202({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
2246
|
-
|
|
2318
|
+
applyAjvFormats(ajv);
|
|
2247
2319
|
const compiled = ajv.compile(raw);
|
|
2248
2320
|
return { ok: true, validate: compiled };
|
|
2249
2321
|
} catch (err) {
|
|
@@ -2628,7 +2700,7 @@ var AsyncMutex = class {
|
|
|
2628
2700
|
this.#locked = true;
|
|
2629
2701
|
return;
|
|
2630
2702
|
}
|
|
2631
|
-
await new Promise((
|
|
2703
|
+
await new Promise((resolve20) => this.#waiters.push(resolve20));
|
|
2632
2704
|
this.#locked = true;
|
|
2633
2705
|
}
|
|
2634
2706
|
unlock() {
|
|
@@ -4627,11 +4699,111 @@ function formatWarning(plugin) {
|
|
|
4627
4699
|
});
|
|
4628
4700
|
}
|
|
4629
4701
|
|
|
4702
|
+
// cli/util/sm-command.ts
|
|
4703
|
+
import { Command, Option } from "clipanion";
|
|
4704
|
+
|
|
4705
|
+
// cli/util/elapsed.ts
|
|
4706
|
+
function startElapsed() {
|
|
4707
|
+
const startNs = process.hrtime.bigint();
|
|
4708
|
+
return {
|
|
4709
|
+
ms() {
|
|
4710
|
+
const elapsedNs = Number(process.hrtime.bigint() - startNs);
|
|
4711
|
+
return Math.round(elapsedNs / 1e6);
|
|
4712
|
+
},
|
|
4713
|
+
formatted() {
|
|
4714
|
+
return formatElapsed(this.ms());
|
|
4715
|
+
}
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4718
|
+
function formatElapsed(ms) {
|
|
4719
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
4720
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
4721
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4722
|
+
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
4723
|
+
return `${minutes}m ${seconds}s`;
|
|
4724
|
+
}
|
|
4725
|
+
function emitDoneStderr(stderr, elapsed, quiet = false) {
|
|
4726
|
+
if (quiet) return;
|
|
4727
|
+
stderr.write(tx(UTIL_TEXTS.doneIn, { elapsed: elapsed.formatted() }));
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
// cli/util/sm-command.ts
|
|
4731
|
+
function isEnvSet(value) {
|
|
4732
|
+
return value !== void 0 && value !== "";
|
|
4733
|
+
}
|
|
4734
|
+
var SmCommand = class extends Command {
|
|
4735
|
+
global = Option.Boolean("-g,--global", false, {
|
|
4736
|
+
description: "Operate on ~/.skill-map/ instead of ./.skill-map/."
|
|
4737
|
+
});
|
|
4738
|
+
json = Option.Boolean("--json", false, {
|
|
4739
|
+
description: "Emit machine-readable output on stdout. Suppresses pretty printing."
|
|
4740
|
+
});
|
|
4741
|
+
quiet = Option.Boolean("-q,--quiet", false, {
|
|
4742
|
+
description: 'Suppress non-error stderr output (including "done in <\u2026>").'
|
|
4743
|
+
});
|
|
4744
|
+
noColor = Option.Boolean("--no-color", false, {
|
|
4745
|
+
description: "Disable ANSI color codes."
|
|
4746
|
+
});
|
|
4747
|
+
verbose = Option.Counter("-v,--verbose", 0, {
|
|
4748
|
+
description: "Increase log level (-v=info, -vv=debug, -vvv=trace)."
|
|
4749
|
+
});
|
|
4750
|
+
db = Option.String("--db", { required: false, description: "Override the database file location (escape hatch)." });
|
|
4751
|
+
/**
|
|
4752
|
+
* Subclasses set this to `false` to opt out of the trailing
|
|
4753
|
+
* `done in <…>` line — appropriate for interactive verbs (`db shell`),
|
|
4754
|
+
* watcher loops (`watch`), and meta verbs that report a fixed
|
|
4755
|
+
* version (`version`, `help`).
|
|
4756
|
+
*/
|
|
4757
|
+
emitElapsed = true;
|
|
4758
|
+
/**
|
|
4759
|
+
* Wall-clock timer started just before `run()`. Subclasses that need
|
|
4760
|
+
* to embed `elapsedMs` in their `--json` output read `this.elapsed.ms()`.
|
|
4761
|
+
* `null` only between `Command` construction and the first
|
|
4762
|
+
* `execute()` call.
|
|
4763
|
+
*/
|
|
4764
|
+
elapsed = null;
|
|
4765
|
+
async execute() {
|
|
4766
|
+
this.applyEnvOverrides();
|
|
4767
|
+
this.applyVerboseLogger();
|
|
4768
|
+
this.elapsed = startElapsed();
|
|
4769
|
+
try {
|
|
4770
|
+
return await this.run();
|
|
4771
|
+
} finally {
|
|
4772
|
+
if (this.emitElapsed) emitDoneStderr(this.context.stderr, this.elapsed, this.quiet);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4776
|
+
* Promote spec env vars into flag values when the flag was left at
|
|
4777
|
+
* default. CLI flag wins over env var (spec § Global flags
|
|
4778
|
+
* precedence: "CLI flag wins over env var. Env var wins over config
|
|
4779
|
+
* file.").
|
|
4780
|
+
*/
|
|
4781
|
+
applyEnvOverrides() {
|
|
4782
|
+
const env = process.env;
|
|
4783
|
+
this.noColor = this.noColor || isEnvSet(env["NO_COLOR"]);
|
|
4784
|
+
this.global = this.global || env["SKILL_MAP_SCOPE"] === "global";
|
|
4785
|
+
this.json = this.json || isEnvSet(env["SKILL_MAP_JSON"]);
|
|
4786
|
+
if (this.db === void 0 && isEnvSet(env["SKILL_MAP_DB"])) {
|
|
4787
|
+
this.db = env["SKILL_MAP_DB"];
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
/**
|
|
4791
|
+
* `-v` / `-vv` / `-vvv` reconfigures the kernel logger. Skipped
|
|
4792
|
+
* when `verbose === 0` so the level configured at `entry.ts` boot
|
|
4793
|
+
* (from `--log-level` / `SKILL_MAP_LOG_LEVEL`) stays in effect.
|
|
4794
|
+
*/
|
|
4795
|
+
applyVerboseLogger() {
|
|
4796
|
+
if (this.verbose <= 0) return;
|
|
4797
|
+
const level = this.verbose >= 3 ? "trace" : this.verbose === 2 ? "debug" : "info";
|
|
4798
|
+
configureLogger(new Logger({ level, stream: process.stderr }));
|
|
4799
|
+
}
|
|
4800
|
+
};
|
|
4801
|
+
|
|
4630
4802
|
// cli/commands/check.ts
|
|
4631
4803
|
var SEVERITY_ORDER = ["error", "warn", "info"];
|
|
4632
|
-
var CheckCommand = class extends
|
|
4804
|
+
var CheckCommand = class extends SmCommand {
|
|
4633
4805
|
static paths = [["check"]];
|
|
4634
|
-
static usage =
|
|
4806
|
+
static usage = Command2.Usage({
|
|
4635
4807
|
category: "Browse",
|
|
4636
4808
|
description: "Print all current issues (reads from DB, faster than sm scan --json | jq).",
|
|
4637
4809
|
details: `
|
|
@@ -4656,27 +4828,24 @@ var CheckCommand = class extends Command {
|
|
|
4656
4828
|
["Use a non-default DB file", "$0 check --db /path/to/skill-map.db"]
|
|
4657
4829
|
]
|
|
4658
4830
|
});
|
|
4659
|
-
|
|
4660
|
-
db = Option.String("--db", { required: false });
|
|
4661
|
-
json = Option.Boolean("--json", false);
|
|
4662
|
-
node = Option.String("-n,--node", {
|
|
4831
|
+
node = Option2.String("-n,--node", {
|
|
4663
4832
|
required: false,
|
|
4664
4833
|
description: "Restrict to issues whose nodeIds include the given path. Combines with --rules and --include-prob."
|
|
4665
4834
|
});
|
|
4666
|
-
rules =
|
|
4835
|
+
rules = Option2.String("--rules", {
|
|
4667
4836
|
required: false,
|
|
4668
4837
|
description: "Comma-separated rule ids (qualified or short). Restrict the issue read; with --include-prob, also filters which prob rules surface in the advisory."
|
|
4669
4838
|
});
|
|
4670
|
-
includeProb =
|
|
4839
|
+
includeProb = Option2.Boolean("--include-prob", false, {
|
|
4671
4840
|
description: "Detect probabilistic Rules and emit a stub advisory naming them (full dispatch lands at Step 10). Default off \u2192 deterministic-only, CI-safe."
|
|
4672
4841
|
});
|
|
4673
|
-
async =
|
|
4842
|
+
async = Option2.Boolean("--async", false, {
|
|
4674
4843
|
description: "Reserved companion to --include-prob: once jobs ship, returns job ids without waiting. No effect today."
|
|
4675
4844
|
});
|
|
4676
|
-
noPlugins =
|
|
4845
|
+
noPlugins = Option2.Boolean("--no-plugins", false, {
|
|
4677
4846
|
description: "Skip drop-in plugin discovery; only kernel built-ins participate in the prob detection. Same flag shape as `sm scan`."
|
|
4678
4847
|
});
|
|
4679
|
-
async
|
|
4848
|
+
async run() {
|
|
4680
4849
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
4681
4850
|
if (!assertDbExists(dbPath, this.context.stderr)) return ExitCode.NotFound;
|
|
4682
4851
|
const ruleFilter = parseRulesFlag(this.rules);
|
|
@@ -4785,32 +4954,7 @@ import {
|
|
|
4785
4954
|
writeFileSync
|
|
4786
4955
|
} from "fs";
|
|
4787
4956
|
import { dirname as dirname5, join as join7 } from "path";
|
|
4788
|
-
import { Command as
|
|
4789
|
-
|
|
4790
|
-
// cli/util/elapsed.ts
|
|
4791
|
-
function startElapsed() {
|
|
4792
|
-
const startNs = process.hrtime.bigint();
|
|
4793
|
-
return {
|
|
4794
|
-
ms() {
|
|
4795
|
-
const elapsedNs = Number(process.hrtime.bigint() - startNs);
|
|
4796
|
-
return Math.round(elapsedNs / 1e6);
|
|
4797
|
-
},
|
|
4798
|
-
formatted() {
|
|
4799
|
-
return formatElapsed(this.ms());
|
|
4800
|
-
}
|
|
4801
|
-
};
|
|
4802
|
-
}
|
|
4803
|
-
function formatElapsed(ms) {
|
|
4804
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
4805
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
4806
|
-
const minutes = Math.floor(ms / 6e4);
|
|
4807
|
-
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
4808
|
-
return `${minutes}m ${seconds}s`;
|
|
4809
|
-
}
|
|
4810
|
-
function emitDoneStderr(stderr, elapsed, quiet = false) {
|
|
4811
|
-
if (quiet) return;
|
|
4812
|
-
stderr.write(tx(UTIL_TEXTS.doneIn, { elapsed: elapsed.formatted() }));
|
|
4813
|
-
}
|
|
4957
|
+
import { Command as Command3, Option as Option3 } from "clipanion";
|
|
4814
4958
|
|
|
4815
4959
|
// cli/util/error-reporter.ts
|
|
4816
4960
|
function formatErrorMessage(err) {
|
|
@@ -4970,9 +5114,9 @@ function formatValueHuman(v) {
|
|
|
4970
5114
|
if (Array.isArray(v) || typeof v === "object" && v !== null) return JSON.stringify(v);
|
|
4971
5115
|
return String(v);
|
|
4972
5116
|
}
|
|
4973
|
-
var ConfigListCommand = class extends
|
|
5117
|
+
var ConfigListCommand = class extends SmCommand {
|
|
4974
5118
|
static paths = [["config", "list"]];
|
|
4975
|
-
static usage =
|
|
5119
|
+
static usage = Command3.Usage({
|
|
4976
5120
|
category: "Config",
|
|
4977
5121
|
description: "Print the effective config after layered merge.",
|
|
4978
5122
|
details: `
|
|
@@ -4981,10 +5125,11 @@ var ConfigListCommand = class extends Command2 {
|
|
|
4981
5125
|
Exempt from "done in <\u2026>" per spec/cli-contract.md \xA7Elapsed time.
|
|
4982
5126
|
`
|
|
4983
5127
|
});
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5128
|
+
strict = Option3.Boolean("--strict", false);
|
|
5129
|
+
// Read-only config inspection: spec § Elapsed time exempts the
|
|
5130
|
+
// config family from the trailing "done in" line.
|
|
5131
|
+
emitElapsed = false;
|
|
5132
|
+
async run() {
|
|
4988
5133
|
const result = tryLoadConfig(
|
|
4989
5134
|
{ scope: this.global ? "global" : "project", strict: this.strict, ...defaultRuntimeContext() },
|
|
4990
5135
|
this.context.stderr
|
|
@@ -5005,9 +5150,9 @@ var ConfigListCommand = class extends Command2 {
|
|
|
5005
5150
|
return ExitCode.Ok;
|
|
5006
5151
|
}
|
|
5007
5152
|
};
|
|
5008
|
-
var ConfigGetCommand = class extends
|
|
5153
|
+
var ConfigGetCommand = class extends SmCommand {
|
|
5009
5154
|
static paths = [["config", "get"]];
|
|
5010
|
-
static usage =
|
|
5155
|
+
static usage = Command3.Usage({
|
|
5011
5156
|
category: "Config",
|
|
5012
5157
|
description: "Read a single config value by dot-path key.",
|
|
5013
5158
|
details: `
|
|
@@ -5015,11 +5160,10 @@ var ConfigGetCommand = class extends Command2 {
|
|
|
5015
5160
|
Exempt from "done in <\u2026>".
|
|
5016
5161
|
`
|
|
5017
5162
|
});
|
|
5018
|
-
key =
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
async execute() {
|
|
5163
|
+
key = Option3.String({ required: true });
|
|
5164
|
+
strict = Option3.Boolean("--strict", false);
|
|
5165
|
+
emitElapsed = false;
|
|
5166
|
+
async run() {
|
|
5023
5167
|
const result = tryLoadConfig(
|
|
5024
5168
|
{ scope: this.global ? "global" : "project", strict: this.strict, ...defaultRuntimeContext() },
|
|
5025
5169
|
this.context.stderr
|
|
@@ -5049,9 +5193,9 @@ var ConfigGetCommand = class extends Command2 {
|
|
|
5049
5193
|
return ExitCode.Ok;
|
|
5050
5194
|
}
|
|
5051
5195
|
};
|
|
5052
|
-
var ConfigShowCommand = class extends
|
|
5196
|
+
var ConfigShowCommand = class extends SmCommand {
|
|
5053
5197
|
static paths = [["config", "show"]];
|
|
5054
|
-
static usage =
|
|
5198
|
+
static usage = Command3.Usage({
|
|
5055
5199
|
category: "Config",
|
|
5056
5200
|
description: "Show a config value with the layer that set it (--source).",
|
|
5057
5201
|
details: `
|
|
@@ -5061,17 +5205,16 @@ var ConfigShowCommand = class extends Command2 {
|
|
|
5061
5205
|
Exempt from "done in <\u2026>".
|
|
5062
5206
|
`
|
|
5063
5207
|
});
|
|
5064
|
-
key =
|
|
5065
|
-
source =
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
strict = Option2.Boolean("--strict", false);
|
|
5208
|
+
key = Option3.String({ required: true });
|
|
5209
|
+
source = Option3.Boolean("--source", false);
|
|
5210
|
+
strict = Option3.Boolean("--strict", false);
|
|
5211
|
+
emitElapsed = false;
|
|
5069
5212
|
// CLI orchestrator: each branch (load failure, forbidden segment,
|
|
5070
5213
|
// unknown key, --json + --source 2x2 dispatch) is one validation gate
|
|
5071
5214
|
// or output-format pick. Splitting per branch scatters the gate from
|
|
5072
5215
|
// the value it gates.
|
|
5073
5216
|
// eslint-disable-next-line complexity
|
|
5074
|
-
async
|
|
5217
|
+
async run() {
|
|
5075
5218
|
const result = tryLoadConfig(
|
|
5076
5219
|
{ scope: this.global ? "global" : "project", strict: this.strict, ...defaultRuntimeContext() },
|
|
5077
5220
|
this.context.stderr
|
|
@@ -5134,9 +5277,9 @@ var LAYER_RANK = {
|
|
|
5134
5277
|
"project-local": 4,
|
|
5135
5278
|
override: 5
|
|
5136
5279
|
};
|
|
5137
|
-
var ConfigSetCommand = class extends
|
|
5280
|
+
var ConfigSetCommand = class extends SmCommand {
|
|
5138
5281
|
static paths = [["config", "set"]];
|
|
5139
|
-
static usage =
|
|
5282
|
+
static usage = Command3.Usage({
|
|
5140
5283
|
category: "Config",
|
|
5141
5284
|
description: "Write a config key. Project file by default; -g writes to user.",
|
|
5142
5285
|
details: `
|
|
@@ -5147,11 +5290,9 @@ var ConfigSetCommand = class extends Command2 {
|
|
|
5147
5290
|
Schema violation \u2192 exit 2, no write performed.
|
|
5148
5291
|
`
|
|
5149
5292
|
});
|
|
5150
|
-
key =
|
|
5151
|
-
value =
|
|
5152
|
-
|
|
5153
|
-
async execute() {
|
|
5154
|
-
const elapsed = startElapsed();
|
|
5293
|
+
key = Option3.String({ required: true });
|
|
5294
|
+
value = Option3.String({ required: true });
|
|
5295
|
+
async run() {
|
|
5155
5296
|
const ctx = defaultRuntimeContext();
|
|
5156
5297
|
const target = this.global ? "user" : "project";
|
|
5157
5298
|
const path = targetSettingsPath(target, ctx.cwd, ctx.homedir);
|
|
@@ -5162,7 +5303,6 @@ var ConfigSetCommand = class extends Command2 {
|
|
|
5162
5303
|
} catch (err) {
|
|
5163
5304
|
if (err instanceof ForbiddenSegmentError) {
|
|
5164
5305
|
this.context.stderr.write(tx(CONFIG_TEXTS.forbiddenKeySegment, { segment: err.segment, key: err.key }));
|
|
5165
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5166
5306
|
return ExitCode.Error;
|
|
5167
5307
|
}
|
|
5168
5308
|
throw err;
|
|
@@ -5171,18 +5311,16 @@ var ConfigSetCommand = class extends Command2 {
|
|
|
5171
5311
|
const result = validators.validate("project-config", current);
|
|
5172
5312
|
if (!result.ok) {
|
|
5173
5313
|
this.context.stderr.write(tx(CONFIG_TEXTS.invalidAfterSet, { errors: result.errors }));
|
|
5174
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5175
5314
|
return ExitCode.Error;
|
|
5176
5315
|
}
|
|
5177
5316
|
writeJsonAtomic(path, current);
|
|
5178
5317
|
this.context.stdout.write(tx(CONFIG_TEXTS.setWritten, { key: this.key, value: formatValueHuman(value), path }));
|
|
5179
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5180
5318
|
return ExitCode.Ok;
|
|
5181
5319
|
}
|
|
5182
5320
|
};
|
|
5183
|
-
var ConfigResetCommand = class extends
|
|
5321
|
+
var ConfigResetCommand = class extends SmCommand {
|
|
5184
5322
|
static paths = [["config", "reset"]];
|
|
5185
|
-
static usage =
|
|
5323
|
+
static usage = Command3.Usage({
|
|
5186
5324
|
category: "Config",
|
|
5187
5325
|
description: "Remove a config key from the target file (project default; -g for user).",
|
|
5188
5326
|
details: `
|
|
@@ -5190,16 +5328,13 @@ var ConfigResetCommand = class extends Command2 {
|
|
|
5190
5328
|
Idempotent \u2014 running twice is safe; absent key prints an info note and exits 0.
|
|
5191
5329
|
`
|
|
5192
5330
|
});
|
|
5193
|
-
key =
|
|
5194
|
-
|
|
5195
|
-
async execute() {
|
|
5196
|
-
const elapsed = startElapsed();
|
|
5331
|
+
key = Option3.String({ required: true });
|
|
5332
|
+
async run() {
|
|
5197
5333
|
const ctx = defaultRuntimeContext();
|
|
5198
5334
|
const target = this.global ? "user" : "project";
|
|
5199
5335
|
const path = targetSettingsPath(target, ctx.cwd, ctx.homedir);
|
|
5200
5336
|
if (!existsSync8(path)) {
|
|
5201
5337
|
this.context.stdout.write(tx(CONFIG_TEXTS.unsetNoOverride, { path, key: this.key }));
|
|
5202
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5203
5338
|
return ExitCode.Ok;
|
|
5204
5339
|
}
|
|
5205
5340
|
const current = readJsonObjectOrEmpty(path);
|
|
@@ -5209,19 +5344,16 @@ var ConfigResetCommand = class extends Command2 {
|
|
|
5209
5344
|
} catch (err) {
|
|
5210
5345
|
if (err instanceof ForbiddenSegmentError) {
|
|
5211
5346
|
this.context.stderr.write(tx(CONFIG_TEXTS.forbiddenKeySegment, { segment: err.segment, key: err.key }));
|
|
5212
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5213
5347
|
return ExitCode.Error;
|
|
5214
5348
|
}
|
|
5215
5349
|
throw err;
|
|
5216
5350
|
}
|
|
5217
5351
|
if (!removed) {
|
|
5218
5352
|
this.context.stdout.write(tx(CONFIG_TEXTS.unsetNoOverride, { path, key: this.key }));
|
|
5219
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5220
5353
|
return ExitCode.Ok;
|
|
5221
5354
|
}
|
|
5222
5355
|
writeJsonAtomic(path, current);
|
|
5223
5356
|
this.context.stdout.write(tx(CONFIG_TEXTS.unsetRemoved, { key: this.key, path }));
|
|
5224
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
5225
5357
|
return ExitCode.Ok;
|
|
5226
5358
|
}
|
|
5227
5359
|
};
|
|
@@ -5237,7 +5369,7 @@ var CONFIG_COMMANDS = [
|
|
|
5237
5369
|
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
5238
5370
|
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
5239
5371
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5240
|
-
import { Command as
|
|
5372
|
+
import { Command as Command4, Option as Option4 } from "clipanion";
|
|
5241
5373
|
|
|
5242
5374
|
// conformance/index.ts
|
|
5243
5375
|
import { spawnSync } from "child_process";
|
|
@@ -5700,9 +5832,9 @@ function resolveBinary() {
|
|
|
5700
5832
|
}
|
|
5701
5833
|
return resolve11(here, "..", "..", "bin", "sm.js");
|
|
5702
5834
|
}
|
|
5703
|
-
var ConformanceRunCommand = class extends
|
|
5835
|
+
var ConformanceRunCommand = class extends SmCommand {
|
|
5704
5836
|
static paths = [["conformance", "run"]];
|
|
5705
|
-
static usage =
|
|
5837
|
+
static usage = Command4.Usage({
|
|
5706
5838
|
category: "Introspection",
|
|
5707
5839
|
description: "Run the conformance suite \u2014 spec-owned cases plus every built-in Provider.",
|
|
5708
5840
|
details: `
|
|
@@ -5736,14 +5868,14 @@ var ConformanceRunCommand = class extends Command3 {
|
|
|
5736
5868
|
]
|
|
5737
5869
|
]
|
|
5738
5870
|
});
|
|
5739
|
-
scope =
|
|
5871
|
+
scope = Option4.String("--scope", {
|
|
5740
5872
|
required: false,
|
|
5741
5873
|
description: "Suite selector: 'all' (default), 'spec', or 'provider:<id>'."
|
|
5742
5874
|
});
|
|
5743
5875
|
// CLI orchestrator: scope resolution + per-case run loop +
|
|
5744
5876
|
// per-result render branches + global pass/fail decision.
|
|
5745
5877
|
// eslint-disable-next-line complexity
|
|
5746
|
-
async
|
|
5878
|
+
async run() {
|
|
5747
5879
|
let scopes;
|
|
5748
5880
|
try {
|
|
5749
5881
|
scopes = selectConformanceScopes(this.scope);
|
|
@@ -5864,7 +5996,7 @@ var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
|
|
|
5864
5996
|
|
|
5865
5997
|
// cli/commands/db.ts
|
|
5866
5998
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5867
|
-
import { chmod, copyFile, mkdir, rm
|
|
5999
|
+
import { chmod, copyFile, mkdir, rm } from "fs/promises";
|
|
5868
6000
|
import { dirname as dirname8, join as join9, resolve as resolve12 } from "path";
|
|
5869
6001
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
5870
6002
|
|
|
@@ -5940,13 +6072,10 @@ var DB_TEXTS = {
|
|
|
5940
6072
|
};
|
|
5941
6073
|
|
|
5942
6074
|
// cli/commands/db.ts
|
|
5943
|
-
import { Command as
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
|
|
5948
|
-
}
|
|
5949
|
-
}
|
|
6075
|
+
import { Command as Command5, Option as Option5 } from "clipanion";
|
|
6076
|
+
|
|
6077
|
+
// cli/util/fs.ts
|
|
6078
|
+
import { stat as stat2 } from "fs/promises";
|
|
5950
6079
|
async function pathExists(path) {
|
|
5951
6080
|
try {
|
|
5952
6081
|
await stat2(path);
|
|
@@ -5964,15 +6093,23 @@ async function statOrNull(path) {
|
|
|
5964
6093
|
throw err;
|
|
5965
6094
|
}
|
|
5966
6095
|
}
|
|
6096
|
+
|
|
6097
|
+
// cli/commands/db.ts
|
|
6098
|
+
var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
6099
|
+
function assertSafeIdentifier(name) {
|
|
6100
|
+
if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
|
|
6101
|
+
throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
5967
6104
|
async function chmodOwnerOnlyBestEffort(target) {
|
|
5968
6105
|
try {
|
|
5969
6106
|
await chmod(target, 384);
|
|
5970
6107
|
} catch {
|
|
5971
6108
|
}
|
|
5972
6109
|
}
|
|
5973
|
-
var DbBackupCommand = class extends
|
|
6110
|
+
var DbBackupCommand = class extends SmCommand {
|
|
5974
6111
|
static paths = [["db", "backup"]];
|
|
5975
|
-
static usage =
|
|
6112
|
+
static usage = Command5.Usage({
|
|
5976
6113
|
category: "Database",
|
|
5977
6114
|
description: "WAL checkpoint + copy the DB file to a backup.",
|
|
5978
6115
|
details: `
|
|
@@ -5982,10 +6119,8 @@ var DbBackupCommand = class extends Command4 {
|
|
|
5982
6119
|
running sm scan afterwards refreshes scan_*.
|
|
5983
6120
|
`
|
|
5984
6121
|
});
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
out = Option4.String("--out", { required: false });
|
|
5988
|
-
async execute() {
|
|
6122
|
+
out = Option5.String("--out", { required: false });
|
|
6123
|
+
async run() {
|
|
5989
6124
|
const path = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
5990
6125
|
if (!assertDbExists(path, this.context.stderr)) return ExitCode.NotFound;
|
|
5991
6126
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -5997,9 +6132,9 @@ var DbBackupCommand = class extends Command4 {
|
|
|
5997
6132
|
return ExitCode.Ok;
|
|
5998
6133
|
}
|
|
5999
6134
|
};
|
|
6000
|
-
var DbRestoreCommand = class extends
|
|
6135
|
+
var DbRestoreCommand = class extends SmCommand {
|
|
6001
6136
|
static paths = [["db", "restore"]];
|
|
6002
|
-
static usage =
|
|
6137
|
+
static usage = Command5.Usage({
|
|
6003
6138
|
category: "Database",
|
|
6004
6139
|
description: "Replace the active DB file with a backup.",
|
|
6005
6140
|
details: `
|
|
@@ -6010,14 +6145,12 @@ var DbRestoreCommand = class extends Command4 {
|
|
|
6010
6145
|
Dry-run bypasses the confirmation prompt.
|
|
6011
6146
|
`
|
|
6012
6147
|
});
|
|
6013
|
-
source =
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
yes = Option4.Boolean("--yes,--force", false);
|
|
6017
|
-
dryRun = Option4.Boolean("-n,--dry-run", false, {
|
|
6148
|
+
source = Option5.String({ required: true });
|
|
6149
|
+
yes = Option5.Boolean("--yes,--force", false);
|
|
6150
|
+
dryRun = Option5.Boolean("-n,--dry-run", false, {
|
|
6018
6151
|
description: "Preview the restore without overwriting the live DB."
|
|
6019
6152
|
});
|
|
6020
|
-
async
|
|
6153
|
+
async run() {
|
|
6021
6154
|
const target = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
6022
6155
|
const sourcePath = resolve12(this.source);
|
|
6023
6156
|
const sourceStat = await statOrNull(sourcePath);
|
|
@@ -6059,9 +6192,9 @@ var DbRestoreCommand = class extends Command4 {
|
|
|
6059
6192
|
return ExitCode.Ok;
|
|
6060
6193
|
}
|
|
6061
6194
|
};
|
|
6062
|
-
var DbResetCommand = class extends
|
|
6195
|
+
var DbResetCommand = class extends SmCommand {
|
|
6063
6196
|
static paths = [["db", "reset"]];
|
|
6064
|
-
static usage =
|
|
6197
|
+
static usage = Command5.Usage({
|
|
6065
6198
|
category: "Database",
|
|
6066
6199
|
description: "Drop scan_* (default), optionally state_*, or delete the DB entirely.",
|
|
6067
6200
|
details: `
|
|
@@ -6075,12 +6208,10 @@ var DbResetCommand = class extends Command4 {
|
|
|
6075
6208
|
preview itself is non-destructive).
|
|
6076
6209
|
`
|
|
6077
6210
|
});
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
yes = Option4.Boolean("--yes,--force", false);
|
|
6083
|
-
dryRun = Option4.Boolean("-n,--dry-run", false, {
|
|
6211
|
+
state = Option5.Boolean("--state", false);
|
|
6212
|
+
hard = Option5.Boolean("--hard", false);
|
|
6213
|
+
yes = Option5.Boolean("--yes,--force", false);
|
|
6214
|
+
dryRun = Option5.Boolean("-n,--dry-run", false, {
|
|
6084
6215
|
description: "Preview the reset without dropping any tables or unlinking any files."
|
|
6085
6216
|
});
|
|
6086
6217
|
// CLI orchestrator: --state vs --hard flag combo + --dry-run + --yes
|
|
@@ -6088,7 +6219,7 @@ var DbResetCommand = class extends Command4 {
|
|
|
6088
6219
|
// expression of the flag semantics; splitting per branch would
|
|
6089
6220
|
// distance the validations from their guards.
|
|
6090
6221
|
// eslint-disable-next-line complexity
|
|
6091
|
-
async
|
|
6222
|
+
async run() {
|
|
6092
6223
|
if (this.state && this.hard) {
|
|
6093
6224
|
this.context.stderr.write(DB_TEXTS.resetStateAndHardMutex);
|
|
6094
6225
|
return ExitCode.Error;
|
|
@@ -6176,9 +6307,9 @@ var DbResetCommand = class extends Command4 {
|
|
|
6176
6307
|
return ExitCode.Ok;
|
|
6177
6308
|
}
|
|
6178
6309
|
};
|
|
6179
|
-
var DbShellCommand = class extends
|
|
6310
|
+
var DbShellCommand = class extends SmCommand {
|
|
6180
6311
|
static paths = [["db", "shell"]];
|
|
6181
|
-
static usage =
|
|
6312
|
+
static usage = Command5.Usage({
|
|
6182
6313
|
category: "Database",
|
|
6183
6314
|
description: "Open an interactive sqlite3 shell on the DB file.",
|
|
6184
6315
|
details: `
|
|
@@ -6187,9 +6318,11 @@ var DbShellCommand = class extends Command4 {
|
|
|
6187
6318
|
sm db dump for a read-only inspection.
|
|
6188
6319
|
`
|
|
6189
6320
|
});
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6321
|
+
// Interactive shell: the spawned `sqlite3` owns the terminal. No
|
|
6322
|
+
// `done in <…>` line — the user expects to see the shell's own
|
|
6323
|
+
// prompt + farewell, not a follow-up trailer once they exit.
|
|
6324
|
+
emitElapsed = false;
|
|
6325
|
+
async run() {
|
|
6193
6326
|
const path = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
6194
6327
|
if (!assertDbExists(path, this.context.stderr)) return ExitCode.NotFound;
|
|
6195
6328
|
const result = spawnSync2("sqlite3", [path], { stdio: "inherit" });
|
|
@@ -6200,22 +6333,20 @@ var DbShellCommand = class extends Command4 {
|
|
|
6200
6333
|
return result.status ?? 0;
|
|
6201
6334
|
}
|
|
6202
6335
|
};
|
|
6203
|
-
var DbDumpCommand = class extends
|
|
6336
|
+
var DbDumpCommand = class extends SmCommand {
|
|
6204
6337
|
static paths = [["db", "dump"]];
|
|
6205
|
-
static usage =
|
|
6338
|
+
static usage = Command5.Usage({
|
|
6206
6339
|
category: "Database",
|
|
6207
6340
|
description: "SQL dump to stdout.",
|
|
6208
6341
|
details: "Read-only. Use --tables <names...> to limit the dump to specific tables."
|
|
6209
6342
|
});
|
|
6210
|
-
|
|
6211
|
-
db = Option4.String("--db", { required: false });
|
|
6212
|
-
tables = Option4.Array("--tables", { required: false });
|
|
6343
|
+
tables = Option5.Array("--tables", { required: false });
|
|
6213
6344
|
// CLI orchestrator: each branch (db existence, per-table identifier
|
|
6214
6345
|
// gate, sqlite3-not-found fallback, exit-status passthrough) is a
|
|
6215
6346
|
// single dispatcher decision. Splitting per branch scatters the gate
|
|
6216
6347
|
// away from the value it gates.
|
|
6217
6348
|
// eslint-disable-next-line complexity
|
|
6218
|
-
async
|
|
6349
|
+
async run() {
|
|
6219
6350
|
const path = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
6220
6351
|
if (!assertDbExists(path, this.context.stderr)) return ExitCode.NotFound;
|
|
6221
6352
|
const args2 = ["-readonly", path, ".dump"];
|
|
@@ -6236,9 +6367,9 @@ var DbDumpCommand = class extends Command4 {
|
|
|
6236
6367
|
return result.status ?? 0;
|
|
6237
6368
|
}
|
|
6238
6369
|
};
|
|
6239
|
-
var DbMigrateCommand = class extends
|
|
6370
|
+
var DbMigrateCommand = class extends SmCommand {
|
|
6240
6371
|
static paths = [["db", "migrate"]];
|
|
6241
|
-
static usage =
|
|
6372
|
+
static usage = Command5.Usage({
|
|
6242
6373
|
category: "Database",
|
|
6243
6374
|
description: "Apply pending kernel + plugin migrations (default) or inspect plan.",
|
|
6244
6375
|
details: `
|
|
@@ -6259,21 +6390,19 @@ var DbMigrateCommand = class extends Command4 {
|
|
|
6259
6390
|
object outside the prefix.
|
|
6260
6391
|
`
|
|
6261
6392
|
});
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
kernelOnly = Option4.Boolean("--kernel-only", false);
|
|
6269
|
-
pluginId = Option4.String("--plugin", { required: false });
|
|
6393
|
+
dryRun = Option5.Boolean("-n,--dry-run", false);
|
|
6394
|
+
status = Option5.Boolean("--status", false);
|
|
6395
|
+
to = Option5.String("--to", { required: false });
|
|
6396
|
+
noBackup = Option5.Boolean("--no-backup", false);
|
|
6397
|
+
kernelOnly = Option5.Boolean("--kernel-only", false);
|
|
6398
|
+
pluginId = Option5.String("--plugin", { required: false });
|
|
6270
6399
|
// Multi-flag CLI orchestrator: validates flag combos, optionally
|
|
6271
6400
|
// discovers plugins, fans out into status / apply branches against
|
|
6272
6401
|
// both the kernel ledger and per-plugin ledgers. Splitting per branch
|
|
6273
6402
|
// would scatter the close-to-call-site flag handling without making
|
|
6274
6403
|
// the verb easier to follow.
|
|
6275
6404
|
// eslint-disable-next-line complexity
|
|
6276
|
-
async
|
|
6405
|
+
async run() {
|
|
6277
6406
|
if (this.kernelOnly && this.pluginId !== void 0) {
|
|
6278
6407
|
this.context.stderr.write(DB_TEXTS.migrateKernelOnlyAndPluginMutex);
|
|
6279
6408
|
return ExitCode.Error;
|
|
@@ -6455,7 +6584,7 @@ var DB_COMMANDS = [
|
|
|
6455
6584
|
];
|
|
6456
6585
|
|
|
6457
6586
|
// cli/commands/export.ts
|
|
6458
|
-
import { Command as
|
|
6587
|
+
import { Command as Command6, Option as Option6 } from "clipanion";
|
|
6459
6588
|
|
|
6460
6589
|
// kernel/scan/query.ts
|
|
6461
6590
|
var HAS_VALUES = /* @__PURE__ */ new Set(["issues"]);
|
|
@@ -6612,9 +6741,9 @@ var SUPPORTED_FORMATS = ["json", "md"];
|
|
|
6612
6741
|
var DEFERRED_FORMATS = {
|
|
6613
6742
|
mermaid: EXPORT_TEXTS.formatDeferredReasonMermaid
|
|
6614
6743
|
};
|
|
6615
|
-
var ExportCommand = class extends
|
|
6744
|
+
var ExportCommand = class extends SmCommand {
|
|
6616
6745
|
static paths = [["export"]];
|
|
6617
|
-
static usage =
|
|
6746
|
+
static usage = Command6.Usage({
|
|
6618
6747
|
category: "Browse",
|
|
6619
6748
|
description: "Filtered export. Query syntax is implementation-defined pre-1.0.",
|
|
6620
6749
|
details: `
|
|
@@ -6638,11 +6767,9 @@ var ExportCommand = class extends Command5 {
|
|
|
6638
6767
|
["Whole graph as Markdown", '$0 export "" --format md']
|
|
6639
6768
|
]
|
|
6640
6769
|
});
|
|
6641
|
-
query =
|
|
6642
|
-
format =
|
|
6643
|
-
|
|
6644
|
-
db = Option5.String("--db", { required: false });
|
|
6645
|
-
async execute() {
|
|
6770
|
+
query = Option6.String({ required: true });
|
|
6771
|
+
format = Option6.String("--format", { required: false });
|
|
6772
|
+
async run() {
|
|
6646
6773
|
const format = (this.format ?? "json").toLowerCase();
|
|
6647
6774
|
if (DEFERRED_FORMATS[format]) {
|
|
6648
6775
|
this.context.stderr.write(
|
|
@@ -6826,7 +6953,7 @@ function pickTitle2(node) {
|
|
|
6826
6953
|
}
|
|
6827
6954
|
|
|
6828
6955
|
// cli/commands/graph.ts
|
|
6829
|
-
import { Command as
|
|
6956
|
+
import { Command as Command7, Option as Option7 } from "clipanion";
|
|
6830
6957
|
|
|
6831
6958
|
// cli/i18n/graph.texts.ts
|
|
6832
6959
|
var GRAPH_TEXTS = {
|
|
@@ -6836,9 +6963,9 @@ var GRAPH_TEXTS = {
|
|
|
6836
6963
|
|
|
6837
6964
|
// cli/commands/graph.ts
|
|
6838
6965
|
var DEFAULT_FORMAT = "ascii";
|
|
6839
|
-
var GraphCommand = class extends
|
|
6966
|
+
var GraphCommand = class extends SmCommand {
|
|
6840
6967
|
static paths = [["graph"]];
|
|
6841
|
-
static usage =
|
|
6968
|
+
static usage = Command7.Usage({
|
|
6842
6969
|
category: "Browse",
|
|
6843
6970
|
description: "Render the full graph via the named formatter.",
|
|
6844
6971
|
details: `
|
|
@@ -6855,15 +6982,13 @@ var GraphCommand = class extends Command6 {
|
|
|
6855
6982
|
["Use a non-default DB file", "$0 graph --db /path/to/skill-map.db"]
|
|
6856
6983
|
]
|
|
6857
6984
|
});
|
|
6858
|
-
format =
|
|
6985
|
+
format = Option7.String("--format", DEFAULT_FORMAT, {
|
|
6859
6986
|
description: `Formatter format. Must match the \`formatId\` field of a registered formatter. Default: ${DEFAULT_FORMAT}.`
|
|
6860
6987
|
});
|
|
6861
|
-
|
|
6862
|
-
db = Option6.String("--db", { required: false });
|
|
6863
|
-
noPlugins = Option6.Boolean("--no-plugins", false, {
|
|
6988
|
+
noPlugins = Option7.Boolean("--no-plugins", false, {
|
|
6864
6989
|
description: "Skip drop-in plugin discovery. Only built-in formatters participate."
|
|
6865
6990
|
});
|
|
6866
|
-
async
|
|
6991
|
+
async run() {
|
|
6867
6992
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
6868
6993
|
if (!assertDbExists(dbPath, this.context.stderr)) return ExitCode.NotFound;
|
|
6869
6994
|
const pluginRuntime = this.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: this.global ? "global" : "project" });
|
|
@@ -6898,12 +7023,12 @@ var GraphCommand = class extends Command6 {
|
|
|
6898
7023
|
import { readFileSync as readFileSync10 } from "fs";
|
|
6899
7024
|
import { createRequire as createRequire4 } from "module";
|
|
6900
7025
|
import { resolve as resolve13 } from "path";
|
|
6901
|
-
import { Command as
|
|
7026
|
+
import { Command as Command8, Option as Option8 } from "clipanion";
|
|
6902
7027
|
|
|
6903
7028
|
// package.json
|
|
6904
7029
|
var package_default = {
|
|
6905
7030
|
name: "@skill-map/cli",
|
|
6906
|
-
version: "0.
|
|
7031
|
+
version: "0.11.0",
|
|
6907
7032
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
6908
7033
|
license: "MIT",
|
|
6909
7034
|
type: "module",
|
|
@@ -6950,6 +7075,7 @@ var package_default = {
|
|
|
6950
7075
|
scripts: {
|
|
6951
7076
|
build: "tsup",
|
|
6952
7077
|
dev: "tsup --watch",
|
|
7078
|
+
"dev:serve": "node ../scripts/dev-serve.js",
|
|
6953
7079
|
typecheck: "tsc --noEmit",
|
|
6954
7080
|
lint: "eslint .",
|
|
6955
7081
|
"lint:fix": "eslint . --fix",
|
|
@@ -6960,17 +7086,20 @@ var package_default = {
|
|
|
6960
7086
|
clean: "rm -rf dist coverage"
|
|
6961
7087
|
},
|
|
6962
7088
|
dependencies: {
|
|
7089
|
+
"@hono/node-server": "2.0.1",
|
|
6963
7090
|
"@skill-map/spec": "*",
|
|
6964
7091
|
ajv: "8.18.0",
|
|
6965
7092
|
"ajv-formats": "3.0.1",
|
|
6966
7093
|
chokidar: "5.0.0",
|
|
6967
7094
|
clipanion: "4.0.0-rc.4",
|
|
7095
|
+
hono: "4.12.16",
|
|
6968
7096
|
ignore: "7.0.5",
|
|
6969
7097
|
"js-tiktoken": "1.0.21",
|
|
6970
7098
|
"js-yaml": "4.1.1",
|
|
6971
7099
|
kysely: "0.28.16",
|
|
6972
7100
|
semver: "7.7.4",
|
|
6973
|
-
typanion: "3.14.0"
|
|
7101
|
+
typanion: "3.14.0",
|
|
7102
|
+
ws: "8.20.0"
|
|
6974
7103
|
},
|
|
6975
7104
|
devDependencies: {
|
|
6976
7105
|
"@eslint/js": "10.0.1",
|
|
@@ -6978,6 +7107,7 @@ var package_default = {
|
|
|
6978
7107
|
"@types/js-yaml": "4.0.9",
|
|
6979
7108
|
"@types/node": "24.12.2",
|
|
6980
7109
|
"@types/semver": "7.7.1",
|
|
7110
|
+
"@types/ws": "8.18.1",
|
|
6981
7111
|
c8: "11.0.0",
|
|
6982
7112
|
eslint: "10.2.1",
|
|
6983
7113
|
"eslint-plugin-import-x": "4.16.2",
|
|
@@ -7074,9 +7204,9 @@ var HELP_TEXTS = {
|
|
|
7074
7204
|
};
|
|
7075
7205
|
|
|
7076
7206
|
// cli/commands/help.ts
|
|
7077
|
-
var HelpCommand = class extends
|
|
7207
|
+
var HelpCommand = class extends Command8 {
|
|
7078
7208
|
static paths = [["help"]];
|
|
7079
|
-
static usage =
|
|
7209
|
+
static usage = Command8.Usage({
|
|
7080
7210
|
category: "Introspection",
|
|
7081
7211
|
description: "Self-describing introspection. --format human|md|json.",
|
|
7082
7212
|
details: `
|
|
@@ -7090,8 +7220,8 @@ var HelpCommand = class extends Command7 {
|
|
|
7090
7220
|
json \u2014 structured surface dump per spec/cli-contract.md.
|
|
7091
7221
|
`
|
|
7092
7222
|
});
|
|
7093
|
-
verbParts =
|
|
7094
|
-
format =
|
|
7223
|
+
verbParts = Option8.Rest({ required: 0 });
|
|
7224
|
+
format = Option8.String("--format", "human");
|
|
7095
7225
|
async execute() {
|
|
7096
7226
|
const format = normalizeFormat(this.format);
|
|
7097
7227
|
if (!format) {
|
|
@@ -7409,8 +7539,8 @@ function renderCompactOverview(verbs) {
|
|
|
7409
7539
|
lines.push(HELP_TEXTS.compactFooter);
|
|
7410
7540
|
return lines.join("\n") + "\n";
|
|
7411
7541
|
}
|
|
7412
|
-
var RootHelpCommand = class extends
|
|
7413
|
-
static paths = [["-h"], ["--help"],
|
|
7542
|
+
var RootHelpCommand = class extends Command8 {
|
|
7543
|
+
static paths = [["-h"], ["--help"], Command8.Default];
|
|
7414
7544
|
async execute() {
|
|
7415
7545
|
const rawDefs = this.cli.definitions();
|
|
7416
7546
|
const verbs = rawDefs.filter((d) => !isBuiltin(d)).map(normalizeDefinition).sort(byPath);
|
|
@@ -7465,9 +7595,9 @@ function registeredVerbPaths(cli2) {
|
|
|
7465
7595
|
}
|
|
7466
7596
|
|
|
7467
7597
|
// cli/commands/init.ts
|
|
7468
|
-
import { mkdir as mkdir2, readFile as readFile2,
|
|
7598
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
7469
7599
|
import { join as join10 } from "path";
|
|
7470
|
-
import { Command as
|
|
7600
|
+
import { Command as Command9, Option as Option9 } from "clipanion";
|
|
7471
7601
|
|
|
7472
7602
|
// kernel/orchestrator.ts
|
|
7473
7603
|
import { createHash } from "crypto";
|
|
@@ -8629,22 +8759,9 @@ function createCliProgressEmitter(stderr) {
|
|
|
8629
8759
|
}
|
|
8630
8760
|
|
|
8631
8761
|
// cli/commands/init.ts
|
|
8632
|
-
var
|
|
8633
|
-
".skill-map/settings.local.json",
|
|
8634
|
-
".skill-map/skill-map.db"
|
|
8635
|
-
];
|
|
8636
|
-
async function pathExists2(path) {
|
|
8637
|
-
try {
|
|
8638
|
-
await stat3(path);
|
|
8639
|
-
return true;
|
|
8640
|
-
} catch (err) {
|
|
8641
|
-
if (err.code === "ENOENT") return false;
|
|
8642
|
-
throw err;
|
|
8643
|
-
}
|
|
8644
|
-
}
|
|
8645
|
-
var InitCommand = class extends Command8 {
|
|
8762
|
+
var InitCommand = class extends SmCommand {
|
|
8646
8763
|
static paths = [["init"]];
|
|
8647
|
-
static usage =
|
|
8764
|
+
static usage = Command9.Usage({
|
|
8648
8765
|
category: "Setup",
|
|
8649
8766
|
description: "Bootstrap the current scope: scaffold .skill-map/, provision DB, run first scan.",
|
|
8650
8767
|
details: `
|
|
@@ -8668,19 +8785,16 @@ var InitCommand = class extends Command8 {
|
|
|
8668
8785
|
["Preview what would be created", "$0 init --dry-run"]
|
|
8669
8786
|
]
|
|
8670
8787
|
});
|
|
8671
|
-
|
|
8672
|
-
description: "Initialise ~/.skill-map/ instead of ./.skill-map/."
|
|
8673
|
-
});
|
|
8674
|
-
noScan = Option8.Boolean("--no-scan", false, {
|
|
8788
|
+
noScan = Option9.Boolean("--no-scan", false, {
|
|
8675
8789
|
description: "Skip the first scan after scaffolding."
|
|
8676
8790
|
});
|
|
8677
|
-
force =
|
|
8791
|
+
force = Option9.Boolean("--force", false, {
|
|
8678
8792
|
description: "Overwrite an existing settings.json / settings.local.json / .skill-mapignore."
|
|
8679
8793
|
});
|
|
8680
|
-
strict =
|
|
8794
|
+
strict = Option9.Boolean("--strict", false, {
|
|
8681
8795
|
description: "Strict mode: fail on any layered-loader warning AND promote frontmatter warnings to errors during the first scan. Same flag as sm scan / sm config."
|
|
8682
8796
|
});
|
|
8683
|
-
dryRun =
|
|
8797
|
+
dryRun = Option9.Boolean("-n,--dry-run", false, {
|
|
8684
8798
|
description: "Preview the scope provisioning without touching the filesystem or the DB. Honours --force for the would-overwrite preview. Skips the first scan unconditionally \u2014 dry-run never persists."
|
|
8685
8799
|
});
|
|
8686
8800
|
// CLI orchestrator: paths setup + dry-run branch (delegated to
|
|
@@ -8688,18 +8802,16 @@ var InitCommand = class extends Command8 {
|
|
|
8688
8802
|
// gitignore management + DB provision + first scan delegation).
|
|
8689
8803
|
// The first-scan branch already lives in `runFirstScan`.
|
|
8690
8804
|
// eslint-disable-next-line complexity
|
|
8691
|
-
async
|
|
8692
|
-
const elapsed = startElapsed();
|
|
8805
|
+
async run() {
|
|
8693
8806
|
const ctx = defaultRuntimeContext();
|
|
8694
8807
|
const scopeRoot = this.global ? ctx.homedir : ctx.cwd;
|
|
8695
8808
|
const skillMapDir = join10(scopeRoot, SKILL_MAP_DIR);
|
|
8696
|
-
const settingsPath =
|
|
8697
|
-
const localPath =
|
|
8698
|
-
const ignorePath =
|
|
8699
|
-
const dbPath =
|
|
8700
|
-
if (await
|
|
8809
|
+
const settingsPath = defaultSettingsPath(scopeRoot);
|
|
8810
|
+
const localPath = defaultLocalSettingsPath(scopeRoot);
|
|
8811
|
+
const ignorePath = defaultIgnoreFilePath(scopeRoot);
|
|
8812
|
+
const dbPath = defaultDbPath(scopeRoot);
|
|
8813
|
+
if (await pathExists(settingsPath) && !this.force) {
|
|
8701
8814
|
this.context.stderr.write(tx(INIT_TEXTS.alreadyInitialised, { settingsPath }));
|
|
8702
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
8703
8815
|
return ExitCode.Error;
|
|
8704
8816
|
}
|
|
8705
8817
|
if (this.dryRun) {
|
|
@@ -8714,15 +8826,14 @@ var InitCommand = class extends Command8 {
|
|
|
8714
8826
|
global: this.global,
|
|
8715
8827
|
noScan: this.noScan
|
|
8716
8828
|
});
|
|
8717
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
8718
8829
|
return ExitCode.Ok;
|
|
8719
8830
|
}
|
|
8720
8831
|
await mkdir2(skillMapDir, { recursive: true });
|
|
8721
8832
|
await writeFile(settingsPath, JSON.stringify({ schemaVersion: 1 }, null, 2) + "\n");
|
|
8722
|
-
if (!await
|
|
8833
|
+
if (!await pathExists(localPath) || this.force) {
|
|
8723
8834
|
await writeFile(localPath, "{}\n");
|
|
8724
8835
|
}
|
|
8725
|
-
if (!await
|
|
8836
|
+
if (!await pathExists(ignorePath) || this.force) {
|
|
8726
8837
|
await writeFile(ignorePath, loadBundledIgnoreText());
|
|
8727
8838
|
}
|
|
8728
8839
|
if (!this.global) {
|
|
@@ -8740,25 +8851,20 @@ var InitCommand = class extends Command8 {
|
|
|
8740
8851
|
await withSqlite({ databasePath: dbPath, autoBackup: false }, async () => {
|
|
8741
8852
|
});
|
|
8742
8853
|
this.context.stdout.write(tx(INIT_TEXTS.initialised, { skillMapDir }));
|
|
8743
|
-
if (this.noScan)
|
|
8744
|
-
|
|
8745
|
-
return ExitCode.Ok;
|
|
8746
|
-
}
|
|
8747
|
-
const scanCode = await runFirstScan(scopeRoot, ctx.homedir, dbPath, this.strict, this.context.stdout, this.context.stderr);
|
|
8748
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
8749
|
-
return scanCode;
|
|
8854
|
+
if (this.noScan) return ExitCode.Ok;
|
|
8855
|
+
return runFirstScan(scopeRoot, ctx.homedir, dbPath, this.strict, this.context.stdout, this.context.stderr);
|
|
8750
8856
|
}
|
|
8751
8857
|
};
|
|
8752
8858
|
async function writeDryRunPlan(stdout, opts) {
|
|
8753
8859
|
stdout.write(INIT_TEXTS.dryRunHeader);
|
|
8754
|
-
if (!await
|
|
8860
|
+
if (!await pathExists(opts.skillMapDir)) {
|
|
8755
8861
|
stdout.write(tx(INIT_TEXTS.dryRunWouldCreateDir, { path: opts.skillMapDir }));
|
|
8756
8862
|
}
|
|
8757
8863
|
stdout.write(await dryRunFileMessage(opts.settingsPath));
|
|
8758
|
-
if (!await
|
|
8864
|
+
if (!await pathExists(opts.localPath) || opts.force) {
|
|
8759
8865
|
stdout.write(await dryRunFileMessage(opts.localPath));
|
|
8760
8866
|
}
|
|
8761
|
-
if (!await
|
|
8867
|
+
if (!await pathExists(opts.ignorePath) || opts.force) {
|
|
8762
8868
|
stdout.write(await dryRunFileMessage(opts.ignorePath));
|
|
8763
8869
|
}
|
|
8764
8870
|
if (!opts.global) await writeDryRunGitignorePlan(stdout, opts.scopeRoot);
|
|
@@ -8768,7 +8874,7 @@ async function writeDryRunPlan(stdout, opts) {
|
|
|
8768
8874
|
);
|
|
8769
8875
|
}
|
|
8770
8876
|
async function dryRunFileMessage(path) {
|
|
8771
|
-
return await
|
|
8877
|
+
return await pathExists(path) ? tx(INIT_TEXTS.dryRunWouldOverwriteFile, { path }) : tx(INIT_TEXTS.dryRunWouldWriteFile, { path });
|
|
8772
8878
|
}
|
|
8773
8879
|
async function writeDryRunGitignorePlan(stdout, scopeRoot) {
|
|
8774
8880
|
const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
|
|
@@ -8848,7 +8954,7 @@ async function runFirstScan(scopeRoot, homedir2, dbPath, strict, stdout, stderr)
|
|
|
8848
8954
|
}
|
|
8849
8955
|
async function previewGitignoreEntries(scopeRoot, entries) {
|
|
8850
8956
|
const path = join10(scopeRoot, ".gitignore");
|
|
8851
|
-
const body = await
|
|
8957
|
+
const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
|
|
8852
8958
|
const present = new Set(
|
|
8853
8959
|
body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
8854
8960
|
);
|
|
@@ -8857,7 +8963,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
|
|
|
8857
8963
|
async function ensureGitignoreEntries(scopeRoot, entries) {
|
|
8858
8964
|
const path = join10(scopeRoot, ".gitignore");
|
|
8859
8965
|
let body = "";
|
|
8860
|
-
if (await
|
|
8966
|
+
if (await pathExists(path)) {
|
|
8861
8967
|
body = await readFile2(path, "utf8");
|
|
8862
8968
|
}
|
|
8863
8969
|
const present = new Set(
|
|
@@ -8877,7 +8983,7 @@ async function ensureGitignoreEntries(scopeRoot, entries) {
|
|
|
8877
8983
|
}
|
|
8878
8984
|
|
|
8879
8985
|
// cli/commands/history.ts
|
|
8880
|
-
import { Command as
|
|
8986
|
+
import { Command as Command10, Option as Option10 } from "clipanion";
|
|
8881
8987
|
|
|
8882
8988
|
// cli/i18n/option-validators.texts.ts
|
|
8883
8989
|
var OPTION_VALIDATORS_TEXTS = {
|
|
@@ -8964,9 +9070,9 @@ function parseStatuses(input, stderr) {
|
|
|
8964
9070
|
}
|
|
8965
9071
|
return parts;
|
|
8966
9072
|
}
|
|
8967
|
-
var HistoryCommand = class extends
|
|
9073
|
+
var HistoryCommand = class extends SmCommand {
|
|
8968
9074
|
static paths = [["history"]];
|
|
8969
|
-
static usage =
|
|
9075
|
+
static usage = Command10.Usage({
|
|
8970
9076
|
category: "History",
|
|
8971
9077
|
description: "Filter execution records. --json emits an array conforming to execution-record.schema.json.",
|
|
8972
9078
|
details: `
|
|
@@ -8986,24 +9092,19 @@ var HistoryCommand = class extends Command9 {
|
|
|
8986
9092
|
["Machine-readable, scoped to one node", "$0 history -n skills/foo.md --json"]
|
|
8987
9093
|
]
|
|
8988
9094
|
});
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
until = Option9.String("--until", { required: false });
|
|
8996
|
-
limit = Option9.String("--limit", { required: false });
|
|
8997
|
-
json = Option9.Boolean("--json", false);
|
|
8998
|
-
quiet = Option9.Boolean("--quiet", false);
|
|
9095
|
+
node = Option10.String("-n", { required: false });
|
|
9096
|
+
action = Option10.String("--action", { required: false });
|
|
9097
|
+
status = Option10.String("--status", { required: false });
|
|
9098
|
+
since = Option10.String("--since", { required: false });
|
|
9099
|
+
until = Option10.String("--until", { required: false });
|
|
9100
|
+
limit = Option10.String("--limit", { required: false });
|
|
8999
9101
|
// CLI list verb: many optional filter flags (`--node`, `--action`,
|
|
9000
9102
|
// `--status`, `--since`, `--until`, `--limit`, `--json`, `--quiet`)
|
|
9001
9103
|
// each adding a guarded mutation to the filter or render path. Each
|
|
9002
9104
|
// branch is single-purpose; splitting per flag would distance the
|
|
9003
9105
|
// validations from the filter they shape.
|
|
9004
9106
|
// eslint-disable-next-line complexity
|
|
9005
|
-
async
|
|
9006
|
-
const elapsed = startElapsed();
|
|
9107
|
+
async run() {
|
|
9007
9108
|
const filter = {};
|
|
9008
9109
|
if (this.node !== void 0) filter.nodePath = this.node;
|
|
9009
9110
|
if (this.action !== void 0) filter.actionId = this.action;
|
|
@@ -9038,14 +9139,13 @@ var HistoryCommand = class extends Command9 {
|
|
|
9038
9139
|
} else {
|
|
9039
9140
|
this.context.stdout.write(renderTable(rows));
|
|
9040
9141
|
}
|
|
9041
|
-
emitDoneStderr(this.context.stderr, elapsed, this.quiet);
|
|
9042
9142
|
return ExitCode.Ok;
|
|
9043
9143
|
});
|
|
9044
9144
|
}
|
|
9045
9145
|
};
|
|
9046
|
-
var HistoryStatsCommand = class extends
|
|
9146
|
+
var HistoryStatsCommand = class extends SmCommand {
|
|
9047
9147
|
static paths = [["history", "stats"]];
|
|
9048
|
-
static usage =
|
|
9148
|
+
static usage = Command10.Usage({
|
|
9049
9149
|
category: "History",
|
|
9050
9150
|
description: "Aggregate counts, tokens, periods, top nodes, and error rates over state_executions. --json conforms to history-stats.schema.json.",
|
|
9051
9151
|
details: `
|
|
@@ -9063,20 +9163,16 @@ var HistoryStatsCommand = class extends Command9 {
|
|
|
9063
9163
|
["Top 5 nodes, JSON", "$0 history stats --top 5 --json"]
|
|
9064
9164
|
]
|
|
9065
9165
|
});
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
period = Option9.String("--period", { required: false });
|
|
9071
|
-
top = Option9.String("--top", { required: false });
|
|
9072
|
-
json = Option9.Boolean("--json", false);
|
|
9073
|
-
quiet = Option9.Boolean("--quiet", false);
|
|
9166
|
+
since = Option10.String("--since", { required: false });
|
|
9167
|
+
until = Option10.String("--until", { required: false });
|
|
9168
|
+
period = Option10.String("--period", { required: false });
|
|
9169
|
+
top = Option10.String("--top", { required: false });
|
|
9074
9170
|
// CLI stats verb: range parsing + window flags + period flag + JSON
|
|
9075
9171
|
// branch + per-period iteration. Each branch is a single-purpose
|
|
9076
9172
|
// gate; the data work lives in `aggregateHistoryStats`.
|
|
9077
9173
|
// eslint-disable-next-line complexity
|
|
9078
|
-
async
|
|
9079
|
-
const elapsed =
|
|
9174
|
+
async run() {
|
|
9175
|
+
const elapsed = this.elapsed;
|
|
9080
9176
|
let sinceMs = null;
|
|
9081
9177
|
let untilMs = Date.now();
|
|
9082
9178
|
if (this.since !== void 0) {
|
|
@@ -9140,7 +9236,6 @@ var HistoryStatsCommand = class extends Command9 {
|
|
|
9140
9236
|
} else {
|
|
9141
9237
|
this.context.stdout.write(renderStats(stats));
|
|
9142
9238
|
}
|
|
9143
|
-
emitDoneStderr(this.context.stderr, elapsed, this.quiet);
|
|
9144
9239
|
return ExitCode.Ok;
|
|
9145
9240
|
});
|
|
9146
9241
|
}
|
|
@@ -9161,8 +9256,8 @@ function renderTable(rows) {
|
|
|
9161
9256
|
HISTORY_TEXTS.tableHeaderTokens,
|
|
9162
9257
|
HISTORY_TEXTS.tableHeaderNodes
|
|
9163
9258
|
);
|
|
9164
|
-
const
|
|
9165
|
-
const lines = [header,
|
|
9259
|
+
const sep5 = "-".repeat(header.length);
|
|
9260
|
+
const lines = [header, sep5];
|
|
9166
9261
|
for (const r of rows) {
|
|
9167
9262
|
const tokens = `${r.tokensIn ?? 0}/${r.tokensOut ?? 0}`;
|
|
9168
9263
|
const duration = r.durationMs === null || r.durationMs === void 0 ? "-" : formatElapsed(r.durationMs);
|
|
@@ -9252,7 +9347,7 @@ function formatRow(...cols) {
|
|
|
9252
9347
|
|
|
9253
9348
|
// cli/commands/jobs.ts
|
|
9254
9349
|
import { unlink } from "fs/promises";
|
|
9255
|
-
import { Command as
|
|
9350
|
+
import { Command as Command11, Option as Option11 } from "clipanion";
|
|
9256
9351
|
|
|
9257
9352
|
// kernel/jobs/orphan-files.ts
|
|
9258
9353
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
@@ -9260,8 +9355,8 @@ import { join as join11, resolve as resolve15 } from "path";
|
|
|
9260
9355
|
function findOrphanJobFiles(jobsDir, referencedPaths) {
|
|
9261
9356
|
let entries;
|
|
9262
9357
|
try {
|
|
9263
|
-
const
|
|
9264
|
-
if (!
|
|
9358
|
+
const stat3 = statSync4(jobsDir);
|
|
9359
|
+
if (!stat3.isDirectory()) {
|
|
9265
9360
|
return { orphanFilePaths: [], referencedCount: referencedPaths.size };
|
|
9266
9361
|
}
|
|
9267
9362
|
entries = readdirSync6(jobsDir);
|
|
@@ -9298,9 +9393,9 @@ var JOBS_TEXTS = {
|
|
|
9298
9393
|
};
|
|
9299
9394
|
|
|
9300
9395
|
// cli/commands/jobs.ts
|
|
9301
|
-
var JobPruneCommand = class extends
|
|
9396
|
+
var JobPruneCommand = class extends SmCommand {
|
|
9302
9397
|
static paths = [["job", "prune"]];
|
|
9303
|
-
static usage =
|
|
9398
|
+
static usage = Command11.Usage({
|
|
9304
9399
|
category: "Jobs",
|
|
9305
9400
|
description: "Retention GC for completed / failed jobs (per config policy). --orphan-files removes MD files with no DB row.",
|
|
9306
9401
|
details: `
|
|
@@ -9327,16 +9422,13 @@ var JobPruneCommand = class extends Command10 {
|
|
|
9327
9422
|
["Preview without touching the DB", "$0 job prune --dry-run --json"]
|
|
9328
9423
|
]
|
|
9329
9424
|
});
|
|
9330
|
-
orphanFiles =
|
|
9425
|
+
orphanFiles = Option11.Boolean("--orphan-files", false, {
|
|
9331
9426
|
description: "Also remove MD files in .skill-map/jobs/ that have no matching state_jobs row."
|
|
9332
9427
|
});
|
|
9333
|
-
dryRun =
|
|
9428
|
+
dryRun = Option11.Boolean("-n,--dry-run", false, {
|
|
9334
9429
|
description: "Report what would be pruned without touching the DB or filesystem."
|
|
9335
9430
|
});
|
|
9336
|
-
|
|
9337
|
-
description: "Emit a structured prune-result document on stdout."
|
|
9338
|
-
});
|
|
9339
|
-
async execute() {
|
|
9431
|
+
async run() {
|
|
9340
9432
|
const ctx = defaultRuntimeContext();
|
|
9341
9433
|
const dbPath = defaultProjectDbPath(ctx);
|
|
9342
9434
|
const jobsDir = defaultProjectJobsDir(ctx);
|
|
@@ -9450,7 +9542,7 @@ function formatPolicy(seconds) {
|
|
|
9450
9542
|
}
|
|
9451
9543
|
|
|
9452
9544
|
// cli/commands/list.ts
|
|
9453
|
-
import { Command as
|
|
9545
|
+
import { Command as Command12, Option as Option12 } from "clipanion";
|
|
9454
9546
|
|
|
9455
9547
|
// cli/i18n/list.texts.ts
|
|
9456
9548
|
var LIST_TEXTS = {
|
|
@@ -9476,9 +9568,9 @@ var SORT_BY = {
|
|
|
9476
9568
|
external_refs_count: { column: "externalRefsCount", direction: "desc" }
|
|
9477
9569
|
};
|
|
9478
9570
|
var PATH_COL_WIDTH = 50;
|
|
9479
|
-
var ListCommand = class extends
|
|
9571
|
+
var ListCommand = class extends SmCommand {
|
|
9480
9572
|
static paths = [["list"]];
|
|
9481
|
-
static usage =
|
|
9573
|
+
static usage = Command12.Usage({
|
|
9482
9574
|
category: "Browse",
|
|
9483
9575
|
description: "Tabular listing of nodes. --json emits an array conforming to node.schema.json.",
|
|
9484
9576
|
details: `
|
|
@@ -9499,14 +9591,11 @@ var ListCommand = class extends Command11 {
|
|
|
9499
9591
|
["Only nodes with issues, machine-readable", "$0 list --issue --json"]
|
|
9500
9592
|
]
|
|
9501
9593
|
});
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
limit = Option11.String("--limit", { required: false });
|
|
9508
|
-
json = Option11.Boolean("--json", false);
|
|
9509
|
-
async execute() {
|
|
9594
|
+
kind = Option12.String("--kind", { required: false });
|
|
9595
|
+
issue = Option12.Boolean("--issue", false);
|
|
9596
|
+
sortBy = Option12.String("--sort-by", { required: false });
|
|
9597
|
+
limit = Option12.String("--limit", { required: false });
|
|
9598
|
+
async run() {
|
|
9510
9599
|
let sortColumn = "path";
|
|
9511
9600
|
let sortDirection = "asc";
|
|
9512
9601
|
if (this.sortBy !== void 0) {
|
|
@@ -9577,8 +9666,8 @@ function renderTable2(nodes, issuesByNode) {
|
|
|
9577
9666
|
LIST_TEXTS.tableHeaderIssues,
|
|
9578
9667
|
LIST_TEXTS.tableHeaderBytes
|
|
9579
9668
|
);
|
|
9580
|
-
const
|
|
9581
|
-
const lines = [header,
|
|
9669
|
+
const sep5 = "-".repeat(header.length);
|
|
9670
|
+
const lines = [header, sep5];
|
|
9582
9671
|
for (const node of nodes) {
|
|
9583
9672
|
lines.push(
|
|
9584
9673
|
formatRow2(
|
|
@@ -9607,7 +9696,7 @@ function formatRow2(path, kind, out, inCount, ext, issues, bytes) {
|
|
|
9607
9696
|
}
|
|
9608
9697
|
|
|
9609
9698
|
// cli/commands/orphans.ts
|
|
9610
|
-
import { Command as
|
|
9699
|
+
import { Command as Command13, Option as Option13 } from "clipanion";
|
|
9611
9700
|
|
|
9612
9701
|
// cli/i18n/orphans.texts.ts
|
|
9613
9702
|
var ORPHANS_TEXTS = {
|
|
@@ -9660,9 +9749,9 @@ async function findActiveOrphanIssues(adapter, predicate) {
|
|
|
9660
9749
|
function isStringArray(v) {
|
|
9661
9750
|
return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
9662
9751
|
}
|
|
9663
|
-
var OrphansCommand = class extends
|
|
9752
|
+
var OrphansCommand = class extends SmCommand {
|
|
9664
9753
|
static paths = [["orphans"]];
|
|
9665
|
-
static usage =
|
|
9754
|
+
static usage = Command13.Usage({
|
|
9666
9755
|
category: "Browse",
|
|
9667
9756
|
description: "List orphan / auto-rename issues from the last scan. --json emits an array conforming to issue.schema.json.",
|
|
9668
9757
|
details: `
|
|
@@ -9677,13 +9766,8 @@ var OrphansCommand = class extends Command12 {
|
|
|
9677
9766
|
["Just the ambiguous ones, JSON", "$0 orphans --kind ambiguous --json"]
|
|
9678
9767
|
]
|
|
9679
9768
|
});
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
kind = Option12.String("--kind", { required: false });
|
|
9683
|
-
json = Option12.Boolean("--json", false);
|
|
9684
|
-
quiet = Option12.Boolean("--quiet", false);
|
|
9685
|
-
async execute() {
|
|
9686
|
-
const elapsed = startElapsed();
|
|
9769
|
+
kind = Option13.String("--kind", { required: false });
|
|
9770
|
+
async run() {
|
|
9687
9771
|
let ruleFilter = null;
|
|
9688
9772
|
if (this.kind !== void 0) {
|
|
9689
9773
|
const map = {
|
|
@@ -9714,14 +9798,13 @@ var OrphansCommand = class extends Command12 {
|
|
|
9714
9798
|
} else {
|
|
9715
9799
|
this.context.stdout.write(renderOrphans(found.map((f) => f.issue)));
|
|
9716
9800
|
}
|
|
9717
|
-
emitDoneStderr(this.context.stderr, elapsed, this.quiet);
|
|
9718
9801
|
return ExitCode.Ok;
|
|
9719
9802
|
});
|
|
9720
9803
|
}
|
|
9721
9804
|
};
|
|
9722
|
-
var OrphansReconcileCommand = class extends
|
|
9805
|
+
var OrphansReconcileCommand = class extends SmCommand {
|
|
9723
9806
|
static paths = [["orphans", "reconcile"]];
|
|
9724
|
-
static usage =
|
|
9807
|
+
static usage = Command13.Usage({
|
|
9725
9808
|
category: "Browse",
|
|
9726
9809
|
description: "Migrate state_* FKs from an orphan path to a live node, resolving the orphan issue.",
|
|
9727
9810
|
details: `
|
|
@@ -9737,14 +9820,10 @@ var OrphansReconcileCommand = class extends Command12 {
|
|
|
9737
9820
|
["Reattach orphan history", "$0 orphans reconcile skills/old.md --to skills/new.md"]
|
|
9738
9821
|
]
|
|
9739
9822
|
});
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
dryRun = Option12.Boolean("-n,--dry-run", false);
|
|
9745
|
-
quiet = Option12.Boolean("--quiet", false);
|
|
9746
|
-
async execute() {
|
|
9747
|
-
const elapsed = startElapsed();
|
|
9823
|
+
orphanPath = Option13.String({ required: true });
|
|
9824
|
+
to = Option13.String("--to", { required: true });
|
|
9825
|
+
dryRun = Option13.Boolean("-n,--dry-run", false);
|
|
9826
|
+
async run() {
|
|
9748
9827
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
9749
9828
|
if (!assertDbExists(dbPath, this.context.stderr)) return ExitCode.NotFound;
|
|
9750
9829
|
return withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
@@ -9807,14 +9886,13 @@ var OrphansReconcileCommand = class extends Command12 {
|
|
|
9807
9886
|
)
|
|
9808
9887
|
);
|
|
9809
9888
|
}
|
|
9810
|
-
emitDoneStderr(this.context.stderr, elapsed, this.quiet);
|
|
9811
9889
|
return ExitCode.Ok;
|
|
9812
9890
|
});
|
|
9813
9891
|
}
|
|
9814
9892
|
};
|
|
9815
|
-
var OrphansUndoRenameCommand = class extends
|
|
9893
|
+
var OrphansUndoRenameCommand = class extends SmCommand {
|
|
9816
9894
|
static paths = [["orphans", "undo-rename"]];
|
|
9817
|
-
static usage =
|
|
9895
|
+
static usage = Command13.Usage({
|
|
9818
9896
|
category: "Browse",
|
|
9819
9897
|
description: "Reverse a medium- or ambiguous-confidence auto-rename. Migrates state_* FKs back, emits a new orphan on the prior path.",
|
|
9820
9898
|
details: `
|
|
@@ -9834,15 +9912,11 @@ var OrphansUndoRenameCommand = class extends Command12 {
|
|
|
9834
9912
|
["Undo an ambiguous, picking a candidate", "$0 orphans undo-rename skills/new.md --from skills/old-a.md"]
|
|
9835
9913
|
]
|
|
9836
9914
|
});
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
dryRun = Option12.Boolean("-n,--dry-run", false);
|
|
9843
|
-
quiet = Option12.Boolean("--quiet", false);
|
|
9844
|
-
async execute() {
|
|
9845
|
-
const elapsed = startElapsed();
|
|
9915
|
+
newPath = Option13.String({ required: true });
|
|
9916
|
+
from = Option13.String("--from", { required: false });
|
|
9917
|
+
force = Option13.Boolean("--force", false);
|
|
9918
|
+
dryRun = Option13.Boolean("-n,--dry-run", false);
|
|
9919
|
+
async run() {
|
|
9846
9920
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
9847
9921
|
if (!assertDbExists(dbPath, this.context.stderr)) return ExitCode.NotFound;
|
|
9848
9922
|
return withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
@@ -9914,7 +9988,6 @@ var OrphansUndoRenameCommand = class extends Command12 {
|
|
|
9914
9988
|
rows: summaryTotal(summary)
|
|
9915
9989
|
})
|
|
9916
9990
|
);
|
|
9917
|
-
emitDoneStderr(this.context.stderr, elapsed, this.quiet);
|
|
9918
9991
|
return ExitCode.Ok;
|
|
9919
9992
|
});
|
|
9920
9993
|
}
|
|
@@ -10008,7 +10081,7 @@ var ORPHANS_COMMANDS = [
|
|
|
10008
10081
|
// cli/commands/plugins.ts
|
|
10009
10082
|
import { existsSync as existsSync13 } from "fs";
|
|
10010
10083
|
import { join as join12, resolve as resolve16 } from "path";
|
|
10011
|
-
import { Command as
|
|
10084
|
+
import { Command as Command14, Option as Option14 } from "clipanion";
|
|
10012
10085
|
|
|
10013
10086
|
// cli/i18n/plugins.texts.ts
|
|
10014
10087
|
var PLUGINS_TEXTS = {
|
|
@@ -10151,17 +10224,15 @@ function builtInRows(resolveEnabled) {
|
|
|
10151
10224
|
};
|
|
10152
10225
|
});
|
|
10153
10226
|
}
|
|
10154
|
-
var PluginsListCommand = class extends
|
|
10227
|
+
var PluginsListCommand = class extends SmCommand {
|
|
10155
10228
|
static paths = [["plugins", "list"]];
|
|
10156
|
-
static usage =
|
|
10229
|
+
static usage = Command14.Usage({
|
|
10157
10230
|
category: "Plugins",
|
|
10158
10231
|
description: "List discovered plugins and their load status.",
|
|
10159
10232
|
details: "Scans <scope>/.skill-map/plugins and ~/.skill-map/plugins (or --plugin-dir <path>). Built-in bundles (claude, core) are listed alongside user plugins."
|
|
10160
10233
|
});
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
json = Option13.Boolean("--json", false);
|
|
10164
|
-
async execute() {
|
|
10234
|
+
pluginDir = Option14.String("--plugin-dir", { required: false });
|
|
10235
|
+
async run() {
|
|
10165
10236
|
const plugins = await loadAll({ global: this.global, pluginDir: this.pluginDir });
|
|
10166
10237
|
const resolveEnabled = await buildResolver(this.global);
|
|
10167
10238
|
const builtIns2 = builtInRows(resolveEnabled);
|
|
@@ -10220,24 +10291,24 @@ function renderPluginRow(p) {
|
|
|
10220
10291
|
tail
|
|
10221
10292
|
}) + "\n";
|
|
10222
10293
|
}
|
|
10223
|
-
var PluginsShowCommand = class extends
|
|
10294
|
+
var PluginsShowCommand = class extends SmCommand {
|
|
10224
10295
|
static paths = [["plugins", "show"]];
|
|
10225
|
-
static usage =
|
|
10296
|
+
static usage = Command14.Usage({
|
|
10226
10297
|
category: "Plugins",
|
|
10227
10298
|
description: "Show a single plugin's manifest + loaded extensions."
|
|
10228
10299
|
});
|
|
10229
|
-
id =
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
json = Option13.Boolean("--json", false);
|
|
10233
|
-
async execute() {
|
|
10300
|
+
id = Option14.String({ required: true });
|
|
10301
|
+
pluginDir = Option14.String("--plugin-dir", { required: false });
|
|
10302
|
+
async run() {
|
|
10234
10303
|
const plugins = await loadAll({ global: this.global, pluginDir: this.pluginDir });
|
|
10235
10304
|
const resolveEnabled = await buildResolver(this.global);
|
|
10236
10305
|
const builtIns2 = builtInRows(resolveEnabled);
|
|
10237
10306
|
const builtIn = builtIns2.find((b) => b.id === this.id);
|
|
10238
10307
|
const match = plugins.find((p) => p.id === this.id);
|
|
10239
10308
|
if (!builtIn && !match) {
|
|
10240
|
-
this.context.stderr.write(
|
|
10309
|
+
this.context.stderr.write(
|
|
10310
|
+
tx(PLUGINS_TEXTS.pluginNotFound, { id: sanitizeForTerminal(this.id) }) + "\n"
|
|
10311
|
+
);
|
|
10241
10312
|
return ExitCode.NotFound;
|
|
10242
10313
|
}
|
|
10243
10314
|
if (this.json) {
|
|
@@ -10423,15 +10494,14 @@ function collectExplorationDirWarnings(plugins, homedir2) {
|
|
|
10423
10494
|
});
|
|
10424
10495
|
return out;
|
|
10425
10496
|
}
|
|
10426
|
-
var PluginsDoctorCommand = class extends
|
|
10497
|
+
var PluginsDoctorCommand = class extends SmCommand {
|
|
10427
10498
|
static paths = [["plugins", "doctor"]];
|
|
10428
|
-
static usage =
|
|
10499
|
+
static usage = Command14.Usage({
|
|
10429
10500
|
category: "Plugins",
|
|
10430
10501
|
description: "Run the full load pass and summarise by failure mode.",
|
|
10431
10502
|
details: "Exit code 0 when every plugin loads or is intentionally disabled; 1 when any plugin is in an error / incompat state."
|
|
10432
10503
|
});
|
|
10433
|
-
|
|
10434
|
-
pluginDir = Option13.String("--plugin-dir", { required: false });
|
|
10504
|
+
pluginDir = Option14.String("--plugin-dir", { required: false });
|
|
10435
10505
|
// Doctor verb: counts by status + applicableKinds warnings +
|
|
10436
10506
|
// explorationDir warnings + bad-plugins issues, each with its own
|
|
10437
10507
|
// gated render. Branching is intrinsic to the multi-section diagnostic
|
|
@@ -10439,7 +10509,7 @@ var PluginsDoctorCommand = class extends Command13 {
|
|
|
10439
10509
|
// `collectApplicableKindWarnings`, `collectExplorationDirWarnings`)
|
|
10440
10510
|
// already encapsulate the data gathering.
|
|
10441
10511
|
// eslint-disable-next-line complexity
|
|
10442
|
-
async
|
|
10512
|
+
async run() {
|
|
10443
10513
|
const plugins = await loadAll({ global: this.global, pluginDir: this.pluginDir });
|
|
10444
10514
|
const resolveEnabled = await buildResolver(this.global);
|
|
10445
10515
|
const builtIns2 = builtInRows(resolveEnabled);
|
|
@@ -10548,17 +10618,25 @@ function resolveToggleTarget(id, catalogue, verb) {
|
|
|
10548
10618
|
if (id.includes("/")) {
|
|
10549
10619
|
const [bundleId, extId, ...rest] = id.split("/");
|
|
10550
10620
|
if (!bundleId || !extId || rest.length > 0) {
|
|
10551
|
-
return {
|
|
10621
|
+
return {
|
|
10622
|
+
error: tx(PLUGINS_TEXTS.qualifiedIdUnknownBundle, {
|
|
10623
|
+
bundleId: sanitizeForTerminal(id)
|
|
10624
|
+
})
|
|
10625
|
+
};
|
|
10552
10626
|
}
|
|
10553
10627
|
const bundle2 = catalogue.find((b) => b.id === bundleId);
|
|
10554
10628
|
if (!bundle2) {
|
|
10555
|
-
return {
|
|
10629
|
+
return {
|
|
10630
|
+
error: tx(PLUGINS_TEXTS.qualifiedIdUnknownBundle, {
|
|
10631
|
+
bundleId: sanitizeForTerminal(bundleId)
|
|
10632
|
+
})
|
|
10633
|
+
};
|
|
10556
10634
|
}
|
|
10557
10635
|
if (bundle2.granularity === "bundle") {
|
|
10558
10636
|
return {
|
|
10559
10637
|
error: tx(PLUGINS_TEXTS.granularityBundleRejectsQualified, {
|
|
10560
|
-
bundleId,
|
|
10561
|
-
extId,
|
|
10638
|
+
bundleId: sanitizeForTerminal(bundleId),
|
|
10639
|
+
extId: sanitizeForTerminal(extId),
|
|
10562
10640
|
verb
|
|
10563
10641
|
})
|
|
10564
10642
|
};
|
|
@@ -10566,9 +10644,9 @@ function resolveToggleTarget(id, catalogue, verb) {
|
|
|
10566
10644
|
if (!bundle2.extensionIds.includes(extId)) {
|
|
10567
10645
|
return {
|
|
10568
10646
|
error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
|
|
10569
|
-
id,
|
|
10570
|
-
bundleId,
|
|
10571
|
-
extId
|
|
10647
|
+
id: sanitizeForTerminal(id),
|
|
10648
|
+
bundleId: sanitizeForTerminal(bundleId),
|
|
10649
|
+
extId: sanitizeForTerminal(extId)
|
|
10572
10650
|
})
|
|
10573
10651
|
};
|
|
10574
10652
|
}
|
|
@@ -10576,34 +10654,30 @@ function resolveToggleTarget(id, catalogue, verb) {
|
|
|
10576
10654
|
}
|
|
10577
10655
|
const bundle = catalogue.find((b) => b.id === id);
|
|
10578
10656
|
if (!bundle) {
|
|
10579
|
-
return { error: tx(PLUGINS_TEXTS.pluginNotFound, { id }) };
|
|
10657
|
+
return { error: tx(PLUGINS_TEXTS.pluginNotFound, { id: sanitizeForTerminal(id) }) };
|
|
10580
10658
|
}
|
|
10581
10659
|
if (bundle.granularity === "extension") {
|
|
10582
10660
|
return {
|
|
10583
10661
|
error: tx(PLUGINS_TEXTS.granularityExtensionRejectsBundleId, {
|
|
10584
|
-
bundleId: id,
|
|
10662
|
+
bundleId: sanitizeForTerminal(id),
|
|
10585
10663
|
verb
|
|
10586
10664
|
})
|
|
10587
10665
|
};
|
|
10588
10666
|
}
|
|
10589
10667
|
return { key: bundle.id };
|
|
10590
10668
|
}
|
|
10591
|
-
var TogglePluginsBase = class extends
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
id = Option13.String({ required: false });
|
|
10669
|
+
var TogglePluginsBase = class extends SmCommand {
|
|
10670
|
+
all = Option14.Boolean("--all", false);
|
|
10671
|
+
id = Option14.String({ required: false });
|
|
10595
10672
|
// eslint-disable-next-line complexity
|
|
10596
10673
|
async toggle(enabled) {
|
|
10597
|
-
const elapsed = startElapsed();
|
|
10598
10674
|
const verb = enabled ? "enable" : "disable";
|
|
10599
10675
|
if (this.all && this.id) {
|
|
10600
10676
|
this.context.stderr.write(PLUGINS_TEXTS.toggleBothIdAndAll);
|
|
10601
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
10602
10677
|
return ExitCode.Error;
|
|
10603
10678
|
}
|
|
10604
10679
|
if (!this.all && !this.id) {
|
|
10605
10680
|
this.context.stderr.write(PLUGINS_TEXTS.toggleNeitherIdNorAll);
|
|
10606
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
10607
10681
|
return ExitCode.Error;
|
|
10608
10682
|
}
|
|
10609
10683
|
const plugins = await loadAll({
|
|
@@ -10618,7 +10692,6 @@ var TogglePluginsBase = class extends Command13 {
|
|
|
10618
10692
|
const resolved = resolveToggleTarget(this.id, catalogue, verb);
|
|
10619
10693
|
if ("error" in resolved) {
|
|
10620
10694
|
this.context.stderr.write(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
|
|
10621
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
10622
10695
|
return ExitCode.NotFound;
|
|
10623
10696
|
}
|
|
10624
10697
|
targets = [resolved.key];
|
|
@@ -10641,13 +10714,12 @@ var TogglePluginsBase = class extends Command13 {
|
|
|
10641
10714
|
this.context.stdout.write(tx(PLUGINS_TEXTS.toggleAppliedManyRow, { id }));
|
|
10642
10715
|
}
|
|
10643
10716
|
}
|
|
10644
|
-
emitDoneStderr(this.context.stderr, elapsed);
|
|
10645
10717
|
return ExitCode.Ok;
|
|
10646
10718
|
}
|
|
10647
10719
|
};
|
|
10648
10720
|
var PluginsEnableCommand = class extends TogglePluginsBase {
|
|
10649
10721
|
static paths = [["plugins", "enable"]];
|
|
10650
|
-
static usage =
|
|
10722
|
+
static usage = Command14.Usage({
|
|
10651
10723
|
category: "Plugins",
|
|
10652
10724
|
description: "Enable a plugin (or --all). Persists in config_plugins.",
|
|
10653
10725
|
details: `
|
|
@@ -10663,13 +10735,13 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
|
|
|
10663
10735
|
with directed guidance.
|
|
10664
10736
|
`
|
|
10665
10737
|
});
|
|
10666
|
-
async
|
|
10738
|
+
async run() {
|
|
10667
10739
|
return this.toggle(true);
|
|
10668
10740
|
}
|
|
10669
10741
|
};
|
|
10670
10742
|
var PluginsDisableCommand = class extends TogglePluginsBase {
|
|
10671
10743
|
static paths = [["plugins", "disable"]];
|
|
10672
|
-
static usage =
|
|
10744
|
+
static usage = Command14.Usage({
|
|
10673
10745
|
category: "Plugins",
|
|
10674
10746
|
description: "Disable a plugin (or --all). Persists in config_plugins; does not delete files.",
|
|
10675
10747
|
details: `
|
|
@@ -10685,7 +10757,7 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
|
|
|
10685
10757
|
with directed guidance.
|
|
10686
10758
|
`
|
|
10687
10759
|
});
|
|
10688
|
-
async
|
|
10760
|
+
async run() {
|
|
10689
10761
|
return this.toggle(false);
|
|
10690
10762
|
}
|
|
10691
10763
|
};
|
|
@@ -10706,7 +10778,7 @@ var PLUGIN_COMMANDS = [
|
|
|
10706
10778
|
// cli/commands/refresh.ts
|
|
10707
10779
|
import { readFile as readFile3 } from "fs/promises";
|
|
10708
10780
|
import { resolve as resolve18 } from "path";
|
|
10709
|
-
import { Command as
|
|
10781
|
+
import { Command as Command15, Option as Option15 } from "clipanion";
|
|
10710
10782
|
|
|
10711
10783
|
// cli/i18n/refresh.texts.ts
|
|
10712
10784
|
var REFRESH_TEXTS = {
|
|
@@ -10747,9 +10819,9 @@ function assertContained2(cwd, rel) {
|
|
|
10747
10819
|
}
|
|
10748
10820
|
|
|
10749
10821
|
// cli/commands/refresh.ts
|
|
10750
|
-
var RefreshCommand = class extends
|
|
10822
|
+
var RefreshCommand = class extends SmCommand {
|
|
10751
10823
|
static paths = [["refresh"]];
|
|
10752
|
-
static usage =
|
|
10824
|
+
static usage = Command15.Usage({
|
|
10753
10825
|
category: "Scan",
|
|
10754
10826
|
description: "Refresh enrichment rows: granular (single node) or batch (every stale row).",
|
|
10755
10827
|
details: `
|
|
@@ -10774,11 +10846,11 @@ var RefreshCommand = class extends Command14 {
|
|
|
10774
10846
|
["Refresh every node with stale enrichments", "$0 refresh --stale"]
|
|
10775
10847
|
]
|
|
10776
10848
|
});
|
|
10777
|
-
nodePath =
|
|
10778
|
-
stale =
|
|
10849
|
+
nodePath = Option15.String({ name: "node", required: false });
|
|
10850
|
+
stale = Option15.Boolean("--stale", false, {
|
|
10779
10851
|
description: "Refresh every node whose probabilistic enrichment row is flagged stale=1."
|
|
10780
10852
|
});
|
|
10781
|
-
noPlugins =
|
|
10853
|
+
noPlugins = Option15.Boolean("--no-plugins", false, {
|
|
10782
10854
|
description: "Skip drop-in plugin discovery; use only the built-in extractor set."
|
|
10783
10855
|
});
|
|
10784
10856
|
// The remaining cyclomatic count comes from CLI ergonomics that don't
|
|
@@ -10787,7 +10859,7 @@ var RefreshCommand = class extends Command14 {
|
|
|
10787
10859
|
// catch, plus the `if (probSkipCount > 0)` advisory. The inner work
|
|
10788
10860
|
// already lives in `#resolveTargetNodes` and `#runDetExtractorsAcrossNodes`.
|
|
10789
10861
|
// eslint-disable-next-line complexity
|
|
10790
|
-
async
|
|
10862
|
+
async run() {
|
|
10791
10863
|
if (this.stale && this.nodePath !== void 0) {
|
|
10792
10864
|
this.context.stderr.write(REFRESH_TEXTS.nodeAndStaleMutex);
|
|
10793
10865
|
return ExitCode.Error;
|
|
@@ -10963,7 +11035,7 @@ function stripFrontmatterFence(text) {
|
|
|
10963
11035
|
var REFRESH_COMMANDS = [RefreshCommand];
|
|
10964
11036
|
|
|
10965
11037
|
// cli/commands/scan.ts
|
|
10966
|
-
import { Command as
|
|
11038
|
+
import { Command as Command17, Option as Option17 } from "clipanion";
|
|
10967
11039
|
|
|
10968
11040
|
// cli/i18n/scan.texts.ts
|
|
10969
11041
|
var SCAN_TEXTS = {
|
|
@@ -11006,8 +11078,156 @@ var SCAN_TEXTS = {
|
|
|
11006
11078
|
compareDeltaIssueRemoved: "- [{{severity}}] {{ruleId}}: {{message}}"
|
|
11007
11079
|
};
|
|
11008
11080
|
|
|
11009
|
-
// cli/
|
|
11010
|
-
|
|
11081
|
+
// cli/util/scan-runner.ts
|
|
11082
|
+
async function runScanForCommand(opts) {
|
|
11083
|
+
const ctx = opts.ctx ?? defaultRuntimeContext();
|
|
11084
|
+
const dbPath = defaultProjectDbPath(ctx);
|
|
11085
|
+
const kernel = createKernel();
|
|
11086
|
+
const pluginRuntime = await preparePluginRuntime(opts);
|
|
11087
|
+
const extensions = registerExtensions(kernel, pluginRuntime, opts);
|
|
11088
|
+
let cfg;
|
|
11089
|
+
try {
|
|
11090
|
+
cfg = loadConfig({ scope: "project", strict: opts.strict, ...ctx }).effective;
|
|
11091
|
+
} catch (err) {
|
|
11092
|
+
return { kind: "config-error", message: formatErrorMessage(err) };
|
|
11093
|
+
}
|
|
11094
|
+
const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
|
|
11095
|
+
const strict = opts.strict || cfg.scan.strict === true;
|
|
11096
|
+
const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
|
|
11097
|
+
const runScanWith = makeScanRunner(kernel, opts, ignoreFilter, strict, extensions);
|
|
11098
|
+
const willPersist = !opts.noBuiltIns && !opts.dryRun;
|
|
11099
|
+
return willPersist ? runPersistPath(opts, dbPath, strict, loadPrior, runScanWith) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
|
|
11100
|
+
}
|
|
11101
|
+
async function preparePluginRuntime(opts) {
|
|
11102
|
+
const pluginRuntime = opts.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ scope: "project" });
|
|
11103
|
+
for (const warn of pluginRuntime.warnings) opts.stderr.write(`${warn}
|
|
11104
|
+
`);
|
|
11105
|
+
return pluginRuntime;
|
|
11106
|
+
}
|
|
11107
|
+
function registerExtensions(kernel, pluginRuntime, opts) {
|
|
11108
|
+
const extensions = composeScanExtensions({
|
|
11109
|
+
noBuiltIns: opts.noBuiltIns,
|
|
11110
|
+
pluginRuntime
|
|
11111
|
+
});
|
|
11112
|
+
if (!opts.noBuiltIns) {
|
|
11113
|
+
const enabledBuiltIns = filterBuiltInManifests(listBuiltIns(), pluginRuntime.resolveEnabled);
|
|
11114
|
+
for (const manifest of enabledBuiltIns) kernel.registry.register(manifest);
|
|
11115
|
+
}
|
|
11116
|
+
for (const manifest of pluginRuntime.manifests) kernel.registry.register(manifest);
|
|
11117
|
+
return extensions;
|
|
11118
|
+
}
|
|
11119
|
+
function buildScanIgnoreFilter(cfg, cwd) {
|
|
11120
|
+
const ignoreFileText = readIgnoreFileText(cwd);
|
|
11121
|
+
const ignoreFilterOpts = {};
|
|
11122
|
+
if (cfg.ignore.length > 0) ignoreFilterOpts.configIgnore = cfg.ignore;
|
|
11123
|
+
if (ignoreFileText !== void 0) ignoreFilterOpts.ignoreFileText = ignoreFileText;
|
|
11124
|
+
return buildIgnoreFilter(ignoreFilterOpts);
|
|
11125
|
+
}
|
|
11126
|
+
function makePriorLoader(noBuiltIns, strict) {
|
|
11127
|
+
return async (adapter) => {
|
|
11128
|
+
if (noBuiltIns) return null;
|
|
11129
|
+
const loaded = await adapter.scans.load();
|
|
11130
|
+
if (loaded.nodes.length === 0) return null;
|
|
11131
|
+
if (strict) {
|
|
11132
|
+
const validators = loadSchemaValidators();
|
|
11133
|
+
const result = validators.validate("scan-result", loaded);
|
|
11134
|
+
if (!result.ok) {
|
|
11135
|
+
throw new Error(tx(SCAN_TEXTS.priorSchemaValidationFailed, { errors: result.errors }));
|
|
11136
|
+
}
|
|
11137
|
+
}
|
|
11138
|
+
return loaded;
|
|
11139
|
+
};
|
|
11140
|
+
}
|
|
11141
|
+
function makeScanRunner(kernel, opts, ignoreFilter, strict, extensions) {
|
|
11142
|
+
return async (prior, priorExtractorRuns) => {
|
|
11143
|
+
if (opts.changed && prior === null) {
|
|
11144
|
+
opts.stderr.write(SCAN_TEXTS.changedNoPriorWarning);
|
|
11145
|
+
}
|
|
11146
|
+
const runOptions = {
|
|
11147
|
+
roots: opts.roots,
|
|
11148
|
+
// Hardcoded `'project'`: spec § Global flags lists `-g/--global`
|
|
11149
|
+
// as universal, but the per-verb § Scan table does not list it
|
|
11150
|
+
// and the semantics of "scan global" are undefined. The
|
|
11151
|
+
// ScanCommand surface accepts `-g` (inherited from SmCommand)
|
|
11152
|
+
// but ignores it here. Wire to `opts.scope` once spec defines
|
|
11153
|
+
// the contract.
|
|
11154
|
+
scope: "project",
|
|
11155
|
+
tokenize: !opts.noTokens,
|
|
11156
|
+
ignoreFilter,
|
|
11157
|
+
strict,
|
|
11158
|
+
emitter: createCliProgressEmitter(opts.stderr)
|
|
11159
|
+
};
|
|
11160
|
+
if (extensions) runOptions.extensions = extensions;
|
|
11161
|
+
if (prior) {
|
|
11162
|
+
runOptions.priorSnapshot = prior;
|
|
11163
|
+
runOptions.enableCache = opts.changed;
|
|
11164
|
+
}
|
|
11165
|
+
if (priorExtractorRuns) runOptions.priorExtractorRuns = priorExtractorRuns;
|
|
11166
|
+
return runScanWithRenames(kernel, runOptions);
|
|
11167
|
+
};
|
|
11168
|
+
}
|
|
11169
|
+
async function runPersistPath(opts, dbPath, strict, loadPrior, runScanWith) {
|
|
11170
|
+
let outcome;
|
|
11171
|
+
try {
|
|
11172
|
+
outcome = await withSqlite({ databasePath: dbPath }, async (adapter) => {
|
|
11173
|
+
const prior = await loadPrior(adapter);
|
|
11174
|
+
const priorExtractorRuns = opts.changed && prior ? await adapter.scans.loadExtractorRuns() : void 0;
|
|
11175
|
+
let scanned;
|
|
11176
|
+
try {
|
|
11177
|
+
scanned = await runScanWith(prior, priorExtractorRuns);
|
|
11178
|
+
} catch (err) {
|
|
11179
|
+
return { kind: "scan-error", message: formatErrorMessage(err) };
|
|
11180
|
+
}
|
|
11181
|
+
if (scanned.result.stats.nodesCount === 0 && !opts.allowEmpty) {
|
|
11182
|
+
const counts = await adapter.scans.countRows();
|
|
11183
|
+
const existing = counts.nodes + counts.links + counts.issues;
|
|
11184
|
+
if (existing > 0) return { kind: "guard", existing };
|
|
11185
|
+
}
|
|
11186
|
+
await adapter.scans.persist(scanned.result, {
|
|
11187
|
+
renameOps: scanned.renameOps,
|
|
11188
|
+
extractorRuns: scanned.extractorRuns,
|
|
11189
|
+
enrichments: scanned.enrichments
|
|
11190
|
+
});
|
|
11191
|
+
return { kind: "ok", ...scanned };
|
|
11192
|
+
});
|
|
11193
|
+
} catch (err) {
|
|
11194
|
+
return { kind: "scan-error", message: formatErrorMessage(err) };
|
|
11195
|
+
}
|
|
11196
|
+
if (outcome.kind === "scan-error") return outcome;
|
|
11197
|
+
if (outcome.kind === "guard") return { kind: "guard-trip", existing: outcome.existing };
|
|
11198
|
+
return {
|
|
11199
|
+
kind: "ok",
|
|
11200
|
+
result: outcome.result,
|
|
11201
|
+
renameOps: outcome.renameOps,
|
|
11202
|
+
persistedTo: dbPath,
|
|
11203
|
+
dbPath,
|
|
11204
|
+
strict
|
|
11205
|
+
};
|
|
11206
|
+
}
|
|
11207
|
+
async function runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith) {
|
|
11208
|
+
let prior;
|
|
11209
|
+
try {
|
|
11210
|
+
prior = opts.noBuiltIns ? null : await tryWithSqlite({ databasePath: dbPath, autoBackup: false }, loadPrior);
|
|
11211
|
+
} catch (err) {
|
|
11212
|
+
return { kind: "scan-error", message: formatErrorMessage(err) };
|
|
11213
|
+
}
|
|
11214
|
+
try {
|
|
11215
|
+
const scanned = await runScanWith(prior);
|
|
11216
|
+
return {
|
|
11217
|
+
kind: "ok",
|
|
11218
|
+
result: scanned.result,
|
|
11219
|
+
renameOps: scanned.renameOps,
|
|
11220
|
+
persistedTo: null,
|
|
11221
|
+
dbPath,
|
|
11222
|
+
strict
|
|
11223
|
+
};
|
|
11224
|
+
} catch (err) {
|
|
11225
|
+
return { kind: "scan-error", message: formatErrorMessage(err) };
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
|
|
11229
|
+
// cli/commands/watch.ts
|
|
11230
|
+
import { Command as Command16, Option as Option16 } from "clipanion";
|
|
11011
11231
|
|
|
11012
11232
|
// cli/i18n/watch.texts.ts
|
|
11013
11233
|
var WATCH_TEXTS = {
|
|
@@ -11020,10 +11240,12 @@ var WATCH_TEXTS = {
|
|
|
11020
11240
|
ready: "sm watch: ready. Press Ctrl+C to stop.\n",
|
|
11021
11241
|
stopped: "sm watch: stopped after {{batchCount}} batch(es).\n",
|
|
11022
11242
|
scannedSummary: "scanned {{nodes}} nodes / {{links}} links / {{issues}} issues in {{durationMs}}ms\n",
|
|
11023
|
-
priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}"
|
|
11243
|
+
priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}",
|
|
11244
|
+
breakerTripped: "sm watch: {{count}} consecutive batch failures \u2014 shutting down. Last error: {{message}}\n"
|
|
11024
11245
|
};
|
|
11025
11246
|
|
|
11026
11247
|
// cli/commands/watch.ts
|
|
11248
|
+
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
11027
11249
|
async function runWatchLoop(opts) {
|
|
11028
11250
|
const { context } = opts;
|
|
11029
11251
|
const runtimeCtx = defaultRuntimeContext();
|
|
@@ -11087,21 +11309,8 @@ async function runWatchLoop(opts) {
|
|
|
11087
11309
|
runOptions.enableCache = true;
|
|
11088
11310
|
}
|
|
11089
11311
|
if (priorExtractorRuns) runOptions.priorExtractorRuns = priorExtractorRuns;
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
let extractorRuns;
|
|
11093
|
-
let enrichments;
|
|
11094
|
-
try {
|
|
11095
|
-
const ran = await runScanWithRenames(kernel, runOptions);
|
|
11096
|
-
result = ran.result;
|
|
11097
|
-
renameOps = ran.renameOps;
|
|
11098
|
-
extractorRuns = ran.extractorRuns;
|
|
11099
|
-
enrichments = ran.enrichments;
|
|
11100
|
-
} catch (err) {
|
|
11101
|
-
const message = formatErrorMessage(err);
|
|
11102
|
-
context.stderr.write(tx(WATCH_TEXTS.scanFailed, { message }));
|
|
11103
|
-
return;
|
|
11104
|
-
}
|
|
11312
|
+
const ran = await runScanWithRenames(kernel, runOptions);
|
|
11313
|
+
const { result, renameOps, extractorRuns, enrichments } = ran;
|
|
11105
11314
|
await withSqlite(
|
|
11106
11315
|
{ databasePath: dbPath },
|
|
11107
11316
|
(writer) => writer.scans.persist(result, { renameOps, extractorRuns, enrichments })
|
|
@@ -11135,21 +11344,38 @@ async function runWatchLoop(opts) {
|
|
|
11135
11344
|
const stopped = new Promise((r) => {
|
|
11136
11345
|
stopResolve = r;
|
|
11137
11346
|
});
|
|
11347
|
+
const breakerLimit = opts.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
11348
|
+
let consecutiveFailures = 0;
|
|
11349
|
+
let exitCode2 = ExitCode.Ok;
|
|
11350
|
+
const handleBatch = async () => {
|
|
11351
|
+
if (stopRequested) return "stop";
|
|
11352
|
+
batchCount++;
|
|
11353
|
+
try {
|
|
11354
|
+
await runOnePass();
|
|
11355
|
+
consecutiveFailures = 0;
|
|
11356
|
+
} catch (err) {
|
|
11357
|
+
const message = formatErrorMessage(err);
|
|
11358
|
+
context.stderr.write(tx(WATCH_TEXTS.batchFailed, { message }));
|
|
11359
|
+
consecutiveFailures += 1;
|
|
11360
|
+
if (breakerLimit > 0 && consecutiveFailures >= breakerLimit) {
|
|
11361
|
+
context.stderr.write(
|
|
11362
|
+
tx(WATCH_TEXTS.breakerTripped, { count: consecutiveFailures, message })
|
|
11363
|
+
);
|
|
11364
|
+
exitCode2 = ExitCode.Error;
|
|
11365
|
+
return "stop";
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
if (opts.maxBatches !== void 0 && batchCount >= opts.maxBatches) return "stop";
|
|
11369
|
+
return "continue";
|
|
11370
|
+
};
|
|
11138
11371
|
const watcher = createChokidarWatcher({
|
|
11139
11372
|
roots: opts.roots,
|
|
11140
11373
|
cwd,
|
|
11141
11374
|
debounceMs,
|
|
11142
11375
|
ignoreFilter,
|
|
11143
11376
|
onBatch: async () => {
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
try {
|
|
11147
|
-
await runOnePass();
|
|
11148
|
-
} catch (err) {
|
|
11149
|
-
const message = formatErrorMessage(err);
|
|
11150
|
-
context.stderr.write(tx(WATCH_TEXTS.batchFailed, { message }));
|
|
11151
|
-
}
|
|
11152
|
-
if (opts.maxBatches !== void 0 && batchCount >= opts.maxBatches) {
|
|
11377
|
+
const next = await handleBatch();
|
|
11378
|
+
if (next === "stop") {
|
|
11153
11379
|
stopRequested = true;
|
|
11154
11380
|
stopResolve?.();
|
|
11155
11381
|
}
|
|
@@ -11176,11 +11402,11 @@ async function runWatchLoop(opts) {
|
|
|
11176
11402
|
if (!opts.json) {
|
|
11177
11403
|
context.stderr.write(tx(WATCH_TEXTS.stopped, { batchCount }));
|
|
11178
11404
|
}
|
|
11179
|
-
return
|
|
11405
|
+
return exitCode2;
|
|
11180
11406
|
}
|
|
11181
|
-
var WatchCommand = class extends
|
|
11407
|
+
var WatchCommand = class extends SmCommand {
|
|
11182
11408
|
static paths = [["watch"]];
|
|
11183
|
-
static usage =
|
|
11409
|
+
static usage = Command16.Usage({
|
|
11184
11410
|
category: "Scan",
|
|
11185
11411
|
description: "Watch roots and run an incremental scan after each debounced batch of filesystem events.",
|
|
11186
11412
|
details: `
|
|
@@ -11204,36 +11430,55 @@ var WatchCommand = class extends Command15 {
|
|
|
11204
11430
|
["Stream ScanResult per batch as ndjson", "$0 watch --json"]
|
|
11205
11431
|
]
|
|
11206
11432
|
});
|
|
11207
|
-
roots =
|
|
11208
|
-
|
|
11209
|
-
description: "Emit one ScanResult document per batch as ndjson on stdout."
|
|
11210
|
-
});
|
|
11211
|
-
noTokens = Option15.Boolean("--no-tokens", false, {
|
|
11433
|
+
roots = Option16.Rest({ name: "roots" });
|
|
11434
|
+
noTokens = Option16.Boolean("--no-tokens", false, {
|
|
11212
11435
|
description: "Skip per-node token counts (cl100k_base BPE)."
|
|
11213
11436
|
});
|
|
11214
|
-
strict =
|
|
11437
|
+
strict = Option16.Boolean("--strict", false, {
|
|
11215
11438
|
description: "Promote frontmatter-validation findings from warn to error inside each batch. Does not change the watcher exit code."
|
|
11216
11439
|
});
|
|
11217
|
-
noPlugins =
|
|
11440
|
+
noPlugins = Option16.Boolean("--no-plugins", false, {
|
|
11218
11441
|
description: "Skip drop-in plugin discovery for the watcher session."
|
|
11219
11442
|
});
|
|
11220
|
-
|
|
11443
|
+
maxConsecutiveFailures = Option16.String("--max-consecutive-failures", {
|
|
11444
|
+
required: false,
|
|
11445
|
+
description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
|
|
11446
|
+
});
|
|
11447
|
+
// Long-running verb — the watcher prints its own "stopped" line on
|
|
11448
|
+
// SIGINT / SIGTERM. Adding `done in <…>` after that would be noise.
|
|
11449
|
+
emitElapsed = false;
|
|
11450
|
+
async run() {
|
|
11221
11451
|
const roots = this.roots.length > 0 ? this.roots : ["."];
|
|
11222
|
-
|
|
11452
|
+
const breaker = parseBreakerLimit(this.maxConsecutiveFailures, this.context.stderr);
|
|
11453
|
+
if (breaker === null) return ExitCode.Error;
|
|
11454
|
+
const watchOpts = {
|
|
11223
11455
|
roots,
|
|
11224
11456
|
json: this.json,
|
|
11225
11457
|
noTokens: this.noTokens,
|
|
11226
11458
|
strict: this.strict,
|
|
11227
11459
|
noPlugins: this.noPlugins,
|
|
11228
11460
|
context: this.context
|
|
11229
|
-
}
|
|
11461
|
+
};
|
|
11462
|
+
if (breaker !== void 0) watchOpts.maxConsecutiveFailures = breaker;
|
|
11463
|
+
return runWatchLoop(watchOpts);
|
|
11230
11464
|
}
|
|
11231
11465
|
};
|
|
11466
|
+
function parseBreakerLimit(raw, stderr) {
|
|
11467
|
+
if (raw === void 0) return void 0;
|
|
11468
|
+
const trimmed = raw.trim();
|
|
11469
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
11470
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
11471
|
+
stderr.write(`sm watch: --max-consecutive-failures must be a non-negative integer (got ${raw})
|
|
11472
|
+
`);
|
|
11473
|
+
return null;
|
|
11474
|
+
}
|
|
11475
|
+
return parsed;
|
|
11476
|
+
}
|
|
11232
11477
|
|
|
11233
11478
|
// cli/commands/scan.ts
|
|
11234
|
-
var ScanCommand = class extends
|
|
11479
|
+
var ScanCommand = class extends SmCommand {
|
|
11235
11480
|
static paths = [["scan"]];
|
|
11236
|
-
static usage =
|
|
11481
|
+
static usage = Command17.Usage({
|
|
11237
11482
|
category: "Scan",
|
|
11238
11483
|
description: "Scan roots for markdown nodes, run extractors and rules.",
|
|
11239
11484
|
details: `
|
|
@@ -11262,198 +11507,87 @@ var ScanCommand = class extends Command16 {
|
|
|
11262
11507
|
["What would the next incremental scan persist?", "$0 scan --changed -n --json"]
|
|
11263
11508
|
]
|
|
11264
11509
|
});
|
|
11265
|
-
roots =
|
|
11266
|
-
|
|
11267
|
-
description: "Emit a machine-readable ScanResult document on stdout."
|
|
11268
|
-
});
|
|
11269
|
-
noBuiltIns = Option16.Boolean("--no-built-ins", false, {
|
|
11510
|
+
roots = Option17.Rest({ name: "roots" });
|
|
11511
|
+
noBuiltIns = Option17.Boolean("--no-built-ins", false, {
|
|
11270
11512
|
description: "Skip the built-in extension set. Yields a zero-filled ScanResult (kernel-empty-boot parity); skips DB persistence."
|
|
11271
11513
|
});
|
|
11272
|
-
noPlugins =
|
|
11514
|
+
noPlugins = Option17.Boolean("--no-plugins", false, {
|
|
11273
11515
|
description: "Skip drop-in plugin discovery. Only the built-in set runs. Combine with --no-built-ins for a fully empty pipeline."
|
|
11274
11516
|
});
|
|
11275
|
-
noTokens =
|
|
11517
|
+
noTokens = Option17.Boolean("--no-tokens", false, {
|
|
11276
11518
|
description: "Skip per-node token counts (cl100k_base BPE). Leaves node.tokens undefined; spec-valid since the field is optional."
|
|
11277
11519
|
});
|
|
11278
|
-
dryRun =
|
|
11520
|
+
dryRun = Option17.Boolean("-n,--dry-run", false, {
|
|
11279
11521
|
description: "Run the scan in memory and skip every DB write. Combined with --changed, still opens the DB read-side to load the prior snapshot."
|
|
11280
11522
|
});
|
|
11281
|
-
changed =
|
|
11523
|
+
changed = Option17.Boolean("--changed", false, {
|
|
11282
11524
|
description: "Incremental scan: reuse unchanged nodes from the persisted prior snapshot. Degrades to a full scan if no prior snapshot exists."
|
|
11283
11525
|
});
|
|
11284
|
-
allowEmpty =
|
|
11526
|
+
allowEmpty = Option17.Boolean("--allow-empty", false, {
|
|
11285
11527
|
description: "Allow a zero-result scan to wipe an already-populated DB (replace-all replace by zero rows). Off by default to avoid the typo-trap where an invalid root silently clears your data."
|
|
11286
11528
|
});
|
|
11287
|
-
strict =
|
|
11529
|
+
strict = Option17.Boolean("--strict", false, {
|
|
11288
11530
|
description: "Promote frontmatter-validation findings from warn to error (exit code 1 on any violation). Overrides scan.strict from config when both are set."
|
|
11289
11531
|
});
|
|
11290
|
-
watch =
|
|
11532
|
+
watch = Option17.Boolean("--watch", false, {
|
|
11291
11533
|
description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
|
|
11292
11534
|
});
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
// load + persist branch (with stranded-orphan guard) + dry-run branch
|
|
11296
|
-
// + JSON / human render with strict self-validation. The core scan
|
|
11297
|
-
// pipeline lives in `runScan` / `runScanWithRenames`; the
|
|
11298
|
-
// `loadPrior` and `runScanWith` closures below encapsulate the DB +
|
|
11299
|
-
// option wiring. Splitting per branch would scatter Clipanion's
|
|
11300
|
-
// `this.<flag>` reads from the validations they shape.
|
|
11301
|
-
// eslint-disable-next-line complexity
|
|
11302
|
-
async execute() {
|
|
11303
|
-
if (this.watch) {
|
|
11304
|
-
if (this.noBuiltIns || this.dryRun || this.changed || this.allowEmpty) {
|
|
11305
|
-
this.context.stderr.write(SCAN_TEXTS.watchCannotCombine);
|
|
11306
|
-
return ExitCode.Error;
|
|
11307
|
-
}
|
|
11308
|
-
const roots2 = this.roots.length > 0 ? this.roots : ["."];
|
|
11309
|
-
return runWatchLoop({
|
|
11310
|
-
roots: roots2,
|
|
11311
|
-
json: this.json,
|
|
11312
|
-
noTokens: this.noTokens,
|
|
11313
|
-
strict: this.strict,
|
|
11314
|
-
noPlugins: this.noPlugins,
|
|
11315
|
-
context: this.context
|
|
11316
|
-
});
|
|
11317
|
-
}
|
|
11535
|
+
async run() {
|
|
11536
|
+
if (this.watch) return this.runWatchAlias();
|
|
11318
11537
|
if (this.changed && this.noBuiltIns) {
|
|
11319
11538
|
this.context.stderr.write(SCAN_TEXTS.changedWithoutBuiltIns);
|
|
11320
11539
|
return ExitCode.Error;
|
|
11321
11540
|
}
|
|
11322
|
-
const kernel = createKernel();
|
|
11323
11541
|
const roots = this.roots.length > 0 ? this.roots : ["."];
|
|
11324
|
-
const
|
|
11325
|
-
|
|
11326
|
-
this.context.stderr.write(`${warn}
|
|
11327
|
-
`);
|
|
11328
|
-
}
|
|
11329
|
-
const extensions = composeScanExtensions({
|
|
11542
|
+
const outcome = await runScanForCommand({
|
|
11543
|
+
roots,
|
|
11330
11544
|
noBuiltIns: this.noBuiltIns,
|
|
11331
|
-
|
|
11545
|
+
noPlugins: this.noPlugins,
|
|
11546
|
+
noTokens: this.noTokens,
|
|
11547
|
+
dryRun: this.dryRun,
|
|
11548
|
+
changed: this.changed,
|
|
11549
|
+
allowEmpty: this.allowEmpty,
|
|
11550
|
+
strict: this.strict,
|
|
11551
|
+
stderr: this.context.stderr
|
|
11332
11552
|
});
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
} catch (err) {
|
|
11344
|
-
const message = formatErrorMessage(err);
|
|
11345
|
-
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message }));
|
|
11553
|
+
return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
|
|
11554
|
+
}
|
|
11555
|
+
/**
|
|
11556
|
+
* `--watch` is a thin alias for the `sm watch` verb. Combining
|
|
11557
|
+
* `--watch` with one-shot-only flags is incoherent — the watcher
|
|
11558
|
+
* always persists incrementally over the prior snapshot.
|
|
11559
|
+
*/
|
|
11560
|
+
async runWatchAlias() {
|
|
11561
|
+
if (this.noBuiltIns || this.dryRun || this.changed || this.allowEmpty) {
|
|
11562
|
+
this.context.stderr.write(SCAN_TEXTS.watchCannotCombine);
|
|
11346
11563
|
return ExitCode.Error;
|
|
11347
11564
|
}
|
|
11348
|
-
|
|
11349
|
-
const
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
}
|
|
11365
|
-
return loaded;
|
|
11366
|
-
};
|
|
11367
|
-
const runScanWith = async (prior, priorExtractorRuns) => {
|
|
11368
|
-
if (this.changed && prior === null) {
|
|
11369
|
-
this.context.stderr.write(SCAN_TEXTS.changedNoPriorWarning);
|
|
11370
|
-
}
|
|
11371
|
-
const runOptions = {
|
|
11372
|
-
roots,
|
|
11373
|
-
// `--global` for `sm scan` lands in Step 6 (config + onboarding).
|
|
11374
|
-
// The orchestrator already accepts the scope override; the CLI
|
|
11375
|
-
// surface defaults to `'project'` until the flag is wired.
|
|
11376
|
-
scope: "project",
|
|
11377
|
-
tokenize: !this.noTokens,
|
|
11378
|
-
ignoreFilter,
|
|
11379
|
-
strict,
|
|
11380
|
-
emitter: createCliProgressEmitter(this.context.stderr)
|
|
11381
|
-
};
|
|
11382
|
-
if (extensions) runOptions.extensions = extensions;
|
|
11383
|
-
if (prior) {
|
|
11384
|
-
runOptions.priorSnapshot = prior;
|
|
11385
|
-
runOptions.enableCache = this.changed;
|
|
11386
|
-
}
|
|
11387
|
-
if (priorExtractorRuns) runOptions.priorExtractorRuns = priorExtractorRuns;
|
|
11388
|
-
return await runScanWithRenames(kernel, runOptions);
|
|
11389
|
-
};
|
|
11390
|
-
const willPersist = !this.noBuiltIns && !this.dryRun;
|
|
11391
|
-
let result;
|
|
11392
|
-
let renameOps;
|
|
11393
|
-
let persistedTo = null;
|
|
11394
|
-
let outcome;
|
|
11395
|
-
if (willPersist) {
|
|
11396
|
-
try {
|
|
11397
|
-
outcome = await withSqlite({ databasePath: dbPath }, async (adapter) => {
|
|
11398
|
-
const prior = await loadPrior(adapter);
|
|
11399
|
-
const priorExtractorRuns = this.changed && prior ? await adapter.scans.loadExtractorRuns() : void 0;
|
|
11400
|
-
let scanned;
|
|
11401
|
-
try {
|
|
11402
|
-
scanned = await runScanWith(prior, priorExtractorRuns);
|
|
11403
|
-
} catch (err) {
|
|
11404
|
-
const message = formatErrorMessage(err);
|
|
11405
|
-
return { kind: "scan-error", message };
|
|
11406
|
-
}
|
|
11407
|
-
if (scanned.result.stats.nodesCount === 0 && !this.allowEmpty) {
|
|
11408
|
-
const counts = await adapter.scans.countRows();
|
|
11409
|
-
const existing = counts.nodes + counts.links + counts.issues;
|
|
11410
|
-
if (existing > 0) return { kind: "guard", existing };
|
|
11411
|
-
}
|
|
11412
|
-
await adapter.scans.persist(scanned.result, {
|
|
11413
|
-
renameOps: scanned.renameOps,
|
|
11414
|
-
extractorRuns: scanned.extractorRuns,
|
|
11415
|
-
enrichments: scanned.enrichments
|
|
11416
|
-
});
|
|
11417
|
-
return { kind: "ok", ...scanned };
|
|
11418
|
-
});
|
|
11419
|
-
} catch (err) {
|
|
11420
|
-
const message = formatErrorMessage(err);
|
|
11421
|
-
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message }));
|
|
11422
|
-
return ExitCode.Error;
|
|
11423
|
-
}
|
|
11424
|
-
if (outcome.kind === "scan-error") {
|
|
11425
|
-
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message: outcome.message }));
|
|
11426
|
-
return ExitCode.Error;
|
|
11427
|
-
}
|
|
11428
|
-
if (outcome.kind === "guard") {
|
|
11429
|
-
this.context.stderr.write(tx(SCAN_TEXTS.guardWipeRefused, { existing: outcome.existing }));
|
|
11430
|
-
return ExitCode.Error;
|
|
11431
|
-
}
|
|
11432
|
-
result = outcome.result;
|
|
11433
|
-
renameOps = outcome.renameOps;
|
|
11434
|
-
persistedTo = dbPath;
|
|
11435
|
-
} else {
|
|
11436
|
-
let prior;
|
|
11437
|
-
try {
|
|
11438
|
-
prior = this.noBuiltIns ? null : await tryWithSqlite(
|
|
11439
|
-
{ databasePath: dbPath, autoBackup: false },
|
|
11440
|
-
loadPrior
|
|
11441
|
-
);
|
|
11442
|
-
} catch (err) {
|
|
11443
|
-
const message = formatErrorMessage(err);
|
|
11444
|
-
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message }));
|
|
11445
|
-
return ExitCode.Error;
|
|
11446
|
-
}
|
|
11447
|
-
try {
|
|
11448
|
-
const scanned = await runScanWith(prior);
|
|
11449
|
-
result = scanned.result;
|
|
11450
|
-
renameOps = scanned.renameOps;
|
|
11451
|
-
} catch (err) {
|
|
11452
|
-
const message = formatErrorMessage(err);
|
|
11453
|
-
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message }));
|
|
11454
|
-
return ExitCode.Error;
|
|
11455
|
-
}
|
|
11565
|
+
this.emitElapsed = false;
|
|
11566
|
+
const roots = this.roots.length > 0 ? this.roots : ["."];
|
|
11567
|
+
return runWatchLoop({
|
|
11568
|
+
roots,
|
|
11569
|
+
json: this.json,
|
|
11570
|
+
noTokens: this.noTokens,
|
|
11571
|
+
strict: this.strict,
|
|
11572
|
+
noPlugins: this.noPlugins,
|
|
11573
|
+
context: this.context
|
|
11574
|
+
});
|
|
11575
|
+
}
|
|
11576
|
+
/** Render the failure branch of `IScanRunResult` to stderr. */
|
|
11577
|
+
renderFailure(outcome) {
|
|
11578
|
+
if (outcome.kind === "guard-trip") {
|
|
11579
|
+
this.context.stderr.write(tx(SCAN_TEXTS.guardWipeRefused, { existing: outcome.existing }));
|
|
11580
|
+
return ExitCode.Error;
|
|
11456
11581
|
}
|
|
11582
|
+
this.context.stderr.write(tx(SCAN_TEXTS.scanFailure, { message: outcome.message }));
|
|
11583
|
+
return ExitCode.Error;
|
|
11584
|
+
}
|
|
11585
|
+
/**
|
|
11586
|
+
* Render the successful outcome to stdout (JSON or human) and compute
|
|
11587
|
+
* the exit code. Exit 1 only when at least one issue is at `error`
|
|
11588
|
+
* severity (mirrors `sm check`, per spec § Exit codes).
|
|
11589
|
+
*/
|
|
11590
|
+
renderOutcome(result, persistedTo, dbPath, strict) {
|
|
11457
11591
|
const exitCode2 = result.issues.some((i) => i.severity === "error") ? ExitCode.Issues : ExitCode.Ok;
|
|
11458
11592
|
if (this.json) {
|
|
11459
11593
|
if (strict) {
|
|
@@ -11494,10 +11628,10 @@ var ScanCommand = class extends Command16 {
|
|
|
11494
11628
|
|
|
11495
11629
|
// cli/commands/scan-compare.ts
|
|
11496
11630
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
|
|
11497
|
-
import { Command as
|
|
11498
|
-
var ScanCompareCommand = class extends
|
|
11631
|
+
import { Command as Command18, Option as Option18 } from "clipanion";
|
|
11632
|
+
var ScanCompareCommand = class extends SmCommand {
|
|
11499
11633
|
static paths = [["scan", "compare-with"]];
|
|
11500
|
-
static usage =
|
|
11634
|
+
static usage = Command18.Usage({
|
|
11501
11635
|
category: "Scan",
|
|
11502
11636
|
description: "Run a fresh scan in memory and emit a delta against the saved ScanResult dump at <dump>. Read-only.",
|
|
11503
11637
|
details: `
|
|
@@ -11525,18 +11659,15 @@ var ScanCompareCommand = class extends Command17 {
|
|
|
11525
11659
|
["JSON output for tooling", "$0 scan compare-with baseline.json --json"]
|
|
11526
11660
|
]
|
|
11527
11661
|
});
|
|
11528
|
-
dump =
|
|
11529
|
-
roots =
|
|
11530
|
-
|
|
11531
|
-
description: "Emit the IScanDelta document as JSON on stdout."
|
|
11532
|
-
});
|
|
11533
|
-
noTokens = Option17.Boolean("--no-tokens", false, {
|
|
11662
|
+
dump = Option18.String({ required: true });
|
|
11663
|
+
roots = Option18.Rest({ name: "roots" });
|
|
11664
|
+
noTokens = Option18.Boolean("--no-tokens", false, {
|
|
11534
11665
|
description: "Skip per-node token counts during the fresh scan."
|
|
11535
11666
|
});
|
|
11536
|
-
strict =
|
|
11667
|
+
strict = Option18.Boolean("--strict", false, {
|
|
11537
11668
|
description: "Promote layered-config warnings and frontmatter-validation findings from warn to error."
|
|
11538
11669
|
});
|
|
11539
|
-
noPlugins =
|
|
11670
|
+
noPlugins = Option18.Boolean("--no-plugins", false, {
|
|
11540
11671
|
description: "Skip drop-in plugin discovery."
|
|
11541
11672
|
});
|
|
11542
11673
|
// Cyclomatic count comes from CLI ergonomics: 3 distinct try/catch
|
|
@@ -11544,7 +11675,7 @@ var ScanCompareCommand = class extends Command17 {
|
|
|
11544
11675
|
// for the JSON branch. The pure pieces already live in
|
|
11545
11676
|
// `loadAndValidateDump` and `computeScanDelta`.
|
|
11546
11677
|
// eslint-disable-next-line complexity
|
|
11547
|
-
async
|
|
11678
|
+
async run() {
|
|
11548
11679
|
const ctx = defaultRuntimeContext();
|
|
11549
11680
|
const roots = this.roots.length > 0 ? this.roots : ["."];
|
|
11550
11681
|
let prior;
|
|
@@ -11719,55 +11850,1768 @@ function renderDeltaIssues(issues) {
|
|
|
11719
11850
|
return lines;
|
|
11720
11851
|
}
|
|
11721
11852
|
|
|
11722
|
-
// cli/commands/
|
|
11723
|
-
import {
|
|
11853
|
+
// cli/commands/serve.ts
|
|
11854
|
+
import { spawn } from "child_process";
|
|
11855
|
+
import { existsSync as existsSync18 } from "fs";
|
|
11856
|
+
import { Command as Command19, Option as Option19 } from "clipanion";
|
|
11724
11857
|
|
|
11725
|
-
//
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
// --- renderHuman labels ------------------------------------------------
|
|
11729
|
-
sectionFrontmatter: "Frontmatter:",
|
|
11730
|
-
sectionLinksOut: "Links out",
|
|
11731
|
-
sectionLinksIn: "Links in",
|
|
11732
|
-
sectionIssues: "Issues",
|
|
11733
|
-
placeholderNone: " (none)",
|
|
11734
|
-
sectionHeader: "{{label}} ({{count}}, {{unique}} unique):",
|
|
11735
|
-
issuesHeader: "Issues ({{count}}):",
|
|
11736
|
-
issueRow: " - [{{severity}}] {{ruleId}}: {{message}}",
|
|
11737
|
-
// --- formatGroupedLink ------------------------------------------------
|
|
11738
|
-
/**
|
|
11739
|
-
* Bullet line for one grouped link in the in/out lists. `{{kind}}` and
|
|
11740
|
-
* `{{endpoint}}` are pre-sanitized by the caller; `{{dup}}` is the
|
|
11741
|
-
* `(×N)` count when the row collapses multiple identical edges, empty
|
|
11742
|
-
* otherwise; `{{sources}}` is the trailing ` sources: a, b` segment
|
|
11743
|
-
* (empty when the link has no sources).
|
|
11744
|
-
*/
|
|
11745
|
-
groupedLinkHead: " - [{{kind}}/{{confidence}}] {{arrow}} {{endpoint}}{{dup}}{{sources}}",
|
|
11746
|
-
groupedLinkDup: " (\xD7{{count}})",
|
|
11747
|
-
groupedLinkSources: " sources: {{values}}",
|
|
11748
|
-
// --- renderNodeHeader labels ------------------------------------------
|
|
11749
|
-
nodeIdentity: "{{path}} [{{kind}}] (provider: {{provider}})",
|
|
11750
|
-
nodeFieldTitle: "title: {{value}}",
|
|
11751
|
-
nodeFieldDescription: "description: {{value}}",
|
|
11752
|
-
nodeFieldStability: "stability: {{value}}",
|
|
11753
|
-
nodeFieldVersion: "version: {{value}}",
|
|
11754
|
-
nodeFieldAuthor: "author: {{value}}",
|
|
11755
|
-
nodeWeight: "Weight: bytes {{total}} total / {{frontmatter}} frontmatter / {{body}} body",
|
|
11756
|
-
nodeTokens: " tokens {{total}} total / {{frontmatter}} frontmatter / {{body}} body",
|
|
11757
|
-
nodeExternalRefs: "External refs: {{count}}"
|
|
11758
|
-
};
|
|
11858
|
+
// server/index.ts
|
|
11859
|
+
import { serve } from "@hono/node-server";
|
|
11860
|
+
import { WebSocketServer } from "ws";
|
|
11759
11861
|
|
|
11760
|
-
//
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11862
|
+
// server/app.ts
|
|
11863
|
+
import { Hono } from "hono";
|
|
11864
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
11865
|
+
|
|
11866
|
+
// server/routes/config.ts
|
|
11867
|
+
import { HTTPException } from "hono/http-exception";
|
|
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
|
+
|
|
12036
|
+
// server/health.ts
|
|
12037
|
+
import { existsSync as existsSync15 } from "fs";
|
|
12038
|
+
var FALLBACK_SCHEMA_VERSION = "1";
|
|
12039
|
+
function buildHealth(deps) {
|
|
12040
|
+
return {
|
|
12041
|
+
ok: true,
|
|
12042
|
+
schemaVersion: FALLBACK_SCHEMA_VERSION,
|
|
12043
|
+
specVersion: deps.specVersion,
|
|
12044
|
+
implVersion: VERSION,
|
|
12045
|
+
scope: deps.scope,
|
|
12046
|
+
db: existsSync15(deps.dbPath) ? "present" : "missing"
|
|
12047
|
+
};
|
|
12048
|
+
}
|
|
12049
|
+
async function resolveSpecVersion2() {
|
|
12050
|
+
try {
|
|
12051
|
+
const mod = await import("@skill-map/spec", { with: { type: "json" } });
|
|
12052
|
+
const version = mod.default?.specPackageVersion;
|
|
12053
|
+
return version ?? "unknown";
|
|
12054
|
+
} catch {
|
|
12055
|
+
return "unknown";
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
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
|
+
|
|
12485
|
+
// server/static.ts
|
|
12486
|
+
import { existsSync as existsSync16 } from "fs";
|
|
12487
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
12488
|
+
import { extname, join as join13 } from "path";
|
|
12489
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
12490
|
+
var INDEX_HTML = "index.html";
|
|
12491
|
+
var PLACEHOLDER_HTML = `<!doctype html>
|
|
12492
|
+
<html lang="en">
|
|
12493
|
+
<head>
|
|
12494
|
+
<meta charset="utf-8" />
|
|
12495
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12496
|
+
<meta name="skill-map-mode" content="live" />
|
|
12497
|
+
<title>skill-map server</title>
|
|
12498
|
+
<style>
|
|
12499
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 36rem; line-height: 1.5; }
|
|
12500
|
+
code { background: #f4f4f4; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
12501
|
+
h1 { font-size: 1.4rem; }
|
|
12502
|
+
</style>
|
|
12503
|
+
</head>
|
|
12504
|
+
<body>
|
|
12505
|
+
<h1>skill-map server is running</h1>
|
|
12506
|
+
<p>The UI bundle was not found. Run <code>npm run build --workspace=ui</code> from the repo root, or pass <code>--ui-dist <path></code>, then restart <code>sm serve</code>.</p>
|
|
12507
|
+
<p>The REST API is available at <code>/api/health</code>.</p>
|
|
12508
|
+
</body>
|
|
12509
|
+
</html>
|
|
12510
|
+
`;
|
|
12511
|
+
function createStaticHandler(uiDist) {
|
|
12512
|
+
if (uiDist === null) return placeholderRootMiddleware();
|
|
12513
|
+
return serveStatic({ root: uiDist });
|
|
12514
|
+
}
|
|
12515
|
+
function createSpaFallback(uiDist) {
|
|
12516
|
+
return async (c, _next) => {
|
|
12517
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
|
|
12518
|
+
if (uiDist === null) return htmlResponse(c, PLACEHOLDER_HTML);
|
|
12519
|
+
const indexPath = join13(uiDist, INDEX_HTML);
|
|
12520
|
+
if (!existsSync16(indexPath)) return htmlResponse(c, PLACEHOLDER_HTML);
|
|
12521
|
+
return fileResponse(c, indexPath);
|
|
12522
|
+
};
|
|
12523
|
+
}
|
|
12524
|
+
function placeholderRootMiddleware() {
|
|
12525
|
+
return async (c, next) => {
|
|
12526
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") return next();
|
|
12527
|
+
if (c.req.path === "/" || c.req.path === "/index.html") {
|
|
12528
|
+
return htmlResponse(c, PLACEHOLDER_HTML);
|
|
12529
|
+
}
|
|
12530
|
+
return next();
|
|
12531
|
+
};
|
|
12532
|
+
}
|
|
12533
|
+
var MIME_TYPES = {
|
|
12534
|
+
".html": "text/html; charset=UTF-8",
|
|
12535
|
+
".js": "application/javascript; charset=UTF-8",
|
|
12536
|
+
".mjs": "application/javascript; charset=UTF-8",
|
|
12537
|
+
".css": "text/css; charset=UTF-8",
|
|
12538
|
+
".json": "application/json; charset=UTF-8",
|
|
12539
|
+
".svg": "image/svg+xml",
|
|
12540
|
+
".png": "image/png",
|
|
12541
|
+
".jpg": "image/jpeg",
|
|
12542
|
+
".jpeg": "image/jpeg",
|
|
12543
|
+
".gif": "image/gif",
|
|
12544
|
+
".webp": "image/webp",
|
|
12545
|
+
".ico": "image/x-icon",
|
|
12546
|
+
".woff": "font/woff",
|
|
12547
|
+
".woff2": "font/woff2",
|
|
12548
|
+
".ttf": "font/ttf",
|
|
12549
|
+
".otf": "font/otf",
|
|
12550
|
+
".txt": "text/plain; charset=UTF-8",
|
|
12551
|
+
".map": "application/json; charset=UTF-8"
|
|
12552
|
+
};
|
|
12553
|
+
function mimeFor(filePath) {
|
|
12554
|
+
const ext = extname(filePath).toLowerCase();
|
|
12555
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
12556
|
+
}
|
|
12557
|
+
function htmlResponse(c, html) {
|
|
12558
|
+
return c.body(html, 200, { "content-type": "text/html; charset=UTF-8" });
|
|
12559
|
+
}
|
|
12560
|
+
async function fileResponse(c, absPath) {
|
|
12561
|
+
const buf = await readFile5(absPath);
|
|
12562
|
+
return c.body(buf, 200, { "content-type": mimeFor(absPath) });
|
|
12563
|
+
}
|
|
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
|
+
|
|
12603
|
+
// server/app.ts
|
|
12604
|
+
function createApp(deps) {
|
|
12605
|
+
const app = new Hono();
|
|
12606
|
+
if (deps.options.devCors) {
|
|
12607
|
+
app.use("*", async (c, next) => {
|
|
12608
|
+
await next();
|
|
12609
|
+
c.res.headers.set("access-control-allow-origin", "*");
|
|
12610
|
+
c.res.headers.set("access-control-allow-methods", "GET,POST,PUT,DELETE,OPTIONS");
|
|
12611
|
+
c.res.headers.set("access-control-allow-headers", "content-type, authorization");
|
|
12612
|
+
});
|
|
12613
|
+
app.options("*", (c) => c.body(null, 204));
|
|
12614
|
+
}
|
|
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);
|
|
12628
|
+
app.all("/api/*", (c) => {
|
|
12629
|
+
throw new HTTPException5(404, { message: `Unknown API endpoint: ${c.req.path}` });
|
|
12630
|
+
});
|
|
12631
|
+
attachBroadcasterRoute(app, deps.broadcaster);
|
|
12632
|
+
app.use("*", createStaticHandler(deps.options.uiDist));
|
|
12633
|
+
app.get("*", createSpaFallback(deps.options.uiDist));
|
|
12634
|
+
app.notFound((c) => {
|
|
12635
|
+
throw new HTTPException5(404, { message: `Not found: ${c.req.path}` });
|
|
12636
|
+
});
|
|
12637
|
+
app.onError((err, c) => {
|
|
12638
|
+
return formatError2(err, c);
|
|
12639
|
+
});
|
|
12640
|
+
return app;
|
|
12641
|
+
}
|
|
12642
|
+
function codeForStatus(status) {
|
|
12643
|
+
if (status === 404) return "not-found";
|
|
12644
|
+
if (status === 400) return "bad-query";
|
|
12645
|
+
return "internal";
|
|
12646
|
+
}
|
|
12647
|
+
function formatError2(err, c) {
|
|
12648
|
+
if (err instanceof HTTPException5) {
|
|
12649
|
+
const status = err.status;
|
|
12650
|
+
const envelope2 = {
|
|
12651
|
+
ok: false,
|
|
12652
|
+
error: {
|
|
12653
|
+
code: codeForStatus(status),
|
|
12654
|
+
message: err.message,
|
|
12655
|
+
details: null
|
|
12656
|
+
}
|
|
12657
|
+
};
|
|
12658
|
+
return c.json(envelope2, status);
|
|
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
|
+
}
|
|
12671
|
+
const envelope = {
|
|
12672
|
+
ok: false,
|
|
12673
|
+
error: {
|
|
12674
|
+
code: "internal",
|
|
12675
|
+
message: formatErrorMessage(err),
|
|
12676
|
+
details: null
|
|
12677
|
+
}
|
|
12678
|
+
};
|
|
12679
|
+
return c.json(envelope, 500);
|
|
12680
|
+
}
|
|
12681
|
+
|
|
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 {
|
|
12705
|
+
}
|
|
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 })
|
|
13004
|
+
);
|
|
13005
|
+
}
|
|
13006
|
+
|
|
13007
|
+
// server/options.ts
|
|
13008
|
+
var DEFAULT_PORT = 4242;
|
|
13009
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
13010
|
+
var DEFAULT_SCOPE = "project";
|
|
13011
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "0:0:0:0:0:0:0:1"]);
|
|
13012
|
+
function isLoopbackHost(host) {
|
|
13013
|
+
return LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
13014
|
+
}
|
|
13015
|
+
function validateServerOptions(input) {
|
|
13016
|
+
const filled = applyDefaults(input);
|
|
13017
|
+
const portError = validatePort(filled.port);
|
|
13018
|
+
if (portError !== null) return { ok: false, error: portError };
|
|
13019
|
+
const scopeError = validateScope(filled.scope);
|
|
13020
|
+
if (scopeError !== null) return { ok: false, error: scopeError };
|
|
13021
|
+
const hostError = validateHost(filled.host, filled.devCors);
|
|
13022
|
+
if (hostError !== null) return { ok: false, error: hostError };
|
|
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
|
|
13038
|
+
};
|
|
13039
|
+
if (input.watcherDebounceMs !== void 0) {
|
|
13040
|
+
options.watcherDebounceMs = input.watcherDebounceMs;
|
|
13041
|
+
}
|
|
13042
|
+
return { ok: true, options };
|
|
13043
|
+
}
|
|
13044
|
+
function applyDefaults(input) {
|
|
13045
|
+
return {
|
|
13046
|
+
port: input.port ?? DEFAULT_PORT,
|
|
13047
|
+
host: input.host ?? DEFAULT_HOST,
|
|
13048
|
+
scope: input.scope ?? DEFAULT_SCOPE,
|
|
13049
|
+
uiDist: input.uiDist ?? null,
|
|
13050
|
+
noBuiltIns: input.noBuiltIns ?? false,
|
|
13051
|
+
noPlugins: input.noPlugins ?? false,
|
|
13052
|
+
open: input.open ?? true,
|
|
13053
|
+
devCors: input.devCors ?? false,
|
|
13054
|
+
noWatcher: input.noWatcher ?? false
|
|
13055
|
+
};
|
|
13056
|
+
}
|
|
13057
|
+
function validatePort(port) {
|
|
13058
|
+
if (!Number.isInteger(port)) {
|
|
13059
|
+
return { code: "port-invalid", message: `port must be an integer (got ${port})`, value: String(port) };
|
|
13060
|
+
}
|
|
13061
|
+
if (port < 0 || port > 65535) {
|
|
13062
|
+
return {
|
|
13063
|
+
code: "port-out-of-range",
|
|
13064
|
+
message: `port must be in [0, 65535] (got ${port})`,
|
|
13065
|
+
value: String(port)
|
|
13066
|
+
};
|
|
13067
|
+
}
|
|
13068
|
+
return null;
|
|
13069
|
+
}
|
|
13070
|
+
function validateScope(scope) {
|
|
13071
|
+
if (scope !== "project" && scope !== "global") {
|
|
13072
|
+
return { code: "scope-invalid", message: `scope must be "project" or "global"`, value: String(scope) };
|
|
13073
|
+
}
|
|
13074
|
+
return null;
|
|
13075
|
+
}
|
|
13076
|
+
function validateHost(host, devCors) {
|
|
13077
|
+
if (devCors && !isLoopbackHost(host)) {
|
|
13078
|
+
return {
|
|
13079
|
+
code: "host-dev-cors-rejected",
|
|
13080
|
+
message: `--dev-cors requires a loopback --host (got ${host})`,
|
|
13081
|
+
value: host
|
|
13082
|
+
};
|
|
13083
|
+
}
|
|
13084
|
+
return null;
|
|
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
|
+
}
|
|
13108
|
+
|
|
13109
|
+
// server/paths.ts
|
|
13110
|
+
import { existsSync as existsSync17, statSync as statSync5 } from "fs";
|
|
13111
|
+
import { dirname as dirname9, isAbsolute as isAbsolute5, join as join14, resolve as resolve19 } from "path";
|
|
13112
|
+
var DEFAULT_UI_REL = join14("ui", "dist", "browser");
|
|
13113
|
+
var INDEX_HTML2 = "index.html";
|
|
13114
|
+
function resolveDefaultUiDist(ctx) {
|
|
13115
|
+
let current = resolve19(ctx.cwd);
|
|
13116
|
+
for (let i = 0; i < 64; i++) {
|
|
13117
|
+
const candidate = join14(current, DEFAULT_UI_REL);
|
|
13118
|
+
if (isUiBundleDir(candidate)) return candidate;
|
|
13119
|
+
const parent = dirname9(current);
|
|
13120
|
+
if (parent === current) return null;
|
|
13121
|
+
current = parent;
|
|
13122
|
+
}
|
|
13123
|
+
return null;
|
|
13124
|
+
}
|
|
13125
|
+
function resolveExplicitUiDist(ctx, raw) {
|
|
13126
|
+
return isAbsolute5(raw) ? raw : resolve19(ctx.cwd, raw);
|
|
13127
|
+
}
|
|
13128
|
+
function isUiBundleDir(path) {
|
|
13129
|
+
if (!existsSync17(path)) return false;
|
|
13130
|
+
try {
|
|
13131
|
+
if (!statSync5(path).isDirectory()) return false;
|
|
13132
|
+
return existsSync17(join14(path, INDEX_HTML2));
|
|
13133
|
+
} catch {
|
|
13134
|
+
return false;
|
|
13135
|
+
}
|
|
13136
|
+
}
|
|
13137
|
+
|
|
13138
|
+
// server/index.ts
|
|
13139
|
+
async function createServer(options, extra = {}) {
|
|
13140
|
+
const specVersion = await resolveSpecVersion2();
|
|
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
|
+
});
|
|
13151
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
13152
|
+
const server = await listenAsync(app.fetch, wss, options.host, options.port);
|
|
13153
|
+
const addr = server.address();
|
|
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
|
+
}
|
|
13181
|
+
let closed = false;
|
|
13182
|
+
const close = async () => {
|
|
13183
|
+
if (closed) return;
|
|
13184
|
+
closed = true;
|
|
13185
|
+
if (watcherService) {
|
|
13186
|
+
try {
|
|
13187
|
+
await watcherService.stop();
|
|
13188
|
+
} catch {
|
|
13189
|
+
}
|
|
13190
|
+
}
|
|
13191
|
+
broadcaster.shutdown();
|
|
13192
|
+
await closeServer(server);
|
|
13193
|
+
wss.close();
|
|
13194
|
+
};
|
|
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 ?? []);
|
|
13207
|
+
}
|
|
13208
|
+
function listenAsync(fetchCallback, wss, host, port) {
|
|
13209
|
+
return new Promise((resolveListen, rejectListen) => {
|
|
13210
|
+
let settled = false;
|
|
13211
|
+
const server = serve(
|
|
13212
|
+
{
|
|
13213
|
+
fetch: fetchCallback,
|
|
13214
|
+
hostname: host,
|
|
13215
|
+
port,
|
|
13216
|
+
websocket: { server: wss }
|
|
13217
|
+
},
|
|
13218
|
+
() => {
|
|
13219
|
+
if (settled) return;
|
|
13220
|
+
settled = true;
|
|
13221
|
+
server.removeListener("error", onBindError);
|
|
13222
|
+
resolveListen(server);
|
|
13223
|
+
}
|
|
13224
|
+
);
|
|
13225
|
+
const onBindError = (err) => {
|
|
13226
|
+
if (settled) return;
|
|
13227
|
+
settled = true;
|
|
13228
|
+
rejectListen(err);
|
|
13229
|
+
};
|
|
13230
|
+
server.once("error", onBindError);
|
|
13231
|
+
});
|
|
13232
|
+
}
|
|
13233
|
+
function closeServer(server) {
|
|
13234
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
13235
|
+
server.close((err) => {
|
|
13236
|
+
if (err) {
|
|
13237
|
+
rejectClose(err);
|
|
13238
|
+
} else {
|
|
13239
|
+
resolveClose();
|
|
13240
|
+
}
|
|
13241
|
+
});
|
|
13242
|
+
server.closeAllConnections?.();
|
|
13243
|
+
});
|
|
13244
|
+
}
|
|
13245
|
+
function normalizeAddress(addr, fallbackHost, fallbackPort) {
|
|
13246
|
+
if (addr === null || typeof addr === "string") {
|
|
13247
|
+
return { host: fallbackHost, port: fallbackPort, family: "IPv4" };
|
|
13248
|
+
}
|
|
13249
|
+
return { host: addr.address, port: addr.port, family: addr.family };
|
|
13250
|
+
}
|
|
13251
|
+
|
|
13252
|
+
// cli/i18n/serve.texts.ts
|
|
13253
|
+
var SERVE_TEXTS = {
|
|
13254
|
+
// Banner emitted to stderr after the listener binds. Mirrors `sm watch`
|
|
13255
|
+
// by writing operational status to stderr (stdout is reserved for
|
|
13256
|
+
// future `--json` boot payloads).
|
|
13257
|
+
boot: "sm serve: listening on http://{{host}}:{{port}} (scope={{scope}}, db={{db}})\n",
|
|
13258
|
+
// Hint shown after the boot line. Branches on --open: when auto-open
|
|
13259
|
+
// is on (default), the message states intent ("opening …"); when
|
|
13260
|
+
// --no-open, it instructs the user to visit the URL manually.
|
|
13261
|
+
// Both end with the Ctrl+C reminder so the operational tail is
|
|
13262
|
+
// identical regardless of branch.
|
|
13263
|
+
bootOpening: "sm serve: opening http://{{host}}:{{port}}/ in your browser. Press Ctrl+C to stop.\n",
|
|
13264
|
+
bootVisitHint: "sm serve: visit http://{{host}}:{{port}}/ in your browser. Press Ctrl+C to stop.\n",
|
|
13265
|
+
// Browser-open failure. Non-fatal — the URL is already printed; the
|
|
13266
|
+
// user can open it manually.
|
|
13267
|
+
openFailed: "sm serve: could not auto-open browser ({{message}}). Visit {{url}} manually.\n",
|
|
13268
|
+
// Bind failure (port in use, EACCES, etc.) → ExitCode.Error.
|
|
13269
|
+
bindFailed: "sm serve: failed to bind {{host}}:{{port}} \u2014 {{message}}\n",
|
|
13270
|
+
// Flag-validation failures — ExitCode.Error.
|
|
13271
|
+
hostDevCorsRejected: "sm serve: --dev-cors requires a loopback --host (got {{host}}). Refusing per Decision #119.\n",
|
|
13272
|
+
portOutOfRange: "sm serve: --port must be an integer in [0, 65535] (got {{value}}).\n",
|
|
13273
|
+
portInvalid: "sm serve: --port must be a non-negative integer (got {{value}}).\n",
|
|
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",
|
|
13278
|
+
// Generic operational error — surfaced when the server itself throws
|
|
13279
|
+
// before the listener binds (e.g. UI bundle missing under explicit
|
|
13280
|
+
// --ui-dist).
|
|
13281
|
+
startupFailed: "sm serve: startup failed \u2014 {{message}}\n",
|
|
13282
|
+
// DB-not-found (--db <path> doesn't exist) → ExitCode.NotFound.
|
|
13283
|
+
dbNotFound: "sm serve: --db {{path}} does not exist.\n",
|
|
13284
|
+
// Shutdown trace — printed once the listener has closed.
|
|
13285
|
+
shutdown: "sm serve: shutdown complete.\n"
|
|
13286
|
+
};
|
|
13287
|
+
|
|
13288
|
+
// cli/commands/serve.ts
|
|
13289
|
+
var ServeCommand = class extends SmCommand {
|
|
13290
|
+
static paths = [["serve"]];
|
|
13291
|
+
static usage = Command19.Usage({
|
|
13292
|
+
category: "Setup",
|
|
13293
|
+
description: "Start the Hono BFF (single-port: REST + WebSocket + SPA bundle).",
|
|
13294
|
+
details: `
|
|
13295
|
+
Boots the skill-map Web UI's backing server. One Node process
|
|
13296
|
+
serves the Angular SPA, the REST API under /api/*, and the
|
|
13297
|
+
WebSocket at /ws \u2014 single-port mandate, no proxy.
|
|
13298
|
+
|
|
13299
|
+
Default port is 4242, default host is 127.0.0.1. The server boots
|
|
13300
|
+
even when the project DB is missing \u2014 /api/health reports
|
|
13301
|
+
'db: missing' so the SPA renders an empty-state CTA instead of
|
|
13302
|
+
failing the connection.
|
|
13303
|
+
|
|
13304
|
+
Loopback-only assumption through v0.6.0 (no per-connection auth on
|
|
13305
|
+
/ws). Combining --dev-cors with a non-loopback --host is rejected.
|
|
13306
|
+
|
|
13307
|
+
SIGINT / SIGTERM trigger a graceful shutdown.
|
|
13308
|
+
`,
|
|
13309
|
+
examples: [
|
|
13310
|
+
["Start on the default port and open the browser", "$0 serve"],
|
|
13311
|
+
["Custom port, no browser auto-open", "$0 serve --port 5000 --no-open"],
|
|
13312
|
+
["Use the global scope DB", "$0 serve --scope global"],
|
|
13313
|
+
["Point at a pre-built UI bundle", "$0 serve --ui-dist ./ui/dist/browser"]
|
|
13314
|
+
]
|
|
13315
|
+
});
|
|
13316
|
+
port = Option19.String("--port", {
|
|
13317
|
+
required: false,
|
|
13318
|
+
description: "Listening port (default 4242). 0 = OS-assigned."
|
|
13319
|
+
});
|
|
13320
|
+
host = Option19.String("--host", {
|
|
13321
|
+
required: false,
|
|
13322
|
+
description: "Listening host (default 127.0.0.1). Loopback-only enforced when --dev-cors is set."
|
|
13323
|
+
});
|
|
13324
|
+
scope = Option19.String("--scope", {
|
|
13325
|
+
required: false,
|
|
13326
|
+
description: "project | global. Alias for -g/--global. Default: project."
|
|
13327
|
+
});
|
|
13328
|
+
noBuiltIns = Option19.Boolean("--no-built-ins", false, {
|
|
13329
|
+
description: "Skip built-in plugin registration (parity with sm scan --no-built-ins)."
|
|
13330
|
+
});
|
|
13331
|
+
noPlugins = Option19.Boolean("--no-plugins", false, {
|
|
13332
|
+
description: "Skip drop-in plugin discovery."
|
|
13333
|
+
});
|
|
13334
|
+
// `Option.Boolean('--open', true)` — Clipanion's parser auto-derives
|
|
13335
|
+
// the `--no-open` inverse for every boolean flag (search for
|
|
13336
|
+
// `--no-${name.slice(2)}` in clipanion's core), so the explicit
|
|
13337
|
+
// `--no-open` descriptor must NOT be declared here or the parser sees
|
|
13338
|
+
// two registrations for the same flag and rejects the invocation
|
|
13339
|
+
// with "Ambiguous Syntax Error". Same convention shipped by every
|
|
13340
|
+
// other `--no-...` flag in the CLI tree.
|
|
13341
|
+
open = Option19.Boolean("--open", true, {
|
|
13342
|
+
description: "Auto-open the SPA in the user's default browser after listen. --no-open opts out."
|
|
13343
|
+
});
|
|
13344
|
+
devCors = Option19.Boolean("--dev-cors", false, {
|
|
13345
|
+
description: "Enable permissive CORS for the Angular dev-server proxy workflow."
|
|
13346
|
+
});
|
|
13347
|
+
// `--ui-dist` is intentionally undocumented in the Usage block above
|
|
13348
|
+
// (the demo build pipeline + tests rely on it; everyday users never
|
|
13349
|
+
// need it). Clipanion still exposes it on the parser; the Usage
|
|
13350
|
+
// omission is the "hidden" contract per the 14.1 brief.
|
|
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 });
|
|
13359
|
+
// Long-running daemon — `done in <…>` after a graceful shutdown is
|
|
13360
|
+
// noise. Mirrors `sm watch`'s opt-out.
|
|
13361
|
+
emitElapsed = false;
|
|
13362
|
+
// CLI orchestrator with multi-flag handling — each `if (this.flag)`
|
|
13363
|
+
// branch is one cyclomatic point. Splitting per branch scatters the
|
|
13364
|
+
// validation away from the flag it gates. Per AGENTS.md §Linting
|
|
13365
|
+
// category 1 ("CLI orchestrators with multi-flag handling").
|
|
13366
|
+
// eslint-disable-next-line complexity
|
|
13367
|
+
async run() {
|
|
13368
|
+
const runtimeCtx = defaultRuntimeContext();
|
|
13369
|
+
const scopeResult = resolveScope(this.scope, this.global);
|
|
13370
|
+
if (!scopeResult.ok) {
|
|
13371
|
+
this.context.stderr.write(
|
|
13372
|
+
tx(SERVE_TEXTS.scopeInvalid, { value: sanitizeForTerminal(scopeResult.value) })
|
|
13373
|
+
);
|
|
13374
|
+
return ExitCode.Error;
|
|
13375
|
+
}
|
|
13376
|
+
const scope = scopeResult.scope;
|
|
13377
|
+
if (scope === "global") this.global = true;
|
|
13378
|
+
const portResult = parsePort(this.port);
|
|
13379
|
+
if (!portResult.ok) {
|
|
13380
|
+
this.context.stderr.write(
|
|
13381
|
+
tx(SERVE_TEXTS.portInvalid, { value: sanitizeForTerminal(portResult.value) })
|
|
13382
|
+
);
|
|
13383
|
+
return ExitCode.Error;
|
|
13384
|
+
}
|
|
13385
|
+
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...runtimeCtx });
|
|
13386
|
+
if (this.db !== void 0 && !existsSync18(dbPath)) {
|
|
13387
|
+
this.context.stderr.write(
|
|
13388
|
+
tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
|
|
13389
|
+
);
|
|
13390
|
+
return ExitCode.NotFound;
|
|
13391
|
+
}
|
|
13392
|
+
const uiDistResult = resolveUiDist(runtimeCtx, this.uiDist);
|
|
13393
|
+
if (!uiDistResult.ok) {
|
|
13394
|
+
this.context.stderr.write(
|
|
13395
|
+
tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(uiDistResult.message) })
|
|
13396
|
+
);
|
|
13397
|
+
return ExitCode.Error;
|
|
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
|
+
}
|
|
13408
|
+
const input = {
|
|
13409
|
+
dbPath,
|
|
13410
|
+
scope,
|
|
13411
|
+
uiDist: uiDistResult.uiDist,
|
|
13412
|
+
noBuiltIns: this.noBuiltIns,
|
|
13413
|
+
noPlugins: this.noPlugins,
|
|
13414
|
+
open: this.open,
|
|
13415
|
+
devCors: this.devCors,
|
|
13416
|
+
noWatcher: this.noWatcher
|
|
13417
|
+
};
|
|
13418
|
+
if (portResult.port !== void 0) input.port = portResult.port;
|
|
13419
|
+
if (this.host !== void 0) input.host = this.host;
|
|
13420
|
+
if (debounceResult.value !== void 0) input.watcherDebounceMs = debounceResult.value;
|
|
13421
|
+
const validation = validateServerOptions(input);
|
|
13422
|
+
if (!validation.ok) {
|
|
13423
|
+
this.context.stderr.write(formatValidationError(validation.error));
|
|
13424
|
+
return ExitCode.Error;
|
|
13425
|
+
}
|
|
13426
|
+
let handle;
|
|
13427
|
+
try {
|
|
13428
|
+
handle = await createServer(validation.options);
|
|
13429
|
+
} catch (err) {
|
|
13430
|
+
const message = formatErrorMessage(err);
|
|
13431
|
+
this.context.stderr.write(
|
|
13432
|
+
tx(SERVE_TEXTS.bindFailed, {
|
|
13433
|
+
host: sanitizeForTerminal(validation.options.host),
|
|
13434
|
+
port: validation.options.port,
|
|
13435
|
+
message: sanitizeForTerminal(message)
|
|
13436
|
+
})
|
|
13437
|
+
);
|
|
13438
|
+
return ExitCode.Error;
|
|
13439
|
+
}
|
|
13440
|
+
this.context.stderr.write(
|
|
13441
|
+
tx(SERVE_TEXTS.boot, {
|
|
13442
|
+
host: sanitizeForTerminal(handle.address.host),
|
|
13443
|
+
port: handle.address.port,
|
|
13444
|
+
scope,
|
|
13445
|
+
db: sanitizeForTerminal(dbPath)
|
|
13446
|
+
})
|
|
13447
|
+
);
|
|
13448
|
+
this.context.stderr.write(
|
|
13449
|
+
tx(validation.options.open ? SERVE_TEXTS.bootOpening : SERVE_TEXTS.bootVisitHint, {
|
|
13450
|
+
host: sanitizeForTerminal(handle.address.host),
|
|
13451
|
+
port: handle.address.port
|
|
13452
|
+
})
|
|
13453
|
+
);
|
|
13454
|
+
if (validation.options.open) {
|
|
13455
|
+
const url = `http://${handle.address.host}:${handle.address.port}/`;
|
|
13456
|
+
tryOpenBrowser(url, this.context.stderr);
|
|
13457
|
+
}
|
|
13458
|
+
await waitForShutdown();
|
|
13459
|
+
await handle.close();
|
|
13460
|
+
this.context.stderr.write(SERVE_TEXTS.shutdown);
|
|
13461
|
+
return ExitCode.Ok;
|
|
13462
|
+
}
|
|
13463
|
+
};
|
|
13464
|
+
function parsePort(raw) {
|
|
13465
|
+
if (raw === void 0) return { ok: true, port: void 0 };
|
|
13466
|
+
const trimmed = raw.trim();
|
|
13467
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
13468
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
|
|
13469
|
+
return { ok: false, value: raw };
|
|
13470
|
+
}
|
|
13471
|
+
return { ok: true, port: parsed };
|
|
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
|
+
}
|
|
13482
|
+
function resolveScope(rawScope, global) {
|
|
13483
|
+
if (rawScope === void 0) return { ok: true, scope: global ? "global" : "project" };
|
|
13484
|
+
if (rawScope === "project" || rawScope === "global") {
|
|
13485
|
+
return { ok: true, scope: rawScope };
|
|
13486
|
+
}
|
|
13487
|
+
return { ok: false, value: rawScope };
|
|
13488
|
+
}
|
|
13489
|
+
function resolveUiDist(ctx, raw) {
|
|
13490
|
+
if (raw === void 0) {
|
|
13491
|
+
return { ok: true, uiDist: resolveDefaultUiDist(ctx) };
|
|
13492
|
+
}
|
|
13493
|
+
const abs = resolveExplicitUiDist(ctx, raw);
|
|
13494
|
+
if (!isUiBundleDir(abs)) {
|
|
13495
|
+
return {
|
|
13496
|
+
ok: false,
|
|
13497
|
+
message: `--ui-dist ${abs} does not exist or is not a directory containing index.html`
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
13500
|
+
return { ok: true, uiDist: abs };
|
|
13501
|
+
}
|
|
13502
|
+
function formatValidationError(err) {
|
|
13503
|
+
switch (err.code) {
|
|
13504
|
+
case "host-dev-cors-rejected":
|
|
13505
|
+
return tx(SERVE_TEXTS.hostDevCorsRejected, { host: sanitizeForTerminal(err.value) });
|
|
13506
|
+
case "port-out-of-range":
|
|
13507
|
+
return tx(SERVE_TEXTS.portOutOfRange, { value: sanitizeForTerminal(err.value) });
|
|
13508
|
+
case "port-invalid":
|
|
13509
|
+
return tx(SERVE_TEXTS.portInvalid, { value: sanitizeForTerminal(err.value) });
|
|
13510
|
+
case "scope-invalid":
|
|
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) });
|
|
13516
|
+
default:
|
|
13517
|
+
return tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(err.message) });
|
|
13518
|
+
}
|
|
13519
|
+
}
|
|
13520
|
+
function waitForShutdown() {
|
|
13521
|
+
return new Promise((resolveShutdown) => {
|
|
13522
|
+
const onSignal = () => {
|
|
13523
|
+
process.removeListener("SIGINT", onSignal);
|
|
13524
|
+
process.removeListener("SIGTERM", onSignal);
|
|
13525
|
+
resolveShutdown();
|
|
13526
|
+
};
|
|
13527
|
+
process.once("SIGINT", onSignal);
|
|
13528
|
+
process.once("SIGTERM", onSignal);
|
|
13529
|
+
});
|
|
13530
|
+
}
|
|
13531
|
+
function tryOpenBrowser(url, stderr) {
|
|
13532
|
+
try {
|
|
13533
|
+
const platform = process.platform;
|
|
13534
|
+
let command;
|
|
13535
|
+
let args2;
|
|
13536
|
+
if (platform === "darwin") {
|
|
13537
|
+
command = "open";
|
|
13538
|
+
args2 = [url];
|
|
13539
|
+
} else if (platform === "win32") {
|
|
13540
|
+
command = "cmd";
|
|
13541
|
+
args2 = ["/c", "start", '""', url];
|
|
13542
|
+
} else {
|
|
13543
|
+
command = "xdg-open";
|
|
13544
|
+
args2 = [url];
|
|
13545
|
+
}
|
|
13546
|
+
const child = spawn(command, args2, { detached: true, stdio: "ignore" });
|
|
13547
|
+
child.on("error", (err) => {
|
|
13548
|
+
stderr.write(
|
|
13549
|
+
tx(SERVE_TEXTS.openFailed, {
|
|
13550
|
+
message: sanitizeForTerminal(formatErrorMessage(err)),
|
|
13551
|
+
url: sanitizeForTerminal(url)
|
|
13552
|
+
})
|
|
13553
|
+
);
|
|
13554
|
+
});
|
|
13555
|
+
child.unref();
|
|
13556
|
+
} catch (err) {
|
|
13557
|
+
stderr.write(
|
|
13558
|
+
tx(SERVE_TEXTS.openFailed, {
|
|
13559
|
+
message: sanitizeForTerminal(formatErrorMessage(err)),
|
|
13560
|
+
url: sanitizeForTerminal(url)
|
|
13561
|
+
})
|
|
13562
|
+
);
|
|
13563
|
+
}
|
|
13564
|
+
}
|
|
13565
|
+
|
|
13566
|
+
// cli/commands/show.ts
|
|
13567
|
+
import { Command as Command20, Option as Option20 } from "clipanion";
|
|
13568
|
+
|
|
13569
|
+
// cli/i18n/show.texts.ts
|
|
13570
|
+
var SHOW_TEXTS = {
|
|
13571
|
+
nodeNotFound: "Node not found: {{nodePath}}\n",
|
|
13572
|
+
// --- renderHuman labels ------------------------------------------------
|
|
13573
|
+
sectionFrontmatter: "Frontmatter:",
|
|
13574
|
+
sectionLinksOut: "Links out",
|
|
13575
|
+
sectionLinksIn: "Links in",
|
|
13576
|
+
sectionIssues: "Issues",
|
|
13577
|
+
placeholderNone: " (none)",
|
|
13578
|
+
sectionHeader: "{{label}} ({{count}}, {{unique}} unique):",
|
|
13579
|
+
issuesHeader: "Issues ({{count}}):",
|
|
13580
|
+
issueRow: " - [{{severity}}] {{ruleId}}: {{message}}",
|
|
13581
|
+
// --- formatGroupedLink ------------------------------------------------
|
|
13582
|
+
/**
|
|
13583
|
+
* Bullet line for one grouped link in the in/out lists. `{{kind}}` and
|
|
13584
|
+
* `{{endpoint}}` are pre-sanitized by the caller; `{{dup}}` is the
|
|
13585
|
+
* `(×N)` count when the row collapses multiple identical edges, empty
|
|
13586
|
+
* otherwise; `{{sources}}` is the trailing ` sources: a, b` segment
|
|
13587
|
+
* (empty when the link has no sources).
|
|
13588
|
+
*/
|
|
13589
|
+
groupedLinkHead: " - [{{kind}}/{{confidence}}] {{arrow}} {{endpoint}}{{dup}}{{sources}}",
|
|
13590
|
+
groupedLinkDup: " (\xD7{{count}})",
|
|
13591
|
+
groupedLinkSources: " sources: {{values}}",
|
|
13592
|
+
// --- renderNodeHeader labels ------------------------------------------
|
|
13593
|
+
nodeIdentity: "{{path}} [{{kind}}] (provider: {{provider}})",
|
|
13594
|
+
nodeFieldTitle: "title: {{value}}",
|
|
13595
|
+
nodeFieldDescription: "description: {{value}}",
|
|
13596
|
+
nodeFieldStability: "stability: {{value}}",
|
|
13597
|
+
nodeFieldVersion: "version: {{value}}",
|
|
13598
|
+
nodeFieldAuthor: "author: {{value}}",
|
|
13599
|
+
nodeWeight: "Weight: bytes {{total}} total / {{frontmatter}} frontmatter / {{body}} body",
|
|
13600
|
+
nodeTokens: " tokens {{total}} total / {{frontmatter}} frontmatter / {{body}} body",
|
|
13601
|
+
nodeExternalRefs: "External refs: {{count}}"
|
|
13602
|
+
};
|
|
13603
|
+
|
|
13604
|
+
// cli/commands/show.ts
|
|
13605
|
+
var ShowCommand = class extends SmCommand {
|
|
13606
|
+
static paths = [["show"]];
|
|
13607
|
+
static usage = Command20.Usage({
|
|
13608
|
+
category: "Browse",
|
|
13609
|
+
description: "Node detail: weight, frontmatter, links, issues.",
|
|
13610
|
+
details: `
|
|
11767
13611
|
Loads a single node from the persisted snapshot, plus every link
|
|
11768
|
-
(in and out) and every current issue touching it.
|
|
11769
|
-
|
|
11770
|
-
|
|
13612
|
+
(in and out) and every current issue touching it. Step 10
|
|
13613
|
+
(findings) and Step 11 (summary) will add fields when their
|
|
13614
|
+
backing tables ship.
|
|
11771
13615
|
|
|
11772
13616
|
Run \`sm scan\` first to populate the DB.
|
|
11773
13617
|
`,
|
|
@@ -11776,11 +13620,8 @@ var ShowCommand = class extends Command18 {
|
|
|
11776
13620
|
["Machine-readable detail", "$0 show .claude/agents/architect.md --json"]
|
|
11777
13621
|
]
|
|
11778
13622
|
});
|
|
11779
|
-
nodePath =
|
|
11780
|
-
|
|
11781
|
-
db = Option18.String("--db", { required: false });
|
|
11782
|
-
json = Option18.Boolean("--json", false);
|
|
11783
|
-
async execute() {
|
|
13623
|
+
nodePath = Option20.String({ required: true });
|
|
13624
|
+
async run() {
|
|
11784
13625
|
const dbPath = resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
|
|
11785
13626
|
if (!assertDbExists(dbPath, this.context.stderr)) return ExitCode.NotFound;
|
|
11786
13627
|
return withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
@@ -11793,9 +13634,7 @@ var ShowCommand = class extends Command18 {
|
|
|
11793
13634
|
node: bundle.node,
|
|
11794
13635
|
linksOut: bundle.linksOut,
|
|
11795
13636
|
linksIn: bundle.linksIn,
|
|
11796
|
-
issues: bundle.issues
|
|
11797
|
-
findings: [],
|
|
11798
|
-
summary: null
|
|
13637
|
+
issues: bundle.issues
|
|
11799
13638
|
};
|
|
11800
13639
|
if (this.json) {
|
|
11801
13640
|
this.context.stdout.write(JSON.stringify(doc) + "\n");
|
|
@@ -11930,7 +13769,7 @@ function rankConfidenceForGrouping(c) {
|
|
|
11930
13769
|
}
|
|
11931
13770
|
|
|
11932
13771
|
// cli/commands/stubs.ts
|
|
11933
|
-
import { Command as
|
|
13772
|
+
import { Command as Command21, Option as Option21 } from "clipanion";
|
|
11934
13773
|
|
|
11935
13774
|
// cli/i18n/stubs.texts.ts
|
|
11936
13775
|
var STUBS_TEXTS = {
|
|
@@ -11945,9 +13784,9 @@ function notImplemented(cmd, verb) {
|
|
|
11945
13784
|
cmd.context.stderr.write(tx(STUBS_TEXTS.notImplemented, { verb }));
|
|
11946
13785
|
return ExitCode.Error;
|
|
11947
13786
|
}
|
|
11948
|
-
var DoctorCommand = class extends
|
|
13787
|
+
var DoctorCommand = class extends Command21 {
|
|
11949
13788
|
static paths = [["doctor"]];
|
|
11950
|
-
static usage =
|
|
13789
|
+
static usage = Command21.Usage({
|
|
11951
13790
|
category: "Setup",
|
|
11952
13791
|
description: planned("Diagnostic report: DB integrity, pending migrations, orphan rows, plugin status, runner availability.")
|
|
11953
13792
|
});
|
|
@@ -11955,23 +13794,23 @@ var DoctorCommand = class extends Command19 {
|
|
|
11955
13794
|
return notImplemented(this, "doctor");
|
|
11956
13795
|
}
|
|
11957
13796
|
};
|
|
11958
|
-
var FindingsCommand = class extends
|
|
13797
|
+
var FindingsCommand = class extends Command21 {
|
|
11959
13798
|
static paths = [["findings"]];
|
|
11960
|
-
static usage =
|
|
13799
|
+
static usage = Command21.Usage({
|
|
11961
13800
|
category: "Browse",
|
|
11962
13801
|
description: planned("Probabilistic findings: injection, stale summaries, low confidence.")
|
|
11963
13802
|
});
|
|
11964
|
-
kind =
|
|
11965
|
-
since =
|
|
11966
|
-
threshold =
|
|
11967
|
-
json =
|
|
13803
|
+
kind = Option21.String("--kind", { required: false });
|
|
13804
|
+
since = Option21.String("--since", { required: false });
|
|
13805
|
+
threshold = Option21.String("--threshold", { required: false });
|
|
13806
|
+
json = Option21.Boolean("--json", false);
|
|
11968
13807
|
async execute() {
|
|
11969
13808
|
return notImplemented(this, "findings");
|
|
11970
13809
|
}
|
|
11971
13810
|
};
|
|
11972
|
-
var ActionsListCommand = class extends
|
|
13811
|
+
var ActionsListCommand = class extends Command21 {
|
|
11973
13812
|
static paths = [["actions", "list"]];
|
|
11974
|
-
static usage =
|
|
13813
|
+
static usage = Command21.Usage({
|
|
11975
13814
|
category: "Jobs",
|
|
11976
13815
|
description: planned("Registered action types (manifest view).")
|
|
11977
13816
|
});
|
|
@@ -11979,138 +13818,125 @@ var ActionsListCommand = class extends Command19 {
|
|
|
11979
13818
|
return notImplemented(this, "actions list");
|
|
11980
13819
|
}
|
|
11981
13820
|
};
|
|
11982
|
-
var ActionsShowCommand = class extends
|
|
13821
|
+
var ActionsShowCommand = class extends Command21 {
|
|
11983
13822
|
static paths = [["actions", "show"]];
|
|
11984
|
-
static usage =
|
|
13823
|
+
static usage = Command21.Usage({
|
|
11985
13824
|
category: "Jobs",
|
|
11986
13825
|
description: planned("Full action manifest, including preconditions and expected duration.")
|
|
11987
13826
|
});
|
|
11988
|
-
id =
|
|
13827
|
+
id = Option21.String({ required: true });
|
|
11989
13828
|
async execute() {
|
|
11990
13829
|
return notImplemented(this, "actions show");
|
|
11991
13830
|
}
|
|
11992
13831
|
};
|
|
11993
|
-
var JobSubmitCommand = class extends
|
|
13832
|
+
var JobSubmitCommand = class extends Command21 {
|
|
11994
13833
|
static paths = [["job", "submit"]];
|
|
11995
|
-
static usage =
|
|
13834
|
+
static usage = Command21.Usage({
|
|
11996
13835
|
category: "Jobs",
|
|
11997
13836
|
description: planned("Enqueue a single job or fan out to every matching node (--all).")
|
|
11998
13837
|
});
|
|
11999
|
-
action =
|
|
12000
|
-
node =
|
|
12001
|
-
all =
|
|
12002
|
-
run =
|
|
12003
|
-
force =
|
|
12004
|
-
ttl =
|
|
12005
|
-
priority =
|
|
13838
|
+
action = Option21.String({ required: true });
|
|
13839
|
+
node = Option21.String("-n", { required: false });
|
|
13840
|
+
all = Option21.Boolean("--all", false);
|
|
13841
|
+
run = Option21.Boolean("--run", false);
|
|
13842
|
+
force = Option21.Boolean("--force", false);
|
|
13843
|
+
ttl = Option21.String("--ttl", { required: false });
|
|
13844
|
+
priority = Option21.String("--priority", { required: false });
|
|
12006
13845
|
async execute() {
|
|
12007
13846
|
return notImplemented(this, "job submit");
|
|
12008
13847
|
}
|
|
12009
13848
|
};
|
|
12010
|
-
var JobListCommand = class extends
|
|
13849
|
+
var JobListCommand = class extends Command21 {
|
|
12011
13850
|
static paths = [["job", "list"]];
|
|
12012
|
-
static usage =
|
|
12013
|
-
status =
|
|
12014
|
-
action =
|
|
12015
|
-
node =
|
|
13851
|
+
static usage = Command21.Usage({ category: "Jobs", description: planned("List jobs.") });
|
|
13852
|
+
status = Option21.String("--status", { required: false });
|
|
13853
|
+
action = Option21.String("--action", { required: false });
|
|
13854
|
+
node = Option21.String("--node", { required: false });
|
|
12016
13855
|
async execute() {
|
|
12017
13856
|
return notImplemented(this, "job list");
|
|
12018
13857
|
}
|
|
12019
13858
|
};
|
|
12020
|
-
var JobShowCommand = class extends
|
|
13859
|
+
var JobShowCommand = class extends Command21 {
|
|
12021
13860
|
static paths = [["job", "show"]];
|
|
12022
|
-
static usage =
|
|
12023
|
-
id =
|
|
13861
|
+
static usage = Command21.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
|
|
13862
|
+
id = Option21.String({ required: true });
|
|
12024
13863
|
async execute() {
|
|
12025
13864
|
return notImplemented(this, "job show");
|
|
12026
13865
|
}
|
|
12027
13866
|
};
|
|
12028
|
-
var JobPreviewCommand = class extends
|
|
13867
|
+
var JobPreviewCommand = class extends Command21 {
|
|
12029
13868
|
static paths = [["job", "preview"]];
|
|
12030
|
-
static usage =
|
|
12031
|
-
id =
|
|
13869
|
+
static usage = Command21.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
|
|
13870
|
+
id = Option21.String({ required: true });
|
|
12032
13871
|
async execute() {
|
|
12033
13872
|
return notImplemented(this, "job preview");
|
|
12034
13873
|
}
|
|
12035
13874
|
};
|
|
12036
|
-
var JobClaimCommand = class extends
|
|
13875
|
+
var JobClaimCommand = class extends Command21 {
|
|
12037
13876
|
static paths = [["job", "claim"]];
|
|
12038
|
-
static usage =
|
|
13877
|
+
static usage = Command21.Usage({
|
|
12039
13878
|
category: "Jobs",
|
|
12040
13879
|
description: planned("Atomic primitive: return next queued job id, mark it running.")
|
|
12041
13880
|
});
|
|
12042
|
-
filter =
|
|
13881
|
+
filter = Option21.String("--filter", { required: false });
|
|
12043
13882
|
async execute() {
|
|
12044
13883
|
return notImplemented(this, "job claim");
|
|
12045
13884
|
}
|
|
12046
13885
|
};
|
|
12047
|
-
var JobRunCommand = class extends
|
|
13886
|
+
var JobRunCommand = class extends Command21 {
|
|
12048
13887
|
static paths = [["job", "run"]];
|
|
12049
|
-
static usage =
|
|
13888
|
+
static usage = Command21.Usage({
|
|
12050
13889
|
category: "Jobs",
|
|
12051
13890
|
description: planned("Full CLI-runner loop: claim + spawn + record.")
|
|
12052
13891
|
});
|
|
12053
|
-
all =
|
|
12054
|
-
max =
|
|
13892
|
+
all = Option21.Boolean("--all", false);
|
|
13893
|
+
max = Option21.String("--max", { required: false });
|
|
12055
13894
|
async execute() {
|
|
12056
13895
|
return notImplemented(this, "job run");
|
|
12057
13896
|
}
|
|
12058
13897
|
};
|
|
12059
|
-
var JobStatusCommand = class extends
|
|
13898
|
+
var JobStatusCommand = class extends Command21 {
|
|
12060
13899
|
static paths = [["job", "status"]];
|
|
12061
|
-
static usage =
|
|
13900
|
+
static usage = Command21.Usage({
|
|
12062
13901
|
category: "Jobs",
|
|
12063
13902
|
description: planned("Counts (per status) or single-job status.")
|
|
12064
13903
|
});
|
|
12065
|
-
id =
|
|
13904
|
+
id = Option21.String({ required: false });
|
|
12066
13905
|
async execute() {
|
|
12067
13906
|
return notImplemented(this, "job status");
|
|
12068
13907
|
}
|
|
12069
13908
|
};
|
|
12070
|
-
var JobCancelCommand = class extends
|
|
13909
|
+
var JobCancelCommand = class extends Command21 {
|
|
12071
13910
|
static paths = [["job", "cancel"]];
|
|
12072
|
-
static usage =
|
|
13911
|
+
static usage = Command21.Usage({
|
|
12073
13912
|
category: "Jobs",
|
|
12074
13913
|
description: planned("Force a running job to failed with reason user-cancelled.")
|
|
12075
13914
|
});
|
|
12076
|
-
id =
|
|
12077
|
-
all =
|
|
13915
|
+
id = Option21.String({ required: false });
|
|
13916
|
+
all = Option21.Boolean("--all", false);
|
|
12078
13917
|
async execute() {
|
|
12079
13918
|
return notImplemented(this, "job cancel");
|
|
12080
13919
|
}
|
|
12081
13920
|
};
|
|
12082
|
-
var RecordCommand = class extends
|
|
13921
|
+
var RecordCommand = class extends Command21 {
|
|
12083
13922
|
static paths = [["record"]];
|
|
12084
|
-
static usage =
|
|
13923
|
+
static usage = Command21.Usage({
|
|
12085
13924
|
category: "Jobs",
|
|
12086
13925
|
description: planned("Close a running job with success or failure. Nonce is the sole credential.")
|
|
12087
13926
|
});
|
|
12088
|
-
id =
|
|
12089
|
-
nonce =
|
|
12090
|
-
status =
|
|
12091
|
-
report =
|
|
12092
|
-
tokensIn =
|
|
12093
|
-
tokensOut =
|
|
12094
|
-
durationMs =
|
|
12095
|
-
model =
|
|
12096
|
-
error =
|
|
13927
|
+
id = Option21.String("--id", { required: true });
|
|
13928
|
+
nonce = Option21.String("--nonce", { required: true });
|
|
13929
|
+
status = Option21.String("--status", { required: true });
|
|
13930
|
+
report = Option21.String("--report", { required: false });
|
|
13931
|
+
tokensIn = Option21.String("--tokens-in", { required: false });
|
|
13932
|
+
tokensOut = Option21.String("--tokens-out", { required: false });
|
|
13933
|
+
durationMs = Option21.String("--duration-ms", { required: false });
|
|
13934
|
+
model = Option21.String("--model", { required: false });
|
|
13935
|
+
error = Option21.String("--error", { required: false });
|
|
12097
13936
|
async execute() {
|
|
12098
13937
|
return notImplemented(this, "record");
|
|
12099
13938
|
}
|
|
12100
13939
|
};
|
|
12101
|
-
var ServeCommand = class extends Command19 {
|
|
12102
|
-
static paths = [["serve"]];
|
|
12103
|
-
static usage = Command19.Usage({
|
|
12104
|
-
category: "Setup",
|
|
12105
|
-
description: planned("Start Hono + WebSocket for the Web UI. Single-port mandate: SPA + REST + WS under one listener.")
|
|
12106
|
-
});
|
|
12107
|
-
port = Option19.String("--port", { required: false });
|
|
12108
|
-
host = Option19.String("--host", { required: false });
|
|
12109
|
-
noOpen = Option19.Boolean("--no-open", false);
|
|
12110
|
-
async execute() {
|
|
12111
|
-
return notImplemented(this, "serve");
|
|
12112
|
-
}
|
|
12113
|
-
};
|
|
12114
13940
|
var STUB_COMMANDS = [
|
|
12115
13941
|
DoctorCommand,
|
|
12116
13942
|
FindingsCommand,
|
|
@@ -12124,12 +13950,11 @@ var STUB_COMMANDS = [
|
|
|
12124
13950
|
JobRunCommand,
|
|
12125
13951
|
JobStatusCommand,
|
|
12126
13952
|
JobCancelCommand,
|
|
12127
|
-
RecordCommand
|
|
12128
|
-
ServeCommand
|
|
13953
|
+
RecordCommand
|
|
12129
13954
|
];
|
|
12130
13955
|
|
|
12131
13956
|
// cli/commands/version.ts
|
|
12132
|
-
import { Command as
|
|
13957
|
+
import { Command as Command22 } from "clipanion";
|
|
12133
13958
|
|
|
12134
13959
|
// cli/i18n/version.texts.ts
|
|
12135
13960
|
var VERSION_TEXTS = {
|
|
@@ -12139,17 +13964,19 @@ var VERSION_TEXTS = {
|
|
|
12139
13964
|
};
|
|
12140
13965
|
|
|
12141
13966
|
// cli/commands/version.ts
|
|
12142
|
-
var VersionCommand = class extends
|
|
13967
|
+
var VersionCommand = class extends SmCommand {
|
|
12143
13968
|
static paths = [["version"]];
|
|
12144
|
-
static usage =
|
|
13969
|
+
static usage = Command22.Usage({
|
|
12145
13970
|
category: "Introspection",
|
|
12146
13971
|
description: "Print the CLI / kernel / spec / runtime / db-schema version matrix."
|
|
12147
13972
|
});
|
|
12148
|
-
|
|
12149
|
-
|
|
13973
|
+
// Informational verb — no `done in <…>` line; the version matrix is
|
|
13974
|
+
// the entire output.
|
|
13975
|
+
emitElapsed = false;
|
|
13976
|
+
async run() {
|
|
12150
13977
|
const runtime = `Node ${process.version}`;
|
|
12151
13978
|
const kernelVersion = VERSION;
|
|
12152
|
-
const specVersion = await
|
|
13979
|
+
const specVersion = await resolveSpecVersion3();
|
|
12153
13980
|
const dbSchema = await resolveDbSchemaVersion();
|
|
12154
13981
|
if (this.json) {
|
|
12155
13982
|
const payload = {
|
|
@@ -12175,7 +14002,7 @@ var VersionCommand = class extends Command20 {
|
|
|
12175
14002
|
return ExitCode.Ok;
|
|
12176
14003
|
}
|
|
12177
14004
|
};
|
|
12178
|
-
async function
|
|
14005
|
+
async function resolveSpecVersion3() {
|
|
12179
14006
|
try {
|
|
12180
14007
|
const mod = await import("@skill-map/spec", { with: { type: "json" } });
|
|
12181
14008
|
const version = mod.default?.specPackageVersion;
|
|
@@ -12211,6 +14038,7 @@ cli.register(HelpCommand);
|
|
|
12211
14038
|
cli.register(InitCommand);
|
|
12212
14039
|
cli.register(ScanCommand);
|
|
12213
14040
|
cli.register(ScanCompareCommand);
|
|
14041
|
+
cli.register(ServeCommand);
|
|
12214
14042
|
cli.register(WatchCommand);
|
|
12215
14043
|
cli.register(VersionCommand);
|
|
12216
14044
|
cli.register(ListCommand);
|