@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/bin.js
CHANGED
|
@@ -414,10 +414,82 @@ var init_daemon_client = __esm({
|
|
|
414
414
|
}
|
|
415
415
|
});
|
|
416
416
|
|
|
417
|
+
// src/width.ts
|
|
418
|
+
import stringWidth from "string-width";
|
|
419
|
+
import stripAnsi from "strip-ansi";
|
|
420
|
+
function clusters(s) {
|
|
421
|
+
return [...segmenter.segment(s)].map(({ segment }) => segment);
|
|
422
|
+
}
|
|
423
|
+
function displayWidth(s) {
|
|
424
|
+
return stringWidth(s);
|
|
425
|
+
}
|
|
426
|
+
function fitLeading(parts, width) {
|
|
427
|
+
let used = 0;
|
|
428
|
+
let count = 0;
|
|
429
|
+
for (const part of parts) {
|
|
430
|
+
const w = displayWidth(part);
|
|
431
|
+
if (used + w > width) {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
used += w;
|
|
435
|
+
count++;
|
|
436
|
+
}
|
|
437
|
+
return count;
|
|
438
|
+
}
|
|
439
|
+
function truncateToWidth(s, width, ellipsis = "\u2026") {
|
|
440
|
+
const plain = stripAnsi(s);
|
|
441
|
+
if (width <= 0) {
|
|
442
|
+
return "";
|
|
443
|
+
}
|
|
444
|
+
if (displayWidth(plain) <= width) {
|
|
445
|
+
return plain;
|
|
446
|
+
}
|
|
447
|
+
const markerParts = clusters(ellipsis);
|
|
448
|
+
const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
|
|
449
|
+
const budget = width - displayWidth(marker);
|
|
450
|
+
const parts = clusters(plain);
|
|
451
|
+
return parts.slice(0, fitLeading(parts, budget)).join("") + marker;
|
|
452
|
+
}
|
|
453
|
+
function truncateMiddle(s, width, ellipsis = "\u2026") {
|
|
454
|
+
const plain = stripAnsi(s);
|
|
455
|
+
if (width <= 0) {
|
|
456
|
+
return "";
|
|
457
|
+
}
|
|
458
|
+
if (displayWidth(plain) <= width) {
|
|
459
|
+
return plain;
|
|
460
|
+
}
|
|
461
|
+
const markerParts = clusters(ellipsis);
|
|
462
|
+
const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
|
|
463
|
+
const remaining = width - displayWidth(marker);
|
|
464
|
+
const headBudget = Math.ceil(remaining / 2);
|
|
465
|
+
const tailBudget = remaining - headBudget;
|
|
466
|
+
const parts = clusters(plain);
|
|
467
|
+
const headCount = fitLeading(parts, headBudget);
|
|
468
|
+
const tail = [];
|
|
469
|
+
let tailUsed = 0;
|
|
470
|
+
for (const part of parts.slice(headCount).reverse()) {
|
|
471
|
+
const w = displayWidth(part);
|
|
472
|
+
if (tailUsed + w > tailBudget) {
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
tail.push(part);
|
|
476
|
+
tailUsed += w;
|
|
477
|
+
}
|
|
478
|
+
return parts.slice(0, headCount).join("") + marker + tail.reverse().join("");
|
|
479
|
+
}
|
|
480
|
+
var segmenter;
|
|
481
|
+
var init_width = __esm({
|
|
482
|
+
"src/width.ts"() {
|
|
483
|
+
"use strict";
|
|
484
|
+
segmenter = new Intl.Segmenter();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
417
488
|
// src/theme.ts
|
|
418
489
|
import pc from "picocolors";
|
|
419
|
-
function badge(state, label = state) {
|
|
420
|
-
const
|
|
490
|
+
function badge(state, label = state, maxWidth) {
|
|
491
|
+
const full = `${BADGE_SYMBOL[state]} ${label}`;
|
|
492
|
+
const text = maxWidth === void 0 ? full : truncateToWidth(full, maxWidth);
|
|
421
493
|
switch (state) {
|
|
422
494
|
case "ok":
|
|
423
495
|
return c.ok(text);
|
|
@@ -438,8 +510,13 @@ function isCiEnv() {
|
|
|
438
510
|
if (v === void 0 || v === "") return false;
|
|
439
511
|
return v !== "0" && v !== "false";
|
|
440
512
|
}
|
|
513
|
+
function quietMode() {
|
|
514
|
+
const v = process.env.NOIR_QUIET;
|
|
515
|
+
return v !== void 0 && v !== "";
|
|
516
|
+
}
|
|
441
517
|
function useColor() {
|
|
442
518
|
if (envFlagSet("NO_COLOR")) return false;
|
|
519
|
+
if (quietMode()) return false;
|
|
443
520
|
if (process.env.CLICOLOR_FORCE === "1") return true;
|
|
444
521
|
if (isCiEnv()) return false;
|
|
445
522
|
return pc.isColorSupported;
|
|
@@ -453,14 +530,17 @@ function terminalWidth() {
|
|
|
453
530
|
const n = Number(fromEnv);
|
|
454
531
|
if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
|
|
455
532
|
}
|
|
456
|
-
const
|
|
457
|
-
if (typeof
|
|
533
|
+
const stderrCols = process.stderr.columns;
|
|
534
|
+
if (typeof stderrCols === "number" && stderrCols > 0) return Math.max(stderrCols, 20);
|
|
535
|
+
const stdoutCols = process.stdout.columns;
|
|
536
|
+
if (typeof stdoutCols === "number" && stdoutCols > 0) return Math.max(stdoutCols, 20);
|
|
458
537
|
return 80;
|
|
459
538
|
}
|
|
460
539
|
var FORCE, c, BADGE_SYMBOL;
|
|
461
540
|
var init_theme = __esm({
|
|
462
541
|
"src/theme.ts"() {
|
|
463
542
|
"use strict";
|
|
543
|
+
init_width();
|
|
464
544
|
FORCE = pc.createColors(true);
|
|
465
545
|
c = {
|
|
466
546
|
/** Green — success / healthy. */
|
|
@@ -559,17 +639,18 @@ function table(rows, cols, opts = {}) {
|
|
|
559
639
|
info("(no rows)", opts);
|
|
560
640
|
return;
|
|
561
641
|
}
|
|
642
|
+
const widths = computeColWidths(rows, cols);
|
|
562
643
|
const t = new Table({
|
|
563
644
|
head: cols.map((col) => c.bold(c.info(col))),
|
|
564
645
|
// Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
|
|
565
646
|
// default red header nor a colored border). All color is ours, via theme.
|
|
566
647
|
style: { head: [], border: [] },
|
|
567
|
-
colWidths:
|
|
648
|
+
colWidths: widths,
|
|
568
649
|
wordWrap: true,
|
|
569
650
|
truncate: "\u2026"
|
|
570
651
|
});
|
|
571
652
|
for (const row of rows) {
|
|
572
|
-
t.push(cols.map((col) =>
|
|
653
|
+
t.push(cols.map((col, i) => truncateCell(row[col], (widths[i] ?? 0) - 2)));
|
|
573
654
|
}
|
|
574
655
|
process.stderr.write(`${t.toString()}
|
|
575
656
|
`);
|
|
@@ -577,43 +658,71 @@ function table(rows, cols, opts = {}) {
|
|
|
577
658
|
function computeColWidths(rows, cols) {
|
|
578
659
|
const n = cols.length;
|
|
579
660
|
if (n === 0) return [];
|
|
580
|
-
const headerLen = cols.map((col) => col
|
|
661
|
+
const headerLen = cols.map((col) => displayWidth(col));
|
|
581
662
|
const natural = cols.map((col, i) => {
|
|
582
|
-
let max = Math.max(headerLen[i] ?? col
|
|
663
|
+
let max = Math.max(headerLen[i] ?? displayWidth(col), MIN_CONTENT_WIDTH);
|
|
583
664
|
for (const row of rows) {
|
|
584
|
-
const len = formatCell(row[col])
|
|
665
|
+
const len = displayWidth(formatCell(row[col]));
|
|
585
666
|
if (len > max) max = len;
|
|
586
667
|
}
|
|
587
668
|
return max;
|
|
588
669
|
});
|
|
589
|
-
const
|
|
670
|
+
const budget = Math.max(n, terminalWidth() - 3 * n - 1);
|
|
590
671
|
const naturalSum = natural.reduce((a, b) => a + b, 0);
|
|
591
|
-
if (naturalSum <=
|
|
672
|
+
if (naturalSum <= budget) {
|
|
592
673
|
return natural.map((w) => w + 2);
|
|
593
674
|
}
|
|
594
675
|
const content = natural.slice();
|
|
595
676
|
let sum = naturalSum;
|
|
596
|
-
const floor = headerLen.map((h) => Math.max(h,
|
|
597
|
-
while (sum >
|
|
677
|
+
const floor = headerLen.map((h) => Math.min(Math.max(h, MIN_CONTENT_WIDTH), budget));
|
|
678
|
+
while (sum > budget) {
|
|
598
679
|
let idx = -1;
|
|
599
680
|
let widest = -1;
|
|
600
681
|
for (let i = 0; i < n; i++) {
|
|
601
682
|
const c2 = content[i] ?? 0;
|
|
602
|
-
const f = floor[i] ??
|
|
683
|
+
const f = floor[i] ?? MIN_CONTENT_WIDTH;
|
|
603
684
|
if (c2 > f && c2 > widest) {
|
|
604
685
|
widest = c2;
|
|
605
686
|
idx = i;
|
|
606
687
|
}
|
|
607
688
|
}
|
|
608
689
|
if (idx === -1) break;
|
|
609
|
-
const overshoot = sum -
|
|
610
|
-
const reducible = (content[idx] ?? 0) - (floor[idx] ??
|
|
690
|
+
const overshoot = sum - budget;
|
|
691
|
+
const reducible = (content[idx] ?? 0) - (floor[idx] ?? MIN_CONTENT_WIDTH);
|
|
611
692
|
const cut = Math.min(overshoot, reducible);
|
|
612
693
|
content[idx] = (content[idx] ?? 0) - cut;
|
|
613
694
|
sum -= cut;
|
|
614
695
|
}
|
|
696
|
+
if (sum > budget) {
|
|
697
|
+
for (let pass = 0; pass < n && sum > budget; pass++) {
|
|
698
|
+
let idx = -1;
|
|
699
|
+
let widest = 0;
|
|
700
|
+
for (let j = 0; j < n; j++) {
|
|
701
|
+
const c2 = content[j] ?? 0;
|
|
702
|
+
if (c2 > 1 && c2 > widest) {
|
|
703
|
+
widest = c2;
|
|
704
|
+
idx = j;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (idx === -1) break;
|
|
708
|
+
const cut = Math.min(sum - budget, (content[idx] ?? 1) - 1);
|
|
709
|
+
content[idx] = (content[idx] ?? 1) - cut;
|
|
710
|
+
sum -= cut;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
615
713
|
return content.map((w) => w + 2);
|
|
616
714
|
}
|
|
715
|
+
function isPathToken(s) {
|
|
716
|
+
if (!/[\\/]/.test(s)) return false;
|
|
717
|
+
return /^(\/|\.\/|\.\.\/|~\/|[A-Za-z]:[\\/])/.test(s) || /\.[A-Za-z0-9][A-Za-z0-9_-]*$/.test(s);
|
|
718
|
+
}
|
|
719
|
+
function truncateCell(value, contentWidth) {
|
|
720
|
+
const s = formatCell(value);
|
|
721
|
+
if (contentWidth <= 0 || displayWidth(s) <= contentWidth) return s;
|
|
722
|
+
if (isPathToken(s)) return truncateMiddle(s, contentWidth);
|
|
723
|
+
if (/\s/.test(s)) return s;
|
|
724
|
+
return truncateToWidth(s, contentWidth);
|
|
725
|
+
}
|
|
617
726
|
function formatCell(value) {
|
|
618
727
|
if (value === void 0 || value === null) return "";
|
|
619
728
|
if (typeof value === "string") return value;
|
|
@@ -624,7 +733,7 @@ function formatCell(value) {
|
|
|
624
733
|
}
|
|
625
734
|
function definitionList(rows, opts = {}) {
|
|
626
735
|
table(
|
|
627
|
-
rows.map((r) => ({ Field: r.label, Value:
|
|
736
|
+
rows.map((r) => ({ Field: r.label, Value: r.value })),
|
|
628
737
|
["Field", "Value"],
|
|
629
738
|
opts
|
|
630
739
|
);
|
|
@@ -705,11 +814,12 @@ function handleError(err) {
|
|
|
705
814
|
}
|
|
706
815
|
process.exitCode = inferExitCode(err);
|
|
707
816
|
}
|
|
708
|
-
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
817
|
+
var EXIT, NoirCliError, NOIR_ERROR_CODE, MIN_CONTENT_WIDTH, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
709
818
|
var init_output = __esm({
|
|
710
819
|
"src/output.ts"() {
|
|
711
820
|
"use strict";
|
|
712
821
|
init_theme();
|
|
822
|
+
init_width();
|
|
713
823
|
EXIT = {
|
|
714
824
|
OK: 0,
|
|
715
825
|
ERROR: 1,
|
|
@@ -727,6 +837,7 @@ var init_output = __esm({
|
|
|
727
837
|
}
|
|
728
838
|
};
|
|
729
839
|
NOIR_ERROR_CODE = "noir.error";
|
|
840
|
+
MIN_CONTENT_WIDTH = 3;
|
|
730
841
|
NoopSpinner = class {
|
|
731
842
|
text = "";
|
|
732
843
|
start(_text) {
|
|
@@ -961,14 +1072,230 @@ var init_context = __esm({
|
|
|
961
1072
|
}
|
|
962
1073
|
});
|
|
963
1074
|
|
|
1075
|
+
// src/workspace-bridge.ts
|
|
1076
|
+
import {
|
|
1077
|
+
ReadBuffer,
|
|
1078
|
+
StreamableHTTPClientTransport as StreamableHTTPClientTransport2,
|
|
1079
|
+
serializeMessage
|
|
1080
|
+
} from "@modelcontextprotocol/client";
|
|
1081
|
+
import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
|
|
1082
|
+
import { pidAlive as pidAlive2, readDaemonToken as readDaemonToken2, readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2, tokenPath as tokenPath2 } from "@noir-ai/daemon";
|
|
1083
|
+
async function resolveWorkspaceDaemon(name, root) {
|
|
1084
|
+
const record = readWorkspaceDaemonRecord2(name);
|
|
1085
|
+
if (record === null) {
|
|
1086
|
+
return { error: `no daemon recorded for workspace ${name}` };
|
|
1087
|
+
}
|
|
1088
|
+
const health = await verifyWorkspaceDaemon(record, name);
|
|
1089
|
+
if (health.kind === "foreign") {
|
|
1090
|
+
return {
|
|
1091
|
+
error: `workspace ${name} is recorded on port ${record.port}, but the daemon answering there serves workspace ${health.workspace}`
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (health.kind === "silent") {
|
|
1095
|
+
return { error: `record exists but the daemon is not answering (pid ${record.pid})` };
|
|
1096
|
+
}
|
|
1097
|
+
const token = readDaemonToken2(name);
|
|
1098
|
+
if (token === null) {
|
|
1099
|
+
return {
|
|
1100
|
+
error: `workspace ${name} daemon is answering but its token is unreadable at ${tokenPath2(name)}`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
let callerProjectId;
|
|
1104
|
+
try {
|
|
1105
|
+
callerProjectId = loadProjectInfo2(root).id;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return {
|
|
1108
|
+
error: `no project identity in ${root} \u2014 run \`noir init\` before serving a workspace from here`
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
return { url: `http://127.0.0.1:${record.port}/mcp?p=${callerProjectId}`, token };
|
|
1112
|
+
}
|
|
1113
|
+
async function verifyWorkspaceDaemon(record, name) {
|
|
1114
|
+
if (!pidAlive2(record.pid)) return { kind: "silent" };
|
|
1115
|
+
return probeHealth(record.port, name, record.pid);
|
|
1116
|
+
}
|
|
1117
|
+
async function probeHealth(port, name, pid) {
|
|
1118
|
+
try {
|
|
1119
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
1120
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
|
|
1121
|
+
});
|
|
1122
|
+
if (!response.ok) return { kind: "silent" };
|
|
1123
|
+
const body = await response.json().catch(() => null);
|
|
1124
|
+
if (body === null) return { kind: "silent" };
|
|
1125
|
+
if (typeof body.workspace === "string" && body.workspace !== name) {
|
|
1126
|
+
return { kind: "foreign", workspace: body.workspace };
|
|
1127
|
+
}
|
|
1128
|
+
if (body.ok !== true || body.pid !== pid) return { kind: "silent" };
|
|
1129
|
+
return { kind: "healthy" };
|
|
1130
|
+
} catch {
|
|
1131
|
+
return { kind: "silent" };
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async function bridgeStdioToWorkspace(name, root) {
|
|
1135
|
+
const resolved = await resolveWorkspaceDaemon(name, root);
|
|
1136
|
+
if ("error" in resolved) {
|
|
1137
|
+
fail(EXIT.DAEMON_DOWN, resolved.error);
|
|
1138
|
+
}
|
|
1139
|
+
const host = new StdioBridgeTransport(process.stdin, process.stdout);
|
|
1140
|
+
const daemon = new StreamableHTTPClientTransport2(new URL(resolved.url), {
|
|
1141
|
+
requestInit: { headers: { Authorization: `Bearer ${resolved.token}` } }
|
|
1142
|
+
});
|
|
1143
|
+
try {
|
|
1144
|
+
await relay(host, daemon);
|
|
1145
|
+
} finally {
|
|
1146
|
+
await daemon.close().catch(() => {
|
|
1147
|
+
});
|
|
1148
|
+
await host.close().catch(() => {
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function relay(host, daemon) {
|
|
1153
|
+
await new Promise((resolve3) => {
|
|
1154
|
+
let settled = false;
|
|
1155
|
+
let closing = false;
|
|
1156
|
+
const settle = () => {
|
|
1157
|
+
if (settled) return;
|
|
1158
|
+
settled = true;
|
|
1159
|
+
closing = true;
|
|
1160
|
+
resolve3();
|
|
1161
|
+
};
|
|
1162
|
+
const closeDaemon = () => {
|
|
1163
|
+
closing = true;
|
|
1164
|
+
void daemon.close().catch(() => {
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
1167
|
+
host.onmessage = (message) => {
|
|
1168
|
+
void daemon.send(message).catch(closeDaemon);
|
|
1169
|
+
};
|
|
1170
|
+
host.onclose = () => {
|
|
1171
|
+
closeDaemon();
|
|
1172
|
+
settle();
|
|
1173
|
+
};
|
|
1174
|
+
host.onerror = () => {
|
|
1175
|
+
closeDaemon();
|
|
1176
|
+
settle();
|
|
1177
|
+
};
|
|
1178
|
+
daemon.onmessage = (message) => {
|
|
1179
|
+
void host.send(message).catch(() => {
|
|
1180
|
+
});
|
|
1181
|
+
};
|
|
1182
|
+
daemon.onclose = () => {
|
|
1183
|
+
void host.close().catch(() => {
|
|
1184
|
+
});
|
|
1185
|
+
settle();
|
|
1186
|
+
};
|
|
1187
|
+
daemon.onerror = (error2) => {
|
|
1188
|
+
if (closing) return;
|
|
1189
|
+
process.stderr.write(
|
|
1190
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
1191
|
+
`
|
|
1192
|
+
);
|
|
1193
|
+
};
|
|
1194
|
+
const failStart = (error2) => {
|
|
1195
|
+
process.stderr.write(
|
|
1196
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
1197
|
+
`
|
|
1198
|
+
);
|
|
1199
|
+
host.onerror?.(error2);
|
|
1200
|
+
closeDaemon();
|
|
1201
|
+
settle();
|
|
1202
|
+
};
|
|
1203
|
+
void daemon.start().catch(failStart);
|
|
1204
|
+
void host.start().catch(failStart);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
function toError(value) {
|
|
1208
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1209
|
+
}
|
|
1210
|
+
var HEALTH_PROBE_TIMEOUT_MS, StdioBridgeTransport;
|
|
1211
|
+
var init_workspace_bridge = __esm({
|
|
1212
|
+
"src/workspace-bridge.ts"() {
|
|
1213
|
+
"use strict";
|
|
1214
|
+
init_output();
|
|
1215
|
+
HEALTH_PROBE_TIMEOUT_MS = 1500;
|
|
1216
|
+
StdioBridgeTransport = class {
|
|
1217
|
+
constructor(stdin, stdout) {
|
|
1218
|
+
this.stdin = stdin;
|
|
1219
|
+
this.stdout = stdout;
|
|
1220
|
+
}
|
|
1221
|
+
stdin;
|
|
1222
|
+
stdout;
|
|
1223
|
+
onclose;
|
|
1224
|
+
onerror;
|
|
1225
|
+
onmessage;
|
|
1226
|
+
buffer = new ReadBuffer();
|
|
1227
|
+
started = false;
|
|
1228
|
+
closed = false;
|
|
1229
|
+
async start() {
|
|
1230
|
+
if (this.started) throw new Error("the stdio bridge transport is already started");
|
|
1231
|
+
this.started = true;
|
|
1232
|
+
this.stdin.on("data", this.onData);
|
|
1233
|
+
this.stdin.on("error", this.onStdinFault);
|
|
1234
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
1235
|
+
this.stdout.on("error", this.onStdoutFault);
|
|
1236
|
+
}
|
|
1237
|
+
send(message) {
|
|
1238
|
+
if (this.closed) return Promise.reject(new Error("the stdio bridge transport is closed"));
|
|
1239
|
+
return new Promise((resolve3, reject) => {
|
|
1240
|
+
if (this.stdout.write(serializeMessage(message))) {
|
|
1241
|
+
resolve3();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const onDrain = () => {
|
|
1245
|
+
this.stdout.off("error", onWriteError);
|
|
1246
|
+
resolve3();
|
|
1247
|
+
};
|
|
1248
|
+
const onWriteError = (error2) => {
|
|
1249
|
+
this.stdout.off("drain", onDrain);
|
|
1250
|
+
reject(error2);
|
|
1251
|
+
};
|
|
1252
|
+
this.stdout.once("drain", onDrain);
|
|
1253
|
+
this.stdout.once("error", onWriteError);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async close() {
|
|
1257
|
+
if (this.closed) return;
|
|
1258
|
+
this.closed = true;
|
|
1259
|
+
this.stdin.off("data", this.onData);
|
|
1260
|
+
this.stdin.off("error", this.onStdinFault);
|
|
1261
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
1262
|
+
this.stdout.off("error", this.onStdoutFault);
|
|
1263
|
+
this.buffer.clear();
|
|
1264
|
+
if (this.stdin.listenerCount("data") === 0) this.stdin.pause();
|
|
1265
|
+
this.onclose?.();
|
|
1266
|
+
}
|
|
1267
|
+
onData = (chunk) => {
|
|
1268
|
+
try {
|
|
1269
|
+
this.buffer.append(chunk);
|
|
1270
|
+
for (; ; ) {
|
|
1271
|
+
const message = this.buffer.readMessage();
|
|
1272
|
+
if (message === null) return;
|
|
1273
|
+
this.onmessage?.(message);
|
|
1274
|
+
}
|
|
1275
|
+
} catch (error2) {
|
|
1276
|
+
this.onerror?.(toError(error2));
|
|
1277
|
+
void this.close();
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
/** The host closed its end of the pipe — the session is over. */
|
|
1281
|
+
onStdinEnd = () => {
|
|
1282
|
+
void this.close();
|
|
1283
|
+
};
|
|
1284
|
+
onStdinFault = (error2) => {
|
|
1285
|
+
this.onerror?.(error2);
|
|
1286
|
+
void this.close();
|
|
1287
|
+
};
|
|
1288
|
+
onStdoutFault = (error2) => {
|
|
1289
|
+
this.onerror?.(error2);
|
|
1290
|
+
};
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
964
1295
|
// src/workspace-mcp.ts
|
|
965
1296
|
import { existsSync, readFileSync } from "fs";
|
|
966
|
-
import {
|
|
967
|
-
import { resolveAdapter } from "@noir-ai/adapters";
|
|
1297
|
+
import { mcpConfigPathFor, noirStdioArgs, TRANSPORT_KEYS } from "@noir-ai/adapters";
|
|
968
1298
|
import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
|
|
969
|
-
function mcpPathFor(root, host) {
|
|
970
|
-
return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
|
|
971
|
-
}
|
|
972
1299
|
function readJson(path) {
|
|
973
1300
|
if (!existsSync(path)) return null;
|
|
974
1301
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -1002,26 +1329,37 @@ function isNoirEmitted(mcpServers) {
|
|
|
1002
1329
|
if (typeof noir.command === "string") return true;
|
|
1003
1330
|
return noir.type === "http";
|
|
1004
1331
|
}
|
|
1005
|
-
function
|
|
1006
|
-
const
|
|
1332
|
+
function noirEntry(mcpServers) {
|
|
1333
|
+
const noir = mcpServers?.noir;
|
|
1334
|
+
if (typeof noir !== "object" || noir === null || Array.isArray(noir)) return {};
|
|
1335
|
+
return noir;
|
|
1336
|
+
}
|
|
1337
|
+
function writeNoirTransport(root, host, transport, opts) {
|
|
1338
|
+
const path = mcpConfigPathFor(host, root);
|
|
1007
1339
|
const existing = loadExisting(path, opts);
|
|
1340
|
+
const kept = Object.fromEntries(
|
|
1341
|
+
Object.entries(noirEntry(existing?.mcpServers)).filter(([key]) => !TRANSPORT_KEYS.has(key))
|
|
1342
|
+
);
|
|
1008
1343
|
const next = {
|
|
1009
1344
|
...existing ?? {},
|
|
1010
1345
|
mcpServers: {
|
|
1011
1346
|
...existing?.mcpServers ?? {},
|
|
1012
|
-
noir: {
|
|
1347
|
+
noir: { ...transport, ...kept }
|
|
1013
1348
|
}
|
|
1014
1349
|
};
|
|
1015
1350
|
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
1016
1351
|
`);
|
|
1017
1352
|
}
|
|
1353
|
+
function writeWorkspaceEntry(root, host, name, opts) {
|
|
1354
|
+
writeNoirTransport(
|
|
1355
|
+
root,
|
|
1356
|
+
host,
|
|
1357
|
+
{ command: resolveNoirCommand(), args: noirStdioArgs(name) },
|
|
1358
|
+
opts
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1018
1361
|
function writeStdioEntry(root, host, opts) {
|
|
1019
|
-
|
|
1020
|
-
const existing = loadExisting(path, opts);
|
|
1021
|
-
const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
|
|
1022
|
-
const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
|
|
1023
|
-
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
1024
|
-
`);
|
|
1362
|
+
writeNoirTransport(root, host, { command: resolveNoirCommand(), args: noirStdioArgs() }, opts);
|
|
1025
1363
|
}
|
|
1026
1364
|
var init_workspace_mcp = __esm({
|
|
1027
1365
|
"src/workspace-mcp.ts"() {
|
|
@@ -1037,7 +1375,7 @@ import {
|
|
|
1037
1375
|
ensureWorkspaceRegistry,
|
|
1038
1376
|
isValidWorkspaceName,
|
|
1039
1377
|
isWorkspaceMember,
|
|
1040
|
-
loadProjectInfo as
|
|
1378
|
+
loadProjectInfo as loadProjectInfo3,
|
|
1041
1379
|
readWorkspaceMarker as readWorkspaceMarker2,
|
|
1042
1380
|
readWorkspaceRegistry,
|
|
1043
1381
|
removeWorkspaceMember,
|
|
@@ -1048,20 +1386,20 @@ import {
|
|
|
1048
1386
|
import {
|
|
1049
1387
|
clearWorkspaceDaemonRecord,
|
|
1050
1388
|
ensureWorkspaceDaemonRunning,
|
|
1051
|
-
pidAlive as
|
|
1052
|
-
readWorkspaceDaemonRecord as
|
|
1389
|
+
pidAlive as pidAlive3,
|
|
1390
|
+
readWorkspaceDaemonRecord as readWorkspaceDaemonRecord3,
|
|
1053
1391
|
spawnDetachedWorkspaceDaemon
|
|
1054
1392
|
} from "@noir-ai/daemon";
|
|
1055
1393
|
function loadProjectOrFail(opts) {
|
|
1056
1394
|
try {
|
|
1057
|
-
return
|
|
1395
|
+
return loadProjectInfo3(process.cwd());
|
|
1058
1396
|
} catch {
|
|
1059
1397
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
1060
1398
|
}
|
|
1061
1399
|
}
|
|
1062
|
-
function finishJoin(project, name,
|
|
1400
|
+
function finishJoin(project, name, opts) {
|
|
1063
1401
|
writeWorkspaceMarker(project.root, name);
|
|
1064
|
-
|
|
1402
|
+
writeWorkspaceEntry(project.root, project.config.host, name, opts);
|
|
1065
1403
|
}
|
|
1066
1404
|
async function daemonStartWorkspace(opts) {
|
|
1067
1405
|
const project = loadProjectOrFail(opts);
|
|
@@ -1094,7 +1432,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
1094
1432
|
}
|
|
1095
1433
|
if (opts.detach === true) {
|
|
1096
1434
|
const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
|
|
1097
|
-
finishJoin(project, opts.name,
|
|
1435
|
+
finishJoin(project, opts.name, opts);
|
|
1098
1436
|
if (opts.json === true) {
|
|
1099
1437
|
process.stdout.write(
|
|
1100
1438
|
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
@@ -1113,7 +1451,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
1113
1451
|
project,
|
|
1114
1452
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
1115
1453
|
});
|
|
1116
|
-
finishJoin(project, opts.name,
|
|
1454
|
+
finishJoin(project, opts.name, opts);
|
|
1117
1455
|
if (opts.json === true) {
|
|
1118
1456
|
process.stdout.write(
|
|
1119
1457
|
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
|
|
@@ -1143,7 +1481,7 @@ async function daemonJoin(opts) {
|
|
|
1143
1481
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
1144
1482
|
});
|
|
1145
1483
|
upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
|
|
1146
|
-
finishJoin(project, opts.name,
|
|
1484
|
+
finishJoin(project, opts.name, opts);
|
|
1147
1485
|
if (opts.json === true) {
|
|
1148
1486
|
process.stdout.write(
|
|
1149
1487
|
`${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
|
|
@@ -1160,19 +1498,20 @@ async function workspaceList(opts) {
|
|
|
1160
1498
|
} catch {
|
|
1161
1499
|
names = [];
|
|
1162
1500
|
}
|
|
1163
|
-
const rows =
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1501
|
+
const rows = await Promise.all(
|
|
1502
|
+
names.map(async (n) => {
|
|
1503
|
+
const reg = readWorkspaceRegistry(n);
|
|
1504
|
+
const rec = readWorkspaceDaemonRecord3(n);
|
|
1505
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, n);
|
|
1506
|
+
const running = health?.kind === "healthy";
|
|
1507
|
+
return {
|
|
1508
|
+
name: n,
|
|
1509
|
+
members: reg?.members.length ?? 0,
|
|
1510
|
+
running,
|
|
1511
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
1512
|
+
};
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1176
1515
|
if (opts.json === true) {
|
|
1177
1516
|
process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
|
|
1178
1517
|
`);
|
|
@@ -1183,7 +1522,8 @@ async function workspaceList(opts) {
|
|
|
1183
1522
|
return;
|
|
1184
1523
|
}
|
|
1185
1524
|
for (const r of rows) {
|
|
1186
|
-
|
|
1525
|
+
const state = r.running ? ", running" : r.stale ? ", not running (stale record)" : "";
|
|
1526
|
+
log(`${r.name} \u2014 ${r.members} member(s)${state}`, opts);
|
|
1187
1527
|
}
|
|
1188
1528
|
}
|
|
1189
1529
|
async function workspaceStatus(opts) {
|
|
@@ -1195,23 +1535,23 @@ async function workspaceStatus(opts) {
|
|
|
1195
1535
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
1196
1536
|
}
|
|
1197
1537
|
const reg = readWorkspaceRegistry(name);
|
|
1198
|
-
const rec =
|
|
1199
|
-
const
|
|
1538
|
+
const rec = readWorkspaceDaemonRecord3(name);
|
|
1539
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, name);
|
|
1540
|
+
const running = health?.kind === "healthy";
|
|
1200
1541
|
const data = {
|
|
1201
1542
|
name,
|
|
1202
1543
|
members: reg?.members.map((m) => m.projectId) ?? [],
|
|
1203
1544
|
running,
|
|
1204
|
-
...rec ? { pid: rec.pid, port: rec.port } : {}
|
|
1545
|
+
...rec ? { pid: rec.pid, port: rec.port } : {},
|
|
1546
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
1205
1547
|
};
|
|
1206
1548
|
if (opts.json === true) {
|
|
1207
1549
|
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
1208
1550
|
`);
|
|
1209
1551
|
return;
|
|
1210
1552
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
opts
|
|
1214
|
-
);
|
|
1553
|
+
const state = running ? `running (pid ${rec?.pid}, port ${rec?.port})` : rec !== null ? `not running (stale record for pid ${rec.pid})` : "not running";
|
|
1554
|
+
log(`workspace ${name}: ${state}`, opts);
|
|
1215
1555
|
for (const m of data.members) log(` member: ${m}`, opts);
|
|
1216
1556
|
}
|
|
1217
1557
|
async function workspaceLeave(opts) {
|
|
@@ -1241,8 +1581,8 @@ async function workspaceStop(opts) {
|
|
|
1241
1581
|
if (!isValidWorkspaceName(name)) {
|
|
1242
1582
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
1243
1583
|
}
|
|
1244
|
-
const rec =
|
|
1245
|
-
if (rec === null || !
|
|
1584
|
+
const rec = readWorkspaceDaemonRecord3(name);
|
|
1585
|
+
if (rec === null || !pidAlive3(rec.pid)) {
|
|
1246
1586
|
if (opts.json === true) {
|
|
1247
1587
|
process.stdout.write(
|
|
1248
1588
|
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
@@ -1253,6 +1593,10 @@ async function workspaceStop(opts) {
|
|
|
1253
1593
|
info("workspace daemon is not running.", opts);
|
|
1254
1594
|
return;
|
|
1255
1595
|
}
|
|
1596
|
+
const health = await verifyWorkspaceDaemon(rec, name);
|
|
1597
|
+
if (health.kind !== "healthy") {
|
|
1598
|
+
fail(EXIT.ERROR, stopRefusal(name, rec, health), opts);
|
|
1599
|
+
}
|
|
1256
1600
|
try {
|
|
1257
1601
|
process.kill(rec.pid, "SIGTERM");
|
|
1258
1602
|
} catch {
|
|
@@ -1267,21 +1611,28 @@ async function workspaceStop(opts) {
|
|
|
1267
1611
|
}
|
|
1268
1612
|
info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
|
|
1269
1613
|
}
|
|
1614
|
+
function stopRefusal(name, rec, health) {
|
|
1615
|
+
if (health.kind === "foreign") {
|
|
1616
|
+
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.`;
|
|
1617
|
+
}
|
|
1618
|
+
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.`;
|
|
1619
|
+
}
|
|
1270
1620
|
var init_workspace = __esm({
|
|
1271
1621
|
"src/commands/workspace.ts"() {
|
|
1272
1622
|
"use strict";
|
|
1273
1623
|
init_output();
|
|
1624
|
+
init_workspace_bridge();
|
|
1274
1625
|
init_workspace_mcp();
|
|
1275
1626
|
}
|
|
1276
1627
|
});
|
|
1277
1628
|
|
|
1278
1629
|
// src/commands/daemon.ts
|
|
1279
|
-
import { loadProjectInfo as
|
|
1630
|
+
import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
|
|
1280
1631
|
import {
|
|
1281
1632
|
clearProjectDaemonRecord,
|
|
1282
1633
|
ensureDaemonRunning as ensureDaemonRunning2,
|
|
1283
|
-
pidAlive as
|
|
1284
|
-
readDaemonToken as
|
|
1634
|
+
pidAlive as pidAlive4,
|
|
1635
|
+
readDaemonToken as readDaemonToken3,
|
|
1285
1636
|
readProjectDaemonRecord as readProjectDaemonRecord2,
|
|
1286
1637
|
spawnDetachedDaemon
|
|
1287
1638
|
} from "@noir-ai/daemon";
|
|
@@ -1293,7 +1644,7 @@ function formatUptime(sec) {
|
|
|
1293
1644
|
}
|
|
1294
1645
|
function resolveCallerProjectId() {
|
|
1295
1646
|
try {
|
|
1296
|
-
return
|
|
1647
|
+
return loadProjectInfo4(process.cwd()).id;
|
|
1297
1648
|
} catch {
|
|
1298
1649
|
return void 0;
|
|
1299
1650
|
}
|
|
@@ -1325,7 +1676,7 @@ async function daemonStart(opts) {
|
|
|
1325
1676
|
}
|
|
1326
1677
|
let project;
|
|
1327
1678
|
try {
|
|
1328
|
-
project =
|
|
1679
|
+
project = loadProjectInfo4(process.cwd());
|
|
1329
1680
|
} catch {
|
|
1330
1681
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
1331
1682
|
}
|
|
@@ -1357,7 +1708,7 @@ async function daemonStart(opts) {
|
|
|
1357
1708
|
}
|
|
1358
1709
|
if (opts.detach === true) {
|
|
1359
1710
|
const existing = readProjectDaemonRecord2(project.id);
|
|
1360
|
-
if (existing &&
|
|
1711
|
+
if (existing && pidAlive4(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
|
|
1361
1712
|
if (opts.json === true) {
|
|
1362
1713
|
process.stdout.write(
|
|
1363
1714
|
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
@@ -1467,7 +1818,7 @@ async function daemonStop(opts) {
|
|
|
1467
1818
|
const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
|
|
1468
1819
|
let signalled = false;
|
|
1469
1820
|
let errMsg2;
|
|
1470
|
-
const healthy =
|
|
1821
|
+
const healthy = pidAlive4(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
|
|
1471
1822
|
if (healthy) {
|
|
1472
1823
|
try {
|
|
1473
1824
|
process.kill(rec.pid, "SIGTERM");
|
|
@@ -1509,7 +1860,7 @@ async function daemonStatus(opts) {
|
|
|
1509
1860
|
if (!rec) {
|
|
1510
1861
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
1511
1862
|
}
|
|
1512
|
-
if (!
|
|
1863
|
+
if (!pidAlive4(rec.pid)) {
|
|
1513
1864
|
clearProjectDaemonRecord(callerProject);
|
|
1514
1865
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
1515
1866
|
}
|
|
@@ -1571,14 +1922,14 @@ async function daemonToken(opts) {
|
|
|
1571
1922
|
opts
|
|
1572
1923
|
);
|
|
1573
1924
|
}
|
|
1574
|
-
if (!
|
|
1925
|
+
if (!pidAlive4(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
|
|
1575
1926
|
fail(
|
|
1576
1927
|
EXIT.DAEMON_DOWN,
|
|
1577
1928
|
"Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
|
|
1578
1929
|
opts
|
|
1579
1930
|
);
|
|
1580
1931
|
}
|
|
1581
|
-
const token =
|
|
1932
|
+
const token = readDaemonToken3(callerProject);
|
|
1582
1933
|
if (token === null) {
|
|
1583
1934
|
fail(
|
|
1584
1935
|
EXIT.DAEMON_DOWN,
|
|
@@ -1609,17 +1960,182 @@ var init_daemon = __esm({
|
|
|
1609
1960
|
}
|
|
1610
1961
|
});
|
|
1611
1962
|
|
|
1963
|
+
// src/hygiene-scan.ts
|
|
1964
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
1965
|
+
import { join } from "path";
|
|
1966
|
+
import { checkHygiene } from "@noir-ai/skills";
|
|
1967
|
+
function readDirEntries(dir) {
|
|
1968
|
+
try {
|
|
1969
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
1970
|
+
} catch {
|
|
1971
|
+
return [];
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
function isDirectory(path) {
|
|
1975
|
+
try {
|
|
1976
|
+
return statSync(path).isDirectory();
|
|
1977
|
+
} catch {
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function hygieneKindOf(path) {
|
|
1982
|
+
if (HYGIENE_MARKDOWN_EXTENSIONS.test(path)) return "markdown";
|
|
1983
|
+
if (HYGIENE_CODE_EXTENSIONS.test(path)) return "code";
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
function hasScannableLayout(root) {
|
|
1987
|
+
if (isDirectory(join(root, "docs")) || isDirectory(join(root, "scripts"))) return true;
|
|
1988
|
+
for (const entry of readDirEntries(join(root, "packages"))) {
|
|
1989
|
+
if (!entry.isDirectory()) continue;
|
|
1990
|
+
if (isDirectory(join(root, "packages", entry.name, "src"))) return true;
|
|
1991
|
+
if (isDirectory(join(root, "packages", entry.name, "test"))) return true;
|
|
1992
|
+
}
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
function addHygieneFile(rel, walk) {
|
|
1996
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
1997
|
+
if (name.startsWith(".") || HYGIENE_EXCLUDED_FILES.has(name)) return;
|
|
1998
|
+
const kind = hygieneKindOf(rel);
|
|
1999
|
+
if (kind === null) return;
|
|
2000
|
+
if (walk.files.length >= HYGIENE_MAX_FILES) {
|
|
2001
|
+
walk.truncated = true;
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
walk.files.push({ path: rel, kind });
|
|
2005
|
+
}
|
|
2006
|
+
function walkScannable(root, relDir, skip, walk) {
|
|
2007
|
+
if (walk.truncated) return;
|
|
2008
|
+
for (const entry of readDirEntries(join(root, relDir))) {
|
|
2009
|
+
if (walk.truncated) return;
|
|
2010
|
+
const rel = `${relDir}/${entry.name}`;
|
|
2011
|
+
if (entry.isDirectory()) {
|
|
2012
|
+
if (HYGIENE_SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || skip.has(rel)) {
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
walkScannable(root, rel, skip, walk);
|
|
2016
|
+
} else if (entry.isFile()) {
|
|
2017
|
+
addHygieneFile(rel, walk);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function collectHygieneFiles(root) {
|
|
2022
|
+
const walk = { files: [], truncated: false };
|
|
2023
|
+
for (const entry of readDirEntries(root)) {
|
|
2024
|
+
if (entry.isFile()) addHygieneFile(entry.name, walk);
|
|
2025
|
+
}
|
|
2026
|
+
walkScannable(root, "docs", HYGIENE_EXCLUDED_DIRS, walk);
|
|
2027
|
+
walkScannable(root, ".claude/skills", NO_SKIP_DIRS, walk);
|
|
2028
|
+
for (const entry of readDirEntries(join(root, "packages"))) {
|
|
2029
|
+
if (!entry.isDirectory()) continue;
|
|
2030
|
+
walkScannable(root, `packages/${entry.name}/src`, NO_SKIP_DIRS, walk);
|
|
2031
|
+
walkScannable(root, `packages/${entry.name}/test`, NO_SKIP_DIRS, walk);
|
|
2032
|
+
}
|
|
2033
|
+
walkScannable(root, "scripts", NO_SKIP_DIRS, walk);
|
|
2034
|
+
walk.files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2035
|
+
return walk;
|
|
2036
|
+
}
|
|
2037
|
+
function hygieneOmissions(result) {
|
|
2038
|
+
const notes = [];
|
|
2039
|
+
if (result.skipped > 0) {
|
|
2040
|
+
notes.push(
|
|
2041
|
+
`${result.skipped} file${result.skipped === 1 ? "" : "s"} over ${HYGIENE_MAX_FILE_BYTES / 1024} KiB skipped`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
if (result.truncated) notes.push(`stopped at ${HYGIENE_MAX_FILES} files`);
|
|
2045
|
+
return notes.length > 0 ? ` (${notes.join("; ")})` : "";
|
|
2046
|
+
}
|
|
2047
|
+
function hygieneCounts(result) {
|
|
2048
|
+
const omitted = hygieneOmissions(result);
|
|
2049
|
+
if (result.scanned === 0) {
|
|
2050
|
+
return `nothing to scan (no packages/*/src, packages/*/test, scripts/ or documents found)${omitted}`;
|
|
2051
|
+
}
|
|
2052
|
+
if (result.findings.length === 0) {
|
|
2053
|
+
return `clean \u2014 ${result.scanned} file${result.scanned === 1 ? "" : "s"} scanned${omitted}`;
|
|
2054
|
+
}
|
|
2055
|
+
const files = new Set(result.findings.map((f) => f.path)).size;
|
|
2056
|
+
return `${result.fail} fail, ${result.warn} warn in ${files} file${files === 1 ? "" : "s"}${omitted}`;
|
|
2057
|
+
}
|
|
2058
|
+
function hygieneDetail(result) {
|
|
2059
|
+
const counts = hygieneCounts(result);
|
|
2060
|
+
if (result.scanned === 0 || result.findings.length === 0) return counts;
|
|
2061
|
+
const named = result.findings.slice(0, HYGIENE_FINDING_CAP);
|
|
2062
|
+
const hidden = result.findings.length - named.length;
|
|
2063
|
+
const where = named.map((f) => `${f.path}:${f.line} ${f.id}`).join("; ");
|
|
2064
|
+
return `${counts} \u2014 ${where}${hidden > 0 ? ` (+${hidden} more)` : ""}`;
|
|
2065
|
+
}
|
|
2066
|
+
function scanOutputHygiene(root) {
|
|
2067
|
+
const empty = {
|
|
2068
|
+
scanned: 0,
|
|
2069
|
+
skipped: 0,
|
|
2070
|
+
truncated: false,
|
|
2071
|
+
findings: [],
|
|
2072
|
+
fail: 0,
|
|
2073
|
+
warn: 0
|
|
2074
|
+
};
|
|
2075
|
+
if (!hasScannableLayout(root)) return empty;
|
|
2076
|
+
const walk = collectHygieneFiles(root);
|
|
2077
|
+
const findings = [];
|
|
2078
|
+
let scanned = 0;
|
|
2079
|
+
let skipped = 0;
|
|
2080
|
+
for (const file of walk.files) {
|
|
2081
|
+
const abs = join(root, file.path);
|
|
2082
|
+
try {
|
|
2083
|
+
if (statSync(abs).size > HYGIENE_MAX_FILE_BYTES) {
|
|
2084
|
+
skipped++;
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
const text = readFileSync2(abs, "utf8");
|
|
2088
|
+
scanned++;
|
|
2089
|
+
for (const found of checkHygiene(text, file.kind)) {
|
|
2090
|
+
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
2091
|
+
}
|
|
2092
|
+
} catch {
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
findings.sort((a, b) => {
|
|
2096
|
+
if (a.tier !== b.tier) return a.tier === "fail" ? -1 : 1;
|
|
2097
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
2098
|
+
return a.line - b.line;
|
|
2099
|
+
});
|
|
2100
|
+
const fail2 = findings.filter((f) => f.tier === "fail").length;
|
|
2101
|
+
return {
|
|
2102
|
+
scanned,
|
|
2103
|
+
skipped,
|
|
2104
|
+
truncated: walk.truncated,
|
|
2105
|
+
findings,
|
|
2106
|
+
fail: fail2,
|
|
2107
|
+
warn: findings.length - fail2
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
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;
|
|
2111
|
+
var init_hygiene_scan = __esm({
|
|
2112
|
+
"src/hygiene-scan.ts"() {
|
|
2113
|
+
"use strict";
|
|
2114
|
+
HYGIENE_FINDING_CAP = 5;
|
|
2115
|
+
HYGIENE_MAX_FILE_BYTES = 512 * 1024;
|
|
2116
|
+
HYGIENE_MAX_FILES = 2e3;
|
|
2117
|
+
HYGIENE_CODE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|sh)$/i;
|
|
2118
|
+
HYGIENE_MARKDOWN_EXTENSIONS = /\.(?:md|mdx)$/i;
|
|
2119
|
+
HYGIENE_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
|
|
2120
|
+
HYGIENE_EXCLUDED_DIRS = /* @__PURE__ */ new Set(["docs/internal", "docs/decisions", "docs/roadmap"]);
|
|
2121
|
+
HYGIENE_EXCLUDED_FILES = /* @__PURE__ */ new Set(["CHANGELOG.md"]);
|
|
2122
|
+
NO_SKIP_DIRS = /* @__PURE__ */ new Set();
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
|
|
1612
2126
|
// src/commands/doctor.ts
|
|
1613
2127
|
import { spawnSync } from "child_process";
|
|
1614
|
-
import { existsSync as existsSync2, readdirSync as
|
|
2128
|
+
import { existsSync as existsSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
1615
2129
|
import { dirname, join as join2, relative } from "path";
|
|
1616
2130
|
import { fileURLToPath } from "url";
|
|
1617
|
-
import {
|
|
2131
|
+
import { mcpConfigPathFor as mcpConfigPathFor2, resolveAdapter } from "@noir-ai/adapters";
|
|
1618
2132
|
import {
|
|
1619
2133
|
detectActiveMethod,
|
|
2134
|
+
ensureOwnerOnly,
|
|
2135
|
+
ensureOwnerOnlyDir,
|
|
1620
2136
|
latestVersionFromCache,
|
|
1621
2137
|
loadNoirEnv,
|
|
1622
|
-
loadProjectInfo as
|
|
2138
|
+
loadProjectInfo as loadProjectInfo5,
|
|
1623
2139
|
NOIR_VERSION as NOIR_VERSION2,
|
|
1624
2140
|
paths,
|
|
1625
2141
|
readInstallRecord,
|
|
@@ -1632,7 +2148,7 @@ import {
|
|
|
1632
2148
|
readScaffoldVersion,
|
|
1633
2149
|
render
|
|
1634
2150
|
} from "@noir-ai/create";
|
|
1635
|
-
import { pidAlive as
|
|
2151
|
+
import { pidAlive as pidAlive5, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
|
|
1636
2152
|
import { resolveModelConfig } from "@noir-ai/model";
|
|
1637
2153
|
async function probeOnnx() {
|
|
1638
2154
|
try {
|
|
@@ -1648,7 +2164,7 @@ function describeOnnx(o) {
|
|
|
1648
2164
|
if (/Cannot find (module|package)/i.test(o.reason)) {
|
|
1649
2165
|
return "not resolvable from CLI probe (best-effort)";
|
|
1650
2166
|
}
|
|
1651
|
-
const r = o.reason
|
|
2167
|
+
const r = truncateToWidth(o.reason, 60);
|
|
1652
2168
|
return `probe failed: ${r}`;
|
|
1653
2169
|
}
|
|
1654
2170
|
function fmtUptime(sec) {
|
|
@@ -1680,7 +2196,7 @@ function checkConfig(root) {
|
|
|
1680
2196
|
};
|
|
1681
2197
|
}
|
|
1682
2198
|
try {
|
|
1683
|
-
const project =
|
|
2199
|
+
const project = loadProjectInfo5(root);
|
|
1684
2200
|
return {
|
|
1685
2201
|
project,
|
|
1686
2202
|
result: {
|
|
@@ -1714,7 +2230,7 @@ async function checkDaemon(checks, project) {
|
|
|
1714
2230
|
});
|
|
1715
2231
|
return;
|
|
1716
2232
|
}
|
|
1717
|
-
if (!
|
|
2233
|
+
if (!pidAlive5(rec.pid)) {
|
|
1718
2234
|
checks.push({
|
|
1719
2235
|
name: "daemon",
|
|
1720
2236
|
status: "warn",
|
|
@@ -1797,11 +2313,21 @@ async function checkStore(checks, project, root) {
|
|
|
1797
2313
|
}
|
|
1798
2314
|
}
|
|
1799
2315
|
}
|
|
2316
|
+
function envModeCause(st) {
|
|
2317
|
+
return st.ctimeMs > st.mtimeMs ? "changed" : "legacy";
|
|
2318
|
+
}
|
|
2319
|
+
function octalMode(mode) {
|
|
2320
|
+
return mode.toString(8).padStart(4, "0");
|
|
2321
|
+
}
|
|
2322
|
+
function describeEnvMode(cause, mode) {
|
|
2323
|
+
const lead = `is ${octalMode(mode)} \u2014 accessible to other accounts; run \`noir sync\` (or \`chmod 600 .noir/.env\`) to make it owner-only.`;
|
|
2324
|
+
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.`;
|
|
2325
|
+
}
|
|
1800
2326
|
function checkNoirEnv(checks, root, loaded) {
|
|
1801
2327
|
const path = join2(paths.noirDir(root), ".env");
|
|
1802
2328
|
let st;
|
|
1803
2329
|
try {
|
|
1804
|
-
st =
|
|
2330
|
+
st = statSync2(path);
|
|
1805
2331
|
} catch {
|
|
1806
2332
|
return;
|
|
1807
2333
|
}
|
|
@@ -1819,8 +2345,8 @@ function checkNoirEnv(checks, root, loaded) {
|
|
|
1819
2345
|
tooOpen ? {
|
|
1820
2346
|
name: "noir-env",
|
|
1821
2347
|
status: "warn",
|
|
1822
|
-
detail: `${path}
|
|
1823
|
-
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode
|
|
2348
|
+
detail: `${path} ${describeEnvMode(envModeCause(st), mode)}`
|
|
2349
|
+
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${octalMode(mode)})` }
|
|
1824
2350
|
);
|
|
1825
2351
|
for (const [key, source] of Object.entries(loaded.sources)) {
|
|
1826
2352
|
if (source !== "file") continue;
|
|
@@ -1920,7 +2446,7 @@ function checkSeedDrift(checks, root) {
|
|
|
1920
2446
|
const seedPath = join2(root, ".noir", ".env.example");
|
|
1921
2447
|
let bytes;
|
|
1922
2448
|
try {
|
|
1923
|
-
bytes =
|
|
2449
|
+
bytes = readFileSync3(seedPath, "utf8");
|
|
1924
2450
|
} catch {
|
|
1925
2451
|
return;
|
|
1926
2452
|
}
|
|
@@ -1938,6 +2464,14 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
1938
2464
|
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1939
2465
|
return null;
|
|
1940
2466
|
}
|
|
2467
|
+
if (project.config.rules.enabled === false) {
|
|
2468
|
+
checks.push({
|
|
2469
|
+
name: "rules budget",
|
|
2470
|
+
status: "ok",
|
|
2471
|
+
detail: "disabled \u2014 rules.enabled is false in .noir/config.yml (RULES.md is not measured)"
|
|
2472
|
+
});
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
1941
2475
|
const rulesPath = paths.rulesMd(root);
|
|
1942
2476
|
if (!existsSync2(rulesPath)) {
|
|
1943
2477
|
checks.push({
|
|
@@ -1949,7 +2483,7 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
1949
2483
|
}
|
|
1950
2484
|
let content;
|
|
1951
2485
|
try {
|
|
1952
|
-
content =
|
|
2486
|
+
content = readFileSync3(rulesPath, "utf8");
|
|
1953
2487
|
} catch (e) {
|
|
1954
2488
|
checks.push({
|
|
1955
2489
|
name: "rules budget",
|
|
@@ -1993,7 +2527,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
1993
2527
|
return null;
|
|
1994
2528
|
}
|
|
1995
2529
|
const host = project.config.host;
|
|
1996
|
-
const adapter =
|
|
2530
|
+
const adapter = resolveAdapter(host);
|
|
1997
2531
|
const ectx = { root };
|
|
1998
2532
|
const expected = [];
|
|
1999
2533
|
const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
|
|
@@ -2002,7 +2536,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
2002
2536
|
}
|
|
2003
2537
|
if (host === "claude") expected.push("CLAUDE.md");
|
|
2004
2538
|
else if (host === "gemini") expected.push("GEMINI.md");
|
|
2005
|
-
expected.push(relOr(
|
|
2539
|
+
expected.push(relOr(mcpConfigPathFor2(host, root), root));
|
|
2006
2540
|
const missing = expected.filter((p) => !existsSync2(joinRoot(root, p)));
|
|
2007
2541
|
const status2 = missing.length === 0 ? "ok" : "warn";
|
|
2008
2542
|
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)`;
|
|
@@ -2029,6 +2563,15 @@ function checkNestedNoir(checks, root) {
|
|
|
2029
2563
|
});
|
|
2030
2564
|
return { detected, paths: found };
|
|
2031
2565
|
}
|
|
2566
|
+
function checkOutputHygiene(checks, root) {
|
|
2567
|
+
const result = scanOutputHygiene(root);
|
|
2568
|
+
checks.push({
|
|
2569
|
+
name: "output hygiene",
|
|
2570
|
+
status: result.fail > 0 ? "fail" : result.warn > 0 ? "warn" : "ok",
|
|
2571
|
+
detail: hygieneDetail(result)
|
|
2572
|
+
});
|
|
2573
|
+
return result;
|
|
2574
|
+
}
|
|
2032
2575
|
function collectDedupCandidates(root) {
|
|
2033
2576
|
const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
|
|
2034
2577
|
const out = [];
|
|
@@ -2037,7 +2580,7 @@ function collectDedupCandidates(root) {
|
|
|
2037
2580
|
if (!existsSync2(abs)) continue;
|
|
2038
2581
|
let text;
|
|
2039
2582
|
try {
|
|
2040
|
-
text =
|
|
2583
|
+
text = readFileSync3(abs, "utf8");
|
|
2041
2584
|
} catch {
|
|
2042
2585
|
continue;
|
|
2043
2586
|
}
|
|
@@ -2097,7 +2640,7 @@ function resolveWorkspacePackagesDir() {
|
|
|
2097
2640
|
const pkgPath = join2(here, "package.json");
|
|
2098
2641
|
if (existsSync2(pkgPath)) {
|
|
2099
2642
|
try {
|
|
2100
|
-
const pkg = JSON.parse(
|
|
2643
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
2101
2644
|
if (pkg.name === "@noir-ai/cli") {
|
|
2102
2645
|
const packagesDir = dirname(here);
|
|
2103
2646
|
if (existsSync2(join2(dirname(packagesDir), "pnpm-workspace.yaml"))) {
|
|
@@ -2145,7 +2688,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
2145
2688
|
}
|
|
2146
2689
|
let entries = [];
|
|
2147
2690
|
try {
|
|
2148
|
-
entries =
|
|
2691
|
+
entries = readdirSync3(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2149
2692
|
} catch (e) {
|
|
2150
2693
|
checks.push({
|
|
2151
2694
|
name: "publish",
|
|
@@ -2162,7 +2705,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
2162
2705
|
if (!existsSync2(pkgPath)) continue;
|
|
2163
2706
|
let pkg;
|
|
2164
2707
|
try {
|
|
2165
|
-
pkg = JSON.parse(
|
|
2708
|
+
pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
2166
2709
|
} catch (e) {
|
|
2167
2710
|
issues.push(`${name}/package.json: unparseable (${msg(e)})`);
|
|
2168
2711
|
checked++;
|
|
@@ -2241,6 +2784,17 @@ function renderHuman(payload, opts) {
|
|
|
2241
2784
|
["Check", "Status", "Detail"],
|
|
2242
2785
|
opts
|
|
2243
2786
|
);
|
|
2787
|
+
if (payload.fixes) {
|
|
2788
|
+
const healed = payload.fixes.filter((f) => f.outcome === "healed");
|
|
2789
|
+
if (healed.length > 0) {
|
|
2790
|
+
log(
|
|
2791
|
+
`fixed ${healed.length} permission${healed.length === 1 ? "" : "s"}: ${healed.map((f) => f.path).join(", ")}`,
|
|
2792
|
+
opts
|
|
2793
|
+
);
|
|
2794
|
+
} else {
|
|
2795
|
+
log("nothing to fix \u2014 permissions already owner-only", opts);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2244
2798
|
const { ok, warn: warnN, fail: fail2 } = payload.summary;
|
|
2245
2799
|
if (fail2 > 0) {
|
|
2246
2800
|
error(
|
|
@@ -2253,12 +2807,24 @@ function renderHuman(payload, opts) {
|
|
|
2253
2807
|
success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
|
|
2254
2808
|
}
|
|
2255
2809
|
}
|
|
2810
|
+
function fixOwnerOnlyPermissions(root, project) {
|
|
2811
|
+
const fixes = [
|
|
2812
|
+
{ path: ".noir/.env", outcome: ensureOwnerOnly(join2(paths.noirDir(root), ".env")) }
|
|
2813
|
+
];
|
|
2814
|
+
if (project) {
|
|
2815
|
+
const dbRel = `.noir/store/${project.id}.db`;
|
|
2816
|
+
fixes.push({ path: dbRel, outcome: ensureOwnerOnly(join2(root, dbRel)) });
|
|
2817
|
+
fixes.push({ path: ".noir/store", outcome: ensureOwnerOnlyDir(paths.storeDir(root)) });
|
|
2818
|
+
}
|
|
2819
|
+
return fixes;
|
|
2820
|
+
}
|
|
2256
2821
|
async function doctor(opts = {}) {
|
|
2257
2822
|
const root = process.cwd();
|
|
2258
2823
|
const checks = [];
|
|
2259
2824
|
await checkRuntime(checks);
|
|
2260
2825
|
const { project, result: configResult } = checkConfig(root);
|
|
2261
2826
|
checks.push(configResult);
|
|
2827
|
+
const fixes = opts.fix === true ? fixOwnerOnlyPermissions(root, project) : void 0;
|
|
2262
2828
|
await checkDaemon(checks, project);
|
|
2263
2829
|
const { vecOk } = await checkNativeDeps(checks);
|
|
2264
2830
|
await checkStore(checks, project, root);
|
|
@@ -2271,6 +2837,7 @@ async function doctor(opts = {}) {
|
|
|
2271
2837
|
const rules = checkRulesMdBudget(checks, root, project);
|
|
2272
2838
|
const host = checkHostArtifacts(checks, root, project);
|
|
2273
2839
|
checkNestedNoir(checks, root);
|
|
2840
|
+
checkOutputHygiene(checks, root);
|
|
2274
2841
|
if (opts.dedup === true) {
|
|
2275
2842
|
await checkSemanticDupDoctor(checks, root, project);
|
|
2276
2843
|
}
|
|
@@ -2294,7 +2861,8 @@ async function doctor(opts = {}) {
|
|
|
2294
2861
|
method: installCheck.method,
|
|
2295
2862
|
installedVersion: installCheck.installedVersion,
|
|
2296
2863
|
latestKnown: installCheck.latestKnown
|
|
2297
|
-
}
|
|
2864
|
+
},
|
|
2865
|
+
...fixes ? { fixes } : {}
|
|
2298
2866
|
};
|
|
2299
2867
|
if (opts.json === true) {
|
|
2300
2868
|
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
@@ -2314,26 +2882,28 @@ var init_doctor = __esm({
|
|
|
2314
2882
|
"src/commands/doctor.ts"() {
|
|
2315
2883
|
"use strict";
|
|
2316
2884
|
init_daemon_client();
|
|
2885
|
+
init_hygiene_scan();
|
|
2317
2886
|
init_output();
|
|
2318
2887
|
init_theme();
|
|
2888
|
+
init_width();
|
|
2319
2889
|
ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
|
|
2320
2890
|
SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2321
2891
|
}
|
|
2322
2892
|
});
|
|
2323
2893
|
|
|
2324
2894
|
// src/commands/env.ts
|
|
2325
|
-
import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as
|
|
2895
|
+
import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
|
|
2326
2896
|
import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
|
|
2327
2897
|
function captureAmbientEnv(env2 = process.env) {
|
|
2328
2898
|
ambientBaseline = { ...env2 };
|
|
2329
2899
|
}
|
|
2330
2900
|
function redactShape(value) {
|
|
2331
2901
|
if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
|
|
2332
|
-
return `${value
|
|
2902
|
+
return `${truncateToWidth(value, 3, "")}\u2026(${value.length})`;
|
|
2333
2903
|
}
|
|
2334
2904
|
function configApiKeyEnvNames(root) {
|
|
2335
2905
|
try {
|
|
2336
|
-
const project =
|
|
2906
|
+
const project = loadProjectInfo6(root);
|
|
2337
2907
|
const names = /* @__PURE__ */ new Set();
|
|
2338
2908
|
for (const provider of Object.values(resolveModelConfig2(project.config.model).providers)) {
|
|
2339
2909
|
if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
|
|
@@ -2402,6 +2972,7 @@ var init_env = __esm({
|
|
|
2402
2972
|
"src/commands/env.ts"() {
|
|
2403
2973
|
"use strict";
|
|
2404
2974
|
init_output();
|
|
2975
|
+
init_width();
|
|
2405
2976
|
CURATED_AMBIENT_KEYS = [
|
|
2406
2977
|
"CLICKUP_API_TOKEN",
|
|
2407
2978
|
"OPENAI_API_KEY",
|
|
@@ -2432,7 +3003,7 @@ import {
|
|
|
2432
3003
|
detectActiveMethod as detectActiveMethod2,
|
|
2433
3004
|
detectInstallMethods,
|
|
2434
3005
|
ensureShimExecutable,
|
|
2435
|
-
loadProjectInfo as
|
|
3006
|
+
loadProjectInfo as loadProjectInfo7,
|
|
2436
3007
|
MANAGED_NODE_VERSION,
|
|
2437
3008
|
NOIR_VERSION as NOIR_VERSION3,
|
|
2438
3009
|
noirHome,
|
|
@@ -2571,7 +3142,7 @@ async function install(opts = {}) {
|
|
|
2571
3142
|
const installedRecord = readInstallRecord2();
|
|
2572
3143
|
let minVersion = "1.6.0";
|
|
2573
3144
|
try {
|
|
2574
|
-
minVersion =
|
|
3145
|
+
minVersion = loadProjectInfo7(process.cwd()).config.update.minVersion;
|
|
2575
3146
|
} catch {
|
|
2576
3147
|
}
|
|
2577
3148
|
const plan = buildMigrationPlan({
|
|
@@ -2648,7 +3219,7 @@ import {
|
|
|
2648
3219
|
detectActiveMethod as detectActiveMethod3,
|
|
2649
3220
|
ensureShimExecutable as ensureShimExecutable2,
|
|
2650
3221
|
fetchLatestVersion,
|
|
2651
|
-
loadProjectInfo as
|
|
3222
|
+
loadProjectInfo as loadProjectInfo8,
|
|
2652
3223
|
readInstallRecord as readInstallRecord3,
|
|
2653
3224
|
readUpdateCache as readUpdateCache2,
|
|
2654
3225
|
runManagerCmd as runManagerCmd2,
|
|
@@ -2728,7 +3299,7 @@ async function update(opts = {}) {
|
|
|
2728
3299
|
fetchSpin.succeed(`Latest: ${latest}`);
|
|
2729
3300
|
let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
|
|
2730
3301
|
try {
|
|
2731
|
-
minVersion =
|
|
3302
|
+
minVersion = loadProjectInfo8(process.cwd()).config.update.minVersion;
|
|
2732
3303
|
} catch {
|
|
2733
3304
|
}
|
|
2734
3305
|
const target = buildUpdateTarget({
|
|
@@ -2878,7 +3449,7 @@ var init_update = __esm({
|
|
|
2878
3449
|
});
|
|
2879
3450
|
|
|
2880
3451
|
// src/commands/status.ts
|
|
2881
|
-
import { loadProjectInfo as
|
|
3452
|
+
import { loadProjectInfo as loadProjectInfo9, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
2882
3453
|
async function tryTool(caller, name, args) {
|
|
2883
3454
|
try {
|
|
2884
3455
|
return await caller.callTool(name, args);
|
|
@@ -2999,7 +3570,7 @@ function renderHuman2(p, opts) {
|
|
|
2999
3570
|
async function gatherStatusPayload(opts) {
|
|
3000
3571
|
let project;
|
|
3001
3572
|
try {
|
|
3002
|
-
project =
|
|
3573
|
+
project = loadProjectInfo9(process.cwd());
|
|
3003
3574
|
} catch {
|
|
3004
3575
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
3005
3576
|
}
|
|
@@ -3029,7 +3600,7 @@ async function status(opts) {
|
|
|
3029
3600
|
env: process.env,
|
|
3030
3601
|
configUpdate: (() => {
|
|
3031
3602
|
try {
|
|
3032
|
-
const info2 =
|
|
3603
|
+
const info2 = loadProjectInfo9(process.cwd());
|
|
3033
3604
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
3034
3605
|
} catch {
|
|
3035
3606
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -3058,7 +3629,7 @@ var init_status = __esm({
|
|
|
3058
3629
|
// src/commands/task.ts
|
|
3059
3630
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3060
3631
|
import { writeFileSync } from "fs";
|
|
3061
|
-
import { loadProjectInfo as
|
|
3632
|
+
import { loadProjectInfo as loadProjectInfo10, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
|
|
3062
3633
|
import {
|
|
3063
3634
|
PHASES,
|
|
3064
3635
|
TASK_CLASSES,
|
|
@@ -3071,7 +3642,7 @@ function skillFor(phase) {
|
|
|
3071
3642
|
}
|
|
3072
3643
|
function writeDoneArtifacts(opts, taskId) {
|
|
3073
3644
|
try {
|
|
3074
|
-
const project =
|
|
3645
|
+
const project = loadProjectInfo10(process.cwd());
|
|
3075
3646
|
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
3076
3647
|
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
3077
3648
|
const nextN = nextArtifactSequence(project.root, "adr");
|
|
@@ -3394,7 +3965,7 @@ function runCheck(cmd) {
|
|
|
3394
3965
|
async function taskVerify(opts) {
|
|
3395
3966
|
let project;
|
|
3396
3967
|
try {
|
|
3397
|
-
project =
|
|
3968
|
+
project = loadProjectInfo10(process.cwd());
|
|
3398
3969
|
} catch {
|
|
3399
3970
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
3400
3971
|
}
|
|
@@ -3500,7 +4071,7 @@ async function taskResearch(opts) {
|
|
|
3500
4071
|
return;
|
|
3501
4072
|
}
|
|
3502
4073
|
renderStatusRow(s, opts);
|
|
3503
|
-
info("
|
|
4074
|
+
info("this is a status view; record findings with `noir task research-record`", opts);
|
|
3504
4075
|
}
|
|
3505
4076
|
async function taskResearchRecord(opts) {
|
|
3506
4077
|
const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
|
|
@@ -3524,10 +4095,7 @@ async function taskResearchRecord(opts) {
|
|
|
3524
4095
|
`);
|
|
3525
4096
|
return;
|
|
3526
4097
|
}
|
|
3527
|
-
success(
|
|
3528
|
-
`research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
|
|
3529
|
-
opts
|
|
3530
|
-
);
|
|
4098
|
+
success(`research recorded \u2192 ${opts.type}: ${truncateToWidth(opts.text, 80)}`, opts);
|
|
3531
4099
|
}
|
|
3532
4100
|
async function taskDecompose(opts) {
|
|
3533
4101
|
const cap = opts.capability.trim();
|
|
@@ -3583,6 +4151,7 @@ var init_task = __esm({
|
|
|
3583
4151
|
init_daemon_client();
|
|
3584
4152
|
init_output();
|
|
3585
4153
|
init_theme();
|
|
4154
|
+
init_width();
|
|
3586
4155
|
PHASE_SKILL = {
|
|
3587
4156
|
// Skill curation: intake+clarify merged into brainstorming;
|
|
3588
4157
|
// execute → executing-plans; verify → verifying; document → wrap.
|
|
@@ -3604,9 +4173,9 @@ import { join as join4 } from "path";
|
|
|
3604
4173
|
import {
|
|
3605
4174
|
defaultHandoffBlock,
|
|
3606
4175
|
hostLaunchDirective,
|
|
3607
|
-
resolveAdapter as
|
|
4176
|
+
resolveAdapter as resolveAdapter2
|
|
3608
4177
|
} from "@noir-ai/adapters";
|
|
3609
|
-
import { loadProjectInfo as
|
|
4178
|
+
import { loadProjectInfo as loadProjectInfo11, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
|
|
3610
4179
|
function heading(name, id) {
|
|
3611
4180
|
return `# Noir handoff \u2014 ${name} (${id})`;
|
|
3612
4181
|
}
|
|
@@ -3702,7 +4271,7 @@ function toHostPayload(p) {
|
|
|
3702
4271
|
}
|
|
3703
4272
|
function resolveDirective(root, p) {
|
|
3704
4273
|
const ctx = { root };
|
|
3705
|
-
const adapter =
|
|
4274
|
+
const adapter = resolveAdapter2(p.host);
|
|
3706
4275
|
const hostPayload = toHostPayload(p);
|
|
3707
4276
|
if (typeof adapter.emitHandoff === "function") {
|
|
3708
4277
|
return adapter.emitHandoff(ctx, hostPayload);
|
|
@@ -3745,7 +4314,7 @@ async function probeOnly(opts) {
|
|
|
3745
4314
|
async function handoff(opts) {
|
|
3746
4315
|
let project;
|
|
3747
4316
|
try {
|
|
3748
|
-
project =
|
|
4317
|
+
project = loadProjectInfo11(process.cwd());
|
|
3749
4318
|
} catch {
|
|
3750
4319
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
3751
4320
|
}
|
|
@@ -3805,7 +4374,7 @@ import gradient from "gradient-string";
|
|
|
3805
4374
|
function renderBanner(opts = {}) {
|
|
3806
4375
|
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
3807
4376
|
const colorOn = opts.color !== false && useColor();
|
|
3808
|
-
if (width <
|
|
4377
|
+
if (width < BLOCK_MIN_WIDTH) {
|
|
3809
4378
|
return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
|
|
3810
4379
|
}
|
|
3811
4380
|
const block = NOIR_BLOCK.join("\n");
|
|
@@ -3821,11 +4390,12 @@ function shouldShowBanner(opts) {
|
|
|
3821
4390
|
if (v !== void 0 && v !== "") return false;
|
|
3822
4391
|
return true;
|
|
3823
4392
|
}
|
|
3824
|
-
var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE;
|
|
4393
|
+
var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE, BLOCK_WIDTH, BLOCK_MIN_WIDTH;
|
|
3825
4394
|
var init_banner = __esm({
|
|
3826
4395
|
"src/banner.ts"() {
|
|
3827
4396
|
"use strict";
|
|
3828
4397
|
init_theme();
|
|
4398
|
+
init_width();
|
|
3829
4399
|
NOIR_BLOCK = [
|
|
3830
4400
|
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
3831
4401
|
"\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",
|
|
@@ -3836,6 +4406,8 @@ var init_banner = __esm({
|
|
|
3836
4406
|
];
|
|
3837
4407
|
NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
3838
4408
|
NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
4409
|
+
BLOCK_WIDTH = NOIR_BLOCK.reduce((max, line) => Math.max(max, displayWidth(line)), 0);
|
|
4410
|
+
BLOCK_MIN_WIDTH = BLOCK_WIDTH + 17;
|
|
3839
4411
|
}
|
|
3840
4412
|
});
|
|
3841
4413
|
|
|
@@ -4135,14 +4707,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
|
4135
4707
|
import {
|
|
4136
4708
|
detectActiveMethod as detectActiveMethod4,
|
|
4137
4709
|
latestVersionFromCache as latestVersionFromCache2,
|
|
4138
|
-
loadProjectInfo as
|
|
4710
|
+
loadProjectInfo as loadProjectInfo12,
|
|
4139
4711
|
NOIR_VERSION as NOIR_VERSION5,
|
|
4140
4712
|
readInstallRecord as readInstallRecord4,
|
|
4141
4713
|
readUpdateCache as readUpdateCache3
|
|
4142
4714
|
} from "@noir-ai/core";
|
|
4143
4715
|
function tryProject() {
|
|
4144
4716
|
try {
|
|
4145
|
-
const info2 =
|
|
4717
|
+
const info2 = loadProjectInfo12(process.cwd());
|
|
4146
4718
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
4147
4719
|
} catch {
|
|
4148
4720
|
return null;
|
|
@@ -4305,7 +4877,7 @@ async function runMenu(opts, deps) {
|
|
|
4305
4877
|
env: process.env,
|
|
4306
4878
|
configUpdate: (() => {
|
|
4307
4879
|
try {
|
|
4308
|
-
const info2 =
|
|
4880
|
+
const info2 = loadProjectInfo12(process.cwd());
|
|
4309
4881
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
4310
4882
|
} catch {
|
|
4311
4883
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -4367,13 +4939,13 @@ var init_home = __esm({
|
|
|
4367
4939
|
});
|
|
4368
4940
|
|
|
4369
4941
|
// src/commands/memory.ts
|
|
4370
|
-
import { readFileSync as
|
|
4371
|
-
import { loadProjectInfo as
|
|
4942
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4943
|
+
import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
4372
4944
|
function resolveWorkspace() {
|
|
4373
4945
|
const routing = workspaceRoutingTarget(process.cwd());
|
|
4374
4946
|
if (routing === null) return null;
|
|
4375
4947
|
try {
|
|
4376
|
-
return { routing, project:
|
|
4948
|
+
return { routing, project: loadProjectInfo13(process.cwd()) };
|
|
4377
4949
|
} catch {
|
|
4378
4950
|
return null;
|
|
4379
4951
|
}
|
|
@@ -4585,14 +5157,14 @@ async function resolveCaptureContent(opts) {
|
|
|
4585
5157
|
if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
|
|
4586
5158
|
if (typeof opts.file === "string" && opts.file.length > 0) {
|
|
4587
5159
|
try {
|
|
4588
|
-
return
|
|
5160
|
+
return readFileSync4(opts.file, "utf8");
|
|
4589
5161
|
} catch {
|
|
4590
5162
|
fail(EXIT.USAGE, `memory capture could not read --file ${opts.file}`, opts);
|
|
4591
5163
|
}
|
|
4592
5164
|
}
|
|
4593
5165
|
if (!process.stdin.isTTY) {
|
|
4594
5166
|
try {
|
|
4595
|
-
return
|
|
5167
|
+
return readFileSync4(0, "utf8");
|
|
4596
5168
|
} catch {
|
|
4597
5169
|
fail(
|
|
4598
5170
|
EXIT.USAGE,
|
|
@@ -4786,7 +5358,7 @@ function describe(step, who) {
|
|
|
4786
5358
|
const m = {
|
|
4787
5359
|
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
4788
5360
|
bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
|
|
4789
|
-
gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
|
|
5361
|
+
gate: "re-run the full six-step gate (lint/build/typecheck/test/docs:validate/hygiene:gate)",
|
|
4790
5362
|
commit: "commit chore(release) + push develop",
|
|
4791
5363
|
"ci-develop": "wait for CI on develop \u2192 green",
|
|
4792
5364
|
"beta-tag": "pnpm release:tag \u2192 beta tag + push",
|
|
@@ -4831,11 +5403,14 @@ async function release(opts) {
|
|
|
4831
5403
|
});
|
|
4832
5404
|
success(`bump: \u2713 version ${version}`, opts);
|
|
4833
5405
|
info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
|
|
4834
|
-
execSync(
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
5406
|
+
execSync(
|
|
5407
|
+
"pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate && pnpm hygiene:gate",
|
|
5408
|
+
{
|
|
5409
|
+
encoding: "utf8",
|
|
5410
|
+
stdio: "inherit"
|
|
5411
|
+
}
|
|
5412
|
+
);
|
|
5413
|
+
success("gate: \u2713 all six gates green", opts);
|
|
4839
5414
|
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
4840
5415
|
spawnSync3("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
4841
5416
|
spawnSync3("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
@@ -5695,10 +6270,10 @@ var init_run_interrupt = __esm({
|
|
|
5695
6270
|
});
|
|
5696
6271
|
|
|
5697
6272
|
// src/run-profiles.ts
|
|
5698
|
-
import { isDeniedEnvKey, loadProjectInfo as
|
|
6273
|
+
import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
|
|
5699
6274
|
function loadRunConfig(root) {
|
|
5700
6275
|
try {
|
|
5701
|
-
return
|
|
6276
|
+
return loadProjectInfo14(root).config;
|
|
5702
6277
|
} catch {
|
|
5703
6278
|
return null;
|
|
5704
6279
|
}
|
|
@@ -5782,6 +6357,8 @@ var init_run_status = __esm({
|
|
|
5782
6357
|
"src/run-status.ts"() {
|
|
5783
6358
|
"use strict";
|
|
5784
6359
|
init_orchestrator();
|
|
6360
|
+
init_theme();
|
|
6361
|
+
init_width();
|
|
5785
6362
|
HOST_STDERR_TAIL_LINES = 20;
|
|
5786
6363
|
RunStatusLine = class {
|
|
5787
6364
|
host;
|
|
@@ -5864,6 +6441,7 @@ var init_run_status = __esm({
|
|
|
5864
6441
|
if (this.animated && this.sharesCursor && this.onRow) {
|
|
5865
6442
|
this.write("\n");
|
|
5866
6443
|
this.onRow = false;
|
|
6444
|
+
this.rendered = "";
|
|
5867
6445
|
}
|
|
5868
6446
|
this.stdoutAtRowStart = chunk.endsWith("\n");
|
|
5869
6447
|
}
|
|
@@ -5894,19 +6472,45 @@ var init_run_status = __esm({
|
|
|
5894
6472
|
return;
|
|
5895
6473
|
}
|
|
5896
6474
|
if (!this.onRow || !this.cursorFree()) return;
|
|
5897
|
-
this.write(
|
|
6475
|
+
this.write(this.erase(this.rendered));
|
|
5898
6476
|
this.rendered = "";
|
|
5899
6477
|
this.onRow = false;
|
|
5900
6478
|
}
|
|
5901
6479
|
/** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
|
|
5902
6480
|
draw() {
|
|
5903
6481
|
if (!this.animated || !this.cursorFree()) return;
|
|
5904
|
-
const text = this.render();
|
|
6482
|
+
const text = truncateToWidth(this.render(), terminalWidth());
|
|
5905
6483
|
if (text === this.rendered) return;
|
|
5906
|
-
this.write(
|
|
6484
|
+
this.write(`${this.erase(this.rendered)}${text}`);
|
|
5907
6485
|
this.rendered = text;
|
|
5908
6486
|
this.onRow = true;
|
|
5909
6487
|
}
|
|
6488
|
+
/**
|
|
6489
|
+
* The escape sequence that clears the status text currently on screen and
|
|
6490
|
+
* leaves the cursor at the start of the row it began on — where the next
|
|
6491
|
+
* render writes. An empty `previous` still clears the row, so a line that has
|
|
6492
|
+
* drawn nothing yet starts from a known-blank one.
|
|
6493
|
+
*
|
|
6494
|
+
* The extent is measured against the terminal width as it is NOW, not as it
|
|
6495
|
+
* was when the text was drawn. Nothing this line writes wraps, but text that
|
|
6496
|
+
* fitted a wider terminal does not stop existing when the window narrows: the
|
|
6497
|
+
* terminal folds it onto rows below, and clearing only the row the cursor
|
|
6498
|
+
* ends on would leave the rest above as residue. So the sequence walks back
|
|
6499
|
+
* up over every row the text occupies. That folding is the behaviour of the
|
|
6500
|
+
* common terminals (iTerm2, Terminal.app, GNOME Terminal and recent xterm);
|
|
6501
|
+
* one that clips the row instead has no continuation to clear.
|
|
6502
|
+
*
|
|
6503
|
+
* The caller must have forgotten the text once its row was abandoned —
|
|
6504
|
+
* `rendered` is only meaningful while `onRow` is true.
|
|
6505
|
+
*/
|
|
6506
|
+
erase(previous) {
|
|
6507
|
+
const rows = Math.max(1, Math.ceil(displayWidth(previous) / terminalWidth()));
|
|
6508
|
+
let sequence = "\r\x1B[K";
|
|
6509
|
+
for (let row = 1; row < rows; row++) {
|
|
6510
|
+
sequence += "\x1B[1A\r\x1B[K";
|
|
6511
|
+
}
|
|
6512
|
+
return sequence;
|
|
6513
|
+
}
|
|
5910
6514
|
/**
|
|
5911
6515
|
* Whether the cursor is at the start of a row nobody else is using. On stdout
|
|
5912
6516
|
* that is not this terminal there is no shared cursor to disturb; when it is,
|
|
@@ -6303,7 +6907,7 @@ var init_conflict = __esm({
|
|
|
6303
6907
|
});
|
|
6304
6908
|
|
|
6305
6909
|
// src/dedup-write.ts
|
|
6306
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
6910
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
6307
6911
|
import { join as join6 } from "path";
|
|
6308
6912
|
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
6309
6913
|
async function checkWritePathDedup(root, res, opts) {
|
|
@@ -6318,7 +6922,7 @@ async function checkWritePathDedup(root, res, opts) {
|
|
|
6318
6922
|
const proposedAbs = join6(root, proposedRel);
|
|
6319
6923
|
let proposedText;
|
|
6320
6924
|
try {
|
|
6321
|
-
proposedText =
|
|
6925
|
+
proposedText = readFileSync5(proposedAbs, "utf8");
|
|
6322
6926
|
} catch {
|
|
6323
6927
|
continue;
|
|
6324
6928
|
}
|
|
@@ -6458,7 +7062,7 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
|
|
|
6458
7062
|
return "replace";
|
|
6459
7063
|
} else if (choice === "mirror") {
|
|
6460
7064
|
try {
|
|
6461
|
-
const existing =
|
|
7065
|
+
const existing = readFileSync5(join6(root, matchedRel), "utf8");
|
|
6462
7066
|
writeFileSync5(
|
|
6463
7067
|
join6(root, matchedRel),
|
|
6464
7068
|
`${existing}
|
|
@@ -6482,7 +7086,7 @@ function cachePath(root) {
|
|
|
6482
7086
|
}
|
|
6483
7087
|
function loadCache(root) {
|
|
6484
7088
|
try {
|
|
6485
|
-
const raw =
|
|
7089
|
+
const raw = readFileSync5(cachePath(root), "utf8");
|
|
6486
7090
|
const parsed = JSON.parse(raw);
|
|
6487
7091
|
if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
|
|
6488
7092
|
if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
|
|
@@ -6514,7 +7118,7 @@ function makeCachedEmbed(cache, embed) {
|
|
|
6514
7118
|
}
|
|
6515
7119
|
function safeRead(abs) {
|
|
6516
7120
|
try {
|
|
6517
|
-
return
|
|
7121
|
+
return readFileSync5(abs, "utf8");
|
|
6518
7122
|
} catch {
|
|
6519
7123
|
return void 0;
|
|
6520
7124
|
}
|
|
@@ -6553,8 +7157,8 @@ var init_dedup_write = __esm({
|
|
|
6553
7157
|
|
|
6554
7158
|
// src/init.ts
|
|
6555
7159
|
import { existsSync as existsSync4 } from "fs";
|
|
6556
|
-
import { resolveAdapter as
|
|
6557
|
-
import { loadProjectInfo as
|
|
7160
|
+
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
7161
|
+
import { loadProjectInfo as loadProjectInfo15, paths as paths3 } from "@noir-ai/core";
|
|
6558
7162
|
import { scaffold } from "@noir-ai/create";
|
|
6559
7163
|
import {
|
|
6560
7164
|
emitSkillsToDir
|
|
@@ -6581,13 +7185,14 @@ async function init(root, opts) {
|
|
|
6581
7185
|
reportPlannedWrites(res);
|
|
6582
7186
|
return res;
|
|
6583
7187
|
}
|
|
7188
|
+
reportEnvHeal(res);
|
|
6584
7189
|
if (res.noop) return res;
|
|
6585
7190
|
const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
|
|
6586
7191
|
res.skillConflicts = skillSummary?.conflicts ?? [];
|
|
6587
7192
|
res.preservedSkills = skillSummary?.preserved ?? [];
|
|
6588
7193
|
let projectInfo;
|
|
6589
7194
|
try {
|
|
6590
|
-
projectInfo =
|
|
7195
|
+
projectInfo = loadProjectInfo15(root);
|
|
6591
7196
|
} catch {
|
|
6592
7197
|
projectInfo = void 0;
|
|
6593
7198
|
}
|
|
@@ -6618,7 +7223,7 @@ function resolveInitHost(root, opts) {
|
|
|
6618
7223
|
if (opts.host !== void 0) return opts.host;
|
|
6619
7224
|
if (!existsSync4(paths3.config(root))) return "claude";
|
|
6620
7225
|
try {
|
|
6621
|
-
return
|
|
7226
|
+
return loadProjectInfo15(root).config.host;
|
|
6622
7227
|
} catch {
|
|
6623
7228
|
process.stderr.write(
|
|
6624
7229
|
`Could not read the configured host from ${paths3.config(root)}; using 'claude'.
|
|
@@ -6649,8 +7254,13 @@ function reportPlannedWrites(res) {
|
|
|
6649
7254
|
for (const p of res.identical) log(` ${p}`);
|
|
6650
7255
|
}
|
|
6651
7256
|
}
|
|
7257
|
+
function reportEnvHeal(res) {
|
|
7258
|
+
if (res.envMode === "healed") {
|
|
7259
|
+
process.stderr.write("Tightened .noir/.env to 0600 (it was readable by group or others).\n");
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
6652
7262
|
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
6653
|
-
const adapter =
|
|
7263
|
+
const adapter = resolveAdapter3(host);
|
|
6654
7264
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
6655
7265
|
if (skillsDir === void 0) {
|
|
6656
7266
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -6714,8 +7324,8 @@ var init_init = __esm({
|
|
|
6714
7324
|
});
|
|
6715
7325
|
|
|
6716
7326
|
// src/commands/skills.ts
|
|
6717
|
-
import { resolveAdapter as
|
|
6718
|
-
import { loadProjectInfo as
|
|
7327
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
7328
|
+
import { loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
|
|
6719
7329
|
import {
|
|
6720
7330
|
buildRegistry,
|
|
6721
7331
|
discoverAll,
|
|
@@ -6733,13 +7343,6 @@ function toRow(s, kind = "builtin") {
|
|
|
6733
7343
|
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
6734
7344
|
return { name: s.name, category, description, kind, status: status2 };
|
|
6735
7345
|
}
|
|
6736
|
-
function truncate(text, max) {
|
|
6737
|
-
if (text.length <= max) return text;
|
|
6738
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
6739
|
-
const boundary = slice.lastIndexOf(" ");
|
|
6740
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
6741
|
-
return `${head.trimEnd()}\u2026`;
|
|
6742
|
-
}
|
|
6743
7346
|
async function skillsList(opts) {
|
|
6744
7347
|
let builtins;
|
|
6745
7348
|
let integrations;
|
|
@@ -6773,7 +7376,9 @@ async function skillsList(opts) {
|
|
|
6773
7376
|
Kind: r.kind,
|
|
6774
7377
|
Category: r.category,
|
|
6775
7378
|
Status: r.status,
|
|
6776
|
-
|
|
7379
|
+
// Display-only cut for the dense table cell; the `--json` payload always
|
|
7380
|
+
// carries the description whole (search snippets must never be truncated).
|
|
7381
|
+
Description: truncateToWidth(r.description, 80)
|
|
6777
7382
|
})),
|
|
6778
7383
|
["Skill", "Kind", "Category", "Status", "Description"],
|
|
6779
7384
|
opts
|
|
@@ -6783,11 +7388,11 @@ async function skillsSync(opts) {
|
|
|
6783
7388
|
const root = process.cwd();
|
|
6784
7389
|
let project;
|
|
6785
7390
|
try {
|
|
6786
|
-
project =
|
|
7391
|
+
project = loadProjectInfo16(root);
|
|
6787
7392
|
} catch {
|
|
6788
7393
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6789
7394
|
}
|
|
6790
|
-
const adapter =
|
|
7395
|
+
const adapter = resolveAdapter4(project.config.host);
|
|
6791
7396
|
if (!adapter.skillsDir) {
|
|
6792
7397
|
const data2 = {
|
|
6793
7398
|
emitted: [],
|
|
@@ -6849,7 +7454,7 @@ async function skillsLint(opts) {
|
|
|
6849
7454
|
);
|
|
6850
7455
|
}
|
|
6851
7456
|
const errored = skills.filter((s) => s.errors.length > 0);
|
|
6852
|
-
const warned = skills.filter((s) => s.warnings.length > 0
|
|
7457
|
+
const warned = skills.filter((s) => s.warnings.length > 0);
|
|
6853
7458
|
const data = { count: skills.length, errored: errored.length, skills };
|
|
6854
7459
|
if (errored.length > 0) {
|
|
6855
7460
|
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
@@ -6927,6 +7532,7 @@ var init_skills = __esm({
|
|
|
6927
7532
|
"use strict";
|
|
6928
7533
|
init_init();
|
|
6929
7534
|
init_output();
|
|
7535
|
+
init_width();
|
|
6930
7536
|
CATEGORY = {
|
|
6931
7537
|
"noir-brainstorming": "discovery",
|
|
6932
7538
|
"noir-sync": "discovery",
|
|
@@ -6953,17 +7559,22 @@ var init_skills = __esm({
|
|
|
6953
7559
|
"noir-backend": "domain",
|
|
6954
7560
|
"noir-doctor": "meta",
|
|
6955
7561
|
"noir-writing-skills": "meta",
|
|
6956
|
-
"noir-rules": "meta"
|
|
7562
|
+
"noir-rules": "meta",
|
|
7563
|
+
"noir-code-hygiene": "meta"
|
|
6957
7564
|
};
|
|
6958
7565
|
}
|
|
6959
7566
|
});
|
|
6960
7567
|
|
|
6961
7568
|
// src/serve.ts
|
|
6962
|
-
import { applyNoirEnv, loadProjectInfo as
|
|
7569
|
+
import { applyNoirEnv, loadProjectInfo as loadProjectInfo17 } from "@noir-ai/core";
|
|
6963
7570
|
import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
|
|
6964
7571
|
async function serve(opts) {
|
|
6965
7572
|
applyNoirEnv(process.cwd());
|
|
6966
|
-
|
|
7573
|
+
if (opts.workspace !== void 0) {
|
|
7574
|
+
await bridgeStdioToWorkspace(opts.workspace, process.cwd());
|
|
7575
|
+
return;
|
|
7576
|
+
}
|
|
7577
|
+
const project = loadProjectInfo17(process.cwd());
|
|
6967
7578
|
if (opts.stdio) {
|
|
6968
7579
|
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
6969
7580
|
return;
|
|
@@ -6989,6 +7600,7 @@ async function serve(opts) {
|
|
|
6989
7600
|
var init_serve = __esm({
|
|
6990
7601
|
"src/serve.ts"() {
|
|
6991
7602
|
"use strict";
|
|
7603
|
+
init_workspace_bridge();
|
|
6992
7604
|
}
|
|
6993
7605
|
});
|
|
6994
7606
|
|
|
@@ -6998,8 +7610,8 @@ __export(create_exports, {
|
|
|
6998
7610
|
create: () => create
|
|
6999
7611
|
});
|
|
7000
7612
|
import { resolve as resolve2 } from "path";
|
|
7001
|
-
import { resolveAdapter as
|
|
7002
|
-
import { loadProjectInfo as
|
|
7613
|
+
import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
|
|
7614
|
+
import { loadProjectInfo as loadProjectInfo18 } from "@noir-ai/core";
|
|
7003
7615
|
import { scaffold as scaffold2 } from "@noir-ai/create";
|
|
7004
7616
|
import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
|
|
7005
7617
|
async function create(dir, opts) {
|
|
@@ -7023,9 +7635,10 @@ async function create(dir, opts) {
|
|
|
7023
7635
|
reportPlannedWrites(res);
|
|
7024
7636
|
return res;
|
|
7025
7637
|
}
|
|
7638
|
+
reportEnvHeal(res);
|
|
7026
7639
|
if (res.noop) return res;
|
|
7027
7640
|
const host = res.host;
|
|
7028
|
-
const adapter =
|
|
7641
|
+
const adapter = resolveAdapter5(host);
|
|
7029
7642
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
7030
7643
|
if (skillsDir === void 0) {
|
|
7031
7644
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -7053,7 +7666,7 @@ async function create(dir, opts) {
|
|
|
7053
7666
|
}
|
|
7054
7667
|
let projectInfo;
|
|
7055
7668
|
try {
|
|
7056
|
-
projectInfo =
|
|
7669
|
+
projectInfo = loadProjectInfo18(root);
|
|
7057
7670
|
} catch {
|
|
7058
7671
|
projectInfo = void 0;
|
|
7059
7672
|
}
|
|
@@ -7080,8 +7693,8 @@ __export(sync_exports, {
|
|
|
7080
7693
|
sync: () => sync
|
|
7081
7694
|
});
|
|
7082
7695
|
import { existsSync as existsSync5 } from "fs";
|
|
7083
|
-
import { resolveAdapter as
|
|
7084
|
-
import { loadProjectInfo as
|
|
7696
|
+
import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
7697
|
+
import { loadProjectInfo as loadProjectInfo19, paths as paths4 } from "@noir-ai/core";
|
|
7085
7698
|
import { scaffold as scaffold3 } from "@noir-ai/create";
|
|
7086
7699
|
import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
|
|
7087
7700
|
async function sync(root, opts = {}) {
|
|
@@ -7090,7 +7703,7 @@ async function sync(root, opts = {}) {
|
|
|
7090
7703
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
7091
7704
|
let projectInfo;
|
|
7092
7705
|
try {
|
|
7093
|
-
projectInfo =
|
|
7706
|
+
projectInfo = loadProjectInfo19(root);
|
|
7094
7707
|
} catch {
|
|
7095
7708
|
projectInfo = void 0;
|
|
7096
7709
|
}
|
|
@@ -7109,10 +7722,11 @@ async function sync(root, opts = {}) {
|
|
|
7109
7722
|
reportPlannedWrites(res);
|
|
7110
7723
|
return res;
|
|
7111
7724
|
}
|
|
7725
|
+
reportEnvHeal(res);
|
|
7112
7726
|
if (res.written.length === 0 && res.identical.length > 0) {
|
|
7113
7727
|
process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
|
|
7114
7728
|
}
|
|
7115
|
-
const adapter =
|
|
7729
|
+
const adapter = resolveAdapter6(host);
|
|
7116
7730
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
7117
7731
|
if (skillsDir === void 0) {
|
|
7118
7732
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -7153,7 +7767,7 @@ function resolveSyncHost(root, opts) {
|
|
|
7153
7767
|
if (opts.host !== void 0) return opts.host;
|
|
7154
7768
|
if (!existsSync5(paths4.config(root))) return "claude";
|
|
7155
7769
|
try {
|
|
7156
|
-
return
|
|
7770
|
+
return loadProjectInfo19(root).config.host;
|
|
7157
7771
|
} catch {
|
|
7158
7772
|
return "claude";
|
|
7159
7773
|
}
|
|
@@ -7278,6 +7892,8 @@ function createProgram() {
|
|
|
7278
7892
|
const nonInteractive = opts.json === true || opts.input === false;
|
|
7279
7893
|
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
7280
7894
|
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
7895
|
+
if (opts.quiet === true) process.env.NOIR_QUIET = "1";
|
|
7896
|
+
else delete process.env.NOIR_QUIET;
|
|
7281
7897
|
emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
|
|
7282
7898
|
});
|
|
7283
7899
|
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(
|
|
@@ -7383,11 +7999,17 @@ function createProgram() {
|
|
|
7383
7999
|
}
|
|
7384
8000
|
);
|
|
7385
8001
|
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
7386
|
-
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").
|
|
7387
|
-
|
|
8002
|
+
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").option(
|
|
8003
|
+
"--workspace <name>",
|
|
8004
|
+
"serve a shared workspace over stdio, bridging to that workspace daemon"
|
|
8005
|
+
).action(async (opts) => {
|
|
8006
|
+
await serve({
|
|
8007
|
+
stdio: opts.stdio === true,
|
|
8008
|
+
...typeof opts.workspace === "string" ? { workspace: opts.workspace } : {}
|
|
8009
|
+
});
|
|
7388
8010
|
});
|
|
7389
8011
|
mcpCmd.action(() => {
|
|
7390
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
8012
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio] [--workspace <name>]");
|
|
7391
8013
|
});
|
|
7392
8014
|
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
7393
8015
|
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(
|
|
@@ -7488,10 +8110,16 @@ function createProgram() {
|
|
|
7488
8110
|
program2.command("doctor").description("environment + project health").option(
|
|
7489
8111
|
"--dedup",
|
|
7490
8112
|
"scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
|
|
7491
|
-
).action(async (...args) => {
|
|
8113
|
+
).option("--fix", "re-assert owner-only permissions on .noir/.env and the store DB + directory").action(async (...args) => {
|
|
7492
8114
|
const g = actionGlobals(args);
|
|
7493
|
-
const
|
|
7494
|
-
|
|
8115
|
+
const opts = trailingCmd(args).opts();
|
|
8116
|
+
const dedup = opts.dedup === true;
|
|
8117
|
+
const fix = opts.fix === true;
|
|
8118
|
+
await doctor({
|
|
8119
|
+
...toCliOptions(g),
|
|
8120
|
+
...dedup ? { dedup: true } : {},
|
|
8121
|
+
...fix ? { fix: true } : {}
|
|
8122
|
+
});
|
|
7495
8123
|
});
|
|
7496
8124
|
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
7497
8125
|
await status(toStatusOptions(actionGlobals(args)));
|
|
@@ -7684,7 +8312,7 @@ function createProgram() {
|
|
|
7684
8312
|
...check === void 0 || check.length === 0 ? {} : { check }
|
|
7685
8313
|
});
|
|
7686
8314
|
});
|
|
7687
|
-
taskGrp.command("research").description("
|
|
8315
|
+
taskGrp.command("research").description("show research status for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
7688
8316
|
const cmd = trailingCmd(args);
|
|
7689
8317
|
const g = cmd.optsWithGlobals();
|
|
7690
8318
|
const first = args[0];
|