@indigoai-us/hq-cli 5.54.0 → 5.54.1

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.
@@ -28,7 +28,7 @@
28
28
  * to silence both check + gate).
29
29
  */
30
30
 
31
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0f613895-ad4a-5211-bf75-a2e843782c47")}catch(e){}}();
31
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00fa2aa9-f5d1-57a7-92db-c441c6bef906")}catch(e){}}();
32
32
  import { spawnSync } from "node:child_process";
33
33
  import { readFileSync } from "node:fs";
34
34
  import path from "node:path";
@@ -185,12 +185,24 @@ function enforceUpdateRequired(decision, deps = {}) {
185
185
  process.exit(75);
186
186
  }
187
187
  const runner = deps.runner ?? runUpdateCommand;
188
- const result = prefix
188
+ // Resolve the concrete install argv once so the sudo fallback below can re-run
189
+ // the EXACT same command under elevation.
190
+ let primaryCmd;
191
+ let primaryArgs;
192
+ if (prefix) {
193
+ primaryCmd = "npm";
194
+ primaryArgs = buildPrefixedInstallArgv(prefix);
195
+ }
196
+ else {
197
+ const parts = (command ?? "").split(/\s+/).filter(Boolean);
198
+ primaryCmd = parts[0] ?? "";
199
+ primaryArgs = parts.slice(1);
200
+ }
201
+ let result = prefix
189
202
  ? (() => {
190
- const args = buildPrefixedInstallArgv(prefix);
191
203
  console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
192
- console.error(chalk.dim(` Running: npm ${args.join(" ")}`));
193
- return performUpdateCommand("npm", args, runner);
204
+ console.error(chalk.dim(` Running: npm ${primaryArgs.join(" ")}`));
205
+ return performUpdateCommand("npm", primaryArgs, runner);
194
206
  })()
