@noir-ai/cli 1.11.2 → 1.12.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +3295 -575
- package/dist/bin.js.map +1 -1
- package/dist/index.js +6136 -6
- package/dist/index.js.map +1 -1
- package/dist/{chunk-QIVHIKYD.js → tui/chunk-2RK6AFZQ.js} +37 -16
- package/dist/tui/chunk-2RK6AFZQ.js.map +1 -0
- package/dist/tui/{chunk-R5YX44UL.js → chunk-ESZTOQEC.js} +211 -2
- package/dist/tui/chunk-ESZTOQEC.js.map +1 -0
- package/dist/tui/chunk-J77RHAWG.js +5010 -0
- package/dist/tui/chunk-J77RHAWG.js.map +1 -0
- package/dist/{create-VK2QMXYS.js → tui/create-BTYLBVV6.js} +4 -4
- package/dist/tui/{create-WKFVM237.js.map → create-BTYLBVV6.js.map} +1 -1
- package/dist/tui/index.js +45 -3454
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{install-JR6ZXNYA.js → install-YGTUSLRO.js} +2 -3
- package/dist/{sync-VXZKODQA.js → tui/sync-LRHPJ5VV.js} +4 -4
- package/dist/tui/{sync-DUWJAF7Z.js.map → sync-LRHPJ5VV.js.map} +1 -1
- package/dist/tui/{update-PGAM6OVL.js → update-LET5BWOU.js} +3 -4
- package/package.json +12 -12
- package/dist/chunk-A5W2E3X2.js +0 -33
- package/dist/chunk-A5W2E3X2.js.map +0 -1
- package/dist/chunk-CT6TYLHN.js +0 -1105
- package/dist/chunk-CT6TYLHN.js.map +0 -1
- package/dist/chunk-I2R55ACD.js +0 -369
- package/dist/chunk-I2R55ACD.js.map +0 -1
- package/dist/chunk-QIVHIKYD.js.map +0 -1
- package/dist/chunk-RON2XESF.js +0 -210
- package/dist/chunk-RON2XESF.js.map +0 -1
- package/dist/create-VK2QMXYS.js.map +0 -1
- package/dist/install-4U4UDDNE.js +0 -13
- package/dist/sync-VXZKODQA.js.map +0 -1
- package/dist/tui/chunk-F6XRS72N.js +0 -1106
- package/dist/tui/chunk-F6XRS72N.js.map +0 -1
- package/dist/tui/chunk-NLK6UNIA.js +0 -223
- package/dist/tui/chunk-NLK6UNIA.js.map +0 -1
- package/dist/tui/chunk-QF6777NJ.js +0 -209
- package/dist/tui/chunk-QF6777NJ.js.map +0 -1
- package/dist/tui/chunk-R5YX44UL.js.map +0 -1
- package/dist/tui/create-WKFVM237.js +0 -79
- package/dist/tui/sync-DUWJAF7Z.js +0 -90
- package/dist/tui/update-PGAM6OVL.js.map +0 -1
- package/dist/update-ZDAVLOXB.js +0 -20
- package/dist/update-ZDAVLOXB.js.map +0 -1
- /package/dist/{install-4U4UDDNE.js.map → tui/install-YGTUSLRO.js.map} +0 -0
- /package/dist/tui/{install-JR6ZXNYA.js.map → update-LET5BWOU.js.map} +0 -0
package/dist/bin.js
CHANGED
|
@@ -1,46 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
EXIT,
|
|
17
|
-
NoirCliError,
|
|
18
|
-
accessibleMode,
|
|
19
|
-
badge,
|
|
20
|
-
c,
|
|
21
|
-
definitionList,
|
|
22
|
-
fail,
|
|
23
|
-
handleError,
|
|
24
|
-
inferExitCode,
|
|
25
|
-
info,
|
|
26
|
-
isInteractive,
|
|
27
|
-
json,
|
|
28
|
-
log,
|
|
29
|
-
requireInteractive,
|
|
30
|
-
spinner,
|
|
31
|
-
success,
|
|
32
|
-
table,
|
|
33
|
-
tip,
|
|
34
|
-
useColor,
|
|
35
|
-
warn
|
|
36
|
-
} from "./chunk-I2R55ACD.js";
|
|
37
|
-
|
|
38
|
-
// src/bin.ts
|
|
39
|
-
import { realpathSync } from "fs";
|
|
40
|
-
import { pathToFileURL } from "url";
|
|
41
|
-
import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
|
|
42
|
-
import { NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
43
|
-
import { Command, Option } from "commander";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
44
16
|
|
|
45
17
|
// src/daemon-client.ts
|
|
46
18
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
@@ -55,8 +27,6 @@ import {
|
|
|
55
27
|
import { createMemoryEngine } from "@noir-ai/memory";
|
|
56
28
|
import { openStore } from "@noir-ai/store";
|
|
57
29
|
import { WorkflowEngine } from "@noir-ai/workflow";
|
|
58
|
-
var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
59
|
-
var PROBE_TIMEOUT_MS = 1500;
|
|
60
30
|
function describeCause(cause) {
|
|
61
31
|
if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
|
|
62
32
|
try {
|
|
@@ -93,8 +63,21 @@ async function probeDaemon(opts = {}) {
|
|
|
93
63
|
return { running: false };
|
|
94
64
|
}
|
|
95
65
|
const body = await res.json().catch(() => null);
|
|
96
|
-
|
|
97
|
-
|
|
66
|
+
const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
|
|
67
|
+
const expectedProject = opts.project?.id ?? (() => {
|
|
68
|
+
try {
|
|
69
|
+
return loadProjectInfo(process.cwd()).id;
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
const projectOk = expectedProject === void 0 || body?.projectId !== void 0 && body.projectId === expectedProject;
|
|
75
|
+
if (body?.ok !== true || !pidOk || !projectOk) {
|
|
76
|
+
if (opts.verbose)
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
!pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : !projectOk ? `noir: daemon probe: daemon serves a different project (${body?.projectId ?? "unknown"}); using in-process fallback
|
|
79
|
+
` : "noir: daemon probe: /health body not ok\n"
|
|
80
|
+
);
|
|
98
81
|
return { running: false };
|
|
99
82
|
}
|
|
100
83
|
return {
|
|
@@ -111,7 +94,12 @@ async function probeDaemon(opts = {}) {
|
|
|
111
94
|
}
|
|
112
95
|
}
|
|
113
96
|
async function resolveDaemon(opts) {
|
|
114
|
-
|
|
97
|
+
let project;
|
|
98
|
+
try {
|
|
99
|
+
project = opts.project ?? loadProjectInfo(process.cwd());
|
|
100
|
+
} catch {
|
|
101
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
102
|
+
}
|
|
115
103
|
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
116
104
|
try {
|
|
117
105
|
const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
|
|
@@ -202,8 +190,22 @@ async function callDaemonTool(opts, name, args = {}) {
|
|
|
202
190
|
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
203
191
|
}
|
|
204
192
|
async function withInProcessRead(opts, fn) {
|
|
205
|
-
|
|
206
|
-
|
|
193
|
+
let project;
|
|
194
|
+
try {
|
|
195
|
+
project = opts.project ?? loadProjectInfo(process.cwd());
|
|
196
|
+
} catch {
|
|
197
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
198
|
+
}
|
|
199
|
+
let store;
|
|
200
|
+
try {
|
|
201
|
+
store = await openStore({ projectId: project.id, root: project.root, readonly: true });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
fail(
|
|
204
|
+
EXIT.ERROR,
|
|
205
|
+
`could not open the store read-only: ${err instanceof Error ? err.message : String(err)}`,
|
|
206
|
+
opts
|
|
207
|
+
);
|
|
208
|
+
}
|
|
207
209
|
try {
|
|
208
210
|
const embedderCfg = resolveEmbedderConfig(project.config.context);
|
|
209
211
|
const embed = createEmbedFn(embedderCfg).embed;
|
|
@@ -233,6 +235,368 @@ async function withInProcessRead(opts, fn) {
|
|
|
233
235
|
});
|
|
234
236
|
}
|
|
235
237
|
}
|
|
238
|
+
var DAEMON_DOWN_HINT, PROBE_TIMEOUT_MS;
|
|
239
|
+
var init_daemon_client = __esm({
|
|
240
|
+
"src/daemon-client.ts"() {
|
|
241
|
+
"use strict";
|
|
242
|
+
init_bin();
|
|
243
|
+
DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
244
|
+
PROBE_TIMEOUT_MS = 1500;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// src/theme.ts
|
|
249
|
+
import pc from "picocolors";
|
|
250
|
+
function badge(state, label = state) {
|
|
251
|
+
const text = `${BADGE_SYMBOL[state]} ${label}`;
|
|
252
|
+
switch (state) {
|
|
253
|
+
case "ok":
|
|
254
|
+
return c.ok(text);
|
|
255
|
+
case "warn":
|
|
256
|
+
return c.warn(text);
|
|
257
|
+
case "error":
|
|
258
|
+
return c.error(text);
|
|
259
|
+
case "info":
|
|
260
|
+
return c.info(text);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function envFlagSet(name) {
|
|
264
|
+
const v = process.env[name];
|
|
265
|
+
return v !== void 0 && v !== "";
|
|
266
|
+
}
|
|
267
|
+
function isCiEnv() {
|
|
268
|
+
const v = process.env.CI;
|
|
269
|
+
if (v === void 0 || v === "") return false;
|
|
270
|
+
return v !== "0" && v !== "false";
|
|
271
|
+
}
|
|
272
|
+
function useColor() {
|
|
273
|
+
if (envFlagSet("NO_COLOR")) return false;
|
|
274
|
+
if (process.env.CLICOLOR_FORCE === "1") return true;
|
|
275
|
+
if (isCiEnv()) return false;
|
|
276
|
+
return pc.isColorSupported;
|
|
277
|
+
}
|
|
278
|
+
function accessibleMode() {
|
|
279
|
+
return envFlagSet("NOIR_ACCESSIBLE");
|
|
280
|
+
}
|
|
281
|
+
function terminalWidth() {
|
|
282
|
+
const fromEnv = process.env.COLUMNS;
|
|
283
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
284
|
+
const n = Number(fromEnv);
|
|
285
|
+
if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
|
|
286
|
+
}
|
|
287
|
+
const cols = process.stdout.columns;
|
|
288
|
+
if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
|
|
289
|
+
return 80;
|
|
290
|
+
}
|
|
291
|
+
var FORCE, c, BADGE_SYMBOL;
|
|
292
|
+
var init_theme = __esm({
|
|
293
|
+
"src/theme.ts"() {
|
|
294
|
+
"use strict";
|
|
295
|
+
FORCE = pc.createColors(true);
|
|
296
|
+
c = {
|
|
297
|
+
/** Green — success / healthy. */
|
|
298
|
+
ok: (s) => useColor() ? FORCE.green(s) : s,
|
|
299
|
+
/** Yellow — degraded but usable. */
|
|
300
|
+
warn: (s) => useColor() ? FORCE.yellow(s) : s,
|
|
301
|
+
/** Red — reserved strictly for ERROR. Never a table header. */
|
|
302
|
+
error: (s) => useColor() ? FORCE.red(s) : s,
|
|
303
|
+
/** Cyan — informational / brand-neutral accent for labels. */
|
|
304
|
+
info: (s) => useColor() ? FORCE.cyan(s) : s,
|
|
305
|
+
/** Blue — the Midnight Cobalt brand role (banner / accent). */
|
|
306
|
+
accent: (s) => useColor() ? FORCE.blue(s) : s,
|
|
307
|
+
/** Dim — secondary text, table borders when color is on. */
|
|
308
|
+
dim: (s) => useColor() ? FORCE.dim(s) : s,
|
|
309
|
+
/** Bold — emphasis (table headers compose this with `info`). */
|
|
310
|
+
bold: (s) => useColor() ? FORCE.bold(s) : s,
|
|
311
|
+
/** Reverse video (SGR 7) — inverted background for the active list row. */
|
|
312
|
+
inverse: (s) => useColor() ? FORCE.inverse(s) : s
|
|
313
|
+
};
|
|
314
|
+
BADGE_SYMBOL = {
|
|
315
|
+
ok: "\u2713",
|
|
316
|
+
warn: "\u26A0",
|
|
317
|
+
error: "\u2717",
|
|
318
|
+
info: "\u2139"
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// src/output.ts
|
|
324
|
+
import Table from "cli-table3";
|
|
325
|
+
import { CommanderError } from "commander";
|
|
326
|
+
import ora from "ora";
|
|
327
|
+
function isJsonMode(opts) {
|
|
328
|
+
return opts.json === true;
|
|
329
|
+
}
|
|
330
|
+
function isQuietMode(opts) {
|
|
331
|
+
return opts.quiet === true;
|
|
332
|
+
}
|
|
333
|
+
function isNoInput(opts = {}) {
|
|
334
|
+
return opts.noInput === true || opts.input === false;
|
|
335
|
+
}
|
|
336
|
+
function envFlagSet2(name) {
|
|
337
|
+
const v = process.env[name];
|
|
338
|
+
return v !== void 0 && v !== "";
|
|
339
|
+
}
|
|
340
|
+
function isInteractive(opts = {}) {
|
|
341
|
+
if (isJsonMode(opts) || isNoInput(opts)) return false;
|
|
342
|
+
if (isCiEnv() || envFlagSet2("NO_COLOR")) return false;
|
|
343
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
344
|
+
}
|
|
345
|
+
function resolveInteractive() {
|
|
346
|
+
if (flaggedNonInteractiveCli()) return false;
|
|
347
|
+
return isInteractive();
|
|
348
|
+
}
|
|
349
|
+
function flaggedNonInteractiveCli() {
|
|
350
|
+
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
351
|
+
return v !== void 0 && v !== "";
|
|
352
|
+
}
|
|
353
|
+
function json(obj) {
|
|
354
|
+
process.stdout.write(`${JSON.stringify(obj)}
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
function log(msg2, opts = {}) {
|
|
358
|
+
if (isJsonMode(opts) || isQuietMode(opts)) return;
|
|
359
|
+
process.stderr.write(`${msg2}
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
function info(msg2, opts = {}) {
|
|
363
|
+
if (isJsonMode(opts) || isQuietMode(opts)) return;
|
|
364
|
+
process.stderr.write(`${c.info(msg2)}
|
|
365
|
+
`);
|
|
366
|
+
}
|
|
367
|
+
function success(msg2, opts = {}) {
|
|
368
|
+
if (isJsonMode(opts) || isQuietMode(opts)) return;
|
|
369
|
+
process.stderr.write(`${c.ok(msg2)}
|
|
370
|
+
`);
|
|
371
|
+
}
|
|
372
|
+
function warn(msg2, opts = {}) {
|
|
373
|
+
if (isJsonMode(opts)) return;
|
|
374
|
+
process.stderr.write(`${c.warn(msg2)}
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
function error(msg2, opts = {}) {
|
|
378
|
+
if (isJsonMode(opts)) return;
|
|
379
|
+
process.stderr.write(`${c.error(msg2)}
|
|
380
|
+
`);
|
|
381
|
+
}
|
|
382
|
+
function tip(msg2, opts = {}) {
|
|
383
|
+
if (isJsonMode(opts) || opts.noTips === true) return;
|
|
384
|
+
process.stderr.write(`${c.warn(msg2)}
|
|
385
|
+
`);
|
|
386
|
+
}
|
|
387
|
+
function table(rows, cols, opts = {}) {
|
|
388
|
+
if (isJsonMode(opts)) return;
|
|
389
|
+
if (rows.length === 0) {
|
|
390
|
+
info("(no rows)", opts);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const t = new Table({
|
|
394
|
+
head: cols.map((col) => c.bold(c.info(col))),
|
|
395
|
+
// Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
|
|
396
|
+
// default red header nor a colored border). All color is ours, via theme.
|
|
397
|
+
style: { head: [], border: [] },
|
|
398
|
+
colWidths: computeColWidths(rows, cols),
|
|
399
|
+
wordWrap: true,
|
|
400
|
+
truncate: "\u2026"
|
|
401
|
+
});
|
|
402
|
+
for (const row of rows) {
|
|
403
|
+
t.push(cols.map((col) => formatCell(row[col])));
|
|
404
|
+
}
|
|
405
|
+
process.stderr.write(`${t.toString()}
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
function computeColWidths(rows, cols) {
|
|
409
|
+
const n = cols.length;
|
|
410
|
+
if (n === 0) return [];
|
|
411
|
+
const headerLen = cols.map((col) => col.length);
|
|
412
|
+
const natural = cols.map((col, i) => {
|
|
413
|
+
let max = Math.max(headerLen[i] ?? col.length, 3);
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
const len = formatCell(row[col]).length;
|
|
416
|
+
if (len > max) max = len;
|
|
417
|
+
}
|
|
418
|
+
return max;
|
|
419
|
+
});
|
|
420
|
+
const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
|
|
421
|
+
const naturalSum = natural.reduce((a, b) => a + b, 0);
|
|
422
|
+
if (naturalSum <= contentBudget) {
|
|
423
|
+
return natural.map((w) => w + 2);
|
|
424
|
+
}
|
|
425
|
+
const content = natural.slice();
|
|
426
|
+
let sum = naturalSum;
|
|
427
|
+
const floor = headerLen.map((h) => Math.max(h, 3));
|
|
428
|
+
while (sum > contentBudget) {
|
|
429
|
+
let idx = -1;
|
|
430
|
+
let widest = -1;
|
|
431
|
+
for (let i = 0; i < n; i++) {
|
|
432
|
+
const c2 = content[i] ?? 0;
|
|
433
|
+
const f = floor[i] ?? 3;
|
|
434
|
+
if (c2 > f && c2 > widest) {
|
|
435
|
+
widest = c2;
|
|
436
|
+
idx = i;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (idx === -1) break;
|
|
440
|
+
const overshoot = sum - contentBudget;
|
|
441
|
+
const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
|
|
442
|
+
const cut = Math.min(overshoot, reducible);
|
|
443
|
+
content[idx] = (content[idx] ?? 0) - cut;
|
|
444
|
+
sum -= cut;
|
|
445
|
+
}
|
|
446
|
+
return content.map((w) => w + 2);
|
|
447
|
+
}
|
|
448
|
+
function formatCell(value) {
|
|
449
|
+
if (value === void 0 || value === null) return "";
|
|
450
|
+
if (typeof value === "string") return value;
|
|
451
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
452
|
+
return String(value);
|
|
453
|
+
}
|
|
454
|
+
return JSON.stringify(value);
|
|
455
|
+
}
|
|
456
|
+
function definitionList(rows, opts = {}) {
|
|
457
|
+
table(
|
|
458
|
+
rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
|
|
459
|
+
["Field", "Value"],
|
|
460
|
+
opts
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function spinner(text = "", opts = {}) {
|
|
464
|
+
if (isJsonMode(opts) || isQuietMode(opts) || !isInteractive(opts)) {
|
|
465
|
+
return new NoopSpinner();
|
|
466
|
+
}
|
|
467
|
+
return new OraSpinner(text);
|
|
468
|
+
}
|
|
469
|
+
function fail(exitCode, message, opts = {}) {
|
|
470
|
+
if (isJsonMode(opts)) {
|
|
471
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
|
|
472
|
+
`);
|
|
473
|
+
} else if (message.length > 0) {
|
|
474
|
+
process.stderr.write(`${message}
|
|
475
|
+
`);
|
|
476
|
+
}
|
|
477
|
+
throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
|
|
478
|
+
}
|
|
479
|
+
function requireInteractive(opts, hint) {
|
|
480
|
+
if (!isInteractive(opts)) {
|
|
481
|
+
fail(
|
|
482
|
+
EXIT.USAGE,
|
|
483
|
+
`${hint} needs an interactive terminal, but input is disabled (non-TTY, --no-input, --json, CI, or NO_COLOR). Re-run in a TTY or supply the value as a flag.`,
|
|
484
|
+
opts
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function commanderExitCode(err) {
|
|
489
|
+
switch (err.code) {
|
|
490
|
+
case "commander.helpDisplayed":
|
|
491
|
+
case "commander.versionDisplayed":
|
|
492
|
+
case "commander.version":
|
|
493
|
+
return EXIT.OK;
|
|
494
|
+
case "commander.unknownCommand":
|
|
495
|
+
return EXIT.NOT_FOUND;
|
|
496
|
+
default:
|
|
497
|
+
return EXIT.USAGE;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function inferExitCode(err) {
|
|
501
|
+
if (err instanceof NoirCliError) return err.exitCode;
|
|
502
|
+
if (err instanceof CommanderError) {
|
|
503
|
+
if (err.code.startsWith("noir.")) return err.exitCode;
|
|
504
|
+
return commanderExitCode(err);
|
|
505
|
+
}
|
|
506
|
+
return EXIT.ERROR;
|
|
507
|
+
}
|
|
508
|
+
function handleError(err) {
|
|
509
|
+
if (err instanceof NoirCliError) {
|
|
510
|
+
if (err.message.length > 0) process.stderr.write(`${err.message}
|
|
511
|
+
`);
|
|
512
|
+
} else if (err instanceof CommanderError) {
|
|
513
|
+
} else {
|
|
514
|
+
process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}
|
|
515
|
+
`);
|
|
516
|
+
}
|
|
517
|
+
process.exitCode = inferExitCode(err);
|
|
518
|
+
}
|
|
519
|
+
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner;
|
|
520
|
+
var init_output = __esm({
|
|
521
|
+
"src/output.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
init_theme();
|
|
524
|
+
EXIT = {
|
|
525
|
+
OK: 0,
|
|
526
|
+
ERROR: 1,
|
|
527
|
+
USAGE: 2,
|
|
528
|
+
NOT_FOUND: 3,
|
|
529
|
+
DAEMON_DOWN: 4,
|
|
530
|
+
CANCELLED: 5
|
|
531
|
+
};
|
|
532
|
+
NoirCliError = class extends Error {
|
|
533
|
+
exitCode;
|
|
534
|
+
constructor(exitCode, message) {
|
|
535
|
+
super(message);
|
|
536
|
+
this.name = "NoirCliError";
|
|
537
|
+
this.exitCode = exitCode;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
NOIR_ERROR_CODE = "noir.error";
|
|
541
|
+
NoopSpinner = class {
|
|
542
|
+
text = "";
|
|
543
|
+
start(_text) {
|
|
544
|
+
return this;
|
|
545
|
+
}
|
|
546
|
+
stop() {
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
clear() {
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
succeed(_text) {
|
|
553
|
+
}
|
|
554
|
+
fail(_text) {
|
|
555
|
+
}
|
|
556
|
+
warn(_text) {
|
|
557
|
+
}
|
|
558
|
+
info(_text) {
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
OraSpinner = class {
|
|
562
|
+
handle;
|
|
563
|
+
constructor(initialText) {
|
|
564
|
+
this.handle = ora(initialText);
|
|
565
|
+
}
|
|
566
|
+
get text() {
|
|
567
|
+
return this.handle.text;
|
|
568
|
+
}
|
|
569
|
+
set text(value) {
|
|
570
|
+
this.handle.text = value;
|
|
571
|
+
}
|
|
572
|
+
start(text) {
|
|
573
|
+
if (text !== void 0) this.handle.text = text;
|
|
574
|
+
this.handle.start();
|
|
575
|
+
return this;
|
|
576
|
+
}
|
|
577
|
+
stop() {
|
|
578
|
+
this.handle.stop();
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
clear() {
|
|
582
|
+
this.handle.clear();
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
succeed(text) {
|
|
586
|
+
this.handle.succeed(text);
|
|
587
|
+
}
|
|
588
|
+
fail(text) {
|
|
589
|
+
this.handle.fail(text);
|
|
590
|
+
}
|
|
591
|
+
warn(text) {
|
|
592
|
+
this.handle.warn(text);
|
|
593
|
+
}
|
|
594
|
+
info(text) {
|
|
595
|
+
this.handle.info(text);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
});
|
|
236
600
|
|
|
237
601
|
// src/commands/context.ts
|
|
238
602
|
function parseLimit(raw, label, opts) {
|
|
@@ -349,8 +713,8 @@ async function contextIndex(opts) {
|
|
|
349
713
|
}
|
|
350
714
|
renderIndex(data, opts.paths, opts);
|
|
351
715
|
}
|
|
352
|
-
function renderIndex(data,
|
|
353
|
-
const scope =
|
|
716
|
+
function renderIndex(data, paths3, opts) {
|
|
717
|
+
const scope = paths3 && paths3.length > 0 ? paths3.join(", ") : ".";
|
|
354
718
|
const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
|
|
355
719
|
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
356
720
|
definitionList(
|
|
@@ -398,6 +762,14 @@ function renderStatus(data, opts) {
|
|
|
398
762
|
opts
|
|
399
763
|
);
|
|
400
764
|
}
|
|
765
|
+
var init_context = __esm({
|
|
766
|
+
"src/commands/context.ts"() {
|
|
767
|
+
"use strict";
|
|
768
|
+
init_daemon_client();
|
|
769
|
+
init_output();
|
|
770
|
+
init_theme();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
401
773
|
|
|
402
774
|
// src/commands/daemon.ts
|
|
403
775
|
import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
|
|
@@ -408,23 +780,41 @@ import {
|
|
|
408
780
|
readDaemonRecord as readDaemonRecord2,
|
|
409
781
|
spawnDetachedDaemon
|
|
410
782
|
} from "@noir-ai/daemon";
|
|
411
|
-
var MODE = "foreground";
|
|
412
783
|
function formatUptime(sec) {
|
|
413
784
|
if (!Number.isFinite(sec) || sec < 0) return "unknown";
|
|
414
785
|
if (sec < 60) return `${sec}s`;
|
|
415
786
|
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
416
787
|
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
417
788
|
}
|
|
418
|
-
|
|
789
|
+
function resolveCallerProjectId() {
|
|
419
790
|
try {
|
|
420
|
-
|
|
421
|
-
|
|
791
|
+
return loadProjectInfo2(process.cwd()).id;
|
|
792
|
+
} catch {
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function isHealthy(port, expectedPid, expectedProjectId) {
|
|
797
|
+
try {
|
|
798
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
799
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
800
|
+
});
|
|
801
|
+
if (res.status !== 200) return false;
|
|
802
|
+
if (expectedPid === void 0) return true;
|
|
803
|
+
const body = await res.json();
|
|
804
|
+
if (body?.ok !== true || body.pid !== expectedPid) return false;
|
|
805
|
+
if (expectedProjectId !== void 0 && body.projectId !== expectedProjectId) return false;
|
|
806
|
+
return true;
|
|
422
807
|
} catch {
|
|
423
808
|
return false;
|
|
424
809
|
}
|
|
425
810
|
}
|
|
426
811
|
async function daemonStart(opts) {
|
|
427
|
-
|
|
812
|
+
let project;
|
|
813
|
+
try {
|
|
814
|
+
project = loadProjectInfo2(process.cwd());
|
|
815
|
+
} catch {
|
|
816
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
817
|
+
}
|
|
428
818
|
if (opts.detachChild === true) {
|
|
429
819
|
const child = await ensureDaemonRunning2({
|
|
430
820
|
project,
|
|
@@ -452,7 +842,7 @@ async function daemonStart(opts) {
|
|
|
452
842
|
}
|
|
453
843
|
if (opts.detach === true) {
|
|
454
844
|
const existing = readDaemonRecord2();
|
|
455
|
-
if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port)) {
|
|
845
|
+
if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
|
|
456
846
|
if (opts.json === true) {
|
|
457
847
|
process.stdout.write(
|
|
458
848
|
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
@@ -466,7 +856,16 @@ async function daemonStart(opts) {
|
|
|
466
856
|
);
|
|
467
857
|
return;
|
|
468
858
|
}
|
|
469
|
-
|
|
859
|
+
let spawned;
|
|
860
|
+
try {
|
|
861
|
+
spawned = await spawnDetachedDaemon({ project });
|
|
862
|
+
} catch (err) {
|
|
863
|
+
fail(
|
|
864
|
+
EXIT.ERROR,
|
|
865
|
+
`noir daemon start --detach failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
866
|
+
opts
|
|
867
|
+
);
|
|
868
|
+
}
|
|
470
869
|
if (opts.json === true) {
|
|
471
870
|
process.stdout.write(
|
|
472
871
|
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
@@ -478,10 +877,20 @@ async function daemonStart(opts) {
|
|
|
478
877
|
return;
|
|
479
878
|
}
|
|
480
879
|
const ds = spinner("Starting Noir daemon...", opts).start();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
880
|
+
let ensured;
|
|
881
|
+
try {
|
|
882
|
+
ensured = await ensureDaemonRunning2({
|
|
883
|
+
project,
|
|
884
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
885
|
+
});
|
|
886
|
+
} catch (err) {
|
|
887
|
+
ds.fail("Daemon failed to start");
|
|
888
|
+
fail(
|
|
889
|
+
EXIT.ERROR,
|
|
890
|
+
`noir daemon start failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
891
|
+
opts
|
|
892
|
+
);
|
|
893
|
+
}
|
|
485
894
|
if (ensured.started) {
|
|
486
895
|
ds.succeed("Daemon started");
|
|
487
896
|
if (opts.json === true) {
|
|
@@ -529,15 +938,19 @@ async function daemonStop(opts) {
|
|
|
529
938
|
}
|
|
530
939
|
const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
|
|
531
940
|
let signalled = false;
|
|
532
|
-
let
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
941
|
+
let errMsg2;
|
|
942
|
+
const callerProject = resolveCallerProjectId();
|
|
943
|
+
const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
|
|
944
|
+
const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
|
|
945
|
+
if (healthy) {
|
|
946
|
+
try {
|
|
947
|
+
process.kill(rec.pid, "SIGTERM");
|
|
948
|
+
signalled = true;
|
|
949
|
+
} catch (err) {
|
|
950
|
+
errMsg2 = err instanceof Error ? err.message : String(err);
|
|
951
|
+
}
|
|
540
952
|
}
|
|
953
|
+
if (!wrongProject) clearDaemonRecord();
|
|
541
954
|
if (opts.json === true) {
|
|
542
955
|
process.stdout.write(
|
|
543
956
|
`${JSON.stringify({
|
|
@@ -546,7 +959,7 @@ async function daemonStop(opts) {
|
|
|
546
959
|
running: signalled,
|
|
547
960
|
stopped: signalled,
|
|
548
961
|
pid: rec.pid,
|
|
549
|
-
...
|
|
962
|
+
...errMsg2 !== void 0 ? { error: errMsg2 } : {}
|
|
550
963
|
}
|
|
551
964
|
})}
|
|
552
965
|
`
|
|
@@ -556,8 +969,9 @@ async function daemonStop(opts) {
|
|
|
556
969
|
if (signalled) {
|
|
557
970
|
ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
|
|
558
971
|
} else {
|
|
559
|
-
|
|
560
|
-
|
|
972
|
+
const reason = wrongProject ? "the daemon belongs to another project (left running)" : errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
|
|
973
|
+
ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${reason}`);
|
|
974
|
+
info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${reason}`, opts);
|
|
561
975
|
}
|
|
562
976
|
}
|
|
563
977
|
async function daemonStatus(opts) {
|
|
@@ -565,6 +979,14 @@ async function daemonStatus(opts) {
|
|
|
565
979
|
if (!rec) {
|
|
566
980
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
567
981
|
}
|
|
982
|
+
const callerProject = resolveCallerProjectId();
|
|
983
|
+
if (rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject) {
|
|
984
|
+
fail(
|
|
985
|
+
EXIT.DAEMON_DOWN,
|
|
986
|
+
"Noir daemon is not running for this project (a daemon for another project is running).",
|
|
987
|
+
opts
|
|
988
|
+
);
|
|
989
|
+
}
|
|
568
990
|
if (!pidAlive2(rec.pid)) {
|
|
569
991
|
clearDaemonRecord();
|
|
570
992
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
@@ -572,19 +994,22 @@ async function daemonStatus(opts) {
|
|
|
572
994
|
const hs = spinner("Probing daemon health...", opts).start();
|
|
573
995
|
let health = null;
|
|
574
996
|
try {
|
|
575
|
-
const res = await fetch(`http://127.0.0.1:${rec.port}/health
|
|
997
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
998
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
999
|
+
});
|
|
576
1000
|
if (res.status === 200) {
|
|
577
1001
|
health = await res.json();
|
|
578
1002
|
}
|
|
579
1003
|
} catch {
|
|
580
1004
|
health = null;
|
|
581
1005
|
}
|
|
582
|
-
|
|
1006
|
+
const pidOk = typeof health?.pid === "number" && health.pid === rec.pid;
|
|
1007
|
+
if (health?.ok !== true || !pidOk) {
|
|
583
1008
|
hs.fail("Daemon not responding");
|
|
584
1009
|
clearDaemonRecord();
|
|
585
1010
|
fail(
|
|
586
1011
|
EXIT.DAEMON_DOWN,
|
|
587
|
-
"Noir daemon is not running (port unresponsive; stale record removed).",
|
|
1012
|
+
health !== null && !pidOk ? "Noir daemon is not running (pid missing/mismatched \u2014 a foreign process holds the recorded port; stale record removed)." : "Noir daemon is not running (port unresponsive; stale record removed).",
|
|
588
1013
|
opts
|
|
589
1014
|
);
|
|
590
1015
|
}
|
|
@@ -614,19 +1039,1140 @@ async function daemonRestart(opts) {
|
|
|
614
1039
|
await daemonStop({ ...opts, json: false, quiet: true });
|
|
615
1040
|
await daemonStart(opts);
|
|
616
1041
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
|
|
1042
|
+
var MODE;
|
|
1043
|
+
var init_daemon = __esm({
|
|
1044
|
+
"src/commands/daemon.ts"() {
|
|
1045
|
+
"use strict";
|
|
1046
|
+
init_daemon_client();
|
|
1047
|
+
init_output();
|
|
1048
|
+
MODE = "foreground";
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
// src/commands/doctor.ts
|
|
1053
|
+
import { spawnSync } from "child_process";
|
|
1054
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
1055
|
+
import { dirname, join, relative } from "path";
|
|
1056
|
+
import { fileURLToPath } from "url";
|
|
1057
|
+
import { resolveAdapter } from "@noir-ai/adapters";
|
|
1058
|
+
import {
|
|
1059
|
+
detectActiveMethod,
|
|
1060
|
+
latestVersionFromCache,
|
|
1061
|
+
loadProjectInfo as loadProjectInfo3,
|
|
1062
|
+
NOIR_VERSION as NOIR_VERSION2,
|
|
1063
|
+
paths,
|
|
1064
|
+
readInstallRecord,
|
|
1065
|
+
readUpdateCache
|
|
1066
|
+
} from "@noir-ai/core";
|
|
1067
|
+
import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
|
|
1068
|
+
import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
|
|
1069
|
+
import { resolveModelConfig } from "@noir-ai/model";
|
|
1070
|
+
async function probeOnnx() {
|
|
1071
|
+
try {
|
|
1072
|
+
const spec = "onnxruntime-node";
|
|
1073
|
+
await import(spec);
|
|
1074
|
+
return { ok: true, reason: "loadable" };
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function describeOnnx(o) {
|
|
1080
|
+
if (o.ok) return "loadable";
|
|
1081
|
+
if (/Cannot find (module|package)/i.test(o.reason)) {
|
|
1082
|
+
return "not resolvable from CLI probe (best-effort)";
|
|
1083
|
+
}
|
|
1084
|
+
const r = o.reason.length > 60 ? `${o.reason.slice(0, 59)}\u2026` : o.reason;
|
|
1085
|
+
return `probe failed: ${r}`;
|
|
1086
|
+
}
|
|
1087
|
+
function fmtUptime(sec) {
|
|
1088
|
+
if (!Number.isFinite(sec) || sec < 0) return "unknown";
|
|
1089
|
+
if (sec < 60) return `${sec}s`;
|
|
1090
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
1091
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
1092
|
+
}
|
|
1093
|
+
function msg(e) {
|
|
1094
|
+
return e instanceof Error ? e.message : String(e);
|
|
1095
|
+
}
|
|
1096
|
+
async function checkRuntime(checks) {
|
|
1097
|
+
checks.push({
|
|
1098
|
+
name: "runtime",
|
|
1099
|
+
status: "ok",
|
|
1100
|
+
detail: `noir ${NOIR_VERSION2} \xB7 node ${process.version} \xB7 ${process.platform}`
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
function checkConfig(root) {
|
|
1104
|
+
const configPath = paths.config(root);
|
|
1105
|
+
if (!existsSync(configPath)) {
|
|
1106
|
+
return {
|
|
1107
|
+
project: void 0,
|
|
1108
|
+
result: {
|
|
1109
|
+
name: "config",
|
|
1110
|
+
status: "warn",
|
|
1111
|
+
detail: `no .noir/config.yml in ${root} \u2014 run \`noir init\``
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
try {
|
|
1116
|
+
const project = loadProjectInfo3(root);
|
|
1117
|
+
return {
|
|
1118
|
+
project,
|
|
1119
|
+
result: {
|
|
1120
|
+
name: "config",
|
|
1121
|
+
status: "ok",
|
|
1122
|
+
detail: `host=${project.config.host} \xB7 mode=${project.config.mode}`
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
return {
|
|
1127
|
+
project: void 0,
|
|
1128
|
+
result: { name: "config", status: "fail", detail: `config parse error: ${msg(e)}` }
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function checkDaemon(checks) {
|
|
1133
|
+
const rec = readDaemonRecord3();
|
|
1134
|
+
if (!rec) {
|
|
1135
|
+
checks.push({
|
|
1136
|
+
name: "daemon",
|
|
1137
|
+
status: "warn",
|
|
1138
|
+
detail: "not running (start with `noir daemon start`)"
|
|
1139
|
+
});
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (!pidAlive3(rec.pid)) {
|
|
1143
|
+
checks.push({
|
|
1144
|
+
name: "daemon",
|
|
1145
|
+
status: "warn",
|
|
1146
|
+
detail: `stale record (pid ${rec.pid} not alive)`
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
1152
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
1153
|
+
});
|
|
1154
|
+
const body = res.status === 200 ? await res.json() : null;
|
|
1155
|
+
const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
|
|
1156
|
+
if (body && body.ok === true && pidOk) {
|
|
1157
|
+
checks.push({
|
|
1158
|
+
name: "daemon",
|
|
1159
|
+
status: "ok",
|
|
1160
|
+
detail: `running (pid ${rec.pid}, port ${rec.port}, up ${fmtUptime(body.uptimeSec ?? 0)})`
|
|
1161
|
+
});
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
checks.push({
|
|
1165
|
+
name: "daemon",
|
|
1166
|
+
status: "warn",
|
|
1167
|
+
detail: `record present but /health unreachable on port ${rec.port}`
|
|
1168
|
+
});
|
|
1169
|
+
} catch {
|
|
1170
|
+
checks.push({
|
|
1171
|
+
name: "daemon",
|
|
1172
|
+
status: "warn",
|
|
1173
|
+
detail: `record present but /health unreachable on port ${rec.port}`
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function checkNativeDeps(checks) {
|
|
1178
|
+
const { vecAvailability } = await import("@noir-ai/store");
|
|
1179
|
+
const vec = vecAvailability();
|
|
1180
|
+
const onnx = await probeOnnx();
|
|
1181
|
+
const vecOk = vec.ok === true;
|
|
1182
|
+
const onnxOk = onnx.ok === true;
|
|
1183
|
+
const status2 = vecOk ? onnxOk ? "ok" : "warn" : "fail";
|
|
1184
|
+
checks.push({
|
|
1185
|
+
name: "native deps",
|
|
1186
|
+
status: status2,
|
|
1187
|
+
detail: `better-sqlite3+sqlite-vec: ${vecOk ? "loadable" : vec.ok === false ? vec.reason : "unknown"} \xB7 onnxruntime-node: ${describeOnnx(onnx)}`
|
|
1188
|
+
});
|
|
1189
|
+
return { vecOk };
|
|
1190
|
+
}
|
|
1191
|
+
async function checkStore(checks, project, root) {
|
|
1192
|
+
if (!project) {
|
|
1193
|
+
checks.push({ name: "store", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const dbPath = paths.storeDb(root, project.id);
|
|
1197
|
+
if (!existsSync(dbPath)) {
|
|
1198
|
+
checks.push({
|
|
1199
|
+
name: "store",
|
|
1200
|
+
status: "warn",
|
|
1201
|
+
detail: `not created yet \u2014 the store DB is created when the daemon first runs (\`noir daemon start\` or index content); expected at ${dbPath}`
|
|
1202
|
+
});
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const { openStore: openStore2 } = await import("@noir-ai/store");
|
|
1206
|
+
let store;
|
|
1207
|
+
try {
|
|
1208
|
+
store = await openStore2({ projectId: project.id, root, readonly: true });
|
|
1209
|
+
checks.push({
|
|
1210
|
+
name: "store",
|
|
1211
|
+
status: "ok",
|
|
1212
|
+
detail: `opens readonly (db: ${paths.storeDb(root, project.id)})`
|
|
1213
|
+
});
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
checks.push({ name: "store", status: "fail", detail: `open failed: ${msg(e)}` });
|
|
1216
|
+
} finally {
|
|
1217
|
+
if (store) {
|
|
1218
|
+
try {
|
|
1219
|
+
await Promise.resolve(store.close());
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function checkNoirEnv(checks, root) {
|
|
1226
|
+
const path = join(paths.noirDir(root), ".env");
|
|
1227
|
+
let st;
|
|
1228
|
+
try {
|
|
1229
|
+
st = statSync(path);
|
|
1230
|
+
} catch {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const mode = st.mode & 511;
|
|
1234
|
+
const tooOpen = (st.mode & 63) !== 0;
|
|
1235
|
+
checks.push(
|
|
1236
|
+
tooOpen ? {
|
|
1237
|
+
name: "noir-env",
|
|
1238
|
+
status: "warn",
|
|
1239
|
+
detail: `${path} is ${mode.toString(8)} \u2014 readable by others; run chmod 600`
|
|
1240
|
+
} : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode.toString(8)})` }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
function checkEmbedder(checks, project, vecOk) {
|
|
1244
|
+
if (!project) {
|
|
1245
|
+
checks.push({ name: "embedder", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const emb = project.config.context.embedder;
|
|
1249
|
+
const kind = emb.kind;
|
|
1250
|
+
const model = typeof emb.model === "string" && emb.model.length > 0 ? emb.model : "<unset>";
|
|
1251
|
+
if (kind === "none") {
|
|
1252
|
+
checks.push({
|
|
1253
|
+
name: "embedder",
|
|
1254
|
+
status: "ok",
|
|
1255
|
+
detail: `kind=none (BM25-only) \xB7 dim=${emb.dim}`
|
|
1256
|
+
});
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (!vecOk) {
|
|
1260
|
+
checks.push({
|
|
1261
|
+
name: "embedder",
|
|
1262
|
+
status: "warn",
|
|
1263
|
+
detail: `${kind}/${model} (${emb.dim}-dim) configured but vec native layer unavailable \u2014 search degrades to BM25`
|
|
1264
|
+
});
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
checks.push({
|
|
1268
|
+
name: "embedder",
|
|
1269
|
+
status: "ok",
|
|
1270
|
+
detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function checkProvider(checks, project) {
|
|
1274
|
+
if (!project) {
|
|
1275
|
+
checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const resolved = resolveModelConfig(project.config.model);
|
|
1279
|
+
const names = Object.keys(resolved.providers);
|
|
1280
|
+
if (names.length === 0) {
|
|
1281
|
+
checks.push({
|
|
1282
|
+
name: "provider",
|
|
1283
|
+
status: "ok",
|
|
1284
|
+
detail: "none configured (offline mode; drafting degrades to templates)"
|
|
1285
|
+
});
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const parts = [];
|
|
1289
|
+
let missing = false;
|
|
1290
|
+
for (const name of names) {
|
|
1291
|
+
const p = resolved.providers[name];
|
|
1292
|
+
if (!p) continue;
|
|
1293
|
+
const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
|
|
1294
|
+
if (!p.hasKey && p.apiKeyEnv) missing = true;
|
|
1295
|
+
parts.push(`${name}/${p.model ?? "?"} (${key})`);
|
|
1296
|
+
}
|
|
1297
|
+
checks.push({
|
|
1298
|
+
name: "provider",
|
|
1299
|
+
status: missing ? "warn" : "ok",
|
|
1300
|
+
detail: parts.join(" \xB7 ")
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
function buildInstallCheck(opts) {
|
|
1304
|
+
const { method, version, latestKnown } = opts;
|
|
1305
|
+
const parts = [`method=${method}`];
|
|
1306
|
+
if (version) parts.push(`v${version}`);
|
|
1307
|
+
const advisory = [];
|
|
1308
|
+
if (method !== "native") advisory.push("native recommended");
|
|
1309
|
+
if (latestKnown && version && latestKnown !== version) advisory.push("update available");
|
|
1310
|
+
const status2 = advisory.length > 0 ? "warn" : "ok";
|
|
1311
|
+
if (advisory.length > 0) parts.push(advisory.join(" + "));
|
|
1312
|
+
return {
|
|
1313
|
+
name: "install",
|
|
1314
|
+
status: status2,
|
|
1315
|
+
detail: parts.join(" \xB7 "),
|
|
1316
|
+
method,
|
|
1317
|
+
installedVersion: version,
|
|
1318
|
+
latestKnown
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function checkScaffoldVersion(checks, root) {
|
|
1322
|
+
const onDisk = readScaffoldVersion(root);
|
|
1323
|
+
const current = CURRENT_SCAFFOLD_VERSION;
|
|
1324
|
+
const drift = onDisk !== null && onDisk !== current;
|
|
1325
|
+
const status2 = onDisk === null || drift ? "warn" : "ok";
|
|
1326
|
+
const detail = onDisk === null ? "no .noir/scaffold-version stamp (run `noir init`)" : drift ? `on-disk ${onDisk} \u2260 current ${current} (run \`noir init --upgrade\`)` : `${onDisk} (up to date)`;
|
|
1327
|
+
checks.push({ name: "scaffold version", status: status2, detail });
|
|
1328
|
+
return { onDisk, current, drift };
|
|
1329
|
+
}
|
|
1330
|
+
function checkRulesMdBudget(checks, root, project) {
|
|
1331
|
+
if (!project) {
|
|
1332
|
+
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
const rulesPath = paths.rulesMd(root);
|
|
1336
|
+
if (!existsSync(rulesPath)) {
|
|
1337
|
+
checks.push({
|
|
1338
|
+
name: "rules budget",
|
|
1339
|
+
status: "ok",
|
|
1340
|
+
detail: "no .noir/rules/RULES.md"
|
|
1341
|
+
});
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
let content;
|
|
1345
|
+
try {
|
|
1346
|
+
content = readFileSync(rulesPath, "utf8");
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
checks.push({
|
|
1349
|
+
name: "rules budget",
|
|
1350
|
+
status: "warn",
|
|
1351
|
+
detail: `RULES.md unreadable: ${msg(e)}`
|
|
1352
|
+
});
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
1356
|
+
const lineCount = content.length === 0 ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
1357
|
+
const lengthBudgetKb = project.config.rules.lengthBudgetKb;
|
|
1358
|
+
const budgetBytes = lengthBudgetKb * 1024;
|
|
1359
|
+
const maxLines = 150;
|
|
1360
|
+
const overBytes = bytes > budgetBytes;
|
|
1361
|
+
const overLines = lineCount > maxLines;
|
|
1362
|
+
const over = overBytes || overLines;
|
|
1363
|
+
const status2 = over ? "warn" : "ok";
|
|
1364
|
+
const kb = (n) => (n / 1024).toFixed(1);
|
|
1365
|
+
const detail = over ? `OVER: ${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines \u2014 trim RULES.md (failure-backed clauses only) or raise rules.lengthBudgetKb` : `${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines (within budget)`;
|
|
1366
|
+
checks.push({ name: "rules budget", status: status2, detail });
|
|
1367
|
+
return {
|
|
1368
|
+
onDisk: { bytes, lines: lineCount },
|
|
1369
|
+
budget: { kb: lengthBudgetKb, maxLines },
|
|
1370
|
+
over
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function summarize(checks) {
|
|
1374
|
+
let ok = 0;
|
|
1375
|
+
let warnN = 0;
|
|
1376
|
+
let fail2 = 0;
|
|
1377
|
+
for (const c2 of checks) {
|
|
1378
|
+
if (c2.status === "ok") ok++;
|
|
1379
|
+
else if (c2.status === "warn") warnN++;
|
|
1380
|
+
else fail2++;
|
|
1381
|
+
}
|
|
1382
|
+
return { ok, warn: warnN, fail: fail2 };
|
|
1383
|
+
}
|
|
1384
|
+
function checkHostArtifacts(checks, root, project) {
|
|
1385
|
+
if (!project) {
|
|
1386
|
+
checks.push({ name: "host artifacts", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
const host = project.config.host;
|
|
1390
|
+
const adapter = resolveAdapter(host);
|
|
1391
|
+
const ectx = { root };
|
|
1392
|
+
const expected = [];
|
|
1393
|
+
const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
|
|
1394
|
+
if (emitsAgentsMd) {
|
|
1395
|
+
expected.push(relOr(adapter.agentsMdPath?.(ectx) ?? "AGENTS.md", root));
|
|
1396
|
+
}
|
|
1397
|
+
if (host === "claude") expected.push("CLAUDE.md");
|
|
1398
|
+
else if (host === "gemini") expected.push("GEMINI.md");
|
|
1399
|
+
expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
|
|
1400
|
+
const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
|
|
1401
|
+
const status2 = missing.length === 0 ? "ok" : "warn";
|
|
1402
|
+
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)`;
|
|
1403
|
+
checks.push({ name: "host artifacts", status: status2, detail });
|
|
1404
|
+
return { active: host, expected, missing };
|
|
1405
|
+
}
|
|
1406
|
+
function checkNestedNoir(checks, root) {
|
|
1407
|
+
const candidates = [
|
|
1408
|
+
".noir/.noir",
|
|
1409
|
+
".noir/CLAUDE.md",
|
|
1410
|
+
".noir/.mcp.json",
|
|
1411
|
+
".noir/.claude",
|
|
1412
|
+
".noir/.gitignore",
|
|
1413
|
+
".noir/.dockerignore",
|
|
1414
|
+
".noir/.npmignore",
|
|
1415
|
+
".noir/.prettierignore"
|
|
1416
|
+
];
|
|
1417
|
+
const found = candidates.filter((rel) => existsSync(join(root, rel)));
|
|
1418
|
+
const detected = found.length > 0;
|
|
1419
|
+
checks.push({
|
|
1420
|
+
name: "nested .noir",
|
|
1421
|
+
status: detected ? "warn" : "ok",
|
|
1422
|
+
detail: detected ? `nested Noir artifacts inside .noir/ (${found.join(", ")}) \u2014 likely from running \`noir init\` inside .noir/. Remove them; the real project is at ${root}.` : "no nested .noir/ artifacts"
|
|
1423
|
+
});
|
|
1424
|
+
return { detected, paths: found };
|
|
1425
|
+
}
|
|
1426
|
+
function collectDedupCandidates(root) {
|
|
1427
|
+
const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
|
|
1428
|
+
const out = [];
|
|
1429
|
+
for (const rel of rels) {
|
|
1430
|
+
const abs = join(root, rel);
|
|
1431
|
+
if (!existsSync(abs)) continue;
|
|
1432
|
+
let text;
|
|
1433
|
+
try {
|
|
1434
|
+
text = readFileSync(abs, "utf8");
|
|
1435
|
+
} catch {
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if (text.trim().length === 0) continue;
|
|
1439
|
+
out.push({ path: rel, text });
|
|
1440
|
+
}
|
|
1441
|
+
return out;
|
|
1442
|
+
}
|
|
1443
|
+
async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
|
|
1444
|
+
if (!project) {
|
|
1445
|
+
checks.push({ name: "semantic dup", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
const candidates = collectDedupCandidates(root);
|
|
1449
|
+
if (candidates.length < 2) {
|
|
1450
|
+
checks.push({
|
|
1451
|
+
name: "semantic dup",
|
|
1452
|
+
status: "ok",
|
|
1453
|
+
detail: `${candidates.length} candidate file(s); nothing to compare`
|
|
1454
|
+
});
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const ctx = await import("@noir-ai/context");
|
|
1458
|
+
let embed = opts.embed;
|
|
1459
|
+
if (!embed) {
|
|
1460
|
+
const cfg = ctx.resolveEmbedderConfig(project.config.context);
|
|
1461
|
+
if (cfg.kind === "none") {
|
|
1462
|
+
checks.push({
|
|
1463
|
+
name: "semantic dup",
|
|
1464
|
+
status: "warn",
|
|
1465
|
+
detail: "embedder kind=none \u2014 set context.embedder.kind=local to enable semantic dedup"
|
|
1466
|
+
});
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
embed = ctx.createEmbedFn(cfg).embed;
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
checks.push({
|
|
1473
|
+
name: "semantic dup",
|
|
1474
|
+
status: "warn",
|
|
1475
|
+
detail: `embedder unavailable \u2014 skipped (${msg(e)})`
|
|
1476
|
+
});
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (!embed) return;
|
|
1481
|
+
const pairs = await ctx.findSemanticDuplicates(candidates, embed, ctx.DEFAULT_DUP_THRESHOLD);
|
|
1482
|
+
checks.push({
|
|
1483
|
+
name: "semantic dup",
|
|
1484
|
+
status: pairs.length > 0 ? "warn" : "ok",
|
|
1485
|
+
detail: pairs.length === 0 ? `no near-duplicates across ${candidates.length} file(s)` : `${pairs.length} near-duplicate pair${pairs.length === 1 ? "" : "s"}: ${pairs.map((p) => `${p.a}\u2248${p.b} (${p.similarity.toFixed(2)})`).join("; ")}`
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function resolveWorkspacePackagesDir() {
|
|
1489
|
+
let here = dirname(fileURLToPath(import.meta.url));
|
|
1490
|
+
for (let i = 0; i < 8; i++) {
|
|
1491
|
+
const pkgPath = join(here, "package.json");
|
|
1492
|
+
if (existsSync(pkgPath)) {
|
|
1493
|
+
try {
|
|
1494
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1495
|
+
if (pkg.name === "@noir-ai/cli") {
|
|
1496
|
+
const packagesDir = dirname(here);
|
|
1497
|
+
if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
|
|
1498
|
+
return packagesDir;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const parent = dirname(here);
|
|
1506
|
+
if (parent === here) break;
|
|
1507
|
+
here = parent;
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
function npmPackDryRun(cliPkgDir) {
|
|
1512
|
+
const probe = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
|
|
1513
|
+
if (probe.status !== 0 || probe.error) {
|
|
1514
|
+
return { error: probe.error ? probe.error.message : "npm not on PATH" };
|
|
1515
|
+
}
|
|
1516
|
+
const res = spawnSync("npm", ["pack", "--dry-run"], {
|
|
1517
|
+
cwd: cliPkgDir,
|
|
1518
|
+
encoding: "utf8",
|
|
1519
|
+
shell: true
|
|
1520
|
+
});
|
|
1521
|
+
if (res.status !== 0 || res.error) {
|
|
1522
|
+
return { error: res.error ? res.error.message : `npm pack exited ${res.status ?? "?"}` };
|
|
1523
|
+
}
|
|
1524
|
+
const out = `${res.stdout ?? ""}
|
|
1525
|
+
${res.stderr ?? ""}`;
|
|
1526
|
+
const totalMatch = out.match(/total files:\s*(\d+)/i);
|
|
1527
|
+
const fileCount = totalMatch?.[1] ? Number.parseInt(totalMatch[1], 10) : null;
|
|
1528
|
+
const distIncluded = /\bdist\//.test(out);
|
|
1529
|
+
return { fileCount, distIncluded };
|
|
1530
|
+
}
|
|
1531
|
+
function checkPublish(checks, packagesDir, opts = {}) {
|
|
1532
|
+
if (packagesDir === null) {
|
|
1533
|
+
checks.push({
|
|
1534
|
+
name: "publish",
|
|
1535
|
+
status: "ok",
|
|
1536
|
+
detail: "skipped \u2014 not a monorepo checkout (no packages/* workspace)"
|
|
1537
|
+
});
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
let entries = [];
|
|
1541
|
+
try {
|
|
1542
|
+
entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
checks.push({
|
|
1545
|
+
name: "publish",
|
|
1546
|
+
status: "warn",
|
|
1547
|
+
detail: `could not read ${packagesDir}: ${msg(e)}`
|
|
1548
|
+
});
|
|
1549
|
+
return { checked: 0, issues: [`unreadable packages dir: ${msg(e)}`] };
|
|
1550
|
+
}
|
|
1551
|
+
const issues = [];
|
|
1552
|
+
let checked = 0;
|
|
1553
|
+
let cliDir = null;
|
|
1554
|
+
for (const name of entries) {
|
|
1555
|
+
const pkgPath = join(packagesDir, name, "package.json");
|
|
1556
|
+
if (!existsSync(pkgPath)) continue;
|
|
1557
|
+
let pkg;
|
|
1558
|
+
try {
|
|
1559
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
issues.push(`${name}/package.json: unparseable (${msg(e)})`);
|
|
1562
|
+
checked++;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
checked++;
|
|
1566
|
+
const pkgName = typeof pkg.name === "string" ? pkg.name : "";
|
|
1567
|
+
if (!pkgName.startsWith("@noir-ai/")) {
|
|
1568
|
+
issues.push(`${name}/package.json: name '${pkgName}' is not @noir-ai/*`);
|
|
1569
|
+
}
|
|
1570
|
+
const version = typeof pkg.version === "string" ? pkg.version : "";
|
|
1571
|
+
if (!SEMVER_ISH.test(version)) {
|
|
1572
|
+
issues.push(`${name}/package.json: version '${version}' is not semver`);
|
|
1573
|
+
}
|
|
1574
|
+
if (!Array.isArray(pkg.files) || pkg.files.length === 0) {
|
|
1575
|
+
issues.push(`${name}/package.json: files missing or empty`);
|
|
1576
|
+
}
|
|
1577
|
+
if (pkgName === "@noir-ai/cli") {
|
|
1578
|
+
if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
|
|
1579
|
+
issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
|
|
1580
|
+
}
|
|
1581
|
+
cliDir = join(packagesDir, name);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
let packNote = "";
|
|
1585
|
+
if (cliDir && opts.skipNpmPack !== true) {
|
|
1586
|
+
const pack = npmPackDryRun(cliDir);
|
|
1587
|
+
if ("error" in pack) {
|
|
1588
|
+
packNote = ` \xB7 npm pack skipped (${pack.error})`;
|
|
1589
|
+
} else {
|
|
1590
|
+
const count = pack.fileCount == null ? "?" : String(pack.fileCount);
|
|
1591
|
+
const dist = pack.distIncluded ? "dist/ included" : "dist/ NOT in tarball";
|
|
1592
|
+
packNote = ` \xB7 npm pack: ${count} files, ${dist}`;
|
|
1593
|
+
if (pack.distIncluded === false) {
|
|
1594
|
+
issues.push("cli npm pack --dry-run: dist/ absent from the tarball");
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const status2 = issues.length > 0 ? "warn" : "ok";
|
|
1599
|
+
const detail = issues.length === 0 ? `${checked}/${entries.length} packages publish-ready${packNote}` : `${issues.length} issue${issues.length === 1 ? "" : "s"} across ${checked} package${checked === 1 ? "" : "s"}${packNote}`;
|
|
1600
|
+
checks.push({ name: "publish", status: status2, detail });
|
|
1601
|
+
return { checked, issues };
|
|
1602
|
+
}
|
|
1603
|
+
function relOr(abs, root) {
|
|
1604
|
+
const posix = abs.replace(/\\/g, "/");
|
|
1605
|
+
if (posix.startsWith("/")) {
|
|
1606
|
+
const rel = relative(root, abs).replace(/\\/g, "/");
|
|
1607
|
+
if (rel.length === 0 || rel.startsWith("..") || rel.startsWith("/")) {
|
|
1608
|
+
throw new Error(`doctor: host path '${abs}' is not under root '${root}'`);
|
|
1609
|
+
}
|
|
1610
|
+
return rel;
|
|
1611
|
+
}
|
|
1612
|
+
if (posix.split("/").some((seg) => seg === "..")) {
|
|
1613
|
+
throw new Error(`doctor: host path '${abs}' escapes root '${root}'`);
|
|
1614
|
+
}
|
|
1615
|
+
return posix;
|
|
1616
|
+
}
|
|
1617
|
+
function joinRoot(root, relPath) {
|
|
1618
|
+
return `${root}/${relPath}`;
|
|
1619
|
+
}
|
|
1620
|
+
function severityBadge(s) {
|
|
1621
|
+
const state = s === "fail" ? "error" : s;
|
|
1622
|
+
return badge(state, s.toUpperCase());
|
|
1623
|
+
}
|
|
1624
|
+
function renderHuman(payload, opts) {
|
|
1625
|
+
log(
|
|
1626
|
+
`noir doctor \u2014 ${payload.checks.length} check${payload.checks.length === 1 ? "" : "s"}`,
|
|
1627
|
+
opts
|
|
1628
|
+
);
|
|
1629
|
+
table(
|
|
1630
|
+
payload.checks.map((c2) => ({
|
|
1631
|
+
Check: c2.name,
|
|
1632
|
+
Status: severityBadge(c2.status),
|
|
1633
|
+
Detail: c2.detail
|
|
1634
|
+
})),
|
|
1635
|
+
["Check", "Status", "Detail"],
|
|
1636
|
+
opts
|
|
1637
|
+
);
|
|
1638
|
+
const { ok, warn: warnN, fail: fail2 } = payload.summary;
|
|
1639
|
+
if (fail2 > 0) {
|
|
1640
|
+
error(
|
|
1641
|
+
`${fail2} critical check${fail2 === 1 ? "" : "s"} failed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`,
|
|
1642
|
+
opts
|
|
1643
|
+
);
|
|
1644
|
+
} else if (warnN > 0) {
|
|
1645
|
+
warn(`all critical checks passed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`, opts);
|
|
1646
|
+
} else {
|
|
1647
|
+
success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
async function doctor(opts = {}) {
|
|
1651
|
+
const root = process.cwd();
|
|
1652
|
+
const checks = [];
|
|
1653
|
+
await checkRuntime(checks);
|
|
1654
|
+
const { project, result: configResult } = checkConfig(root);
|
|
1655
|
+
checks.push(configResult);
|
|
1656
|
+
await checkDaemon(checks);
|
|
1657
|
+
const { vecOk } = await checkNativeDeps(checks);
|
|
1658
|
+
await checkStore(checks, project, root);
|
|
1659
|
+
checkEmbedder(checks, project, vecOk);
|
|
1660
|
+
checkProvider(checks, project);
|
|
1661
|
+
checkNoirEnv(checks, root);
|
|
1662
|
+
const scaffold4 = checkScaffoldVersion(checks, root);
|
|
1663
|
+
const rules = checkRulesMdBudget(checks, root, project);
|
|
1664
|
+
const host = checkHostArtifacts(checks, root, project);
|
|
1665
|
+
checkNestedNoir(checks, root);
|
|
1666
|
+
if (opts.dedup === true) {
|
|
1667
|
+
await checkSemanticDupDoctor(checks, root, project);
|
|
1668
|
+
}
|
|
1669
|
+
const publish = checkPublish(checks, resolveWorkspacePackagesDir());
|
|
1670
|
+
const installCheck = buildInstallCheck({
|
|
1671
|
+
method: detectActiveMethod(),
|
|
1672
|
+
version: readInstallRecord()?.version ?? null,
|
|
1673
|
+
latestKnown: latestVersionFromCache(readUpdateCache(), "latest")
|
|
1674
|
+
});
|
|
1675
|
+
checks.push(installCheck);
|
|
1676
|
+
const summary = summarize(checks);
|
|
1677
|
+
const payload = {
|
|
1678
|
+
noir: NOIR_VERSION2,
|
|
1679
|
+
checks,
|
|
1680
|
+
summary,
|
|
1681
|
+
scaffold: scaffold4,
|
|
1682
|
+
rules,
|
|
1683
|
+
host,
|
|
1684
|
+
publish,
|
|
1685
|
+
install: {
|
|
1686
|
+
method: installCheck.method,
|
|
1687
|
+
installedVersion: installCheck.installedVersion,
|
|
1688
|
+
latestKnown: installCheck.latestKnown
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
if (opts.json === true) {
|
|
1692
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
1693
|
+
`);
|
|
1694
|
+
} else {
|
|
1695
|
+
renderHuman(payload, opts);
|
|
1696
|
+
}
|
|
1697
|
+
if (summary.fail > 0) {
|
|
1698
|
+
throw new NoirCliError(
|
|
1699
|
+
EXIT.ERROR,
|
|
1700
|
+
opts.json === true ? "" : `noir doctor: ${summary.fail} critical check(s) failed`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
var SEMVER_ISH;
|
|
1705
|
+
var init_doctor = __esm({
|
|
1706
|
+
"src/commands/doctor.ts"() {
|
|
1707
|
+
"use strict";
|
|
1708
|
+
init_daemon_client();
|
|
1709
|
+
init_output();
|
|
1710
|
+
init_theme();
|
|
1711
|
+
SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// src/commands/install.ts
|
|
1716
|
+
var install_exports = {};
|
|
1717
|
+
__export(install_exports, {
|
|
1718
|
+
buildMigrationPlan: () => buildMigrationPlan,
|
|
1719
|
+
install: () => install,
|
|
1720
|
+
installManagedNode: () => installManagedNode
|
|
1721
|
+
});
|
|
1722
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
1723
|
+
import { join as join2 } from "path";
|
|
1724
|
+
import {
|
|
1725
|
+
atomicWriteFile,
|
|
1726
|
+
detectActiveMethod as detectActiveMethod2,
|
|
1727
|
+
detectInstallMethods,
|
|
1728
|
+
ensureShimExecutable,
|
|
1729
|
+
loadProjectInfo as loadProjectInfo4,
|
|
1730
|
+
MANAGED_NODE_VERSION,
|
|
1731
|
+
NOIR_VERSION as NOIR_VERSION3,
|
|
1732
|
+
noirHome,
|
|
1733
|
+
provisionManagedNode,
|
|
1734
|
+
readInstallRecord as readInstallRecord2,
|
|
1735
|
+
runManagerCmd,
|
|
1736
|
+
writeInstallRecord
|
|
1737
|
+
} from "@noir-ai/core";
|
|
1738
|
+
function semverLt(a, b) {
|
|
1739
|
+
const seg = (s) => Number(s.replace(/^v/, "")) || 0;
|
|
1740
|
+
const pa = a.split(".").map(seg);
|
|
1741
|
+
const pb = b.split(".").map(seg);
|
|
1742
|
+
for (let i = 0; i < 3; i++) {
|
|
1743
|
+
const x = pa[i] ?? 0;
|
|
1744
|
+
const y = pb[i] ?? 0;
|
|
1745
|
+
if (x !== y) return x < y;
|
|
1746
|
+
}
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
function buildMigrationPlan(opts) {
|
|
1750
|
+
const detected = opts.detected;
|
|
1751
|
+
const shouldMigrate = detected.length > 0 && opts.currentMethod !== "native";
|
|
1752
|
+
const nativeVersion = opts.targetSpec;
|
|
1753
|
+
let isDowngrade = false;
|
|
1754
|
+
if (opts.installedVersion && nativeVersion !== "latest" && nativeVersion !== "beta") {
|
|
1755
|
+
isDowngrade = semverLt(nativeVersion, opts.installedVersion);
|
|
1756
|
+
}
|
|
1757
|
+
const floor = opts.minVersion ?? "1.6.0";
|
|
1758
|
+
const belowMinVersion = nativeVersion !== "latest" && nativeVersion !== "beta" && semverLt(nativeVersion, floor);
|
|
1759
|
+
return {
|
|
1760
|
+
detected,
|
|
1761
|
+
currentMethod: opts.currentMethod,
|
|
1762
|
+
targetSpec: opts.targetSpec,
|
|
1763
|
+
installedVersion: opts.installedVersion,
|
|
1764
|
+
shouldMigrate,
|
|
1765
|
+
isDowngrade,
|
|
1766
|
+
belowMinVersion,
|
|
1767
|
+
nativeVersion,
|
|
1768
|
+
prevUninstallCmd: detected[0]?.uninstallCmd ?? null,
|
|
1769
|
+
autoUninstall: false
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
async function installManagedNode(opts) {
|
|
1773
|
+
const spec = opts.version ?? opts.channel ?? "latest";
|
|
1774
|
+
const home2 = noirHome();
|
|
1775
|
+
const cliDir = join2(home2, "cli");
|
|
1776
|
+
const binDir = join2(home2, "bin");
|
|
1777
|
+
mkdirSync(cliDir, { recursive: true });
|
|
1778
|
+
mkdirSync(binDir, { recursive: true });
|
|
1779
|
+
let runtime;
|
|
1780
|
+
try {
|
|
1781
|
+
runtime = await provisionManagedNode({
|
|
1782
|
+
...opts.provision ?? {},
|
|
1783
|
+
env: opts.env
|
|
1784
|
+
});
|
|
1785
|
+
} catch (err) {
|
|
1786
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1787
|
+
return { ok: false, version: null, error: `managed-Node provision failed: ${reason}` };
|
|
1788
|
+
}
|
|
1789
|
+
if (runtime.source === "system") {
|
|
1790
|
+
warn(
|
|
1791
|
+
`managed-Node download failed; using system Node ${runtime.version} at ${runtime.nodeBin}.`
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
const { nodeBin, npmBin } = runtime;
|
|
1795
|
+
const { code, stderr } = await runManagerCmd(
|
|
1796
|
+
[npmBin, "install", "-g", `@noir-ai/cli@${spec}`, `--prefix=${cliDir}`],
|
|
1797
|
+
{ env: opts.env, timeoutMs: 12e4 }
|
|
1798
|
+
);
|
|
1799
|
+
if (code !== 0) {
|
|
1800
|
+
return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
|
|
1801
|
+
}
|
|
1802
|
+
const shim = join2(binDir, "noir");
|
|
1803
|
+
const shimBody = `#!/usr/bin/env bash
|
|
1804
|
+
"${nodeBin}" "${join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
|
|
1805
|
+
`;
|
|
1806
|
+
atomicWriteFile(shim, shimBody);
|
|
1807
|
+
chmodSync(shim, 493);
|
|
1808
|
+
ensureShimExecutable();
|
|
1809
|
+
const ver = await runManagerCmd(
|
|
1810
|
+
[
|
|
1811
|
+
nodeBin,
|
|
1812
|
+
join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
|
|
1813
|
+
"--version"
|
|
1814
|
+
],
|
|
1815
|
+
{ env: opts.env, timeoutMs: 3e4 }
|
|
1816
|
+
);
|
|
1817
|
+
const version = ver.code === 0 ? ver.stdout.trim() : null;
|
|
1818
|
+
writeInstallRecord({
|
|
1819
|
+
method: "native",
|
|
1820
|
+
version: version ?? "0.0.0",
|
|
1821
|
+
channel: opts.channel ?? "latest",
|
|
1822
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1823
|
+
// Reflect which runtime backs this install: the pinned managed version
|
|
1824
|
+
// (e.g. '22.23.2') for managed, or 'system-<v>' for the fallback so doctor
|
|
1825
|
+
// can distinguish a fallback install from a real managed one.
|
|
1826
|
+
managedRuntimeVersion: runtime.source === "managed" ? MANAGED_NODE_VERSION : `system-${runtime.version}`
|
|
1827
|
+
});
|
|
1828
|
+
return { ok: true, version, runtimeSource: runtime.source };
|
|
1829
|
+
}
|
|
1830
|
+
async function install(opts = {}) {
|
|
1831
|
+
let dismissOutcome;
|
|
1832
|
+
if (opts.dismiss === true) {
|
|
1833
|
+
const rec = readInstallRecord2();
|
|
1834
|
+
if (!rec) {
|
|
1835
|
+
dismissOutcome = { dismissed: false, version: null };
|
|
1836
|
+
if (!opts.json) info("No install record found; nothing to dismiss.");
|
|
1837
|
+
} else {
|
|
1838
|
+
const dismissed = new Set(rec.dismissedVersions ?? []);
|
|
1839
|
+
if (!dismissed.has(NOIR_VERSION3)) dismissed.add(NOIR_VERSION3);
|
|
1840
|
+
writeInstallRecord({ ...rec, dismissedVersions: [...dismissed] });
|
|
1841
|
+
dismissOutcome = { dismissed: true, version: NOIR_VERSION3 };
|
|
1842
|
+
if (!opts.json) success(`Migration banner dismissed for ${NOIR_VERSION3}.`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (opts.dismiss === true && opts.list !== true) {
|
|
1846
|
+
if (opts.json) process.stdout.write(`${JSON.stringify({ ok: true, data: dismissOutcome })}
|
|
1847
|
+
`);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (opts.list === true) {
|
|
1851
|
+
const detected2 = await detectInstallMethods(process.env);
|
|
1852
|
+
if (opts.json) {
|
|
1853
|
+
process.stdout.write(
|
|
1854
|
+
`${JSON.stringify({ ok: true, data: { ...dismissOutcome ?? {}, detected: detected2 } })}
|
|
1855
|
+
`
|
|
1856
|
+
);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
info("Detected installs:");
|
|
1860
|
+
for (const d of detected2) info(` ${d.method} (${d.version ?? "unknown"})`);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
const currentMethod = detectActiveMethod2();
|
|
1864
|
+
const detected = await detectInstallMethods(process.env);
|
|
1865
|
+
const installedRecord = readInstallRecord2();
|
|
1866
|
+
let minVersion = "1.6.0";
|
|
1867
|
+
try {
|
|
1868
|
+
minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
|
|
1869
|
+
} catch {
|
|
1870
|
+
}
|
|
1871
|
+
const plan = buildMigrationPlan({
|
|
1872
|
+
detected,
|
|
1873
|
+
currentMethod,
|
|
1874
|
+
targetSpec: opts.spec ?? "latest",
|
|
1875
|
+
installedVersion: installedRecord?.version ?? null,
|
|
1876
|
+
minVersion
|
|
1877
|
+
});
|
|
1878
|
+
if (plan.belowMinVersion) {
|
|
1879
|
+
fail(
|
|
1880
|
+
EXIT.USAGE,
|
|
1881
|
+
`refusing install of ${plan.nativeVersion} (below minVersion ${minVersion})`,
|
|
1882
|
+
opts
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
if (plan.isDowngrade) {
|
|
1886
|
+
fail(
|
|
1887
|
+
EXIT.USAGE,
|
|
1888
|
+
`refusing downgrade to ${plan.nativeVersion} (installed ${plan.installedVersion})`,
|
|
1889
|
+
opts
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const installSpin = spinner("Installing Noir via native installer...", opts).start();
|
|
1893
|
+
const result = await installManagedNode({
|
|
1894
|
+
channel: plan.nativeVersion === "beta" ? "beta" : void 0,
|
|
1895
|
+
version: plan.nativeVersion.startsWith("v") ? plan.nativeVersion : plan.nativeVersion === "latest" || plan.nativeVersion === "beta" ? void 0 : plan.nativeVersion,
|
|
1896
|
+
env: process.env
|
|
1897
|
+
});
|
|
1898
|
+
if (!result.ok) {
|
|
1899
|
+
installSpin.fail("Native install failed");
|
|
1900
|
+
fail(EXIT.ERROR, result.error ?? "native install failed", opts);
|
|
1901
|
+
}
|
|
1902
|
+
installSpin.succeed(`Installed noir ${result.version}`);
|
|
1903
|
+
if (plan.prevUninstallCmd && opts.uninstallPrev !== true) {
|
|
1904
|
+
warn(`To finish the migration, uninstall the previous install: ${plan.prevUninstallCmd}`);
|
|
1905
|
+
warn(
|
|
1906
|
+
" Or re-run with --uninstall-prev to do it now. Rollback: reinstall via the previous manager."
|
|
1907
|
+
);
|
|
1908
|
+
} else if (plan.prevUninstallCmd && opts.uninstallPrev === true) {
|
|
1909
|
+
const { code } = await runManagerCmd(plan.prevUninstallCmd.split(" "), { env: process.env });
|
|
1910
|
+
if (code === 0) success("Previous install removed.");
|
|
1911
|
+
else warn("Previous install NOT removed (non-zero exit). You can remove it manually.");
|
|
1912
|
+
}
|
|
1913
|
+
if (opts.json === true) {
|
|
1914
|
+
process.stdout.write(
|
|
1915
|
+
`${JSON.stringify({ ok: true, data: { version: result.version, runtimeSource: result.runtimeSource } })}
|
|
1916
|
+
`
|
|
1917
|
+
);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
info(`Installed native: ${result.version}.`);
|
|
1921
|
+
info("If `noir` still points at the old install, run: hash -r && which -a noir");
|
|
1922
|
+
success(`noir ${result.version} installed via native. Run \`noir doctor\` to verify.`);
|
|
1923
|
+
}
|
|
1924
|
+
var init_install = __esm({
|
|
1925
|
+
"src/commands/install.ts"() {
|
|
1926
|
+
"use strict";
|
|
1927
|
+
init_output();
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
// src/commands/update.ts
|
|
1932
|
+
var update_exports = {};
|
|
1933
|
+
__export(update_exports, {
|
|
1934
|
+
DEFAULT_UPDATE_CONFIG: () => DEFAULT_UPDATE_CONFIG,
|
|
1935
|
+
buildUpdateNotice: () => buildUpdateNotice,
|
|
1936
|
+
buildUpdateTarget: () => buildUpdateTarget,
|
|
1937
|
+
runAsyncUpdateCheck: () => runAsyncUpdateCheck,
|
|
1938
|
+
update: () => update,
|
|
1939
|
+
updateAdvice: () => updateAdvice
|
|
1940
|
+
});
|
|
1941
|
+
import {
|
|
1942
|
+
detectActiveMethod as detectActiveMethod3,
|
|
1943
|
+
ensureShimExecutable as ensureShimExecutable2,
|
|
1944
|
+
fetchLatestVersion,
|
|
1945
|
+
loadProjectInfo as loadProjectInfo5,
|
|
1946
|
+
readInstallRecord as readInstallRecord3,
|
|
1947
|
+
readUpdateCache as readUpdateCache2,
|
|
1948
|
+
runManagerCmd as runManagerCmd2,
|
|
1949
|
+
shouldCheckForUpdate,
|
|
1950
|
+
writeUpdateCache
|
|
1951
|
+
} from "@noir-ai/core";
|
|
1952
|
+
function buildUpdateTarget(opts) {
|
|
1953
|
+
const targetSpec = opts.spec ?? opts.channel;
|
|
1954
|
+
const concrete = opts.spec != null && opts.spec !== "latest" && opts.spec !== "beta" ? opts.spec : opts.latestKnown;
|
|
1955
|
+
const isUpgrade = concrete != null && opts.currentVersion != null && semverGt(concrete, opts.currentVersion);
|
|
1956
|
+
const floor = opts.minVersion ?? "1.6.0";
|
|
1957
|
+
const belowMinVersion = concrete != null && semverLt2(concrete, floor);
|
|
1958
|
+
return {
|
|
1959
|
+
method: opts.method,
|
|
1960
|
+
targetSpec,
|
|
1961
|
+
currentVersion: opts.currentVersion,
|
|
1962
|
+
latestKnown: opts.latestKnown,
|
|
1963
|
+
isUpgrade,
|
|
1964
|
+
belowMinVersion
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
function semverGt(a, b) {
|
|
1968
|
+
const seg = (s) => Number(s.replace(/^v/, "")) || 0;
|
|
1969
|
+
const pa = a.split(".").map(seg);
|
|
1970
|
+
const pb = b.split(".").map(seg);
|
|
1971
|
+
for (let i = 0; i < 3; i++) {
|
|
1972
|
+
const x = pa[i] ?? 0;
|
|
1973
|
+
const y = pb[i] ?? 0;
|
|
1974
|
+
if (x !== y) return x > y;
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
function semverLt2(a, b) {
|
|
1979
|
+
const seg = (s) => Number(s.replace(/^v/, "")) || 0;
|
|
1980
|
+
const pa = a.split(".").map(seg);
|
|
1981
|
+
const pb = b.split(".").map(seg);
|
|
1982
|
+
for (let i = 0; i < 3; i++) {
|
|
1983
|
+
const x = pa[i] ?? 0;
|
|
1984
|
+
const y = pb[i] ?? 0;
|
|
1985
|
+
if (x !== y) return x < y;
|
|
1986
|
+
}
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
async function update(opts = {}) {
|
|
1990
|
+
if (process.env.NOIR_DISABLE_UPDATES !== void 0) {
|
|
1991
|
+
fail(
|
|
1992
|
+
EXIT.USAGE,
|
|
1993
|
+
"self-update is disabled (NOIR_DISABLE_UPDATES is set); update via your package manager or image rebuild.",
|
|
1994
|
+
opts
|
|
1995
|
+
);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const method = detectActiveMethod3();
|
|
1999
|
+
const rec = readInstallRecord3();
|
|
2000
|
+
const currentVersion = rec?.version ?? null;
|
|
2001
|
+
if (opts.check === true) {
|
|
2002
|
+
const checkSpin = spinner("Checking for updates...", opts).start();
|
|
2003
|
+
const latest2 = await fetchLatestVersion("latest");
|
|
2004
|
+
if (latest2) {
|
|
2005
|
+
checkSpin.succeed(`Latest: ${latest2} (you have ${currentVersion ?? "unknown"})`);
|
|
2006
|
+
if (opts.json === true) {
|
|
2007
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: { latest: latest2, currentVersion } })}
|
|
2008
|
+
`);
|
|
2009
|
+
}
|
|
2010
|
+
} else {
|
|
2011
|
+
checkSpin.fail("Could not reach the registry.");
|
|
2012
|
+
fail(EXIT.ERROR, "Could not reach the registry.", opts);
|
|
2013
|
+
}
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
const fetchSpin = spinner("Checking for updates...", opts).start();
|
|
2017
|
+
const latest = await fetchLatestVersion("latest");
|
|
2018
|
+
if (latest === null) {
|
|
2019
|
+
fetchSpin.fail("Could not reach the registry.");
|
|
2020
|
+
fail(EXIT.ERROR, "Could not reach the registry.", opts);
|
|
2021
|
+
}
|
|
2022
|
+
fetchSpin.succeed(`Latest: ${latest}`);
|
|
2023
|
+
let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
|
|
2024
|
+
try {
|
|
2025
|
+
minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2028
|
+
const target = buildUpdateTarget({
|
|
2029
|
+
method,
|
|
2030
|
+
channel: "latest",
|
|
2031
|
+
spec: opts.spec,
|
|
2032
|
+
currentVersion,
|
|
2033
|
+
latestKnown: latest,
|
|
2034
|
+
minVersion
|
|
2035
|
+
});
|
|
2036
|
+
if (target.belowMinVersion) {
|
|
2037
|
+
warn(`Target ${target.targetSpec} is below the minVersion floor (${minVersion}).`);
|
|
2038
|
+
fail(
|
|
2039
|
+
EXIT.USAGE,
|
|
2040
|
+
`refusing update to ${target.targetSpec} (below minVersion ${minVersion})`,
|
|
2041
|
+
opts
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
if (!target.isUpgrade) {
|
|
2045
|
+
fetchSpin.succeed(`noir ${currentVersion} is up to date`);
|
|
2046
|
+
if (opts.json === true) {
|
|
2047
|
+
process.stdout.write(
|
|
2048
|
+
`${JSON.stringify({ ok: true, data: { version: currentVersion, upToDate: true } })}
|
|
2049
|
+
`
|
|
2050
|
+
);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
info(`noir ${currentVersion} is up to date.`, opts);
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
if (method === "native") {
|
|
2057
|
+
const updateSpin = spinner(
|
|
2058
|
+
`Updating to ${target.latestKnown} via native installer...`,
|
|
2059
|
+
opts
|
|
2060
|
+
).start();
|
|
2061
|
+
const res = await installManagedNode({
|
|
2062
|
+
version: target.targetSpec === "latest" ? void 0 : target.targetSpec,
|
|
2063
|
+
env: process.env
|
|
2064
|
+
});
|
|
2065
|
+
if (!res.ok) {
|
|
2066
|
+
updateSpin.fail("Update failed");
|
|
2067
|
+
fail(EXIT.ERROR, res.error ?? "update failed", opts);
|
|
2068
|
+
}
|
|
2069
|
+
ensureShimExecutable2();
|
|
2070
|
+
updateSpin.succeed(`Updated to noir ${res.version}`);
|
|
2071
|
+
if (opts.json === true) {
|
|
2072
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: { version: res.version } })}
|
|
2073
|
+
`);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
success(`Updated to ${res.version}.`);
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
const cmd = updateCmdFor(method, target.targetSpec);
|
|
2080
|
+
if (!cmd)
|
|
2081
|
+
fail(EXIT.USAGE, `cannot auto-update a ${method} install; use the manager directly`, opts);
|
|
2082
|
+
const mgrSpin = spinner(`Updating via ${method}...`, opts).start();
|
|
2083
|
+
const { code, stderr } = await runManagerCmd2(cmd, { env: process.env });
|
|
2084
|
+
if (code !== 0) {
|
|
2085
|
+
mgrSpin.fail(`Update failed`);
|
|
2086
|
+
fail(EXIT.ERROR, `update failed: ${stderr.slice(0, 300)}`, opts);
|
|
2087
|
+
}
|
|
2088
|
+
mgrSpin.succeed("Updated");
|
|
2089
|
+
if (opts.json === true) {
|
|
2090
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: { version: target.targetSpec } })}
|
|
2091
|
+
`);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
success("Updated.");
|
|
2095
|
+
}
|
|
2096
|
+
function updateCmdFor(method, spec) {
|
|
2097
|
+
switch (method) {
|
|
2098
|
+
case "npm":
|
|
2099
|
+
return ["npm", "install", "-g", `@noir-ai/cli@${spec}`];
|
|
2100
|
+
case "pnpm":
|
|
2101
|
+
return ["pnpm", "add", "-g", `@noir-ai/cli@${spec}`];
|
|
2102
|
+
case "yarn":
|
|
2103
|
+
return ["yarn", "global", "add", `@noir-ai/cli@${spec}`];
|
|
2104
|
+
case "bun":
|
|
2105
|
+
return ["bun", "add", "-g", `@noir-ai/cli@${spec}`];
|
|
2106
|
+
case "homebrew":
|
|
2107
|
+
return ["brew", "upgrade", "noir"];
|
|
2108
|
+
case "scoop":
|
|
2109
|
+
return ["scoop", "update", "noir"];
|
|
2110
|
+
default:
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function updateAdvice(method) {
|
|
2115
|
+
switch (method) {
|
|
2116
|
+
case "npm":
|
|
2117
|
+
return "run `npm install -g @noir-ai/cli@latest`";
|
|
2118
|
+
case "pnpm":
|
|
2119
|
+
return "run `pnpm add -g @noir-ai/cli@latest`";
|
|
2120
|
+
case "yarn":
|
|
2121
|
+
return "run `yarn global add @noir-ai/cli@latest`";
|
|
2122
|
+
case "bun":
|
|
2123
|
+
return "run `bun add -g @noir-ai/cli@latest`";
|
|
2124
|
+
case "homebrew":
|
|
2125
|
+
return "run `brew upgrade noir`";
|
|
2126
|
+
case "scoop":
|
|
2127
|
+
return "run `scoop update noir`";
|
|
2128
|
+
case "native":
|
|
2129
|
+
return "run `noir update`";
|
|
2130
|
+
default:
|
|
2131
|
+
return "run `noir update`";
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
function buildUpdateNotice(opts) {
|
|
2135
|
+
const { method, currentVersion, latestKnown } = opts;
|
|
2136
|
+
if (currentVersion == null || latestKnown == null) return null;
|
|
2137
|
+
if (!semverGt(latestKnown, currentVersion)) return null;
|
|
2138
|
+
return { latestVersion: latestKnown, currentVersion, method, advice: updateAdvice(method) };
|
|
2139
|
+
}
|
|
2140
|
+
async function runAsyncUpdateCheck(opts) {
|
|
2141
|
+
try {
|
|
2142
|
+
const cache = readUpdateCache2();
|
|
2143
|
+
if (!shouldCheckForUpdate({ env: opts.env, configUpdate: opts.configUpdate, cache })) return;
|
|
2144
|
+
const controller = new AbortController();
|
|
2145
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
2146
|
+
const latest = await fetchLatestVersion(opts.configUpdate.channel, controller.signal);
|
|
2147
|
+
clearTimeout(timer);
|
|
2148
|
+
if (latest != null) {
|
|
2149
|
+
writeUpdateCache({
|
|
2150
|
+
lastCheckAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2151
|
+
latestVersion: latest,
|
|
2152
|
+
channel: opts.configUpdate.channel
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
var DEFAULT_UPDATE_CONFIG;
|
|
2159
|
+
var init_update = __esm({
|
|
2160
|
+
"src/commands/update.ts"() {
|
|
2161
|
+
"use strict";
|
|
2162
|
+
init_output();
|
|
2163
|
+
init_install();
|
|
2164
|
+
DEFAULT_UPDATE_CONFIG = {
|
|
2165
|
+
checkEnabled: true,
|
|
2166
|
+
checkIntervalHours: 24,
|
|
2167
|
+
channel: "latest",
|
|
2168
|
+
minVersion: "1.6.0",
|
|
2169
|
+
display: "notice"
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
627
2173
|
|
|
628
2174
|
// src/commands/status.ts
|
|
629
|
-
import { loadProjectInfo as
|
|
2175
|
+
import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
630
2176
|
async function tryTool(caller, name, args) {
|
|
631
2177
|
try {
|
|
632
2178
|
return await caller.callTool(name, args);
|
|
@@ -677,7 +2223,7 @@ function pickMemory(m) {
|
|
|
677
2223
|
}
|
|
678
2224
|
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
679
2225
|
return {
|
|
680
|
-
noir:
|
|
2226
|
+
noir: NOIR_VERSION4,
|
|
681
2227
|
project: { id: project.id, name: project.name },
|
|
682
2228
|
host: project.config.host,
|
|
683
2229
|
daemon: {
|
|
@@ -725,7 +2271,7 @@ function describeMemory(m) {
|
|
|
725
2271
|
if (!m) return "unavailable (memory engine not wired)";
|
|
726
2272
|
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
727
2273
|
}
|
|
728
|
-
function
|
|
2274
|
+
function renderHuman2(p, opts) {
|
|
729
2275
|
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
730
2276
|
definitionList(
|
|
731
2277
|
[
|
|
@@ -745,7 +2291,12 @@ function renderHuman(p, opts) {
|
|
|
745
2291
|
}
|
|
746
2292
|
}
|
|
747
2293
|
async function gatherStatusPayload(opts) {
|
|
748
|
-
|
|
2294
|
+
let project;
|
|
2295
|
+
try {
|
|
2296
|
+
project = loadProjectInfo6(process.cwd());
|
|
2297
|
+
} catch {
|
|
2298
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2299
|
+
}
|
|
749
2300
|
const probe = await probeDaemon(opts);
|
|
750
2301
|
let host = null;
|
|
751
2302
|
let store = null;
|
|
@@ -772,7 +2323,7 @@ async function status(opts) {
|
|
|
772
2323
|
env: process.env,
|
|
773
2324
|
configUpdate: (() => {
|
|
774
2325
|
try {
|
|
775
|
-
const info2 =
|
|
2326
|
+
const info2 = loadProjectInfo6(process.cwd());
|
|
776
2327
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
777
2328
|
} catch {
|
|
778
2329
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -786,37 +2337,35 @@ async function status(opts) {
|
|
|
786
2337
|
`);
|
|
787
2338
|
return;
|
|
788
2339
|
}
|
|
789
|
-
|
|
2340
|
+
renderHuman2(payload, opts);
|
|
790
2341
|
}
|
|
2342
|
+
var init_status = __esm({
|
|
2343
|
+
"src/commands/status.ts"() {
|
|
2344
|
+
"use strict";
|
|
2345
|
+
init_daemon_client();
|
|
2346
|
+
init_output();
|
|
2347
|
+
init_theme();
|
|
2348
|
+
init_update();
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
791
2351
|
|
|
792
2352
|
// src/commands/task.ts
|
|
793
|
-
import { spawnSync } from "child_process";
|
|
794
|
-
import {
|
|
795
|
-
import { loadProjectInfo as
|
|
2353
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2354
|
+
import { writeFileSync } from "fs";
|
|
2355
|
+
import { loadProjectInfo as loadProjectInfo7, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
|
|
796
2356
|
import {
|
|
797
2357
|
PHASES,
|
|
798
2358
|
TASK_CLASSES,
|
|
799
2359
|
writeChangelogStub,
|
|
800
2360
|
writeDecisionStub
|
|
801
2361
|
} from "@noir-ai/workflow";
|
|
802
|
-
var PHASE_SKILL = {
|
|
803
|
-
// C3 curation (2026-08-10): intake+clarify merged into brainstorming;
|
|
804
|
-
// execute → executing-plans; verify → verifying; document → wrap.
|
|
805
|
-
intake: "noir-brainstorming",
|
|
806
|
-
clarify: "noir-brainstorming",
|
|
807
|
-
spec: "noir-spec",
|
|
808
|
-
plan: "noir-planning",
|
|
809
|
-
execute: "noir-executing-plans",
|
|
810
|
-
verify: "noir-verifying",
|
|
811
|
-
document: "noir-wrap"
|
|
812
|
-
};
|
|
813
2362
|
function skillFor(phase) {
|
|
814
2363
|
if (typeof phase !== "string") return null;
|
|
815
2364
|
return PHASE_SKILL[phase] ?? null;
|
|
816
2365
|
}
|
|
817
2366
|
function writeDoneArtifacts(opts, taskId) {
|
|
818
2367
|
try {
|
|
819
|
-
const project =
|
|
2368
|
+
const project = loadProjectInfo7(process.cwd());
|
|
820
2369
|
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
821
2370
|
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
822
2371
|
const nextN = nextArtifactSequence(project.root, "adr");
|
|
@@ -861,8 +2410,8 @@ function renderStatusRow(s, opts) {
|
|
|
861
2410
|
);
|
|
862
2411
|
definitionList(rows, opts);
|
|
863
2412
|
}
|
|
864
|
-
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
865
2413
|
function nextGateAfter(phase) {
|
|
2414
|
+
if (phase === "verify") return "verify";
|
|
866
2415
|
const cur = PHASES.indexOf(phase);
|
|
867
2416
|
for (const p of GATE_PHASES) {
|
|
868
2417
|
if (PHASES.indexOf(p) > cur) return p;
|
|
@@ -995,6 +2544,9 @@ async function taskAdvance(opts) {
|
|
|
995
2544
|
if (typeof opts.force === "string" && opts.force.length > 0) {
|
|
996
2545
|
args.force = { reason: opts.force };
|
|
997
2546
|
}
|
|
2547
|
+
if (opts.skip === true) {
|
|
2548
|
+
args.skip = true;
|
|
2549
|
+
}
|
|
998
2550
|
const res = await callDaemonTool(
|
|
999
2551
|
opts,
|
|
1000
2552
|
"workflow_advance",
|
|
@@ -1048,11 +2600,6 @@ async function taskResume(opts) {
|
|
|
1048
2600
|
const res = await callDaemonTool(opts, "workflow_resume", args);
|
|
1049
2601
|
if (res.resumable !== true) {
|
|
1050
2602
|
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
|
|
1051
|
-
if (opts.json === true) {
|
|
1052
|
-
process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
|
|
1053
|
-
`);
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
2603
|
fail(EXIT.ERROR, `task resume: ${detail}`, opts);
|
|
1057
2604
|
}
|
|
1058
2605
|
if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
|
|
@@ -1122,7 +2669,7 @@ async function taskAbandon(opts) {
|
|
|
1122
2669
|
}
|
|
1123
2670
|
function runCheck(cmd) {
|
|
1124
2671
|
try {
|
|
1125
|
-
const buf =
|
|
2672
|
+
const buf = spawnSync2("sh", ["-c", cmd], {
|
|
1126
2673
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1127
2674
|
encoding: "utf8"
|
|
1128
2675
|
});
|
|
@@ -1132,14 +2679,19 @@ function runCheck(cmd) {
|
|
|
1132
2679
|
const status2 = buf.status;
|
|
1133
2680
|
return {
|
|
1134
2681
|
exitCode: typeof status2 === "number" ? status2 : 1,
|
|
1135
|
-
digest:
|
|
2682
|
+
digest: sha256Hex(out)
|
|
1136
2683
|
};
|
|
1137
2684
|
} catch {
|
|
1138
|
-
return { exitCode: 1, digest:
|
|
2685
|
+
return { exitCode: 1, digest: sha256Hex("") };
|
|
1139
2686
|
}
|
|
1140
2687
|
}
|
|
1141
2688
|
async function taskVerify(opts) {
|
|
1142
|
-
|
|
2689
|
+
let project;
|
|
2690
|
+
try {
|
|
2691
|
+
project = loadProjectInfo7(process.cwd());
|
|
2692
|
+
} catch {
|
|
2693
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
2694
|
+
}
|
|
1143
2695
|
const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
|
|
1144
2696
|
const all = cfgChecks.map((c2) => ({
|
|
1145
2697
|
name: c2.name,
|
|
@@ -1161,7 +2713,7 @@ async function taskVerify(opts) {
|
|
|
1161
2713
|
if (checks.length === 0) {
|
|
1162
2714
|
fail(
|
|
1163
2715
|
EXIT.USAGE,
|
|
1164
|
-
"task verify: no verify checks configured (set workflow.gate.verify.checks in noir.
|
|
2716
|
+
"task verify: no verify checks configured (set workflow.gate.verify.checks in .noir/config.yml)",
|
|
1165
2717
|
opts
|
|
1166
2718
|
);
|
|
1167
2719
|
}
|
|
@@ -1186,17 +2738,40 @@ async function taskVerify(opts) {
|
|
|
1186
2738
|
const res = await callDaemonTool(opts, "workflow_advance", { evidence });
|
|
1187
2739
|
if (res.ok === true) {
|
|
1188
2740
|
if (opts.json === true) {
|
|
1189
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
|
|
2741
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: { ...res, evidence } })}
|
|
1190
2742
|
`);
|
|
1191
2743
|
return;
|
|
1192
2744
|
}
|
|
1193
|
-
|
|
2745
|
+
const landed = res.state;
|
|
2746
|
+
if (landed === "done") {
|
|
2747
|
+
success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
|
|
2748
|
+
} else {
|
|
2749
|
+
info(
|
|
2750
|
+
`verify \u2014 ${evidence.summary} \u2192 advanced to ${landed} (the verify gate fires at \`done\`; run \`noir task verify\` again when there)`,
|
|
2751
|
+
opts
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
1194
2754
|
renderStatusRow(res, opts);
|
|
1195
2755
|
return;
|
|
1196
2756
|
}
|
|
2757
|
+
const detail = res.error ?? res.pendingGate?.reason ?? "verify failed";
|
|
1197
2758
|
if (opts.json === true) {
|
|
1198
|
-
process.stdout.write(
|
|
1199
|
-
|
|
2759
|
+
process.stdout.write(
|
|
2760
|
+
`${JSON.stringify({
|
|
2761
|
+
ok: false,
|
|
2762
|
+
error: { code: EXIT.ERROR, message: `task verify: ${detail}` },
|
|
2763
|
+
// S9 `{ok:false, error}` + a payload — recovery context + evidence nest
|
|
2764
|
+
// under `data`, not as top-level siblings (same envelope shape as the
|
|
2765
|
+
// success path; consumers read `data` for the full picture).
|
|
2766
|
+
data: {
|
|
2767
|
+
...res.pendingGate !== void 0 ? { pendingGate: res.pendingGate } : {},
|
|
2768
|
+
...res.recovery !== void 0 ? { recovery: res.recovery } : {},
|
|
2769
|
+
evidence
|
|
2770
|
+
}
|
|
2771
|
+
})}
|
|
2772
|
+
`
|
|
2773
|
+
);
|
|
2774
|
+
process.exitCode = EXIT.ERROR;
|
|
1200
2775
|
return;
|
|
1201
2776
|
}
|
|
1202
2777
|
if (res.pendingGate) {
|
|
@@ -1208,9 +2783,8 @@ async function taskVerify(opts) {
|
|
|
1208
2783
|
).join(" | ")}`,
|
|
1209
2784
|
opts
|
|
1210
2785
|
);
|
|
1211
|
-
} else {
|
|
1212
|
-
fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
|
|
1213
2786
|
}
|
|
2787
|
+
fail(EXIT.ERROR, `task verify: ${detail}`, opts);
|
|
1214
2788
|
}
|
|
1215
2789
|
async function taskResearch(opts) {
|
|
1216
2790
|
const s = await fetchStatus(opts, opts.id);
|
|
@@ -1254,7 +2828,7 @@ async function taskDecompose(opts) {
|
|
|
1254
2828
|
if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
|
|
1255
2829
|
const plan = {
|
|
1256
2830
|
capabilityId: cap,
|
|
1257
|
-
intent: `Capability ${cap} \u2014
|
|
2831
|
+
intent: `Capability ${cap} \u2014 offline template (run with a provider for an LLM-drafted plan)`,
|
|
1258
2832
|
slices: [
|
|
1259
2833
|
{
|
|
1260
2834
|
id: "s1-walking-skeleton",
|
|
@@ -1272,6 +2846,19 @@ async function taskDecompose(opts) {
|
|
|
1272
2846
|
],
|
|
1273
2847
|
status: { "s1-walking-skeleton": "planned" }
|
|
1274
2848
|
};
|
|
2849
|
+
if (typeof opts.out === "string" && opts.out.length > 0) {
|
|
2850
|
+
try {
|
|
2851
|
+
writeFileSync(opts.out, `${JSON.stringify(plan, null, 2)}
|
|
2852
|
+
`, "utf8");
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
fail(
|
|
2855
|
+
EXIT.ERROR,
|
|
2856
|
+
`task decompose: could not write ${opts.out}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2857
|
+
opts
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
if (opts.json !== true) info(`wrote plan to ${opts.out}`, opts);
|
|
2861
|
+
}
|
|
1275
2862
|
if (opts.json === true) {
|
|
1276
2863
|
process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
|
|
1277
2864
|
`);
|
|
@@ -1283,9 +2870,37 @@ async function taskDecompose(opts) {
|
|
|
1283
2870
|
}
|
|
1284
2871
|
tip("each piece enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document workflow", opts);
|
|
1285
2872
|
}
|
|
2873
|
+
var PHASE_SKILL, GATE_PHASES;
|
|
2874
|
+
var init_task = __esm({
|
|
2875
|
+
"src/commands/task.ts"() {
|
|
2876
|
+
"use strict";
|
|
2877
|
+
init_daemon_client();
|
|
2878
|
+
init_output();
|
|
2879
|
+
init_theme();
|
|
2880
|
+
PHASE_SKILL = {
|
|
2881
|
+
// C3 curation (2026-08-10): intake+clarify merged into brainstorming;
|
|
2882
|
+
// execute → executing-plans; verify → verifying; document → wrap.
|
|
2883
|
+
intake: "noir-brainstorming",
|
|
2884
|
+
clarify: "noir-brainstorming",
|
|
2885
|
+
spec: "noir-spec",
|
|
2886
|
+
plan: "noir-planning",
|
|
2887
|
+
execute: "noir-executing-plans",
|
|
2888
|
+
verify: "noir-verifying",
|
|
2889
|
+
document: "noir-wrap"
|
|
2890
|
+
};
|
|
2891
|
+
GATE_PHASES = ["spec", "plan", "verify"];
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
1286
2894
|
|
|
1287
2895
|
// src/commands/handoff.ts
|
|
1288
|
-
|
|
2896
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2897
|
+
import { join as join3 } from "path";
|
|
2898
|
+
import {
|
|
2899
|
+
defaultHandoffBlock,
|
|
2900
|
+
hostLaunchDirective,
|
|
2901
|
+
resolveAdapter as resolveAdapter2
|
|
2902
|
+
} from "@noir-ai/adapters";
|
|
2903
|
+
import { loadProjectInfo as loadProjectInfo8, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
|
|
1289
2904
|
function heading(name, id) {
|
|
1290
2905
|
return `# Noir handoff \u2014 ${name} (${id})`;
|
|
1291
2906
|
}
|
|
@@ -1381,7 +2996,7 @@ function toHostPayload(p) {
|
|
|
1381
2996
|
}
|
|
1382
2997
|
function resolveDirective(root, p) {
|
|
1383
2998
|
const ctx = { root };
|
|
1384
|
-
const adapter =
|
|
2999
|
+
const adapter = resolveAdapter2(p.host);
|
|
1385
3000
|
const hostPayload = toHostPayload(p);
|
|
1386
3001
|
if (typeof adapter.emitHandoff === "function") {
|
|
1387
3002
|
return adapter.emitHandoff(ctx, hostPayload);
|
|
@@ -1422,7 +3037,12 @@ async function probeOnly(opts) {
|
|
|
1422
3037
|
return probeDaemon(opts);
|
|
1423
3038
|
}
|
|
1424
3039
|
async function handoff(opts) {
|
|
1425
|
-
|
|
3040
|
+
let project;
|
|
3041
|
+
try {
|
|
3042
|
+
project = loadProjectInfo8(process.cwd());
|
|
3043
|
+
} catch {
|
|
3044
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
3045
|
+
}
|
|
1426
3046
|
const host = project.config.host;
|
|
1427
3047
|
const snapshot = await gatherStatusPayload(opts);
|
|
1428
3048
|
const probe = await probeOnly(opts);
|
|
@@ -1448,45 +3068,34 @@ async function handoff(opts) {
|
|
|
1448
3068
|
}
|
|
1449
3069
|
};
|
|
1450
3070
|
full.directive = resolveDirective(project.root, full);
|
|
1451
|
-
if (opts.json === true) {
|
|
1452
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
|
|
1453
|
-
`);
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
const md = renderMarkdown(full);
|
|
1457
3071
|
if (opts.write === true) {
|
|
3072
|
+
const md = renderMarkdown(full);
|
|
1458
3073
|
const path = handoffFilePath(project.root, task?.taskId, project.id);
|
|
1459
|
-
|
|
1460
|
-
|
|
3074
|
+
mkdirSync2(join3(project.root, NOIR_DIR, "handoff"), { recursive: true });
|
|
3075
|
+
writeFileSync2(path, md, "utf8");
|
|
1461
3076
|
info(`handoff written to ${path}`, opts);
|
|
3077
|
+
}
|
|
3078
|
+
if (opts.json === true) {
|
|
3079
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
|
|
3080
|
+
`);
|
|
1462
3081
|
return;
|
|
1463
3082
|
}
|
|
1464
|
-
process.stdout.write(
|
|
3083
|
+
if (opts.write !== true) process.stdout.write(renderMarkdown(full));
|
|
1465
3084
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
3085
|
+
var SEED_LIMIT;
|
|
3086
|
+
var init_handoff = __esm({
|
|
3087
|
+
"src/commands/handoff.ts"() {
|
|
3088
|
+
"use strict";
|
|
3089
|
+
init_daemon_client();
|
|
3090
|
+
init_output();
|
|
3091
|
+
init_status();
|
|
3092
|
+
init_task();
|
|
3093
|
+
SEED_LIMIT = 5;
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
1477
3096
|
|
|
1478
3097
|
// src/banner.ts
|
|
1479
3098
|
import gradient from "gradient-string";
|
|
1480
|
-
var NOIR_BLOCK = [
|
|
1481
|
-
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
1482
|
-
"\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",
|
|
1483
|
-
"\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
1484
|
-
"\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
|
|
1485
|
-
"\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
1486
|
-
"\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
|
|
1487
|
-
];
|
|
1488
|
-
var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
1489
|
-
var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
1490
3099
|
function renderBanner(opts = {}) {
|
|
1491
3100
|
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
1492
3101
|
const colorOn = opts.color !== false && useColor();
|
|
@@ -1506,26 +3115,25 @@ function shouldShowBanner(opts) {
|
|
|
1506
3115
|
if (v !== void 0 && v !== "") return false;
|
|
1507
3116
|
return true;
|
|
1508
3117
|
}
|
|
3118
|
+
var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE;
|
|
3119
|
+
var init_banner = __esm({
|
|
3120
|
+
"src/banner.ts"() {
|
|
3121
|
+
"use strict";
|
|
3122
|
+
init_theme();
|
|
3123
|
+
NOIR_BLOCK = [
|
|
3124
|
+
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
3125
|
+
"\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",
|
|
3126
|
+
"\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
3127
|
+
"\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
|
|
3128
|
+
"\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
3129
|
+
"\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
|
|
3130
|
+
];
|
|
3131
|
+
NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
3132
|
+
NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
1509
3135
|
|
|
1510
3136
|
// src/tui/commands/registry.ts
|
|
1511
|
-
var DESTRUCTIVE_PREFIXES = [
|
|
1512
|
-
["context", "index"],
|
|
1513
|
-
["memory", "save"],
|
|
1514
|
-
["memory", "forget"],
|
|
1515
|
-
["memory", "consolidate"],
|
|
1516
|
-
["task", "new"],
|
|
1517
|
-
["task", "advance"],
|
|
1518
|
-
["skills", "sync"],
|
|
1519
|
-
["daemon", "start"],
|
|
1520
|
-
["daemon", "stop"],
|
|
1521
|
-
["daemon", "restart"],
|
|
1522
|
-
["init"],
|
|
1523
|
-
["create"],
|
|
1524
|
-
["sync"],
|
|
1525
|
-
["install"],
|
|
1526
|
-
["update"],
|
|
1527
|
-
["run"]
|
|
1528
|
-
];
|
|
1529
3137
|
function isDestructive(argv) {
|
|
1530
3138
|
return DESTRUCTIVE_PREFIXES.some((prefix) => {
|
|
1531
3139
|
if (argv.length < prefix.length) return false;
|
|
@@ -1566,180 +3174,32 @@ function buildPaletteCommands(program2) {
|
|
|
1566
3174
|
collectLeaves(program2, [], out);
|
|
1567
3175
|
return out;
|
|
1568
3176
|
}
|
|
3177
|
+
var DESTRUCTIVE_PREFIXES;
|
|
3178
|
+
var init_registry = __esm({
|
|
3179
|
+
"src/tui/commands/registry.ts"() {
|
|
3180
|
+
"use strict";
|
|
3181
|
+
DESTRUCTIVE_PREFIXES = [
|
|
3182
|
+
["context", "index"],
|
|
3183
|
+
["memory", "save"],
|
|
3184
|
+
["memory", "forget"],
|
|
3185
|
+
["memory", "consolidate"],
|
|
3186
|
+
["task", "new"],
|
|
3187
|
+
["task", "advance"],
|
|
3188
|
+
["skills", "sync"],
|
|
3189
|
+
["daemon", "start"],
|
|
3190
|
+
["daemon", "stop"],
|
|
3191
|
+
["daemon", "restart"],
|
|
3192
|
+
["init"],
|
|
3193
|
+
["create"],
|
|
3194
|
+
["sync"],
|
|
3195
|
+
["install"],
|
|
3196
|
+
["update"],
|
|
3197
|
+
["run"]
|
|
3198
|
+
];
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
1569
3201
|
|
|
1570
3202
|
// src/tui/commands/sections.ts
|
|
1571
|
-
var HOME_SECTIONS = [
|
|
1572
|
-
{
|
|
1573
|
-
id: "status",
|
|
1574
|
-
label: "Status & context",
|
|
1575
|
-
hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
|
|
1576
|
-
items: [
|
|
1577
|
-
{
|
|
1578
|
-
id: "status",
|
|
1579
|
-
label: "Status",
|
|
1580
|
-
hint: "project + daemon + workflow + store snapshot (noir status)"
|
|
1581
|
-
},
|
|
1582
|
-
{
|
|
1583
|
-
id: "context index",
|
|
1584
|
-
label: "Index project",
|
|
1585
|
-
hint: "(re)index files into context \xB7 \u26A0 destructive",
|
|
1586
|
-
destructive: true
|
|
1587
|
-
},
|
|
1588
|
-
{
|
|
1589
|
-
id: "context search",
|
|
1590
|
-
label: "Search context",
|
|
1591
|
-
hint: "hybrid search over indexed files",
|
|
1592
|
-
needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
1593
|
-
},
|
|
1594
|
-
{
|
|
1595
|
-
id: "doctor",
|
|
1596
|
-
label: "Doctor",
|
|
1597
|
-
hint: "environment + project health checks"
|
|
1598
|
-
},
|
|
1599
|
-
{
|
|
1600
|
-
id: "context status",
|
|
1601
|
-
label: "Context status",
|
|
1602
|
-
hint: "index freshness + counts"
|
|
1603
|
-
}
|
|
1604
|
-
]
|
|
1605
|
-
},
|
|
1606
|
-
{
|
|
1607
|
-
id: "memory",
|
|
1608
|
-
label: "Memory",
|
|
1609
|
-
hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
|
|
1610
|
-
items: [
|
|
1611
|
-
{
|
|
1612
|
-
id: "memory recall",
|
|
1613
|
-
label: "Recall memory",
|
|
1614
|
-
hint: "search cross-session memory",
|
|
1615
|
-
needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
1616
|
-
},
|
|
1617
|
-
{
|
|
1618
|
-
id: "memory save",
|
|
1619
|
-
label: "Save memory",
|
|
1620
|
-
hint: "save an observation \xB7 \u26A0 destructive",
|
|
1621
|
-
destructive: true,
|
|
1622
|
-
needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
|
|
1623
|
-
},
|
|
1624
|
-
{
|
|
1625
|
-
id: "memory sessions",
|
|
1626
|
-
label: "Sessions",
|
|
1627
|
-
hint: "list recent memory sessions"
|
|
1628
|
-
},
|
|
1629
|
-
{
|
|
1630
|
-
id: "memory forget",
|
|
1631
|
-
label: "Forget memory",
|
|
1632
|
-
hint: "forget observations by id \xB7 \u26A0 destructive",
|
|
1633
|
-
destructive: true
|
|
1634
|
-
},
|
|
1635
|
-
{
|
|
1636
|
-
id: "memory consolidate",
|
|
1637
|
-
label: "Consolidate",
|
|
1638
|
-
hint: "fold observations into lessons \xB7 \u26A0 destructive",
|
|
1639
|
-
destructive: true
|
|
1640
|
-
}
|
|
1641
|
-
]
|
|
1642
|
-
},
|
|
1643
|
-
{
|
|
1644
|
-
id: "workflow",
|
|
1645
|
-
label: "Workflow",
|
|
1646
|
-
hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
1647
|
-
items: [
|
|
1648
|
-
{
|
|
1649
|
-
id: "task next",
|
|
1650
|
-
label: "Next task",
|
|
1651
|
-
hint: "suggest next phase + applicable skill"
|
|
1652
|
-
},
|
|
1653
|
-
{
|
|
1654
|
-
id: "task status",
|
|
1655
|
-
label: "Task status",
|
|
1656
|
-
hint: "active task status"
|
|
1657
|
-
},
|
|
1658
|
-
{
|
|
1659
|
-
id: "task advance",
|
|
1660
|
-
label: "Advance task",
|
|
1661
|
-
hint: "advance to next phase \xB7 \u26A0 destructive",
|
|
1662
|
-
destructive: true
|
|
1663
|
-
},
|
|
1664
|
-
{
|
|
1665
|
-
id: "handoff",
|
|
1666
|
-
label: "Handoff",
|
|
1667
|
-
hint: "ready-to-paste host prompt"
|
|
1668
|
-
},
|
|
1669
|
-
{
|
|
1670
|
-
id: "wrap",
|
|
1671
|
-
label: "Wrap (end session)",
|
|
1672
|
-
hint: "session-end handoff",
|
|
1673
|
-
dispatch: ["wrap", "--write"]
|
|
1674
|
-
}
|
|
1675
|
-
]
|
|
1676
|
-
},
|
|
1677
|
-
{
|
|
1678
|
-
id: "setup",
|
|
1679
|
-
label: "Setup & maintenance",
|
|
1680
|
-
hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
|
|
1681
|
-
items: [
|
|
1682
|
-
{
|
|
1683
|
-
id: "init",
|
|
1684
|
-
label: "Init",
|
|
1685
|
-
hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
|
|
1686
|
-
destructive: true
|
|
1687
|
-
},
|
|
1688
|
-
{
|
|
1689
|
-
id: "create",
|
|
1690
|
-
label: "Create",
|
|
1691
|
-
hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
|
|
1692
|
-
destructive: true
|
|
1693
|
-
},
|
|
1694
|
-
{
|
|
1695
|
-
id: "sync",
|
|
1696
|
-
label: "Sync",
|
|
1697
|
-
hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
|
|
1698
|
-
destructive: true
|
|
1699
|
-
},
|
|
1700
|
-
{
|
|
1701
|
-
id: "skills list",
|
|
1702
|
-
label: "Skills list",
|
|
1703
|
-
hint: "list installed skills"
|
|
1704
|
-
},
|
|
1705
|
-
{
|
|
1706
|
-
id: "skills sync",
|
|
1707
|
-
label: "Sync skills",
|
|
1708
|
-
hint: "re-emit builtin skills \xB7 \u26A0 destructive",
|
|
1709
|
-
destructive: true
|
|
1710
|
-
},
|
|
1711
|
-
{
|
|
1712
|
-
id: "update",
|
|
1713
|
-
label: "Update",
|
|
1714
|
-
hint: "check for a new version \xB7 \u26A0 destructive",
|
|
1715
|
-
destructive: true
|
|
1716
|
-
},
|
|
1717
|
-
{
|
|
1718
|
-
id: "install",
|
|
1719
|
-
label: "Install / migrate",
|
|
1720
|
-
hint: "native install / migrate \xB7 \u26A0 destructive",
|
|
1721
|
-
destructive: true
|
|
1722
|
-
}
|
|
1723
|
-
]
|
|
1724
|
-
},
|
|
1725
|
-
{
|
|
1726
|
-
id: "dashboard",
|
|
1727
|
-
label: "Dashboard (full-screen)",
|
|
1728
|
-
hint: "noir tui \xB7 noir palette \xB7 home",
|
|
1729
|
-
items: [
|
|
1730
|
-
{
|
|
1731
|
-
id: "tui",
|
|
1732
|
-
label: "Dashboard (full-screen)",
|
|
1733
|
-
hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
|
|
1734
|
-
},
|
|
1735
|
-
{
|
|
1736
|
-
id: "palette",
|
|
1737
|
-
label: "All commands (fuzzy palette)",
|
|
1738
|
-
hint: "fuzzy search over every command (noir palette)"
|
|
1739
|
-
}
|
|
1740
|
-
]
|
|
1741
|
-
}
|
|
1742
|
-
];
|
|
1743
3203
|
async function resolveSections(commands) {
|
|
1744
3204
|
const byId = new Map(commands.map((c2) => [c2.id, c2]));
|
|
1745
3205
|
const out = [];
|
|
@@ -1759,11 +3219,198 @@ async function resolveSections(commands) {
|
|
|
1759
3219
|
}
|
|
1760
3220
|
return out;
|
|
1761
3221
|
}
|
|
3222
|
+
var HOME_SECTIONS;
|
|
3223
|
+
var init_sections = __esm({
|
|
3224
|
+
"src/tui/commands/sections.ts"() {
|
|
3225
|
+
"use strict";
|
|
3226
|
+
HOME_SECTIONS = [
|
|
3227
|
+
{
|
|
3228
|
+
id: "status",
|
|
3229
|
+
label: "Status & context",
|
|
3230
|
+
hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
|
|
3231
|
+
items: [
|
|
3232
|
+
{
|
|
3233
|
+
id: "status",
|
|
3234
|
+
label: "Status",
|
|
3235
|
+
hint: "project + daemon + workflow + store snapshot (noir status)"
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
id: "context index",
|
|
3239
|
+
label: "Index project",
|
|
3240
|
+
hint: "(re)index files into context \xB7 \u26A0 destructive",
|
|
3241
|
+
destructive: true
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
id: "context search",
|
|
3245
|
+
label: "Search context",
|
|
3246
|
+
hint: "hybrid search over indexed files",
|
|
3247
|
+
needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
id: "doctor",
|
|
3251
|
+
label: "Doctor",
|
|
3252
|
+
hint: "environment + project health checks"
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
id: "context status",
|
|
3256
|
+
label: "Context status",
|
|
3257
|
+
hint: "index freshness + counts"
|
|
3258
|
+
}
|
|
3259
|
+
]
|
|
3260
|
+
},
|
|
3261
|
+
{
|
|
3262
|
+
id: "memory",
|
|
3263
|
+
label: "Memory",
|
|
3264
|
+
hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
|
|
3265
|
+
items: [
|
|
3266
|
+
{
|
|
3267
|
+
id: "memory recall",
|
|
3268
|
+
label: "Recall memory",
|
|
3269
|
+
hint: "search cross-session memory",
|
|
3270
|
+
needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
id: "memory save",
|
|
3274
|
+
label: "Save memory",
|
|
3275
|
+
hint: "save an observation \xB7 \u26A0 destructive",
|
|
3276
|
+
destructive: true,
|
|
3277
|
+
needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
id: "memory sessions",
|
|
3281
|
+
label: "Sessions",
|
|
3282
|
+
hint: "list recent memory sessions"
|
|
3283
|
+
},
|
|
3284
|
+
{
|
|
3285
|
+
id: "memory forget",
|
|
3286
|
+
label: "Forget memory",
|
|
3287
|
+
hint: "forget observations by id \xB7 \u26A0 destructive",
|
|
3288
|
+
destructive: true
|
|
3289
|
+
},
|
|
3290
|
+
{
|
|
3291
|
+
id: "memory consolidate",
|
|
3292
|
+
label: "Consolidate",
|
|
3293
|
+
hint: "fold observations into lessons \xB7 \u26A0 destructive",
|
|
3294
|
+
destructive: true
|
|
3295
|
+
}
|
|
3296
|
+
]
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
id: "workflow",
|
|
3300
|
+
label: "Workflow",
|
|
3301
|
+
hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
3302
|
+
items: [
|
|
3303
|
+
{
|
|
3304
|
+
id: "task next",
|
|
3305
|
+
label: "Next task",
|
|
3306
|
+
hint: "suggest next phase + applicable skill"
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
id: "task status",
|
|
3310
|
+
label: "Task status",
|
|
3311
|
+
hint: "active task status"
|
|
3312
|
+
},
|
|
3313
|
+
{
|
|
3314
|
+
id: "task advance",
|
|
3315
|
+
label: "Advance task",
|
|
3316
|
+
hint: "advance to next phase \xB7 \u26A0 destructive",
|
|
3317
|
+
destructive: true
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
id: "handoff",
|
|
3321
|
+
label: "Handoff",
|
|
3322
|
+
hint: "ready-to-paste host prompt"
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
id: "wrap",
|
|
3326
|
+
label: "Wrap (end session)",
|
|
3327
|
+
hint: "session-end handoff",
|
|
3328
|
+
dispatch: ["wrap", "--write"]
|
|
3329
|
+
}
|
|
3330
|
+
]
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
id: "setup",
|
|
3334
|
+
label: "Setup & maintenance",
|
|
3335
|
+
hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
|
|
3336
|
+
items: [
|
|
3337
|
+
{
|
|
3338
|
+
id: "init",
|
|
3339
|
+
label: "Init",
|
|
3340
|
+
hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
|
|
3341
|
+
destructive: true
|
|
3342
|
+
},
|
|
3343
|
+
{
|
|
3344
|
+
id: "create",
|
|
3345
|
+
label: "Create",
|
|
3346
|
+
hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
|
|
3347
|
+
destructive: true
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
id: "sync",
|
|
3351
|
+
label: "Sync",
|
|
3352
|
+
hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
|
|
3353
|
+
destructive: true
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
id: "skills list",
|
|
3357
|
+
label: "Skills list",
|
|
3358
|
+
hint: "list installed skills"
|
|
3359
|
+
},
|
|
3360
|
+
{
|
|
3361
|
+
id: "skills sync",
|
|
3362
|
+
label: "Sync skills",
|
|
3363
|
+
hint: "re-emit builtin skills \xB7 \u26A0 destructive",
|
|
3364
|
+
destructive: true
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
id: "update",
|
|
3368
|
+
label: "Update",
|
|
3369
|
+
hint: "check for a new version \xB7 \u26A0 destructive",
|
|
3370
|
+
destructive: true
|
|
3371
|
+
},
|
|
3372
|
+
{
|
|
3373
|
+
id: "install",
|
|
3374
|
+
label: "Install / migrate",
|
|
3375
|
+
hint: "native install / migrate \xB7 \u26A0 destructive",
|
|
3376
|
+
destructive: true
|
|
3377
|
+
}
|
|
3378
|
+
]
|
|
3379
|
+
},
|
|
3380
|
+
{
|
|
3381
|
+
id: "dashboard",
|
|
3382
|
+
label: "Dashboard (full-screen)",
|
|
3383
|
+
hint: "noir tui \xB7 noir palette \xB7 home",
|
|
3384
|
+
items: [
|
|
3385
|
+
{
|
|
3386
|
+
id: "tui",
|
|
3387
|
+
label: "Dashboard (full-screen)",
|
|
3388
|
+
hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
|
|
3389
|
+
},
|
|
3390
|
+
{
|
|
3391
|
+
id: "palette",
|
|
3392
|
+
label: "All commands (fuzzy palette)",
|
|
3393
|
+
hint: "fuzzy search over every command (noir palette)"
|
|
3394
|
+
}
|
|
3395
|
+
]
|
|
3396
|
+
}
|
|
3397
|
+
];
|
|
3398
|
+
}
|
|
3399
|
+
});
|
|
1762
3400
|
|
|
1763
3401
|
// src/commands/home.ts
|
|
3402
|
+
import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
3403
|
+
import {
|
|
3404
|
+
detectActiveMethod as detectActiveMethod4,
|
|
3405
|
+
latestVersionFromCache as latestVersionFromCache2,
|
|
3406
|
+
loadProjectInfo as loadProjectInfo9,
|
|
3407
|
+
NOIR_VERSION as NOIR_VERSION5,
|
|
3408
|
+
readInstallRecord as readInstallRecord4,
|
|
3409
|
+
readUpdateCache as readUpdateCache3
|
|
3410
|
+
} from "@noir-ai/core";
|
|
1764
3411
|
function tryProject() {
|
|
1765
3412
|
try {
|
|
1766
|
-
const info2 =
|
|
3413
|
+
const info2 = loadProjectInfo9(process.cwd());
|
|
1767
3414
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
1768
3415
|
} catch {
|
|
1769
3416
|
return null;
|
|
@@ -1774,7 +3421,6 @@ function shouldShowMigrationBanner(rec, currentVersion) {
|
|
|
1774
3421
|
if (rec.dismissedVersions?.includes(currentVersion)) return false;
|
|
1775
3422
|
return true;
|
|
1776
3423
|
}
|
|
1777
|
-
var COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
|
|
1778
3424
|
async function home(opts, deps) {
|
|
1779
3425
|
if (isInteractive(opts) && opts.tui !== false) {
|
|
1780
3426
|
await runMenu(opts, deps);
|
|
@@ -1927,7 +3573,7 @@ async function runMenu(opts, deps) {
|
|
|
1927
3573
|
env: process.env,
|
|
1928
3574
|
configUpdate: (() => {
|
|
1929
3575
|
try {
|
|
1930
|
-
const info2 =
|
|
3576
|
+
const info2 = loadProjectInfo9(process.cwd());
|
|
1931
3577
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
1932
3578
|
} catch {
|
|
1933
3579
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -1948,8 +3594,8 @@ ${COMMANDS_HINT}
|
|
|
1948
3594
|
`
|
|
1949
3595
|
);
|
|
1950
3596
|
}
|
|
1951
|
-
const rec =
|
|
1952
|
-
if (rec && shouldShowMigrationBanner(rec,
|
|
3597
|
+
const rec = readInstallRecord4();
|
|
3598
|
+
if (rec && shouldShowMigrationBanner(rec, NOIR_VERSION5)) {
|
|
1953
3599
|
process.stderr.write(
|
|
1954
3600
|
`
|
|
1955
3601
|
${c.warn(`noir installed via ${rec.method}`)} \u2014 consider \`noir install\` for the native path (auto-update, no npm prefix/PATH issues). Dismiss with: \`noir install --dismiss\` (persisted per version).
|
|
@@ -1958,9 +3604,9 @@ ${COMMANDS_HINT}
|
|
|
1958
3604
|
);
|
|
1959
3605
|
}
|
|
1960
3606
|
const updateNotice = buildUpdateNotice({
|
|
1961
|
-
method:
|
|
1962
|
-
currentVersion:
|
|
1963
|
-
latestKnown:
|
|
3607
|
+
method: detectActiveMethod4(),
|
|
3608
|
+
currentVersion: NOIR_VERSION5,
|
|
3609
|
+
latestKnown: latestVersionFromCache2(readUpdateCache3(), "latest")
|
|
1964
3610
|
});
|
|
1965
3611
|
if (updateNotice) {
|
|
1966
3612
|
process.stderr.write(
|
|
@@ -1974,6 +3620,19 @@ ${COMMANDS_HINT}
|
|
|
1974
3620
|
const sections = await resolveSections(deps.commands ?? []);
|
|
1975
3621
|
await runGroupedMenu(clack, opts, deps, sections);
|
|
1976
3622
|
}
|
|
3623
|
+
var COMMANDS_HINT;
|
|
3624
|
+
var init_home = __esm({
|
|
3625
|
+
"src/commands/home.ts"() {
|
|
3626
|
+
"use strict";
|
|
3627
|
+
init_banner();
|
|
3628
|
+
init_output();
|
|
3629
|
+
init_theme();
|
|
3630
|
+
init_registry();
|
|
3631
|
+
init_sections();
|
|
3632
|
+
init_update();
|
|
3633
|
+
COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
|
|
3634
|
+
}
|
|
3635
|
+
});
|
|
1977
3636
|
|
|
1978
3637
|
// src/commands/memory.ts
|
|
1979
3638
|
function parseLimit2(raw, label, opts) {
|
|
@@ -2022,16 +3681,16 @@ async function memoryRecall(opts) {
|
|
|
2022
3681
|
}
|
|
2023
3682
|
if (!probe.running) {
|
|
2024
3683
|
await withInProcessRead(opts, async (engines) => {
|
|
2025
|
-
const
|
|
3684
|
+
const { hits: hits2, degraded, mode } = await engines.memory.recallWithMeta(opts.query, {
|
|
2026
3685
|
...limit === void 0 ? {} : { limit }
|
|
2027
3686
|
});
|
|
2028
|
-
const data2 = { query: opts.query, hits:
|
|
3687
|
+
const data2 = { query: opts.query, hits: hits2, degraded, mode };
|
|
2029
3688
|
if (opts.json === true) {
|
|
2030
3689
|
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
2031
3690
|
`);
|
|
2032
3691
|
return;
|
|
2033
3692
|
}
|
|
2034
|
-
renderRecall(data2.query,
|
|
3693
|
+
renderRecall(data2.query, data2.hits, data2.degraded, opts);
|
|
2035
3694
|
});
|
|
2036
3695
|
return;
|
|
2037
3696
|
}
|
|
@@ -2235,9 +3894,17 @@ async function memoryConsolidate(opts) {
|
|
|
2235
3894
|
const suffix = logged ? " (logged)" : "";
|
|
2236
3895
|
fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
|
|
2237
3896
|
}
|
|
3897
|
+
var init_memory = __esm({
|
|
3898
|
+
"src/commands/memory.ts"() {
|
|
3899
|
+
"use strict";
|
|
3900
|
+
init_daemon_client();
|
|
3901
|
+
init_output();
|
|
3902
|
+
init_theme();
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
2238
3905
|
|
|
2239
3906
|
// src/commands/release.ts
|
|
2240
|
-
import { execSync, spawnSync as
|
|
3907
|
+
import { execSync, spawnSync as spawnSync3 } from "child_process";
|
|
2241
3908
|
function describe(step, who) {
|
|
2242
3909
|
const m = {
|
|
2243
3910
|
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
@@ -2256,21 +3923,6 @@ function describe(step, who) {
|
|
|
2256
3923
|
};
|
|
2257
3924
|
return `${m[step]} [${who}]`;
|
|
2258
3925
|
}
|
|
2259
|
-
var ALL = [
|
|
2260
|
-
"preflight",
|
|
2261
|
-
"bump",
|
|
2262
|
-
"gate",
|
|
2263
|
-
"commit",
|
|
2264
|
-
"ci-develop",
|
|
2265
|
-
"beta-tag",
|
|
2266
|
-
"human-approve-beta",
|
|
2267
|
-
"merge-main",
|
|
2268
|
-
"ci-main",
|
|
2269
|
-
"stable-tag",
|
|
2270
|
-
"human-approve-stable",
|
|
2271
|
-
"dist-bump",
|
|
2272
|
-
"sync"
|
|
2273
|
-
];
|
|
2274
3926
|
async function release(opts) {
|
|
2275
3927
|
if (opts.dryRun) {
|
|
2276
3928
|
log("noir release --dry-run (no changes will be made)", opts);
|
|
@@ -2296,7 +3948,7 @@ async function release(opts) {
|
|
|
2296
3948
|
if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
|
|
2297
3949
|
success("preflight: \u2713 clean, on develop", opts);
|
|
2298
3950
|
info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
|
|
2299
|
-
|
|
3951
|
+
spawnSync3("node", ["scripts/bump-version.mjs", version], {
|
|
2300
3952
|
encoding: "utf8",
|
|
2301
3953
|
stdio: "inherit"
|
|
2302
3954
|
});
|
|
@@ -2308,8 +3960,8 @@ async function release(opts) {
|
|
|
2308
3960
|
});
|
|
2309
3961
|
success("gate: \u2713 full gate green", opts);
|
|
2310
3962
|
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
2311
|
-
|
|
2312
|
-
|
|
3963
|
+
spawnSync3("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
3964
|
+
spawnSync3("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
2313
3965
|
encoding: "utf8",
|
|
2314
3966
|
stdio: "inherit"
|
|
2315
3967
|
});
|
|
@@ -2331,30 +3983,163 @@ async function release(opts) {
|
|
|
2331
3983
|
fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
2332
3984
|
}
|
|
2333
3985
|
}
|
|
3986
|
+
var ALL;
|
|
3987
|
+
var init_release = __esm({
|
|
3988
|
+
"src/commands/release.ts"() {
|
|
3989
|
+
"use strict";
|
|
3990
|
+
init_output();
|
|
3991
|
+
ALL = [
|
|
3992
|
+
"preflight",
|
|
3993
|
+
"bump",
|
|
3994
|
+
"gate",
|
|
3995
|
+
"commit",
|
|
3996
|
+
"ci-develop",
|
|
3997
|
+
"beta-tag",
|
|
3998
|
+
"human-approve-beta",
|
|
3999
|
+
"merge-main",
|
|
4000
|
+
"ci-main",
|
|
4001
|
+
"stable-tag",
|
|
4002
|
+
"human-approve-stable",
|
|
4003
|
+
"dist-bump",
|
|
4004
|
+
"sync"
|
|
4005
|
+
];
|
|
4006
|
+
}
|
|
4007
|
+
});
|
|
2334
4008
|
|
|
2335
|
-
// src/
|
|
2336
|
-
import {
|
|
2337
|
-
import {
|
|
2338
|
-
|
|
4009
|
+
// src/shell-bridge.ts
|
|
4010
|
+
import { spawn } from "child_process";
|
|
4011
|
+
import { basename } from "path";
|
|
4012
|
+
function isEligibleForShellFallback(name, opts = {}) {
|
|
4013
|
+
const { env = process.env, platform = process.platform } = opts;
|
|
4014
|
+
if (platform === "win32") return false;
|
|
4015
|
+
if (name.length === 0 || name.includes("/") || !NAME_RE.test(name)) return false;
|
|
4016
|
+
const shell = env.SHELL;
|
|
4017
|
+
if (!shell || shell.length === 0) return false;
|
|
4018
|
+
return SUPPORTED_SHELLS.has(basename(shell).toLowerCase());
|
|
4019
|
+
}
|
|
4020
|
+
function parseProbeOutput(stdout) {
|
|
4021
|
+
let path = "";
|
|
4022
|
+
let kind = "";
|
|
4023
|
+
for (const line of stdout.split("\n")) {
|
|
4024
|
+
if (line.startsWith("N:")) path = line.slice(2).trim();
|
|
4025
|
+
else if (line.startsWith("K:")) kind = line.slice(2).trim();
|
|
4026
|
+
}
|
|
4027
|
+
return { path, kind };
|
|
4028
|
+
}
|
|
4029
|
+
function classifyResolution(path, kind) {
|
|
4030
|
+
if (path.trim().startsWith("/")) return "path";
|
|
4031
|
+
const k = kind.trim().toLowerCase();
|
|
4032
|
+
if (k.includes("alias")) return "alias";
|
|
4033
|
+
if (k.includes("function")) return "function";
|
|
4034
|
+
return "none";
|
|
4035
|
+
}
|
|
4036
|
+
function shellKind(shell) {
|
|
4037
|
+
const kind = basename(shell).toLowerCase();
|
|
4038
|
+
return SUPPORTED_SHELLS.has(kind) ? kind : null;
|
|
4039
|
+
}
|
|
4040
|
+
function probeCommand(name, shell, opts) {
|
|
4041
|
+
const kind = shellKind(shell);
|
|
4042
|
+
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS2;
|
|
4043
|
+
return new Promise((resolve2) => {
|
|
4044
|
+
const script = kind ? PROBE_SCRIPTS[kind] : "";
|
|
4045
|
+
const flags = kind ? PROBE_FLAGS[kind] : "-lic";
|
|
4046
|
+
const probeArgs = kind === "fish" ? [name] : ["noirbridge", name];
|
|
4047
|
+
const child = spawn(shell, [flags, script, ...probeArgs], {
|
|
4048
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4049
|
+
detached: true,
|
|
4050
|
+
// new process group so the timeout can kill rc-spawned children
|
|
4051
|
+
...opts.env ? { env: opts.env } : {}
|
|
4052
|
+
});
|
|
4053
|
+
let stdout = "";
|
|
4054
|
+
child.stdout.setEncoding("utf8");
|
|
4055
|
+
child.stdout.on("data", (c2) => {
|
|
4056
|
+
stdout += c2;
|
|
4057
|
+
});
|
|
4058
|
+
child.stderr.resume();
|
|
4059
|
+
const timer = setTimeout(() => {
|
|
4060
|
+
if (child.pid) {
|
|
4061
|
+
try {
|
|
4062
|
+
process.kill(-child.pid, "SIGKILL");
|
|
4063
|
+
} catch {
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
resolve2({ stdout, timedOut: true });
|
|
4067
|
+
}, timeoutMs);
|
|
4068
|
+
child.on("error", () => {
|
|
4069
|
+
clearTimeout(timer);
|
|
4070
|
+
resolve2({ stdout, timedOut: false });
|
|
4071
|
+
});
|
|
4072
|
+
child.on("close", () => {
|
|
4073
|
+
clearTimeout(timer);
|
|
4074
|
+
resolve2({ stdout, timedOut: false });
|
|
4075
|
+
});
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
async function resolveCommandViaShell(name, opts = {}) {
|
|
4079
|
+
const { env = process.env } = opts;
|
|
4080
|
+
if (!isEligibleForShellFallback(name, opts)) return { kind: "unsupported" };
|
|
4081
|
+
const shell = env.SHELL;
|
|
4082
|
+
const cached = pathCache.get(`${shell}:${name}`);
|
|
4083
|
+
if (cached) return { kind: "path", path: cached, shell };
|
|
4084
|
+
const probe = await probeCommand(name, shell, opts);
|
|
4085
|
+
if (probe.timedOut) {
|
|
4086
|
+
return {
|
|
4087
|
+
kind: "error",
|
|
4088
|
+
message: `shell resolution timed out after ${opts.timeoutMs ?? PROBE_TIMEOUT_MS2}ms`
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
const { path, kind } = parseProbeOutput(probe.stdout);
|
|
4092
|
+
const cls = classifyResolution(path, kind);
|
|
4093
|
+
if (cls === "path") {
|
|
4094
|
+
pathCache.set(`${shell}:${name}`, path);
|
|
4095
|
+
return { kind: "path", path, shell };
|
|
4096
|
+
}
|
|
4097
|
+
if (cls === "alias" || cls === "function") return { kind: cls, shell };
|
|
4098
|
+
return { kind: "none", shell };
|
|
4099
|
+
}
|
|
4100
|
+
function buildBridgeArgs(name, hostArgs, shell) {
|
|
4101
|
+
const kind = shellKind(shell) ?? "zsh";
|
|
4102
|
+
if (kind === "fish") {
|
|
4103
|
+
return { binary: shell, args: ["-ic", `${name} $argv`, ...hostArgs] };
|
|
4104
|
+
}
|
|
4105
|
+
if (kind === "bash") {
|
|
4106
|
+
return {
|
|
4107
|
+
binary: shell,
|
|
4108
|
+
args: [
|
|
4109
|
+
"-lic",
|
|
4110
|
+
`shopt -s expand_aliases; [ -r "$HOME/.bashrc" ] && . "$HOME/.bashrc" 2>/dev/null; ${name} "$@"`,
|
|
4111
|
+
"noirbridge",
|
|
4112
|
+
...hostArgs
|
|
4113
|
+
]
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
return { binary: shell, args: ["-lic", `${name} "$@"`, "noirbridge", ...hostArgs] };
|
|
4117
|
+
}
|
|
4118
|
+
var PROBE_TIMEOUT_MS2, NAME_RE, SUPPORTED_SHELLS, PROBE_FLAGS, PROBE_SCRIPTS, pathCache;
|
|
4119
|
+
var init_shell_bridge = __esm({
|
|
4120
|
+
"src/shell-bridge.ts"() {
|
|
4121
|
+
"use strict";
|
|
4122
|
+
PROBE_TIMEOUT_MS2 = 3e3;
|
|
4123
|
+
NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
4124
|
+
SUPPORTED_SHELLS = /* @__PURE__ */ new Set(["zsh", "bash", "fish"]);
|
|
4125
|
+
PROBE_FLAGS = {
|
|
4126
|
+
zsh: "-lic",
|
|
4127
|
+
bash: "-lic",
|
|
4128
|
+
fish: "-ic"
|
|
4129
|
+
};
|
|
4130
|
+
PROBE_SCRIPTS = {
|
|
4131
|
+
zsh: 'printf "\\nN:%s" "$(whence -p "$1")"; printf "\\nK:%s" "$(whence -w "$1")"',
|
|
4132
|
+
bash: 'shopt -s expand_aliases; [ -r "$HOME/.bashrc" ] && . "$HOME/.bashrc" 2>/dev/null; printf "\\nN:%s" "$(type -P "$1")"; printf "\\nK:%s" "$(type -t "$1")"',
|
|
4133
|
+
fish: 'printf "\\nN:%s" (type -P "$argv[1]"); printf "\\nK:%s" (type -t "$argv[1]")'
|
|
4134
|
+
};
|
|
4135
|
+
pathCache = /* @__PURE__ */ new Map();
|
|
4136
|
+
}
|
|
4137
|
+
});
|
|
2339
4138
|
|
|
2340
4139
|
// src/orchestrator.ts
|
|
2341
|
-
import { spawn } from "child_process";
|
|
4140
|
+
import { spawn as spawn2 } from "child_process";
|
|
4141
|
+
import { constants } from "os";
|
|
2342
4142
|
import { createInterface } from "readline";
|
|
2343
|
-
var HOST_BINARY = {
|
|
2344
|
-
claude: "claude",
|
|
2345
|
-
gemini: "gemini",
|
|
2346
|
-
opencode: "opencode",
|
|
2347
|
-
cursor: "cursor-agent",
|
|
2348
|
-
"agents-md": null
|
|
2349
|
-
// emits AGENTS.md — a file host, not a spawnable CLI
|
|
2350
|
-
};
|
|
2351
|
-
var HOST_FLAGS = {
|
|
2352
|
-
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
2353
|
-
gemini: ["-p", "--output-format", "stream-json"],
|
|
2354
|
-
opencode: ["run"],
|
|
2355
|
-
cursor: ["-p"],
|
|
2356
|
-
"agents-md": []
|
|
2357
|
-
};
|
|
2358
4143
|
function resolveHostRun(host, customBinary) {
|
|
2359
4144
|
const binary = customBinary && customBinary.length > 0 ? customBinary : HOST_BINARY[host];
|
|
2360
4145
|
if (!binary) return null;
|
|
@@ -2369,6 +4154,10 @@ function str(v) {
|
|
|
2369
4154
|
function num(v) {
|
|
2370
4155
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
2371
4156
|
}
|
|
4157
|
+
function signalNumber(signal) {
|
|
4158
|
+
const n = constants.signals[signal];
|
|
4159
|
+
return typeof n === "number" ? n : void 0;
|
|
4160
|
+
}
|
|
2372
4161
|
function usageFrom(v) {
|
|
2373
4162
|
const o = asObj(v);
|
|
2374
4163
|
if (!o) return void 0;
|
|
@@ -2416,7 +4205,14 @@ function normalizeStreamEvent(raw) {
|
|
|
2416
4205
|
kind: "assistant",
|
|
2417
4206
|
messageId: message ? str(message.id) : void 0,
|
|
2418
4207
|
text: messageText(message),
|
|
2419
|
-
usage: message ? usageFrom(message.usage) : void 0
|
|
4208
|
+
usage: message ? usageFrom(message.usage) : void 0,
|
|
4209
|
+
// An API error message is a diagnostic, not the answer — flag it so the
|
|
4210
|
+
// caller never streams it as assistant text (optional-when-true keeps
|
|
4211
|
+
// exact-shape consumers of the existing union intact).
|
|
4212
|
+
...r.is_api_error_message === true || typeof r.error === "string" ? {
|
|
4213
|
+
isError: true,
|
|
4214
|
+
...typeof r.error === "string" ? { errorCategory: r.error } : {}
|
|
4215
|
+
} : {}
|
|
2420
4216
|
};
|
|
2421
4217
|
}
|
|
2422
4218
|
if (type === "result") {
|
|
@@ -2431,60 +4227,41 @@ function normalizeStreamEvent(raw) {
|
|
|
2431
4227
|
}
|
|
2432
4228
|
return { kind: "other" };
|
|
2433
4229
|
}
|
|
2434
|
-
var UsageReducer = class {
|
|
2435
|
-
maxByMessage = /* @__PURE__ */ new Map();
|
|
2436
|
-
input = 0;
|
|
2437
|
-
output = 0;
|
|
2438
|
-
costUsd = 0;
|
|
2439
|
-
turns = 0;
|
|
2440
|
-
/** Feed one normalized event into the reducer. */
|
|
2441
|
-
add(event) {
|
|
2442
|
-
if (event.kind === "assistant" && event.messageId && event.usage) {
|
|
2443
|
-
const key = event.messageId;
|
|
2444
|
-
const input = event.usage.inputTokens ?? 0;
|
|
2445
|
-
const output = event.usage.outputTokens ?? 0;
|
|
2446
|
-
const prev = this.maxByMessage.get(key);
|
|
2447
|
-
if (!prev) {
|
|
2448
|
-
this.maxByMessage.set(key, { input, output });
|
|
2449
|
-
this.input += input;
|
|
2450
|
-
this.output += output;
|
|
2451
|
-
} else {
|
|
2452
|
-
this.input += Math.max(0, input - prev.input);
|
|
2453
|
-
this.output += Math.max(0, output - prev.output);
|
|
2454
|
-
this.maxByMessage.set(key, {
|
|
2455
|
-
input: Math.max(prev.input, input),
|
|
2456
|
-
output: Math.max(prev.output, output)
|
|
2457
|
-
});
|
|
2458
|
-
}
|
|
2459
|
-
} else if (event.kind === "result") {
|
|
2460
|
-
if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
|
|
2461
|
-
if (event.numTurns !== void 0) this.turns = event.numTurns;
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
/** The current accumulator snapshot. */
|
|
2465
|
-
snapshot() {
|
|
2466
|
-
return {
|
|
2467
|
-
inputTokens: this.input,
|
|
2468
|
-
outputTokens: this.output,
|
|
2469
|
-
totalCostUsd: this.costUsd,
|
|
2470
|
-
numTurns: this.turns
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
};
|
|
2474
4230
|
function runHost(opts) {
|
|
2475
4231
|
const spec = resolveHostRun(opts.host, opts.customBinary);
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
4232
|
+
if (!spec) {
|
|
4233
|
+
return Promise.reject(
|
|
4234
|
+
new Error(`host '${opts.host}' is not a spawnable CLI (no default command)`)
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
return spawnAndConsume(
|
|
4238
|
+
spec.binary,
|
|
4239
|
+
[...spec.flags, ...opts.extraArgs ?? [], opts.prompt],
|
|
4240
|
+
opts,
|
|
4241
|
+
false
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
function spawnAndConsume(binary, args, opts, shellRun) {
|
|
4245
|
+
return new Promise((resolve2, reject) => {
|
|
4246
|
+
const child = spawn2(binary, [...args], {
|
|
4247
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4248
|
+
...opts.env ? { env: opts.env } : {}
|
|
2483
4249
|
});
|
|
2484
4250
|
const reducer = new UsageReducer();
|
|
2485
4251
|
let eventCount = 0;
|
|
2486
4252
|
let stderrBuf = "";
|
|
2487
|
-
|
|
4253
|
+
let errored = false;
|
|
4254
|
+
let errorText;
|
|
4255
|
+
let errorCategory;
|
|
4256
|
+
let spawnError = null;
|
|
4257
|
+
child.on("error", (err) => {
|
|
4258
|
+
spawnError = err;
|
|
4259
|
+
if (!shellRun && err.code === "ENOENT") {
|
|
4260
|
+
void shellFallback(binary, args, opts, resolve2, reject, err);
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
reject(err);
|
|
4264
|
+
});
|
|
2488
4265
|
const rl = createInterface({ input: child.stdout });
|
|
2489
4266
|
rl.on("line", (line) => {
|
|
2490
4267
|
opts.onLine?.(line);
|
|
@@ -2493,6 +4270,13 @@ function runHost(opts) {
|
|
|
2493
4270
|
const event = normalizeStreamEvent(raw);
|
|
2494
4271
|
if (event === null) return;
|
|
2495
4272
|
eventCount += 1;
|
|
4273
|
+
if (event.kind === "assistant" && event.isError === true) {
|
|
4274
|
+
errored = true;
|
|
4275
|
+
if (errorText === void 0 && event.text && event.text.length > 0) errorText = event.text;
|
|
4276
|
+
if (errorCategory === void 0 && event.errorCategory) errorCategory = event.errorCategory;
|
|
4277
|
+
} else if (event.kind === "result" && event.isError === true) {
|
|
4278
|
+
errored = true;
|
|
4279
|
+
}
|
|
2496
4280
|
reducer.add(event);
|
|
2497
4281
|
opts.onEvent?.(event);
|
|
2498
4282
|
});
|
|
@@ -2500,13 +4284,161 @@ function runHost(opts) {
|
|
|
2500
4284
|
child.stderr.on("data", (chunk) => {
|
|
2501
4285
|
stderrBuf += chunk;
|
|
2502
4286
|
});
|
|
2503
|
-
child.on("close", (code) => {
|
|
2504
|
-
|
|
4287
|
+
child.on("close", (code, signal) => {
|
|
4288
|
+
if (spawnError !== null) return;
|
|
4289
|
+
const killed = code === null && signal != null;
|
|
4290
|
+
resolve2({
|
|
4291
|
+
exitCode: killed ? 128 + (signalNumber(signal) ?? 0) : code ?? 0,
|
|
4292
|
+
usage: reducer.snapshot(),
|
|
4293
|
+
eventCount,
|
|
4294
|
+
stderr: stderrBuf,
|
|
4295
|
+
isError: errored || killed,
|
|
4296
|
+
errorText: killed && errorText === void 0 ? `terminated by signal ${signal}` : errorText,
|
|
4297
|
+
errorCategory
|
|
4298
|
+
});
|
|
2505
4299
|
});
|
|
2506
4300
|
});
|
|
2507
4301
|
}
|
|
4302
|
+
async function shellFallback(binary, args, opts, resolve2, reject, originalErr) {
|
|
4303
|
+
try {
|
|
4304
|
+
const res = await resolveCommandViaShell(binary, { env: opts.env ?? process.env });
|
|
4305
|
+
if (res.kind === "path") {
|
|
4306
|
+
resolve2(await spawnAndConsume(res.path, args, opts, true));
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
if (res.kind === "alias" || res.kind === "function") {
|
|
4310
|
+
const bridge = buildBridgeArgs(binary, args, res.shell);
|
|
4311
|
+
resolve2(await spawnAndConsume(bridge.binary, bridge.args, opts, true));
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
} catch {
|
|
4315
|
+
}
|
|
4316
|
+
reject(originalErr);
|
|
4317
|
+
}
|
|
4318
|
+
var HOST_BINARY, HOST_FLAGS, UsageReducer;
|
|
4319
|
+
var init_orchestrator = __esm({
|
|
4320
|
+
"src/orchestrator.ts"() {
|
|
4321
|
+
"use strict";
|
|
4322
|
+
init_shell_bridge();
|
|
4323
|
+
HOST_BINARY = {
|
|
4324
|
+
claude: "claude",
|
|
4325
|
+
gemini: "gemini",
|
|
4326
|
+
opencode: "opencode",
|
|
4327
|
+
cursor: "cursor-agent",
|
|
4328
|
+
"agents-md": null
|
|
4329
|
+
// emits AGENTS.md — a file host, not a spawnable CLI
|
|
4330
|
+
};
|
|
4331
|
+
HOST_FLAGS = {
|
|
4332
|
+
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
4333
|
+
gemini: ["-p", "--output-format", "stream-json"],
|
|
4334
|
+
opencode: ["run"],
|
|
4335
|
+
cursor: ["-p"],
|
|
4336
|
+
"agents-md": []
|
|
4337
|
+
};
|
|
4338
|
+
UsageReducer = class {
|
|
4339
|
+
maxByMessage = /* @__PURE__ */ new Map();
|
|
4340
|
+
input = 0;
|
|
4341
|
+
output = 0;
|
|
4342
|
+
costUsd = 0;
|
|
4343
|
+
turns = 0;
|
|
4344
|
+
/** Feed one normalized event into the reducer. */
|
|
4345
|
+
add(event) {
|
|
4346
|
+
if (event.kind === "assistant" && event.messageId && event.usage) {
|
|
4347
|
+
const key = event.messageId;
|
|
4348
|
+
const input = event.usage.inputTokens ?? 0;
|
|
4349
|
+
const output = event.usage.outputTokens ?? 0;
|
|
4350
|
+
const prev = this.maxByMessage.get(key);
|
|
4351
|
+
if (!prev) {
|
|
4352
|
+
this.maxByMessage.set(key, { input, output });
|
|
4353
|
+
this.input += input;
|
|
4354
|
+
this.output += output;
|
|
4355
|
+
} else {
|
|
4356
|
+
this.input += Math.max(0, input - prev.input);
|
|
4357
|
+
this.output += Math.max(0, output - prev.output);
|
|
4358
|
+
this.maxByMessage.set(key, {
|
|
4359
|
+
input: Math.max(prev.input, input),
|
|
4360
|
+
output: Math.max(prev.output, output)
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
} else if (event.kind === "result") {
|
|
4364
|
+
if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
|
|
4365
|
+
if (event.numTurns !== void 0) this.turns = event.numTurns;
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
/** The current accumulator snapshot. */
|
|
4369
|
+
snapshot() {
|
|
4370
|
+
return {
|
|
4371
|
+
inputTokens: this.input,
|
|
4372
|
+
outputTokens: this.output,
|
|
4373
|
+
totalCostUsd: this.costUsd,
|
|
4374
|
+
numTurns: this.turns
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
|
|
4381
|
+
// src/run-profiles.ts
|
|
4382
|
+
import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
|
|
4383
|
+
function loadRunConfig(root) {
|
|
4384
|
+
try {
|
|
4385
|
+
return loadProjectInfo10(root).config;
|
|
4386
|
+
} catch {
|
|
4387
|
+
return null;
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
function expandEnvVars(value, env) {
|
|
4391
|
+
return value.replace(
|
|
4392
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
4393
|
+
(match, name) => env[name] !== void 0 ? env[name] : match
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
function resolveRunProfile(requested, config, env) {
|
|
4397
|
+
const profiles = config.run?.profiles ?? {};
|
|
4398
|
+
const names = Object.keys(profiles);
|
|
4399
|
+
const name = requested ?? env.NOIR_PROFILE ?? config.run?.defaultProfile;
|
|
4400
|
+
if (name === void 0 || name.length === 0) {
|
|
4401
|
+
return { ok: true, profile: {} };
|
|
4402
|
+
}
|
|
4403
|
+
const profile = profiles[name];
|
|
4404
|
+
if (!profile) {
|
|
4405
|
+
const available = names.length > 0 ? names.join(", ") : "(none defined \u2014 add a run.profiles block to .noir/config.yml)";
|
|
4406
|
+
return {
|
|
4407
|
+
ok: false,
|
|
4408
|
+
message: `unknown run profile "${name}" \u2014 no profile named "${name}" is defined under run.profiles in .noir/config.yml. Available: ${available}`
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
const envExpanded = {};
|
|
4412
|
+
if (profile.env) {
|
|
4413
|
+
for (const [k, v] of Object.entries(profile.env)) {
|
|
4414
|
+
envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return {
|
|
4418
|
+
ok: true,
|
|
4419
|
+
profile: { binary: profile.binary, env: envExpanded, args: profile.args, profileName: name }
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
function listProfiles(config) {
|
|
4423
|
+
const profiles = config.run?.profiles ?? {};
|
|
4424
|
+
const def = config.run?.defaultProfile;
|
|
4425
|
+
return Object.keys(profiles).map((name) => ({
|
|
4426
|
+
NAME: name,
|
|
4427
|
+
DEFAULT: name === def ? "*" : "",
|
|
4428
|
+
BINARY: profiles[name]?.binary ?? ""
|
|
4429
|
+
}));
|
|
4430
|
+
}
|
|
4431
|
+
var init_run_profiles = __esm({
|
|
4432
|
+
"src/run-profiles.ts"() {
|
|
4433
|
+
"use strict";
|
|
4434
|
+
}
|
|
4435
|
+
});
|
|
2508
4436
|
|
|
2509
4437
|
// src/commands/run.ts
|
|
4438
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4439
|
+
import { join as join4 } from "path";
|
|
4440
|
+
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
4441
|
+
import { parseConfig } from "@noir-ai/core";
|
|
2510
4442
|
function formatUsage(u) {
|
|
2511
4443
|
const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
|
|
2512
4444
|
const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
|
|
@@ -2514,7 +4446,7 @@ function formatUsage(u) {
|
|
|
2514
4446
|
}
|
|
2515
4447
|
function streamEvent(event, opts) {
|
|
2516
4448
|
if (opts.json === true) return;
|
|
2517
|
-
if (event.kind === "assistant" && event.text && event.text.length > 0) {
|
|
4449
|
+
if (event.kind === "assistant" && event.text && event.text.length > 0 && event.isError !== true) {
|
|
2518
4450
|
process.stdout.write(event.text);
|
|
2519
4451
|
}
|
|
2520
4452
|
}
|
|
@@ -2524,100 +4456,108 @@ async function run(prompt, opts) {
|
|
|
2524
4456
|
fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
|
|
2525
4457
|
}
|
|
2526
4458
|
if (prompt.length === 0) {
|
|
2527
|
-
|
|
2528
|
-
|
|
4459
|
+
const selectors = [];
|
|
4460
|
+
if (opts.command) selectors.push(`--command ${opts.command}`);
|
|
4461
|
+
if (opts.profile) selectors.push(`--profile ${opts.profile}`);
|
|
4462
|
+
const usage = selectors.length > 0 ? `noir run ${selectors.join(" ")} <prompt>` : "noir run <prompt>";
|
|
4463
|
+
fail(EXIT.USAGE, `a prompt is required: \`${usage}\``, opts);
|
|
4464
|
+
}
|
|
4465
|
+
const config = loadRunConfig(process.cwd()) ?? parseConfig({});
|
|
4466
|
+
const resolved = resolveRunProfile(opts.profile, config, process.env);
|
|
4467
|
+
if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
|
|
4468
|
+
const profile = resolved.profile;
|
|
4469
|
+
const customBinary = opts.command ?? profile.binary;
|
|
4470
|
+
const extraArgs = profile.args;
|
|
4471
|
+
const env = profile.env ? mergeEnv(process.env, profile.env) : void 0;
|
|
2529
4472
|
const transcriptLines = [];
|
|
4473
|
+
let result;
|
|
2530
4474
|
try {
|
|
2531
|
-
|
|
4475
|
+
result = await runHost({
|
|
2532
4476
|
host,
|
|
2533
4477
|
prompt,
|
|
2534
|
-
customBinary
|
|
4478
|
+
customBinary,
|
|
4479
|
+
extraArgs,
|
|
4480
|
+
env,
|
|
2535
4481
|
onLine: (line) => transcriptLines.push(line),
|
|
2536
4482
|
onEvent: (event) => streamEvent(event, opts)
|
|
2537
4483
|
});
|
|
2538
|
-
if (opts.json !== true) {
|
|
2539
|
-
process.stdout.write("\n");
|
|
2540
|
-
}
|
|
2541
|
-
const transcript = writeTranscript(host, transcriptLines);
|
|
2542
|
-
if (opts.json === true) {
|
|
2543
|
-
json({
|
|
2544
|
-
ok: true,
|
|
2545
|
-
data: {
|
|
2546
|
-
host,
|
|
2547
|
-
prompt,
|
|
2548
|
-
exitCode: result.exitCode,
|
|
2549
|
-
usage: result.usage,
|
|
2550
|
-
numTurns: result.usage.numTurns,
|
|
2551
|
-
events: result.eventCount,
|
|
2552
|
-
transcript
|
|
2553
|
-
}
|
|
2554
|
-
});
|
|
2555
|
-
return;
|
|
2556
|
-
}
|
|
2557
|
-
if (result.exitCode !== 0) {
|
|
2558
|
-
log(`host exited ${result.exitCode}`, opts);
|
|
2559
|
-
if (result.stderr.length > 0) log(result.stderr.trim(), opts);
|
|
2560
|
-
}
|
|
2561
|
-
success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
|
|
2562
|
-
log(`transcript: ${transcript}`, opts);
|
|
2563
4484
|
} catch (err) {
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
4485
|
+
const binary = customBinary && customBinary.length > 0 ? customBinary : host;
|
|
4486
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4487
|
+
const enoent = err?.code === "ENOENT";
|
|
4488
|
+
const guidance = enoent ? ` No executable '${binary}' was found. Shell aliases and functions (e.g. from .zshrc) are invisible to noir \u2014 use an executable on PATH, an absolute path, or a launcher script such as ~/.local/bin/${binary}.` : "";
|
|
4489
|
+
const subject = customBinary && customBinary.length > 0 ? `custom command '${customBinary}'` : `host '${host}'`;
|
|
4490
|
+
fail(EXIT.ERROR, `failed to run ${subject}: ${detail}.${guidance}`, opts);
|
|
4491
|
+
}
|
|
4492
|
+
const transcript = writeTranscript(host, transcriptLines);
|
|
4493
|
+
const failed = result.exitCode !== 0 || result.isError;
|
|
4494
|
+
if (failed) {
|
|
4495
|
+
const binary = customBinary && customBinary.length > 0 ? customBinary : host;
|
|
4496
|
+
const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : result.stderr.trim() || `exit code ${result.exitCode}`;
|
|
4497
|
+
const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(reason);
|
|
4498
|
+
let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
|
|
4499
|
+
if (isAuth) {
|
|
4500
|
+
message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
|
|
4501
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
4502
|
+
message += ` Note: ANTHROPIC_API_KEY is set in your environment \u2014 it overrides the logged-in account in \`-p\` mode; unset it (or fix the key) if you meant to use your subscription.`;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
|
|
4506
|
+
fail(EXIT.ERROR, message, opts);
|
|
4507
|
+
}
|
|
4508
|
+
if (opts.json === true) {
|
|
4509
|
+
json({
|
|
4510
|
+
ok: true,
|
|
4511
|
+
data: {
|
|
4512
|
+
host,
|
|
4513
|
+
prompt,
|
|
4514
|
+
exitCode: result.exitCode,
|
|
4515
|
+
usage: result.usage,
|
|
4516
|
+
numTurns: result.usage.numTurns,
|
|
4517
|
+
events: result.eventCount,
|
|
4518
|
+
transcript
|
|
4519
|
+
}
|
|
4520
|
+
});
|
|
4521
|
+
return;
|
|
2569
4522
|
}
|
|
4523
|
+
process.stdout.write("\n");
|
|
4524
|
+
success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
|
|
4525
|
+
log(`transcript: ${transcript}`, opts);
|
|
4526
|
+
}
|
|
4527
|
+
function mergeEnv(base, overlay) {
|
|
4528
|
+
const merged = { ...base, ...overlay };
|
|
4529
|
+
return Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== void 0));
|
|
2570
4530
|
}
|
|
2571
4531
|
function writeTranscript(host, lines) {
|
|
2572
4532
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2573
|
-
const dir =
|
|
2574
|
-
const file =
|
|
4533
|
+
const dir = join4(process.cwd(), ".noir", "transcripts");
|
|
4534
|
+
const file = join4(dir, `${host}-${ts}.jsonl`);
|
|
2575
4535
|
try {
|
|
2576
|
-
|
|
2577
|
-
|
|
4536
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
4537
|
+
writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
|
|
2578
4538
|
} catch {
|
|
2579
4539
|
return "(not persisted)";
|
|
2580
4540
|
}
|
|
2581
4541
|
return file;
|
|
2582
4542
|
}
|
|
4543
|
+
var init_run = __esm({
|
|
4544
|
+
"src/commands/run.ts"() {
|
|
4545
|
+
"use strict";
|
|
4546
|
+
init_orchestrator();
|
|
4547
|
+
init_output();
|
|
4548
|
+
init_run_profiles();
|
|
4549
|
+
}
|
|
4550
|
+
});
|
|
2583
4551
|
|
|
2584
4552
|
// src/commands/skills.ts
|
|
2585
|
-
import {
|
|
2586
|
-
import { loadProjectInfo as
|
|
4553
|
+
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
4554
|
+
import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
|
|
2587
4555
|
import {
|
|
2588
4556
|
buildRegistry,
|
|
2589
4557
|
discoverAll,
|
|
2590
4558
|
emitSkillsToDir,
|
|
2591
4559
|
lintSkill
|
|
2592
4560
|
} from "@noir-ai/skills";
|
|
2593
|
-
var CATEGORY = {
|
|
2594
|
-
"noir-brainstorming": "discovery",
|
|
2595
|
-
"noir-sync": "discovery",
|
|
2596
|
-
"noir-checkpoint": "discovery",
|
|
2597
|
-
"noir-exploring": "discovery",
|
|
2598
|
-
"noir-spec": "spec",
|
|
2599
|
-
"noir-planning": "plan",
|
|
2600
|
-
"noir-prd": "plan",
|
|
2601
|
-
"noir-executing-plans": "execute",
|
|
2602
|
-
"noir-test-driven-development": "execute",
|
|
2603
|
-
"noir-systematic-debugging": "execute",
|
|
2604
|
-
"noir-subagent": "execute",
|
|
2605
|
-
"noir-parallel": "execute",
|
|
2606
|
-
"noir-verifying": "verify",
|
|
2607
|
-
"noir-security": "verify",
|
|
2608
|
-
"noir-wrap": "document",
|
|
2609
|
-
"noir-readme": "document",
|
|
2610
|
-
"noir-shipping": "git",
|
|
2611
|
-
"noir-worktree": "git",
|
|
2612
|
-
"noir-recall": "memory",
|
|
2613
|
-
"noir-remember": "memory",
|
|
2614
|
-
"noir-context": "context",
|
|
2615
|
-
"noir-frontend": "domain",
|
|
2616
|
-
"noir-backend": "domain",
|
|
2617
|
-
"noir-doctor": "meta",
|
|
2618
|
-
"noir-writing-skills": "meta",
|
|
2619
|
-
"noir-rules": "meta"
|
|
2620
|
-
};
|
|
2621
4561
|
function categoryOf(name) {
|
|
2622
4562
|
const mapped = CATEGORY[name];
|
|
2623
4563
|
if (mapped !== void 0) return mapped;
|
|
@@ -2677,8 +4617,14 @@ async function skillsList(opts) {
|
|
|
2677
4617
|
}
|
|
2678
4618
|
async function skillsSync(opts) {
|
|
2679
4619
|
const root = process.cwd();
|
|
2680
|
-
|
|
2681
|
-
|
|
4620
|
+
let project;
|
|
4621
|
+
try {
|
|
4622
|
+
project = loadProjectInfo11(root);
|
|
4623
|
+
} catch {
|
|
4624
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
4625
|
+
}
|
|
4626
|
+
const adapter = resolveAdapter3(project.config.host);
|
|
4627
|
+
if (!adapter.skillsDir) {
|
|
2682
4628
|
const data2 = {
|
|
2683
4629
|
emitted: [],
|
|
2684
4630
|
references: 0,
|
|
@@ -2693,8 +4639,9 @@ async function skillsSync(opts) {
|
|
|
2693
4639
|
info("This host has no skill emitter; nothing to sync.", opts);
|
|
2694
4640
|
return;
|
|
2695
4641
|
}
|
|
2696
|
-
const dir =
|
|
2697
|
-
const
|
|
4642
|
+
const dir = adapter.skillsDir({ root });
|
|
4643
|
+
const target = project.config.host;
|
|
4644
|
+
const summary = await emitSkillsToDir(dir, { target });
|
|
2698
4645
|
const pruned = summary.pruned ?? [];
|
|
2699
4646
|
const data = {
|
|
2700
4647
|
emitted: summary.emitted,
|
|
@@ -2733,22 +4680,37 @@ async function skillsLint(opts) {
|
|
|
2733
4680
|
const errored = skills.filter((s) => s.errors.length > 0);
|
|
2734
4681
|
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
2735
4682
|
const data = { count: skills.length, errored: errored.length, skills };
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
4683
|
+
if (errored.length > 0) {
|
|
4684
|
+
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
4685
|
+
if (opts.json === true) {
|
|
4686
|
+
process.stdout.write(
|
|
4687
|
+
`${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
|
|
4688
|
+
`
|
|
4689
|
+
);
|
|
4690
|
+
process.exitCode = EXIT.ERROR;
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
2744
4693
|
warn(
|
|
2745
4694
|
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
2746
4695
|
opts
|
|
2747
4696
|
);
|
|
4697
|
+
for (const s of errored) {
|
|
4698
|
+
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
4699
|
+
}
|
|
4700
|
+
if (warned.length > 0) {
|
|
4701
|
+
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
4702
|
+
for (const s of warned) {
|
|
4703
|
+
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
fail(EXIT.ERROR, message, opts);
|
|
2748
4707
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
4708
|
+
if (opts.json === true) {
|
|
4709
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
4710
|
+
`);
|
|
4711
|
+
return;
|
|
2751
4712
|
}
|
|
4713
|
+
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
2752
4714
|
if (warned.length > 0) {
|
|
2753
4715
|
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
2754
4716
|
for (const s of warned) {
|
|
@@ -2788,8 +4750,725 @@ async function skillsRegistry(opts) {
|
|
|
2788
4750
|
opts
|
|
2789
4751
|
);
|
|
2790
4752
|
}
|
|
4753
|
+
var CATEGORY;
|
|
4754
|
+
var init_skills = __esm({
|
|
4755
|
+
"src/commands/skills.ts"() {
|
|
4756
|
+
"use strict";
|
|
4757
|
+
init_output();
|
|
4758
|
+
CATEGORY = {
|
|
4759
|
+
"noir-brainstorming": "discovery",
|
|
4760
|
+
"noir-sync": "discovery",
|
|
4761
|
+
"noir-checkpoint": "discovery",
|
|
4762
|
+
"noir-exploring": "discovery",
|
|
4763
|
+
"noir-spec": "spec",
|
|
4764
|
+
"noir-planning": "plan",
|
|
4765
|
+
"noir-prd": "plan",
|
|
4766
|
+
"noir-executing-plans": "execute",
|
|
4767
|
+
"noir-test-driven-development": "execute",
|
|
4768
|
+
"noir-systematic-debugging": "execute",
|
|
4769
|
+
"noir-subagent": "execute",
|
|
4770
|
+
"noir-parallel": "execute",
|
|
4771
|
+
"noir-verifying": "verify",
|
|
4772
|
+
"noir-security": "verify",
|
|
4773
|
+
"noir-wrap": "document",
|
|
4774
|
+
"noir-readme": "document",
|
|
4775
|
+
"noir-shipping": "git",
|
|
4776
|
+
"noir-worktree": "git",
|
|
4777
|
+
"noir-recall": "memory",
|
|
4778
|
+
"noir-remember": "memory",
|
|
4779
|
+
"noir-context": "context",
|
|
4780
|
+
"noir-frontend": "domain",
|
|
4781
|
+
"noir-backend": "domain",
|
|
4782
|
+
"noir-doctor": "meta",
|
|
4783
|
+
"noir-writing-skills": "meta",
|
|
4784
|
+
"noir-rules": "meta"
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
|
|
4789
|
+
// src/conflict.ts
|
|
4790
|
+
import {
|
|
4791
|
+
lineDiff
|
|
4792
|
+
} from "@noir-ai/create";
|
|
4793
|
+
function flaggedNonInteractive() {
|
|
4794
|
+
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
4795
|
+
return v !== void 0 && v !== "";
|
|
4796
|
+
}
|
|
4797
|
+
function buildConflictOpts(input = {}) {
|
|
4798
|
+
if (input.force === true) {
|
|
4799
|
+
return { conflictPolicy: "overwrite" };
|
|
4800
|
+
}
|
|
4801
|
+
if (input.interactive === false) {
|
|
4802
|
+
return { conflictPolicy: "preserve" };
|
|
4803
|
+
}
|
|
4804
|
+
if (input.interactive === true) {
|
|
4805
|
+
return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
|
|
4806
|
+
}
|
|
4807
|
+
if (flaggedNonInteractive() || !isInteractive()) {
|
|
4808
|
+
return { conflictPolicy: "preserve" };
|
|
4809
|
+
}
|
|
4810
|
+
return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
|
|
4811
|
+
}
|
|
4812
|
+
function renderDiffPreview(ctx) {
|
|
4813
|
+
const lines = lineDiff(ctx.existing, ctx.proposed);
|
|
4814
|
+
if (lines.length === 0) return;
|
|
4815
|
+
const out = [`noir: conflict in ${ctx.relPath}`];
|
|
4816
|
+
for (const dl of lines) {
|
|
4817
|
+
if (dl.type === "add") out.push(c.ok(`+ ${dl.line}`));
|
|
4818
|
+
else if (dl.type === "del") out.push(c.error(`- ${dl.line}`));
|
|
4819
|
+
else out.push(c.dim(` ${dl.line}`));
|
|
4820
|
+
}
|
|
4821
|
+
process.stderr.write(`${out.join("\n")}
|
|
4822
|
+
`);
|
|
4823
|
+
}
|
|
4824
|
+
async function clackConflictResolver(ctx) {
|
|
4825
|
+
renderDiffPreview(ctx);
|
|
4826
|
+
const clack = await import("@clack/prompts");
|
|
4827
|
+
const options = [
|
|
4828
|
+
{
|
|
4829
|
+
value: "replace",
|
|
4830
|
+
label: "Replace",
|
|
4831
|
+
hint: "overwrite with the template (discard your edits)"
|
|
4832
|
+
},
|
|
4833
|
+
{ value: "rename", label: "Rename", hint: "keep yours as <file>.local; write the template" },
|
|
4834
|
+
{
|
|
4835
|
+
value: "duplicate",
|
|
4836
|
+
label: "Create duplicate",
|
|
4837
|
+
hint: "write template to <file>.noir; keep yours"
|
|
4838
|
+
},
|
|
4839
|
+
{ value: "preserve", label: "Keep mine", hint: "skip; leave your file unchanged" },
|
|
4840
|
+
{ value: "cancel", label: "Cancel", hint: "abort the scaffold" }
|
|
4841
|
+
];
|
|
4842
|
+
if (ctx.mergedWithMarkers !== void 0) {
|
|
4843
|
+
options.splice(4, 0, {
|
|
4844
|
+
value: "merge",
|
|
4845
|
+
label: "Merge (with conflict markers)",
|
|
4846
|
+
hint: "write the 3-way merge with <<<<<<< / ||||||| / ======= / >>>>>>> markers"
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
const choice = await clack.select({
|
|
4850
|
+
message: `${ctx.relPath} was edited and differs from the Noir template.`,
|
|
4851
|
+
initialValue: "preserve",
|
|
4852
|
+
options
|
|
4853
|
+
});
|
|
4854
|
+
if (clack.isCancel(choice)) return "cancel";
|
|
4855
|
+
const resolution = choice;
|
|
4856
|
+
const mode = ctx.mode ?? "regenerate";
|
|
4857
|
+
const supportsApplyToAll = mode !== "managedBlock" && mode !== "managedBlocks";
|
|
4858
|
+
if (supportsApplyToAll) {
|
|
4859
|
+
const remember = await clack.select({
|
|
4860
|
+
message: `Apply "${labelFor(resolution)}" to all remaining conflicts this run?`,
|
|
4861
|
+
initialValue: "no",
|
|
4862
|
+
options: [
|
|
4863
|
+
{ value: "yes", label: "Yes", hint: "reuse this choice for the rest of this run" },
|
|
4864
|
+
{ value: "no", label: "No", hint: "prompt again for each conflict" }
|
|
4865
|
+
]
|
|
4866
|
+
});
|
|
4867
|
+
if (!clack.isCancel(remember) && remember === "yes") {
|
|
4868
|
+
return { resolution, applyToAll: true };
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
return resolution;
|
|
4872
|
+
}
|
|
4873
|
+
function labelFor(resolution) {
|
|
4874
|
+
switch (resolution) {
|
|
4875
|
+
case "replace":
|
|
4876
|
+
return "Replace";
|
|
4877
|
+
case "rename":
|
|
4878
|
+
return "Rename";
|
|
4879
|
+
case "duplicate":
|
|
4880
|
+
return "Create duplicate";
|
|
4881
|
+
case "preserve":
|
|
4882
|
+
return "Keep mine";
|
|
4883
|
+
case "merge":
|
|
4884
|
+
return "Merge (with conflict markers)";
|
|
4885
|
+
case "cancel":
|
|
4886
|
+
return "Cancel";
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
var init_conflict = __esm({
|
|
4890
|
+
"src/conflict.ts"() {
|
|
4891
|
+
"use strict";
|
|
4892
|
+
init_output();
|
|
4893
|
+
init_theme();
|
|
4894
|
+
}
|
|
4895
|
+
});
|
|
4896
|
+
|
|
4897
|
+
// src/dedup-write.ts
|
|
4898
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4899
|
+
import { join as join5 } from "path";
|
|
4900
|
+
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
4901
|
+
async function checkWritePathDedup(root, res, opts) {
|
|
4902
|
+
const result = { found: [], conflicts: [] };
|
|
4903
|
+
const writtenHostFiles = res.written.filter((p) => HOST_CONTEXT_RELS.has(p));
|
|
4904
|
+
if (writtenHostFiles.length === 0) return result;
|
|
4905
|
+
const cache = loadCache(root);
|
|
4906
|
+
const embed = await resolveEmbedder(opts, cache);
|
|
4907
|
+
if (embed === void 0) return result;
|
|
4908
|
+
const cachedEmbed = makeCachedEmbed(cache, embed);
|
|
4909
|
+
for (const proposedRel of writtenHostFiles) {
|
|
4910
|
+
const proposedAbs = join5(root, proposedRel);
|
|
4911
|
+
let proposedText;
|
|
4912
|
+
try {
|
|
4913
|
+
proposedText = readFileSync2(proposedAbs, "utf8");
|
|
4914
|
+
} catch {
|
|
4915
|
+
continue;
|
|
4916
|
+
}
|
|
4917
|
+
if (proposedText.trim().length === 0) continue;
|
|
4918
|
+
const candidates = collectDedupCandidates(root).filter((c2) => c2.path !== proposedRel);
|
|
4919
|
+
if (candidates.length === 0) continue;
|
|
4920
|
+
const ctx = await import("@noir-ai/context");
|
|
4921
|
+
const proposed = { path: proposedRel, text: proposedText };
|
|
4922
|
+
let best;
|
|
4923
|
+
try {
|
|
4924
|
+
best = await ctx.findNearestDuplicate(
|
|
4925
|
+
proposed,
|
|
4926
|
+
candidates,
|
|
4927
|
+
async (t) => cachedEmbed(t),
|
|
4928
|
+
INFO_THRESHOLD
|
|
4929
|
+
);
|
|
4930
|
+
} catch (e) {
|
|
4931
|
+
process.stderr.write(
|
|
4932
|
+
`${c.dim(`noir: semantic-dedup skipped for ${proposedRel} (${errMsg(e)}).`)}
|
|
4933
|
+
`
|
|
4934
|
+
);
|
|
4935
|
+
continue;
|
|
4936
|
+
}
|
|
4937
|
+
if (best === null) continue;
|
|
4938
|
+
const matchedRel = best.a === proposedRel ? best.b : best.a;
|
|
4939
|
+
const sim = best.similarity;
|
|
4940
|
+
result.found.push({ proposedRel, matchedRel, similarity: sim });
|
|
4941
|
+
let resolution = "preserve";
|
|
4942
|
+
if (sim >= ACTION_THRESHOLD) {
|
|
4943
|
+
if (opts.interactive) {
|
|
4944
|
+
resolution = await promptDedupAction(root, proposedRel, matchedRel, proposedText, sim);
|
|
4945
|
+
} else {
|
|
4946
|
+
process.stderr.write(
|
|
4947
|
+
`${c.warn("noir: near-duplicate")} ${proposedRel} \u2248 ${matchedRel} (cosine ${sim.toFixed(2)}); creating anyway.
|
|
4948
|
+
`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
} else {
|
|
4952
|
+
process.stderr.write(
|
|
4953
|
+
`${c.dim(`noir: hint ${proposedRel} looks similar to existing ${matchedRel} (cosine ${sim.toFixed(2)}).`)}
|
|
4954
|
+
`
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
const matchedText = safeRead(join5(root, matchedRel));
|
|
4958
|
+
result.conflicts.push({
|
|
4959
|
+
path: proposedRel,
|
|
4960
|
+
mode: "artifact",
|
|
4961
|
+
similarity: sim,
|
|
4962
|
+
existingSha: sha256Hex12(matchedText ?? ""),
|
|
4963
|
+
proposedSha: sha256Hex12(proposedText),
|
|
4964
|
+
resolution
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
saveCache(root, cache);
|
|
4968
|
+
return result;
|
|
4969
|
+
}
|
|
4970
|
+
async function resolveEmbedder(opts, cache) {
|
|
4971
|
+
const ctx = await import("@noir-ai/context");
|
|
4972
|
+
let modelId;
|
|
4973
|
+
if (opts.project !== void 0) {
|
|
4974
|
+
const cfg = ctx.resolveEmbedderConfig(opts.project.config.context);
|
|
4975
|
+
if (cfg.kind === "none") {
|
|
4976
|
+
process.stderr.write(`${c.dim("noir: semantic-dedup skipped (embedder kind=none).")}
|
|
4977
|
+
`);
|
|
4978
|
+
return void 0;
|
|
4979
|
+
}
|
|
4980
|
+
modelId = cfg.kind === "local" ? cfg.model ?? ctx.DEFAULT_LOCAL_MODEL : cfg.model ?? cfg.kind;
|
|
4981
|
+
if (modelId !== void 0 && cache.model !== modelId) {
|
|
4982
|
+
cache.model = modelId;
|
|
4983
|
+
cache.entries = {};
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
if (opts.embed !== void 0) return opts.embed;
|
|
4987
|
+
if (modelId === void 0) return void 0;
|
|
4988
|
+
let embed;
|
|
4989
|
+
try {
|
|
4990
|
+
const project = opts.project;
|
|
4991
|
+
if (project === void 0) return void 0;
|
|
4992
|
+
const cfg = ctx.resolveEmbedderConfig(project.config.context);
|
|
4993
|
+
embed = ctx.createEmbedFn(cfg).embed;
|
|
4994
|
+
} catch (e) {
|
|
4995
|
+
process.stderr.write(
|
|
4996
|
+
`${c.dim(`noir: semantic-dedup skipped (embedder unavailable: ${errMsg(e)}).`)}
|
|
4997
|
+
`
|
|
4998
|
+
);
|
|
4999
|
+
return void 0;
|
|
5000
|
+
}
|
|
5001
|
+
const ping = await withTimeout(
|
|
5002
|
+
embed("ping").then(() => true),
|
|
5003
|
+
EMBEDDER_PING_TIMEOUT_MS
|
|
5004
|
+
);
|
|
5005
|
+
if (ping !== true) {
|
|
5006
|
+
process.stderr.write(
|
|
5007
|
+
`${c.dim("noir: semantic-dedup skipped (embedder load too slow or unavailable).")}
|
|
5008
|
+
`
|
|
5009
|
+
);
|
|
5010
|
+
return void 0;
|
|
5011
|
+
}
|
|
5012
|
+
return embed;
|
|
5013
|
+
}
|
|
5014
|
+
async function promptDedupAction(root, proposedRel, matchedRel, proposedText, sim) {
|
|
5015
|
+
const clack = await import("@clack/prompts");
|
|
5016
|
+
const choice = await clack.select({
|
|
5017
|
+
message: `${proposedRel} is near-duplicate of existing ${matchedRel} (cosine ${sim.toFixed(2)}). Create anyway?`,
|
|
5018
|
+
initialValue: "create",
|
|
5019
|
+
options: [
|
|
5020
|
+
{
|
|
5021
|
+
value: "create",
|
|
5022
|
+
label: "Create anyway",
|
|
5023
|
+
hint: "keep the new file as-is (default)"
|
|
5024
|
+
},
|
|
5025
|
+
{ value: "skip", label: "Skip", hint: `delete ${proposedRel}; keep ${matchedRel}` },
|
|
5026
|
+
{
|
|
5027
|
+
value: "replace",
|
|
5028
|
+
label: "Replace existing",
|
|
5029
|
+
hint: `overwrite ${matchedRel} with this content`
|
|
5030
|
+
},
|
|
5031
|
+
{
|
|
5032
|
+
value: "mirror",
|
|
5033
|
+
label: "Mirror into existing",
|
|
5034
|
+
hint: `append this content to ${matchedRel}`
|
|
5035
|
+
}
|
|
5036
|
+
]
|
|
5037
|
+
});
|
|
5038
|
+
if (clack.isCancel(choice)) return "preserve";
|
|
5039
|
+
if (choice === "skip") {
|
|
5040
|
+
try {
|
|
5041
|
+
rmSync(join5(root, proposedRel), { force: true });
|
|
5042
|
+
} catch {
|
|
5043
|
+
}
|
|
5044
|
+
return "preserve";
|
|
5045
|
+
} else if (choice === "replace") {
|
|
5046
|
+
try {
|
|
5047
|
+
writeFileSync4(join5(root, matchedRel), proposedText, "utf8");
|
|
5048
|
+
} catch {
|
|
5049
|
+
}
|
|
5050
|
+
return "replace";
|
|
5051
|
+
} else if (choice === "mirror") {
|
|
5052
|
+
try {
|
|
5053
|
+
const existing = readFileSync2(join5(root, matchedRel), "utf8");
|
|
5054
|
+
writeFileSync4(
|
|
5055
|
+
join5(root, matchedRel),
|
|
5056
|
+
`${existing}
|
|
5057
|
+
|
|
5058
|
+
<!-- mirrored from ${proposedRel} -->
|
|
5059
|
+
${proposedText}
|
|
5060
|
+
`,
|
|
5061
|
+
"utf8"
|
|
5062
|
+
);
|
|
5063
|
+
} catch {
|
|
5064
|
+
}
|
|
5065
|
+
return "duplicate";
|
|
5066
|
+
}
|
|
5067
|
+
return "preserve";
|
|
5068
|
+
}
|
|
5069
|
+
function emptyCache() {
|
|
5070
|
+
return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
|
|
5071
|
+
}
|
|
5072
|
+
function cachePath(root) {
|
|
5073
|
+
return join5(root, ".noir", "dedup-cache.json");
|
|
5074
|
+
}
|
|
5075
|
+
function loadCache(root) {
|
|
5076
|
+
try {
|
|
5077
|
+
const raw = readFileSync2(cachePath(root), "utf8");
|
|
5078
|
+
const parsed = JSON.parse(raw);
|
|
5079
|
+
if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
|
|
5080
|
+
if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
|
|
5081
|
+
return {
|
|
5082
|
+
version: DEDUP_CACHE_VERSION,
|
|
5083
|
+
model: typeof parsed.model === "string" ? parsed.model : "",
|
|
5084
|
+
entries: parsed.entries
|
|
5085
|
+
};
|
|
5086
|
+
} catch {
|
|
5087
|
+
return emptyCache();
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
function saveCache(root, cache) {
|
|
5091
|
+
try {
|
|
5092
|
+
mkdirSync4(join5(root, ".noir"), { recursive: true });
|
|
5093
|
+
writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
|
|
5094
|
+
} catch {
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
function makeCachedEmbed(cache, embed) {
|
|
5098
|
+
return async (text) => {
|
|
5099
|
+
const key = sha256Hex2(text);
|
|
5100
|
+
const cached = cache.entries[key];
|
|
5101
|
+
if (cached !== void 0) return Float32Array.from(cached.v);
|
|
5102
|
+
const v = await embed(text);
|
|
5103
|
+
cache.entries[key] = { v: Array.from(v) };
|
|
5104
|
+
return v;
|
|
5105
|
+
};
|
|
5106
|
+
}
|
|
5107
|
+
function safeRead(abs) {
|
|
5108
|
+
try {
|
|
5109
|
+
return readFileSync2(abs, "utf8");
|
|
5110
|
+
} catch {
|
|
5111
|
+
return void 0;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
function withTimeout(p, ms) {
|
|
5115
|
+
return new Promise((resolve2) => {
|
|
5116
|
+
const t = setTimeout(() => resolve2(void 0), ms);
|
|
5117
|
+
p.then(
|
|
5118
|
+
(v) => {
|
|
5119
|
+
clearTimeout(t);
|
|
5120
|
+
resolve2(v);
|
|
5121
|
+
},
|
|
5122
|
+
() => {
|
|
5123
|
+
clearTimeout(t);
|
|
5124
|
+
resolve2(void 0);
|
|
5125
|
+
}
|
|
5126
|
+
);
|
|
5127
|
+
});
|
|
5128
|
+
}
|
|
5129
|
+
function errMsg(e) {
|
|
5130
|
+
return e instanceof Error ? e.message : String(e);
|
|
5131
|
+
}
|
|
5132
|
+
var HOST_CONTEXT_RELS, ACTION_THRESHOLD, INFO_THRESHOLD, EMBEDDER_PING_TIMEOUT_MS, DEDUP_CACHE_VERSION;
|
|
5133
|
+
var init_dedup_write = __esm({
|
|
5134
|
+
"src/dedup-write.ts"() {
|
|
5135
|
+
"use strict";
|
|
5136
|
+
init_doctor();
|
|
5137
|
+
init_theme();
|
|
5138
|
+
HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
|
|
5139
|
+
ACTION_THRESHOLD = 0.95;
|
|
5140
|
+
INFO_THRESHOLD = 0.85;
|
|
5141
|
+
EMBEDDER_PING_TIMEOUT_MS = 5e3;
|
|
5142
|
+
DEDUP_CACHE_VERSION = 1;
|
|
5143
|
+
}
|
|
5144
|
+
});
|
|
5145
|
+
|
|
5146
|
+
// src/init.ts
|
|
5147
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
5148
|
+
import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
|
|
5149
|
+
import { scaffold } from "@noir-ai/create";
|
|
5150
|
+
import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
|
|
5151
|
+
async function init(root, opts) {
|
|
5152
|
+
assertTransportUrl(opts);
|
|
5153
|
+
const host = opts.host ?? "claude";
|
|
5154
|
+
const interactive = resolveInteractive();
|
|
5155
|
+
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5156
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
5157
|
+
const res = await scaffold({
|
|
5158
|
+
root,
|
|
5159
|
+
mode: "init",
|
|
5160
|
+
host,
|
|
5161
|
+
transport: opts.transport,
|
|
5162
|
+
interactive,
|
|
5163
|
+
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
5164
|
+
...opts.upgrade === true ? { upgrade: true } : {},
|
|
5165
|
+
...opts.force === true ? { force: true } : {},
|
|
5166
|
+
...dryRun ? { dryRun: true } : {},
|
|
5167
|
+
...conflictOpts
|
|
5168
|
+
});
|
|
5169
|
+
if (dryRun) {
|
|
5170
|
+
reportPlannedWrites(res);
|
|
5171
|
+
return res;
|
|
5172
|
+
}
|
|
5173
|
+
if (res.noop) return res;
|
|
5174
|
+
await emitHostSkills(root, host, conflictOpts, interactive);
|
|
5175
|
+
let projectInfo;
|
|
5176
|
+
try {
|
|
5177
|
+
projectInfo = loadProjectInfo12(root);
|
|
5178
|
+
} catch {
|
|
5179
|
+
projectInfo = void 0;
|
|
5180
|
+
}
|
|
5181
|
+
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
5182
|
+
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
5183
|
+
process.stderr.write(
|
|
5184
|
+
`Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
|
|
5185
|
+
`
|
|
5186
|
+
);
|
|
5187
|
+
process.stderr.write(
|
|
5188
|
+
"Next: run `noir` to open the home menu (or `noir status` for a snapshot).\n"
|
|
5189
|
+
);
|
|
5190
|
+
return res;
|
|
5191
|
+
}
|
|
5192
|
+
function reportPlannedWrites(res) {
|
|
5193
|
+
log("Dry run \u2014 no files were written.");
|
|
5194
|
+
if (res.written.length > 0) {
|
|
5195
|
+
log("Planned writes:");
|
|
5196
|
+
for (const p of res.written) log(` ${p}`);
|
|
5197
|
+
}
|
|
5198
|
+
if (res.skipped.length > 0) {
|
|
5199
|
+
log("Would leave as-is (already present):");
|
|
5200
|
+
for (const p of res.skipped) log(` ${p}`);
|
|
5201
|
+
}
|
|
5202
|
+
if (res.identical.length > 0) {
|
|
5203
|
+
log("Would rewrite (byte-identical, no-op):");
|
|
5204
|
+
for (const p of res.identical) log(` ${p}`);
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
5208
|
+
const adapter = resolveAdapter4(host);
|
|
5209
|
+
const skillsDir = adapter.skillsDir?.({ root });
|
|
5210
|
+
if (skillsDir === void 0) {
|
|
5211
|
+
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
5212
|
+
`);
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
const target = host;
|
|
5216
|
+
const skillOpts = {
|
|
5217
|
+
includeIntegrations: true,
|
|
5218
|
+
target,
|
|
5219
|
+
conflictPolicy: conflictOpts.conflictPolicy,
|
|
5220
|
+
interactive
|
|
5221
|
+
};
|
|
5222
|
+
if (conflictOpts.onConflict !== void 0) {
|
|
5223
|
+
skillOpts.onConflict = conflictOpts.onConflict;
|
|
5224
|
+
}
|
|
5225
|
+
const summary = await emitSkillsToDir2(skillsDir, skillOpts);
|
|
5226
|
+
const relDir = skillsDir.replace(`${root}/`, "");
|
|
5227
|
+
process.stderr.write(
|
|
5228
|
+
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
5229
|
+
`
|
|
5230
|
+
);
|
|
5231
|
+
}
|
|
5232
|
+
function assertTransportUrl(opts) {
|
|
5233
|
+
if (opts.transport === "streamable-http" && opts.url === void 0) {
|
|
5234
|
+
throw new Error("--transport streamable-http requires --url");
|
|
5235
|
+
}
|
|
5236
|
+
if (opts.url !== void 0) {
|
|
5237
|
+
assertLocalhostUrl(opts.url);
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
function assertLocalhostUrl(raw) {
|
|
5241
|
+
let url;
|
|
5242
|
+
try {
|
|
5243
|
+
url = new URL(raw);
|
|
5244
|
+
} catch {
|
|
5245
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
5246
|
+
}
|
|
5247
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
5248
|
+
throw new Error("Only http/https URLs are supported");
|
|
5249
|
+
}
|
|
5250
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(url.hostname)) {
|
|
5251
|
+
throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
var init_init = __esm({
|
|
5255
|
+
"src/init.ts"() {
|
|
5256
|
+
"use strict";
|
|
5257
|
+
init_conflict();
|
|
5258
|
+
init_dedup_write();
|
|
5259
|
+
init_output();
|
|
5260
|
+
}
|
|
5261
|
+
});
|
|
5262
|
+
|
|
5263
|
+
// src/serve.ts
|
|
5264
|
+
import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
5265
|
+
import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
|
|
5266
|
+
async function serve(opts) {
|
|
5267
|
+
applyNoirEnv(process.cwd());
|
|
5268
|
+
const project = loadProjectInfo13(process.cwd());
|
|
5269
|
+
if (opts.stdio) {
|
|
5270
|
+
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
try {
|
|
5274
|
+
const { url } = await ensureDaemonRunning3({
|
|
5275
|
+
project,
|
|
5276
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
5277
|
+
});
|
|
5278
|
+
process.stderr.write(
|
|
5279
|
+
`Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
|
|
5280
|
+
`
|
|
5281
|
+
);
|
|
5282
|
+
} catch (err) {
|
|
5283
|
+
process.stderr.write(
|
|
5284
|
+
`Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
|
|
5285
|
+
`
|
|
5286
|
+
);
|
|
5287
|
+
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
var init_serve = __esm({
|
|
5291
|
+
"src/serve.ts"() {
|
|
5292
|
+
"use strict";
|
|
5293
|
+
}
|
|
5294
|
+
});
|
|
5295
|
+
|
|
5296
|
+
// src/commands/create.ts
|
|
5297
|
+
var create_exports = {};
|
|
5298
|
+
__export(create_exports, {
|
|
5299
|
+
create: () => create
|
|
5300
|
+
});
|
|
5301
|
+
import { resolve } from "path";
|
|
5302
|
+
import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
|
|
5303
|
+
import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
|
|
5304
|
+
import { scaffold as scaffold2 } from "@noir-ai/create";
|
|
5305
|
+
import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
|
|
5306
|
+
async function create(dir, opts) {
|
|
5307
|
+
assertTransportUrl(opts);
|
|
5308
|
+
const root = resolve(dir ?? process.cwd());
|
|
5309
|
+
const host = opts.host ?? "claude";
|
|
5310
|
+
const interactive = resolveInteractive();
|
|
5311
|
+
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5312
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
5313
|
+
const res = await scaffold2({
|
|
5314
|
+
root,
|
|
5315
|
+
mode: "create",
|
|
5316
|
+
host,
|
|
5317
|
+
transport: opts.transport,
|
|
5318
|
+
interactive,
|
|
5319
|
+
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
5320
|
+
...opts.force === true ? { force: true } : {},
|
|
5321
|
+
...dryRun ? { dryRun: true } : {},
|
|
5322
|
+
...conflictOpts
|
|
5323
|
+
});
|
|
5324
|
+
if (dryRun) {
|
|
5325
|
+
reportPlannedWrites(res);
|
|
5326
|
+
return res;
|
|
5327
|
+
}
|
|
5328
|
+
if (res.noop) return res;
|
|
5329
|
+
const adapter = resolveAdapter5(host);
|
|
5330
|
+
const skillsDir = adapter.skillsDir?.({ root });
|
|
5331
|
+
if (skillsDir === void 0) {
|
|
5332
|
+
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
5333
|
+
`);
|
|
5334
|
+
} else {
|
|
5335
|
+
const target = host;
|
|
5336
|
+
const skillOpts = {
|
|
5337
|
+
includeIntegrations: true,
|
|
5338
|
+
target,
|
|
5339
|
+
conflictPolicy: conflictOpts.conflictPolicy,
|
|
5340
|
+
interactive
|
|
5341
|
+
};
|
|
5342
|
+
if (conflictOpts.onConflict !== void 0) {
|
|
5343
|
+
skillOpts.onConflict = conflictOpts.onConflict;
|
|
5344
|
+
}
|
|
5345
|
+
const summary = await emitSkillsToDir3(skillsDir, skillOpts);
|
|
5346
|
+
const relDir = skillsDir.replace(`${root}/`, "");
|
|
5347
|
+
process.stderr.write(
|
|
5348
|
+
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
5349
|
+
`
|
|
5350
|
+
);
|
|
5351
|
+
}
|
|
5352
|
+
let projectInfo;
|
|
5353
|
+
try {
|
|
5354
|
+
projectInfo = loadProjectInfo14(root);
|
|
5355
|
+
} catch {
|
|
5356
|
+
projectInfo = void 0;
|
|
5357
|
+
}
|
|
5358
|
+
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
5359
|
+
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
5360
|
+
process.stderr.write(`Noir created in ${root} (host: ${host}, transport: ${opts.transport}).
|
|
5361
|
+
`);
|
|
5362
|
+
process.stderr.write("Next: cd into the new directory, then run `noir` to open the home menu.\n");
|
|
5363
|
+
return res;
|
|
5364
|
+
}
|
|
5365
|
+
var init_create = __esm({
|
|
5366
|
+
"src/commands/create.ts"() {
|
|
5367
|
+
"use strict";
|
|
5368
|
+
init_conflict();
|
|
5369
|
+
init_dedup_write();
|
|
5370
|
+
init_init();
|
|
5371
|
+
init_output();
|
|
5372
|
+
}
|
|
5373
|
+
});
|
|
5374
|
+
|
|
5375
|
+
// src/sync.ts
|
|
5376
|
+
var sync_exports = {};
|
|
5377
|
+
__export(sync_exports, {
|
|
5378
|
+
sync: () => sync
|
|
5379
|
+
});
|
|
5380
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5381
|
+
import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
5382
|
+
import { loadProjectInfo as loadProjectInfo15, paths as paths2 } from "@noir-ai/core";
|
|
5383
|
+
import { scaffold as scaffold3 } from "@noir-ai/create";
|
|
5384
|
+
import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
|
|
5385
|
+
async function sync(root, opts = {}) {
|
|
5386
|
+
const host = resolveSyncHost(root, opts);
|
|
5387
|
+
const interactive = resolveInteractive();
|
|
5388
|
+
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5389
|
+
let projectInfo;
|
|
5390
|
+
try {
|
|
5391
|
+
projectInfo = loadProjectInfo15(root);
|
|
5392
|
+
} catch {
|
|
5393
|
+
projectInfo = void 0;
|
|
5394
|
+
}
|
|
5395
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
5396
|
+
const res = await scaffold3({
|
|
5397
|
+
root,
|
|
5398
|
+
mode: "sync",
|
|
5399
|
+
host,
|
|
5400
|
+
interactive,
|
|
5401
|
+
...dryRun ? { dryRun: true } : {},
|
|
5402
|
+
...conflictOpts,
|
|
5403
|
+
// Merge defaults TRUE inside scaffold; only forward an explicit opt-out.
|
|
5404
|
+
...opts.mergeManagedRegions === false ? { mergeManagedRegions: false } : {}
|
|
5405
|
+
});
|
|
5406
|
+
if (dryRun) {
|
|
5407
|
+
reportPlannedWrites(res);
|
|
5408
|
+
return res;
|
|
5409
|
+
}
|
|
5410
|
+
if (res.written.length === 0 && res.identical.length > 0) {
|
|
5411
|
+
process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
|
|
5412
|
+
}
|
|
5413
|
+
const adapter = resolveAdapter6(host);
|
|
5414
|
+
const skillsDir = adapter.skillsDir?.({ root });
|
|
5415
|
+
if (skillsDir === void 0) {
|
|
5416
|
+
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
5417
|
+
`);
|
|
5418
|
+
} else {
|
|
5419
|
+
const target = host;
|
|
5420
|
+
const skillOpts = {
|
|
5421
|
+
includeIntegrations: true,
|
|
5422
|
+
target,
|
|
5423
|
+
conflictPolicy: conflictOpts.conflictPolicy,
|
|
5424
|
+
interactive
|
|
5425
|
+
};
|
|
5426
|
+
if (conflictOpts.onConflict !== void 0) {
|
|
5427
|
+
skillOpts.onConflict = conflictOpts.onConflict;
|
|
5428
|
+
}
|
|
5429
|
+
const summary = await emitSkillsToDir4(skillsDir, skillOpts);
|
|
5430
|
+
const relDir = skillsDir.replace(`${root}/`, "");
|
|
5431
|
+
process.stderr.write(
|
|
5432
|
+
`Synced ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
5433
|
+
`
|
|
5434
|
+
);
|
|
5435
|
+
const pruned = summary.pruned ?? [];
|
|
5436
|
+
if (pruned.length > 0) {
|
|
5437
|
+
process.stderr.write(
|
|
5438
|
+
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}
|
|
5439
|
+
`
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
5444
|
+
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
5445
|
+
return res;
|
|
5446
|
+
}
|
|
5447
|
+
function resolveSyncHost(root, opts) {
|
|
5448
|
+
if (opts.host !== void 0) return opts.host;
|
|
5449
|
+
if (!existsSync2(paths2.config(root))) return "claude";
|
|
5450
|
+
try {
|
|
5451
|
+
return loadProjectInfo15(root).config.host;
|
|
5452
|
+
} catch {
|
|
5453
|
+
return "claude";
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
var init_sync = __esm({
|
|
5457
|
+
"src/sync.ts"() {
|
|
5458
|
+
"use strict";
|
|
5459
|
+
init_conflict();
|
|
5460
|
+
init_dedup_write();
|
|
5461
|
+
init_init();
|
|
5462
|
+
init_output();
|
|
5463
|
+
}
|
|
5464
|
+
});
|
|
2791
5465
|
|
|
2792
5466
|
// src/bin.ts
|
|
5467
|
+
import { realpathSync } from "fs";
|
|
5468
|
+
import { pathToFileURL } from "url";
|
|
5469
|
+
import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
|
|
5470
|
+
import { applyNoirEnv as applyNoirEnv2, NOIR_VERSION as NOIR_VERSION6 } from "@noir-ai/core";
|
|
5471
|
+
import { Command, Option } from "commander";
|
|
2793
5472
|
function buildPaletteCommandsForTui() {
|
|
2794
5473
|
return buildPaletteCommands(createProgram());
|
|
2795
5474
|
}
|
|
@@ -2828,14 +5507,14 @@ function toStatusOptions(g) {
|
|
|
2828
5507
|
json: g.json === true,
|
|
2829
5508
|
quiet: g.quiet === true,
|
|
2830
5509
|
verbose: g.verbose === true,
|
|
2831
|
-
input: g.input !== false
|
|
5510
|
+
input: g.input !== false,
|
|
5511
|
+
...g.tips === false ? { noTips: true } : {}
|
|
2832
5512
|
};
|
|
2833
5513
|
}
|
|
2834
5514
|
function parseHost(raw) {
|
|
2835
5515
|
if (raw === void 0) return void 0;
|
|
2836
5516
|
return raw;
|
|
2837
5517
|
}
|
|
2838
|
-
var DEPRECATIONS = [];
|
|
2839
5518
|
function commandPath(cmd) {
|
|
2840
5519
|
const path = [];
|
|
2841
5520
|
let c2 = cmd;
|
|
@@ -2859,7 +5538,7 @@ function emitDeprecationHintsFor(cmd, opts) {
|
|
|
2859
5538
|
}
|
|
2860
5539
|
function createProgram() {
|
|
2861
5540
|
const program2 = new Command();
|
|
2862
|
-
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(
|
|
5541
|
+
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION6, "-v, --version").addOption(new Option("--json", "emit machine-readable JSON to stdout")).addOption(new Option("--no-input", "never prompt; error if input is required")).addOption(new Option("--quiet", "suppress non-essential diagnostics")).addOption(new Option("--verbose", "show additional diagnostic detail")).addOption(new Option("--cwd <path>", "run as if started in <path>")).addOption(
|
|
2863
5542
|
new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
|
|
2864
5543
|
).addOption(
|
|
2865
5544
|
new Option(
|
|
@@ -2889,6 +5568,7 @@ function createProgram() {
|
|
|
2889
5568
|
);
|
|
2890
5569
|
}
|
|
2891
5570
|
}
|
|
5571
|
+
applyNoirEnv2(process.cwd());
|
|
2892
5572
|
const nonInteractive = opts.json === true || opts.input === false;
|
|
2893
5573
|
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
2894
5574
|
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
@@ -2937,8 +5617,8 @@ function createProgram() {
|
|
|
2937
5617
|
const force = opts.force === true;
|
|
2938
5618
|
const host = parseHost(opts.host);
|
|
2939
5619
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
2940
|
-
const { create } = await
|
|
2941
|
-
const result = await
|
|
5620
|
+
const { create: create2 } = await Promise.resolve().then(() => (init_create(), create_exports));
|
|
5621
|
+
const result = await create2(dir, {
|
|
2942
5622
|
transport,
|
|
2943
5623
|
url: opts.url,
|
|
2944
5624
|
...force ? { force } : {},
|
|
@@ -2976,13 +5656,13 @@ function createProgram() {
|
|
|
2976
5656
|
).choices(SUPPORTED_HOSTS2)
|
|
2977
5657
|
).action(
|
|
2978
5658
|
async (opts, cmd) => {
|
|
2979
|
-
const { sync } = await
|
|
5659
|
+
const { sync: sync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
2980
5660
|
const host = parseHost(opts.host);
|
|
2981
5661
|
const force = opts.force === true;
|
|
2982
5662
|
const merge = opts.merge === true;
|
|
2983
5663
|
const noMergeRegions = opts.mergeRegions === false;
|
|
2984
5664
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
2985
|
-
const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await
|
|
5665
|
+
const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync2(process.cwd()) : await sync2(process.cwd(), {
|
|
2986
5666
|
...host !== void 0 ? { host } : {},
|
|
2987
5667
|
...force ? { force } : {},
|
|
2988
5668
|
...merge ? { merge } : {},
|
|
@@ -3170,7 +5850,7 @@ function createProgram() {
|
|
|
3170
5850
|
...id === void 0 ? {} : { id }
|
|
3171
5851
|
});
|
|
3172
5852
|
});
|
|
3173
|
-
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
|
|
5853
|
+
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--skip", "record the landing gate as skipped (advance continues)").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
|
|
3174
5854
|
const cmd = trailingCmd(args);
|
|
3175
5855
|
const g = cmd.optsWithGlobals();
|
|
3176
5856
|
const to = typeof g.to === "string" ? g.to : void 0;
|
|
@@ -3179,6 +5859,7 @@ function createProgram() {
|
|
|
3179
5859
|
...toCliOptions(g),
|
|
3180
5860
|
...to === void 0 ? {} : { to },
|
|
3181
5861
|
...force === void 0 ? {} : { force },
|
|
5862
|
+
...g.skip === true ? { skip: true } : {},
|
|
3182
5863
|
...g.artifacts === false ? { noArtifacts: true } : {}
|
|
3183
5864
|
});
|
|
3184
5865
|
});
|
|
@@ -3230,17 +5911,15 @@ function createProgram() {
|
|
|
3230
5911
|
...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
|
|
3231
5912
|
});
|
|
3232
5913
|
});
|
|
3233
|
-
taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--
|
|
5914
|
+
taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
|
|
3234
5915
|
const cmd = trailingCmd(args);
|
|
3235
5916
|
const g = cmd.optsWithGlobals();
|
|
3236
5917
|
const first = args[0];
|
|
3237
5918
|
const id = typeof first === "string" ? first : void 0;
|
|
3238
|
-
const last = g.last === true;
|
|
3239
5919
|
const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
|
|
3240
5920
|
await taskResume({
|
|
3241
5921
|
...toCliOptions(g),
|
|
3242
5922
|
...id === void 0 ? {} : { id },
|
|
3243
|
-
...last ? { last } : {},
|
|
3244
5923
|
...prompt === void 0 ? {} : { prompt }
|
|
3245
5924
|
});
|
|
3246
5925
|
});
|
|
@@ -3277,9 +5956,9 @@ function createProgram() {
|
|
|
3277
5956
|
"dismiss the migration banner for the current version (persists in install.json)"
|
|
3278
5957
|
).argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(
|
|
3279
5958
|
async (spec, opts, cmd) => {
|
|
3280
|
-
const { install } = await
|
|
5959
|
+
const { install: install2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
3281
5960
|
const cli = cmd.optsWithGlobals();
|
|
3282
|
-
await
|
|
5961
|
+
await install2({
|
|
3283
5962
|
...cli,
|
|
3284
5963
|
spec,
|
|
3285
5964
|
list: opts.list === true,
|
|
@@ -3290,9 +5969,9 @@ function createProgram() {
|
|
|
3290
5969
|
);
|
|
3291
5970
|
installCmd.alias("migrate");
|
|
3292
5971
|
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) => {
|
|
3293
|
-
const { update } = await
|
|
5972
|
+
const { update: update2 } = await Promise.resolve().then(() => (init_update(), update_exports));
|
|
3294
5973
|
const cli = cmd.optsWithGlobals();
|
|
3295
|
-
await
|
|
5974
|
+
await update2({ ...cli, spec, check: opts.check === true });
|
|
3296
5975
|
});
|
|
3297
5976
|
function buildHandoffOptions(g) {
|
|
3298
5977
|
return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
|
|
@@ -3353,17 +6032,35 @@ function createProgram() {
|
|
|
3353
6032
|
};
|
|
3354
6033
|
await runPalette(opts, dispatch);
|
|
3355
6034
|
});
|
|
3356
|
-
program2.command("run").description("ask the host agent a question and print the answer").argument("[prompt...]", "prompt to send to the host").addOption(new Option("--host <id>", "host to drive (default claude)").choices(SUPPORTED_HOSTS2)).option("--command <binary>", "custom host binary (e.g. claude-work)").action(async (...args) => {
|
|
6035
|
+
program2.command("run").description("ask the host agent a question and print the answer").argument("[prompt...]", "prompt to send to the host").addOption(new Option("--host <id>", "host to drive (default claude)").choices(SUPPORTED_HOSTS2)).option("--command <binary>", "custom host binary (e.g. claude-work)").option("--profile <name>", "use a named run profile (run.profiles in .noir/config.yml)").option("--list-profiles", "list configured run profiles and exit").action(async (...args) => {
|
|
3357
6036
|
const cmd = trailingCmd(args);
|
|
3358
6037
|
const g = cmd.optsWithGlobals();
|
|
3359
6038
|
const prompt = cmd.args.join(" ").trim();
|
|
3360
6039
|
const opts = {
|
|
3361
6040
|
...toCliOptions(g),
|
|
3362
6041
|
...typeof g.host === "string" ? { host: g.host } : {},
|
|
3363
|
-
...typeof g.command === "string" ? { command: g.command } : {}
|
|
6042
|
+
...typeof g.command === "string" ? { command: g.command } : {},
|
|
6043
|
+
...typeof g.profile === "string" ? { profile: g.profile } : {}
|
|
3364
6044
|
};
|
|
6045
|
+
if (g.listProfiles === true) {
|
|
6046
|
+
listProfilesCommand(opts);
|
|
6047
|
+
return;
|
|
6048
|
+
}
|
|
3365
6049
|
await run(prompt, opts);
|
|
3366
6050
|
});
|
|
6051
|
+
function listProfilesCommand(opts) {
|
|
6052
|
+
const config = loadRunConfig(process.cwd());
|
|
6053
|
+
const rows = config ? listProfiles(config) : [];
|
|
6054
|
+
if (opts.json === true) {
|
|
6055
|
+
json({ ok: true, data: rows });
|
|
6056
|
+
return;
|
|
6057
|
+
}
|
|
6058
|
+
if (rows.length === 0) {
|
|
6059
|
+
info("(no run profiles configured \u2014 add a run.profiles block to .noir/config.yml)", opts);
|
|
6060
|
+
return;
|
|
6061
|
+
}
|
|
6062
|
+
table(rows, ["NAME", "DEFAULT", "BINARY"], opts);
|
|
6063
|
+
}
|
|
3367
6064
|
program2.action(async (...args) => {
|
|
3368
6065
|
const cmd = trailingCmd(args);
|
|
3369
6066
|
const leftovers = cmd.args;
|
|
@@ -3378,21 +6075,6 @@ function createProgram() {
|
|
|
3378
6075
|
});
|
|
3379
6076
|
return program2;
|
|
3380
6077
|
}
|
|
3381
|
-
var program = createProgram();
|
|
3382
|
-
var homeDeps = {
|
|
3383
|
-
dispatch: async (argv) => {
|
|
3384
|
-
const sub = createProgram();
|
|
3385
|
-
try {
|
|
3386
|
-
await sub.parseAsync([...argv], { from: "user" });
|
|
3387
|
-
} catch (err) {
|
|
3388
|
-
handleError(err);
|
|
3389
|
-
}
|
|
3390
|
-
},
|
|
3391
|
-
// The grouped home menu resolves its sections against the LIVE palette
|
|
3392
|
-
// registry (home-consolidation S1/S2) — the same source the TUI palette
|
|
3393
|
-
// uses — so the menu cannot drift from the commander tree.
|
|
3394
|
-
commands: buildPaletteCommands(createProgram())
|
|
3395
|
-
};
|
|
3396
6078
|
async function run2(argv = []) {
|
|
3397
6079
|
try {
|
|
3398
6080
|
await program.parseAsync([...argv], { from: "user" });
|
|
@@ -3401,18 +6083,56 @@ async function run2(argv = []) {
|
|
|
3401
6083
|
}
|
|
3402
6084
|
return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
|
|
3403
6085
|
}
|
|
3404
|
-
var
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
6086
|
+
var DEPRECATIONS, program, homeDeps, isMainModule;
|
|
6087
|
+
var init_bin = __esm({
|
|
6088
|
+
"src/bin.ts"() {
|
|
6089
|
+
init_context();
|
|
6090
|
+
init_daemon();
|
|
6091
|
+
init_doctor();
|
|
6092
|
+
init_handoff();
|
|
6093
|
+
init_home();
|
|
6094
|
+
init_memory();
|
|
6095
|
+
init_release();
|
|
6096
|
+
init_run();
|
|
6097
|
+
init_skills();
|
|
6098
|
+
init_status();
|
|
6099
|
+
init_task();
|
|
6100
|
+
init_init();
|
|
6101
|
+
init_output();
|
|
6102
|
+
init_run_profiles();
|
|
6103
|
+
init_serve();
|
|
6104
|
+
init_registry();
|
|
6105
|
+
DEPRECATIONS = [];
|
|
6106
|
+
program = createProgram();
|
|
6107
|
+
homeDeps = {
|
|
6108
|
+
dispatch: async (argv) => {
|
|
6109
|
+
const sub = createProgram();
|
|
6110
|
+
try {
|
|
6111
|
+
await sub.parseAsync([...argv], { from: "user" });
|
|
6112
|
+
} catch (err) {
|
|
6113
|
+
handleError(err);
|
|
6114
|
+
}
|
|
6115
|
+
},
|
|
6116
|
+
// The grouped home menu resolves its sections against the LIVE palette
|
|
6117
|
+
// registry (home-consolidation S1/S2) — the same source the TUI palette
|
|
6118
|
+
// uses — so the menu cannot drift from the commander tree.
|
|
6119
|
+
commands: buildPaletteCommands(createProgram())
|
|
6120
|
+
};
|
|
6121
|
+
isMainModule = (() => {
|
|
6122
|
+
try {
|
|
6123
|
+
const entry = process.argv[1];
|
|
6124
|
+
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
6125
|
+
return pathToFileURL(realpathSync(entry)).href === import.meta.url;
|
|
6126
|
+
} catch {
|
|
6127
|
+
return false;
|
|
6128
|
+
}
|
|
6129
|
+
})();
|
|
6130
|
+
if (isMainModule) {
|
|
6131
|
+
void run2(process.argv.slice(2));
|
|
6132
|
+
}
|
|
3411
6133
|
}
|
|
3412
|
-
})
|
|
3413
|
-
|
|
3414
|
-
void run2(process.argv.slice(2));
|
|
3415
|
-
}
|
|
6134
|
+
});
|
|
6135
|
+
init_bin();
|
|
3416
6136
|
export {
|
|
3417
6137
|
DEPRECATIONS,
|
|
3418
6138
|
EXIT,
|