@indigoai-us/hq-cli 5.12.5 → 5.13.1
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 +65 -0
- package/dist/commands/cloud.d.ts +68 -0
- package/dist/commands/cloud.js +459 -8
- package/dist/index.js +11 -2
- package/package.json +1 -1
- package/src/commands/cloud.push-all.test.ts +381 -0
- package/src/commands/cloud.selectors.test.ts +155 -0
- package/src/commands/cloud.ts +674 -8
- package/src/index.ts +10 -0
package/dist/commands/cloud.js
CHANGED
|
@@ -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]="
|
|
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
|
-
|
|
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:
|
|
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>.
|
|
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=
|
|
863
|
+
//# debugId=7bc55d66-7b02-5b45-9444-710bb6d8dfd6
|
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]="56a9367e-c800-55df-9c55-44274965648e")}catch(e){}}();
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { initSentry, Sentry } from "./sentry.js";
|
|
9
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
@@ -31,6 +31,15 @@ import { registerMembersCommand } from "./commands/members.js";
|
|
|
31
31
|
import { registerFeedbackCommand } from "./commands/feedback.js";
|
|
32
32
|
import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
|
|
33
33
|
import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
|
|
34
|
+
// Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
|
|
35
|
+
const onPipeError = (err) => {
|
|
36
|
+
if (err.code === "EPIPE") {
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
};
|
|
41
|
+
process.stdout.on("error", onPipeError);
|
|
42
|
+
process.stderr.on("error", onPipeError);
|
|
34
43
|
initSentry();
|
|
35
44
|
maybeWarnNewVersion();
|
|
36
45
|
const program = new Command();
|
|
@@ -110,4 +119,4 @@ registerFeedbackCommand(program);
|
|
|
110
119
|
}
|
|
111
120
|
})();
|
|
112
121
|
//# sourceMappingURL=index.js.map
|
|
113
|
-
//# debugId=
|
|
122
|
+
//# debugId=56a9367e-c800-55df-9c55-44274965648e
|