@indigoai-us/hq-cli 5.18.3 → 5.20.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 +14 -0
- package/dist/commands/cloud.d.ts +35 -0
- package/dist/commands/cloud.js +82 -15
- package/dist/commands/files-browse.d.ts +42 -5
- package/dist/commands/files-browse.js +130 -26
- package/package.json +2 -2
- package/src/commands/cloud.pull-all.test.ts +98 -0
- package/src/commands/cloud.push-all.test.ts +75 -0
- package/src/commands/cloud.selectors.test.ts +53 -0
- package/src/commands/cloud.ts +112 -7
- package/src/commands/files-browse.test.ts +212 -0
- package/src/commands/files-browse.ts +219 -41
|
@@ -493,4 +493,102 @@ describe("pullAll", () => {
|
|
|
493
493
|
expect(call.hqRoot).toBe("/Users/me/scratch/hq");
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
|
+
|
|
497
|
+
// ── 11. skipPersonal — fanout omits the canonical-person leg ──────────────
|
|
498
|
+
//
|
|
499
|
+
// Mirrors the upstream `hq-sync-runner --skip-personal` flag (hq-cloud
|
|
500
|
+
// 5.25.0) plumbed through to the CLI as `hq sync pull --all --no-personal`.
|
|
501
|
+
// When the option is true, `pullAll` must (a) skip the canonical-person
|
|
502
|
+
// bucket even when one exists, and (b) leave companies untouched.
|
|
503
|
+
|
|
504
|
+
it("skipPersonal=true omits the personal leg even when a person entity exists", async () => {
|
|
505
|
+
const vaultClient = makeVaultClient({
|
|
506
|
+
memberships: [
|
|
507
|
+
{ companyUid: "cmp_acme" },
|
|
508
|
+
{ companyUid: "cmp_globex" },
|
|
509
|
+
],
|
|
510
|
+
persons: [
|
|
511
|
+
{
|
|
512
|
+
uid: "psn_alice",
|
|
513
|
+
type: "person",
|
|
514
|
+
slug: "alice",
|
|
515
|
+
bucketName: "hq-vault-psn-alice",
|
|
516
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
entitiesBySlug: {
|
|
520
|
+
cmp_acme: { slug: "acme" },
|
|
521
|
+
cmp_globex: { slug: "globex" },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
const sync = makeSyncSpy();
|
|
525
|
+
|
|
526
|
+
const result = await pullAll(
|
|
527
|
+
{ hqRoot: "/tmp/hq", skipPersonal: true },
|
|
528
|
+
{ vaultClient, sync: sync.fn },
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(sync.calls.map((c) => c.company)).toEqual([
|
|
532
|
+
"cmp_acme",
|
|
533
|
+
"cmp_globex",
|
|
534
|
+
]);
|
|
535
|
+
for (const call of sync.calls) {
|
|
536
|
+
expect(call.personalMode).toBeUndefined();
|
|
537
|
+
expect(call.journalSlug).toBeUndefined();
|
|
538
|
+
}
|
|
539
|
+
expect(result.attempted).toBe(2);
|
|
540
|
+
expect(result.perCompany.map((r) => r.slug)).toEqual(["acme", "globex"]);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("skipPersonal omitted/false keeps the personal leg in the fanout (baseline)", async () => {
|
|
544
|
+
const vaultClient = makeVaultClient({
|
|
545
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
546
|
+
persons: [
|
|
547
|
+
{
|
|
548
|
+
uid: "psn_alice",
|
|
549
|
+
type: "person",
|
|
550
|
+
slug: "alice",
|
|
551
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
entitiesBySlug: { cmp_acme: { slug: "acme" } },
|
|
555
|
+
});
|
|
556
|
+
const sync = makeSyncSpy();
|
|
557
|
+
|
|
558
|
+
await pullAll(
|
|
559
|
+
{ hqRoot: "/tmp/hq" },
|
|
560
|
+
{ vaultClient, sync: sync.fn },
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
expect(sync.calls.map((c) => c.company)).toEqual([
|
|
564
|
+
"cmp_acme",
|
|
565
|
+
"psn_alice",
|
|
566
|
+
]);
|
|
567
|
+
expect(sync.calls[1].personalMode).toBe(true);
|
|
568
|
+
expect(sync.calls[1].journalSlug).toBe("personal");
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("skipPersonal=true with empty memberships produces zero sync calls", async () => {
|
|
572
|
+
const vaultClient = makeVaultClient({
|
|
573
|
+
memberships: [],
|
|
574
|
+
persons: [
|
|
575
|
+
{
|
|
576
|
+
uid: "psn_alice",
|
|
577
|
+
type: "person",
|
|
578
|
+
slug: "alice",
|
|
579
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
});
|
|
583
|
+
const sync = makeSyncSpy();
|
|
584
|
+
|
|
585
|
+
const result = await pullAll(
|
|
586
|
+
{ hqRoot: "/tmp/hq", skipPersonal: true },
|
|
587
|
+
{ vaultClient, sync: sync.fn },
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(sync.calls).toEqual([]);
|
|
591
|
+
expect(result.attempted).toBe(0);
|
|
592
|
+
expect(result.perCompany).toEqual([]);
|
|
593
|
+
});
|
|
496
594
|
});
|
|
@@ -378,4 +378,79 @@ describe("pushAll", () => {
|
|
|
378
378
|
expect(result.filesUploaded).toBe(3);
|
|
379
379
|
expect(result.filesDeleted).toBe(4);
|
|
380
380
|
});
|
|
381
|
+
|
|
382
|
+
// ── skipPersonal — fanout omits the canonical-person leg ──────────────────
|
|
383
|
+
//
|
|
384
|
+
// Symmetric with the `pullAll` skipPersonal coverage in
|
|
385
|
+
// `cloud.pull-all.test.ts`. Wired through from `hq sync push --all
|
|
386
|
+
// --no-personal` (Commander's `--no-personal` is the auto-negation of the
|
|
387
|
+
// `--personal` single-target selector). Verifies (a) the personal leg is
|
|
388
|
+
// dropped even when a canonical person exists, and (b) the baseline
|
|
389
|
+
// behavior — no flag → personal stays in the plan.
|
|
390
|
+
|
|
391
|
+
it("skipPersonal=true omits the personal leg even when a person entity exists", async () => {
|
|
392
|
+
const hqRoot = makeHqRoot([".git", "companies", "repos", "workspace"]);
|
|
393
|
+
const vaultClient = makeVaultClient({
|
|
394
|
+
memberships: [
|
|
395
|
+
{ companyUid: "cmp_acme" },
|
|
396
|
+
{ companyUid: "cmp_globex" },
|
|
397
|
+
],
|
|
398
|
+
persons: [
|
|
399
|
+
{
|
|
400
|
+
uid: "psn_alice",
|
|
401
|
+
type: "person",
|
|
402
|
+
slug: "alice",
|
|
403
|
+
bucketName: "hq-vault-psn-alice",
|
|
404
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
entitiesBySlug: {
|
|
408
|
+
cmp_acme: { slug: "acme" },
|
|
409
|
+
cmp_globex: { slug: "globex" },
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
const share = makeShareSpy();
|
|
413
|
+
|
|
414
|
+
const result = await pushAll(
|
|
415
|
+
{ hqRoot, skipPersonal: true },
|
|
416
|
+
{ vaultClient, share: share.fn },
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
expect(share.calls.map((c) => c.company)).toEqual([
|
|
420
|
+
"cmp_acme",
|
|
421
|
+
"cmp_globex",
|
|
422
|
+
]);
|
|
423
|
+
for (const call of share.calls) {
|
|
424
|
+
expect(call.personalMode).toBeUndefined();
|
|
425
|
+
expect(call.journalSlug).toBeUndefined();
|
|
426
|
+
}
|
|
427
|
+
expect(result.attempted).toBe(2);
|
|
428
|
+
expect(result.perCompany.map((r) => r.slug)).toEqual(["acme", "globex"]);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("skipPersonal omitted keeps the personal leg in the plan (baseline)", async () => {
|
|
432
|
+
const hqRoot = makeHqRoot([".git", "companies", "repos", "workspace"]);
|
|
433
|
+
const vaultClient = makeVaultClient({
|
|
434
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
435
|
+
persons: [
|
|
436
|
+
{
|
|
437
|
+
uid: "psn_alice",
|
|
438
|
+
type: "person",
|
|
439
|
+
slug: "alice",
|
|
440
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
entitiesBySlug: { cmp_acme: { slug: "acme" } },
|
|
444
|
+
});
|
|
445
|
+
const share = makeShareSpy();
|
|
446
|
+
|
|
447
|
+
await pushAll({ hqRoot }, { vaultClient, share: share.fn });
|
|
448
|
+
|
|
449
|
+
expect(share.calls.map((c) => c.company)).toEqual([
|
|
450
|
+
"cmp_acme",
|
|
451
|
+
"psn_alice",
|
|
452
|
+
]);
|
|
453
|
+
expect(share.calls[1].personalMode).toBe(true);
|
|
454
|
+
expect(share.calls[1].journalSlug).toBe("personal");
|
|
455
|
+
});
|
|
381
456
|
});
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { describe, expect, it, vi } from "vitest";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
+
assertNoPersonalPositionalPaths,
|
|
14
15
|
assertSingleSelector,
|
|
15
16
|
resolveCanonicalPersonUid,
|
|
16
17
|
type PullAllVaultClient,
|
|
@@ -76,6 +77,58 @@ describe("assertSingleSelector", () => {
|
|
|
76
77
|
});
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
// ── assertNoPersonalPositionalPaths (hq-cli#25) ─────────────────────────────
|
|
81
|
+
//
|
|
82
|
+
// The combination `--personal <path>` silently bypasses
|
|
83
|
+
// PERSONAL_VAULT_EXCLUDED_TOP_LEVEL (which is only applied by
|
|
84
|
+
// computePersonalVaultPaths). Real incident: 196 companies/{slug}/** objects
|
|
85
|
+
// uploaded to a personal vault. Refusal forces the operator to drop one of
|
|
86
|
+
// the two — either `--personal` (full vault scope) or the positional paths
|
|
87
|
+
// (specific subset against the active company).
|
|
88
|
+
|
|
89
|
+
describe("assertNoPersonalPositionalPaths", () => {
|
|
90
|
+
it("accepts --personal alone (no positional paths)", () => {
|
|
91
|
+
expect(() => assertNoPersonalPositionalPaths({ personal: true }, undefined)).not.toThrow();
|
|
92
|
+
expect(() => assertNoPersonalPositionalPaths({ personal: true }, [])).not.toThrow();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("accepts positional paths without --personal", () => {
|
|
96
|
+
expect(() => assertNoPersonalPositionalPaths({ personal: false }, ["./scratch"])).not.toThrow();
|
|
97
|
+
expect(() => assertNoPersonalPositionalPaths({}, ["./scratch"])).not.toThrow();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("accepts neither --personal nor positional paths", () => {
|
|
101
|
+
expect(() => assertNoPersonalPositionalPaths({}, undefined)).not.toThrow();
|
|
102
|
+
expect(() => assertNoPersonalPositionalPaths({}, [])).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws when --personal combines with one positional path", () => {
|
|
106
|
+
expect(() => assertNoPersonalPositionalPaths({ personal: true }, ["./scratch"])).toThrow(
|
|
107
|
+
/--personal.*cannot be combined with explicit \[paths\]/,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("throws when --personal combines with multiple positional paths", () => {
|
|
112
|
+
expect(() =>
|
|
113
|
+
assertNoPersonalPositionalPaths({ personal: true }, ["./a", "./b", "./c"]),
|
|
114
|
+
).toThrow(/PERSONAL_VAULT_EXCLUDED_TOP_LEVEL/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("error message names the dangerous prefixes so the operator understands the risk", () => {
|
|
118
|
+
let caught: Error | null = null;
|
|
119
|
+
try {
|
|
120
|
+
assertNoPersonalPositionalPaths({ personal: true }, ["/Users/corey/Documents/HQ"]);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
caught = e as Error;
|
|
123
|
+
}
|
|
124
|
+
expect(caught).not.toBeNull();
|
|
125
|
+
expect(caught!.message).toContain(".git/");
|
|
126
|
+
expect(caught!.message).toContain("companies/");
|
|
127
|
+
expect(caught!.message).toContain("repos/");
|
|
128
|
+
expect(caught!.message).toContain("workspace/");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
79
132
|
// ── resolveCanonicalPersonUid ──────────────────────────────────────────────
|
|
80
133
|
|
|
81
134
|
function makeClient(persons: Array<{
|
package/src/commands/cloud.ts
CHANGED
|
@@ -125,6 +125,17 @@ export interface PullAllOptions {
|
|
|
125
125
|
* legacy behavior for this run via `--mode-all`.
|
|
126
126
|
*/
|
|
127
127
|
modeAllOverride?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* When `true`, skip the canonical-person-entity leg entirely — the
|
|
130
|
+
* fanout only visits the caller's company memberships. Mirrors the
|
|
131
|
+
* `--skip-personal` flag and `HQ_SYNC_SKIP_PERSONAL` env var that
|
|
132
|
+
* `@indigoai-us/hq-cloud`'s `sync-runner` exposes in `--companies`
|
|
133
|
+
* mode (see hq-cloud 5.25.0 `resolveSkipPersonal`). Surfaced on the
|
|
134
|
+
* CLI as `hq sync pull --all --no-personal` so the AppBar HQ Sync
|
|
135
|
+
* menubar toggle (and CLI users opting out one-off) can drop the
|
|
136
|
+
* personal vault from the run without touching the rest of the plan.
|
|
137
|
+
*/
|
|
138
|
+
skipPersonal?: boolean;
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
export interface PullAllRow {
|
|
@@ -173,6 +184,13 @@ export interface PushAllOptions {
|
|
|
173
184
|
hqRoot: string;
|
|
174
185
|
onConflict?: ConflictStrategy;
|
|
175
186
|
message?: string;
|
|
187
|
+
/**
|
|
188
|
+
* When `true`, skip the canonical-person-entity leg entirely — the
|
|
189
|
+
* fanout only visits the caller's company memberships. Symmetric with
|
|
190
|
+
* `PullAllOptions.skipPersonal`; surfaced on the CLI as
|
|
191
|
+
* `hq sync push --all --no-personal`.
|
|
192
|
+
*/
|
|
193
|
+
skipPersonal?: boolean;
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
export interface PushAllRow {
|
|
@@ -252,7 +270,7 @@ export async function pullAll(
|
|
|
252
270
|
});
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
const personal = pickCanonicalPerson(persons);
|
|
273
|
+
const personal = options.skipPersonal ? null : pickCanonicalPerson(persons);
|
|
256
274
|
if (personal) {
|
|
257
275
|
plan.push({
|
|
258
276
|
slug: "personal",
|
|
@@ -385,7 +403,7 @@ export async function pushAll(
|
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
const personal = pickCanonicalPerson(persons);
|
|
406
|
+
const personal = options.skipPersonal ? null : pickCanonicalPerson(persons);
|
|
389
407
|
if (personal) {
|
|
390
408
|
plan.push({
|
|
391
409
|
slug: "personal",
|
|
@@ -451,6 +469,35 @@ export async function resolveCanonicalPersonUid(
|
|
|
451
469
|
return pick.uid;
|
|
452
470
|
}
|
|
453
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Refuse `hq sync push --personal <path>` — the combination silently
|
|
474
|
+
* bypasses `PERSONAL_VAULT_EXCLUDED_TOP_LEVEL` (which is only applied by
|
|
475
|
+
* `computePersonalVaultPaths`), risking cross-scope upload of `companies/`,
|
|
476
|
+
* `repos/`, `workspace/`, or `.git/` content to the personal vault. Real
|
|
477
|
+
* incident (2026-05-21): a single command uploaded 196 `companies/{slug}/**`
|
|
478
|
+
* objects to a personal vault before being killed. Cleanup required a
|
|
479
|
+
* hand-rolled S3 sweep. Closes hq-cli#25.
|
|
480
|
+
*
|
|
481
|
+
* Refusal — not silent filtering — is intentional: explicit is better than
|
|
482
|
+
* implicit guesswork, and the legitimate "I want to push a subset of my
|
|
483
|
+
* personal vault" use case has a clean workaround (drop `--personal`, the
|
|
484
|
+
* subset upload targets the active company via standard semantics).
|
|
485
|
+
*/
|
|
486
|
+
export function assertNoPersonalPositionalPaths(opts: {
|
|
487
|
+
personal?: boolean;
|
|
488
|
+
}, paths: string[] | undefined): void {
|
|
489
|
+
if (opts.personal && paths && paths.length > 0) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
"`--personal` cannot be combined with explicit [paths]: " +
|
|
492
|
+
"positional paths bypass the PERSONAL_VAULT_EXCLUDED_TOP_LEVEL " +
|
|
493
|
+
"guard (skips .git/, companies/, repos/, workspace/), risking " +
|
|
494
|
+
"cross-scope upload of company data to the personal vault. " +
|
|
495
|
+
"Use bare `--personal` to push the whole personal scope, OR " +
|
|
496
|
+
"drop `--personal` to push specific paths to the active company.",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
454
501
|
/**
|
|
455
502
|
* Refuse ambiguous selector combinations. `--all`, `--personal`, and
|
|
456
503
|
* `--company` are mutually exclusive — at most one may be set per
|
|
@@ -608,6 +655,15 @@ export function registerCloudCommands(program: Command): void {
|
|
|
608
655
|
"(.git, companies, repos, workspace) — same scope as `--all`'s " +
|
|
609
656
|
"personal slot. Mutually exclusive with --company and --all.",
|
|
610
657
|
)
|
|
658
|
+
.option(
|
|
659
|
+
"--no-personal",
|
|
660
|
+
"In `--all` mode, skip the canonical-person leg of the fanout — " +
|
|
661
|
+
"only push to the caller's company memberships. Mirrors the " +
|
|
662
|
+
"upstream `hq-sync-runner --skip-personal` flag (hq-cloud 5.25.0); " +
|
|
663
|
+
"wired so the AppBar HQ Sync menubar can drop personal sync via " +
|
|
664
|
+
"a toggle, and so CLI users can opt out one-off. Ignored outside " +
|
|
665
|
+
"`--all`.",
|
|
666
|
+
)
|
|
611
667
|
.action(
|
|
612
668
|
async (
|
|
613
669
|
paths: string[],
|
|
@@ -649,10 +705,17 @@ export function registerCloudCommands(program: Command): void {
|
|
|
649
705
|
);
|
|
650
706
|
process.exit(1);
|
|
651
707
|
}
|
|
708
|
+
// `options.personal === false` happens when the user passed
|
|
709
|
+
// `--no-personal` (Commander's auto-negation of the `--personal`
|
|
710
|
+
// selector). In `--all` mode that means "skip the personal leg of
|
|
711
|
+
// the fanout" — wired through to `pushAll.skipPersonal`. Outside
|
|
712
|
+
// `--all` the flag has no effect (logged above as part of the
|
|
713
|
+
// option's help text).
|
|
652
714
|
await runPushAll(
|
|
653
715
|
options.hqRoot,
|
|
654
716
|
options.message,
|
|
655
717
|
options.onConflict,
|
|
718
|
+
options.personal === false,
|
|
656
719
|
);
|
|
657
720
|
return;
|
|
658
721
|
}
|
|
@@ -675,6 +738,8 @@ export function registerCloudCommands(program: Command): void {
|
|
|
675
738
|
"to have already resolved entity + credentials. Pick one.",
|
|
676
739
|
);
|
|
677
740
|
}
|
|
741
|
+
// Closes hq-cli#25 — see `assertNoPersonalPositionalPaths` doc-block.
|
|
742
|
+
assertNoPersonalPositionalPaths(options, paths);
|
|
678
743
|
|
|
679
744
|
log(chalk.bold("\nHQ Sync — Push"));
|
|
680
745
|
log(` HQ root: ${options.hqRoot}`);
|
|
@@ -854,6 +919,13 @@ export function registerCloudCommands(program: Command): void {
|
|
|
854
919
|
"from the cached Cognito session. Mutually exclusive with --company " +
|
|
855
920
|
"and --all.",
|
|
856
921
|
)
|
|
922
|
+
.option(
|
|
923
|
+
"--no-personal",
|
|
924
|
+
"In `--all` mode, skip the canonical-person leg of the fanout — " +
|
|
925
|
+
"only pull the caller's company memberships. Mirrors the upstream " +
|
|
926
|
+
"`hq-sync-runner --skip-personal` flag (hq-cloud 5.25.0). Ignored " +
|
|
927
|
+
"outside `--all`.",
|
|
928
|
+
)
|
|
857
929
|
.option(
|
|
858
930
|
"--mode-all",
|
|
859
931
|
"US-011: opt out of the strict narrow-hint refusal for this run. " +
|
|
@@ -880,10 +952,14 @@ export function registerCloudCommands(program: Command): void {
|
|
|
880
952
|
process.exit(1);
|
|
881
953
|
}
|
|
882
954
|
if (options.all) {
|
|
955
|
+
// `options.personal === false` is Commander's auto-negation of
|
|
956
|
+
// `--personal`; in `--all` mode that means "drop the personal
|
|
957
|
+
// leg from the fanout" (see `--no-personal` option above).
|
|
883
958
|
await runPullAll(
|
|
884
959
|
options.hqRoot,
|
|
885
960
|
options.onConflict,
|
|
886
961
|
options.modeAll === true,
|
|
962
|
+
options.personal === false,
|
|
887
963
|
);
|
|
888
964
|
return;
|
|
889
965
|
}
|
|
@@ -1069,6 +1145,13 @@ export function registerCloudCommands(program: Command): void {
|
|
|
1069
1145
|
"Sync the caller's canonical personal vault bidirectionally. " +
|
|
1070
1146
|
"Mutually exclusive with --company and --all.",
|
|
1071
1147
|
)
|
|
1148
|
+
.option(
|
|
1149
|
+
"--no-personal",
|
|
1150
|
+
"In `--all` mode, skip the canonical-person leg of both the push " +
|
|
1151
|
+
"and pull fanouts — only sync the caller's company memberships. " +
|
|
1152
|
+
"Mirrors the upstream `hq-sync-runner --skip-personal` flag " +
|
|
1153
|
+
"(hq-cloud 5.25.0). Ignored outside `--all`.",
|
|
1154
|
+
)
|
|
1072
1155
|
.option(
|
|
1073
1156
|
"--mode-all",
|
|
1074
1157
|
"US-011: opt out of the strict narrow-hint refusal for this run. " +
|
|
@@ -1088,11 +1171,16 @@ export function registerCloudCommands(program: Command): void {
|
|
|
1088
1171
|
try {
|
|
1089
1172
|
assertSingleSelector(options, "now");
|
|
1090
1173
|
if (options.all) {
|
|
1174
|
+
// `options.personal === false` is Commander's auto-negation
|
|
1175
|
+
// of `--personal`; in `--all` mode that means "drop the
|
|
1176
|
+
// personal leg from both legs of the bidirectional fanout"
|
|
1177
|
+
// (see `--no-personal` option above).
|
|
1091
1178
|
await runNowAll(
|
|
1092
1179
|
options.hqRoot,
|
|
1093
1180
|
options.message,
|
|
1094
1181
|
options.onConflict,
|
|
1095
1182
|
options.modeAll === true,
|
|
1183
|
+
options.personal === false,
|
|
1096
1184
|
);
|
|
1097
1185
|
return;
|
|
1098
1186
|
}
|
|
@@ -1119,10 +1207,15 @@ async function runPullAll(
|
|
|
1119
1207
|
hqRoot: string,
|
|
1120
1208
|
onConflict?: ConflictStrategy,
|
|
1121
1209
|
modeAllOverride?: boolean,
|
|
1210
|
+
skipPersonal?: boolean,
|
|
1122
1211
|
): Promise<void> {
|
|
1123
1212
|
console.log(chalk.bold("\nHQ Sync — Pull (all)"));
|
|
1124
1213
|
console.log(` HQ root: ${hqRoot}`);
|
|
1125
|
-
console.log(` Strategy: ${onConflict ?? "(interactive)"}
|
|
1214
|
+
console.log(` Strategy: ${onConflict ?? "(interactive)"}`);
|
|
1215
|
+
if (skipPersonal) {
|
|
1216
|
+
console.log(` Personal: skipped (--no-personal)`);
|
|
1217
|
+
}
|
|
1218
|
+
console.log("");
|
|
1126
1219
|
|
|
1127
1220
|
let result: PullAllResult;
|
|
1128
1221
|
try {
|
|
@@ -1150,6 +1243,7 @@ async function runPullAll(
|
|
|
1150
1243
|
...(onConflict ? { onConflict } : {}),
|
|
1151
1244
|
narrowHintLevel: resolveBannerLevel(),
|
|
1152
1245
|
...(modeAllOverride ? { modeAllOverride: true } : {}),
|
|
1246
|
+
...(skipPersonal ? { skipPersonal: true } : {}),
|
|
1153
1247
|
},
|
|
1154
1248
|
{
|
|
1155
1249
|
vaultClient: adapter,
|
|
@@ -1254,10 +1348,15 @@ async function runPushAll(
|
|
|
1254
1348
|
hqRoot: string,
|
|
1255
1349
|
message?: string,
|
|
1256
1350
|
onConflict?: ConflictStrategy,
|
|
1351
|
+
skipPersonal?: boolean,
|
|
1257
1352
|
): Promise<void> {
|
|
1258
1353
|
console.log(chalk.bold("\nHQ Sync — Push (all)"));
|
|
1259
1354
|
console.log(` HQ root: ${hqRoot}`);
|
|
1260
|
-
console.log(` Strategy: ${onConflict ?? "(interactive)"}
|
|
1355
|
+
console.log(` Strategy: ${onConflict ?? "(interactive)"}`);
|
|
1356
|
+
if (skipPersonal) {
|
|
1357
|
+
console.log(` Personal: skipped (--no-personal)`);
|
|
1358
|
+
}
|
|
1359
|
+
console.log("");
|
|
1261
1360
|
|
|
1262
1361
|
let result: PushAllResult;
|
|
1263
1362
|
try {
|
|
@@ -1283,6 +1382,7 @@ async function runPushAll(
|
|
|
1283
1382
|
hqRoot,
|
|
1284
1383
|
...(onConflict ? { onConflict } : {}),
|
|
1285
1384
|
...(message ? { message } : {}),
|
|
1385
|
+
...(skipPersonal ? { skipPersonal: true } : {}),
|
|
1286
1386
|
},
|
|
1287
1387
|
{
|
|
1288
1388
|
vaultClient: adapter,
|
|
@@ -1521,21 +1621,26 @@ async function runNowAll(
|
|
|
1521
1621
|
message?: string,
|
|
1522
1622
|
onConflict?: ConflictStrategy,
|
|
1523
1623
|
modeAllOverride?: boolean,
|
|
1624
|
+
skipPersonal?: boolean,
|
|
1524
1625
|
): Promise<void> {
|
|
1525
1626
|
console.log(chalk.bold("\nHQ Sync — Now (all)"));
|
|
1526
1627
|
console.log(` HQ root: ${hqRoot}`);
|
|
1527
|
-
console.log(` Strategy: ${onConflict ?? "(interactive)"}
|
|
1628
|
+
console.log(` Strategy: ${onConflict ?? "(interactive)"}`);
|
|
1629
|
+
if (skipPersonal) {
|
|
1630
|
+
console.log(` Personal: skipped (--no-personal)`);
|
|
1631
|
+
}
|
|
1632
|
+
console.log("");
|
|
1528
1633
|
|
|
1529
1634
|
// Push first (matches runner), then pull. Re-uses the per-leg orchestrators
|
|
1530
1635
|
// so the per-target rendering, error isolation, and exit codes are
|
|
1531
1636
|
// identical to running `push --all` then `pull --all` back-to-back.
|
|
1532
1637
|
console.log(chalk.dim("→ push --all"));
|
|
1533
|
-
await runPushAll(hqRoot, message, onConflict);
|
|
1638
|
+
await runPushAll(hqRoot, message, onConflict, skipPersonal);
|
|
1534
1639
|
console.log(chalk.dim("\n→ pull --all"));
|
|
1535
1640
|
// US-011: forward --mode-all so the strict refusal applies to the
|
|
1536
1641
|
// pull leg (push doesn't need a narrow-hint — the narrow ritual is
|
|
1537
1642
|
// pull-side).
|
|
1538
|
-
await runPullAll(hqRoot, onConflict, modeAllOverride);
|
|
1643
|
+
await runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal);
|
|
1539
1644
|
}
|
|
1540
1645
|
|
|
1541
1646
|
/**
|