@indigoai-us/hq-cli 5.12.4 → 5.13.0

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,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [5.13.0] — 2026-05-14
6
+
7
+ ### Added
8
+
9
+ - **`hq sync now` — bidirectional sync command.** Mirrors the AppBar HQ Sync
10
+ "Sync Now" button: pushes local changes to the target vault, then pulls
11
+ remote updates. Push runs first so the subsequent pull doesn't redownload
12
+ files that were about to be broadcast (matches `hq-sync-runner` ordering).
13
+ Supports `--company <slug>`, `--personal`, `--all`, `--message <msg>`,
14
+ `--on-conflict <strategy>`, and `--hq-root <path>`. The `--all` form
15
+ composes `pushAll` then `pullAll`, fanning out across every membership and
16
+ the canonical personal vault in a single invocation.
17
+
18
+ - **`--personal` flag on `hq sync pull`, `hq sync push`, and `hq sync now`.**
19
+ Targets the caller's canonical person-entity vault without needing the
20
+ person UID — resolves it from the cached Cognito session, then routes
21
+ through `personalMode` (no `companies/<slug>/` prefix) with `journalSlug:
22
+ "personal"` for idempotency-state parity with the Rust personal first-push.
23
+ For `push --personal` with no explicit `[paths]`, defaults to the same
24
+ top-level scope as `--all`'s personal slot (every top-level entry under
25
+ `<hq-root>` minus `.git`, `companies`, `repos`, `workspace`). Mutually
26
+ exclusive with `--all` and `--company`.
27
+
28
+ - **`--all` flag on `hq sync push`.** Symmetric with `pull --all`: pushes
29
+ every company you are a member of plus the canonical personal vault in
30
+ a single invocation. Company targets push `<hq-root>/companies/<slug>/`;
31
+ personal pushes the canonical personal-vault scope. Each leg uses the
32
+ menubar runner's bulk defaults (`skipUnchanged: true`,
33
+ `propagateDeletes: true`) so re-runs are cheap and on-disk deletes
34
+ propagate to the vault. Cannot be combined with explicit `[paths]` or
35
+ `--creds-from-stdin` — the fanout needs to vend per-target credentials.
36
+
37
+ ### Changed
38
+
39
+ - **Selector flags on `hq sync {pull,push,now}` are mutually exclusive.**
40
+ `--all`, `--personal`, and `--company` are now validated as a single
41
+ selector: at most one may be set per invocation; zero means "use the
42
+ active company from `.hq/config.json`". Conflicting combinations are
43
+ rejected with a clear error before any network calls.
44
+
45
+ ### Fixed
46
+
47
+ - **Swallow `EPIPE` on stdout/stderr to stop Sentry fatals.** When a
48
+ downstream consumer (`| head`, closed pager, broken shell pipe) shut the
49
+ read end of the CLI's stdout or stderr, Node raised `EPIPE` that Sentry
50
+ then captured as a fatal. Both streams now ignore `EPIPE` quietly so the
51
+ CLI exits cleanly when piped into truncating consumers, and the Sentry
52
+ dashboard stops filling up with non-actionable pipe errors.
53
+
3
54
  ## [5.12.4] — 2026-05-12
4
55
 
5
56
  ### Changed
@@ -58,6 +58,50 @@ export interface PullAllRow {
58
58
  result?: SyncCallResult;
59
59
  error?: string;
60
60
  }
