@indigoai-us/hq-cloud 6.11.12 → 6.11.13

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.
Files changed (107) hide show
  1. package/dist/bin/sync-runner-company.d.ts +35 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -0
  3. package/dist/bin/sync-runner-company.js +290 -0
  4. package/dist/bin/sync-runner-company.js.map +1 -0
  5. package/dist/bin/sync-runner-events.d.ts +12 -0
  6. package/dist/bin/sync-runner-events.d.ts.map +1 -0
  7. package/dist/bin/sync-runner-events.js +12 -0
  8. package/dist/bin/sync-runner-events.js.map +1 -0
  9. package/dist/bin/sync-runner-planning.d.ts +53 -0
  10. package/dist/bin/sync-runner-planning.d.ts.map +1 -0
  11. package/dist/bin/sync-runner-planning.js +59 -0
  12. package/dist/bin/sync-runner-planning.js.map +1 -0
  13. package/dist/bin/sync-runner-rollup.d.ts +24 -0
  14. package/dist/bin/sync-runner-rollup.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-rollup.js +46 -0
  16. package/dist/bin/sync-runner-rollup.js.map +1 -0
  17. package/dist/bin/sync-runner-telemetry.d.ts +5 -0
  18. package/dist/bin/sync-runner-telemetry.d.ts.map +1 -0
  19. package/dist/bin/sync-runner-telemetry.js +5 -0
  20. package/dist/bin/sync-runner-telemetry.js.map +1 -0
  21. package/dist/bin/sync-runner-watch-loop.d.ts +17 -0
  22. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -0
  23. package/dist/bin/sync-runner-watch-loop.js +372 -0
  24. package/dist/bin/sync-runner-watch-loop.js.map +1 -0
  25. package/dist/bin/sync-runner-watch-routes.d.ts +25 -0
  26. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -0
  27. package/dist/bin/sync-runner-watch-routes.js +74 -0
  28. package/dist/bin/sync-runner-watch-routes.js.map +1 -0
  29. package/dist/bin/sync-runner.d.ts +3 -54
  30. package/dist/bin/sync-runner.d.ts.map +1 -1
  31. package/dist/bin/sync-runner.js +73 -1154
  32. package/dist/bin/sync-runner.js.map +1 -1
  33. package/dist/cli/reindex.d.ts.map +1 -1
  34. package/dist/cli/reindex.js +34 -17
  35. package/dist/cli/reindex.js.map +1 -1
  36. package/dist/cli/reindex.test.js +39 -5
  37. package/dist/cli/reindex.test.js.map +1 -1
  38. package/dist/cli/rescue-classify-ordering.test.js +17 -0
  39. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  40. package/dist/cli/rescue-core.d.ts +45 -0
  41. package/dist/cli/rescue-core.d.ts.map +1 -1
  42. package/dist/cli/rescue-core.js +197 -170
  43. package/dist/cli/rescue-core.js.map +1 -1
  44. package/dist/cli/share.d.ts.map +1 -1
  45. package/dist/cli/share.js +224 -676
  46. package/dist/cli/share.js.map +1 -1
  47. package/dist/cli/sync.d.ts.map +1 -1
  48. package/dist/cli/sync.js +399 -726
  49. package/dist/cli/sync.js.map +1 -1
  50. package/dist/cli/sync.test.js +20 -0
  51. package/dist/cli/sync.test.js.map +1 -1
  52. package/dist/daemon-worker.d.ts +2 -2
  53. package/dist/daemon-worker.js +3 -3
  54. package/dist/daemon-worker.js.map +1 -1
  55. package/dist/object-io.js +1 -1
  56. package/dist/object-io.js.map +1 -1
  57. package/dist/remote-pull.d.ts +2 -2
  58. package/dist/remote-pull.d.ts.map +1 -1
  59. package/dist/remote-pull.js +23 -3
  60. package/dist/remote-pull.js.map +1 -1
  61. package/dist/remote-pull.test.js +24 -2
  62. package/dist/remote-pull.test.js.map +1 -1
  63. package/dist/sync/push-receiver.d.ts +6 -0
  64. package/dist/sync/push-receiver.d.ts.map +1 -1
  65. package/dist/sync/push-receiver.js +32 -2
  66. package/dist/sync/push-receiver.js.map +1 -1
  67. package/dist/sync/push-receiver.test.js +31 -0
  68. package/dist/sync/push-receiver.test.js.map +1 -1
  69. package/dist/sync-core.d.ts +27 -0
  70. package/dist/sync-core.d.ts.map +1 -0
  71. package/dist/sync-core.js +54 -0
  72. package/dist/sync-core.js.map +1 -0
  73. package/dist/vault-client.d.ts.map +1 -1
  74. package/dist/vault-client.js +284 -36
  75. package/dist/vault-client.js.map +1 -1
  76. package/dist/vault-client.test.js +59 -0
  77. package/dist/vault-client.test.js.map +1 -1
  78. package/dist/watcher.d.ts +2 -20
  79. package/dist/watcher.d.ts.map +1 -1
  80. package/dist/watcher.js +3 -113
  81. package/dist/watcher.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/bin/sync-runner-company.ts +350 -0
  84. package/src/bin/sync-runner-events.ts +25 -0
  85. package/src/bin/sync-runner-planning.ts +121 -0
  86. package/src/bin/sync-runner-rollup.ts +72 -0
  87. package/src/bin/sync-runner-telemetry.ts +8 -0
  88. package/src/bin/sync-runner-watch-loop.ts +443 -0
  89. package/src/bin/sync-runner-watch-routes.ts +86 -0
  90. package/src/bin/sync-runner.ts +96 -1253
  91. package/src/cli/reindex.test.ts +41 -3
  92. package/src/cli/reindex.ts +35 -19
  93. package/src/cli/rescue-classify-ordering.test.ts +20 -0
  94. package/src/cli/rescue-core.ts +252 -176
  95. package/src/cli/share.ts +363 -705
  96. package/src/cli/sync.test.ts +25 -0
  97. package/src/cli/sync.ts +612 -802
  98. package/src/daemon-worker.ts +3 -3
  99. package/src/object-io.ts +1 -1
  100. package/src/remote-pull.test.ts +30 -1
  101. package/src/remote-pull.ts +29 -4
  102. package/src/sync/push-receiver.test.ts +35 -0
  103. package/src/sync/push-receiver.ts +41 -2
  104. package/src/sync-core.ts +58 -0
  105. package/src/vault-client.test.ts +74 -0
  106. package/src/vault-client.ts +395 -43
  107. package/src/watcher.ts +6 -141
