@skill-map/cli 0.31.0 → 0.32.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  // cli/entry.ts
2
- import { existsSync as existsSync30 } from "fs";
2
+ import { existsSync as existsSync31 } from "fs";
3
3
  import { Builtins, Cli as Cli2 } from "clipanion";
4
4
 
5
5
  // kernel/adapters/in-memory-progress.ts
@@ -2963,7 +2963,7 @@ var UPDATE_CHECK_TEXTS = {
2963
2963
  // package.json
2964
2964
  var package_default = {
2965
2965
  name: "@skill-map/cli",
2966
- version: "0.31.0",
2966
+ version: "0.32.0",
2967
2967
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2968
2968
  license: "MIT",
2969
2969
  type: "module",
@@ -3893,7 +3893,6 @@ var defaults_default = {
3893
3893
  watch: {
3894
3894
  debounceMs: 300
3895
3895
  },
3896
- extraFolders: [],
3897
3896
  referencePaths: []
3898
3897
  },
3899
3898
  plugins: {},
@@ -3919,7 +3918,6 @@ var defaults_default = {
3919
3918
  // kernel/config/loader.ts
3920
3919
  var PROJECT_LOCAL_ONLY_KEYS = /* @__PURE__ */ new Set([
3921
3920
  "allowEditSmFiles",
3922
- "scan.extraFolders",
3923
3921
  "scan.referencePaths"
3924
3922
  ]);
3925
3923
  var DEFAULTS = defaults_default;
@@ -4258,7 +4256,6 @@ function writeJsonAtomic(path, content) {
4258
4256
 
4259
4257
  // core/config/helper.ts
4260
4258
  var PRIVACY_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
4261
- "scan.extraFolders",
4262
4259
  "scan.referencePaths"
4263
4260
  ]);
4264
4261
  var ProjectLocalOnlyKeyError = class extends Error {
@@ -4330,19 +4327,17 @@ var ConfigValidationError = class extends Error {
4330
4327
  function projectPathExposure(inputs) {
4331
4328
  const empty = { expandsSurface: false, exposedPaths: [] };
4332
4329
  if (!PRIVACY_SENSITIVE_KEYS.has(inputs.key)) return empty;
4333
- if (inputs.key === "scan.extraFolders" || inputs.key === "scan.referencePaths") {
4334
- if (!Array.isArray(inputs.value)) return empty;
4335
- const before = readConfigValue(inputs.key, {
4336
- cwd: inputs.cwd,
4337
- default: []
4338
- }) ?? [];
4339
- const beforeSet = new Set(before);
4340
- const added = inputs.value.filter((entry) => typeof entry === "string").filter((entry) => !beforeSet.has(entry));
4341
- const exposed = added.map((entry) => resolveScanPathForExposure(entry, inputs.cwd)).filter((abs) => abs !== null && !isUnderProject(abs, inputs.cwd));
4342
- if (exposed.length === 0) return empty;
4343
- return { expandsSurface: true, exposedPaths: exposed };
4344
- }
4345
- return empty;
4330
+ if (inputs.key !== "scan.referencePaths") return empty;
4331
+ if (!Array.isArray(inputs.value)) return empty;
4332
+ const before = readConfigValue(inputs.key, {
4333
+ cwd: inputs.cwd,
4334
+ default: []
4335
+ }) ?? [];
4336
+ const beforeSet = new Set(before);
4337
+ const added = inputs.value.filter((entry) => typeof entry === "string").filter((entry) => !beforeSet.has(entry));
4338
+ const exposed = added.map((entry) => resolveScanPathForExposure(entry, inputs.cwd)).filter((abs) => abs !== null && !isUnderProject(abs, inputs.cwd));
4339
+ if (exposed.length === 0) return empty;
4340
+ return { expandsSurface: true, exposedPaths: exposed };
4346
4341
  }
4347
4342
  function resolveScanPathForExposure(raw, cwd) {
4348
4343
  if (raw.startsWith("~/")) return resolve6(join4(osHomedir(), raw.slice(2)));
@@ -4890,7 +4885,7 @@ var AsyncMutex = class {
4890
4885
  this.#locked = true;
4891
4886
  return;
4892
4887
  }
4893
- await new Promise((resolve37) => this.#waiters.push(resolve37));
4888
+ await new Promise((resolve38) => this.#waiters.push(resolve38));
4894
4889
  this.#locked = true;
4895
4890
  }
4896
4891
  unlock() {
@@ -8081,9 +8076,9 @@ function providerKindFailure(opts, status, fileName, errDescription) {
8081
8076
  }
8082
8077
  };
8083
8078
  }
8084
- function isDirectorySafe(path, statSync12) {
8079
+ function isDirectorySafe(path, statSync13) {
8085
8080
  try {
8086
- return statSync12(path).isDirectory();
8081
+ return statSync13(path).isDirectory();
8087
8082
  } catch {
8088
8083
  return false;
8089
8084
  }
@@ -9879,7 +9874,7 @@ var ConfigSetCommand = class extends SmCommand {
9879
9874
  key = Option4.String({ required: true });
9880
9875
  value = Option4.String({ required: true });
9881
9876
  yes = Option4.Boolean("--yes", false, {
9882
- description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.extraFolders / scan.referencePaths)."
9877
+ description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.referencePaths)."
9883
9878
  });
9884
9879
  // CLI orchestrator: each branch is one validation gate (forbidden
9885
9880
  // segment / privacy guard / schema violation) or output dispatch.
@@ -14346,12 +14341,6 @@ var SCAN_RUNNER_TEXTS = {
14346
14341
  * validation.
14347
14342
  */
14348
14343
  priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}. Run `sm db backup` then re-scan without --strict to rebuild from disk.",
14349
- /**
14350
- * Honest disclosure when the scan surface expanded beyond the cwd
14351
- * via `scan.extraFolders`. The list of paths makes it obvious which
14352
- * extra folders the operator just opted into.
14353
- */
14354
- includingExtraFoldersAdvisory: "Including extra folders: {{paths}}",
14355
14344
  /**
14356
14345
  * Reference-paths walker hit `REFERENCE_WALK_MAX_FILES` and stopped
14357
14346
  * early. The set may be incomplete for link validation; `core/broken-ref`
@@ -14366,6 +14355,14 @@ var SCAN_RUNNER_TEXTS = {
14366
14355
  referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.'
14367
14356
  };
14368
14357
 
14358
+ // core/runtime/scan-roots.ts
14359
+ function resolveScanRoots(inputs) {
14360
+ if (inputs.positionalRoots.length > 0) {
14361
+ return inputs.positionalRoots.slice();
14362
+ }
14363
+ return ["."];
14364
+ }
14365
+
14369
14366
  // core/runtime/reference-paths-walker.ts
14370
14367
  import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14371
14368
  import { homedir as osHomedir2 } from "os";
@@ -14427,26 +14424,6 @@ function safeStat(path) {
14427
14424
  }
14428
14425
  }
14429
14426
 
14430
- // core/runtime/scan-roots.ts
14431
- function resolveScanRoots(inputs) {
14432
- if (inputs.positionalRoots.length > 0) {
14433
- return {
14434
- roots: inputs.positionalRoots.slice(),
14435
- fromExtra: []
14436
- };
14437
- }
14438
- const cwdRoot = ".";
14439
- const extra = inputs.extraFolders.map((r) => resolveScanPath(r, inputs.cwd));
14440
- const seen = /* @__PURE__ */ new Set();
14441
- const out = [cwdRoot];
14442
- for (const root of extra) {
14443
- if (seen.has(root)) continue;
14444
- seen.add(root);
14445
- out.push(root);
14446
- }
14447
- return { roots: out, fromExtra: extra };
14448
- }
14449
-
14450
14427
  // core/runtime/scan-runner.ts
14451
14428
  async function runScanForCommand(opts) {
14452
14429
  const ctx = opts.ctx ?? defaultRuntimeContext();
@@ -14464,13 +14441,7 @@ async function runScanForCommand(opts) {
14464
14441
  const strict = opts.strict || cfg.scan.strict === true;
14465
14442
  let effectiveRoots;
14466
14443
  try {
14467
- const resolution = resolveScanRoots({
14468
- positionalRoots: opts.roots,
14469
- cwd: ctx.cwd,
14470
- extraFolders: cfg.scan.extraFolders
14471
- });
14472
- effectiveRoots = resolution.roots;
14473
- emitRootsAdvisory(resolution.fromExtra, opts);
14444
+ effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
14474
14445
  } catch (err) {
14475
14446
  return { kind: "config-error", message: formatErrorMessage(err) };
14476
14447
  }
@@ -14495,12 +14466,6 @@ async function runScanForCommand(opts) {
14495
14466
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
14496
14467
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
14497
14468
  }
14498
- function emitRootsAdvisory(fromExtra, opts) {
14499
- if (fromExtra.length === 0) return;
14500
- opts.printer.info(
14501
- tx(SCAN_RUNNER_TEXTS.includingExtraFoldersAdvisory, { paths: fromExtra.join(", ") }) + "\n"
14502
- );
14503
- }
14504
14469
  function emitReferenceWalkAdvisory(walk2, opts) {
14505
14470
  if (walk2.truncated) {
14506
14471
  opts.printer.warn(SCAN_RUNNER_TEXTS.referenceWalkTruncated);
@@ -18518,6 +18483,13 @@ function createWatcherRuntime(opts) {
18518
18483
  strict,
18519
18484
  emitter
18520
18485
  };
18486
+ if (cfg.scan.referencePaths.length > 0) {
18487
+ const walk2 = walkReferencePaths(cfg.scan.referencePaths, cwd);
18488
+ if (walk2.paths.size > 0) {
18489
+ runOptions.referenceablePaths = walk2.paths;
18490
+ runOptions.cwd = cwd;
18491
+ }
18492
+ }
18521
18493
  if (composed) runOptions.extensions = composed;
18522
18494
  if (priorState) {
18523
18495
  runOptions.priorSnapshot = priorState.snapshot;
@@ -18952,12 +18924,11 @@ var ScanCommand = class extends SmCommand {
18952
18924
  the prior snapshot from the DB, reuse unchanged nodes, and only
18953
18925
  reprocess new / modified files.
18954
18926
 
18955
- Scans honour scan.extraFolders (append extra dirs verbatim, the
18956
- only way to extend the scan beyond cwd) and scan.referencePaths
18957
- (walk the configured dirs for link-validation only; files there
18958
- are not indexed). Both are
18927
+ Scans honour scan.referencePaths (walk the configured dirs for
18928
+ link-validation only; files there are not indexed). The key is
18959
18929
  privacy-sensitive; see "sm config set --help" for the --yes
18960
- gate.
18930
+ gate. To extend the indexed scan beyond cwd, pass extra roots
18931
+ positionally.
18961
18932
  `,
18962
18933
  examples: [
18963
18934
  ["Scan the current directory", "$0 scan"],
@@ -19418,7 +19389,7 @@ function renderDeltaIssues(issues) {
19418
19389
 
19419
19390
  // cli/commands/serve.ts
19420
19391
  import { spawn as spawn2 } from "child_process";
19421
- import { existsSync as existsSync27 } from "fs";
19392
+ import { existsSync as existsSync28 } from "fs";
19422
19393
  import { Command as Command33, Option as Option31 } from "clipanion";
19423
19394
 
19424
19395
  // cli/util/browser-launch.ts
@@ -19440,7 +19411,7 @@ import { WebSocketServer } from "ws";
19440
19411
  // server/app.ts
19441
19412
  import { Hono } from "hono";
19442
19413
  import { bodyLimit } from "hono/body-limit";
19443
- import { HTTPException as HTTPException13 } from "hono/http-exception";
19414
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
19444
19415
 
19445
19416
  // core/config/service.ts
19446
19417
  var ConfigService = class {
@@ -19682,13 +19653,60 @@ var SERVER_TEXTS = {
19682
19653
  // silently widen the scan surface.
19683
19654
  projectPrefsBodyNotJson: "Request body must be valid JSON.",
19684
19655
  projectPrefsBodyNotObject: "Request body must be a JSON object.",
19685
- projectPrefsBodyEmpty: "Request body must contain a `scan` block with at least one of `extraFolders`, `referencePaths`.",
19656
+ projectPrefsBodyEmpty: "Request body must contain a `scan` block with `referencePaths`.",
19686
19657
  projectPrefsConfirmNotBoolean: "`confirm` must be a boolean.",
19687
- projectPrefsScanNotObject: '`scan` must be an object (e.g. `{"scan": {"extraFolders": ["~/.claude/agents"]}}`).',
19658
+ projectPrefsScanNotObject: '`scan` must be an object (e.g. `{"scan": {"referencePaths": ["~/Documents"]}}`).',
19688
19659
  projectPrefsListNotArray: "`{{key}}` must be an array of strings.",
19689
19660
  projectPrefsListEntryNotString: "`{{key}}` entries must be strings.",
19690
19661
  projectPrefsConfirmRequired: "This change opens disk access outside the project: {{paths}}. Re-issue the request with `confirm: true` to proceed.",
19691
19662
  projectPrefsPersistFailed: "Could not persist `{{key}}`: {{message}}",
19663
+ // Returned for every NEW entry that does not resolve to an existing
19664
+ // directory on disk. The list is comma-separated; pre-existing
19665
+ // entries are not re-validated.
19666
+ projectPrefsPathNotFound: "These folders do not exist on disk: {{paths}}. Add only paths that already exist.",
19667
+ // AJV `pattern` violation, an entry contains a comma. The UI rejects
19668
+ // comma input client-side; this message is the server-side safety
19669
+ // net (defense in depth).
19670
+ projectPrefsEntryHasComma: "Folder entries must not contain commas. Add one folder per entry.",
19671
+ // Server-stderr advisories emitted by `PATCH /api/project-preferences`
19672
+ // after a successful write. The operator running `sm serve` sees
19673
+ // each add / remove on the console without opening the config file.
19674
+ // `{{detail}}` is composed in JS (see `formatPathDetail`) so the
19675
+ // single template covers all three path shapes (home / relative /
19676
+ // absolute) without a template explosion.
19677
+ projectPrefsPathAdded: "project-prefs: + {{key}} {{detail}}",
19678
+ projectPrefsPathRemoved: "project-prefs: - {{key}} {{detail}}",
19679
+ // `PATCH /api/project-preferences` mutated `scan.*` and the
19680
+ // post-write `watcherService.restart()` call threw. The on-disk
19681
+ // write itself succeeded; the operator sees this advisory and
19682
+ // restarts the server to pick up the new root list manually.
19683
+ projectPrefsWatcherRestartFailed: "project-prefs: watcher restart after scan-config write failed ({{message}}). Restart `sm serve` to pick up the new roots.",
19684
+ // ---- project-ignore route (routes/project-ignore.ts) -------------------
19685
+ //
19686
+ // GET / PATCH /api/project-ignore. Backing is the project-root
19687
+ // `.skillmapignore` file (gitignore-syntax). Comments and blank
19688
+ // lines are preserved on write; only the active pattern list is
19689
+ // exchanged over the wire. No privacy gate, the patterns narrow the
19690
+ // scan surface and never widen disk access.
19691
+ projectIgnoreBodyNotJson: "Request body must be valid JSON.",
19692
+ projectIgnoreBodyNotObject: "Request body must be a JSON object.",
19693
+ projectIgnoreBodyEmpty: "Request body must contain a `patterns` array.",
19694
+ projectIgnoreListNotArray: "`patterns` must be an array of strings.",
19695
+ projectIgnoreEntryNotString: "`patterns` entries must be strings.",
19696
+ // AJV `minLength: 1` on each pattern after the route trims server-side;
19697
+ // surfaces when the operator sends `" "` or an empty string.
19698
+ projectIgnorePatternEmpty: "Pattern entries must not be empty or whitespace-only.",
19699
+ // AJV `pattern` violation: a single pattern carried a newline,
19700
+ // carriage return, or other ASCII control character. The UI rejects
19701
+ // these client-side; this message is the server-side safety net.
19702
+ projectIgnorePatternHasControlChar: "Pattern entries must be a single line without control characters.",
19703
+ // Duplicate detection runs after trim; the UI rejects duplicates
19704
+ // client-side, this is the server-side safety net.
19705
+ projectIgnorePatternDuplicate: 'Duplicate pattern: "{{pattern}}". Each pattern must be unique.',
19706
+ projectIgnorePersistFailed: "Could not persist `.skillmapignore`: {{message}}",
19707
+ projectIgnorePatternAdded: "project-ignore: + {{pattern}}",
19708
+ projectIgnorePatternRemoved: "project-ignore: - {{pattern}}",
19709
+ projectIgnoreWatcherRestartFailed: "project-ignore: watcher restart after `.skillmapignore` write failed ({{message}}). Restart `sm serve` to pick up the new filter.",
19692
19710
  // A connected client's outbound buffer exceeded the backpressure
19693
19711
  // threshold. The broadcaster closes the client with code 1009 and
19694
19712
  // unregisters it. Logged so operators can spot a wedged consumer.
@@ -20922,26 +20940,203 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
20922
20940
  }
20923
20941
  });
20924
20942
 
20925
- // server/routes/project-preferences.ts
20943
+ // server/routes/project-ignore.ts
20926
20944
  import { HTTPException as HTTPException10 } from "hono/http-exception";
20927
- function registerProjectPreferencesRoute(app, deps) {
20928
- app.get("/api/project-preferences", (c) => {
20945
+
20946
+ // server/util/skillmapignore-io.ts
20947
+ import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
20948
+ import { resolve as resolve33 } from "path";
20949
+ var IGNORE_FILENAME2 = ".skillmapignore";
20950
+ function readPatterns(cwd) {
20951
+ const path = resolve33(cwd, IGNORE_FILENAME2);
20952
+ if (!existsSync25(path)) return [];
20953
+ let raw;
20954
+ try {
20955
+ raw = readFileSync19(path, "utf8");
20956
+ } catch {
20957
+ return [];
20958
+ }
20959
+ return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
20960
+ }
20961
+ function writePatterns(cwd, nextPatterns) {
20962
+ const path = resolve33(cwd, IGNORE_FILENAME2);
20963
+ const prior = existsSync25(path) ? safeRead(path) : "";
20964
+ const content = buildContent(prior, nextPatterns);
20965
+ writeFileSync4(path, content, "utf8");
20966
+ }
20967
+ function safeRead(path) {
20968
+ try {
20969
+ return readFileSync19(path, "utf8");
20970
+ } catch {
20971
+ return "";
20972
+ }
20973
+ }
20974
+ function buildContent(prior, nextPatterns) {
20975
+ const wanted = new Set(nextPatterns);
20976
+ const kept = /* @__PURE__ */ new Set();
20977
+ const outLines = [];
20978
+ for (const line of splitLines(prior)) {
20979
+ pushPriorLine(line, wanted, kept, outLines);
20980
+ }
20981
+ appendNewPatterns(nextPatterns, kept, outLines);
20982
+ return outLines.length === 0 ? "" : outLines.join("\n") + "\n";
20983
+ }
20984
+ function splitLines(prior) {
20985
+ if (prior.length === 0) return [];
20986
+ const lines = prior.split(/\r?\n/);
20987
+ if (lines[lines.length - 1] === "") lines.pop();
20988
+ return lines;
20989
+ }
20990
+ function pushPriorLine(line, wanted, kept, outLines) {
20991
+ const trimmed = line.trim();
20992
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
20993
+ outLines.push(line);
20994
+ return;
20995
+ }
20996
+ if (wanted.has(trimmed)) {
20997
+ outLines.push(trimmed);
20998
+ kept.add(trimmed);
20999
+ }
21000
+ }
21001
+ function appendNewPatterns(nextPatterns, kept, outLines) {
21002
+ for (const p of nextPatterns) {
21003
+ if (kept.has(p)) continue;
21004
+ outLines.push(p);
21005
+ kept.add(p);
21006
+ }
21007
+ }
21008
+
21009
+ // server/routes/project-ignore.ts
21010
+ function registerProjectIgnoreRoute(app, deps) {
21011
+ app.get("/api/project-ignore", (c) => {
20929
21012
  return c.json(buildEnvelope2(deps));
20930
21013
  });
20931
- app.patch("/api/project-preferences", async (c) => {
21014
+ app.patch("/api/project-ignore", async (c) => {
20932
21015
  const body = await parsePatchBody3(c.req.raw);
20933
- applyPatch2(deps, body);
21016
+ await applyPatch2(deps, body);
20934
21017
  return c.json(buildEnvelope2(deps));
20935
21018
  });
20936
21019
  }
20937
21020
  function buildEnvelope2(deps) {
21021
+ const cwd = deps.runtimeContext.cwd;
21022
+ return { patterns: readPatterns(cwd) };
21023
+ }
21024
+ async function applyPatch2(deps, body) {
21025
+ const cwd = deps.runtimeContext.cwd;
21026
+ const trimmed = [];
21027
+ const seen = /* @__PURE__ */ new Set();
21028
+ for (const raw of body.patterns) {
21029
+ const t = raw.trim();
21030
+ if (t.length === 0) {
21031
+ throw new HTTPException10(400, {
21032
+ message: SERVER_TEXTS.projectIgnorePatternEmpty
21033
+ });
21034
+ }
21035
+ if (seen.has(t)) {
21036
+ throw new HTTPException10(400, {
21037
+ message: tx(SERVER_TEXTS.projectIgnorePatternDuplicate, { pattern: t })
21038
+ });
21039
+ }
21040
+ seen.add(t);
21041
+ trimmed.push(t);
21042
+ }
21043
+ const before = readPatterns(cwd);
21044
+ try {
21045
+ writePatterns(cwd, trimmed);
21046
+ } catch (err) {
21047
+ throw new HTTPException10(400, {
21048
+ message: tx(SERVER_TEXTS.projectIgnorePersistFailed, {
21049
+ message: formatErrorMessage(err)
21050
+ })
21051
+ });
21052
+ }
21053
+ logPatternChanges(before, trimmed);
21054
+ if (arrayChanged(before, trimmed)) await maybeRestartWatcher(deps);
21055
+ }
21056
+ function arrayChanged(before, next) {
21057
+ if (before.length !== next.length) return true;
21058
+ const beforeSet = new Set(before);
21059
+ for (const p of next) {
21060
+ if (!beforeSet.has(p)) return true;
21061
+ }
21062
+ return false;
21063
+ }
21064
+ function logPatternChanges(before, next) {
21065
+ const beforeSet = new Set(before);
21066
+ const nextSet = new Set(next);
21067
+ for (const p of next) {
21068
+ if (beforeSet.has(p)) continue;
21069
+ log.warn(
21070
+ tx(SERVER_TEXTS.projectIgnorePatternAdded, {
21071
+ pattern: sanitizeForTerminal(p)
21072
+ })
21073
+ );
21074
+ }
21075
+ for (const p of before) {
21076
+ if (nextSet.has(p)) continue;
21077
+ log.warn(
21078
+ tx(SERVER_TEXTS.projectIgnorePatternRemoved, {
21079
+ pattern: sanitizeForTerminal(p)
21080
+ })
21081
+ );
21082
+ }
21083
+ }
21084
+ async function maybeRestartWatcher(deps) {
21085
+ const watcher = deps.watcherHolder.current;
21086
+ if (!watcher) return;
21087
+ try {
21088
+ await watcher.restart();
21089
+ } catch (err) {
21090
+ log.warn(
21091
+ tx(SERVER_TEXTS.projectIgnoreWatcherRestartFailed, {
21092
+ message: formatErrorMessage(err)
21093
+ })
21094
+ );
21095
+ }
21096
+ }
21097
+ var PATCH_BODY_SCHEMA2 = {
21098
+ type: "object",
21099
+ additionalProperties: false,
21100
+ required: ["patterns"],
21101
+ properties: {
21102
+ patterns: {
21103
+ type: "array",
21104
+ items: {
21105
+ type: "string",
21106
+ pattern: "^[^\\n\\r\\x00-\\x1F\\x7F]+$"
21107
+ }
21108
+ }
21109
+ }
21110
+ };
21111
+ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21112
+ notJson: SERVER_TEXTS.projectIgnoreBodyNotJson,
21113
+ notObject: SERVER_TEXTS.projectIgnoreBodyNotObject,
21114
+ invalid: SERVER_TEXTS.projectIgnoreBodyEmpty,
21115
+ mapping: {
21116
+ "/patterns:required": SERVER_TEXTS.projectIgnoreBodyEmpty,
21117
+ "/patterns:type:array": SERVER_TEXTS.projectIgnoreListNotArray,
21118
+ "/patterns/*:type:string": SERVER_TEXTS.projectIgnoreEntryNotString,
21119
+ "/patterns/*:pattern": SERVER_TEXTS.projectIgnorePatternHasControlChar
21120
+ }
21121
+ });
21122
+
21123
+ // server/routes/project-preferences.ts
21124
+ import { statSync as statSync10 } from "fs";
21125
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
21126
+ function registerProjectPreferencesRoute(app, deps) {
21127
+ app.get("/api/project-preferences", (c) => {
21128
+ return c.json(buildEnvelope3(deps));
21129
+ });
21130
+ app.patch("/api/project-preferences", async (c) => {
21131
+ const body = await parsePatchBody4(c.req.raw);
21132
+ await applyPatch3(deps, body);
21133
+ return c.json(buildEnvelope3(deps));
21134
+ });
21135
+ }
21136
+ function buildEnvelope3(deps) {
20938
21137
  const cwd = deps.runtimeContext.cwd;
20939
21138
  return {
20940
21139
  scan: {
20941
- extraFolders: readConfigValue("scan.extraFolders", {
20942
- cwd,
20943
- default: []
20944
- }) ?? [],
20945
21140
  referencePaths: readConfigValue("scan.referencePaths", {
20946
21141
  cwd,
20947
21142
  default: []
@@ -20949,46 +21144,138 @@ function buildEnvelope2(deps) {
20949
21144
  }
20950
21145
  };
20951
21146
  }
20952
- function applyPatch2(deps, body) {
21147
+ async function applyPatch3(deps, body) {
20953
21148
  const writes = collectWrites(body);
20954
21149
  if (writes.length === 0) return;
20955
21150
  const cwd = deps.runtimeContext.cwd;
21151
+ const missingPaths = collectMissingPaths(writes, cwd);
21152
+ if (missingPaths.length > 0) {
21153
+ throw new HTTPException11(400, {
21154
+ message: tx(SERVER_TEXTS.projectPrefsPathNotFound, {
21155
+ paths: missingPaths.join(", ")
21156
+ })
21157
+ });
21158
+ }
20956
21159
  const exposures = writes.map((w) => projectPathExposure({ key: w.key, value: w.value, cwd })).filter((e) => e.expandsSurface);
20957
21160
  if (exposures.length > 0 && body.confirm !== true) {
20958
21161
  const exposed = exposures.flatMap((e) => e.exposedPaths);
20959
- throw new HTTPException10(412, {
21162
+ throw new HTTPException11(412, {
20960
21163
  message: tx(SERVER_TEXTS.projectPrefsConfirmRequired, {
20961
21164
  paths: exposed.join(", ")
20962
21165
  })
20963
21166
  });
20964
21167
  }
21168
+ let scanSurfaceMutated = false;
20965
21169
  for (const w of writes) {
20966
- try {
20967
- writeConfigValue(w.key, w.value, { target: "project-local", cwd });
20968
- } catch (err) {
20969
- const status = err instanceof ConfigValidationError ? 400 : 400;
20970
- throw new HTTPException10(status, {
20971
- message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
20972
- key: w.key,
20973
- message: formatErrorMessage(err)
20974
- })
20975
- });
20976
- }
21170
+ if (runWrite(w, cwd)) scanSurfaceMutated = true;
20977
21171
  }
21172
+ if (scanSurfaceMutated) await maybeRestartWatcher2(deps);
20978
21173
  deps.configService.reload();
20979
21174
  }
20980
21175
  function collectWrites(body) {
20981
21176
  if (!body.scan) return [];
20982
21177
  const out = [];
20983
- if (Array.isArray(body.scan.extraFolders)) {
20984
- out.push({ key: "scan.extraFolders", value: body.scan.extraFolders });
20985
- }
20986
21178
  if (Array.isArray(body.scan.referencePaths)) {
20987
21179
  out.push({ key: "scan.referencePaths", value: body.scan.referencePaths });
20988
21180
  }
20989
21181
  return out;
20990
21182
  }
20991
- var PATCH_BODY_SCHEMA2 = {
21183
+ function collectMissingPaths(writes, cwd) {
21184
+ const missing = [];
21185
+ for (const w of writes) {
21186
+ if (!Array.isArray(w.value)) continue;
21187
+ const current = readConfigValue(w.key, { cwd, default: [] }) ?? [];
21188
+ const currentSet = new Set(current);
21189
+ for (const entry of w.value) {
21190
+ if (currentSet.has(entry)) continue;
21191
+ if (!isExistingDirectory(entry, cwd)) missing.push(entry);
21192
+ }
21193
+ }
21194
+ return missing;
21195
+ }
21196
+ async function maybeRestartWatcher2(deps) {
21197
+ const watcher = deps.watcherHolder.current;
21198
+ if (!watcher) return;
21199
+ try {
21200
+ await watcher.restart();
21201
+ } catch (err) {
21202
+ log.warn(
21203
+ tx(SERVER_TEXTS.projectPrefsWatcherRestartFailed, {
21204
+ message: formatErrorMessage(err)
21205
+ })
21206
+ );
21207
+ }
21208
+ }
21209
+ function runWrite(w, cwd) {
21210
+ const before = readConfigValue(w.key, { cwd, default: [] }) ?? [];
21211
+ try {
21212
+ writeConfigValue(w.key, w.value, { target: "project-local", cwd });
21213
+ } catch (err) {
21214
+ throw new HTTPException11(400, {
21215
+ message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
21216
+ key: w.key,
21217
+ message: formatErrorMessage(err)
21218
+ })
21219
+ });
21220
+ }
21221
+ logPathChanges(w.key, before, w.value, cwd);
21222
+ return arrayChanged2(before, w.value);
21223
+ }
21224
+ function arrayChanged2(before, nextValue) {
21225
+ if (!Array.isArray(nextValue)) return false;
21226
+ const next = nextValue;
21227
+ if (before.length !== next.length) return true;
21228
+ const beforeSet = new Set(before);
21229
+ for (const p of next) {
21230
+ if (!beforeSet.has(p)) return true;
21231
+ }
21232
+ return false;
21233
+ }
21234
+ function logPathChanges(key, before, nextValue, cwd) {
21235
+ if (!Array.isArray(nextValue)) return;
21236
+ const next = nextValue;
21237
+ const beforeSet = new Set(before);
21238
+ const nextSet = new Set(next);
21239
+ for (const path of next) {
21240
+ if (beforeSet.has(path)) continue;
21241
+ log.warn(
21242
+ tx(SERVER_TEXTS.projectPrefsPathAdded, {
21243
+ key,
21244
+ detail: formatPathDetail(path, cwd)
21245
+ })
21246
+ );
21247
+ }
21248
+ for (const path of before) {
21249
+ if (nextSet.has(path)) continue;
21250
+ log.warn(
21251
+ tx(SERVER_TEXTS.projectPrefsPathRemoved, {
21252
+ key,
21253
+ detail: formatPathDetail(path, cwd)
21254
+ })
21255
+ );
21256
+ }
21257
+ }
21258
+ function formatPathDetail(path, cwd) {
21259
+ const safePath = sanitizeForTerminal(path);
21260
+ if (path.startsWith("~/") || path === "~") {
21261
+ const abs2 = sanitizeForTerminal(resolveScanPath(path, cwd));
21262
+ return `${safePath} (home) \u2192 ${abs2}`;
21263
+ }
21264
+ if (path.startsWith("/")) {
21265
+ return `${safePath} (absolute)`;
21266
+ }
21267
+ const abs = sanitizeForTerminal(resolveScanPath(path, cwd));
21268
+ return `${safePath} (relative) \u2192 ${abs}`;
21269
+ }
21270
+ function isExistingDirectory(entry, cwd) {
21271
+ const abs = resolveScanPath(entry, cwd);
21272
+ try {
21273
+ return statSync10(abs).isDirectory();
21274
+ } catch {
21275
+ return false;
21276
+ }
21277
+ }
21278
+ var PATCH_BODY_SCHEMA3 = {
20992
21279
  type: "object",
20993
21280
  additionalProperties: false,
20994
21281
  required: ["scan"],
@@ -20999,13 +21286,15 @@ var PATCH_BODY_SCHEMA2 = {
20999
21286
  additionalProperties: false,
21000
21287
  minProperties: 1,
21001
21288
  properties: {
21002
- extraFolders: { type: "array", items: { type: "string" } },
21003
- referencePaths: { type: "array", items: { type: "string" } }
21289
+ referencePaths: {
21290
+ type: "array",
21291
+ items: { type: "string", pattern: "^[^,]+$" }
21292
+ }
21004
21293
  }
21005
21294
  }
21006
21295
  }
21007
21296
  };
21008
- var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21297
+ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21009
21298
  notJson: SERVER_TEXTS.projectPrefsBodyNotJson,
21010
21299
  notObject: SERVER_TEXTS.projectPrefsBodyNotObject,
21011
21300
  invalid: SERVER_TEXTS.projectPrefsBodyEmpty,
@@ -21014,15 +21303,14 @@ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21014
21303
  "/scan:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
21015
21304
  "/scan:type:object": SERVER_TEXTS.projectPrefsScanNotObject,
21016
21305
  "/confirm:type:boolean": SERVER_TEXTS.projectPrefsConfirmNotBoolean,
21017
- "/scan/extraFolders:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.extraFolders" }),
21018
21306
  "/scan/referencePaths:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.referencePaths" }),
21019
- "/scan/extraFolders/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.extraFolders" }),
21020
- "/scan/referencePaths/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.referencePaths" })
21307
+ "/scan/referencePaths/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.referencePaths" }),
21308
+ "/scan/referencePaths/*:pattern": SERVER_TEXTS.projectPrefsEntryHasComma
21021
21309
  }
21022
21310
  });
21023
21311
 
21024
21312
  // server/routes/scan.ts
21025
- import { HTTPException as HTTPException11 } from "hono/http-exception";
21313
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
21026
21314
 
21027
21315
  // server/scan-mutex.ts
21028
21316
  var inFlight = null;
@@ -21030,14 +21318,14 @@ async function withScanMutex(fn) {
21030
21318
  if (inFlight !== null) {
21031
21319
  throw new ScanBusyError();
21032
21320
  }
21033
- let resolve37;
21321
+ let resolve38;
21034
21322
  inFlight = new Promise((r) => {
21035
- resolve37 = r;
21323
+ resolve38 = r;
21036
21324
  });
21037
21325
  try {
21038
21326
  return await fn();
21039
21327
  } finally {
21040
- resolve37();
21328
+ resolve38();
21041
21329
  inFlight = null;
21042
21330
  }
21043
21331
  }
@@ -21077,61 +21365,80 @@ function buildWatcherErrorEvent(data) {
21077
21365
  }
21078
21366
 
21079
21367
  // server/watcher.ts
21080
- var WATCH_ROOT = ".";
21081
21368
  function createWatcherService(opts) {
21082
- const runtimeOpts = {
21083
- dbPath: opts.options.dbPath,
21084
- roots: [WATCH_ROOT],
21085
- runtimeContext: opts.runtimeContext,
21086
- noBuiltIns: opts.options.noBuiltIns,
21087
- noPlugins: opts.options.noPlugins,
21088
- emitterFactory: () => buildBroadcasterEmitter(opts.broadcaster),
21089
- runInitialBatch: true,
21090
- // BFF ordering: subscribe first so edits arriving during the initial
21091
- // scan queue against the armed chokidar and fire a follow-up batch.
21092
- subscribeBeforeInitial: true,
21093
- failOnInitialBatchError: false,
21094
- events: {
21095
- onBatch: (outcome) => {
21096
- if (outcome.kind === "error") {
21369
+ let currentRuntime = null;
21370
+ const buildRuntimeOpts = () => {
21371
+ const runtimeOpts = {
21372
+ dbPath: opts.options.dbPath,
21373
+ roots: ["."],
21374
+ runtimeContext: opts.runtimeContext,
21375
+ noBuiltIns: opts.options.noBuiltIns,
21376
+ noPlugins: opts.options.noPlugins,
21377
+ emitterFactory: () => buildBroadcasterEmitter(opts.broadcaster),
21378
+ runInitialBatch: true,
21379
+ // BFF ordering: subscribe first so edits arriving during the
21380
+ // initial scan queue against the armed chokidar and fire a
21381
+ // follow-up batch.
21382
+ subscribeBeforeInitial: true,
21383
+ failOnInitialBatchError: false,
21384
+ events: {
21385
+ onBatch: (outcome) => {
21386
+ if (outcome.kind === "error") {
21387
+ log.warn(
21388
+ tx(SERVER_TEXTS.watcherBatchFailed, {
21389
+ message: sanitizeForTerminal(outcome.message)
21390
+ })
21391
+ );
21392
+ }
21393
+ },
21394
+ onWatcherError: (message) => {
21097
21395
  log.warn(
21098
- tx(SERVER_TEXTS.watcherBatchFailed, {
21099
- message: sanitizeForTerminal(outcome.message)
21396
+ tx(SERVER_TEXTS.watcherError, {
21397
+ message: sanitizeForTerminal(message)
21398
+ })
21399
+ );
21400
+ opts.broadcaster.broadcast(buildWatcherErrorEvent({ message }));
21401
+ },
21402
+ onPluginWarning: (message) => {
21403
+ log.warn(sanitizeForTerminal(message));
21404
+ },
21405
+ onReady: (info) => {
21406
+ opts.broadcaster.broadcast(
21407
+ buildWatcherStartedEvent({ roots: info.roots, debounceMs: info.debounceMs })
21408
+ );
21409
+ log.info(
21410
+ tx(SERVER_TEXTS.watcherReady, {
21411
+ roots: info.roots.join(","),
21412
+ debounceMs: String(info.debounceMs)
21100
21413
  })
21101
21414
  );
21102
21415
  }
21103
- },
21104
- onWatcherError: (message) => {
21105
- log.warn(
21106
- tx(SERVER_TEXTS.watcherError, {
21107
- message: sanitizeForTerminal(message)
21108
- })
21109
- );
21110
- opts.broadcaster.broadcast(buildWatcherErrorEvent({ message }));
21111
- },
21112
- onPluginWarning: (message) => {
21113
- log.warn(sanitizeForTerminal(message));
21114
- },
21115
- onReady: (info) => {
21116
- opts.broadcaster.broadcast(
21117
- buildWatcherStartedEvent({ roots: info.roots, debounceMs: info.debounceMs })
21118
- );
21119
- log.info(
21120
- tx(SERVER_TEXTS.watcherReady, {
21121
- roots: info.roots.join(","),
21122
- debounceMs: String(info.debounceMs)
21123
- })
21124
- );
21125
21416
  }
21417
+ };
21418
+ if (opts.debounceMsOverride !== void 0) {
21419
+ runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
21126
21420
  }
21421
+ return runtimeOpts;
21127
21422
  };
21128
- if (opts.debounceMsOverride !== void 0) {
21129
- runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
21130
- }
21131
- const handle = createWatcherRuntime(runtimeOpts);
21132
21423
  return {
21133
- start: handle.start,
21134
- stop: handle.stop
21424
+ async start() {
21425
+ currentRuntime = createWatcherRuntime(buildRuntimeOpts());
21426
+ await currentRuntime.start();
21427
+ },
21428
+ async stop() {
21429
+ if (currentRuntime) {
21430
+ await currentRuntime.stop();
21431
+ currentRuntime = null;
21432
+ }
21433
+ },
21434
+ async restart() {
21435
+ if (currentRuntime) {
21436
+ await currentRuntime.stop();
21437
+ currentRuntime = null;
21438
+ }
21439
+ currentRuntime = createWatcherRuntime(buildRuntimeOpts());
21440
+ await currentRuntime.start();
21441
+ }
21135
21442
  };
21136
21443
  }
21137
21444
  function buildBroadcasterEmitter(broadcaster) {
@@ -21158,7 +21465,7 @@ function registerScanRoute(app, deps) {
21158
21465
  }
21159
21466
  async function runPersistedScan(c, deps) {
21160
21467
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21161
- throw new HTTPException11(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
21468
+ throw new HTTPException12(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
21162
21469
  }
21163
21470
  const dbExists = await tryWithSqlite(
21164
21471
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -21187,7 +21494,7 @@ async function runPersistedScan(c, deps) {
21187
21494
  emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
21188
21495
  });
21189
21496
  if (outcome.kind !== "ok") {
21190
- throw new HTTPException11(500, {
21497
+ throw new HTTPException12(500, {
21191
21498
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
21192
21499
  });
21193
21500
  }
@@ -21195,7 +21502,7 @@ async function runPersistedScan(c, deps) {
21195
21502
  });
21196
21503
  } catch (err) {
21197
21504
  if (err instanceof ScanBusyError) {
21198
- throw new HTTPException11(409, { message: SERVER_TEXTS.scanPostBusy });
21505
+ throw new HTTPException12(409, { message: SERVER_TEXTS.scanPostBusy });
21199
21506
  }
21200
21507
  throw err;
21201
21508
  }
@@ -21259,7 +21566,7 @@ function groupTagsBySource2(rows) {
21259
21566
  }
21260
21567
  async function runFreshScan(deps) {
21261
21568
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21262
- throw new HTTPException11(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
21569
+ throw new HTTPException12(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
21263
21570
  }
21264
21571
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
21265
21572
  const outcome = await runScanForCommand({
@@ -21288,7 +21595,7 @@ async function runFreshScan(deps) {
21288
21595
  printer: bffScanRunnerPrinter
21289
21596
  });
21290
21597
  if (outcome.kind !== "ok") {
21291
- throw new HTTPException11(500, {
21598
+ throw new HTTPException12(500, {
21292
21599
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
21293
21600
  });
21294
21601
  }
@@ -21322,8 +21629,8 @@ function emptyScanResult() {
21322
21629
  }
21323
21630
 
21324
21631
  // server/routes/sidecar.ts
21325
- import { HTTPException as HTTPException12 } from "hono/http-exception";
21326
- import { resolve as resolve33 } from "path";
21632
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
21633
+ import { resolve as resolve34 } from "path";
21327
21634
  var STATUS_FRESH = "fresh";
21328
21635
  var ENVELOPE_KIND2 = "sidecar.bumped";
21329
21636
  var BUMP_BODY_SCHEMA = {
@@ -21357,13 +21664,13 @@ function registerSidecarRoutes(app, deps) {
21357
21664
  let absPath;
21358
21665
  try {
21359
21666
  assertContained(deps.runtimeContext.cwd, node.path);
21360
- absPath = resolve33(deps.runtimeContext.cwd, node.path);
21667
+ absPath = resolve34(deps.runtimeContext.cwd, node.path);
21361
21668
  } catch (err) {
21362
- throw new HTTPException12(500, { message: formatErrorMessage(err) });
21669
+ throw new HTTPException13(500, { message: formatErrorMessage(err) });
21363
21670
  }
21364
21671
  const result = invokeBump2(node, absPath, body);
21365
21672
  if (result.report.ok === false && result.report.reason === "fresh") {
21366
- throw new HTTPException12(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
21673
+ throw new HTTPException13(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
21367
21674
  }
21368
21675
  if (result.report.ok === true && result.report.noop === true) {
21369
21676
  const envelope2 = {
@@ -21390,7 +21697,7 @@ function registerSidecarRoutes(app, deps) {
21390
21697
  }
21391
21698
  } catch (err) {
21392
21699
  if (err instanceof EConsentRequiredError) throw err;
21393
- throw new HTTPException12(500, { message: formatErrorMessage(err) });
21700
+ throw new HTTPException13(500, { message: formatErrorMessage(err) });
21394
21701
  }
21395
21702
  if (body.confirm === true) {
21396
21703
  deps.configService.reload();
@@ -21427,7 +21734,7 @@ async function loadNode(deps, nodePath) {
21427
21734
  );
21428
21735
  const node = persisted?.nodes.find((n) => n.path === nodePath);
21429
21736
  if (!node) {
21430
- throw new HTTPException12(404, {
21737
+ throw new HTTPException13(404, {
21431
21738
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
21432
21739
  });
21433
21740
  }
@@ -21435,7 +21742,7 @@ async function loadNode(deps, nodePath) {
21435
21742
  }
21436
21743
  function invokeBump2(node, absPath, body) {
21437
21744
  if (!bumpAction.invoke) {
21438
- throw new HTTPException12(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
21745
+ throw new HTTPException13(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
21439
21746
  }
21440
21747
  const input = {};
21441
21748
  if (body.force === true) input.force = true;
@@ -21481,7 +21788,7 @@ function registerUpdateStatusRoute(app, deps) {
21481
21788
  }
21482
21789
 
21483
21790
  // server/static.ts
21484
- import { existsSync as existsSync25 } from "fs";
21791
+ import { existsSync as existsSync26 } from "fs";
21485
21792
  import { readFile as readFile5 } from "fs/promises";
21486
21793
  import { extname, join as join17 } from "path";
21487
21794
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -21536,7 +21843,7 @@ function createSpaFallback(opts) {
21536
21843
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21537
21844
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21538
21845
  const indexPath = join17(opts.uiDist, INDEX_HTML);
21539
- if (!existsSync25(indexPath)) return htmlResponse(c, placeholder);
21846
+ if (!existsSync26(indexPath)) return htmlResponse(c, placeholder);
21540
21847
  return fileResponse(c, indexPath);
21541
21848
  };
21542
21849
  }
@@ -21622,13 +21929,13 @@ function attachBroadcasterRoute(app, broadcaster) {
21622
21929
 
21623
21930
  // server/app.ts
21624
21931
  var BODY_LIMIT_BYTES = 1024 * 1024;
21625
- var DbMissingError = class extends HTTPException13 {
21932
+ var DbMissingError = class extends HTTPException14 {
21626
21933
  constructor(message) {
21627
21934
  super(500, { message });
21628
21935
  this.name = "DbMissingError";
21629
21936
  }
21630
21937
  };
21631
- var BulkValidationError = class extends HTTPException13 {
21938
+ var BulkValidationError = class extends HTTPException14 {
21632
21939
  id;
21633
21940
  code;
21634
21941
  constructor(init) {
@@ -21638,7 +21945,7 @@ var BulkValidationError = class extends HTTPException13 {
21638
21945
  this.code = init.code;
21639
21946
  }
21640
21947
  };
21641
- var LoopbackGateError = class extends HTTPException13 {
21948
+ var LoopbackGateError = class extends HTTPException14 {
21642
21949
  code;
21643
21950
  constructor(init) {
21644
21951
  super(403, { message: init.message });
@@ -21658,7 +21965,7 @@ function createApp(deps) {
21658
21965
  bodyLimit({
21659
21966
  maxSize: BODY_LIMIT_BYTES,
21660
21967
  onError: () => {
21661
- throw new HTTPException13(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
21968
+ throw new HTTPException14(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
21662
21969
  }
21663
21970
  })
21664
21971
  );
@@ -21682,7 +21989,8 @@ function createApp(deps) {
21682
21989
  kindRegistry: deps.kindRegistry,
21683
21990
  contributionsRegistry: deps.contributionsRegistry,
21684
21991
  pluginRuntime: deps.pluginRuntime,
21685
- configService
21992
+ configService,
21993
+ watcherHolder: deps.watcherHolder
21686
21994
  };
21687
21995
  registerScanRoute(app, { ...routeDeps, broadcaster: deps.broadcaster });
21688
21996
  registerNodesRoutes(app, routeDeps);
@@ -21698,8 +22006,9 @@ function createApp(deps) {
21698
22006
  registerUpdateStatusRoute(app, routeDeps);
21699
22007
  registerPreferencesRoute(app, routeDeps);
21700
22008
  registerProjectPreferencesRoute(app, routeDeps);
22009
+ registerProjectIgnoreRoute(app, routeDeps);
21701
22010
  app.all("/api/*", (c) => {
21702
- throw new HTTPException13(404, {
22011
+ throw new HTTPException14(404, {
21703
22012
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
21704
22013
  });
21705
22014
  });
@@ -21707,7 +22016,7 @@ function createApp(deps) {
21707
22016
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
21708
22017
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
21709
22018
  app.notFound((c) => {
21710
- throw new HTTPException13(404, {
22019
+ throw new HTTPException14(404, {
21711
22020
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
21712
22021
  });
21713
22022
  });
@@ -21762,7 +22071,7 @@ function formatError2(err, c) {
21762
22071
  };
21763
22072
  return c.json(envelope, 403);
21764
22073
  }
21765
- if (err instanceof HTTPException13) {
22074
+ if (err instanceof HTTPException14) {
21766
22075
  const status = err.status;
21767
22076
  const envelope = {
21768
22077
  ok: false,
@@ -22105,8 +22414,8 @@ function validateNoUi(noUi, uiDist) {
22105
22414
  }
22106
22415
 
22107
22416
  // server/paths.ts
22108
- import { existsSync as existsSync26, statSync as statSync10 } from "fs";
22109
- import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve34 } from "path";
22417
+ import { existsSync as existsSync27, statSync as statSync11 } from "fs";
22418
+ import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve35 } from "path";
22110
22419
  import { fileURLToPath as fileURLToPath5 } from "url";
22111
22420
  var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
22112
22421
  var PACKAGE_UI_REL = "ui";
@@ -22117,13 +22426,13 @@ function resolveDefaultUiDist(ctx) {
22117
22426
  return walkUpForUi(ctx.cwd);
22118
22427
  }
22119
22428
  function resolveExplicitUiDist(ctx, raw) {
22120
- return isAbsolute9(raw) ? raw : resolve34(ctx.cwd, raw);
22429
+ return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
22121
22430
  }
22122
22431
  function isUiBundleDir(path) {
22123
- if (!existsSync26(path)) return false;
22432
+ if (!existsSync27(path)) return false;
22124
22433
  try {
22125
- if (!statSync10(path).isDirectory()) return false;
22126
- return existsSync26(join18(path, INDEX_HTML2));
22434
+ if (!statSync11(path).isDirectory()) return false;
22435
+ return existsSync27(join18(path, INDEX_HTML2));
22127
22436
  } catch {
22128
22437
  return false;
22129
22438
  }
@@ -22151,7 +22460,7 @@ function resolvePackageBundledUiFrom(here) {
22151
22460
  return null;
22152
22461
  }
22153
22462
  function walkUpForUi(startDir) {
22154
- let current = resolve34(startDir);
22463
+ let current = resolve35(startDir);
22155
22464
  for (let i = 0; i < 64; i++) {
22156
22465
  const candidate = join18(current, DEFAULT_UI_REL);
22157
22466
  if (isUiBundleDir(candidate)) return candidate;
@@ -22169,6 +22478,7 @@ async function createServer(options, extra = {}) {
22169
22478
  const broadcaster = new WsBroadcaster();
22170
22479
  const { pluginRuntime, kindRegistry } = await assemblePluginRuntime(options, runtimeContext);
22171
22480
  const { kernel, contributionsRegistry } = assembleKernel(pluginRuntime, options.noBuiltIns);
22481
+ const watcherHolder = { current: null };
22172
22482
  const app = createApp({
22173
22483
  options,
22174
22484
  specVersion,
@@ -22177,6 +22487,7 @@ async function createServer(options, extra = {}) {
22177
22487
  kindRegistry,
22178
22488
  contributionsRegistry,
22179
22489
  pluginRuntime,
22490
+ watcherHolder,
22180
22491
  kernel
22181
22492
  });
22182
22493
  const wss = new WebSocketServer({ noServer: true });
@@ -22196,6 +22507,7 @@ async function createServer(options, extra = {}) {
22196
22507
  try {
22197
22508
  await candidate.start();
22198
22509
  watcherService = candidate;
22510
+ watcherHolder.current = candidate;
22199
22511
  } catch (err) {
22200
22512
  const message = formatErrorMessage(err);
22201
22513
  log.warn(
@@ -22390,7 +22702,8 @@ function renderBanner(input) {
22390
22702
  dbDisplay,
22391
22703
  pathDisplay: formatCwdPath(input.cwd),
22392
22704
  browserLine,
22393
- colorEnabled: input.colorEnabled
22705
+ colorEnabled: input.colorEnabled,
22706
+ referencePaths: input.referencePaths ?? []
22394
22707
  });
22395
22708
  }
22396
22709
  function resolveColorEnabled(opts) {
@@ -22447,11 +22760,23 @@ function renderFiglet(input) {
22447
22760
  lines.push(` ${dimOpen}Server${dimClose} ${greenUnderline}${input.url}${greenUnderlineClose}`);
22448
22761
  lines.push(` ${dimOpen}Path${dimClose} ${input.pathDisplay}`);
22449
22762
  lines.push(` ${dimOpen}DB${dimClose} ${input.dbDisplay}`);
22763
+ lines.push(...renderListRows("Refs", input.referencePaths, dimOpen, dimClose));
22450
22764
  lines.push("");
22451
22765
  lines.push(` ${dimOpen}${input.browserLine}${dimClose}`);
22452
22766
  lines.push("");
22453
22767
  return lines.join("\n") + "\n";
22454
22768
  }
22769
+ function renderListRows(label, values, dimOpen, dimClose) {
22770
+ if (values.length === 0) return [];
22771
+ const out = [];
22772
+ const labelPad = " ".repeat(Math.max(1, 9 - label.length));
22773
+ const continuationPad = " ".repeat(11);
22774
+ out.push(` ${dimOpen}${label}${dimClose}${labelPad}${sanitizeForTerminal(values[0])}`);
22775
+ for (let i = 1; i < values.length; i += 1) {
22776
+ out.push(`${continuationPad}${sanitizeForTerminal(values[i])}`);
22777
+ }
22778
+ return out;
22779
+ }
22455
22780
  var EMPTY_ANSI = {
22456
22781
  dimOpen: "",
22457
22782
  dimClose: "",
@@ -22571,7 +22896,7 @@ var ServeCommand = class extends SmCommand {
22571
22896
  return ExitCode.Error;
22572
22897
  }
22573
22898
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22574
- if (this.db !== void 0 && !existsSync27(dbPath)) {
22899
+ if (this.db !== void 0 && !existsSync28(dbPath)) {
22575
22900
  this.printer.info(
22576
22901
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22577
22902
  );
@@ -22647,6 +22972,12 @@ var ServeCommand = class extends SmCommand {
22647
22972
  noColorFlag: this.noColor,
22648
22973
  env: process.env
22649
22974
  });
22975
+ let referencePaths = [];
22976
+ try {
22977
+ const cfg = loadConfig({ cwd: runtimeCtx.cwd }).effective;
22978
+ referencePaths = cfg.scan.referencePaths;
22979
+ } catch {
22980
+ }
22650
22981
  this.printer.info(
22651
22982
  renderBanner({
22652
22983
  version: VERSION,
@@ -22656,7 +22987,8 @@ var ServeCommand = class extends SmCommand {
22656
22987
  cwd: runtimeCtx.cwd,
22657
22988
  openBrowser: validation.options.open,
22658
22989
  isTTY,
22659
- colorEnabled
22990
+ colorEnabled,
22991
+ referencePaths
22660
22992
  })
22661
22993
  );
22662
22994
  if (validation.options.open) {
@@ -23067,8 +23399,8 @@ function rankConfidenceForGrouping(c) {
23067
23399
  }
23068
23400
 
23069
23401
  // cli/commands/sidecar.ts
23070
- import { existsSync as existsSync28, unlinkSync as unlinkSync2 } from "fs";
23071
- import { resolve as resolve35 } from "path";
23402
+ import { existsSync as existsSync29, unlinkSync as unlinkSync2 } from "fs";
23403
+ import { resolve as resolve36 } from "path";
23072
23404
  import { Command as Command35, Option as Option33 } from "clipanion";
23073
23405
 
23074
23406
  // cli/i18n/sidecar.texts.ts
@@ -23219,7 +23551,7 @@ var SidecarRefreshCommand = class extends SmCommand {
23219
23551
  let absPath;
23220
23552
  try {
23221
23553
  assertContained(ctx.cwd, node.path);
23222
- absPath = resolve35(ctx.cwd, node.path);
23554
+ absPath = resolve36(ctx.cwd, node.path);
23223
23555
  } catch (err) {
23224
23556
  this.printer.error(
23225
23557
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23500,7 +23832,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23500
23832
  let absPath;
23501
23833
  try {
23502
23834
  assertContained(ctx.cwd, node.path);
23503
- absPath = resolve35(ctx.cwd, node.path);
23835
+ absPath = resolve36(ctx.cwd, node.path);
23504
23836
  } catch (err) {
23505
23837
  this.printer.error(
23506
23838
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23508,7 +23840,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23508
23840
  return ExitCode.Error;
23509
23841
  }
23510
23842
  const sidecarAbsPath = sidecarPathFor(absPath);
23511
- if (existsSync28(sidecarAbsPath) && this.force !== true) {
23843
+ if (existsSync29(sidecarAbsPath) && this.force !== true) {
23512
23844
  this.printer.error(
23513
23845
  tx(SIDECAR_TEXTS.annotateExists, {
23514
23846
  glyph: errGlyph,
@@ -23518,7 +23850,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23518
23850
  );
23519
23851
  return ExitCode.Error;
23520
23852
  }
23521
- if (existsSync28(sidecarAbsPath) && this.force === true) {
23853
+ if (existsSync29(sidecarAbsPath) && this.force === true) {
23522
23854
  try {
23523
23855
  unlinkSync2(sidecarAbsPath);
23524
23856
  } catch (err) {
@@ -23748,8 +24080,8 @@ var STUB_COMMANDS = [
23748
24080
  ];
23749
24081
 
23750
24082
  // cli/commands/tutorial.ts
23751
- import { cpSync as cpSync2, existsSync as existsSync29, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync11 } from "fs";
23752
- import { dirname as dirname19, join as join19, resolve as resolve36 } from "path";
24083
+ import { cpSync as cpSync2, existsSync as existsSync30, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync12 } from "fs";
24084
+ import { dirname as dirname19, join as join19, resolve as resolve37 } from "path";
23753
24085
  import { fileURLToPath as fileURLToPath6 } from "url";
23754
24086
  import { Command as Command37, Option as Option35 } from "clipanion";
23755
24087
 
@@ -23847,7 +24179,7 @@ var TutorialCommand = class extends SmCommand {
23847
24179
  const spec = VARIANT_SPECS[variant];
23848
24180
  const targetDir = join19(ctx.cwd, ".claude", "skills", spec.slug);
23849
24181
  const targetDisplay = `.claude/skills/${spec.slug}/`;
23850
- if (existsSync29(targetDir) && !this.force) {
24182
+ if (existsSync30(targetDir) && !this.force) {
23851
24183
  this.printer.error(
23852
24184
  tx(TUTORIAL_TEXTS.alreadyExists, {
23853
24185
  glyph: errGlyph,
@@ -23923,14 +24255,14 @@ function resolveSkillSourceDir(variant) {
23923
24255
  const here = dirname19(fileURLToPath6(import.meta.url));
23924
24256
  const candidates = [
23925
24257
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
23926
- resolve36(here, "../../..", spec.sourceDir),
24258
+ resolve37(here, "../../..", spec.sourceDir),
23927
24259
  // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
23928
- resolve36(here, "cli/tutorial", spec.slug),
24260
+ resolve37(here, "cli/tutorial", spec.slug),
23929
24261
  // bundled fallback: any-depth → cli/tutorial/<slug>
23930
- resolve36(here, "../cli/tutorial", spec.slug)
24262
+ resolve37(here, "../cli/tutorial", spec.slug)
23931
24263
  ];
23932
24264
  for (const candidate of candidates) {
23933
- if (existsSync29(candidate) && statSync11(candidate).isDirectory()) {
24265
+ if (existsSync30(candidate) && statSync12(candidate).isDirectory()) {
23934
24266
  cachedSourceDirs.set(variant, candidate);
23935
24267
  return candidate;
23936
24268
  }
@@ -24107,7 +24439,7 @@ await lifecycleDispatcher.dispatch(
24107
24439
  process.exit(exitCode);
24108
24440
  function resolveBareDefault() {
24109
24441
  const ctx = defaultRuntimeContext();
24110
- if (existsSync30(defaultProjectDbPath(ctx))) {
24442
+ if (existsSync31(defaultProjectDbPath(ctx))) {
24111
24443
  return ["serve"];
24112
24444
  }
24113
24445
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));