@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.
- package/CHANGELOG.md +136 -0
- package/README.md +123 -302
- package/bin/niro.js +12 -1
- package/docs/agent-antigravity.md +37 -0
- package/docs/agent-claude-code.md +48 -0
- package/docs/agent-claude-desktop.md +47 -0
- package/docs/agent-cline.md +42 -0
- package/docs/agent-codex.md +45 -0
- package/docs/agent-copilot-cli.md +42 -0
- package/docs/agent-cursor.md +44 -0
- package/docs/agent-factory.md +42 -0
- package/docs/agent-gemini-cli.md +42 -0
- package/docs/agent-goose.md +42 -0
- package/docs/agent-kiro.md +37 -0
- package/docs/agent-opencode.md +37 -0
- package/docs/agent-vscode.md +45 -0
- package/docs/agent-windsurf.md +38 -0
- package/docs/answer-shapes.md +69 -0
- package/docs/builds-and-updates.md +68 -0
- package/docs/cloud-README.md +372 -0
- package/docs/configuring-a-folder.md +51 -0
- package/docs/connect-ci.md +24 -0
- package/docs/connect-overview.md +20 -0
- package/docs/connect-review-bots.md +17 -0
- package/docs/connect-team-server.md +12 -0
- package/docs/connect-terminal.md +49 -0
- package/docs/connect-web-agents.md +9 -0
- package/docs/connect-your-ai-assistant.md +59 -175
- package/docs/coverage-audit-and-consent.md +51 -0
- package/docs/evaluate-what-niro-did.md +271 -0
- package/docs/faq-assistants-and-mcp.md +122 -0
- package/docs/faq-feedback-and-receipts.md +33 -0
- package/docs/faq-indexing.md +127 -0
- package/docs/faq-install-and-setup.md +103 -0
- package/docs/faq-licence-and-plans.md +86 -0
- package/docs/faq-login-and-account.md +69 -0
- package/docs/faq-offline.md +34 -0
- package/docs/faq-performance.md +33 -0
- package/docs/faq-privacy-and-security.md +113 -0
- package/docs/faq-uninstall.md +47 -0
- package/docs/faq-updates.md +59 -0
- package/docs/faq-worktrees-and-branches.md +59 -0
- package/docs/faq-wrong-or-missing-answers.md +275 -0
- package/docs/hosted-only.md +33 -0
- package/docs/how-the-graph-works.md +47 -0
- package/docs/index.md +157 -0
- package/docs/licence-and-plans.md +72 -0
- package/docs/lifecycle-debug.md +48 -0
- package/docs/lifecycle-onboarding.md +32 -0
- package/docs/lifecycle-overview.md +77 -0
- package/docs/lifecycle-plan-and-design.md +44 -0
- package/docs/lifecycle-review.md +30 -0
- package/docs/lifecycle-security-and-privacy.md +35 -0
- package/docs/lifecycle-test.md +28 -0
- package/docs/lifecycle-write-code.md +35 -0
- package/docs/llms-full.txt +8015 -0
- package/docs/llms.txt +126 -0
- package/docs/playbook-add-a-feature.md +108 -0
- package/docs/playbook-change-safely.md +93 -0
- package/docs/playbook-data-and-events.md +107 -0
- package/docs/playbook-debug-after-a-change.md +96 -0
- package/docs/playbook-debug-config.md +95 -0
- package/docs/playbook-debug-exception.md +124 -0
- package/docs/playbook-debug-failing-service-call.md +119 -0
- package/docs/playbook-debug-slow-path.md +80 -0
- package/docs/playbook-debug-wrong-value.md +137 -0
- package/docs/playbook-onboard.md +95 -0
- package/docs/playbook-review-a-change.md +72 -0
- package/docs/playbook-test-a-change.md +73 -0
- package/docs/reference-cli.md +113 -0
- package/docs/reference-config-files.md +42 -0
- package/docs/reference-engine-commands.md +56 -0
- package/docs/reference-environment-variables.md +89 -0
- package/docs/reference-exit-codes.md +67 -0
- package/docs/reference-limits.md +75 -0
- package/docs/reference-mcp-tools.md +326 -0
- package/docs/reference-messages.md +180 -0
- package/docs/secrets-and-masking.md +48 -0
- package/docs/supported-c.md +130 -0
- package/docs/supported-cpp.md +157 -0
- package/docs/supported-csharp.md +211 -0
- package/docs/supported-go.md +258 -0
- package/docs/supported-java.md +226 -0
- package/docs/supported-javascript.md +223 -0
- package/docs/supported-kotlin.md +124 -0
- package/docs/supported-overview.md +89 -0
- package/docs/supported-php.md +157 -0
- package/docs/supported-python.md +224 -0
- package/docs/supported-ruby.md +162 -0
- package/docs/supported-rust.md +150 -0
- package/docs/supported-swift.md +133 -0
- package/docs/supported-typescript.md +220 -0
- package/docs/tokens-speed-and-accuracy.md +99 -0
- package/docs/troubleshooting.md +42 -0
- package/docs/uninstall.md +74 -0
- package/docs/what-leaves-your-machine.md +71 -0
- package/docs/where-things-are-stored.md +59 -0
- package/docs/worktrees-and-branches.md +55 -0
- package/package.json +5 -3
- package/src/commands/add-repo.js +5 -0
- package/src/commands/auditSend.js +67 -0
- package/src/commands/build.js +6 -0
- package/src/commands/docsSend.js +73 -0
- package/src/commands/engine.js +120 -0
- package/src/commands/engineRoutes.js +105 -0
- package/src/commands/guide.js +254 -0
- package/src/commands/init.js +6 -2
- package/src/commands/initLocal.js +96 -0
- package/src/commands/journalSend.js +47 -0
- package/src/commands/login.js +89 -12
- package/src/commands/mcp.js +22 -6
- package/src/commands/plan.js +99 -0
- package/src/commands/repoPush.js +33 -0
- package/src/commands/requestIntegration.js +38 -0
- package/src/commands/share.js +56 -0
- package/src/commands/start.js +451 -371
- package/src/commands/status.js +57 -0
- package/src/commands/takeover.js +6 -0
- package/src/commands/uninstall.js +180 -0
- package/src/commands/upgrade.js +38 -8
- package/src/commands/watch.js +420 -6
- package/src/index.js +174 -3
- package/src/lib/agentApi.js +32 -1
- package/src/lib/api.js +20 -6
- package/src/lib/auditUpload.js +293 -0
- package/src/lib/branchDrift.js +149 -0
- package/src/lib/choose.js +116 -0
- package/src/lib/cloudGuard.js +74 -0
- package/src/lib/credentials.js +2 -2
- package/src/lib/docsMarked.js +55 -0
- package/src/lib/docsState.js +213 -0
- package/src/lib/docsUpload.js +481 -0
- package/src/lib/engine.js +619 -0
- package/src/lib/folderSync.js +2 -1
- package/src/lib/graphCheck.js +40 -0
- package/src/lib/guide.js +293 -0
- package/src/lib/guideSearch.js +252 -0
- package/src/lib/integrationRequest.js +89 -0
- package/src/lib/journalUpload.js +147 -0
- package/src/lib/languages.js +129 -0
- package/src/lib/lease.js +375 -0
- package/src/lib/localBuild.js +285 -0
- package/src/lib/localEngine.js +210 -0
- package/src/lib/localOnboard.js +124 -0
- package/src/lib/localWorktrees.js +243 -0
- package/src/lib/machineCheck.js +58 -0
- package/src/lib/mcpClients.js +10 -1
- package/src/lib/migrationNotice.js +118 -0
- package/src/lib/modelFetch.js +153 -0
- package/src/lib/nestedOutput.js +23 -9
- package/src/lib/onboardingLog.js +34 -0
- package/src/lib/planUsage.js +165 -0
- package/src/lib/progressBar.js +46 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/redact.js +155 -0
- package/src/lib/release.js +33 -0
- package/src/lib/repoPush.js +495 -0
- package/src/lib/secret.js +1 -1
- package/src/lib/service.js +372 -63
- package/src/lib/shareCode.js +367 -0
- package/src/lib/shareExclusions.js +114 -0
- package/src/lib/shareFiles.js +129 -0
- package/src/lib/spinner.js +9 -5
- package/src/lib/watchDaemon.js +33 -10
- package/src/lib/watchState.js +12 -1
- package/src/lib/windowsPaths.js +67 -0
- package/src/mcp/lineCheck.js +382 -0
- package/src/mcp/lineName.js +92 -0
- package/src/mcp/localBackend.js +751 -0
- package/src/mcp/localMode.js +383 -0
- package/src/mcp/localSkill.js +189 -0
- package/src/mcp/server.js +610 -79
- package/src/mcp/setup.js +519 -131
- package/src/mcp/shellSearchHook.js +509 -0
- package/src/mcp/teardown.js +173 -91
- package/src/mcp/usingNiro.js +39 -0
- package/docs/working-on-branches.md +0 -162
package/src/lib/service.js
CHANGED
|
@@ -13,6 +13,7 @@ const fs = require("fs");
|
|
|
13
13
|
const os = require("os");
|
|
14
14
|
const path = require("path");
|
|
15
15
|
const color = require("./color");
|
|
16
|
+
const { system32 } = require("./windowsPaths");
|
|
16
17
|
|
|
17
18
|
const PROCESS_NAME = "niro-watch";
|
|
18
19
|
|
|
@@ -23,6 +24,9 @@ const PROCESS_NAME = "niro-watch";
|
|
|
23
24
|
* `niro status` must never do that; flows that go on to run pm2 anyway
|
|
24
25
|
* (install/uninstall) don't care either way.
|
|
25
26
|
*/
|
|
27
|
+
/** pm2 is pm2.cmd on Windows, which needs a shell; elsewhere a shell plus an args array trips DEP0190. */
|
|
28
|
+
const PM2_SHELL = process.platform === "win32";
|
|
29
|
+
|
|
26
30
|
function pm2Available() {
|
|
27
31
|
const probe = process.platform === "win32" ? "where pm2" : "command -v pm2";
|
|
28
32
|
const r = spawnSync(probe, { encoding: "utf8", shell: true, stdio: "pipe" });
|
|
@@ -45,111 +49,378 @@ function pm2DaemonAlive() {
|
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
// ─── the watcher's environment ───────────────────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// `pm2 start` copies the environment of the pm2 command into the app, and `pm2 save` writes that
|
|
55
|
+
// copy into ~/.pm2/dump.pm2. Run from a developer's shell, that was the whole shell: 291 names in
|
|
56
|
+
// one test home, 79 of them shaped like secrets (cloud keys, tokens, passwords), in a file other
|
|
57
|
+
// users could read. So the pm2 commands that create or save niro-watch are given only the names
|
|
58
|
+
// below. They are what `niro watch`, the engine it starts and the two runtimes under them read:
|
|
59
|
+
//
|
|
60
|
+
// - the NIRO_ settings niro-cli reads (the four NIRO_*_URL, NIRO_MCP_MODE, the timeouts and
|
|
61
|
+
// upload limits), named one by one, and every engine setting (NIRO_LOCAL_HOME, NIRO_LOCAL_BIN,
|
|
62
|
+
// NIRO_LOCAL_*). Not every NIRO_ name: a developer's shell can hold the Niro services' own
|
|
63
|
+
// settings (a JWT secret, a database password), and in one test home 5 of those got through a
|
|
64
|
+
// plain NIRO_ prefix. test/serviceWatcherEnv.test.js fails when niro-cli reads a NIRO_ name
|
|
65
|
+
// that is missing here. NIRO_API_KEY and NIRO_AUDIT_API_KEY are left out on purpose (see
|
|
66
|
+
// WATCHER_ENV_EXCLUDED): the watcher signs its uploads with the `niro login` token instead.
|
|
67
|
+
// - HOME and its Windows forms: where ~/.niro, ~/.niro-local and ~/.pm2 are.
|
|
68
|
+
// - PATH: how the engine and node are found. PM2_HOME: which PM2 this is.
|
|
69
|
+
// - USER, LOGNAME, USERNAME: who the user is, read by PM2 and by Python.
|
|
70
|
+
// - LANG, LANGUAGE, LC_*, TZ: how text is decoded and times are shown.
|
|
71
|
+
// - TMPDIR, TMP, TEMP: where the engine binary unpacks itself and writes temporary files.
|
|
72
|
+
// - NO_COLOR, FORCE_COLOR: whether the watcher's log has colour codes.
|
|
73
|
+
// - NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR, the proxy names and NODE_USE_ENV_PROXY:
|
|
74
|
+
// without them a watcher behind a company proxy or private certificate cannot upload.
|
|
75
|
+
// - Windows system names, without which node and Python do not start there, and
|
|
76
|
+
// NoDefaultCurrentDirectoryInExePath (windowsPaths.harden), so cmd.exe never runs a pm2.cmd or
|
|
77
|
+
// node.exe from the current folder.
|
|
78
|
+
const WATCHER_ENV_NAMES = new Set([
|
|
79
|
+
"HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA",
|
|
80
|
+
"PATH", "PM2_HOME",
|
|
81
|
+
"USER", "LOGNAME", "USERNAME",
|
|
82
|
+
"LANG", "LANGUAGE", "TZ",
|
|
83
|
+
"TMPDIR", "TMP", "TEMP",
|
|
84
|
+
"NO_COLOR", "FORCE_COLOR",
|
|
85
|
+
"NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
86
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "NODE_USE_ENV_PROXY",
|
|
87
|
+
"SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "PATHEXT", "PROGRAMDATA",
|
|
88
|
+
"NODEFAULTCURRENTDIRECTORYINEXEPATH",
|
|
89
|
+
"NIRO_API_URL", "NIRO_MCP_SERVER_URL", "NIRO_BILLING_URL", "NIRO_AUDIT_URL",
|
|
90
|
+
"NIRO_MCP_MODE", "NIRO_MCP_TRANSPORT", "NIRO_DISABLE_ROUTING",
|
|
91
|
+
"NIRO_ENGINE_HOME", "NIRO_ENGINE_LOCK_STALE_MS", "NIRO_ENGINE_LOCK_WAIT_MS",
|
|
92
|
+
"NIRO_LEASE_MIN_RENEW_MS", "NIRO_LEASE_RETRY_MS", "NIRO_LEASE_TIMEOUT_MS",
|
|
93
|
+
"NIRO_DOCS_TIMEOUT_MS", "NIRO_DOCS_MODEL_TIMEOUT_MS", "NIRO_DELETE_SETTLE_MS",
|
|
94
|
+
"NIRO_MISSING_CONFIRM_MS", "NIRO_SSE_LIVENESS_MS",
|
|
95
|
+
"NIRO_UPLOAD_BACKOFF_BASE_MS", "NIRO_UPLOAD_CONCURRENCY", "NIRO_UPLOAD_MAX_FILES",
|
|
96
|
+
"NIRO_UPLOAD_MAX_TOTAL_BYTES", "NIRO_UPLOAD_TIMEOUT_MS", "NIRO_SHARE_BATCH_FILES",
|
|
97
|
+
]);
|
|
98
|
+
const WATCHER_ENV_PREFIXES = ["NIRO_LOCAL_", "LC_"];
|
|
99
|
+
// NIRO_ names niro-cli reads that hand something from one process to the next, not settings: the
|
|
100
|
+
// bootstrap and upgrade markers of `niro start` / `niro upgrade`, the output indent, and the lease
|
|
101
|
+
// pipe numbers the engine is given by whoever starts it. A watcher that inherited them would act
|
|
102
|
+
// on a handover that was never meant for it.
|
|
103
|
+
// Also the two API keys. When one is set, the coverage audit signs with it instead of the login
|
|
104
|
+
// token (auditUpload.js authHeaders); in a developer's shell it is usually a key for Niro's own
|
|
105
|
+
// services, and it would be saved in dump.pm2. Without them the watcher's audit and journal
|
|
106
|
+
// uploads sign with the `niro login` token (Niraj, 2026-11-06: "yes drop it").
|
|
107
|
+
const WATCHER_ENV_EXCLUDED = new Set([
|
|
108
|
+
"NIRO_START_BOOTSTRAPPED", "NIRO_UPGRADE_HANDOVER", "NIRO_OUTPUT_PREFIX",
|
|
109
|
+
"NIRO_LOCAL_LEASE_FD_IN", "NIRO_LOCAL_LEASE_FD_OUT",
|
|
110
|
+
"NIRO_API_KEY", "NIRO_AUDIT_API_KEY",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True when the watcher is given this variable. Names are compared in capitals: Windows names are
|
|
115
|
+
* not case sensitive (`Path`, `SystemRoot`), and proxies are often written in lower case.
|
|
116
|
+
*/
|
|
117
|
+
function watcherNeeds(name) {
|
|
118
|
+
const upper = String(name).toUpperCase();
|
|
119
|
+
if (WATCHER_ENV_EXCLUDED.has(upper)) return false;
|
|
120
|
+
return WATCHER_ENV_NAMES.has(upper) || WATCHER_ENV_PREFIXES.some((p) => upper.startsWith(p));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The part of `source` the watcher is started with: the names above, values unchanged. */
|
|
124
|
+
function watcherEnv(source = process.env) {
|
|
125
|
+
const env = {};
|
|
126
|
+
for (const [name, value] of Object.entries(source)) {
|
|
127
|
+
if (value !== undefined && watcherNeeds(name)) env[name] = value;
|
|
128
|
+
}
|
|
129
|
+
return env;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Make PM2's saved process list readable by this user only.
|
|
134
|
+
*
|
|
135
|
+
* PM2 creates dump.pm2 and dump.pm2.bak with the default file mode (664 under the usual umask)
|
|
136
|
+
* and afterwards rewrites them in place, which keeps whatever mode they have; so setting 600 once
|
|
137
|
+
* holds through later `pm2 save` and restart (checked with PM2 7.0.4). After a save niro-cli made,
|
|
138
|
+
* the backup is also replaced by the dump just written: PM2 fills the backup with the previous
|
|
139
|
+
* dump, and on an install made before this change that one holds the whole shell environment.
|
|
140
|
+
*/
|
|
141
|
+
function secureDump({ refreshBackup = false } = {}) {
|
|
142
|
+
const dump = path.join(pm2Home(), "dump.pm2");
|
|
143
|
+
const backup = path.join(pm2Home(), "dump.pm2.bak");
|
|
50
144
|
try {
|
|
51
|
-
|
|
145
|
+
if (refreshBackup && fs.existsSync(dump)) fs.copyFileSync(dump, backup);
|
|
146
|
+
} catch {
|
|
147
|
+
// keep going: the modes below still matter
|
|
148
|
+
}
|
|
149
|
+
for (const file of [dump, backup]) {
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(file)) fs.chmodSync(file, 0o600);
|
|
152
|
+
} catch {
|
|
153
|
+
// nothing to stop for: niro-watch runs either way
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* True when PM2's saved niro-watch entry holds environment names the watcher is no longer given,
|
|
160
|
+
* which means a niro-cli from before this change saved it with the whole shell environment.
|
|
161
|
+
* PM2 adds names of its own (PM2_HOME, PM2_USAGE, unique_id, the app's name): those start with
|
|
162
|
+
* PM2_ or contain lower case, and are not counted. Neither are the names a shell sets by itself
|
|
163
|
+
* (PWD, OLDPWD, SHLVL, _): when `pm2` is a shell script (a pnpm global install, an asdf shim),
|
|
164
|
+
* the shell adds them to the short list on its way to PM2, and counting them restarted the
|
|
165
|
+
* watcher on every `niro start` (3 of 3 in the check of 2026-11-06). A whole-shell entry still
|
|
166
|
+
* has plenty of other names (SHELL, TERM, the cloud keys), so it is still caught.
|
|
167
|
+
*/
|
|
168
|
+
const SHELL_OWN_NAMES = new Set(["PWD", "OLDPWD", "SHLVL", "_"]);
|
|
169
|
+
|
|
170
|
+
function savedEnvIsWide() {
|
|
171
|
+
let list;
|
|
172
|
+
try {
|
|
173
|
+
list = JSON.parse(fs.readFileSync(path.join(pm2Home(), "dump.pm2"), "utf8"));
|
|
52
174
|
} catch {
|
|
53
175
|
return false;
|
|
54
176
|
}
|
|
177
|
+
if (!Array.isArray(list)) return false;
|
|
178
|
+
const entry = list.find((p) => p && p.name === PROCESS_NAME);
|
|
179
|
+
if (!entry || !entry.env || typeof entry.env !== "object") return false;
|
|
180
|
+
return Object.keys(entry.env).some((name) =>
|
|
181
|
+
!watcherNeeds(name) && !/^PM2_/i.test(name) && name !== "NODE_APP_INSTANCE" && !SHELL_OWN_NAMES.has(name)
|
|
182
|
+
&& name === name.toUpperCase());
|
|
55
183
|
}
|
|
56
184
|
|
|
57
185
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
186
|
+
* `pm2 start` for niro-watch. On Windows PM2's daemon starts a bare "node" without a shell, which
|
|
187
|
+
* looks in the folder niro ran from before the PATH, so it is given this node by full path. pm2 runs
|
|
188
|
+
* through cmd.exe there, and Node joins a shell command's words unquoted, so a path with a space
|
|
189
|
+
* ("C:\Program Files", a user folder) is quoted. Elsewhere the PATH's node, which keeps working
|
|
190
|
+
* after a node upgrade.
|
|
62
191
|
*/
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
192
|
+
function pm2StartArgs(niroBin, platform = process.platform, node = process.execPath) {
|
|
193
|
+
if (platform !== "win32") return ["start", niroBin, "--name", PROCESS_NAME, "--", "watch"];
|
|
194
|
+
const word = (p) => (/\s/.test(p) ? `"${p}"` : p);
|
|
195
|
+
return ["start", word(niroBin), "--name", PROCESS_NAME, "--interpreter", word(node), "--", "watch"];
|
|
196
|
+
}
|
|
65
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Delete, start and save niro-watch with only the environment it needs, then make the saved list
|
|
200
|
+
* private. Throws when `pm2 start` fails. Never touches the start-on-login hook.
|
|
201
|
+
*/
|
|
202
|
+
function startWatcher() {
|
|
66
203
|
const niroBin = process.argv[1];
|
|
204
|
+
const env = watcherEnv();
|
|
67
205
|
|
|
68
206
|
// Stop any existing instance so we can re-register cleanly
|
|
69
|
-
spawnSync("pm2", ["delete", PROCESS_NAME], { shell:
|
|
207
|
+
spawnSync("pm2", ["delete", PROCESS_NAME], { shell: PM2_SHELL, stdio: "pipe", env });
|
|
70
208
|
|
|
71
209
|
// PM2's own output is captured, not inherited. Inheriting it put ~55 lines into the middle
|
|
72
210
|
// of setup: a box-drawing process table, the entire generated systemd unit file, and the
|
|
73
211
|
// user's PATH twice. None of it is ours and none of it is actionable while it is working.
|
|
74
212
|
// It IS printed on failure, where it is the only diagnostic there is.
|
|
75
|
-
const startResult = spawnSync(
|
|
76
|
-
"pm2",
|
|
77
|
-
["start", niroBin, "--name", PROCESS_NAME, "--", "watch"],
|
|
78
|
-
{ shell: true, encoding: "utf8" }
|
|
79
|
-
);
|
|
213
|
+
const startResult = spawnSync("pm2", pm2StartArgs(niroBin), { shell: PM2_SHELL, encoding: "utf8", env });
|
|
80
214
|
if (startResult.status !== 0) {
|
|
81
215
|
console.error(color.dim(`${startResult.stdout || ""}${startResult.stderr || ""}`));
|
|
82
216
|
throw new Error("pm2 start failed");
|
|
83
217
|
}
|
|
84
218
|
|
|
85
219
|
// Persist the process list so PM2 restores it after a manual `pm2 resurrect`
|
|
86
|
-
spawnSync("pm2", ["save"], { shell:
|
|
220
|
+
const saved = spawnSync("pm2", ["save"], { shell: PM2_SHELL, encoding: "utf8", env });
|
|
221
|
+
secureDump({ refreshBackup: saved.status === 0 });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* For an install made before the watcher's environment was limited: when the saved niro-watch
|
|
226
|
+
* entry still holds the whole shell environment, start it again with the limited one, which
|
|
227
|
+
* rewrites dump.pm2 and its backup; otherwise only make the saved list private. Returns true when
|
|
228
|
+
* it started the watcher again. Asks for no password and leaves the start-on-login hook alone.
|
|
229
|
+
*/
|
|
230
|
+
function refreshSavedEnv() {
|
|
231
|
+
if (!pm2Available()) return false;
|
|
232
|
+
if (!savedEnvIsWide()) {
|
|
233
|
+
secureDump();
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
startWatcher();
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** True when PM2's saved process list (pm2 save) includes niro-watch. */
|
|
241
|
+
function dumpHasProcess() {
|
|
242
|
+
try {
|
|
243
|
+
return fs.readFileSync(path.join(pm2Home(), "dump.pm2"), "utf8").includes(`"${PROCESS_NAME}"`);
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── start on login, Windows ─────────────────────────────────────────────────
|
|
250
|
+
//
|
|
251
|
+
// `pm2 startup` knows systemd and launchd, not Windows: there it prints no command, which the
|
|
252
|
+
// install below used to read as "already configured" while nothing would start at login. So on
|
|
253
|
+
// Windows, and only there, one value under the user's own Run key starts PM2's saved list
|
|
254
|
+
// (`pm2 resurrect`) at every login: per user, no administrator rights, removed by uninstall.
|
|
255
|
+
const WINDOWS_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The Run-key value that starts niro-watch at login on Windows: its name and the command it runs.
|
|
259
|
+
*
|
|
260
|
+
* `pm2Cmd`: pm2.cmd's full path, found on the PATH when the entry is written. At sign-in Windows uses the saved PATH,
|
|
261
|
+
* which may not hold npm's folder (a PATH set only in a shell profile, some nvm, Volta or Scoop set-ups), so a bare
|
|
262
|
+
* name could go unfound and the watcher silently not start. Only when none is found is the bare name used.
|
|
263
|
+
*/
|
|
264
|
+
function windowsLoginEntry(pm2Cmd = require("./engine").onThePath("pm2.cmd")) {
|
|
265
|
+
// ponytail: a hidden PowerShell may flash for a moment at login; a VBScript launcher would hide
|
|
266
|
+
// it completely, add it if people notice.
|
|
267
|
+
// pm2.cmd, not pm2: PowerShell would pick npm's pm2.ps1, and Windows 10 and 11 refuse to run
|
|
268
|
+
// scripts by default (Restricted), so nothing would start and nothing would say so.
|
|
269
|
+
// A full path is called with PowerShell's & on a single-quoted string, where a quote is doubled.
|
|
270
|
+
const run = pm2Cmd ? `"& '${pm2Cmd.replace(/'/g, "''")}' resurrect"` : "pm2.cmd resurrect";
|
|
271
|
+
return {
|
|
272
|
+
key: WINDOWS_RUN_KEY,
|
|
273
|
+
name: PROCESS_NAME,
|
|
274
|
+
command: `powershell.exe -NoProfile -WindowStyle Hidden -Command ${run}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Add the Run-key value. True on success, "no_autostart" (the watcher still runs) otherwise. */
|
|
279
|
+
function startOnWindowsLogin(run = spawnSync) {
|
|
280
|
+
const entry = windowsLoginEntry();
|
|
281
|
+
const r = run(system32("reg.exe"), ["add", entry.key, "/v", entry.name, "/t", "REG_SZ", "/d", entry.command, "/f"], {
|
|
282
|
+
stdio: "pipe", encoding: "utf8",
|
|
283
|
+
});
|
|
284
|
+
if (r.status !== 0) {
|
|
285
|
+
console.log(color.yellow(` ⚠ Could not set niro-watch to start on login: ${(r.stderr || r.stdout || "").trim()}`));
|
|
286
|
+
return "no_autostart";
|
|
287
|
+
}
|
|
288
|
+
console.log(` ${color.green("✓")} Starts again at every login.`);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Remove the Run-key value: `{ hook: "none" }` when it is not there, `"removed"`, or `"manual"` with
|
|
294
|
+
* the command when reg refuses (asked first, so a refusal is never reported as nothing there).
|
|
295
|
+
*/
|
|
296
|
+
function stopOnWindowsLogin(run = spawnSync) {
|
|
297
|
+
const entry = windowsLoginEntry();
|
|
298
|
+
const reg = system32("reg.exe");
|
|
299
|
+
if (run(reg, ["query", entry.key, "/v", entry.name], { stdio: "pipe" }).status !== 0) return { hook: "none" };
|
|
300
|
+
if (run(reg, ["delete", entry.key, "/v", entry.name, "/f"], { stdio: "pipe" }).status === 0) return { hook: "removed" };
|
|
301
|
+
return { hook: "manual", command: `reg delete "${entry.key}" /v ${entry.name} /f` };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* What to show before sudo runs, and its `-p` prompt as one single-quoted shell word. sudo itself
|
|
306
|
+
* names the account whose password it wants (%p): the user's own, or root's where sudo is set to
|
|
307
|
+
* ask for that (targetpw, rootpw), and no user lookup is needed (os.userInfo throws in a container
|
|
308
|
+
* with no passwd entry).
|
|
309
|
+
*
|
|
310
|
+
* `sameLineAs`: the heading printed just above (niro start), with nothing after it yet. niro goes
|
|
311
|
+
* back up a line and rewrites it ending " · ", so heading and question share a line. The prompt
|
|
312
|
+
* itself never moves the cursor: sudo repeats it after a wrong password, and a cursor-up in it
|
|
313
|
+
* erased sudo's "Sorry, try again." When sudo still remembers the password it prints no prompt,
|
|
314
|
+
* so the heading is left alone (`sudo -n true` asks without prompting).
|
|
315
|
+
*/
|
|
316
|
+
function sudoLead(sameLineAs, pad, run = spawnSync) {
|
|
317
|
+
const quoted = (words) => `'${words.replace(/'/g, "'\\''")}'`;
|
|
318
|
+
const ask = "Password for %p: ";
|
|
319
|
+
if (sameLineAs && run("sudo", ["-n", "true"], { stdio: "ignore" }).status !== 0) {
|
|
320
|
+
return { before: `\x1b[1A\x1b[2K${sameLineAs} · `, prompt: quoted(ask) };
|
|
321
|
+
}
|
|
322
|
+
return { before: "", prompt: quoted(`${pad}${ask}`) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Install and start the niro-watch PM2 process, then wire it into the OS
|
|
327
|
+
* startup system. Prints an explanation before each privileged step.
|
|
328
|
+
*
|
|
329
|
+
* Returns true on success, "no_autostart" when niro-watch runs but could not be added to the
|
|
330
|
+
* OS startup (still truthy: the daemon is up), false if PM2 is not installed.
|
|
331
|
+
*/
|
|
332
|
+
function install({ sameLineAs = null } = {}) {
|
|
333
|
+
if (!pm2Available()) return false;
|
|
334
|
+
|
|
335
|
+
startWatcher();
|
|
336
|
+
|
|
337
|
+
if (process.platform === "win32") return startOnWindowsLogin();
|
|
87
338
|
|
|
88
339
|
// Wire into OS startup — pm2 startup detects systemd / launchd / etc.
|
|
89
340
|
// It prints the exact privileged command we need to run.
|
|
90
|
-
console.log(
|
|
91
|
-
`\n ${color.bold("Setting up auto-start on login")} — PM2 will ask for your password to register`
|
|
92
|
-
);
|
|
93
|
-
console.log(
|
|
94
|
-
` with the system startup manager (${getInitSystem()}). This only needs to happen once.\n`
|
|
95
|
-
);
|
|
96
|
-
|
|
97
341
|
const startupProbe = spawnSync("pm2", ["startup"], {
|
|
98
|
-
shell:
|
|
342
|
+
shell: PM2_SHELL,
|
|
99
343
|
encoding: "utf8",
|
|
100
344
|
});
|
|
101
345
|
|
|
102
346
|
const setupCmd = extractSetupCommand(startupProbe.stdout + startupProbe.stderr);
|
|
103
347
|
if (setupCmd) {
|
|
104
|
-
|
|
348
|
+
// sudo's own prompt says what the password is for, instead of a paragraph and the command.
|
|
349
|
+
// Spaces, not the rail: sudo mangles the multi-byte "│" in its prompt.
|
|
350
|
+
const pad = " ".repeat(require("./nestedOutput").currentPad().length + 2);
|
|
351
|
+
const lead = sudoLead(sameLineAs, pad);
|
|
352
|
+
const cmd = setupCmd.replace(/^sudo /, `sudo -p ${lead.prompt} `);
|
|
353
|
+
// pm2 writes its launchd plist before it runs its own `mkdir -p`, so a home without
|
|
354
|
+
// ~/Library/LaunchAgents (a new account) fails with ENOENT. Make the folder first.
|
|
355
|
+
if (process.platform === "darwin") {
|
|
356
|
+
fs.mkdirSync(path.join(os.homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
357
|
+
}
|
|
105
358
|
// stdio is split: stdin stays connected so sudo can still prompt for the password, but
|
|
106
359
|
// PM2's forty lines of generated unit file are captured and only shown if it fails.
|
|
107
|
-
|
|
360
|
+
if (lead.before) process.stdout.write(lead.before);
|
|
361
|
+
const r = spawnSync(cmd, {
|
|
108
362
|
shell: true,
|
|
109
363
|
stdio: ["inherit", "pipe", "pipe"],
|
|
110
364
|
encoding: "utf8",
|
|
111
365
|
});
|
|
112
366
|
if (r.status !== 0) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
367
|
+
// PM2's output here is its whole generated plist/unit file; the last line is the reason.
|
|
368
|
+
const reason = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").pop();
|
|
369
|
+
console.log(color.yellow(` ⚠ Could not set niro-watch to start on login${reason ? `: ${reason}` : ""}`));
|
|
370
|
+
console.log(color.dim(` To retry: ${setupCmd}`));
|
|
371
|
+
return "no_autostart";
|
|
119
372
|
}
|
|
120
373
|
} else {
|
|
121
374
|
// PM2 may print nothing if startup is already configured
|
|
122
|
-
console.log(`
|
|
375
|
+
console.log(` ${color.green("✓")} Startup already configured.`);
|
|
123
376
|
}
|
|
124
377
|
|
|
125
378
|
return true;
|
|
126
379
|
}
|
|
127
380
|
|
|
128
381
|
/**
|
|
129
|
-
* Stop niro-watch and
|
|
382
|
+
* Stop niro-watch and take it out of PM2, quietly, without asking for a password.
|
|
383
|
+
*
|
|
384
|
+
* PM2's start-on-login entry belongs to PM2, not to Niro: it starts every app in PM2's saved
|
|
385
|
+
* list. Once niro-watch is out of that list (`pm2 save --force`, so an empty list is saved too),
|
|
386
|
+
* nothing of Niro's starts on login, so the entry is left alone while other PM2 apps use it. When
|
|
387
|
+
* niro-watch was the only one, the entry goes too: on macOS by unloading and deleting the user's
|
|
388
|
+
* own launchd file, which needs no password; elsewhere it needs root, so the command is returned
|
|
389
|
+
* for the caller to show rather than prompting for a password in the middle of an uninstall.
|
|
390
|
+
*
|
|
391
|
+
* Returns false when PM2 is not installed (nothing to remove), else
|
|
392
|
+
* `{ hook: "removed" | "kept" | "none" | "manual", command? }`.
|
|
130
393
|
*/
|
|
131
394
|
function uninstall() {
|
|
132
395
|
if (!pm2Available()) return false;
|
|
396
|
+
const quiet = { shell: PM2_SHELL, stdio: "pipe", encoding: "utf8" };
|
|
397
|
+
spawnSync("pm2", ["delete", PROCESS_NAME], quiet);
|
|
398
|
+
const saved = spawnSync("pm2", ["save", "--force"], quiet);
|
|
399
|
+
// The backup PM2 just made is the list from before, niro-watch's environment included.
|
|
400
|
+
secureDump({ refreshBackup: saved.status === 0 });
|
|
133
401
|
|
|
134
|
-
spawnSync("pm2", ["
|
|
135
|
-
|
|
402
|
+
const listed = spawnSync("pm2", ["jlist"], quiet);
|
|
403
|
+
let others = 0;
|
|
404
|
+
try { others = JSON.parse(listed.stdout || "[]").length; } catch { others = 1; /* unsure: keep it */ }
|
|
405
|
+
if (others > 0) return { hook: "kept" };
|
|
136
406
|
|
|
137
|
-
|
|
138
|
-
`\n ${color.bold("Removing auto-start hook")} — PM2 may ask for your password.\n`
|
|
139
|
-
);
|
|
140
|
-
const unstartupResult = spawnSync("pm2", ["unstartup"], {
|
|
141
|
-
shell: true,
|
|
142
|
-
encoding: "utf8",
|
|
143
|
-
});
|
|
144
|
-
const setupCmd = extractSetupCommand(
|
|
145
|
-
unstartupResult.stdout + unstartupResult.stderr
|
|
146
|
-
);
|
|
147
|
-
if (setupCmd) {
|
|
148
|
-
console.log(` Running: ${color.dim(setupCmd)}\n`);
|
|
149
|
-
spawnSync(setupCmd, { shell: true, stdio: "inherit" });
|
|
150
|
-
}
|
|
407
|
+
if (process.platform === "win32") return stopOnWindowsLogin();
|
|
151
408
|
|
|
152
|
-
|
|
409
|
+
if (process.platform === "darwin") {
|
|
410
|
+
const user = os.userInfo().username;
|
|
411
|
+
const plist = path.join(os.homedir(), "Library", "LaunchAgents", `pm2.${user}.plist`);
|
|
412
|
+
if (!fs.existsSync(plist)) return { hook: "none" };
|
|
413
|
+
spawnSync("launchctl", ["unload", "-w", plist], { stdio: "pipe" });
|
|
414
|
+
try {
|
|
415
|
+
fs.unlinkSync(plist);
|
|
416
|
+
return { hook: "removed" };
|
|
417
|
+
} catch {
|
|
418
|
+
// fall through to the command
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const probe = spawnSync("pm2", ["unstartup"], quiet);
|
|
422
|
+
const command = extractSetupCommand(`${probe.stdout || ""}${probe.stderr || ""}`);
|
|
423
|
+
return command ? { hook: "manual", command } : { hook: "none" };
|
|
153
424
|
}
|
|
154
425
|
|
|
155
426
|
/**
|
|
@@ -157,16 +428,38 @@ function uninstall() {
|
|
|
157
428
|
*
|
|
158
429
|
* `npm install -g` swaps the files on disk. A process that is already running keeps the code it
|
|
159
430
|
* loaded when it started, so after an upgrade the watcher is still on the old version until this
|
|
160
|
-
* runs.
|
|
431
|
+
* runs. It registers the script of the niro running this (process.argv[1]); during
|
|
432
|
+
* `niro upgrade --finish` that is the copy just installed. Returns false when PM2 is missing or
|
|
433
|
+
* the start failed; the caller decides what to say.
|
|
161
434
|
*/
|
|
162
435
|
function restart() {
|
|
163
436
|
if (!pm2Available()) return false;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
437
|
+
// Not `pm2 restart`: that keeps the environment saved when the watcher was first started, which
|
|
438
|
+
// on an install from before 2026-11-06 is the whole shell. Delete, start and save instead, with
|
|
439
|
+
// the short list, then make the saved list private. No `pm2 startup`, so no password prompt.
|
|
440
|
+
try {
|
|
441
|
+
startWatcher();
|
|
442
|
+
return true;
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* When the running niro-watch process started, in ms since the epoch, or null when PM2 cannot
|
|
450
|
+
* say. Compared with the install time of this copy to tell a watcher still on old code.
|
|
451
|
+
*/
|
|
452
|
+
function startedAt() {
|
|
453
|
+
if (!pm2Available() || !pm2DaemonAlive()) return null;
|
|
454
|
+
const r = spawnSync("pm2", ["jlist"], { shell: PM2_SHELL, stdio: "pipe", encoding: "utf8" });
|
|
455
|
+
if (r.status !== 0) return null;
|
|
456
|
+
try {
|
|
457
|
+
const proc = JSON.parse(r.stdout).find((p) => p.name === PROCESS_NAME);
|
|
458
|
+
const at = proc && proc.pm2_env && proc.pm2_env.pm_uptime;
|
|
459
|
+
return typeof at === "number" ? at : null;
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
170
463
|
}
|
|
171
464
|
|
|
172
465
|
/**
|
|
@@ -177,7 +470,7 @@ function status() {
|
|
|
177
470
|
console.log(color.yellow(" PM2 is not installed. Run: npm install -g pm2"));
|
|
178
471
|
return;
|
|
179
472
|
}
|
|
180
|
-
spawnSync("pm2", ["show", PROCESS_NAME], { shell:
|
|
473
|
+
spawnSync("pm2", ["show", PROCESS_NAME], { shell: PM2_SHELL, stdio: "inherit" });
|
|
181
474
|
}
|
|
182
475
|
|
|
183
476
|
/**
|
|
@@ -196,7 +489,7 @@ function processState() {
|
|
|
196
489
|
return dumpHasProcess() ? "stopped" : "missing";
|
|
197
490
|
}
|
|
198
491
|
const r = spawnSync("pm2", ["show", PROCESS_NAME], {
|
|
199
|
-
shell:
|
|
492
|
+
shell: PM2_SHELL,
|
|
200
493
|
encoding: "utf8",
|
|
201
494
|
stdio: "pipe",
|
|
202
495
|
});
|
|
@@ -209,8 +502,14 @@ function processState() {
|
|
|
209
502
|
* the user sees progress and any permission error verbatim. Returns true on
|
|
210
503
|
* success. shell:true so npm.cmd resolves on Windows.
|
|
211
504
|
*/
|
|
212
|
-
function installPm2() {
|
|
213
|
-
const r = spawnSync("npm", ["install", "-g", "pm2"], {
|
|
505
|
+
function installPm2({ quiet = false } = {}) {
|
|
506
|
+
const r = spawnSync("npm", ["install", "-g", "pm2"], {
|
|
507
|
+
shell: process.platform === "win32",
|
|
508
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
509
|
+
encoding: "utf8",
|
|
510
|
+
});
|
|
511
|
+
// quiet: npm's lines are shown only when it failed, where they are the only diagnostic.
|
|
512
|
+
if (quiet && r.status !== 0) console.log(color.dim(`${r.stdout || ""}${r.stderr || ""}`.trim()));
|
|
214
513
|
return r.status === 0;
|
|
215
514
|
}
|
|
216
515
|
|
|
@@ -243,4 +542,14 @@ function getInitSystem() {
|
|
|
243
542
|
}
|
|
244
543
|
}
|
|
245
544
|
|
|
246
|
-
module.exports = {
|
|
545
|
+
module.exports = {
|
|
546
|
+
windowsLoginEntry,
|
|
547
|
+
startOnWindowsLogin,
|
|
548
|
+
stopOnWindowsLogin,
|
|
549
|
+
pm2StartArgs,
|
|
550
|
+
sudoLead,
|
|
551
|
+
install, uninstall, restart, startedAt, status, processState, pm2Available, installPm2,
|
|
552
|
+
refreshSavedEnv,
|
|
553
|
+
// exported for tests
|
|
554
|
+
watcherEnv, savedEnvIsWide, secureDump,
|
|
555
|
+
};
|