@@ -0,0 +1,443 @@
1
+ import * as path from "path";
2
+ import { VaultClient } from "../index.js";
3
+ import { pickCanonicalPersonEntity } from "../vault-client.js";
4
+ import {
5
+ acquireOperationLock,
6
+ withOperationLock,
7
+ OperationLockedError,
8
+ OPERATION_LOCKED_EXIT,
9
+ } from "../operation-lock.js";
10
+ import { describeError } from "../lib/describe-error.js";
11
+ import { getOrCreateMachineId } from "../lib/machine-id.js";
12
+ import {
13
+ TreeWatcher,
14
+ WatchPushDriver,
15
+ systemClock,
16
+ type TreeChangeBatch,
17
+ } from "../watcher.js";
18
+ import {
19
+ NoopPushReceiver,
20
+ type PushReceiver,
21
+ type SyncEngineFn,
22
+ } from "../sync/push-receiver.js";
23
+ import {
24
+ resolveEventSync,
25
+ startEventSync as defaultStartEventSync,
26
+ type EventSyncHandles,
27
+ } from "../sync/event-sync.js";
28
+ import {
29
+ buildFullFanoutPushArgv,
30
+ buildTargetedPullArgv,
31
+ buildTargetedPushArgv,
32
+ routeChangeToTarget,
33
+ routesForBatch,
34
+ type WatchRoute,
35
+ } from "./sync-runner-watch-routes.js";
36
+ import type { RunnerLoopDeps, WatcherSurface } from "./sync-runner.js";
37
+
38
+ export interface ParsedLoopArgs {
39
+ hqRoot: string;
40
+ lockTimeoutSec?: number;
41
+ }
42
+
43
+ export interface WatchLoopRuntime {
44
+ runPassWithOperationLockAlreadyHeld: (passArgv: string[]) => Promise<number>;
45
+ defaultGetIdTokenClaims: () => { email?: string } | null;
46
+ defaultGetAccessToken: () => Promise<string>;
47
+ apiUrl: string;
48
+ region: string;
49
+ }
50
+
51
+ export async function runOneShotWithOperationLock(
52
+ argv: string[],
53
+ parsed: ParsedLoopArgs,
54
+ deps: RunnerLoopDeps,
55
+ runtime: WatchLoopRuntime,
56
+ ): Promise<number> {
57
+ const runOnce =
58
+ deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
59
+ try {
60
+ return await withOperationLock(parsed.hqRoot, "sync", () => runOnce(argv), {
61
+ timeoutSec: parsed.lockTimeoutSec,
62
+ });
63
+ } catch (err) {
64
+ if (err instanceof OperationLockedError) {
65
+ process.stderr.write(err.message + "\n");
66
+ return OPERATION_LOCKED_EXIT;
67
+ }
68
+ throw err;
69
+ }
70
+ }
71
+
72
+ export async function runWatchLoop(
73
+ argv: string[],
74
+ parsed: ParsedLoopArgs,
75
+ deps: RunnerLoopDeps,
76
+ runtime: WatchLoopRuntime,
77
+ ): Promise<number> {
78
+ const sleep =
79
+ deps.sleep ??
80
+ ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
81
+ const runPass =
82
+ deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
83
+ const pollIdx = argv.indexOf("--poll-remote-ms");
84
+ const pollMs =
85
+ pollIdx >= 0 && argv[pollIdx + 1] ? Number(argv[pollIdx + 1]) : 600_000;
86
+ const eventPush = argv.includes("--event-push");
87
+ const companiesMode = argv.includes("--companies");
88
+ const hqRoot = parsed.hqRoot;
89
+
90
+ const passArgv = argv.filter((a, i) => {
91
+ if (a === "--watch") return false;
92
+ if (a === "--poll-remote-ms") return false;
93
+ if (a === "--event-push") return false;
94
+ if (i > 0 && argv[i - 1] === "--poll-remote-ms") return false;
95
+ return true;
96
+ });
97
+
98
+ if (parsed.lockTimeoutSec === 0) {
99
+ try {
100
+ const handle = acquireOperationLock(hqRoot, "sync", {
101
+ timeoutSec: 0,
102
+ onWaitStart: () => undefined,
103
+ });
104
+ handle.release();
105
+ } catch (err) {
106
+ if (err instanceof OperationLockedError) {
107
+ process.stderr.write(err.message + "\n");
108
+ return OPERATION_LOCKED_EXIT;
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ const runPassWithLock = async (passArgvForRun: string[]): Promise<number> => {
115
+ try {
116
+ const handle = acquireOperationLock(hqRoot, "sync", {
117
+ timeoutSec: 0,
118
+ onWaitStart: () => undefined,
119
+ });
120
+ try {
121
+ return await runPass(passArgvForRun);
122
+ } finally {
123
+ handle.release();
124
+ }
125
+ } catch (err) {
126
+ if (!(err instanceof OperationLockedError)) throw err;
127
+ if (parsed.lockTimeoutSec === 0) {
128
+ process.stderr.write(err.message + "\n");
129
+ return OPERATION_LOCKED_EXIT;
130
+ }
131
+ }
132
+
133
+ try {
134
+ return await withOperationLock(
135
+ hqRoot,
136
+ "sync",
137
+ () => runPass(passArgvForRun),
138
+ { timeoutSec: parsed.lockTimeoutSec },
139
+ );
140
+ } catch (err) {
141
+ if (err instanceof OperationLockedError) {
142
+ process.stderr.write(err.message + "\n");
143
+ return OPERATION_LOCKED_EXIT;
144
+ }
145
+ throw err;
146
+ }
147
+ };
148
+
149
+ let stopped = false;
150
+ let activePass: Promise<number> | null = null;
151
+ type QueuedPass = {
152
+ argv: string[];
153
+ resolve: (code: number) => void;
154
+ reject: (err: unknown) => void;
155
+ };
156
+ const pendingPasses: QueuedPass[] = [];
157
+ const resolveStoppedQueue = (): void => {
158
+ while (pendingPasses.length > 0) {
159
+ pendingPasses.shift()?.resolve(0);
160
+ }
161
+ };
162
+ const drainQueuedPasses = (): void => {
163
+ if (activePass !== null) return;
164
+ if (stopped) {
165
+ resolveStoppedQueue();
166
+ return;
167
+ }
168
+ const next = pendingPasses.shift();
169
+ if (!next) return;
170
+ const current = startGuardedPass(next.argv);
171
+ void current.then(next.resolve, next.reject);
172
+ };
173
+ const startGuardedPass = (passArgvForRun: string[]): Promise<number> => {
174
+ const current = stopped
175
+ ? Promise.resolve(0)
176
+ : runPassWithLock(passArgvForRun);
177
+ activePass = current;
178
+ void current
179
+ .finally(() => {
180
+ if (activePass === current) {
181
+ activePass = null;
182
+ drainQueuedPasses();
183
+ }
184
+ })
185
+ .catch(() => undefined);
186
+ return current;
187
+ };
188
+ const runGuarded = (passArgvForRun: string[]): Promise<number> => {
189
+ if (activePass === null && pendingPasses.length === 0) {
190
+ return startGuardedPass(passArgvForRun);
191
+ }
192
+
193
+ return new Promise<number>((resolve, reject) => {
194
+ pendingPasses.push({ argv: passArgvForRun, resolve, reject });
195
+ drainQueuedPasses();
196
+ });
197
+ };
198
+
199
+ let watcher: WatcherSurface | null = null;
200
+ let driver: WatchPushDriver | null = null;
201
+ let detachSignal: (() => void) | null = null;
202
+ const pendingWatcherPaths = new Map<string, string>();
203
+ let pendingWatcherOriginalBatch: TreeChangeBatch | null = null;
204
+ let pendingWatcherBareChange = false;
205
+ let pendingWatcherOverflowed = false;
206
+ let pendingWatcherDroppedPaths = 0;
207
+ let pendingWatcherDroppedBytes = 0;
208
+ const addPendingWatcherChange = (
209
+ changedRelPath?: string,
210
+ batch?: TreeChangeBatch,
211
+ ): void => {
212
+ if (batch) {
213
+ pendingWatcherOriginalBatch =
214
+ pendingWatcherPaths.size === 0 &&
215
+ !pendingWatcherBareChange &&
216
+ !pendingWatcherOverflowed &&
217
+ !batch.overflowed
218
+ ? batch
219
+ : null;
220
+ for (const [absolutePath, relativePath] of batch.paths.entries()) {
221
+ pendingWatcherPaths.set(absolutePath, relativePath);
222
+ }
223
+ if (batch.overflowed) {
224
+ pendingWatcherOverflowed = true;
225
+ pendingWatcherDroppedPaths += batch.droppedPaths ?? 0;
226
+ pendingWatcherDroppedBytes += batch.droppedBytes ?? 0;
227
+ }
228
+ return;
229
+ }
230
+ if (changedRelPath) {
231
+ pendingWatcherOriginalBatch = null;
232
+ pendingWatcherPaths.set(path.join(hqRoot, changedRelPath), changedRelPath);
233
+ return;
234
+ }
235
+ pendingWatcherOriginalBatch = null;
236
+ pendingWatcherBareChange = true;
237
+ };
238
+ const takePendingWatcherChange = (): {
239
+ rel: string | null;
240
+ batch: TreeChangeBatch | null;
241
+ } => {
242
+ const batch =
243
+ pendingWatcherOriginalBatch !== null && !pendingWatcherOverflowed
244
+ ? pendingWatcherOriginalBatch
245
+ : pendingWatcherPaths.size > 0 || pendingWatcherOverflowed
246
+ ? {
247
+ paths: new Map(pendingWatcherPaths),
248
+ ...(pendingWatcherOverflowed
249
+ ? {
250
+ overflowed: true,
251
+ droppedPaths: pendingWatcherDroppedPaths,
252
+ droppedBytes: pendingWatcherDroppedBytes,
253
+ }
254
+ : {}),
255
+ }
256
+ : null;
257
+ const rel = [...pendingWatcherPaths.values()][0] ?? null;
258
+ const fallbackRel = rel ?? (pendingWatcherBareChange ? null : null);
259
+ pendingWatcherPaths.clear();
260
+ pendingWatcherOriginalBatch = null;
261
+ pendingWatcherBareChange = false;
262
+ pendingWatcherOverflowed = false;
263
+ pendingWatcherDroppedPaths = 0;
264
+ pendingWatcherDroppedBytes = 0;
265
+ return { rel: fallbackRel, batch };
266
+ };
267
+
268
+ let receiver: PushReceiver | null = null;
269
+ let eventSync: EventSyncHandles | null = null;
270
+
271
+ if (eventPush) {
272
+ const clock = deps.clock ?? systemClock;
273
+ const debounceMs = 2000;
274
+ const createWatcher =
275
+ deps.createWatcher ??
276
+ ((opts) =>
277
+ new TreeWatcher({
278
+ hqRoot: opts.hqRoot,
279
+ debounceMs: opts.debounceMs,
280
+ clock: opts.clock,
281
+ personalMode: !companiesMode,
282
+ }));
283
+ watcher = createWatcher({ hqRoot, debounceMs, clock });
284
+
285
+ driver = new WatchPushDriver({
286
+ debounceMs: 0,
287
+ clock,
288
+ push: async () => {
289
+ if (stopped) return;
290
+ const { rel, batch: batchForPublish } = takePendingWatcherChange();
291
+ const batchRoutes = batchForPublish
292
+ ? routesForBatch(batchForPublish)
293
+ : new Map<string, WatchRoute>();
294
+ let targetedArgv: string[];
295
+ if (batchForPublish?.overflowed || batchRoutes.size > 1) {
296
+ targetedArgv = buildFullFanoutPushArgv(passArgv);
297
+ } else {
298
+ const route =
299
+ batchRoutes.size === 1
300
+ ? [...batchRoutes.values()][0]
301
+ : rel
302
+ ? routeChangeToTarget(rel)
303
+ : { kind: "personal" as const };
304
+ if (!route) return;
305
+ targetedArgv = buildTargetedPushArgv(route, passArgv);
306
+ }
307
+ const result = await runGuarded(targetedArgv);
308
+ if (
309
+ result === 0 &&
310
+ !stopped &&
311
+ eventSync &&
312
+ !batchForPublish?.overflowed
313
+ ) {
314
+ const batch: TreeChangeBatch | null =
315
+ batchForPublish ??
316
+ (rel ? { paths: new Map([[path.join(hqRoot, rel), rel]]) } : null);
317
+ if (batch) eventSync.publishBatch(batch);
318
+ }
319
+ },
320
+ });
321
+
322
+ watcher.onChange((changedRelPath, batch) => {
323
+ if (stopped) return;
324
+ addPendingWatcherChange(changedRelPath, batch);
325
+ driver?.notifyChange();
326
+ });
327
+ watcher.start();
328
+
329
+ const receiverSyncFn: SyncEngineFn = async (ctx) => {
330
+ if (stopped) return;
331
+ const route = routeChangeToTarget(ctx.event.relativePath);
332
+ if (!route) return;
333
+ const targetedArgv = buildTargetedPullArgv(route, passArgv);
334
+ const result = await runGuarded(targetedArgv);
335
+ if (result !== 0) {
336
+ throw new Error(`targeted pull failed with exit code ${result}`);
337
+ }
338
+ };
339
+ const createReceiver = deps.createReceiver ?? (() => new NoopPushReceiver());
340
+ receiver = createReceiver({ syncFn: receiverSyncFn, hqRoot });
341
+ void Promise.resolve(receiver.start()).catch(() => undefined);
342
+
343
+ const getClaims =
344
+ deps.getIdTokenClaims ?? runtime.defaultGetIdTokenClaims;
345
+ const email = getClaims()?.email;
346
+ if (resolveEventSync(email, process.env.HQ_SYNC_EVENT_SYNC)) {
347
+ const getAccessToken =
348
+ deps.getAccessToken ?? runtime.defaultGetAccessToken;
349
+ const startES = deps.startEventSync ?? defaultStartEventSync;
350
+ void (async () => {
351
+ const handles = await startES({
352
+ hqRoot,
353
+ apiUrl: runtime.apiUrl,
354
+ authToken: getAccessToken,
355
+ deviceId: getOrCreateMachineId(hqRoot),
356
+ resolveTenantId: async () => {
357
+ const client = new VaultClient({
358
+ apiUrl: runtime.apiUrl,
359
+ authToken: getAccessToken,
360
+ region: runtime.region,
361
+ });
362
+ const persons = await client.entity.listByType("person");
363
+ const pick = pickCanonicalPersonEntity(persons);
364
+ if (!pick?.uid) {
365
+ throw new Error("no canonical person entity for this account");
366
+ }
367
+ return pick.uid;
368
+ },
369
+ syncFn: receiverSyncFn,
370
+ log: (m) => process.stderr.write(`${m}\n`),
371
+ });
372
+ if (!handles) return;
373
+ if (stopped) {
374
+ void handles.dispose();
375
+ return;
376
+ }
377
+ eventSync = handles;
378
+ })().catch((err) => {
379
+ process.stderr.write(
380
+ `event-sync: wiring failed, continuing poll-only: ${describeError(err)}\n`,
381
+ );
382
+ });
383
+ }
384
+ }
385
+
386
+ let resolveStopped: (() => void) | null = null;
387
+ const stoppedSignal = new Promise<void>((resolve) => {
388
+ resolveStopped = resolve;
389
+ });
390
+ const shutdown = (): void => {
391
+ if (stopped) return;
392
+ stopped = true;
393
+ resolveStoppedQueue();
394
+ try {
395
+ void receiver?.dispose();
396
+ } catch {
397
+ /* ignore */
398
+ }
399
+ try {
400
+ void eventSync?.dispose();
401
+ } catch {
402
+ /* ignore */
403
+ }
404
+ eventSync = null;
405
+ try {
406
+ driver?.dispose();
407
+ } catch {
408
+ /* ignore */
409
+ }
410
+ try {
411
+ watcher?.dispose();
412
+ } catch {
413
+ /* ignore */
414
+ }
415
+ resolveStopped?.();
416
+ };
417
+ const onShutdownSignal =
418
+ deps.onShutdownSignal ??
419
+ ((handler: () => void) => {
420
+ const wrapped = () => handler();
421
+ process.on("SIGTERM", wrapped);
422
+ process.on("SIGINT", wrapped);
423
+ return () => {
424
+ process.off("SIGTERM", wrapped);
425
+ process.off("SIGINT", wrapped);
426
+ };
427
+ });
428
+ detachSignal = onShutdownSignal(shutdown);
429
+
430
+ try {
431
+ while (!stopped) {
432
+ const result = await runGuarded(passArgv);
433
+ if (result !== 0) {
434
+ return result;
435
+ }
436
+ await Promise.race([sleep(pollMs), stoppedSignal]);
437
+ }
438
+ return 0;
439
+ } finally {
440
+ shutdown();
441
+ detachSignal?.();
442
+ }
443
+ }
@@ -0,0 +1,86 @@
1
+ import * as path from "path";
2
+ import type { TreeChangeBatch } from "../watcher.js";
3
+
4
+ export type WatchRoute =
5
+ | { kind: "company"; slug: string }
6
+ | { kind: "personal" };
7
+
8
+ /**
9
+ * Route a changed relative path to the push target that owns it.
10
+ *
11
+ * - `companies/<slug>/...` -> a single-company route for `<slug>`.
12
+ * - anything else under hqRoot -> the personal route.
13
+ */
14
+ export function routeChangeToTarget(relPath: string): WatchRoute | null {
15
+ const norm = relPath.split(path.sep).join("/").replace(/^\.\//, "");
16
+ if (norm === "" || norm.startsWith("..")) return null;
17
+ const segments = norm.split("/").filter((s) => s.length > 0);
18
+ if (segments.length === 0) return null;
19
+ if (segments[0] === "companies") {
20
+ if (segments.length < 2 || segments[1].length === 0) return null;
21
+ return { kind: "company", slug: segments[1] };
22
+ }
23
+ return { kind: "personal" };
24
+ }
25
+
26
+ /**
27
+ * Build the argv for a targeted push pass from a routed change.
28
+ */
29
+ export function buildTargetedPushArgv(
30
+ route: WatchRoute,
31
+ baseArgv: string[],
32
+ ): string[] {
33
+ const carried = carriedFlags(baseArgv);
34
+ if (route.kind === "company") {
35
+ return ["--company", route.slug, "--direction", "push", ...carried];
36
+ }
37
+ return ["--companies", "--direction", "push", ...carried];
38
+ }
39
+
40
+ export function buildFullFanoutPushArgv(baseArgv: string[]): string[] {
41
+ return ["--companies", "--direction", "push", ...carriedFlags(baseArgv)];
42
+ }
43
+
44
+ /**
45
+ * Build the argv for a targeted pull pass from a routed change.
46
+ */
47
+ export function buildTargetedPullArgv(
48
+ route: WatchRoute,
49
+ baseArgv: string[],
50
+ ): string[] {
51
+ const carried = carriedFlags(baseArgv);
52
+ if (route.kind === "company") {
53
+ return ["--company", route.slug, "--direction", "pull", ...carried];
54
+ }
55
+ return ["--companies", "--direction", "pull", ...carried];
56
+ }
57
+
58
+ function routeKey(route: WatchRoute): string {
59
+ return route.kind === "company" ? `company:${route.slug}` : "personal";
60
+ }
61
+
62
+ export function routesForBatch(batch: TreeChangeBatch): Map<string, WatchRoute> {
63
+ const routes = new Map<string, WatchRoute>();
64
+ for (const relPath of batch.paths.values()) {
65
+ const route = routeChangeToTarget(relPath);
66
+ if (!route) continue;
67
+ routes.set(routeKey(route), route);
68
+ }
69
+ return routes;
70
+ }
71
+
72
+ /**
73
+ * Extract the `--hq-root` / `--on-conflict` pair-flags from a base argv so a
74
+ * re-targeted pass inherits the same root and conflict policy.
75
+ */
76
+ function carriedFlags(baseArgv: string[]): string[] {
77
+ const carried: string[] = [];
78
+ for (let i = 0; i < baseArgv.length; i++) {
79
+ const a = baseArgv[i];
80
+ if (a === "--hq-root" || a === "--on-conflict") {
81
+ carried.push(a);
82
+ if (baseArgv[i + 1] !== undefined) carried.push(baseArgv[++i]);
83
+ }
84
+ }
85
+ return carried;
86
+ }