@noir-ai/cli 1.11.2-beta.1 → 1.12.0-beta.1

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