@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.
- package/dist/bin.d.ts +30 -2
- package/dist/bin.js +869 -582
- package/dist/bin.js.map +1 -1
- package/dist/chunk-A5W2E3X2.js +33 -0
- package/dist/chunk-A5W2E3X2.js.map +1 -0
- package/dist/{chunk-J2Y3BOVK.js → chunk-SVFQGAOF.js} +32 -8
- package/dist/chunk-SVFQGAOF.js.map +1 -0
- package/dist/{chunk-3QMQXBII.js → chunk-YES5UCU5.js} +648 -40
- package/dist/chunk-YES5UCU5.js.map +1 -0
- package/dist/{create-VK7JX6IB.js → create-7ERBL42K.js} +31 -7
- package/dist/create-7ERBL42K.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +5 -4
- package/dist/{output-De3lKbZq.d.ts → output-BNE9htEm.d.ts} +9 -0
- package/dist/sync-SCAF6Z5H.js +82 -0
- package/dist/sync-SCAF6Z5H.js.map +1 -0
- package/dist/tui/chunk-OMF7XZMN.js +99 -0
- package/dist/tui/chunk-OMF7XZMN.js.map +1 -0
- package/dist/tui/chunk-YNEBQJXA.js +1243 -0
- package/dist/tui/chunk-YNEBQJXA.js.map +1 -0
- package/dist/tui/create-U2YJH5QC.js +72 -0
- package/dist/tui/create-U2YJH5QC.js.map +1 -0
- package/dist/tui/index.js +2253 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/sync-VYRXCGK4.js +81 -0
- package/dist/tui/sync-VYRXCGK4.js.map +1 -0
- package/package.json +15 -11
- package/dist/chunk-3QMQXBII.js.map +0 -1
- package/dist/chunk-J2Y3BOVK.js.map +0 -1
- package/dist/chunk-OF4OO5W6.js +0 -190
- package/dist/chunk-OF4OO5W6.js.map +0 -1
- package/dist/create-VK7JX6IB.js.map +0 -1
- package/dist/sync-7XFGCRJN.js +0 -55
- package/dist/sync-7XFGCRJN.js.map +0 -1
|
@@ -1,13 +1,287 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
290
|
-
for (const
|
|
291
|
-
if (
|
|
292
|
-
else if (
|
|
293
|
-
else
|
|
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((
|
|
541
|
-
Check:
|
|
542
|
-
Status:
|
|
543
|
-
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 (
|
|
826
|
+
const { ok, warn: warnN, fail: fail2 } = payload.summary;
|
|
827
|
+
if (fail2 > 0) {
|
|
550
828
|
error(
|
|
551
|
-
`${
|
|
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/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
`
|
|
1082
|
+
`${c.dim(`noir: semantic-dedup skipped (embedder unavailable: ${errMsg(e)}).`)}
|
|
619
1083
|
`
|
|
620
1084
|
);
|
|
621
|
-
|
|
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
|
-
|
|
1093
|
+
`${c.dim("noir: semantic-dedup skipped (embedder load too slow or unavailable).")}
|
|
624
1094
|
`
|
|
625
1095
|
);
|
|
626
|
-
|
|
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
|
-
|
|
1239
|
+
buildConflictOpts,
|
|
1240
|
+
checkWritePathDedup
|
|
633
1241
|
};
|
|
634
|
-
//# sourceMappingURL=chunk-
|
|
1242
|
+
//# sourceMappingURL=chunk-YES5UCU5.js.map
|