@noir-ai/cli 1.3.0-beta.6 → 1.4.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,1243 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/output.ts
9
+ import Table from "cli-table3";
10
+ import { CommanderError } from "commander";
11
+ import ora from "ora";
12
+
13
+ // src/theme.ts
14
+ import pc from "picocolors";
15
+ var FORCE = pc.createColors(true);
16
+ var c = {
17
+ /** Green — success / healthy. */
18
+ ok: (s) => useColor() ? FORCE.green(s) : s,
19
+ /** Yellow — degraded but usable. */
20
+ warn: (s) => useColor() ? FORCE.yellow(s) : s,
21
+ /** Red — reserved strictly for ERROR. Never a table header. */
22
+ error: (s) => useColor() ? FORCE.red(s) : s,
23
+ /** Cyan — informational / brand-neutral accent for labels. */
24
+ info: (s) => useColor() ? FORCE.cyan(s) : s,
25
+ /** Blue — the Midnight Cobalt brand role (banner / accent). */
26
+ accent: (s) => useColor() ? FORCE.blue(s) : s,
27
+ /** Dim — secondary text, table borders when color is on. */
28
+ dim: (s) => useColor() ? FORCE.dim(s) : s,
29
+ /** Bold — emphasis (table headers compose this with `info`). */
30
+ bold: (s) => useColor() ? FORCE.bold(s) : s
31
+ };
32
+ var BADGE_SYMBOL = {
33
+ ok: "\u2713",
34
+ warn: "\u26A0",
35
+ error: "\u2717",
36
+ info: "\u2139"
37
+ };
38
+ function badge(state, label = state) {
39
+ const text = `${BADGE_SYMBOL[state]} ${label}`;
40
+ switch (state) {
41
+ case "ok":
42
+ return c.ok(text);
43
+ case "warn":
44
+ return c.warn(text);
45
+ case "error":
46
+ return c.error(text);
47
+ case "info":
48
+ return c.info(text);
49
+ }
50
+ }
51
+ function envFlagSet(name) {
52
+ const v = process.env[name];
53
+ return v !== void 0 && v !== "";
54
+ }
55
+ function isCiEnv() {
56
+ const v = process.env.CI;
57
+ if (v === void 0 || v === "") return false;
58
+ return v !== "0" && v !== "false";
59
+ }
60
+ function useColor() {
61
+ if (envFlagSet("NO_COLOR")) return false;
62
+ if (process.env.CLICOLOR_FORCE === "1") return true;
63
+ if (isCiEnv()) return false;
64
+ return pc.isColorSupported;
65
+ }
66
+ function accessibleMode() {
67
+ return envFlagSet("NOIR_ACCESSIBLE");
68
+ }
69
+ function terminalWidth() {
70
+ const fromEnv = process.env.COLUMNS;
71
+ if (fromEnv !== void 0 && fromEnv !== "") {
72
+ const n = Number(fromEnv);
73
+ if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
74
+ }
75
+ const cols = process.stdout.columns;
76
+ if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
77
+ return 80;
78
+ }
79
+
80
+ // src/output.ts
81
+ var EXIT = {
82
+ OK: 0,
83
+ ERROR: 1,
84
+ USAGE: 2,
85
+ NOT_FOUND: 3,
86
+ DAEMON_DOWN: 4,
87
+ CANCELLED: 5
88
+ };
89
+ var NoirCliError = class extends Error {
90
+ exitCode;
91
+ constructor(exitCode, message) {
92
+ super(message);
93
+ this.name = "NoirCliError";
94
+ this.exitCode = exitCode;
95
+ }
96
+ };
97
+ var NOIR_ERROR_CODE = "noir.error";
98
+ function isJsonMode(opts) {
99
+ return opts.json === true;
100
+ }
101
+ function isQuietMode(opts) {
102
+ return opts.quiet === true;
103
+ }
104
+ function isNoInput(opts = {}) {
105
+ return opts.noInput === true || opts.input === false;
106
+ }
107
+ function envFlagSet2(name) {
108
+ const v = process.env[name];
109
+ return v !== void 0 && v !== "";
110
+ }
111
+ function isInteractive(opts = {}) {
112
+ if (isJsonMode(opts) || isNoInput(opts)) return false;
113
+ if (isCiEnv() || envFlagSet2("NO_COLOR")) return false;
114
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
115
+ }
116
+ function resolveInteractive() {
117
+ if (flaggedNonInteractiveCli()) return false;
118
+ return isInteractive();
119
+ }
120
+ function flaggedNonInteractiveCli() {
121
+ const v = process.env.NOIR_NON_INTERACTIVE;
122
+ return v !== void 0 && v !== "";
123
+ }
124
+ function json(obj) {
125
+ process.stdout.write(`${JSON.stringify(obj)}
126
+ `);
127
+ }
128
+ function log(msg2, opts = {}) {
129
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
130
+ process.stderr.write(`${msg2}
131
+ `);
132
+ }
133
+ function info(msg2, opts = {}) {
134
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
135
+ process.stderr.write(`${c.info(msg2)}
136
+ `);
137
+ }
138
+ function success(msg2, opts = {}) {
139
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
140
+ process.stderr.write(`${c.ok(msg2)}
141
+ `);
142
+ }
143
+ function warn(msg2, opts = {}) {
144
+ if (isJsonMode(opts)) return;
145
+ process.stderr.write(`${c.warn(msg2)}
146
+ `);
147
+ }
148
+ function error(msg2, opts = {}) {
149
+ if (isJsonMode(opts)) return;
150
+ process.stderr.write(`${c.error(msg2)}
151
+ `);
152
+ }
153
+ function tip(msg2, opts = {}) {
154
+ if (isJsonMode(opts) || opts.noTips === true) return;
155
+ process.stderr.write(`${c.warn(msg2)}
156
+ `);
157
+ }
158
+ function table(rows, cols, opts = {}) {
159
+ if (isJsonMode(opts)) return;
160
+ if (rows.length === 0) {
161
+ info("(no rows)", opts);
162
+ return;
163
+ }
164
+ const t = new Table({
165
+ head: cols.map((col) => c.bold(c.info(col))),
166
+ // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
167
+ // default red header nor a colored border). All color is ours, via theme.
168
+ style: { head: [], border: [] },
169
+ colWidths: computeColWidths(rows, cols),
170
+ wordWrap: true,
171
+ truncate: "\u2026"
172
+ });
173
+ for (const row of rows) {
174
+ t.push(cols.map((col) => formatCell(row[col])));
175
+ }
176
+ process.stderr.write(`${t.toString()}
177
+ `);
178
+ }
179
+ function computeColWidths(rows, cols) {
180
+ const n = cols.length;
181
+ if (n === 0) return [];
182
+ const headerLen = cols.map((col) => col.length);
183
+ const natural = cols.map((col, i) => {
184
+ let max = Math.max(headerLen[i] ?? col.length, 3);
185
+ for (const row of rows) {
186
+ const len = formatCell(row[col]).length;
187
+ if (len > max) max = len;
188
+ }
189
+ return max;
190
+ });
191
+ const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
192
+ const naturalSum = natural.reduce((a, b) => a + b, 0);
193
+ if (naturalSum <= contentBudget) {
194
+ return natural.map((w) => w + 2);
195
+ }
196
+ const content = natural.slice();
197
+ let sum = naturalSum;
198
+ const floor = headerLen.map((h) => Math.max(h, 3));
199
+ while (sum > contentBudget) {
200
+ let idx = -1;
201
+ let widest = -1;
202
+ for (let i = 0; i < n; i++) {
203
+ const c2 = content[i] ?? 0;
204
+ const f = floor[i] ?? 3;
205
+ if (c2 > f && c2 > widest) {
206
+ widest = c2;
207
+ idx = i;
208
+ }
209
+ }
210
+ if (idx === -1) break;
211
+ const overshoot = sum - contentBudget;
212
+ const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
213
+ const cut = Math.min(overshoot, reducible);
214
+ content[idx] = (content[idx] ?? 0) - cut;
215
+ sum -= cut;
216
+ }
217
+ return content.map((w) => w + 2);
218
+ }
219
+ function formatCell(value) {
220
+ if (value === void 0 || value === null) return "";
221
+ if (typeof value === "string") return value;
222
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
223
+ return String(value);
224
+ }
225
+ return JSON.stringify(value);
226
+ }
227
+ function definitionList(rows, opts = {}) {
228
+ table(
229
+ rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
230
+ ["Field", "Value"],
231
+ opts
232
+ );
233
+ }
234
+ function fail(exitCode, message, opts = {}) {
235
+ if (isJsonMode(opts)) {
236
+ process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
237
+ `);
238
+ } else if (message.length > 0) {
239
+ process.stderr.write(`${message}
240
+ `);
241
+ }
242
+ throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
243
+ }
244
+ function requireInteractive(opts, hint) {
245
+ if (!isInteractive(opts)) {
246
+ fail(
247
+ EXIT.USAGE,
248
+ `${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.`,
249
+ opts
250
+ );
251
+ }
252
+ }
253
+ function commanderExitCode(err) {
254
+ switch (err.code) {
255
+ case "commander.helpDisplayed":
256
+ case "commander.versionDisplayed":
257
+ case "commander.version":
258
+ return EXIT.OK;
259
+ case "commander.unknownCommand":
260
+ return EXIT.NOT_FOUND;
261
+ default:
262
+ return EXIT.USAGE;
263
+ }
264
+ }
265
+ function inferExitCode(err) {
266
+ if (err instanceof NoirCliError) return err.exitCode;
267
+ if (err instanceof CommanderError) {
268
+ if (err.code.startsWith("noir.")) return err.exitCode;
269
+ return commanderExitCode(err);
270
+ }
271
+ return EXIT.ERROR;
272
+ }
273
+ function handleError(err) {
274
+ if (err instanceof NoirCliError) {
275
+ if (err.message.length > 0) process.stderr.write(`${err.message}
276
+ `);
277
+ } else if (err instanceof CommanderError) {
278
+ } else {
279
+ process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}
280
+ `);
281
+ }
282
+ process.exitCode = inferExitCode(err);
283
+ }
284
+
285
+ // src/conflict.ts
286
+ function flaggedNonInteractive() {
287
+ const v = process.env.NOIR_NON_INTERACTIVE;
288
+ return v !== void 0 && v !== "";
289
+ }
290
+ function buildConflictOpts(input = {}) {
291
+ if (input.force === true) {
292
+ return { conflictPolicy: "overwrite" };
293
+ }
294
+ if (input.interactive === false) {
295
+ return { conflictPolicy: "preserve" };
296
+ }
297
+ if (input.interactive === true) {
298
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
299
+ }
300
+ if (flaggedNonInteractive() || !isInteractive()) {
301
+ return { conflictPolicy: "preserve" };
302
+ }
303
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
304
+ }
305
+ function renderDiffPreview(ctx) {
306
+ const { lineDiff: ld } = __require("@noir-ai/create");
307
+ const lines = ld(ctx.existing, ctx.proposed);
308
+ if (lines.length === 0) return;
309
+ const out = [`noir: conflict in ${ctx.relPath}`];
310
+ for (const dl of lines) {
311
+ if (dl.type === "add") out.push(c.ok(`+ ${dl.line}`));
312
+ else if (dl.type === "del") out.push(c.error(`- ${dl.line}`));
313
+ else out.push(c.dim(` ${dl.line}`));
314
+ }
315
+ process.stderr.write(`${out.join("\n")}
316
+ `);
317
+ }
318
+ async function clackConflictResolver(ctx) {
319
+ renderDiffPreview(ctx);
320
+ const clack = await import("@clack/prompts");
321
+ const options = [
322
+ {
323
+ value: "replace",
324
+ label: "Replace",
325
+ hint: "overwrite with the template (discard your edits)"
326
+ },
327
+ { value: "rename", label: "Rename", hint: "keep yours as <file>.local; write the template" },
328
+ {
329
+ value: "duplicate",
330
+ label: "Create duplicate",
331
+ hint: "write template to <file>.noir; keep yours"
332
+ },
333
+ { value: "preserve", label: "Keep mine", hint: "skip; leave your file unchanged" },
334
+ { value: "cancel", label: "Cancel", hint: "abort the scaffold" }
335
+ ];
336
+ if (ctx.mergedWithMarkers !== void 0) {
337
+ options.splice(4, 0, {
338
+ value: "merge",
339
+ label: "Merge (with conflict markers)",
340
+ hint: "write the 3-way merge with <<<<<<< / ||||||| / ======= / >>>>>>> markers"
341
+ });
342
+ }
343
+ const choice = await clack.select({
344
+ message: `${ctx.relPath} was edited and differs from the Noir template.`,
345
+ initialValue: "preserve",
346
+ options
347
+ });
348
+ if (clack.isCancel(choice)) return "cancel";
349
+ const resolution = choice;
350
+ const mode = ctx.mode ?? "regenerate";
351
+ if (mode === "regenerate") {
352
+ const remember = await clack.select({
353
+ message: `Apply "${labelFor(resolution)}" to all remaining conflicts this run?`,
354
+ initialValue: "no",
355
+ options: [
356
+ { value: "yes", label: "Yes", hint: "reuse this choice for the rest of this run" },
357
+ { value: "no", label: "No", hint: "prompt again for each conflict" }
358
+ ]
359
+ });
360
+ if (!clack.isCancel(remember) && remember === "yes") {
361
+ return { resolution, applyToAll: true };
362
+ }
363
+ }
364
+ return resolution;
365
+ }
366
+ function labelFor(resolution) {
367
+ switch (resolution) {
368
+ case "replace":
369
+ return "Replace";
370
+ case "rename":
371
+ return "Rename";
372
+ case "duplicate":
373
+ return "Create duplicate";
374
+ case "preserve":
375
+ return "Keep mine";
376
+ case "merge":
377
+ return "Merge (with conflict markers)";
378
+ case "cancel":
379
+ return "Cancel";
380
+ }
381
+ }
382
+
383
+ // src/dedup-write.ts
384
+ import { createHash } from "crypto";
385
+ import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
386
+ import { join as join2 } from "path";
387
+
388
+ // src/commands/doctor.ts
389
+ import { spawnSync } from "child_process";
390
+ import { existsSync, readdirSync, readFileSync } from "fs";
391
+ import { dirname, join, relative } from "path";
392
+ import { fileURLToPath } from "url";
393
+ import { resolveAdapter } from "@noir-ai/adapters";
394
+ import { loadProjectInfo, NOIR_VERSION, paths } from "@noir-ai/core";
395
+ import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
396
+ import { pidAlive, readDaemonRecord } from "@noir-ai/daemon";
397
+ import { resolveModelConfig } from "@noir-ai/model";
398
+ async function probeOnnx() {
399
+ try {
400
+ const spec = "onnxruntime-node";
401
+ await import(spec);
402
+ return { ok: true, reason: "loadable" };
403
+ } catch (e) {
404
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
405
+ }
406
+ }
407
+ function describeOnnx(o) {
408
+ if (o.ok) return "loadable";
409
+ if (/Cannot find (module|package)/i.test(o.reason)) {
410
+ return "not resolvable from CLI probe (best-effort)";
411
+ }
412
+ const r = o.reason.length > 60 ? `${o.reason.slice(0, 59)}\u2026` : o.reason;
413
+ return `probe failed: ${r}`;
414
+ }
415
+ function fmtUptime(sec) {
416
+ if (!Number.isFinite(sec) || sec < 0) return "unknown";
417
+ if (sec < 60) return `${sec}s`;
418
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
419
+ return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
420
+ }
421
+ function msg(e) {
422
+ return e instanceof Error ? e.message : String(e);
423
+ }
424
+ async function checkRuntime(checks) {
425
+ checks.push({
426
+ name: "runtime",
427
+ status: "ok",
428
+ detail: `noir ${NOIR_VERSION} \xB7 node ${process.version} \xB7 ${process.platform}`
429
+ });
430
+ }
431
+ function checkConfig(root) {
432
+ const configPath = paths.config(root);
433
+ if (!existsSync(configPath)) {
434
+ return {
435
+ project: void 0,
436
+ result: {
437
+ name: "config",
438
+ status: "warn",
439
+ detail: `no .noir/config.yml in ${root} \u2014 run \`noir init\``
440
+ }
441
+ };
442
+ }
443
+ try {
444
+ const project = loadProjectInfo(root);
445
+ return {
446
+ project,
447
+ result: {
448
+ name: "config",
449
+ status: "ok",
450
+ detail: `host=${project.config.host} \xB7 mode=${project.config.mode}`
451
+ }
452
+ };
453
+ } catch (e) {
454
+ return {
455
+ project: void 0,
456
+ result: { name: "config", status: "fail", detail: `config parse error: ${msg(e)}` }
457
+ };
458
+ }
459
+ }
460
+ async function checkDaemon(checks) {
461
+ const rec = readDaemonRecord();
462
+ if (!rec) {
463
+ checks.push({
464
+ name: "daemon",
465
+ status: "warn",
466
+ detail: "not running (start with `noir daemon start`)"
467
+ });
468
+ return;
469
+ }
470
+ if (!pidAlive(rec.pid)) {
471
+ checks.push({
472
+ name: "daemon",
473
+ status: "warn",
474
+ detail: `stale record (pid ${rec.pid} not alive)`
475
+ });
476
+ return;
477
+ }
478
+ try {
479
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
480
+ const body = res.status === 200 ? await res.json() : null;
481
+ if (body && body.ok === true) {
482
+ checks.push({
483
+ name: "daemon",
484
+ status: "ok",
485
+ detail: `running (pid ${rec.pid}, port ${rec.port}, up ${fmtUptime(body.uptimeSec ?? 0)})`
486
+ });
487
+ return;
488
+ }
489
+ checks.push({
490
+ name: "daemon",
491
+ status: "warn",
492
+ detail: `record present but /health unreachable on port ${rec.port}`
493
+ });
494
+ } catch {
495
+ checks.push({
496
+ name: "daemon",
497
+ status: "warn",
498
+ detail: `record present but /health unreachable on port ${rec.port}`
499
+ });
500
+ }
501
+ }
502
+ async function checkNativeDeps(checks) {
503
+ const { vecAvailability } = await import("@noir-ai/store");
504
+ const vec = vecAvailability();
505
+ const onnx = await probeOnnx();
506
+ const vecOk = vec.ok === true;
507
+ const onnxOk = onnx.ok === true;
508
+ const status = vecOk ? onnxOk ? "ok" : "warn" : "fail";
509
+ checks.push({
510
+ name: "native deps",
511
+ status,
512
+ detail: `better-sqlite3+sqlite-vec: ${vecOk ? "loadable" : vec.ok === false ? vec.reason : "unknown"} \xB7 onnxruntime-node: ${describeOnnx(onnx)}`
513
+ });
514
+ return { vecOk };
515
+ }
516
+ async function checkStore(checks, project, root) {
517
+ if (!project) {
518
+ checks.push({ name: "store", status: "warn", detail: "skipped \u2014 not initialized" });
519
+ return;
520
+ }
521
+ const dbPath = paths.storeDb(root, project.id);
522
+ if (!existsSync(dbPath)) {
523
+ checks.push({
524
+ name: "store",
525
+ status: "warn",
526
+ detail: `not created yet \u2014 the store DB is created when the daemon first runs (\`noir daemon start\` or index content); expected at ${dbPath}`
527
+ });
528
+ return;
529
+ }
530
+ const { openStore } = await import("@noir-ai/store");
531
+ let store;
532
+ try {
533
+ store = await openStore({ projectId: project.id, root, readonly: true });
534
+ checks.push({
535
+ name: "store",
536
+ status: "ok",
537
+ detail: `opens readonly (db: ${paths.storeDb(root, project.id)})`
538
+ });
539
+ } catch (e) {
540
+ checks.push({ name: "store", status: "fail", detail: `open failed: ${msg(e)}` });
541
+ } finally {
542
+ if (store) {
543
+ try {
544
+ await Promise.resolve(store.close());
545
+ } catch {
546
+ }
547
+ }
548
+ }
549
+ }
550
+ function checkEmbedder(checks, project, vecOk) {
551
+ if (!project) {
552
+ checks.push({ name: "embedder", status: "warn", detail: "skipped \u2014 not initialized" });
553
+ return;
554
+ }
555
+ const emb = project.config.context.embedder;
556
+ const kind = emb.kind;
557
+ const model = typeof emb.model === "string" && emb.model.length > 0 ? emb.model : "<unset>";
558
+ if (kind === "none") {
559
+ checks.push({
560
+ name: "embedder",
561
+ status: "ok",
562
+ detail: `kind=none (BM25-only) \xB7 dim=${emb.dim}`
563
+ });
564
+ return;
565
+ }
566
+ if (!vecOk) {
567
+ checks.push({
568
+ name: "embedder",
569
+ status: "warn",
570
+ detail: `${kind}/${model} (${emb.dim}-dim) configured but vec native layer unavailable \u2014 search degrades to BM25`
571
+ });
572
+ return;
573
+ }
574
+ checks.push({
575
+ name: "embedder",
576
+ status: "ok",
577
+ detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
578
+ });
579
+ }
580
+ function checkProvider(checks, project) {
581
+ if (!project) {
582
+ checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
583
+ return;
584
+ }
585
+ const resolved = resolveModelConfig(project.config.model);
586
+ const names = Object.keys(resolved.providers);
587
+ if (names.length === 0) {
588
+ checks.push({
589
+ name: "provider",
590
+ status: "ok",
591
+ detail: "none configured (offline mode; drafting degrades to templates)"
592
+ });
593
+ return;
594
+ }
595
+ const parts = [];
596
+ let missing = false;
597
+ for (const name of names) {
598
+ const p = resolved.providers[name];
599
+ if (!p) continue;
600
+ const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
601
+ if (!p.hasKey && p.apiKeyEnv) missing = true;
602
+ parts.push(`${name}/${p.model ?? "?"} (${key})`);
603
+ }
604
+ checks.push({
605
+ name: "provider",
606
+ status: missing ? "warn" : "ok",
607
+ detail: parts.join(" \xB7 ")
608
+ });
609
+ }
610
+ function checkScaffoldVersion(checks, root) {
611
+ const onDisk = readScaffoldVersion(root);
612
+ const current = CURRENT_SCAFFOLD_VERSION;
613
+ const drift = onDisk !== null && onDisk !== current;
614
+ const status = onDisk === null || drift ? "warn" : "ok";
615
+ 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)`;
616
+ checks.push({ name: "scaffold version", status, detail });
617
+ return { onDisk, current, drift };
618
+ }
619
+ function checkRulesMdBudget(checks, root, project) {
620
+ if (!project) {
621
+ checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
622
+ return null;
623
+ }
624
+ const rulesPath = paths.rulesMd(root);
625
+ if (!existsSync(rulesPath)) {
626
+ checks.push({
627
+ name: "rules budget",
628
+ status: "ok",
629
+ detail: "no .noir/rules/RULES.md"
630
+ });
631
+ return null;
632
+ }
633
+ let content;
634
+ try {
635
+ content = readFileSync(rulesPath, "utf8");
636
+ } catch (e) {
637
+ checks.push({
638
+ name: "rules budget",
639
+ status: "warn",
640
+ detail: `RULES.md unreadable: ${msg(e)}`
641
+ });
642
+ return null;
643
+ }
644
+ const bytes = Buffer.byteLength(content, "utf8");
645
+ const lineCount = content.length === 0 ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
646
+ const lengthBudgetKb = project.config.rules.lengthBudgetKb;
647
+ const budgetBytes = lengthBudgetKb * 1024;
648
+ const maxLines = 150;
649
+ const overBytes = bytes > budgetBytes;
650
+ const overLines = lineCount > maxLines;
651
+ const over = overBytes || overLines;
652
+ const status = over ? "warn" : "ok";
653
+ const kb = (n) => (n / 1024).toFixed(1);
654
+ 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)`;
655
+ checks.push({ name: "rules budget", status, detail });
656
+ return {
657
+ onDisk: { bytes, lines: lineCount },
658
+ budget: { kb: lengthBudgetKb, maxLines },
659
+ over
660
+ };
661
+ }
662
+ function summarize(checks) {
663
+ let ok = 0;
664
+ let warnN = 0;
665
+ let fail2 = 0;
666
+ for (const c2 of checks) {
667
+ if (c2.status === "ok") ok++;
668
+ else if (c2.status === "warn") warnN++;
669
+ else fail2++;
670
+ }
671
+ return { ok, warn: warnN, fail: fail2 };
672
+ }
673
+ function checkHostArtifacts(checks, root, project) {
674
+ if (!project) {
675
+ checks.push({ name: "host artifacts", status: "warn", detail: "skipped \u2014 not initialized" });
676
+ return null;
677
+ }
678
+ const host = project.config.host;
679
+ const adapter = resolveAdapter(host);
680
+ const ectx = { root };
681
+ const expected = [];
682
+ const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
683
+ if (emitsAgentsMd) {
684
+ expected.push(relOr(adapter.agentsMdPath?.(ectx) ?? "AGENTS.md", root));
685
+ }
686
+ if (host === "claude") expected.push("CLAUDE.md");
687
+ else if (host === "gemini") expected.push("GEMINI.md");
688
+ expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
689
+ const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
690
+ const status = missing.length === 0 ? "ok" : "warn";
691
+ 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)`;
692
+ checks.push({ name: "host artifacts", status, detail });
693
+ return { active: host, expected, missing };
694
+ }
695
+ function checkNestedNoir(checks, root) {
696
+ const candidates = [
697
+ ".noir/.noir",
698
+ ".noir/CLAUDE.md",
699
+ ".noir/.mcp.json",
700
+ ".noir/.claude",
701
+ ".noir/.gitignore",
702
+ ".noir/.dockerignore",
703
+ ".noir/.npmignore",
704
+ ".noir/.prettierignore"
705
+ ];
706
+ const found = candidates.filter((rel) => existsSync(join(root, rel)));
707
+ const detected = found.length > 0;
708
+ checks.push({
709
+ name: "nested .noir",
710
+ status: detected ? "warn" : "ok",
711
+ detail: detected ? `nested Noir artifacts inside .noir/ (${found.join(", ")}) \u2014 likely from running \`noir init\` inside .noir/. Remove them; the real project is at ${root}.` : "no nested .noir/ artifacts"
712
+ });
713
+ return { detected, paths: found };
714
+ }
715
+ function collectDedupCandidates(root) {
716
+ const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
717
+ const out = [];
718
+ for (const rel of rels) {
719
+ const abs = join(root, rel);
720
+ if (!existsSync(abs)) continue;
721
+ let text;
722
+ try {
723
+ text = readFileSync(abs, "utf8");
724
+ } catch {
725
+ continue;
726
+ }
727
+ if (text.trim().length === 0) continue;
728
+ out.push({ path: rel, text });
729
+ }
730
+ return out;
731
+ }
732
+ async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
733
+ if (!project) {
734
+ checks.push({ name: "semantic dup", status: "warn", detail: "skipped \u2014 not initialized" });
735
+ return;
736
+ }
737
+ const candidates = collectDedupCandidates(root);
738
+ if (candidates.length < 2) {
739
+ checks.push({
740
+ name: "semantic dup",
741
+ status: "ok",
742
+ detail: `${candidates.length} candidate file(s); nothing to compare`
743
+ });
744
+ return;
745
+ }
746
+ const ctx = await import("@noir-ai/context");
747
+ let embed = opts.embed;
748
+ if (!embed) {
749
+ const cfg = ctx.resolveEmbedderConfig(project.config.context);
750
+ if (cfg.kind === "none") {
751
+ checks.push({
752
+ name: "semantic dup",
753
+ status: "warn",
754
+ detail: "embedder kind=none \u2014 set context.embedder.kind=local to enable semantic dedup"
755
+ });
756
+ return;
757
+ }
758
+ try {
759
+ embed = ctx.createEmbedFn(cfg).embed;
760
+ } catch (e) {
761
+ checks.push({
762
+ name: "semantic dup",
763
+ status: "warn",
764
+ detail: `embedder unavailable \u2014 skipped (${msg(e)})`
765
+ });
766
+ return;
767
+ }
768
+ }
769
+ if (!embed) return;
770
+ const pairs = await ctx.findSemanticDuplicates(candidates, embed, ctx.DEFAULT_DUP_THRESHOLD);
771
+ checks.push({
772
+ name: "semantic dup",
773
+ status: pairs.length > 0 ? "warn" : "ok",
774
+ detail: pairs.length === 0 ? `no near-duplicates across ${candidates.length} file(s)` : `${pairs.length} near-duplicate pair${pairs.length === 1 ? "" : "s"}: ${pairs.map((p) => `${p.a}\u2248${p.b} (${p.similarity.toFixed(2)})`).join("; ")}`
775
+ });
776
+ }
777
+ function resolveWorkspacePackagesDir() {
778
+ let here = dirname(fileURLToPath(import.meta.url));
779
+ for (let i = 0; i < 8; i++) {
780
+ const pkgPath = join(here, "package.json");
781
+ if (existsSync(pkgPath)) {
782
+ try {
783
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
784
+ if (pkg.name === "@noir-ai/cli") {
785
+ const packagesDir = dirname(here);
786
+ if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
787
+ return packagesDir;
788
+ }
789
+ return null;
790
+ }
791
+ } catch {
792
+ }
793
+ }
794
+ const parent = dirname(here);
795
+ if (parent === here) break;
796
+ here = parent;
797
+ }
798
+ return null;
799
+ }
800
+ var SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
801
+ function npmPackDryRun(cliPkgDir) {
802
+ const probe = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
803
+ if (probe.status !== 0 || probe.error) {
804
+ return { error: probe.error ? probe.error.message : "npm not on PATH" };
805
+ }
806
+ const res = spawnSync("npm", ["pack", "--dry-run"], {
807
+ cwd: cliPkgDir,
808
+ encoding: "utf8",
809
+ shell: true
810
+ });
811
+ if (res.status !== 0 || res.error) {
812
+ return { error: res.error ? res.error.message : `npm pack exited ${res.status ?? "?"}` };
813
+ }
814
+ const out = `${res.stdout ?? ""}
815
+ ${res.stderr ?? ""}`;
816
+ const totalMatch = out.match(/total files:\s*(\d+)/i);
817
+ const fileCount = totalMatch?.[1] ? Number.parseInt(totalMatch[1], 10) : null;
818
+ const distIncluded = /\bdist\//.test(out);
819
+ return { fileCount, distIncluded };
820
+ }
821
+ function checkPublish(checks, packagesDir, opts = {}) {
822
+ if (packagesDir === null) {
823
+ checks.push({
824
+ name: "publish",
825
+ status: "ok",
826
+ detail: "skipped \u2014 not a monorepo checkout (no packages/* workspace)"
827
+ });
828
+ return null;
829
+ }
830
+ let entries = [];
831
+ try {
832
+ entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
833
+ } catch (e) {
834
+ checks.push({
835
+ name: "publish",
836
+ status: "warn",
837
+ detail: `could not read ${packagesDir}: ${msg(e)}`
838
+ });
839
+ return { checked: 0, issues: [`unreadable packages dir: ${msg(e)}`] };
840
+ }
841
+ const issues = [];
842
+ let checked = 0;
843
+ let cliDir = null;
844
+ for (const name of entries) {
845
+ const pkgPath = join(packagesDir, name, "package.json");
846
+ if (!existsSync(pkgPath)) continue;
847
+ let pkg;
848
+ try {
849
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
850
+ } catch (e) {
851
+ issues.push(`${name}/package.json: unparseable (${msg(e)})`);
852
+ checked++;
853
+ continue;
854
+ }
855
+ checked++;
856
+ const pkgName = typeof pkg.name === "string" ? pkg.name : "";
857
+ if (!pkgName.startsWith("@noir-ai/")) {
858
+ issues.push(`${name}/package.json: name '${pkgName}' is not @noir-ai/*`);
859
+ }
860
+ const version = typeof pkg.version === "string" ? pkg.version : "";
861
+ if (!SEMVER_ISH.test(version)) {
862
+ issues.push(`${name}/package.json: version '${version}' is not semver`);
863
+ }
864
+ if (!Array.isArray(pkg.files) || pkg.files.length === 0) {
865
+ issues.push(`${name}/package.json: files missing or empty`);
866
+ }
867
+ if (pkgName === "@noir-ai/cli") {
868
+ if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
869
+ issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
870
+ }
871
+ cliDir = join(packagesDir, name);
872
+ }
873
+ }
874
+ let packNote = "";
875
+ if (cliDir && opts.skipNpmPack !== true) {
876
+ const pack = npmPackDryRun(cliDir);
877
+ if ("error" in pack) {
878
+ packNote = ` \xB7 npm pack skipped (${pack.error})`;
879
+ } else {
880
+ const count = pack.fileCount == null ? "?" : String(pack.fileCount);
881
+ const dist = pack.distIncluded ? "dist/ included" : "dist/ NOT in tarball";
882
+ packNote = ` \xB7 npm pack: ${count} files, ${dist}`;
883
+ if (pack.distIncluded === false) {
884
+ issues.push("cli npm pack --dry-run: dist/ absent from the tarball");
885
+ }
886
+ }
887
+ }
888
+ const status = issues.length > 0 ? "warn" : "ok";
889
+ 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}`;
890
+ checks.push({ name: "publish", status, detail });
891
+ return { checked, issues };
892
+ }
893
+ function relOr(abs, root) {
894
+ const posix = abs.replace(/\\/g, "/");
895
+ if (posix.startsWith("/")) {
896
+ const rel = relative(root, abs).replace(/\\/g, "/");
897
+ if (rel.length === 0 || rel.startsWith("..") || rel.startsWith("/")) {
898
+ throw new Error(`doctor: host path '${abs}' is not under root '${root}'`);
899
+ }
900
+ return rel;
901
+ }
902
+ if (posix.split("/").some((seg) => seg === "..")) {
903
+ throw new Error(`doctor: host path '${abs}' escapes root '${root}'`);
904
+ }
905
+ return posix;
906
+ }
907
+ function joinRoot(root, relPath) {
908
+ return `${root}/${relPath}`;
909
+ }
910
+ function severityBadge(s) {
911
+ const state = s === "fail" ? "error" : s;
912
+ return badge(state, s.toUpperCase());
913
+ }
914
+ function renderHuman(payload, opts) {
915
+ log(
916
+ `noir doctor \u2014 ${payload.checks.length} check${payload.checks.length === 1 ? "" : "s"}`,
917
+ opts
918
+ );
919
+ table(
920
+ payload.checks.map((c2) => ({
921
+ Check: c2.name,
922
+ Status: severityBadge(c2.status),
923
+ Detail: c2.detail
924
+ })),
925
+ ["Check", "Status", "Detail"],
926
+ opts
927
+ );
928
+ const { ok, warn: warnN, fail: fail2 } = payload.summary;
929
+ if (fail2 > 0) {
930
+ error(
931
+ `${fail2} critical check${fail2 === 1 ? "" : "s"} failed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`,
932
+ opts
933
+ );
934
+ } else if (warnN > 0) {
935
+ warn(`all critical checks passed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`, opts);
936
+ } else {
937
+ success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
938
+ }
939
+ }
940
+ async function doctor(opts = {}) {
941
+ const root = process.cwd();
942
+ const checks = [];
943
+ await checkRuntime(checks);
944
+ const { project, result: configResult } = checkConfig(root);
945
+ checks.push(configResult);
946
+ await checkDaemon(checks);
947
+ const { vecOk } = await checkNativeDeps(checks);
948
+ await checkStore(checks, project, root);
949
+ checkEmbedder(checks, project, vecOk);
950
+ checkProvider(checks, project);
951
+ const scaffold = checkScaffoldVersion(checks, root);
952
+ const rules = checkRulesMdBudget(checks, root, project);
953
+ const host = checkHostArtifacts(checks, root, project);
954
+ checkNestedNoir(checks, root);
955
+ if (opts.dedup === true) {
956
+ await checkSemanticDupDoctor(checks, root, project);
957
+ }
958
+ const publish = checkPublish(checks, resolveWorkspacePackagesDir());
959
+ const summary = summarize(checks);
960
+ const payload = {
961
+ noir: NOIR_VERSION,
962
+ checks,
963
+ summary,
964
+ scaffold,
965
+ rules,
966
+ host,
967
+ publish
968
+ };
969
+ if (opts.json === true) {
970
+ process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
971
+ `);
972
+ } else {
973
+ renderHuman(payload, opts);
974
+ }
975
+ if (summary.fail > 0) {
976
+ throw new NoirCliError(
977
+ EXIT.ERROR,
978
+ opts.json === true ? "" : `noir doctor: ${summary.fail} critical check(s) failed`
979
+ );
980
+ }
981
+ }
982
+
983
+ // src/dedup-write.ts
984
+ var HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
985
+ var ACTION_THRESHOLD = 0.95;
986
+ var INFO_THRESHOLD = 0.85;
987
+ var EMBEDDER_PING_TIMEOUT_MS = 5e3;
988
+ var DEDUP_CACHE_VERSION = 1;
989
+ async function checkWritePathDedup(root, res, opts) {
990
+ const result = { found: [], conflicts: [] };
991
+ const writtenHostFiles = res.written.filter((p) => HOST_CONTEXT_RELS.has(p));
992
+ if (writtenHostFiles.length === 0) return result;
993
+ const cache = loadCache(root);
994
+ const embed = await resolveEmbedder(opts, cache);
995
+ if (embed === void 0) return result;
996
+ const cachedEmbed = makeCachedEmbed(cache, embed);
997
+ for (const proposedRel of writtenHostFiles) {
998
+ const proposedAbs = join2(root, proposedRel);
999
+ let proposedText;
1000
+ try {
1001
+ proposedText = readFileSync2(proposedAbs, "utf8");
1002
+ } catch {
1003
+ continue;
1004
+ }
1005
+ if (proposedText.trim().length === 0) continue;
1006
+ const candidates = collectDedupCandidates(root).filter((c2) => c2.path !== proposedRel);
1007
+ if (candidates.length === 0) continue;
1008
+ const ctx = await import("@noir-ai/context");
1009
+ const proposed = { path: proposedRel, text: proposedText };
1010
+ let best;
1011
+ try {
1012
+ best = await ctx.findNearestDuplicate(
1013
+ proposed,
1014
+ candidates,
1015
+ async (t) => cachedEmbed(t),
1016
+ INFO_THRESHOLD
1017
+ );
1018
+ } catch (e) {
1019
+ process.stderr.write(
1020
+ `${c.dim(`noir: semantic-dedup skipped for ${proposedRel} (${errMsg(e)}).`)}
1021
+ `
1022
+ );
1023
+ continue;
1024
+ }
1025
+ if (best === null) continue;
1026
+ const matchedRel = best.a === proposedRel ? best.b : best.a;
1027
+ const sim = best.similarity;
1028
+ result.found.push({ proposedRel, matchedRel, similarity: sim });
1029
+ if (sim >= ACTION_THRESHOLD) {
1030
+ if (opts.interactive) {
1031
+ await promptDedupAction(root, proposedRel, matchedRel, proposedText, sim);
1032
+ } else {
1033
+ process.stderr.write(
1034
+ `${c.warn("noir: near-duplicate")} ${proposedRel} \u2248 ${matchedRel} (cosine ${sim.toFixed(2)}); creating anyway.
1035
+ `
1036
+ );
1037
+ }
1038
+ } else {
1039
+ process.stderr.write(
1040
+ `${c.dim(`noir: hint ${proposedRel} looks similar to existing ${matchedRel} (cosine ${sim.toFixed(2)}).`)}
1041
+ `
1042
+ );
1043
+ }
1044
+ const matchedText = safeRead(join2(root, matchedRel));
1045
+ result.conflicts.push({
1046
+ path: proposedRel,
1047
+ mode: "artifact",
1048
+ similarity: sim,
1049
+ existingSha: sha256Hex12(matchedText ?? ""),
1050
+ proposedSha: sha256Hex12(proposedText),
1051
+ resolution: "preserve"
1052
+ });
1053
+ }
1054
+ saveCache(root, cache);
1055
+ return result;
1056
+ }
1057
+ async function resolveEmbedder(opts, cache) {
1058
+ const ctx = await import("@noir-ai/context");
1059
+ let modelId;
1060
+ if (opts.project !== void 0) {
1061
+ const cfg = ctx.resolveEmbedderConfig(opts.project.config.context);
1062
+ if (cfg.kind === "none") {
1063
+ process.stderr.write(`${c.dim("noir: semantic-dedup skipped (embedder kind=none).")}
1064
+ `);
1065
+ return void 0;
1066
+ }
1067
+ modelId = cfg.kind === "local" ? cfg.model ?? ctx.DEFAULT_LOCAL_MODEL : cfg.model ?? cfg.kind;
1068
+ if (modelId !== void 0 && cache.model !== modelId) {
1069
+ cache.model = modelId;
1070
+ cache.entries = {};
1071
+ }
1072
+ }
1073
+ if (opts.embed !== void 0) return opts.embed;
1074
+ if (modelId === void 0) return void 0;
1075
+ let embed;
1076
+ try {
1077
+ const project = opts.project;
1078
+ if (project === void 0) return void 0;
1079
+ const cfg = ctx.resolveEmbedderConfig(project.config.context);
1080
+ embed = ctx.createEmbedFn(cfg).embed;
1081
+ } catch (e) {
1082
+ process.stderr.write(
1083
+ `${c.dim(`noir: semantic-dedup skipped (embedder unavailable: ${errMsg(e)}).`)}
1084
+ `
1085
+ );
1086
+ return void 0;
1087
+ }
1088
+ const ping = await withTimeout(
1089
+ embed("ping").then(() => true),
1090
+ EMBEDDER_PING_TIMEOUT_MS
1091
+ );
1092
+ if (ping !== true) {
1093
+ process.stderr.write(
1094
+ `${c.dim("noir: semantic-dedup skipped (embedder load too slow or unavailable).")}
1095
+ `
1096
+ );
1097
+ return void 0;
1098
+ }
1099
+ return embed;
1100
+ }
1101
+ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, sim) {
1102
+ const clack = await import("@clack/prompts");
1103
+ const choice = await clack.select({
1104
+ message: `${proposedRel} is near-duplicate of existing ${matchedRel} (cosine ${sim.toFixed(2)}). Create anyway?`,
1105
+ initialValue: "create",
1106
+ options: [
1107
+ {
1108
+ value: "create",
1109
+ label: "Create anyway",
1110
+ hint: "keep the new file as-is (default)"
1111
+ },
1112
+ { value: "skip", label: "Skip", hint: `delete ${proposedRel}; keep ${matchedRel}` },
1113
+ {
1114
+ value: "replace",
1115
+ label: "Replace existing",
1116
+ hint: `overwrite ${matchedRel} with this content`
1117
+ },
1118
+ {
1119
+ value: "mirror",
1120
+ label: "Mirror into existing",
1121
+ hint: `append this content to ${matchedRel}`
1122
+ }
1123
+ ]
1124
+ });
1125
+ if (clack.isCancel(choice)) return;
1126
+ if (choice === "skip") {
1127
+ try {
1128
+ rmSync(join2(root, proposedRel), { force: true });
1129
+ } catch {
1130
+ }
1131
+ } else if (choice === "replace") {
1132
+ try {
1133
+ writeFileSync(join2(root, matchedRel), proposedText, "utf8");
1134
+ } catch {
1135
+ }
1136
+ } else if (choice === "mirror") {
1137
+ try {
1138
+ const existing = readFileSync2(join2(root, matchedRel), "utf8");
1139
+ writeFileSync(
1140
+ join2(root, matchedRel),
1141
+ `${existing}
1142
+
1143
+ <!-- mirrored from ${proposedRel} -->
1144
+ ${proposedText}
1145
+ `,
1146
+ "utf8"
1147
+ );
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ }
1152
+ function emptyCache() {
1153
+ return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
1154
+ }
1155
+ function cachePath(root) {
1156
+ return join2(root, ".noir", "dedup-cache.json");
1157
+ }
1158
+ function loadCache(root) {
1159
+ try {
1160
+ const raw = readFileSync2(cachePath(root), "utf8");
1161
+ const parsed = JSON.parse(raw);
1162
+ if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
1163
+ if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
1164
+ return {
1165
+ version: DEDUP_CACHE_VERSION,
1166
+ model: typeof parsed.model === "string" ? parsed.model : "",
1167
+ entries: parsed.entries
1168
+ };
1169
+ } catch {
1170
+ return emptyCache();
1171
+ }
1172
+ }
1173
+ function saveCache(root, cache) {
1174
+ try {
1175
+ mkdirSync(join2(root, ".noir"), { recursive: true });
1176
+ writeFileSync(cachePath(root), JSON.stringify(cache), "utf8");
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ function makeCachedEmbed(cache, embed) {
1181
+ return async (text) => {
1182
+ const key = createHash("sha256").update(text, "utf8").digest("hex");
1183
+ const cached = cache.entries[key];
1184
+ if (cached !== void 0) return Float32Array.from(cached.v);
1185
+ const v = await embed(text);
1186
+ cache.entries[key] = { v: Array.from(v) };
1187
+ return v;
1188
+ };
1189
+ }
1190
+ function sha256Hex12(text) {
1191
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 12);
1192
+ }
1193
+ function safeRead(abs) {
1194
+ try {
1195
+ return readFileSync2(abs, "utf8");
1196
+ } catch {
1197
+ return void 0;
1198
+ }
1199
+ }
1200
+ function withTimeout(p, ms) {
1201
+ return new Promise((resolve) => {
1202
+ const t = setTimeout(() => resolve(void 0), ms);
1203
+ p.then(
1204
+ (v) => {
1205
+ clearTimeout(t);
1206
+ resolve(v);
1207
+ },
1208
+ () => {
1209
+ clearTimeout(t);
1210
+ resolve(void 0);
1211
+ }
1212
+ );
1213
+ });
1214
+ }
1215
+ function errMsg(e) {
1216
+ return e instanceof Error ? e.message : String(e);
1217
+ }
1218
+
1219
+ export {
1220
+ c,
1221
+ badge,
1222
+ useColor,
1223
+ accessibleMode,
1224
+ terminalWidth,
1225
+ EXIT,
1226
+ NoirCliError,
1227
+ isInteractive,
1228
+ resolveInteractive,
1229
+ json,
1230
+ log,
1231
+ info,
1232
+ warn,
1233
+ tip,
1234
+ table,
1235
+ definitionList,
1236
+ fail,
1237
+ requireInteractive,
1238
+ handleError,
1239
+ doctor,
1240
+ buildConflictOpts,
1241
+ checkWritePathDedup
1242
+ };
1243
+ //# sourceMappingURL=chunk-YNEBQJXA.js.map