@niroai/niro 0.17.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 +136 -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 +6 -2
  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
@@ -4,6 +4,9 @@ const color = require("../lib/color");
4
4
  const watchState = require("../lib/watchState");
5
5
  const watchDaemon = require("../lib/watchDaemon");
6
6
  const resolveProject = require("./_resolveProject");
7
+ const localOnboard = require("../lib/localOnboard");
8
+ const lease = require("../lib/lease");
9
+ const mcpClients = require("../lib/mcpClients");
7
10
 
8
11
  function tintStatus(st) {
9
12
  if (st === "COMPLETED") return color.green(st);
@@ -36,7 +39,61 @@ const AUTO_SYNC_LINES = {
36
39
  pm2_missing: color.yellow("OFF — PM2 is missing. Run `npm install -g pm2`, then `niro service install`"),
37
40
  };
38
41
 
42
+ /**
43
+ * `niro status` in a folder the local engine holds: everything is answered on this machine,
44
+ * plus the plan and whether code is shared, from the lease. Returns false when the folder is
45
+ * not local, so the cloud answer below runs as before.
46
+ */
47
+ async function localStatus(opts) {
48
+ const found = await localOnboard.worktreeFor(process.cwd(), { inside: true }).catch(() => null);
49
+ if (!found) return false;
50
+ const { project, worktree } = found;
51
+ const signedIn = !!(credentials.load() || {}).token || !!(credentials.load() || {}).cliToken;
52
+ const claims = signedIn ? await lease.currentClaims().catch(() => null) : null;
53
+ const shareCode = claims && typeof claims.share_code === "boolean" ? claims.share_code : null;
54
+ let assistants = [];
55
+ try {
56
+ assistants = mcpClients.clientNames(mcpClients.configuredClients());
57
+ } catch {
58
+ // Unknown is shown as none.
59
+ }
60
+ const autoSync = watchDaemon.watchDaemonState();
61
+
62
+ if (opts.json) {
63
+ console.log(JSON.stringify({
64
+ local: true,
65
+ project: project.name,
66
+ folder: worktree.path,
67
+ indexed: !!worktree.indexed,
68
+ last_build_at: worktree.last_build_at || null,
69
+ plan: (claims && claims.plan_name) || null,
70
+ share_code: shareCode,
71
+ assistants,
72
+ auto_sync: autoSync,
73
+ }));
74
+ return true;
75
+ }
76
+ const label = (s) => color.dim(s.padEnd(13));
77
+ console.log(`${label("Project:")} ${color.bold(project.name)} ${color.dim("(indexed on this machine)")}`);
78
+ console.log(`${label("Folder:")} ${worktree.path}`);
79
+ console.log(`${label("Graph:")} ${worktree.indexed ? color.green("built") : color.yellow("not built yet — run `niro start` here")}`);
80
+ if (worktree.last_build_at) console.log(`${label("Last build:")} ${color.dim(worktree.last_build_at)}`);
81
+ console.log(`${label("Plan:")} ${(claims && claims.plan_name) || color.dim(signedIn ? "unknown (could not reach Niro)" : "not signed in")}`);
82
+ // Help improve Niro is shown by `niro plan`, not here (2026-11-11); --json still carries share_code.
83
+ console.log(`${label("Assistant:")} ${assistants.length ? assistants.join(", ") : color.yellow("none — run `niro mcp install`")}`);
84
+ console.log(`${label("Auto-sync:")} ${AUTO_SYNC_LINES[autoSync] || autoSync}`);
85
+ return true;
86
+ }
87
+
39
88
  async function status(projectIdArg, opts = {}) {
89
+ if (!projectIdArg && (await localStatus(opts))) return;
90
+ // Without the cloud there is nothing else to ask: a folder the engine does not hold is simply
91
+ // not set up yet, so say that rather than sending them to the cloud's `niro init`.
92
+ if (!require("../lib/release").cloud) {
93
+ console.log(`This folder is not indexed on this machine. Run ${color.cyan("niro start")} here to set it up.`);
94
+ process.exitCode = 1;
95
+ return;
96
+ }
40
97
  credentials.requireCreds();
41
98
  const projectId = await resolveProject.resolveProjectId(projectIdArg, { apiUrl: opts.apiUrl });
42
99
  const data = await api.get(`/api/graph/${encodeURIComponent(projectId)}/status`);
@@ -23,6 +23,7 @@ const prompt = require("../lib/prompt");
23
23
  const watchState = require("../lib/watchState");
24
24
  const resolveCtxLib = require("../lib/resolveContext");
25
25
  const spinner = require("../lib/spinner");
26
+ const cloudGuard = require("../lib/cloudGuard");
26
27
 
27
28
  const ok = (s) => color.green(s);
28
29
  const warn = (s) => color.yellow(s);
@@ -40,6 +41,11 @@ async function takeover(pathArg, opts = {}) {
40
41
  + "Select the backend with `niro login` / your config first, then run this without --api-url.");
41
42
  }
42
43
  const targetDir = require("path").resolve(pathArg || process.cwd());
44
+ // Said, not refused. A temporary project is deliberately a cloud project (decision 3): it
45
+ // serves pull request review and root cause analysis, which the local graph does not
46
+ // answer, and an assistant following `should_create_temp_project` runs this command with
47
+ // no terminal, so a refusal or a prompt here would break that route.
48
+ await cloudGuard.noticeIfIndexedLocally(targetDir, { what: "A temporary project" });
43
49
  const { repoRoot, branch } = git.inspect(targetDir);
44
50
  if (!repoRoot) {
45
51
  throw new Error("Not a git repository — run takeover inside the repo you want to work on.");
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `niro uninstall` — take Niro off this machine (design §9.5, addendum D4).
3
+ *
4
+ * In order, because each step needs what the next removes:
5
+ * 1. the assistant entries `niro mcp install` wrote (teardown.js --all -y)
6
+ * 2. the niro-watch daemon
7
+ * 3. ~/.niro (credentials, config, the engine) and ~/.niro-local (the engine's registry and
8
+ * its graph, graph.db)
9
+ *
10
+ * Asks first, and a no changes nothing. A failed step is said and the rest still run, so a
11
+ * broken editor config cannot leave credentials behind. The npm package itself is the one
12
+ * thing a running program cannot remove cleanly, so the command to do it is printed.
13
+ */
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+
18
+ const prompt = require("../lib/prompt");
19
+ const color = require("../lib/color");
20
+ const service = require("../lib/service");
21
+
22
+ const n = (x) => x.toLocaleString("en-US");
23
+ const tilde = (p) => p.replace(os.homedir(), "~");
24
+
25
+ /**
26
+ * What is on this machine, for the list shown before asking. Every part is best effort: a broken
27
+ * editor config or a missing engine must not stop someone from removing Niro.
28
+ */
29
+ async function survey({ claims, folders, assistants, watcher }) {
30
+ const [c, f] = await Promise.all([
31
+ claims().catch(() => null),
32
+ folders().catch(() => []),
33
+ ]);
34
+ let names = [];
35
+ try { names = assistants(); } catch { /* shown as "your AI assistants" */ }
36
+ let watch = null;
37
+ try { watch = watcher(); } catch { /* unknown */ }
38
+ return { claims: c, folders: f, assistants: names, watch };
39
+ }
40
+
41
+ /**
42
+ * Send everything the engine is holding, batch after batch, and return how many went. Stops when
43
+ * a round sends nothing, so a queue the server keeps refusing cannot loop.
44
+ */
45
+ async function flushUploads() {
46
+ let total = 0;
47
+ for (const send of [() => require("../lib/auditUpload").uploadPending(), () => require("../lib/journalUpload").uploadPending()]) {
48
+ for (let round = 0; round < 20; round++) {
49
+ const r = await send();
50
+ const sent = (r && (r.submitted || r.sent)) || 0;
51
+ total += sent;
52
+ if (sent === 0) break;
53
+ }
54
+ }
55
+ return total;
56
+ }
57
+
58
+ async function uninstall(opts = {}, {
59
+ claims = () => withTimeout(require("../lib/lease").currentClaims(), 4000),
60
+ folders = () => require("../lib/planUsage").folders(),
61
+ assistants = () => { const m = require("../lib/mcpClients"); return m.clientNames(m.configuredClients()); },
62
+ watcher = () => require("../lib/watchDaemon").watchDaemonState(),
63
+ flush = flushUploads,
64
+ } = {}) {
65
+ const home = os.homedir();
66
+ const niroDir = path.join(home, ".niro");
67
+ const localDir = path.join(home, ".niro-local");
68
+ const creds = (() => { try { return require("../lib/credentials").load(); } catch { return null; } })();
69
+ const s = await survey({ claims, folders, assistants, watcher });
70
+
71
+ // Only what is still here: a second run after a full uninstall has nothing to list.
72
+ const has = {
73
+ assistants: s.assistants.length > 0,
74
+ watcher: s.watch === "running" || s.watch === "stopped",
75
+ graphs: fs.existsSync(localDir),
76
+ account: fs.existsSync(niroDir),
77
+ };
78
+ if (!Object.values(has).some(Boolean)) {
79
+ console.log(`\nNiro is not installed on this machine, so there is nothing to remove. If the ${color.cyan("niro")} command is still here: ${color.cyan("npm uninstall -g @niroai/niro")}\n`);
80
+ return;
81
+ }
82
+
83
+ console.log("");
84
+ console.log("┌ Remove Niro from this machine");
85
+ console.log("│");
86
+ console.log("│ This removes:");
87
+ if (has.assistants) console.log(`│ · Niro from ${s.assistants.join(", ")}`);
88
+ if (has.watcher) console.log(`│ · the background watcher (niro-watch)${s.watch === "running" ? ", now running" : ""}`);
89
+ const lines = s.folders.reduce((sum, f) => sum + f.lines, 0);
90
+ if (has.graphs) {
91
+ console.log(s.folders.length
92
+ ? `│ · ${s.folders.length} indexed folder${s.folders.length === 1 ? "" : "s"} and their graphs, ${n(lines)} lines ${color.dim(tilde(localDir))}`
93
+ : `│ · the local engine's graphs ${color.dim(tilde(localDir))}`);
94
+ }
95
+ if (has.account) console.log(`│ · ${creds && creds.email ? `your sign-in (${creds.email}), ` : "your sign-in, "}settings and the local engine ${color.dim(tilde(niroDir))}`);
96
+ console.log(`│ Your code and repositories are not touched. Your Niro account stays.`);
97
+ if (s.claims && s.claims.share_code) {
98
+ console.log("│");
99
+ console.log(`│ ${color.yellow("⚠")} Help improve Niro is on for your account. Uninstalling does not turn it off;`);
100
+ console.log(`│ turn it off at ${color.cyan(`${require("../lib/planUsage").consoleUrl()}/settings?tab=privacy`)}`);
101
+ }
102
+ console.log("│");
103
+ console.log(`│ ${color.dim(`To stop indexing one folder instead: niro plan remove <folder>`)}`);
104
+ console.log("│");
105
+
106
+ if (!opts.yes) {
107
+ if (prompt.isNonInteractive()) {
108
+ console.error("└ Nothing removed: there is no terminal to confirm on. Pass --yes to go ahead.");
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ if (!(await prompt.confirm("◆ Remove all of this?", false))) {
113
+ console.log("└ Nothing removed.");
114
+ return;
115
+ }
116
+ console.log("│");
117
+ }
118
+
119
+ let failed = 0;
120
+ const attempt = async (what, fn) => {
121
+ try {
122
+ await fn();
123
+ console.log(`│ ${color.green("✓")} ${what}`);
124
+ } catch (err) {
125
+ failed += 1;
126
+ console.log(`│ ${color.yellow("⚠")} ${what}: ${err && err.message ? err.message : err}`);
127
+ }
128
+ };
129
+
130
+ // What the engine still holds to send goes before the graphs are deleted with it. Not a removal
131
+ // step: a failure is said, and uninstalling carries on.
132
+ if (has.graphs) {
133
+ try {
134
+ const sent = await withTimeout(flush(), 60000);
135
+ if (sent > 0) console.log(`│ ${color.green("✓")} Sent ${sent} waiting usage and audit record${sent === 1 ? "" : "s"} to Niro`);
136
+ } catch (err) {
137
+ console.log(`│ ${color.yellow("⚠")} Could not send the waiting usage and audit records: ${err && err.message ? err.message : err}`);
138
+ }
139
+ }
140
+
141
+ // The assistants' entries are cleared whenever anything else is being removed, even when none
142
+ // was detected: detection is best effort and the teardown is harmless where nothing is set.
143
+ await attempt(has.assistants ? `Removed Niro from ${s.assistants.join(", ")}` : "Cleared Niro from your assistants' settings",
144
+ () => require("./mcp").uninstall(["--all", "-y"], { quiet: true }));
145
+ let startupCommand = null;
146
+ if (has.watcher) {
147
+ await attempt("Stopped and removed the background watcher", () => {
148
+ // false means PM2 is not here, so there is no daemon to remove: not a failure.
149
+ const r = service.uninstall();
150
+ if (r && r.hook === "manual") startupCommand = r.command;
151
+ });
152
+ }
153
+ if (has.graphs) {
154
+ await attempt(s.folders.length ? `Deleted ${s.folders.length} graph${s.folders.length === 1 ? "" : "s"} ${color.dim(tilde(localDir))}` : `Deleted ${tilde(localDir)}`,
155
+ () => fs.rmSync(localDir, { recursive: true, force: true }));
156
+ }
157
+ if (has.account) {
158
+ await attempt(`Deleted your sign-in, settings and the engine ${color.dim(tilde(niroDir))}`,
159
+ () => fs.rmSync(niroDir, { recursive: true, force: true }));
160
+ }
161
+
162
+ if (failed) process.exitCode = 1;
163
+ console.log("│");
164
+ console.log(`└ ${failed ? "Niro is partly removed; see the problems above." : "Niro is removed from this machine."}`);
165
+ console.log("");
166
+ console.log(" Two last steps:");
167
+ console.log(` 1. Remove the command itself: ${color.cyan("npm uninstall -g @niroai/niro")}`);
168
+ console.log(" 2. Restart your AI assistants so they stop loading Niro.");
169
+ if (startupCommand) {
170
+ // PM2's start-on-login entry needs root to remove here, and starts nothing of Niro's now.
171
+ console.log(color.dim(` PM2 still starts on login (with nothing of Niro's). To remove that too: ${startupCommand}`));
172
+ }
173
+ console.log("");
174
+ }
175
+
176
+ function withTimeout(promise, ms) {
177
+ return Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(null), ms).unref())]);
178
+ }
179
+
180
+ module.exports = { uninstall };
@@ -107,7 +107,7 @@ function replaceAndHandOver(force) {
107
107
  console.log(`\n Upgrading niro. Installed now: ${VERSION}\n`);
108
108
 
109
109
  const installed = childProcess.spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
110
- shell: true,
110
+ shell: process.platform === "win32",
111
111
  stdio: "inherit",
112
112
  });