61
+ export interface ShareCallOptions {
62
+ company: string;
63
+ hqRoot: string;
64
+ paths: string[];
65
+ onConflict?: ConflictStrategy;
66
+ personalMode?: boolean;
67
+ journalSlug?: string;
68
+ message?: string;
69
+ skipUnchanged?: boolean;
70
+ propagateDeletes?: boolean;
71
+ }
72
+ export interface ShareCallResult {
73
+ filesUploaded: number;
74
+ bytesUploaded: number;
75
+ filesSkipped: number;
76
+ filesDeleted: number;
77
+ conflictPaths: string[];
78
+ aborted: boolean;
79
+ }
80
+ export interface PushAllDeps {
81
+ vaultClient: PullAllVaultClient;
82
+ share: (options: ShareCallOptions) => Promise<ShareCallResult>;
83
+ }
84
+ export interface PushAllOptions {
85
+ hqRoot: string;
86
+ onConflict?: ConflictStrategy;
87
+ message?: string;
88
+ }
89
+ export interface PushAllRow {
90
+ slug: string;
91
+ result?: ShareCallResult;
92
+ error?: string;
93
+ }
94
+ export interface PushAllResult {
95
+ attempted: number;
96
+ filesUploaded: number;
97
+ bytesUploaded: number;
98
+ filesDeleted: number;
99
+ errors: Array<{
100
+ company: string;
101
+ message: string;
102
+ }>;
103
+ perCompany: PushAllRow[];
104
+ }
61
105
  export interface PullAllResult {
62
106
  attempted: number;
63
107
  filesDownloaded: number;
@@ -70,5 +114,29 @@ export interface PullAllResult {
70
114
  perCompany: PullAllRow[];
71
115
  }
72
116
  export declare function pullAll(options: PullAllOptions, deps: PullAllDeps): Promise<PullAllResult>;
117
+ /**
118
+ * Drives `hq sync push --all`: same membership + canonical-person fanout as
119
+ * pullAll, but each leg calls share() with the runner's bulk defaults
120
+ * (skipUnchanged: true, propagateDeletes: true). Pure function with injected
121
+ * deps so tests can drive it without network or filesystem.
122
+ */
123
+ export declare function pushAll(options: PushAllOptions, deps: PushAllDeps): Promise<PushAllResult>;
124
+ /**
125
+ * Resolve the canonical person entity UID for the logged-in user. Used by
126
+ * `hq sync {push,pull,now} --personal` to target the personal vault without
127
+ * the caller needing to know the UID. Throws a clean error if the user has
128
+ * no person entity (typically means they haven't run `hq onboard`).
129
+ */
130
+ export declare function resolveCanonicalPersonUid(vaultClient: PullAllVaultClient): Promise<string>;
131
+ /**
132
+ * Refuse ambiguous selector combinations. `--all`, `--personal`, and
133
+ * `--company` are mutually exclusive — at most one may be set per
134
+ * invocation; zero means "use the active company from .hq/config.json".
135
+ */
136
+ export declare function assertSingleSelector(opts: {
137
+ all?: boolean;
138
+ personal?: boolean;
139
+ company?: string;
140
+ }, command: string): void;
73
141
  export declare function registerCloudCommands(program: Command): void;
74
142
  //# sourceMappingURL=cloud.d.ts.map
@@ -13,11 +13,11 @@
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]="e7f5f66e-78d5-5787-acb4-fe35424f69c9")}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]="7bc55d66-7b02-5b45-9444-710bb6d8dfd6")}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, VaultClient, } from "@indigoai-us/hq-cloud";
20
+ import { share, sync, readJournal, getJournalPath, loadCachedTokens, VaultClient, computePersonalVaultPaths, } from "@indigoai-us/hq-cloud";
21
21
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
22
22
  // Oldest-first by createdAt, ties broken by uid lexicographic — matches
23
23
  // `pickCanonicalPersonEntity` in @indigoai-us/hq-cloud so the CLI lands on
@@ -93,6 +93,115 @@ export async function pullAll(options, deps) {
93
93
  }
94
94
  return result;
95
95
  }
