@indigoai-us/hq-cli 5.50.1 → 5.50.2

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.
@@ -29,13 +29,17 @@
29
29
  */
30
30
 
31
31
  import { spawnSync } from "node:child_process";
32
+ import { readFileSync } from "node:fs";
33
+ import path from "node:path";
34
+ import { fileURLToPath } from "node:url";
32
35
  import chalk from "chalk";
33
- import { CLI_VERSION } from "../cli-version.js";
36
+ import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
34
37
  import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
35
38
 
36
39
  const CLIENT_ID = "hq-cli";
37
40
  const ENDPOINT_PATH = "/v1/client-version/check";
38
41
  const FETCH_TIMEOUT_MS = 3_000;
42
+ const LATEST_PACKAGE_SPEC = `${CLI_NAME}@latest`;
39
43
 
40
44
  interface VersionCheckResponse {
41
45
  clientId: string;
@@ -53,6 +57,53 @@ function isOptedOut(): boolean {
53
57
  return process.env.HQ_NO_UPDATE_CHECK === "1";
54
58
  }
55
59
 
60
+ function findRunningPackageRoot(): string | null {
61
+ let dir = path.dirname(fileURLToPath(import.meta.url));
62
+ while (true) {
63
+ try {
64
+ const pkg = JSON.parse(
65
+ readFileSync(path.join(dir, "package.json"), "utf-8"),
66
+ ) as { name?: unknown };
67
+ if (pkg.name === CLI_NAME) return dir;
68
+ } catch {
69
+ // Keep walking; compiled installs usually start under dist/.
70
+ }
71
+
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir) return null;
74
+ dir = parent;
75
+ }
76
+ }
77
+
78
+ export function npmPrefixFromPackageDir(pkgDir: string): string | null {
79
+ const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
80
+ const segments = normalized.split("/");
81
+ const nodeModulesIndex = segments.lastIndexOf("node_modules");
82
+ if (nodeModulesIndex === -1) return null;
83
+
84
+ const prefixEnd =
85
+ segments[nodeModulesIndex - 1] === "lib"
86
+ ? nodeModulesIndex - 1
87
+ : nodeModulesIndex;
88
+ const prefix = segments.slice(0, prefixEnd).join("/");
89
+ if (prefix === "" && normalized.startsWith("/")) return "/";
90
+ return prefix || null;
91
+ }
92
+
93
+ export function resolveRunningPrefix(): string | null {
94
+ try {
95
+ const pkgRoot = findRunningPackageRoot();
96
+ if (!pkgRoot) return null;
97
+ return npmPrefixFromPackageDir(pkgRoot);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ export function buildPrefixedInstallArgv(prefix: string): string[] {
104
+ return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
105
+ }
106
+
56
107
  /**
57
108
  * Hit POST /v1/client-version/check. Returns the parsed body on 200, or
58
109
  * `null` on any failure (caller treats as "no gate"). Tight 3s timeout —
@@ -95,13 +146,10 @@ async function fetchVersionDecision(): Promise<VersionCheckResponse | null> {
95
146
  * forcing a re-invocation would run twice on the same process and feel
96
147
  * janky; instead we print a clear "rerun your command" message and exit.
97
148
  */
98
- function performUpdate(
99
- command: string,
100
- ): { ok: boolean; detail?: string } {
101
- const parts = command.split(/\s+/).filter(Boolean);
102
- if (parts.length === 0) return { ok: false, detail: "empty command" };
103
- const cmd = parts[0]!;
104
- const args = parts.slice(1);
149
+ type UpdateResult = { ok: boolean; detail?: string };
150
+ type UpdateRunner = (cmd: string, args: string[]) => UpdateResult;
151
+
152
+ function runUpdateCommand(cmd: string, args: string[]): UpdateResult {
105
153
  try {
106
154
  const result = spawnSync(cmd, args, { stdio: "inherit" });
107
155
  if (result.status !== 0) {
@@ -116,6 +164,25 @@ function performUpdate(
116
164
  }
117
165
  }
118
166
 
167
+ function performUpdateCommand(
168
+ cmd: string,
169
+ args: string[],
170
+ runner: UpdateRunner = runUpdateCommand,
171
+ ): UpdateResult {
172
+ return runner(cmd, args);
173
+ }
174
+
175
+ function performUpdate(
176
+ command: string,
177
+ runner: UpdateRunner = runUpdateCommand,
178
+ ): UpdateResult {
179
+ const parts = command.split(/\s+/).filter(Boolean);
180
+ if (parts.length === 0) return { ok: false, detail: "empty command" };
181
+ const cmd = parts[0]!;
182
+ const args = parts.slice(1);
183
+ return performUpdateCommand(cmd, args, runner);
184
+ }
185
+
119
186
  /**
120
187
  * Soft notify when the server says we're below `latestVersion` but still ≥
121
188
  * `minVersion`. Single chalk-yellow line on stderr; never blocks.
@@ -140,7 +207,14 @@ function nudgeUpdateRecommended(decision: VersionCheckResponse): void {
140
207
  * 0 — update succeeded; user must rerun their command
141
208
  * 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
142
209
  */
143
- function enforceUpdateRequired(decision: VersionCheckResponse): never {
210
+ function enforceUpdateRequired(
211
+ decision: VersionCheckResponse,
212
+ deps: {
213
+ performUpdateString?: (command: string) => UpdateResult;
214
+ resolvePrefix?: () => string | null;
215
+ runner?: UpdateRunner;
216
+ } = {},
217
+ ): never {
144
218
  const banner = chalk.red.bold(
145
219
  `✗ hq-cli ${decision.currentVersion} is below the minimum required version (${decision.minVersion}).`,
146
220
  );
@@ -148,7 +222,8 @@ function enforceUpdateRequired(decision: VersionCheckResponse): never {
148
222
  if (decision.message) console.error(chalk.dim(` ${decision.message}`));
149
223
 
150
224
  const command = decision.updateCommand;
151
- if (!command) {
225
+ const prefix = (deps.resolvePrefix ?? resolveRunningPrefix)();
226
+ if (!command && !prefix) {
152
227
  console.error(
153
228
  chalk.red(
154
229
  " No updateCommand provided by hq-pro — see https://hq.indigo.ai/docs/cli-update for manual steps.",
@@ -160,13 +235,28 @@ function enforceUpdateRequired(decision: VersionCheckResponse): never {
160
235
  process.exit(75);
161
236
  }
162
237
 
163
- console.error(chalk.dim(` Running: ${command}`));
164
- const result = performUpdate(command);
238
+ const runner = deps.runner ?? runUpdateCommand;
239
+ const result = prefix
240
+ ? (() => {
241
+ const args = buildPrefixedInstallArgv(prefix);
242
+ console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
243
+ console.error(chalk.dim(` Running: npm ${args.join(" ")}`));
244
+ return performUpdateCommand("npm", args, runner);
245
+ })()
246
+ : (() => {
247
+ console.error(chalk.dim(` Running: ${command}`));
248
+ return deps.performUpdateString
249
+ ? deps.performUpdateString(command!)
250
+ : performUpdate(command!, runner);
251
+ })();
165
252
  if (!result.ok) {
166
253
  console.error(
167
254
  chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`),
168
255
  );
169
- console.error(chalk.dim(` Try manually: ${command}`));
256
+ const manual = prefix
257
+ ? `npm ${buildPrefixedInstallArgv(prefix).join(" ")}`
258
+ : command!;
259
+ console.error(chalk.dim(` Try manually: ${manual}`));
170
260
  process.exit(75);
171
261
  }
172
262
 
@@ -215,5 +305,11 @@ export const __test__ = {
215
305
  CLIENT_ID,
216
306
  ENDPOINT_PATH,
217
307
  FETCH_TIMEOUT_MS,
308
+ buildPrefixedInstallArgv,
309
+ enforceUpdateRequired,
310
+ npmPrefixFromPackageDir,
218
311
  performUpdate,
312
+ performUpdateCommand,
313
+ runUpdateCommand,
314
+ resolveRunningPrefix,
219
315
  };