@niroai/niro 0.16.0 → 0.18.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +123 -302
  3. package/bin/niro.js +12 -1
  4. package/docs/agent-antigravity.md +37 -0
  5. package/docs/agent-claude-code.md +48 -0
  6. package/docs/agent-claude-desktop.md +47 -0
  7. package/docs/agent-cline.md +42 -0
  8. package/docs/agent-codex.md +45 -0
  9. package/docs/agent-copilot-cli.md +42 -0
  10. package/docs/agent-cursor.md +44 -0
  11. package/docs/agent-factory.md +42 -0
  12. package/docs/agent-gemini-cli.md +42 -0
  13. package/docs/agent-goose.md +42 -0
  14. package/docs/agent-kiro.md +37 -0
  15. package/docs/agent-opencode.md +37 -0
  16. package/docs/agent-vscode.md +45 -0
  17. package/docs/agent-windsurf.md +38 -0
  18. package/docs/answer-shapes.md +69 -0
  19. package/docs/builds-and-updates.md +68 -0
  20. package/docs/cloud-README.md +372 -0
  21. package/docs/configuring-a-folder.md +51 -0
  22. package/docs/connect-ci.md +24 -0
  23. package/docs/connect-overview.md +20 -0
  24. package/docs/connect-review-bots.md +17 -0
  25. package/docs/connect-team-server.md +12 -0
  26. package/docs/connect-terminal.md +49 -0
  27. package/docs/connect-web-agents.md +9 -0
  28. package/docs/connect-your-ai-assistant.md +59 -175
  29. package/docs/coverage-audit-and-consent.md +51 -0
  30. package/docs/evaluate-what-niro-did.md +271 -0
  31. package/docs/faq-assistants-and-mcp.md +122 -0
  32. package/docs/faq-feedback-and-receipts.md +33 -0
  33. package/docs/faq-indexing.md +127 -0
  34. package/docs/faq-install-and-setup.md +103 -0
  35. package/docs/faq-licence-and-plans.md +86 -0
  36. package/docs/faq-login-and-account.md +69 -0
  37. package/docs/faq-offline.md +34 -0
  38. package/docs/faq-performance.md +33 -0
  39. package/docs/faq-privacy-and-security.md +113 -0
  40. package/docs/faq-uninstall.md +47 -0
  41. package/docs/faq-updates.md +59 -0
  42. package/docs/faq-worktrees-and-branches.md +59 -0
  43. package/docs/faq-wrong-or-missing-answers.md +275 -0
  44. package/docs/hosted-only.md +33 -0
  45. package/docs/how-the-graph-works.md +47 -0
  46. package/docs/index.md +157 -0
  47. package/docs/licence-and-plans.md +72 -0
  48. package/docs/lifecycle-debug.md +48 -0
  49. package/docs/lifecycle-onboarding.md +32 -0
  50. package/docs/lifecycle-overview.md +77 -0
  51. package/docs/lifecycle-plan-and-design.md +44 -0
  52. package/docs/lifecycle-review.md +30 -0
  53. package/docs/lifecycle-security-and-privacy.md +35 -0
  54. package/docs/lifecycle-test.md +28 -0
  55. package/docs/lifecycle-write-code.md +35 -0
  56. package/docs/llms-full.txt +8015 -0
  57. package/docs/llms.txt +126 -0
  58. package/docs/playbook-add-a-feature.md +108 -0
  59. package/docs/playbook-change-safely.md +93 -0
  60. package/docs/playbook-data-and-events.md +107 -0
  61. package/docs/playbook-debug-after-a-change.md +96 -0
  62. package/docs/playbook-debug-config.md +95 -0
  63. package/docs/playbook-debug-exception.md +124 -0
  64. package/docs/playbook-debug-failing-service-call.md +119 -0
  65. package/docs/playbook-debug-slow-path.md +80 -0
  66. package/docs/playbook-debug-wrong-value.md +137 -0
  67. package/docs/playbook-onboard.md +95 -0
  68. package/docs/playbook-review-a-change.md +72 -0
  69. package/docs/playbook-test-a-change.md +73 -0
  70. package/docs/reference-cli.md +113 -0
  71. package/docs/reference-config-files.md +42 -0
  72. package/docs/reference-engine-commands.md +56 -0
  73. package/docs/reference-environment-variables.md +89 -0
  74. package/docs/reference-exit-codes.md +67 -0
  75. package/docs/reference-limits.md +75 -0
  76. package/docs/reference-mcp-tools.md +326 -0
  77. package/docs/reference-messages.md +180 -0
  78. package/docs/secrets-and-masking.md +48 -0
  79. package/docs/supported-c.md +130 -0
  80. package/docs/supported-cpp.md +157 -0
  81. package/docs/supported-csharp.md +211 -0
  82. package/docs/supported-go.md +258 -0
  83. package/docs/supported-java.md +226 -0
  84. package/docs/supported-javascript.md +223 -0
  85. package/docs/supported-kotlin.md +124 -0
  86. package/docs/supported-overview.md +89 -0
  87. package/docs/supported-php.md +157 -0
  88. package/docs/supported-python.md +224 -0
  89. package/docs/supported-ruby.md +162 -0
  90. package/docs/supported-rust.md +150 -0
  91. package/docs/supported-swift.md +133 -0
  92. package/docs/supported-typescript.md +220 -0
  93. package/docs/tokens-speed-and-accuracy.md +99 -0
  94. package/docs/troubleshooting.md +42 -0
  95. package/docs/uninstall.md +74 -0
  96. package/docs/what-leaves-your-machine.md +71 -0
  97. package/docs/where-things-are-stored.md +59 -0
  98. package/docs/worktrees-and-branches.md +55 -0
  99. package/package.json +5 -3
  100. package/src/commands/add-repo.js +5 -0
  101. package/src/commands/auditSend.js +67 -0
  102. package/src/commands/build.js +6 -0
  103. package/src/commands/docsSend.js +73 -0
  104. package/src/commands/engine.js +120 -0
  105. package/src/commands/engineRoutes.js +105 -0
  106. package/src/commands/guide.js +254 -0
  107. package/src/commands/init.js +16 -7
  108. package/src/commands/initLocal.js +96 -0
  109. package/src/commands/journalSend.js +47 -0
  110. package/src/commands/login.js +89 -12
  111. package/src/commands/mcp.js +22 -6
  112. package/src/commands/plan.js +99 -0
  113. package/src/commands/repoPush.js +33 -0
  114. package/src/commands/requestIntegration.js +38 -0
  115. package/src/commands/share.js +56 -0
  116. package/src/commands/start.js +451 -371
  117. package/src/commands/status.js +57 -0
  118. package/src/commands/takeover.js +6 -0
  119. package/src/commands/uninstall.js +180 -0
  120. package/src/commands/upgrade.js +38 -8
  121. package/src/commands/watch.js +420 -6
  122. package/src/index.js +174 -3
  123. package/src/lib/agentApi.js +32 -1
  124. package/src/lib/api.js +20 -6
  125. package/src/lib/auditUpload.js +293 -0
  126. package/src/lib/branchDrift.js +149 -0
  127. package/src/lib/choose.js +116 -0
  128. package/src/lib/cloudGuard.js +74 -0
  129. package/src/lib/credentials.js +2 -2
  130. package/src/lib/docsMarked.js +55 -0
  131. package/src/lib/docsState.js +213 -0
  132. package/src/lib/docsUpload.js +481 -0
  133. package/src/lib/engine.js +619 -0
  134. package/src/lib/folderSync.js +2 -1
  135. package/src/lib/graphCheck.js +40 -0
  136. package/src/lib/guide.js +293 -0
  137. package/src/lib/guideSearch.js +252 -0
  138. package/src/lib/integrationRequest.js +89 -0
  139. package/src/lib/journalUpload.js +147 -0
  140. package/src/lib/languages.js +129 -0
  141. package/src/lib/lease.js +375 -0
  142. package/src/lib/localBuild.js +285 -0
  143. package/src/lib/localEngine.js +210 -0
  144. package/src/lib/localOnboard.js +124 -0
  145. package/src/lib/localWorktrees.js +243 -0
  146. package/src/lib/machineCheck.js +58 -0
  147. package/src/lib/mcpClients.js +10 -1
  148. package/src/lib/migrationNotice.js +118 -0
  149. package/src/lib/modelFetch.js +153 -0
  150. package/src/lib/nestedOutput.js +23 -9
  151. package/src/lib/onboardingLog.js +34 -0
  152. package/src/lib/planUsage.js +165 -0
  153. package/src/lib/progressBar.js +46 -0
  154. package/src/lib/prompt.js +1 -1
  155. package/src/lib/redact.js +155 -0
  156. package/src/lib/release.js +33 -0
  157. package/src/lib/repoPush.js +495 -0
  158. package/src/lib/secret.js +1 -1
  159. package/src/lib/service.js +372 -63
  160. package/src/lib/shareCode.js +367 -0
  161. package/src/lib/shareExclusions.js +114 -0
  162. package/src/lib/shareFiles.js +129 -0
  163. package/src/lib/spinner.js +9 -5
  164. package/src/lib/watchDaemon.js +33 -10
  165. package/src/lib/watchState.js +12 -1
  166. package/src/lib/windowsPaths.js +67 -0
  167. package/src/mcp/lineCheck.js +382 -0
  168. package/src/mcp/lineName.js +92 -0
  169. package/src/mcp/localBackend.js +751 -0
  170. package/src/mcp/localMode.js +383 -0
  171. package/src/mcp/localSkill.js +189 -0
  172. package/src/mcp/server.js +610 -79
  173. package/src/mcp/setup.js +519 -131
  174. package/src/mcp/shellSearchHook.js +509 -0
  175. package/src/mcp/teardown.js +173 -91
  176. package/src/mcp/usingNiro.js +39 -0
  177. package/docs/working-on-branches.md +0 -162
