@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
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
|
+
|
|
49
|
+
## [5.12.2] — 2026-05-10
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`hq sync pull --all`** — fan out across every company you're a member of
|
|
54
|
+
plus your personal vault into `<hq-root>` in one shot. Companies land at
|
|
55
|
+
`<hq-root>/companies/<slug>`; the personal vault syncs at `<hq-root>` itself.
|
|
56
|
+
Mirrors the orchestration `hq-sync-runner --companies --direction pull` does
|
|
57
|
+
but reachable from the CLI front-end. Used by the Outpost cloud-init to
|
|
58
|
+
prime `/home/ec2-user/hq` on first boot.
|
|
59
|
+
|
|
3
60
|
## [5.12.1] — 2026-05-09
|
|
4
61
|
|
|
5
62
|
### Fixed
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -13,5 +13,62 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
import { Command } from "commander";
|
|
16
|
+
import { type ConflictStrategy } from "@indigoai-us/hq-cloud";
|
|
17
|
+
export interface PullAllVaultClient {
|
|
18
|
+
listMyMemberships(): Promise<Array<{
|
|
19
|
+
companyUid: string;
|
|
20
|
+
}>>;
|
|
21
|
+
listPersonEntities(): Promise<Array<{
|
|
22
|
+
uid: string;
|
|
23
|
+
type: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
bucketName?: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
}>>;
|
|
28
|
+
getEntity(uid: string): Promise<{
|
|
29
|
+
slug?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
} | null>;
|
|
32
|
+
}
|
|
33
|
+
export interface SyncCallOptions {
|
|
34
|
+
company: string;
|
|
35
|
+
hqRoot: string;
|
|
36
|
+
onConflict?: ConflictStrategy;
|
|
37
|
+
personalMode?: boolean;
|
|
38
|
+
journalSlug?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SyncCallResult {
|
|
41
|
+
filesDownloaded: number;
|
|
42
|
+
bytesDownloaded: number;
|
|
43
|
+
filesSkipped: number;
|
|
44
|
+
conflicts: number;
|
|
45
|
+
conflictPaths: string[];
|
|
46
|
+
aborted: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PullAllDeps {
|
|
49
|
+
vaultClient: PullAllVaultClient;
|
|
50
|
+
sync: (options: SyncCallOptions) => Promise<SyncCallResult>;
|
|
51
|
+
}
|
|
52
|
+
export interface PullAllOptions {
|
|
53
|
+
hqRoot: string;
|
|
54
|
+
onConflict?: ConflictStrategy;
|
|
55
|
+
}
|
|
56
|
+
export interface PullAllRow {
|
|
57
|
+
slug: string;
|
|
58
|
+
result?: SyncCallResult;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface PullAllResult {
|
|
62
|
+
attempted: number;
|
|
63
|
+
filesDownloaded: number;
|
|
64
|
+
bytesDownloaded: number;
|
|
65
|
+
conflicts: number;
|
|
66
|
+
errors: Array<{
|
|
67
|
+
company: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}>;
|
|
70
|
+
perCompany: PullAllRow[];
|
|
71
|
+
}
|
|
72
|
+
export declare function pullAll(options: PullAllOptions, deps: PullAllDeps): Promise<PullAllResult>;
|
|
16
73
|
export declare function registerCloudCommands(program: Command): void;
|
|
17
74
|
//# sourceMappingURL=cloud.d.ts.map
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,12 +13,86 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!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]="
|
|
16
|
+
!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]="e7f5f66e-78d5-5787-acb4-fe35424f69c9")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import { share, sync, readJournal, getJournalPath, loadCachedTokens, } from "@indigoai-us/hq-cloud";
|
|
20
|
+
import { share, sync, readJournal, getJournalPath, loadCachedTokens, VaultClient, } from "@indigoai-us/hq-cloud";
|
|
21
21
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
22
|
+
// Oldest-first by createdAt, ties broken by uid lexicographic — matches
|
|
23
|
+
// `pickCanonicalPersonEntity` in @indigoai-us/hq-cloud so the CLI lands on
|
|
24
|
+
// the same person bucket that `hq-sync-runner` picks.
|
|
25
|
+
function pickCanonicalPerson(persons) {
|
|
26
|
+
const onlyPersons = persons.filter((e) => e.type === "person");
|
|
27
|
+
if (onlyPersons.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
return [...onlyPersons].sort((a, b) => {
|
|
30
|
+
if (a.createdAt !== b.createdAt)
|
|
31
|
+
return a.createdAt < b.createdAt ? -1 : 1;
|
|
32
|
+
return a.uid < b.uid ? -1 : 1;
|
|
33
|
+
})[0];
|
|
34
|
+
}
|
|
35
|
+
export async function pullAll(options, deps) {
|
|
36
|
+
const memberships = await deps.vaultClient.listMyMemberships();
|
|
37
|
+
const persons = await deps.vaultClient.listPersonEntities();
|
|
38
|
+
const plan = [];
|
|
39
|
+
for (const m of memberships) {
|
|
40
|
+
let slug = m.companyUid;
|
|
41
|
+
try {
|
|
42
|
+
const info = await deps.vaultClient.getEntity(m.companyUid);
|
|
43
|
+
if (info?.slug)
|
|
44
|
+
slug = info.slug;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Best-effort — keep UID as the row label rather than aborting the run.
|
|
48
|
+
}
|
|
49
|
+
plan.push({
|
|
50
|
+
slug,
|
|
51
|
+
syncOptions: {
|
|
52
|
+
company: m.companyUid,
|
|
53
|
+
hqRoot: options.hqRoot,
|
|
54
|
+
...(options.onConflict ? { onConflict: options.onConflict } : {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const personal = pickCanonicalPerson(persons);
|
|
59
|
+
if (personal) {
|
|
60
|
+
plan.push({
|
|
61
|
+
slug: "personal",
|
|
62
|
+
syncOptions: {
|
|
63
|
+
company: personal.uid,
|
|
64
|
+
hqRoot: options.hqRoot,
|
|
65
|
+
personalMode: true,
|
|
66
|
+
journalSlug: "personal",
|
|
67
|
+
...(options.onConflict ? { onConflict: options.onConflict } : {}),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const result = {
|
|
72
|
+
attempted: 0,
|
|
73
|
+
filesDownloaded: 0,
|
|
74
|
+
bytesDownloaded: 0,
|
|
75
|
+
conflicts: 0,
|
|
76
|
+
errors: [],
|
|
77
|
+
perCompany: [],
|
|
78
|
+
};
|
|
79
|
+
for (const entry of plan) {
|
|
80
|
+
result.attempted += 1;
|
|
81
|
+
try {
|
|
82
|
+
const r = await deps.sync(entry.syncOptions);
|
|
83
|
+
result.filesDownloaded += r.filesDownloaded;
|
|
84
|
+
result.bytesDownloaded += r.bytesDownloaded;
|
|
85
|
+
result.conflicts += r.conflicts;
|
|
86
|
+
result.perCompany.push({ slug: entry.slug, result: r });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
result.errors.push({ company: entry.slug, message });
|
|
91
|
+
result.perCompany.push({ slug: entry.slug, error: message });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
22
96
|
export function registerCloudCommands(program) {
|
|
23
97
|
program
|
|
24
98
|
.command("push")
|
|
@@ -143,7 +217,14 @@ export function registerCloudCommands(program) {
|
|
|
143
217
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
144
218
|
.option("--company <slug>", "Company slug or UID (defaults to active company in .hq/config.json)")
|
|
145
219
|
.option("--on-conflict <strategy>", "Conflict strategy: overwrite | keep | abort (omit for interactive)")
|
|
220
|
+
.option("--all", "Pull every company you are a member of plus your personal vault " +
|
|
221
|
+
"into <hq-root>. Companies land at <hq-root>/companies/<slug>; " +
|
|
222
|
+
"the personal vault syncs at <hq-root>. Ignores --company.")
|
|
146
223
|
.action(async (options) => {
|
|
224
|
+
if (options.all) {
|
|
225
|
+
await runPullAll(options.hqRoot, options.onConflict);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
147
228
|
try {
|
|
148
229
|
console.log(chalk.bold("\nHQ Sync — Pull"));
|
|
149
230
|
console.log(` HQ root: ${options.hqRoot}`);
|
|
@@ -211,6 +292,68 @@ export function registerCloudCommands(program) {
|
|
|
211
292
|
}
|
|
212
293
|
});
|
|
213
294
|
}
|
|
295
|
+
async function runPullAll(hqRoot, onConflict) {
|
|
296
|
+
console.log(chalk.bold("\nHQ Sync — Pull (all)"));
|
|
297
|
+
console.log(` HQ root: ${hqRoot}`);
|
|
298
|
+
console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
|
|
299
|
+
let result;
|
|
300
|
+
try {
|
|
301
|
+
const accessToken = await ensureCognitoToken();
|
|
302
|
+
const vaultConfig = buildVaultConfig(accessToken);
|
|
303
|
+
const realClient = new VaultClient(vaultConfig);
|
|
304
|
+
const adapter = {
|
|
305
|
+
listMyMemberships: () => realClient.listMyMemberships(),
|
|
306
|
+
listPersonEntities: () => realClient.entity.listByType("person"),
|
|
307
|
+
getEntity: async (uid) => {
|
|
308
|
+
try {
|
|
309
|
+
return await realClient.entity.get(uid);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
result = await pullAll({ hqRoot, ...(onConflict ? { onConflict } : {}) }, {
|
|
317
|
+
vaultClient: adapter,
|
|
318
|
+
sync: (opts) => sync({
|
|
319
|
+
company: opts.company,
|
|
320
|
+
hqRoot: opts.hqRoot,
|
|
321
|
+
vaultConfig,
|
|
322
|
+
...(opts.onConflict ? { onConflict: opts.onConflict } : {}),
|
|
323
|
+
...(opts.personalMode !== undefined
|
|
324
|
+
? { personalMode: opts.personalMode }
|
|
325
|
+
: {}),
|
|
326
|
+
...(opts.journalSlug !== undefined
|
|
327
|
+
? { journalSlug: opts.journalSlug }
|
|
328
|
+
: {}),
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
console.error(chalk.red("\n✗ Pull-all failed:"), err instanceof Error ? err.message : String(err));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
for (const row of result.perCompany) {
|
|
337
|
+
if (row.error) {
|
|
338
|
+
console.log(chalk.red(` ✗ ${row.slug}: ${row.error}`));
|
|
339
|
+
}
|
|
340
|
+
else if (row.result) {
|
|
341
|
+
const r = row.result;
|
|
342
|
+
const status = r.aborted ? chalk.yellow("⚠") : chalk.green("✓");
|
|
343
|
+
console.log(` ${status} ${row.slug}: ${r.filesDownloaded} file(s), ` +
|
|
344
|
+
`${formatBytes(r.bytesDownloaded)}, ` +
|
|
345
|
+
`${r.filesSkipped} skipped, ${r.conflicts} conflict(s)` +
|
|
346
|
+
(r.aborted ? " — aborted" : ""));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const errored = result.errors.length;
|
|
350
|
+
const summary = `\nPulled ${result.filesDownloaded} file(s) ` +
|
|
351
|
+
`(${formatBytes(result.bytesDownloaded)}) across ${result.attempted} ` +
|
|
352
|
+
`target(s); ${result.conflicts} conflict(s); ${errored} error(s)`;
|
|
353
|
+
console.log(errored > 0 ? chalk.yellow(summary) : chalk.green(summary));
|
|
354
|
+
if (errored > 0)
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
214
357
|
function formatBytes(bytes) {
|
|
215
358
|
if (bytes === 0)
|
|
216
359
|
return "0 B";
|
|
@@ -266,4 +409,4 @@ function resolveUploadAuthorFromCache() {
|
|
|
266
409
|
}
|
|
267
410
|
}
|
|
268
411
|
//# sourceMappingURL=cloud.js.map
|
|
269
|
-
//# debugId=
|
|
412
|
+
//# debugId=e7f5f66e-78d5-5787-acb4-fe35424f69c9
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -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
|