@indigoai-us/hq-cli 5.17.0 → 5.18.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/dist/commands/cloud.d.ts +63 -1
- package/dist/commands/cloud.js +214 -11
- package/dist/commands/files-browse.d.ts +178 -0
- package/dist/commands/files-browse.js +348 -0
- package/dist/commands/files.d.ts +1 -1
- package/dist/commands/files.js +6 -2
- package/dist/commands/sync-mode.d.ts +115 -0
- package/dist/commands/sync-mode.js +249 -0
- package/dist/commands/sync-narrow.d.ts +154 -0
- package/dist/commands/sync-narrow.js +327 -0
- package/dist/index.js +11 -3
- package/dist/lib/local-tree-diff.d.ts +94 -0
- package/dist/lib/local-tree-diff.js +244 -0
- package/dist/lib/narrow-hint-banner.d.ts +102 -0
- package/dist/lib/narrow-hint-banner.js +144 -0
- package/package.json +2 -2
- package/src/commands/cloud.pull-all.test.ts +170 -1
- package/src/commands/cloud.pull-per-company.test.ts +188 -0
- package/src/commands/cloud.ts +327 -5
- package/src/commands/files-browse.test.ts +475 -0
- package/src/commands/files-browse.ts +561 -0
- package/src/commands/files.ts +6 -1
- package/src/commands/sync-mode.test.ts +366 -0
- package/src/commands/sync-mode.ts +387 -0
- package/src/commands/sync-narrow.test.ts +573 -0
- package/src/commands/sync-narrow.ts +541 -0
- package/src/index.ts +9 -1
- package/src/lib/hq-cloud-dep.smoke.test.ts +75 -0
- package/src/lib/local-tree-diff.test.ts +262 -0
- package/src/lib/local-tree-diff.ts +330 -0
- package/src/lib/narrow-hint-banner.test.ts +235 -0
- package/src/lib/narrow-hint-banner.ts +212 -0
package/src/commands/cloud.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
computePersonalVaultPaths,
|
|
29
29
|
type ConflictStrategy,
|
|
30
30
|
type EntityContext,
|
|
31
|
+
type MembershipSyncConfig,
|
|
31
32
|
type SyncProgressEvent,
|
|
32
33
|
type UploadAuthor,
|
|
33
34
|
} from "@indigoai-us/hq-cloud";
|
|
@@ -37,6 +38,12 @@ import {
|
|
|
37
38
|
ensureCognitoToken,
|
|
38
39
|
buildVaultConfig,
|
|
39
40
|
} from "../utils/cognito-session.js";
|
|
41
|
+
import {
|
|
42
|
+
emitNarrowHint,
|
|
43
|
+
isStrictRefusal,
|
|
44
|
+
resolveBannerLevel,
|
|
45
|
+
type BannerLevel,
|
|
46
|
+
} from "../lib/narrow-hint-banner.js";
|
|
40
47
|
|
|
41
48
|
interface CommonSyncOptions {
|
|
42
49
|
hqRoot: string;
|
|
@@ -53,7 +60,9 @@ interface CommonSyncOptions {
|
|
|
53
60
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
61
|
|
|
55
62
|
export interface PullAllVaultClient {
|
|
56
|
-
listMyMemberships(): Promise<
|
|
63
|
+
listMyMemberships(): Promise<
|
|
64
|
+
Array<{ companyUid: string; membershipKey?: string }>
|
|
65
|
+
>;
|
|
57
66
|
listPersonEntities(): Promise<
|
|
58
67
|
Array<{
|
|
59
68
|
uid: string;
|
|
@@ -64,6 +73,15 @@ export interface PullAllVaultClient {
|
|
|
64
73
|
}>
|
|
65
74
|
>;
|
|
66
75
|
getEntity(uid: string): Promise<{ slug?: string; name?: string } | null>;
|
|
76
|
+
/**
|
|
77
|
+
* US-011: optional — when present, `pullAll` calls it once per
|
|
78
|
+
* membership to surface the narrow-hint banner for all-mode owners.
|
|
79
|
+
* Absent on legacy adapters (push-all et al.) where the banner is not
|
|
80
|
+
* applicable.
|
|
81
|
+
*/
|
|
82
|
+
getMembershipSyncConfig?: (
|
|
83
|
+
membershipId: string,
|
|
84
|
+
) => Promise<MembershipSyncConfig>;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
export interface SyncCallOptions {
|
|
@@ -91,6 +109,22 @@ export interface PullAllDeps {
|
|
|
91
109
|
export interface PullAllOptions {
|
|
92
110
|
hqRoot: string;
|
|
93
111
|
onConflict?: ConflictStrategy;
|
|
112
|
+
/**
|
|
113
|
+
* US-011: banner level for the narrow-hint nudge. Defaults to `'hint'`
|
|
114
|
+
* — see `resolveBannerLevel` for the env-driven override. The
|
|
115
|
+
* `'strict'` level causes `pullAll` to refuse to sync any membership
|
|
116
|
+
* still on `syncMode: 'all'` unless `modeAllOverride` is true.
|
|
117
|
+
*
|
|
118
|
+
* TODO(hq-core-staging release N+2): default flips to 'warning'.
|
|
119
|
+
* TODO(hq-core-staging release N+3): default flips to 'strict'.
|
|
120
|
+
*/
|
|
121
|
+
narrowHintLevel?: BannerLevel;
|
|
122
|
+
/**
|
|
123
|
+
* US-011: when `true`, strict-mode does NOT refuse all-mode
|
|
124
|
+
* memberships — the operator has explicitly opted into keeping the
|
|
125
|
+
* legacy behavior for this run via `--mode-all`.
|
|
126
|
+
*/
|
|
127
|
+
modeAllOverride?: boolean;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
export interface PullAllRow {
|
|
@@ -168,6 +202,9 @@ export interface PullAllResult {
|
|
|
168
202
|
interface PlanEntry {
|
|
169
203
|
slug: string;
|
|
170
204
|
syncOptions: SyncCallOptions;
|
|
205
|
+
/** US-011: tracked so we can resolve sync-config + emit the narrow hint. */
|
|
206
|
+
companyUid?: string;
|
|
207
|
+
membershipKey?: string;
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
// Oldest-first by createdAt, ties broken by uid lexicographic — matches
|
|
@@ -191,6 +228,9 @@ export async function pullAll(
|
|
|
191
228
|
const memberships = await deps.vaultClient.listMyMemberships();
|
|
192
229
|
const persons = await deps.vaultClient.listPersonEntities();
|
|
193
230
|
|
|
231
|
+
const narrowHintLevel: BannerLevel = options.narrowHintLevel ?? "hint";
|
|
232
|
+
const getSyncConfig = deps.vaultClient.getMembershipSyncConfig;
|
|
233
|
+
|
|
194
234
|
const plan: PlanEntry[] = [];
|
|
195
235
|
for (const m of memberships) {
|
|
196
236
|
let slug = m.companyUid;
|
|
@@ -202,6 +242,8 @@ export async function pullAll(
|
|
|
202
242
|
}
|
|
203
243
|
plan.push({
|
|
204
244
|
slug,
|
|
245
|
+
companyUid: m.companyUid,
|
|
246
|
+
...(m.membershipKey ? { membershipKey: m.membershipKey } : {}),
|
|
205
247
|
syncOptions: {
|
|
206
248
|
company: m.companyUid,
|
|
207
249
|
hqRoot: options.hqRoot,
|
|
@@ -235,12 +277,63 @@ export async function pullAll(
|
|
|
235
277
|
|
|
236
278
|
for (const entry of plan) {
|
|
237
279
|
result.attempted += 1;
|
|
280
|
+
|
|
281
|
+
// US-011: resolve the membership's effective sync mode so we can
|
|
282
|
+
// either nudge an all-mode owner toward `hq sync narrow` OR refuse
|
|
283
|
+
// the leg outright when strict-mode is on and the operator didn't
|
|
284
|
+
// pass `--mode-all`. Sync-config lookup is best-effort — a 404 or
|
|
285
|
+
// network blip should never block the sync itself, so we fall back
|
|
286
|
+
// to syncMode='all' (the legacy default) and skip the banner.
|
|
287
|
+
let resolvedMode: MembershipSyncConfig["syncMode"] | undefined;
|
|
288
|
+
if (entry.membershipKey && getSyncConfig) {
|
|
289
|
+
try {
|
|
290
|
+
const cfg = await getSyncConfig(entry.membershipKey);
|
|
291
|
+
resolvedMode = cfg.syncMode;
|
|
292
|
+
} catch {
|
|
293
|
+
resolvedMode = undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
resolvedMode === "all" &&
|
|
299
|
+
isStrictRefusal(resolvedMode, narrowHintLevel) &&
|
|
300
|
+
!options.modeAllOverride &&
|
|
301
|
+
entry.companyUid
|
|
302
|
+
) {
|
|
303
|
+
// Emit the strict-level banner once, then mark the leg as errored
|
|
304
|
+
// without invoking sync(). The operator either narrows the
|
|
305
|
+
// membership (`hq sync narrow --apply`) or passes `--mode-all` to
|
|
306
|
+
// opt back in.
|
|
307
|
+
emitNarrowHint({
|
|
308
|
+
companyUid: entry.companyUid,
|
|
309
|
+
syncMode: resolvedMode,
|
|
310
|
+
level: narrowHintLevel,
|
|
311
|
+
});
|
|
312
|
+
const message =
|
|
313
|
+
"Refusing to pull all-mode membership in strict mode. " +
|
|
314
|
+
"Run `hq sync narrow --apply` to migrate, or re-run with --mode-all.";
|
|
315
|
+
result.errors.push({ company: entry.slug, message });
|
|
316
|
+
result.perCompany.push({ slug: entry.slug, error: message });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
238
320
|
try {
|
|
239
321
|
const r = await deps.sync(entry.syncOptions);
|
|
240
322
|
result.filesDownloaded += r.filesDownloaded;
|
|
241
323
|
result.bytesDownloaded += r.bytesDownloaded;
|
|
242
324
|
result.conflicts += r.conflicts;
|
|
243
325
|
result.perCompany.push({ slug: entry.slug, result: r });
|
|
326
|
+
|
|
327
|
+
// Banner emitted AFTER the leg succeeds so it appears alongside
|
|
328
|
+
// the per-company summary line and doesn't get scrolled off by
|
|
329
|
+
// sync chatter.
|
|
330
|
+
if (resolvedMode === "all" && entry.companyUid) {
|
|
331
|
+
emitNarrowHint({
|
|
332
|
+
companyUid: entry.companyUid,
|
|
333
|
+
syncMode: resolvedMode,
|
|
334
|
+
level: narrowHintLevel,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
244
337
|
} catch (err) {
|
|
245
338
|
const message = err instanceof Error ? err.message : String(err);
|
|
246
339
|
result.errors.push({ company: entry.slug, message });
|
|
@@ -380,6 +473,86 @@ export function assertSingleSelector(opts: {
|
|
|
380
473
|
}
|
|
381
474
|
}
|
|
382
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Per-company pull resolution helper used by `hq sync pull --company <slug>`
|
|
478
|
+
* (US-011 fix, 2026-05-21). Mirrors the inline lookup that `runNowSingle`
|
|
479
|
+
* does for sync-now. Pulled out so the action handler stays thin AND so
|
|
480
|
+
* unit tests can exercise the banner / strict-refusal decision without
|
|
481
|
+
* spinning up commander + a real VaultClient.
|
|
482
|
+
*
|
|
483
|
+
* Input shape:
|
|
484
|
+
* - `targetCompany` — slug or UID the caller passed to `--company`. If
|
|
485
|
+
* undefined, the helper short-circuits to a "no resolution" result
|
|
486
|
+
* (the action handler falls back to .hq/config.json via sync()).
|
|
487
|
+
* - `client` — minimal VaultClient surface: listMyMemberships + entity.get
|
|
488
|
+
* + getMembershipSyncConfig.
|
|
489
|
+
*
|
|
490
|
+
* Output: `{ resolvedCompanyUid, resolvedMode }` — either may be undefined
|
|
491
|
+
* if the membership / sync-config call failed. Both undefined is a clean
|
|
492
|
+
* degradation — the caller pulls without a banner.
|
|
493
|
+
*/
|
|
494
|
+
export interface PerCompanyPullResolveClient {
|
|
495
|
+
listMyMemberships(): Promise<Array<{ companyUid: string; membershipKey: string }>>;
|
|
496
|
+
getMembershipSyncConfig(
|
|
497
|
+
membershipKey: string,
|
|
498
|
+
): Promise<{ syncMode: MembershipSyncConfig["syncMode"] }>;
|
|
499
|
+
entity: { get(uid: string): Promise<{ slug?: string }> };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface PerCompanyPullResolveResult {
|
|
503
|
+
resolvedCompanyUid: string | undefined;
|
|
504
|
+
resolvedMode: MembershipSyncConfig["syncMode"] | undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function resolvePerCompanyPullPlan(
|
|
508
|
+
client: PerCompanyPullResolveClient,
|
|
509
|
+
targetCompany: string | undefined,
|
|
510
|
+
): Promise<PerCompanyPullResolveResult> {
|
|
511
|
+
if (!targetCompany) return { resolvedCompanyUid: undefined, resolvedMode: undefined };
|
|
512
|
+
try {
|
|
513
|
+
const memberships = await client.listMyMemberships();
|
|
514
|
+
// Direct UID / membershipKey match first (cheapest).
|
|
515
|
+
const direct = memberships.find(
|
|
516
|
+
(m) => m.companyUid === targetCompany || m.membershipKey === targetCompany,
|
|
517
|
+
);
|
|
518
|
+
if (direct) {
|
|
519
|
+
let mode: MembershipSyncConfig["syncMode"] | undefined;
|
|
520
|
+
try {
|
|
521
|
+
const cfg = await client.getMembershipSyncConfig(direct.membershipKey);
|
|
522
|
+
mode = cfg.syncMode;
|
|
523
|
+
} catch {
|
|
524
|
+
mode = undefined;
|
|
525
|
+
}
|
|
526
|
+
return { resolvedCompanyUid: direct.companyUid, resolvedMode: mode };
|
|
527
|
+
}
|
|
528
|
+
// Slug match — listMyMemberships returns companyUid only, so fan out
|
|
529
|
+
// entity.get to find the row whose slug matches the caller's input.
|
|
530
|
+
for (const m of memberships) {
|
|
531
|
+
try {
|
|
532
|
+
const entity = await client.entity.get(m.companyUid);
|
|
533
|
+
if (entity.slug === targetCompany) {
|
|
534
|
+
let mode: MembershipSyncConfig["syncMode"] | undefined;
|
|
535
|
+
try {
|
|
536
|
+
const cfg = await client.getMembershipSyncConfig(m.membershipKey);
|
|
537
|
+
mode = cfg.syncMode;
|
|
538
|
+
} catch {
|
|
539
|
+
mode = undefined;
|
|
540
|
+
}
|
|
541
|
+
return { resolvedCompanyUid: m.companyUid, resolvedMode: mode };
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
// Entity not visible — skip and continue. Worst case the loop ends
|
|
545
|
+
// with no match and we return undefined for both — the pull still
|
|
546
|
+
// proceeds, banner just stays quiet.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
// listMyMemberships failed — degrade silently. Sync still works without
|
|
551
|
+
// the banner; this matches the runPullAll catch behavior.
|
|
552
|
+
}
|
|
553
|
+
return { resolvedCompanyUid: undefined, resolvedMode: undefined };
|
|
554
|
+
}
|
|
555
|
+
|
|
383
556
|
export function registerCloudCommands(program: Command): void {
|
|
384
557
|
program
|
|
385
558
|
.command("push")
|
|
@@ -681,12 +854,20 @@ export function registerCloudCommands(program: Command): void {
|
|
|
681
854
|
"from the cached Cognito session. Mutually exclusive with --company " +
|
|
682
855
|
"and --all.",
|
|
683
856
|
)
|
|
857
|
+
.option(
|
|
858
|
+
"--mode-all",
|
|
859
|
+
"US-011: opt out of the strict narrow-hint refusal for this run. " +
|
|
860
|
+
"Has no effect today (default narrow-hint level is 'hint'); " +
|
|
861
|
+
"wired so future hq-core-staging releases can flip the default to " +
|
|
862
|
+
"'strict' without re-touching this command.",
|
|
863
|
+
)
|
|
684
864
|
.action(
|
|
685
865
|
async (
|
|
686
866
|
options: CommonSyncOptions & {
|
|
687
867
|
onConflict?: ConflictStrategy;
|
|
688
868
|
all?: boolean;
|
|
689
869
|
personal?: boolean;
|
|
870
|
+
modeAll?: boolean;
|
|
690
871
|
},
|
|
691
872
|
) => {
|
|
692
873
|
try {
|
|
@@ -699,7 +880,11 @@ export function registerCloudCommands(program: Command): void {
|
|
|
699
880
|
process.exit(1);
|
|
700
881
|
}
|
|
701
882
|
if (options.all) {
|
|
702
|
-
await runPullAll(
|
|
883
|
+
await runPullAll(
|
|
884
|
+
options.hqRoot,
|
|
885
|
+
options.onConflict,
|
|
886
|
+
options.modeAll === true,
|
|
887
|
+
);
|
|
703
888
|
return;
|
|
704
889
|
}
|
|
705
890
|
if (options.personal) {
|
|
@@ -712,10 +897,50 @@ export function registerCloudCommands(program: Command): void {
|
|
|
712
897
|
console.log(` Company: ${options.company ?? "(from .hq/config.json)"}\n`);
|
|
713
898
|
|
|
714
899
|
const accessToken = await ensureCognitoToken();
|
|
900
|
+
const vaultConfig = buildVaultConfig(accessToken);
|
|
901
|
+
|
|
902
|
+
// US-011 (2026-05-21 fix): resolve the caller's sync-config for
|
|
903
|
+
// the targeted membership BEFORE the pull, so we can (a) emit the
|
|
904
|
+
// narrow-hint banner after success if still on all-mode and
|
|
905
|
+
// (b) respect strict-mode refusal mirror of the --all + sync-now
|
|
906
|
+
// paths. Failure to resolve degrades silently — pull still works,
|
|
907
|
+
// banner just stays quiet (same as the catch in runPullAll).
|
|
908
|
+
const narrowHintLevel: BannerLevel = resolveBannerLevel();
|
|
909
|
+
const { resolvedCompanyUid, resolvedMode } =
|
|
910
|
+
await resolvePerCompanyPullPlan(
|
|
911
|
+
new VaultClient(vaultConfig),
|
|
912
|
+
options.company,
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
// Strict-mode refusal: matches runPullAll + runNowSingle behavior.
|
|
916
|
+
// Default banner level is 'hint' which never triggers refusal —
|
|
917
|
+
// wired now so future hq-core-staging releases can flip the
|
|
918
|
+
// default to 'strict' without re-touching this command.
|
|
919
|
+
if (
|
|
920
|
+
resolvedMode === "all" &&
|
|
921
|
+
isStrictRefusal(resolvedMode, narrowHintLevel) &&
|
|
922
|
+
options.modeAll !== true &&
|
|
923
|
+
resolvedCompanyUid
|
|
924
|
+
) {
|
|
925
|
+
emitNarrowHint({
|
|
926
|
+
companyUid: resolvedCompanyUid,
|
|
927
|
+
syncMode: resolvedMode,
|
|
928
|
+
level: narrowHintLevel,
|
|
929
|
+
});
|
|
930
|
+
console.error(
|
|
931
|
+
chalk.red(
|
|
932
|
+
"\n✗ Pull refused: strict narrow-hint mode is on and this " +
|
|
933
|
+
"membership still pulls everything. Run `hq sync narrow --apply` " +
|
|
934
|
+
"to migrate, or re-run with --mode-all.",
|
|
935
|
+
),
|
|
936
|
+
);
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
|
|
715
940
|
const result = await sync({
|
|
716
941
|
company: options.company,
|
|
717
942
|
onConflict: options.onConflict,
|
|
718
|
-
vaultConfig
|
|
943
|
+
vaultConfig,
|
|
719
944
|
hqRoot: options.hqRoot,
|
|
720
945
|
});
|
|
721
946
|
|
|
@@ -733,6 +958,17 @@ export function registerCloudCommands(program: Command): void {
|
|
|
733
958
|
`\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`,
|
|
734
959
|
),
|
|
735
960
|
);
|
|
961
|
+
|
|
962
|
+
// US-011 (2026-05-21 fix): emit the hint banner after success
|
|
963
|
+
// so it appears alongside the summary line. Mirrors the wiring
|
|
964
|
+
// in runPullAll (cloud.ts:331) and runNowSingle (cloud.ts:1371).
|
|
965
|
+
if (resolvedMode === "all" && resolvedCompanyUid) {
|
|
966
|
+
emitNarrowHint({
|
|
967
|
+
companyUid: resolvedCompanyUid,
|
|
968
|
+
syncMode: resolvedMode,
|
|
969
|
+
level: narrowHintLevel,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
736
972
|
} catch (err) {
|
|
737
973
|
console.error(
|
|
738
974
|
chalk.red("\n✗ Pull failed:"),
|
|
@@ -833,6 +1069,12 @@ export function registerCloudCommands(program: Command): void {
|
|
|
833
1069
|
"Sync the caller's canonical personal vault bidirectionally. " +
|
|
834
1070
|
"Mutually exclusive with --company and --all.",
|
|
835
1071
|
)
|
|
1072
|
+
.option(
|
|
1073
|
+
"--mode-all",
|
|
1074
|
+
"US-011: opt out of the strict narrow-hint refusal for this run. " +
|
|
1075
|
+
"No-op today; wired so future hq-core-staging releases can flip " +
|
|
1076
|
+
"the default narrow-hint level to 'strict'.",
|
|
1077
|
+
)
|
|
836
1078
|
.action(
|
|
837
1079
|
async (
|
|
838
1080
|
options: CommonSyncOptions & {
|
|
@@ -840,6 +1082,7 @@ export function registerCloudCommands(program: Command): void {
|
|
|
840
1082
|
message?: string;
|
|
841
1083
|
all?: boolean;
|
|
842
1084
|
personal?: boolean;
|
|
1085
|
+
modeAll?: boolean;
|
|
843
1086
|
},
|
|
844
1087
|
) => {
|
|
845
1088
|
try {
|
|
@@ -849,6 +1092,7 @@ export function registerCloudCommands(program: Command): void {
|
|
|
849
1092
|
options.hqRoot,
|
|
850
1093
|
options.message,
|
|
851
1094
|
options.onConflict,
|
|
1095
|
+
options.modeAll === true,
|
|
852
1096
|
);
|
|
853
1097
|
return;
|
|
854
1098
|
}
|
|
@@ -858,6 +1102,7 @@ export function registerCloudCommands(program: Command): void {
|
|
|
858
1102
|
options.personal === true,
|
|
859
1103
|
options.message,
|
|
860
1104
|
options.onConflict,
|
|
1105
|
+
options.modeAll === true,
|
|
861
1106
|
);
|
|
862
1107
|
} catch (err) {
|
|
863
1108
|
console.error(
|
|
@@ -873,6 +1118,7 @@ export function registerCloudCommands(program: Command): void {
|
|
|
873
1118
|
async function runPullAll(
|
|
874
1119
|
hqRoot: string,
|
|
875
1120
|
onConflict?: ConflictStrategy,
|
|
1121
|
+
modeAllOverride?: boolean,
|
|
876
1122
|
): Promise<void> {
|
|
877
1123
|
console.log(chalk.bold("\nHQ Sync — Pull (all)"));
|
|
878
1124
|
console.log(` HQ root: ${hqRoot}`);
|
|
@@ -894,10 +1140,17 @@ async function runPullAll(
|
|
|
894
1140
|
return null;
|
|
895
1141
|
}
|
|
896
1142
|
},
|
|
1143
|
+
getMembershipSyncConfig: (id: string) =>
|
|
1144
|
+
realClient.getMembershipSyncConfig(id),
|
|
897
1145
|
};
|
|
898
1146
|
|
|
899
1147
|
result = await pullAll(
|
|
900
|
-
{
|
|
1148
|
+
{
|
|
1149
|
+
hqRoot,
|
|
1150
|
+
...(onConflict ? { onConflict } : {}),
|
|
1151
|
+
narrowHintLevel: resolveBannerLevel(),
|
|
1152
|
+
...(modeAllOverride ? { modeAllOverride: true } : {}),
|
|
1153
|
+
},
|
|
901
1154
|
{
|
|
902
1155
|
vaultClient: adapter,
|
|
903
1156
|
sync: (opts) =>
|
|
@@ -1095,6 +1348,7 @@ async function runNowSingle(
|
|
|
1095
1348
|
personal: boolean,
|
|
1096
1349
|
message?: string,
|
|
1097
1350
|
onConflict?: ConflictStrategy,
|
|
1351
|
+
modeAllOverride?: boolean,
|
|
1098
1352
|
): Promise<void> {
|
|
1099
1353
|
console.log(chalk.bold("\nHQ Sync — Now"));
|
|
1100
1354
|
console.log(` HQ root: ${hqRoot}`);
|
|
@@ -1168,6 +1422,59 @@ async function runNowSingle(
|
|
|
1168
1422
|
process.exit(1);
|
|
1169
1423
|
}
|
|
1170
1424
|
|
|
1425
|
+
// US-011: resolve membership sync-config so we can either nudge an
|
|
1426
|
+
// all-mode owner or refuse the pull when strict-mode is on. Skipped
|
|
1427
|
+
// for personal targets (personal vault has no membership row) and
|
|
1428
|
+
// for resolution failures (best-effort — never block sync). The
|
|
1429
|
+
// lookup runs BEFORE the pull leg so strict refusal can short-circuit
|
|
1430
|
+
// without burning a sync.
|
|
1431
|
+
const narrowHintLevel: BannerLevel = resolveBannerLevel();
|
|
1432
|
+
let resolvedMode: MembershipSyncConfig["syncMode"] | undefined;
|
|
1433
|
+
let resolvedCompanyUid: string | undefined;
|
|
1434
|
+
if (!personalMode && targetCompany) {
|
|
1435
|
+
try {
|
|
1436
|
+
const client = new VaultClient(vaultConfig);
|
|
1437
|
+
const memberships = await client.listMyMemberships();
|
|
1438
|
+
const match = memberships.find(
|
|
1439
|
+
(m) => m.companyUid === targetCompany || m.membershipKey === targetCompany,
|
|
1440
|
+
);
|
|
1441
|
+
if (match) {
|
|
1442
|
+
resolvedCompanyUid = match.companyUid;
|
|
1443
|
+
try {
|
|
1444
|
+
const cfg = await client.getMembershipSyncConfig(
|
|
1445
|
+
match.membershipKey,
|
|
1446
|
+
);
|
|
1447
|
+
resolvedMode = cfg.syncMode;
|
|
1448
|
+
} catch {
|
|
1449
|
+
resolvedMode = undefined;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
} catch {
|
|
1453
|
+
resolvedMode = undefined;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (
|
|
1458
|
+
resolvedMode === "all" &&
|
|
1459
|
+
isStrictRefusal(resolvedMode, narrowHintLevel) &&
|
|
1460
|
+
!modeAllOverride &&
|
|
1461
|
+
resolvedCompanyUid
|
|
1462
|
+
) {
|
|
1463
|
+
emitNarrowHint({
|
|
1464
|
+
companyUid: resolvedCompanyUid,
|
|
1465
|
+
syncMode: resolvedMode,
|
|
1466
|
+
level: narrowHintLevel,
|
|
1467
|
+
});
|
|
1468
|
+
console.error(
|
|
1469
|
+
chalk.red(
|
|
1470
|
+
"\n✗ Sync now refused: strict narrow-hint mode is on and this " +
|
|
1471
|
+
"membership still pulls everything. Run `hq sync narrow --apply` " +
|
|
1472
|
+
"to migrate, or re-run with --mode-all.",
|
|
1473
|
+
),
|
|
1474
|
+
);
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1171
1478
|
console.log(chalk.dim(" → pull leg"));
|
|
1172
1479
|
const pullResult = await sync({
|
|
1173
1480
|
company: targetCompany,
|
|
@@ -1188,6 +1495,17 @@ async function runNowSingle(
|
|
|
1188
1495
|
console.log(chalk.yellow("\n⚠ Sync now finished with pull leg aborted."));
|
|
1189
1496
|
process.exit(1);
|
|
1190
1497
|
}
|
|
1498
|
+
|
|
1499
|
+
// US-011: emit the hint banner after a successful pull so it
|
|
1500
|
+
// appears at the bottom of the summary rather than mid-stream.
|
|
1501
|
+
if (resolvedMode === "all" && resolvedCompanyUid) {
|
|
1502
|
+
emitNarrowHint({
|
|
1503
|
+
companyUid: resolvedCompanyUid,
|
|
1504
|
+
syncMode: resolvedMode,
|
|
1505
|
+
level: narrowHintLevel,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1191
1509
|
console.log(chalk.green("\n✓ Sync now complete"));
|
|
1192
1510
|
} catch (err) {
|
|
1193
1511
|
console.error(
|
|
@@ -1202,6 +1520,7 @@ async function runNowAll(
|
|
|
1202
1520
|
hqRoot: string,
|
|
1203
1521
|
message?: string,
|
|
1204
1522
|
onConflict?: ConflictStrategy,
|
|
1523
|
+
modeAllOverride?: boolean,
|
|
1205
1524
|
): Promise<void> {
|
|
1206
1525
|
console.log(chalk.bold("\nHQ Sync — Now (all)"));
|
|
1207
1526
|
console.log(` HQ root: ${hqRoot}`);
|
|
@@ -1213,7 +1532,10 @@ async function runNowAll(
|
|
|
1213
1532
|
console.log(chalk.dim("→ push --all"));
|
|
1214
1533
|
await runPushAll(hqRoot, message, onConflict);
|
|
1215
1534
|
console.log(chalk.dim("\n→ pull --all"));
|
|
1216
|
-
|
|
1535
|
+
// US-011: forward --mode-all so the strict refusal applies to the
|
|
1536
|
+
// pull leg (push doesn't need a narrow-hint — the narrow ritual is
|
|
1537
|
+
// pull-side).
|
|
1538
|
+
await runPullAll(hqRoot, onConflict, modeAllOverride);
|
|
1217
1539
|
}
|
|
1218
1540
|
|
|
1219
1541
|
/**
|