@indigoai-us/hq-cloud 5.23.0 → 5.25.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/dist/bin/sync-runner.d.ts +58 -3
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +84 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +90 -3
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +86 -20
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +332 -62
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +490 -6
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +48 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/personal-vault-exclusions.d.ts +128 -0
- package/dist/personal-vault-exclusions.d.ts.map +1 -0
- package/dist/personal-vault-exclusions.js +231 -0
- package/dist/personal-vault-exclusions.js.map +1 -0
- package/dist/personal-vault-exclusions.test.d.ts +22 -0
- package/dist/personal-vault-exclusions.test.d.ts.map +1 -0
- package/dist/personal-vault-exclusions.test.js +198 -0
- package/dist/personal-vault-exclusions.test.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +113 -3
- package/src/bin/sync-runner.ts +125 -5
- package/src/cli/share.test.ts +585 -6
- package/src/cli/share.ts +461 -86
- package/src/cli/sync.ts +50 -0
- package/src/index.ts +10 -0
- package/src/personal-vault-exclusions.test.ts +256 -0
- package/src/personal-vault-exclusions.ts +277 -0
package/src/bin/sync-runner.ts
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* --company <slug-or-uid> Sync a single company (alternative to --companies)
|
|
15
15
|
* --on-conflict <strategy> abort | overwrite | keep (default: abort)
|
|
16
16
|
* --hq-root <path> Local HQ directory (default: $HOME/hq)
|
|
17
|
+
* --skip-personal Drop the personal target from the --companies
|
|
18
|
+
* fanout. Combined with HQ_SYNC_SKIP_PERSONAL env
|
|
19
|
+
* (either truthy disables personal sync). No-op
|
|
20
|
+
* outside --companies mode.
|
|
17
21
|
* --json Ignored — ndjson on stdout is the default and
|
|
18
22
|
* only output mode. Accepted for symmetry with the
|
|
19
23
|
* AppBar's argv in case someone passes it.
|
|
@@ -120,6 +124,63 @@ const DEFAULT_VAULT_API_URL =
|
|
|
120
124
|
|
|
121
125
|
const DEFAULT_HQ_ROOT = path.join(os.homedir(), "hq");
|
|
122
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Delete-propagation policy honored by the push leg of bidirectional sync.
|
|
129
|
+
*
|
|
130
|
+
* Default `"currency-gated"` in 5.25 — flipped from `"owned-only"` after
|
|
131
|
+
* one machine (Indigo / corey) ran the 5.24 code path through real syncs
|
|
132
|
+
* for a week without surfacing surprise behavior. Currency-gated does a
|
|
133
|
+
* per-file ETag HEAD before propagating any local-delete to S3: if the
|
|
134
|
+
* remote object's current ETag no longer matches the journal's last-
|
|
135
|
+
* recorded one, the delete is refused and the next pull leg re-pulls the
|
|
136
|
+
* file via the standard 3-way merge path. This is strictly safer than
|
|
137
|
+
* `owned-only` (which propagates any local-delete the journal can prove
|
|
138
|
+
* came from this device) — the only delete-class that changes behavior
|
|
139
|
+
* is "deleted locally + modified remotely by another device", which
|
|
140
|
+
* previously destroyed remote work and now becomes a pull-and-conflict.
|
|
141
|
+
*
|
|
142
|
+
* Env override `HQ_SYNC_DELETE_POLICY=owned-only|all|currency-gated` is
|
|
143
|
+
* also the rollback knob — anyone surprised by 5.25's flip can revert
|
|
144
|
+
* to `owned-only` without redeploying. `all` is the unsafe-mirror mode
|
|
145
|
+
* previously used by the runner pre-5.20 — included only as an
|
|
146
|
+
* emergency reconcile lever, not a recommended default.
|
|
147
|
+
*/
|
|
148
|
+
export type DeletePropagationPolicy = "currency-gated" | "owned-only" | "all";
|
|
149
|
+
|
|
150
|
+
export function resolveDeletePolicy(): DeletePropagationPolicy {
|
|
151
|
+
const env = process.env.HQ_SYNC_DELETE_POLICY;
|
|
152
|
+
if (env === "owned-only" || env === "all" || env === "currency-gated") {
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
return "currency-gated";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve whether to skip the personal target in a `--companies` fanout.
|
|
160
|
+
*
|
|
161
|
+
* Two inputs combine: the `--skip-personal` CLI flag (parsed into
|
|
162
|
+
* `ParsedArgs.skipPersonal`) and the `HQ_SYNC_SKIP_PERSONAL` env var. Either
|
|
163
|
+
* being truthy skips the personal target — flag wins on conflict (CLI
|
|
164
|
+
* flag is the explicit-for-this-invocation knob, env is the persistent
|
|
165
|
+
* default usually set by the menubar in the spawned child process).
|
|
166
|
+
*
|
|
167
|
+
* Env truthy values: `1`, `true`, `yes` (case-insensitive). Anything else
|
|
168
|
+
* (including missing) is treated as falsy — same shape as classic
|
|
169
|
+
* Unix opt-in env conventions; conservative to avoid surprising opt-outs.
|
|
170
|
+
*
|
|
171
|
+
* Use case: the menubar app exposes a "Sync personal vault" toggle in
|
|
172
|
+
* Settings (default ON, matching the auto-provisioning UX). When the user
|
|
173
|
+
* flips it off, the menubar spawns `hq sync` with this env set so the
|
|
174
|
+
* fanout drops the personal target before walking the user's entire HQ
|
|
175
|
+
* tree (a sync that would otherwise scan thousands of files, including
|
|
176
|
+
* the new personal-vault default exclusions, just to do nothing useful).
|
|
177
|
+
*/
|
|
178
|
+
export function resolveSkipPersonal(flag: boolean): boolean {
|
|
179
|
+
if (flag) return true;
|
|
180
|
+
const env = (process.env.HQ_SYNC_SKIP_PERSONAL ?? "").toLowerCase();
|
|
181
|
+
return env === "1" || env === "true" || env === "yes";
|
|
182
|
+
}
|
|
183
|
+
|
|
123
184
|
// Personal-vault scope (exclusion list + path computer) lives in
|
|
124
185
|
// `../personal-vault.ts` so the `hq sync` CLI and this runner share the same
|
|
125
186
|
// rules. Re-exported here for back-compat with any callers still importing
|
|
@@ -169,12 +230,19 @@ export type RunnerEvent =
|
|
|
169
230
|
/**
|
|
170
231
|
* Upload counters. Always emitted (0 when the run was pull-only) so
|
|
171
232
|
* downstream consumers don't need to conditionally read the field.
|
|
172
|
-
* Tauri's `SyncCompleteEvent` ignores extra fields today; adding them
|
|
173
|
-
* to the Rust struct is a follow-up when the UI needs to surface push
|
|
174
|
-
* totals.
|
|
175
233
|
*/
|
|
176
234
|
filesUploaded: number;
|
|
177
235
|
bytesUploaded: number;
|
|
236
|
+
/**
|
|
237
|
+
* Push-side counters added in 5.25. Always emitted as numbers (0
|
|
238
|
+
* when no push leg ran). Tauri's `SyncCompleteEvent` carries them
|
|
239
|
+
* as Option<u32> for back-compat with <5.25 engines that don't
|
|
240
|
+
* include them; structural-typing-wise, the union just adds
|
|
241
|
+
* properties on top of `SyncResult`.
|
|
242
|
+
*/
|
|
243
|
+
filesTombstoned: number;
|
|
244
|
+
filesRefusedStale: number;
|
|
245
|
+
filesExcludedByPolicy: number;
|
|
178
246
|
} & SyncResult)
|
|
179
247
|
| {
|
|
180
248
|
type: "all-complete";
|
|
@@ -374,6 +442,13 @@ interface ParsedArgs {
|
|
|
374
442
|
watch: boolean;
|
|
375
443
|
/** Auto-sync (Beta): ms between remote-pull passes. Required when watch=true. */
|
|
376
444
|
pollRemoteMs?: number;
|
|
445
|
+
/**
|
|
446
|
+
* Drop the personal target from the fanout. Combined with the
|
|
447
|
+
* `HQ_SYNC_SKIP_PERSONAL` env var by `resolveSkipPersonal()` — either
|
|
448
|
+
* truthy disables personal sync for this run. No-op outside `--companies`
|
|
449
|
+
* mode (single-company runs never visit the personal target).
|
|
450
|
+
*/
|
|
451
|
+
skipPersonal: boolean;
|
|
377
452
|
}
|
|
378
453
|
|
|
379
454
|
function parseArgs(argv: string[]): ParsedArgs | { error: string } {
|
|
@@ -384,6 +459,7 @@ function parseArgs(argv: string[]): ParsedArgs | { error: string } {
|
|
|
384
459
|
let direction: Direction = "pull";
|
|
385
460
|
let watch = false;
|
|
386
461
|
let pollRemoteMs: number | undefined;
|
|
462
|
+
let skipPersonal = false;
|
|
387
463
|
|
|
388
464
|
for (let i = 0; i < argv.length; i++) {
|
|
389
465
|
const arg = argv[i];
|
|
@@ -437,6 +513,12 @@ function parseArgs(argv: string[]): ParsedArgs | { error: string } {
|
|
|
437
513
|
case "--json":
|
|
438
514
|
// Accepted but ignored — ndjson is the only output mode.
|
|
439
515
|
break;
|
|
516
|
+
case "--skip-personal":
|
|
517
|
+
// Drop the personal target from the fanout. No-op outside
|
|
518
|
+
// --companies mode. Combined with HQ_SYNC_SKIP_PERSONAL env via
|
|
519
|
+
// resolveSkipPersonal().
|
|
520
|
+
skipPersonal = true;
|
|
521
|
+
break;
|
|
440
522
|
default:
|
|
441
523
|
return { error: `Unknown argument: ${arg}` };
|
|
442
524
|
}
|
|
@@ -452,7 +534,7 @@ function parseArgs(argv: string[]): ParsedArgs | { error: string } {
|
|
|
452
534
|
return { error: "--poll-remote-ms requires --watch" };
|
|
453
535
|
}
|
|
454
536
|
|
|
455
|
-
return { companies, company, onConflict, hqRoot, direction, watch, pollRemoteMs };
|
|
537
|
+
return { companies, company, onConflict, hqRoot, direction, watch, pollRemoteMs, skipPersonal };
|
|
456
538
|
}
|
|
457
539
|
|
|
458
540
|
// ---------------------------------------------------------------------------
|
|
@@ -670,7 +752,15 @@ export async function runRunner(
|
|
|
670
752
|
plan.push({ uid: m.companyUid, slug, ...(name ? { name } : {}) });
|
|
671
753
|
}
|
|
672
754
|
|
|
673
|
-
if (parsed.companies) {
|
|
755
|
+
if (parsed.companies && !resolveSkipPersonal(parsed.skipPersonal)) {
|
|
756
|
+
// Personal-target fanout slot. Skipped entirely when --skip-personal
|
|
757
|
+
// (or HQ_SYNC_SKIP_PERSONAL=1) is set — see resolveSkipPersonal doc for
|
|
758
|
+
// the rationale (menubar opt-out for users who only want company sync).
|
|
759
|
+
// When skipped, the fanout-plan event below carries only company
|
|
760
|
+
// memberships and no "personal" slug; downstream consumers (menubar
|
|
761
|
+
// workspaces row, status surfaces) should already tolerate that
|
|
762
|
+
// shape since pre-5.25 fanout often had it (a user with no person
|
|
763
|
+
// entity yet, or before the canonical-person-entity machinery landed).
|
|
674
764
|
const persons = await client.entity.listByType("person");
|
|
675
765
|
const pick = pickCanonicalPersonEntity(persons);
|
|
676
766
|
if (pick?.bucketName) {
|
|
@@ -799,6 +889,9 @@ export async function runRunner(
|
|
|
799
889
|
bytesUploaded: 0,
|
|
800
890
|
filesSkipped: 0,
|
|
801
891
|
filesDeleted: 0,
|
|
892
|
+
filesTombstoned: 0,
|
|
893
|
+
filesRefusedStale: 0,
|
|
894
|
+
filesExcludedByPolicy: 0,
|
|
802
895
|
conflictPaths: [],
|
|
803
896
|
aborted: false,
|
|
804
897
|
};
|
|
@@ -834,7 +927,16 @@ export async function runRunner(
|
|
|
834
927
|
// — DeleteObject writes a delete-marker, prior versions remain
|
|
835
928
|
// recoverable). Without this, a deleted file resurfaces on the
|
|
836
929
|
// next pull because the remote object is still listable.
|
|
930
|
+
//
|
|
931
|
+
// Policy default in 5.24 is `owned-only` (pre-5.24 behavior;
|
|
932
|
+
// preserved for the soak window). `HQ_SYNC_DELETE_POLICY` env
|
|
933
|
+
// can opt INTO the safer `currency-gated` (per-file HEAD + ETag
|
|
934
|
+
// verification) or the unsafe `all` (emergency reconcile only).
|
|
935
|
+
// Default flips to `currency-gated` in 5.25 after at least one
|
|
936
|
+
// machine has soaked the new path. Both personal and company
|
|
937
|
+
// targets use the same resolver — same engine, same flip.
|
|
837
938
|
propagateDeletes: true,
|
|
939
|
+
propagateDeletePolicy: resolveDeletePolicy(),
|
|
838
940
|
onEvent: tagAndEmit,
|
|
839
941
|
...(uploadAuthor ? { author: uploadAuthor } : {}),
|
|
840
942
|
// Mirror the pull-side seam: only spread these for the personal
|
|
@@ -891,6 +993,17 @@ export async function runRunner(
|
|
|
891
993
|
filesUploaded: pushResult.filesUploaded,
|
|
892
994
|
bytesUploaded: pushResult.bytesUploaded,
|
|
893
995
|
filesSkipped: pullResult.filesSkipped + pushResult.filesSkipped,
|
|
996
|
+
// Push-side counters surfaced on `complete` so the menubar's
|
|
997
|
+
// `SyncCompleteEvent` (which carries them as Option<u32> for
|
|
998
|
+
// back-compat with pre-5.25 engines) can render the new totals.
|
|
999
|
+
// Always emitted as numbers (0 when no push leg ran) so Rust's
|
|
1000
|
+
// serde decodes them as `Some(0)` rather than `None` — distinct
|
|
1001
|
+
// from the legacy-engine `None` and useful when the UI wants to
|
|
1002
|
+
// distinguish "engine ran, nothing tombstoned" from "engine
|
|
1003
|
+
// didn't report".
|
|
1004
|
+
filesTombstoned: pushResult.filesTombstoned,
|
|
1005
|
+
filesRefusedStale: pushResult.filesRefusedStale,
|
|
1006
|
+
filesExcludedByPolicy: pushResult.filesExcludedByPolicy,
|
|
894
1007
|
// Sourced from the merged path list so push-side conflicts are
|
|
895
1008
|
// counted too — `ShareResult` doesn't expose a numeric counter,
|
|
896
1009
|
// and using `pullResult.conflicts` alone silently dropped any
|
|
@@ -928,6 +1041,13 @@ export async function runRunner(
|
|
|
928
1041
|
filesUploaded: state.filesUploaded,
|
|
929
1042
|
bytesUploaded: state.bytesUploaded,
|
|
930
1043
|
filesSkipped: 0,
|
|
1044
|
+
// Mid-flight throw: we have no clean ShareResult to read these
|
|
1045
|
+
// from. Report 0 so the event shape stays stable; the partial
|
|
1046
|
+
// counts above already reflect what actually moved before the
|
|
1047
|
+
// throw.
|
|
1048
|
+
filesTombstoned: 0,
|
|
1049
|
+
filesRefusedStale: 0,
|
|
1050
|
+
filesExcludedByPolicy: 0,
|
|
931
1051
|
conflicts: 0,
|
|
932
1052
|
conflictPaths: [],
|
|
933
1053
|
aborted: true,
|