@noir-ai/cli 1.3.0-beta.6 → 1.4.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.
@@ -1,13 +1,287 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- EXIT,
4
- NoirCliError,
5
- error,
6
- log,
7
- success,
8
- table,
9
- warn
10
- } from "./chunk-OF4OO5W6.js";
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/output.ts
10
+ import Table from "cli-table3";
11
+ import { CommanderError } from "commander";
12
+ import ora from "ora";
13
+
14
+ // src/theme.ts
15
+ import pc from "picocolors";
16
+ var FORCE = pc.createColors(true);
17
+ var c = {
18
+ /** Green — success / healthy. */
19
+ ok: (s) => useColor() ? FORCE.green(s) : s,
20
+ /** Yellow — degraded but usable. */
21
+ warn: (s) => useColor() ? FORCE.yellow(s) : s,
22
+ /** Red — reserved strictly for ERROR. Never a table header. */
23
+ error: (s) => useColor() ? FORCE.red(s) : s,
24
+ /** Cyan — informational / brand-neutral accent for labels. */
25
+ info: (s) => useColor() ? FORCE.cyan(s) : s,
26
+ /** Blue — the Midnight Cobalt brand role (banner / accent). */
27
+ accent: (s) => useColor() ? FORCE.blue(s) : s,
28
+ /** Dim — secondary text, table borders when color is on. */
29
+ dim: (s) => useColor() ? FORCE.dim(s) : s,
30
+ /** Bold — emphasis (table headers compose this with `info`). */
31
+ bold: (s) => useColor() ? FORCE.bold(s) : s
32
+ };
33
+ var BADGE_SYMBOL = {
34
+ ok: "\u2713",
35
+ warn: "\u26A0",
36
+ error: "\u2717",
37
+ info: "\u2139"
38
+ };
39
+ function badge(state, label = state) {
40
+ const text = `${BADGE_SYMBOL[state]} ${label}`;
41
+ switch (state) {
42
+ case "ok":
43
+ return c.ok(text);
44
+ case "warn":
45
+ return c.warn(text);
46
+ case "error":
47
+ return c.error(text);
48
+ case "info":
49
+ return c.info(text);
50
+ }
51
+ }
52
+ function envFlagSet(name) {
53
+ const v = process.env[name];
54
+ return v !== void 0 && v !== "";
55
+ }
56
+ function isCiEnv() {
57
+ const v = process.env.CI;
58
+ if (v === void 0 || v === "") return false;
59
+ return v !== "0" && v !== "false";
60
+ }
61
+ function useColor() {
62
+ if (envFlagSet("NO_COLOR")) return false;
63
+ if (process.env.CLICOLOR_FORCE === "1") return true;
64
+ if (isCiEnv()) return false;
65
+ return pc.isColorSupported;
66
+ }
67
+ function accessibleMode() {
68
+ return envFlagSet("NOIR_ACCESSIBLE");
69
+ }
70
+ function terminalWidth() {
71
+ const fromEnv = process.env.COLUMNS;
72
+ if (fromEnv !== void 0 && fromEnv !== "") {
73
+ const n = Number(fromEnv);
74
+ if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
75
+ }
76
+ const cols = process.stdout.columns;
77
+ if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
78
+ return 80;
79
+ }
80
+
81
+ // src/output.ts
82
+ var EXIT = {
83
+ OK: 0,
84
+ ERROR: 1,
85
+ USAGE: 2,
86
+ NOT_FOUND: 3,
87
+ DAEMON_DOWN: 4,
88
+ CANCELLED: 5
89
+ };
90
+ var NoirCliError = class extends Error {
91
+ exitCode;
92
+ constructor(exitCode, message) {
93
+ super(message);
94
+ this.name = "NoirCliError";
95
+ this.exitCode = exitCode;
96
+ }
97
+ };
98
+ var NOIR_ERROR_CODE = "noir.error";
99
+ function isJsonMode(opts) {
100
+ return opts.json === true;
101
+ }
102
+ function isQuietMode(opts) {
103
+ return opts.quiet === true;
104
+ }
105
+ function isNoInput(opts = {}) {
106
+ return opts.noInput === true || opts.input === false;
107
+ }
108
+ function envFlagSet2(name) {
109
+ const v = process.env[name];
110
+ return v !== void 0 && v !== "";
111
+ }
112
+ function isInteractive(opts = {}) {
113
+ if (isJsonMode(opts) || isNoInput(opts)) return false;
114
+ if (isCiEnv() || envFlagSet2("NO_COLOR")) return false;
115
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
116
+ }
117
+ function resolveInteractive() {
118
+ if (flaggedNonInteractiveCli()) return false;
119
+ return isInteractive();
120
+ }
121
+ function flaggedNonInteractiveCli() {
122
+ const v = process.env.NOIR_NON_INTERACTIVE;
123
+ return v !== void 0 && v !== "";
124
+ }
125
+ function json(obj) {
126
+ process.stdout.write(`${JSON.stringify(obj)}
127
+ `);
128
+ }
129
+ function log(msg2, opts = {}) {
130
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
131
+ process.stderr.write(`${msg2}
132
+ `);
133
+ }
134
+ function info(msg2, opts = {}) {
135
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
136
+ process.stderr.write(`${c.info(msg2)}
137
+ `);
138
+ }
139
+ function success(msg2, opts = {}) {
140
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
141
+ process.stderr.write(`${c.ok(msg2)}
142
+ `);
143
+ }
144
+ function warn(msg2, opts = {}) {
145
+ if (isJsonMode(opts)) return;
146
+ process.stderr.write(`${c.warn(msg2)}
147
+ `);
148
+ }
149
+ function error(msg2, opts = {}) {
150
+ if (isJsonMode(opts)) return;
151
+ process.stderr.write(`${c.error(msg2)}
152
+ `);
153
+ }
154
+ function tip(msg2, opts = {}) {
155
+ if (isJsonMode(opts) || opts.noTips === true) return;
156
+ process.stderr.write(`${c.warn(msg2)}
157
+ `);
158
+ }
159
+ function table(rows, cols, opts = {}) {
160
+ if (isJsonMode(opts)) return;
161
+ if (rows.length === 0) {
162
+ info("(no rows)", opts);
163
+ return;
164
+ }
165
+ const t = new Table({
166
+ head: cols.map((col) => c.bold(c.info(col))),
167
+ // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
168
+ // default red header nor a colored border). All color is ours, via theme.
169
+ style: { head: [], border: [] },
170
+ colWidths: computeColWidths(rows, cols),
171
+ wordWrap: true,
172
+ truncate: "\u2026"
173
+ });
174
+ for (const row of rows) {
175
+ t.push(cols.map((col) => formatCell(row[col])));
176
+ }
177
+ process.stderr.write(`${t.toString()}
178
+ `);
179
+ }
180
+ function computeColWidths(rows, cols) {
181
+ const n = cols.length;
182
+ if (n === 0) return [];
183
+ const headerLen = cols.map((col) => col.length);
184
+ const natural = cols.map((col, i) => {
185
+ let max = Math.max(headerLen[i] ?? col.length, 3);
186
+ for (const row of rows) {
187
+ const len = formatCell(row[col]).length;
188
+ if (len > max) max = len;
189
+ }
190
+ return max;
191
+ });
192
+ const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
193
+ const naturalSum = natural.reduce((a, b) => a + b, 0);
194
+ if (naturalSum <= contentBudget) {
195
+ return natural.map((w) => w + 2);
196
+ }
197
+ const content = natural.slice();
198
+ let sum = naturalSum;
199
+ const floor = headerLen.map((h) => Math.max(h, 3));
200
+ while (sum > contentBudget) {
201
+ let idx = -1;
202
+ let widest = -1;
203
+ for (let i = 0; i < n; i++) {
204
+ const c2 = content[i] ?? 0;
205
+ const f = floor[i] ?? 3;
206
+ if (c2 > f && c2 > widest) {
207
+ widest = c2;
208
+ idx = i;
209
+ }
210
+ }
211
+ if (idx === -1) break;
212
+ const overshoot = sum - contentBudget;
213
+ const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
214
+ const cut = Math.min(overshoot, reducible);
215
+ content[idx] = (content[idx] ?? 0) - cut;
216
+ sum -= cut;
217
+ }
218
+ return content.map((w) => w + 2);
219
+ }
220
+ function formatCell(value) {
221
+ if (value === void 0 || value === null) return "";
222
+ if (typeof value === "string") return value;
223
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
224
+ return String(value);
225
+ }
226
+ return JSON.stringify(value);
227
+ }
228
+ function definitionList(rows, opts = {}) {
229
+ table(
230
+ rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
231
+ ["Field", "Value"],
232
+ opts
233
+ );
234
+ }
235
+ function fail(exitCode, message, opts = {}) {
236
+ if (isJsonMode(opts)) {
237
+ process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
238
+ `);
239
+ } else if (message.length > 0) {
240
+ process.stderr.write(`${message}
241
+ `);
242
+ }
243
+ throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
244
+ }
245
+ function requireInteractive(opts, hint) {
246
+ if (!isInteractive(opts)) {
247
+ fail(
248
+ EXIT.USAGE,
249
+ `${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.`,
250
+ opts
251
+ );
252
+ }
253
+ }
254
+ function commanderExitCode(err) {
255
+ switch (err.code) {
256
+ case "commander.helpDisplayed":
257
+ case "commander.versionDisplayed":
258
+ case "commander.version":
259
+ return EXIT.OK;
260
+ case "commander.unknownCommand":
261
+ return EXIT.NOT_FOUND;
262
+ default:
263
+ return EXIT.USAGE;
264
+ }
265
+ }
266
+ function inferExitCode(err) {
267
+ if (err instanceof NoirCliError) return err.exitCode;
268
+ if (err instanceof CommanderError) {
269
+ if (err.code.startsWith("noir.")) return err.exitCode;
270
+ return commanderExitCode(err);
271
+ }
272
+ return EXIT.ERROR;
273
+ }
274
+ function handleError(err) {
275
+ if (err instanceof NoirCliError) {
276
+ if (err.message.length > 0) process.stderr.write(`${err.message}
277
+ `);
278
+ } else if (err instanceof CommanderError) {
279
+ } else {
280
+ process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}
281
+ `);
282
+ }
283
+ process.exitCode = inferExitCode(err);
284
+ }
11
285
 
