@niroai/niro 0.12.0 → 0.15.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.
@@ -0,0 +1,400 @@
1
+ /**
2
+ * `niro upgrade` — replace this install with the latest published package, then put back the
3
+ * things replacing the package cannot put back for itself.
4
+ *
5
+ * Why this runs as two processes.
6
+ *
7
+ * Every command in this CLI is loaded lazily, inside its own action handler (src/index.js), and
8
+ * `npm install -g` replaces the folder those files are loaded from. A single process that ran npm
9
+ * and then carried on would be running the modules it loaded before the swap alongside modules
10
+ * loaded after it. Nothing tests that mixture. start.js has always avoided the same trap the same
11
+ * way: bootstrapIfNpx installs globally only when it is running from the npx cache, so it never
12
+ * overwrites the tree it is executing, and then it hands over.
13
+ *
14
+ * phase one, this copy run npm, then exec the NEW global binary with --finish and stop.
15
+ * phase two, the new copy a fresh process reading the new tree: restart the watcher, refresh
16
+ * each configured client's hooks and skills, ask for an assistant
17
+ * restart.
18
+ *
19
+ * Phase one requires nothing between the npm call and the hand-over, and it also catches its own
20
+ * errors rather than letting them reach src/index.js `run()`, whose catch does
21
+ * `require("./lib/printError")` — a lazy load out of the folder npm has just replaced. That is the
22
+ * one path where the swap could still bite, so it is closed here rather than described.
23
+ *
24
+ * What it deliberately does not do is write a settings file itself. Refreshing hooks means
25
+ * spawning the existing installer, once per client. A second write path into
26
+ * ~/.claude/settings.json was built once, reviewed three times and reverted on 2026-07-30
27
+ * (d3ff3d8) after it was found deleting users' own hooks, changing that file from mode 0600 to
28
+ * 0644 while it can hold API keys, and, with HOME unset, writing into a project's git-tracked
29
+ * .claude/settings.json. The installer already gets those cases right, and here it runs because
30
+ * the user asked for it.
31
+ */
32
+ const childProcess = require("child_process");
33
+ const fs = require("fs");
34
+ const path = require("path");
35
+
36
+ const color = require("../lib/color");
37
+ const service = require("../lib/service");
38
+ const mcpClients = require("../lib/mcpClients");
39
+ const mcpCmd = require("./mcp");
40
+ const startCmd = require("./start");
41
+
42
+ const PACKAGE_NAME = "@niroai/niro";
43
+ const VERSION = require("../../package.json").version;
44
+
45
+ /**
46
+ * Set on the process phase one hands over to. If phase one ever sees it already set, it is itself
47
+ * a hand-over child that failed to reach phase two, which means the newly installed version does
48
+ * not understand `--finish`. Without this, that version would run npm and hand over again, without
49
+ * bound. start.js guards its own hand-over with BOOTSTRAP_ENV for the same reason.
50
+ */
51
+ const HANDOVER_ENV = "NIRO_UPGRADE_HANDOVER";
52
+
53
+ /** Exit codes, so a script can tell the failures apart. */
54
+ const EXIT_NPM_FAILED = 1;
55
+ const EXIT_NO_BINARY = 2;
56
+ const EXIT_HANDOVER_FAILED = 3;
57
+ const EXIT_REFRESH_INCOMPLETE = 4;
58
+ const EXIT_WRONG_COPY = 5;
59
+
60
+ /** `niro upgrade` (phase one) and `niro upgrade --finish` (phase two). */
61
+ async function upgrade(opts = {}) {
62
+ if (opts.finish) return finish(opts.from);
63
+ return replaceAndHandOver(Boolean(opts.force));
64
+ }
65
+
66
+ /**
67
+ * Phase one. Replace the package, then hand over to the copy that replaced it.
68
+ *
69
+ * Everything this function touches is already loaded by the requires at the top of this file, and
70
+ * every failure is reported here rather than thrown, because a throw would reach a catch that
71
+ * lazily loads a module out of the folder npm has just swapped.
72
+ */
73
+ function replaceAndHandOver(force) {
74
+ if (process.env[HANDOVER_ENV]) {
75
+ console.error(color.red("\n The version just installed does not support `niro upgrade --finish`."));
76
+ console.error(" The package itself is upgraded. Your hooks, skills and watcher are not.");
77
+ console.error(` Finish by hand: ${color.cyan("niro mcp install")} for each assistant you use,`);
78
+ console.error(` then ${color.cyan("niro service install")} if you run the background watcher.\n`);
79
+ process.exitCode = EXIT_HANDOVER_FAILED;
80
+ return;
81
+ }
82
+
83
+ const running = describeRunningCopy();
84
+ if (running.kind === "linked" && !force) {
85
+ console.error(color.yellow("\n Not upgrading: niro is npm-linked to a working copy."));
86
+ console.error(` ${color.cyan("npm prefix -g")} says the global package folder is a link:`);
87
+ console.error(` ${running.pkgDir}`);
88
+ console.error(` -> ${running.linkTarget}`);
89
+ console.error(" `npm install -g` would replace that link with the published package, and");
90
+ console.error(" `niro` would stop running your checkout. Your files would be untouched, but");
91
+ console.error(` you would have to run ${color.cyan("npm link")} in the checkout to get it back.`);
92
+ console.error(` Upgrade anyway with ${color.cyan("niro upgrade --force")}.\n`);
93
+ process.exitCode = EXIT_WRONG_COPY;
94
+ return;
95
+ }
96
+ if (running.kind === "elsewhere" && !force) {
97
+ console.error(color.yellow("\n Not upgrading: you are not running the globally installed niro."));
98
+ console.error(` running: ${running.self || "unknown"}`);
99
+ console.error(` would upgrade: ${running.pkgDir || "the npm global folder"}`);
100
+ console.error(" Those are different copies, so this would replace one you are not using and");
101
+ console.error(" leave the one you are.");
102
+ console.error(` Upgrade the global copy anyway with ${color.cyan("niro upgrade --force")}.\n`);
103
+ process.exitCode = EXIT_WRONG_COPY;
104
+ return;
105
+ }
106
+
107
+ console.log(`\n Upgrading niro. Installed now: ${VERSION}\n`);
108
+
109
+ const installed = childProcess.spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
110
+ shell: true,
111
+ stdio: "inherit",
112
+ });
113
+
114
+ if (installed.signal) {
115
+ // Ctrl+C reaches the whole process group, so npm dies with this process. Whether it left the
116
+ // global folder half-written is npm's business and not something this can know, so it is not
117
+ // claimed either way.
118
+ console.error(color.yellow(`\n Interrupted (${installed.signal}) while npm was running.`));
119
+ console.error(" Whether the package was replaced is unknown. Check with `niro --version`,");
120
+ console.error(` then run ${color.cyan("niro upgrade")} again.\n`);
121
+ process.exitCode = EXIT_NPM_FAILED;
122
+ return;
123
+ }
124
+
125
+ if (installed.status !== 0) {
126
+ console.error(color.red("\n npm could not replace the package."));
127
+ console.error(" The usual cause is that your npm global folder needs root. Find it with");
128
+ console.error(` ${color.cyan("npm prefix -g")}, then either fix its ownership or re-run the install`);
129
+ console.error(" with the privileges it needs:");
130
+ console.error(` ${color.cyan(`npm install -g ${PACKAGE_NAME}@latest`)}`);
131
+ console.error(` Then run ${color.cyan("niro upgrade --finish")} to refresh your assistant files.\n`);
132
+ process.exitCode = EXIT_NPM_FAILED;
133
+ return;
134
+ }
135
+
136
+ const bin = startCmd.globalNiroPath();
137
+ if (!bin) {
138
+ console.error(color.red("\n npm reported success, but there is no niro in your npm global folder."));
139
+ console.error(` Check ${color.cyan("npm prefix -g")} and that its bin folder is on your PATH.`);
140
+ console.error(" The package is upgraded; only the refresh step was skipped. Run it with");
141
+ console.error(` ${color.cyan("niro upgrade --finish")} once niro is back on PATH.\n`);
142
+ process.exitCode = EXIT_NO_BINARY;
143
+ return;
144
+ }
145
+
146
+ if (!supportsFinish(bin)) {
147
+ // The version just installed has no `upgrade` command, which is what happens when the newest
148
+ // published version predates this one. Saying "re-run niro upgrade --finish" here would send
149
+ // the user to a command that cannot exist.
150
+ console.error(color.yellow("\n The package is upgraded, but the version now installed has no"));
151
+ console.error(" `niro upgrade` command, so it cannot refresh your assistant files itself.");
152
+ console.error(" Finish by hand:");
153
+ console.error(` ${color.cyan("niro mcp install")} once per assistant you use`);
154
+ console.error(` ${color.cyan("niro service install")} if you run the background watcher`);
155
+ console.error(" Then restart your assistant.\n");
156
+ process.exitCode = EXIT_HANDOVER_FAILED;
157
+ return;
158
+ }
159
+
160
+ // The hand-over. A fresh process reads the new tree cleanly; this one cannot. VERSION comes from
161
+ // package.json, so it is a semver string and safe on a Windows command line. The Windows branch
162
+ // is untested: spawning a .cmd shim needs a shell on current Node, and a global prefix
163
+ // containing spaces is why the path is quoted.
164
+ const env = { ...process.env, [HANDOVER_ENV]: "1" };
165
+ const handover = process.platform === "win32"
166
+ ? childProcess.spawnSync(`"${bin}" upgrade --finish --from ${VERSION}`, { stdio: "inherit", shell: true, env })
167
+ : childProcess.spawnSync(bin, ["upgrade", "--finish", "--from", VERSION], { stdio: "inherit", env });
168
+
169
+ if (handover.error) {
170
+ // spawnSync could not start the child at all: a non-executable bin link, a directory where the
171
+ // binary should be, a broken shim. Without this the command exits silently.
172
+ console.error(color.red(`\n Could not run the upgraded niro at ${bin}:`));
173
+ console.error(` ${handover.error.message}`);
174
+ console.error(" The package is upgraded; your hooks, skills and watcher are not.");
175
+ console.error(` Try ${color.cyan("niro upgrade --finish")}, or reinstall with`);
176
+ console.error(` ${color.cyan(`npm install -g ${PACKAGE_NAME}@latest`)}.\n`);
177
+ process.exitCode = EXIT_HANDOVER_FAILED;
178
+ return;
179
+ }
180
+
181
+ if (handover.signal) {
182
+ console.error(color.yellow(`\n Interrupted (${handover.signal}) while refreshing.`));
183
+ console.error(` The package is upgraded. Finish with ${color.cyan("niro upgrade --finish")}.\n`);
184
+ process.exitCode = EXIT_HANDOVER_FAILED;
185
+ return;
186
+ }
187
+
188
+ if (handover.status !== 0) {
189
+ console.error(`\n The upgraded niro exited with ${handover.status} while refreshing.`);
190
+ console.error(` The package is upgraded. Re-run ${color.cyan("niro upgrade --finish")} to try again.\n`);
191
+ }
192
+ process.exitCode = handover.status === null ? EXIT_HANDOVER_FAILED : handover.status;
193
+ }
194
+
195
+ /** Where npm would install this package globally. */
196
+ function globalPackageDir() {
197
+ const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: true, encoding: "utf8" });
198
+ if (r.status !== 0) return null;
199
+ const prefix = String(r.stdout || "").trim();
200
+ if (!prefix) return null;
201
+ return process.platform === "win32"
202
+ ? path.join(prefix, "node_modules", PACKAGE_NAME.replace("/", path.sep))
203
+ : path.join(prefix, "lib", "node_modules", PACKAGE_NAME.replace("/", path.sep));
204
+ }
205
+
206
+ /**
207
+ * Which copy of niro is running this, checked BEFORE npm is allowed to touch anything.
208
+ *
209
+ * "global" this process is the globally installed package, so replacing it is the job
210
+ * "linked" the global package folder is a symlink, which is what `npm link` leaves behind.
211
+ * `npm install -g` deletes that link and puts the published package in its place,
212
+ * so a developer working on niro loses the wiring between `niro` and their checkout.
213
+ * "elsewhere" running from somewhere else entirely: a checkout invoked directly, or the npx
214
+ * cache. Upgrading then replaces a copy the user is not running.
215
+ *
216
+ * This exists because the first real run of `niro upgrade` was from a linked checkout and silently
217
+ * replaced it with the published package.
218
+ */
219
+ function describeRunningCopy(pkgDirOverride) {
220
+ // The directory is a parameter so a unit test can exercise every branch without shelling out to
221
+ // npm at all. An end-to-end rehearsal takes the other route and sets npm_config_prefix, which
222
+ // npm does honour (scripts/rehearse-upgrade.sh).
223
+ const pkgDir = pkgDirOverride === undefined ? globalPackageDir() : pkgDirOverride;
224
+
225
+ let self = null;
226
+ try {
227
+ self = fs.realpathSync(process.argv[1]);
228
+ } catch (err) {
229
+ self = null;
230
+ }
231
+
232
+ if (pkgDir) {
233
+ try {
234
+ if (fs.lstatSync(pkgDir).isSymbolicLink()) {
235
+ return { kind: "linked", pkgDir, linkTarget: fs.realpathSync(pkgDir), self };
236
+ }
237
+ } catch (err) {
238
+ // Not installed globally at all. Falls through to "elsewhere".
239
+ }
240
+ }
241
+
242
+ let pkgReal = null;
243
+ try {
244
+ pkgReal = pkgDir ? fs.realpathSync(pkgDir) : null;
245
+ } catch (err) {
246
+ pkgReal = null;
247
+ }
248
+ if (pkgReal && self && (self === pkgReal || self.startsWith(pkgReal + path.sep))) {
249
+ return { kind: "global", pkgDir, linkTarget: null, self };
250
+ }
251
+ return { kind: "elsewhere", pkgDir, linkTarget: null, self };
252
+ }
253
+
254
+ /**
255
+ * Does the version now installed actually have `niro upgrade --finish`?
256
+ *
257
+ * Asked rather than assumed, because the newest published version can predate this one: a machine
258
+ * running an unreleased build upgrades "up" to a package with no `upgrade` command at all. Without
259
+ * this, the hand-over fails with commander's bare `error: unknown command 'upgrade'` and the user
260
+ * is told to re-run a command that cannot exist.
261
+ */
262
+ function supportsFinish(bin) {
263
+ const probe = process.platform === "win32"
264
+ ? childProcess.spawnSync(`"${bin}" upgrade --help`, { shell: true, encoding: "utf8", stdio: "pipe" })
265
+ : childProcess.spawnSync(bin, ["upgrade", "--help"], { encoding: "utf8", stdio: "pipe" });
266
+ if (probe.error || probe.status !== 0) return false;
267
+ return String(probe.stdout || "").includes("--finish");
268
+ }
269
+
270
+ /** Phase two, running from the new package: put back what npm could not. */
271
+ async function finish(fromVersion) {
272
+ console.log(fromVersion && fromVersion !== VERSION
273
+ ? `\n niro ${fromVersion} to ${VERSION}\n`
274
+ : `\n niro ${VERSION}\n`);
275
+
276
+ // The watcher first. It is one command and cannot block, whereas a client refresh can sit on a
277
+ // prompt for as long as the user is away, and until the restart happens the watcher is running
278
+ // code from a package that no longer exists on disk.
279
+ restartWatcher();
280
+
281
+ const complete = await refreshAssistantFiles();
282
+ if (!complete) process.exitCode = EXIT_REFRESH_INCOMPLETE;
283
+
284
+ console.log(` ${color.yellow("One thing left:")} restart your AI assistant.`);
285
+ console.log(" Hooks and skills are read when it starts, so an assistant that is already open");
286
+ console.log(" is still using the old ones.\n");
287
+ }
288
+
289
+ /**
290
+ * Re-run the installer once per configured client, so their hooks, skills and instruction files
291
+ * match the package that is now installed. Returns false when anything was skipped or failed, so
292
+ * the caller can set a non-zero exit code: a script driving this needs to know that half of it
293
+ * did nothing.
294
+ *
295
+ * One client failing does not stop the others. They are separate files owned by separate tools,
296
+ * and a missing editor CLI is an ordinary reason for one to fail.
297
+ */
298
+ async function refreshAssistantFiles() {
299
+ const { home, configured } = mcpClients.configuredClients();
300
+
301
+ if (!home) {
302
+ console.log(` ${color.yellow("Skipped the assistant files:")} there is no usable home folder.`);
303
+ console.log(" HOME is unset or empty, so every path would have been read relative to the");
304
+ console.log(" folder you are standing in, which is not yours to read or write.");
305
+ console.log(` Set HOME, then run ${color.cyan("niro upgrade --finish")}.\n`);
306
+ return false;
307
+ }
308
+
309
+ if (configured.length === 0) {
310
+ console.log(" No configured assistant found, so there were no hooks or skills to refresh.");
311
+ console.log(` Connect one with ${color.cyan("niro mcp install")}.\n`);
312
+ return true;
313
+ }
314
+
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`);
318
+ }
319
+
320
+ // Cursor's installer writes a project-scoped rule to <cwd>/.cursor/rules/niro.mdc, so refreshing
321
+ // it leaves a file in whatever folder the upgrade was run from. That is the installer's design,
322
+ // not something to change here, but it is a surprise when you only asked to upgrade.
323
+ if (configured.some((c) => c.id === "cursor")) {
324
+ console.log(` ${color.yellow("Note:")} refreshing Cursor also writes a project rule to`);
325
+ console.log(` ${path.join(process.cwd(), ".cursor", "rules", "niro.mdc")}`);
326
+ console.log(" because Cursor rules are per-folder. Delete it if you did not want it here.\n");
327
+ }
328
+
329
+ const total = configured.length;
330
+ console.log(` Refreshing hooks and skills for ${total} client${total === 1 ? "" : "s"}:`);
331
+ for (const client of configured) {
332
+ console.log(` ${client.name} (${client.where})`);
333
+ }
334
+ console.log("");
335
+
336
+ let done = 0;
337
+ let failed = 0;
338
+ for (const client of configured) {
339
+ console.log(color.dim(` [${done + failed + 1} of ${total}] ${client.name}`));
340
+ try {
341
+ await mcpCmd.install(["--client", client.id]);
342
+ done += 1;
343
+ } catch (err) {
344
+ failed += 1;
345
+ console.error(` ${color.yellow("!")} ${client.name} was not refreshed: ${err.message}`);
346
+ console.error(` Run ${color.cyan(`niro mcp install --client ${client.id}`)} to see why.\n`);
347
+ }
348
+ }
349
+
350
+ if (failed > 0) {
351
+ console.log(` ${done} of ${total} refreshed, ${failed} failed.`);
352
+ console.log(` Re-run ${color.cyan("niro upgrade --finish")} once the failures above are sorted.\n`);
353
+ return false;
354
+ }
355
+ console.log(` ${color.green("Done.")} ${done} of ${total} refreshed.\n`);
356
+ return true;
357
+ }
358
+
359
+ /**
360
+ * Restart the background watcher, which is a long-running process still holding the code it loaded
361
+ * when it started. Replacing files on disk cannot change a process that is already running.
362
+ */
363
+ function restartWatcher() {
364
+ if (!service.pm2Available()) {
365
+ console.log(" Background watcher: PM2 is not installed, so there is nothing to restart.\n");
366
+ return;
367
+ }
368
+
369
+ const state = service.processState();
370
+ if (state !== "online") {
371
+ const what = state === "stopped" ? "registered but not running" : "not installed";
372
+ console.log(` Background watcher: ${what}, so it was left alone.\n`);
373
+ return;
374
+ }
375
+
376
+ console.log(" Restarting the background watcher, which is still running the old code...");
377
+ 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.
381
+ console.log(` ${color.green("Done.")} niro-watch restarted. Check it with ${color.cyan("niro service status")}.\n`);
382
+ } else {
383
+ console.log(` ${color.yellow("!")} Could not restart it. Run ${color.cyan("niro service install")}.\n`);
384
+ }
385
+ }
386
+
387
+ module.exports = {
388
+ upgrade,
389
+ // exported for tests
390
+ refreshAssistantFiles,
391
+ restartWatcher,
392
+ describeRunningCopy,
393
+ supportsFinish,
394
+ HANDOVER_ENV,
395
+ EXIT_NPM_FAILED,
396
+ EXIT_NO_BINARY,
397
+ EXIT_HANDOVER_FAILED,
398
+ EXIT_REFRESH_INCOMPLETE,
399
+ EXIT_WRONG_COPY,
400
+ };
@@ -878,13 +878,42 @@ function matchesPatterns(relativePath, includePatterns, excludePatterns) {
878
878
  return !excludePatterns.some(p => minimatch(relativePath, p));
879
879
  }
880
880
 
881
- /** Simple glob matching without a dependency: handles ** and * patterns. */
881
+ // Every character that is special inside a regular expression EXCEPT `*`, which is the
882
+ // only wildcard this glob syntax has. Escaping `.` alone was the defect, because a pattern
883
+ // is not ours to choose: the server names a ROOT-LEVEL file by its own filename
884
+ // (ai-orchestrator SourceFileFilterService.derivePatterns, the `else` branch) and the new
885
+ // directory prompt above names a directory by its own path, so a real name reaches this
886
+ // function verbatim. `main.c++` at a repo root became /^main\.c++$/, "Nothing to repeat",
887
+ // and that throw killed EVERY sync pass for the folder: the file event gate, the delta and
888
+ // backstop walk, and the rescue check that runs before a delete. The quieter half of the
889
+ // same defect never threw at all: `app (copy).js` built a valid regex with a capture group,
890
+ // so the file silently stopped matching its own pattern and was never synced again.
891
+ // No /g flag: `test` on a global regex advances lastIndex between calls.
892
+ const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/;
893
+
894
+ /**
895
+ * Simple glob matching without a dependency: handles ** and * patterns.
896
+ *
897
+ * One pass over the pattern, so every character that is not a wildcard is escaped as a
898
+ * literal and the result is always a valid regular expression. The previous version
899
+ * escaped `.` and then routed `**` through a `§§` placeholder, which also corrupted any
900
+ * pattern that genuinely contained `§§`.
901
+ */
882
902
  function minimatch(filePath, pattern) {
883
- const regexStr = pattern
884
- .replace(/\./g, "\\.")
885
- .replace(/\*\*/g, "§§") // temp placeholder
886
- .replace(/\*/g, "[^/]*")
887
- .replace(/§§/g, ".*");
903
+ let regexStr = "";
904
+ for (let i = 0; i < pattern.length; i++) {
905
+ const ch = pattern[i];
906
+ if (ch === "*") {
907
+ if (pattern[i + 1] === "*") {
908
+ regexStr += ".*"; // ** crosses directory separators
909
+ i++;
910
+ } else {
911
+ regexStr += "[^/]*"; // * stays inside one path segment
912
+ }
913
+ continue;
914
+ }
915
+ regexStr += REGEX_SPECIAL_CHARS.test(ch) ? "\\" + ch : ch;
916
+ }
888
917
  return new RegExp("^" + regexStr + "$").test(filePath);
889
918
  }
890
919
 
@@ -944,8 +973,11 @@ function buildMinimalManifest(rootDir, includePatterns, excludePatterns) {
944
973
  }
945
974
 
946
975
  // handleFileEvent + deltaSyncOnResume are exported for unit testing the
947
- // gitignore/fail-closed guards without a live server or fs.watch loop.
976
+ // gitignore/fail-closed guards without a live server or fs.watch loop. matchesPatterns is
977
+ // exported for the same reason: it is the gate all three sync paths share, and a pattern that
978
+ // cannot be compiled stops every one of them, so it is worth testing on its own.
948
979
  module.exports = { watch, startWatch, startPeriodicSweep, handleFileEvent, deltaSyncOnResume, handleEntryFailure,
980
+ matchesPatterns,
949
981
  queueDelete, flushPendingDeletes, pendingDeletes, DELETE_SETTLE_MS, backstopTick, notHolding,
950
982
  unreadableRoots,
951
983
  nextBackoffMs, shouldAttemptBackstop, recordBackstopFailure, recordBackstopSuccess };
package/src/index.js CHANGED
@@ -309,6 +309,17 @@ function main() {
309
309
  await deleteCmd.deleteProject(projectId, { ...opts });
310
310
  }));
311
311
 
312
+ program
313
+ .command("upgrade")
314
+ .description("Upgrade niro to the latest published version, then refresh the hooks, skills and background watcher that replacing the package leaves untouched.")
315
+ .option("--finish", "Only refresh hooks, skills and the watcher; do not run npm. `niro upgrade` runs this for you in the new version once the package is replaced.")
316
+ .option("--from <version>", "The version upgraded from, for the summary line. Set automatically by `niro upgrade`.")
317
+ .option("--force", "Upgrade the global copy even when you are running a linked or local checkout, which `npm install -g` would replace.")
318
+ .action((opts) => run(async () => {
319
+ const upgradeCmd = require("./commands/upgrade");
320
+ await upgradeCmd.upgrade(opts);
321
+ }));
322
+
312
323
  const svc = program
313
324
  .command("service")
314
325
  .description("Manage the niro-watch background daemon (auto-sync on login).");
@@ -34,11 +34,25 @@ const prompt = require("./prompt");
34
34
  const color = require("./color");
35
35
 
36
36
  const BATCH_SIZE = 50;
37
+ // MUST stay identical to ai-orchestrator SourceFileFilterService.SOURCE_EXTENSIONS.
38
+ // Both are hand-maintained and nothing enforced that until test/sourceExtensionDrift.test.js
39
+ // here, which reads the Java file, and ai-orchestrator SourceExtensionDriftTest, which reads
40
+ // THIS file. The two repositories build separately, so the guard is on both sides. The
41
+ // server's /filter is the authoritative gate, so an extension missing here is never
42
+ // offered and never uploaded, whatever the server would have accepted.
37
43
  const SOURCE_EXTENSIONS = new Set([
38
44
  ".java", ".py", ".go", ".js", ".ts", ".tsx", ".jsx",
39
45
  ".cjs", ".mjs", ".cts", ".mts",
40
46
  ".kt", ".scala", ".rs", ".rb", ".php", ".cs", ".swift",
41
47
  ".c", ".cpp", ".h", ".hpp", ".dart",
48
+ // The remaining C++ extensions ast-parser routes to its C++ grammar and CppMapper.
49
+ ".cc", ".cxx", ".hxx", ".hh", ".h++", ".c++", ".tcc", ".ipp", ".inl", ".tpp",
50
+ // Ten more the parser routes to a real mapper and that this list did not offer, so the
51
+ // user's Kotlin build scripts, C# scripts, Rakefiles, gemspecs, legacy PHP files and
52
+ // Python stubs were never uploaded. Added 2026-09-14 alongside .jsx.
53
+ ".kts", ".csx", ".rake", ".gemspec",
54
+ ".phtml", ".php3", ".php4", ".php5",
55
+ ".pyi", ".pyw",
42
56
  ]);
43
57
 
44
58
  // Build/config files the server's project-structure resolver needs to detect