@niroai/niro 0.9.0 → 0.10.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.
@@ -218,7 +218,7 @@ async function setEnv(projectId, pairs, opts) {
218
218
  console.log(JSON.stringify({ project_id: projectId, git_id: gitId, changed_keys: changedKeys }));
219
219
  } else {
220
220
  console.log(`${color.green("✓")} Updated env var(s) on repo ${color.dim(gitId)}: ${changedKeys.map((k) => color.bold(k)).join(", ")}`);
221
- console.log(color.dim(" (values are not shown)"));
221
+ console.log(color.dim(" (values are not shown)"));
222
222
  }
223
223
  }
224
224
 
@@ -291,12 +291,12 @@ async function listEnv(projectId, opts) {
291
291
  const appears = s.appears_in > 1
292
292
  ? color.yellow(` (appears in ${s.appears_in} files)`)
293
293
  : (s.appears_in === 1 ? color.dim(" (1 file)") : "");
294
- console.log(` - ${color.bold(s.key)}${set}${src}${appears}`);
294
+ console.log(` - ${color.bold(s.key)}${set}${src}${appears}`);
295
295
  if (s.appears_in > 1) {
296
296
  for (const c of s.candidates) {
297
297
  const shown = c.value == null ? color.dim("(empty)") : c.value;
298
298
  const marker = c.is_current ? color.green(" [current]") : "";
299
- console.log(` ${color.dim("•")} ${c.source_file}: ${shown}${marker}`);
299
+ console.log(` ${color.dim("•")} ${c.source_file}: ${shown}${marker}`);
300
300
  }
301
301
  }
302
302
  }