195
207
  : (() => {
196
208
  console.error(chalk.dim(` Running: ${command}`));
@@ -198,6 +210,20 @@ function enforceUpdateRequired(decision, deps = {}) {
198
210
  ? deps.performUpdateString(command)
199
211
  : performUpdate(command, runner);
200
212
  })();
213
+ // A root-owned global install (e.g. a system `/usr` install where the CLI runs
214
+ // unprivileged — the outpost agent boxes) can't rewrite the prefix's bin dir,
215
+ // so the install above fails with EACCES (`rename /usr/bin/hq`). Retry ONCE
216
+ // under non-interactive sudo: `sudo -n` fails fast WITHOUT a password prompt
217
+ // when passwordless sudo isn't configured, so interactive installs (e.g.
218
+ // Homebrew on macOS, where the first attempt already succeeded anyway) fall
219
+ // through to the manual path unchanged, while headless boxes with passwordless
220
+ // sudo self-update cleanly.
221
+ if (!result.ok && primaryCmd) {
222
+ console.error(chalk.dim(` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`));
223
+ const sudoResult = performUpdateCommand("sudo", ["-n", primaryCmd, ...primaryArgs], runner);
224
+ if (sudoResult.ok)
225
+ result = sudoResult;
226
+ }
201
227
  if (!result.ok) {
202
228
  console.error(chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`));
203
229
  const manual = prefix
@@ -253,4 +279,4 @@ export const __test__ = {
253
279
  resolveRunningPrefix,
254
280
  };
255
281
  //# sourceMappingURL=version-gate.js.map
256
- //# debugId=0f613895-ad4a-5211-bf75-a2e843782c47
282
+ //# debugId=00fa2aa9-f5d1-57a7-92db-c441c6bef906
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.54.0",
3
+ "version": "5.54.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -364,4 +364,76 @@ describe("enforceVersionGate — hard-update path", () => {
364
364
  expect(exitSpy).toHaveBeenCalledWith(0);
365
365
  expect(performUpdateString).toHaveBeenCalledWith(updateCommand);
366
366
  });
367
+
368
+ it("retries under `sudo -n` when the unprivileged install fails (root-owned global)", async () => {
369
+ vi.spyOn(console, "error").mockImplementation(() => {});
370
+ const exitSpy = vi
371
+ .spyOn(process, "exit")
372
+ .mockImplementation(((code?: number) => {
373
+ throw new Error(`__process_exit__:${code ?? 0}`);
374
+ }) as never);
375
+ // First (unprivileged) npm install fails with EACCES; the sudo -n retry succeeds.
376
+ const runner = vi
377
+ .fn()
378
+ .mockImplementation((cmd: string) =>
379
+ cmd === "sudo" ? { ok: true } : { ok: false, detail: "EACCES" },
380
+ );
381
+ const { __test__ } = await loadModule();
382
+ const prefix = "/usr";
383
+
384
+ expect(() =>
385
+ __test__.enforceUpdateRequired(
386
+ {
387
+ clientId: "hq-cli",
388
+ currentVersion: "5.10.0",
389
+ minVersion: "5.20.0",
390
+ latestVersion: "5.24.0",
391
+ updateRequired: true,
392
+ updateRecommended: false,
393
+ updateCommand: "npm install -g @indigoai-us/hq-cli@latest",
394
+ },
395
+ { resolvePrefix: () => prefix, runner },
396
+ ),
397
+ ).toThrow(/__process_exit__:0/); // succeeds after the sudo retry
398
+
399
+ expect(exitSpy).toHaveBeenCalledWith(0);
400
+ const installArgs = [
401
+ "install",
402
+ "-g",
403
+ "--prefix",
404
+ prefix,
405
+ "@indigoai-us/hq-cli@latest",
406
+ ];
407
+ expect(runner).toHaveBeenCalledWith("npm", installArgs);
408
+ expect(runner).toHaveBeenCalledWith("sudo", ["-n", "npm", ...installArgs]);
409
+ });
410
+
411
+ it("exits 75 when BOTH the unprivileged install and the sudo -n retry fail", async () => {
412
+ vi.spyOn(console, "error").mockImplementation(() => {});
413
+ const exitSpy = vi
414
+ .spyOn(process, "exit")
415
+ .mockImplementation(((code?: number) => {
416
+ throw new Error(`__process_exit__:${code ?? 0}`);
417
+ }) as never);
418
+ const runner = vi.fn().mockReturnValue({ ok: false, detail: "EACCES" });
419
+ const { __test__ } = await loadModule();
420
+
421
+ expect(() =>
422
+ __test__.enforceUpdateRequired(
423
+ {
424
+ clientId: "hq-cli",
425
+ currentVersion: "5.10.0",
426
+ minVersion: "5.20.0",
427
+ latestVersion: "5.24.0",
428
+ updateRequired: true,
429
+ updateRecommended: false,
430
+ updateCommand: "npm install -g @indigoai-us/hq-cli@latest",
431
+ },
432
+ { resolvePrefix: () => "/usr", runner },
433
+ ),
434
+ ).toThrow(/__process_exit__:75/);
435
+
436
+ expect(exitSpy).toHaveBeenCalledWith(75);
437
+ expect(runner).toHaveBeenCalledWith("sudo", expect.arrayContaining(["-n"]));
438
+ });
367
439
  });
@@ -236,12 +236,24 @@ function enforceUpdateRequired(
236
236
  }
237
237
 
238
238
  const runner = deps.runner ?? runUpdateCommand;
239
- const result = prefix
239
+ // Resolve the concrete install argv once so the sudo fallback below can re-run
240
+ // the EXACT same command under elevation.
241
+ let primaryCmd: string;
242
+ let primaryArgs: string[];
243
+ if (prefix) {
244
+ primaryCmd = "npm";
245
+ primaryArgs = buildPrefixedInstallArgv(prefix);
246
+ } else {
247
+ const parts = (command ?? "").split(/\s+/).filter(Boolean);
248
+ primaryCmd = parts[0] ?? "";
249
+ primaryArgs = parts.slice(1);
250
+ }
251
+
252
+ let result = prefix
240
253
  ? (() => {
241
- const args = buildPrefixedInstallArgv(prefix);
242
254
  console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
243
- console.error(chalk.dim(` Running: npm ${args.join(" ")}`));
244
- return performUpdateCommand("npm", args, runner);
255
+ console.error(chalk.dim(` Running: npm ${primaryArgs.join(" ")}`));
256
+ return performUpdateCommand("npm", primaryArgs, runner);
245
257
  })()
246
258
  : (() => {
247
259
  console.error(chalk.dim(` Running: ${command}`));
@@ -249,6 +261,29 @@ function enforceUpdateRequired(
249
261
  ? deps.performUpdateString(command!)
250
262
  : performUpdate(command!, runner);
251
263
  })();
264
+
265
+ // A root-owned global install (e.g. a system `/usr` install where the CLI runs
266
+ // unprivileged — the outpost agent boxes) can't rewrite the prefix's bin dir,
267
+ // so the install above fails with EACCES (`rename /usr/bin/hq`). Retry ONCE
268
+ // under non-interactive sudo: `sudo -n` fails fast WITHOUT a password prompt
269
+ // when passwordless sudo isn't configured, so interactive installs (e.g.
270
+ // Homebrew on macOS, where the first attempt already succeeded anyway) fall
271
+ // through to the manual path unchanged, while headless boxes with passwordless
272
+ // sudo self-update cleanly.
273
+ if (!result.ok && primaryCmd) {
274
+ console.error(
275
+ chalk.dim(
276
+ ` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`,
277
+ ),
278
+ );
279
+ const sudoResult = performUpdateCommand(
280
+ "sudo",
281
+ ["-n", primaryCmd, ...primaryArgs],
282
+ runner,
283
+ );
284
+ if (sudoResult.ok) result = sudoResult;
285
+ }
286
+
252
287
  if (!result.ok) {
253
288
  console.error(
254
289
  chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`),