@noir-ai/cli 1.15.0 → 1.16.0
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/README.md +1 -1
- package/dist/bin.js +806 -178
- package/dist/bin.js.map +1 -1
- package/dist/hygiene-scan.d.ts +65 -0
- package/dist/hygiene-scan.js +165 -0
- package/dist/hygiene-scan.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +836 -210
- package/dist/index.js.map +1 -1
- package/dist/tui/{chunk-5KWETEMF.js → chunk-6AUAIEFG.js} +668 -170
- package/dist/tui/chunk-6AUAIEFG.js.map +1 -0
- package/dist/tui/{chunk-TPNJEN4B.js → chunk-CQU2INJH.js} +2 -2
- package/dist/tui/{chunk-KETMM3BA.js → chunk-KVJIPKVE.js} +133 -20
- package/dist/tui/chunk-KVJIPKVE.js.map +1 -0
- package/dist/tui/{create-6KDIFPUL.js → create-ZZN7XFLG.js} +6 -4
- package/dist/tui/create-ZZN7XFLG.js.map +1 -0
- package/dist/tui/index.js +37 -19
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{install-KA35ONQP.js → install-R5U2WTAQ.js} +2 -2
- package/dist/tui/{sync-AII5WCRO.js → sync-GHEPFPLX.js} +6 -4
- package/dist/tui/sync-GHEPFPLX.js.map +1 -0
- package/dist/tui/{update-XGKJ4KBR.js → update-ZFOLOSZ2.js} +3 -3
- package/package.json +18 -12
- package/dist/tui/chunk-5KWETEMF.js.map +0 -1
- package/dist/tui/chunk-KETMM3BA.js.map +0 -1
- package/dist/tui/create-6KDIFPUL.js.map +0 -1
- package/dist/tui/sync-AII5WCRO.js.map +0 -1
- /package/dist/tui/{chunk-TPNJEN4B.js.map → chunk-CQU2INJH.js.map} +0 -0
- /package/dist/tui/{install-KA35ONQP.js.map → install-R5U2WTAQ.js.map} +0 -0
- /package/dist/tui/{update-XGKJ4KBR.js.map → update-ZFOLOSZ2.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -14,10 +14,82 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/width.ts
|
|
18
|
+
import stringWidth from "string-width";
|
|
19
|
+
import stripAnsi from "strip-ansi";
|
|
20
|
+
function clusters(s) {
|
|
21
|
+
return [...segmenter.segment(s)].map(({ segment }) => segment);
|
|
22
|
+
}
|
|
23
|
+
function displayWidth(s) {
|
|
24
|
+
return stringWidth(s);
|
|
25
|
+
}
|
|
26
|
+
function fitLeading(parts, width) {
|
|
27
|
+
let used = 0;
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const w = displayWidth(part);
|
|
31
|
+
if (used + w > width) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
used += w;
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
function truncateToWidth(s, width, ellipsis = "\u2026") {
|
|
40
|
+
const plain = stripAnsi(s);
|
|
41
|
+
if (width <= 0) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
if (displayWidth(plain) <= width) {
|
|
45
|
+
return plain;
|
|
46
|
+
}
|
|
47
|
+
const markerParts = clusters(ellipsis);
|
|
48
|
+
const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
|
|
49
|
+
const budget = width - displayWidth(marker);
|
|
50
|
+
const parts = clusters(plain);
|
|
51
|
+
return parts.slice(0, fitLeading(parts, budget)).join("") + marker;
|
|
52
|
+
}
|
|
53
|
+
function truncateMiddle(s, width, ellipsis = "\u2026") {
|
|
54
|
+
const plain = stripAnsi(s);
|
|
55
|
+
if (width <= 0) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
if (displayWidth(plain) <= width) {
|
|
59
|
+
return plain;
|
|
60
|
+
}
|
|
61
|
+
const markerParts = clusters(ellipsis);
|
|
62
|
+
const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
|
|
63
|
+
const remaining = width - displayWidth(marker);
|
|
64
|
+
const headBudget = Math.ceil(remaining / 2);
|
|
65
|
+
const tailBudget = remaining - headBudget;
|
|
66
|
+
const parts = clusters(plain);
|
|
67
|
+
const headCount = fitLeading(parts, headBudget);
|
|
68
|
+
const tail = [];
|
|
69
|
+
let tailUsed = 0;
|
|
70
|
+
for (const part of parts.slice(headCount).reverse()) {
|
|
71
|
+
const w = displayWidth(part);
|
|
72
|
+
if (tailUsed + w > tailBudget) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
tail.push(part);
|
|
76
|
+
tailUsed += w;
|
|
77
|
+
}
|
|
78
|
+
return parts.slice(0, headCount).join("") + marker + tail.reverse().join("");
|
|
79
|
+
}
|
|
80
|
+
var segmenter;
|
|
81
|
+
var init_width = __esm({
|
|
82
|
+
"src/width.ts"() {
|
|
83
|
+
"use strict";
|
|
84
|
+
segmenter = new Intl.Segmenter();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
17
88
|
// src/theme.ts
|
|
18
89
|
import pc from "picocolors";
|
|
19
|
-
function badge(state, label = state) {
|
|
20
|
-
const
|
|
90
|
+
function badge(state, label = state, maxWidth) {
|
|
91
|
+
const full = `${BADGE_SYMBOL[state]} ${label}`;
|
|
92
|
+
const text = maxWidth === void 0 ? full : truncateToWidth(full, maxWidth);
|
|
21
93
|
switch (state) {
|
|
22
94
|
case "ok":
|
|
23
95
|
return c.ok(text);
|
|
@@ -38,8 +110,13 @@ function isCiEnv() {
|
|
|
38
110
|
if (v === void 0 || v === "") return false;
|
|
39
111
|
return v !== "0" && v !== "false";
|
|
40
112
|
}
|
|
113
|
+
function quietMode() {
|
|
114
|
+
const v = process.env.NOIR_QUIET;
|
|
115
|
+
return v !== void 0 && v !== "";
|
|
116
|
+
}
|
|
41
117
|
function useColor() {
|
|
42
118
|
if (envFlagSet("NO_COLOR")) return false;
|
|
119
|
+
if (quietMode()) return false;
|
|
43
120
|
if (process.env.CLICOLOR_FORCE === "1") return true;
|
|
44
121
|
if (isCiEnv()) return false;
|
|
45
122
|
return pc.isColorSupported;
|
|
@@ -53,14 +130,17 @@ function terminalWidth() {
|
|
|
53
130
|
const n = Number(fromEnv);
|
|
54
131
|
if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
|
|
55
132
|
}
|
|
56
|
-
const
|
|
57
|
-
if (typeof
|
|
133
|
+
const stderrCols = process.stderr.columns;
|
|
134
|
+
if (typeof stderrCols === "number" && stderrCols > 0) return Math.max(stderrCols, 20);
|
|
135
|
+
const stdoutCols = process.stdout.columns;
|
|
136
|
+
if (typeof stdoutCols === "number" && stdoutCols > 0) return Math.max(stdoutCols, 20);
|
|
58
137
|
return 80;
|
|
59
138
|
}
|
|
60
139
|
var FORCE, c, BADGE_SYMBOL;
|
|
61
140
|
var init_theme = __esm({
|
|
62
141
|
"src/theme.ts"() {
|
|
63
142
|
"use strict";
|
|
143
|
+
init_width();
|
|
64
144
|
FORCE = pc.createColors(true);
|
|
65
145
|
c = {
|
|
66
146
|
/** Green — success / healthy. */
|
|
@@ -159,17 +239,18 @@ function table(rows, cols, opts = {}) {
|
|
|
159
239
|
info("(no rows)", opts);
|
|
160
240
|
return;
|
|
161
241
|
}
|
|
242
|
+
const widths = computeColWidths(rows, cols);
|
|
162
243
|
const t = new Table({
|
|
163
244
|
head: cols.map((col) => c.bold(c.info(col))),
|
|
164
245
|
// Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
|
|
165
246
|
// default red header nor a colored border). All color is ours, via theme.
|
|
166
247
|
style: { head: [], border: [] },
|
|
167
|
-
colWidths:
|
|
248
|
+
colWidths: widths,
|
|
168
249
|
wordWrap: true,
|
|
169
250
|
truncate: "\u2026"
|
|
170
251
|
});
|
|
171
252
|
for (const row of rows) {
|
|
172
|
-
t.push(cols.map((col) =>
|
|
253
|
+
t.push(cols.map((col, i) => truncateCell(row[col], (widths[i] ?? 0) - 2)));
|
|
173
254
|
}
|
|
174
255
|
process.stderr.write(`${t.toString()}
|
|
175
256
|
`);
|
|
@@ -177,43 +258,71 @@ function table(rows, cols, opts = {}) {
|
|
|
177
258
|
function computeColWidths(rows, cols) {
|
|
178
259
|
const n = cols.length;
|
|
179
260
|
if (n === 0) return [];
|
|
180
|
-
const headerLen = cols.map((col) => col
|
|
261
|
+
const headerLen = cols.map((col) => displayWidth(col));
|
|
181
262
|
const natural = cols.map((col, i) => {
|
|
182
|
-
let max = Math.max(headerLen[i] ?? col
|
|
263
|
+
let max = Math.max(headerLen[i] ?? displayWidth(col), MIN_CONTENT_WIDTH);
|
|
183
264
|
for (const row of rows) {
|
|
184
|
-
const len = formatCell(row[col])
|
|
265
|
+
const len = displayWidth(formatCell(row[col]));
|
|
185
266
|
if (len > max) max = len;
|
|
186
267
|
}
|
|
187
268
|
return max;
|
|
188
269
|
});
|
|
189
|
-
const
|
|
270
|
+
const budget = Math.max(n, terminalWidth() - 3 * n - 1);
|
|
190
271
|
const naturalSum = natural.reduce((a, b) => a + b, 0);
|
|
191
|
-
if (naturalSum <=
|
|
272
|
+
if (naturalSum <= budget) {
|
|
192
273
|
return natural.map((w) => w + 2);
|
|
193
274
|
}
|
|
194
275
|
const content = natural.slice();
|
|
195
276
|
let sum = naturalSum;
|
|
196
|
-
const floor = headerLen.map((h) => Math.max(h,
|
|
197
|
-
while (sum >
|
|
277
|
+
const floor = headerLen.map((h) => Math.min(Math.max(h, MIN_CONTENT_WIDTH), budget));
|
|
278
|
+
while (sum > budget) {
|
|
198
279
|
let idx = -1;
|
|
199
280
|
let widest = -1;
|
|
200
281
|
for (let i = 0; i < n; i++) {
|
|
201
282
|
const c2 = content[i] ?? 0;
|
|
202
|
-
const f = floor[i] ??
|
|
283
|
+
const f = floor[i] ?? MIN_CONTENT_WIDTH;
|
|
203
284
|
if (c2 > f && c2 > widest) {
|
|
204
285
|
widest = c2;
|
|
205
286
|
idx = i;
|
|
206
287
|
}
|
|
207
288
|
}
|
|
208
289
|
if (idx === -1) break;
|
|
209
|
-
const overshoot = sum -
|
|
210
|
-
const reducible = (content[idx] ?? 0) - (floor[idx] ??
|
|
290
|
+
const overshoot = sum - budget;
|
|
291
|
+
const reducible = (content[idx] ?? 0) - (floor[idx] ?? MIN_CONTENT_WIDTH);
|
|
211
292
|
const cut = Math.min(overshoot, reducible);
|
|
212
293
|
content[idx] = (content[idx] ?? 0) - cut;
|
|
213
294
|
sum -= cut;
|
|
214
295
|
}
|
|
296
|
+
if (sum > budget) {
|
|
297
|
+
for (let pass = 0; pass < n && sum > budget; pass++) {
|
|
298
|
+
let idx = -1;
|
|
299
|
+
let widest = 0;
|
|
300
|
+
for (let j = 0; j < n; j++) {
|
|
301
|
+
const c2 = content[j] ?? 0;
|
|
302
|
+
if (c2 > 1 && c2 > widest) {
|
|
303
|
+
widest = c2;
|
|
304
|
+
idx = j;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (idx === -1) break;
|
|
308
|
+
const cut = Math.min(sum - budget, (content[idx] ?? 1) - 1);
|
|
309
|
+
content[idx] = (content[idx] ?? 1) - cut;
|
|
310
|
+
sum -= cut;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
215
313
|
return content.map((w) => w + 2);
|
|
216
314
|
}
|
|
315
|
+
function isPathToken(s) {
|
|
316
|
+
if (!/[\\/]/.test(s)) return false;
|
|
317
|
+
return /^(\/|\.\/|\.\.\/|~\/|[A-Za-z]:[\\/])/.test(s) || /\.[A-Za-z0-9][A-Za-z0-9_-]*$/.test(s);
|
|
318
|
+
}
|
|
319
|
+
function truncateCell(value, contentWidth) {
|
|
320
|
+
const s = formatCell(value);
|
|
321
|
+
if (contentWidth <= 0 || displayWidth(s) <= contentWidth) return s;
|
|
322
|
+
if (isPathToken(s)) return truncateMiddle(s, contentWidth);
|
|
323
|
+
if (/\s/.test(s)) return s;
|
|
324
|
+
return truncateToWidth(s, contentWidth);
|
|
325
|
+
}
|
|
217
326
|
function formatCell(value) {
|
|
218
327
|
if (value === void 0 || value === null) return "";
|
|
219
328
|
if (typeof value === "string") return value;
|
|
@@ -224,7 +333,7 @@ function formatCell(value) {
|
|
|
224
333
|
}
|
|
225
334
|
function definitionList(rows, opts = {}) {
|
|
226
335
|
table(
|
|
227
|
-
rows.map((r) => ({ Field: r.label, Value:
|
|
336
|
+
rows.map((r) => ({ Field: r.label, Value: r.value })),
|
|
228
337
|
["Field", "Value"],
|
|
229
338
|
opts
|
|
230
339
|
);
|
|
@@ -305,11 +414,12 @@ function handleError(err) {
|
|
|
305
414
|
}
|
|
306
415
|
process.exitCode = inferExitCode(err);
|
|
307
416
|
}
|
|
308
|
-
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
417
|
+
var EXIT, NoirCliError, NOIR_ERROR_CODE, MIN_CONTENT_WIDTH, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
309
418
|
var init_output = __esm({
|
|
310
419
|
"src/output.ts"() {
|
|
311
420
|
"use strict";
|
|
312
421
|
init_theme();
|
|
422
|
+
init_width();
|
|
313
423
|
EXIT = {
|
|
314
424
|
OK: 0,
|
|
315
425
|
ERROR: 1,
|
|
@@ -327,6 +437,7 @@ var init_output = __esm({
|
|
|
327
437
|
}
|
|
328
438
|
};
|
|
329
439
|
NOIR_ERROR_CODE = "noir.error";
|
|
440
|
+
MIN_CONTENT_WIDTH = 3;
|
|
330
441
|
NoopSpinner = class {
|
|
331
442
|
text = "";
|
|
332
443
|
start(_text) {
|
|
@@ -561,14 +672,230 @@ var init_context = __esm({
|
|
|
561
672
|
}
|
|
562
673
|
});
|
|
563
674
|
|
|
675
|
+
// src/workspace-bridge.ts
|
|
676
|
+
import {
|
|
677
|
+
ReadBuffer,
|
|
678
|
+
StreamableHTTPClientTransport,
|
|
679
|
+
serializeMessage
|
|
680
|
+
} from "@modelcontextprotocol/client";
|
|
681
|
+
import { loadProjectInfo } from "@noir-ai/core";
|
|
682
|
+
import { pidAlive, readDaemonToken, readWorkspaceDaemonRecord, tokenPath } from "@noir-ai/daemon";
|
|
683
|
+
async function resolveWorkspaceDaemon(name, root) {
|
|
684
|
+
const record = readWorkspaceDaemonRecord(name);
|
|
685
|
+
if (record === null) {
|
|
686
|
+
return { error: `no daemon recorded for workspace ${name}` };
|
|
687
|
+
}
|
|
688
|
+
const health = await verifyWorkspaceDaemon(record, name);
|
|
689
|
+
if (health.kind === "foreign") {
|
|
690
|
+
return {
|
|
691
|
+
error: `workspace ${name} is recorded on port ${record.port}, but the daemon answering there serves workspace ${health.workspace}`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (health.kind === "silent") {
|
|
695
|
+
return { error: `record exists but the daemon is not answering (pid ${record.pid})` };
|
|
696
|
+
}
|
|
697
|
+
const token = readDaemonToken(name);
|
|
698
|
+
if (token === null) {
|
|
699
|
+
return {
|
|
700
|
+
error: `workspace ${name} daemon is answering but its token is unreadable at ${tokenPath(name)}`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
let callerProjectId;
|
|
704
|
+
try {
|
|
705
|
+
callerProjectId = loadProjectInfo(root).id;
|
|
706
|
+
} catch {
|
|
707
|
+
return {
|
|
708
|
+
error: `no project identity in ${root} \u2014 run \`noir init\` before serving a workspace from here`
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return { url: `http://127.0.0.1:${record.port}/mcp?p=${callerProjectId}`, token };
|
|
712
|
+
}
|
|
713
|
+
async function verifyWorkspaceDaemon(record, name) {
|
|
714
|
+
if (!pidAlive(record.pid)) return { kind: "silent" };
|
|
715
|
+
return probeHealth(record.port, name, record.pid);
|
|
716
|
+
}
|
|
717
|
+
async function probeHealth(port, name, pid) {
|
|
718
|
+
try {
|
|
719
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
720
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
|
|
721
|
+
});
|
|
722
|
+
if (!response.ok) return { kind: "silent" };
|
|
723
|
+
const body = await response.json().catch(() => null);
|
|
724
|
+
if (body === null) return { kind: "silent" };
|
|
725
|
+
if (typeof body.workspace === "string" && body.workspace !== name) {
|
|
726
|
+
return { kind: "foreign", workspace: body.workspace };
|
|
727
|
+
}
|
|
728
|
+
if (body.ok !== true || body.pid !== pid) return { kind: "silent" };
|
|
729
|
+
return { kind: "healthy" };
|
|
730
|
+
} catch {
|
|
731
|
+
return { kind: "silent" };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function bridgeStdioToWorkspace(name, root) {
|
|
735
|
+
const resolved = await resolveWorkspaceDaemon(name, root);
|
|
736
|
+
if ("error" in resolved) {
|
|
737
|
+
fail(EXIT.DAEMON_DOWN, resolved.error);
|
|
738
|
+
}
|
|
739
|
+
const host = new StdioBridgeTransport(process.stdin, process.stdout);
|
|
740
|
+
const daemon = new StreamableHTTPClientTransport(new URL(resolved.url), {
|
|
741
|
+
requestInit: { headers: { Authorization: `Bearer ${resolved.token}` } }
|
|
742
|
+
});
|
|
743
|
+
try {
|
|
744
|
+
await relay(host, daemon);
|
|
745
|
+
} finally {
|
|
746
|
+
await daemon.close().catch(() => {
|
|
747
|
+
});
|
|
748
|
+
await host.close().catch(() => {
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function relay(host, daemon) {
|
|
753
|
+
await new Promise((resolve3) => {
|
|
754
|
+
let settled = false;
|
|
755
|
+
let closing = false;
|
|
756
|
+
const settle = () => {
|
|
757
|
+
if (settled) return;
|
|
758
|
+
settled = true;
|
|
759
|
+
closing = true;
|
|
760
|
+
resolve3();
|
|
761
|
+
};
|
|
762
|
+
const closeDaemon = () => {
|
|
763
|
+
closing = true;
|
|
764
|
+
void daemon.close().catch(() => {
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
host.onmessage = (message) => {
|
|
768
|
+
void daemon.send(message).catch(closeDaemon);
|
|
769
|
+
};
|
|
770
|
+
host.onclose = () => {
|
|
771
|
+
closeDaemon();
|
|
772
|
+
settle();
|
|
773
|
+
};
|
|
774
|
+
host.onerror = () => {
|
|
775
|
+
closeDaemon();
|
|
776
|
+
settle();
|
|
777
|
+
};
|
|
778
|
+
daemon.onmessage = (message) => {
|
|
779
|
+
void host.send(message).catch(() => {
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
daemon.onclose = () => {
|
|
783
|
+
void host.close().catch(() => {
|
|
784
|
+
});
|
|
785
|
+
settle();
|
|
786
|
+
};
|
|
787
|
+
daemon.onerror = (error2) => {
|
|
788
|
+
if (closing) return;
|
|
789
|
+
process.stderr.write(
|
|
790
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
791
|
+
`
|
|
792
|
+
);
|
|
793
|
+
};
|
|
794
|
+
const failStart = (error2) => {
|
|
795
|
+
process.stderr.write(
|
|
796
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
797
|
+
`
|
|
798
|
+
);
|
|
799
|
+
host.onerror?.(error2);
|
|
800
|
+
closeDaemon();
|
|
801
|
+
settle();
|
|
802
|
+
};
|
|
803
|
+
void daemon.start().catch(failStart);
|
|
804
|
+
void host.start().catch(failStart);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function toError(value) {
|
|
808
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
809
|
+
}
|
|
810
|
+
var HEALTH_PROBE_TIMEOUT_MS, StdioBridgeTransport;
|
|
811
|
+
var init_workspace_bridge = __esm({
|
|
812
|
+
"src/workspace-bridge.ts"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
init_output();
|
|
815
|
+
HEALTH_PROBE_TIMEOUT_MS = 1500;
|
|
816
|
+
StdioBridgeTransport = class {
|
|
817
|
+
constructor(stdin, stdout) {
|
|
818
|
+
this.stdin = stdin;
|
|
819
|
+
this.stdout = stdout;
|
|
820
|
+
}
|
|
821
|
+
stdin;
|
|
822
|
+
stdout;
|
|
823
|
+
onclose;
|
|
824
|
+
onerror;
|
|
825
|
+
onmessage;
|
|
826
|
+
buffer = new ReadBuffer();
|
|
827
|
+
started = false;
|
|
828
|
+
closed = false;
|
|
829
|
+
async start() {
|
|
830
|
+
if (this.started) throw new Error("the stdio bridge transport is already started");
|
|
831
|
+
this.started = true;
|
|
832
|
+
this.stdin.on("data", this.onData);
|
|
833
|
+
this.stdin.on("error", this.onStdinFault);
|
|
834
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
835
|
+
this.stdout.on("error", this.onStdoutFault);
|
|
836
|
+
}
|
|
837
|
+
send(message) {
|
|
838
|
+
if (this.closed) return Promise.reject(new Error("the stdio bridge transport is closed"));
|
|
839
|
+
return new Promise((resolve3, reject) => {
|
|
840
|
+
if (this.stdout.write(serializeMessage(message))) {
|
|
841
|
+
resolve3();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const onDrain = () => {
|
|
845
|
+
this.stdout.off("error", onWriteError);
|
|
846
|
+
resolve3();
|
|
847
|
+
};
|
|
848
|
+
const onWriteError = (error2) => {
|
|
849
|
+
this.stdout.off("drain", onDrain);
|
|
850
|
+
reject(error2);
|
|
851
|
+
};
|
|
852
|
+
this.stdout.once("drain", onDrain);
|
|
853
|
+
this.stdout.once("error", onWriteError);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async close() {
|
|
857
|
+
if (this.closed) return;
|
|
858
|
+
this.closed = true;
|
|
859
|
+
this.stdin.off("data", this.onData);
|
|
860
|
+
this.stdin.off("error", this.onStdinFault);
|
|
861
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
862
|
+
this.stdout.off("error", this.onStdoutFault);
|
|
863
|
+
this.buffer.clear();
|
|
864
|
+
if (this.stdin.listenerCount("data") === 0) this.stdin.pause();
|
|
865
|
+
this.onclose?.();
|
|
866
|
+
}
|
|
867
|
+
onData = (chunk) => {
|
|
868
|
+
try {
|
|
869
|
+
this.buffer.append(chunk);
|
|
870
|
+
for (; ; ) {
|
|
871
|
+
const message = this.buffer.readMessage();
|
|
872
|
+
if (message === null) return;
|
|
873
|
+
this.onmessage?.(message);
|
|
874
|
+
}
|
|
875
|
+
} catch (error2) {
|
|
876
|
+
this.onerror?.(toError(error2));
|
|
877
|
+
void this.close();
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
/** The host closed its end of the pipe — the session is over. */
|
|
881
|
+
onStdinEnd = () => {
|
|
882
|
+
void this.close();
|
|
883
|
+
};
|
|
884
|
+
onStdinFault = (error2) => {
|
|
885
|
+
this.onerror?.(error2);
|
|
886
|
+
void this.close();
|
|
887
|
+
};
|
|
888
|
+
onStdoutFault = (error2) => {
|
|
889
|
+
this.onerror?.(error2);
|
|
890
|
+
};
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
564
895
|
// src/workspace-mcp.ts
|
|
565
896
|
import { existsSync, readFileSync } from "fs";
|
|
566
|
-
import {
|
|
567
|
-
import { resolveAdapter } from "@noir-ai/adapters";
|
|
897
|
+
import { mcpConfigPathFor, noirStdioArgs, TRANSPORT_KEYS } from "@noir-ai/adapters";
|
|
568
898
|
import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
|
|
569
|
-
function mcpPathFor(root, host) {
|
|
570
|
-
return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
|
|
571
|
-
}
|
|
572
899
|
function readJson(path) {
|
|
573
900
|
if (!existsSync(path)) return null;
|
|
574
901
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -602,26 +929,37 @@ function isNoirEmitted(mcpServers) {
|
|
|
602
929
|
if (typeof noir.command === "string") return true;
|
|
603
930
|
return noir.type === "http";
|
|
604
931
|
}
|
|
605
|
-
function
|
|
606
|
-
const
|
|
932
|
+
function noirEntry(mcpServers) {
|
|
933
|
+
const noir = mcpServers?.noir;
|
|
934
|
+
if (typeof noir !== "object" || noir === null || Array.isArray(noir)) return {};
|
|
935
|
+
return noir;
|
|
936
|
+
}
|
|
937
|
+
function writeNoirTransport(root, host, transport, opts) {
|
|
938
|
+
const path = mcpConfigPathFor(host, root);
|
|
607
939
|
const existing = loadExisting(path, opts);
|
|
940
|
+
const kept = Object.fromEntries(
|
|
941
|
+
Object.entries(noirEntry(existing?.mcpServers)).filter(([key]) => !TRANSPORT_KEYS.has(key))
|
|
942
|
+
);
|
|
608
943
|
const next = {
|
|
609
944
|
...existing ?? {},
|
|
610
945
|
mcpServers: {
|
|
611
946
|
...existing?.mcpServers ?? {},
|
|
612
|
-
noir: {
|
|
947
|
+
noir: { ...transport, ...kept }
|
|
613
948
|
}
|
|
614
949
|
};
|
|
615
950
|
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
616
951
|
`);
|
|
617
952
|
}
|
|
953
|
+
function writeWorkspaceEntry(root, host, name, opts) {
|
|
954
|
+
writeNoirTransport(
|
|
955
|
+
root,
|
|
956
|
+
host,
|
|
957
|
+
{ command: resolveNoirCommand(), args: noirStdioArgs(name) },
|
|
958
|
+
opts
|
|
959
|
+
);
|
|
960
|
+
}
|
|
618
961
|
function writeStdioEntry(root, host, opts) {
|
|
619
|
-
|
|
620
|
-
const existing = loadExisting(path, opts);
|
|
621
|
-
const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
|
|
622
|
-
const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
|
|
623
|
-
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
624
|
-
`);
|
|
962
|
+
writeNoirTransport(root, host, { command: resolveNoirCommand(), args: noirStdioArgs() }, opts);
|
|
625
963
|
}
|
|
626
964
|
var init_workspace_mcp = __esm({
|
|
627
965
|
"src/workspace-mcp.ts"() {
|
|
@@ -637,7 +975,7 @@ import {
|
|
|
637
975
|
ensureWorkspaceRegistry,
|
|
638
976
|
isValidWorkspaceName,
|
|
639
977
|
isWorkspaceMember,
|
|
640
|
-
loadProjectInfo,
|
|
978
|
+
loadProjectInfo as loadProjectInfo2,
|
|
641
979
|
readWorkspaceMarker,
|
|
642
980
|
readWorkspaceRegistry,
|
|
643
981
|
removeWorkspaceMember,
|
|
@@ -648,20 +986,20 @@ import {
|
|
|
648
986
|
import {
|
|
649
987
|
clearWorkspaceDaemonRecord,
|
|
650
988
|
ensureWorkspaceDaemonRunning,
|
|
651
|
-
pidAlive,
|
|
652
|
-
readWorkspaceDaemonRecord,
|
|
989
|
+
pidAlive as pidAlive2,
|
|
990
|
+
readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
|
|
653
991
|
spawnDetachedWorkspaceDaemon
|
|
654
992
|
} from "@noir-ai/daemon";
|
|
655
993
|
function loadProjectOrFail(opts) {
|
|
656
994
|
try {
|
|
657
|
-
return
|
|
995
|
+
return loadProjectInfo2(process.cwd());
|
|
658
996
|
} catch {
|
|
659
997
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
660
998
|
}
|
|
661
999
|
}
|
|
662
|
-
function finishJoin(project, name,
|
|
1000
|
+
function finishJoin(project, name, opts) {
|
|
663
1001
|
writeWorkspaceMarker(project.root, name);
|
|
664
|
-
|
|
1002
|
+
writeWorkspaceEntry(project.root, project.config.host, name, opts);
|
|
665
1003
|
}
|
|
666
1004
|
async function daemonStartWorkspace(opts) {
|
|
667
1005
|
const project = loadProjectOrFail(opts);
|
|
@@ -694,7 +1032,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
694
1032
|
}
|
|
695
1033
|
if (opts.detach === true) {
|
|
696
1034
|
const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
|
|
697
|
-
finishJoin(project, opts.name,
|
|
1035
|
+
finishJoin(project, opts.name, opts);
|
|
698
1036
|
if (opts.json === true) {
|
|
699
1037
|
process.stdout.write(
|
|
700
1038
|
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
@@ -713,7 +1051,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
713
1051
|
project,
|
|
714
1052
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
715
1053
|
});
|
|
716
|
-
finishJoin(project, opts.name,
|
|
1054
|
+
finishJoin(project, opts.name, opts);
|
|
717
1055
|
if (opts.json === true) {
|
|
718
1056
|
process.stdout.write(
|
|
719
1057
|
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
|
|
@@ -743,7 +1081,7 @@ async function daemonJoin(opts) {
|
|
|
743
1081
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
744
1082
|
});
|
|
745
1083
|
upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
|
|
746
|
-
finishJoin(project, opts.name,
|
|
1084
|
+
finishJoin(project, opts.name, opts);
|
|
747
1085
|
if (opts.json === true) {
|
|
748
1086
|
process.stdout.write(
|
|
749
1087
|
`${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
|
|
@@ -760,19 +1098,20 @@ async function workspaceList(opts) {
|
|
|
760
1098
|
} catch {
|
|
761
1099
|
names = [];
|
|
762
1100
|
}
|
|
763
|
-
const rows =
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1101
|
+
const rows = await Promise.all(
|
|
1102
|
+
names.map(async (n) => {
|
|
1103
|
+
const reg = readWorkspaceRegistry(n);
|
|
1104
|
+
const rec = readWorkspaceDaemonRecord2(n);
|
|
1105
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, n);
|
|
1106
|
+
const running = health?.kind === "healthy";
|
|
1107
|
+
return {
|
|
1108
|
+
name: n,
|
|
1109
|
+
members: reg?.members.length ?? 0,
|
|
1110
|
+
running,
|
|
1111
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
1112
|
+
};
|
|
1113
|
+
})
|
|
1114
|
+
);
|
|
776
1115
|
if (opts.json === true) {
|
|
777
1116
|
process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
|
|
778
1117
|
`);
|
|
@@ -783,7 +1122,8 @@ async function workspaceList(opts) {
|
|
|
783
1122
|
return;
|
|
784
1123
|
}
|
|
785
1124
|
for (const r of rows) {
|
|
786
|
-
|
|
1125
|
+
const state = r.running ? ", running" : r.stale ? ", not running (stale record)" : "";
|
|
1126
|
+
log(`${r.name} \u2014 ${r.members} member(s)${state}`, opts);
|
|
787
1127
|
}
|
|
788
1128
|
}
|
|
789
1129
|
async function workspaceStatus(opts) {
|
|
@@ -795,23 +1135,23 @@ async function workspaceStatus(opts) {
|
|
|
795
1135
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
796
1136
|
}
|
|
797
1137
|
const reg = readWorkspaceRegistry(name);
|
|
798
|
-
const rec =
|
|
799
|
-
const
|
|
1138
|
+
const rec = readWorkspaceDaemonRecord2(name);
|
|
1139
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, name);
|
|
1140
|
+
const running = health?.kind === "healthy";
|
|
800
1141
|
const data = {
|
|
801
1142
|
name,
|
|
802
1143
|
members: reg?.members.map((m) => m.projectId) ?? [],
|
|
803
1144
|
running,
|
|
804
|
-
...rec ? { pid: rec.pid, port: rec.port } : {}
|
|
1145
|
+
...rec ? { pid: rec.pid, port: rec.port } : {},
|
|
1146
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
805
1147
|
};
|
|
806
1148
|
if (opts.json === true) {
|
|
807
1149
|
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
808
1150
|
`);
|
|
809
1151
|
return;
|
|
810
1152
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
opts
|
|
814
|
-
);
|
|
1153
|
+
const state = running ? `running (pid ${rec?.pid}, port ${rec?.port})` : rec !== null ? `not running (stale record for pid ${rec.pid})` : "not running";
|
|
1154
|
+
log(`workspace ${name}: ${state}`, opts);
|
|
815
1155
|
for (const m of data.members) log(` member: ${m}`, opts);
|
|
816
1156
|
}
|
|
817
1157
|
async function workspaceLeave(opts) {
|
|
@@ -841,8 +1181,8 @@ async function workspaceStop(opts) {
|
|
|
841
1181
|
if (!isValidWorkspaceName(name)) {
|
|
842
1182
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
843
1183
|
}
|
|
844
|
-
const rec =
|
|
845
|
-
if (rec === null || !
|
|
1184
|
+
const rec = readWorkspaceDaemonRecord2(name);
|
|
1185
|
+
if (rec === null || !pidAlive2(rec.pid)) {
|
|
846
1186
|
if (opts.json === true) {
|
|
847
1187
|
process.stdout.write(
|
|
848
1188
|
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
@@ -853,6 +1193,10 @@ async function workspaceStop(opts) {
|
|
|
853
1193
|
info("workspace daemon is not running.", opts);
|
|
854
1194
|
return;
|
|
855
1195
|
}
|
|
1196
|
+
const health = await verifyWorkspaceDaemon(rec, name);
|
|
1197
|
+
if (health.kind !== "healthy") {
|
|
1198
|
+
fail(EXIT.ERROR, stopRefusal(name, rec, health), opts);
|
|
1199
|
+
}
|
|
856
1200
|
try {
|
|
857
1201
|
process.kill(rec.pid, "SIGTERM");
|
|
858
1202
|
} catch {
|
|
@@ -867,21 +1211,28 @@ async function workspaceStop(opts) {
|
|
|
867
1211
|
}
|
|
868
1212
|
info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
|
|
869
1213
|
}
|
|
1214
|
+
function stopRefusal(name, rec, health) {
|
|
1215
|
+
if (health.kind === "foreign") {
|
|
1216
|
+
return `refusing to stop workspace ${name}: the daemon answering on port ${rec.port} serves workspace ${health.workspace}, so the recorded pid ${rec.pid} is not this workspace's daemon.`;
|
|
1217
|
+
}
|
|
1218
|
+
return `refusing to stop workspace ${name}: the process recorded for it (pid ${rec.pid}) does not answer /health as this workspace, so it may be an unrelated process.`;
|
|
1219
|
+
}
|
|
870
1220
|
var init_workspace = __esm({
|
|
871
1221
|
"src/commands/workspace.ts"() {
|
|
872
1222
|
"use strict";
|
|
873
1223
|
init_output();
|
|
1224
|
+
init_workspace_bridge();
|
|
874
1225
|
init_workspace_mcp();
|
|
875
1226
|
}
|
|
876
1227
|
});
|
|
877
1228
|
|
|
878
1229
|
// src/commands/daemon.ts
|
|
879
|
-
import { loadProjectInfo as
|
|
1230
|
+
import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
|
|
880
1231
|
import {
|
|
881
1232
|
clearProjectDaemonRecord,
|
|
882
1233
|
ensureDaemonRunning,
|
|
883
|
-
pidAlive as
|
|
884
|
-
readDaemonToken,
|
|
1234
|
+
pidAlive as pidAlive3,
|
|
1235
|
+
readDaemonToken as readDaemonToken2,
|
|
885
1236
|
readProjectDaemonRecord,
|
|
886
1237
|
spawnDetachedDaemon
|
|
887
1238
|
} from "@noir-ai/daemon";
|
|
@@ -893,7 +1244,7 @@ function formatUptime(sec) {
|
|
|
893
1244
|
}
|
|
894
1245
|
function resolveCallerProjectId() {
|
|
895
1246
|
try {
|
|
896
|
-
return
|
|
1247
|
+
return loadProjectInfo3(process.cwd()).id;
|
|
897
1248
|
} catch {
|
|
898
1249
|
return void 0;
|
|
899
1250
|
}
|
|
@@ -925,7 +1276,7 @@ async function daemonStart(opts) {
|
|
|
925
1276
|
}
|
|
926
1277
|
let project;
|
|
927
1278
|
try {
|
|
928
|
-
project =
|
|
1279
|
+
project = loadProjectInfo3(process.cwd());
|
|
929
1280
|
} catch {
|
|
930
1281
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
931
1282
|
}
|
|
@@ -957,7 +1308,7 @@ async function daemonStart(opts) {
|
|
|
957
1308
|
}
|
|
958
1309
|
if (opts.detach === true) {
|
|
959
1310
|
const existing = readProjectDaemonRecord(project.id);
|
|
960
|
-
if (existing &&
|
|
1311
|
+
if (existing && pidAlive3(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
|
|
961
1312
|
if (opts.json === true) {
|
|
962
1313
|
process.stdout.write(
|
|
963
1314
|
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
@@ -1067,7 +1418,7 @@ async function daemonStop(opts) {
|
|
|
1067
1418
|
const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
|
|
1068
1419
|
let signalled = false;
|
|
1069
1420
|
let errMsg2;
|
|
1070
|
-
const healthy =
|
|
1421
|
+
const healthy = pidAlive3(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
|
|
1071
1422
|
if (healthy) {
|
|
1072
1423
|
try {
|
|
1073
1424
|
process.kill(rec.pid, "SIGTERM");
|
|
@@ -1109,7 +1460,7 @@ async function daemonStatus(opts) {
|
|
|
1109
1460
|
if (!rec) {
|
|
1110
1461
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
1111
1462
|
}
|
|
1112
|
-
if (!
|
|
1463
|
+
if (!pidAlive3(rec.pid)) {
|
|
1113
1464
|
clearProjectDaemonRecord(callerProject);
|
|
1114
1465
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
1115
1466
|
}
|
|
@@ -1171,14 +1522,14 @@ async function daemonToken(opts) {
|
|
|
1171
1522
|
opts
|
|
1172
1523
|
);
|
|
1173
1524
|
}
|
|
1174
|
-
if (!
|
|
1525
|
+
if (!pidAlive3(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
|
|
1175
1526
|
fail(
|
|
1176
1527
|
EXIT.DAEMON_DOWN,
|
|
1177
1528
|
"Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
|
|
1178
1529
|
opts
|
|
1179
1530
|
);
|
|
1180
1531
|
}
|
|
1181
|
-
const token =
|
|
1532
|
+
const token = readDaemonToken2(callerProject);
|
|
1182
1533
|
if (token === null) {
|
|
1183
1534
|
fail(
|
|
1184
1535
|
EXIT.DAEMON_DOWN,
|
|
@@ -1210,18 +1561,18 @@ var init_daemon = __esm({
|
|
|
1210
1561
|
});
|
|
1211
1562
|
|
|
1212
1563
|
// src/commands/env.ts
|
|
1213
|
-
import { loadNoirEnv, loadProjectInfo as
|
|
1564
|
+
import { loadNoirEnv, loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
|
|
1214
1565
|
import { resolveModelConfig } from "@noir-ai/model";
|
|
1215
1566
|
function captureAmbientEnv(env2 = process.env) {
|
|
1216
1567
|
ambientBaseline = { ...env2 };
|
|
1217
1568
|
}
|
|
1218
1569
|
function redactShape(value) {
|
|
1219
1570
|
if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
|
|
1220
|
-
return `${value
|
|
1571
|
+
return `${truncateToWidth(value, 3, "")}\u2026(${value.length})`;
|
|
1221
1572
|
}
|
|
1222
1573
|
function configApiKeyEnvNames(root) {
|
|
1223
1574
|
try {
|
|
1224
|
-
const project =
|
|
1575
|
+
const project = loadProjectInfo4(root);
|
|
1225
1576
|
const names = /* @__PURE__ */ new Set();
|
|
1226
1577
|
for (const provider of Object.values(resolveModelConfig(project.config.model).providers)) {
|
|
1227
1578
|
if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
|
|
@@ -1290,6 +1641,7 @@ var init_env = __esm({
|
|
|
1290
1641
|
"src/commands/env.ts"() {
|
|
1291
1642
|
"use strict";
|
|
1292
1643
|
init_output();
|
|
1644
|
+
init_width();
|
|
1293
1645
|
CURATED_AMBIENT_KEYS = [
|
|
1294
1646
|
"CLICKUP_API_TOKEN",
|
|
1295
1647
|
"OPENAI_API_KEY",
|
|
@@ -1314,13 +1666,13 @@ __export(install_exports, {
|
|
|
1314
1666
|
installManagedNode: () => installManagedNode
|
|
1315
1667
|
});
|
|
1316
1668
|
import { chmodSync, mkdirSync } from "fs";
|
|
1317
|
-
import { join
|
|
1669
|
+
import { join } from "path";
|
|
1318
1670
|
import {
|
|
1319
1671
|
atomicWriteFile as atomicWriteFile2,
|
|
1320
1672
|
detectActiveMethod,
|
|
1321
1673
|
detectInstallMethods,
|
|
1322
1674
|
ensureShimExecutable,
|
|
1323
|
-
loadProjectInfo as
|
|
1675
|
+
loadProjectInfo as loadProjectInfo5,
|
|
1324
1676
|
MANAGED_NODE_VERSION,
|
|
1325
1677
|
NOIR_VERSION,
|
|
1326
1678
|
noirHome,
|
|
@@ -1366,8 +1718,8 @@ function buildMigrationPlan(opts) {
|
|
|
1366
1718
|
async function installManagedNode(opts) {
|
|
1367
1719
|
const spec = opts.version ?? opts.channel ?? "latest";
|
|
1368
1720
|
const home2 = noirHome();
|
|
1369
|
-
const cliDir =
|
|
1370
|
-
const binDir =
|
|
1721
|
+
const cliDir = join(home2, "cli");
|
|
1722
|
+
const binDir = join(home2, "bin");
|
|
1371
1723
|
mkdirSync(cliDir, { recursive: true });
|
|
1372
1724
|
mkdirSync(binDir, { recursive: true });
|
|
1373
1725
|
let runtime;
|
|
@@ -1393,9 +1745,9 @@ async function installManagedNode(opts) {
|
|
|
1393
1745
|
if (code !== 0) {
|
|
1394
1746
|
return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
|
|
1395
1747
|
}
|
|
1396
|
-
const shim =
|
|
1748
|
+
const shim = join(binDir, "noir");
|
|
1397
1749
|
const shimBody = `#!/usr/bin/env bash
|
|
1398
|
-
"${nodeBin}" "${
|
|
1750
|
+
"${nodeBin}" "${join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
|
|
1399
1751
|
`;
|
|
1400
1752
|
atomicWriteFile2(shim, shimBody);
|
|
1401
1753
|
chmodSync(shim, 493);
|
|
@@ -1403,7 +1755,7 @@ async function installManagedNode(opts) {
|
|
|
1403
1755
|
const ver = await runManagerCmd(
|
|
1404
1756
|
[
|
|
1405
1757
|
nodeBin,
|
|
1406
|
-
|
|
1758
|
+
join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
|
|
1407
1759
|
"--version"
|
|
1408
1760
|
],
|
|
1409
1761
|
{ env: opts.env, timeoutMs: 3e4 }
|
|
@@ -1459,7 +1811,7 @@ async function install(opts = {}) {
|
|
|
1459
1811
|
const installedRecord = readInstallRecord();
|
|
1460
1812
|
let minVersion = "1.6.0";
|
|
1461
1813
|
try {
|
|
1462
|
-
minVersion =
|
|
1814
|
+
minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
|
|
1463
1815
|
} catch {
|
|
1464
1816
|
}
|
|
1465
1817
|
const plan = buildMigrationPlan({
|
|
@@ -1536,7 +1888,7 @@ import {
|
|
|
1536
1888
|
detectActiveMethod as detectActiveMethod2,
|
|
1537
1889
|
ensureShimExecutable as ensureShimExecutable2,
|
|
1538
1890
|
fetchLatestVersion,
|
|
1539
|
-
loadProjectInfo as
|
|
1891
|
+
loadProjectInfo as loadProjectInfo6,
|
|
1540
1892
|
readInstallRecord as readInstallRecord2,
|
|
1541
1893
|
readUpdateCache,
|
|
1542
1894
|
runManagerCmd as runManagerCmd2,
|
|
@@ -1616,7 +1968,7 @@ async function update(opts = {}) {
|
|
|
1616
1968
|
fetchSpin.succeed(`Latest: ${latest}`);
|
|
1617
1969
|
let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
|
|
1618
1970
|
try {
|
|
1619
|
-
minVersion =
|
|
1971
|
+
minVersion = loadProjectInfo6(process.cwd()).config.update.minVersion;
|
|
1620
1972
|
} catch {
|
|
1621
1973
|
}
|
|
1622
1974
|
const target = buildUpdateTarget({
|
|
@@ -1766,7 +2118,7 @@ var init_update = __esm({
|
|
|
1766
2118
|
});
|
|
1767
2119
|
|
|
1768
2120
|
// src/commands/status.ts
|
|
1769
|
-
import { loadProjectInfo as
|
|
2121
|
+
import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
1770
2122
|
async function tryTool(caller, name, args) {
|
|
1771
2123
|
try {
|
|
1772
2124
|
return await caller.callTool(name, args);
|
|
@@ -1887,7 +2239,7 @@ function renderHuman(p, opts) {
|
|
|
1887
2239
|
async function gatherStatusPayload(opts) {
|
|
1888
2240
|
let project;
|
|
1889
2241
|
try {
|
|
1890
|
-
project =
|
|
2242
|
+
project = loadProjectInfo7(process.cwd());
|
|
1891
2243
|
} catch {
|
|
1892
2244
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
1893
2245
|
}
|
|
@@ -1917,7 +2269,7 @@ async function status(opts) {
|
|
|
1917
2269
|
env: process.env,
|
|
1918
2270
|
configUpdate: (() => {
|
|
1919
2271
|
try {
|
|
1920
|
-
const info2 =
|
|
2272
|
+
const info2 = loadProjectInfo7(process.cwd());
|
|
1921
2273
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
1922
2274
|
} catch {
|
|
1923
2275
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -1946,7 +2298,7 @@ var init_status = __esm({
|
|
|
1946
2298
|
// src/commands/task.ts
|
|
1947
2299
|
import { spawnSync } from "child_process";
|
|
1948
2300
|
import { writeFileSync } from "fs";
|
|
1949
|
-
import { loadProjectInfo as
|
|
2301
|
+
import { loadProjectInfo as loadProjectInfo8, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
|
|
1950
2302
|
import {
|
|
1951
2303
|
PHASES,
|
|
1952
2304
|
TASK_CLASSES,
|
|
@@ -1959,7 +2311,7 @@ function skillFor(phase) {
|
|
|
1959
2311
|
}
|
|
1960
2312
|
function writeDoneArtifacts(opts, taskId) {
|
|
1961
2313
|
try {
|
|
1962
|
-
const project =
|
|
2314
|
+
const project = loadProjectInfo8(process.cwd());
|
|
1963
2315
|
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
1964
2316
|
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
1965
2317
|
const nextN = nextArtifactSequence(project.root, "adr");
|
|
@@ -2282,7 +2634,7 @@ function runCheck(cmd) {
|
|
|
2282
2634
|
async function taskVerify(opts) {
|
|
2283
2635
|
let project;
|
|
2284
2636
|
try {
|
|
2285
|
-
project =
|
|
2637
|
+
project = loadProjectInfo8(process.cwd());
|
|
2286
2638
|
} catch {
|
|
2287
2639
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2288
2640
|
}
|
|
@@ -2388,7 +2740,7 @@ async function taskResearch(opts) {
|
|
|
2388
2740
|
return;
|
|
2389
2741
|
}
|
|
2390
2742
|
renderStatusRow(s, opts);
|
|
2391
|
-
info("
|
|
2743
|
+
info("this is a status view; record findings with `noir task research-record`", opts);
|
|
2392
2744
|
}
|
|
2393
2745
|
async function taskResearchRecord(opts) {
|
|
2394
2746
|
const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
|
|
@@ -2412,10 +2764,7 @@ async function taskResearchRecord(opts) {
|
|
|
2412
2764
|
`);
|
|
2413
2765
|
return;
|
|
2414
2766
|
}
|
|
2415
|
-
success(
|
|
2416
|
-
`research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
|
|
2417
|
-
opts
|
|
2418
|
-
);
|
|
2767
|
+
success(`research recorded \u2192 ${opts.type}: ${truncateToWidth(opts.text, 80)}`, opts);
|
|
2419
2768
|
}
|
|
2420
2769
|
async function taskDecompose(opts) {
|
|
2421
2770
|
const cap = opts.capability.trim();
|
|
@@ -2471,6 +2820,7 @@ var init_task = __esm({
|
|
|
2471
2820
|
init_daemon_client();
|
|
2472
2821
|
init_output();
|
|
2473
2822
|
init_theme();
|
|
2823
|
+
init_width();
|
|
2474
2824
|
PHASE_SKILL = {
|
|
2475
2825
|
// Skill curation: intake+clarify merged into brainstorming;
|
|
2476
2826
|
// execute → executing-plans; verify → verifying; document → wrap.
|
|
@@ -2488,13 +2838,13 @@ var init_task = __esm({
|
|
|
2488
2838
|
|
|
2489
2839
|
// src/commands/handoff.ts
|
|
2490
2840
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2491
|
-
import { join as
|
|
2841
|
+
import { join as join2 } from "path";
|
|
2492
2842
|
import {
|
|
2493
2843
|
defaultHandoffBlock,
|
|
2494
2844
|
hostLaunchDirective,
|
|
2495
|
-
resolveAdapter
|
|
2845
|
+
resolveAdapter
|
|
2496
2846
|
} from "@noir-ai/adapters";
|
|
2497
|
-
import { loadProjectInfo as
|
|
2847
|
+
import { loadProjectInfo as loadProjectInfo9, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
|
|
2498
2848
|
function heading(name, id) {
|
|
2499
2849
|
return `# Noir handoff \u2014 ${name} (${id})`;
|
|
2500
2850
|
}
|
|
@@ -2590,7 +2940,7 @@ function toHostPayload(p) {
|
|
|
2590
2940
|
}
|
|
2591
2941
|
function resolveDirective(root, p) {
|
|
2592
2942
|
const ctx = { root };
|
|
2593
|
-
const adapter =
|
|
2943
|
+
const adapter = resolveAdapter(p.host);
|
|
2594
2944
|
const hostPayload = toHostPayload(p);
|
|
2595
2945
|
if (typeof adapter.emitHandoff === "function") {
|
|
2596
2946
|
return adapter.emitHandoff(ctx, hostPayload);
|
|
@@ -2633,7 +2983,7 @@ async function probeOnly(opts) {
|
|
|
2633
2983
|
async function handoff(opts) {
|
|
2634
2984
|
let project;
|
|
2635
2985
|
try {
|
|
2636
|
-
project =
|
|
2986
|
+
project = loadProjectInfo9(process.cwd());
|
|
2637
2987
|
} catch {
|
|
2638
2988
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2639
2989
|
}
|
|
@@ -2665,7 +3015,7 @@ async function handoff(opts) {
|
|
|
2665
3015
|
if (opts.write === true) {
|
|
2666
3016
|
const md = renderMarkdown(full);
|
|
2667
3017
|
const path = handoffFilePath(project.root, task?.taskId, project.id);
|
|
2668
|
-
mkdirSync2(
|
|
3018
|
+
mkdirSync2(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
|
|
2669
3019
|
writeFileSync2(path, md, "utf8");
|
|
2670
3020
|
info(`handoff written to ${path}`, opts);
|
|
2671
3021
|
}
|
|
@@ -2693,7 +3043,7 @@ import gradient from "gradient-string";
|
|
|
2693
3043
|
function renderBanner(opts = {}) {
|
|
2694
3044
|
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
2695
3045
|
const colorOn = opts.color !== false && useColor();
|
|
2696
|
-
if (width <
|
|
3046
|
+
if (width < BLOCK_MIN_WIDTH) {
|
|
2697
3047
|
return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
|
|
2698
3048
|
}
|
|
2699
3049
|
const block = NOIR_BLOCK.join("\n");
|
|
@@ -2709,11 +3059,12 @@ function shouldShowBanner(opts) {
|
|
|
2709
3059
|
if (v !== void 0 && v !== "") return false;
|
|
2710
3060
|
return true;
|
|
2711
3061
|
}
|
|
2712
|
-
var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE;
|
|
3062
|
+
var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE, BLOCK_WIDTH, BLOCK_MIN_WIDTH;
|
|
2713
3063
|
var init_banner = __esm({
|
|
2714
3064
|
"src/banner.ts"() {
|
|
2715
3065
|
"use strict";
|
|
2716
3066
|
init_theme();
|
|
3067
|
+
init_width();
|
|
2717
3068
|
NOIR_BLOCK = [
|
|
2718
3069
|
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
2719
3070
|
"\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
@@ -2724,6 +3075,8 @@ var init_banner = __esm({
|
|
|
2724
3075
|
];
|
|
2725
3076
|
NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
2726
3077
|
NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
3078
|
+
BLOCK_WIDTH = NOIR_BLOCK.reduce((max, line) => Math.max(max, displayWidth(line)), 0);
|
|
3079
|
+
BLOCK_MIN_WIDTH = BLOCK_WIDTH + 17;
|
|
2727
3080
|
}
|
|
2728
3081
|
});
|
|
2729
3082
|
|
|
@@ -3023,14 +3376,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
|
3023
3376
|
import {
|
|
3024
3377
|
detectActiveMethod as detectActiveMethod3,
|
|
3025
3378
|
latestVersionFromCache,
|
|
3026
|
-
loadProjectInfo as
|
|
3379
|
+
loadProjectInfo as loadProjectInfo10,
|
|
3027
3380
|
NOIR_VERSION as NOIR_VERSION3,
|
|
3028
3381
|
readInstallRecord as readInstallRecord3,
|
|
3029
3382
|
readUpdateCache as readUpdateCache2
|
|
3030
3383
|
} from "@noir-ai/core";
|
|
3031
3384
|
function tryProject() {
|
|
3032
3385
|
try {
|
|
3033
|
-
const info2 =
|
|
3386
|
+
const info2 = loadProjectInfo10(process.cwd());
|
|
3034
3387
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
3035
3388
|
} catch {
|
|
3036
3389
|
return null;
|
|
@@ -3193,7 +3546,7 @@ async function runMenu(opts, deps) {
|
|
|
3193
3546
|
env: process.env,
|
|
3194
3547
|
configUpdate: (() => {
|
|
3195
3548
|
try {
|
|
3196
|
-
const info2 =
|
|
3549
|
+
const info2 = loadProjectInfo10(process.cwd());
|
|
3197
3550
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
3198
3551
|
} catch {
|
|
3199
3552
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -3256,12 +3609,12 @@ var init_home = __esm({
|
|
|
3256
3609
|
|
|
3257
3610
|
// src/commands/memory.ts
|
|
3258
3611
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3259
|
-
import { loadProjectInfo as
|
|
3612
|
+
import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
|
|
3260
3613
|
function resolveWorkspace() {
|
|
3261
3614
|
const routing = workspaceRoutingTarget(process.cwd());
|
|
3262
3615
|
if (routing === null) return null;
|
|
3263
3616
|
try {
|
|
3264
|
-
return { routing, project:
|
|
3617
|
+
return { routing, project: loadProjectInfo11(process.cwd()) };
|
|
3265
3618
|
} catch {
|
|
3266
3619
|
return null;
|
|
3267
3620
|
}
|
|
@@ -3674,7 +4027,7 @@ function describe(step, who) {
|
|
|
3674
4027
|
const m = {
|
|
3675
4028
|
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
3676
4029
|
bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
|
|
3677
|
-
gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
|
|
4030
|
+
gate: "re-run the full six-step gate (lint/build/typecheck/test/docs:validate/hygiene:gate)",
|
|
3678
4031
|
commit: "commit chore(release) + push develop",
|
|
3679
4032
|
"ci-develop": "wait for CI on develop \u2192 green",
|
|
3680
4033
|
"beta-tag": "pnpm release:tag \u2192 beta tag + push",
|
|
@@ -3719,11 +4072,14 @@ async function release(opts) {
|
|
|
3719
4072
|
});
|
|
3720
4073
|
success(`bump: \u2713 version ${version}`, opts);
|
|
3721
4074
|
info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
|
|
3722
|
-
execSync(
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4075
|
+
execSync(
|
|
4076
|
+
"pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate && pnpm hygiene:gate",
|
|
4077
|
+
{
|
|
4078
|
+
encoding: "utf8",
|
|
4079
|
+
stdio: "inherit"
|
|
4080
|
+
}
|
|
4081
|
+
);
|
|
4082
|
+
success("gate: \u2713 all six gates green", opts);
|
|
3727
4083
|
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
3728
4084
|
spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
3729
4085
|
spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
@@ -4583,10 +4939,10 @@ var init_run_interrupt = __esm({
|
|
|
4583
4939
|
});
|
|
4584
4940
|
|
|
4585
4941
|
// src/run-profiles.ts
|
|
4586
|
-
import { isDeniedEnvKey, loadProjectInfo as
|
|
4942
|
+
import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
|
|
4587
4943
|
function loadRunConfig(root) {
|
|
4588
4944
|
try {
|
|
4589
|
-
return
|
|
4945
|
+
return loadProjectInfo12(root).config;
|
|
4590
4946
|
} catch {
|
|
4591
4947
|
return null;
|
|
4592
4948
|
}
|
|
@@ -4670,6 +5026,8 @@ var init_run_status = __esm({
|
|
|
4670
5026
|
"src/run-status.ts"() {
|
|
4671
5027
|
"use strict";
|
|
4672
5028
|
init_orchestrator();
|
|
5029
|
+
init_theme();
|
|
5030
|
+
init_width();
|
|
4673
5031
|
HOST_STDERR_TAIL_LINES = 20;
|
|
4674
5032
|
RunStatusLine = class {
|
|
4675
5033
|
host;
|
|
@@ -4752,6 +5110,7 @@ var init_run_status = __esm({
|
|
|
4752
5110
|
if (this.animated && this.sharesCursor && this.onRow) {
|
|
4753
5111
|
this.write("\n");
|
|
4754
5112
|
this.onRow = false;
|
|
5113
|
+
this.rendered = "";
|
|
4755
5114
|
}
|
|
4756
5115
|
this.stdoutAtRowStart = chunk.endsWith("\n");
|
|
4757
5116
|
}
|
|
@@ -4782,19 +5141,45 @@ var init_run_status = __esm({
|
|
|
4782
5141
|
return;
|
|
4783
5142
|
}
|
|
4784
5143
|
if (!this.onRow || !this.cursorFree()) return;
|
|
4785
|
-
this.write(
|
|
5144
|
+
this.write(this.erase(this.rendered));
|
|
4786
5145
|
this.rendered = "";
|
|
4787
5146
|
this.onRow = false;
|
|
4788
5147
|
}
|
|
4789
5148
|
/** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
|
|
4790
5149
|
draw() {
|
|
4791
5150
|
if (!this.animated || !this.cursorFree()) return;
|
|
4792
|
-
const text = this.render();
|
|
5151
|
+
const text = truncateToWidth(this.render(), terminalWidth());
|
|
4793
5152
|
if (text === this.rendered) return;
|
|
4794
|
-
this.write(
|
|
5153
|
+
this.write(`${this.erase(this.rendered)}${text}`);
|
|
4795
5154
|
this.rendered = text;
|
|
4796
5155
|
this.onRow = true;
|
|
4797
5156
|
}
|
|
5157
|
+
/**
|
|
5158
|
+
* The escape sequence that clears the status text currently on screen and
|
|
5159
|
+
* leaves the cursor at the start of the row it began on — where the next
|
|
5160
|
+
* render writes. An empty `previous` still clears the row, so a line that has
|
|
5161
|
+
* drawn nothing yet starts from a known-blank one.
|
|
5162
|
+
*
|
|
5163
|
+
* The extent is measured against the terminal width as it is NOW, not as it
|
|
5164
|
+
* was when the text was drawn. Nothing this line writes wraps, but text that
|
|
5165
|
+
* fitted a wider terminal does not stop existing when the window narrows: the
|
|
5166
|
+
* terminal folds it onto rows below, and clearing only the row the cursor
|
|
5167
|
+
* ends on would leave the rest above as residue. So the sequence walks back
|
|
5168
|
+
* up over every row the text occupies. That folding is the behaviour of the
|
|
5169
|
+
* common terminals (iTerm2, Terminal.app, GNOME Terminal and recent xterm);
|
|
5170
|
+
* one that clips the row instead has no continuation to clear.
|
|
5171
|
+
*
|
|
5172
|
+
* The caller must have forgotten the text once its row was abandoned —
|
|
5173
|
+
* `rendered` is only meaningful while `onRow` is true.
|
|
5174
|
+
*/
|
|
5175
|
+
erase(previous) {
|
|
5176
|
+
const rows = Math.max(1, Math.ceil(displayWidth(previous) / terminalWidth()));
|
|
5177
|
+
let sequence = "\r\x1B[K";
|
|
5178
|
+
for (let row = 1; row < rows; row++) {
|
|
5179
|
+
sequence += "\x1B[1A\r\x1B[K";
|
|
5180
|
+
}
|
|
5181
|
+
return sequence;
|
|
5182
|
+
}
|
|
4798
5183
|
/**
|
|
4799
5184
|
* Whether the cursor is at the start of a row nobody else is using. On stdout
|
|
4800
5185
|
* that is not this terminal there is no shared cursor to disturb; when it is,
|
|
@@ -4832,7 +5217,7 @@ var init_run_status = __esm({
|
|
|
4832
5217
|
|
|
4833
5218
|
// src/commands/run.ts
|
|
4834
5219
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4835
|
-
import { join as
|
|
5220
|
+
import { join as join3 } from "path";
|
|
4836
5221
|
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
4837
5222
|
import { isDeniedEnvKey as isDeniedEnvKey2, loadNoirEnv as loadNoirEnv2, NOIR_DIR as NOIR_DIR2, parseConfig, paths } from "@noir-ai/core";
|
|
4838
5223
|
function credentialNote(env2, sources) {
|
|
@@ -4892,7 +5277,7 @@ async function runOnce(prompt, opts, resumeSessionId) {
|
|
|
4892
5277
|
const customBinary = opts.command ?? profile.binary;
|
|
4893
5278
|
const extraArgs = resumeSessionId === void 0 ? profile.args : [...profile.args ?? [], "--resume", resumeSessionId];
|
|
4894
5279
|
const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
|
|
4895
|
-
if (!existsSync2(
|
|
5280
|
+
if (!existsSync2(join3(root, NOIR_DIR2, ".env"))) {
|
|
4896
5281
|
log(
|
|
4897
5282
|
`This project has no ${NOIR_DIR2}/.env \u2014 that is where Noir reads project credentials and run config from. Run \`noir init\` to scaffold one (\`noir run\` works without it).`,
|
|
4898
5283
|
opts
|
|
@@ -5055,7 +5440,7 @@ function safeTranscript(host, lines) {
|
|
|
5055
5440
|
function writeTranscript(host, lines, root = process.cwd()) {
|
|
5056
5441
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5057
5442
|
const dir = paths.transcriptsDir(root);
|
|
5058
|
-
const file =
|
|
5443
|
+
const file = join3(dir, `${host}-${ts}.jsonl`);
|
|
5059
5444
|
try {
|
|
5060
5445
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
5061
5446
|
writeFileSync4(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
|
|
@@ -5192,7 +5577,7 @@ var init_conflict = __esm({
|
|
|
5192
5577
|
|
|
5193
5578
|
// src/dedup-write.ts
|
|
5194
5579
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
5195
|
-
import { join as
|
|
5580
|
+
import { join as join4 } from "path";
|
|
5196
5581
|
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
5197
5582
|
async function checkWritePathDedup(root, res, opts) {
|
|
5198
5583
|
const result = { found: [], conflicts: [] };
|
|
@@ -5203,7 +5588,7 @@ async function checkWritePathDedup(root, res, opts) {
|
|
|
5203
5588
|
if (embed === void 0) return result;
|
|
5204
5589
|
const cachedEmbed = makeCachedEmbed(cache, embed);
|
|
5205
5590
|
for (const proposedRel of writtenHostFiles) {
|
|
5206
|
-
const proposedAbs =
|
|
5591
|
+
const proposedAbs = join4(root, proposedRel);
|
|
5207
5592
|
let proposedText;
|
|
5208
5593
|
try {
|
|
5209
5594
|
proposedText = readFileSync3(proposedAbs, "utf8");
|
|
@@ -5250,7 +5635,7 @@ async function checkWritePathDedup(root, res, opts) {
|
|
|
5250
5635
|
`
|
|
5251
5636
|
);
|
|
5252
5637
|
}
|
|
5253
|
-
const matchedText = safeRead(
|
|
5638
|
+
const matchedText = safeRead(join4(root, matchedRel));
|
|
5254
5639
|
result.conflicts.push({
|
|
5255
5640
|
path: proposedRel,
|
|
5256
5641
|
mode: "artifact",
|
|
@@ -5334,21 +5719,21 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
|
|
|
5334
5719
|
if (clack.isCancel(choice)) return "preserve";
|
|
5335
5720
|
if (choice === "skip") {
|
|
5336
5721
|
try {
|
|
5337
|
-
rmSync(
|
|
5722
|
+
rmSync(join4(root, proposedRel), { force: true });
|
|
5338
5723
|
} catch {
|
|
5339
5724
|
}
|
|
5340
5725
|
return "preserve";
|
|
5341
5726
|
} else if (choice === "replace") {
|
|
5342
5727
|
try {
|
|
5343
|
-
writeFileSync5(
|
|
5728
|
+
writeFileSync5(join4(root, matchedRel), proposedText, "utf8");
|
|
5344
5729
|
} catch {
|
|
5345
5730
|
}
|
|
5346
5731
|
return "replace";
|
|
5347
5732
|
} else if (choice === "mirror") {
|
|
5348
5733
|
try {
|
|
5349
|
-
const existing = readFileSync3(
|
|
5734
|
+
const existing = readFileSync3(join4(root, matchedRel), "utf8");
|
|
5350
5735
|
writeFileSync5(
|
|
5351
|
-
|
|
5736
|
+
join4(root, matchedRel),
|
|
5352
5737
|
`${existing}
|
|
5353
5738
|
|
|
5354
5739
|
<!-- mirrored from ${proposedRel} -->
|
|
@@ -5366,7 +5751,7 @@ function emptyCache() {
|
|
|
5366
5751
|
return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
|
|
5367
5752
|
}
|
|
5368
5753
|
function cachePath(root) {
|
|
5369
|
-
return
|
|
5754
|
+
return join4(root, ".noir", "dedup-cache.json");
|
|
5370
5755
|
}
|
|
5371
5756
|
function loadCache(root) {
|
|
5372
5757
|
try {
|
|
@@ -5385,7 +5770,7 @@ function loadCache(root) {
|
|
|
5385
5770
|
}
|
|
5386
5771
|
function saveCache(root, cache) {
|
|
5387
5772
|
try {
|
|
5388
|
-
mkdirSync4(
|
|
5773
|
+
mkdirSync4(join4(root, ".noir"), { recursive: true });
|
|
5389
5774
|
writeFileSync5(cachePath(root), JSON.stringify(cache), "utf8");
|
|
5390
5775
|
} catch {
|
|
5391
5776
|
}
|
|
@@ -5441,8 +5826,8 @@ var init_dedup_write = __esm({
|
|
|
5441
5826
|
|
|
5442
5827
|
// src/init.ts
|
|
5443
5828
|
import { existsSync as existsSync3 } from "fs";
|
|
5444
|
-
import { resolveAdapter as
|
|
5445
|
-
import { loadProjectInfo as
|
|
5829
|
+
import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
|
|
5830
|
+
import { loadProjectInfo as loadProjectInfo13, paths as paths2 } from "@noir-ai/core";
|
|
5446
5831
|
import { scaffold } from "@noir-ai/create";
|
|
5447
5832
|
import {
|
|
5448
5833
|
emitSkillsToDir
|
|
@@ -5469,13 +5854,14 @@ async function init(root, opts) {
|
|
|
5469
5854
|
reportPlannedWrites(res);
|
|
5470
5855
|
return res;
|
|
5471
5856
|
}
|
|
5857
|
+
reportEnvHeal(res);
|
|
5472
5858
|
if (res.noop) return res;
|
|
5473
5859
|
const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
|
|
5474
5860
|
res.skillConflicts = skillSummary?.conflicts ?? [];
|
|
5475
5861
|
res.preservedSkills = skillSummary?.preserved ?? [];
|
|
5476
5862
|
let projectInfo;
|
|
5477
5863
|
try {
|
|
5478
|
-
projectInfo =
|
|
5864
|
+
projectInfo = loadProjectInfo13(root);
|
|
5479
5865
|
} catch {
|
|
5480
5866
|
projectInfo = void 0;
|
|
5481
5867
|
}
|
|
@@ -5506,7 +5892,7 @@ function resolveInitHost(root, opts) {
|
|
|
5506
5892
|
if (opts.host !== void 0) return opts.host;
|
|
5507
5893
|
if (!existsSync3(paths2.config(root))) return "claude";
|
|
5508
5894
|
try {
|
|
5509
|
-
return
|
|
5895
|
+
return loadProjectInfo13(root).config.host;
|
|
5510
5896
|
} catch {
|
|
5511
5897
|
process.stderr.write(
|
|
5512
5898
|
`Could not read the configured host from ${paths2.config(root)}; using 'claude'.
|
|
@@ -5537,8 +5923,13 @@ function reportPlannedWrites(res) {
|
|
|
5537
5923
|
for (const p of res.identical) log(` ${p}`);
|
|
5538
5924
|
}
|
|
5539
5925
|
}
|
|
5926
|
+
function reportEnvHeal(res) {
|
|
5927
|
+
if (res.envMode === "healed") {
|
|
5928
|
+
process.stderr.write("Tightened .noir/.env to 0600 (it was readable by group or others).\n");
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5540
5931
|
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
5541
|
-
const adapter =
|
|
5932
|
+
const adapter = resolveAdapter2(host);
|
|
5542
5933
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
5543
5934
|
if (skillsDir === void 0) {
|
|
5544
5935
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -5602,8 +5993,8 @@ var init_init = __esm({
|
|
|
5602
5993
|
});
|
|
5603
5994
|
|
|
5604
5995
|
// src/commands/skills.ts
|
|
5605
|
-
import { resolveAdapter as
|
|
5606
|
-
import { loadProjectInfo as
|
|
5996
|
+
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
5997
|
+
import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
|
|
5607
5998
|
import {
|
|
5608
5999
|
buildRegistry,
|
|
5609
6000
|
discoverAll,
|
|
@@ -5621,13 +6012,6 @@ function toRow(s, kind = "builtin") {
|
|
|
5621
6012
|
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
5622
6013
|
return { name: s.name, category, description, kind, status: status2 };
|
|
5623
6014
|
}
|
|
5624
|
-
function truncate(text, max) {
|
|
5625
|
-
if (text.length <= max) return text;
|
|
5626
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
5627
|
-
const boundary = slice.lastIndexOf(" ");
|
|
5628
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
5629
|
-
return `${head.trimEnd()}\u2026`;
|
|
5630
|
-
}
|
|
5631
6015
|
async function skillsList(opts) {
|
|
5632
6016
|
let builtins;
|
|
5633
6017
|
let integrations;
|
|
@@ -5661,7 +6045,9 @@ async function skillsList(opts) {
|
|
|
5661
6045
|
Kind: r.kind,
|
|
5662
6046
|
Category: r.category,
|
|
5663
6047
|
Status: r.status,
|
|
5664
|
-
|
|
6048
|
+
// Display-only cut for the dense table cell; the `--json` payload always
|
|
6049
|
+
// carries the description whole (search snippets must never be truncated).
|
|
6050
|
+
Description: truncateToWidth(r.description, 80)
|
|
5665
6051
|
})),
|
|
5666
6052
|
["Skill", "Kind", "Category", "Status", "Description"],
|
|
5667
6053
|
opts
|
|
@@ -5671,11 +6057,11 @@ async function skillsSync(opts) {
|
|
|
5671
6057
|
const root = process.cwd();
|
|
5672
6058
|
let project;
|
|
5673
6059
|
try {
|
|
5674
|
-
project =
|
|
6060
|
+
project = loadProjectInfo14(root);
|
|
5675
6061
|
} catch {
|
|
5676
6062
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
5677
6063
|
}
|
|
5678
|
-
const adapter =
|
|
6064
|
+
const adapter = resolveAdapter3(project.config.host);
|
|
5679
6065
|
if (!adapter.skillsDir) {
|
|
5680
6066
|
const data2 = {
|
|
5681
6067
|
emitted: [],
|
|
@@ -5737,7 +6123,7 @@ async function skillsLint(opts) {
|
|
|
5737
6123
|
);
|
|
5738
6124
|
}
|
|
5739
6125
|
const errored = skills.filter((s) => s.errors.length > 0);
|
|
5740
|
-
const warned = skills.filter((s) => s.warnings.length > 0
|
|
6126
|
+
const warned = skills.filter((s) => s.warnings.length > 0);
|
|
5741
6127
|
const data = { count: skills.length, errored: errored.length, skills };
|
|
5742
6128
|
if (errored.length > 0) {
|
|
5743
6129
|
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
@@ -5815,6 +6201,7 @@ var init_skills = __esm({
|
|
|
5815
6201
|
"use strict";
|
|
5816
6202
|
init_init();
|
|
5817
6203
|
init_output();
|
|
6204
|
+
init_width();
|
|
5818
6205
|
CATEGORY = {
|
|
5819
6206
|
"noir-brainstorming": "discovery",
|
|
5820
6207
|
"noir-sync": "discovery",
|
|
@@ -5841,17 +6228,22 @@ var init_skills = __esm({
|
|
|
5841
6228
|
"noir-backend": "domain",
|
|
5842
6229
|
"noir-doctor": "meta",
|
|
5843
6230
|
"noir-writing-skills": "meta",
|
|
5844
|
-
"noir-rules": "meta"
|
|
6231
|
+
"noir-rules": "meta",
|
|
6232
|
+
"noir-code-hygiene": "meta"
|
|
5845
6233
|
};
|
|
5846
6234
|
}
|
|
5847
6235
|
});
|
|
5848
6236
|
|
|
5849
6237
|
// src/serve.ts
|
|
5850
|
-
import { applyNoirEnv, loadProjectInfo as
|
|
6238
|
+
import { applyNoirEnv, loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
|
|
5851
6239
|
import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
|
|
5852
6240
|
async function serve(opts) {
|
|
5853
6241
|
applyNoirEnv(process.cwd());
|
|
5854
|
-
|
|
6242
|
+
if (opts.workspace !== void 0) {
|
|
6243
|
+
await bridgeStdioToWorkspace(opts.workspace, process.cwd());
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6246
|
+
const project = loadProjectInfo15(process.cwd());
|
|
5855
6247
|
if (opts.stdio) {
|
|
5856
6248
|
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
5857
6249
|
return;
|
|
@@ -5877,6 +6269,7 @@ async function serve(opts) {
|
|
|
5877
6269
|
var init_serve = __esm({
|
|
5878
6270
|
"src/serve.ts"() {
|
|
5879
6271
|
"use strict";
|
|
6272
|
+
init_workspace_bridge();
|
|
5880
6273
|
}
|
|
5881
6274
|
});
|
|
5882
6275
|
|
|
@@ -5886,8 +6279,8 @@ __export(create_exports, {
|
|
|
5886
6279
|
create: () => create
|
|
5887
6280
|
});
|
|
5888
6281
|
import { resolve as resolve2 } from "path";
|
|
5889
|
-
import { resolveAdapter as
|
|
5890
|
-
import { loadProjectInfo as
|
|
6282
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
6283
|
+
import { loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
|
|
5891
6284
|
import { scaffold as scaffold2 } from "@noir-ai/create";
|
|
5892
6285
|
import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
|
|
5893
6286
|
async function create(dir, opts) {
|
|
@@ -5911,9 +6304,10 @@ async function create(dir, opts) {
|
|
|
5911
6304
|
reportPlannedWrites(res);
|
|
5912
6305
|
return res;
|
|
5913
6306
|
}
|
|
6307
|
+
reportEnvHeal(res);
|
|
5914
6308
|
if (res.noop) return res;
|
|
5915
6309
|
const host = res.host;
|
|
5916
|
-
const adapter =
|
|
6310
|
+
const adapter = resolveAdapter4(host);
|
|
5917
6311
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
5918
6312
|
if (skillsDir === void 0) {
|
|
5919
6313
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -5941,7 +6335,7 @@ async function create(dir, opts) {
|
|
|
5941
6335
|
}
|
|
5942
6336
|
let projectInfo;
|
|
5943
6337
|
try {
|
|
5944
|
-
projectInfo =
|
|
6338
|
+
projectInfo = loadProjectInfo16(root);
|
|
5945
6339
|
} catch {
|
|
5946
6340
|
projectInfo = void 0;
|
|
5947
6341
|
}
|
|
@@ -5968,8 +6362,8 @@ __export(sync_exports, {
|
|
|
5968
6362
|
sync: () => sync
|
|
5969
6363
|
});
|
|
5970
6364
|
import { existsSync as existsSync4 } from "fs";
|
|
5971
|
-
import { resolveAdapter as
|
|
5972
|
-
import { loadProjectInfo as
|
|
6365
|
+
import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
|
|
6366
|
+
import { loadProjectInfo as loadProjectInfo17, paths as paths3 } from "@noir-ai/core";
|
|
5973
6367
|
import { scaffold as scaffold3 } from "@noir-ai/create";
|
|
5974
6368
|
import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
|
|
5975
6369
|
async function sync(root, opts = {}) {
|
|
@@ -5978,7 +6372,7 @@ async function sync(root, opts = {}) {
|
|
|
5978
6372
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5979
6373
|
let projectInfo;
|
|
5980
6374
|
try {
|
|
5981
|
-
projectInfo =
|
|
6375
|
+
projectInfo = loadProjectInfo17(root);
|
|
5982
6376
|
} catch {
|
|
5983
6377
|
projectInfo = void 0;
|
|
5984
6378
|
}
|
|
@@ -5997,10 +6391,11 @@ async function sync(root, opts = {}) {
|
|
|
5997
6391
|
reportPlannedWrites(res);
|
|
5998
6392
|
return res;
|
|
5999
6393
|
}
|
|
6394
|
+
reportEnvHeal(res);
|
|
6000
6395
|
if (res.written.length === 0 && res.identical.length > 0) {
|
|
6001
6396
|
process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
|
|
6002
6397
|
}
|
|
6003
|
-
const adapter =
|
|
6398
|
+
const adapter = resolveAdapter5(host);
|
|
6004
6399
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
6005
6400
|
if (skillsDir === void 0) {
|
|
6006
6401
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -6041,7 +6436,7 @@ function resolveSyncHost(root, opts) {
|
|
|
6041
6436
|
if (opts.host !== void 0) return opts.host;
|
|
6042
6437
|
if (!existsSync4(paths3.config(root))) return "claude";
|
|
6043
6438
|
try {
|
|
6044
|
-
return
|
|
6439
|
+
return loadProjectInfo17(root).config.host;
|
|
6045
6440
|
} catch {
|
|
6046
6441
|
return "claude";
|
|
6047
6442
|
}
|
|
@@ -6163,6 +6558,8 @@ function createProgram() {
|
|
|
6163
6558
|
const nonInteractive = opts.json === true || opts.input === false;
|
|
6164
6559
|
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
6165
6560
|
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
6561
|
+
if (opts.quiet === true) process.env.NOIR_QUIET = "1";
|
|
6562
|
+
else delete process.env.NOIR_QUIET;
|
|
6166
6563
|
emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
|
|
6167
6564
|
});
|
|
6168
6565
|
program2.command("init").description("scaffold Noir in the current project (.noir/, .mcp.json, CLAUDE.md, skills)").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option(
|
|
@@ -6268,11 +6665,17 @@ function createProgram() {
|
|
|
6268
6665
|
}
|
|
6269
6666
|
);
|
|
6270
6667
|
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
6271
|
-
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").
|
|
6272
|
-
|
|
6668
|
+
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").option(
|
|
6669
|
+
"--workspace <name>",
|
|
6670
|
+
"serve a shared workspace over stdio, bridging to that workspace daemon"
|
|
6671
|
+
).action(async (opts) => {
|
|
6672
|
+
await serve({
|
|
6673
|
+
stdio: opts.stdio === true,
|
|
6674
|
+
...typeof opts.workspace === "string" ? { workspace: opts.workspace } : {}
|
|
6675
|
+
});
|
|
6273
6676
|
});
|
|
6274
6677
|
mcpCmd.action(() => {
|
|
6275
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
6678
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio] [--workspace <name>]");
|
|
6276
6679
|
});
|
|
6277
6680
|
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
6278
6681
|
daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").option("--workspace <name>", "start a shared cross-repo workspace daemon instead").option("--force", "overwrite a non-Noir .mcp.json entry (--workspace only)").addOption(
|
|
@@ -6373,10 +6776,16 @@ function createProgram() {
|
|
|
6373
6776
|
program2.command("doctor").description("environment + project health").option(
|
|
6374
6777
|
"--dedup",
|
|
6375
6778
|
"scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
|
|
6376
|
-
).action(async (...args) => {
|
|
6779
|
+
).option("--fix", "re-assert owner-only permissions on .noir/.env and the store DB + directory").action(async (...args) => {
|
|
6377
6780
|
const g = actionGlobals(args);
|
|
6378
|
-
const
|
|
6379
|
-
|
|
6781
|
+
const opts = trailingCmd(args).opts();
|
|
6782
|
+
const dedup = opts.dedup === true;
|
|
6783
|
+
const fix = opts.fix === true;
|
|
6784
|
+
await doctor({
|
|
6785
|
+
...toCliOptions(g),
|
|
6786
|
+
...dedup ? { dedup: true } : {},
|
|
6787
|
+
...fix ? { fix: true } : {}
|
|
6788
|
+
});
|
|
6380
6789
|
});
|
|
6381
6790
|
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
6382
6791
|
await status(toStatusOptions(actionGlobals(args)));
|
|
@@ -6569,7 +6978,7 @@ function createProgram() {
|
|
|
6569
6978
|
...check === void 0 || check.length === 0 ? {} : { check }
|
|
6570
6979
|
});
|
|
6571
6980
|
});
|
|
6572
|
-
taskGrp.command("research").description("
|
|
6981
|
+
taskGrp.command("research").description("show research status for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
6573
6982
|
const cmd = trailingCmd(args);
|
|
6574
6983
|
const g = cmd.optsWithGlobals();
|
|
6575
6984
|
const first = args[0];
|
|
@@ -6816,22 +7225,22 @@ var init_bin = __esm({
|
|
|
6816
7225
|
});
|
|
6817
7226
|
|
|
6818
7227
|
// src/daemon-client.ts
|
|
6819
|
-
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
7228
|
+
import { Client, StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/client";
|
|
6820
7229
|
import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
6821
7230
|
import {
|
|
6822
|
-
loadProjectInfo as
|
|
7231
|
+
loadProjectInfo as loadProjectInfo18,
|
|
6823
7232
|
NOIR_VERSION as NOIR_VERSION5,
|
|
6824
7233
|
readWorkspaceMarker as readWorkspaceMarker2,
|
|
6825
7234
|
workspaceStoreDbPath
|
|
6826
7235
|
} from "@noir-ai/core";
|
|
6827
7236
|
import {
|
|
6828
7237
|
ensureDaemonRunning as ensureDaemonRunning3,
|
|
6829
|
-
pidAlive as
|
|
6830
|
-
readDaemonToken as
|
|
7238
|
+
pidAlive as pidAlive4,
|
|
7239
|
+
readDaemonToken as readDaemonToken3,
|
|
6831
7240
|
readProjectDaemonRecord as readProjectDaemonRecord2,
|
|
6832
|
-
readWorkspaceDaemonRecord as
|
|
7241
|
+
readWorkspaceDaemonRecord as readWorkspaceDaemonRecord3,
|
|
6833
7242
|
resolveGateConfig,
|
|
6834
|
-
tokenPath
|
|
7243
|
+
tokenPath as tokenPath2
|
|
6835
7244
|
} from "@noir-ai/daemon";
|
|
6836
7245
|
import { createMemoryEngine } from "@noir-ai/memory";
|
|
6837
7246
|
import { openStore } from "@noir-ai/store";
|
|
@@ -6854,7 +7263,7 @@ function failDaemonDown(opts, cause) {
|
|
|
6854
7263
|
async function probeDaemon(opts = {}) {
|
|
6855
7264
|
const expectedProject = opts.project?.id ?? (() => {
|
|
6856
7265
|
try {
|
|
6857
|
-
return
|
|
7266
|
+
return loadProjectInfo18(process.cwd()).id;
|
|
6858
7267
|
} catch {
|
|
6859
7268
|
return void 0;
|
|
6860
7269
|
}
|
|
@@ -6868,7 +7277,7 @@ async function probeDaemon(opts = {}) {
|
|
|
6868
7277
|
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
6869
7278
|
return { running: false };
|
|
6870
7279
|
}
|
|
6871
|
-
if (!
|
|
7280
|
+
if (!pidAlive4(rec.pid)) {
|
|
6872
7281
|
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
6873
7282
|
`);
|
|
6874
7283
|
return { running: false };
|
|
@@ -6907,7 +7316,7 @@ async function probeDaemon(opts = {}) {
|
|
|
6907
7316
|
async function resolveDaemon(opts) {
|
|
6908
7317
|
let project;
|
|
6909
7318
|
try {
|
|
6910
|
-
project = opts.project ??
|
|
7319
|
+
project = opts.project ?? loadProjectInfo18(process.cwd());
|
|
6911
7320
|
} catch {
|
|
6912
7321
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6913
7322
|
}
|
|
@@ -6968,8 +7377,8 @@ async function resolveDaemon(opts) {
|
|
|
6968
7377
|
return { client, stop };
|
|
6969
7378
|
}
|
|
6970
7379
|
function httpTransport(url, scopeKey) {
|
|
6971
|
-
const token = scopeKey === null ? null :
|
|
6972
|
-
return new
|
|
7380
|
+
const token = scopeKey === null ? null : readDaemonToken3(scopeKey);
|
|
7381
|
+
return new StreamableHTTPClientTransport2(
|
|
6973
7382
|
new URL(url),
|
|
6974
7383
|
// Verified present: StreamableHTTPClientTransportOptions.requestInit?: RequestInit
|
|
6975
7384
|
token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}
|
|
@@ -6977,7 +7386,7 @@ function httpTransport(url, scopeKey) {
|
|
|
6977
7386
|
}
|
|
6978
7387
|
function callerScopeKey(opts) {
|
|
6979
7388
|
try {
|
|
6980
|
-
return (opts.project ??
|
|
7389
|
+
return (opts.project ?? loadProjectInfo18(process.cwd())).id;
|
|
6981
7390
|
} catch {
|
|
6982
7391
|
return null;
|
|
6983
7392
|
}
|
|
@@ -7007,7 +7416,7 @@ function failWorkspaceConnect(opts, routing, cause) {
|
|
|
7007
7416
|
}
|
|
7008
7417
|
fail(
|
|
7009
7418
|
EXIT.DAEMON_DOWN,
|
|
7010
|
-
`workspace ${JSON.stringify(routing.name)} daemon is running but rejected this client's token (HTTP 401) \u2014 its token file is ${
|
|
7419
|
+
`workspace ${JSON.stringify(routing.name)} daemon is running but rejected this client's token (HTTP 401) \u2014 its token file is ${tokenPath2(routing.name)}; make sure it is readable, or restart the workspace daemon with \`noir daemon start --workspace ${routing.name}\` to mint a fresh one.`,
|
|
7011
7420
|
opts
|
|
7012
7421
|
);
|
|
7013
7422
|
}
|
|
@@ -7078,7 +7487,7 @@ async function callDaemonTool(opts, name, args = {}) {
|
|
|
7078
7487
|
async function withInProcessRead(opts, fn) {
|
|
7079
7488
|
let project;
|
|
7080
7489
|
try {
|
|
7081
|
-
project = opts.project ??
|
|
7490
|
+
project = opts.project ?? loadProjectInfo18(process.cwd());
|
|
7082
7491
|
} catch {
|
|
7083
7492
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
7084
7493
|
}
|
|
@@ -7125,14 +7534,14 @@ function workspaceRoutingTarget(root) {
|
|
|
7125
7534
|
const name = readWorkspaceMarker2(root);
|
|
7126
7535
|
if (name === null) return null;
|
|
7127
7536
|
try {
|
|
7128
|
-
return { name, projectId:
|
|
7537
|
+
return { name, projectId: loadProjectInfo18(root).id };
|
|
7129
7538
|
} catch {
|
|
7130
7539
|
return null;
|
|
7131
7540
|
}
|
|
7132
7541
|
}
|
|
7133
7542
|
async function probeWorkspaceDaemon(routing) {
|
|
7134
|
-
const rec =
|
|
7135
|
-
if (rec === null || !
|
|
7543
|
+
const rec = readWorkspaceDaemonRecord3(routing.name);
|
|
7544
|
+
if (rec === null || !pidAlive4(rec.pid)) return { running: false };
|
|
7136
7545
|
try {
|
|
7137
7546
|
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
7138
7547
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
@@ -7215,17 +7624,182 @@ var init_daemon_client = __esm({
|
|
|
7215
7624
|
}
|
|
7216
7625
|
});
|
|
7217
7626
|
|
|
7627
|
+
// src/hygiene-scan.ts
|
|
7628
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
|
|
7629
|
+
import { join as join5 } from "path";
|
|
7630
|
+
import { checkHygiene } from "@noir-ai/skills";
|
|
7631
|
+
function readDirEntries(dir) {
|
|
7632
|
+
try {
|
|
7633
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
7634
|
+
} catch {
|
|
7635
|
+
return [];
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
function isDirectory(path) {
|
|
7639
|
+
try {
|
|
7640
|
+
return statSync(path).isDirectory();
|
|
7641
|
+
} catch {
|
|
7642
|
+
return false;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
function hygieneKindOf(path) {
|
|
7646
|
+
if (HYGIENE_MARKDOWN_EXTENSIONS.test(path)) return "markdown";
|
|
7647
|
+
if (HYGIENE_CODE_EXTENSIONS.test(path)) return "code";
|
|
7648
|
+
return null;
|
|
7649
|
+
}
|
|
7650
|
+
function hasScannableLayout(root) {
|
|
7651
|
+
if (isDirectory(join5(root, "docs")) || isDirectory(join5(root, "scripts"))) return true;
|
|
7652
|
+
for (const entry of readDirEntries(join5(root, "packages"))) {
|
|
7653
|
+
if (!entry.isDirectory()) continue;
|
|
7654
|
+
if (isDirectory(join5(root, "packages", entry.name, "src"))) return true;
|
|
7655
|
+
if (isDirectory(join5(root, "packages", entry.name, "test"))) return true;
|
|
7656
|
+
}
|
|
7657
|
+
return false;
|
|
7658
|
+
}
|
|
7659
|
+
function addHygieneFile(rel, walk) {
|
|
7660
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
7661
|
+
if (name.startsWith(".") || HYGIENE_EXCLUDED_FILES.has(name)) return;
|
|
7662
|
+
const kind = hygieneKindOf(rel);
|
|
7663
|
+
if (kind === null) return;
|
|
7664
|
+
if (walk.files.length >= HYGIENE_MAX_FILES) {
|
|
7665
|
+
walk.truncated = true;
|
|
7666
|
+
return;
|
|
7667
|
+
}
|
|
7668
|
+
walk.files.push({ path: rel, kind });
|
|
7669
|
+
}
|
|
7670
|
+
function walkScannable(root, relDir, skip, walk) {
|
|
7671
|
+
if (walk.truncated) return;
|
|
7672
|
+
for (const entry of readDirEntries(join5(root, relDir))) {
|
|
7673
|
+
if (walk.truncated) return;
|
|
7674
|
+
const rel = `${relDir}/${entry.name}`;
|
|
7675
|
+
if (entry.isDirectory()) {
|
|
7676
|
+
if (HYGIENE_SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || skip.has(rel)) {
|
|
7677
|
+
continue;
|
|
7678
|
+
}
|
|
7679
|
+
walkScannable(root, rel, skip, walk);
|
|
7680
|
+
} else if (entry.isFile()) {
|
|
7681
|
+
addHygieneFile(rel, walk);
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
function collectHygieneFiles(root) {
|
|
7686
|
+
const walk = { files: [], truncated: false };
|
|
7687
|
+
for (const entry of readDirEntries(root)) {
|
|
7688
|
+
if (entry.isFile()) addHygieneFile(entry.name, walk);
|
|
7689
|
+
}
|
|
7690
|
+
walkScannable(root, "docs", HYGIENE_EXCLUDED_DIRS, walk);
|
|
7691
|
+
walkScannable(root, ".claude/skills", NO_SKIP_DIRS, walk);
|
|
7692
|
+
for (const entry of readDirEntries(join5(root, "packages"))) {
|
|
7693
|
+
if (!entry.isDirectory()) continue;
|
|
7694
|
+
walkScannable(root, `packages/${entry.name}/src`, NO_SKIP_DIRS, walk);
|
|
7695
|
+
walkScannable(root, `packages/${entry.name}/test`, NO_SKIP_DIRS, walk);
|
|
7696
|
+
}
|
|
7697
|
+
walkScannable(root, "scripts", NO_SKIP_DIRS, walk);
|
|
7698
|
+
walk.files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
7699
|
+
return walk;
|
|
7700
|
+
}
|
|
7701
|
+
function hygieneOmissions(result) {
|
|
7702
|
+
const notes = [];
|
|
7703
|
+
if (result.skipped > 0) {
|
|
7704
|
+
notes.push(
|
|
7705
|
+
`${result.skipped} file${result.skipped === 1 ? "" : "s"} over ${HYGIENE_MAX_FILE_BYTES / 1024} KiB skipped`
|
|
7706
|
+
);
|
|
7707
|
+
}
|
|
7708
|
+
if (result.truncated) notes.push(`stopped at ${HYGIENE_MAX_FILES} files`);
|
|
7709
|
+
return notes.length > 0 ? ` (${notes.join("; ")})` : "";
|
|
7710
|
+
}
|
|
7711
|
+
function hygieneCounts(result) {
|
|
7712
|
+
const omitted = hygieneOmissions(result);
|
|
7713
|
+
if (result.scanned === 0) {
|
|
7714
|
+
return `nothing to scan (no packages/*/src, packages/*/test, scripts/ or documents found)${omitted}`;
|
|
7715
|
+
}
|
|
7716
|
+
if (result.findings.length === 0) {
|
|
7717
|
+
return `clean \u2014 ${result.scanned} file${result.scanned === 1 ? "" : "s"} scanned${omitted}`;
|
|
7718
|
+
}
|
|
7719
|
+
const files = new Set(result.findings.map((f) => f.path)).size;
|
|
7720
|
+
return `${result.fail} fail, ${result.warn} warn in ${files} file${files === 1 ? "" : "s"}${omitted}`;
|
|
7721
|
+
}
|
|
7722
|
+
function hygieneDetail(result) {
|
|
7723
|
+
const counts = hygieneCounts(result);
|
|
7724
|
+
if (result.scanned === 0 || result.findings.length === 0) return counts;
|
|
7725
|
+
const named = result.findings.slice(0, HYGIENE_FINDING_CAP);
|
|
7726
|
+
const hidden = result.findings.length - named.length;
|
|
7727
|
+
const where = named.map((f) => `${f.path}:${f.line} ${f.id}`).join("; ");
|
|
7728
|
+
return `${counts} \u2014 ${where}${hidden > 0 ? ` (+${hidden} more)` : ""}`;
|
|
7729
|
+
}
|
|
7730
|
+
function scanOutputHygiene(root) {
|
|
7731
|
+
const empty = {
|
|
7732
|
+
scanned: 0,
|
|
7733
|
+
skipped: 0,
|
|
7734
|
+
truncated: false,
|
|
7735
|
+
findings: [],
|
|
7736
|
+
fail: 0,
|
|
7737
|
+
warn: 0
|
|
7738
|
+
};
|
|
7739
|
+
if (!hasScannableLayout(root)) return empty;
|
|
7740
|
+
const walk = collectHygieneFiles(root);
|
|
7741
|
+
const findings = [];
|
|
7742
|
+
let scanned = 0;
|
|
7743
|
+
let skipped = 0;
|
|
7744
|
+
for (const file of walk.files) {
|
|
7745
|
+
const abs = join5(root, file.path);
|
|
7746
|
+
try {
|
|
7747
|
+
if (statSync(abs).size > HYGIENE_MAX_FILE_BYTES) {
|
|
7748
|
+
skipped++;
|
|
7749
|
+
continue;
|
|
7750
|
+
}
|
|
7751
|
+
const text = readFileSync4(abs, "utf8");
|
|
7752
|
+
scanned++;
|
|
7753
|
+
for (const found of checkHygiene(text, file.kind)) {
|
|
7754
|
+
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
7755
|
+
}
|
|
7756
|
+
} catch {
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
findings.sort((a, b) => {
|
|
7760
|
+
if (a.tier !== b.tier) return a.tier === "fail" ? -1 : 1;
|
|
7761
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
7762
|
+
return a.line - b.line;
|
|
7763
|
+
});
|
|
7764
|
+
const fail2 = findings.filter((f) => f.tier === "fail").length;
|
|
7765
|
+
return {
|
|
7766
|
+
scanned,
|
|
7767
|
+
skipped,
|
|
7768
|
+
truncated: walk.truncated,
|
|
7769
|
+
findings,
|
|
7770
|
+
fail: fail2,
|
|
7771
|
+
warn: findings.length - fail2
|
|
7772
|
+
};
|
|
7773
|
+
}
|
|
7774
|
+
var HYGIENE_FINDING_CAP, HYGIENE_MAX_FILE_BYTES, HYGIENE_MAX_FILES, HYGIENE_CODE_EXTENSIONS, HYGIENE_MARKDOWN_EXTENSIONS, HYGIENE_SKIP_DIRS, HYGIENE_EXCLUDED_DIRS, HYGIENE_EXCLUDED_FILES, NO_SKIP_DIRS;
|
|
7775
|
+
var init_hygiene_scan = __esm({
|
|
7776
|
+
"src/hygiene-scan.ts"() {
|
|
7777
|
+
"use strict";
|
|
7778
|
+
HYGIENE_FINDING_CAP = 5;
|
|
7779
|
+
HYGIENE_MAX_FILE_BYTES = 512 * 1024;
|
|
7780
|
+
HYGIENE_MAX_FILES = 2e3;
|
|
7781
|
+
HYGIENE_CODE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|sh)$/i;
|
|
7782
|
+
HYGIENE_MARKDOWN_EXTENSIONS = /\.(?:md|mdx)$/i;
|
|
7783
|
+
HYGIENE_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
|
|
7784
|
+
HYGIENE_EXCLUDED_DIRS = /* @__PURE__ */ new Set(["docs/internal", "docs/decisions", "docs/roadmap"]);
|
|
7785
|
+
HYGIENE_EXCLUDED_FILES = /* @__PURE__ */ new Set(["CHANGELOG.md"]);
|
|
7786
|
+
NO_SKIP_DIRS = /* @__PURE__ */ new Set();
|
|
7787
|
+
}
|
|
7788
|
+
});
|
|
7789
|
+
|
|
7218
7790
|
// src/commands/doctor.ts
|
|
7219
7791
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7220
|
-
import { existsSync as existsSync5, readdirSync as
|
|
7792
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
7221
7793
|
import { dirname, join as join6, relative } from "path";
|
|
7222
7794
|
import { fileURLToPath } from "url";
|
|
7223
|
-
import { resolveAdapter as
|
|
7795
|
+
import { mcpConfigPathFor as mcpConfigPathFor2, resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
7224
7796
|
import {
|
|
7225
7797
|
detectActiveMethod as detectActiveMethod4,
|
|
7798
|
+
ensureOwnerOnly,
|
|
7799
|
+
ensureOwnerOnlyDir,
|
|
7226
7800
|
latestVersionFromCache as latestVersionFromCache2,
|
|
7227
7801
|
loadNoirEnv as loadNoirEnv3,
|
|
7228
|
-
loadProjectInfo as
|
|
7802
|
+
loadProjectInfo as loadProjectInfo19,
|
|
7229
7803
|
NOIR_VERSION as NOIR_VERSION6,
|
|
7230
7804
|
paths as paths4,
|
|
7231
7805
|
readInstallRecord as readInstallRecord4,
|
|
@@ -7238,7 +7812,7 @@ import {
|
|
|
7238
7812
|
readScaffoldVersion,
|
|
7239
7813
|
render
|
|
7240
7814
|
} from "@noir-ai/create";
|
|
7241
|
-
import { pidAlive as
|
|
7815
|
+
import { pidAlive as pidAlive5, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
|
|
7242
7816
|
import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
|
|
7243
7817
|
async function probeOnnx() {
|
|
7244
7818
|
try {
|
|
@@ -7254,7 +7828,7 @@ function describeOnnx(o) {
|
|
|
7254
7828
|
if (/Cannot find (module|package)/i.test(o.reason)) {
|
|
7255
7829
|
return "not resolvable from CLI probe (best-effort)";
|
|
7256
7830
|
}
|
|
7257
|
-
const r = o.reason
|
|
7831
|
+
const r = truncateToWidth(o.reason, 60);
|
|
7258
7832
|
return `probe failed: ${r}`;
|
|
7259
7833
|
}
|
|
7260
7834
|
function fmtUptime(sec) {
|
|
@@ -7286,7 +7860,7 @@ function checkConfig(root) {
|
|
|
7286
7860
|
};
|
|
7287
7861
|
}
|
|
7288
7862
|
try {
|
|
7289
|
-
const project =
|
|
7863
|
+
const project = loadProjectInfo19(root);
|
|
7290
7864
|
return {
|
|
7291
7865
|
project,
|
|
7292
7866
|
result: {
|
|
@@ -7320,7 +7894,7 @@ async function checkDaemon(checks, project) {
|
|
|
7320
7894
|
});
|
|
7321
7895
|
return;
|
|
7322
7896
|
}
|
|
7323
|
-
if (!
|
|
7897
|
+
if (!pidAlive5(rec.pid)) {
|
|
7324
7898
|
checks.push({
|
|
7325
7899
|
name: "daemon",
|
|
7326
7900
|
status: "warn",
|
|
@@ -7403,11 +7977,21 @@ async function checkStore(checks, project, root) {
|
|
|
7403
7977
|
}
|
|
7404
7978
|
}
|
|
7405
7979
|
}
|
|
7980
|
+
function envModeCause(st) {
|
|
7981
|
+
return st.ctimeMs > st.mtimeMs ? "changed" : "legacy";
|
|
7982
|
+
}
|
|
7983
|
+
function octalMode(mode) {
|
|
7984
|
+
return mode.toString(8).padStart(4, "0");
|
|
7985
|
+
}
|
|
7986
|
+
function describeEnvMode(cause, mode) {
|
|
7987
|
+
const lead = `is ${octalMode(mode)} \u2014 accessible to other accounts; run \`noir sync\` (or \`chmod 600 .noir/.env\`) to make it owner-only.`;
|
|
7988
|
+
return cause === "legacy" ? `${lead} Nothing has changed its mode since the file was written, so the file predates the owner-only contract (an older Noir seeded it, a clone delivered it, it was created by hand, or it was saved by a tool that replaces the file) \u2014 this is not a change you made.` : `${lead} Its mode was changed after the file was written (a \`chmod\`, a \`chown\`, or a restore that re-applied metadata), so no older Noir left it this way.`;
|
|
7989
|
+
}
|
|
7406
7990
|
function checkNoirEnv(checks, root, loaded) {
|
|
7407
7991
|
const path = join6(paths4.noirDir(root), ".env");
|
|
7408
7992
|
let st;
|
|
7409
7993
|
try {
|
|
7410
|
-
st =
|
|
7994
|
+
st = statSync2(path);
|
|
7411
7995
|
} catch {
|
|
7412
7996
|
return;
|
|
7413
7997
|
}
|
|
@@ -7425,8 +8009,8 @@ function checkNoirEnv(checks, root, loaded) {
|
|
|
7425
8009
|
tooOpen ? {
|
|
7426
8010
|
name: "noir-env",
|
|
7427
8011
|
status: "warn",
|
|
7428
|
-
detail: `${path}
|
|
7429
|
-
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode
|
|
8012
|
+
detail: `${path} ${describeEnvMode(envModeCause(st), mode)}`
|
|
8013
|
+
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${octalMode(mode)})` }
|
|
7430
8014
|
);
|
|
7431
8015
|
for (const [key, source] of Object.entries(loaded.sources)) {
|
|
7432
8016
|
if (source !== "file") continue;
|
|
@@ -7526,7 +8110,7 @@ function checkSeedDrift(checks, root) {
|
|
|
7526
8110
|
const seedPath = join6(root, ".noir", ".env.example");
|
|
7527
8111
|
let bytes;
|
|
7528
8112
|
try {
|
|
7529
|
-
bytes =
|
|
8113
|
+
bytes = readFileSync5(seedPath, "utf8");
|
|
7530
8114
|
} catch {
|
|
7531
8115
|
return;
|
|
7532
8116
|
}
|
|
@@ -7544,6 +8128,14 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
7544
8128
|
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
7545
8129
|
return null;
|
|
7546
8130
|
}
|
|
8131
|
+
if (project.config.rules.enabled === false) {
|
|
8132
|
+
checks.push({
|
|
8133
|
+
name: "rules budget",
|
|
8134
|
+
status: "ok",
|
|
8135
|
+
detail: "disabled \u2014 rules.enabled is false in .noir/config.yml (RULES.md is not measured)"
|
|
8136
|
+
});
|
|
8137
|
+
return null;
|
|
8138
|
+
}
|
|
7547
8139
|
const rulesPath = paths4.rulesMd(root);
|
|
7548
8140
|
if (!existsSync5(rulesPath)) {
|
|
7549
8141
|
checks.push({
|
|
@@ -7555,7 +8147,7 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
7555
8147
|
}
|
|
7556
8148
|
let content;
|
|
7557
8149
|
try {
|
|
7558
|
-
content =
|
|
8150
|
+
content = readFileSync5(rulesPath, "utf8");
|
|
7559
8151
|
} catch (e) {
|
|
7560
8152
|
checks.push({
|
|
7561
8153
|
name: "rules budget",
|
|
@@ -7599,7 +8191,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
7599
8191
|
return null;
|
|
7600
8192
|
}
|
|
7601
8193
|
const host = project.config.host;
|
|
7602
|
-
const adapter =
|
|
8194
|
+
const adapter = resolveAdapter6(host);
|
|
7603
8195
|
const ectx = { root };
|
|
7604
8196
|
const expected = [];
|
|
7605
8197
|
const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
|
|
@@ -7608,7 +8200,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
7608
8200
|
}
|
|
7609
8201
|
if (host === "claude") expected.push("CLAUDE.md");
|
|
7610
8202
|
else if (host === "gemini") expected.push("GEMINI.md");
|
|
7611
|
-
expected.push(relOr(
|
|
8203
|
+
expected.push(relOr(mcpConfigPathFor2(host, root), root));
|
|
7612
8204
|
const missing = expected.filter((p) => !existsSync5(joinRoot(root, p)));
|
|
7613
8205
|
const status2 = missing.length === 0 ? "ok" : "warn";
|
|
7614
8206
|
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)`;
|
|
@@ -7635,6 +8227,15 @@ function checkNestedNoir(checks, root) {
|
|
|
7635
8227
|
});
|
|
7636
8228
|
return { detected, paths: found };
|
|
7637
8229
|
}
|
|
8230
|
+
function checkOutputHygiene(checks, root) {
|
|
8231
|
+
const result = scanOutputHygiene(root);
|
|
8232
|
+
checks.push({
|
|
8233
|
+
name: "output hygiene",
|
|
8234
|
+
status: result.fail > 0 ? "fail" : result.warn > 0 ? "warn" : "ok",
|
|
8235
|
+
detail: hygieneDetail(result)
|
|
8236
|
+
});
|
|
8237
|
+
return result;
|
|
8238
|
+
}
|
|
7638
8239
|
function collectDedupCandidates(root) {
|
|
7639
8240
|
const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
|
|
7640
8241
|
const out = [];
|
|
@@ -7643,7 +8244,7 @@ function collectDedupCandidates(root) {
|
|
|
7643
8244
|
if (!existsSync5(abs)) continue;
|
|
7644
8245
|
let text;
|
|
7645
8246
|
try {
|
|
7646
|
-
text =
|
|
8247
|
+
text = readFileSync5(abs, "utf8");
|
|
7647
8248
|
} catch {
|
|
7648
8249
|
continue;
|
|
7649
8250
|
}
|
|
@@ -7703,7 +8304,7 @@ function resolveWorkspacePackagesDir() {
|
|
|
7703
8304
|
const pkgPath = join6(here, "package.json");
|
|
7704
8305
|
if (existsSync5(pkgPath)) {
|
|
7705
8306
|
try {
|
|
7706
|
-
const pkg = JSON.parse(
|
|
8307
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
7707
8308
|
if (pkg.name === "@noir-ai/cli") {
|
|
7708
8309
|
const packagesDir = dirname(here);
|
|
7709
8310
|
if (existsSync5(join6(dirname(packagesDir), "pnpm-workspace.yaml"))) {
|
|
@@ -7751,7 +8352,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
7751
8352
|
}
|
|
7752
8353
|
let entries = [];
|
|
7753
8354
|
try {
|
|
7754
|
-
entries =
|
|
8355
|
+
entries = readdirSync3(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7755
8356
|
} catch (e) {
|
|
7756
8357
|
checks.push({
|
|
7757
8358
|
name: "publish",
|
|
@@ -7768,7 +8369,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
7768
8369
|
if (!existsSync5(pkgPath)) continue;
|
|
7769
8370
|
let pkg;
|
|
7770
8371
|
try {
|
|
7771
|
-
pkg = JSON.parse(
|
|
8372
|
+
pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
7772
8373
|
} catch (e) {
|
|
7773
8374
|
issues.push(`${name}/package.json: unparseable (${msg(e)})`);
|
|
7774
8375
|
checked++;
|
|
@@ -7847,6 +8448,17 @@ function renderHuman2(payload, opts) {
|
|
|
7847
8448
|
["Check", "Status", "Detail"],
|
|
7848
8449
|
opts
|
|
7849
8450
|
);
|
|
8451
|
+
if (payload.fixes) {
|
|
8452
|
+
const healed = payload.fixes.filter((f) => f.outcome === "healed");
|
|
8453
|
+
if (healed.length > 0) {
|
|
8454
|
+
log(
|
|
8455
|
+
`fixed ${healed.length} permission${healed.length === 1 ? "" : "s"}: ${healed.map((f) => f.path).join(", ")}`,
|
|
8456
|
+
opts
|
|
8457
|
+
);
|
|
8458
|
+
} else {
|
|
8459
|
+
log("nothing to fix \u2014 permissions already owner-only", opts);
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
7850
8462
|
const { ok, warn: warnN, fail: fail2 } = payload.summary;
|
|
7851
8463
|
if (fail2 > 0) {
|
|
7852
8464
|
error(
|
|
@@ -7859,12 +8471,24 @@ function renderHuman2(payload, opts) {
|
|
|
7859
8471
|
success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
|
|
7860
8472
|
}
|
|
7861
8473
|
}
|
|
8474
|
+
function fixOwnerOnlyPermissions(root, project) {
|
|
8475
|
+
const fixes = [
|
|
8476
|
+
{ path: ".noir/.env", outcome: ensureOwnerOnly(join6(paths4.noirDir(root), ".env")) }
|
|
8477
|
+
];
|
|
8478
|
+
if (project) {
|
|
8479
|
+
const dbRel = `.noir/store/${project.id}.db`;
|
|
8480
|
+
fixes.push({ path: dbRel, outcome: ensureOwnerOnly(join6(root, dbRel)) });
|
|
8481
|
+
fixes.push({ path: ".noir/store", outcome: ensureOwnerOnlyDir(paths4.storeDir(root)) });
|
|
8482
|
+
}
|
|
8483
|
+
return fixes;
|
|
8484
|
+
}
|
|
7862
8485
|
async function doctor(opts = {}) {
|
|
7863
8486
|
const root = process.cwd();
|
|
7864
8487
|
const checks = [];
|
|
7865
8488
|
await checkRuntime(checks);
|
|
7866
8489
|
const { project, result: configResult } = checkConfig(root);
|
|
7867
8490
|
checks.push(configResult);
|
|
8491
|
+
const fixes = opts.fix === true ? fixOwnerOnlyPermissions(root, project) : void 0;
|
|
7868
8492
|
await checkDaemon(checks, project);
|
|
7869
8493
|
const { vecOk } = await checkNativeDeps(checks);
|
|
7870
8494
|
await checkStore(checks, project, root);
|
|
@@ -7877,6 +8501,7 @@ async function doctor(opts = {}) {
|
|
|
7877
8501
|
const rules = checkRulesMdBudget(checks, root, project);
|
|
7878
8502
|
const host = checkHostArtifacts(checks, root, project);
|
|
7879
8503
|
checkNestedNoir(checks, root);
|
|
8504
|
+
checkOutputHygiene(checks, root);
|
|
7880
8505
|
if (opts.dedup === true) {
|
|
7881
8506
|
await checkSemanticDupDoctor(checks, root, project);
|
|
7882
8507
|
}
|
|
@@ -7900,7 +8525,8 @@ async function doctor(opts = {}) {
|
|
|
7900
8525
|
method: installCheck.method,
|
|
7901
8526
|
installedVersion: installCheck.installedVersion,
|
|
7902
8527
|
latestKnown: installCheck.latestKnown
|
|
7903
|
-
}
|
|
8528
|
+
},
|
|
8529
|
+
...fixes ? { fixes } : {}
|
|
7904
8530
|
};
|
|
7905
8531
|
if (opts.json === true) {
|
|
7906
8532
|
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
@@ -7920,17 +8546,17 @@ var init_doctor = __esm({
|
|
|
7920
8546
|
"src/commands/doctor.ts"() {
|
|
7921
8547
|
"use strict";
|
|
7922
8548
|
init_daemon_client();
|
|
8549
|
+
init_hygiene_scan();
|
|
7923
8550
|
init_output();
|
|
7924
8551
|
init_theme();
|
|
8552
|
+
init_width();
|
|
7925
8553
|
ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
|
|
7926
8554
|
SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7927
8555
|
}
|
|
7928
8556
|
});
|
|
7929
8557
|
|
|
7930
|
-
// src/doctor.ts
|
|
7931
|
-
init_doctor();
|
|
7932
|
-
|
|
7933
8558
|
// src/index.ts
|
|
8559
|
+
init_doctor();
|
|
7934
8560
|
init_init();
|
|
7935
8561
|
init_serve();
|
|
7936
8562
|
export {
|