@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.
- package/CHANGELOG.md +57 -0
- package/dist/commands/cloud.d.ts +57 -0
- package/dist/commands/cloud.js +146 -3
- package/dist/commands/files.d.ts +41 -0
- package/dist/commands/files.js +283 -79
- package/dist/index.js +6 -4
- package/dist/utils/version-check.d.ts +3 -0
- package/dist/utils/version-check.js +80 -0
- package/package.json +2 -1
- package/src/commands/cloud.pull-all.test.ts +327 -0
- package/src/commands/cloud.ts +240 -0
- package/src/commands/files.test.ts +504 -0
- package/src/commands/files.ts +403 -84
- package/src/index.ts +7 -2
- package/src/utils/version-check.test.ts +146 -0
- package/src/utils/version-check.ts +83 -0
package/dist/commands/files.js
CHANGED
|
@@ -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]="
|
|
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
|
|
14
|
-
.description("Share a
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
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 =
|
|
105
|
-
|
|
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}' / ${
|
|
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 ${
|
|
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.
|
|
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.
|
|
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=
|
|
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]="
|
|
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.
|
|
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=
|
|
113
|
+
//# debugId=efa927a6-3cad-5122-928e-f7b055b533e8
|
|
@@ -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.
|
|
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"
|