@@ -583,14 +583,14 @@ function maskPair(pair) {
583
583
 
584
584
  function printUsage() {
585
585
  console.error("niro edit — change an existing project. At least one action is required:");
586
- console.error(" --add-repo <gitId> map a registered repo to the project");
587
- console.error(" --remove-repo <gitId> unmap a repo (non-destructive)");
588
- console.error(" --set-env KEY=VALUE set an env var (repeatable; value never printed)");
589
- console.error(" --set-env-from KEY=FILE adopt the value FILE carries for KEY (repeatable; value never printed)");
590
- console.error(" --active-files <files> property files to keep active for env-var defaults; the rest are ignored (default: all)");
591
- console.error(" --list-env list env var keys + per-file candidates (secret values masked)");
592
- console.error(" --rescan-env rescan the repo for env vars");
593
- console.error(" --alias <newAlias> rename the project");
586
+ console.error(" --add-repo <gitId> map a registered repo to the project");
587
+ console.error(" --remove-repo <gitId> unmap a repo (non-destructive)");
588
+ console.error(" --set-env KEY=VALUE set an env var (repeatable; value never printed)");
589
+ console.error(" --set-env-from KEY=FILE adopt the value FILE carries for KEY (repeatable; value never printed)");
590
+ console.error(" --active-files <files> property files to keep active for env-var defaults; the rest are ignored (default: all)");
591
+ console.error(" --list-env list env var keys + per-file candidates (secret values masked)");
592
+ console.error(" --rescan-env rescan the repo for env vars");
593
+ console.error(" --alias <newAlias> rename the project");
594
594
  console.error("Target project: --project <alias|id>, or a .niro-project pin in this folder.");
595
595
  console.error("Env actions target --repo <gitId> (auto-selected when the project has one repo).");
596
596
  }
@@ -218,7 +218,7 @@ function printHuman(p) {
218
218
  if (proj.status) bits.push(`status: ${proj.status}`);
219
219
  if (proj.doc_gen_status) bits.push(`docs: ${proj.doc_gen_status}`);
220
220
  if (proj.updated_at) bits.push(`updated: ${proj.updated_at}`);
221
- console.log(` - ${name}${bits.length ? " — " + color.dim(bits.join(", ")) : ""}`);
221
+ console.log(` - ${name}${bits.length ? " — " + color.dim(bits.join(", ")) : ""}`);
222
222
  for (const f of proj.folders || []) {
223
223
  // Marked so the folder you are STANDING IN is obvious at a glance; the whole point is telling
224
224
  // "this worktree is the one Niro reflects" from "this one is not".
@@ -233,7 +233,7 @@ function printHuman(p) {
233
233
  temp = color.dim(` via temporary project ${f.temp_project_id}`)
234
234
  + (f.reflected === null ? color.dim(" (could not confirm)") : "");
235
235
  }
236
- console.log(color.dim(` syncing from ${f.path}`) +
236
+ console.log(color.dim(` syncing from ${f.path}`) +
237
237
  (f.branch ? color.dim(` on branch ${f.branch}`) : "") + temp + here);
238
238
  }
239
239
  }
@@ -41,6 +41,12 @@ const color = require("../lib/color");
41
41
  const watchDaemon = require("../lib/watchDaemon");
42
42
  const { login } = require("./login");
43
43
  const { watchStatus } = require("./build");
44
+ const { currentIndent } = require("../lib/nestedOutput");
45
+
46
+ /** Depth as spaces, for the \r-written lines that bypass withIndent's console wrappers. */
47
+ function nestPad() {
48
+ return " ".repeat(currentIndent());
49
+ }
44
50
  const { fetchPreview, buildCandidates } = require("./edit");
45
51
  const { resolveProjectId } = require("./_resolveProject");
46
52
  const { projectsToRows } = require("./projects");
@@ -52,6 +58,13 @@ const warn = (s) => color.yellow(s);
52
58
  const fail = (s) => color.red(s);
53
59
 
54
60
  async function init(pathArg, opts = {}) {
61
+ // Set by `niro start`, which frames its own steps around this one. Two things then change,
62
+ // both presentation only: the internal 1/3..3/3 markers are dropped (they collided with the
63
+ // wizard's own 4/5 and read as a second, conflicting count), and the closing banner is left
64
+ // to the wizard, which was otherwise telling the user to run `niro mcp install` immediately
65
+ // before doing it for them.
66
+ const nested = !!opts.nested;
67
+ const substep = (n) => (nested ? step("●") : step(n));
55
68
  const targetDir = resolveTargetDir(pathArg);
56
69
  // Validate flag values up-front so a bad --auto-build/--confirm-build/--quiet-period
57
70
  // fails BEFORE any server-side mutation (otherwise a typo leaves an orphan project).
@@ -143,10 +156,10 @@ async function init(pathArg, opts = {}) {
143
156
  // inconclusive — make the user (or an agent, via --force) confirm rather than proceed.
144
157
  if (gitInfo.originUrl && (resolveFailed || ctx.state === "error")) {
145
158
  console.log(warn(
146
- `\n ⚠ Couldn't verify whether this repo is already registered` +
159
+ `\n ⚠ Couldn't verify whether this repo is already registered` +
147
160
  `${ctx.error ? ` (${ctx.error})` : ""}.`
148
161
  ));
149
- if (!(await requireProceed(" Proceed without the duplicate check?", opts))) {
162
+ if (!(await requireProceed(" Proceed without the duplicate check?", opts))) {
150
163
  process.exitCode = 1;
151
164
  return;
152
165
  }
@@ -181,7 +194,7 @@ async function init(pathArg, opts = {}) {
181
194
  branch: defaultBranch || null,
182
195
  tracked: true, // locally-tracked for detection only; no gitId => daemon never syncs it
183
196
  }, currentApiUrl);
184
- console.log(color.dim(" Niro is now tracking this folder locally (no sandbox) so cross-repo answers can include it."));
197
+ console.log(color.dim(" Niro is now tracking this folder locally (no sandbox) so cross-repo answers can include it."));
185
198
  }
186
199
 
187
200
  if (onIndexedBranch.length) {
@@ -192,12 +205,12 @@ async function init(pathArg, opts = {}) {
192
205
  if (!opts.addTo) {
193
206
  if (prompt.isNonInteractive()) {
194
207
  console.log(color.dim(
195
- " Already indexed — nothing to do. Pass --add-to <project> to also map it elsewhere, or run 'niro build' to re-index."
208
+ " Already indexed — nothing to do. Pass --add-to <project> to also map it elsewhere, or run 'niro build' to re-index."
196
209
  ));
197
210
  return;
198
211
  }
199
- if (!(await prompt.confirm(" Add it to a DIFFERENT project too?", false))) {
200
- console.log(color.dim(" Nothing to do — use 'niro build' to re-index or 'niro edit' to manage it."));
212
+ if (!(await prompt.confirm(" Add it to a DIFFERENT project too?", false))) {
213
+ console.log(color.dim(" Nothing to do — use 'niro build' to re-index or 'niro edit' to manage it."));
201
214
  return;
202
215
  }
203
216
  }
@@ -209,17 +222,17 @@ async function init(pathArg, opts = {}) {
209
222
  .join(", ");
210
223
  const yourBranch = defaultBranch ? `\`${defaultBranch}\`` : "your current branch";
211
224
  console.log(`\n${color.green("✓")} This repo is part of: ${where}.`);
212
- console.log(` But ${yourBranch} isn't indexed by Niro — answers would reflect the indexed branch, not your local work.`);
213
- console.log(` ${color.cyan("Run `niro sandbox` here")} to index your branch privately ` +
225
+ console.log(` But ${yourBranch} isn't indexed by Niro — answers would reflect the indexed branch, not your local work.`);
226
+ console.log(` ${color.cyan("Run `niro sandbox` here")} to index your branch privately ` +
214
227
  color.dim("(original project unchanged; `niro discard` to undo)."));
215
228
  if (!opts.addTo) {
216
229
  if (prompt.isNonInteractive()) {
217
230
  console.log(color.dim(
218
- " Pass --add-to <project> to map this repo into a DIFFERENT project, or run 'niro sandbox' for your branch."
231
+ " Pass --add-to <project> to map this repo into a DIFFERENT project, or run 'niro sandbox' for your branch."
219
232
  ));
220
233
  return;
221
234
  }
222
- if (!(await prompt.confirm(" Add this repo to a DIFFERENT project instead?", false))) {
235
+ if (!(await prompt.confirm(" Add this repo to a DIFFERENT project instead?", false))) {
223
236
  return;
224
237
  }
225
238
  }
@@ -232,29 +245,29 @@ async function init(pathArg, opts = {}) {
232
245
  // catches children indexed via the web UI / as separate repos.
233
246
  const childScan = await scanIndexedChildren(targetDir, opts);
234
247
  if (childScan.indexed.length) {
235
- console.error(fail(`\n This folder contains ${childScan.indexed.length} repo(s) already indexed in Niro:`));
248
+ console.error(fail(`\n This folder contains ${childScan.indexed.length} repo(s) already indexed in Niro:`));
236
249
  for (const c of childScan.indexed) {
237
250
  const where = c.projects.map((p) => `"${p.alias || p.project_id}"`).join(", ");
238
- console.error(` ${c.name} -> ${where}`);
251
+ console.error(` ${c.name} -> ${where}`);
239
252
  }
240
- console.error(" A parent of already-indexed repos can't be onboarded — it would upload that content again.");
241
- console.error(" Onboard each repo from its own folder instead.");
253
+ console.error(" A parent of already-indexed repos can't be onboarded — it would upload that content again.");
254
+ console.error(" Onboard each repo from its own folder instead.");
242
255
  // Hard refuse: there's no legitimate reason to upload a parent over indexed repos.
243
256
  // --force stays as a deliberate escape hatch for scripts/agents that really mean it.
244
257
  if (!opts.force) {
245
258
  process.exitCode = 1;
246
259
  return;
247
260
  }
248
- console.log(warn(" --force set — onboarding the parent anyway."));
261
+ console.log(warn(" --force set — onboarding the parent anyway."));
249
262
  }
250
263
  if (childScan.unverified.length) {
251
264
  console.log(warn(
252
- ` ⚠ Couldn't check ${childScan.unverified.length} nested repo(s) ` +
265
+ ` ⚠ Couldn't check ${childScan.unverified.length} nested repo(s) ` +
253
266
  `(${childScan.unverified.map((c) => c.name).join(", ")}) — they may also be indexed.`
254
267
  ));
255
268
  }
256
269
  if (childScan.truncated) {
257
- console.log(color.dim(` (Stopped after the first ${CHILD_SCAN_MAX} nested repos; deeper ones weren't checked.)`));
270
+ console.log(color.dim(` (Stopped after the first ${CHILD_SCAN_MAX} nested repos; deeper ones weren't checked.)`));
258
271
  }
259
272
  const indexedChildPaths = new Set(childScan.indexed.map((c) => path.resolve(c.path)));
260
273
 
@@ -307,17 +320,17 @@ async function init(pathArg, opts = {}) {
307
320
  const crossRemote = ctxProjects.find((p) => p.source_type === "REMOTE_GIT");
308
321
  if (crossUpload) {
309
322
  console.log(warn(
310
- `\n ⚠ This repo is already registered in project "${crossUpload.project_id}"; ` +
323
+ `\n ⚠ This repo is already registered in project "${crossUpload.project_id}"; ` +
311
324
  `reusing that repo, attaching to "${projectId}" without re-uploading.`
312
325
  ));
313
326
  reuseGitId = crossUpload.git_id;
314
327
  skipUpload = true;
315
328
  } else if (crossRemote) {
316
329
  console.log(warn(
317
- `\n ⚠ This repo is already a REMOTE repo in project "${crossRemote.project_id}"; ` +
330
+ `\n ⚠ This repo is already a REMOTE repo in project "${crossRemote.project_id}"; ` +
318
331
  `proceeding uploads a SEPARATE local copy.`
319
332
  ));
320
- if (!(await requireProceed(" Proceed and upload a separate local copy?", opts))) {
333
+ if (!(await requireProceed(" Proceed and upload a separate local copy?", opts))) {
321
334
  process.exitCode = 1;
322
335
  return;
323
336
  }
@@ -334,10 +347,10 @@ async function init(pathArg, opts = {}) {
334
347
  return;
335
348
  }
336
349
  console.log(warn(
337
- `\n ⚠ This folder overlaps "${o.path}"` +
350
+ `\n ⚠ This folder overlaps "${o.path}"` +
338
351
  `${o.projectId ? ` (in project "${o.projectId}")` : ""}.`
339
352
  ));
340
- if (!(await requireProceed(" Proceed despite the folder overlap?", opts))) {
353
+ if (!(await requireProceed(" Proceed despite the folder overlap?", opts))) {
341
354
  process.exitCode = 1;
342
355
  return;
343
356
  }
@@ -357,27 +370,27 @@ async function init(pathArg, opts = {}) {
357
370
  // reuseGitId came straight from resolveContext (the current backend), so it
358
371
  // is known-good — no existence check needed.
359
372
  gitId = reuseGitId;
360
- console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
373
+ console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
361
374
  } else if (existing && existing.gitId) {
362
375
  if (ctxProjects.some((p) => p.git_id === existing.gitId)) {
363
376
  // Already mapped to >=1 project on THIS backend -> fully onboarded, and the
364
377
  // server just vouched for it via resolveContext. Reuse, skip the upload.
365
378
  gitId = existing.gitId;
366
379
  skipUpload = true;
367
- console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
380
+ console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
368
381
  } else if (await agentApi.repoExists(existing.gitId)) {
369
382
  // Registered but not yet in any project (partial onboarding interrupted
370
383
  // before the project POST). The repo still exists on this backend -> finish
371
384
  // it by uploading.
372
385
  gitId = existing.gitId;
373
- console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}).`);
386
+ console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}).`);
374
387
  } else {
375
388
  // Self-heal: the cached gitId does NOT exist on the current backend — the
376
389
  // backend was switched (cached gitId belongs to another stack) or the repo
377
390
  // was deleted. Drop the stale entry and register fresh instead of crashing
378
391
  // on the first call with a dead gitId.
379
392
  console.log(warn(
380
- `\n ⚠ Cached repo (gitId ${existing.gitId}) is not on this backend — ` +
393
+ `\n ⚠ Cached repo (gitId ${existing.gitId}) is not on this backend — ` +
381
394
  `re-registering it here.`
382
395
  ));
383
396
  watchState.remove(targetDir);
@@ -407,7 +420,7 @@ async function init(pathArg, opts = {}) {
407
420
  fileHashes: {},
408
421
  lastSyncedAt: null,
409
422
  }, currentApiUrl);
410
- console.log(`\n ${step("●")} Registered repo for upload (gitId ${color.dim(gitId)}).`);
423
+ console.log(`\n ${step("●")} Registered repo for upload (gitId ${color.dim(gitId)}).`);
411
424
  }
412
425
 
413
426
  // UPLOAD STEP — skipped entirely when reusing a repo that is ALREADY mapped to a
@@ -419,7 +432,7 @@ async function init(pathArg, opts = {}) {
419
432
 
420
433
  // Build the manifest, surface oversize files (the 256 KiB server cap skips
421
434
  // them), then ask the server which files to include.
422
- process.stdout.write(`\n Scanning ${color.cyan(targetDir)}...`);
435
+ process.stdout.write(`\n${nestPad()} Scanning ${color.cyan(targetDir)}...`);
423
436
  const manifest = folderSync.buildManifest(targetDir);
424
437
  process.stdout.write(` ${manifest.length} files found.\n`);
425
438
  const filterResult = await agentApi.filterManifest(gitId, manifest);
@@ -430,7 +443,7 @@ async function init(pathArg, opts = {}) {
430
443
  const excludedCount = filterResult.excluded_count || filterResult.excludedCount || 0;
431
444
  const includePatterns = filterResult.include_patterns || filterResult.includePatterns || [];
432
445
  const excludePatterns = filterResult.exclude_patterns || filterResult.excludePatterns || [];
433
- console.log(` ${ok("Will sync:")} ${includedCount} files ${warn("Excluded:")} ${excludedCount} files`);
446
+ console.log(` ${ok("Will sync:")} ${includedCount} files ${warn("Excluded:")} ${excludedCount} files`);
434
447
 
435
448
  // Push include/exclude patterns to the server so the build pipeline respects
436
449
  // them. module_path "" (root) — there is no build-file-detected module yet.
@@ -442,18 +455,18 @@ async function init(pathArg, opts = {}) {
442
455
  excluded: excludePatterns,
443
456
  });
444
457
  } catch (err) {
445
- console.log(warn(` ⚠ Could not save patterns to server: ${err.message}`));
458
+ console.log(warn(` ⚠ Could not save patterns to server: ${err.message}`));
446
459
  }
447
460
  }
448
461
  watchState.update(targetDir, { includePatterns, excludePatterns });
449
462
 
450
- console.log(`\n ${step("1/3")} Uploading ${includedPaths.length} files (branch ${color.cyan(defaultBranch || "detached HEAD")})...`);
463
+ console.log(`\n ${substep("1/3")} Uploading ${includedPaths.length} files (branch ${color.cyan(defaultBranch || "detached HEAD")})...`);
451
464
  // Reuse the sizes the manifest already computed so the aggregate guard does not re-stat every file.
452
465
  const sizes = {};
453
466
  for (const m of manifest) sizes[m.path] = m.size_bytes;
454
467
  await folderSync.doInitialSync(gitId, targetDir, includedPaths, { sizes });
455
468
  } else {
456
- console.log(` ${step("1/3")} ${ok("Skipping upload — the server already has this repo's files.")}`);
469
+ console.log(` ${substep("1/3")} ${ok("Skipping upload — the server already has this repo's files.")}`);
457
470
  }
458
471
 
459
472
  let project;
@@ -462,13 +475,13 @@ async function init(pathArg, opts = {}) {
462
475
  // same git-mappings endpoint `niro edit --add-repo` uses. Do NOT create a
463
476
  // project. The discovered env vars come back via the preview fetch below,
464
477
  // not from this response (POST git-mappings returns the updated project).
465
- console.log(` ${step("2/3")} Adding repo to existing project "${color.bold(projectId)}" and scanning for environment variables...`);
478
+ console.log(` ${substep("2/3")} Adding repo to existing project "${color.bold(projectId)}" and scanning for environment variables...`);
466
479
  project = await api.post(
467
480
  `/api/projects/${encodeURIComponent(projectId)}/git-mappings`,
468
481
  { [gitId]: {} }
469
482
  );
470
483
  } else {
471
- console.log(` ${step("2/3")} Creating project "${color.bold(projectId)}" and scanning for environment variables...`);
484
+ console.log(` ${substep("2/3")} Creating project "${color.bold(projectId)}" and scanning for environment variables...`);
472
485
  // Send the chosen name as `alias` (account-scoped, duplicates across
473
486
  // accounts allowed by design) — never as `project_id`. Letting the server
474
487
  // assign the opaque project_id avoids colliding with an unrelated
@@ -488,13 +501,13 @@ async function init(pathArg, opts = {}) {
488
501
  // tools / `niro build` defaults) follow the most recent one.
489
502
  if (existing && existing.projectId && existing.projectId !== projectId) {
490
503
  console.log(warn(
491
- ` ⚠ This folder was pinned to "${existing.projectId}"; it stays mapped there, ` +
504
+ ` ⚠ This folder was pinned to "${existing.projectId}"; it stays mapped there, ` +
492
505
  `but is now also attached to "${projectId}" and re-pinned to it.`
493
506
  ));
494
507
  }
495
508
  const { path: pinPath } = niroProjectFile.write(repoRoot, projectId, { overwrite: true });
496
509
  watchState.update(targetDir, { projectId });
497
- console.log(` ${ok("✓")} Pinned project to ${color.dim(pinPath)}`);
510
+ console.log(` ${ok("✓")} Pinned project to ${color.dim(pinPath)}`);
498
511
 
499
512
  let envVars = extractEnvVars(project, gitId);
500
513
  // EXISTING mode attaches via POST /git-mappings, whose response may not carry the
@@ -518,9 +531,9 @@ async function init(pathArg, opts = {}) {
518
531
  }
519
532
  let filledVars = envVars;
520
533
  if (envVars.length === 0) {
521
- console.log(" No env vars detected in this repo — skipping.");
534
+ console.log(" No env vars detected in this repo — skipping.");
522
535
  } else if (opts.defaults) {
523
- console.log(` Found ${envVars.length} env var(s); leaving all at defaults (--defaults).`);
536
+ console.log(` Found ${envVars.length} env var(s); leaving all at defaults (--defaults).`);
524
537
  } else if (prompt.isNonInteractive()) {
525
538
  console.error(fail(
526
539
  `Found ${envVars.length} env var(s) that need values and no terminal is available to prompt.\n` +
@@ -529,7 +542,7 @@ async function init(pathArg, opts = {}) {
529
542
  process.exitCode = 1;
530
543
  return;
531
544
  } else {
532
- console.log(` Found ${envVars.length} env var(s). When a var appears in several files, pick which file's value to use.`);
545
+ console.log(` Found ${envVars.length} env var(s). When a var appears in several files, pick which file's value to use.`);
533
546
  // Show, per var, the candidate value each committed file carries (source-file
534
547
  // parity with the web UI). Reuse the preview fetched above when available; if a
535
548
  // fresh fetch fails, fall back to a plain prompt rather than blocking onboarding.
@@ -542,7 +555,7 @@ async function init(pathArg, opts = {}) {
542
555
  activePropertiesFiles = preview.activePropertiesFiles;
543
556
  } catch (err) {
544
557
  varsByFile = {};
545
- console.log(warn(` ⚠ Could not load env-var source files (${err.message}) — prompting without candidates.`));
558
+ console.log(warn(` ⚠ Could not load env-var source files (${err.message}) — prompting without candidates.`));
546
559
  }
547
560
  }
548
561
 
@@ -569,9 +582,9 @@ async function init(pathArg, opts = {}) {
569
582
  }
570
583
 
571
584
  if (opts.defaults) {
572
- console.log(" Skipping indexing-scope review (--defaults).");
585
+ console.log(" Skipping indexing-scope review (--defaults).");
573
586
  } else if (prompt.isNonInteractive()) {
574
- console.log(" Skipping indexing-scope review (no terminal to prompt; using detected defaults).");
587
+ console.log(" Skipping indexing-scope review (no terminal to prompt; using detected defaults).");
575
588
  } else {
576
589
  await reviewIndexingScope(gitId);
577
590
  }
@@ -582,7 +595,7 @@ async function init(pathArg, opts = {}) {
582
595
  const quietPeriodSeconds = parseQuietPeriod(opts.quietPeriod, 30);
583
596
  await saveAutoBuild(projectId, autoBuildFlag, quietPeriodSeconds);
584
597
  } else if (opts.defaults) {
585
- console.log(" Leaving auto-build disabled (--defaults).");
598
+ console.log(" Leaving auto-build disabled (--defaults).");
586
599
  } else if (prompt.isNonInteractive()) {
587
600
  console.error(fail(
588
601
  "Auto-build setting is required and no terminal is available to prompt.\n" +
@@ -609,12 +622,12 @@ async function init(pathArg, opts = {}) {
609
622
  process.exitCode = 1;
610
623
  return;
611
624
  } else {
612
- shouldBuild = await prompt.confirm(" Trigger a full index build now?", true);
625
+ shouldBuild = await prompt.confirm(" Trigger a full index build now?", true);
613
626
  }
614
627
  if (!shouldBuild) {
615
- console.log(` ${warn("⚠")} Skipping build. Run ${color.cyan("niro build")} when you're ready.`);
628
+ console.log(` ${warn("⚠")} Skipping build. Run ${color.cyan("niro build")} when you're ready.`);
616
629
  } else {
617
- console.log(` ${step("3/3")} Triggering full index build...`);
630
+ console.log(` ${substep("3/3")} Triggering full index build...`);
618
631
  await api.post(
619
632
  `/api/graph/${encodeURIComponent(projectId)}/build`,
620
633
  {},
@@ -622,7 +635,7 @@ async function init(pathArg, opts = {}) {
622
635
  );
623
636
 
624
637
  if (opts.watch === false) {
625
- console.log(` Build triggered. Run ${color.cyan("niro status")} to check progress.`);
638
+ console.log(` Build triggered. Run ${color.cyan("niro status")} to check progress.`);
626
639
  } else {
627
640
  await watchStatus(projectId);
628
641
  }
@@ -634,12 +647,20 @@ async function init(pathArg, opts = {}) {
634
647
  // The closing banner must not read as an all-clear when auto-sync is dead —
635
648
  // customers were only discovering the missing daemon after a reboot.
636
649
  if (daemon.ok) {
637
- console.log(`\n${ok("✓ Done.")} Niro MCP tools launched from this repo will now use this project automatically.`);
650
+ if (nested) {
651
+ console.log(`\n ${ok("✓")} Indexed. Niro MCP tools launched from this repo will use this project.`);
652
+ } else {
653
+ console.log(`\n${ok("✓ Done.")} Niro MCP tools launched from this repo will now use this project automatically.`);
654
+ }
638
655
  } else {
639
656
  console.log(`\n${warn("✓ Done, with 1 warning:")} auto-sync is OFF — this folder will not stay in sync (see above for the fix).`);
640
657
  console.log(`Niro MCP tools launched from this repo will still use this project automatically.`);
641
658
  }
642
- console.log(`Next: ${color.cyan("niro status")}, or ${color.cyan("niro mcp install")} to configure Claude Code / Cursor / Windsurf.`);
659
+ // The wizard prints its own closing screen, and step 5 IS the mcp install this line points
660
+ // at, so saying it here contradicts what happens two seconds later.
661
+ if (!nested) {
662
+ console.log(`Next: ${color.cyan("niro status")}, or ${color.cyan("niro mcp install")} to configure Claude Code / Cursor / Windsurf.`);
663
+ }
643
664
  }
644
665
 
645
666
  /**
@@ -654,7 +675,7 @@ async function init(pathArg, opts = {}) {
654
675
  async function requireProceed(msg, opts = {}) {
655
676
  if (opts.force) return true;
656
677
  if (prompt.isNonInteractive()) {
657
- console.error(fail(" Pass --force to proceed past this warning."));
678
+ console.error(fail(" Pass --force to proceed past this warning."));
658
679
  return false;
659
680
  }
660
681
  return await prompt.confirm(msg, false);
@@ -797,7 +818,7 @@ async function fetchExistingProjects(opts) {
797
818
  const data = await api.get("/api/projects", { apiUrl: opts.apiUrl });
798
819
  return projectsToRows(data);
799
820
  } catch (err) {
800
- console.log(warn(` ⚠ Could not load your existing projects (${err.message}) — creating a new project.`));
821
+ console.log(warn(` ⚠ Could not load your existing projects (${err.message}) — creating a new project.`));
801
822
  return [];
802
823
  }
803
824
  }
@@ -807,13 +828,13 @@ async function fetchExistingProjects(opts) {
807
828
  * "NEW", an existing project_id, or null when the answer is invalid.
808
829
  */
809
830
  async function chooseTarget(rows) {
810
- console.log(`\n ${color.bold("New project, or add to an existing one?")}`);
811
- console.log(` [n] create a new project`);
831
+ console.log(`\n ${color.bold("New project, or add to an existing one?")}`);
832
+ console.log(` [n] create a new project`);
812
833
  rows.forEach((r, i) => {
813
834
  const label = r.alias ? `${r.alias} (${color.dim(r.project_id)})` : color.dim(r.project_id);
814
- console.log(` [${i + 1}] ${label}`);
835
+ console.log(` [${i + 1}] ${label}`);
815
836
  });
816
- const answer = (await prompt.ask(` Choose 'n' for new, or 1-${rows.length} to add to an existing project`, "n")).trim();
837
+ const answer = (await prompt.ask(` Choose 'n' for new, or 1-${rows.length} to add to an existing project`, "n")).trim();
817
838
  if (!answer || answer.toLowerCase() === "n") return "NEW";
818
839
  const idx = Number.parseInt(answer, 10) - 1;
819
840
  if (Number.isInteger(idx) && idx >= 0 && idx < rows.length) {
@@ -882,13 +903,13 @@ async function resolveAvailableProjectName({ defaultName, opts }) {
882
903
 
883
904
  let candidate = defaultName;
884
905
  while (true) {
885
- const answer = await prompt.ask("Project name", candidate);
906
+ const answer = await prompt.ask(" Project name", candidate);
886
907
  if (!answer) {
887
- console.error(fail("Project name is required."));
908
+ console.error(fail(" Project name is required."));
888
909
  return null;
889
910
  }
890
911
  if (!(await aliasTaken(answer, opts))) return answer;
891
- console.log(` ${warn("⚠")} "${color.bold(answer)}" already exists on this account — pick another.`);
912
+ console.log(` ${warn("⚠")} "${color.bold(answer)}" already exists on this account — pick another.`);
892
913
  candidate = answer;
893
914
  }
894
915
  }
@@ -913,18 +934,18 @@ async function reviewIndexingScope(gitId) {
913
934
  try {
914
935
  details = await api.get(`/api/git/${encodeURIComponent(gitId)}`);
915
936
  } catch (err) {
916
- console.log(` Could not fetch project resolutions (${err.message}) — skipping scope review.`);
937
+ console.log(` Could not fetch project resolutions (${err.message}) — skipping scope review.`);
917
938
  return;
918
939
  }
919
940
  const resolutions = (details && (details.project_resolutions || details.projectResolutions)) || {};
920
941
  const modulePaths = Object.keys(resolutions);
921
942
  if (modulePaths.length === 0) {
922
- console.log(" No module resolutions detected — skipping scope review.");
943
+ console.log(" No module resolutions detected — skipping scope review.");
923
944
  return;
924
945
  }
925
946
 
926
- console.log(`\n ${color.bold("Review indexing scope")} (${modulePaths.length} module(s) detected).`);
927
- console.log(color.dim(` Glob patterns use ** for recursive match. Press Enter to keep the detected set.`));
947
+ console.log(`\n ${color.bold("Review indexing scope")} (${modulePaths.length} module(s) detected).`);
948
+ console.log(color.dim(` Glob patterns use ** for recursive match. Press Enter to keep the detected set.`));
928
949
 
929
950
  for (const modulePath of modulePaths) {
930
951
  const resolution = resolutions[modulePath] || {};
@@ -933,12 +954,12 @@ async function reviewIndexingScope(gitId) {
933
954
  const pkg = resolution.package_name || resolution.packageName;
934
955
  const label = pkg ? `${modulePath} (${color.dim(pkg)})` : modulePath;
935
956
 
936
- console.log(`\n ${color.bold("[" + label + "]")}`);
937
- console.log(` ${color.green("Included")}: ${formatList(includeDirs)}`);
938
- console.log(` ${color.yellow("Excluded")}: ${formatList(excluded)}`);
957
+ console.log(`\n ${color.bold("[" + label + "]")}`);
958
+ console.log(` ${color.green("Included")}: ${formatList(includeDirs)}`);
959
+ console.log(` ${color.yellow("Excluded")}: ${formatList(excluded)}`);
939
960
 
940
- const addedIncludes = await promptGlobList(" Add inclusion patterns", GLOB_INCLUDE_EXAMPLES);
941
- const addedExcludes = await promptGlobList(" Add exclusion patterns", GLOB_EXCLUDE_EXAMPLES);
961
+ const addedIncludes = await promptGlobList(" Add inclusion patterns", GLOB_INCLUDE_EXAMPLES);
962
+ const addedExcludes = await promptGlobList(" Add exclusion patterns", GLOB_EXCLUDE_EXAMPLES);
942
963
 
943
964
  if (addedIncludes.length === 0 && addedExcludes.length === 0) continue;
944
965
 
@@ -951,9 +972,9 @@ async function reviewIndexingScope(gitId) {
951
972
  include_dirs: nextIncludes,
952
973
  excluded: nextExcluded,
953
974
  });
954
- console.log(` ${ok("✓")} Updated.`);
975
+ console.log(` ${ok("✓")} Updated.`);
955
976
  } catch (err) {
956
- console.log(` ${fail("✗")} Failed to update scope: ${err.message}`);
977
+ console.log(` ${fail("✗")} Failed to update scope: ${err.message}`);
957
978
  }
958
979
  }
959
980
  }
@@ -968,10 +989,10 @@ async function promptGlobList(label, exampleHint) {
968
989
  }
969
990
 
970
991
  async function promptAutoBuild(projectId) {
971
- const enabled = await prompt.confirm(" Enable auto-build when files change?", false);
992
+ const enabled = await prompt.confirm(" Enable auto-build when files change?", false);
972
993
  let quietPeriodSeconds = 30;
973
994
  if (enabled) {
974
- const raw = await prompt.ask(" Quiet period in seconds before rebuild", "30");
995
+ const raw = await prompt.ask(" Quiet period in seconds before rebuild", "30");
975
996
  const parsed = Number.parseInt(raw, 10);
976
997
  if (Number.isFinite(parsed) && parsed >= 0) quietPeriodSeconds = parsed;
977
998
  }
@@ -985,9 +1006,9 @@ async function saveAutoBuild(projectId, enabled, quietPeriodSeconds) {
985
1006
  quiet_period_seconds: quietPeriodSeconds,
986
1007
  });
987
1008
  const statusLabel = enabled ? ok(`ON (quiet ${quietPeriodSeconds}s)`) : color.dim("off");
988
- console.log(` Auto-build: ${statusLabel}.`);
1009
+ console.log(` Auto-build: ${statusLabel}.`);
989
1010
  } catch (err) {
990
- console.log(` ${fail("✗")} Failed to save auto-build setting: ${err.message}`);
1011
+ console.log(` ${fail("✗")} Failed to save auto-build setting: ${err.message}`);
991
1012
  }
992
1013
  }
993
1014
 
@@ -1066,13 +1087,13 @@ async function promptActivePropertyFiles(varsByFile, currentActive) {
1066
1087
  : null; // null => all active
1067
1088
  const isActive = (f) => activeSet == null || activeSet.has(f);
1068
1089
 
1069
- console.log(`\n ${color.bold("Property files Niro found")} (their values become env-var defaults):`);
1090
+ console.log(`\n ${color.bold("Property files Niro found")} (their values become env-var defaults):`);
1070
1091
  files.forEach((f, i) => {
1071
1092
  const state = isActive(f) ? ok("[active]") : warn("[ignored]");
1072
- console.log(` [${i + 1}] ${f} ${state}`);
1093
+ console.log(` [${i + 1}] ${f} ${state}`);
1073
1094
  });
1074
1095
  const answer = (await prompt.ask(
1075
- " Enter to use ALL, or list numbers to IGNORE (e.g. 2,3)",
1096
+ " Enter to use ALL, or list numbers to IGNORE (e.g. 2,3)",
1076
1097
  ""
1077
1098
  )).trim();
1078
1099
 
@@ -1089,18 +1110,18 @@ async function promptActivePropertyFiles(varsByFile, currentActive) {
1089
1110
  );
1090
1111
  const active = files.filter((_f, i) => !ignoreIdx.has(i));
1091
1112
  if (active.length === files.length) {
1092
- console.log(color.dim(" No valid numbers recognised — keeping all property files active."));
1113
+ console.log(color.dim(" No valid numbers recognised — keeping all property files active."));
1093
1114
  return files.slice();
1094
1115
  }
1095
1116
  if (active.length === 0) {
1096
1117
  // Ignoring every file isn't a durable state (an empty active set reads back as
1097
1118
  // "all active"). Keep all; the user can still leave values blank and set them
1098
1119
  // later with `niro edit --set-env`.
1099
- console.log(warn(" Can't ignore every property file — keeping all active."));
1120
+ console.log(warn(" Can't ignore every property file — keeping all active."));
1100
1121
  return files.slice();
1101
1122
  }
1102
1123
  const ignored = files.filter((_f, i) => ignoreIdx.has(i));
1103
- console.log(` ${warn("Ignoring")}: ${ignored.join(", ")}`);
1124
+ console.log(` ${warn("Ignoring")}: ${ignored.join(", ")}`);
1104
1125
  return active;
1105
1126
  }
1106
1127
 
@@ -1132,24 +1153,24 @@ async function promptEnvVars(envVars, varsByFile = {}, activeFiles = null) {
1132
1153
  const candidates = buildCandidates(varsByFile, key, v, activeFiles);
1133
1154
 
1134
1155
  if (candidates.length > 1) {
1135
- console.log(`\n ${color.bold(key)}${currentSource ? color.dim(` (current: ${currentSource})`) : ""}`);
1156
+ console.log(`\n ${color.bold(key)}${currentSource ? color.dim(` (current: ${currentSource})`) : ""}`);
1136
1157
  candidates.forEach((c, i) => {
1137
1158
  // Mask when the key looks secret OR the value embeds credentials.
1138
1159
  const hide = secret || looksSecretValue(c.value);
1139
1160
  const shown = c.value == null ? "(empty)" : (hide ? "****" : c.value);
1140
1161
  const marker = (c.is_current || c.is_default) ? color.green(" [current default]") : "";
1141
- console.log(` [${i + 1}] ${shown} ${color.dim("from " + c.source_file)}${marker}`);
1162
+ console.log(` [${i + 1}] ${shown} ${color.dim("from " + c.source_file)}${marker}`);
1142
1163
  });
1143
- console.log(` [c] enter a custom value`);
1144
- const choice = (await prompt.ask(` Choose 1-${candidates.length}, c, or Enter to keep current`, "")).trim();
1164
+ console.log(` [c] enter a custom value`);
1165
+ const choice = (await prompt.ask(` Choose 1-${candidates.length}, c, or Enter to keep current`, "")).trim();
1145
1166
  if (!choice) {
1146
1167
  out.push({ ...v });
1147
1168
  continue;
1148
1169
  }
1149
1170
  if (choice.toLowerCase() === "c") {
1150
1171
  const answer = secret
1151
- ? await prompt.askHidden(` Value for ${key}`)
1152
- : await prompt.ask(` Value for ${key}`, "");
1172
+ ? await prompt.askHidden(` Value for ${key}`)
1173
+ : await prompt.ask(` Value for ${key}`, "");
1153
1174
  out.push({ ...v, user_value: answer || null });
1154
1175
  continue;
1155
1176
  }
@@ -1170,10 +1191,10 @@ async function promptEnvVars(envVars, varsByFile = {}, activeFiles = null) {
1170
1191
  let hint = currentSource ? ` (from ${currentSource})` : "";
1171
1192
  if (!currentValue) hint += " (no default — please enter a value)";
1172
1193
  if (secret) {
1173
- const answer = await prompt.askHidden(` ${key}${hint} (hidden, Enter to keep current)`);
1194
+ const answer = await prompt.askHidden(` ${key}${hint} (hidden, Enter to keep current)`);
1174
1195
  out.push({ ...v, user_value: answer || (v.user_value || null) });
1175
1196
  } else {
1176
- const answer = await prompt.ask(` ${key}${hint}`, currentValue);
1197
+ const answer = await prompt.ask(` ${key}${hint}`, currentValue);
1177
1198
  out.push({ ...v, user_value: answer || null });
1178
1199
  }
1179
1200
  }