@@ -1,270 +1,503 @@
1
1
  /**
2
- * `niro start` — the one command a new user runs.
2
+ * `niro start` — the first run: one wizard, then a ready screen. The graph is built and kept
3
+ * on this machine (Niro Local). A cloud ("web") mode is planned for a later version.
3
4
  *
4
- * A thin ORCHESTRATOR over the commands that already exist. It deliberately owns no
5
- * onboarding logic of its own: a second implementation of project creation or env
6
- * scanning would drift from init.js within a release. Every step below either calls an
7
- * existing command module or asks a question.
5
+ * 1/5 Checking this machine OS and arch, free disk, Node, the folder. Windows stops
6
+ * here, before any account exists.
7
+ * The npx-to-global install happens here too, quietly.
8
+ * 2/5 Sign in login.js device flow. Waiting, blocked and a Teams request
9
+ * end the wizard with login.js's own words and exit code.
10
+ * 3/5 Local engine engine.ensure(); checksum only, never "signature". Then
11
+ * the engine is asked whether its graph file opens.
12
+ * 4/5 Building your graph niro-local init --progress (adds and builds), streamed into a bar.
13
+ * 5/5 Connect your AI assistant the existing `niro mcp install` wizard, full client list.
14
+ * Niro is ready graph, assistant, plan; the watch daemon keeps it current.
8
15
  *
9
- * 0. Beta gate — access code, asked BEFORE anything is installed or contacted.
10
- * 1. Bootstrap — install globally and re-exec when running from the npx cache.
11
- * 2. Sign in — login.js (device flow), skipped when credentials exist.
12
- * 3. Choose folder — show what was detected, confirm, re-ask on a refusal.
13
- * 4. Onboard — init.js, unchanged.
14
- * 5. Connect — mcp.js install(), skipped when this machine already did it.
16
+ * Each step checks its own completion and prints ✓ instead of redoing it, so a second run
17
+ * resumes at the first unfinished step and a run after full success changes nothing:
18
+ * credentials, the engine resolved, the folder indexed per `niro-local projects --json`, and
19
+ * `mcpConnectedAt`.
15
20
  *
16
- * Why step 0 precedes step 1: the beta needs an access code and a work email, so
17
- * someone without a code cannot get an account. Asking first costs one prompt and
18
- * saves them installing software they cannot use. Nothing touches their machine
19
- * before they answer.
21
+ * No cloud fallback (design §9.3, and Niraj's D2 of 2026-10-31): an engine that cannot open its
22
+ * graph says why in its own words and the wizard stops. Each step's timing and outcome go to
23
+ * ~/.niro/onboarding.json.
20
24
  *
21
- * Why step 1 exists at all: service.js registers the watch daemon with PM2 using
25
+ * Why the npx hand-over exists at all: service.js registers the watch daemon with PM2 using
22
26
  * `process.argv[1]`. Under npx that path lives in ~/.npm/_npx/<hash>/, which npm
23
- * garbage-collects on its own schedule, so a watcher registered from an npx run would
24
- * look healthy and then stop syncing days later with no error anywhere. npx is the
25
- * bootstrapper, not the way to run Niro.
27
+ * garbage-collects on its own schedule, so a watcher registered from an npx run would look
28
+ * healthy and then stop days later with no error anywhere.
26
29
  */
27
30
  const path = require("path");
28
31
  const fs = require("fs");
29
- // Required as a namespace, not destructured, so tests can substitute spawnSync
30
- // without shelling out to npm or re-execing a real binary.
32
+ const os = require("os");
33
+ // Required as namespaces, not destructured, so tests can substitute each collaborator.
31
34
  const childProcess = require("child_process");
32
35
 
33
36
  const color = require("../lib/color");
34
37
  const prompt = require("../lib/prompt");
35
38
  const credentials = require("../lib/credentials");
36
39
  const config = require("../lib/config");
37
- const git = require("../lib/git");
38
40
  const onboardTarget = require("../lib/onboardTarget");
41
+ const machineCheck = require("../lib/machineCheck");
42
+ const engine = require("../lib/engine");
43
+ const graphCheck = require("../lib/graphCheck");
44
+ const localOnboard = require("../lib/localOnboard");
45
+ const languages = require("../lib/languages");
46
+ const lease = require("../lib/lease");
47
+ const progressBar = require("../lib/progressBar");
48
+ const onboardingLog = require("../lib/onboardingLog");
49
+ const watchDaemon = require("../lib/watchDaemon");
50
+ const mcpClients = require("../lib/mcpClients");
51
+ const planUsage = require("../lib/planUsage");
52
+ const service = require("../lib/service");
53
+ const watchState = require("../lib/watchState");
54
+ const migrationNotice = require("../lib/migrationNotice");
55
+ const release = require("../lib/release");
39
56
 
40
- const step = (n) => color.bold(color.cyan(n));
41
57
  const ok = (s) => color.green(s);
42
58
  const warn = (s) => color.yellow(s);
43
59
  const fail = (s) => color.red(s);
44
60
 
45
- const TOTAL_STEPS = 5;
61
+ /** The steps, in order; a step's number is its place here. */
62
+ const order = ["machine", "sign-in", "engine", "build", "assistant"];
46
63
  const PACKAGE_NAME = "@niroai/niro";