12
286
  // src/commands/doctor.ts
13
287
  import { spawnSync } from "child_process";
@@ -286,13 +560,13 @@ function checkRulesMdBudget(checks, root, project) {
286
560
  function summarize(checks) {
287
561
  let ok = 0;
288
562
  let warnN = 0;
289
- let fail = 0;
290
- for (const c of checks) {
291
- if (c.status === "ok") ok++;
292
- else if (c.status === "warn") warnN++;
293
- else fail++;
563
+ let fail2 = 0;
564
+ for (const c2 of checks) {
565
+ if (c2.status === "ok") ok++;
566
+ else if (c2.status === "warn") warnN++;
567
+ else fail2++;
294
568
  }
295
- return { ok, warn: warnN, fail };
569
+ return { ok, warn: warnN, fail: fail2 };
296
570
  }
297
571
  function checkHostArtifacts(checks, root, project) {
298
572
  if (!project) {
@@ -531,24 +805,28 @@ function relOr(abs, root) {
531
805
  function joinRoot(root, relPath) {
532
806
  return `${root}/${relPath}`;
533
807
  }
808
+ function severityBadge(s) {
809
+ const state = s === "fail" ? "error" : s;
810
+ return badge(state, s.toUpperCase());
811
+ }
534
812
  function renderHuman(payload, opts) {
535
813
  log(
536
814
  `noir doctor \u2014 ${payload.checks.length} check${payload.checks.length === 1 ? "" : "s"}`,
537
815
  opts
538
816
  );
539
817
  table(
540
- payload.checks.map((c) => ({
541
- Check: c.name,
542
- Status: c.status.toUpperCase(),
543
- Detail: c.detail
818
+ payload.checks.map((c2) => ({
819
+ Check: c2.name,
820
+ Status: severityBadge(c2.status),
821
+ Detail: c2.detail
544
822
  })),
545
823
  ["Check", "Status", "Detail"],
546
824
  opts
547
825
  );
548
- const { ok, warn: warnN, fail } = payload.summary;
549
- if (fail > 0) {
826
+ const { ok, warn: warnN, fail: fail2 } = payload.summary;
827
+ if (fail2 > 0) {
550
828
  error(
551
- `${fail} critical check${fail === 1 ? "" : "s"} failed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`,
829
+ `${fail2} critical check${fail2 === 1 ? "" : "s"} failed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`,
552
830
  opts
553
831
  );
554
832
  } else if (warnN > 0) {
@@ -600,35 +878,365 @@ async function doctor(opts = {}) {
600
878
  }
601
879
  }
602
880
 
603
- // src/serve.ts
604
- import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
605
- import { ensureDaemonRunning, startStdioServer } from "@noir-ai/daemon";
606
- async function serve(opts) {
607
- const project = loadProjectInfo2(process.cwd());
608
- if (opts.stdio) {
609
- await startStdioServer({ project, transport: "stdio", daemon: false });
610
- return;
881
+ // src/conflict.ts
882
+ function flaggedNonInteractive() {
883
+ const v = process.env.NOIR_NON_INTERACTIVE;
884
+ return v !== void 0 && v !== "";
885
+ }
886
+ function buildConflictOpts(input = {}) {
887
+ if (input.force === true) {
888
+ return { conflictPolicy: "overwrite" };
611
889
  }
612
- try {
613
- const { url } = await ensureDaemonRunning({
614
- project,
615
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
890
+ if (input.interactive === false) {
891
+ return { conflictPolicy: "preserve" };
892
+ }
893
+ if (input.interactive === true) {
894
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
895
+ }
896
+ if (flaggedNonInteractive() || !isInteractive()) {
897
+ return { conflictPolicy: "preserve" };
898
+ }
899
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
900
+ }
901
+ function renderDiffPreview(ctx) {
902
+ const { lineDiff: ld } = __require("@noir-ai/create");
903
+ const lines = ld(ctx.existing, ctx.proposed);
904
+ if (lines.length === 0) return;
905
+ const out = [`noir: conflict in ${ctx.relPath}`];
906
+ for (const dl of lines) {
907
+ if (dl.type === "add") out.push(c.ok(`+ ${dl.line}`));
908
+ else if (dl.type === "del") out.push(c.error(`- ${dl.line}`));
909
+ else out.push(c.dim(` ${dl.line}`));
910
+ }
911
+ process.stderr.write(`${out.join("\n")}
912
+ `);
913
+ }
914
+ async function clackConflictResolver(ctx) {
915
+ renderDiffPreview(ctx);
916
+ const clack = await import("@clack/prompts");
917
+ const options = [
918
+ {
919
+ value: "replace",
920
+ label: "Replace",
921
+ hint: "overwrite with the template (discard your edits)"
922
+ },
923
+ { value: "rename", label: "Rename", hint: "keep yours as <file>.local; write the template" },
924
+ {
925
+ value: "duplicate",
926
+ label: "Create duplicate",
927
+ hint: "write template to <file>.noir; keep yours"
928
+ },
929
+ { value: "preserve", label: "Keep mine", hint: "skip; leave your file unchanged" },
930
+ { value: "cancel", label: "Cancel", hint: "abort the scaffold" }
931
+ ];
932
+ if (ctx.mergedWithMarkers !== void 0) {
933
+ options.splice(4, 0, {
934
+ value: "merge",
935
+ label: "Merge (with conflict markers)",
936
+ hint: "write the 3-way merge with <<<<<<< / ||||||| / ======= / >>>>>>> markers"
616
937
  });
938
+ }
939
+ const choice = await clack.select({
940
+ message: `${ctx.relPath} was edited and differs from the Noir template.`,
941
+ initialValue: "preserve",
942
+ options
943
+ });
944
+ if (clack.isCancel(choice)) return "cancel";
945
+ const resolution = choice;
946
+ const mode = ctx.mode ?? "regenerate";
947
+ if (mode === "regenerate") {
948
+ const remember = await clack.select({
949
+ message: `Apply "${labelFor(resolution)}" to all remaining conflicts this run?`,
950
+ initialValue: "no",
951
+ options: [
952
+ { value: "yes", label: "Yes", hint: "reuse this choice for the rest of this run" },
953
+ { value: "no", label: "No", hint: "prompt again for each conflict" }
954
+ ]
955
+ });
956
+ if (!clack.isCancel(remember) && remember === "yes") {
957
+ return { resolution, applyToAll: true };
958
+ }
959
+ }
960
+ return resolution;
961
+ }
962
+ function labelFor(resolution) {
963
+ switch (resolution) {
964
+ case "replace":
965
+ return "Replace";
966
+ case "rename":
967
+ return "Rename";
968
+ case "duplicate":
969
+ return "Create duplicate";
970
+ case "preserve":
971
+ return "Keep mine";
972
+ case "merge":
973
+ return "Merge (with conflict markers)";
974
+ case "cancel":
975
+ return "Cancel";
976
+ }
977
+ }
978
+
979
+ // src/dedup-write.ts
980
+ import { createHash } from "crypto";
981
+ import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
982
+ import { join as join2 } from "path";
983
+ var HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
984
+ var ACTION_THRESHOLD = 0.95;
985
+ var INFO_THRESHOLD = 0.85;
986
+ var EMBEDDER_PING_TIMEOUT_MS = 5e3;
987
+ var DEDUP_CACHE_VERSION = 1;
988
+ async function checkWritePathDedup(root, res, opts) {
989
+ const result = { found: [], conflicts: [] };
990
+ const writtenHostFiles = res.written.filter((p) => HOST_CONTEXT_RELS.has(p));
991
+ if (writtenHostFiles.length === 0) return result;
992
+ const cache = loadCache(root);
993
+ const embed = await resolveEmbedder(opts, cache);
994
+ if (embed === void 0) return result;
995
+ const cachedEmbed = makeCachedEmbed(cache, embed);
996
+ for (const proposedRel of writtenHostFiles) {
997
+ const proposedAbs = join2(root, proposedRel);
998
+ let proposedText;
999
+ try {
1000
+ proposedText = readFileSync2(proposedAbs, "utf8");
1001
+ } catch {
1002
+ continue;
1003
+ }
1004
+ if (proposedText.trim().length === 0) continue;
1005
+ const candidates = collectDedupCandidates(root).filter((c2) => c2.path !== proposedRel);
1006
+ if (candidates.length === 0) continue;
1007
+ const ctx = await import("@noir-ai/context");
1008
+ const proposed = { path: proposedRel, text: proposedText };
1009
+ let best;
1010
+ try {
1011
+ best = await ctx.findNearestDuplicate(
1012
+ proposed,
1013
+ candidates,
1014
+ async (t) => cachedEmbed(t),
1015
+ INFO_THRESHOLD
1016
+ );
1017
+ } catch (e) {
1018
+ process.stderr.write(
1019
+ `${c.dim(`noir: semantic-dedup skipped for ${proposedRel} (${errMsg(e)}).`)}
1020
+ `
1021
+ );
1022
+ continue;
1023
+ }
1024
+ if (best === null) continue;
1025
+ const matchedRel = best.a === proposedRel ? best.b : best.a;
1026
+ const sim = best.similarity;
1027
+ result.found.push({ proposedRel, matchedRel, similarity: sim });
1028
+ if (sim >= ACTION_THRESHOLD) {
1029
+ if (opts.interactive) {
1030
+ await promptDedupAction(root, proposedRel, matchedRel, proposedText, sim);
1031
+ } else {
1032
+ process.stderr.write(
1033
+ `${c.warn("noir: near-duplicate")} ${proposedRel} \u2248 ${matchedRel} (cosine ${sim.toFixed(2)}); creating anyway.
1034
+ `
1035
+ );
1036
+ }
1037
+ } else {
1038
+ process.stderr.write(
1039
+ `${c.dim(`noir: hint ${proposedRel} looks similar to existing ${matchedRel} (cosine ${sim.toFixed(2)}).`)}
1040
+ `
1041
+ );
1042
+ }
1043
+ const matchedText = safeRead(join2(root, matchedRel));
1044
+ result.conflicts.push({
1045
+ path: proposedRel,
1046
+ mode: "artifact",
1047
+ similarity: sim,
1048
+ existingSha: sha256Hex12(matchedText ?? ""),
1049
+ proposedSha: sha256Hex12(proposedText),
1050
+ resolution: "preserve"
1051
+ });
1052
+ }
1053
+ saveCache(root, cache);
1054
+ return result;
1055
+ }
1056
+ async function resolveEmbedder(opts, cache) {
1057
+ const ctx = await import("@noir-ai/context");
1058
+ let modelId;
1059
+ if (opts.project !== void 0) {
1060
+ const cfg = ctx.resolveEmbedderConfig(opts.project.config.context);
1061
+ if (cfg.kind === "none") {
1062
+ process.stderr.write(`${c.dim("noir: semantic-dedup skipped (embedder kind=none).")}
1063
+ `);
1064
+ return void 0;
1065
+ }
1066
+ modelId = cfg.kind === "local" ? cfg.model ?? ctx.DEFAULT_LOCAL_MODEL : cfg.model ?? cfg.kind;
1067
+ if (modelId !== void 0 && cache.model !== modelId) {
1068
+ cache.model = modelId;
1069
+ cache.entries = {};
1070
+ }
1071
+ }
1072
+ if (opts.embed !== void 0) return opts.embed;
1073
+ if (modelId === void 0) return void 0;
1074
+ let embed;
1075
+ try {
1076
+ const project = opts.project;
1077
+ if (project === void 0) return void 0;
1078
+ const cfg = ctx.resolveEmbedderConfig(project.config.context);
1079
+ embed = ctx.createEmbedFn(cfg).embed;
1080
+ } catch (e) {
617
1081
  process.stderr.write(
618
- `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
1082
+ `${c.dim(`noir: semantic-dedup skipped (embedder unavailable: ${errMsg(e)}).`)}
619
1083
  `
620
1084
  );
621
- } catch (err) {
1085
+ return void 0;
1086
+ }
1087
+ const ping = await withTimeout(
1088
+ embed("ping").then(() => true),
1089
+ EMBEDDER_PING_TIMEOUT_MS
1090
+ );
1091
+ if (ping !== true) {
622
1092
  process.stderr.write(
623
- `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
1093
+ `${c.dim("noir: semantic-dedup skipped (embedder load too slow or unavailable).")}
624
1094
  `
625
1095
  );
626
- await startStdioServer({ project, transport: "stdio", daemon: false });
1096
+ return void 0;
627
1097
  }
1098
+ return embed;
1099
+ }
1100
+ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, sim) {
1101
+ const clack = await import("@clack/prompts");
1102
+ const choice = await clack.select({
1103
+ message: `${proposedRel} is near-duplicate of existing ${matchedRel} (cosine ${sim.toFixed(2)}). Create anyway?`,
1104
+ initialValue: "create",
1105
+ options: [
1106
+ {
1107
+ value: "create",
1108
+ label: "Create anyway",
1109
+ hint: "keep the new file as-is (default)"
1110
+ },
1111
+ { value: "skip", label: "Skip", hint: `delete ${proposedRel}; keep ${matchedRel}` },
1112
+ {
1113
+ value: "replace",
1114
+ label: "Replace existing",
1115
+ hint: `overwrite ${matchedRel} with this content`
1116
+ },
1117
+ {
1118
+ value: "mirror",
1119
+ label: "Mirror into existing",
1120
+ hint: `append this content to ${matchedRel}`
1121
+ }
1122
+ ]
1123
+ });
1124
+ if (clack.isCancel(choice)) return;
1125
+ if (choice === "skip") {
1126
+ try {
1127
+ rmSync(join2(root, proposedRel), { force: true });
1128
+ } catch {
1129
+ }
1130
+ } else if (choice === "replace") {
1131
+ try {
1132
+ writeFileSync(join2(root, matchedRel), proposedText, "utf8");
1133
+ } catch {
1134
+ }
1135
+ } else if (choice === "mirror") {
1136
+ try {
1137
+ const existing = readFileSync2(join2(root, matchedRel), "utf8");
1138
+ writeFileSync(
1139
+ join2(root, matchedRel),
1140
+ `${existing}
1141
+
1142
+ <!-- mirrored from ${proposedRel} -->
1143
+ ${proposedText}
1144
+ `,
1145
+ "utf8"
1146
+ );
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ }
1151
+ function emptyCache() {
1152
+ return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
1153
+ }
1154
+ function cachePath(root) {
1155
+ return join2(root, ".noir", "dedup-cache.json");
1156
+ }
1157
+ function loadCache(root) {
1158
+ try {
1159
+ const raw = readFileSync2(cachePath(root), "utf8");
1160
+ const parsed = JSON.parse(raw);
1161
+ if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
1162
+ if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
1163
+ return {
1164
+ version: DEDUP_CACHE_VERSION,
1165
+ model: typeof parsed.model === "string" ? parsed.model : "",
1166
+ entries: parsed.entries
1167
+ };
1168
+ } catch {
1169
+ return emptyCache();
1170
+ }
1171
+ }
1172
+ function saveCache(root, cache) {
1173
+ try {
1174
+ mkdirSync(join2(root, ".noir"), { recursive: true });
1175
+ writeFileSync(cachePath(root), JSON.stringify(cache), "utf8");
1176
+ } catch {
1177
+ }
1178
+ }
1179
+ function makeCachedEmbed(cache, embed) {
1180
+ return async (text) => {
1181
+ const key = createHash("sha256").update(text, "utf8").digest("hex");
1182
+ const cached = cache.entries[key];
1183
+ if (cached !== void 0) return Float32Array.from(cached.v);
1184
+ const v = await embed(text);
1185
+ cache.entries[key] = { v: Array.from(v) };
1186
+ return v;
1187
+ };
1188
+ }
1189
+ function sha256Hex12(text) {
1190
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 12);
1191
+ }
1192
+ function safeRead(abs) {
1193
+ try {
1194
+ return readFileSync2(abs, "utf8");
1195
+ } catch {
1196
+ return void 0;
1197
+ }
1198
+ }
1199
+ function withTimeout(p, ms) {
1200
+ return new Promise((resolve) => {
1201
+ const t = setTimeout(() => resolve(void 0), ms);
1202
+ p.then(
1203
+ (v) => {
1204
+ clearTimeout(t);
1205
+ resolve(v);
1206
+ },
1207
+ () => {
1208
+ clearTimeout(t);
1209
+ resolve(void 0);
1210
+ }
1211
+ );
1212
+ });
1213
+ }
1214
+ function errMsg(e) {
1215
+ return e instanceof Error ? e.message : String(e);
628
1216
  }
629
1217
 
630
1218
  export {
1219
+ c,
1220
+ badge,
1221
+ useColor,
1222
+ accessibleMode,
1223
+ EXIT,
1224
+ NoirCliError,
1225
+ isInteractive,
1226
+ resolveInteractive,
1227
+ json,
1228
+ log,
1229
+ info,
1230
+ warn,
1231
+ tip,
1232
+ table,
1233
+ definitionList,
1234
+ fail,
1235
+ requireInteractive,
1236
+ inferExitCode,
1237
+ handleError,
631
1238
  doctor,
632
- serve
1239
+ buildConflictOpts,
1240
+ checkWritePathDedup
633
1241
  };
634
- //# sourceMappingURL=chunk-3QMQXBII.js.map
1242
+ //# sourceMappingURL=chunk-YES5UCU5.js.map