@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
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
DEFAULT_UPDATE_CONFIG,
|
|
3
3
|
buildUpdateNotice,
|
|
4
4
|
runAsyncUpdateCheck
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CQU2INJH.js";
|
|
6
6
|
import {
|
|
7
7
|
EXIT,
|
|
8
8
|
NoirCliError,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
badge,
|
|
11
11
|
c,
|
|
12
12
|
definitionList,
|
|
13
|
+
displayWidth,
|
|
13
14
|
error,
|
|
14
15
|
fail,
|
|
15
16
|
failAndExit,
|
|
@@ -23,31 +24,33 @@ import {
|
|
|
23
24
|
spinner,
|
|
24
25
|
success,
|
|
25
26
|
table,
|
|
27
|
+
terminalWidth,
|
|
26
28
|
tip,
|
|
29
|
+
truncateToWidth,
|
|
27
30
|
useColor,
|
|
28
31
|
warn
|
|
29
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-KVJIPKVE.js";
|
|
30
33
|
|
|
31
34
|
// src/commands/status.ts
|
|
32
|
-
import { loadProjectInfo as
|
|
35
|
+
import { loadProjectInfo as loadProjectInfo15, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
|
|
33
36
|
|
|
34
37
|
// src/daemon-client.ts
|
|
35
|
-
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
38
|
+
import { Client, StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/client";
|
|
36
39
|
import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
37
40
|
import {
|
|
38
|
-
loadProjectInfo as
|
|
41
|
+
loadProjectInfo as loadProjectInfo14,
|
|
39
42
|
NOIR_VERSION as NOIR_VERSION4,
|
|
40
43
|
readWorkspaceMarker as readWorkspaceMarker2,
|
|
41
44
|
workspaceStoreDbPath
|
|
42
45
|
} from "@noir-ai/core";
|
|
43
46
|
import {
|
|
44
47
|
ensureDaemonRunning as ensureDaemonRunning3,
|
|
45
|
-
pidAlive as
|
|
46
|
-
readDaemonToken as
|
|
48
|
+
pidAlive as pidAlive5,
|
|
49
|
+
readDaemonToken as readDaemonToken3,
|
|
47
50
|
readProjectDaemonRecord as readProjectDaemonRecord3,
|
|
48
|
-
readWorkspaceDaemonRecord as
|
|
51
|
+
readWorkspaceDaemonRecord as readWorkspaceDaemonRecord3,
|
|
49
52
|
resolveGateConfig,
|
|
50
|
-
tokenPath
|
|
53
|
+
tokenPath as tokenPath2
|
|
51
54
|
} from "@noir-ai/daemon";
|
|
52
55
|
import { createMemoryEngine } from "@noir-ai/memory";
|
|
53
56
|
import { openStore } from "@noir-ai/store";
|
|
@@ -226,12 +229,12 @@ function renderStatus(data, opts) {
|
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
// src/commands/daemon.ts
|
|
229
|
-
import { loadProjectInfo as
|
|
232
|
+
import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
|
|
230
233
|
import {
|
|
231
234
|
clearProjectDaemonRecord,
|
|
232
235
|
ensureDaemonRunning,
|
|
233
|
-
pidAlive as
|
|
234
|
-
readDaemonToken,
|
|
236
|
+
pidAlive as pidAlive3,
|
|
237
|
+
readDaemonToken as readDaemonToken2,
|
|
235
238
|
readProjectDaemonRecord,
|
|
236
239
|
spawnDetachedDaemon
|
|
237
240
|
} from "@noir-ai/daemon";
|
|
@@ -243,7 +246,7 @@ import {
|
|
|
243
246
|
ensureWorkspaceRegistry,
|
|
244
247
|
isValidWorkspaceName,
|
|
245
248
|
isWorkspaceMember,
|
|
246
|
-
loadProjectInfo,
|
|
249
|
+
loadProjectInfo as loadProjectInfo2,
|
|
247
250
|
readWorkspaceMarker,
|
|
248
251
|
readWorkspaceRegistry,
|
|
249
252
|
removeWorkspaceMember,
|
|
@@ -254,19 +257,228 @@ import {
|
|
|
254
257
|
import {
|
|
255
258
|
clearWorkspaceDaemonRecord,
|
|
256
259
|
ensureWorkspaceDaemonRunning,
|
|
257
|
-
pidAlive,
|
|
258
|
-
readWorkspaceDaemonRecord,
|
|
260
|
+
pidAlive as pidAlive2,
|
|
261
|
+
readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
|
|
259
262
|
spawnDetachedWorkspaceDaemon
|
|
260
263
|
} from "@noir-ai/daemon";
|
|
261
264
|
|
|
265
|
+
// src/workspace-bridge.ts
|
|
266
|
+
import {
|
|
267
|
+
ReadBuffer,
|
|
268
|
+
StreamableHTTPClientTransport,
|
|
269
|
+
serializeMessage
|
|
270
|
+
} from "@modelcontextprotocol/client";
|
|
271
|
+
import { loadProjectInfo } from "@noir-ai/core";
|
|
272
|
+
import { pidAlive, readDaemonToken, readWorkspaceDaemonRecord, tokenPath } from "@noir-ai/daemon";
|
|
273
|
+
var HEALTH_PROBE_TIMEOUT_MS = 1500;
|
|
274
|
+
async function resolveWorkspaceDaemon(name, root) {
|
|
275
|
+
const record = readWorkspaceDaemonRecord(name);
|
|
276
|
+
if (record === null) {
|
|
277
|
+
return { error: `no daemon recorded for workspace ${name}` };
|
|
278
|
+
}
|
|
279
|
+
const health = await verifyWorkspaceDaemon(record, name);
|
|
280
|
+
if (health.kind === "foreign") {
|
|
281
|
+
return {
|
|
282
|
+
error: `workspace ${name} is recorded on port ${record.port}, but the daemon answering there serves workspace ${health.workspace}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (health.kind === "silent") {
|
|
286
|
+
return { error: `record exists but the daemon is not answering (pid ${record.pid})` };
|
|
287
|
+
}
|
|
288
|
+
const token = readDaemonToken(name);
|
|
289
|
+
if (token === null) {
|
|
290
|
+
return {
|
|
291
|
+
error: `workspace ${name} daemon is answering but its token is unreadable at ${tokenPath(name)}`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
let callerProjectId;
|
|
295
|
+
try {
|
|
296
|
+
callerProjectId = loadProjectInfo(root).id;
|
|
297
|
+
} catch {
|
|
298
|
+
return {
|
|
299
|
+
error: `no project identity in ${root} \u2014 run \`noir init\` before serving a workspace from here`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { url: `http://127.0.0.1:${record.port}/mcp?p=${callerProjectId}`, token };
|
|
303
|
+
}
|
|
304
|
+
async function verifyWorkspaceDaemon(record, name) {
|
|
305
|
+
if (!pidAlive(record.pid)) return { kind: "silent" };
|
|
306
|
+
return probeHealth(record.port, name, record.pid);
|
|
307
|
+
}
|
|
308
|
+
async function probeHealth(port, name, pid) {
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
311
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
|
|
312
|
+
});
|
|
313
|
+
if (!response.ok) return { kind: "silent" };
|
|
314
|
+
const body = await response.json().catch(() => null);
|
|
315
|
+
if (body === null) return { kind: "silent" };
|
|
316
|
+
if (typeof body.workspace === "string" && body.workspace !== name) {
|
|
317
|
+
return { kind: "foreign", workspace: body.workspace };
|
|
318
|
+
}
|
|
319
|
+
if (body.ok !== true || body.pid !== pid) return { kind: "silent" };
|
|
320
|
+
return { kind: "healthy" };
|
|
321
|
+
} catch {
|
|
322
|
+
return { kind: "silent" };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function bridgeStdioToWorkspace(name, root) {
|
|
326
|
+
const resolved = await resolveWorkspaceDaemon(name, root);
|
|
327
|
+
if ("error" in resolved) {
|
|
328
|
+
fail(EXIT.DAEMON_DOWN, resolved.error);
|
|
329
|
+
}
|
|
330
|
+
const host = new StdioBridgeTransport(process.stdin, process.stdout);
|
|
331
|
+
const daemon = new StreamableHTTPClientTransport(new URL(resolved.url), {
|
|
332
|
+
requestInit: { headers: { Authorization: `Bearer ${resolved.token}` } }
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
await relay(host, daemon);
|
|
336
|
+
} finally {
|
|
337
|
+
await daemon.close().catch(() => {
|
|
338
|
+
});
|
|
339
|
+
await host.close().catch(() => {
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function relay(host, daemon) {
|
|
344
|
+
await new Promise((resolve2) => {
|
|
345
|
+
let settled = false;
|
|
346
|
+
let closing = false;
|
|
347
|
+
const settle = () => {
|
|
348
|
+
if (settled) return;
|
|
349
|
+
settled = true;
|
|
350
|
+
closing = true;
|
|
351
|
+
resolve2();
|
|
352
|
+
};
|
|
353
|
+
const closeDaemon = () => {
|
|
354
|
+
closing = true;
|
|
355
|
+
void daemon.close().catch(() => {
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
host.onmessage = (message) => {
|
|
359
|
+
void daemon.send(message).catch(closeDaemon);
|
|
360
|
+
};
|
|
361
|
+
host.onclose = () => {
|
|
362
|
+
closeDaemon();
|
|
363
|
+
settle();
|
|
364
|
+
};
|
|
365
|
+
host.onerror = () => {
|
|
366
|
+
closeDaemon();
|
|
367
|
+
settle();
|
|
368
|
+
};
|
|
369
|
+
daemon.onmessage = (message) => {
|
|
370
|
+
void host.send(message).catch(() => {
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
daemon.onclose = () => {
|
|
374
|
+
void host.close().catch(() => {
|
|
375
|
+
});
|
|
376
|
+
settle();
|
|
377
|
+
};
|
|
378
|
+
daemon.onerror = (error2) => {
|
|
379
|
+
if (closing) return;
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
382
|
+
`
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
const failStart = (error2) => {
|
|
386
|
+
process.stderr.write(
|
|
387
|
+
`noir: workspace bridge: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
388
|
+
`
|
|
389
|
+
);
|
|
390
|
+
host.onerror?.(error2);
|
|
391
|
+
closeDaemon();
|
|
392
|
+
settle();
|
|
393
|
+
};
|
|
394
|
+
void daemon.start().catch(failStart);
|
|
395
|
+
void host.start().catch(failStart);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
var StdioBridgeTransport = class {
|
|
399
|
+
constructor(stdin, stdout) {
|
|
400
|
+
this.stdin = stdin;
|
|
401
|
+
this.stdout = stdout;
|
|
402
|
+
}
|
|
403
|
+
stdin;
|
|
404
|
+
stdout;
|
|
405
|
+
onclose;
|
|
406
|
+
onerror;
|
|
407
|
+
onmessage;
|
|
408
|
+
buffer = new ReadBuffer();
|
|
409
|
+
started = false;
|
|
410
|
+
closed = false;
|
|
411
|
+
async start() {
|
|
412
|
+
if (this.started) throw new Error("the stdio bridge transport is already started");
|
|
413
|
+
this.started = true;
|
|
414
|
+
this.stdin.on("data", this.onData);
|
|
415
|
+
this.stdin.on("error", this.onStdinFault);
|
|
416
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
417
|
+
this.stdout.on("error", this.onStdoutFault);
|
|
418
|
+
}
|
|
419
|
+
send(message) {
|
|
420
|
+
if (this.closed) return Promise.reject(new Error("the stdio bridge transport is closed"));
|
|
421
|
+
return new Promise((resolve2, reject) => {
|
|
422
|
+
if (this.stdout.write(serializeMessage(message))) {
|
|
423
|
+
resolve2();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const onDrain = () => {
|
|
427
|
+
this.stdout.off("error", onWriteError);
|
|
428
|
+
resolve2();
|
|
429
|
+
};
|
|
430
|
+
const onWriteError = (error2) => {
|
|
431
|
+
this.stdout.off("drain", onDrain);
|
|
432
|
+
reject(error2);
|
|
433
|
+
};
|
|
434
|
+
this.stdout.once("drain", onDrain);
|
|
435
|
+
this.stdout.once("error", onWriteError);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async close() {
|
|
439
|
+
if (this.closed) return;
|
|
440
|
+
this.closed = true;
|
|
441
|
+
this.stdin.off("data", this.onData);
|
|
442
|
+
this.stdin.off("error", this.onStdinFault);
|
|
443
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
444
|
+
this.stdout.off("error", this.onStdoutFault);
|
|
445
|
+
this.buffer.clear();
|
|
446
|
+
if (this.stdin.listenerCount("data") === 0) this.stdin.pause();
|
|
447
|
+
this.onclose?.();
|
|
448
|
+
}
|
|
449
|
+
onData = (chunk) => {
|
|
450
|
+
try {
|
|
451
|
+
this.buffer.append(chunk);
|
|
452
|
+
for (; ; ) {
|
|
453
|
+
const message = this.buffer.readMessage();
|
|
454
|
+
if (message === null) return;
|
|
455
|
+
this.onmessage?.(message);
|
|
456
|
+
}
|
|
457
|
+
} catch (error2) {
|
|
458
|
+
this.onerror?.(toError(error2));
|
|
459
|
+
void this.close();
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
/** The host closed its end of the pipe — the session is over. */
|
|
463
|
+
onStdinEnd = () => {
|
|
464
|
+
void this.close();
|
|
465
|
+
};
|
|
466
|
+
onStdinFault = (error2) => {
|
|
467
|
+
this.onerror?.(error2);
|
|
468
|
+
void this.close();
|
|
469
|
+
};
|
|
470
|
+
onStdoutFault = (error2) => {
|
|
471
|
+
this.onerror?.(error2);
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
function toError(value) {
|
|
475
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
476
|
+
}
|
|
477
|
+
|
|
262
478
|
// src/workspace-mcp.ts
|
|
263
479
|
import { existsSync, readFileSync } from "fs";
|
|
264
|
-
import {
|
|
265
|
-
import { resolveAdapter } from "@noir-ai/adapters";
|
|
480
|
+
import { mcpConfigPathFor, noirStdioArgs, TRANSPORT_KEYS } from "@noir-ai/adapters";
|
|
266
481
|
import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
|
|
267
|
-
function mcpPathFor(root, host) {
|
|
268
|
-
return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
|
|
269
|
-
}
|
|
270
482
|
function readJson(path) {
|
|
271
483
|
if (!existsSync(path)) return null;
|
|
272
484
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -300,39 +512,50 @@ function isNoirEmitted(mcpServers) {
|
|
|
300
512
|
if (typeof noir.command === "string") return true;
|
|
301
513
|
return noir.type === "http";
|
|
302
514
|
}
|
|
303
|
-
function
|
|
304
|
-
const
|
|
515
|
+
function noirEntry(mcpServers) {
|
|
516
|
+
const noir = mcpServers?.noir;
|
|
517
|
+
if (typeof noir !== "object" || noir === null || Array.isArray(noir)) return {};
|
|
518
|
+
return noir;
|
|
519
|
+
}
|
|
520
|
+
function writeNoirTransport(root, host, transport, opts) {
|
|
521
|
+
const path = mcpConfigPathFor(host, root);
|
|
305
522
|
const existing = loadExisting(path, opts);
|
|
523
|
+
const kept = Object.fromEntries(
|
|
524
|
+
Object.entries(noirEntry(existing?.mcpServers)).filter(([key]) => !TRANSPORT_KEYS.has(key))
|
|
525
|
+
);
|
|
306
526
|
const next = {
|
|
307
527
|
...existing ?? {},
|
|
308
528
|
mcpServers: {
|
|
309
529
|
...existing?.mcpServers ?? {},
|
|
310
|
-
noir: {
|
|
530
|
+
noir: { ...transport, ...kept }
|
|
311
531
|
}
|
|
312
532
|
};
|
|
313
533
|
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
314
534
|
`);
|
|
315
535
|
}
|
|
536
|
+
function writeWorkspaceEntry(root, host, name, opts) {
|
|
537
|
+
writeNoirTransport(
|
|
538
|
+
root,
|
|
539
|
+
host,
|
|
540
|
+
{ command: resolveNoirCommand(), args: noirStdioArgs(name) },
|
|
541
|
+
opts
|
|
542
|
+
);
|
|
543
|
+
}
|
|
316
544
|
function writeStdioEntry(root, host, opts) {
|
|
317
|
-
|
|
318
|
-
const existing = loadExisting(path, opts);
|
|
319
|
-
const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
|
|
320
|
-
const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
|
|
321
|
-
atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
|
|
322
|
-
`);
|
|
545
|
+
writeNoirTransport(root, host, { command: resolveNoirCommand(), args: noirStdioArgs() }, opts);
|
|
323
546
|
}
|
|
324
547
|
|
|
325
548
|
// src/commands/workspace.ts
|
|
326
549
|
function loadProjectOrFail(opts) {
|
|
327
550
|
try {
|
|
328
|
-
return
|
|
551
|
+
return loadProjectInfo2(process.cwd());
|
|
329
552
|
} catch {
|
|
330
553
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
331
554
|
}
|
|
332
555
|
}
|
|
333
|
-
function finishJoin(project, name,
|
|
556
|
+
function finishJoin(project, name, opts) {
|
|
334
557
|
writeWorkspaceMarker(project.root, name);
|
|
335
|
-
|
|
558
|
+
writeWorkspaceEntry(project.root, project.config.host, name, opts);
|
|
336
559
|
}
|
|
337
560
|
async function daemonStartWorkspace(opts) {
|
|
338
561
|
const project = loadProjectOrFail(opts);
|
|
@@ -365,7 +588,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
365
588
|
}
|
|
366
589
|
if (opts.detach === true) {
|
|
367
590
|
const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
|
|
368
|
-
finishJoin(project, opts.name,
|
|
591
|
+
finishJoin(project, opts.name, opts);
|
|
369
592
|
if (opts.json === true) {
|
|
370
593
|
process.stdout.write(
|
|
371
594
|
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
@@ -384,7 +607,7 @@ async function daemonStartWorkspace(opts) {
|
|
|
384
607
|
project,
|
|
385
608
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
386
609
|
});
|
|
387
|
-
finishJoin(project, opts.name,
|
|
610
|
+
finishJoin(project, opts.name, opts);
|
|
388
611
|
if (opts.json === true) {
|
|
389
612
|
process.stdout.write(
|
|
390
613
|
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
|
|
@@ -414,7 +637,7 @@ async function daemonJoin(opts) {
|
|
|
414
637
|
idleTimeoutSec: project.config.workspace.idleTimeoutSec
|
|
415
638
|
});
|
|
416
639
|
upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
|
|
417
|
-
finishJoin(project, opts.name,
|
|
640
|
+
finishJoin(project, opts.name, opts);
|
|
418
641
|
if (opts.json === true) {
|
|
419
642
|
process.stdout.write(
|
|
420
643
|
`${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
|
|
@@ -431,19 +654,20 @@ async function workspaceList(opts) {
|
|
|
431
654
|
} catch {
|
|
432
655
|
names = [];
|
|
433
656
|
}
|
|
434
|
-
const rows =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
657
|
+
const rows = await Promise.all(
|
|
658
|
+
names.map(async (n) => {
|
|
659
|
+
const reg = readWorkspaceRegistry(n);
|
|
660
|
+
const rec = readWorkspaceDaemonRecord2(n);
|
|
661
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, n);
|
|
662
|
+
const running = health?.kind === "healthy";
|
|
663
|
+
return {
|
|
664
|
+
name: n,
|
|
665
|
+
members: reg?.members.length ?? 0,
|
|
666
|
+
running,
|
|
667
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
668
|
+
};
|
|
669
|
+
})
|
|
670
|
+
);
|
|
447
671
|
if (opts.json === true) {
|
|
448
672
|
process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
|
|
449
673
|
`);
|
|
@@ -454,7 +678,8 @@ async function workspaceList(opts) {
|
|
|
454
678
|
return;
|
|
455
679
|
}
|
|
456
680
|
for (const r of rows) {
|
|
457
|
-
|
|
681
|
+
const state = r.running ? ", running" : r.stale ? ", not running (stale record)" : "";
|
|
682
|
+
log(`${r.name} \u2014 ${r.members} member(s)${state}`, opts);
|
|
458
683
|
}
|
|
459
684
|
}
|
|
460
685
|
async function workspaceStatus(opts) {
|
|
@@ -466,23 +691,23 @@ async function workspaceStatus(opts) {
|
|
|
466
691
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
467
692
|
}
|
|
468
693
|
const reg = readWorkspaceRegistry(name);
|
|
469
|
-
const rec =
|
|
470
|
-
const
|
|
694
|
+
const rec = readWorkspaceDaemonRecord2(name);
|
|
695
|
+
const health = rec === null ? null : await verifyWorkspaceDaemon(rec, name);
|
|
696
|
+
const running = health?.kind === "healthy";
|
|
471
697
|
const data = {
|
|
472
698
|
name,
|
|
473
699
|
members: reg?.members.map((m) => m.projectId) ?? [],
|
|
474
700
|
running,
|
|
475
|
-
...rec ? { pid: rec.pid, port: rec.port } : {}
|
|
701
|
+
...rec ? { pid: rec.pid, port: rec.port } : {},
|
|
702
|
+
...rec !== null && !running ? { stale: true } : {}
|
|
476
703
|
};
|
|
477
704
|
if (opts.json === true) {
|
|
478
705
|
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
479
706
|
`);
|
|
480
707
|
return;
|
|
481
708
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
opts
|
|
485
|
-
);
|
|
709
|
+
const state = running ? `running (pid ${rec?.pid}, port ${rec?.port})` : rec !== null ? `not running (stale record for pid ${rec.pid})` : "not running";
|
|
710
|
+
log(`workspace ${name}: ${state}`, opts);
|
|
486
711
|
for (const m of data.members) log(` member: ${m}`, opts);
|
|
487
712
|
}
|
|
488
713
|
async function workspaceLeave(opts) {
|
|
@@ -512,8 +737,8 @@ async function workspaceStop(opts) {
|
|
|
512
737
|
if (!isValidWorkspaceName(name)) {
|
|
513
738
|
fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
|
|
514
739
|
}
|
|
515
|
-
const rec =
|
|
516
|
-
if (rec === null || !
|
|
740
|
+
const rec = readWorkspaceDaemonRecord2(name);
|
|
741
|
+
if (rec === null || !pidAlive2(rec.pid)) {
|
|
517
742
|
if (opts.json === true) {
|
|
518
743
|
process.stdout.write(
|
|
519
744
|
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
@@ -524,6 +749,10 @@ async function workspaceStop(opts) {
|
|
|
524
749
|
info("workspace daemon is not running.", opts);
|
|
525
750
|
return;
|
|
526
751
|
}
|
|
752
|
+
const health = await verifyWorkspaceDaemon(rec, name);
|
|
753
|
+
if (health.kind !== "healthy") {
|
|
754
|
+
fail(EXIT.ERROR, stopRefusal(name, rec, health), opts);
|
|
755
|
+
}
|
|
527
756
|
try {
|
|
528
757
|
process.kill(rec.pid, "SIGTERM");
|
|
529
758
|
} catch {
|
|
@@ -538,6 +767,12 @@ async function workspaceStop(opts) {
|
|
|
538
767
|
}
|
|
539
768
|
info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
|
|
540
769
|
}
|
|
770
|
+
function stopRefusal(name, rec, health) {
|
|
771
|
+
if (health.kind === "foreign") {
|
|
772
|
+
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.`;
|
|
773
|
+
}
|
|
774
|
+
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.`;
|
|
775
|
+
}
|
|
541
776
|
|
|
542
777
|
// src/commands/daemon.ts
|
|
543
778
|
var MODE = "foreground";
|
|
@@ -549,7 +784,7 @@ function formatUptime(sec) {
|
|
|
549
784
|
}
|
|
550
785
|
function resolveCallerProjectId() {
|
|
551
786
|
try {
|
|
552
|
-
return
|
|
787
|
+
return loadProjectInfo3(process.cwd()).id;
|
|
553
788
|
} catch {
|
|
554
789
|
return void 0;
|
|
555
790
|
}
|
|
@@ -581,7 +816,7 @@ async function daemonStart(opts) {
|
|
|
581
816
|
}
|
|
582
817
|
let project;
|
|
583
818
|
try {
|
|
584
|
-
project =
|
|
819
|
+
project = loadProjectInfo3(process.cwd());
|
|
585
820
|
} catch {
|
|
586
821
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
587
822
|
}
|
|
@@ -613,7 +848,7 @@ async function daemonStart(opts) {
|
|
|
613
848
|
}
|
|
614
849
|
if (opts.detach === true) {
|
|
615
850
|
const existing = readProjectDaemonRecord(project.id);
|
|
616
|
-
if (existing &&
|
|
851
|
+
if (existing && pidAlive3(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
|
|
617
852
|
if (opts.json === true) {
|
|
618
853
|
process.stdout.write(
|
|
619
854
|
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
@@ -723,7 +958,7 @@ async function daemonStop(opts) {
|
|
|
723
958
|
const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
|
|
724
959
|
let signalled = false;
|
|
725
960
|
let errMsg2;
|
|
726
|
-
const healthy =
|
|
961
|
+
const healthy = pidAlive3(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
|
|
727
962
|
if (healthy) {
|
|
728
963
|
try {
|
|
729
964
|
process.kill(rec.pid, "SIGTERM");
|
|
@@ -765,7 +1000,7 @@ async function daemonStatus(opts) {
|
|
|
765
1000
|
if (!rec) {
|
|
766
1001
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
767
1002
|
}
|
|
768
|
-
if (!
|
|
1003
|
+
if (!pidAlive3(rec.pid)) {
|
|
769
1004
|
clearProjectDaemonRecord(callerProject);
|
|
770
1005
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
771
1006
|
}
|
|
@@ -827,14 +1062,14 @@ async function daemonToken(opts) {
|
|
|
827
1062
|
opts
|
|
828
1063
|
);
|
|
829
1064
|
}
|
|
830
|
-
if (!
|
|
1065
|
+
if (!pidAlive3(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
|
|
831
1066
|
fail(
|
|
832
1067
|
EXIT.DAEMON_DOWN,
|
|
833
1068
|
"Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
|
|
834
1069
|
opts
|
|
835
1070
|
);
|
|
836
1071
|
}
|
|
837
|
-
const token =
|
|
1072
|
+
const token = readDaemonToken2(callerProject);
|
|
838
1073
|
if (token === null) {
|
|
839
1074
|
fail(
|
|
840
1075
|
EXIT.DAEMON_DOWN,
|
|
@@ -857,15 +1092,17 @@ async function daemonRestart(opts) {
|
|
|
857
1092
|
|
|
858
1093
|
// src/commands/doctor.ts
|
|
859
1094
|
import { spawnSync } from "child_process";
|
|
860
|
-
import { existsSync as existsSync2, readdirSync as
|
|
1095
|
+
import { existsSync as existsSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
861
1096
|
import { dirname, join as join2, relative } from "path";
|
|
862
1097
|
import { fileURLToPath } from "url";
|
|
863
|
-
import {
|
|
1098
|
+
import { mcpConfigPathFor as mcpConfigPathFor2, resolveAdapter } from "@noir-ai/adapters";
|
|
864
1099
|
import {
|
|
865
1100
|
detectActiveMethod,
|
|
1101
|
+
ensureOwnerOnly,
|
|
1102
|
+
ensureOwnerOnlyDir,
|
|
866
1103
|
latestVersionFromCache,
|
|
867
1104
|
loadNoirEnv,
|
|
868
|
-
loadProjectInfo as
|
|
1105
|
+
loadProjectInfo as loadProjectInfo4,
|
|
869
1106
|
NOIR_VERSION,
|
|
870
1107
|
paths,
|
|
871
1108
|
readInstallRecord,
|
|
@@ -878,8 +1115,167 @@ import {
|
|
|
878
1115
|
readScaffoldVersion,
|
|
879
1116
|
render
|
|
880
1117
|
} from "@noir-ai/create";
|
|
881
|
-
import { pidAlive as
|
|
1118
|
+
import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord2 } from "@noir-ai/daemon";
|
|
882
1119
|
import { resolveModelConfig } from "@noir-ai/model";
|
|
1120
|
+
|
|
1121
|
+
// src/hygiene-scan.ts
|
|
1122
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
1123
|
+
import { join } from "path";
|
|
1124
|
+
import { checkHygiene } from "@noir-ai/skills";
|
|
1125
|
+
var HYGIENE_FINDING_CAP = 5;
|
|
1126
|
+
var HYGIENE_MAX_FILE_BYTES = 512 * 1024;
|
|
1127
|
+
var HYGIENE_MAX_FILES = 2e3;
|
|
1128
|
+
var HYGIENE_CODE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|sh)$/i;
|
|
1129
|
+
var HYGIENE_MARKDOWN_EXTENSIONS = /\.(?:md|mdx)$/i;
|
|
1130
|
+
var HYGIENE_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
|
|
1131
|
+
var HYGIENE_EXCLUDED_DIRS = /* @__PURE__ */ new Set(["docs/internal", "docs/decisions", "docs/roadmap"]);
|
|
1132
|
+
var HYGIENE_EXCLUDED_FILES = /* @__PURE__ */ new Set(["CHANGELOG.md"]);
|
|
1133
|
+
function readDirEntries(dir) {
|
|
1134
|
+
try {
|
|
1135
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
1136
|
+
} catch {
|
|
1137
|
+
return [];
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function isDirectory(path) {
|
|
1141
|
+
try {
|
|
1142
|
+
return statSync(path).isDirectory();
|
|
1143
|
+
} catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function hygieneKindOf(path) {
|
|
1148
|
+
if (HYGIENE_MARKDOWN_EXTENSIONS.test(path)) return "markdown";
|
|
1149
|
+
if (HYGIENE_CODE_EXTENSIONS.test(path)) return "code";
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
function hasScannableLayout(root) {
|
|
1153
|
+
if (isDirectory(join(root, "docs")) || isDirectory(join(root, "scripts"))) return true;
|
|
1154
|
+
for (const entry of readDirEntries(join(root, "packages"))) {
|
|
1155
|
+
if (!entry.isDirectory()) continue;
|
|
1156
|
+
if (isDirectory(join(root, "packages", entry.name, "src"))) return true;
|
|
1157
|
+
if (isDirectory(join(root, "packages", entry.name, "test"))) return true;
|
|
1158
|
+
}
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
function addHygieneFile(rel, walk) {
|
|
1162
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
1163
|
+
if (name.startsWith(".") || HYGIENE_EXCLUDED_FILES.has(name)) return;
|
|
1164
|
+
const kind = hygieneKindOf(rel);
|
|
1165
|
+
if (kind === null) return;
|
|
1166
|
+
if (walk.files.length >= HYGIENE_MAX_FILES) {
|
|
1167
|
+
walk.truncated = true;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
walk.files.push({ path: rel, kind });
|
|
1171
|
+
}
|
|
1172
|
+
function walkScannable(root, relDir, skip, walk) {
|
|
1173
|
+
if (walk.truncated) return;
|
|
1174
|
+
for (const entry of readDirEntries(join(root, relDir))) {
|
|
1175
|
+
if (walk.truncated) return;
|
|
1176
|
+
const rel = `${relDir}/${entry.name}`;
|
|
1177
|
+
if (entry.isDirectory()) {
|
|
1178
|
+
if (HYGIENE_SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || skip.has(rel)) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
walkScannable(root, rel, skip, walk);
|
|
1182
|
+
} else if (entry.isFile()) {
|
|
1183
|
+
addHygieneFile(rel, walk);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
var NO_SKIP_DIRS = /* @__PURE__ */ new Set();
|
|
1188
|
+
function collectHygieneFiles(root) {
|
|
1189
|
+
const walk = { files: [], truncated: false };
|
|
1190
|
+
for (const entry of readDirEntries(root)) {
|
|
1191
|
+
if (entry.isFile()) addHygieneFile(entry.name, walk);
|
|
1192
|
+
}
|
|
1193
|
+
walkScannable(root, "docs", HYGIENE_EXCLUDED_DIRS, walk);
|
|
1194
|
+
walkScannable(root, ".claude/skills", NO_SKIP_DIRS, walk);
|
|
1195
|
+
for (const entry of readDirEntries(join(root, "packages"))) {
|
|
1196
|
+
if (!entry.isDirectory()) continue;
|
|
1197
|
+
walkScannable(root, `packages/${entry.name}/src`, NO_SKIP_DIRS, walk);
|
|
1198
|
+
walkScannable(root, `packages/${entry.name}/test`, NO_SKIP_DIRS, walk);
|
|
1199
|
+
}
|
|
1200
|
+
walkScannable(root, "scripts", NO_SKIP_DIRS, walk);
|
|
1201
|
+
walk.files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1202
|
+
return walk;
|
|
1203
|
+
}
|
|
1204
|
+
function hygieneOmissions(result) {
|
|
1205
|
+
const notes = [];
|
|
1206
|
+
if (result.skipped > 0) {
|
|
1207
|
+
notes.push(
|
|
1208
|
+
`${result.skipped} file${result.skipped === 1 ? "" : "s"} over ${HYGIENE_MAX_FILE_BYTES / 1024} KiB skipped`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (result.truncated) notes.push(`stopped at ${HYGIENE_MAX_FILES} files`);
|
|
1212
|
+
return notes.length > 0 ? ` (${notes.join("; ")})` : "";
|
|
1213
|
+
}
|
|
1214
|
+
function hygieneCounts(result) {
|
|
1215
|
+
const omitted = hygieneOmissions(result);
|
|
1216
|
+
if (result.scanned === 0) {
|
|
1217
|
+
return `nothing to scan (no packages/*/src, packages/*/test, scripts/ or documents found)${omitted}`;
|
|
1218
|
+
}
|
|
1219
|
+
if (result.findings.length === 0) {
|
|
1220
|
+
return `clean \u2014 ${result.scanned} file${result.scanned === 1 ? "" : "s"} scanned${omitted}`;
|
|
1221
|
+
}
|
|
1222
|
+
const files = new Set(result.findings.map((f) => f.path)).size;
|
|
1223
|
+
return `${result.fail} fail, ${result.warn} warn in ${files} file${files === 1 ? "" : "s"}${omitted}`;
|
|
1224
|
+
}
|
|
1225
|
+
function hygieneDetail(result) {
|
|
1226
|
+
const counts = hygieneCounts(result);
|
|
1227
|
+
if (result.scanned === 0 || result.findings.length === 0) return counts;
|
|
1228
|
+
const named = result.findings.slice(0, HYGIENE_FINDING_CAP);
|
|
1229
|
+
const hidden = result.findings.length - named.length;
|
|
1230
|
+
const where = named.map((f) => `${f.path}:${f.line} ${f.id}`).join("; ");
|
|
1231
|
+
return `${counts} \u2014 ${where}${hidden > 0 ? ` (+${hidden} more)` : ""}`;
|
|
1232
|
+
}
|
|
1233
|
+
function scanOutputHygiene(root) {
|
|
1234
|
+
const empty = {
|
|
1235
|
+
scanned: 0,
|
|
1236
|
+
skipped: 0,
|
|
1237
|
+
truncated: false,
|
|
1238
|
+
findings: [],
|
|
1239
|
+
fail: 0,
|
|
1240
|
+
warn: 0
|
|
1241
|
+
};
|
|
1242
|
+
if (!hasScannableLayout(root)) return empty;
|
|
1243
|
+
const walk = collectHygieneFiles(root);
|
|
1244
|
+
const findings = [];
|
|
1245
|
+
let scanned = 0;
|
|
1246
|
+
let skipped = 0;
|
|
1247
|
+
for (const file of walk.files) {
|
|
1248
|
+
const abs = join(root, file.path);
|
|
1249
|
+
try {
|
|
1250
|
+
if (statSync(abs).size > HYGIENE_MAX_FILE_BYTES) {
|
|
1251
|
+
skipped++;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
const text = readFileSync2(abs, "utf8");
|
|
1255
|
+
scanned++;
|
|
1256
|
+
for (const found of checkHygiene(text, file.kind)) {
|
|
1257
|
+
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
findings.sort((a, b) => {
|
|
1263
|
+
if (a.tier !== b.tier) return a.tier === "fail" ? -1 : 1;
|
|
1264
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
1265
|
+
return a.line - b.line;
|
|
1266
|
+
});
|
|
1267
|
+
const fail2 = findings.filter((f) => f.tier === "fail").length;
|
|
1268
|
+
return {
|
|
1269
|
+
scanned,
|
|
1270
|
+
skipped,
|
|
1271
|
+
truncated: walk.truncated,
|
|
1272
|
+
findings,
|
|
1273
|
+
fail: fail2,
|
|
1274
|
+
warn: findings.length - fail2
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/commands/doctor.ts
|
|
883
1279
|
async function probeOnnx() {
|
|
884
1280
|
try {
|
|
885
1281
|
const spec = "onnxruntime-node";
|
|
@@ -894,7 +1290,7 @@ function describeOnnx(o) {
|
|
|
894
1290
|
if (/Cannot find (module|package)/i.test(o.reason)) {
|
|
895
1291
|
return "not resolvable from CLI probe (best-effort)";
|
|
896
1292
|
}
|
|
897
|
-
const r = o.reason
|
|
1293
|
+
const r = truncateToWidth(o.reason, 60);
|
|
898
1294
|
return `probe failed: ${r}`;
|
|
899
1295
|
}
|
|
900
1296
|
function fmtUptime(sec) {
|
|
@@ -926,7 +1322,7 @@ function checkConfig(root) {
|
|
|
926
1322
|
};
|
|
927
1323
|
}
|
|
928
1324
|
try {
|
|
929
|
-
const project =
|
|
1325
|
+
const project = loadProjectInfo4(root);
|
|
930
1326
|
return {
|
|
931
1327
|
project,
|
|
932
1328
|
result: {
|
|
@@ -960,7 +1356,7 @@ async function checkDaemon(checks, project) {
|
|
|
960
1356
|
});
|
|
961
1357
|
return;
|
|
962
1358
|
}
|
|
963
|
-
if (!
|
|
1359
|
+
if (!pidAlive4(rec.pid)) {
|
|
964
1360
|
checks.push({
|
|
965
1361
|
name: "daemon",
|
|
966
1362
|
status: "warn",
|
|
@@ -1044,11 +1440,21 @@ async function checkStore(checks, project, root) {
|
|
|
1044
1440
|
}
|
|
1045
1441
|
}
|
|
1046
1442
|
var ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
|
|
1443
|
+
function envModeCause(st) {
|
|
1444
|
+
return st.ctimeMs > st.mtimeMs ? "changed" : "legacy";
|
|
1445
|
+
}
|
|
1446
|
+
function octalMode(mode) {
|
|
1447
|
+
return mode.toString(8).padStart(4, "0");
|
|
1448
|
+
}
|
|
1449
|
+
function describeEnvMode(cause, mode) {
|
|
1450
|
+
const lead = `is ${octalMode(mode)} \u2014 accessible to other accounts; run \`noir sync\` (or \`chmod 600 .noir/.env\`) to make it owner-only.`;
|
|
1451
|
+
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.`;
|
|
1452
|
+
}
|
|
1047
1453
|
function checkNoirEnv(checks, root, loaded) {
|
|
1048
1454
|
const path = join2(paths.noirDir(root), ".env");
|
|
1049
1455
|
let st;
|
|
1050
1456
|
try {
|
|
1051
|
-
st =
|
|
1457
|
+
st = statSync2(path);
|
|
1052
1458
|
} catch {
|
|
1053
1459
|
return;
|
|
1054
1460
|
}
|
|
@@ -1066,8 +1472,8 @@ function checkNoirEnv(checks, root, loaded) {
|
|
|
1066
1472
|
tooOpen ? {
|
|
1067
1473
|
name: "noir-env",
|
|
1068
1474
|
status: "warn",
|
|
1069
|
-
detail: `${path}
|
|
1070
|
-
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode
|
|
1475
|
+
detail: `${path} ${describeEnvMode(envModeCause(st), mode)}`
|
|
1476
|
+
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${octalMode(mode)})` }
|
|
1071
1477
|
);
|
|
1072
1478
|
for (const [key, source] of Object.entries(loaded.sources)) {
|
|
1073
1479
|
if (source !== "file") continue;
|
|
@@ -1167,7 +1573,7 @@ function checkSeedDrift(checks, root) {
|
|
|
1167
1573
|
const seedPath = join2(root, ".noir", ".env.example");
|
|
1168
1574
|
let bytes;
|
|
1169
1575
|
try {
|
|
1170
|
-
bytes =
|
|
1576
|
+
bytes = readFileSync3(seedPath, "utf8");
|
|
1171
1577
|
} catch {
|
|
1172
1578
|
return;
|
|
1173
1579
|
}
|
|
@@ -1185,6 +1591,14 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
1185
1591
|
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1186
1592
|
return null;
|
|
1187
1593
|
}
|
|
1594
|
+
if (project.config.rules.enabled === false) {
|
|
1595
|
+
checks.push({
|
|
1596
|
+
name: "rules budget",
|
|
1597
|
+
status: "ok",
|
|
1598
|
+
detail: "disabled \u2014 rules.enabled is false in .noir/config.yml (RULES.md is not measured)"
|
|
1599
|
+
});
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1188
1602
|
const rulesPath = paths.rulesMd(root);
|
|
1189
1603
|
if (!existsSync2(rulesPath)) {
|
|
1190
1604
|
checks.push({
|
|
@@ -1196,7 +1610,7 @@ function checkRulesMdBudget(checks, root, project) {
|
|
|
1196
1610
|
}
|
|
1197
1611
|
let content;
|
|
1198
1612
|
try {
|
|
1199
|
-
content =
|
|
1613
|
+
content = readFileSync3(rulesPath, "utf8");
|
|
1200
1614
|
} catch (e) {
|
|
1201
1615
|
checks.push({
|
|
1202
1616
|
name: "rules budget",
|
|
@@ -1240,7 +1654,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
1240
1654
|
return null;
|
|
1241
1655
|
}
|
|
1242
1656
|
const host = project.config.host;
|
|
1243
|
-
const adapter =
|
|
1657
|
+
const adapter = resolveAdapter(host);
|
|
1244
1658
|
const ectx = { root };
|
|
1245
1659
|
const expected = [];
|
|
1246
1660
|
const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
|
|
@@ -1249,7 +1663,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
1249
1663
|
}
|
|
1250
1664
|
if (host === "claude") expected.push("CLAUDE.md");
|
|
1251
1665
|
else if (host === "gemini") expected.push("GEMINI.md");
|
|
1252
|
-
expected.push(relOr(
|
|
1666
|
+
expected.push(relOr(mcpConfigPathFor2(host, root), root));
|
|
1253
1667
|
const missing = expected.filter((p) => !existsSync2(joinRoot(root, p)));
|
|
1254
1668
|
const status2 = missing.length === 0 ? "ok" : "warn";
|
|
1255
1669
|
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)`;
|
|
@@ -1276,6 +1690,15 @@ function checkNestedNoir(checks, root) {
|
|
|
1276
1690
|
});
|
|
1277
1691
|
return { detected, paths: found };
|
|
1278
1692
|
}
|
|
1693
|
+
function checkOutputHygiene(checks, root) {
|
|
1694
|
+
const result = scanOutputHygiene(root);
|
|
1695
|
+
checks.push({
|
|
1696
|
+
name: "output hygiene",
|
|
1697
|
+
status: result.fail > 0 ? "fail" : result.warn > 0 ? "warn" : "ok",
|
|
1698
|
+
detail: hygieneDetail(result)
|
|
1699
|
+
});
|
|
1700
|
+
return result;
|
|
1701
|
+
}
|
|
1279
1702
|
function collectDedupCandidates(root) {
|
|
1280
1703
|
const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
|
|
1281
1704
|
const out = [];
|
|
@@ -1284,7 +1707,7 @@ function collectDedupCandidates(root) {
|
|
|
1284
1707
|
if (!existsSync2(abs)) continue;
|
|
1285
1708
|
let text;
|
|
1286
1709
|
try {
|
|
1287
|
-
text =
|
|
1710
|
+
text = readFileSync3(abs, "utf8");
|
|
1288
1711
|
} catch {
|
|
1289
1712
|
continue;
|
|
1290
1713
|
}
|
|
@@ -1344,7 +1767,7 @@ function resolveWorkspacePackagesDir() {
|
|
|
1344
1767
|
const pkgPath = join2(here, "package.json");
|
|
1345
1768
|
if (existsSync2(pkgPath)) {
|
|
1346
1769
|
try {
|
|
1347
|
-
const pkg = JSON.parse(
|
|
1770
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1348
1771
|
if (pkg.name === "@noir-ai/cli") {
|
|
1349
1772
|
const packagesDir = dirname(here);
|
|
1350
1773
|
if (existsSync2(join2(dirname(packagesDir), "pnpm-workspace.yaml"))) {
|
|
@@ -1393,7 +1816,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
1393
1816
|
}
|
|
1394
1817
|
let entries = [];
|
|
1395
1818
|
try {
|
|
1396
|
-
entries =
|
|
1819
|
+
entries = readdirSync3(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1397
1820
|
} catch (e) {
|
|
1398
1821
|
checks.push({
|
|
1399
1822
|
name: "publish",
|
|
@@ -1410,7 +1833,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
1410
1833
|
if (!existsSync2(pkgPath)) continue;
|
|
1411
1834
|
let pkg;
|
|
1412
1835
|
try {
|
|
1413
|
-
pkg = JSON.parse(
|
|
1836
|
+
pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1414
1837
|
} catch (e) {
|
|
1415
1838
|
issues.push(`${name}/package.json: unparseable (${msg(e)})`);
|
|
1416
1839
|
checked++;
|
|
@@ -1489,6 +1912,17 @@ function renderHuman(payload, opts) {
|
|
|
1489
1912
|
["Check", "Status", "Detail"],
|
|
1490
1913
|
opts
|
|
1491
1914
|
);
|
|
1915
|
+
if (payload.fixes) {
|
|
1916
|
+
const healed = payload.fixes.filter((f) => f.outcome === "healed");
|
|
1917
|
+
if (healed.length > 0) {
|
|
1918
|
+
log(
|
|
1919
|
+
`fixed ${healed.length} permission${healed.length === 1 ? "" : "s"}: ${healed.map((f) => f.path).join(", ")}`,
|
|
1920
|
+
opts
|
|
1921
|
+
);
|
|
1922
|
+
} else {
|
|
1923
|
+
log("nothing to fix \u2014 permissions already owner-only", opts);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1492
1926
|
const { ok, warn: warnN, fail: fail2 } = payload.summary;
|
|
1493
1927
|
if (fail2 > 0) {
|
|
1494
1928
|
error(
|
|
@@ -1501,12 +1935,24 @@ function renderHuman(payload, opts) {
|
|
|
1501
1935
|
success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
|
|
1502
1936
|
}
|
|
1503
1937
|
}
|
|
1938
|
+
function fixOwnerOnlyPermissions(root, project) {
|
|
1939
|
+
const fixes = [
|
|
1940
|
+
{ path: ".noir/.env", outcome: ensureOwnerOnly(join2(paths.noirDir(root), ".env")) }
|
|
1941
|
+
];
|
|
1942
|
+
if (project) {
|
|
1943
|
+
const dbRel = `.noir/store/${project.id}.db`;
|
|
1944
|
+
fixes.push({ path: dbRel, outcome: ensureOwnerOnly(join2(root, dbRel)) });
|
|
1945
|
+
fixes.push({ path: ".noir/store", outcome: ensureOwnerOnlyDir(paths.storeDir(root)) });
|
|
1946
|
+
}
|
|
1947
|
+
return fixes;
|
|
1948
|
+
}
|
|
1504
1949
|
async function doctor(opts = {}) {
|
|
1505
1950
|
const root = process.cwd();
|
|
1506
1951
|
const checks = [];
|
|
1507
1952
|
await checkRuntime(checks);
|
|
1508
1953
|
const { project, result: configResult } = checkConfig(root);
|
|
1509
1954
|
checks.push(configResult);
|
|
1955
|
+
const fixes = opts.fix === true ? fixOwnerOnlyPermissions(root, project) : void 0;
|
|
1510
1956
|
await checkDaemon(checks, project);
|
|
1511
1957
|
const { vecOk } = await checkNativeDeps(checks);
|
|
1512
1958
|
await checkStore(checks, project, root);
|
|
@@ -1519,6 +1965,7 @@ async function doctor(opts = {}) {
|
|
|
1519
1965
|
const rules = checkRulesMdBudget(checks, root, project);
|
|
1520
1966
|
const host = checkHostArtifacts(checks, root, project);
|
|
1521
1967
|
checkNestedNoir(checks, root);
|
|
1968
|
+
checkOutputHygiene(checks, root);
|
|
1522
1969
|
if (opts.dedup === true) {
|
|
1523
1970
|
await checkSemanticDupDoctor(checks, root, project);
|
|
1524
1971
|
}
|
|
@@ -1542,7 +1989,8 @@ async function doctor(opts = {}) {
|
|
|
1542
1989
|
method: installCheck.method,
|
|
1543
1990
|
installedVersion: installCheck.installedVersion,
|
|
1544
1991
|
latestKnown: installCheck.latestKnown
|
|
1545
|
-
}
|
|
1992
|
+
},
|
|
1993
|
+
...fixes ? { fixes } : {}
|
|
1546
1994
|
};
|
|
1547
1995
|
if (opts.json === true) {
|
|
1548
1996
|
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
@@ -1559,7 +2007,7 @@ async function doctor(opts = {}) {
|
|
|
1559
2007
|
}
|
|
1560
2008
|
|
|
1561
2009
|
// src/commands/env.ts
|
|
1562
|
-
import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as
|
|
2010
|
+
import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as loadProjectInfo5 } from "@noir-ai/core";
|
|
1563
2011
|
import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
|
|
1564
2012
|
var CURATED_AMBIENT_KEYS = [
|
|
1565
2013
|
"CLICKUP_API_TOKEN",
|
|
@@ -1580,11 +2028,11 @@ function captureAmbientEnv(env2 = process.env) {
|
|
|
1580
2028
|
var SHORT_VALUE_MAX = 8;
|
|
1581
2029
|
function redactShape(value) {
|
|
1582
2030
|
if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
|
|
1583
|
-
return `${value
|
|
2031
|
+
return `${truncateToWidth(value, 3, "")}\u2026(${value.length})`;
|
|
1584
2032
|
}
|
|
1585
2033
|
function configApiKeyEnvNames(root) {
|
|
1586
2034
|
try {
|
|
1587
|
-
const project =
|
|
2035
|
+
const project = loadProjectInfo5(root);
|
|
1588
2036
|
const names = /* @__PURE__ */ new Set();
|
|
1589
2037
|
for (const provider of Object.values(resolveModelConfig2(project.config.model).providers)) {
|
|
1590
2038
|
if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
|
|
@@ -1655,14 +2103,14 @@ import { join as join3 } from "path";
|
|
|
1655
2103
|
import {
|
|
1656
2104
|
defaultHandoffBlock,
|
|
1657
2105
|
hostLaunchDirective,
|
|
1658
|
-
resolveAdapter as
|
|
2106
|
+
resolveAdapter as resolveAdapter2
|
|
1659
2107
|
} from "@noir-ai/adapters";
|
|
1660
|
-
import { loadProjectInfo as
|
|
2108
|
+
import { loadProjectInfo as loadProjectInfo7, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
|
|
1661
2109
|
|
|
1662
2110
|
// src/commands/task.ts
|
|
1663
2111
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1664
2112
|
import { writeFileSync } from "fs";
|
|
1665
|
-
import { loadProjectInfo as
|
|
2113
|
+
import { loadProjectInfo as loadProjectInfo6, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
|
|
1666
2114
|
import {
|
|
1667
2115
|
PHASES,
|
|
1668
2116
|
TASK_CLASSES,
|
|
@@ -1686,7 +2134,7 @@ function skillFor(phase) {
|
|
|
1686
2134
|
}
|
|
1687
2135
|
function writeDoneArtifacts(opts, taskId) {
|
|
1688
2136
|
try {
|
|
1689
|
-
const project =
|
|
2137
|
+
const project = loadProjectInfo6(process.cwd());
|
|
1690
2138
|
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
1691
2139
|
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
1692
2140
|
const nextN = nextArtifactSequence(project.root, "adr");
|
|
@@ -2010,7 +2458,7 @@ function runCheck(cmd) {
|
|
|
2010
2458
|
async function taskVerify(opts) {
|
|
2011
2459
|
let project;
|
|
2012
2460
|
try {
|
|
2013
|
-
project =
|
|
2461
|
+
project = loadProjectInfo6(process.cwd());
|
|
2014
2462
|
} catch {
|
|
2015
2463
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2016
2464
|
}
|
|
@@ -2116,7 +2564,7 @@ async function taskResearch(opts) {
|
|
|
2116
2564
|
return;
|
|
2117
2565
|
}
|
|
2118
2566
|
renderStatusRow(s, opts);
|
|
2119
|
-
info("
|
|
2567
|
+
info("this is a status view; record findings with `noir task research-record`", opts);
|
|
2120
2568
|
}
|
|
2121
2569
|
async function taskResearchRecord(opts) {
|
|
2122
2570
|
const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
|
|
@@ -2140,10 +2588,7 @@ async function taskResearchRecord(opts) {
|
|
|
2140
2588
|
`);
|
|
2141
2589
|
return;
|
|
2142
2590
|
}
|
|
2143
|
-
success(
|
|
2144
|
-
`research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
|
|
2145
|
-
opts
|
|
2146
|
-
);
|
|
2591
|
+
success(`research recorded \u2192 ${opts.type}: ${truncateToWidth(opts.text, 80)}`, opts);
|
|
2147
2592
|
}
|
|
2148
2593
|
async function taskDecompose(opts) {
|
|
2149
2594
|
const cap = opts.capability.trim();
|
|
@@ -2290,7 +2735,7 @@ function toHostPayload(p) {
|
|
|
2290
2735
|
}
|
|
2291
2736
|
function resolveDirective(root, p) {
|
|
2292
2737
|
const ctx = { root };
|
|
2293
|
-
const adapter =
|
|
2738
|
+
const adapter = resolveAdapter2(p.host);
|
|
2294
2739
|
const hostPayload = toHostPayload(p);
|
|
2295
2740
|
if (typeof adapter.emitHandoff === "function") {
|
|
2296
2741
|
return adapter.emitHandoff(ctx, hostPayload);
|
|
@@ -2333,7 +2778,7 @@ async function probeOnly(opts) {
|
|
|
2333
2778
|
async function handoff(opts) {
|
|
2334
2779
|
let project;
|
|
2335
2780
|
try {
|
|
2336
|
-
project =
|
|
2781
|
+
project = loadProjectInfo7(process.cwd());
|
|
2337
2782
|
} catch {
|
|
2338
2783
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2339
2784
|
}
|
|
@@ -2382,7 +2827,7 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
|
2382
2827
|
import {
|
|
2383
2828
|
detectActiveMethod as detectActiveMethod2,
|
|
2384
2829
|
latestVersionFromCache as latestVersionFromCache2,
|
|
2385
|
-
loadProjectInfo as
|
|
2830
|
+
loadProjectInfo as loadProjectInfo8,
|
|
2386
2831
|
NOIR_VERSION as NOIR_VERSION2,
|
|
2387
2832
|
readInstallRecord as readInstallRecord2,
|
|
2388
2833
|
readUpdateCache as readUpdateCache2
|
|
@@ -2400,10 +2845,12 @@ var NOIR_BLOCK = [
|
|
|
2400
2845
|
];
|
|
2401
2846
|
var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
2402
2847
|
var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
2848
|
+
var BLOCK_WIDTH = NOIR_BLOCK.reduce((max, line) => Math.max(max, displayWidth(line)), 0);
|
|
2849
|
+
var BLOCK_MIN_WIDTH = BLOCK_WIDTH + 17;
|
|
2403
2850
|
function renderBanner(opts = {}) {
|
|
2404
2851
|
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
2405
2852
|
const colorOn = opts.color !== false && useColor();
|
|
2406
|
-
if (width <
|
|
2853
|
+
if (width < BLOCK_MIN_WIDTH) {
|
|
2407
2854
|
return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
|
|
2408
2855
|
}
|
|
2409
2856
|
const block = NOIR_BLOCK.join("\n");
|
|
@@ -2710,7 +3157,7 @@ async function resolveSections(commands) {
|
|
|
2710
3157
|
// src/commands/home.ts
|
|
2711
3158
|
function tryProject() {
|
|
2712
3159
|
try {
|
|
2713
|
-
const info2 =
|
|
3160
|
+
const info2 = loadProjectInfo8(process.cwd());
|
|
2714
3161
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
2715
3162
|
} catch {
|
|
2716
3163
|
return null;
|
|
@@ -2874,7 +3321,7 @@ async function runMenu(opts, deps) {
|
|
|
2874
3321
|
env: process.env,
|
|
2875
3322
|
configUpdate: (() => {
|
|
2876
3323
|
try {
|
|
2877
|
-
const info2 =
|
|
3324
|
+
const info2 = loadProjectInfo8(process.cwd());
|
|
2878
3325
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
2879
3326
|
} catch {
|
|
2880
3327
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -2923,13 +3370,13 @@ ${COMMANDS_HINT}
|
|
|
2923
3370
|
}
|
|
2924
3371
|
|
|
2925
3372
|
// src/commands/memory.ts
|
|
2926
|
-
import { readFileSync as
|
|
2927
|
-
import { loadProjectInfo as
|
|
3373
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3374
|
+
import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
|
|
2928
3375
|
function resolveWorkspace() {
|
|
2929
3376
|
const routing = workspaceRoutingTarget(process.cwd());
|
|
2930
3377
|
if (routing === null) return null;
|
|
2931
3378
|
try {
|
|
2932
|
-
return { routing, project:
|
|
3379
|
+
return { routing, project: loadProjectInfo9(process.cwd()) };
|
|
2933
3380
|
} catch {
|
|
2934
3381
|
return null;
|
|
2935
3382
|
}
|
|
@@ -3141,14 +3588,14 @@ async function resolveCaptureContent(opts) {
|
|
|
3141
3588
|
if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
|
|
3142
3589
|
if (typeof opts.file === "string" && opts.file.length > 0) {
|
|
3143
3590
|
try {
|
|
3144
|
-
return
|
|
3591
|
+
return readFileSync4(opts.file, "utf8");
|
|
3145
3592
|
} catch {
|
|
3146
3593
|
fail(EXIT.USAGE, `memory capture could not read --file ${opts.file}`, opts);
|
|
3147
3594
|
}
|
|
3148
3595
|
}
|
|
3149
3596
|
if (!process.stdin.isTTY) {
|
|
3150
3597
|
try {
|
|
3151
|
-
return
|
|
3598
|
+
return readFileSync4(0, "utf8");
|
|
3152
3599
|
} catch {
|
|
3153
3600
|
fail(
|
|
3154
3601
|
EXIT.USAGE,
|
|
@@ -3334,7 +3781,7 @@ function describe(step, who) {
|
|
|
3334
3781
|
const m = {
|
|
3335
3782
|
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
3336
3783
|
bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
|
|
3337
|
-
gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
|
|
3784
|
+
gate: "re-run the full six-step gate (lint/build/typecheck/test/docs:validate/hygiene:gate)",
|
|
3338
3785
|
commit: "commit chore(release) + push develop",
|
|
3339
3786
|
"ci-develop": "wait for CI on develop \u2192 green",
|
|
3340
3787
|
"beta-tag": "pnpm release:tag \u2192 beta tag + push",
|
|
@@ -3394,11 +3841,14 @@ async function release(opts) {
|
|
|
3394
3841
|
});
|
|
3395
3842
|
success(`bump: \u2713 version ${version}`, opts);
|
|
3396
3843
|
info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
|
|
3397
|
-
execSync(
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3844
|
+
execSync(
|
|
3845
|
+
"pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate && pnpm hygiene:gate",
|
|
3846
|
+
{
|
|
3847
|
+
encoding: "utf8",
|
|
3848
|
+
stdio: "inherit"
|
|
3849
|
+
}
|
|
3850
|
+
);
|
|
3851
|
+
success("gate: \u2713 all six gates green", opts);
|
|
3402
3852
|
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
3403
3853
|
spawnSync3("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
3404
3854
|
spawnSync3("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
@@ -4206,10 +4656,10 @@ var RunInterrupt = class {
|
|
|
4206
4656
|
};
|
|
4207
4657
|
|
|
4208
4658
|
// src/run-profiles.ts
|
|
4209
|
-
import { isDeniedEnvKey, loadProjectInfo as
|
|
4659
|
+
import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
|
|
4210
4660
|
function loadRunConfig(root) {
|
|
4211
4661
|
try {
|
|
4212
|
-
return
|
|
4662
|
+
return loadProjectInfo10(root).config;
|
|
4213
4663
|
} catch {
|
|
4214
4664
|
return null;
|
|
4215
4665
|
}
|
|
@@ -4356,6 +4806,7 @@ var RunStatusLine = class {
|
|
|
4356
4806
|
if (this.animated && this.sharesCursor && this.onRow) {
|
|
4357
4807
|
this.write("\n");
|
|
4358
4808
|
this.onRow = false;
|
|
4809
|
+
this.rendered = "";
|
|
4359
4810
|
}
|
|
4360
4811
|
this.stdoutAtRowStart = chunk.endsWith("\n");
|
|
4361
4812
|
}
|
|
@@ -4386,19 +4837,45 @@ var RunStatusLine = class {
|
|
|
4386
4837
|
return;
|
|
4387
4838
|
}
|
|
4388
4839
|
if (!this.onRow || !this.cursorFree()) return;
|
|
4389
|
-
this.write(
|
|
4840
|
+
this.write(this.erase(this.rendered));
|
|
4390
4841
|
this.rendered = "";
|
|
4391
4842
|
this.onRow = false;
|
|
4392
4843
|
}
|
|
4393
4844
|
/** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
|
|
4394
4845
|
draw() {
|
|
4395
4846
|
if (!this.animated || !this.cursorFree()) return;
|
|
4396
|
-
const text = this.render();
|
|
4847
|
+
const text = truncateToWidth(this.render(), terminalWidth());
|
|
4397
4848
|
if (text === this.rendered) return;
|
|
4398
|
-
this.write(
|
|
4849
|
+
this.write(`${this.erase(this.rendered)}${text}`);
|
|
4399
4850
|
this.rendered = text;
|
|
4400
4851
|
this.onRow = true;
|
|
4401
4852
|
}
|
|
4853
|
+
/**
|
|
4854
|
+
* The escape sequence that clears the status text currently on screen and
|
|
4855
|
+
* leaves the cursor at the start of the row it began on — where the next
|
|
4856
|
+
* render writes. An empty `previous` still clears the row, so a line that has
|
|
4857
|
+
* drawn nothing yet starts from a known-blank one.
|
|
4858
|
+
*
|
|
4859
|
+
* The extent is measured against the terminal width as it is NOW, not as it
|
|
4860
|
+
* was when the text was drawn. Nothing this line writes wraps, but text that
|
|
4861
|
+
* fitted a wider terminal does not stop existing when the window narrows: the
|
|
4862
|
+
* terminal folds it onto rows below, and clearing only the row the cursor
|
|
4863
|
+
* ends on would leave the rest above as residue. So the sequence walks back
|
|
4864
|
+
* up over every row the text occupies. That folding is the behaviour of the
|
|
4865
|
+
* common terminals (iTerm2, Terminal.app, GNOME Terminal and recent xterm);
|
|
4866
|
+
* one that clips the row instead has no continuation to clear.
|
|
4867
|
+
*
|
|
4868
|
+
* The caller must have forgotten the text once its row was abandoned —
|
|
4869
|
+
* `rendered` is only meaningful while `onRow` is true.
|
|
4870
|
+
*/
|
|
4871
|
+
erase(previous) {
|
|
4872
|
+
const rows = Math.max(1, Math.ceil(displayWidth(previous) / terminalWidth()));
|
|
4873
|
+
let sequence = "\r\x1B[K";
|
|
4874
|
+
for (let row = 1; row < rows; row++) {
|
|
4875
|
+
sequence += "\x1B[1A\r\x1B[K";
|
|
4876
|
+
}
|
|
4877
|
+
return sequence;
|
|
4878
|
+
}
|
|
4402
4879
|
/**
|
|
4403
4880
|
* Whether the cursor is at the start of a row nobody else is using. On stdout
|
|
4404
4881
|
* that is not this terminal there is no shared cursor to disturb; when it is,
|
|
@@ -4678,8 +5155,8 @@ function writeTranscript(host, lines, root = process.cwd()) {
|
|
|
4678
5155
|
}
|
|
4679
5156
|
|
|
4680
5157
|
// src/commands/skills.ts
|
|
4681
|
-
import { resolveAdapter as
|
|
4682
|
-
import { loadProjectInfo as
|
|
5158
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
5159
|
+
import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
|
|
4683
5160
|
import {
|
|
4684
5161
|
buildRegistry,
|
|
4685
5162
|
discoverAll,
|
|
@@ -4689,8 +5166,8 @@ import {
|
|
|
4689
5166
|
|
|
4690
5167
|
// src/init.ts
|
|
4691
5168
|
import { existsSync as existsSync4 } from "fs";
|
|
4692
|
-
import { resolveAdapter as
|
|
4693
|
-
import { loadProjectInfo as
|
|
5169
|
+
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
5170
|
+
import { loadProjectInfo as loadProjectInfo11, paths as paths3 } from "@noir-ai/core";
|
|
4694
5171
|
import { scaffold } from "@noir-ai/create";
|
|
4695
5172
|
import {
|
|
4696
5173
|
emitSkillsToDir
|
|
@@ -4798,7 +5275,7 @@ function labelFor(resolution) {
|
|
|
4798
5275
|
}
|
|
4799
5276
|
|
|
4800
5277
|
// src/dedup-write.ts
|
|
4801
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
5278
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
4802
5279
|
import { join as join5 } from "path";
|
|
4803
5280
|
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
4804
5281
|
var HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
|
|
@@ -4818,7 +5295,7 @@ async function checkWritePathDedup(root, res, opts) {
|
|
|
4818
5295
|
const proposedAbs = join5(root, proposedRel);
|
|
4819
5296
|
let proposedText;
|
|
4820
5297
|
try {
|
|
4821
|
-
proposedText =
|
|
5298
|
+
proposedText = readFileSync5(proposedAbs, "utf8");
|
|
4822
5299
|
} catch {
|
|
4823
5300
|
continue;
|
|
4824
5301
|
}
|
|
@@ -4958,7 +5435,7 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
|
|
|
4958
5435
|
return "replace";
|
|
4959
5436
|
} else if (choice === "mirror") {
|
|
4960
5437
|
try {
|
|
4961
|
-
const existing =
|
|
5438
|
+
const existing = readFileSync5(join5(root, matchedRel), "utf8");
|
|
4962
5439
|
writeFileSync5(
|
|
4963
5440
|
join5(root, matchedRel),
|
|
4964
5441
|
`${existing}
|
|
@@ -4982,7 +5459,7 @@ function cachePath(root) {
|
|
|
4982
5459
|
}
|
|
4983
5460
|
function loadCache(root) {
|
|
4984
5461
|
try {
|
|
4985
|
-
const raw =
|
|
5462
|
+
const raw = readFileSync5(cachePath(root), "utf8");
|
|
4986
5463
|
const parsed = JSON.parse(raw);
|
|
4987
5464
|
if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
|
|
4988
5465
|
if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
|
|
@@ -5014,7 +5491,7 @@ function makeCachedEmbed(cache, embed) {
|
|
|
5014
5491
|
}
|
|
5015
5492
|
function safeRead(abs) {
|
|
5016
5493
|
try {
|
|
5017
|
-
return
|
|
5494
|
+
return readFileSync5(abs, "utf8");
|
|
5018
5495
|
} catch {
|
|
5019
5496
|
return void 0;
|
|
5020
5497
|
}
|
|
@@ -5061,13 +5538,14 @@ async function init(root, opts) {
|
|
|
5061
5538
|
reportPlannedWrites(res);
|
|
5062
5539
|
return res;
|
|
5063
5540
|
}
|
|
5541
|
+
reportEnvHeal(res);
|
|
5064
5542
|
if (res.noop) return res;
|
|
5065
5543
|
const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
|
|
5066
5544
|
res.skillConflicts = skillSummary?.conflicts ?? [];
|
|
5067
5545
|
res.preservedSkills = skillSummary?.preserved ?? [];
|
|
5068
5546
|
let projectInfo;
|
|
5069
5547
|
try {
|
|
5070
|
-
projectInfo =
|
|
5548
|
+
projectInfo = loadProjectInfo11(root);
|
|
5071
5549
|
} catch {
|
|
5072
5550
|
projectInfo = void 0;
|
|
5073
5551
|
}
|
|
@@ -5098,7 +5576,7 @@ function resolveInitHost(root, opts) {
|
|
|
5098
5576
|
if (opts.host !== void 0) return opts.host;
|
|
5099
5577
|
if (!existsSync4(paths3.config(root))) return "claude";
|
|
5100
5578
|
try {
|
|
5101
|
-
return
|
|
5579
|
+
return loadProjectInfo11(root).config.host;
|
|
5102
5580
|
} catch {
|
|
5103
5581
|
process.stderr.write(
|
|
5104
5582
|
`Could not read the configured host from ${paths3.config(root)}; using 'claude'.
|
|
@@ -5129,8 +5607,13 @@ function reportPlannedWrites(res) {
|
|
|
5129
5607
|
for (const p of res.identical) log(` ${p}`);
|
|
5130
5608
|
}
|
|
5131
5609
|
}
|
|
5610
|
+
function reportEnvHeal(res) {
|
|
5611
|
+
if (res.envMode === "healed") {
|
|
5612
|
+
process.stderr.write("Tightened .noir/.env to 0600 (it was readable by group or others).\n");
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5132
5615
|
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
5133
|
-
const adapter =
|
|
5616
|
+
const adapter = resolveAdapter3(host);
|
|
5134
5617
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
5135
5618
|
if (skillsDir === void 0) {
|
|
5136
5619
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
@@ -5212,7 +5695,8 @@ var CATEGORY = {
|
|
|
5212
5695
|
"noir-backend": "domain",
|
|
5213
5696
|
"noir-doctor": "meta",
|
|
5214
5697
|
"noir-writing-skills": "meta",
|
|
5215
|
-
"noir-rules": "meta"
|
|
5698
|
+
"noir-rules": "meta",
|
|
5699
|
+
"noir-code-hygiene": "meta"
|
|
5216
5700
|
};
|
|
5217
5701
|
function categoryOf(name) {
|
|
5218
5702
|
const mapped = CATEGORY[name];
|
|
@@ -5225,13 +5709,6 @@ function toRow(s, kind = "builtin") {
|
|
|
5225
5709
|
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
5226
5710
|
return { name: s.name, category, description, kind, status: status2 };
|
|
5227
5711
|
}
|
|
5228
|
-
function truncate(text, max) {
|
|
5229
|
-
if (text.length <= max) return text;
|
|
5230
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
5231
|
-
const boundary = slice.lastIndexOf(" ");
|
|
5232
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
5233
|
-
return `${head.trimEnd()}\u2026`;
|
|
5234
|
-
}
|
|
5235
5712
|
async function skillsList(opts) {
|
|
5236
5713
|
let builtins;
|
|
5237
5714
|
let integrations;
|
|
@@ -5265,7 +5742,9 @@ async function skillsList(opts) {
|
|
|
5265
5742
|
Kind: r.kind,
|
|
5266
5743
|
Category: r.category,
|
|
5267
5744
|
Status: r.status,
|
|
5268
|
-
|
|
5745
|
+
// Display-only cut for the dense table cell; the `--json` payload always
|
|
5746
|
+
// carries the description whole (search snippets must never be truncated).
|
|
5747
|
+
Description: truncateToWidth(r.description, 80)
|
|
5269
5748
|
})),
|
|
5270
5749
|
["Skill", "Kind", "Category", "Status", "Description"],
|
|
5271
5750
|
opts
|
|
@@ -5275,11 +5754,11 @@ async function skillsSync(opts) {
|
|
|
5275
5754
|
const root = process.cwd();
|
|
5276
5755
|
let project;
|
|
5277
5756
|
try {
|
|
5278
|
-
project =
|
|
5757
|
+
project = loadProjectInfo12(root);
|
|
5279
5758
|
} catch {
|
|
5280
5759
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
5281
5760
|
}
|
|
5282
|
-
const adapter =
|
|
5761
|
+
const adapter = resolveAdapter4(project.config.host);
|
|
5283
5762
|
if (!adapter.skillsDir) {
|
|
5284
5763
|
const data2 = {
|
|
5285
5764
|
emitted: [],
|
|
@@ -5341,7 +5820,7 @@ async function skillsLint(opts) {
|
|
|
5341
5820
|
);
|
|
5342
5821
|
}
|
|
5343
5822
|
const errored = skills.filter((s) => s.errors.length > 0);
|
|
5344
|
-
const warned = skills.filter((s) => s.warnings.length > 0
|
|
5823
|
+
const warned = skills.filter((s) => s.warnings.length > 0);
|
|
5345
5824
|
const data = { count: skills.length, errored: errored.length, skills };
|
|
5346
5825
|
if (errored.length > 0) {
|
|
5347
5826
|
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
@@ -5415,11 +5894,15 @@ async function skillsRegistry(opts) {
|
|
|
5415
5894
|
}
|
|
5416
5895
|
|
|
5417
5896
|
// src/serve.ts
|
|
5418
|
-
import { applyNoirEnv, loadProjectInfo as
|
|
5897
|
+
import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
5419
5898
|
import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
|
|
5420
5899
|
async function serve(opts) {
|
|
5421
5900
|
applyNoirEnv(process.cwd());
|
|
5422
|
-
|
|
5901
|
+
if (opts.workspace !== void 0) {
|
|
5902
|
+
await bridgeStdioToWorkspace(opts.workspace, process.cwd());
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
const project = loadProjectInfo13(process.cwd());
|
|
5423
5906
|
if (opts.stdio) {
|
|
5424
5907
|
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
5425
5908
|
return;
|
|
@@ -5549,6 +6032,8 @@ function createProgram() {
|
|
|
5549
6032
|
const nonInteractive = opts.json === true || opts.input === false;
|
|
5550
6033
|
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
5551
6034
|
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
6035
|
+
if (opts.quiet === true) process.env.NOIR_QUIET = "1";
|
|
6036
|
+
else delete process.env.NOIR_QUIET;
|
|
5552
6037
|
emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
|
|
5553
6038
|
});
|
|
5554
6039
|
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(
|
|
@@ -5596,7 +6081,7 @@ function createProgram() {
|
|
|
5596
6081
|
const force = opts.force === true;
|
|
5597
6082
|
const host = parseHost(opts.host);
|
|
5598
6083
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
5599
|
-
const { create } = await import("./create-
|
|
6084
|
+
const { create } = await import("./create-ZZN7XFLG.js");
|
|
5600
6085
|
const result = await create(dir, {
|
|
5601
6086
|
transport,
|
|
5602
6087
|
url: opts.url,
|
|
@@ -5635,7 +6120,7 @@ function createProgram() {
|
|
|
5635
6120
|
).choices(SUPPORTED_HOSTS2)
|
|
5636
6121
|
).action(
|
|
5637
6122
|
async (opts, cmd) => {
|
|
5638
|
-
const { sync } = await import("./sync-
|
|
6123
|
+
const { sync } = await import("./sync-GHEPFPLX.js");
|
|
5639
6124
|
const host = parseHost(opts.host);
|
|
5640
6125
|
const force = opts.force === true;
|
|
5641
6126
|
const merge = opts.merge === true;
|
|
@@ -5654,11 +6139,17 @@ function createProgram() {
|
|
|
5654
6139
|
}
|
|
5655
6140
|
);
|
|
5656
6141
|
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
5657
|
-
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").
|
|
5658
|
-
|
|
6142
|
+
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").option(
|
|
6143
|
+
"--workspace <name>",
|
|
6144
|
+
"serve a shared workspace over stdio, bridging to that workspace daemon"
|
|
6145
|
+
).action(async (opts) => {
|
|
6146
|
+
await serve({
|
|
6147
|
+
stdio: opts.stdio === true,
|
|
6148
|
+
...typeof opts.workspace === "string" ? { workspace: opts.workspace } : {}
|
|
6149
|
+
});
|
|
5659
6150
|
});
|
|
5660
6151
|
mcpCmd.action(() => {
|
|
5661
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
6152
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio] [--workspace <name>]");
|
|
5662
6153
|
});
|
|
5663
6154
|
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
5664
6155
|
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(
|
|
@@ -5759,10 +6250,16 @@ function createProgram() {
|
|
|
5759
6250
|
program2.command("doctor").description("environment + project health").option(
|
|
5760
6251
|
"--dedup",
|
|
5761
6252
|
"scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
|
|
5762
|
-
).action(async (...args) => {
|
|
6253
|
+
).option("--fix", "re-assert owner-only permissions on .noir/.env and the store DB + directory").action(async (...args) => {
|
|
5763
6254
|
const g = actionGlobals(args);
|
|
5764
|
-
const
|
|
5765
|
-
|
|
6255
|
+
const opts = trailingCmd(args).opts();
|
|
6256
|
+
const dedup = opts.dedup === true;
|
|
6257
|
+
const fix = opts.fix === true;
|
|
6258
|
+
await doctor({
|
|
6259
|
+
...toCliOptions(g),
|
|
6260
|
+
...dedup ? { dedup: true } : {},
|
|
6261
|
+
...fix ? { fix: true } : {}
|
|
6262
|
+
});
|
|
5766
6263
|
});
|
|
5767
6264
|
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
5768
6265
|
await status(toStatusOptions(actionGlobals(args)));
|
|
@@ -5955,7 +6452,7 @@ function createProgram() {
|
|
|
5955
6452
|
...check === void 0 || check.length === 0 ? {} : { check }
|
|
5956
6453
|
});
|
|
5957
6454
|
});
|
|
5958
|
-
taskGrp.command("research").description("
|
|
6455
|
+
taskGrp.command("research").description("show research status for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
5959
6456
|
const cmd = trailingCmd(args);
|
|
5960
6457
|
const g = cmd.optsWithGlobals();
|
|
5961
6458
|
const first = args[0];
|
|
@@ -6021,7 +6518,7 @@ function createProgram() {
|
|
|
6021
6518
|
"dismiss the migration banner for the current version (persists in install.json)"
|
|
6022
6519
|
).argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(
|
|
6023
6520
|
async (spec, opts, cmd) => {
|
|
6024
|
-
const { install } = await import("./install-
|
|
6521
|
+
const { install } = await import("./install-R5U2WTAQ.js");
|
|
6025
6522
|
const cli = cmd.optsWithGlobals();
|
|
6026
6523
|
await install({
|
|
6027
6524
|
...cli,
|
|
@@ -6034,7 +6531,7 @@ function createProgram() {
|
|
|
6034
6531
|
);
|
|
6035
6532
|
installCmd.alias("migrate");
|
|
6036
6533
|
program2.command("update").description("update Noir to the latest version via the active install method").option("--check", "check for a new version without changing anything").argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(async (spec, opts, cmd) => {
|
|
6037
|
-
const { update } = await import("./update-
|
|
6534
|
+
const { update } = await import("./update-ZFOLOSZ2.js");
|
|
6038
6535
|
const cli = cmd.optsWithGlobals();
|
|
6039
6536
|
await update({ ...cli, spec, check: opts.check === true });
|
|
6040
6537
|
});
|
|
@@ -6197,7 +6694,7 @@ function failDaemonDown(opts, cause) {
|
|
|
6197
6694
|
async function probeDaemon(opts = {}) {
|
|
6198
6695
|
const expectedProject = opts.project?.id ?? (() => {
|
|
6199
6696
|
try {
|
|
6200
|
-
return
|
|
6697
|
+
return loadProjectInfo14(process.cwd()).id;
|
|
6201
6698
|
} catch {
|
|
6202
6699
|
return void 0;
|
|
6203
6700
|
}
|
|
@@ -6211,7 +6708,7 @@ async function probeDaemon(opts = {}) {
|
|
|
6211
6708
|
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
6212
6709
|
return { running: false };
|
|
6213
6710
|
}
|
|
6214
|
-
if (!
|
|
6711
|
+
if (!pidAlive5(rec.pid)) {
|
|
6215
6712
|
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
6216
6713
|
`);
|
|
6217
6714
|
return { running: false };
|
|
@@ -6250,7 +6747,7 @@ async function probeDaemon(opts = {}) {
|
|
|
6250
6747
|
async function resolveDaemon(opts) {
|
|
6251
6748
|
let project;
|
|
6252
6749
|
try {
|
|
6253
|
-
project = opts.project ??
|
|
6750
|
+
project = opts.project ?? loadProjectInfo14(process.cwd());
|
|
6254
6751
|
} catch {
|
|
6255
6752
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6256
6753
|
}
|
|
@@ -6311,8 +6808,8 @@ async function resolveDaemon(opts) {
|
|
|
6311
6808
|
return { client, stop };
|
|
6312
6809
|
}
|
|
6313
6810
|
function httpTransport(url, scopeKey) {
|
|
6314
|
-
const token = scopeKey === null ? null :
|
|
6315
|
-
return new
|
|
6811
|
+
const token = scopeKey === null ? null : readDaemonToken3(scopeKey);
|
|
6812
|
+
return new StreamableHTTPClientTransport2(
|
|
6316
6813
|
new URL(url),
|
|
6317
6814
|
// Verified present: StreamableHTTPClientTransportOptions.requestInit?: RequestInit
|
|
6318
6815
|
token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}
|
|
@@ -6320,7 +6817,7 @@ function httpTransport(url, scopeKey) {
|
|
|
6320
6817
|
}
|
|
6321
6818
|
function callerScopeKey(opts) {
|
|
6322
6819
|
try {
|
|
6323
|
-
return (opts.project ??
|
|
6820
|
+
return (opts.project ?? loadProjectInfo14(process.cwd())).id;
|
|
6324
6821
|
} catch {
|
|
6325
6822
|
return null;
|
|
6326
6823
|
}
|
|
@@ -6350,7 +6847,7 @@ function failWorkspaceConnect(opts, routing, cause) {
|
|
|
6350
6847
|
}
|
|
6351
6848
|
fail(
|
|
6352
6849
|
EXIT.DAEMON_DOWN,
|
|
6353
|
-
`workspace ${JSON.stringify(routing.name)} daemon is running but rejected this client's token (HTTP 401) \u2014 its token file is ${
|
|
6850
|
+
`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.`,
|
|
6354
6851
|
opts
|
|
6355
6852
|
);
|
|
6356
6853
|
}
|
|
@@ -6421,7 +6918,7 @@ async function callDaemonTool(opts, name, args = {}) {
|
|
|
6421
6918
|
async function withInProcessRead(opts, fn) {
|
|
6422
6919
|
let project;
|
|
6423
6920
|
try {
|
|
6424
|
-
project = opts.project ??
|
|
6921
|
+
project = opts.project ?? loadProjectInfo14(process.cwd());
|
|
6425
6922
|
} catch {
|
|
6426
6923
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6427
6924
|
}
|
|
@@ -6468,14 +6965,14 @@ function workspaceRoutingTarget(root) {
|
|
|
6468
6965
|
const name = readWorkspaceMarker2(root);
|
|
6469
6966
|
if (name === null) return null;
|
|
6470
6967
|
try {
|
|
6471
|
-
return { name, projectId:
|
|
6968
|
+
return { name, projectId: loadProjectInfo14(root).id };
|
|
6472
6969
|
} catch {
|
|
6473
6970
|
return null;
|
|
6474
6971
|
}
|
|
6475
6972
|
}
|
|
6476
6973
|
async function probeWorkspaceDaemon(routing) {
|
|
6477
|
-
const rec =
|
|
6478
|
-
if (rec === null || !
|
|
6974
|
+
const rec = readWorkspaceDaemonRecord3(routing.name);
|
|
6975
|
+
if (rec === null || !pidAlive5(rec.pid)) return { running: false };
|
|
6479
6976
|
try {
|
|
6480
6977
|
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
6481
6978
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
@@ -6670,7 +7167,7 @@ function renderHuman2(p, opts) {
|
|
|
6670
7167
|
async function gatherStatusPayload(opts) {
|
|
6671
7168
|
let project;
|
|
6672
7169
|
try {
|
|
6673
|
-
project =
|
|
7170
|
+
project = loadProjectInfo15(process.cwd());
|
|
6674
7171
|
} catch {
|
|
6675
7172
|
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6676
7173
|
}
|
|
@@ -6700,7 +7197,7 @@ async function status(opts) {
|
|
|
6700
7197
|
env: process.env,
|
|
6701
7198
|
configUpdate: (() => {
|
|
6702
7199
|
try {
|
|
6703
|
-
const info2 =
|
|
7200
|
+
const info2 = loadProjectInfo15(process.cwd());
|
|
6704
7201
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
6705
7202
|
} catch {
|
|
6706
7203
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -6744,8 +7241,9 @@ export {
|
|
|
6744
7241
|
buildConflictOpts,
|
|
6745
7242
|
checkWritePathDedup,
|
|
6746
7243
|
reportPlannedWrites,
|
|
7244
|
+
reportEnvHeal,
|
|
6747
7245
|
preservedStaleLine,
|
|
6748
7246
|
assertTransportUrl,
|
|
6749
7247
|
buildPaletteCommandsForTui
|
|
6750
7248
|
};
|
|
6751
|
-
//# sourceMappingURL=chunk-
|
|
7249
|
+
//# sourceMappingURL=chunk-6AUAIEFG.js.map
|