@indigoai-us/hq-cli 5.12.1 → 5.12.3

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.
@@ -1,86 +1,133 @@
1
1
 
2
- !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]="a754383e-f129-5052-a8d8-59ea590b4f3d")}catch(e){}}();
2
+ !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]="50b160eb-d996-5ac2-8efc-b1791b469f3a")}catch(e){}}();
3
3
  import chalk from "chalk";
4
+ import open from "open";
4
5
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
6
  import { vaultApiFetch, getCompanyUid } from "./secrets.js";
6
7
  import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Pure helpers (exported for unit tests)
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Parse a human-friendly duration string ("15m", "1h", "24h", "2d") into
13
+ * milliseconds. Returns null on parse failure. Mirrors the parser used in
14
+ * `secrets generate-link` but kept local so files.ts can be tested in
15
+ * isolation without importing the much larger secrets command surface.
16
+ */
17
+ export function parseDuration(input) {
18
+ const match = input.match(/^(\d+)(m|h|d)$/);
19
+ if (!match)
20
+ return null;
21
+ const value = parseInt(match[1], 10);
22
+ const unit = match[2];
23
+ if (unit === "m")
24
+ return value * 60 * 1000;
25
+ if (unit === "h")
26
+ return value * 60 * 60 * 1000;
27
+ if (unit === "d")
28
+ return value * 24 * 60 * 60 * 1000;
29
+ return null;
30
+ }
31
+ /** PRD upper bound on browser-launch share-session expiry. */
32
+ export const MAX_SHARE_SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24h
33
+ /**
34
+ * POST /files/{companyUid}/share-session — mint a browser-launch share
35
+ * session token. Returns the parsed response. Throws ShareSessionHttpError
36
+ * with a status + actionable message for any non-2xx so callers can render
37
+ * a single consistent error path.
38
+ */
39
+ export async function mintShareSession(params) {
40
+ const body = { paths: params.paths };
41
+ if (params.expiresInMs != null)
42
+ body.expiresInMs = params.expiresInMs;
43
+ const res = await vaultApiFetch({
44
+ token: params.token,
45
+ path: `/files/${encodeURIComponent(params.companyUid)}/share-session`,
46
+ method: "POST",
47
+ body,
48
+ });
49
+ if (!res.ok) {
50
+ const errBody = (await res.json().catch(() => ({})));
51
+ throw new ShareSessionHttpError(res.status, errBody.message ?? errBody.error ?? res.statusText, errBody.path);
52
+ }
53
+ return (await res.json());
54
+ }
55
+ export class ShareSessionHttpError extends Error {
56
+ status;
57
+ path;
58
+ constructor(status, message, path) {
59
+ super(message);
60
+ this.status = status;
61
+ this.path = path;
62
+ this.name = "ShareSessionHttpError";
63
+ }
64
+ }
65
+ /**
66
+ * Map a ShareSessionHttpError to user-facing copy. Centralizes the
67
+ * status → message mapping so the share command and any future caller
68
+ * stay in sync.
69
+ */
70
+ export function formatShareSessionError(err) {
71
+ if (err.status === 401) {
72
+ return "Not authenticated — please run `hq login`";
73
+ }
74
+ if (err.status === 403) {
75
+ if (err.path) {
76
+ return `Not authorized to share '${err.path}' — you need read access on every path`;
77
+ }
78
+ return "Not authorized — you need to be a company member with read access on every path";
79
+ }
80
+ if (err.status === 400) {
81
+ return `Invalid request: ${err.message}`;
82
+ }
83
+ if (err.status >= 500) {
84
+ return `Server error: ${err.message}`;
85
+ }
86
+ return err.message || `Request failed (${err.status})`;
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Command registration
90
+ // ---------------------------------------------------------------------------
7
91
  export function registerFilesCommand(program) {
8
92
  const files = program
9
93
  .command("files")
10
94
  .description("Manage file access controls in HQ vault")
11
95
  .option("--company <slug>", "Company slug (resolves to companyUid)");
12
96
  files
13
- .command("share <prefix>")
14
- .description("Share a file prefix with a person or group")
15
- .requiredOption("--with <principal>", "Email address or group id to share with")
16
- .requiredOption("--permission <level>", "Permission level: read | write")
17
- .action(async (prefix, opts) => {
97
+ .command("share [paths...]")
98
+ .description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
99
+ .option("--with <principal>", "Email address, group id, or '@all' to share with every active company member")
100
+ .option("--permission <level>", "Permission level (only with --with): read | write")
101
+ .option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
102
+ .option("--no-open", "Print the share-session URL but do not launch the browser")
103
+ .action(async (paths, opts) => {
18
104
  try {
19
- const canonicalPrefix = normalizeFilePrefix(prefix);
20
- if (!["read", "write"].includes(opts.permission)) {
21
- console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write`));
105
+ if (!paths || paths.length === 0) {
106
+ console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
22
107
  process.exit(1);
23
108
  }
24
- const principal = opts.with;
25
- const isEmail = EMAIL_PATTERN.test(principal);
26
- const isGroup = GROUP_ID_PATTERN.test(principal);
27
- if (!isEmail && !isGroup) {
28
- console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
29
- process.exit(1);
30
- }
31
- const granteeType = isEmail ? "email" : "group";
32
- const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
33
- const token = await ensureCognitoToken();
34
- const companySlug = files.opts().company;
35
- const companyUid = await getCompanyUid(token, companySlug);
36
- let res = await vaultApiFetch({
37
- token,
38
- path: `/files/${encodeURIComponent(companyUid)}/acl/grant`,
39
- method: "POST",
40
- body: { prefix: canonicalPrefix, granteeType, granteeId, permission: opts.permission },
41
- });
42
- // No ACL row exists yet for this prefix. Auto-create one with this
43
- // grant as its first entry, then report success — saves the caller
44
- // from needing a separate "create" step.
45
- let autoCreated = false;
46
- if (res.status === 404) {
47
- res = await vaultApiFetch({
48
- token,
49
- path: `/files/${encodeURIComponent(companyUid)}/acl`,
50
- method: "POST",
51
- body: {
52
- prefix: canonicalPrefix,
53
- entries: [{ granteeType, granteeId, permission: opts.permission }],
54
- },
55
- });
56
- autoCreated = res.ok;
57
- }
58
- if (!res.ok) {
59
- const body = await res.json().catch(() => ({}));
60
- if (res.status === 401) {
61
- console.error(chalk.red("Not authenticated — please run `hq login`"));
62
- }
63
- else if (res.status === 403) {
64
- console.error(chalk.red("Not authorized to share this file prefix"));
65
- }
66
- else if (res.status === 404) {
67
- console.error(chalk.red("ACL record not found — the prefix may not have an ACL yet"));
68
- }
69
- else if (res.status === 409) {
70
- console.error(chalk.red("Concurrent modification — please retry"));
109
+ // Fork: --with present → existing direct-grant path. No --with
110
+ // browser-launch share-session path. Per PRD, the direct-grant
111
+ // path is unchanged from US-001 and operates on a single prefix.
112
+ if (opts.with !== undefined) {
113
+ if (paths.length !== 1) {
114
+ console.error(chalk.red("Direct grant (--with) takes exactly one prefix. Pass multiple paths only when minting a share-session URL."));
115
+ process.exit(1);
71
116
  }
72
- else if (res.status >= 500) {
73
- console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
74
- }
75
- else {
76
- console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
77
- }
78
- process.exit(1);
117
+ await runDirectGrant({
118
+ prefix: paths[0],
119
+ principal: opts.with,
120
+ permission: opts.permission,
121
+ companySlug: files.opts().company,
122
+ });
123
+ return;
79
124
  }
80
- const data = await res.json();
81
- const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
82
- const verb = autoCreated ? "Created ACL and granted" : "Granted";
83
- console.log(chalk.green(`${verb} ${opts.permission} on ${printedPrefix} to ${granteeId}`));
125
+ await runShareSession({
126
+ paths,
127
+ expires: opts.expires,
128
+ launchBrowser: opts.open,
129
+ companySlug: files.opts().company,
130
+ });
84
131
  }
85
132
  catch (err) {
86
133
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -90,19 +137,29 @@ export function registerFilesCommand(program) {
90
137
  files
91
138
  .command("unshare <prefix>")
92
139
  .description("Remove a file access grant")
93
- .requiredOption("--with <principal>", "Email address or group id to remove")
140
+ .requiredOption("--with <principal>", "Email address, group id, or '@all' to remove the company-wide grant")
94
141
  .action(async (prefix, opts) => {
95
142
  try {
96
143
  const canonicalPrefix = normalizeFilePrefix(prefix);
97
144
  const principal = opts.with;
98
- const isEmail = EMAIL_PATTERN.test(principal);
99
- const isGroup = GROUP_ID_PATTERN.test(principal);
100
- if (!isEmail && !isGroup) {
101
- console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
145
+ const isAll = principal === "@all";
146
+ const isEmail = !isAll && EMAIL_PATTERN.test(principal);
147
+ const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
148
+ if (!isAll && !isEmail && !isGroup) {
149
+ console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, or a group id matching grp_<alphanumeric>`));
102
150
  process.exit(1);
103
151
  }
104
- const granteeType = isEmail ? "email" : "group";
105
- const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
152
+ const granteeType = isAll
153
+ ? "company-wide"
154
+ : isEmail
155
+ ? "email"
156
+ : "group";
157
+ const granteeId = isAll
158
+ ? ""
159
+ : isEmail
160
+ ? principal.trim().toLowerCase()
161
+ : principal;
162
+ const principalLabel = isAll ? "everyone in the company" : granteeId;
106
163
  const token = await ensureCognitoToken();
107
164
  const companySlug = files.opts().company;
108
165
  const companyUid = await getCompanyUid(token, companySlug);
@@ -121,7 +178,7 @@ export function registerFilesCommand(program) {
121
178
  console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
122
179
  }
123
180
  else if (res.status === 404) {
124
- console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${granteeId}`));
181
+ console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${principalLabel}`));
125
182
  return;
126
183
  }
127
184
  else if (res.status >= 500) {
@@ -132,7 +189,7 @@ export function registerFilesCommand(program) {
132
189
  }
133
190
  process.exit(1);
134
191
  }
135
- console.log(chalk.green(`Removed grant for ${granteeId} on '${canonicalPrefix}'`));
192
+ console.log(chalk.green(`Removed grant for ${principalLabel} on '${canonicalPrefix}'`));
136
193
  }
137
194
  catch (err) {
138
195
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -206,9 +263,17 @@ export function registerFilesCommand(program) {
206
263
  else {
207
264
  console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
208
265
  }
266
+ // Display labels for grantee identifiers — `company-wide` entries
267
+ // store `granteeId === ""` on the wire, but a blank cell is confusing
268
+ // in tabular output, so we render a human-readable phrase instead.
269
+ function displayGrantee(e) {
270
+ if (e.granteeType === "company-wide")
271
+ return "Everyone in company";
272
+ return e.granteeId;
273
+ }
209
274
  function printEntryTable(rows, showSource) {
210
275
  const TYPE_W = Math.max(4, ...rows.map((e) => e.granteeType.length));
211
- const GRANTEE_W = Math.max(7, ...rows.map((e) => e.granteeId.length));
276
+ const GRANTEE_W = Math.max(7, ...rows.map((e) => displayGrantee(e).length));
212
277
  const PERM_W = Math.max(10, ...rows.map((e) => e.permission.length));
213
278
  const BY_W = Math.max(10, ...rows.map((e) => e.grantedBy.length));
214
279
  const SRC_W = showSource
@@ -228,7 +293,7 @@ export function registerFilesCommand(program) {
228
293
  const grantedAt = e.grantedAt.slice(0, 10);
229
294
  const cols = [
230
295
  e.granteeType.padEnd(TYPE_W),
231
- e.granteeId.padEnd(GRANTEE_W),
296
+ displayGrantee(e).padEnd(GRANTEE_W),
232
297
  e.permission.padEnd(PERM_W),
233
298
  e.grantedBy.padEnd(BY_W),
234
299
  grantedAt,
@@ -266,5 +331,144 @@ export function registerFilesCommand(program) {
266
331
  }
267
332
  });
268
333
  }
334
+ async function runDirectGrant(params) {
335
+ const canonicalPrefix = normalizeFilePrefix(params.prefix);
336
+ if (!params.permission) {
337
+ console.error(chalk.red("--permission is required when --with is set (read | write)"));
338
+ process.exit(1);
339
+ }
340
+ if (!["read", "write"].includes(params.permission)) {
341
+ console.error(chalk.red(`Invalid permission '${params.permission}': must be one of read, write`));
342
+ process.exit(1);
343
+ }
344
+ const principal = params.principal;
345
+ // `@all` is the company-wide sentinel — maps to a single typed
346
+ // 'company-wide' ACL entry whose applicability is computed at resolve
347
+ // time from the active-member list. We send granteeId as the empty
348
+ // string to match the canonical storage form on the server.
349
+ const isAll = principal === "@all";
350
+ const isEmail = !isAll && EMAIL_PATTERN.test(principal);
351
+ const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
352
+ if (!isAll && !isEmail && !isGroup) {
353
+ console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, or a group id matching grp_<alphanumeric>`));
354
+ process.exit(1);
355
+ }
356
+ const granteeType = isAll ? "company-wide" : isEmail ? "email" : "group";
357
+ const granteeId = isAll
358
+ ? ""
359
+ : isEmail
360
+ ? principal.trim().toLowerCase()
361
+ : principal;
362
+ // Display label used in success messages — `@all` reads better than `""`.
363
+ const principalLabel = isAll ? "everyone in the company" : granteeId;
364
+ const token = await ensureCognitoToken();
365
+ const companyUid = await getCompanyUid(token, params.companySlug);
366
+ let res = await vaultApiFetch({
367
+ token,
368
+ path: `/files/${encodeURIComponent(companyUid)}/acl/grant`,
369
+ method: "POST",
370
+ body: {
371
+ prefix: canonicalPrefix,
372
+ granteeType,
373
+ granteeId,
374
+ permission: params.permission,
375
+ },
376
+ });
377
+ // No ACL row exists yet for this prefix. Auto-create one with this
378
+ // grant as its first entry, then report success — saves the caller
379
+ // from needing a separate "create" step.
380
+ let autoCreated = false;
381
+ if (res.status === 404) {
382
+ res = await vaultApiFetch({
383
+ token,
384
+ path: `/files/${encodeURIComponent(companyUid)}/acl`,
385
+ method: "POST",
386
+ body: {
387
+ prefix: canonicalPrefix,
388
+ entries: [{ granteeType, granteeId, permission: params.permission }],
389
+ },
390
+ });
391
+ autoCreated = res.ok;
392
+ }
393
+ if (!res.ok) {
394
+ const body = (await res.json().catch(() => ({})));
395
+ if (res.status === 401) {
396
+ console.error(chalk.red("Not authenticated — please run `hq login`"));
397
+ }
398
+ else if (res.status === 403) {
399
+ console.error(chalk.red("Not authorized to share this file prefix"));
400
+ }
401
+ else if (res.status === 404) {
402
+ console.error(chalk.red("ACL record not found — the prefix may not have an ACL yet"));
403
+ }
404
+ else if (res.status === 409) {
405
+ console.error(chalk.red("Concurrent modification — please retry"));
406
+ }
407
+ else if (res.status >= 500) {
408
+ console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
409
+ }
410
+ else {
411
+ console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
412
+ }
413
+ process.exit(1);
414
+ }
415
+ const data = (await res.json());
416
+ const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
417
+ const verb = autoCreated ? "Created ACL and granted" : "Granted";
418
+ console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
419
+ }
420
+ async function runShareSession(params) {
421
+ // Normalize every path through the shared prefix helper so a trailing
422
+ // `/` becomes `/*` consistently with the direct-grant path.
423
+ const normalizedPaths = params.paths.map(normalizeFilePrefix);
424
+ let expiresInMs;
425
+ if (params.expires !== undefined) {
426
+ const parsed = parseDuration(params.expires);
427
+ if (parsed === null) {
428
+ console.error(chalk.red(`Invalid duration '${params.expires}'. Use formats like 15m, 1h, 24h.`));
429
+ process.exit(1);
430
+ }
431
+ if (parsed > MAX_SHARE_SESSION_EXPIRY_MS) {
432
+ console.error(chalk.red("Maximum share-session expiry is 24h. For longer sharing, use `hq files share <prefix> --with <principal>`."));
433
+ process.exit(1);
434
+ }
435
+ expiresInMs = parsed;
436
+ }
437
+ const token = await ensureCognitoToken();
438
+ const companyUid = await getCompanyUid(token, params.companySlug);
439
+ let session;
440
+ try {
441
+ session = await mintShareSession({
442
+ token,
443
+ companyUid,
444
+ paths: normalizedPaths,
445
+ expiresInMs,
446
+ });
447
+ }
448
+ catch (err) {
449
+ if (err instanceof ShareSessionHttpError) {
450
+ console.error(chalk.red(formatShareSessionError(err)));
451
+ process.exit(1);
452
+ }
453
+ throw err;
454
+ }
455
+ console.log(chalk.green("Share-session URL generated:"));
456
+ console.log(`\n ${session.url}\n`);
457
+ console.log(chalk.dim(` Paths: ${normalizedPaths.join(", ")}`));
458
+ console.log(chalk.dim(` Expires: ${session.expiresAt}`));
459
+ if (params.launchBrowser) {
460
+ // Best-effort browser launch — failures (no display, missing handler)
461
+ // shouldn't fail the command since the URL is already printed.
462
+ try {
463
+ await open(session.url);
464
+ }
465
+ catch (err) {
466
+ console.error(chalk.yellow(`Couldn't launch browser automatically (${err instanceof Error ? err.message : String(err)}). Copy the URL above.`));
467
+ }
468
+ }
469
+ else {
470
+ console.log(chalk.dim(" --no-open: copy the URL above to share manually."));
471
+ }
472
+ }
269
473
  //# sourceMappingURL=files.js.map
270
- //# debugId=a754383e-f129-5052-a8d8-59ea590b4f3d
474
+ //# debugId=50b160eb-d996-5ac2-8efc-b1791b469f3a
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !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]="65033669-3ba8-5aac-8a98-49827f627ff9")}catch(e){}}();
6
+ !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]="efa927a6-3cad-5122-928e-f7b055b533e8")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -30,12 +30,14 @@ import { registerFilesCommand } from "./commands/files.js";
30
30
  import { registerMembersCommand } from "./commands/members.js";
31
31
  import { registerFeedbackCommand } from "./commands/feedback.js";
32
32
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
33
+ import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
33
34
  initSentry();
35
+ maybeWarnNewVersion();
34
36
  const program = new Command();
35
37
  program
36
38
  .name("hq")
37
39
  .description("HQ management CLI — modules, packages, and cloud sync")
38
- .version("5.12.1");
40
+ .version("5.12.3");
39
41
  // Module management subcommand group
40
42
  const modulesCmd = program
41
43
  .command("modules")
@@ -104,8 +106,8 @@ registerFeedbackCommand(program);
104
106
  process.exitCode = 1;
105
107
  }
106
108
  finally {
107
- await Sentry.flush(2000);
109
+ await Promise.allSettled([refreshVersionCache(), Sentry.flush(2000)]);
108
110
  }
109
111
  })();
110
112
  //# sourceMappingURL=index.js.map
111
- //# debugId=65033669-3ba8-5aac-8a98-49827f627ff9
113
+ //# debugId=efa927a6-3cad-5122-928e-f7b055b533e8
@@ -0,0 +1,3 @@
1
+ export declare function maybeWarnNewVersion(): void;
2
+ export declare function refreshVersionCache(): Promise<void>;
3
+ //# sourceMappingURL=version-check.d.ts.map
@@ -0,0 +1,80 @@
1
+
2
+ !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]="99545a82-414c-5fed-a478-ea8e63009643")}catch(e){}}();
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import semver from "semver";
7
+ import chalk from "chalk";
8
+ import { CLI_VERSION } from "../cli-version.js";
9
+ const PACKAGE_NAME = "@indigoai-us/hq-cli";
10
+ const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
11
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12
+ const FETCH_TIMEOUT_MS = 3_000;
13
+ function cachePath() {
14
+ return path.join(os.homedir(), ".hq", "version-check.json");
15
+ }
16
+ function isOptedOut() {
17
+ return process.env.HQ_NO_UPDATE_CHECK === "1";
18
+ }
19
+ function readCache() {
20
+ try {
21
+ const raw = fs.readFileSync(cachePath(), "utf-8");
22
+ const parsed = JSON.parse(raw);
23
+ if (typeof parsed.latest !== "string" ||
24
+ typeof parsed.fetchedAt !== "number") {
25
+ return null;
26
+ }
27
+ return { latest: parsed.latest, fetchedAt: parsed.fetchedAt };
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ function writeCache(entry) {
34
+ try {
35
+ const file = cachePath();
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+ fs.writeFileSync(file, JSON.stringify(entry));
38
+ }
39
+ catch {
40
+ // best-effort; never break the CLI on cache write failure
41
+ }
42
+ }
43
+ export function maybeWarnNewVersion() {
44
+ if (isOptedOut())
45
+ return;
46
+ const entry = readCache();
47
+ if (!entry)
48
+ return;
49
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS)
50
+ return;
51
+ const current = semver.valid(CLI_VERSION);
52
+ const latest = semver.valid(entry.latest);
53
+ if (!current || !latest)
54
+ return;
55
+ if (!semver.gt(latest, current))
56
+ return;
57
+ const msg = chalk.yellow(`⚠ A new version of hq is available: ${entry.latest} (current: ${CLI_VERSION}). It's recommended to update.`);
58
+ console.error(msg);
59
+ }
60
+ export async function refreshVersionCache() {
61
+ if (isOptedOut())
62
+ return;
63
+ try {
64
+ const res = await fetch(REGISTRY_URL, {
65
+ headers: { Accept: "application/json" },
66
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
67
+ });
68
+ if (!res.ok)
69
+ return;
70
+ const body = (await res.json());
71
+ if (typeof body.version !== "string")
72
+ return;
73
+ writeCache({ latest: body.version, fetchedAt: Date.now() });
74
+ }
75
+ catch {
76
+ // best-effort; offline / registry down / timeout — silent
77
+ }
78
+ }
79
+ //# sourceMappingURL=version-check.js.map
80
+ //# debugId=99545a82-414c-5fed-a478-ea8e63009643
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.12.1",
3
+ "version": "5.12.3",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "chalk": "^5.3.0",
22
22
  "commander": "^12.1.0",
23
23
  "js-yaml": "^4.1.0",
24
+ "open": "^10.1.0",
24
25
  "simple-git": "^3.27.0",
25
26
  "semver": "^7.6.3",
26
27
  "varlock": "1.0.0"