96
+ /**
97
+ * Drives `hq sync push --all`: same membership + canonical-person fanout as
98
+ * pullAll, but each leg calls share() with the runner's bulk defaults
99
+ * (skipUnchanged: true, propagateDeletes: true). Pure function with injected
100
+ * deps so tests can drive it without network or filesystem.
101
+ */
102
+ export async function pushAll(options, deps) {
103
+ const memberships = await deps.vaultClient.listMyMemberships();
104
+ const persons = await deps.vaultClient.listPersonEntities();
105
+ const plan = [];
106
+ for (const m of memberships) {
107
+ let slug = m.companyUid;
108
+ try {
109
+ const info = await deps.vaultClient.getEntity(m.companyUid);
110
+ if (info?.slug)
111
+ slug = info.slug;
112
+ }
113
+ catch {
114
+ // Best-effort — keep UID as the row label.
115
+ }
116
+ plan.push({
117
+ slug,
118
+ shareOptions: {
119
+ company: m.companyUid,
120
+ hqRoot: options.hqRoot,
121
+ paths: [path.join(options.hqRoot, "companies", slug)],
122
+ skipUnchanged: true,
123
+ propagateDeletes: true,
124
+ ...(options.onConflict ? { onConflict: options.onConflict } : {}),
125
+ ...(options.message ? { message: options.message } : {}),
126
+ },
127
+ });
128
+ }
129
+ const personal = pickCanonicalPerson(persons);
130
+ if (personal) {
131
+ plan.push({
132
+ slug: "personal",
133
+ shareOptions: {
134
+ company: personal.uid,
135
+ hqRoot: options.hqRoot,
136
+ paths: computePersonalVaultPaths(options.hqRoot),
137
+ personalMode: true,
138
+ journalSlug: "personal",
139
+ skipUnchanged: true,
140
+ propagateDeletes: true,
141
+ ...(options.onConflict ? { onConflict: options.onConflict } : {}),
142
+ ...(options.message ? { message: options.message } : {}),
143
+ },
144
+ });
145
+ }
146
+ const result = {
147
+ attempted: 0,
148
+ filesUploaded: 0,
149
+ bytesUploaded: 0,
150
+ filesDeleted: 0,
151
+ errors: [],
152
+ perCompany: [],
153
+ };
154
+ for (const entry of plan) {
155
+ result.attempted += 1;
156
+ try {
157
+ const r = await deps.share(entry.shareOptions);
158
+ result.filesUploaded += r.filesUploaded;
159
+ result.bytesUploaded += r.bytesUploaded;
160
+ result.filesDeleted += r.filesDeleted;
161
+ result.perCompany.push({ slug: entry.slug, result: r });
162
+ }
163
+ catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ result.errors.push({ company: entry.slug, message });
166
+ result.perCompany.push({ slug: entry.slug, error: message });
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+ /**
172
+ * Resolve the canonical person entity UID for the logged-in user. Used by
173
+ * `hq sync {push,pull,now} --personal` to target the personal vault without
174
+ * the caller needing to know the UID. Throws a clean error if the user has
175
+ * no person entity (typically means they haven't run `hq onboard`).
176
+ */
177
+ export async function resolveCanonicalPersonUid(vaultClient) {
178
+ const persons = await vaultClient.listPersonEntities();
179
+ const pick = pickCanonicalPerson(persons);
180
+ if (!pick) {
181
+ throw new Error("No personal vault found for the logged-in user. Run `hq onboard` " +
182
+ "first, or check `hq whoami` to confirm you're signed in to the " +
183
+ "right account.");
184
+ }
185
+ return pick.uid;
186
+ }
187
+ /**
188
+ * Refuse ambiguous selector combinations. `--all`, `--personal`, and
189
+ * `--company` are mutually exclusive — at most one may be set per
190
+ * invocation; zero means "use the active company from .hq/config.json".
191
+ */
192
+ export function assertSingleSelector(opts, command) {
193
+ const selectors = [];
194
+ if (opts.all)
195
+ selectors.push("--all");
196
+ if (opts.personal)
197
+ selectors.push("--personal");
198
+ if (opts.company)
199
+ selectors.push(`--company ${opts.company}`);
200
+ if (selectors.length > 1) {
201
+ throw new Error(`\`hq sync ${command}\` accepts at most one of --all, --personal, ` +
202
+ `--company; got: ${selectors.join(", ")}.`);
203
+ }
204
+ }
96
205
  export function registerCloudCommands(program) {
97
206
  program
98
207
  .command("push")
@@ -112,7 +221,42 @@ export function registerCloudCommands(program) {
112
221
  "synthetic `{type:\"complete\",...}` line is appended at the end with " +
113
222
  "the final ShareResult. Subprocess callers parse these to render their " +
114
223
  "own UI (e.g. AppBar Tauri events).")
224
+ .option("--all", "Push every company you are a member of plus your personal vault. " +
225
+ "Company targets push `<hq-root>/companies/<slug>/`; personal pushes " +
226
+ "every top-level entry under <hq-root> minus the excluded set " +
227
+ "(.git, companies, repos, workspace). Implies --skip-unchanged + " +
228
+ "--propagate-deletes. Mutually exclusive with --company, --personal, " +
229
+ "and any explicit [paths].")
230
+ .option("--personal", "Push to the caller's canonical personal vault (resolved from the " +
231
+ "cached Cognito session). When no [paths] are given, defaults to " +
232
+ "every top-level entry under <hq-root> minus the excluded set " +
233
+ "(.git, companies, repos, workspace) — same scope as `--all`'s " +
234
+ "personal slot. Mutually exclusive with --company and --all.")
115
235
  .action(async (paths, options) => {
236
+ try {
237
+ assertSingleSelector(options, "push");
238
+ }
239
+ catch (err) {
240
+ console.error(chalk.red("\n✗ Push failed:"), err instanceof Error ? err.message : String(err));
241
+ process.exit(1);
242
+ }
243
+ if (options.all) {
244
+ if (paths && paths.length > 0) {
245
+ console.error(chalk.red("\n✗ Push failed:"), "`--all` cannot be combined with explicit [paths]. " +
246
+ "Drop the paths to fan out to every membership + personal, " +
247
+ "or drop --all to push specific paths to a single target.");
248
+ process.exit(1);
249
+ }
250
+ if (options.credsFromStdin) {
251
+ console.error(chalk.red("\n✗ Push failed:"), "`--all` cannot be combined with --creds-from-stdin (fanout " +
252
+ "needs to vend per-target credentials via the cached Cognito " +
253
+ "session). Run separate `--creds-from-stdin` invocations per " +
254
+ "target instead.");
255
+ process.exit(1);
256
+ }
257
+ await runPushAll(options.hqRoot, options.message, options.onConflict);
258
+ return;
259
+ }
116
260
  const jsonMode = options.json === true;
117
261
  // Suppress the human banner/result output in JSON mode — the parent
118
262
  // process renders its own UI from the stderr ndjson stream.
@@ -124,11 +268,14 @@ export function registerCloudCommands(program) {
124
268
  process.stderr.write(JSON.stringify(event) + "\n");
125
269
  };
126
270
  try {
127
- const targetPaths = paths && paths.length > 0 ? paths : [process.cwd()];
271
+ if (options.personal && options.credsFromStdin) {
272
+ throw new Error("`--personal` cannot be combined with --creds-from-stdin: " +
273
+ "--personal resolves the canonical person UID via the cached " +
274
+ "Cognito session, while --creds-from-stdin expects the caller " +
275
+ "to have already resolved entity + credentials. Pick one.");
276
+ }
128
277
  log(chalk.bold("\nHQ Sync — Push"));
129
278
  log(` HQ root: ${options.hqRoot}`);
130
- log(` Company: ${options.company ?? "(from .hq/config.json or stdin)"}`);
131
- log(` Paths: ${targetPaths.join(", ")}\n`);
132
279
  // Resolve credentials. Two paths:
133
280
  // 1. --creds-from-stdin: parse JSON EntityContext from stdin (the
134
281
  // AppBar shell-out contract — vend-child upstream, pipe in here).
@@ -153,6 +300,39 @@ export function registerCloudCommands(program) {
153
300
  const accessToken = await ensureCognitoToken();
154
301
  vaultConfig = buildVaultConfig(accessToken);
155
302
  }
303
+ // Resolve the target. For `--personal`, look up the caller's
304
+ // canonical person entity and force personalMode + journalSlug so
305
+ // share() lands files at hqRoot directly (no companies/<slug>/
306
+ // prefix). For everything else, the company is whatever the user
307
+ // passed or the active company from .hq/config.json.
308
+ let targetCompany = options.company;
309
+ let personalMode = false;
310
+ let journalSlug;
311
+ if (options.personal) {
312
+ const client = new VaultClient(vaultConfig);
313
+ targetCompany = await resolveCanonicalPersonUid({
314
+ listMyMemberships: () => client.listMyMemberships(),
315
+ listPersonEntities: () => client.entity.listByType("person"),
316
+ getEntity: async () => null,
317
+ });
318
+ personalMode = true;
319
+ journalSlug = "personal";
320
+ }
321
+ // Default paths. For `--personal` with no explicit paths, default to
322
+ // the canonical personal-vault top-level scope so re-runs from a
323
+ // user shell push the same surface as `--all`'s personal slot.
324
+ // Otherwise preserve the historical "default to cwd" semantics so
325
+ // `hq sync push <file>` and bare `hq sync push` from inside the
326
+ // company tree both work as before.
327
+ const targetPaths = paths && paths.length > 0
328
+ ? paths
329
+ : options.personal
330
+ ? computePersonalVaultPaths(options.hqRoot)
331
+ : [process.cwd()];
332
+ log(` Company: ${options.personal
333
+ ? `(personal: ${targetCompany})`
334
+ : (options.company ?? "(from .hq/config.json or stdin)")}`);
335
+ log(` Paths: ${targetPaths.join(", ")}\n`);
156
336
  // In JSON mode, forward every share() event verbatim to stderr as
157
337
  // ndjson. In human mode, share()'s defaultConsoleLogger handles the
158
338
  // rendering (no onEvent → falls through to stdout/stderr printing).
@@ -168,13 +348,15 @@ export function registerCloudCommands(program) {
168
348
  const author = resolveUploadAuthorFromCache();
169
349
  const result = await share({
170
350
  paths: targetPaths,
171
- company: options.company,
351
+ company: targetCompany,
172
352
  message: options.message,
173
353
  onConflict: options.onConflict,
174
354
  vaultConfig,
175
355
  entityContext,
176
356
  hqRoot: options.hqRoot,
177
357
  onEvent,
358
+ ...(personalMode ? { personalMode: true } : {}),
359
+ ...(journalSlug !== undefined ? { journalSlug } : {}),
178
360
  ...(author ? { author } : {}),
179
361
  });
180
362
  if (jsonMode) {
@@ -219,12 +401,28 @@ export function registerCloudCommands(program) {
219
401
  .option("--on-conflict <strategy>", "Conflict strategy: overwrite | keep | abort (omit for interactive)")
220
402
  .option("--all", "Pull every company you are a member of plus your personal vault " +
221
403
  "into <hq-root>. Companies land at <hq-root>/companies/<slug>; " +
222
- "the personal vault syncs at <hq-root>. Ignores --company.")
404
+ "the personal vault syncs at <hq-root>. Mutually exclusive with " +
405
+ "--company and --personal.")
406
+ .option("--personal", "Pull the caller's canonical personal vault into <hq-root> directly " +
407
+ "(no companies/<slug>/ prefix). Resolves the person UID automatically " +
408
+ "from the cached Cognito session. Mutually exclusive with --company " +
409
+ "and --all.")
223
410
  .action(async (options) => {
411
+ try {
412
+ assertSingleSelector(options, "pull");
413
+ }
414
+ catch (err) {
415
+ console.error(chalk.red("\n✗ Pull failed:"), err instanceof Error ? err.message : String(err));
416
+ process.exit(1);
417
+ }
224
418
  if (options.all) {
225
419
  await runPullAll(options.hqRoot, options.onConflict);
226
420
  return;
227
421
  }
422
+ if (options.personal) {
423
+ await runPullPersonal(options.hqRoot, options.onConflict);
424
+ return;
425
+ }
228
426
  try {
229
427
  console.log(chalk.bold("\nHQ Sync — Pull"));
230
428
  console.log(` HQ root: ${options.hqRoot}`);
@@ -291,6 +489,33 @@ export function registerCloudCommands(program) {
291
489
  process.exit(1);
292
490
  }
293
491
  });
492
+ program
493
+ .command("now")
494
+ .description("Bidirectional sync: push local changes, then pull remote updates " +
495
+ "(mirrors AppBar HQ Sync's \"Sync Now\" button)")
496
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
497
+ .option("--company <slug>", "Company slug or UID (defaults to active company in .hq/config.json)")
498
+ .option("--message <msg>", "Optional message attached to journal entries for the push leg")
499
+ .option("--on-conflict <strategy>", "Conflict strategy: overwrite | keep | abort (omit for interactive)")
500
+ .option("--all", "Sync every company you are a member of plus your personal vault " +
501
+ "(pushAll then pullAll). Mutually exclusive with --company and " +
502
+ "--personal.")
503
+ .option("--personal", "Sync the caller's canonical personal vault bidirectionally. " +
504
+ "Mutually exclusive with --company and --all.")
505
+ .action(async (options) => {
506
+ try {
507
+ assertSingleSelector(options, "now");
508
+ if (options.all) {
509
+ await runNowAll(options.hqRoot, options.message, options.onConflict);
510
+ return;
511
+ }
512
+ await runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict);
513
+ }
514
+ catch (err) {
515
+ console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
516
+ process.exit(1);
517
+ }
518
+ });
294
519
  }
295
520
  async function runPullAll(hqRoot, onConflict) {
296
521
  console.log(chalk.bold("\nHQ Sync — Pull (all)"));
@@ -354,6 +579,232 @@ async function runPullAll(hqRoot, onConflict) {
354
579
  if (errored > 0)
355
580
  process.exit(1);
356
581
  }
582
+ async function runPullPersonal(hqRoot, onConflict) {
583
+ console.log(chalk.bold("\nHQ Sync — Pull (personal)"));
584
+ console.log(` HQ root: ${hqRoot}`);
585
+ console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
586
+ try {
587
+ const accessToken = await ensureCognitoToken();
588
+ const vaultConfig = buildVaultConfig(accessToken);
589
+ const client = new VaultClient(vaultConfig);
590
+ const personalUid = await resolveCanonicalPersonUid({
591
+ listMyMemberships: () => client.listMyMemberships(),
592
+ listPersonEntities: () => client.entity.listByType("person"),
593
+ getEntity: async () => null,
594
+ });
595
+ const result = await sync({
596
+ company: personalUid,
597
+ ...(onConflict ? { onConflict } : {}),
598
+ vaultConfig,
599
+ hqRoot,
600
+ personalMode: true,
601
+ journalSlug: "personal",
602
+ });
603
+ if (result.aborted) {
604
+ console.log(chalk.yellow(`\n⚠ Pull aborted (${result.filesDownloaded} downloaded, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
605
+ process.exit(1);
606
+ }
607
+ console.log(chalk.green(`\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
608
+ }
609
+ catch (err) {
610
+ console.error(chalk.red("\n✗ Pull (personal) failed:"), err instanceof Error ? err.message : String(err));
611
+ process.exit(1);
612
+ }
613
+ }
614
+ async function runPushAll(hqRoot, message, onConflict) {
615
+ console.log(chalk.bold("\nHQ Sync — Push (all)"));
616
+ console.log(` HQ root: ${hqRoot}`);
617
+ console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
618
+ let result;
619
+ try {
620
+ const accessToken = await ensureCognitoToken();
621
+ const vaultConfig = buildVaultConfig(accessToken);
622
+ const realClient = new VaultClient(vaultConfig);
623
+ const author = resolveUploadAuthorFromCache();
624
+ const adapter = {
625
+ listMyMemberships: () => realClient.listMyMemberships(),
626
+ listPersonEntities: () => realClient.entity.listByType("person"),
627
+ getEntity: async (uid) => {
628
+ try {
629
+ return await realClient.entity.get(uid);
630
+ }
631
+ catch {
632
+ return null;
633
+ }
634
+ },
635
+ };
636
+ result = await pushAll({
637
+ hqRoot,
638
+ ...(onConflict ? { onConflict } : {}),
639
+ ...(message ? { message } : {}),
640
+ }, {
641
+ vaultClient: adapter,
642
+ share: (opts) => share({
643
+ paths: opts.paths,
644
+ company: opts.company,
645
+ vaultConfig,
646
+ hqRoot: opts.hqRoot,
647
+ ...(opts.onConflict ? { onConflict: opts.onConflict } : {}),
648
+ ...(opts.personalMode !== undefined
649
+ ? { personalMode: opts.personalMode }
650
+ : {}),
651
+ ...(opts.journalSlug !== undefined
652
+ ? { journalSlug: opts.journalSlug }
653
+ : {}),
654
+ ...(opts.message !== undefined ? { message: opts.message } : {}),
655
+ ...(opts.skipUnchanged !== undefined
656
+ ? { skipUnchanged: opts.skipUnchanged }
657
+ : {}),
658
+ ...(opts.propagateDeletes !== undefined
659
+ ? { propagateDeletes: opts.propagateDeletes }
660
+ : {}),
661
+ ...(author ? { author } : {}),
662
+ }),
663
+ });
664
+ }
665
+ catch (err) {
666
+ console.error(chalk.red("\n✗ Push-all failed:"), err instanceof Error ? err.message : String(err));
667
+ process.exit(1);
668
+ }
669
+ for (const row of result.perCompany) {
670
+ if (row.error) {
671
+ console.log(chalk.red(` ✗ ${row.slug}: ${row.error}`));
672
+ }
673
+ else if (row.result) {
674
+ const r = row.result;
675
+ const status = r.aborted ? chalk.yellow("⚠") : chalk.green("✓");
676
+ console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
677
+ `${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
678
+ `${r.filesDeleted} deleted, ${r.conflictPaths.length} conflict(s)` +
679
+ (r.aborted ? " — aborted" : ""));
680
+ }
681
+ }
682
+ const errored = result.errors.length;
683
+ const summary = `\nPushed ${result.filesUploaded} file(s) ` +
684
+ `(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
685
+ `target(s); ${result.filesDeleted} deleted; ${errored} error(s)`;
686
+ console.log(errored > 0 ? chalk.yellow(summary) : chalk.green(summary));
687
+ if (errored > 0)
688
+ process.exit(1);
689
+ }
690
+ async function runNowSingle(hqRoot, company, personal, message, onConflict) {
691
+ console.log(chalk.bold("\nHQ Sync — Now"));
692
+ console.log(` HQ root: ${hqRoot}`);
693
+ console.log(` Target: ${personal ? "(personal)" : (company ?? "(active company)")}`);
694
+ console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
695
+ try {
696
+ const accessToken = await ensureCognitoToken();
697
+ const vaultConfig = buildVaultConfig(accessToken);
698
+ const author = resolveUploadAuthorFromCache();
699
+ // Resolve the target. For --personal, look up the canonical person and
700
+ // route paths/journal through personalMode. Otherwise use the company arg
701
+ // (or fall back to the active company inside share()/sync()).
702
+ let targetCompany = company;
703
+ let personalMode = false;
704
+ let journalSlug;
705
+ let pushPaths;
706
+ if (personal) {
707
+ const client = new VaultClient(vaultConfig);
708
+ targetCompany = await resolveCanonicalPersonUid({
709
+ listMyMemberships: () => client.listMyMemberships(),
710
+ listPersonEntities: () => client.entity.listByType("person"),
711
+ getEntity: async () => null,
712
+ });
713
+ personalMode = true;
714
+ journalSlug = "personal";
715
+ pushPaths = computePersonalVaultPaths(hqRoot);
716
+ }
717
+ else {
718
+ // For company targets we need a concrete slug to compute the push path.
719
+ // share() can resolve `company` itself for the upload, but the path
720
+ // computation must happen here. Use the explicit company if given;
721
+ // otherwise fall back to the active-company resolution inside the
722
+ // engine and compute paths from `hqRoot/companies` (share() will refuse
723
+ // anything outside that subtree anyway).
724
+ const slug = company ?? readActiveCompanySlug(hqRoot);
725
+ if (!slug) {
726
+ throw new Error("No company specified and no active company found. " +
727
+ "Use --company <slug>, --personal, or set up .hq/config.json.");
728
+ }
729
+ pushPaths = [path.join(hqRoot, "companies", slug)];
730
+ }
731
+ // Push first so the subsequent pull doesn't redownload files we were
732
+ // about to broadcast (matches hq-sync-runner ordering).
733
+ console.log(chalk.dim(" → push leg"));
734
+ const pushResult = await share({
735
+ paths: pushPaths,
736
+ company: targetCompany,
737
+ vaultConfig,
738
+ hqRoot,
739
+ skipUnchanged: true,
740
+ propagateDeletes: true,
741
+ ...(onConflict ? { onConflict } : {}),
742
+ ...(message ? { message } : {}),
743
+ ...(personalMode ? { personalMode: true } : {}),
744
+ ...(journalSlug !== undefined ? { journalSlug } : {}),
745
+ ...(author ? { author } : {}),
746
+ });
747
+ console.log(` ${pushResult.aborted ? chalk.yellow("⚠") : chalk.green("✓")} ` +
748
+ `${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
749
+ `${pushResult.filesDeleted} deleted` +
750
+ (pushResult.aborted ? " — aborted" : ""));
751
+ if (pushResult.aborted) {
752
+ console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
753
+ process.exit(1);
754
+ }
755
+ console.log(chalk.dim(" → pull leg"));
756
+ const pullResult = await sync({
757
+ company: targetCompany,
758
+ vaultConfig,
759
+ hqRoot,
760
+ ...(onConflict ? { onConflict } : {}),
761
+ ...(personalMode ? { personalMode: true } : {}),
762
+ ...(journalSlug !== undefined ? { journalSlug } : {}),
763
+ });
764
+ console.log(` ${pullResult.aborted ? chalk.yellow("⚠") : chalk.green("✓")} ` +
765
+ `${pullResult.filesDownloaded} downloaded, ${pullResult.filesSkipped} skipped, ` +
766
+ `${pullResult.conflicts} conflict(s)` +
767
+ (pullResult.aborted ? " — aborted" : ""));
768
+ if (pullResult.aborted) {
769
+ console.log(chalk.yellow("\n⚠ Sync now finished with pull leg aborted."));
770
+ process.exit(1);
771
+ }
772
+ console.log(chalk.green("\n✓ Sync now complete"));
773
+ }
774
+ catch (err) {
775
+ console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
776
+ process.exit(1);
777
+ }
778
+ }
779
+ async function runNowAll(hqRoot, message, onConflict) {
780
+ console.log(chalk.bold("\nHQ Sync — Now (all)"));
781
+ console.log(` HQ root: ${hqRoot}`);
782
+ console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
783
+ // Push first (matches runner), then pull. Re-uses the per-leg orchestrators
784
+ // so the per-target rendering, error isolation, and exit codes are
785
+ // identical to running `push --all` then `pull --all` back-to-back.
786
+ console.log(chalk.dim("→ push --all"));
787
+ await runPushAll(hqRoot, message, onConflict);
788
+ console.log(chalk.dim("\n→ pull --all"));
789
+ await runPullAll(hqRoot, onConflict);
790
+ }
791
+ /**
792
+ * Best-effort read of the active company slug from `<hqRoot>/.hq/config.json`.
793
+ * Returns undefined when the file is missing, malformed, or has no
794
+ * `activeCompany` field — `runNowSingle` surfaces a clean error in that case.
795
+ */
796
+ function readActiveCompanySlug(hqRoot) {
797
+ const configPath = path.join(hqRoot, ".hq", "config.json");
798
+ if (!fs.existsSync(configPath))
799
+ return undefined;
800
+ try {
801
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
802
+ return cfg.activeCompany;
803
+ }
804
+ catch {
805
+ return undefined;
806
+ }
807
+ }
357
808
  function formatBytes(bytes) {
358
809
  if (bytes === 0)
359
810
  return "0 B";
@@ -409,4 +860,4 @@ function resolveUploadAuthorFromCache() {
409
860
  }
410
861
  }
411
862
  //# sourceMappingURL=cloud.js.map
412
- //# debugId=e7f5f66e-78d5-5787-acb4-fe35424f69c9
863
+ //# debugId=7bc55d66-7b02-5b45-9444-710bb6d8dfd6