@indigoai-us/hq-cloud 6.13.5 → 6.14.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-company.d.ts +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +41 -2
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +9 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +75 -50
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +3 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +52 -1
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +10 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +597 -45
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +61 -2
- package/src/bin/sync-runner-watch-loop.ts +105 -54
- package/src/bin/sync-runner-watch-routes.ts +57 -1
- package/src/bin/sync-runner.test.ts +697 -50
- package/src/bin/sync-runner.ts +11 -0
package/package.json
CHANGED
|
@@ -6,6 +6,13 @@ import type { ConflictStrategy } from "../cli/conflict.js";
|
|
|
6
6
|
import type { UploadAuthor } from "../s3.js";
|
|
7
7
|
import type { VaultServiceConfig } from "../index.js";
|
|
8
8
|
import { resolvePullScope, type PullScope } from "../sync/pull-scope.js";
|
|
9
|
+
import {
|
|
10
|
+
coalescePrefixes,
|
|
11
|
+
pathToScopePrefix,
|
|
12
|
+
toScopePrefixEntries,
|
|
13
|
+
type ScopePrefixEntry,
|
|
14
|
+
type ScopePrefixInput,
|
|
15
|
+
} from "../prefix-coalesce.js";
|
|
9
16
|
import { describeError } from "../lib/describe-error.js";
|
|
10
17
|
import type {
|
|
11
18
|
DeletePropagationPolicy,
|
|
@@ -42,6 +49,7 @@ export async function executeCompanyFanout(options: {
|
|
|
42
49
|
shareFn: (options: ShareOptions) => Promise<ShareResult>;
|
|
43
50
|
resolveDeletePolicy: () => DeletePropagationPolicy;
|
|
44
51
|
emit: (event: RunnerEvent) => void;
|
|
52
|
+
scopePaths?: string[];
|
|
45
53
|
}): Promise<CompanyFanoutResult> {
|
|
46
54
|
const doPush =
|
|
47
55
|
options.direction === "push" || options.direction === "both";
|
|
@@ -196,6 +204,10 @@ export async function executeCompanyFanout(options: {
|
|
|
196
204
|
|
|
197
205
|
if (doPush) {
|
|
198
206
|
activePhase = "push";
|
|
207
|
+
const pushPrefixSet = effectivePushPrefixSet(
|
|
208
|
+
scope.prefixSet,
|
|
209
|
+
options.scopePaths ?? [],
|
|
210
|
+
);
|
|
199
211
|
const pushPaths =
|
|
200
212
|
target.personalMode === true
|
|
201
213
|
? computePersonalVaultPaths(options.hqRoot, { teamSyncedSlugs })
|
|
@@ -224,8 +236,8 @@ export async function executeCompanyFanout(options: {
|
|
|
224
236
|
...(decommissionPrefixes && decommissionPrefixes.length > 0
|
|
225
237
|
? { decommissionPrefixes }
|
|
226
238
|
: {}),
|
|
227
|
-
...(
|
|
228
|
-
? { prefixSet:
|
|
239
|
+
...(pushPrefixSet !== undefined
|
|
240
|
+
? { prefixSet: pushPrefixSet }
|
|
229
241
|
: {}),
|
|
230
242
|
});
|
|
231
243
|
}
|
|
@@ -364,6 +376,53 @@ export async function executeCompanyFanout(options: {
|
|
|
364
376
|
return { stateByCompany, errors, allConflicts };
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
function effectivePushPrefixSet(
|
|
380
|
+
aclPrefixSet: readonly ScopePrefixInput[] | undefined,
|
|
381
|
+
scopePaths: readonly string[],
|
|
382
|
+
): string[] | undefined {
|
|
383
|
+
if (scopePaths.length === 0) {
|
|
384
|
+
return aclPrefixSet === undefined
|
|
385
|
+
? undefined
|
|
386
|
+
: coalescePrefixes(aclPrefixSet);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const requested = scopePaths.flatMap((p) => [
|
|
390
|
+
pathToScopePrefix(p),
|
|
391
|
+
pathToScopePrefix(p.endsWith("/") ? p : p + "/"),
|
|
392
|
+
]);
|
|
393
|
+
if (aclPrefixSet === undefined) {
|
|
394
|
+
return coalescePrefixes(requested);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const aclEntries = toScopePrefixEntries(aclPrefixSet);
|
|
398
|
+
const requestedEntries = toScopePrefixEntries(requested);
|
|
399
|
+
const intersection: ScopePrefixEntry[] = [];
|
|
400
|
+
for (const requestedEntry of requestedEntries) {
|
|
401
|
+
for (const aclEntry of aclEntries) {
|
|
402
|
+
if (entryCoversEntry(aclEntry, requestedEntry)) {
|
|
403
|
+
intersection.push(requestedEntry);
|
|
404
|
+
} else if (entryCoversEntry(requestedEntry, aclEntry)) {
|
|
405
|
+
intersection.push(aclEntry);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return coalescePrefixes(intersection);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function entryCoversEntry(
|
|
413
|
+
entry: ScopePrefixEntry,
|
|
414
|
+
candidate: ScopePrefixEntry,
|
|
415
|
+
): boolean {
|
|
416
|
+
if (entry.prefix === "") return true;
|
|
417
|
+
if (entry.match === "prefix") {
|
|
418
|
+
return (
|
|
419
|
+
candidate.prefix === entry.prefix ||
|
|
420
|
+
candidate.prefix.startsWith(entry.prefix)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return candidate.match === "exact" && candidate.prefix === entry.prefix;
|
|
424
|
+
}
|
|
425
|
+
|
|
367
426
|
function normalizePullChangedPaths(
|
|
368
427
|
target: RunnerTarget,
|
|
369
428
|
changedPaths: string[] | undefined,
|
|
@@ -12,7 +12,6 @@ import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
|
12
12
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
13
13
|
import {
|
|
14
14
|
TreeWatcher,
|
|
15
|
-
WatchPushDriver,
|
|
16
15
|
systemClock,
|
|
17
16
|
type TreeChangeBatch,
|
|
18
17
|
} from "../watcher.js";
|
|
@@ -27,18 +26,28 @@ import {
|
|
|
27
26
|
type EventSyncHandles,
|
|
28
27
|
} from "../sync/event-sync.js";
|
|
29
28
|
import {
|
|
30
|
-
|
|
29
|
+
buildScopedDrainPullArgv,
|
|
30
|
+
buildScopedPushArgv,
|
|
31
31
|
buildTargetedPullArgv,
|
|
32
|
-
buildTargetedPushArgv,
|
|
33
32
|
routeChangeToTarget,
|
|
34
|
-
|
|
33
|
+
routeKey,
|
|
35
34
|
type WatchRoute,
|
|
36
35
|
} from "./sync-runner-watch-routes.js";
|
|
37
|
-
import type { RunnerLoopDeps, WatcherSurface } from "./sync-runner.js";
|
|
36
|
+
import type { Direction, RunnerLoopDeps, WatcherSurface } from "./sync-runner.js";
|
|
37
|
+
|
|
38
|
+
const FULL_RECONCILE_EVERY_TICKS = 10;
|
|
38
39
|
|
|
39
40
|
export interface ParsedLoopArgs {
|
|
40
41
|
hqRoot: string;
|
|
41
42
|
lockTimeoutSec?: number;
|
|
43
|
+
/**
|
|
44
|
+
* The run's resolved sync direction, straight from `parseArgs` (the single
|
|
45
|
+
* source of truth — same default as the parser). The drain reads this to
|
|
46
|
+
* decide whether scoped pushes and/or the pull leg run; it must NOT re-parse
|
|
47
|
+
* `--direction` from argv, which mis-defaults an omitted flag and disagrees
|
|
48
|
+
* with the parser on duplicate flags.
|
|
49
|
+
*/
|
|
50
|
+
direction?: Direction;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export interface WatchLoopRuntime {
|
|
@@ -92,9 +101,19 @@ export async function runWatchLoop(
|
|
|
92
101
|
if (a === "--watch") return false;
|
|
93
102
|
if (a === "--poll-remote-ms") return false;
|
|
94
103
|
if (a === "--event-push") return false;
|
|
104
|
+
if (a === "--scope-path") return false;
|
|
95
105
|
if (i > 0 && argv[i - 1] === "--poll-remote-ms") return false;
|
|
106
|
+
if (i > 0 && argv[i - 1] === "--scope-path") return false;
|
|
96
107
|
return true;
|
|
97
108
|
});
|
|
109
|
+
// Authoritative direction from parseArgs — never re-derive from argv (that
|
|
110
|
+
// mis-defaults an omitted --direction to "both" and picks the wrong
|
|
111
|
+
// occurrence on duplicates). Fall back to the parser's own default ("pull").
|
|
112
|
+
const originalDirection: Direction = parsed.direction ?? "pull";
|
|
113
|
+
const drainRunsPush =
|
|
114
|
+
originalDirection === "both" || originalDirection === "push";
|
|
115
|
+
const drainRunsPull =
|
|
116
|
+
originalDirection === "both" || originalDirection === "pull";
|
|
98
117
|
|
|
99
118
|
if (parsed.lockTimeoutSec === 0) {
|
|
100
119
|
try {
|
|
@@ -198,7 +217,6 @@ export async function runWatchLoop(
|
|
|
198
217
|
};
|
|
199
218
|
|
|
200
219
|
let watcher: WatcherSurface | null = null;
|
|
201
|
-
let driver: WatchPushDriver | null = null;
|
|
202
220
|
let detachSignal: (() => void) | null = null;
|
|
203
221
|
const pendingWatcherPaths = new Map<string, string>();
|
|
204
222
|
let pendingWatcherOriginalBatch: TreeChangeBatch | null = null;
|
|
@@ -237,8 +255,8 @@ export async function runWatchLoop(
|
|
|
237
255
|
pendingWatcherBareChange = true;
|
|
238
256
|
};
|
|
239
257
|
const takePendingWatcherChange = (): {
|
|
240
|
-
rel: string | null;
|
|
241
258
|
batch: TreeChangeBatch | null;
|
|
259
|
+
bare: boolean;
|
|
242
260
|
} => {
|
|
243
261
|
const batch =
|
|
244
262
|
pendingWatcherOriginalBatch !== null && !pendingWatcherOverflowed
|
|
@@ -255,15 +273,81 @@ export async function runWatchLoop(
|
|
|
255
273
|
: {}),
|
|
256
274
|
}
|
|
257
275
|
: null;
|
|
258
|
-
const
|
|
259
|
-
const fallbackRel = rel ?? (pendingWatcherBareChange ? null : null);
|
|
276
|
+
const bare = pendingWatcherBareChange;
|
|
260
277
|
pendingWatcherPaths.clear();
|
|
261
278
|
pendingWatcherOriginalBatch = null;
|
|
262
279
|
pendingWatcherBareChange = false;
|
|
263
280
|
pendingWatcherOverflowed = false;
|
|
264
281
|
pendingWatcherDroppedPaths = 0;
|
|
265
282
|
pendingWatcherDroppedBytes = 0;
|
|
266
|
-
return {
|
|
283
|
+
return { batch, bare };
|
|
284
|
+
};
|
|
285
|
+
const restorePendingWatcherPaths = (paths: Map<string, string>): void => {
|
|
286
|
+
pendingWatcherOriginalBatch = null;
|
|
287
|
+
for (const [absolutePath, relativePath] of paths.entries()) {
|
|
288
|
+
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const groupBatchByRoute = (
|
|
293
|
+
batch: TreeChangeBatch,
|
|
294
|
+
): Map<
|
|
295
|
+
string,
|
|
296
|
+
{ route: WatchRoute; paths: Map<string, string>; relPaths: string[] }
|
|
297
|
+
> => {
|
|
298
|
+
const grouped = new Map<
|
|
299
|
+
string,
|
|
300
|
+
{ route: WatchRoute; paths: Map<string, string>; relPaths: string[] }
|
|
301
|
+
>();
|
|
302
|
+
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
303
|
+
const route = routeChangeToTarget(relativePath);
|
|
304
|
+
if (!route) continue;
|
|
305
|
+
const key = routeKey(route);
|
|
306
|
+
const existing =
|
|
307
|
+
grouped.get(key) ??
|
|
308
|
+
{ route, paths: new Map<string, string>(), relPaths: [] };
|
|
309
|
+
existing.paths.set(absolutePath, relativePath);
|
|
310
|
+
existing.relPaths = [...new Set([...existing.paths.values()])];
|
|
311
|
+
grouped.set(key, existing);
|
|
312
|
+
}
|
|
313
|
+
return grouped;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const publishBatchIfAvailable = (paths: Map<string, string>): void => {
|
|
317
|
+
if (!stopped && eventSync && paths.size > 0) {
|
|
318
|
+
eventSync.publishBatch({ paths: new Map(paths) });
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const runScopedDrain = async (
|
|
323
|
+
batch: TreeChangeBatch | null,
|
|
324
|
+
): Promise<number> => {
|
|
325
|
+
if (drainRunsPush && batch && batch.paths.size > 0) {
|
|
326
|
+
const grouped = groupBatchByRoute(batch);
|
|
327
|
+
for (const group of grouped.values()) {
|
|
328
|
+
const scopedArgv = buildScopedPushArgv(
|
|
329
|
+
group.route,
|
|
330
|
+
group.relPaths,
|
|
331
|
+
passArgv,
|
|
332
|
+
);
|
|
333
|
+
try {
|
|
334
|
+
const result = await runGuarded(scopedArgv);
|
|
335
|
+
if (result === 0) {
|
|
336
|
+
publishBatchIfAvailable(group.paths);
|
|
337
|
+
} else {
|
|
338
|
+
restorePendingWatcherPaths(group.paths);
|
|
339
|
+
}
|
|
340
|
+
} catch (err) {
|
|
341
|
+
restorePendingWatcherPaths(group.paths);
|
|
342
|
+
process.stderr.write(
|
|
343
|
+
`watch scoped push failed, will retry next tick: ${describeError(err)}\n`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!drainRunsPull) return 0;
|
|
350
|
+
return runGuarded(buildScopedDrainPullArgv(passArgv));
|
|
267
351
|
};
|
|
268
352
|
|
|
269
353
|
let receiver: PushReceiver | null = null;
|
|
@@ -283,47 +367,9 @@ export async function runWatchLoop(
|
|
|
283
367
|
}));
|
|
284
368
|
watcher = createWatcher({ hqRoot, debounceMs, clock });
|
|
285
369
|
|
|
286
|
-
driver = new WatchPushDriver({
|
|
287
|
-
debounceMs: 0,
|
|
288
|
-
clock,
|
|
289
|
-
push: async () => {
|
|
290
|
-
if (stopped) return;
|
|
291
|
-
const { rel, batch: batchForPublish } = takePendingWatcherChange();
|
|
292
|
-
const batchRoutes = batchForPublish
|
|
293
|
-
? routesForBatch(batchForPublish)
|
|
294
|
-
: new Map<string, WatchRoute>();
|
|
295
|
-
let targetedArgv: string[];
|
|
296
|
-
if (batchForPublish?.overflowed || batchRoutes.size > 1) {
|
|
297
|
-
targetedArgv = buildFullFanoutPushArgv(passArgv);
|
|
298
|
-
} else {
|
|
299
|
-
const route =
|
|
300
|
-
batchRoutes.size === 1
|
|
301
|
-
? [...batchRoutes.values()][0]
|
|
302
|
-
: rel
|
|
303
|
-
? routeChangeToTarget(rel)
|
|
304
|
-
: { kind: "personal" as const };
|
|
305
|
-
if (!route) return;
|
|
306
|
-
targetedArgv = buildTargetedPushArgv(route, passArgv);
|
|
307
|
-
}
|
|
308
|
-
const result = await runGuarded(targetedArgv);
|
|
309
|
-
if (
|
|
310
|
-
result === 0 &&
|
|
311
|
-
!stopped &&
|
|
312
|
-
eventSync &&
|
|
313
|
-
!batchForPublish?.overflowed
|
|
314
|
-
) {
|
|
315
|
-
const batch: TreeChangeBatch | null =
|
|
316
|
-
batchForPublish ??
|
|
317
|
-
(rel ? { paths: new Map([[path.join(hqRoot, rel), rel]]) } : null);
|
|
318
|
-
if (batch) eventSync.publishBatch(batch);
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
|
|
323
370
|
watcher.onChange((changedRelPath, batch) => {
|
|
324
371
|
if (stopped) return;
|
|
325
372
|
addPendingWatcherChange(changedRelPath, batch);
|
|
326
|
-
driver?.notifyChange();
|
|
327
373
|
});
|
|
328
374
|
watcher.start();
|
|
329
375
|
|
|
@@ -403,11 +449,6 @@ export async function runWatchLoop(
|
|
|
403
449
|
/* ignore */
|
|
404
450
|
}
|
|
405
451
|
eventSync = null;
|
|
406
|
-
try {
|
|
407
|
-
driver?.dispose();
|
|
408
|
-
} catch {
|
|
409
|
-
/* ignore */
|
|
410
|
-
}
|
|
411
452
|
try {
|
|
412
453
|
watcher?.dispose();
|
|
413
454
|
} catch {
|
|
@@ -428,9 +469,19 @@ export async function runWatchLoop(
|
|
|
428
469
|
});
|
|
429
470
|
detachSignal = onShutdownSignal(shutdown);
|
|
430
471
|
|
|
472
|
+
let pollTick = 0;
|
|
431
473
|
try {
|
|
432
474
|
while (!stopped) {
|
|
433
|
-
|
|
475
|
+
pollTick++;
|
|
476
|
+
const pending = takePendingWatcherChange();
|
|
477
|
+
const shouldRunFull =
|
|
478
|
+
pollTick === 1 ||
|
|
479
|
+
pending.bare ||
|
|
480
|
+
pending.batch?.overflowed === true ||
|
|
481
|
+
pollTick % FULL_RECONCILE_EVERY_TICKS === 0;
|
|
482
|
+
const result = shouldRunFull
|
|
483
|
+
? await runGuarded(passArgv)
|
|
484
|
+
: await runScopedDrain(pending.batch);
|
|
434
485
|
if (result === TRANSIENT_NETWORK_EXIT) {
|
|
435
486
|
// The machine was briefly offline (the discovery fetch failed after
|
|
436
487
|
// retries). That is NOT a crash — stay alive and retry on the next
|
|
@@ -37,10 +37,48 @@ export function buildTargetedPushArgv(
|
|
|
37
37
|
return ["--companies", "--direction", "push", ...carried];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function buildScopedPushArgv(
|
|
41
|
+
route: WatchRoute,
|
|
42
|
+
relPaths: readonly string[],
|
|
43
|
+
baseArgv: string[],
|
|
44
|
+
): string[] {
|
|
45
|
+
const scopeArgs = scopedPathArgs(route, relPaths);
|
|
46
|
+
const carried = carriedFlags(baseArgv);
|
|
47
|
+
if (route.kind === "company") {
|
|
48
|
+
return [
|
|
49
|
+
"--company",
|
|
50
|
+
route.slug,
|
|
51
|
+
"--direction",
|
|
52
|
+
"push",
|
|
53
|
+
...scopeArgs,
|
|
54
|
+
...carried,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
return ["--personal", "--direction", "push", ...scopeArgs, ...carried];
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
export function buildFullFanoutPushArgv(baseArgv: string[]): string[] {
|
|
41
61
|
return ["--companies", "--direction", "push", ...carriedFlags(baseArgv)];
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
export function buildScopedDrainPullArgv(baseArgv: string[]): string[] {
|
|
65
|
+
const carried = carriedFlags(baseArgv);
|
|
66
|
+
if (baseArgv.includes("--personal")) {
|
|
67
|
+
return ["--personal", "--direction", "pull", ...carried];
|
|
68
|
+
}
|
|
69
|
+
const companyIdx = baseArgv.indexOf("--company");
|
|
70
|
+
if (companyIdx >= 0 && baseArgv[companyIdx + 1] !== undefined) {
|
|
71
|
+
return [
|
|
72
|
+
"--company",
|
|
73
|
+
baseArgv[companyIdx + 1],
|
|
74
|
+
"--direction",
|
|
75
|
+
"pull",
|
|
76
|
+
...carried,
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
return ["--companies", "--direction", "pull", ...carried];
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
/**
|
|
45
83
|
* Build the argv for a targeted pull pass from a routed change.
|
|
46
84
|
*/
|
|
@@ -55,7 +93,7 @@ export function buildTargetedPullArgv(
|
|
|
55
93
|
return ["--companies", "--direction", "pull", ...carried];
|
|
56
94
|
}
|
|
57
95
|
|
|
58
|
-
function routeKey(route: WatchRoute): string {
|
|
96
|
+
export function routeKey(route: WatchRoute): string {
|
|
59
97
|
return route.kind === "company" ? `company:${route.slug}` : "personal";
|
|
60
98
|
}
|
|
61
99
|
|
|
@@ -69,6 +107,24 @@ export function routesForBatch(batch: TreeChangeBatch): Map<string, WatchRoute>
|
|
|
69
107
|
return routes;
|
|
70
108
|
}
|
|
71
109
|
|
|
110
|
+
function scopedPathArgs(route: WatchRoute, relPaths: readonly string[]): string[] {
|
|
111
|
+
const args: string[] = [];
|
|
112
|
+
for (const relPath of relPaths) {
|
|
113
|
+
const scopePath = scopePathForRoute(route, relPath);
|
|
114
|
+
if (scopePath === null) continue;
|
|
115
|
+
args.push("--scope-path", scopePath);
|
|
116
|
+
}
|
|
117
|
+
return args;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scopePathForRoute(route: WatchRoute, relPath: string): string | null {
|
|
121
|
+
const norm = relPath.split(path.sep).join("/").replace(/^\.\//, "");
|
|
122
|
+
if (route.kind === "personal") return norm;
|
|
123
|
+
const prefix = `companies/${route.slug}/`;
|
|
124
|
+
if (norm.startsWith(prefix)) return norm.slice(prefix.length);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
/**
|
|
73
129
|
* Extract the `--hq-root` / `--on-conflict` pair-flags from a base argv so a
|
|
74
130
|
* re-targeted pass inherits the same root and conflict policy.
|