@noir-ai/cli 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import {
6
6
  spinner,
7
7
  success,
8
8
  warn
9
- } from "./chunk-KETMM3BA.js";
9
+ } from "./chunk-KVJIPKVE.js";
10
10
 
11
11
  // src/commands/update.ts
12
12
  import {
@@ -242,4 +242,4 @@ export {
242
242
  buildUpdateNotice,
243
243
  runAsyncUpdateCheck
244
244
  };
245
- //# sourceMappingURL=chunk-TPNJEN4B.js.map
245
+ //# sourceMappingURL=chunk-CQU2INJH.js.map
@@ -23,6 +23,77 @@ import ora from "ora";
23
23
 
24
24
  // src/theme.ts
25
25
  import pc from "picocolors";
26
+
27
+ // src/width.ts
28
+ import stringWidth from "string-width";
29
+ import stripAnsi from "strip-ansi";
30
+ var segmenter = new Intl.Segmenter();
31
+ function clusters(s) {
32
+ return [...segmenter.segment(s)].map(({ segment }) => segment);
33
+ }
34
+ function displayWidth(s) {
35
+ return stringWidth(s);
36
+ }
37
+ function fitLeading(parts, width) {
38
+ let used = 0;
39
+ let count = 0;
40
+ for (const part of parts) {
41
+ const w = displayWidth(part);
42
+ if (used + w > width) {
43
+ break;
44
+ }
45
+ used += w;
46
+ count++;
47
+ }
48
+ return count;
49
+ }
50
+ function truncateToWidth(s, width, ellipsis = "\u2026") {
51
+ const plain = stripAnsi(s);
52
+ if (width <= 0) {
53
+ return "";
54
+ }
55
+ if (displayWidth(plain) <= width) {
56
+ return plain;
57
+ }
58
+ const markerParts = clusters(ellipsis);
59
+ const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
60
+ const budget = width - displayWidth(marker);
61
+ const parts = clusters(plain);
62
+ return parts.slice(0, fitLeading(parts, budget)).join("") + marker;
63
+ }
64
+ function truncateMiddle(s, width, ellipsis = "\u2026") {
65
+ const plain = stripAnsi(s);
66
+ if (width <= 0) {
67
+ return "";
68
+ }
69
+ if (displayWidth(plain) <= width) {
70
+ return plain;
71
+ }
72
+ const markerParts = clusters(ellipsis);
73
+ const marker = markerParts.slice(0, fitLeading(markerParts, width)).join("");
74
+ const remaining = width - displayWidth(marker);
75
+ const headBudget = Math.ceil(remaining / 2);
76
+ const tailBudget = remaining - headBudget;
77
+ const parts = clusters(plain);
78
+ const headCount = fitLeading(parts, headBudget);
79
+ const tail = [];
80
+ let tailUsed = 0;
81
+ for (const part of parts.slice(headCount).reverse()) {
82
+ const w = displayWidth(part);
83
+ if (tailUsed + w > tailBudget) {
84
+ break;
85
+ }
86
+ tail.push(part);
87
+ tailUsed += w;
88
+ }
89
+ return parts.slice(0, headCount).join("") + marker + tail.reverse().join("");
90
+ }
91
+ function padToWidth(s, width) {
92
+ const pad = width - displayWidth(s);
93
+ return pad > 0 ? s + " ".repeat(pad) : s;
94
+ }
95
+
96
+ // src/theme.ts
26
97
  var FORCE = pc.createColors(true);
27
98
  var c = {
28
99
  /** Green — success / healthy. */
@@ -48,8 +119,9 @@ var BADGE_SYMBOL = {
48
119
  error: "\u2717",
49
120
  info: "\u2139"
50
121
  };
51
- function badge(state, label = state) {
52
- const text = `${BADGE_SYMBOL[state]} ${label}`;
122
+ function badge(state, label = state, maxWidth) {
123
+ const full = `${BADGE_SYMBOL[state]} ${label}`;
124
+ const text = maxWidth === void 0 ? full : truncateToWidth(full, maxWidth);
53
125
  switch (state) {
54
126
  case "ok":
55
127
  return c.ok(text);
@@ -70,8 +142,13 @@ function isCiEnv() {
70
142
  if (v === void 0 || v === "") return false;
71
143
  return v !== "0" && v !== "false";
72
144
  }
145
+ function quietMode() {
146
+ const v = process.env.NOIR_QUIET;
147
+ return v !== void 0 && v !== "";
148
+ }
73
149
  function useColor() {
74
150
  if (envFlagSet("NO_COLOR")) return false;
151
+ if (quietMode()) return false;
75
152
  if (process.env.CLICOLOR_FORCE === "1") return true;
76
153
  if (isCiEnv()) return false;
77
154
  return pc.isColorSupported;
@@ -85,15 +162,17 @@ function terminalWidth() {
85
162
  const n = Number(fromEnv);
86
163
  if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
87
164
  }
88
- const cols = process.stdout.columns;
89
- if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
165
+ const stderrCols = process.stderr.columns;
166
+ if (typeof stderrCols === "number" && stderrCols > 0) return Math.max(stderrCols, 20);
167
+ const stdoutCols = process.stdout.columns;
168
+ if (typeof stdoutCols === "number" && stdoutCols > 0) return Math.max(stdoutCols, 20);
90
169
  return 80;
91
170
  }
92
171
  function contentWidth(border = 2, padding = 2) {
93
172
  return Math.max(20, terminalWidth() - border - padding);
94
173
  }
95
- function divider() {
96
- return c.dim("\u2500".repeat(Math.max(1, contentWidth())));
174
+ function divider(width = contentWidth()) {
175
+ return c.dim("\u2500".repeat(Math.max(1, width)));
97
176
  }
98
177
 
99
178
  // src/output.ts
@@ -180,61 +259,91 @@ function table(rows, cols, opts = {}) {
180
259
  info("(no rows)", opts);
181
260
  return;
182
261
  }
262
+ const widths = computeColWidths(rows, cols);
183
263
  const t = new Table({
184
264
  head: cols.map((col) => c.bold(c.info(col))),
185
265
  // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
186
266
  // default red header nor a colored border). All color is ours, via theme.
187
267
  style: { head: [], border: [] },
188
- colWidths: computeColWidths(rows, cols),
268
+ colWidths: widths,
189
269
  wordWrap: true,
190
270
  truncate: "\u2026"
191
271
  });
192
272
  for (const row of rows) {
193
- t.push(cols.map((col) => formatCell(row[col])));
273
+ t.push(cols.map((col, i) => truncateCell(row[col], (widths[i] ?? 0) - 2)));
194
274
  }
195
275
  process.stderr.write(`${t.toString()}
196
276
  `);
197
277
  }
278
+ var MIN_CONTENT_WIDTH = 3;
198
279
  function computeColWidths(rows, cols) {
199
280
  const n = cols.length;
200
281
  if (n === 0) return [];
201
- const headerLen = cols.map((col) => col.length);
282
+ const headerLen = cols.map((col) => displayWidth(col));
202
283
  const natural = cols.map((col, i) => {
203
- let max = Math.max(headerLen[i] ?? col.length, 3);
284
+ let max = Math.max(headerLen[i] ?? displayWidth(col), MIN_CONTENT_WIDTH);
204
285
  for (const row of rows) {
205
- const len = formatCell(row[col]).length;
286
+ const len = displayWidth(formatCell(row[col]));
206
287
  if (len > max) max = len;
207
288
  }
208
289
  return max;
209
290
  });
210
- const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
291
+ const budget = Math.max(n, terminalWidth() - 3 * n - 1);
211
292
  const naturalSum = natural.reduce((a, b) => a + b, 0);
212
- if (naturalSum <= contentBudget) {
293
+ if (naturalSum <= budget) {
213
294
  return natural.map((w) => w + 2);
214
295
  }
215
296
  const content = natural.slice();
216
297
  let sum = naturalSum;
217
- const floor = headerLen.map((h) => Math.max(h, 3));
218
- while (sum > contentBudget) {
298
+ const floor = headerLen.map((h) => Math.min(Math.max(h, MIN_CONTENT_WIDTH), budget));
299
+ while (sum > budget) {
219
300
  let idx = -1;
220
301
  let widest = -1;
221
302
  for (let i = 0; i < n; i++) {
222
303
  const c2 = content[i] ?? 0;
223
- const f = floor[i] ?? 3;
304
+ const f = floor[i] ?? MIN_CONTENT_WIDTH;
224
305
  if (c2 > f && c2 > widest) {
225
306
  widest = c2;
226
307
  idx = i;
227
308
  }
228
309
  }
229
310
  if (idx === -1) break;
230
- const overshoot = sum - contentBudget;
231
- const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
311
+ const overshoot = sum - budget;
312
+ const reducible = (content[idx] ?? 0) - (floor[idx] ?? MIN_CONTENT_WIDTH);
232
313
  const cut = Math.min(overshoot, reducible);
233
314
  content[idx] = (content[idx] ?? 0) - cut;
234
315
  sum -= cut;
235
316
  }
317
+ if (sum > budget) {
318
+ for (let pass = 0; pass < n && sum > budget; pass++) {
319
+ let idx = -1;
320
+ let widest = 0;
321
+ for (let j = 0; j < n; j++) {
322
+ const c2 = content[j] ?? 0;
323
+ if (c2 > 1 && c2 > widest) {
324
+ widest = c2;
325
+ idx = j;
326
+ }
327
+ }
328
+ if (idx === -1) break;
329
+ const cut = Math.min(sum - budget, (content[idx] ?? 1) - 1);
330
+ content[idx] = (content[idx] ?? 1) - cut;
331
+ sum -= cut;
332
+ }
333
+ }
236
334
  return content.map((w) => w + 2);
237
335
  }
336
+ function isPathToken(s) {
337
+ if (!/[\\/]/.test(s)) return false;
338
+ return /^(\/|\.\/|\.\.\/|~\/|[A-Za-z]:[\\/])/.test(s) || /\.[A-Za-z0-9][A-Za-z0-9_-]*$/.test(s);
339
+ }
340
+ function truncateCell(value, contentWidth2) {
341
+ const s = formatCell(value);
342
+ if (contentWidth2 <= 0 || displayWidth(s) <= contentWidth2) return s;
343
+ if (isPathToken(s)) return truncateMiddle(s, contentWidth2);
344
+ if (/\s/.test(s)) return s;
345
+ return truncateToWidth(s, contentWidth2);
346
+ }
238
347
  function formatCell(value) {
239
348
  if (value === void 0 || value === null) return "";
240
349
  if (typeof value === "string") return value;
@@ -245,7 +354,7 @@ function formatCell(value) {
245
354
  }
246
355
  function definitionList(rows, opts = {}) {
247
356
  table(
248
- rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
357
+ rows.map((r) => ({ Field: r.label, Value: r.value })),
249
358
  ["Field", "Value"],
250
359
  opts
251
360
  );
@@ -574,10 +683,14 @@ async function install(opts = {}) {
574
683
  }
575
684
 
576
685
  export {
686
+ displayWidth,
687
+ truncateToWidth,
688
+ padToWidth,
577
689
  c,
578
690
  badge,
579
691
  useColor,
580
692
  accessibleMode,
693
+ terminalWidth,
581
694
  contentWidth,
582
695
  divider,
583
696
  EXIT,
@@ -602,4 +715,4 @@ export {
602
715
  installManagedNode,
603
716
  install
604
717
  };
605
- //# sourceMappingURL=chunk-KETMM3BA.js.map
718
+ //# sourceMappingURL=chunk-KVJIPKVE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/commands/install.ts","../../src/output.ts","../../src/theme.ts","../../src/width.ts"],"sourcesContent":["import { chmodSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport {\n atomicWriteFile,\n type DetectResult,\n detectActiveMethod,\n detectInstallMethods,\n ensureShimExecutable,\n loadProjectInfo,\n MANAGED_NODE_VERSION,\n NOIR_VERSION,\n noirHome,\n type ProvisionedNode,\n type ProvisionOptions,\n provisionManagedNode,\n readInstallRecord,\n runManagerCmd,\n writeInstallRecord,\n} from '@noir-ai/core';\nimport { type CliOptions, EXIT, fail, info, spinner, success, warn } from '../output.js';\n\nexport interface InstallOptions extends CliOptions {\n list?: boolean;\n uninstallPrev?: boolean;\n spec?: string;\n /** Dismiss the migration banner for the current CLI version (persists in\n * install.json's `dismissedVersions`). Typically passed with `--list`. */\n dismiss?: boolean;\n}\n\nexport interface MigrationPlan {\n detected: DetectResult[];\n currentMethod: string;\n targetSpec: string;\n installedVersion: string | null;\n shouldMigrate: boolean;\n isDowngrade: boolean;\n /**\n * True when the resolved target is a concrete version below the configured\n * `update.minVersion` floor. Channel targets\n * (`latest`/`beta`) resolve to the newest at install time and never trip it.\n * Enforced in {@link install} with the same warn/refuse pattern as the\n * downgrade guard.\n */\n belowMinVersion: boolean;\n nativeVersion: string;\n prevUninstallCmd: string | null;\n autoUninstall: boolean; // always false unless --uninstall-prev\n}\n\n/** Per-segment numeric semver comparison. Returns true when `a < b`. */\nfunction semverLt(a: string, b: string): boolean {\n // Strip a leading `v` (npm-idiomatic exact-version spelling that\n // installManagedNode explicitly supports) — bare Number('v1.9.0') is NaN → 0.\n const seg = (s: string): number => Number(s.replace(/^v/, '')) || 0;\n const pa = a.split('.').map(seg);\n const pb = b.split('.').map(seg);\n for (let i = 0; i < 3; i++) {\n const x = pa[i] ?? 0;\n const y = pb[i] ?? 0;\n if (x !== y) return x < y;\n }\n return false;\n}\n\n/** Pure: build the migration plan from detection + target. No I/O. */\nexport function buildMigrationPlan(opts: {\n detected: DetectResult[];\n currentMethod: string;\n targetSpec: string;\n installedVersion: string | null;\n /**\n * The `update.minVersion` floor from config (default `'1.6.0'`). When\n * omitted, the config default applies. See the floor check in {@link install}.\n */\n minVersion?: string;\n}): MigrationPlan {\n const detected = opts.detected;\n const shouldMigrate = detected.length > 0 && opts.currentMethod !== 'native';\n const nativeVersion = opts.targetSpec;\n let isDowngrade = false;\n if (opts.installedVersion && nativeVersion !== 'latest' && nativeVersion !== 'beta') {\n isDowngrade = semverLt(nativeVersion, opts.installedVersion);\n }\n // minVersion floor: only a concrete version below the floor trips it;\n // 'latest'/'beta' resolve to the newest at install time (always >= floor).\n const floor = opts.minVersion ?? '1.6.0';\n const belowMinVersion =\n nativeVersion !== 'latest' && nativeVersion !== 'beta' && semverLt(nativeVersion, floor);\n return {\n detected,\n currentMethod: opts.currentMethod,\n targetSpec: opts.targetSpec,\n installedVersion: opts.installedVersion,\n shouldMigrate,\n isDowngrade,\n belowMinVersion,\n nativeVersion,\n prevUninstallCmd: detected[0]?.uninstallCmd ?? null,\n autoUninstall: false,\n };\n}\n\nexport interface InstallManagedNodeOptions {\n channel?: string;\n version?: string;\n env: NodeJS.ProcessEnv;\n /**\n * Forwarded into {@link provisionManagedNode}. Carries the offline mock\n * seams (`fetch`, `exec`, `target`, `signal`) so the install pipeline is\n * unit-testable without network — same pattern as `provisionManagedNode`'s\n * own suite in `packages/core/test/node-provision.test.ts`.\n */\n provision?: Pick<ProvisionOptions, 'fetch' | 'exec' | 'target' | 'signal'>;\n}\n\n/** Normalized record of which runtime backed an install. */\nexport interface InstallManagedNodeResult {\n ok: boolean;\n version: string | null;\n /** What runtime backed this install: `'managed'` (downloaded into ~/.noir)\n * or `'system'` (used the system-Node fallback). */\n runtimeSource?: ProvisionedNode['source'];\n error?: string;\n}\n\n/**\n * Provision the managed Node runtime and install `@noir-ai/cli` into an\n * isolated `~/.noir/cli` prefix, shimmed from `~/.noir/bin/noir`.\n *\n * Replaces the prior \"not provisioned\" fail branch: instead of requiring the\n * user to pre-run `install.sh`/`install.ps1`, this now delegates to\n * {@link provisionManagedNode} (download + verify + extract, fail-closed), then\n * installs the CLI with the provisioned npm. On a system-Node fallback the\n * fallback node/npm paths are used and a warning is emitted; the install\n * record's `managedRuntimeVersion` reflects which path was taken so `noir\n * doctor` can report it accurately.\n */\nexport async function installManagedNode(\n opts: InstallManagedNodeOptions,\n): Promise<InstallManagedNodeResult> {\n const spec = opts.version ?? opts.channel ?? 'latest';\n const home = noirHome();\n const cliDir = join(home, 'cli');\n const binDir = join(home, 'bin');\n mkdirSync(cliDir, { recursive: true });\n mkdirSync(binDir, { recursive: true });\n\n // Provision the managed Node runtime. Idempotent — re-runs are no-ops.\n // On any failure, provisionManagedNode either falls back to a system Node\n // >=22 (returns { source: 'system' }) or throws — we surface the throw as an\n // {ok:false} envelope rather than letting it kill the CLI.\n let runtime: ProvisionedNode;\n try {\n runtime = await provisionManagedNode({\n ...(opts.provision ?? {}),\n env: opts.env,\n });\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, version: null, error: `managed-Node provision failed: ${reason}` };\n }\n if (runtime.source === 'system') {\n warn(\n `managed-Node download failed; using system Node ${runtime.version} at ${runtime.nodeBin}.`,\n );\n }\n\n const { nodeBin, npmBin } = runtime;\n\n // Install @noir-ai/cli into the isolated prefix using the provisioned npm.\n const { code, stderr } = await runManagerCmd(\n [npmBin, 'install', '-g', `@noir-ai/cli@${spec}`, `--prefix=${cliDir}`],\n { env: opts.env, timeoutMs: 120_000 },\n );\n if (code !== 0) {\n return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };\n }\n\n // Shim: ~/.noir/bin/noir -> provisioned node + isolated prefix.\n const shim = join(binDir, 'noir');\n const shimBody = `#!/usr/bin/env bash\\n\"${nodeBin}\" \"${join(cliDir, 'lib', 'node_modules', '@noir-ai', 'cli', 'dist', 'bin.js')}\" \"$@\"\\n`;\n atomicWriteFile(shim, shimBody);\n chmodSync(shim, 0o755); // must be executable — atomicWriteFile sets 0o644 by default\n ensureShimExecutable(); // defense-in-depth: re-assert 0o755 (idempotent, never throws)\n // (POSIX shim; Windows uses a .cmd wrapper — install.sh/install.ps1.)\n\n // Resolve installed version via the provisioned node.\n const ver = await runManagerCmd(\n [\n nodeBin,\n join(cliDir, 'lib', 'node_modules', '@noir-ai', 'cli', 'dist', 'bin.js'),\n '--version',\n ],\n { env: opts.env, timeoutMs: 30_000 },\n );\n const version = ver.code === 0 ? ver.stdout.trim() : null;\n writeInstallRecord({\n method: 'native',\n version: version ?? '0.0.0',\n channel: opts.channel ?? 'latest',\n installedAt: new Date().toISOString(),\n // Reflect which runtime backs this install: the pinned managed version\n // (e.g. '22.23.2') for managed, or 'system-<v>' for the fallback so doctor\n // can distinguish a fallback install from a real managed one.\n managedRuntimeVersion:\n runtime.source === 'managed' ? MANAGED_NODE_VERSION : `system-${runtime.version}`,\n });\n return { ok: true, version, runtimeSource: runtime.source };\n}\n\nexport async function install(opts: InstallOptions = {}): Promise<void> {\n // --dismiss: append the current CLI version to install.json's\n // `dismissedVersions` so the migration banner stops showing for this\n // version. Idempotent (no duplicate entries). Typically combined with\n // `--list`; works standalone too. Requires an existing install record —\n // if there's none, there's nothing to dismiss (no banner was shown).\n // Track the dismiss outcome so `--dismiss --list --json` folds it into the\n // SINGLE list envelope — never two JSON documents on stdout.\n let dismissOutcome: { dismissed: boolean; version: string | null } | undefined;\n if (opts.dismiss === true) {\n const rec = readInstallRecord();\n if (!rec) {\n dismissOutcome = { dismissed: false, version: null };\n if (!opts.json) info('No install record found; nothing to dismiss.');\n } else {\n const dismissed = new Set(rec.dismissedVersions ?? []);\n if (!dismissed.has(NOIR_VERSION)) dismissed.add(NOIR_VERSION);\n writeInstallRecord({ ...rec, dismissedVersions: [...dismissed] });\n dismissOutcome = { dismissed: true, version: NOIR_VERSION };\n if (!opts.json) success(`Migration banner dismissed for ${NOIR_VERSION}.`);\n }\n }\n\n // --dismiss alone (no --list): emit the single result now.\n if (opts.dismiss === true && opts.list !== true) {\n if (opts.json) process.stdout.write(`${JSON.stringify({ ok: true, data: dismissOutcome })}\\n`);\n return;\n }\n\n if (opts.list === true) {\n const detected = await detectInstallMethods(process.env);\n if (opts.json) {\n process.stdout.write(\n `${JSON.stringify({ ok: true, data: { ...(dismissOutcome ?? {}), detected } })}\\n`,\n );\n return;\n }\n info('Detected installs:');\n for (const d of detected) info(` ${d.method} (${d.version ?? 'unknown'})`);\n return;\n }\n\n const currentMethod = detectActiveMethod();\n const detected = await detectInstallMethods(process.env);\n const installedRecord = readInstallRecord();\n // Read the `update.minVersion` floor from the project config (falls\n // back to the config default '1.6.0' when the project isn't initialized or\n // the block is absent). Same try/catch pattern as home.ts's update-config read.\n let minVersion = '1.6.0';\n try {\n minVersion = loadProjectInfo(process.cwd()).config.update.minVersion;\n } catch {\n // Not initialized yet — the config default applies.\n }\n const plan = buildMigrationPlan({\n detected,\n currentMethod,\n targetSpec: opts.spec ?? 'latest',\n installedVersion: installedRecord?.version ?? null,\n minVersion,\n });\n\n // Refuse to install a version below the configured minVersion floor, and\n // refuse a downgrade, UNCONDITIONALLY. The floor/downgrade-guard exist as\n // safety invariants; gating them on `--no-input` meant the common interactive\n // path (and `--json`, which does not set input:false) silently proceeded with\n // only a stderr warning — the invariant was never actually enforced.\n if (plan.belowMinVersion) {\n fail(\n EXIT.USAGE,\n `refusing install of ${plan.nativeVersion} (below minVersion ${minVersion})`,\n opts,\n );\n }\n\n if (plan.isDowngrade) {\n fail(\n EXIT.USAGE,\n `refusing downgrade to ${plan.nativeVersion} (installed ${plan.installedVersion})`,\n opts,\n );\n }\n\n const installSpin = spinner('Installing Noir via native installer...', opts).start();\n const result = await installManagedNode({\n channel: plan.nativeVersion === 'beta' ? 'beta' : undefined,\n version: plan.nativeVersion.startsWith('v')\n ? plan.nativeVersion\n : plan.nativeVersion === 'latest' || plan.nativeVersion === 'beta'\n ? undefined\n : plan.nativeVersion,\n env: process.env,\n });\n if (!result.ok) {\n installSpin.fail('Native install failed');\n fail(EXIT.ERROR, result.error ?? 'native install failed', opts);\n }\n installSpin.succeed(`Installed noir ${result.version}`);\n\n // Migration step runs FIRST, in BOTH modes — the --json early-return below\n // must not skip it (a scripted/CI migration relying on --uninstall-prev would\n // otherwise silently leave the stale install in place while reporting success).\n // Its diagnostics go to stderr (warn/success), which --json leaves untouched.\n if (plan.prevUninstallCmd && opts.uninstallPrev !== true) {\n warn(`To finish the migration, uninstall the previous install: ${plan.prevUninstallCmd}`);\n warn(\n ' Or re-run with --uninstall-prev to do it now. Rollback: reinstall via the previous manager.',\n );\n } else if (plan.prevUninstallCmd && opts.uninstallPrev === true) {\n const { code } = await runManagerCmd(plan.prevUninstallCmd.split(' '), { env: process.env });\n if (code === 0) success('Previous install removed.');\n else warn('Previous install NOT removed (non-zero exit). You can remove it manually.');\n }\n\n // Under --json: emit the canonical {ok:true,data} success envelope on stdout.\n if (opts.json === true) {\n process.stdout.write(\n `${JSON.stringify({ ok: true, data: { version: result.version, runtimeSource: result.runtimeSource } })}\\n`,\n );\n return;\n }\n\n info(`Installed native: ${result.version}.`);\n // Claude Code #41806/#27910 mitigation: the old npm bin may still resolve\n // first in $PATH after migration (the shell hashes command paths, and a\n // stale global install can shadow ~/.noir/bin). Hint the user to refresh.\n info('If `noir` still points at the old install, run: hash -r && which -a noir');\n success(`noir ${result.version} installed via native. Run \\`noir doctor\\` to verify.`);\n}\n","// Centralized output + exit-code infrastructure for `@noir-ai/cli`.\n//\n// Stream discipline: machine-readable data → stdout; every other\n// diagnostic (progress, warnings, errors, tables, help) → stderr. Under `--json`\n// the decorated stderr helpers are silenced so stdout stays pristine JSON, and\n// `fail()` emits a structured `{ok:false,error}` envelope to stdout instead.\n//\n// Decoration (picocolors / cli-table3 / ora) auto-disables under `--json`,\n// `--quiet`, `CI`, `NO_COLOR`, or a non-TTY, so the same code path is safe in an\n// interactive shell, a pipe, and CI. The `--quiet` half is enforced by the\n// colour authority itself: bin.ts's `preAction` bridges the flag to `NOIR_QUIET`,\n// which `theme.useColor()` reads, so a quiet run emits no ANSI even where a\n// string is coloured without consulting this module's `opts`.\n//\n// Exit codes: 0 ok · 1 error · 2 usage · 3 not-found · 4 daemon-down ·\n// 5 cancelled · 130/143 interrupted (128 + the signal that stopped the run:\n// SIGINT and SIGTERM). The first six are the `EXIT` enum below; the interrupt\n// codes come from the run's own stop path, not from `fail()`'s contract. These\n// constants live HERE (the single source of truth); bin.ts re-exports them so\n// existing imports from `./bin.js` keep working.\n\nimport Table from 'cli-table3';\nimport { CommanderError } from 'commander';\nimport ora, { type Ora } from 'ora';\nimport { c, isCiEnv, terminalWidth } from './theme.js';\nimport { displayWidth, truncateMiddle, truncateToWidth } from './width.js';\n\n// Exit-code contract + error types\nexport const EXIT = {\n OK: 0,\n ERROR: 1,\n USAGE: 2,\n NOT_FOUND: 3,\n DAEMON_DOWN: 4,\n CANCELLED: 5,\n} as const;\n\nexport type ExitCode = (typeof EXIT)[keyof typeof EXIT];\n\n/**\n * Application-level CLI error carrying a contracted exit code. Thrown directly by\n * bin.ts for legacy usage paths; `fail()` is the ergonomic throw helper.\n * `inferExitCode` / `handleError` map it onto `process.exitCode`.\n */\nexport class NoirCliError extends Error {\n readonly exitCode: number;\n constructor(exitCode: number, message: string) {\n super(message);\n this.name = 'NoirCliError';\n this.exitCode = exitCode;\n }\n}\n\n/**\n * The `code` stamped onto every CommanderError our own `fail()` throws, so\n * `inferExitCode` can tell \"we threw this on purpose with an authoritative\n * exitCode\" apart from \"commander threw this for help/usage/unknown-command\"\n * (which must be remapped onto the CLI exit-code contract by `commanderExitCode`).\n */\nconst NOIR_ERROR_CODE = 'noir.error';\n\n// Global-option shape. Commander stores `--no-input` under the attribute\n// `input` (default `true`; the flag sets it `false`) — see commander's\n// `Option.attributeName()` which strips the leading `no-`. Both `input`\n// (commander's real key) and `noInput` (the spec's intent) are accepted so\n// callers may pass either commander's raw globals or a hand-built object.\nexport interface CliOptions {\n readonly json?: boolean;\n readonly quiet?: boolean;\n readonly verbose?: boolean;\n /** Commander's storage for `--no-input` (`false` ⇒ no input). */\n readonly input?: boolean;\n /** Convenience alias matching the `--no-input` intent (`true` ⇒ no input). */\n readonly noInput?: boolean;\n /** TUI-policy flag. `false` (`--no-tui`) forces bare `noir` onto the\n * non-interactive `status` path even in a TTY; `true` (`--tui`) is an\n * advisory hint that still requires a TTY; absent (auto) defers to\n * {@link isInteractive}. Advisory only — never hard-gates a subcommand. */\n readonly tui?: boolean;\n /** Hint-suppression flag (`--no-tips`). When `true`, redirect /\n * deprecation hints (and any other {@link tip} output) are silenced for\n * CI / log-friendly runs. */\n readonly noTips?: boolean;\n}\n\nfunction isJsonMode(opts: CliOptions): boolean {\n return opts.json === true;\n}\n\nfunction isQuietMode(opts: CliOptions): boolean {\n return opts.quiet === true;\n}\n\n/** True when the user has asked for no interactive input (either spelling). */\nexport function isNoInput(opts: CliOptions = {}): boolean {\n return opts.noInput === true || opts.input === false;\n}\n\nfunction envFlagSet(name: string): boolean {\n // NO_COLOR spec: present AND non-empty (regardless of value) disables color.\n const v = process.env[name];\n return v !== undefined && v !== '';\n}\n\n// `isCiEnv` is imported from ./theme.js (the color authority) — see note there.\n\n// Interactivity gate. Drives both the @clack home menu and the\n// decoration of every helper below. Requires BOTH stdin and stdout to be TTYs:\n// @clack reads keypresses from stdin while ora/picocolors render to stdout.\nexport function isInteractive(opts: CliOptions = {}): boolean {\n if (isJsonMode(opts) || isNoInput(opts)) return false;\n if (isCiEnv() || envFlagSet('NO_COLOR')) return false;\n return process.stdin.isTTY === true && process.stdout.isTTY === true;\n}\n\n/**\n * Derive the explicit `interactive` flag the scaffold engine reads (via\n * {@link ScaffoldOptions.interactive}) from the bridge bin.ts owns. The bin's\n * `preAction` sets `NOIR_NON_INTERACTIVE=1` under `--json`/`--no-input`; this\n * helper folds that with the {@link isInteractive} TTY/CI/NO_COLOR gate so a\n * single boolean flows into the engine + `buildConflictOpts`. The ENGINE itself\n * never reads `process.env` for interactivity — only this CLI helper does.\n */\nexport function resolveInteractive(): boolean {\n if (flaggedNonInteractiveCli()) return false;\n return isInteractive();\n}\n\n/** True when the bin's `preAction` flagged this invocation non-interactive\n * (`--json` / `--no-input` → `NOIR_NON_INTERACTIVE`). CLI-internal; the engine\n * reads {@link ScaffoldOptions.interactive} instead. */\nfunction flaggedNonInteractiveCli(): boolean {\n const v = process.env.NOIR_NON_INTERACTIVE;\n return v !== undefined && v !== '';\n}\n\n// Data output (stdout). The ONLY helper here that writes to stdout; under\n// `--json` a command calls this exactly once with its full payload and never\n// mixes other stdout writes, keeping machine output pristine.\nexport function json(obj: unknown): void {\n process.stdout.write(`${JSON.stringify(obj)}\\n`);\n}\n\n// Human diagnostics (stderr). picocolors auto-strips ANSI under NO_COLOR /\n// non-TTY, and each helper additionally short-circuits under `--json`\n// (decoration off, payload is the output) — and `info`/`log`/`success` also\n// under `--quiet`. `warn`/`error` survive `--quiet` (they carry signal).\nexport function log(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${msg}\\n`);\n}\n\nexport function info(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${c.info(msg)}\\n`);\n}\n\nexport function success(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || isQuietMode(opts)) return;\n process.stderr.write(`${c.ok(msg)}\\n`);\n}\n\nexport function warn(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts)) return;\n process.stderr.write(`${c.warn(msg)}\\n`);\n}\n\nexport function error(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts)) return;\n process.stderr.write(`${c.error(msg)}\\n`);\n}\n\n/**\n * Redirect / deprecation hint (stderr). Silenced under `--json` (a CI\n * consumer's stdout envelope must stay pristine) AND under `--no-tips`\n * ({@link CliOptions.noTips}). This is the ONLY helper deprecation / redirect\n * notices should use, so a single `--no-tips` flag quiets them all in CI / logs.\n * Style matches {@link warn} (these are advisory, not errors).\n */\nexport function tip(msg: string, opts: CliOptions = {}): void {\n if (isJsonMode(opts) || opts.noTips === true) return;\n process.stderr.write(`${c.warn(msg)}\\n`);\n}\n\n// Tables (stderr). Suppressed entirely under `--json` — the command has\n// already emitted the rows as a JSON array via `json()`.\n//\n// Design: cli-table3 styles headers/borders via `@colors/colors`\n// (whose NO_COLOR/TTY semantics differ from picocolors), which painted EVERY\n// header red while the body stripped — RED is now reserved strictly for ERROR.\n// We bypass `@colors/colors` entirely by passing EMPTY `style.head`/`style.border`\n// arrays (cli-table3's `wrapWithStyleColors` early-returns on length 0) and\n// PRE-COLOR the header strings ourselves via `theme.c` (picocolors). Result:\n// the header, border, and body all strip consistently under NO_COLOR / non-TTY,\n// because picocolors is the SOLE color authority. Responsive `colWidths` are\n// measured in DISPLAY columns (see `./width.js`) so a coloured or wide cell is\n// allocated the space it actually draws, and cells too long for their column are\n// cut by that same module rather than by cli-table3's code-unit arithmetic.\n// `wordWrap` keeps free-text cells readable; `truncate: '…'` is the final\n// backstop for a long token inside a wrapped cell.\nexport function table(\n rows: readonly Record<string, unknown>[],\n cols: readonly string[],\n opts: CliOptions = {},\n): void {\n if (isJsonMode(opts)) return;\n if (rows.length === 0) {\n info('(no rows)', opts);\n return;\n }\n const widths = computeColWidths(rows, cols);\n const t = new Table({\n head: cols.map((col) => c.bold(c.info(col))),\n // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the\n // default red header nor a colored border). All color is ours, via theme.\n style: { head: [], border: [] },\n colWidths: widths,\n wordWrap: true,\n truncate: '…',\n });\n for (const row of rows) {\n t.push(cols.map((col, i) => truncateCell(row[col], (widths[i] ?? 0) - 2)));\n }\n process.stderr.write(`${t.toString()}\\n`);\n}\n\n/**\n * The narrowest a column's content area may normally be, in display columns.\n * Below this a cell is nothing but its ellipsis, so it is only reached by the\n * final overflow pass, never by the ordinary greedy trim.\n */\nconst MIN_CONTENT_WIDTH = 3;\n\n/**\n * Compute per-column widths (cli-table3 `colWidths`, which INCLUDE each column's\n * 2 padding chars) that fit `terminalWidth()`. Strategy:\n *\n * 1. Measure each column's NATURAL content width in DISPLAY columns (longest\n * cell, at least a small minimum content width).\n * 2. If the natural total fits, use it as-is — every cell renders whole.\n * 3. Otherwise GREEDILY TRIM ONLY THE WIDEST column, down to its own header\n * length (never below the minimum content width, and never held open above\n * the content budget), until the row fits. A long header can therefore not\n * pin a column open (the header wraps), while narrow columns (paths, ids,\n * statuses) still give up as little as possible: they are single tokens\n * that can't word-wrap, so truncating them loses information.\n * 4. When every column is already at that floor and the row still does not\n * fit (many columns, so the per-column padding alone exhausts the width),\n * make one final bounded pass that shrinks the widest columns below the\n * floor. The cells those columns hold are then drawn with a visible\n * ellipsis, so the row stays inside the terminal instead of overflowing.\n *\n * Widths are measured through `displayWidth` (ANSI stripped, wide glyphs count 2)\n * so a coloured badge or a CJK character does not inflate the column past what it\n * draws. cli-table3 accepts only positive integers (negatives collapse). Each\n * returned width is `content + 2` to reserve the column's padding, so the\n * content area equals the measured width and the math against `terminalWidth()`\n * is exact.\n */\nfunction computeColWidths(\n rows: readonly Record<string, unknown>[],\n cols: readonly string[],\n): number[] {\n const n = cols.length;\n if (n === 0) return [];\n const headerLen = cols.map((col) => displayWidth(col));\n const natural = cols.map((col, i) => {\n let max = Math.max(headerLen[i] ?? displayWidth(col), MIN_CONTENT_WIDTH);\n for (const row of rows) {\n const len = displayWidth(formatCell(row[col]));\n if (len > max) max = len;\n }\n return max;\n });\n // cli-table3 row overhead: per column 1 left border + 2 padding, plus a final\n // right border ⇒ 3n + 1 non-content chars. `budget` is what remains for the\n // sum of column CONTENT areas, and it is the ONLY budget this function uses:\n // every \"does it fit\" test below — the natural-width fast path included —\n // compares against it, so no branch can return a row wider than the terminal.\n //\n // Floored at `n` (one content column per column) deliberately. A narrower\n // budget would be a lie twice over: cli-table3 accepts only POSITIVE integer\n // widths, so a zero or negative column collapses rather than shrinking; and\n // whenever `4n + 1 > terminalWidth()` the row is unachievable anyway — the\n // borders and padding alone no longer fit, so no arrangement of the content\n // can save it. The floor keeps the trim from driving a column to zero on such\n // a terminal and gives the final pass a natural stopping point (all columns\n // one content column wide). Do not \"fix\" it away as redundant.\n const budget = Math.max(n, terminalWidth() - 3 * n - 1);\n const naturalSum = natural.reduce((a, b) => a + b, 0);\n if (naturalSum <= budget) {\n return natural.map((w) => w + 2);\n }\n // Overflow: greedily cut the widest reducible column down to its floor.\n const content = natural.slice();\n let sum = naturalSum;\n // A column's floor is its own header — so a table that can fit its headers\n // keeps every one of them on a single line — capped at the whole budget,\n // because a header wider than that can never be honoured anyway. Without the\n // cap, one very long header would pin its column open and push the row past\n // the terminal.\n const floor = headerLen.map((h) => Math.min(Math.max(h, MIN_CONTENT_WIDTH), budget));\n while (sum > budget) {\n let idx = -1;\n let widest = -1;\n for (let i = 0; i < n; i++) {\n const c = content[i] ?? 0;\n const f = floor[i] ?? MIN_CONTENT_WIDTH;\n if (c > f && c > widest) {\n widest = c;\n idx = i;\n }\n }\n if (idx === -1) break; // every column is at its floor — stop\n const overshoot = sum - budget;\n const reducible = (content[idx] ?? 0) - (floor[idx] ?? MIN_CONTENT_WIDTH);\n const cut = Math.min(overshoot, reducible);\n content[idx] = (content[idx] ?? 0) - cut;\n sum -= cut;\n }\n // Final bounded pass: when the columns are all at their floor yet the row\n // still will not fit, shrink the widest columns below that floor (down to a\n // single content column each) so the cells ellipsise and the row fits. The\n // pass visits each column at most once, so it always terminates.\n if (sum > budget) {\n for (let pass = 0; pass < n && sum > budget; pass++) {\n let idx = -1;\n let widest = 0;\n for (let j = 0; j < n; j++) {\n const c = content[j] ?? 0;\n if (c > 1 && c > widest) {\n widest = c;\n idx = j;\n }\n }\n if (idx === -1) break; // every column is a single column wide — give up\n const cut = Math.min(sum - budget, (content[idx] ?? 1) - 1);\n content[idx] = (content[idx] ?? 1) - cut;\n sum -= cut;\n }\n }\n return content.map((w) => w + 2);\n}\n\n/**\n * True for a value shaped like a filesystem path. Paths legitimately contain\n * whitespace (a directory named \"My Projects\"), so this is a SHAPE test rather\n * than a single-token test: the value must carry a separator AND either start\n * like a path — a leading `/`, `./`, `../`, `~/`, or a Windows drive prefix such\n * as `C:\\` — or end in a file extension (`…/store.db`). A sentence that merely\n * contains a slash (\"the spec/plan docs\") matches neither, so it is still left\n * to `wordWrap` instead of being cut.\n */\nfunction isPathToken(s: string): boolean {\n if (!/[\\\\/]/.test(s)) return false;\n return /^(\\/|\\.\\/|\\.\\.\\/|~\\/|[A-Za-z]:[\\\\/])/.test(s) || /\\.[A-Za-z0-9][A-Za-z0-9_-]*$/.test(s);\n}\n\n/**\n * A cell value cut to fit its column's content area.\n *\n * Only a cell that OVERFLOWS is touched, and the untouched path returns the\n * value byte-for-byte — which is what keeps a coloured cell coloured, since the\n * truncators strip ANSI and return plain text. A path is cut in the MIDDLE, so\n * the leading directories and the final file name both survive; it is tested\n * first because a path can contain spaces and would otherwise be mistaken for\n * free text. A non-path multi-word cell is left entirely to `wordWrap`, and a\n * single-token cell — which cannot wrap — is cut at the tail. Every cut is\n * measured in display columns and never splits a grapheme cluster.\n */\nfunction truncateCell(value: unknown, contentWidth: number): string {\n const s = formatCell(value);\n if (contentWidth <= 0 || displayWidth(s) <= contentWidth) return s;\n if (isPathToken(s)) return truncateMiddle(s, contentWidth);\n if (/\\s/.test(s)) return s; // multi-word free text: `wordWrap` folds it\n return truncateToWidth(s, contentWidth);\n}\n\nfunction formatCell(value: unknown): string {\n if (value === undefined || value === null) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n return JSON.stringify(value);\n}\n\n// Definition list (stderr). A two-column Field/Value rendering for snapshot-\n// shaped output (status / task / context-status). `status.ts` hand-rolled this\n// exact shape; this generalizes it. Rendered via the same responsive `table()`,\n// so it inherits the clean (non-red) header + NO_COLOR behavior automatically.\nexport interface DefinitionRow {\n /** Field label (left column). */\n label: string;\n /** Field value; passed through unchanged so the table formats it exactly once. */\n value: unknown;\n}\n\nexport function definitionList(rows: readonly DefinitionRow[], opts: CliOptions = {}): void {\n table(\n rows.map((r) => ({ Field: r.label, Value: r.value })),\n ['Field', 'Value'],\n opts,\n );\n}\n\n// Spinner (stderr). ora when interactive; a no-op otherwise so scriptable /\n// CI / `--json` / `--quiet` runs pay nothing and never animate a pipe.\nexport interface Spinner {\n start(text?: string): Spinner;\n stop(): Spinner;\n clear(): Spinner;\n succeed(text?: string): void;\n fail(text?: string): void;\n warn(text?: string): void;\n info(text?: string): void;\n text: string;\n}\n\nclass NoopSpinner implements Spinner {\n text = '';\n start(_text?: string): Spinner {\n return this;\n }\n stop(): Spinner {\n return this;\n }\n clear(): Spinner {\n return this;\n }\n succeed(_text?: string): void {}\n fail(_text?: string): void {}\n warn(_text?: string): void {}\n info(_text?: string): void {}\n}\n\n/** Wraps an `ora` instance so the `Spinner` interface stays decoupled from ora's exact type. */\nclass OraSpinner implements Spinner {\n private readonly handle: Ora;\n\n constructor(initialText: string) {\n this.handle = ora(initialText);\n }\n\n get text(): string {\n return this.handle.text;\n }\n set text(value: string) {\n this.handle.text = value;\n }\n\n start(text?: string): Spinner {\n if (text !== undefined) this.handle.text = text;\n this.handle.start();\n return this;\n }\n\n stop(): Spinner {\n this.handle.stop();\n return this;\n }\n\n clear(): Spinner {\n this.handle.clear();\n return this;\n }\n\n succeed(text?: string): void {\n this.handle.succeed(text);\n }\n\n fail(text?: string): void {\n this.handle.fail(text);\n }\n\n warn(text?: string): void {\n this.handle.warn(text);\n }\n\n info(text?: string): void {\n this.handle.info(text);\n }\n}\n\nexport function spinner(text = '', opts: CliOptions = {}): Spinner {\n if (isJsonMode(opts) || isQuietMode(opts) || !isInteractive(opts)) {\n return new NoopSpinner();\n }\n return new OraSpinner(text);\n}\n\n// Failure + exit-code mapping\n\n/**\n * The one-line `{ok:false,error}` envelope every `--json` failure writes. Built\n * in one place so a failure written by `fail()` and one written on the way out\n * of a signal handler cannot drift apart.\n */\nfunction failureEnvelope(exitCode: number, message: string): string {\n return `${JSON.stringify({ ok: false, error: { code: exitCode, message } })}\\n`;\n}\n\n/**\n * Write a diagnostic and throw a `CommanderError` carrying the given exit code,\n * so bin.ts's `exitOverride` + `handleError` surface it as `process.exitCode`\n * without any mid-action `process.exit`. Under `--json` the message becomes a\n * structured `{ok:false,error}` envelope on STDOUT; otherwise plain text on\n * STDERR. Always throws (`never`).\n */\nexport function fail(exitCode: number, message: string, opts: CliOptions = {}): never {\n if (isJsonMode(opts)) {\n process.stdout.write(failureEnvelope(exitCode, message));\n } else if (message.length > 0) {\n process.stderr.write(`${message}\\n`);\n }\n throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);\n}\n\n/** How long a leaving path waits for its own last line to land before it goes. */\nconst EXIT_FLUSH_MS = 250;\n\n/**\n * {@link fail} for the paths that leave from a signal handler, where there is no\n * caller left to catch what it throws: the verdict is written and the exit\n * follows it, with the same shape and the same exit code an ordinary failure\n * would have produced. Under `--json` the envelope is the run's last output, so\n * the exit waits for the write to land — a process that exits with stdout still\n * queued loses it — and goes without it if the write never lands, because the\n * whole point of this path is to leave.\n */\nexport function failAndExit(exitCode: number, message: string, opts: CliOptions = {}): void {\n // The code this process leaves with, even if the write below never lands.\n process.exitCode = exitCode;\n const jsonMode = isJsonMode(opts);\n const text = jsonMode\n ? failureEnvelope(exitCode, message)\n : message.length > 0\n ? `${message}\\n`\n : '';\n if (text.length === 0) {\n process.exit(exitCode);\n }\n let left = false;\n const leave = (): void => {\n if (left) return;\n left = true;\n process.exit(exitCode);\n };\n (jsonMode ? process.stdout : process.stderr).write(text, leave);\n setTimeout(leave, EXIT_FLUSH_MS);\n}\n\n/**\n * Guard for commands that would otherwise block on a prompt. Honors the\n * hard rule: \"no blocking prompts when !isTTY or --no-input — error exit 2\n * naming the missing flag instead\". `hint` describes the operation so the\n * message points the user at the flag-based alternative.\n */\nexport function requireInteractive(opts: CliOptions, hint: string): void {\n if (!isInteractive(opts)) {\n fail(\n EXIT.USAGE,\n `${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.`,\n opts,\n );\n }\n}\n\n// Remap commander's own exit codes onto the CLI exit-code contract:\n// help/version → 0 · unknown command → 3 · every other usage error → 2\nfunction commanderExitCode(err: CommanderError): number {\n switch (err.code) {\n case 'commander.helpDisplayed':\n case 'commander.versionDisplayed':\n case 'commander.version': // commander v12 uses this code for --version (exit 0)\n return EXIT.OK;\n case 'commander.unknownCommand':\n return EXIT.NOT_FOUND;\n default:\n return EXIT.USAGE;\n }\n}\n\n/** Map any thrown value onto the CLI exit-code contract. */\nexport function inferExitCode(err: unknown): number {\n if (err instanceof NoirCliError) return err.exitCode;\n if (err instanceof CommanderError) {\n // Errors our own `fail()` threw carry a `noir.*` code and an authoritative\n // exitCode — respect it. Commander's own errors are remapped by `code`.\n if (err.code.startsWith('noir.')) return err.exitCode;\n return commanderExitCode(err);\n }\n return EXIT.ERROR;\n}\n\n/**\n * Map a thrown error onto stderr + `process.exitCode`. Never throws, never\n * calls `process.exit` (commander's `exitOverride` already prevented that for\n * commander's own errors). Used by the bin entry's `run()`.\n */\nexport function handleError(err: unknown): void {\n if (err instanceof NoirCliError) {\n if (err.message.length > 0) process.stderr.write(`${err.message}\\n`);\n } else if (err instanceof CommanderError) {\n // If `fail()` threw it, the message is already written (stderr or the JSON\n // stdout envelope); if commander threw it, `configureOutput.writeErr`\n // already wrote it during parse. Don't double-write either way.\n } else {\n process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = inferExitCode(err);\n}\n","// Single owner of the Noir CLI's semantic palette + status badges.\n//\n// Built on `picocolors` (the SAME lib the rest of the CLI already uses), so the\n// decoration strips consistently under NO_COLOR / non-TTY. cli-table3 styles\n// via a SECOND lib (@colors/colors) whose NO_COLOR/TTY semantics differ — that\n// split is what painted every table header red while the body stripped. Routing\n// ALL color through this module (the `c` object + `badge()`) closes that leak:\n// there is exactly one color authority, and it honors NO_COLOR / CLICOLOR_FORCE\n// / `--quiet` / TTY uniformly.\n//\n// Accessibility:\n// - `badge()` ALWAYS returns SYMBOL + TEXT LABEL (e.g. `⚠ warn`), so NO_COLOR\n// and colorblind users get identical information. Color is decorative.\n// - `NOIR_ACCESSIBLE` disables the banner gradient (→ solid accent) and is the\n// documented opt-in for users who want the most legible form; badges already\n// carry symbol+text unconditionally, so the flag is a no-op for them.\n\nimport pc from 'picocolors';\nimport { truncateToWidth } from './width.js';\n\n// A private always-on colorizer. The PUBLIC `c` object gates every call through\n// `useColor()`, so `useColor()` is the single live authority over whether ANY\n// ANSI leaves this module. (The default `picocolors` export captures its\n// `isColorSupported` snapshot once at module load and does NOT honor\n// CLICOLOR_FORCE; gating a forced instance here fixes both.)\nconst FORCE = pc.createColors(true);\n\n/** Badge state → semantic color role. */\nexport type BadgeState = 'ok' | 'warn' | 'error' | 'info';\n\n/**\n * The semantic palette. Each method returns its input unchanged when color is\n * off (NO_COLOR / `--quiet` / non-TTY / not forced), so callers can wrap freely\n * without guarding. Compose with `c.bold(c.info(s))` etc.\n */\nexport const c = {\n /** Green — success / healthy. */\n ok: (s: string): string => (useColor() ? FORCE.green(s) : s),\n /** Yellow — degraded but usable. */\n warn: (s: string): string => (useColor() ? FORCE.yellow(s) : s),\n /** Red — reserved strictly for ERROR. Never a table header. */\n error: (s: string): string => (useColor() ? FORCE.red(s) : s),\n /** Cyan — informational / brand-neutral accent for labels. */\n info: (s: string): string => (useColor() ? FORCE.cyan(s) : s),\n /** Blue — the Midnight Cobalt brand role (banner / accent). */\n accent: (s: string): string => (useColor() ? FORCE.blue(s) : s),\n /** Dim — secondary text, table borders when color is on. */\n dim: (s: string): string => (useColor() ? FORCE.dim(s) : s),\n /** Bold — emphasis (table headers compose this with `info`). */\n bold: (s: string): string => (useColor() ? FORCE.bold(s) : s),\n /** Reverse video (SGR 7) — inverted background for the active list row. */\n inverse: (s: string): string => (useColor() ? FORCE.inverse(s) : s),\n};\n\n// @clack-style status symbols (the vocabulary Noir inherits via @clack/prompts\n// + ora). Paired 1:1 with a state name so a screen reader / pipe / colorblind\n// user reads the same fact a sighted interactive user sees.\nconst BADGE_SYMBOL: Readonly<Record<BadgeState, string>> = {\n ok: '✓',\n warn: '⚠',\n error: '✗',\n info: 'ℹ',\n};\n\n/**\n * Render a status badge: always `SYMBOL LABEL`, colored by `state` when color\n * is on. The TEXT LABEL is unconditional — this is the accessibility invariant.\n * `label` defaults to the state name, so `badge('ok')` reads `✓ ok`.\n *\n * `maxWidth` bounds the badge to that many terminal columns — pass it when the\n * badge goes into a column of a known width (a table cell). The text is cut\n * BEFORE the color is applied, because the truncators strip ANSI escapes and\n * return plain text; colouring first would leave the badge uncoloured or, worse,\n * with its colour codes cut in half.\n *\n * @example badge('ok') // → green \"✓ ok\"\n * @example badge('warn','degraded') // → yellow \"⚠ degraded\"\n * @example badge('error','down') // → red \"✗ down\"\n * @example badge('warn','degraded',12) // → yellow \"⚠ degraded\", ≤ 12 columns\n */\nexport function badge(state: BadgeState, label: string = state, maxWidth?: number): string {\n const full = `${BADGE_SYMBOL[state]} ${label}`;\n const text = maxWidth === undefined ? full : truncateToWidth(full, maxWidth);\n switch (state) {\n case 'ok':\n return c.ok(text);\n case 'warn':\n return c.warn(text);\n case 'error':\n return c.error(text);\n case 'info':\n return c.info(text);\n }\n}\n\n// Environment gates. Evaluated LIVE (not captured at module load) so tests that\n// flip NO_COLOR / CLICOLOR_FORCE between cases see the right answer, and so a\n// user exporting NO_COLOR mid-session is honored on the next render.\n\nfunction envFlagSet(name: string): boolean {\n // NO_COLOR spec: present AND non-empty disables color (any value).\n const v = process.env[name];\n return v !== undefined && v !== '';\n}\n\n/**\n * True when running under a CI runner (GitHub Actions / GitLab / CircleCI /\n * Drone set `CI=true` at the conventional value). Treats the explicit opt-outs\n * `0` / `false` as \"not CI\" so a user can force-disable the guard. Owned here\n * (the color authority) and re-exported so output.ts doesn't duplicate it.\n */\nexport function isCiEnv(): boolean {\n const v = process.env.CI;\n if (v === undefined || v === '') return false;\n return v !== '0' && v !== 'false';\n}\n\n/**\n * `NOIR_QUIET`: the value the CLI's own `preAction` writes for a `--quiet`\n * invocation, so this module — which never sees the parsed command options —\n * reads the same quiet signal the output helpers take from their `opts`. Read\n * live like every other gate here. The CLI owns the variable: it sets it to\n * `1` for a quiet run and deletes whatever value it found otherwise, so a\n * hand-exported one has no effect (the same \"output, not input\" rule as\n * `NOIR_NON_INTERACTIVE`).\n */\nfunction quietMode(): boolean {\n const v = process.env.NOIR_QUIET;\n return v !== undefined && v !== '';\n}\n\n/**\n * The single authority on whether decoration should emit ANSI. Honors:\n * - `NO_COLOR` → always off (spec: present + non-empty).\n * - `NOIR_QUIET` (`--quiet`) → always off. The documented contract says\n * decoration auto-disables under `--quiet`, so a\n * quiet run wins even over `CLICOLOR_FORCE` (which\n * remains the escape hatch for a run that did NOT\n * ask for quiet).\n * - `CLICOLOR_FORCE=1` → always on (forces color on a redirected stream).\n * - `CI=true` (alone) → always OFF. picocolors' `isColorSupported`\n * snapshot turns color ON under `!!env.CI` (it\n * assumes a CI viewer that renders ANSI). But ANSI\n * escapes inflate the raw byte length of rendered\n * lines, which breaks the responsive-width\n * guarantee `max(line.length) <= terminalWidth()`\n * that `table()` exists to uphold — and the\n * regression test that locks it (fits 60/80/120 in\n * theme.test.ts). The CLI's own top-of-file\n * contract states decoration auto-disables under\n * CI / non-TTY; this honors that. A CI viewer that\n * wants color sets `CLICOLOR_FORCE=1` (above).\n * - otherwise → picocolors' detection (TTY / FORCE_COLOR).\n */\nexport function useColor(): boolean {\n if (envFlagSet('NO_COLOR')) return false;\n if (quietMode()) return false;\n if (process.env.CLICOLOR_FORCE === '1') return true;\n if (isCiEnv()) return false;\n return pc.isColorSupported;\n}\n\n/**\n * `NOIR_ACCESSIBLE`: opt-in maximum legibility. Badges already carry symbol+\n * text unconditionally (so the flag is a no-op for them); the material effect\n * is disabling the banner gradient (→ solid accent) and is reserved here for\n * future accessible-display tweaks.\n */\nexport function accessibleMode(): boolean {\n return envFlagSet('NOIR_ACCESSIBLE');\n}\n\n/**\n * Terminal width in columns. Honors `COLUMNS` (the conventional override), then\n * the width of the stream the CLI's tables are written to (stderr), falling\n * back to stdout and finally to 80 (the same default `picocolors` /\n * `cli-table3` assume). Floored at a small minimum so a misconfigured shell\n * never produces a degenerate zero/negative-width table.\n */\nexport function terminalWidth(): number {\n const fromEnv = process.env.COLUMNS;\n if (fromEnv !== undefined && fromEnv !== '') {\n const n = Number(fromEnv);\n if (Number.isInteger(n) && n > 0) return Math.max(n, 20);\n }\n const stderrCols = process.stderr.columns;\n if (typeof stderrCols === 'number' && stderrCols > 0) return Math.max(stderrCols, 20);\n const stdoutCols = process.stdout.columns;\n if (typeof stdoutCols === 'number' && stdoutCols > 0) return Math.max(stdoutCols, 20);\n return 80;\n}\n\n/**\n * Usable content width inside a bordered TUI panel. Ink's `borderStyle` adds a\n * 1-char border on each side, and `paddingX={1}` adds 1 char inside that — so a\n * full-width rounded panel has `terminalWidth - 4` columns of usable text.\n * Components that truncate long lines (OutputPane) MUST use this, not\n * {@link terminalWidth}, or content overflows the border.\n *\n * @param border chars consumed by the border (default 2 = left + right).\n * @param padding chars consumed by inner padding (default 2 = left + right).\n */\nexport function contentWidth(border = 2, padding = 2): number {\n return Math.max(20, terminalWidth() - border - padding);\n}\n\n/**\n * A horizontal divider line of `─` sized for the current content width. Used\n * inside bordered panels to separate regions (e.g. status bar ↔ output ↔ input)\n * without nesting Ink borders. Always dim so it reads as structure, not content.\n *\n * `width` defaults to a full-width panel's content width. A divider drawn inside\n * a nested padded row must pass that row's budget instead: a dash run longer\n * than the box it sits in is wrapped by Ink, which leaves a stub of dashes on a\n * line of its own.\n */\nexport function divider(width = contentWidth()): string {\n return c.dim('─'.repeat(Math.max(1, width)));\n}\n","// The single authority for how wide text will actually draw in the terminal.\n//\n// `String.length` is a UTF-16 code-unit count, which is the wrong unit for\n// layout: an ANSI colour escape measures as visible even though it draws\n// nothing, an emoji is a surrogate pair yet occupies two columns, and a wide\n// CJK glyph occupies two columns while counting as one. Every call site that\n// lays text out in columns must measure through here so the answer is the\n// number of terminal columns the text will occupy, not the number of code\n// units it happens to be made of.\nimport stringWidth from 'string-width';\nimport stripAnsi from 'strip-ansi';\n\n/** Terminal grapheme clusters, in order — the unit a renderer draws whole. */\nconst segmenter = new Intl.Segmenter();\n\nfunction clusters(s: string): string[] {\n return [...segmenter.segment(s)].map(({ segment }) => segment);\n}\n\n/**\n * The number of terminal columns `s` occupies when drawn. ANSI escape\n * sequences are not counted; wide and emoji glyphs count as two columns.\n */\nexport function displayWidth(s: string): number {\n return stringWidth(s);\n}\n\n/**\n * How many of the leading `parts` fit within `width` columns. Each part is\n * charged its whole-cluster width, so a cluster is kept or dropped whole and\n * a surrogate pair can never be split.\n */\nfunction fitLeading(parts: readonly string[], width: number): number {\n let used = 0;\n let count = 0;\n\n for (const part of parts) {\n const w = displayWidth(part);\n if (used + w > width) {\n break;\n }\n used += w;\n count++;\n }\n\n return count;\n}\n\n/**\n * `s` cut to at most `width` columns, with `ellipsis` marking the cut. The\n * marker is charged against the budget, so the result never exceeds `width`.\n * A marker wider than the budget is itself trimmed rather than allowed to\n * overflow.\n *\n * The result is plain text: ANSI colour escapes are stripped before measuring\n * and are not carried into the output, so the caller must re-apply colour\n * after truncating if it is wanted.\n */\nexport function truncateToWidth(s: string, width: number, ellipsis = '…'): string {\n const plain = stripAnsi(s);\n\n if (width <= 0) {\n return '';\n }\n if (displayWidth(plain) <= width) {\n return plain;\n }\n\n const markerParts = clusters(ellipsis);\n const marker = markerParts.slice(0, fitLeading(markerParts, width)).join('');\n const budget = width - displayWidth(marker);\n\n const parts = clusters(plain);\n return parts.slice(0, fitLeading(parts, budget)).join('') + marker;\n}\n\n/**\n * `s` cut to at most `width` columns by removing the middle, so both the\n * leading and trailing text survive — used for long paths where the start\n * (directory) and end (file name) are the parts that identify it.\n *\n * Like `truncateToWidth`, the result is plain text with ANSI escapes\n * stripped; the caller re-applies colour after truncating.\n */\nexport function truncateMiddle(s: string, width: number, ellipsis = '…'): string {\n const plain = stripAnsi(s);\n\n if (width <= 0) {\n return '';\n }\n if (displayWidth(plain) <= width) {\n return plain;\n }\n\n const markerParts = clusters(ellipsis);\n const marker = markerParts.slice(0, fitLeading(markerParts, width)).join('');\n const remaining = width - displayWidth(marker);\n const headBudget = Math.ceil(remaining / 2);\n const tailBudget = remaining - headBudget;\n\n const parts = clusters(plain);\n const headCount = fitLeading(parts, headBudget);\n\n // The tail is drawn from the clusters the head left behind, walking\n // backwards, so the two halves can never claim the same text twice.\n const tail: string[] = [];\n let tailUsed = 0;\n for (const part of parts.slice(headCount).reverse()) {\n const w = displayWidth(part);\n if (tailUsed + w > tailBudget) {\n break;\n }\n tail.push(part);\n tailUsed += w;\n }\n\n return parts.slice(0, headCount).join('') + marker + tail.reverse().join('');\n}\n\n/**\n * `s` right-padded with spaces to exactly `width` columns, measured by\n * display width. Returns `s` untouched when it already fills the width.\n */\nexport function padToWidth(s: string, width: number): string {\n const pad = width - displayWidth(s);\n\n return pad > 0 ? s + ' '.repeat(pad) : s;\n}\n"],"mappings":"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","names":["envFlagSet","c","contentWidth","detected"]}
@@ -3,12 +3,13 @@ import {
3
3
  buildConflictOpts,
4
4
  checkWritePathDedup,
5
5
  preservedStaleLine,
6
+ reportEnvHeal,
6
7
  reportPlannedWrites
7
- } from "./chunk-5KWETEMF.js";
8
- import "./chunk-TPNJEN4B.js";
8
+ } from "./chunk-6AUAIEFG.js";
9
+ import "./chunk-CQU2INJH.js";
9
10
  import {
10
11
  resolveInteractive
11
- } from "./chunk-KETMM3BA.js";
12
+ } from "./chunk-KVJIPKVE.js";
12
13
 
13
14
  // src/commands/create.ts
14
15
  import { resolve } from "path";
@@ -37,6 +38,7 @@ async function create(dir, opts) {
37
38
  reportPlannedWrites(res);
38
39
  return res;
39
40
  }
41
+ reportEnvHeal(res);
40
42
  if (res.noop) return res;
41
43
  const host = res.host;
42
44
  const adapter = resolveAdapter(host);
@@ -81,4 +83,4 @@ async function create(dir, opts) {
81
83
  export {
82
84
  create
83
85
  };
84
- //# sourceMappingURL=create-6KDIFPUL.js.map
86
+ //# sourceMappingURL=create-ZZN7XFLG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/commands/create.ts"],"sourcesContent":["// `noir create [dir]` — greenfield first-run for the AI layer only.\n//\n// New command. Bootstraps Noir's AI layer (.noir/ store, host\n// pointers, skills) into `dir` (created if absent), with NO external\n// scaffolder chaining (the AI layer only; no `pnpm create` wrapping).\n//\n// Reuses init's transport/url handling (including the localhost security\n// gate) by calling into the same engine: `scaffold({mode:'create', root})`.\n// The only difference from `noir init` is the mode tag (which selects the\n// manifest's first-run subset + generates a fresh project id unconditionally)\n// and that the target dir is `resolve(dir ?? cwd)` rather than `process.cwd()`.\n//\n// Multi-host: gains `--host <id>` (default `'claude'`); the host drives\n// both scaffold emission + skills emission via `resolveAdapter(host)` —\n// shared with init through `emitHostSkills`. See `init.ts` for the matrix.\n//\n// Stream discipline matches init: diagnostics → stderr, nothing on stdout\n// (so `noir create --json` is safe by construction — there is no data payload).\n//\n// Exit codes: `assertTransportUrl` is called DIRECTLY so transport/url\n// errors propagate as plain `Error` → bin.ts's handleError → exit 1, EXACTLY\n// matching `noir init` for the same error class. (The CLI exit-code contract\n// lists usage=2 for a missing `--url`; neither `init` nor `create` implements\n// that split today —\n// both report it as a plain error=1 — and the review asked for consistency\n// over a one-sided fix. Changing them together is a separate, contract-level\n// change.) The target dir is created by the engine's `create` mode (the\n// duplicate mkdir that used to live here is gone — one attributable failure\n// point, in scaffold.ts).\n\nimport { resolve } from 'node:path';\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { loadProjectInfo, type ProjectInfo } from '@noir-ai/core';\nimport { type ScaffoldResult, scaffold } from '@noir-ai/create';\nimport { type CompileTarget, emitSkillsToDir } from '@noir-ai/skills';\nimport { buildConflictOpts } from '../conflict.js';\nimport { checkWritePathDedup } from '../dedup-write.js';\nimport {\n assertTransportUrl,\n preservedStaleLine,\n reportEnvHeal,\n reportPlannedWrites,\n} from '../init.js';\nimport { resolveInteractive } from '../output.js';\n\nexport interface CreateOptions {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n /** Target host. Defaults to `'claude'`. */\n host?: HostId;\n /** Re-scaffold even if already initialized. */\n force?: boolean;\n /** `--dry-run`/`--preview` — report the planned writes to stderr\n * without touching disk (incl. NOT creating the target dir). */\n dryRun?: boolean;\n /** Alias for `--dry-run`. Kept on the options bag so direct callers can\n * pass either spelling; the bin collapses both flags before dispatch. */\n preview?: boolean;\n}\n\n/**\n * Bootstrap Noir's AI layer in `dir`. Returns the {@link ScaffoldResult} (with\n * structured `conflicts[]` + any dedup records appended); the bin\n * emits it under `--json`. `undefined` when the already-initialized guard\n * short-circuited (a no-op).\n *\n * @param dir Target directory (created if absent). Defaults to `process.cwd()`.\n * @param opts Transport + URL + host (same semantics as `noir init`).\n */\nexport async function create(\n dir: string | undefined,\n opts: CreateOptions,\n): Promise<ScaffoldResult | undefined> {\n // Call assertTransportUrl directly so the SAME error class yields the SAME\n // exit code as `noir init` (plain Error → exit 1 for both missing-url and\n // non-localhost). The previous NoirCliError(EXIT.USAGE) wrapper made `create`\n // exit 2 where `init` exits 1.\n assertTransportUrl(opts);\n\n const root = resolve(dir ?? process.cwd());\n // Host is left to the engine (explicit `--host` > the project's configured\n // host > `'claude'`) so `create --force` over an existing project keeps its\n // host; `res.host` reports the host actually emitted.\n // Derive interactive once (consistency with init/sync) so both scaffold\n // and skills emission see the same hermetic flag.\n const interactive = resolveInteractive();\n const conflictOpts = buildConflictOpts({ force: opts.force, interactive });\n // --dry-run/--preview collapse to a single dryRun boolean. The engine\n // skips the target-dir mkdir + every write; skills emission + dedup are\n // skipped too (they would touch disk / load the embedder).\n const dryRun = opts.dryRun === true || opts.preview === true;\n\n const res = await scaffold({\n root,\n mode: 'create',\n ...(opts.host !== undefined ? { host: opts.host } : {}),\n transport: opts.transport,\n interactive,\n ...(opts.url !== undefined ? { url: opts.url } : {}),\n ...(opts.force === true ? { force: true } : {}),\n ...(dryRun ? { dryRun: true } : {}),\n ...conflictOpts,\n });\n // dry-run reports the planned writes and stops BEFORE skills emission +\n // the \"created\" message — the target dir was NOT created, so nothing may\n // touch disk. (The engine's create-mode mkdir is gated on !dryRun.)\n if (dryRun) {\n reportPlannedWrites(res);\n return res;\n }\n // Report the permission heal BEFORE the already-initialized check, exactly as\n // init does: `create --force` over an existing tree reaches a `.noir/.env`\n // that an earlier version may have left group-readable, and a file fixed\n // without a word is the silent drift the re-assert exists to end.\n reportEnvHeal(res);\n // A no-op (already-initialized guard) must not re-emit skills.\n if (res.noop) return res;\n\n // The host the engine actually emitted (explicit --host, or the project's\n // configured host resolved inside scaffold).\n const host: HostId = res.host;\n\n // Skills are out-of-manifest by design — same composition as init.\n const adapter = resolveAdapter(host);\n const skillsDir = adapter.skillsDir?.({ root });\n if (skillsDir === undefined) {\n // Standardized wording — same phrase across init/sync/create so logs\n // grep uniformly. (Each command used to phrase this differently.)\n process.stderr.write(`host '${host}' has no skill emitter; skipping skills\\n`);\n } else {\n const target: CompileTarget = host;\n // Forward conflictOpts so an interactive `noir create` with a\n // conflicting skill emit consults the resolver (mirrors init/sync).\n type SkillEmitOpts = NonNullable<Parameters<typeof emitSkillsToDir>[1]>;\n const skillOpts: SkillEmitOpts = {\n includeIntegrations: true,\n target,\n conflictPolicy: conflictOpts.conflictPolicy,\n interactive,\n };\n if (conflictOpts.onConflict !== undefined) {\n skillOpts.onConflict = conflictOpts.onConflict as SkillEmitOpts['onConflict'];\n }\n const summary = await emitSkillsToDir(skillsDir, skillOpts);\n const relDir = skillsDir.replace(`${root}/`, '');\n process.stderr.write(\n `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).\\n`,\n );\n // The emitted count above is skills that are fully current; a skill whose\n // files were preserved is stale on disk and has to be named, in the same\n // words `noir init` uses.\n const stale = preservedStaleLine(summary.preserved ?? []);\n if (stale !== undefined) process.stderr.write(`${stale}\\n`);\n }\n\n // Write-path semantic dedup. Greenfield create rarely has\n // existing host-context candidates (the target dir is fresh), so the fast\n // path fires; `--force` on a pre-existing Noir project reads the config and\n // runs the full dedup. Best-effort: a missing/corrupt config warn-skips.\n let projectInfo: ProjectInfo | undefined;\n try {\n projectInfo = loadProjectInfo(root);\n } catch {\n projectInfo = undefined;\n }\n const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });\n if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);\n\n process.stderr.write(`Noir created in ${root} (host: ${host}, transport: ${opts.transport}).\\n`);\n process.stderr.write('Next: cd into the new directory, then run `noir` to open the home menu.\\n');\n return res;\n}\n"],"mappings":";;;;;;;;;;;;;;AA8BA,SAAS,eAAe;AACxB,SAAsB,sBAAsB;AAC5C,SAAS,uBAAyC;AAClD,SAA8B,gBAAgB;AAC9C,SAA6B,uBAAuB;AAmCpD,eAAsB,OACpB,KACA,MACqC;AAKrC,qBAAmB,IAAI;AAEvB,QAAM,OAAO,QAAQ,OAAO,QAAQ,IAAI,CAAC;AAMzC,QAAM,cAAc,mBAAmB;AACvC,QAAM,eAAe,kBAAkB,EAAE,OAAO,KAAK,OAAO,YAAY,CAAC;AAIzE,QAAM,SAAS,KAAK,WAAW,QAAQ,KAAK,YAAY;AAExD,QAAM,MAAM,MAAM,SAAS;AAAA,IACzB;AAAA,IACA,MAAM;AAAA,IACN,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,IACrD,WAAW,KAAK;AAAA,IAChB;AAAA,IACA,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,UAAU,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAI,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,IACjC,GAAG;AAAA,EACL,CAAC;AAID,MAAI,QAAQ;AACV,wBAAoB,GAAG;AACvB,WAAO;AAAA,EACT;AAKA,gBAAc,GAAG;AAEjB,MAAI,IAAI,KAAM,QAAO;AAIrB,QAAM,OAAe,IAAI;AAGzB,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,YAAY,QAAQ,YAAY,EAAE,KAAK,CAAC;AAC9C,MAAI,cAAc,QAAW;AAG3B,YAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,CAA2C;AAAA,EAC/E,OAAO;AACL,UAAM,SAAwB;AAI9B,UAAM,YAA2B;AAAA,MAC/B,qBAAqB;AAAA,MACrB;AAAA,MACA,gBAAgB,aAAa;AAAA,MAC7B;AAAA,IACF;AACA,QAAI,aAAa,eAAe,QAAW;AACzC,gBAAU,aAAa,aAAa;AAAA,IACtC;AACA,UAAM,UAAU,MAAM,gBAAgB,WAAW,SAAS;AAC1D,UAAM,SAAS,UAAU,QAAQ,GAAG,IAAI,KAAK,EAAE;AAC/C,YAAQ,OAAO;AAAA,MACb,WAAW,QAAQ,QAAQ,MAAM,mBAAmB,MAAM,cAAc,MAAM;AAAA;AAAA,IAChF;AAIA,UAAM,QAAQ,mBAAmB,QAAQ,aAAa,CAAC,CAAC;AACxD,QAAI,UAAU,OAAW,SAAQ,OAAO,MAAM,GAAG,KAAK;AAAA,CAAI;AAAA,EAC5D;AAMA,MAAI;AACJ,MAAI;AACF,kBAAc,gBAAgB,IAAI;AAAA,EACpC,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,QAAM,QAAQ,MAAM,oBAAoB,MAAM,KAAK,EAAE,aAAa,SAAS,YAAY,CAAC;AACxF,MAAI,MAAM,UAAU,SAAS,EAAG,KAAI,UAAU,KAAK,GAAG,MAAM,SAAS;AAErE,UAAQ,OAAO,MAAM,mBAAmB,IAAI,WAAW,IAAI,gBAAgB,KAAK,SAAS;AAAA,CAAM;AAC/F,UAAQ,OAAO,MAAM,2EAA2E;AAChG,SAAO;AACT;","names":[]}