@noir-ai/cli 1.0.0-beta.1 → 1.2.0-beta.2

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.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/init.ts
4
+ import { resolveAdapter } from "@noir-ai/adapters";
5
+ import { scaffold } from "@noir-ai/create";
6
+ import { emitSkillsToDir } from "@noir-ai/skills";
7
+ async function init(root, opts) {
8
+ assertTransportUrl(opts);
9
+ const host = opts.host ?? "claude";
10
+ await scaffold({
11
+ root,
12
+ mode: "init",
13
+ host,
14
+ transport: opts.transport,
15
+ ...opts.url !== void 0 ? { url: opts.url } : {},
16
+ ...opts.upgrade === true ? { upgrade: true } : {}
17
+ });
18
+ await emitHostSkills(root, host);
19
+ process.stderr.write(
20
+ `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
21
+ `
22
+ );
23
+ process.stderr.write(
24
+ "Next: run `noir` to open the home menu (or `noir status` for a snapshot).\n"
25
+ );
26
+ }
27
+ async function emitHostSkills(root, host) {
28
+ const adapter = resolveAdapter(host);
29
+ const skillsDir = adapter.skillsDir?.({ root });
30
+ if (skillsDir === void 0) {
31
+ process.stderr.write(`host '${host}' has no skill emitter; skipping skills
32
+ `);
33
+ return;
34
+ }
35
+ const target = host;
36
+ const summary = await emitSkillsToDir(skillsDir, { includeIntegrations: true, target });
37
+ const relDir = skillsDir.replace(`${root}/`, "");
38
+ process.stderr.write(
39
+ `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
40
+ `
41
+ );
42
+ }
43
+ function assertTransportUrl(opts) {
44
+ if (opts.transport === "streamable-http" && opts.url === void 0) {
45
+ throw new Error("--transport streamable-http requires --url");
46
+ }
47
+ if (opts.url !== void 0) {
48
+ assertLocalhostUrl(opts.url);
49
+ }
50
+ }
51
+ function assertLocalhostUrl(raw) {
52
+ let url;
53
+ try {
54
+ url = new URL(raw);
55
+ } catch {
56
+ throw new Error(`Invalid URL: ${raw}`);
57
+ }
58
+ if (!["http:", "https:"].includes(url.protocol)) {
59
+ throw new Error("Only http/https URLs are supported");
60
+ }
61
+ if (!["localhost", "127.0.0.1", "::1"].includes(url.hostname)) {
62
+ throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);
63
+ }
64
+ }
65
+
66
+ export {
67
+ init,
68
+ assertTransportUrl
69
+ };
70
+ //# sourceMappingURL=chunk-7WKKZ452.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/init.ts"],"sourcesContent":["// `noir init` — first-run + `--upgrade`.\n//\n// Slice S-T2 refactor: the ad-hoc writers (syncIgnores, writeManagedRegion,\n// writeFileSync for .mcp.json/project.id/config.yml/NOIR.md/RULES.md) are\n// replaced by a single call into `@noir-ai/create`'s `scaffold({mode:'init'})`.\n// The manifest + three-mode writer are the source of truth for what init\n// emits; this module is now a thin caller that owns ONLY:\n// - the transport/url precondition (the localhost security gate, whose\n// error strings are locked by url-validation.test.ts), and\n// - skills emission (out-of-manifest by design — composed after scaffold()).\n//\n// S10 multi-host: the 8 direct `claudeAdapter` imports across init/sync/create\n// collapsed to `resolveAdapter(host)` where `host` comes from `--host <id>`\n// (default `'claude'`). The adapter drives (a) the manifest via\n// `scaffold({host})` and (b) skills emission — claude/cursor have a skill dir;\n// gemini/agents-md/opencode have no skill concept and the call is skipped.\n//\n// Deliberate behavior changes vs the predecessor (spec-aligned latent-bug\n// fixes; see S-T1 contract notes + CHANGELOG):\n// - `.noir/project.id` → skipIfExists (predecessor overwrote on every init,\n// orphaning the store DB named after the id). Re-init now preserves it.\n// - `.noir/config.yml` → skipIfExists (predecessor overwrote).\n// - `.noir/NOIR.md` → managedBlock with BRIEF_BLOCK markers (predecessor\n// wrote the whole file with no markers). First-run output GAINS markers;\n// user notes outside the markers survive re-runs.\n// - `.noir/scaffold-version` is now stamped on init/create (engine-owned).\n\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { scaffold } from '@noir-ai/create';\nimport { type CompileTarget, emitSkillsToDir } from '@noir-ai/skills';\n\nexport interface InitOptions {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n /** `noir init --upgrade`: run scaffold migrations from the on-disk\n * scaffold-version to current, then re-emit ONLY the runtime subset\n * (regenerate + managedBlock). skipIfExists seeds are left alone so user\n * edits survive. */\n upgrade?: boolean;\n /** S10 target host. Defaults to `'claude'` (the regression anchor). Drives\n * both scaffold emission (the manifest's host-specific half) and skills\n * emission (skipped for hosts with no `skillsDir`). */\n host?: HostId;\n}\n\nexport async function init(root: string, opts: InitOptions): Promise<void> {\n assertTransportUrl(opts);\n\n const host: HostId = opts.host ?? 'claude';\n\n await scaffold({\n root,\n mode: 'init',\n host,\n transport: opts.transport,\n ...(opts.url !== undefined ? { url: opts.url } : {}),\n ...(opts.upgrade === true ? { upgrade: true } : {}),\n });\n\n await emitHostSkills(root, host);\n\n process.stderr.write(\n `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).\\n`,\n );\n process.stderr.write(\n 'Next: run `noir` to open the home menu (or `noir status` for a snapshot).\\n',\n );\n}\n\n/**\n * Compose skill emission onto the resolved adapter's `skillsDir` (claude →\n * `.claude/skills/`; cursor → `.cursor/rules/` compiled as `.mdc`; the other\n * three hosts have no skill concept and are skipped with a stderr note).\n *\n * The `CompileTarget` matches the host id (S10 foundation widened the enum to\n * the same union) so cursor skills compile to the `.mdc` rule shape via\n * `compileSkill(_, 'cursor')`; the others keep the verbatim SKILL.md format.\n */\nasync function emitHostSkills(root: string, host: HostId): Promise<void> {\n const adapter = resolveAdapter(host);\n const skillsDir = adapter.skillsDir?.({ root });\n if (skillsDir === undefined) {\n // N1: standardized wording — same phrase across init/sync/create so logs\n // grep uniformly. (Pre-N1 each command phrased this differently.)\n process.stderr.write(`host '${host}' has no skill emitter; skipping skills\\n`);\n return;\n }\n const target: CompileTarget = host;\n const summary = await emitSkillsToDir(skillsDir, { includeIntegrations: true, target });\n const relDir = skillsDir.replace(`${root}/`, '');\n process.stderr.write(\n `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).\\n`,\n );\n}\n\n/**\n * Transport + URL precondition. The error strings here are the SECURITY GATE\n * locked by `url-validation.test.ts` — do not rephrase. Thrown errors are\n * plain `Error`s (bin.ts's `main().catch` maps them to stderr + exitCode 1).\n *\n * Exported so `noir create` (commands/create.ts) reuses the SAME gate instead\n * of duplicating the localhost allowlist.\n */\nexport function assertTransportUrl(opts: {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n}): void {\n if (opts.transport === 'streamable-http' && opts.url === undefined) {\n throw new Error('--transport streamable-http requires --url');\n }\n if (opts.url !== undefined) {\n assertLocalhostUrl(opts.url);\n }\n}\n\n// Validate a streamable-http --url is http(s) and localhost-only. Gate 2's\n// daemon binds 127.0.0.1, so persisting a non-localhost URL is a footgun.\n// Not exported: the single external surface is assertTransportUrl(); thrown\n// errors propagate to bin.ts's main().catch (stderr + exitCode 1).\nfunction assertLocalhostUrl(raw: string): void {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Invalid URL: ${raw}`);\n }\n if (!['http:', 'https:'].includes(url.protocol)) {\n throw new Error('Only http/https URLs are supported');\n }\n if (!['localhost', '127.0.0.1', '::1'].includes(url.hostname)) {\n throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);\n }\n}\n"],"mappings":";;;AA2BA,SAAsB,sBAAsB;AAC5C,SAAS,gBAAgB;AACzB,SAA6B,uBAAuB;AAgBpD,eAAsB,KAAK,MAAc,MAAkC;AACzE,qBAAmB,IAAI;AAEvB,QAAM,OAAe,KAAK,QAAQ;AAElC,QAAM,SAAS;AAAA,IACb;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,WAAW,KAAK;AAAA,IAChB,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,EACnD,CAAC;AAED,QAAM,eAAe,MAAM,IAAI;AAE/B,UAAQ,OAAO;AAAA,IACb,uBAAuB,IAAI,WAAW,IAAI,gBAAgB,KAAK,SAAS;AAAA;AAAA,EAC1E;AACA,UAAQ,OAAO;AAAA,IACb;AAAA,EACF;AACF;AAWA,eAAe,eAAe,MAAc,MAA6B;AACvE,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,YAAY,QAAQ,YAAY,EAAE,KAAK,CAAC;AAC9C,MAAI,cAAc,QAAW;AAG3B,YAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,CAA2C;AAC7E;AAAA,EACF;AACA,QAAM,SAAwB;AAC9B,QAAM,UAAU,MAAM,gBAAgB,WAAW,EAAE,qBAAqB,MAAM,OAAO,CAAC;AACtF,QAAM,SAAS,UAAU,QAAQ,GAAG,IAAI,KAAK,EAAE;AAC/C,UAAQ,OAAO;AAAA,IACb,WAAW,QAAQ,QAAQ,MAAM,mBAAmB,MAAM,cAAc,MAAM;AAAA;AAAA,EAChF;AACF;AAUO,SAAS,mBAAmB,MAG1B;AACP,MAAI,KAAK,cAAc,qBAAqB,KAAK,QAAQ,QAAW;AAClE,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,MAAI,KAAK,QAAQ,QAAW;AAC1B,uBAAmB,KAAK,GAAG;AAAA,EAC7B;AACF;AAMA,SAAS,mBAAmB,KAAmB;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,GAAG;AAAA,EACnB,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB,GAAG,EAAE;AAAA,EACvC;AACA,MAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC/C,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,MAAI,CAAC,CAAC,aAAa,aAAa,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC7D,UAAM,IAAI,MAAM,0CAA0C,IAAI,QAAQ,GAAG;AAAA,EAC3E;AACF;","names":[]}
@@ -105,6 +105,7 @@ function commanderExitCode(err) {
105
105
  switch (err.code) {
106
106
  case "commander.helpDisplayed":
107
107
  case "commander.versionDisplayed":
108
+ case "commander.version":
108
109
  return EXIT.OK;
109
110
  case "commander.unknownCommand":
110
111
  return EXIT.NOT_FOUND;
@@ -133,8 +134,13 @@ function handleError(err) {
133
134
  }
134
135
 
135
136
  // src/commands/doctor.ts
136
- import { existsSync } from "fs";
137
+ import { spawnSync } from "child_process";
138
+ import { existsSync, readdirSync, readFileSync } from "fs";
139
+ import { dirname, join, relative } from "path";
140
+ import { fileURLToPath } from "url";
141
+ import { resolveAdapter } from "@noir-ai/adapters";
137
142
  import { loadProjectInfo, NOIR_VERSION, paths } from "@noir-ai/core";
143
+ import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
138
144
  import { pidAlive, readDaemonRecord } from "@noir-ai/daemon";
139
145
  import { resolveModelConfig } from "@noir-ai/model";
140
146
  async function probeOnnx() {
@@ -260,6 +266,15 @@ async function checkStore(checks, project, root) {
260
266
  checks.push({ name: "store", status: "warn", detail: "skipped \u2014 not initialized" });
261
267
  return;
262
268
  }
269
+ const dbPath = paths.storeDb(root, project.id);
270
+ if (!existsSync(dbPath)) {
271
+ checks.push({
272
+ name: "store",
273
+ status: "warn",
274
+ detail: `not created yet \u2014 the store DB is created when the daemon first runs (\`noir daemon start\` or index content); expected at ${dbPath}`
275
+ });
276
+ return;
277
+ }
263
278
  const { openStore } = await import("@noir-ai/store");
264
279
  let store;
265
280
  try {
@@ -340,6 +355,58 @@ function checkProvider(checks, project) {
340
355
  detail: parts.join(" \xB7 ")
341
356
  });
342
357
  }
358
+ function checkScaffoldVersion(checks, root) {
359
+ const onDisk = readScaffoldVersion(root);
360
+ const current = CURRENT_SCAFFOLD_VERSION;
361
+ const drift = onDisk !== null && onDisk !== current;
362
+ const status = onDisk === null || drift ? "warn" : "ok";
363
+ const detail = onDisk === null ? "no .noir/scaffold-version stamp (run `noir init`)" : drift ? `on-disk ${onDisk} \u2260 current ${current} (run \`noir init --upgrade\`)` : `${onDisk} (up to date)`;
364
+ checks.push({ name: "scaffold version", status, detail });
365
+ return { onDisk, current, drift };
366
+ }
367
+ function checkRulesMdBudget(checks, root, project) {
368
+ if (!project) {
369
+ checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
370
+ return null;
371
+ }
372
+ const rulesPath = paths.rulesMd(root);
373
+ if (!existsSync(rulesPath)) {
374
+ checks.push({
375
+ name: "rules budget",
376
+ status: "ok",
377
+ detail: "no .noir/rules/RULES.md"
378
+ });
379
+ return null;
380
+ }
381
+ let content;
382
+ try {
383
+ content = readFileSync(rulesPath, "utf8");
384
+ } catch (e) {
385
+ checks.push({
386
+ name: "rules budget",
387
+ status: "warn",
388
+ detail: `RULES.md unreadable: ${msg(e)}`
389
+ });
390
+ return null;
391
+ }
392
+ const bytes = Buffer.byteLength(content, "utf8");
393
+ const lineCount = content.length === 0 ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
394
+ const lengthBudgetKb = project.config.rules.lengthBudgetKb;
395
+ const budgetBytes = lengthBudgetKb * 1024;
396
+ const maxLines = 150;
397
+ const overBytes = bytes > budgetBytes;
398
+ const overLines = lineCount > maxLines;
399
+ const over = overBytes || overLines;
400
+ const status = over ? "warn" : "ok";
401
+ const kb = (n) => (n / 1024).toFixed(1);
402
+ const detail = over ? `OVER: ${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines \u2014 trim RULES.md (failure-backed clauses only) or raise rules.lengthBudgetKb` : `${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines (within budget)`;
403
+ checks.push({ name: "rules budget", status, detail });
404
+ return {
405
+ onDisk: { bytes, lines: lineCount },
406
+ budget: { kb: lengthBudgetKb, maxLines },
407
+ over
408
+ };
409
+ }
343
410
  function summarize(checks) {
344
411
  let ok = 0;
345
412
  let warnN = 0;
@@ -351,6 +418,161 @@ function summarize(checks) {
351
418
  }
352
419
  return { ok, warn: warnN, fail: fail2 };
353
420
  }
421
+ function checkHostArtifacts(checks, root, project) {
422
+ if (!project) {
423
+ checks.push({ name: "host artifacts", status: "warn", detail: "skipped \u2014 not initialized" });
424
+ return null;
425
+ }
426
+ const host = project.config.host;
427
+ const adapter = resolveAdapter(host);
428
+ const ectx = { root };
429
+ const expected = [];
430
+ const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
431
+ if (emitsAgentsMd) {
432
+ expected.push(relOr(adapter.agentsMdPath?.(ectx) ?? "AGENTS.md", root));
433
+ }
434
+ if (host === "claude") expected.push("CLAUDE.md");
435
+ else if (host === "gemini") expected.push("GEMINI.md");
436
+ expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
437
+ const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
438
+ const status = missing.length === 0 ? "ok" : "warn";
439
+ const detail = missing.length === 0 ? `host=${host} \xB7 all ${expected.length} primary artifacts present` : `host=${host} \xB7 missing ${missing.join(", ")} (run \`noir sync\` to restore)`;
440
+ checks.push({ name: "host artifacts", status, detail });
441
+ return { active: host, expected, missing };
442
+ }
443
+ function resolveWorkspacePackagesDir() {
444
+ let here = dirname(fileURLToPath(import.meta.url));
445
+ for (let i = 0; i < 8; i++) {
446
+ const pkgPath = join(here, "package.json");
447
+ if (existsSync(pkgPath)) {
448
+ try {
449
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
450
+ if (pkg.name === "@noir-ai/cli") {
451
+ const packagesDir = dirname(here);
452
+ if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
453
+ return packagesDir;
454
+ }
455
+ return null;
456
+ }
457
+ } catch {
458
+ }
459
+ }
460
+ const parent = dirname(here);
461
+ if (parent === here) break;
462
+ here = parent;
463
+ }
464
+ return null;
465
+ }
466
+ var SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
467
+ function npmPackDryRun(cliPkgDir) {
468
+ const probe = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
469
+ if (probe.status !== 0 || probe.error) {
470
+ return { error: probe.error ? probe.error.message : "npm not on PATH" };
471
+ }
472
+ const res = spawnSync("npm", ["pack", "--dry-run"], {
473
+ cwd: cliPkgDir,
474
+ encoding: "utf8",
475
+ shell: true
476
+ });
477
+ if (res.status !== 0 || res.error) {
478
+ return { error: res.error ? res.error.message : `npm pack exited ${res.status ?? "?"}` };
479
+ }
480
+ const out = `${res.stdout ?? ""}
481
+ ${res.stderr ?? ""}`;
482
+ const totalMatch = out.match(/total files:\s*(\d+)/i);
483
+ const fileCount = totalMatch?.[1] ? Number.parseInt(totalMatch[1], 10) : null;
484
+ const distIncluded = /\bdist\//.test(out);
485
+ return { fileCount, distIncluded };
486
+ }
487
+ function checkPublish(checks, packagesDir, opts = {}) {
488
+ if (packagesDir === null) {
489
+ checks.push({
490
+ name: "publish",
491
+ status: "ok",
492
+ detail: "skipped \u2014 not a monorepo checkout (no packages/* workspace)"
493
+ });
494
+ return null;
495
+ }
496
+ let entries = [];
497
+ try {
498
+ entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
499
+ } catch (e) {
500
+ checks.push({
501
+ name: "publish",
502
+ status: "warn",
503
+ detail: `could not read ${packagesDir}: ${msg(e)}`
504
+ });
505
+ return { checked: 0, issues: [`unreadable packages dir: ${msg(e)}`] };
506
+ }
507
+ const issues = [];
508
+ let checked = 0;
509
+ let cliDir = null;
510
+ for (const name of entries) {
511
+ const pkgPath = join(packagesDir, name, "package.json");
512
+ if (!existsSync(pkgPath)) continue;
513
+ let pkg;
514
+ try {
515
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
516
+ } catch (e) {
517
+ issues.push(`${name}/package.json: unparseable (${msg(e)})`);
518
+ checked++;
519
+ continue;
520
+ }
521
+ checked++;
522
+ const pkgName = typeof pkg.name === "string" ? pkg.name : "";
523
+ if (!pkgName.startsWith("@noir-ai/")) {
524
+ issues.push(`${name}/package.json: name '${pkgName}' is not @noir-ai/*`);
525
+ }
526
+ const version = typeof pkg.version === "string" ? pkg.version : "";
527
+ if (!SEMVER_ISH.test(version)) {
528
+ issues.push(`${name}/package.json: version '${version}' is not semver`);
529
+ }
530
+ if (!Array.isArray(pkg.files) || pkg.files.length === 0) {
531
+ issues.push(`${name}/package.json: files missing or empty`);
532
+ }
533
+ if (pkgName === "@noir-ai/cli") {
534
+ if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
535
+ issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
536
+ }
537
+ cliDir = join(packagesDir, name);
538
+ }
539
+ }
540
+ let packNote = "";
541
+ if (cliDir && opts.skipNpmPack !== true) {
542
+ const pack = npmPackDryRun(cliDir);
543
+ if ("error" in pack) {
544
+ packNote = ` \xB7 npm pack skipped (${pack.error})`;
545
+ } else {
546
+ const count = pack.fileCount == null ? "?" : String(pack.fileCount);
547
+ const dist = pack.distIncluded ? "dist/ included" : "dist/ NOT in tarball";
548
+ packNote = ` \xB7 npm pack: ${count} files, ${dist}`;
549
+ if (pack.distIncluded === false) {
550
+ issues.push("cli npm pack --dry-run: dist/ absent from the tarball");
551
+ }
552
+ }
553
+ }
554
+ const status = issues.length > 0 ? "warn" : "ok";
555
+ const detail = issues.length === 0 ? `${checked}/${entries.length} packages publish-ready${packNote}` : `${issues.length} issue${issues.length === 1 ? "" : "s"} across ${checked} package${checked === 1 ? "" : "s"}${packNote}`;
556
+ checks.push({ name: "publish", status, detail });
557
+ return { checked, issues };
558
+ }
559
+ function relOr(abs, root) {
560
+ const posix = abs.replace(/\\/g, "/");
561
+ if (posix.startsWith("/")) {
562
+ const rel = relative(root, abs).replace(/\\/g, "/");
563
+ if (rel.length === 0 || rel.startsWith("..") || rel.startsWith("/")) {
564
+ throw new Error(`doctor: host path '${abs}' is not under root '${root}'`);
565
+ }
566
+ return rel;
567
+ }
568
+ if (posix.split("/").some((seg) => seg === "..")) {
569
+ throw new Error(`doctor: host path '${abs}' escapes root '${root}'`);
570
+ }
571
+ return posix;
572
+ }
573
+ function joinRoot(root, relPath) {
574
+ return `${root}/${relPath}`;
575
+ }
354
576
  function renderHuman(payload, opts) {
355
577
  log(
356
578
  `noir doctor \u2014 ${payload.checks.length} check${payload.checks.length === 1 ? "" : "s"}`,
@@ -388,8 +610,20 @@ async function doctor(opts = {}) {
388
610
  await checkStore(checks, project, root);
389
611
  checkEmbedder(checks, project, vecOk);
390
612
  checkProvider(checks, project);
613
+ const scaffold = checkScaffoldVersion(checks, root);
614
+ const rules = checkRulesMdBudget(checks, root, project);
615
+ const host = checkHostArtifacts(checks, root, project);
616
+ const publish = checkPublish(checks, resolveWorkspacePackagesDir());
391
617
  const summary = summarize(checks);
392
- const payload = { noir: NOIR_VERSION, checks, summary };
618
+ const payload = {
619
+ noir: NOIR_VERSION,
620
+ checks,
621
+ summary,
622
+ scaffold,
623
+ rules,
624
+ host,
625
+ publish
626
+ };
393
627
  if (opts.json === true) {
394
628
  process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
395
629
  `);
@@ -404,88 +638,6 @@ async function doctor(opts = {}) {
404
638
  }
405
639
  }
406
640
 
407
- // src/init.ts
408
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
409
- import { join } from "path";
410
- import { claudeAdapter } from "@noir-ai/adapters";
411
- import { CONTEXT_BLOCK_BEGIN, CONTEXT_BLOCK_END, createProjectId, paths as paths2 } from "@noir-ai/core";
412
- import { emitSkillsToDir } from "@noir-ai/skills";
413
- async function init(root, opts) {
414
- if (opts.transport === "streamable-http" && opts.url === void 0) {
415
- throw new Error("--transport streamable-http requires --url");
416
- }
417
- if (opts.url !== void 0) {
418
- assertLocalhostUrl(opts.url);
419
- }
420
- mkdirSync(paths2.noirDir(root), { recursive: true });
421
- const id = createProjectId();
422
- writeFileSync(paths2.projectId(root), `${id}
423
- `, "utf8");
424
- writeFileSync(paths2.config(root), "host: claude\nmode: full\n", "utf8");
425
- writeFileSync(
426
- paths2.noirMd(root),
427
- `# Noir context
428
-
429
- Project id: \`${id}\`
430
-
431
- <!-- Noir auto-manages this file. Host context files @import it. -->
432
- `,
433
- "utf8"
434
- );
435
- writeFileSync(
436
- join(root, ".mcp.json"),
437
- `${claudeAdapter.emitMcpConfig({ root }, opts)}
438
- `,
439
- "utf8"
440
- );
441
- const existing = safeRead(join(root, "CLAUDE.md"));
442
- writeFileSync(
443
- join(root, "CLAUDE.md"),
444
- replaceBlock(existing, claudeAdapter.emitContext({ root })),
445
- "utf8"
446
- );
447
- if (claudeAdapter.skillsDir) {
448
- const summary = await emitSkillsToDir(claudeAdapter.skillsDir({ root }));
449
- process.stderr.write(`Emitted ${summary.emitted.length} Noir skills to .claude/skills/.
450
- `);
451
- }
452
- process.stderr.write(`Noir initialized in ${root} (transport: ${opts.transport}).
453
- `);
454
- }
455
- function safeRead(file) {
456
- try {
457
- return readFileSync(file, "utf8");
458
- } catch {
459
- return "";
460
- }
461
- }
462
- function replaceBlock(content, block) {
463
- const begin = CONTEXT_BLOCK_BEGIN;
464
- const end = CONTEXT_BLOCK_END;
465
- const re = new RegExp(`${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}\\n?`, "g");
466
- const stripped = content.replace(re, "");
467
- return `${stripped ? `${stripped.trimEnd()}
468
-
469
- ` : ""}${block}`;
470
- }
471
- function escapeRe(s) {
472
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
473
- }
474
- function assertLocalhostUrl(raw) {
475
- let url;
476
- try {
477
- url = new URL(raw);
478
- } catch {
479
- throw new Error(`Invalid URL: ${raw}`);
480
- }
481
- if (!["http:", "https:"].includes(url.protocol)) {
482
- throw new Error("Only http/https URLs are supported");
483
- }
484
- if (!["localhost", "127.0.0.1", "::1"].includes(url.hostname)) {
485
- throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);
486
- }
487
- }
488
-
489
641
  // src/serve.ts
490
642
  import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
491
643
  import { ensureDaemonRunning, startStdioServer } from "@noir-ai/daemon";
@@ -519,12 +671,12 @@ export {
519
671
  isInteractive,
520
672
  log,
521
673
  info,
674
+ warn,
522
675
  table,
523
676
  fail,
524
677
  inferExitCode,
525
678
  handleError,
526
679
  doctor,
527
- init,
528
680
  serve
529
681
  };
530
- //# sourceMappingURL=chunk-UXT7YWQY.js.map
682
+ //# sourceMappingURL=chunk-I3L3KNJM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/output.ts","../src/commands/doctor.ts","../src/serve.ts"],"sourcesContent":["// S9 t2 — centralized output + exit-code infrastructure for `@noir-ai/cli`.\n//\n// Stream discipline (S9 DS-4): machine-readable data → stdout; every other\n// diagnostic (progress, warnings, errors, tables, help) → stderr. Under `--json`\n// the decorated stderr helpers are silenced so stdout stays pristine JSON, and\n// `fail()` emits a structured `{ok:false,error}` envelope to stdout instead.\n//\n// Decoration (picocolors / cli-table3 / ora) auto-disables under `--json`,\n// `--quiet`, `CI`, `NO_COLOR`, or a non-TTY, so the same code path is safe in an\n// interactive shell, a pipe, and CI.\n//\n// Exit codes (S9 DS-4): 0 ok · 1 error · 2 usage · 3 not-found · 4 daemon-down ·\n// 5 cancelled. These constants live HERE (the single source of truth); bin.ts\n// re-exports them so existing imports from `./bin.js` keep working.\n\nimport Table from 'cli-table3';\nimport { CommanderError } from 'commander';\nimport ora, { type Ora } from 'ora';\nimport pc from 'picocolors';\n\n// ---------------------------------------------------------------------------\n// Exit-code contract + error types\n// ---------------------------------------------------------------------------\nexport const EXIT = {\n OK: 0,\n ERROR: 1,\n USAGE: 2,\n NOT_FOUND: 3,\n DAEMON_DOWN: 4,\n CANCELLED: 5,\n} as const;\n\nexport type ExitCode = (typeof EXIT)[keyof typeof EXIT];\n\n/**\n * Application-level CLI error carrying an S9 exit code. Thrown directly by\n * bin.ts for legacy usage paths; `fail()` is the ergonomic throw helper.\n * `inferExitCode` / `handleError` map it onto `process.exitCode`.\n */\nexport class NoirCliError extends Error {\n readonly exitCode: number;\n constructor(exitCode: number, message: string) {\n super(message);\n this.name = 'NoirCliError';\n this.exitCode = exitCode;\n }\n}\n\n/**\n * The `code` stamped onto every CommanderError our own `fail()` throws, so\n * `inferExitCode` can tell \"we threw this on purpose with an authoritative\n * exitCode\" apart from \"commander threw this for help/usage/unknown-command\"\n * (which must be remapped onto the S9 contract by `commanderExitCode`).\n */\nconst NOIR_ERROR_CODE = 'noir.error';\n\n// ---------------------------------------------------------------------------\n// Global-option shape. Commander stores `--no-input` under the attribute\n// `input` (default `true`; the flag sets it `false`) — see commander's\n// `Option.attributeName()` which strips the leading `no-`. Both `input`\n// (commander's real key) and `noInput` (the spec's intent) are accepted so\n// callers may pass either commander's raw globals or a hand-built object.\n// ---------------------------------------------------------------------------\nexport interface CliOptions {\n readonly json?: boolean;\n readonly quiet?: boolean;\n readonly verbose?: boolean;\n /** Commander's storage for `--no-input` (`false` ⇒ no input). */\n readonly input?: boolean;\n /** Convenience alias matching the `--no-input` intent (`true` ⇒ no input). */\n readonly noInput?: boolean;\n}\n\nfunction isJsonMode(opts: CliOptions): boolean {\n return opts.json === true;\n}\n\nfunction isQuietMode(opts: CliOptions): boolean {\n return opts.quiet === true;\n}\n\n/** True when the user has asked for no interactive input (either spelling). */\nexport function isNoInput(opts: CliOptions = {}): boolean {\n return opts.noInput === true || opts.input === false;\n}\n\nfunction envFlagSet(name: string): boolean {\n // NO_COLOR spec: present AND non-empty (regardless of value) disables color.\n const v = process.env[name];\n return v !== undefined && v !== '';\n}\n\nfunction isCiEnv(): boolean {\n // CI conventions: set to `true`, `1`, or a server name. Treat explicit\n // opt-outs (\"0\"/\"false\") as \"not CI\" so users can force-disable the guard.\n const v = process.env.CI;\n if (v === undefined || v === '') return false;\n return v !== '0' && v !== 'false';\n}\n\n// ---------------------------------------------------------------------------\n// Interactivity gate (DS-3/DS-7). Drives both the @clack home menu and the\n// decoration of every helper below. Requires BOTH stdin and stdout to be TTYs:\n// @clack reads keypresses from stdin while ora/picocolors render to stdout.\n// ---------------------------------------------------------------------------\nexport function isInteractive(opts: CliOptions = {}): boolean {\n if (isJsonMode(opts) || isNoInput(opts)) return false;\n if (isCiEnv() || envFlagSet('NO_COLOR')) return false;\n return process.stdin.isTTY === true && process.stdout.isTTY === true;\n}\n\n// ---------------------------------------------------------------------------\n// Data output (stdout). The ONLY helper here that writes to stdout; under\n// `--json` a command calls this exactly once with its full payload and never\n// mixes other stdout writes, keeping machine output pristine.\n// ---------------------------------------------------------------------------\nexport function json(obj: unknown): void {\n process.stdout.write(`${JSON.stringify(obj)}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Human diagnostics (stderr). picocolors auto-strips ANSI under NO_COLOR /\n// non-TTY, and each helper additionally short-circuits under `--json`\n// (decoration off, payload is the output) — and `info`/`log`/`success` also\n// under `--quiet`. `warn`/`error` survive `--quiet` (they carry signal).\n// ---------------------------------------------------------------------------\nexport function log(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${msg}\\n`);\n}\n\nexport function info(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${pc.cyan(msg)}\\n`);\n}\n\nexport function success(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${pc.green(msg)}\\n`);\n}\n\nexport function warn(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts)) return;\n process.stderr.write(`${pc.yellow(msg)}\\n`);\n}\n\nexport function error(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts)) return;\n process.stderr.write(`${pc.red(msg)}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Tables (stderr). Suppressed entirely under `--json` — the command has\n// already emitted the rows as a JSON array via `json()`. cli-table3 itself is\n// isTTY-aware, but we gate it here so JSON pipes never see table text.\n// ---------------------------------------------------------------------------\nexport function table(\n rows: readonly Record<string, unknown>[],\n cols: readonly string[],\n opts: CliOptions = {},\n): void {\n if (isJsonMode(opts)) return;\n if (rows.length === 0) {\n info('(no rows)', opts);\n return;\n }\n const t = new Table({ head: [...cols] });\n for (const row of rows) {\n t.push(cols.map((col) => formatCell(row[col])));\n }\n process.stderr.write(`${t.toString()}\\n`);\n}\n\nfunction formatCell(value: unknown): string {\n if (value === undefined || value === null) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n return JSON.stringify(value);\n}\n\n// ---------------------------------------------------------------------------\n// Spinner (stderr). ora when interactive; a no-op otherwise so scriptable /\n// CI / `--json` / `--quiet` runs pay nothing and never animate a pipe.\n// ---------------------------------------------------------------------------\nexport interface Spinner {\n start(text?: string): Spinner;\n stop(): Spinner;\n clear(): Spinner;\n succeed(text?: string): void;\n fail(text?: string): void;\n warn(text?: string): void;\n info(text?: string): void;\n text: string;\n}\n\nclass NoopSpinner implements Spinner {\n text = '';\n start(_text?: string): Spinner {\n return this;\n }\n stop(): Spinner {\n return this;\n }\n clear(): Spinner {\n return this;\n }\n succeed(_text?: string): void {}\n fail(_text?: string): void {}\n warn(_text?: string): void {}\n info(_text?: string): void {}\n}\n\n/** Wraps an `ora` instance so the `Spinner` interface stays decoupled from ora's exact type. */\nclass OraSpinner implements Spinner {\n private readonly handle: Ora;\n\n constructor(initialText: string) {\n this.handle = ora(initialText);\n }\n\n get text(): string {\n return this.handle.text;\n }\n set text(value: string) {\n this.handle.text = value;\n }\n\n start(text?: string): Spinner {\n if (text !== undefined) this.handle.text = text;\n this.handle.start();\n return this;\n }\n\n stop(): Spinner {\n this.handle.stop();\n return this;\n }\n\n clear(): Spinner {\n this.handle.clear();\n return this;\n }\n\n succeed(text?: string): void {\n this.handle.succeed(text);\n }\n\n fail(text?: string): void {\n this.handle.fail(text);\n }\n\n warn(text?: string): void {\n this.handle.warn(text);\n }\n\n info(text?: string): void {\n this.handle.info(text);\n }\n}\n\nexport function spinner(text = '', opts: CliOptions = {}): Spinner {\n if (isJsonMode(opts) || isQuietMode(opts) || !isInteractive(opts)) {\n return new NoopSpinner();\n }\n return new OraSpinner(text);\n}\n\n// ---------------------------------------------------------------------------\n// Failure + exit-code mapping\n// ---------------------------------------------------------------------------\n\n/**\n * Write a diagnostic and throw a `CommanderError` carrying the S9 exit code,\n * so bin.ts's `exitOverride` + `handleError` surface it as `process.exitCode`\n * without any mid-action `process.exit`. Under `--json` the message becomes a\n * structured `{ok:false,error}` envelope on STDOUT; otherwise plain text on\n * STDERR. Always throws (`never`).\n */\nexport function fail(exitCode: number, message: string, opts: CliOptions = {}): never {\n if (isJsonMode(opts)) {\n process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}\\n`);\n } else if (message.length > 0) {\n process.stderr.write(`${message}\\n`);\n }\n throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);\n}\n\n/**\n * Guard for commands that would otherwise block on a prompt. Honors the S9\n * hard rule: \"no blocking prompts when !isTTY or --no-input — error exit 2\n * naming the missing flag instead\". `hint` describes the operation so the\n * message points the user at the flag-based alternative.\n */\nexport function requireInteractive(opts: CliOptions, hint: string): void {\n if (!isInteractive(opts)) {\n fail(\n EXIT.USAGE,\n `${hint} needs an interactive terminal, but input is disabled (non-TTY, --no-input, --json, CI, or NO_COLOR). Re-run in a TTY or supply the value as a flag.`,\n opts,\n );\n }\n}\n\n// Remap commander's own exit codes onto the S9 contract:\n// help/version → 0 · unknown command → 3 · every other usage error → 2\nfunction commanderExitCode(err: CommanderError): number {\n switch (err.code) {\n case 'commander.helpDisplayed':\n case 'commander.versionDisplayed':\n case 'commander.version': // commander v12 uses this code for --version (exit 0)\n return EXIT.OK;\n case 'commander.unknownCommand':\n return EXIT.NOT_FOUND;\n default:\n return EXIT.USAGE;\n }\n}\n\n/** Map any thrown value onto the S9 exit-code contract. */\nexport function inferExitCode(err: unknown): number {\n if (err instanceof NoirCliError) return err.exitCode;\n if (err instanceof CommanderError) {\n // Errors our own `fail()` threw carry a `noir.*` code and an authoritative\n // exitCode — respect it. Commander's own errors are remapped by `code`.\n if (err.code.startsWith('noir.')) return err.exitCode;\n return commanderExitCode(err);\n }\n return EXIT.ERROR;\n}\n\n/**\n * Map a thrown error onto stderr + `process.exitCode`. Never throws, never\n * calls `process.exit` (commander's `exitOverride` already prevented that for\n * commander's own errors). Used by the bin entry's `run()`.\n */\nexport function handleError(err: unknown): void {\n if (err instanceof NoirCliError) {\n if (err.message.length > 0) process.stderr.write(`${err.message}\\n`);\n } else if (err instanceof CommanderError) {\n // If `fail()` threw it, the message is already written (stderr or the JSON\n // stdout envelope); if commander threw it, `configureOutput.writeErr`\n // already wrote it during parse. Don't double-write either way.\n } else {\n process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = inferExitCode(err);\n}\n","// S9 t6 — `noir doctor`.\n//\n// Environment + project health. Runs a fixed set of checks and renders a\n// results table (human, stderr) or the versioned `{ok,data}` envelope (--json,\n// stdout). Exit 1 if any CRITICAL (fail) check is unhealthy, else 0.\n//\n// Severity model (S9 spec F8 / t6):\n// - ok — healthy.\n// - warn — degraded but usable (daemon down, onnx missing, provider key\n// missing, project not initialized). NEVER triggers exit 1.\n// - fail — CRITICAL: the product is broken on this host. Only `config` (parse\n// error), `native deps` (better-sqlite3 / sqlite-vec will not load),\n// and `store` (open fails) can fail. Exit 1 if any fail.\n//\n// Honesty rules: provider status uses `resolveModelConfig` — a PURE projection\n// of the user's config + env-var NAME presence; it makes NO live call (blueprint\n// D5/DS-6). Native-dep probes are best-effort try/catch (the store's native\n// binaries are probed via `vecAvailability`, the single cross-package surface;\n// onnxruntime-node is the context engine's own dep and is probed separately).\n//\n// Cold-start (NF6): the @noir-ai/store import (which loads better-sqlite3) is\n// DYNAMIC, inside the action, so merely running `noir status` / `noir --help`\n// never pays the native-bindings cost. @noir-ai/model is imported eagerly — its\n// provider self-registration is light (SDKs load lazily inside `complete()`).\n//\n// Stream discipline (S9 DS-4): `--json` writes `{ok:true, data:{checks,summary}}`\n// to stdout (the only stdout write). Doctor always emits `ok:true` because the\n// COMMAND succeeded in producing a diagnosis; the system-health pass/fail is\n// signaled by the EXIT CODE (0 healthy / 1 critical failure) — the same shape\n// `npm audit --json` uses (valid JSON out, non-zero exit when issues found).\n\nimport { spawnSync } from 'node:child_process';\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { dirname, join, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { loadProjectInfo, NOIR_VERSION, type ProjectInfo, paths } from '@noir-ai/core';\nimport { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from '@noir-ai/create';\nimport { pidAlive, readDaemonRecord } from '@noir-ai/daemon';\nimport { resolveModelConfig } from '@noir-ai/model';\nimport {\n type CliOptions,\n EXIT,\n error as err,\n log,\n NoirCliError,\n success,\n table,\n warn,\n} from '../output.js';\n\n/** Options accepted by `doctor` (the global flags only). */\nexport interface DoctorOptions extends CliOptions {}\n\ntype Severity = 'ok' | 'warn' | 'fail';\n\n/** One row of the doctor report (table cell + `--json` element). */\nexport interface CheckResult {\n name: string;\n status: Severity;\n detail: string;\n}\n\n/** The `data` payload of `noir doctor --json`. */\nexport interface DoctorPayload {\n noir: string;\n checks: CheckResult[];\n summary: { ok: number; warn: number; fail: number };\n /** Scaffold-version drift (slice S). `onDisk` is null when the stamp is\n * absent (uninitialized or pre-Slice-S project); `drift` is true only when\n * a stamp is present AND differs from the engine's current version. */\n scaffold: { onDisk: string | null; current: string; drift: boolean };\n /** RULES.md budget measurement (R5). `null` when the project isn't\n * initialized OR `.noir/rules/RULES.md` is absent — in either case there\n * is nothing to measure and the check row stays informational (warn skip\n * or ok \"no RULES.md\"). Never drives `fail` — over-budget is a `warn`. */\n rules: {\n onDisk: { bytes: number; lines: number };\n budget: { kb: number; maxLines: number };\n over: boolean;\n } | null;\n /** S10 host report. `null` when the project isn't initialized (no resolved\n * host); otherwise carries the active `host` + the list of repo-relative\n * primary artifact paths the doctor verified (AGENTS.md for agents-md/\n * cursor/opencode; the host's native context file when distinct from\n * AGENTS.md (claude → CLAUDE.md, gemini → GEMINI.md); the host's MCP\n * config). The check row is `ok` when\n * all present, `warn` (NEVER `fail`) when any are missing — re-running\n * `noir sync` restores them. */\n host: { active: HostId; expected: string[]; missing: string[] } | null;\n /** S11 publish-readiness report (repo-developer-facing, advisory). `null`\n * when doctor is NOT running from a monorepo checkout (a global install has\n * no workspace package.json set to validate → the row reports `ok`\n * \"skipped\"). Otherwise carries the count of workspace `package.json` files\n * validated + the list of advisory issues found (bad semver, missing\n * `files`, cli missing `bin`, …). The row severity is `warn` when any issues\n * are present, `ok` otherwise — NEVER `fail` (a missing field does not break\n * the local install). */\n publish: { checked: number; issues: string[] } | null;\n}\n\n// ---------------------------------------------------------------------------\n// Small helpers\n// ---------------------------------------------------------------------------\n\n/** Best-effort probe of the `onnxruntime-node` native binding. */\nasync function probeOnnx(): Promise<{ ok: boolean; reason: string }> {\n try {\n // Variable specifier (not a string literal) so tsc does NOT try to resolve\n // a module the CLI package does not directly depend on — onnxruntime-node\n // is the context engine's dep and resolves through ITS node_modules at\n // runtime. Best-effort: hosts that hoist it resolve; otherwise not-loadable.\n const spec = 'onnxruntime-node';\n await import(spec);\n return { ok: true, reason: 'loadable' };\n } catch (e) {\n return { ok: false, reason: e instanceof Error ? e.message : String(e) };\n }\n}\n\n/** Human label for the onnx probe outcome (hides confusing module-not-found noise). */\nfunction describeOnnx(o: { ok: boolean; reason: string }): string {\n if (o.ok) return 'loadable';\n if (/Cannot find (module|package)/i.test(o.reason)) {\n return 'not resolvable from CLI probe (best-effort)';\n }\n const r = o.reason.length > 60 ? `${o.reason.slice(0, 59)}…` : o.reason;\n return `probe failed: ${r}`;\n}\n\nfunction fmtUptime(sec: number): string {\n if (!Number.isFinite(sec) || sec < 0) return 'unknown';\n if (sec < 60) return `${sec}s`;\n if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;\n return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;\n}\n\nfunction msg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n// ---------------------------------------------------------------------------\n// Checks. Each appends to `checks`; project-dependent ones short-circuit to a\n// `warn` \"skipped\" row when the project isn't initialized (so the table still\n// accounts for them, but a pre-init `noir doctor` never exit-1s on them).\n// ---------------------------------------------------------------------------\n\nasync function checkRuntime(checks: CheckResult[]): Promise<void> {\n checks.push({\n name: 'runtime',\n status: 'ok',\n detail: `noir ${NOIR_VERSION} · node ${process.version} · ${process.platform}`,\n });\n}\n\nfunction checkConfig(root: string): { project: ProjectInfo | undefined; result: CheckResult } {\n const configPath = paths.config(root);\n if (!existsSync(configPath)) {\n return {\n project: undefined,\n result: {\n name: 'config',\n status: 'warn',\n detail: `no .noir/config.yml in ${root} — run \\`noir init\\``,\n },\n };\n }\n try {\n const project = loadProjectInfo(root);\n return {\n project,\n result: {\n name: 'config',\n status: 'ok',\n detail: `host=${project.config.host} · mode=${project.config.mode}`,\n },\n };\n } catch (e) {\n return {\n project: undefined,\n result: { name: 'config', status: 'fail', detail: `config parse error: ${msg(e)}` },\n };\n }\n}\n\nasync function checkDaemon(checks: CheckResult[]): Promise<void> {\n const rec = readDaemonRecord();\n if (!rec) {\n checks.push({\n name: 'daemon',\n status: 'warn',\n detail: 'not running (start with `noir daemon start`)',\n });\n return;\n }\n if (!pidAlive(rec.pid)) {\n checks.push({\n name: 'daemon',\n status: 'warn',\n detail: `stale record (pid ${rec.pid} not alive)`,\n });\n return;\n }\n try {\n const res = await fetch(`http://127.0.0.1:${rec.port}/health`);\n const body =\n res.status === 200 ? ((await res.json()) as { ok?: boolean; uptimeSec?: number }) : null;\n if (body && body.ok === true) {\n checks.push({\n name: 'daemon',\n status: 'ok',\n detail: `running (pid ${rec.pid}, port ${rec.port}, up ${fmtUptime(body.uptimeSec ?? 0)})`,\n });\n return;\n }\n checks.push({\n name: 'daemon',\n status: 'warn',\n detail: `record present but /health unreachable on port ${rec.port}`,\n });\n } catch {\n checks.push({\n name: 'daemon',\n status: 'warn',\n detail: `record present but /health unreachable on port ${rec.port}`,\n });\n }\n}\n\nasync function checkNativeDeps(checks: CheckResult[]): Promise<{ vecOk: boolean }> {\n // vecAvailability probes better-sqlite3 + sqlite-vec together (the store opens\n // a Database and loads sqlite-vec). Dynamic import keeps the native bindings\n // out of cold-start for every other command.\n const { vecAvailability } = await import('@noir-ai/store');\n const vec = vecAvailability();\n const onnx = await probeOnnx();\n const vecOk = vec.ok === true;\n const onnxOk = onnx.ok === true;\n // The sqlite side is CRITICAL (the store cannot open without it). onnx only\n // backs local embeddings, so its absence is a warning (search → BM25-only).\n const status: Severity = vecOk ? (onnxOk ? 'ok' : 'warn') : 'fail';\n checks.push({\n name: 'native deps',\n status,\n detail: `better-sqlite3+sqlite-vec: ${vecOk ? 'loadable' : vec.ok === false ? vec.reason : 'unknown'} · onnxruntime-node: ${describeOnnx(onnx)}`,\n });\n return { vecOk };\n}\n\nasync function checkStore(\n checks: CheckResult[],\n project: ProjectInfo | undefined,\n root: string,\n): Promise<void> {\n if (!project) {\n checks.push({ name: 'store', status: 'warn', detail: 'skipped — not initialized' });\n return;\n }\n const dbPath = paths.storeDb(root, project.id);\n if (!existsSync(dbPath)) {\n // Fresh project: the store DB is created LAZILY when the daemon first runs\n // (or when content is indexed) — `noir init`/`create` does not pre-create it.\n // Its absence on an otherwise-initialized project is EXPECTED, not a failure:\n // surfacing it as `fail` would alarm every new user on their first `noir doctor`.\n // Warn (never exit-1) with the action that materializes the store.\n checks.push({\n name: 'store',\n status: 'warn',\n detail: `not created yet — the store DB is created when the daemon first runs (\\`noir daemon start\\` or index content); expected at ${dbPath}`,\n });\n return;\n }\n const { openStore } = await import('@noir-ai/store');\n let store: { close(): Promise<void> | void } | undefined;\n try {\n store = await openStore({ projectId: project.id, root, readonly: true });\n checks.push({\n name: 'store',\n status: 'ok',\n detail: `opens readonly (db: ${paths.storeDb(root, project.id)})`,\n });\n } catch (e) {\n checks.push({ name: 'store', status: 'fail', detail: `open failed: ${msg(e)}` });\n } finally {\n if (store) {\n try {\n await Promise.resolve(store.close());\n } catch {\n /* a close error must not mask the open result */\n }\n }\n }\n}\n\nfunction checkEmbedder(\n checks: CheckResult[],\n project: ProjectInfo | undefined,\n vecOk: boolean,\n): void {\n if (!project) {\n checks.push({ name: 'embedder', status: 'warn', detail: 'skipped — not initialized' });\n return;\n }\n const emb = project.config.context.embedder;\n const kind = emb.kind;\n const model = typeof emb.model === 'string' && emb.model.length > 0 ? emb.model : '<unset>';\n if (kind === 'none') {\n checks.push({\n name: 'embedder',\n status: 'ok',\n detail: `kind=none (BM25-only) · dim=${emb.dim}`,\n });\n return;\n }\n if (!vecOk) {\n // vec native layer is the embedder's hard prereq (vecs write into vec0).\n checks.push({\n name: 'embedder',\n status: 'warn',\n detail: `${kind}/${model} (${emb.dim}-dim) configured but vec native layer unavailable — search degrades to BM25`,\n });\n return;\n }\n checks.push({\n name: 'embedder',\n status: 'ok',\n detail: `${kind}/${model} (${emb.dim}-dim) · vec backend available`,\n });\n}\n\nfunction checkProvider(checks: CheckResult[], project: ProjectInfo | undefined): void {\n if (!project) {\n checks.push({ name: 'provider', status: 'warn', detail: 'skipped — not initialized' });\n return;\n }\n // PURE projection — resolveModelConfig reads config + env-var NAMES only; it\n // NEVER makes a live call and NEVER infers a provider from env-var presence.\n const resolved = resolveModelConfig(project.config.model);\n const names = Object.keys(resolved.providers);\n if (names.length === 0) {\n checks.push({\n name: 'provider',\n status: 'ok',\n detail: 'none configured (offline mode; drafting degrades to templates)',\n });\n return;\n }\n const parts: string[] = [];\n let missing = false;\n for (const name of names) {\n const p = resolved.providers[name];\n if (!p) continue;\n const key = p.hasKey ? 'key present' : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : 'anonymous';\n if (!p.hasKey && p.apiKeyEnv) missing = true;\n parts.push(`${name}/${p.model ?? '?'} (${key})`);\n }\n // Provider readiness is never CRITICAL — the model layer degrades to `null`\n // (templates) when a key is missing, so this is an ok/warn signal only.\n checks.push({\n name: 'provider',\n status: missing ? 'warn' : 'ok',\n detail: parts.join(' · '),\n });\n}\n\nfunction checkScaffoldVersion(\n checks: CheckResult[],\n root: string,\n): {\n onDisk: string | null;\n current: string;\n drift: boolean;\n} {\n // readScaffoldVersion never throws — it returns null on an absent/malformed\n // stamp, so doctor keeps reporting even on a partially-initialized project.\n const onDisk = readScaffoldVersion(root);\n const current = CURRENT_SCAFFOLD_VERSION;\n const drift = onDisk !== null && onDisk !== current;\n // Drift is NEVER critical: a stale scaffold still works (the manifest entries\n // are forward-compatible), it just means `noir init --upgrade` will run\n // pending migrations. Absent stamp on an uninitialized project → warn (parity\n // with the other project-dependent checks that skip-with-warn pre-init).\n const status: Severity = onDisk === null || drift ? 'warn' : 'ok';\n const detail =\n onDisk === null\n ? 'no .noir/scaffold-version stamp (run `noir init`)'\n : drift\n ? `on-disk ${onDisk} ≠ current ${current} (run \\`noir init --upgrade\\`)`\n : `${onDisk} (up to date)`;\n checks.push({ name: 'scaffold version', status, detail });\n return { onDisk, current, drift };\n}\n\n/**\n * Soft RULES.md budget cap (R5). Mirrors the Failure-Backed Rules doctrine:\n * an over-budget RULES.md tends to accumulate stale / speculative clauses, so\n * doctor warns (NEVER fails — purely a hygiene nudge) when the file exceeds\n * the configured `lengthBudgetKb` (default 6 KB) OR a 150-line ceiling. The\n * check is honest about the three \"nothing to measure\" cases:\n *\n * • project not initialized → skip-warn (parity with store/embedder/provider)\n * • RULES.md absent → ok informational (no file = no budget problem)\n * • RULES.md unreadable → warn (treat like a malformed-stamp: surface it)\n *\n * Returns the structured measurement for the `--json` envelope's `data.rules`,\n * or `null` when there is nothing to measure (absent / skipped).\n */\nfunction checkRulesMdBudget(\n checks: CheckResult[],\n root: string,\n project: ProjectInfo | undefined,\n): {\n onDisk: { bytes: number; lines: number };\n budget: { kb: number; maxLines: number };\n over: boolean;\n} | null {\n if (!project) {\n checks.push({ name: 'rules budget', status: 'warn', detail: 'skipped — not initialized' });\n return null;\n }\n const rulesPath = paths.rulesMd(root);\n if (!existsSync(rulesPath)) {\n // No RULES.md is fine — most projects don't have one yet. Informational ok\n // so a green run stays green without forcing a file creation.\n checks.push({\n name: 'rules budget',\n status: 'ok',\n detail: 'no .noir/rules/RULES.md',\n });\n return null;\n }\n // Read + measure. A read/decode failure surfaces as a warn (parallel to the\n // malformed-stamp handling in checkScaffoldVersion): the doctor keeps\n // reporting rather than crashing, and the user sees a clear hint.\n let content: string;\n try {\n content = readFileSync(rulesPath, 'utf8');\n } catch (e) {\n checks.push({\n name: 'rules budget',\n status: 'warn',\n detail: `RULES.md unreadable: ${msg(e)}`,\n });\n return null;\n }\n // bytes = UTF-8 file size on disk (the natural unit for a KB budget). lines\n // uses wc-style counting: count of '\\n' chars, +1 if the last line has no\n // trailing newline; 0 only for an empty file.\n const bytes = Buffer.byteLength(content, 'utf8');\n const lineCount =\n content.length === 0 ? 0 : content.split('\\n').length - (content.endsWith('\\n') ? 1 : 0);\n const lengthBudgetKb = project.config.rules.lengthBudgetKb;\n const budgetBytes = lengthBudgetKb * 1024;\n const maxLines = 150;\n const overBytes = bytes > budgetBytes;\n const overLines = lineCount > maxLines;\n const over = overBytes || overLines;\n const status: Severity = over ? 'warn' : 'ok';\n const kb = (n: number): string => (n / 1024).toFixed(1);\n const detail = over\n ? `OVER: ${kb(bytes)} KB / ${lengthBudgetKb} KB · ${lineCount}/${maxLines} lines — trim RULES.md (failure-backed clauses only) or raise rules.lengthBudgetKb`\n : `${kb(bytes)} KB / ${lengthBudgetKb} KB · ${lineCount}/${maxLines} lines (within budget)`;\n checks.push({ name: 'rules budget', status, detail });\n return {\n onDisk: { bytes, lines: lineCount },\n budget: { kb: lengthBudgetKb, maxLines },\n over,\n };\n}\n\nfunction summarize(checks: CheckResult[]): DoctorPayload['summary'] {\n let ok = 0;\n let warnN = 0;\n let fail = 0;\n for (const c of checks) {\n if (c.status === 'ok') ok++;\n else if (c.status === 'warn') warnN++;\n else fail++;\n }\n return { ok, warn: warnN, fail };\n}\n\n/**\n * S10 — host-artifacts presence check. Reports the ACTIVE host (read from\n * `project.config.host`) + verifies the host's primary emission paths exist on\n * disk. The expected set mirrors `buildHostArtifacts` in @noir-ai/create:\n *\n * - AGENTS.md for the hosts that emit it (agents-md/cursor/opencode — their\n * native context surface IS AGENTS.md). claude/gemini do NOT emit AGENTS.md\n * (their CLAUDE.md/GEMINI.md @-import the canonical .noir/ sources; a root\n * AGENTS.md would double-import them — fix-wave I1 removed that delta).\n * - The host's native context file when distinct from AGENTS.md\n * (claude → CLAUDE.md; gemini → GEMINI.md; agents-md/cursor/opencode →\n * none, AGENTS.md IS the context). Cursor's working-rules ride AGENTS.md's\n * `@.noir/rules/RULES.md` import — NO separate `.cursor/rules/noir-contract.mdc`\n * host-rules pointer (it was `noir-`-prefixed and the C3 cursor flat-skill\n * prune deleted it on every sync).\n * - The host's MCP config (.mcp.json / .gemini/mcp.json / .cursor/mcp.json /\n * opencode.json).\n *\n * Severity is `ok` when every expected path exists, `warn` when any are\n * missing (NEVER `fail` — a missing host artifact is restored by `noir sync`,\n * not a critical product failure). Mirrors the policy of the other\n * presence/projection checks (scaffold-version drift, RULES.md budget): warn,\n * surface the remediation, never block.\n *\n * Returns the structured payload for the `--json` envelope's `data.host`, or\n * `null` when the project isn't initialized (no resolved host).\n */\nfunction checkHostArtifacts(\n checks: CheckResult[],\n root: string,\n project: ProjectInfo | undefined,\n): { active: HostId; expected: string[]; missing: string[] } | null {\n if (!project) {\n checks.push({ name: 'host artifacts', status: 'warn', detail: 'skipped — not initialized' });\n return null;\n }\n const host = project.config.host;\n const adapter = resolveAdapter(host);\n const ectx = { root };\n\n // Build the expected repo-relative path list. Mirrors buildHostArtifacts in\n // @noir-ai/create so a missing row maps 1:1 to a manifest entry the user can\n // restore with `noir sync`. Kept inline (not imported) so doctor stays a\n // READ-ONLY health probe with zero write-side coupling.\n const expected: string[] = [];\n // AGENTS.md — only for hosts whose emitContext IS the AGENTS.md (I1: claude/\n // gemini skip it — their native context file already covers .noir/).\n const emitsAgentsMd = host === 'agents-md' || host === 'cursor' || host === 'opencode';\n if (emitsAgentsMd) {\n expected.push(relOr(adapter.agentsMdPath?.(ectx) ?? 'AGENTS.md', root));\n }\n // Host-native context file (when distinct from AGENTS.md).\n if (host === 'claude') expected.push('CLAUDE.md');\n else if (host === 'gemini') expected.push('GEMINI.md');\n // Cursor has NO separate host-rules .mdc — the prior `noir-contract.mdc`\n // pointer was REMOVED (it was `noir-`-prefixed, so the C3 cursor flat-skill\n // prune in emitSkillsToDir deleted it on every noir init/create/sync).\n // Cursor's rules ride AGENTS.md's `@.noir/rules/RULES.md` import instead.\n // Host MCP config (fallback .mcp.json for claude/agents-md).\n expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? '.mcp.json', root));\n\n const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));\n const status: Severity = missing.length === 0 ? 'ok' : 'warn';\n const detail =\n missing.length === 0\n ? `host=${host} · all ${expected.length} primary artifacts present`\n : `host=${host} · missing ${missing.join(', ')} (run \\`noir sync\\` to restore)`;\n checks.push({ name: 'host artifacts', status, detail });\n return { active: host, expected, missing };\n}\n\n// ---------------------------------------------------------------------------\n// S11 — publish-readiness check (advisory, repo-developer-facing).\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the workspace `packages/` directory from the CLI's own location —\n * walk up from `import.meta.url` to the nearest `package.json` named\n * `@noir-ai/cli`, then return its PARENT dir (the `packages/` root) IF that\n * dir's parent carries the monorepo marker (`pnpm-workspace.yaml`). Returns\n * `null` when not running from a monorepo checkout (a global `npm install -g`\n * has no `packages/*` workspace to validate → the publish check skips).\n *\n * The walk is robust across layouts: source (`packages/cli/src/commands/`),\n * built (`packages/cli/dist/`), and bundled-chunk (`packages/cli/dist/`) all\n * walk up to `packages/cli/package.json`. In a global install the same walk\n * lands on `node_modules/@noir-ai/cli/package.json`, whose parent\n * (`node_modules/@noir-ai/`) has NO `pnpm-workspace.yaml` parent → null.\n */\nfunction resolveWorkspacePackagesDir(): string | null {\n let here = dirname(fileURLToPath(import.meta.url));\n // Bound the walk so a pathological layout never loops the filesystem.\n for (let i = 0; i < 8; i++) {\n const pkgPath = join(here, 'package.json');\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name?: string };\n if (pkg.name === '@noir-ai/cli') {\n // `here` is the cli package dir; `dirname(here)` is `packages/`.\n const packagesDir = dirname(here);\n // Monorepo gate: the repo root (`dirname(packagesDir)`) carries\n // `pnpm-workspace.yaml`. A global install fails this gate.\n if (existsSync(join(dirname(packagesDir), 'pnpm-workspace.yaml'))) {\n return packagesDir;\n }\n return null;\n }\n } catch {\n // Malformed package.json — keep walking.\n }\n }\n const parent = dirname(here);\n if (parent === here) break; // filesystem root\n here = parent;\n }\n return null;\n}\n\n/**\n * Permissive semver-ish regex for the advisory check. Accepts release and\n * pre-release/build forms (`1.0.0`, `1.1.0-beta.1`, `1.2.0+build.4`). Strict\n * enough to catch the obvious foot-guns (a git SHA, a literal \"latest\", a\n * missing patch) without pulling in the `semver` package for a warn-level\n * check. `npm publish` itself enforces rigor at pack time.\n */\nconst SEMVER_ISH = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;\n\n/** Best-effort `npm pack --dry-run` on the cli package — read-only (it does\n * NOT write the tarball). Returns `{ fileCount, distIncluded }` parsed from\n * npm's notice output, or `{ error }` when npm is unavailable / the pack\n * fails. Gated on npm being on PATH (the spec's expensive-but-optional leg):\n * we probe `npm --version` first and skip-with-note if it isn't resolvable. */\nfunction npmPackDryRun(\n cliPkgDir: string,\n): { fileCount: number | null; distIncluded: boolean | null } | { error: string } {\n // Probe npm availability first (cheap). shell:true so Windows finds npm.cmd.\n const probe = spawnSync('npm', ['--version'], { encoding: 'utf8', shell: true });\n if (probe.status !== 0 || probe.error) {\n return { error: probe.error ? probe.error.message : 'npm not on PATH' };\n }\n // `npm pack --dry-run` lists the tarball contents without writing the file.\n // It IS read-only (npm's own docs: \"--dry-run ... does not write anything\").\n const res = spawnSync('npm', ['pack', '--dry-run'], {\n cwd: cliPkgDir,\n encoding: 'utf8',\n shell: true,\n });\n if (res.status !== 0 || res.error) {\n return { error: res.error ? res.error.message : `npm pack exited ${res.status ?? '?'}` };\n }\n const out = `${res.stdout ?? ''}\\n${res.stderr ?? ''}`;\n // `npm notice total files: 42` — the canonical summary line.\n const totalMatch = out.match(/total files:\\s*(\\d+)/i);\n const fileCount = totalMatch?.[1] ? Number.parseInt(totalMatch[1], 10) : null;\n // `dist/` appears in the tarball contents (`npm notice <size> dist/<file>`).\n const distIncluded = /\\bdist\\//.test(out);\n return { fileCount, distIncluded };\n}\n\n/**\n * S11 publish-readiness check. For every workspace `package.json` under\n * `packagesDir`, validate publishability:\n * - `name` starts with `@noir-ai/`\n * - `version` matches {@link SEMVER_ISH}\n * - `files` is a non-empty array\n * - the cli package has a `bin` field\n *\n * Optionally runs `npm pack --dry-run` on the cli package (best-effort, gated\n * on npm being available) to report the file count + confirm `dist/` ships.\n *\n * Severity is ALWAYS `ok` or `warn` — NEVER `fail`: a missing/odd package.json\n * field does not break the local install (this is an advisory nudge to repo\n * developers, not a project-facing health check). The check runs ALWAYS (it\n * produces a row in every doctor report) but returns `null` from\n * `data.publish` when not running from a monorepo (`packagesDir === null`).\n */\nexport function checkPublish(\n checks: CheckResult[],\n packagesDir: string | null,\n opts: { skipNpmPack?: boolean } = {},\n): { checked: number; issues: string[] } | null {\n if (packagesDir === null) {\n checks.push({\n name: 'publish',\n status: 'ok',\n detail: 'skipped — not a monorepo checkout (no packages/* workspace)',\n });\n return null;\n }\n // Discover workspace package.json files (depth-1 dirs only — no glob dep).\n let entries: string[] = [];\n try {\n entries = readdirSync(packagesDir, { withFileTypes: true })\n .filter((d) => d.isDirectory())\n .map((d) => d.name);\n } catch (e) {\n checks.push({\n name: 'publish',\n status: 'warn',\n detail: `could not read ${packagesDir}: ${msg(e)}`,\n });\n return { checked: 0, issues: [`unreadable packages dir: ${msg(e)}`] };\n }\n\n const issues: string[] = [];\n let checked = 0;\n let cliDir: string | null = null;\n for (const name of entries) {\n const pkgPath = join(packagesDir, name, 'package.json');\n if (!existsSync(pkgPath)) continue;\n let pkg: Record<string, unknown>;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as Record<string, unknown>;\n } catch (e) {\n issues.push(`${name}/package.json: unparseable (${msg(e)})`);\n checked++;\n continue;\n }\n checked++;\n const pkgName = typeof pkg.name === 'string' ? pkg.name : '';\n if (!pkgName.startsWith('@noir-ai/')) {\n issues.push(`${name}/package.json: name '${pkgName}' is not @noir-ai/*`);\n }\n const version = typeof pkg.version === 'string' ? pkg.version : '';\n if (!SEMVER_ISH.test(version)) {\n issues.push(`${name}/package.json: version '${version}' is not semver`);\n }\n if (!Array.isArray(pkg.files) || pkg.files.length === 0) {\n issues.push(`${name}/package.json: files missing or empty`);\n }\n if (pkgName === '@noir-ai/cli') {\n if (pkg.bin == null || (typeof pkg.bin === 'object' && Object.keys(pkg.bin).length === 0)) {\n issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);\n }\n cliDir = join(packagesDir, name);\n }\n }\n\n // Best-effort npm pack --dry-run on the cli package. Gated on npm being\n // available; a failure here is surfaced in the detail but does NOT add an\n // issue (it is environmental, not a package.json defect). `skipNpmPack` is a\n // testability seam — synthetic test fixtures have no real `dist/`, so the\n // dist-absent signal would otherwise pollute the package.json-validation\n // assertions. Production (`doctor()`) leaves it unset.\n let packNote = '';\n if (cliDir && opts.skipNpmPack !== true) {\n const pack = npmPackDryRun(cliDir);\n if ('error' in pack) {\n packNote = ` · npm pack skipped (${pack.error})`;\n } else {\n const count = pack.fileCount == null ? '?' : String(pack.fileCount);\n const dist = pack.distIncluded ? 'dist/ included' : 'dist/ NOT in tarball';\n packNote = ` · npm pack: ${count} files, ${dist}`;\n if (pack.distIncluded === false) {\n issues.push('cli npm pack --dry-run: dist/ absent from the tarball');\n }\n }\n }\n\n const status: Severity = issues.length > 0 ? 'warn' : 'ok';\n const detail =\n issues.length === 0\n ? `${checked}/${entries.length} packages publish-ready${packNote}`\n : `${issues.length} issue${issues.length === 1 ? '' : 's'} across ${checked} package${checked === 1 ? '' : 's'}${packNote}`;\n checks.push({ name: 'publish', status, detail });\n return { checked, issues };\n}\n\n/** Repo-relative POSIX form of `abs` under `root`. Defensive (N2): rejects\n * paths that escape `root` — mirrors `hostRel` in @noir-ai/create/manifest.ts.\n * A future adapter that returns a stray path fails loudly here instead of\n * producing a misleading \"missing\" row for a path doctor would never probe.\n * Relative literals (e.g. `'AGENTS.md'`, `'.mcp.json'`) pass through unchanged\n * AFTER the `..`-escape check so a buggy literal could not exfiltrate either. */\nfunction relOr(abs: string, root: string): string {\n const posix = abs.replace(/\\\\/g, '/');\n if (posix.startsWith('/')) {\n const rel = relative(root, abs).replace(/\\\\/g, '/');\n if (rel.length === 0 || rel.startsWith('..') || rel.startsWith('/')) {\n throw new Error(`doctor: host path '${abs}' is not under root '${root}'`);\n }\n return rel;\n }\n // Already-relative literal: still reject `..` segments (a `../etc/passwd`\n // literal from a buggy adapter must not silently pass through).\n if (posix.split('/').some((seg) => seg === '..')) {\n throw new Error(`doctor: host path '${abs}' escapes root '${root}'`);\n }\n return posix;\n}\n\n/** Join `root` with a repo-relative POSIX path for an existsSync probe. */\nfunction joinRoot(root: string, relPath: string): string {\n return `${root}/${relPath}`;\n}\n\nfunction renderHuman(payload: DoctorPayload, opts: CliOptions): void {\n log(\n `noir doctor — ${payload.checks.length} check${payload.checks.length === 1 ? '' : 's'}`,\n opts,\n );\n table(\n payload.checks.map((c) => ({\n Check: c.name,\n Status: c.status.toUpperCase(),\n Detail: c.detail,\n })),\n ['Check', 'Status', 'Detail'],\n opts,\n );\n const { ok, warn: warnN, fail } = payload.summary;\n if (fail > 0) {\n err(\n `${fail} critical check${fail === 1 ? '' : 's'} failed (${warnN} warning${warnN === 1 ? '' : 's'}, ${ok} ok)`,\n opts,\n );\n } else if (warnN > 0) {\n warn(`all critical checks passed (${warnN} warning${warnN === 1 ? '' : 's'}, ${ok} ok)`, opts);\n } else {\n success(`all ${ok} check${ok === 1 ? '' : 's'} passed`, opts);\n }\n}\n\n/**\n * `noir doctor`: run all checks, render, and exit.\n *\n * Under `--json` writes `{ok:true, data: DoctorPayload}` to stdout (always —\n * the command produced a diagnosis). The exit code carries health: 0 when no\n * check failed, 1 when any CRITICAL (`fail`) check is unhealthy.\n */\nexport async function doctor(opts: DoctorOptions = {}): Promise<void> {\n const root = process.cwd();\n const checks: CheckResult[] = [];\n\n await checkRuntime(checks);\n const { project, result: configResult } = checkConfig(root);\n checks.push(configResult);\n await checkDaemon(checks);\n const { vecOk } = await checkNativeDeps(checks);\n await checkStore(checks, project, root);\n checkEmbedder(checks, project, vecOk);\n checkProvider(checks, project);\n const scaffold = checkScaffoldVersion(checks, root);\n const rules = checkRulesMdBudget(checks, root, project);\n const host = checkHostArtifacts(checks, root, project);\n const publish = checkPublish(checks, resolveWorkspacePackagesDir());\n\n const summary = summarize(checks);\n const payload: DoctorPayload = {\n noir: NOIR_VERSION,\n checks,\n summary,\n scaffold,\n rules,\n host,\n publish,\n };\n\n if (opts.json === true) {\n process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}\\n`);\n } else {\n renderHuman(payload, opts);\n }\n\n if (summary.fail > 0) {\n // Under --json the data envelope is already on stdout; throw with an empty\n // message so handleError sets exitCode=1 without a redundant stderr line.\n // Under human mode the summary line above already named the failures.\n throw new NoirCliError(\n EXIT.ERROR,\n opts.json === true ? '' : `noir doctor: ${summary.fail} critical check(s) failed`,\n );\n }\n}\n","import { loadProjectInfo } from '@noir-ai/core';\nimport { ensureDaemonRunning, startStdioServer } from '@noir-ai/daemon';\n\nexport async function serve(opts: { stdio: boolean }): Promise<void> {\n const project = loadProjectInfo(process.cwd());\n if (opts.stdio) {\n await startStdioServer({ project, transport: 'stdio', daemon: false });\n return; // stdio server runs until stdin closes\n }\n // Prefer the shared daemon; on failure fall back to in-process stdio so Noir\n // keeps working even when the daemon cannot start (FS-degradation path).\n try {\n const { url } = await ensureDaemonRunning({\n project,\n idleTimeoutSec: project.config.daemon.idleTimeoutSec,\n });\n process.stderr.write(\n `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)\\n`,\n );\n } catch (err) {\n process.stderr.write(\n `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.\\n`,\n );\n await startStdioServer({ project, transport: 'stdio', daemon: false });\n }\n}\n"],"mappings":"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","names":["msg","fail","loadProjectInfo"]}