@indigoai-us/hq-cli 5.12.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.12.3] — 2026-05-12
4
+
5
+ ### Added
6
+
7
+ - **`hq files share <prefix>...` (no `--with`) — browser-launched share-session flow.**
8
+ Mints an encrypted single-use token via `POST /files/{companyUid}/share-session`,
9
+ opens the default browser to `https://hq.{co}.com/share-session/<token>`, and lets
10
+ the issuer batch-pick recipients (members, groups, "Share with All") with per-recipient
11
+ read/write before submitting all grants in one round-trip. Variadic — accepts multiple
12
+ paths in a single invocation: `hq files share path/a/ path/b/ path/c/`.
13
+
14
+ - **`--no-open` flag on `hq files share`** — prints the share-session URL without
15
+ launching a browser. Useful for headless contexts (SSH sessions, CI, paste-into-chat
16
+ workflows). Output includes the URL, paths, and `expiresAt` timestamp.
17
+
18
+ - **`--with @all` for company-wide grants.** Writes a single ACL entry with
19
+ `granteeType: 'company-wide'` covering every active member of the company, replacing
20
+ the legacy `open: true` flag pattern with explicit, individually-revocable grants.
21
+ Works on both `hq files share` and `hq files unshare`. Members added after the grant
22
+ resolve through the company-wide entry automatically at vend-time — no ACL re-write
23
+ needed.
24
+
25
+ ### Fixed
26
+
27
+ - **`hq --version` now prints the correct version.** The embedded version
28
+ constant had drifted from `package.json` since 5.12.1 (the 5.12.2 release
29
+ still reported `5.12.1`).
30
+
31
+ ### Backwards Compatibility
32
+
33
+ - The legacy direct-grant form (`hq files share <prefix> --with <principal> --permission <level>`)
34
+ is unchanged. The browser flow only triggers when `--with` is absent. Scripted
35
+ automation calling the direct-grant form requires no changes.
36
+
37
+ ### Security Notes
38
+
39
+ - Share-session tokens are AES-256-GCM encrypted with the master key and pin the issuer's
40
+ identity, the requested paths, and `maxPermissionByPath` at mint time. The web page
41
+ cannot grant beyond what the issuer had at mint, even if mutated client-side.
42
+ - Default TTL: 15 minutes. Bounded `60s..7d` server-side.
43
+ - Single-use: the submit endpoint claims the token's nonce atomically (DynamoDB
44
+ `attribute_not_exists`); a second submit returns 409.
45
+ - **Treat share-session URLs as live capabilities** — do not paste them into commits,
46
+ thread files, journals, or any persistent surface. The 15-minute TTL is defense
47
+ in depth, not a license to log them.
48
+
3
49
  ## [5.12.2] — 2026-05-10
4
50
 
5
51
  ### Added
@@ -1,3 +1,44 @@
1
1
  import { Command } from "commander";
2
+ /**
3
+ * Parse a human-friendly duration string ("15m", "1h", "24h", "2d") into
4
+ * milliseconds. Returns null on parse failure. Mirrors the parser used in
5
+ * `secrets generate-link` but kept local so files.ts can be tested in
6
+ * isolation without importing the much larger secrets command surface.
7
+ */
8
+ export declare function parseDuration(input: string): number | null;
9
+ /** PRD upper bound on browser-launch share-session expiry. */
10
+ export declare const MAX_SHARE_SESSION_EXPIRY_MS: number;
11
+ export interface ShareSessionResponse {
12
+ url: string;
13
+ token?: string;
14
+ expiresAt: string;
15
+ nonce?: string;
16
+ paths?: string[];
17
+ maxPermissionByPath?: Record<string, string>;
18
+ }
19
+ export interface MintShareSessionParams {
20
+ token: string;
21
+ companyUid: string;
22
+ paths: string[];
23
+ expiresInMs?: number;
24
+ }
25
+ /**
26
+ * POST /files/{companyUid}/share-session — mint a browser-launch share
27
+ * session token. Returns the parsed response. Throws ShareSessionHttpError
28
+ * with a status + actionable message for any non-2xx so callers can render
29
+ * a single consistent error path.
30
+ */
31
+ export declare function mintShareSession(params: MintShareSessionParams): Promise<ShareSessionResponse>;
32
+ export declare class ShareSessionHttpError extends Error {
33
+ readonly status: number;
34
+ readonly path?: string | undefined;
35
+ constructor(status: number, message: string, path?: string | undefined);
36
+ }
37
+ /**
38
+ * Map a ShareSessionHttpError to user-facing copy. Centralizes the
39
+ * status → message mapping so the share command and any future caller
40
+ * stay in sync.
41
+ */
42
+ export declare function formatShareSessionError(err: ShareSessionHttpError): string;
2
43
  export declare function registerFilesCommand(program: Command): void;
3
44
  //# sourceMappingURL=files.d.ts.map
@@ -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.2",
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"