@noir-ai/cli 1.2.0-beta.1 → 1.3.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 CHANGED
@@ -1,25 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ doctor,
4
+ serve
5
+ } from "./chunk-J5HAAQI4.js";
6
+ import {
7
+ init
8
+ } from "./chunk-E5XVH7QR.js";
2
9
  import {
3
10
  EXIT,
4
11
  NoirCliError,
5
- doctor,
6
12
  fail,
7
13
  handleError,
8
14
  inferExitCode,
9
15
  info,
10
16
  isInteractive,
11
17
  log,
12
- serve,
13
18
  table,
14
19
  warn
15
- } from "./chunk-OMUB7XOA.js";
16
- import {
17
- init
18
- } from "./chunk-UGUQQSWH.js";
20
+ } from "./chunk-OF4OO5W6.js";
19
21
 
20
22
  // src/bin.ts
23
+ import { realpathSync } from "fs";
21
24
  import { pathToFileURL } from "url";
22
- import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
25
+ import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
23
26
  import { NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
24
27
  import { Command, Option } from "commander";
25
28
 
@@ -459,15 +462,50 @@ async function daemonRestart(opts) {
459
462
  }
460
463
 
461
464
  // src/commands/home.ts
465
+ import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
462
466
  import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
467
+
468
+ // src/banner.ts
469
+ import pc from "picocolors";
470
+ var NOIR_BLOCK = [
471
+ "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 ",
472
+ "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
473
+ "\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551",
474
+ "\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
475
+ "\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 \u2588\u2588\u2551",
476
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
477
+ ];
478
+ var ROW_COLORS = [pc.magenta, pc.magentaBright, pc.blue, pc.blueBright, pc.cyan, pc.cyanBright];
479
+ var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
480
+ function renderBanner(opts = {}) {
481
+ const width = opts.width ?? process.stdout.columns ?? 80;
482
+ const color = opts.color ?? true;
483
+ if (width < 50) {
484
+ return color ? `${pc.magenta("\u25C6")} noir` : "\u25C6 noir";
485
+ }
486
+ return NOIR_BLOCK.map((line, i) => color ? (ROW_COLORS[i] ?? pc.cyan)(line) : line).join("\n");
487
+ }
488
+ function shouldShowBanner(opts) {
489
+ if (opts.quiet === true || opts.json === true) return false;
490
+ const v = process.env.NOIR_NO_BANNER;
491
+ if (v !== void 0 && v !== "") return false;
492
+ return true;
493
+ }
494
+
495
+ // src/commands/home.ts
463
496
  function tryProject() {
464
497
  try {
465
498
  const info2 = loadProjectInfo3(process.cwd());
466
- return { id: info2.id, name: info2.name };
499
+ return { id: info2.id, name: info2.name, host: info2.config.host };
467
500
  } catch {
468
501
  return null;
469
502
  }
470
503
  }
504
+ var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
505
+ function hostDirection(host) {
506
+ const others = SUPPORTED_HOSTS.filter((h) => h !== host);
507
+ return `\u2192 host: ${host}. Open \`${host}\` to start development \u2014 Noir set the rules, skills, and memory; ${host} runs the code. (other hosts: ${others.join(", ")})`;
508
+ }
471
509
  async function home(opts, deps) {
472
510
  if (isInteractive(opts)) {
473
511
  await runMenu(opts, deps);
@@ -482,8 +520,20 @@ async function home(opts, deps) {
482
520
  async function runMenu(opts, deps) {
483
521
  const clack = await import("@clack/prompts");
484
522
  const project = tryProject();
485
- const banner = project ? `noir \u2014 ${project.name}` : "noir";
486
- clack.intro(banner);
523
+ if (shouldShowBanner(opts)) {
524
+ const host = project?.host ?? "claude";
525
+ process.stderr.write(
526
+ `
527
+ ${renderBanner()}
528
+ ${NOIR_TAGLINE}
529
+
530
+ ${hostDirection(host)}
531
+ ${COMMANDS_HINT}
532
+
533
+ `
534
+ );
535
+ }
536
+ clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
487
537
  const choice = await clack.select({
488
538
  message: "What would you like to do?",
489
539
  initialValue: "status",
@@ -1239,11 +1289,14 @@ function createProgram() {
1239
1289
  );
1240
1290
  }
1241
1291
  }
1292
+ const nonInteractive = opts.json === true || opts.input === false;
1293
+ if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
1294
+ else delete process.env.NOIR_NON_INTERACTIVE;
1242
1295
  });
1243
1296
  program2.command("init").description("scaffold Noir in the current project (.noir/, .mcp.json, CLAUDE.md, skills)").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option(
1244
1297
  "--upgrade",
1245
1298
  "run scaffold migrations before re-emitting (re-run on an existing project)"
1246
- ).addOption(
1299
+ ).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
1247
1300
  // S10: target host. Defaults to `'claude'` (the regression anchor). The
1248
1301
  // chosen host is forwarded to scaffold() + skills emission via
1249
1302
  // resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
@@ -1251,39 +1304,49 @@ function createProgram() {
1251
1304
  new Option(
1252
1305
  "--host <id>",
1253
1306
  "target agentic CLI (default: claude) \u2014 drives host-side emission"
1254
- ).choices(SUPPORTED_HOSTS)
1307
+ ).choices(SUPPORTED_HOSTS2)
1255
1308
  ).action(
1256
1309
  async (opts) => {
1257
1310
  const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1258
1311
  const upgrade = opts.upgrade === true;
1312
+ const force = opts.force === true;
1259
1313
  const host = parseHost(opts.host);
1260
1314
  await init(process.cwd(), {
1261
1315
  transport,
1262
1316
  url: opts.url,
1263
1317
  ...upgrade ? { upgrade } : {},
1318
+ ...force ? { force } : {},
1264
1319
  ...host !== void 0 ? { host } : {}
1265
1320
  });
1266
1321
  }
1267
1322
  );
1268
- program2.command("create [dir]").description("bootstrap the Noir AI layer in a new or empty directory").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").addOption(
1323
+ program2.command("create [dir]").description("bootstrap the Noir AI layer in a new or empty directory").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
1269
1324
  new Option(
1270
1325
  "--host <id>",
1271
1326
  "target agentic CLI (default: claude) \u2014 drives host-side emission"
1272
- ).choices(SUPPORTED_HOSTS)
1327
+ ).choices(SUPPORTED_HOSTS2)
1273
1328
  ).action(
1274
1329
  async (dir, opts) => {
1275
1330
  const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1331
+ const force = opts.force === true;
1276
1332
  const host = parseHost(opts.host);
1277
- const { create } = await import("./create-WP5ZKMCW.js");
1333
+ const { create } = await import("./create-2P3T7UGZ.js");
1278
1334
  await create(dir, {
1279
1335
  transport,
1280
1336
  url: opts.url,
1337
+ ...force ? { force } : {},
1281
1338
  ...host !== void 0 ? { host } : {}
1282
1339
  });
1283
1340
  }
1284
1341
  );
