@skill-map/cli 0.50.1 → 0.52.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,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d01bcaaf-2cc8-5f97-915b-f8aac50c50c0")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="91ef81c7-e785-5818-839a-1f18ea093b57")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -246,7 +246,7 @@ function bucketByKind(kind, instance, bag) {
246
246
  // package.json
247
247
  var package_default = {
248
248
  name: "@skill-map/cli",
249
- version: "0.50.1",
249
+ version: "0.52.0",
250
250
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
251
  license: "MIT",
252
252
  type: "module",
@@ -1968,14 +1968,13 @@ var annotationStaleAnalyzer = {
1968
1968
  const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
1969
1969
  issues.push({
1970
1970
  analyzerId: ID10,
1971
- severity: "warn",
1971
+ severity: "info",
1972
1972
  nodeIds: [node.path],
1973
1973
  message,
1974
1974
  data: { status }
1975
1975
  });
1976
1976
  ctx.emitContribution(node.path, "staleIcon", {
1977
1977
  value: 0,
1978
- severity: "warn",
1979
1978
  tooltip: tooltipFor(status)
1980
1979
  });
1981
1980
  }
@@ -3987,7 +3986,7 @@ var nodeSupersedeAction = {
3987
3986
  }
3988
3987
  };
3989
3988
 
3990
- // core/update-check/index.ts
3989
+ // kernel/update-check/index.ts
3991
3990
  var SEMVER_SHAPE_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
