@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 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
@@ -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
@@ -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]="213f074e-13c2-5b95-9519-813adf9adfa9")}catch(e){}}();
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=213f074e-13c2-5b95-9519-813adf9adfa9
412
+ //# debugId=e7f5f66e-78d5-5787-acb4-fe35424f69c9
@@ -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