47
-
48
- /** Offered in the welcome screen and again at the end. Not shown to returning users. */
49
- const BOOK_A_CALL = "https://tidycal.com/nirajaswani/45-minutes-meeting-mpy606v";
50
-
51
- /**
52
- * Private-beta copy. There is no endpoint that reports whether the gate is on, and
53
- * adding one to serve a first prompt is not worth the round-trip, so this is a local
54
- * constant. DELETE THIS BLOCK (and betaGate below) when the beta ends — see
55
- * docs/usecases/first-run-wizard.md.
56
- */
57
- const BETA = {
58
- on: true,
59
- // The marketing site, NOT the app: niro.ai, not the niroai.dev hosts everything else in
60
- // this CLI talks to. Confirmed by the user 2026-08-27 — it cannot be derived from anything
61
- // in this repo, so it must not be guessed at from the backend hostnames.
62
- requestUrl: "https://www.niro.ai",
63
- requestAction: '"Request Access"', // the button on that home page
64
- };
64
+ const GB = 1024 ** 3;
65
65
 
66
66
  /** Set on the re-exec so the installed copy can never bootstrap a second time. */
67
67
  const BOOTSTRAP_ENV = "NIRO_START_BOOTSTRAPPED";
68
68
 
69
+ /** Where the graph lives, said plainly on the ready screen: niro-local's one SQLite file. */
70
+
71
+ // The setup rail: ┌ opens, │ carries, ◆ is the step under way, ◇ a finished one, └ closes.
72
+ const rail = (s = "") => console.log(s ? `│ ${s}` : "│");
73
+ const num = (name) => `${order.indexOf(name) + 1}/${order.length}`;
74
+ // A step that already printed its ◆ heading closes without repeating the number.
75
+ const opened = new Set();
76
+ const active = (name, title) => { opened.add(name); console.log(`◆ ${num(name)} ${title}`); };
77
+ const done = (name, text) => console.log(opened.has(name) ? `◇ ${text}` : `◇ ${num(name)} ${text}`);
78
+
79
+ /** Time one step and leave a note of how it ended. `fn` returns its outcome, or throws. */
80
+ async function timed(step, fn) {
81
+ const startedAt = new Date().toISOString();
82
+ let outcome = "error";
83
+ try {
84
+ outcome = await fn();
85
+ return outcome;
86
+ } finally {
87
+ onboardingLog.record({ step, startedAt, endedAt: new Date().toISOString(), outcome: String(outcome) });
88
+ }
89
+ }
90
+
91
+ /** Stop the wizard: every step returns a string outcome, and these are the ones that end it. */
92
+ const GO = "ok";
93
+ const STOPPED = new Set(["refused", "waiting", "blocked", "request_submitted", "failed", "handed_over"]);
94
+
69
95
  async function start(pathArg, opts = {}) {
96
+ opened.clear();
70
97
  if (prompt.isNonInteractive()) {
71
98
  console.error(fail("`niro start` is interactive and there is no terminal to prompt on."));
72
- console.error(" For scripts and agents use the individual commands, which take flags:");
73
- console.error(` ${color.cyan("niro login --api-key")}`);
74
- console.error(` ${color.cyan("niro init --defaults")}`);
99
+ console.error(" Run it once in a terminal: signing in opens your browser. After that, these take flags:");
100
+ console.error(` ${color.cyan("niro engine install")}`);
101
+ console.error(` ${color.cyan("niro init <folder>")}`);
75
102
  console.error(` ${color.cyan("niro mcp install --client <id>")}`);
76
103
  process.exitCode = 1;
77
104
  return;
78
105
  }
79
106
 
80
- // One decision, read once and used everywhere the wizard treats a newcomer differently:
81
- // the banner, the welcome screen, the what-is-installed note, the beta gate and the
82
- // booking offer. Separate checks would eventually disagree with each other.
83
- const firstRun = !hasCredentials();
107
+ const dir = path.resolve(pathArg || process.cwd());
84
108
 
85
- // True in the process step 1 handed over to. That process has no credentials yet, so it
86
- // looks like a first run and would replay the welcome screen and ask for the access code a
87
- // SECOND time. The parent already showed both and already got an answer; repeating them
88
- // makes the install look like a restart. Observed live: the whole opening printed twice.
89
- const handedOver = !!process.env[BOOTSTRAP_ENV];
109
+ // The handed-over child repeats nothing the parent already printed.
110
+ if (!process.env[BOOTSTRAP_ENV]) printWelcome();
90
111
 
91
- console.log(`\n ${color.bold("Niro")} ${color.dim("·")} ${firstRun ? "first-time setup" : "set up another repo"}\n`);
112
+ const ctx = { dir, claims: null, built: null, worktree: null };
113
+ const run = {
114
+ machine: () => checkMachine(ctx, pathArg),
115
+ "sign-in": () => signIn(ctx),
116
+ engine: () => ensureEngine(),
117
+ build: () => buildGraph(ctx),
118
+ assistant: () => connectAssistant(ctx),
119
+ };
120
+ for (const step of order) {
121
+ const outcome = await timed(step, run[step]);
122
+ if (STOPPED.has(outcome)) return;
123
+ }
124
+ await moveWebFolders(ctx);
125
+ await keepItCurrent(ctx);
126
+ await printReady(ctx);
127
+ }
92
128
 
93
- if (firstRun && !handedOver) printWelcome();
129
+ /** Close the rail the way it opened: the outcome on └, then what Niro does, off the rail. */
130
+ const LINKS = {
131
+ app: "https://console.niroai.dev/",
132
+ docs: "https://help.niroai.dev/cli/",
133
+ discord: "https://discord.gg/gcbzePZHn",
134
+ };
94
135
 
95
- // 0. Beta gate. Skipped for anyone already holding credentials: they plainly have an
96
- // account, and asking again would be theatre. Also skipped after a hand-over, where it
97
- // was answered a moment ago in the parent.
98
- if (!handedOver && !(await betaGate())) return;
136
+ function closeReady(who = "your assistant", restart = false) {
137
+ rail();
138
+ console.log(`└ ${color.bold(ok("Niro is ready"))}`);
139
+ // Right under "Niro is ready": what to do next, when an assistant was just connected.
140
+ if (restart) console.log(color.bold(`Restart ${who} to start using Niro.`));
141
+ console.log("");
142
+ console.log(`Carry on using ${who} as usual, and ask it about your code. When it needs`);
143
+ console.log(`facts, it calls Niro, and you will see lines such as ${color.cyan("niro · get_reverse_call_chain")}.`);
144
+ console.log(`\n${color.bold(color.cyan("Useful commands"))}`);
145
+ for (const [cmd, what] of USEFUL) console.log(` ${color.cyan(cmd.padEnd(18))}${what}`);
146
+ const link = (label, url) => `${color.dim(label)} ${color.cyan(url)}`;
147
+ console.log(`\n${[link("App", LINKS.app), link("Docs", LINKS.docs), link("Discord", LINKS.discord)].join(" ")}`);
148
+ console.log("");
149
+ }
99
150
 
100
- // 1. Bootstrap. Returns true when it re-execed, meaning this process is done.
101
- if (await bootstrapIfNpx(pathArg, firstRun)) return;
151
+ function printWelcome() {
152
+ console.log("");
153
+ console.log(`┌ Niro setup ${color.dim(`v${require("../../package.json").version}`)}`);
154
+ rail();
155
+ rail(color.bold("Give your AI assistant verified facts about your code: call chains, impact, cross-repo links."));
156
+ rail(color.dim("Built on this machine · 5 steps · about 5 minutes"));
157
+ rail();
158
+ }
102
159
 