1285
1342
  program2.command("sync").description(
1286
1343
  "re-emit Noir managed files (.mcp.json, CLAUDE.md blocks, NOIR.md brief, ignores) + skills"
1344
+ ).option(
1345
+ "--force",
1346
+ "overwrite differing regenerated files without prompting (bypasses the conflict menu)"
1347
+ ).option(
1348
+ "--merge",
1349
+ "three-way merge managed regions (preserve hand-edits inside <!-- noir:* --> markers)"
1287
1350
  ).addOption(
1288
1351
  // S10: optional `--host` override. When omitted, sync reads host from
1289
1352
  // `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
@@ -1291,14 +1354,20 @@ function createProgram() {
1291
1354
  new Option(
1292
1355
  "--host <id>",
1293
1356
  "override the configured host (advanced; default reads .noir/config.yml)"
1294
- ).choices(SUPPORTED_HOSTS)
1357
+ ).choices(SUPPORTED_HOSTS2)
1295
1358
  ).action(async (opts) => {
1296
- const { sync } = await import("./sync-IEH2BVXU.js");
1359
+ const { sync } = await import("./sync-7XFGCRJN.js");
1297
1360
  const host = parseHost(opts.host);
1298
- if (host === void 0) {
1361
+ const force = opts.force === true;
1362
+ const merge = opts.merge === true;
1363
+ if (host === void 0 && !force && !merge) {
1299
1364
  await sync(process.cwd());
1300
1365
  } else {
1301
- await sync(process.cwd(), { host });
1366
+ await sync(process.cwd(), {
1367
+ ...host !== void 0 ? { host } : {},
1368
+ ...force ? { force } : {},
1369
+ ...merge ? { merge } : {}
1370
+ });
1302
1371
  }
1303
1372
  });
1304
1373
  const mcpCmd = program2.command("mcp").description("MCP server control");
@@ -1327,8 +1396,13 @@ function createProgram() {
1327
1396
  daemonGrp.action(() => {
1328
1397
  throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
1329
1398
  });
1330
- program2.command("doctor").description("environment + project health").action(async (...args) => {
1331
- await doctor(toCliOptions(actionGlobals(args)));
1399
+ program2.command("doctor").description("environment + project health").option(
1400
+ "--dedup",
1401
+ "scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
1402
+ ).action(async (...args) => {
1403
+ const g = actionGlobals(args);
1404
+ const dedup = trailingCmd(args).opts().dedup === true;
1405
+ await doctor({ ...toCliOptions(g), ...dedup ? { dedup: true } : {} });
1332
1406
  });
1333
1407
  program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
1334
1408
  await status(toStatusOptions(actionGlobals(args)));
@@ -1507,7 +1581,8 @@ async function run(argv = []) {
1507
1581
  var isMainModule = (() => {
1508
1582
  try {
1509
1583
  const entry = process.argv[1];
1510
- return typeof entry === "string" && entry.length > 0 ? pathToFileURL(entry).href === import.meta.url : false;
1584
+ if (typeof entry !== "string" || entry.length === 0) return false;
1585
+ return pathToFileURL(realpathSync(entry)).href === import.meta.url;
1511
1586
  } catch {
1512
1587
  return false;
1513
1588
  }