@indigoai-us/hq-cloud 6.18.87 → 6.18.89
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.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/reconcile-skip-reason.d.ts +23 -0
- package/dist/cli/reconcile-skip-reason.d.ts.map +1 -0
- package/dist/cli/reconcile-skip-reason.js +60 -0
- package/dist/cli/reconcile-skip-reason.js.map +1 -0
- package/dist/cli/rescue-core.d.ts +16 -1
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +83 -27
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +31 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +193 -28
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts +36 -2
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +135 -27
- package/dist/cli/sync.js.map +1 -1
- package/dist/local-link-warning.d.ts +48 -0
- package/dist/local-link-warning.d.ts.map +1 -0
- package/dist/local-link-warning.js +138 -0
- package/dist/local-link-warning.js.map +1 -0
- package/dist/object-io.d.ts +16 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +25 -2
- package/dist/object-io.js.map +1 -1
- package/dist/outposts/cli-billing.d.ts +2 -0
- package/dist/outposts/cli-billing.d.ts.map +1 -1
- package/dist/outposts/cli-billing.js +13 -1
- package/dist/outposts/cli-billing.js.map +1 -1
- package/dist/outposts/client.d.ts +37 -6
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +100 -18
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts +11 -5
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +945 -63
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/company-outposts.d.ts +92 -0
- package/dist/outposts/company-outposts.d.ts.map +1 -0
- package/dist/outposts/company-outposts.js +194 -0
- package/dist/outposts/company-outposts.js.map +1 -0
- package/dist/outposts/company.d.ts +67 -9
- package/dist/outposts/company.d.ts.map +1 -1
- package/dist/outposts/company.js +211 -11
- package/dist/outposts/company.js.map +1 -1
- package/dist/outposts/errors.d.ts +7 -0
- package/dist/outposts/errors.d.ts.map +1 -1
- package/dist/outposts/errors.js +7 -0
- package/dist/outposts/errors.js.map +1 -1
- package/dist/outposts/host-deps.d.ts +2 -0
- package/dist/outposts/host-deps.d.ts.map +1 -1
- package/dist/outposts/host-deps.js +12 -0
- package/dist/outposts/host-deps.js.map +1 -1
- package/dist/s3.d.ts +7 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +33 -12
- package/dist/s3.js.map +1 -1
- package/dist/sync-progress.d.ts.map +1 -1
- package/dist/sync-progress.js +3 -0
- package/dist/sync-progress.js.map +1 -1
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +5 -1
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Broadcasts local file(s) to the company's S3 vault bucket.
|
|
5
5
|
* Refuses to overwrite a newer remote version without prompting.
|
|
6
6
|
*/
|
|
7
|
+
import { localLinkWarning, rememberLinkWarning, brokenLinkSkipEvent, LinkWarningCollector, LOCAL_LINK_REMEDY } from "../local-link-warning.js";
|
|
7
8
|
import * as fs from "fs";
|
|
8
9
|
import * as os from "os";
|
|
9
10
|
import * as path from "path";
|
|
@@ -29,6 +30,7 @@ import { claimNeverSyncedLocalNotice, formatSecretsNotSyncedLine, isNeverSyncedV
|
|
|
29
30
|
import { getActiveFullPassDecodeWindow, getActiveFullPassUnitSize, } from "../sync/full-pass-caps.js";
|
|
30
31
|
import { isGeneratedCoreMirrorKey, isPersonalVaultDeletionExcluded, isSessionLogDeletionKey, purgeExcludedPersonalVaultJournalEntries, } from "../personal-vault.js";
|
|
31
32
|
import { pickWinner, resolveConflict, usesVersionAwareWinner } from "./conflict.js";
|
|
33
|
+
import { createHumanSkipLineLog, formatReconcileSkipLine, isUpToDateSkipReason, } from "./reconcile-skip-reason.js";
|
|
32
34
|
import { fetchFileTombstones, } from "./tombstones.js";
|
|
33
35
|
import { compileScopeMatcher, } from "../prefix-coalesce.js";
|
|
34
36
|
import { hasRemoteChanged, isAccessDenied, resolveActiveCompany, resolveTransferConcurrency, resolveUploadByteBudget, readHostLoad, } from "../sync-core.js";
|
|
@@ -490,6 +492,32 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, onto
|
|
|
490
492
|
const localHash = hashSymlinkTarget(entry.target);
|
|
491
493
|
const journalEntry = journalRows(journal).get(relativePath);
|
|
492
494
|
const portable = portablePushSymlinkTarget(absolutePath, relativePath, entry.target, journalEntry?.wireSymlinkTarget, symlinkContext);
|
|
495
|
+
// Skip-unchanged runs before the link check: a dangling link already
|
|
496
|
+
// synced under this exact target is settled state, not a new warning
|
|
497
|
+
// every pass. A refused target never reaches this gate, as before.
|
|
498
|
+
if (skipUnchanged && !portable.refused) {
|
|
499
|
+
const existing = skipFingerprint(journal, relativePath);
|
|
500
|
+
// `!localDiverges`: see the regular-file gate below — a divergent entry
|
|
501
|
+
// stores the current LOCAL hash, so hash equality here proves only that
|
|
502
|
+
// local has not moved since the pull-side hold, never that it matches remote.
|
|
503
|
+
if (existing && existing.hash === localHash && !existing.localDiverges) {
|
|
504
|
+
omittedSkips += 1;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const warning = localLinkWarning(symlinkContext?.syncRoot ?? path.dirname(absolutePath), absolutePath, relativePath);
|
|
509
|
+
// A journaled link whose target string changed is still an upload, even
|
|
510
|
+
// when the new target does not exist yet. Skipping that retarget drops
|
|
511
|
+
// the edit and warns on every later pass. A never-synced broken link, a
|
|
512
|
+
// loop, and an unreadable link stay skips.
|
|
513
|
+
const uploadMissingRetarget = warning?.reason === "missing_target" &&
|
|
514
|
+
!portable.refused &&
|
|
515
|
+
journalEntry?.hash !== undefined &&
|
|
516
|
+
journalEntry.hash !== localHash;
|
|
517
|
+
if (warning && !uploadMissingRetarget && (!portable.refused || warning.reason === "loop")) {
|
|
518
|
+
items.push({ action: "skip-local-link", absolutePath, relativePath, warning });
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
493
521
|
if (portable.refused) {
|
|
494
522
|
items.push({
|
|
495
523
|
action: "refuse-unsafe-symlink",
|
|
@@ -500,16 +528,6 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, onto
|
|
|
500
528
|
});
|
|
501
529
|
continue;
|
|
502
530
|
}
|
|
503
|
-
if (skipUnchanged) {
|
|
504
|
-
const existing = skipFingerprint(journal, relativePath);
|
|
505
|
-
// `!localDiverges`: see the regular-file gate below — a divergent entry
|
|
506
|
-
// stores the current LOCAL hash, so hash equality here proves only that
|
|
507
|
-
// local has not moved since the pull-side hold, never that it matches remote.
|
|
508
|
-
if (existing && existing.hash === localHash && !existing.localDiverges) {
|
|
509
|
-
omittedSkips += 1;
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
531
|
items.push({
|
|
514
532
|
action: "upload",
|
|
515
533
|
kind: "symlink",
|
|
@@ -1087,6 +1105,14 @@ export async function share(options) {
|
|
|
1087
1105
|
}
|
|
1088
1106
|
async function shareOnce(options) {
|
|
1089
1107
|
const run = await createPushRunContext(options);
|
|
1108
|
+
try {
|
|
1109
|
+
return await runShareOnce(options, run);
|
|
1110
|
+
}
|
|
1111
|
+
finally {
|
|
1112
|
+
run.humanSkipLines?.flush();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function runShareOnce(options, run) {
|
|
1090
1116
|
const counters = createShareCounters();
|
|
1091
1117
|
const filesRefusedStalePaths = [];
|
|
1092
1118
|
const conflictPaths = [];
|
|
@@ -1149,7 +1175,9 @@ async function shareOnce(options) {
|
|
|
1149
1175
|
// A reconcile cursor is an all-or-nothing boundary for this bounded apply
|
|
1150
1176
|
// slice. Upload and delete errors are intentionally reported per-path so an
|
|
1151
1177
|
// ordinary runner can continue, but none of those retryable outcomes may
|
|
1152
|
-
// move the cursor beyond the failed mutation.
|
|
1178
|
+
// move the cursor beyond the failed mutation. Broken links are deliberate
|
|
1179
|
+
// skips: their current slice is settled without claiming a file transfer,
|
|
1180
|
+
// and a repaired link remains eligible in a later discovery epoch.
|
|
1153
1181
|
if (!uploadResult.hadUploadFailure &&
|
|
1154
1182
|
uploadResult.workerErrors.length === 0 &&
|
|
1155
1183
|
!pathResults.some((result) => result.status === "refused")) {
|
|
@@ -1220,7 +1248,9 @@ async function createPushRunContext(options) {
|
|
|
1220
1248
|
// load average and self-trip the busy step-down to 32.
|
|
1221
1249
|
const hostLoadAtStart = readHostLoad();
|
|
1222
1250
|
const propagateDeletePolicy = options.propagateDeletePolicy ?? "owned-only";
|
|
1223
|
-
const
|
|
1251
|
+
const humanSkipLines = options.onEvent ? undefined : createHumanSkipLineLog();
|
|
1252
|
+
const baseEmit = options.onEvent
|
|
1253
|
+
?? ((event) => defaultConsoleLogger(event, humanSkipLines));
|
|
1224
1254
|
if (vaultConfig && entityContext) {
|
|
1225
1255
|
throw new Error("share() requires exactly one of `vaultConfig` or `entityContext`, not both. " +
|
|
1226
1256
|
"Pass `vaultConfig` to vend credentials internally, or `entityContext` to use pre-vended ones.");
|
|
@@ -1306,6 +1336,7 @@ async function createPushRunContext(options) {
|
|
|
1306
1336
|
: ctx.slug,
|
|
1307
1337
|
phase: "push",
|
|
1308
1338
|
});
|
|
1339
|
+
const linkWarnings = new LinkWarningCollector();
|
|
1309
1340
|
const emit = (event) => {
|
|
1310
1341
|
baseEmit(event);
|
|
1311
1342
|
recordProgress(event);
|
|
@@ -1429,6 +1460,8 @@ async function createPushRunContext(options) {
|
|
|
1429
1460
|
personalConnectorSettingsSync,
|
|
1430
1461
|
symlinkTargetRefusal,
|
|
1431
1462
|
reportedUnsafeSymlinkPaths: new Set(),
|
|
1463
|
+
linkWarnings,
|
|
1464
|
+
unreadableLinks: 0,
|
|
1432
1465
|
skillRegistrationFallbacks: 0,
|
|
1433
1466
|
paths,
|
|
1434
1467
|
message,
|
|
@@ -1440,6 +1473,7 @@ async function createPushRunContext(options) {
|
|
|
1440
1473
|
propagateDeletes,
|
|
1441
1474
|
propagateDeletePolicy,
|
|
1442
1475
|
emit,
|
|
1476
|
+
humanSkipLines,
|
|
1443
1477
|
companyRef,
|
|
1444
1478
|
ctx,
|
|
1445
1479
|
privateFolderMatcher,
|
|
@@ -1532,6 +1566,9 @@ function createShareCounters() {
|
|
|
1532
1566
|
filesUploaded: 0,
|
|
1533
1567
|
bytesUploaded: 0,
|
|
1534
1568
|
filesSkipped: 0,
|
|
1569
|
+
filesSkippedUpToDate: 0,
|
|
1570
|
+
filesSkippedConflict: 0,
|
|
1571
|
+
filesSkippedBrokenLink: 0,
|
|
1535
1572
|
filesDeleted: 0,
|
|
1536
1573
|
filesTombstoned: 0,
|
|
1537
1574
|
filesRefusedStale: 0,
|
|
@@ -2139,6 +2176,14 @@ async function buildSharePlans(run) {
|
|
|
2139
2176
|
run.unreachablePaths.set(namedPath, reason);
|
|
2140
2177
|
}
|
|
2141
2178
|
},
|
|
2179
|
+
onUnreadableLink: (key) => {
|
|
2180
|
+
const warning = { path: key, reason: "unreadable_link", remedy: LOCAL_LINK_REMEDY };
|
|
2181
|
+
if (run.linkWarnings.has(warning.path))
|
|
2182
|
+
return;
|
|
2183
|
+
run.unreadableLinks++;
|
|
2184
|
+
if (rememberLinkWarning(run.linkWarnings, warning))
|
|
2185
|
+
run.emit(brokenLinkSkipEvent(warning));
|
|
2186
|
+
},
|
|
2142
2187
|
onLinkedSubtree: (rel) => run.linkedSubtreeSet.add(rel),
|
|
2143
2188
|
onHeartbeat: run.options.onHeartbeat,
|
|
2144
2189
|
cooperativeCheckpoint: run.options.cooperativeCheckpoint,
|
|
@@ -2249,6 +2294,10 @@ async function buildSharePlans(run) {
|
|
|
2249
2294
|
const deletePlan = run.propagateDeletes === true
|
|
2250
2295
|
? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths, run.quarantinedPaths)
|
|
2251
2296
|
: { toDelete: [], toTombstone: [], refusedStale: [] };
|
|
2297
|
+
for (const warning of deletePlan.brokenLinkSkipped ?? []) {
|
|
2298
|
+
if (rememberLinkWarning(run.linkWarnings, warning))
|
|
2299
|
+
run.emit(brokenLinkSkipEvent(warning));
|
|
2300
|
+
}
|
|
2252
2301
|
const decommissionPlan = !run.scopedJournal && (run.options.decommissionPrefixes ?? []).length > 0
|
|
2253
2302
|
? computeDecommissionPlan(run.journal, run.options.decommissionPrefixes ?? [], run.propagateDeletePolicy, new Set([...deletePlan.toDelete.map((item) => item.key), ...deletePlan.toTombstone]), run.skipSecretPaths)
|
|
2254
2303
|
: [];
|
|
@@ -2456,6 +2505,8 @@ function debugPlanLimit(fields) {
|
|
|
2456
2505
|
}
|
|
2457
2506
|
async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
2458
2507
|
counters.filesSkipped += pushPlan.preplannedFilesSkipped;
|
|
2508
|
+
counters.filesSkippedUpToDate += pushPlan.preplannedFilesSkipped;
|
|
2509
|
+
run.shareCounters = counters;
|
|
2459
2510
|
const TRANSFER_CONCURRENCY = resolveTransferConcurrency(run.hostLoadAtStart, run.options.transferPriority ?? "chore");
|
|
2460
2511
|
let conflictPromptChain = Promise.resolve();
|
|
2461
2512
|
const resolveConflictSerialized = (info) => {
|
|
@@ -2645,12 +2696,27 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2645
2696
|
emitServerOwnedRefusal(run, item.relativePath);
|
|
2646
2697
|
}
|
|
2647
2698
|
}
|
|
2699
|
+
else if (item.action === "skip-local-link") {
|
|
2700
|
+
counters.filesSkippedBrokenLink++;
|
|
2701
|
+
if (rememberLinkWarning(run.linkWarnings, item.warning)) {
|
|
2702
|
+
run.emit(brokenLinkSkipEvent(item.warning));
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2648
2705
|
else if (item.action === "refuse-unsafe-symlink") {
|
|
2706
|
+
// One event per refused link: skip-unsafe-symlink-target carries the
|
|
2707
|
+
// remedy, so a broken outside-root link is recorded in linkWarnings but
|
|
2708
|
+
// never also emitted as skip-broken-link (which double-counted progress).
|
|
2709
|
+
const warning = localLinkWarning(run.syncRoot, item.absolutePath, item.relativePath);
|
|
2710
|
+
if (warning) {
|
|
2711
|
+
counters.filesSkippedBrokenLink++;
|
|
2712
|
+
rememberLinkWarning(run.linkWarnings, warning);
|
|
2713
|
+
}
|
|
2649
2714
|
run.emit({
|
|
2650
2715
|
type: "skip-unsafe-symlink-target",
|
|
2651
2716
|
path: item.relativePath,
|
|
2652
2717
|
classification: item.classification,
|
|
2653
2718
|
shape: item.shape,
|
|
2719
|
+
...(warning ? { remedy: warning.remedy } : {}),
|
|
2654
2720
|
});
|
|
2655
2721
|
}
|
|
2656
2722
|
else if (item.action === "skip-size-limit") {
|
|
@@ -2678,6 +2744,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2678
2744
|
size: item.restamp.size,
|
|
2679
2745
|
});
|
|
2680
2746
|
}
|
|
2747
|
+
emitPushSkip(run, item.relativePath, "already_up_to_date");
|
|
2681
2748
|
}
|
|
2682
2749
|
counters.filesSkipped++;
|
|
2683
2750
|
};
|
|
@@ -2710,6 +2777,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2710
2777
|
});
|
|
2711
2778
|
if (refreshed === null) {
|
|
2712
2779
|
counters.filesSkipped++;
|
|
2780
|
+
emitPushSkip(run, relativePath, "vanished_before_upload");
|
|
2713
2781
|
return null;
|
|
2714
2782
|
}
|
|
2715
2783
|
if (refreshed.action !== "upload") {
|
|
@@ -2726,6 +2794,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2726
2794
|
// cannot monopolise a worker; the next sync pass will try it again.
|
|
2727
2795
|
if (replanAttempts >= 3) {
|
|
2728
2796
|
counters.filesSkipped++;
|
|
2797
|
+
emitPushSkip(run, relativePath, "retry_budget_exhausted");
|
|
2729
2798
|
return;
|
|
2730
2799
|
}
|
|
2731
2800
|
await processUploadItem(refreshed, replanAttempts + 1, samePathHold);
|
|
@@ -2876,8 +2945,11 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2876
2945
|
!entityIsFreshForCreate(fs.readFileSync(absolutePath, "utf8"), Date.now());
|
|
2877
2946
|
if ((cloudWinsEntity && !remoteMeta && journalEntry) || staleEntityCreate) {
|
|
2878
2947
|
counters.filesSkipped++;
|
|
2879
|
-
if (journalEntry && journalEntry.hash === localHash)
|
|
2948
|
+
if (journalEntry && journalEntry.hash === localHash) {
|
|
2949
|
+
emitPushSkip(run, relativePath, "vault_deleted_local_unchanged");
|
|
2880
2950
|
return;
|
|
2951
|
+
}
|
|
2952
|
+
counters.filesSkippedConflict++;
|
|
2881
2953
|
conflictPaths.push(relativePath);
|
|
2882
2954
|
if (!isMaterializationPathStillContained(run.syncRoot, item.absolutePath)) {
|
|
2883
2955
|
run.emit({ type: "conflict", path: relativePath, direction: "push", resolution: "skip" });
|
|
@@ -2910,6 +2982,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2910
2982
|
// explicit `overwrite` — consent given with the deletion in view — falls
|
|
2911
2983
|
// through to the create below.
|
|
2912
2984
|
counters.filesSkipped++;
|
|
2985
|
+
counters.filesSkippedConflict++;
|
|
2986
|
+
emitPushSkip(run, relativePath, "remote_deleted_not_recreated");
|
|
2913
2987
|
return;
|
|
2914
2988
|
}
|
|
2915
2989
|
}
|
|
@@ -2932,7 +3006,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2932
3006
|
preserveSyncedAt: true,
|
|
2933
3007
|
});
|
|
2934
3008
|
checkpointShareJournal(run);
|
|
2935
|
-
run
|
|
3009
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
2936
3010
|
counters.filesSkipped++;
|
|
2937
3011
|
return;
|
|
2938
3012
|
}
|
|
@@ -2949,6 +3023,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2949
3023
|
conflictPaths.push(relativePath);
|
|
2950
3024
|
run.emit({ type: "conflict", path: relativePath, direction: "push", resolution: "skip" });
|
|
2951
3025
|
counters.filesSkipped++;
|
|
3026
|
+
counters.filesSkippedConflict++;
|
|
2952
3027
|
return;
|
|
2953
3028
|
}
|
|
2954
3029
|
// Every path below that needs the remote BODY — the no-row equality
|
|
@@ -2983,6 +3058,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2983
3058
|
wireSymlinkTarget: remoteTarget,
|
|
2984
3059
|
});
|
|
2985
3060
|
flushShareJournal(run);
|
|
3061
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
2986
3062
|
counters.filesSkipped++;
|
|
2987
3063
|
return;
|
|
2988
3064
|
}
|
|
@@ -3058,7 +3134,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3058
3134
|
settleLandedReconcile(run, relativePath, outcome);
|
|
3059
3135
|
}
|
|
3060
3136
|
checkpointShareJournal(run);
|
|
3061
|
-
run
|
|
3137
|
+
emitPushSkip(run, relativePath, "already_landed");
|
|
3062
3138
|
counters.filesSkipped++;
|
|
3063
3139
|
return;
|
|
3064
3140
|
}
|
|
@@ -3086,6 +3162,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3086
3162
|
// upload drops nothing. Leave the journal alone; the next pass retries.
|
|
3087
3163
|
boardProbe.discard();
|
|
3088
3164
|
counters.filesSkipped++;
|
|
3165
|
+
emitPushSkip(run, relativePath, "remote_body_unreadable");
|
|
3089
3166
|
return;
|
|
3090
3167
|
}
|
|
3091
3168
|
const outcome = mergeAppendOnlyPushConflict(run, item, boardProbe, normalizeEtag(remoteMeta.etag), mergeBoardJson, replaceIfUnchanged);
|
|
@@ -3094,8 +3171,10 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3094
3171
|
fenceOnObservedRemote = true;
|
|
3095
3172
|
}
|
|
3096
3173
|
else if (outcome !== "not-mergeable") {
|
|
3097
|
-
if (outcome === "write-failed")
|
|
3174
|
+
if (outcome === "write-failed") {
|
|
3175
|
+
// The merge already emitted the error. A second skip line repeats it.
|
|
3098
3176
|
hadUploadFailure = true;
|
|
3177
|
+
}
|
|
3099
3178
|
counters.filesSkipped++;
|
|
3100
3179
|
return;
|
|
3101
3180
|
}
|
|
@@ -3111,6 +3190,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3111
3190
|
run.emit({ type: "conflict", path: relativePath, direction: "push", resolution: "keep" });
|
|
3112
3191
|
await adoptRemoteOnPush(run, item, normalizeEtag(remoteMeta.etag), undefined);
|
|
3113
3192
|
counters.filesSkipped++;
|
|
3193
|
+
counters.filesSkippedConflict++;
|
|
3114
3194
|
return;
|
|
3115
3195
|
}
|
|
3116
3196
|
// Version-aware winner (strategy unset or `keep`): fetch the remote
|
|
@@ -3149,8 +3229,10 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3149
3229
|
if (conflictIndex >= 0)
|
|
3150
3230
|
conflictPaths.splice(conflictIndex, 1);
|
|
3151
3231
|
if (outcome !== "publish-local") {
|
|
3152
|
-
if (outcome === "write-failed")
|
|
3232
|
+
if (outcome === "write-failed") {
|
|
3233
|
+
// The merge already emitted the error. A second skip line repeats it.
|
|
3153
3234
|
hadUploadFailure = true;
|
|
3235
|
+
}
|
|
3154
3236
|
counters.filesSkipped++;
|
|
3155
3237
|
return;
|
|
3156
3238
|
}
|
|
@@ -3192,6 +3274,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3192
3274
|
// so the conflict is retried next pass.
|
|
3193
3275
|
winnerProbe?.discard();
|
|
3194
3276
|
counters.filesSkipped++;
|
|
3277
|
+
counters.filesSkippedConflict++;
|
|
3278
|
+
emitPushSkip(run, relativePath, "held_for_next_pass");
|
|
3195
3279
|
return;
|
|
3196
3280
|
}
|
|
3197
3281
|
recordPushConflict(run, item, {
|
|
@@ -3228,12 +3312,14 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3228
3312
|
// tree, never as a sibling twin.
|
|
3229
3313
|
await adoptRemoteOnPush(run, item, normalizeEtag(remoteMeta.etag), decision, winnerProbe);
|
|
3230
3314
|
counters.filesSkipped++;
|
|
3315
|
+
counters.filesSkippedConflict++;
|
|
3231
3316
|
return;
|
|
3232
3317
|
}
|
|
3233
3318
|
}
|
|
3234
3319
|
if (resolution === "skip") {
|
|
3235
3320
|
// Leave both sides alone; no sibling mirror is written any more.
|
|
3236
3321
|
counters.filesSkipped++;
|
|
3322
|
+
counters.filesSkippedConflict++;
|
|
3237
3323
|
return;
|
|
3238
3324
|
}
|
|
3239
3325
|
// `publish-local` on a FRESH two-sided conflict must fence on the etag
|
|
@@ -3458,6 +3544,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3458
3544
|
: undefined;
|
|
3459
3545
|
if (builtOnThisPut) {
|
|
3460
3546
|
adoptLandedSamePathUpload(run, item, builtOnThisPut);
|
|
3547
|
+
if (alreadyCurrent)
|
|
3548
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
3461
3549
|
return;
|
|
3462
3550
|
}
|
|
3463
3551
|
// A row on disk at an etag this process has since replaced came from
|
|
@@ -3471,6 +3559,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3471
3559
|
journalRows(run.journal).set(relativePath, latestEntry);
|
|
3472
3560
|
else
|
|
3473
3561
|
journalRows(run.journal).delete(relativePath);
|
|
3562
|
+
if (alreadyCurrent)
|
|
3563
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
3474
3564
|
return;
|
|
3475
3565
|
}
|
|
3476
3566
|
}
|
|
@@ -3514,7 +3604,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3514
3604
|
bytes: size,
|
|
3515
3605
|
});
|
|
3516
3606
|
if (alreadyCurrent) {
|
|
3517
|
-
run
|
|
3607
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
3518
3608
|
}
|
|
3519
3609
|
else {
|
|
3520
3610
|
run.emit({
|
|
@@ -3565,6 +3655,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3565
3655
|
await adoptRemoteOnPush(run, item, normalizeEtag(latest.etag), undefined);
|
|
3566
3656
|
}
|
|
3567
3657
|
counters.filesSkipped++;
|
|
3658
|
+
counters.filesSkippedConflict++;
|
|
3568
3659
|
return;
|
|
3569
3660
|
}
|
|
3570
3661
|
if (usesVersionAwareWinner(run.onConflict)) {
|
|
@@ -3601,7 +3692,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3601
3692
|
preserveSyncedAt: true,
|
|
3602
3693
|
});
|
|
3603
3694
|
checkpointShareJournal(run);
|
|
3604
|
-
run
|
|
3695
|
+
emitPushSkip(run, relativePath, "already_up_to_date");
|
|
3605
3696
|
counters.filesSkipped++;
|
|
3606
3697
|
return;
|
|
3607
3698
|
}
|
|
@@ -3616,8 +3707,14 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3616
3707
|
const conflictIndex = conflictPaths.lastIndexOf(relativePath);
|
|
3617
3708
|
if (conflictIndex >= 0)
|
|
3618
3709
|
conflictPaths.splice(conflictIndex, 1);
|
|
3619
|
-
if (outcome === "write-failed")
|
|
3710
|
+
if (outcome === "write-failed") {
|
|
3711
|
+
// The merge already emitted the error. A second skip line repeats it.
|
|
3620
3712
|
hadUploadFailure = true;
|
|
3713
|
+
}
|
|
3714
|
+
else if (outcome === "publish-local") {
|
|
3715
|
+
counters.filesSkippedConflict++;
|
|
3716
|
+
emitPushSkip(run, relativePath, "held_for_next_pass");
|
|
3717
|
+
}
|
|
3621
3718
|
counters.filesSkipped++;
|
|
3622
3719
|
return;
|
|
3623
3720
|
}
|
|
@@ -3638,6 +3735,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3638
3735
|
// newer remote until its body has been safely parked.
|
|
3639
3736
|
probe.discard();
|
|
3640
3737
|
counters.filesSkipped++;
|
|
3738
|
+
counters.filesSkippedConflict++;
|
|
3739
|
+
emitPushSkip(run, relativePath, "held_for_next_pass");
|
|
3641
3740
|
return;
|
|
3642
3741
|
}
|
|
3643
3742
|
recordPushConflict(run, item, {
|
|
@@ -3684,6 +3783,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3684
3783
|
return;
|
|
3685
3784
|
if (isPreconditionFailed(retryErr)) {
|
|
3686
3785
|
counters.filesSkipped++;
|
|
3786
|
+
counters.filesSkippedConflict++;
|
|
3787
|
+
emitPushSkip(run, relativePath, "held_for_next_pass");
|
|
3687
3788
|
return;
|
|
3688
3789
|
}
|
|
3689
3790
|
hadUploadFailure = true;
|
|
@@ -3709,6 +3810,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3709
3810
|
// both copies and the journal unchanged until the next pass.
|
|
3710
3811
|
probe.discard();
|
|
3711
3812
|
counters.filesSkipped++;
|
|
3813
|
+
counters.filesSkippedConflict++;
|
|
3814
|
+
emitPushSkip(run, relativePath, "held_for_next_pass");
|
|
3712
3815
|
return;
|
|
3713
3816
|
}
|
|
3714
3817
|
recordPushConflict(run, item, {
|
|
@@ -3724,6 +3827,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3724
3827
|
// Deliberately NOT `type: "error"` (a hold is a benign per-file
|
|
3725
3828
|
// skip; the conflict event + index row above already record it).
|
|
3726
3829
|
counters.filesSkipped++;
|
|
3830
|
+
counters.filesSkippedConflict++;
|
|
3727
3831
|
return;
|
|
3728
3832
|
}
|
|
3729
3833
|
await adoptRemoteOnPush(run, item, latest?.etag
|
|
@@ -3765,6 +3869,8 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3765
3869
|
// non-interactive strategy returns — stays fenced.
|
|
3766
3870
|
if (resolution === "publish-local") {
|
|
3767
3871
|
counters.filesSkipped++;
|
|
3872
|
+
counters.filesSkippedConflict++;
|
|
3873
|
+
emitPushSkip(run, relativePath, "body_changed_during_decision");
|
|
3768
3874
|
return;
|
|
3769
3875
|
}
|
|
3770
3876
|
if (resolution === "overwrite") {
|
|
@@ -3799,6 +3905,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3799
3905
|
}
|
|
3800
3906
|
// `skip` and `diff`: leave both sides for the next pass.
|
|
3801
3907
|
counters.filesSkipped++;
|
|
3908
|
+
counters.filesSkippedConflict++;
|
|
3802
3909
|
return;
|
|
3803
3910
|
}
|
|
3804
3911
|
if (isAccessDenied(err)) {
|
|
@@ -3908,6 +4015,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3908
4015
|
if (item === null) {
|
|
3909
4016
|
queue.shift();
|
|
3910
4017
|
counters.filesSkipped++;
|
|
4018
|
+
emitPushSkip(run, previewItem.relativePath, "vanished_before_upload");
|
|
3911
4019
|
skippedSinceCooperativeCheckpoint++;
|
|
3912
4020
|
continue;
|
|
3913
4021
|
}
|
|
@@ -4405,9 +4513,11 @@ function mergeAppendOnlyPushConflict(run, item, probe, remoteEtag, merge = merge
|
|
|
4405
4513
|
});
|
|
4406
4514
|
return "write-failed";
|
|
4407
4515
|
}
|
|
4408
|
-
if (appended === "changed")
|
|
4516
|
+
if (appended === "changed") {
|
|
4517
|
+
emitPushSkip(run, item.relativePath, "body_changed_during_decision");
|
|
4409
4518
|
return "local-changed";
|
|
4410
|
-
|
|
4519
|
+
}
|
|
4520
|
+
emitPushSkip(run, item.relativePath, merged.equals(remoteBody) ? "already_up_to_date" : "merged_with_remote");
|
|
4411
4521
|
if (!merged.equals(remoteBody))
|
|
4412
4522
|
return "merged-locally";
|
|
4413
4523
|
const lstat = fs.lstatSync(item.absolutePath);
|
|
@@ -4493,6 +4603,7 @@ async function adoptRemoteOnPush(run, item, remoteHash, decision, probe) {
|
|
|
4493
4603
|
symlinkTargetLinkPath: run.options.personalMode === true
|
|
4494
4604
|
? personalSymlinkTargetLinkPath(item.relativePath)
|
|
4495
4605
|
: `companies/${run.ctx.slug}/${item.relativePath}`,
|
|
4606
|
+
syncRoot: run.syncRoot,
|
|
4496
4607
|
});
|
|
4497
4608
|
}
|
|
4498
4609
|
remoteAdopted = true;
|
|
@@ -5190,7 +5301,10 @@ function buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths,
|
|
|
5190
5301
|
return {
|
|
5191
5302
|
filesUploaded: counters.filesUploaded,
|
|
5192
5303
|
bytesUploaded: counters.bytesUploaded,
|
|
5193
|
-
|
|
5304
|
+
linkWarnings: run.linkWarnings.warnings,
|
|
5305
|
+
linkWarningsOmitted: run.linkWarnings.omitted,
|
|
5306
|
+
filesSkipped: counters.filesSkipped + run.unreadableLinks,
|
|
5307
|
+
filesSkippedDetail: skipDetail(counters, run.unreadableLinks),
|
|
5194
5308
|
filesDeleted: counters.filesDeleted,
|
|
5195
5309
|
filesTombstoned: counters.filesTombstoned,
|
|
5196
5310
|
filesRefusedStale: counters.filesRefusedStale,
|
|
@@ -5213,11 +5327,24 @@ function buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths,
|
|
|
5213
5327
|
aborted,
|
|
5214
5328
|
};
|
|
5215
5329
|
}
|
|
5330
|
+
function skipDetail(counters, unreadableLinks) {
|
|
5331
|
+
const upToDate = counters.filesSkippedUpToDate;
|
|
5332
|
+
const conflict = counters.filesSkippedConflict;
|
|
5333
|
+
const brokenLink = counters.filesSkippedBrokenLink + unreadableLinks;
|
|
5334
|
+
const other = Math.max(0, counters.filesSkipped + unreadableLinks - upToDate - conflict - brokenLink);
|
|
5335
|
+
return { upToDate, conflict, brokenLink, other };
|
|
5336
|
+
}
|
|
5337
|
+
function emitPushSkip(run, filePath, reason) {
|
|
5338
|
+
run.emit({ type: "reconciled", path: filePath, direction: "push", reason });
|
|
5339
|
+
if (run.shareCounters && isUpToDateSkipReason(reason)) {
|
|
5340
|
+
run.shareCounters.filesSkippedUpToDate++;
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5216
5343
|
/**
|
|
5217
5344
|
* Default human-readable share output. Preserves the exact format the CLI
|
|
5218
5345
|
* emitted before `onEvent` was added — tty users see no change.
|
|
5219
5346
|
*/
|
|
5220
|
-
function defaultConsoleLogger(event) {
|
|
5347
|
+
function defaultConsoleLogger(event, humanSkipLines) {
|
|
5221
5348
|
if (event.type === "plan") {
|
|
5222
5349
|
if (event.filesToUpload > 0) {
|
|
5223
5350
|
console.log(`Plan: ${event.filesToUpload} to upload (${event.bytesToUpload} bytes), ${event.filesToSkip} unchanged`);
|
|
@@ -5245,6 +5372,12 @@ function defaultConsoleLogger(event) {
|
|
|
5245
5372
|
else if (event.type === "conflict") {
|
|
5246
5373
|
console.error(` ⚠ conflict (${event.direction}): ${event.path} — ${event.resolution}`);
|
|
5247
5374
|
}
|
|
5375
|
+
else if (event.type === "reconciled") {
|
|
5376
|
+
if (humanSkipLines)
|
|
5377
|
+
humanSkipLines.note(event.path, event.reason, event.direction);
|
|
5378
|
+
else
|
|
5379
|
+
console.log(formatReconcileSkipLine(event.path, event.reason, event.direction));
|
|
5380
|
+
}
|
|
5248
5381
|
else if (event.type === "push-refused-server-owned") {
|
|
5249
5382
|
const nextStep = event.nextStep === "use-source-ingestion"
|
|
5250
5383
|
? "Add the source through its supported integration or ingestion flow, then run hq sync pull."
|
|
@@ -5259,6 +5392,9 @@ function defaultConsoleLogger(event) {
|
|
|
5259
5392
|
console.warn(` ... and ${event.count - event.samplePaths.length} more`);
|
|
5260
5393
|
}
|
|
5261
5394
|
}
|
|
5395
|
+
else if (event.type === "skip-broken-link") {
|
|
5396
|
+
console.warn(` ! ${event.path} skipped — ${event.message}`);
|
|
5397
|
+
}
|
|
5262
5398
|
else if (event.type === "error") {
|
|
5263
5399
|
console.error(` ✗ ${event.path} — ${event.message}`);
|
|
5264
5400
|
}
|
|
@@ -5728,6 +5864,8 @@ async function collectFiles(paths, hqRoot, syncRoot, filter, hooks = {}) {
|
|
|
5728
5864
|
const target = readlinkOrNull(absolutePath);
|
|
5729
5865
|
if (target === null) {
|
|
5730
5866
|
console.error(` Warning: ${p} is an unreadable symbolic link, skipping.`);
|
|
5867
|
+
if (hooks.onUnreadableLink)
|
|
5868
|
+
hooks.onUnreadableLink(relativePath);
|
|
5731
5869
|
hooks.onUnreachablePath?.(p, "unreadable-link");
|
|
5732
5870
|
continue;
|
|
5733
5871
|
}
|
|
@@ -5829,6 +5967,8 @@ async function walkDir(dir, syncRoot, filter, results, hooks = {}, cooperativeWo
|
|
|
5829
5967
|
const target = readlinkOrNull(absolutePath);
|
|
5830
5968
|
if (target === null) {
|
|
5831
5969
|
console.error(` Warning: ${linkRelative} is an unreadable symbolic link, skipping.`);
|
|
5970
|
+
if (hooks.onUnreadableLink)
|
|
5971
|
+
hooks.onUnreadableLink(linkRelative);
|
|
5832
5972
|
hooks.onUnreachablePath?.(linkRelative, "unreadable-link");
|
|
5833
5973
|
continue;
|
|
5834
5974
|
}
|
|
@@ -6253,6 +6393,8 @@ companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
|
|
|
6253
6393
|
// of roots; test each row by walking its ancestors, never rows x roots.
|
|
6254
6394
|
const scopeRootSet = new Set(scopeRoots);
|
|
6255
6395
|
const everyRowInScope = scopeRootSet.has("");
|
|
6396
|
+
const linkInspectCache = new Map();
|
|
6397
|
+
const brokenLinkPaths = new Set();
|
|
6256
6398
|
for (const [relativeKey, entry] of journalRows(journal).entries()) {
|
|
6257
6399
|
if (excludedPaths.has(relativeKey))
|
|
6258
6400
|
continue;
|
|
@@ -6302,20 +6444,43 @@ companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
|
|
|
6302
6444
|
inScopeJournalEntries++;
|
|
6303
6445
|
const localPath = localPathForVaultKey(syncRoot, relativeKey);
|
|
6304
6446
|
let presentLocally = true;
|
|
6447
|
+
let lstatError;
|
|
6305
6448
|
try {
|
|
6306
6449
|
fs.lstatSync(localPath);
|
|
6307
6450
|
}
|
|
6308
6451
|
catch (err) {
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6452
|
+
const code = err && typeof err === "object" && "code" in err
|
|
6453
|
+
? err.code
|
|
6454
|
+
: undefined;
|
|
6455
|
+
// ELOOP and ENOTDIR come from a directory link above the key (a loop,
|
|
6456
|
+
// or a link that no longer resolves to a directory). They are settled
|
|
6457
|
+
// below only when that link is broken; otherwise they still throw.
|
|
6458
|
+
if (code === "ENOENT" || code === "ELOOP" || code === "ENOTDIR") {
|
|
6313
6459
|
presentLocally = false;
|
|
6460
|
+
if (code !== "ENOENT")
|
|
6461
|
+
lstatError = err;
|
|
6314
6462
|
}
|
|
6315
6463
|
else {
|
|
6316
6464
|
throw err;
|
|
6317
6465
|
}
|
|
6318
6466
|
}
|
|
6467
|
+
// A missing child under a broken directory link (dangling or looping)
|
|
6468
|
+
// means the link hides the file, not that the user deleted it. Record one
|
|
6469
|
+
// warning per link and keep the key out of both sides of the
|
|
6470
|
+
// bulk-asymmetry ratio, like a tombstone.
|
|
6471
|
+
if (!presentLocally) {
|
|
6472
|
+
const linkWarning = localLinkWarning(syncRoot, path.dirname(localPath), path.posix.dirname(relativeKey), linkInspectCache);
|
|
6473
|
+
if (!linkWarning && lstatError !== undefined)
|
|
6474
|
+
throw lstatError;
|
|
6475
|
+
if (linkWarning) {
|
|
6476
|
+
inScopeJournalEntries--;
|
|
6477
|
+
if (!brokenLinkPaths.has(linkWarning.path)) {
|
|
6478
|
+
brokenLinkPaths.add(linkWarning.path);
|
|
6479
|
+
(plan.brokenLinkSkipped ??= []).push(linkWarning);
|
|
6480
|
+
}
|
|
6481
|
+
continue;
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6319
6484
|
const materializedKnowledgeDir = presentLocally &&
|
|
6320
6485
|
isMaterializedKnowledgeDirectory(localPath, relativeKey, entry, companyScoped);
|
|
6321
6486
|
if (presentLocally && !materializedKnowledgeDir)
|