103
- // 2. Sign in.
104
- if (!(await ensureSignedIn())) return;
160
+ /** Step 1. Nothing here contacts Niro or changes the machine, bar the npx hand-over. */
161
+ async function checkMachine(ctx, pathArg) {
162
+ active("machine", "Checking this machine");
163
+ const m = machineCheck.check();
164
+ if (m.platformProblem) {
165
+ rail(`${fail("✗")} ${m.os}, ${m.arch}`);
166
+ rail(m.platformProblem);
167
+ rail("Niro Local runs on macOS and Linux. On Windows, use WSL 2.");
168
+ console.log("└");
169
+ process.exitCode = 1;
170
+ return "refused";
171
+ }
172
+ rail(`${ok("✓")} ${m.os}, ${m.arch}`);
173
+ if (m.freeBytes !== null) {
174
+ const free = `${(m.freeBytes / GB).toFixed(1)} GB free`;
175
+ rail(m.lowDisk ? `${warn("⚠")} ${free}. A large repository's graph may need more.` : `${ok("✓")} ${free}`);
176
+ }
177
+ if (!m.nodeOk) {
178
+ rail(`${fail("✗")} Node ${m.node}. Niro needs Node 18 or newer.`);
179
+ console.log("└");
180
+ process.exitCode = 1;
181
+ return "refused";
182
+ }
105
183
 
106
- // 3. Choose the folder.
107
- const target = await chooseFolder(pathArg);
108
- if (!target) return;
184
+ const problem = describeProblem(ctx.dir);
185
+ if (problem) {
186
+ rail(`${fail("✗")} ${problem.headline}`);
187
+ for (const line of problem.detail) rail(line);
188
+ console.log("└");
189
+ process.exitCode = 1;
190
+ return "refused";
191
+ }
192
+ rail();
193
+ return (await bootstrapIfNpx(pathArg)) ? "handed_over" : GO;
194
+ }
109
195
 
110
- // 4. Onboard. init re-validates the folder itself; it stays the authority.
111
- console.log(`\n ${step(`4/${TOTAL_STEPS}`)} Onboarding and building the index\n`);
112
- const initCmd = require("./init");
113
- // Everything init prints belongs under this step, including what its own helpers print.
114
- const { withIndent } = require("../lib/nestedOutput");
115
- await withIndent(4, () => initCmd.init(target, { nested: true }));
116
- if (process.exitCode) return; // init already explained why
196
+ /** Step 2. login.js prints its own outcome; this only decides whether to go on. */
197
+ async function signIn(ctx) {
198
+ if (!hasCredentials()) {
199
+ active("sign-in", "Sign in");
200
+ rail("Sign in, choose a plan and approve this terminal in one screen.");
201
+ const { withPrefix } = require("../lib/nestedOutput");
202
+ const result = await withPrefix("│", () => require("./login").login({}));
203
+ if (result && result.waiting) return "waiting";
204
+ if (result && result.blocked) return "blocked";
205
+ if (result && result.requestSubmitted) return "request_submitted";
206
+ if (!hasCredentials()) {
207
+ console.error(fail("\n✗ Not signed in, so there is nothing to set up yet."));
208
+ console.error(` Run ${color.cyan("niro start")} again when you are ready.`);
209
+ process.exitCode = 1;
210
+ return "failed";
211
+ }
212
+ }
213
+ const email = credentials.load().email;
214
+ ctx.claims = await lease.currentClaims().catch(() => null);
215
+ const plan = ctx.claims && ctx.claims.plan_name ? ` · ${ctx.claims.plan_name}` : "";
216
+ done("sign-in", `${ok("✓")} Signed in${email ? ` as ${email}` : ""}${plan}`);
217
+ return GO;
218
+ }
117
219
 
118
- // 5. Connect an assistant.
119
- await connectAssistant();
220
+ /** Step 3. */
221
+ async function ensureEngine() {
222
+ const found = engine.resolve();
223
+ if (found) {
224
+ if (!(await graphOpens())) return "failed";
225
+ done("engine", ok(`✓ Engine ready${engineVersion(found.path)}`));
226
+ return GO;
227
+ }
228
+ active("engine", "Local engine");
229
+ let where;
230
+ try {
231
+ // engine.ensure says "Downloading the local engine <version> for <label>..." itself.
232
+ where = await engine.ensure({ log: (line) => rail(line) });
233
+ } catch (err) {
234
+ rail(`${fail("✗")} ${err && err.message ? err.message : err}`);
235
+ if (!err || err.code !== "EENGINEREFUSED") rail(`Try again with ${color.cyan("niro start")}.`);
236
+ console.log("└");
237
+ process.exitCode = 1;
238
+ return "failed";
239
+ }
240
+ if (!(await graphOpens())) return "failed";
241
+ rail(ok(`✓ Engine ready${engineVersion(where)}`));
242
+ rail();
243
+ return GO;
244
+ }
120
245
 
121
- if (firstRun) await offerMeasurement();
246
+ /** Can the engine open its graph file? If not, its own words, and the wizard stops. */
247
+ async function graphOpens() {
248
+ const usable = await graphCheck.graphDatabaseUsable();
249
+ if (usable.ok) return true;
250
+ rail(`${fail("✗")} ${usable.problem}`);
251
+ console.log("└");
252
+ process.exitCode = 1;
253
+ return false;
254
+ }
122
255
 
123
- printNextSteps(firstRun);
256
+ /** Step 4. */
257
+ async function buildGraph(ctx) {
258
+ const known = await localOnboard.worktreeFor(ctx.dir);
259
+ ctx.worktree = known;
260
+ if (known && known.worktree.indexed) {
261
+ const when = known.worktree.last_build_at ? `, ${ago(known.worktree.last_build_at)}` : "";
262
+ done("build", ok(`✓ Graph built for ${known.project.name}${when}`));
263
+ return GO;
264
+ }
265
+ active("build", "Building your graph");
266
+ rail(`Project ${known ? known.project.name : path.basename(ctx.dir)}`);
267
+ // At or over the limit the engine refuses a folder that adds new lines, so the same question is
268
+ // asked of the engine here first (`refusesBuild`) and such a folder goes to the upgrade page, not to
269
+ // an error. A folder the engine already counts (a temporary copy, a service inside an indexed root)
270
+ // builds.
271
+ const before = await planUsage.linesUsed();
272
+ if (await planUsage.refusesBuild(ctx.claims, before, ctx.dir)) return overLimit(ctx, before);
273
+ const bar = progressBar.create({ prefix: "│ " });
274
+ const onProgress = (d, n) => bar.update(d, n);
275
+ try {
276
+ // A folder not added yet: the engine's `init` adds it and builds it, in one command, so it is built once. A
277
+ // folder added but not built yet (a step 4 that stopped part way): its build.
278
+ ctx.built = known ? await localOnboard.build(ctx.dir, onProgress) : await localOnboard.initAndBuild(ctx.dir, onProgress);
279
+ } catch (err) {
280
+ bar.stop();
281
+ const words = (err && (err.stderr || err.message)) || String(err);
282
+ if (/^Nothing that looks like a project in /m.test(words)) {
283
+ // The engine's refusal names niro-local and its own command; say it in niro start's words.
284
+ rail(`${fail("✗")} No project found here`);
285
+ rail(` Niro looks for a build file (package.json, pom.xml, go.mod, …) in this folder or up to 3 folders down.`);
286
+ rail(` Run ${color.cyan("niro start")} in your project's folder, or index this one anyway: ${color.cyan("niro init --force")}`);
287
+ rail(` ${color.dim(`Languages supported: ${languages.supportedList()}.`)}`);
288
+ } else {
289
+ for (const line of words.split("\n")) rail(`${line.replace(/^error:\s*/, "")}`);
290
+ rail(`${fail("✗")} The graph was not built. Fix the above and run ${color.cyan("niro start")} again.`);
291
+ }
292
+ console.log("└");
293
+ process.exitCode = 1;
294
+ return "failed";
295
+ }
296
+ bar.stop();
297
+ done("build", ok(`✓ Graph built: ${counts(ctx.built)} in ${ctx.built.seconds} s`));
298
+ if (!foundLanguages(ctx).length) {
299
+ rail(`${warn("⚠")} No code in a supported language here. Niro reads ${languages.supportedList()}.`);
300
+ }
301
+ const used = await planUsage.linesUsed();
302
+ ctx.plan = planUsage.describe(ctx.claims, used);
303
+ ctx.noRoom = planUsage.noRoomNote(ctx.claims, used);
304
+ // The plan is shown once, in the Setup complete summary (2026-11-11), not under the build line.
305
+ if (ctx.noRoom) {
306
+ rail(color.yellow(ctx.noRoom));
307
+ rail(`Run ${color.cyan("niro plan")} to unlock unlimited lines or start a Pro free trial, or free lines with ${color.cyan("niro remove --path <folder>")}.`);
308
+ }
309
+ rail();
310
+ return GO;
124
311
  }