3992
3991
  async function fetchLatestVersion(pkg, opts) {
3993
3992
  const controller = new AbortController();
@@ -4122,9 +4121,9 @@ function ansiFor(opts) {
4122
4121
  import { randomUUID } from "crypto";
4123
4122
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "fs";
4124
4123
  import { homedir } from "os";
4125
- import { join as join2 } from "path";
4124
+ import { join as join3 } from "path";
4126
4125
 
4127
- // core/config/atomic-write.ts
4126
+ // kernel/util/atomic-write.ts
4128
4127
  import {
4129
4128
  closeSync,
4130
4129
  constants as fsConstants,
@@ -4182,17 +4181,31 @@ function writeJsonAtomic(path, content) {
4182
4181
  }
4183
4182
 
4184
4183
  // core/paths/db-path.ts
4185
- import { join, resolve as resolve6 } from "path";
4184
+ import { join as join2, resolve as resolve6 } from "path";
4185
+
4186
+ // kernel/util/skill-map-paths.ts
4187
+ import { join } from "path";
4186
4188
  var SKILL_MAP_DIR = ".skill-map";
4189
+ var KERNEL_SKILL_MAP_DIR = SKILL_MAP_DIR;
4190
+ var SETTINGS_FILENAME = "settings.json";
4191
+ var LOCAL_SETTINGS_FILENAME = "settings.local.json";
4192
+ function kernelSettingsPath(scopeRoot) {
4193
+ return join(scopeRoot, KERNEL_SKILL_MAP_DIR, SETTINGS_FILENAME);
4194
+ }
4195
+ function kernelLocalSettingsPath(scopeRoot) {
4196
+ return join(scopeRoot, KERNEL_SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);
4197
+ }
4198
+
4199
+ // core/paths/db-path.ts
4187
4200
  var DB_FILENAME = "skill-map.db";
4188
4201
  var JOBS_DIRNAME = "jobs";
4189
4202
  var PLUGINS_DIRNAME = "plugins";
4190
- var SETTINGS_FILENAME = "settings.json";
4191
- var LOCAL_SETTINGS_FILENAME = "settings.local.json";
4203
+ var SETTINGS_FILENAME2 = "settings.json";
4204
+ var LOCAL_SETTINGS_FILENAME2 = "settings.local.json";
4192
4205
  var IGNORE_FILENAME = ".skillmapignore";
4193
4206
  var DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;
4194
4207
  var GITIGNORE_ENTRIES = [
4195
- `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,
4208
+ `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME2}`,
4196
4209
  `${SKILL_MAP_DIR}/${DB_FILENAME}`
4197
4210
  ];
4198
4211
  function resolveDbPath(options) {
@@ -4209,23 +4222,23 @@ function defaultProjectPluginsDir(ctx) {
4209
4222
  return resolve6(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);
4210
4223
  }
4211
4224
  function defaultDbPath(scopeRoot) {
4212
- return join(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);
4225
+ return join2(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);
4213
4226
  }
4214
4227
  function defaultSettingsPath(scopeRoot) {
4215
- return join(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME);
4228
+ return join2(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME2);
4216
4229
  }
4217
4230
  function defaultLocalSettingsPath(scopeRoot) {
4218
- return join(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);
4231
+ return join2(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME2);
4219
4232
  }
4220
4233
  function defaultIgnoreFilePath(scopeRoot) {
4221
- return join(scopeRoot, IGNORE_FILENAME);
4234
+ return join2(scopeRoot, IGNORE_FILENAME);
4222
4235
  }
4223
4236
 
4224
4237
  // cli/util/user-settings-store.ts
4225
4238
  var FILENAME = "settings.json";
4226
4239
  var SCHEMA_VERSION = 1;
4227
4240
  function userSettingsFilePath() {
4228
- return join2(homedir(), SKILL_MAP_DIR, FILENAME);
4241
+ return join3(homedir(), SKILL_MAP_DIR, FILENAME);
4229
4242
  }
4230
4243
  function defaultSettings() {
4231
4244
  return { schemaVersion: SCHEMA_VERSION, updateCheck: {}, telemetry: {} };
@@ -4264,7 +4277,7 @@ function backfillSubObjects(settings) {
4264
4277
  };
4265
4278
  }
4266
4279
  function writeUserSettings(patch) {
4267
- const dir = join2(homedir(), SKILL_MAP_DIR);
4280
+ const dir = join3(homedir(), SKILL_MAP_DIR);
4268
4281
  const path = userSettingsFilePath();
4269
4282
  try {
4270
4283
  const current = readUserSettings();
@@ -5361,18 +5374,6 @@ var CONFIG_LOADER_TEXTS = {
5361
5374
  projectLocalOnlyStripped: "[config:{{layer}}] key {{key}} is project-local only; stripped from the committed project layer. Move it to .skill-map/settings.local.json (gitignored, per-checkout)."
5362
5375
  };
5363
5376
 
5364
- // kernel/util/skill-map-paths.ts
5365
- import { join as join3 } from "path";
5366
- var KERNEL_SKILL_MAP_DIR = SKILL_MAP_DIR;
5367
- var SETTINGS_FILENAME2 = "settings.json";
5368
- var LOCAL_SETTINGS_FILENAME2 = "settings.local.json";
5369
- function kernelSettingsPath(scopeRoot) {
5370
- return join3(scopeRoot, KERNEL_SKILL_MAP_DIR, SETTINGS_FILENAME2);
5371
- }
5372
- function kernelLocalSettingsPath(scopeRoot) {
5373
- return join3(scopeRoot, KERNEL_SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME2);
5374
- }
5375
-
5376
5377
  // config/defaults.json
5377
5378
  var defaults_default = {
5378
5379
  schemaVersion: 1,
@@ -5830,11 +5831,20 @@ var FilesystemSidecarStore = class {
5830
5831
  * files in the repo and entries are tiny).
5831
5832
  */
5832
5833
  #locks = /* @__PURE__ */ new Map();
5834
+ /** Injected consent gate, see {@link TSidecarConsentGate}. */
5835
+ #consentGate;
5836
+ /**
5837
+ * @param consentGate the write-consent gate, invoked first inside
5838
+ * `applyPatch`. Production wires
5839
+ * `core/config/sidecar-consent.ts:ensureSidecarWritesAllowed`; tests
5840
+ * wire the same function (to exercise the real config-backed gate)
5841
+ * or a stub.
5842
+ */
5843
+ constructor(consentGate) {
5844
+ this.#consentGate = consentGate;
5845
+ }
5833
5846
  async applyPatch(sidecarAbsPath, changes, consent) {
5834
- ensureSidecarWritesAllowed({
5835
- confirm: consent.confirm,
5836
- cwd: consent.cwd
5837
- });
5847
+ this.#consentGate(consent);
5838
5848
  const prev = this.#locks.get(sidecarAbsPath) ?? Promise.resolve();
5839
5849
  let release;
5840
5850
  const settled = new Promise((res) => {
@@ -6058,6 +6068,17 @@ function ensureGitForStaged(cwd) {
6058
6068
  }
6059
6069
  return "ok";
6060
6070
  }
6071
+ function resolveGitAuthorName(cwd) {
6072
+ if (!isInsideGitRepo(cwd)) return null;
6073
+ const result = spawnSync("git", ["config", "user.name"], {
6074
+ cwd,
6075
+ stdio: ["ignore", "pipe", "pipe"],
6076
+ encoding: "utf8"
6077
+ });
6078
+ if (result.error !== void 0 || result.status !== 0) return null;
6079
+ const name = (result.stdout ?? "").trim();
6080
+ return name.length > 0 ? name : null;
6081
+ }
6061
6082
  function stageSidecar(cwd, sidecarAbsPath) {
6062
6083
  const result = spawnSync("git", ["add", "--", sidecarAbsPath], {
6063
6084
  cwd,
@@ -7858,7 +7879,7 @@ function rowToContribution(row) {
7858
7879
  };
7859
7880
  }
7860
7881
 
7861
- // core/sqlite/schema-fingerprint.ts
7882
+ // kernel/adapters/sqlite/schema-fingerprint.ts
7862
7883
  import { createHash } from "crypto";
7863
7884
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
7864
7885
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
@@ -8979,13 +9000,14 @@ function isAllowedLstatError(err) {
8979
9000
 
8980
9001
  // cli/commands/bump-plan.ts
8981
9002
  function computeBumpPlan(nodes, options) {
9003
+ const invoker = resolveGitAuthorName(options.cwd) ?? "cli";
8982
9004
  const items = [];
8983
9005
  for (const node of nodes) {
8984
- items.push(planOne(node, options));
9006
+ items.push(planOne(node, options, invoker));
8985
9007
  }
8986
9008
  return { items };
8987
9009
  }
8988
- function planOne(node, options) {
9010
+ function planOne(node, options, invoker) {
8989
9011
  let absPath;
8990
9012
  try {
8991
9013
  assertContained(options.cwd, node.path);
@@ -8999,7 +9021,7 @@ function planOne(node, options) {
8999
9021
  }
9000
9022
  let result;
9001
9023
  try {
9002
- result = invokeBumpFor(node, absPath, options.force);
9024
+ result = invokeBumpFor(node, absPath, options.force, invoker);
9003
9025
  } catch (err) {
9004
9026
  return {
9005
9027
  nodePath: node.path,
@@ -9021,7 +9043,7 @@ function planOne(node, options) {
9021
9043
  report: result.report
9022
9044
  };
9023
9045
  }
9024
- function invokeBumpFor(node, absPath, force) {
9046
+ function invokeBumpFor(node, absPath, force, invoker) {
9025
9047
  if (!nodeBumpAction.invoke) {
9026
9048
  throw new Error("built-in bump action is missing its invoke()");
9027
9049
  }
@@ -9030,7 +9052,7 @@ function invokeBumpFor(node, absPath, force) {
9030
9052
  return nodeBumpAction.invoke(input, {
9031
9053
  node,
9032
9054
  nodeAbsolutePath: absPath,
9033
- invoker: "cli",
9055
+ invoker,
9034
9056
  now: () => /* @__PURE__ */ new Date(),
9035
9057
  settings: {}
9036
9058
  });
@@ -9334,7 +9356,7 @@ var BumpCommand = class extends SmCommand {
9334
9356
  * the staging missed).
9335
9357
  */
9336
9358
  async #executePending(plan, cwd, ansi) {
9337
- const store = new FilesystemSidecarStore();
9359
+ const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
9338
9360
  const ctx = defaultRuntimeContext();
9339
9361
  const consent = {
9340
9362
  confirm: this.yes,
@@ -9480,7 +9502,7 @@ function buildBumpedOutcome(item, sidecarPath) {
9480
9502
  if (item.report.createdSidecar === true) outcome.createdSidecar = true;
9481
9503
  return outcome;
9482
9504
  }
9483
- async function applyBumpWrites(item, consent, store = new FilesystemSidecarStore()) {
9505
+ async function applyBumpWrites(item, consent, store = new FilesystemSidecarStore(ensureSidecarWritesAllowed)) {
9484
9506
  let sidecarPath;
9485
9507
  try {
9486
9508
  for (const w of item.writes) {
@@ -11423,20 +11445,9 @@ function trimRedundantPath(message, primary) {
11423
11445
  import { existsSync as existsSync16 } from "fs";
11424
11446
  import { Command as Command4, Option as Option4 } from "clipanion";
11425
11447
 
11426
- // core/config/active-provider.ts
11448
+ // kernel/scan/detect-providers.ts
11427
11449
  import { existsSync as existsSync15 } from "fs";
11428
11450
  import { join as join10 } from "path";
11429
- function resolveActiveProvider(cwd, providers = []) {
11430
- const detected = detectProvidersFromFilesystem(cwd, providers);
11431
- const fromConfig = readConfigValue("activeProvider", { cwd });
11432
- if (typeof fromConfig === "string" && fromConfig.length > 0) {
11433
- return { resolved: fromConfig, source: "config", detected };
11434
- }
11435
- if (detected.length > 0) {
11436
- return { resolved: detected[0], source: "autodetect", detected };
11437
- }
11438
- return { resolved: null, source: "none", detected };
11439
- }
11440
11451
  function detectProvidersFromFilesystem(cwd, providers) {
11441
11452
  const seen = /* @__PURE__ */ new Set();
11442
11453
  const out = [];
@@ -11451,6 +11462,19 @@ function detectProvidersFromFilesystem(cwd, providers) {
11451
11462
  return out;
11452
11463
  }
11453
11464
 
11465
+ // core/config/active-provider.ts
11466
+ function resolveActiveProvider(cwd, providers = []) {
11467
+ const detected = detectProvidersFromFilesystem(cwd, providers);
11468
+ const fromConfig = readConfigValue("activeProvider", { cwd });
11469
+ if (typeof fromConfig === "string" && fromConfig.length > 0) {
11470
+ return { resolved: fromConfig, source: "config", detected };
11471
+ }
11472
+ if (detected.length > 0) {
11473
+ return { resolved: detected[0], source: "autodetect", detected };
11474
+ }
11475
+ return { resolved: null, source: "none", detected };
11476
+ }
11477
+
11454
11478
  // cli/util/path-display.ts
11455
11479
  import { isAbsolute as isAbsolute4, relative as pathRelative } from "path";
11456
11480
  function relativeIfBelow(path, cwd) {
@@ -16327,7 +16351,7 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
16327
16351
  for (const parseIssue of result.issues) {
16328
16352
  issues.push({
16329
16353
  analyzerId: "invalid-sidecar",
16330
- severity: "warn",
16354
+ severity: "error",
16331
16355
  nodeIds: [nodePathForIssue],
16332
16356
  message: parseIssue.message,
16333
16357
  data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
@@ -16982,7 +17006,7 @@ function resolveActiveProviderOption(optionValue, roots, providers) {
16982
17006
  for (const root of roots) {
16983
17007
  const absRoot = isAbsolute7(root) ? root : resolve28(root);
16984
17008
  if (!existsSync23(absRoot)) continue;
16985
- const detected = resolveActiveProvider(absRoot, providers).resolved;
17009
+ const detected = detectProvidersFromFilesystem(absRoot, providers)[0] ?? null;
16986
17010
  if (detected !== null) return detected;
16987
17011
  }
16988
17012
  return null;
@@ -22272,7 +22296,7 @@ var IntentionalFailCommand = class extends SmCommand {
22272
22296
  throw new Error(INTENTIONAL_FAIL_TEXTS.errorMessage);
22273
22297
  }, 0);
22274
22298
  await new Promise((resolve40) => setTimeout(resolve40, 5e3));
22275
- return 1;
22299
+ return ExitCode.Issues;
22276
22300
  }
22277
22301
  };
22278
22302
 
@@ -22341,6 +22365,25 @@ var SCAN_TEXTS = {
22341
22365
  persistedTo: " {{dbPath}}\n",
22342
22366
  /** Body line for dry-run mode, same indent, marker tail. */
22343
22367
  wouldPersist: " would persist to {{dbPath}} (dry-run)\n",
22368
+ /**
22369
+ * Count-row nouns for the `{{counts}}` block in `scannedSummary`.
22370
+ * The caller selects the singular / plural form on `count === 1`
22371
+ * (English plural rule), so both forms live in the catalog instead of
22372
+ * being hand-suffixed with `s` at the call site (per the i18n
22373
+ * contract: catalog strings, no `${word}s` interpolation). `info` is
22374
+ * uncountable in English (no `infos`), so it carries a single form;
22375
+ * `countNoIssues` is the all-clean placeholder.
22376
+ */
22377
+ countNodeNounSingular: "node",
22378
+ countNodeNounPlural: "nodes",
22379
+ countLinkNounSingular: "link",
22380
+ countLinkNounPlural: "links",
22381
+ countErrorNounSingular: "error",
22382
+ countErrorNounPlural: "errors",
22383
+ countWarningNounSingular: "warning",
22384
+ countWarningNounPlural: "warnings",
22385
+ countInfoNoun: "info",
22386
+ countNoIssues: "0 issues",
22344
22387
  /**
22345
22388
  * Cap-hit notice, printed when the walker stopped accepting nodes
22346
22389
  * because `--max-nodes` (or the `scan.maxNodes` setting) was reached.
@@ -23422,27 +23465,29 @@ function fillSeverityBucket(bucket, nodeIds) {
23422
23465
  function formatScanCounts(opts) {
23423
23466
  const { nodes, links, severities, ansi } = opts;
23424
23467
  const parts = [
23425
- `${nodes} ${plural(nodes, "node")}`,
23426
- `${links} ${plural(links, "link")}`
23468
+ `${nodes} ${countNoun(nodes, SCAN_TEXTS.countNodeNounSingular, SCAN_TEXTS.countNodeNounPlural)}`,
23469
+ `${links} ${countNoun(links, SCAN_TEXTS.countLinkNounSingular, SCAN_TEXTS.countLinkNounPlural)}`
23427
23470
  ];
23428
23471
  const total = severities.errors + severities.warns + severities.info;
23429
23472
  if (total === 0) {
23430
- parts.push(ansi.dim("0 issues"));
23473
+ parts.push(ansi.dim(SCAN_TEXTS.countNoIssues));
23431
23474
  } else {
23432
23475
  if (severities.errors > 0) {
23433
- parts.push(ansi.red(`${severities.errors} ${plural(severities.errors, "error")}`));
23476
+ const noun = countNoun(severities.errors, SCAN_TEXTS.countErrorNounSingular, SCAN_TEXTS.countErrorNounPlural);
23477
+ parts.push(ansi.red(`${severities.errors} ${noun}`));
23434
23478
  }
23435
23479
  if (severities.warns > 0) {
23436
- parts.push(ansi.yellow(`${severities.warns} ${plural(severities.warns, "warning")}`));
23480
+ const noun = countNoun(severities.warns, SCAN_TEXTS.countWarningNounSingular, SCAN_TEXTS.countWarningNounPlural);
23481
+ parts.push(ansi.yellow(`${severities.warns} ${noun}`));
23437
23482
  }
23438
23483
  if (severities.info > 0) {
23439
- parts.push(ansi.dim(`${severities.info} info`));
23484
+ parts.push(ansi.dim(`${severities.info} ${SCAN_TEXTS.countInfoNoun}`));
23440
23485
  }
23441
23486
  }
23442
23487
  return parts.join(" \xB7 ");
23443
23488
  }
23444
- function plural(count, word) {
23445
- return count === 1 ? word : `${word}s`;
23489
+ function countNoun(count, singular, plural) {
23490
+ return count === 1 ? singular : plural;
23446
23491
  }
23447
23492
 
23448
23493
  // cli/commands/scan-compare.ts
@@ -23888,11 +23933,10 @@ var SERVER_TEXTS = {
23888
23933
  // here (after static + SPA fallback have had their turn).
23889
23934
  unknownPath: "Not found: {{path}}.",
23890
23935
  // ---- sidecar bump route (routes/sidecar.ts) ------------------------------
23891
- // 409 refusal when a fresh node is bumped without `force`. The
23892
- // `sidecar-fresh:` prefix is load-bearing, the UI pattern-matches
23893
- // it (the global `app.onError` already maps HTTP 409 to the
23894
- // `sidecar-fresh` envelope `code`, so the prefix is for log-grep
23895
- // affinity with the CLI's bump verb).
23936
+ // 409 refusal when a fresh node is bumped without `force`. Dispatch
23937
+ // is via the typed `ConflictError` (`code: 'sidecar-fresh'`), so the
23938
+ // `sidecar-fresh:` prefix is NOT load-bearing; it stays only for
23939
+ // log-grep affinity with the CLI's bump verb.
23896
23940
  sidecarFreshRefusal: "sidecar-fresh: Node is fresh; pass force:true to bump anyway.",
23897
23941
  // 400 envelopes thrown by `parseBody` when the request payload is
23898
23942
  // malformed. Each branch has its own key so the UI / log can
@@ -23920,9 +23964,9 @@ var SERVER_TEXTS = {
23920
23964
  // dropped half the pipeline. Same gate the `?fresh=1` GET applies.
23921
23965
  scanPostRequiresFullPipeline: "POST /api/scan cannot run while the server was started with --no-built-ins or --no-plugins (would persist a partial DB).",
23922
23966
  // 409, another scan (watcher batch or another POST) is in flight.
23923
- // The `scan-busy:` prefix is load-bearing: HTTP 409 maps to
23924
- // `scan-busy` in `app.onError`'s `codeForStatus`, but the prefix
23925
- // keeps log-grep affinity with the CLI's `sm scan` verb.
23967
+ // Dispatch is via the typed `ConflictError` (`code: 'scan-busy'`), so
23968
+ // the `scan-busy:` prefix is NOT load-bearing; it stays only for
23969
+ // log-grep affinity with the CLI's `sm scan` verb.
23926
23970
  scanPostBusy: "scan-busy: Another scan is already in flight; retry once it finishes.",
23927
23971
  // 500, DB missing on a write path. Read paths degrade to empty
23928
23972
  // shapes; mutations cannot persist without a DB so they fail fast.
@@ -24535,6 +24579,7 @@ function registerHealthRoute(app, deps) {
24535
24579
  var DEFAULT_LIMIT = 100;
24536
24580
  var MAX_LIMIT = 1e3;
24537
24581
  var BFF_MAX_BULK_CONTRIBUTIONS = 200;
24582
+ var MAX_WS_CLIENTS = 64;
24538
24583
 
24539
24584
  // server/routes/issues.ts
24540
24585
  function registerIssuesRoute(app, deps) {
@@ -24767,7 +24812,7 @@ function registerNodesRoutes(app, deps) {
24767
24812
  const tags = result?.tags ?? [];
24768
24813
  if (!bundle) {
24769
24814
  throw new HTTPException7(404, {
24770
- message: tx(SERVER_TEXTS.nodeNotFound, { path: nodePath })
24815
+ message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
24771
24816
  });
24772
24817
  }
24773
24818
  const decoratedNode = { ...bundle.node, isFavorite, contributions, tags };
@@ -25774,24 +25819,39 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
25774
25819
  import { existsSync as existsSync28 } from "fs";
25775
25820
  import { HTTPException as HTTPException13 } from "hono/http-exception";
25776
25821
  function registerActiveProviderRoute(app, deps) {
25777
- app.get("/api/active-provider", (c) => {
25778
- return c.json(buildEnvelope4(deps));
25822
+ app.get("/api/active-provider", async (c) => {
25823
+ return c.json(await buildEnvelope4(deps));
25779
25824
  });
25780
25825
  app.patch("/api/active-provider", async (c) => {
25781
25826
  const body = await parsePatchBody5(c.req.raw);
25782
25827
  const result = applyLensSwitch(deps, body.activeProvider);
25783
25828
  deps.configService.reload();
25784
- return c.json({ ...buildEnvelope4(deps), switch: result });
25829
+ return c.json({ ...await buildEnvelope4(deps), switch: result });
25785
25830
  });
25786
25831
  }
25787
- function buildEnvelope4(deps) {
25832
+ async function buildEnvelope4(deps) {
25788
25833
  const r = resolveActiveProvider(deps.runtimeContext.cwd, deps.providers);
25789
25834
  return {
25790
25835
  activeProvider: r.resolved,
25791
25836
  detected: r.detected,
25792
- source: r.source
25837
+ source: r.source,
25838
+ selectable: await resolveSelectableProviders(deps)
25793
25839
  };
25794
25840
  }
25841
+ async function resolveSelectableProviders(deps) {
25842
+ const resolveEnabled = await buildFreshResolver({
25843
+ databasePath: deps.options.dbPath,
25844
+ effectiveConfig: () => deps.configService.effective(),
25845
+ fallbackResolver: deps.pluginRuntime.resolveEnabled
25846
+ });
25847
+ const selectable = /* @__PURE__ */ new Set();
25848
+ for (const provider of deps.providers) {
25849
+ if (isPluginExtensionEnabled(provider, resolveEnabled)) {
25850
+ selectable.add(provider.id);
25851
+ }
25852
+ }
25853
+ return [...selectable];
25854
+ }
25795
25855
  function applyLensSwitch(deps, newValue) {
25796
25856
  const cwd = deps.runtimeContext.cwd;
25797
25857
  try {
@@ -26057,7 +26117,7 @@ async function runPersistedScan(c, deps) {
26057
26117
  });
26058
26118
  } catch (err) {
26059
26119
  if (err instanceof ScanBusyError) {
26060
- throw new HTTPException14(409, { message: SERVER_TEXTS.scanPostBusy });
26120
+ throw new ConflictError({ code: "scan-busy", message: SERVER_TEXTS.scanPostBusy });
26061
26121
  }
26062
26122
  throw err;
26063
26123
  }
@@ -26248,9 +26308,9 @@ function registerSidecarRoutes(app, deps) {
26248
26308
  } catch (err) {
26249
26309
  throw new HTTPException15(400, { message: formatErrorMessage(err) });
26250
26310
  }
26251
- const result = invokeBump2(node, absPath, body);
26311
+ const result = invokeBump2(node, absPath, body, deps.runtimeContext.cwd);
26252
26312
  if (result.report.ok === false && result.report.reason === "fresh") {
26253
- throw new HTTPException15(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
26313
+ throw new ConflictError({ code: "sidecar-fresh", message: SERVER_TEXTS.sidecarFreshRefusal });
26254
26314
  }
26255
26315
  if (result.report.ok === true && result.report.noop === true) {
26256
26316
  const envelope2 = {
@@ -26265,7 +26325,7 @@ function registerSidecarRoutes(app, deps) {
26265
26325
  };
26266
26326
  return c.json(envelope2);
26267
26327
  }
26268
- const store = new FilesystemSidecarStore();
26328
+ const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
26269
26329
  try {
26270
26330
  for (const w of result.writes ?? []) {
26271
26331
  if (w.kind === "sidecar") {
@@ -26320,7 +26380,7 @@ async function loadNode(deps, nodePath) {
26320
26380
  }
26321
26381
  return node;
26322
26382
  }
26323
- function invokeBump2(node, absPath, body) {
26383
+ function invokeBump2(node, absPath, body, cwd) {
26324
26384
  if (!nodeBumpAction.invoke) {
26325
26385
  throw new HTTPException15(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
26326
26386
  }
@@ -26329,7 +26389,7 @@ function invokeBump2(node, absPath, body) {
26329
26389
  return nodeBumpAction.invoke(input, {
26330
26390
  node,
26331
26391
  nodeAbsolutePath: absPath,
26332
- invoker: "ui",
26392
+ invoker: resolveGitAuthorName(cwd) ?? "ui",
26333
26393
  now: () => /* @__PURE__ */ new Date(),
26334
26394
  settings: {}
26335
26395
  });
@@ -26533,6 +26593,14 @@ var LoopbackGateError = class extends HTTPException16 {
26533
26593
  this.code = init.code;
26534
26594
  }
26535
26595
  };
26596
+ var ConflictError = class extends HTTPException16 {
26597
+ code;
26598
+ constructor(init) {
26599
+ super(409, { message: init.message });
26600
+ this.name = "ConflictError";
26601
+ this.code = init.code;
26602
+ }
26603
+ };
26536
26604
  function createApp(deps) {
26537
26605
  const app = new Hono();
26538
26606
  const configService = new ConfigService({
@@ -26609,16 +26677,12 @@ function createApp(deps) {
26609
26677
  });
26610
26678
  return app;
26611
26679
  }
26612
- function codeForStatus(status, message) {
26680
+ function codeForStatus(status) {
26613
26681
  if (status === 404) return "not-found";
26614
26682
  if (status === 400) return "bad-query";
26615
26683
  if (status === 403) return "locked";
26616
26684
  if (status === 412) return "confirm-required";
26617
26685
  if (status === 413) return "payload-too-large";
26618
- if (status === 409) {
26619
- if (message.startsWith("scan-busy:")) return "scan-busy";
26620
- return "sidecar-fresh";
26621
- }
26622
26686
  return "internal";
26623
26687
  }
26624
26688
  function formatError2(err, c) {
@@ -26655,12 +26719,23 @@ function formatError2(err, c) {
26655
26719
  };
26656
26720
  return c.json(envelope, 403);
26657
26721
  }
26722
+ if (err instanceof ConflictError) {
26723
+ const envelope = {
26724
+ ok: false,
26725
+ error: {
26726
+ code: err.code,
26727
+ message: err.message,
26728
+ details: null
26729
+ }
26730
+ };
26731
+ return c.json(envelope, 409);
26732
+ }
26658
26733
  if (err instanceof HTTPException16) {
26659
26734
  const status = err.status;
26660
26735
  const envelope = {
26661
26736
  ok: false,
26662
26737
  error: {
26663
- code: codeForStatus(status, err.message),
26738
+ code: codeForStatus(status),
26664
26739
  message: err.message,
26665
26740
  details: null
26666
26741
  }
@@ -26711,6 +26786,7 @@ function formatInternalErrorFallThrough(err, c) {
26711
26786
  var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
26712
26787
  var CLOSE_CODE_GOING_AWAY = 1001;
26713
26788
  var CLOSE_CODE_MESSAGE_TOO_BIG = 1009;
26789
+ var CLOSE_CODE_TRY_AGAIN_LATER = 1013;
26714
26790
  var READY_STATE_OPEN = 1;
26715
26791
  var WsBroadcaster = class {
26716
26792
  #clients = /* @__PURE__ */ new Set();
@@ -26733,6 +26809,13 @@ var WsBroadcaster = class {
26733
26809
  }
26734
26810
  return;
26735
26811
  }
26812
+ if (this.#clients.size >= MAX_WS_CLIENTS) {
26813
+ try {
26814
+ ws.close(CLOSE_CODE_TRY_AGAIN_LATER, "too many connections");
26815
+ } catch {
26816
+ }
26817
+ return;
26818
+ }
26736
26819
  this.#clients.add(ws);
26737
26820
  }
26738
26821
  /**
@@ -26979,14 +27062,19 @@ function validatePort(port) {
26979
27062
  return null;
26980
27063
  }
26981
27064
  function validateHost(host, devCors) {
26982
- if (devCors && !isLoopbackHost(host)) {
27065
+ if (isLoopbackHost(host)) return null;
27066
+ if (devCors) {
26983
27067
  return {
26984
27068
  code: "host-dev-cors-rejected",
26985
27069
  message: `--dev-cors requires a loopback --host (got ${host})`,
26986
27070
  value: host
26987
27071
  };
26988
27072
  }
26989
- return null;
27073
+ return {
27074
+ code: "host-not-loopback",
27075
+ message: `--host must be a loopback address; multi-host serve is not supported pre-1.0 (got ${host})`,
27076
+ value: host
27077
+ };
26990
27078
  }
26991
27079
  function validateWatcher(noWatcher, noBuiltIns, _noPlugins) {
26992
27080
  if (noWatcher) return null;
@@ -27274,6 +27362,14 @@ var SERVE_TEXTS = {
27274
27362
  */
27275
27363
  hostDevCorsRejected: "{{glyph}} sm serve: --dev-cors requires a loopback --host (got {{host}}).\n {{hint}}\n",
27276
27364
  hostDevCorsRejectedHint: "Use --host 127.0.0.1 (or ::1) when --dev-cors is set. Multi-host serve reopens after v0.6.0 (Decision #119).",
27365
+ /**
27366
+ * §3.1b error block when `--host` is any non-loopback address (without
27367
+ * `--dev-cors`). The BFF is loopback-only and unauthenticated pre-1.0
27368
+ * (Decision #119), so binding off-loopback is refused outright rather
27369
+ * than relying on the DNS-rebinding gate as the sole control.
27370
+ */
27371
+ hostNotLoopback: "{{glyph}} sm serve: --host must be a loopback address (got {{host}}).\n {{hint}}\n",
27372
+ hostNotLoopbackHint: "Use --host 127.0.0.1 (or ::1). The server has no auth and is loopback-only; multi-host serve reopens after v0.6.0 (Decision #119).",
27277
27373
  /**
27278
27374
  * §3.1b error block when `--port` falls outside the [0, 65535] range.
27279
27375
  * Hint names the accepted range so the operator can re-run.
@@ -27822,6 +27918,12 @@ function formatValidationError(err, ansi) {
27822
27918
  host: sanitizeForTerminal(err.value),
27823
27919
  hint: ansi.dim(SERVE_TEXTS.hostDevCorsRejectedHint)
27824
27920
  });
27921
+ case "host-not-loopback":
27922
+ return tx(SERVE_TEXTS.hostNotLoopback, {
27923
+ glyph: errGlyph,
27924
+ host: sanitizeForTerminal(err.value),
27925
+ hint: ansi.dim(SERVE_TEXTS.hostNotLoopbackHint)
27926
+ });
27825
27927
  case "port-out-of-range":
27826
27928
  return tx(SERVE_TEXTS.portOutOfRange, {
27827
27929
  glyph: errGlyph,
@@ -28414,7 +28516,7 @@ var SidecarRefreshCommand = class extends SmCommand {
28414
28516
  );
28415
28517
  return ExitCode.Ok;
28416
28518
  }
28417
- const store = new FilesystemSidecarStore();
28519
+ const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
28418
28520
  try {
28419
28521
  await store.applyPatch(
28420
28522
  sidecarAbsPath,
@@ -28701,7 +28803,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
28701
28803
  return ExitCode.Error;
28702
28804
  }
28703
28805
  }
28704
- const store = new FilesystemSidecarStore();
28806
+ const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
28705
28807
  try {
28706
28808
  await store.applyPatch(
28707
28809
  sidecarAbsPath,
@@ -29490,4 +29592,4 @@ function resolveBareDefault() {
29490
29592
  process.exit(ExitCode.Error);
29491
29593
  }
29492
29594
  //# sourceMappingURL=cli.js.map
29493
- //# debugId=d01bcaaf-2cc8-5f97-915b-f8aac50c50c0
29595
+ //# debugId=91ef81c7-e785-5818-839a-1f18ea093b57