113
113
 
@@ -194,7 +194,7 @@ function replaceAndHandOver(force) {
194
194
 
195
195
  /** Where npm would install this package globally. */
196
196
  function globalPackageDir() {
197
- const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: true, encoding: "utf8" });
197
+ const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: process.platform === "win32", encoding: "utf8" });
198
198
  if (r.status !== 0) return null;
199
199
  const prefix = String(r.stdout || "").trim();
200
200
  if (!prefix) return null;
@@ -281,11 +281,31 @@ async function finish(fromVersion) {
281
281
  const complete = await refreshAssistantFiles();
282
282
  if (!complete) process.exitCode = EXIT_REFRESH_INCOMPLETE;
283
283
 
284
+ await upgradeEngine();
285
+
286
+ // Said after the files are in place, so it is the last thing on screen and is read. Local
287
+ // state only: no network call, because an upgrade must not be able to hang on one.
288
+ for (const line of require("../lib/migrationNotice").lines()) console.log(line);
289
+
284
290
  console.log(` ${color.yellow("One thing left:")} restart your AI assistant.`);
285
291
  console.log(" Hooks and skills are read when it starts, so an assistant that is already open");
286
292
  console.log(" is still using the old ones.\n");
287
293
  }
288
294
 
295
+ /**
296
+ * The local engine moves with the CLI, so a machine holds one version of it. Failing to fetch it does not fail the
297
+ * upgrade: the engine already there keeps working, and the words say how to try again.
298
+ */
299
+ async function upgradeEngine() {
300
+ const engine = require("../lib/engine");
301
+ try {
302
+ await engine.upgradeInstalled({ log: (line) => console.log(` ${line}`) });
303
+ } catch (err) {
304
+ console.log(` The local engine was not upgraded: ${err.message}`);
305
+ console.log(` Try again with ${color.cyan("niro engine install")}.\n`);
306
+ }
307
+ }
308
+
289
309
  /**
290
310
  * Re-run the installer once per configured client, so their hooks, skills and instruction files
291
311
  * match the package that is now installed. Returns false when anything was skipped or failed, so
@@ -312,9 +332,18 @@ async function refreshAssistantFiles() {
312
332
  return true;
313
333
  }
314
334
 
315
- if (!startCmd.hasCredentials()) {
316
- console.log(` ${color.yellow("Heads up:")} you are not signed in, so each client's installer will ask`);
317
- console.log(` for an API key. ${color.cyan("niro login")} first if you would rather it did not.\n`);
335
+ // The installer's own test (src/mcp/setup.js main): after `niro login --api-key` there is a key but no
336
+ // cli token, and it still refuses. With the cloud it then asks for a key; without, it exits 1 for every
337
+ // client and the run ends "N of M refreshed, K failed". Called through the module so tests can stub it.
338
+ if (!require("../mcp/setup").hasCliToken()) {
339
+ if (require("../lib/release").cloud) {
340
+ console.log(` ${color.yellow("Heads up:")} you are not signed in, so each client's installer will ask`);
341
+ console.log(` for an API key. ${color.cyan("niro login")} first if you would rather it did not.\n`);
342
+ } else {
343
+ console.log(` ${color.yellow("Heads up:")} you are not signed in, so each client's installer will stop`);
344
+ console.log(" without refreshing it.");
345
+ console.log(` Run ${color.cyan("niro login")}, then ${color.cyan("niro upgrade --finish")}.\n`);
346
+ }
318
347
  }
319
348
 
320
349
  // Cursor's installer writes a project-scoped rule to <cwd>/.cursor/rules/niro.mdc, so refreshing
@@ -375,9 +404,10 @@ function restartWatcher() {
375
404
 
376
405
  console.log(" Restarting the background watcher, which is still running the old code...");
377
406
  if (service.restart()) {
378
- // Deliberately not "now on <version>": pm2 re-execs the script path recorded when the daemon
379
- // was installed (service.js records process.argv[1]), which is not necessarily the copy that
380
- // was just upgraded. All that is known here is that pm2 restarted it.
407
+ // service.restart() deletes, starts and saves niro-watch with the short environment list and
408
+ // makes ~/.pm2/dump.pm2 private, so an install saved with the whole shell is rewritten here
409
+ // too. It registers process.argv[1], which in `--finish` is the copy just installed; still not
410
+ // "now on <version>", since only PM2's answer is known here.
381
411
  console.log(` ${color.green("Done.")} niro-watch restarted. Check it with ${color.cyan("niro service status")}.\n`);
382
412
  } else {
383
413
  console.log(` ${color.yellow("!")} Could not restart it. Run ${color.cyan("niro service install")}.\n`);