125
312
 
126
- /**
127
- * The opening screen: what this thing is for, before asking anyone to install it.
128
- *
129
- * Four lines, not a tour. Someone who has just typed an unfamiliar command deserves to know
130
- * what they are about to spend five minutes on, and someone evaluating deserves to know a
131
- * person is available before they invest that time — which is why the call is offered here
132
- * and not only at the end.
133
- */
134
- function printWelcome() {
135
- console.log(" Niro maps how your code actually fits together, so your AI");
136
- console.log(" assistant answers from that map instead of guessing.\n");
137
- // "in", not "with": the point is that there is no new tool and no new window, you ask
138
- // inside the thing already open. "it" is dropped because it read ambiguously against
139
- // "the assistant" — and without it the line fits on one.
140
- console.log(` ${color.cyan("·")} Ask anything about your code, in the assistant you already use`);
141
- console.log(` ${color.cyan("·")} See what breaks before you change it, across services`);
142
- console.log(` ${color.cyan("·")} Get a standing review of the issues already in your code`);
143
- console.log(` ${color.cyan("·")} Find bugs' root causes from your alerts`);
144
- console.log(` ${color.cyan("·")} Measure it: run half your sessions without Niro and compare\n`);
145
- console.log(" Setting up takes about five minutes.");
146
- console.log(" Prefer a walkthrough first? 45 minutes with us:");
147
- console.log(` ${color.cyan(BOOK_A_CALL)}\n`);
313
+ /** No room under the line limit: say so in two lines and open the console's choice page. */
314
+ function overLimit(ctx, used) {
315
+ const max = ctx.claims.max_lines;
316
+ const n = (x) => x.toLocaleString("en-US");
317
+ done("build", `${fail("✗")} Not built: this machine already holds ${n(used)} lines of code, over the ${n(max)} line limit`);
318
+ const url = planUsage.limitUrl(used, max);
319
+ rail(`Choose how to continue: unlock unlimited lines, a Pro free trial, or stay under the limit.`);
320
+ rail(`Opening ${color.cyan(url)}`);
321
+ require("../lib/browser").open(url);
322
+ rail(`Then run ${color.cyan("niro start")} again. ${color.dim(`Or see the options here: niro plan`)}`);
323
+ rail(`To free lines instead, stop indexing a folder: ${color.cyan("niro remove --path <folder>")}`);
324
+ console.log("└");
325
+ process.exitCode = 1;
326
+ return "failed";
148
327
  }
149
328
 
150
329
  /**
151
- * Offer the A/B measurement window, once, at the end.
152
- *
153
- * Not in the opening, even though the capability is claimed there: enabling needs a signed-in
154
- * account, and asking someone to measure a thing they have not yet seen work is the wrong
155
- * order. First run only, and it defaults to NO — this is a measurement option, not something
156
- * to be nudged into. A returning user finds it in the next-steps list instead.
157
- *
158
- * Never fails the wizard. The repo IS indexed by this point; a measurement window is a nicety
159
- * and a network hiccup here must not read as a broken setup.
330
+ * Step 5. The `niro mcp install` wizard, which keeps the full client list. `[]` so the
331
+ * wizard's own argv (`start <path>`) is never forwarded to it as client flags.
160
332
  */
161
- async function offerMeasurement() {
162
- const abCmd = require("./ab");
163
- console.log("\n One more thing, entirely optional.\n");
164
- console.log(` Niro can run ${color.bold("off")} for half your sessions for the next`);
165
- console.log(` ${abCmd.DEFAULT_WINDOW_DAYS} days, so you can see the difference rather than take`);
166
- console.log(" our word for it. It applies to your whole account, and");
167
- console.log(` ${color.cyan("niro ab disable")} stops it at any time.\n`);
168
-
169
- if (!(await prompt.confirm(" Turn on A/B measurement?", false))) {
170
- console.log(`\n Left off. Turn it on later with ${color.cyan("niro ab enable")}.`);
171
- return;
333
+ async function connectAssistant(ctx) {
334
+ if (config.load().mcpConnectedAt) {
335
+ const names = assistantNames();
336
+ done("assistant", ok(`✓ ${names.length ? names.join(", ") : "Assistant"} connected`));
337
+ return GO;
172
338
  }
339
+ active("assistant", "Connect your AI assistant");
173
340
  try {
174
- await abCmd.enable({ yes: true });
341
+ const { withPrefix } = require("../lib/nestedOutput");
342
+ await withPrefix("│", () => require("./mcp").install([]));
343
+ config.save({ mcpConnectedAt: new Date().toISOString() });
344
+ done("assistant", ok("✓ Assistant connected"));
345
+ ctx.restartAssistant = true; // said after "Niro is ready", where it is the next thing to do
175
346
  } catch (err) {
176
- console.log(`\n ${warn("⚠")} Could not start the measurement window.`);
177
- console.log(` Your repo is indexed and working. Try ${color.cyan("niro ab enable")} later.`);
178
- console.log(` (${err && err.message ? err.message : err})`);
347
+ // The graph IS built by now; one command retries this. A wizard that printed to the terminal has
348
+ // already said why ("✗ Claude Code could not be set up"), so its exit code is not repeated.
349
+ done("assistant", warn("⚠ No assistant connected"));
350
+ if (!(err && err.alreadySaid)) rail(`Could not configure an assistant: ${err && err.message ? err.message : err}`);
351
+ rail(`Run ${color.cyan("niro mcp install")} when you are ready.`);
352
+ rail();
353
+ return "skipped";
179
354
  }
355
+ rail();
356
+ return GO;
180
357
  }
181
358
 
182
359
  /**
183
- * What to do now that a repo is indexed.
184
- *
185
- * Ordered by when they will need it, not by how impressive it is: one question they can paste
186
- * straight away, then the three things they will reach for over the next few days. The web app
187
- * line is where code review and the graph views get named, because those are things you look
188
- * at rather than ask for.
189
- *
190
- * The web address is whatever the server told us at login, never a constant: a self-hosted
191
- * install has its own, and a wrong link here is worse than none. Omitted when unknown, which
192
- * is the case for anyone who logged in before the CLI started recording it.
360
+ * Folders set up with Niro web, in a release without the cloud: say they are no longer uploaded,
361
+ * offer to index them here, then retire the list so it is said once. The folder just built is
362
+ * skipped, and indexing stops at the line limit rather than sending anyone to the upgrade page.
193
363
  */
194
- function printNextSteps(firstRun) {
195
- const appUrl = config.load().appUrl;
196
-
197
- console.log(`\n ${ok("✓ You are set up.")}`);
198
- console.log("\n Ask this in the assistant you just connected, from this folder:\n");
199
- console.log(` ${color.bold('"What are the entry points into this repo, and what does each one do?"')}`);
200
-
201
- console.log("\n When you need them:\n");
202
- if (appUrl) {
203
- console.log(` ${color.cyan(appUrl)}`);
204
- console.log(" the graph, impact views and your code review, in the browser");
205
- }
206
- console.log(` ${color.cyan("niro start")} in another repo`);
207
- console.log(" cross-service answers need more than one repo indexed");
208
- console.log(` ${color.cyan("niro new-temp-project")}`);
209
- console.log(" run it once on a branch Niro has not indexed, so answers match your work");
210
- console.log(` ${color.cyan("niro status")}`);
211
- console.log(" check the index is current");
212
- console.log(` ${color.cyan("niro ab")}`);
213
- console.log(" measure what Niro changes, by running it off half the time");
214
-
215
- if (firstRun) {
216
- console.log(`\n Questions, or want us to walk through your codebase with you?`);
217
- console.log(` 45 minutes with us: ${color.cyan(BOOK_A_CALL)}`);
364
+ async function moveWebFolders(ctx, { folders = watchState.allLocal, ask = prompt.confirm,
365
+ init = (dir) => require("./initLocal").initLocal(dir, { openUrl: () => {} }) } = {}) {
366
+ if (release.cloud) return;
367
+ const web = (() => { try { return folders() || []; } catch { return []; } })();
368
+ if (web.length === 0) return;
369
+ for (const line of migrationNotice.webLines(web)) console.log(line);
370
+ const here = web.filter((f) => path.resolve(f.folderPath) !== ctx.dir && fs.existsSync(f.folderPath));
371
+ if (here.length > 0 && await ask(` Index ${here.length === 1 ? "it" : `these ${here.length} folders`} on this machine now?`, false)) {
372
+ for (const f of here) {
373
+ const used = await planUsage.linesUsed().catch(() => 0);
374
+ if (await planUsage.refusesBuild(ctx.claims, used, f.folderPath)) {
375
+ console.log(` Stopped: the line limit is reached. See ${color.cyan("niro plan")}.`);
376
+ break;
377
+ }
378
+ await init(f.folderPath);
379
+ process.exitCode = 0;
380
+ }
218
381
  }
382
+ try { watchState.retireToWeb(); } catch { /* kept as is; the note shows again next time */ }
219
383
  console.log("");
220
384
  }
221
385
 
222
386
  /**
223
- * What one `npm install` actually puts on the machine.
224
- *
225
- * Printed at the install step, because that is the moment software lands and the honest time
226
- * to say what it is. Three things arrive from one package and nothing else in the flow says
227
- * so: the CLI is visible (they just typed it), but the MCP server and the skills are not, and
228
- * a developer who does not know the skills exist will never think to ask for a rebuild in
229
- * plain English.
230
- *
231
- * First run only, like the welcome screen. Someone setting up their fifth repo knows.
387
+ * True when niro-watch started before this copy was installed, so it still runs the code of the
388
+ * version before: `npm install -g` swaps the files but not a running process.
232
389
  */
233
- function printWhatIsInstalled() {
234
- console.log(" One package, three parts:\n");
235
- console.log(` ${color.bold("niro")} the command you are running now: onboard a repo,`);
236
- console.log(" check status, rebuild, manage projects");
237
- console.log(` ${color.bold("MCP server")} how your AI assistant reaches Niro's map of your`);
238
- console.log(" code, so it answers from that instead of guessing");
239
- console.log(` ${color.bold("two skills")} so you can say \"index this repo\" or \"what breaks`);
240
- console.log(" if I change this\" instead of remembering commands");
241
- console.log("");
390
+ function watcherIsOld() {
391
+ const at = service.startedAt();
392
+ if (!at) return false;
393
+ try {
394
+ return at < fs.statSync(path.join(__dirname, "..", "..", "package.json")).ctimeMs; // ctime: npm may stamp a fixed mtime
395
+ } catch {
396
+ return false;
397
+ }
242
398
  }
243
399
 
244
- /**
245
- * Step 0. Returns false when the user has no access code, having changed nothing.
246
- *
247
- * Deliberately NOT a server capability probe: see BETA above.
248
- */
249
- async function betaGate() {
250
- if (!BETA.on) return true;
251
- if (hasCredentials()) return true; // already has an account on this machine
252
-
253
- console.log(" Niro is in private beta. To create an account you need:");
254
- console.log(" · an access code (the browser signup form asks for it by that name)");
255
- console.log(" · a work email address (personal addresses are not accepted yet)\n");
256
- console.log(` No access code? Click ${BETA.requestAction} at ${color.cyan(BETA.requestUrl)}\n`);
257
-
258
- const hasCode = await prompt.confirm(" Do you have an access code?", true);
259
- if (hasCode) return true;
260
-
261
- console.log(`\n No problem. Open ${color.cyan(BETA.requestUrl)} and click ${BETA.requestAction},`);
262
- console.log(" then run this again when your code arrives:\n");
263
- console.log(` ${color.cyan(`npx ${PACKAGE_NAME} start`)}\n`);
264
- console.log(" Nothing was installed or changed on this machine.\n");
265
- return false;
400
+ /** The watch daemon, so "updates automatically" on the ready screen is true. */
401
+ async function keepItCurrent(ctx) {
402
+ if (watchDaemon.watchDaemonState() === "running") {
403
+ // A watcher saved by an older niro-cli holds the whole shell environment in ~/.pm2/dump.pm2:
404
+ // this starts it again with only what it needs (no password, the login hook is left alone).
405
+ let refreshed;
406
+ try { refreshed = service.refreshSavedEnv(); } catch { refreshed = null; }
407
+ if (refreshed !== null) {
408
+ if (!refreshed && watcherIsOld()) service.restart();
409
+ ctx.watching = true;
410
+ return;
411
+ }
412
+ // Starting it again failed, so it is not running now: the steps below install it as usual.
413
+ }
414
+ const heading = "◆ Keeping your graph current";
415
+ console.log(heading);
416
+ const { withPrefix } = require("../lib/nestedOutput");
417
+ const r = await withPrefix("│", () => watchDaemon.ensureWatchDaemon({ quiet: true, heading }));
418
+ ctx.watching = !!(r && r.ok);
419
+ if (!ctx.watching) console.log(`◇ ${warn("⚠")} The graph will not update on its own. Run ${color.cyan("niro service install")} to retry`);
420
+ else if (r.autostart === false) console.log(`◇ ${ok("✓")} Updating as files change, until you log out`);
421
+ else console.log(`◇ ${ok("✓")} Updating as files change, and after every login`);
422
+ rail();
423
+ }
424
+
425
+ /** The commands worth knowing after setup, on the ready screen. */
426
+ const USEFUL = [
427
+ ["niro init", "Add another repo: run it inside that folder"],
428
+ ["niro status", "Check this folder's graph"],
429
+ ["niro plan", "See your line usage, folders and options"],
430
+ ["niro mcp install", "Connect another AI assistant"],
431
+ ["niro uninstall", "Remove Niro from this machine"],
432
+ ];
433
+
434
+ /** The folder's languages, read and not, looked at once per run: listing a large repository takes a while. */
435
+ function scanLanguages(ctx) {
436
+ if (!ctx.languages) ctx.languages = languages.scan(ctx.dir);
437
+ return ctx.languages;
438
+ }
439
+
440
+ const foundLanguages = (ctx) => scanLanguages(ctx).found;
441
+
442
+ /** The request command for a language name, quoted when the shell would read it otherwise ("visual basic", "f#"). */
443
+ function requestCommand(name) {
444
+ const word = name.toLowerCase();
445
+ return `niro request-integration ${/^[a-z0-9.+-]+$/.test(word) ? word : `"${word}"`}`;
446
+ }
447
+
448
+ async function printReady(ctx) {
449
+ const names = assistantNames();
450
+ console.log(`${ok("◆")} Setup complete`);
451
+ const last = ctx.worktree && ctx.worktree.worktree && ctx.worktree.worktree.last_build_at;
452
+ rail(`Graph ${ctx.built ? `${counts(ctx.built)} · built just now` : last ? `built ${ago(last)}` : "built"}`);
453
+ const { found, others } = scanLanguages(ctx);
454
+ rail(`Languages ${found.length ? languages.short(found, 4) : `none found. Niro reads ${languages.supportedList()}`}`);
455
+ if (others.length) {
456
+ rail(`Not read ${languages.short(others, 3)}. Ask for ${others.length === 1 ? "it" : "one"}: ${color.cyan(requestCommand(others[0]))}`);
457
+ }
458
+ rail(`Assistant ${names.length ? names.join(", ") : `not connected. Run ${color.cyan("niro mcp install")}`}`);
459
+ rail(`Sync ${ctx.watching ? "on, updates as files change" : `off. Run ${color.cyan("niro service install")}`}`);
460
+ const plan = ctx.plan || planUsage.describe(ctx.claims, await planUsage.linesUsed());
461
+ if (plan) rail(`Plan ${plan}`);
462
+ if (ctx.noRoom) {
463
+ rail(` ${color.yellow(ctx.noRoom)}`);
464
+ rail(` Run ${color.cyan("niro plan")} to unlock unlimited lines or start a Pro free trial.`);
465
+ }
466
+ closeReady(names.length === 1 ? names[0] : "your assistant", !!ctx.restartAssistant);
467
+ }
468
+
469
+ function assistantNames() {
470
+ try {
471
+ return mcpClients.clientNames(mcpClients.configuredClients());
472
+ } catch {
473
+ return [];
474
+ }
475
+ }
476
+
477
+ /** " (0.1.1)": the version folder of an engine from the download cache, else nothing. */
478
+ function engineVersion(executable) {
479
+ const m = /[\\/]bin[\\/]([^\\/]+)[\\/]niro-local-[^\\/]+[\\/]/.exec(String(executable || ""));
480
+ return m ? ` (${m[1]})` : "";
266
481
  }
267
482
 
483
+ /** "just now", "5 min ago", "3 h ago", then the local date: when the graph was last built. */
484
+ function ago(iso, now = Date.now()) {
485
+ const minutes = Math.floor((now - Date.parse(iso)) / 60000);
486
+ if (!Number.isFinite(minutes)) return `last built ${iso}`;
487
+ if (minutes < 1) return "just now";
488
+ if (minutes < 60) return `${minutes} min ago`;
489
+ if (minutes < 24 * 60) return `${Math.floor(minutes / 60)} h ago`;
490
+ return `on ${new Date(iso).toLocaleDateString("en-CA")}`;
491
+ }
492
+
493
+ function counts(b) {
494
+ const parts = [];
495
+ if (b.files !== null && b.files !== undefined) parts.push(`${b.files.toLocaleString("en-US")} files`);
496
+ if (b.lines !== null && b.lines !== undefined) parts.push(`${b.lines.toLocaleString("en-US")} lines`);
497
+ return parts.join(", ") || "done";
498
+ }
499
+
500
+
268
501
  /**
269
502
  * The same condition credentials.requireCreds() enforces, and deliberately not a second
270
503
  * definition of it: any usable credential counts, because the request layer refreshes an
@@ -293,7 +526,7 @@ function runningFromNpxCache(argv1 = process.argv[1]) {
293
526
  * npm's own global prefix is the only answer that cannot be the cache.
294
527
  */
295
528
  function globalNiroPath() {
296
- const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: true, encoding: "utf8" });
529
+ const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: process.platform === "win32", encoding: "utf8" });
297
530
  if (r.status !== 0) return null;
298
531
  const prefix = String(r.stdout || "").trim();
299
532
  if (!prefix) return null;
@@ -332,27 +565,18 @@ function isOlder(a, b) {
332
565
  * prompted for: the alternative is a question whose wrong answer produces a silently
333
566
  * dead watch daemon days later.
334
567
  */
335
- async function bootstrapIfNpx(pathArg, firstRun = false) {
336
- const handedOver = !!process.env[BOOTSTRAP_ENV];
337
- if (handedOver || !runningFromNpxCache()) {
338
- // Nothing to do: either this IS the handed-over child, or niro was installed
339
- // properly in the first place. Still announced, so the wizard shows all five steps
340
- // and reads as a finite process rather than an open-ended one.
341
- console.log(` ${step(`1/${TOTAL_STEPS}`)} ${ok("✓")} niro is installed.\n`);
342
- if (firstRun && !handedOver) printWhatIsInstalled();
343
- return false;
344
- }
568
+ async function bootstrapIfNpx(pathArg) {
569
+ // Nothing to do, and nothing said: either this IS the handed-over child, or niro was
570
+ // installed properly in the first place. Step 1's machine lines already cover it.
571
+ if (process.env[BOOTSTRAP_ENV] || !runningFromNpxCache()) return false;
345
572
 
346
- console.log(` ${step(`1/${TOTAL_STEPS}`)} Installing niro\n`);
347
- if (firstRun) printWhatIsInstalled();
348
- console.log(" Background sync has to outlive npx's temporary cache, so Niro");
349
- console.log(" installs itself properly first.\n");
573
+ rail("Installing niro so background updates outlive npx's temporary cache…");
350
574
 
351
575
  let bin = globalNiroPath();
352
576
  if (bin && isCurrent(bin)) {
353
- console.log(` ${ok("✓")} Already installed at ${bin}.\n`);
577
+ rail(`${ok("✓")} Already installed at ${bin}`);
354
578
  } else {
355
- const r = childProcess.spawnSync("npm", ["install", "-g", PACKAGE_NAME], { shell: true, stdio: "inherit" });
579
+ const r = childProcess.spawnSync("npm", ["install", "-g", PACKAGE_NAME], { shell: process.platform === "win32", stdio: "inherit" });
356
580
  bin = r.status === 0 ? globalNiroPath() : null;
357
581
  // Only claim success once the binary is actually THERE. The old wording announced an
358
582
  // install that had been skipped, which is how an empty global folder came to look fine.
@@ -363,7 +587,7 @@ async function bootstrapIfNpx(pathArg, firstRun = false) {
363
587
  process.exitCode = 1;
364
588
  return true;
365
589
  }
366
- console.log(` ${ok("✓")} ${PACKAGE_NAME} installed at ${bin}.\n`);
590
+ rail(`${ok("✓")} ${PACKAGE_NAME} installed at ${bin}`);
367
591
  }
368
592
 
369
593
  const args = ["start"];
@@ -378,84 +602,15 @@ async function bootstrapIfNpx(pathArg, firstRun = false) {
378
602
  return true;
379
603
  }
380
604
 
381
- /** Step 2. Returns false only when login failed. */
382
- async function ensureSignedIn() {
383
- if (hasCredentials()) {
384
- const creds = credentials.load();
385
- console.log(` ${step(`2/${TOTAL_STEPS}`)} ${ok("✓")} Signed in${creds.email ? ` as ${color.bold(creds.email)}` : ""}.\n`);
386
- return true;
387
- }
388
-
389
- console.log(` ${step(`2/${TOTAL_STEPS}`)} Sign in\n`);
390
- console.log(" Opening your browser. Sign in there, or create your account");
391
- console.log(" with your access code.\n");
392
- console.log(" Creating an account? You will get a verification email. Take");
393
- console.log(" your time. This keeps waiting.\n");
394
-
395
- const loginCmd = require("./login");
396
- await loginCmd.login({});
397
- if (!hasCredentials()) {
398
- console.error(fail("\n ✗ Not signed in, so there is nothing to onboard to."));
399
- console.error(` Run ${color.cyan("niro start")} again when you are ready.`);
400
- process.exitCode = 1;
401
- return false;
402
- }
403
- return true;
404
- }
405
-
406
605
  /**
407
- * Step 3. Confirm what is about to be uploaded, and keep asking on a refusal.
408
- *
409
- * The refusal RULES are not re-implemented here: protectedRootReason and
410
- * findProjectIndicators are the same functions init.js validates with, so the preview
411
- * can never disagree with the gate. init.js re-runs the authoritative check.
606
+ * Why this folder can never be indexed, in the wizard's voice, or null when that is for the engine to say.
412
607
  *
413
- * Returns the chosen absolute path, or null if the user gave up.
414
- */
415
- async function chooseFolder(pathArg) {
416
- let candidate = path.resolve(pathArg || process.cwd());
417
-
418
- for (;;) {
419
- console.log(` ${step(`3/${TOTAL_STEPS}`)} Choose what to index\n`);
420
-
421
- const problem = describeProblem(candidate);
422
- if (problem) {
423
- console.log(` ${fail("✗")} ${problem.headline}\n`);
424
- for (const line of problem.detail) console.log(` ${line}`);
425
- console.log("");
426
- const answer = await prompt.ask(" Which folder do you want to index? (blank to give up)");
427
- if (!answer || !answer.trim()) {
428
- console.log(`\n Stopped. Nothing was changed. Run ${color.cyan("niro start")} from a project folder.\n`);
429
- return null;
430
- }
431
- candidate = path.resolve(expandHome(answer.trim()));
432
- console.log("");
433
- continue;
434
- }
435
-
436
- printPreview(candidate);
437
- if (await prompt.confirm(" Index this folder?", true)) return candidate;
438
-
439
- const answer = await prompt.ask(" Which folder instead? (blank to give up)");
440
- if (!answer || !answer.trim()) {
441
- console.log(`\n Stopped. Nothing was changed.\n`);
442
- return null;
443
- }
444
- candidate = path.resolve(expandHome(answer.trim()));
445
- console.log("");
446
- }
447
- }
448
-
449
- /** A leading ~ is expanded here because a prompt answer never goes through a shell. */
450
- function expandHome(p) {
451
- if (p === "~") return require("os").homedir();
452
- if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(require("os").homedir(), p.slice(2));
453
- return p;
454
- }
455
-
456
- /**
457
- * Why this folder cannot be onboarded, in the wizard's voice, or null if it can.
458
- * Mirrors ensureOnboardable's two rules by calling the same functions it calls.
608
+ * Only what needs no engine: a path that is not a folder, and a protected folder (home, the filesystem root, a system
609
+ * folder). Whether the folder holds a project is the engine's question, asked by the engine's own `init`
610
+ * (localOnboard.initAndBuild), because niro-cli's own answer disagreed with it: niro-cli looked for a build file two levels
611
+ * down and had no rule for a folder of repositories, the engine looks three levels down and accepts one, so the
612
+ * engine offered `niro init` for folders `niro init` then refused (VP-release-texts-2 note A; Niraj, 2026-11-06:
613
+ * "niro should ask niro-engine"). `niro start` runs this before the engine is downloaded, so no engine can be asked.
459
614
  */
460
615
  function describeProblem(dir) {
461
616
  if (!fs.existsSync(dir)) {
@@ -469,100 +624,25 @@ function describeProblem(dir) {
469
624
  if (reason) {
470
625
  return {
471
626
  headline: `${dir} is ${reason}. Niro will not index it.`,
472
- // Says only what is true. folderSync never descends into a dot directory
473
- // (folderSync.js walkDir) and only uploads source, build and committed config files
474
- // (isUploadableName), so keys, documents and photos are never eligible and claiming
475
- // otherwise is scaremongering. What IS lost is .gitignore: it is only honoured when
476
- // the onboarded folder is itself a git work tree, and a home folder is not one, so
477
- // each nested repo's ignored files stop being protected.
478
627
  detail: [
479
- "It would upload source from every repository under it, and lose the",
480
- ".gitignore protection each one has, because the folder you point at",
481
- "has to be the git repository for that to apply.",
482
- ],
483
- };
484
- }
485
-
486
- const { indicators } = onboardTarget.findProjectIndicators(dir);
487
- if (indicators.size === 0) {
488
- return {
489
- headline: `No project found in ${dir}.`,
490
- detail: [
491
- "Niro looks for a build file (package.json, pom.xml, go.mod, ...) in",
492
- "this folder or just below it. None found.",
493
- `If you really mean this folder, use ${color.cyan("niro init --force")} instead.`,
628
+ "It would index every repository under it as one project.",
629
+ "Run niro start from your project's folder instead.",
494
630
  ],
495
631
  };
496
632
  }
497
633
  return null;
498
634
  }
499
635
 
500
- /**
501
- * The screen that prevents the accident: what is about to be uploaded, before it is.
502
- *
503
- * Deliberately cheap — no full tree walk. The project scan has already run for the
504
- * refusal check and git.inspect reads two small files, so this adds nothing the wizard
505
- * was not doing anyway. init.js does the real manifest walk a moment later.
506
- */
507
- function printPreview(dir) {
508
- const { roots, indicators } = onboardTarget.findProjectIndicators(dir);
509
- const info = git.inspect(dir);
510
-
511
- console.log(` Folder: ${color.bold(dir)}`);
512
- console.log(` Detected: ${onboardTarget.describeIndicators(indicators)}`);
513
- console.log(` Projects: ${roots} module${roots === 1 ? "" : "s"}`);
514
- if (info.repoRoot) {
515
- console.log(` Git: branch ${info.branch || "(detached)"}${info.originUrl ? `, ${info.originUrl}` : ""}`);
516
- } else {
517
- console.log(` Git: ${color.dim("not a git repo, which is fine; it uploads as-is")}`);
518
- }
519
- console.log("");
520
- }
521
-
522
- /**
523
- * Step 5. `mcp install` prompts for the client itself and resolves the key from the
524
- * login session, so there is nothing to pass it.
525
- *
526
- * "Already connected" is a wizard-local flag rather than a probe of 14 clients' config
527
- * files: those paths live inside src/mcp/setup.js and are not exported, and a wrong
528
- * guess here would silently skip the step that makes Niro usable. The flag only ever
529
- * records what THIS wizard did, and re-running mcp install is harmless anyway.
530
- */
531
- async function connectAssistant() {
532
- const saved = config.load();
533
- if (saved.mcpConnectedAt) {
534
- console.log(`\n ${step(`5/${TOTAL_STEPS}`)} ${ok("✓")} Assistant already connected on this machine.`);
535
- console.log(` Connect another with ${color.cyan("niro mcp install")}.`);
536
- return;
537
- }
538
-
539
- console.log(`\n ${step(`5/${TOTAL_STEPS}`)} Connect your AI assistant\n`);
540
- const mcpCmd = require("./mcp");
541
- try {
542
- await mcpCmd.install();
543
- config.save({ mcpConnectedAt: new Date().toISOString() });
544
- } catch (err) {
545
- // Never fail the whole wizard here: the repo IS indexed by this point, and the
546
- // user can retry a single command.
547
- console.log(`\n ${warn("⚠")} Could not configure an assistant automatically.`);
548
- console.log(` Run ${color.cyan("niro mcp install")} when you are ready.`);
549
- console.log(` (${err && err.message ? err.message : err})`);
550
- }
551
- }
552
636
 
553
637
  module.exports = {
554
638
  start,
555
639
  // exported for tests
556
640
  hasCredentials,
557
- printWelcome,
558
- printWhatIsInstalled,
559
- offerMeasurement,
560
- printNextSteps,
561
- BOOK_A_CALL,
562
641
  runningFromNpxCache,
563
642
  globalNiroPath,
564
643
  isOlder,
565
644
  describeProblem,
566
- expandHome,
645
+ moveWebFolders,
646
+ watcherIsOld,
567
647
  BOOTSTRAP_ENV,
568
648
  };