@wspc/cli 0.1.15 → 0.1.17
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/cli.js +386 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/spec/openapi.json +14 -0
package/dist/cli.js
CHANGED
|
@@ -995,6 +995,15 @@ var driveManifestGet = (options) => (options.client ?? client).get({
|
|
|
995
995
|
url: "/drive/libraries/{id}/manifest",
|
|
996
996
|
...options
|
|
997
997
|
});
|
|
998
|
+
var driveFileMove = (options) => (options.client ?? client).post({
|
|
999
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
1000
|
+
url: "/drive/libraries/{id}/files/move",
|
|
1001
|
+
...options,
|
|
1002
|
+
headers: {
|
|
1003
|
+
"Content-Type": "application/json",
|
|
1004
|
+
...options.headers
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
998
1007
|
var driveFileRestore = (options) => (options.client ?? client).post({
|
|
999
1008
|
security: [{ scheme: "bearer", type: "http" }],
|
|
1000
1009
|
url: "/drive/libraries/{id}/files/restore",
|
|
@@ -1520,9 +1529,9 @@ function createConsistencyFetch(opts) {
|
|
|
1520
1529
|
}
|
|
1521
1530
|
|
|
1522
1531
|
// src/version.ts
|
|
1523
|
-
var VERSION = "0.1.
|
|
1524
|
-
var SPEC_SHA = "
|
|
1525
|
-
var SPEC_FETCHED_AT = "2026-07-
|
|
1532
|
+
var VERSION = "0.1.17";
|
|
1533
|
+
var SPEC_SHA = "74d4defc";
|
|
1534
|
+
var SPEC_FETCHED_AT = "2026-07-13T06:40:42.304Z";
|
|
1526
1535
|
var API_BASE = "https://api.wspc.ai";
|
|
1527
1536
|
|
|
1528
1537
|
// src/index.ts
|
|
@@ -3020,7 +3029,7 @@ var driveFileHistoryCommand = new Command29("history").description("Get drive fi
|
|
|
3020
3029
|
|
|
3021
3030
|
// src/generated/cli/drive/manifest/get.ts
|
|
3022
3031
|
import { Command as Command30 } from "commander";
|
|
3023
|
-
var driveManifestGetCommand = new Command30("get").description("Get a drive library manifest").argument("<id>", "id").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").option("--path-prefix <value>", "path_prefix").action(async (id, opts) => {
|
|
3032
|
+
var driveManifestGetCommand = new Command30("get").description("Get a drive library manifest").argument("<id>", "id").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").option("--path-prefix <value>", "path_prefix").option("--since-cursor <value>", "since_cursor").action(async (id, opts) => {
|
|
3024
3033
|
const client2 = await loadSdkClient();
|
|
3025
3034
|
const result = await driveManifestGet({
|
|
3026
3035
|
client: client2._rawClient,
|
|
@@ -3031,7 +3040,8 @@ var driveManifestGetCommand = new Command30("get").description("Get a drive libr
|
|
|
3031
3040
|
limit: opts.limit,
|
|
3032
3041
|
cursor: opts.cursor,
|
|
3033
3042
|
include_deleted: opts.includeDeleted,
|
|
3034
|
-
path_prefix: opts.pathPrefix
|
|
3043
|
+
path_prefix: opts.pathPrefix,
|
|
3044
|
+
since_cursor: opts.sinceCursor
|
|
3035
3045
|
}
|
|
3036
3046
|
});
|
|
3037
3047
|
if (result.error || !result.response?.ok) {
|
|
@@ -4851,7 +4861,9 @@ async function expectJsonResult(result) {
|
|
|
4851
4861
|
if (result.data == null) throw new Error("empty response");
|
|
4852
4862
|
return result.data;
|
|
4853
4863
|
}
|
|
4854
|
-
|
|
4864
|
+
function syncClientHeaders(clientId) {
|
|
4865
|
+
return { "x-wspc-client": clientId === void 0 ? "drive-sync" : `drive-sync/${clientId}` };
|
|
4866
|
+
}
|
|
4855
4867
|
function driveContentUrl(baseUrl, id) {
|
|
4856
4868
|
const baseWithTrailingSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
4857
4869
|
return new URL(`drive/libraries/${encodeURIComponent(id)}/files/content`, baseWithTrailingSlash);
|
|
@@ -4859,6 +4871,7 @@ function driveContentUrl(baseUrl, id) {
|
|
|
4859
4871
|
async function createDriveApi(opts = {}) {
|
|
4860
4872
|
const client2 = await loadSdkClientWithAuthedFetch(opts);
|
|
4861
4873
|
const rawClient = client2._rawClient;
|
|
4874
|
+
const clientHeaders = syncClientHeaders(opts.clientId);
|
|
4862
4875
|
return {
|
|
4863
4876
|
async getLibrary(id) {
|
|
4864
4877
|
const result = await driveLibraryGet({
|
|
@@ -4867,11 +4880,14 @@ async function createDriveApi(opts = {}) {
|
|
|
4867
4880
|
});
|
|
4868
4881
|
return expectJsonResult(result);
|
|
4869
4882
|
},
|
|
4870
|
-
async getManifest(id, cursor) {
|
|
4883
|
+
async getManifest(id, cursor, sinceCursor) {
|
|
4884
|
+
const query = {};
|
|
4885
|
+
if (cursor !== void 0) query.cursor = cursor;
|
|
4886
|
+
if (sinceCursor !== void 0) query.since_cursor = sinceCursor;
|
|
4871
4887
|
const result = await driveManifestGet({
|
|
4872
4888
|
client: rawClient,
|
|
4873
4889
|
path: { id },
|
|
4874
|
-
...
|
|
4890
|
+
...Object.keys(query).length > 0 ? { query } : {}
|
|
4875
4891
|
});
|
|
4876
4892
|
return expectJsonResult(result);
|
|
4877
4893
|
},
|
|
@@ -4879,7 +4895,7 @@ async function createDriveApi(opts = {}) {
|
|
|
4879
4895
|
const result = await driveFileDelete({
|
|
4880
4896
|
client: rawClient,
|
|
4881
4897
|
path: { id },
|
|
4882
|
-
headers:
|
|
4898
|
+
headers: clientHeaders,
|
|
4883
4899
|
body: {
|
|
4884
4900
|
path,
|
|
4885
4901
|
expected_entry_version: expectedEntryVersion
|
|
@@ -4887,6 +4903,19 @@ async function createDriveApi(opts = {}) {
|
|
|
4887
4903
|
});
|
|
4888
4904
|
return expectJsonResult(result);
|
|
4889
4905
|
},
|
|
4906
|
+
async moveFile(id, fromPath, toPath, expectedEntryVersion) {
|
|
4907
|
+
const result = await driveFileMove({
|
|
4908
|
+
client: rawClient,
|
|
4909
|
+
path: { id },
|
|
4910
|
+
headers: clientHeaders,
|
|
4911
|
+
body: {
|
|
4912
|
+
from_path: fromPath,
|
|
4913
|
+
to_path: toPath,
|
|
4914
|
+
...expectedEntryVersion === void 0 ? {} : { expected_entry_version: expectedEntryVersion }
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4917
|
+
return expectJsonResult(result);
|
|
4918
|
+
},
|
|
4890
4919
|
async uploadFile(id, path, body, sha256, expectedEntryVersion) {
|
|
4891
4920
|
const url = driveContentUrl(client2.baseUrl, id);
|
|
4892
4921
|
url.searchParams.set("path", path);
|
|
@@ -4898,7 +4927,7 @@ async function createDriveApi(opts = {}) {
|
|
|
4898
4927
|
headers: {
|
|
4899
4928
|
"content-type": "application/octet-stream",
|
|
4900
4929
|
"x-drive-content-sha256": sha256,
|
|
4901
|
-
...
|
|
4930
|
+
...clientHeaders
|
|
4902
4931
|
},
|
|
4903
4932
|
body
|
|
4904
4933
|
});
|
|
@@ -5113,7 +5142,7 @@ function isOptionalIsoTimestamp(value) {
|
|
|
5113
5142
|
}
|
|
5114
5143
|
function isValidDriveState(value) {
|
|
5115
5144
|
if (!isRecord2(value)) return false;
|
|
5116
|
-
if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord2(value.entries) || !isRecord2(value.conflicts) || value.realtime !== void 0 && !isDriveRealtimeState(value.realtime)) {
|
|
5145
|
+
if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord2(value.entries) || !isRecord2(value.conflicts) || value.realtime !== void 0 && !isDriveRealtimeState(value.realtime) || value.scan_cache !== void 0 && !isRecord2(value.scan_cache) || value.manifest_cursor !== void 0 && typeof value.manifest_cursor !== "string") {
|
|
5117
5146
|
return false;
|
|
5118
5147
|
}
|
|
5119
5148
|
for (const entry of Object.values(value.entries)) {
|
|
@@ -5122,8 +5151,16 @@ function isValidDriveState(value) {
|
|
|
5122
5151
|
for (const conflict2 of Object.values(value.conflicts)) {
|
|
5123
5152
|
if (!isDriveConflict(conflict2)) return false;
|
|
5124
5153
|
}
|
|
5154
|
+
if (value.scan_cache !== void 0) {
|
|
5155
|
+
for (const entry of Object.values(value.scan_cache)) {
|
|
5156
|
+
if (!isDriveScanCacheEntry(entry)) return false;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5125
5159
|
return true;
|
|
5126
5160
|
}
|
|
5161
|
+
function isDriveScanCacheEntry(value) {
|
|
5162
|
+
return isRecord2(value) && typeof value.mtime_ms === "number" && typeof value.size_bytes === "number" && typeof value.sha256 === "string";
|
|
5163
|
+
}
|
|
5127
5164
|
|
|
5128
5165
|
// src/handwritten/commands/drive/bind.ts
|
|
5129
5166
|
async function assertExistingDirectory(path) {
|
|
@@ -5158,7 +5195,7 @@ function driveBindCommand() {
|
|
|
5158
5195
|
// src/handwritten/commands/drive/sync.ts
|
|
5159
5196
|
import { Command as Command80 } from "commander";
|
|
5160
5197
|
import { mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
5161
|
-
import { basename as basename3, dirname as dirname2, join as join6, posix as
|
|
5198
|
+
import { basename as basename3, dirname as dirname2, join as join6, posix as pathPosix3, resolve as resolve3 } from "path";
|
|
5162
5199
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
5163
5200
|
|
|
5164
5201
|
// src/handwritten/commands/drive/debug-log.ts
|
|
@@ -5204,7 +5241,7 @@ function decideDriveAction(entry, local, remote) {
|
|
|
5204
5241
|
const remoteStatus = getRemoteStatus(entry, remote);
|
|
5205
5242
|
if (!remote) return decideRemoteMissing(localStatus);
|
|
5206
5243
|
if (!local) return decideLocalMissing(entry, remoteStatus);
|
|
5207
|
-
return decideBothPresent(entry, localStatus, remoteStatus);
|
|
5244
|
+
return decideBothPresent(entry, localStatus, remoteStatus, local, remote);
|
|
5208
5245
|
}
|
|
5209
5246
|
function decideWithoutBase(local, remote) {
|
|
5210
5247
|
if (local && !remote) return { type: "upload_create", expectedEntryVersion: 0 };
|
|
@@ -5226,7 +5263,8 @@ function decideLocalMissing(entry, remoteStatus) {
|
|
|
5226
5263
|
if (remoteStatus === "unchanged") return { type: "delete_remote", expectedEntryVersion: entry.entry_version };
|
|
5227
5264
|
return { type: "conflict", reason: "remote_changed_before_delete" };
|
|
5228
5265
|
}
|
|
5229
|
-
function decideBothPresent(entry, localStatus, remoteStatus) {
|
|
5266
|
+
function decideBothPresent(entry, localStatus, remoteStatus, local, remote) {
|
|
5267
|
+
const converged = remote.content_sha256 !== void 0 && local.sha256 === remote.content_sha256;
|
|
5230
5268
|
if (localStatus === "unchanged" && remoteStatus === "content_same_new_version") {
|
|
5231
5269
|
return { type: "state_only" };
|
|
5232
5270
|
}
|
|
@@ -5234,7 +5272,7 @@ function decideBothPresent(entry, localStatus, remoteStatus) {
|
|
|
5234
5272
|
return { type: "conflict", reason: "remote_missing_entry_version" };
|
|
5235
5273
|
}
|
|
5236
5274
|
if (localStatus === "unknown" && remoteStatus !== "unchanged") {
|
|
5237
|
-
return { type: "conflict", reason: "unknown_local_base_remote_changed" };
|
|
5275
|
+
return converged ? { type: "state_only" } : { type: "conflict", reason: "unknown_local_base_remote_changed" };
|
|
5238
5276
|
}
|
|
5239
5277
|
if (localStatus === "unknown") return { type: "conflict", reason: "unknown_local_base" };
|
|
5240
5278
|
if (localStatus === "unchanged" && remoteStatus === "changed") return { type: "download" };
|
|
@@ -5242,7 +5280,7 @@ function decideBothPresent(entry, localStatus, remoteStatus) {
|
|
|
5242
5280
|
return { type: "upload_update", expectedEntryVersion: entry.entry_version };
|
|
5243
5281
|
}
|
|
5244
5282
|
if (localStatus !== "unchanged" && remoteStatus !== "unchanged") {
|
|
5245
|
-
return { type: "conflict", reason: "local_and_remote_changed" };
|
|
5283
|
+
return converged ? { type: "state_only" } : { type: "conflict", reason: "local_and_remote_changed" };
|
|
5246
5284
|
}
|
|
5247
5285
|
return { type: "unchanged" };
|
|
5248
5286
|
}
|
|
@@ -5416,8 +5454,7 @@ function mergeText3(base, local, remote) {
|
|
|
5416
5454
|
}
|
|
5417
5455
|
function conflictCopyPath(path, side, timestamp, versionId) {
|
|
5418
5456
|
const parsed = pathPosix.parse(path);
|
|
5419
|
-
const
|
|
5420
|
-
const fileName = `${parsed.name}.${side}-conflict-${timestamp}-${shortVersionId}${parsed.ext}`;
|
|
5457
|
+
const fileName = `${parsed.name}.${side}-conflict-${timestamp}-${safeVersionId(versionId)}${parsed.ext}`;
|
|
5421
5458
|
if (parsed.dir === "") {
|
|
5422
5459
|
return fileName;
|
|
5423
5460
|
}
|
|
@@ -5446,25 +5483,33 @@ function hasTextHint(path, mimeType) {
|
|
|
5446
5483
|
function normalizeLines(text) {
|
|
5447
5484
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
5448
5485
|
}
|
|
5449
|
-
function
|
|
5450
|
-
const
|
|
5451
|
-
return
|
|
5486
|
+
function safeVersionId(versionId) {
|
|
5487
|
+
const safeVersionId2 = versionId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
5488
|
+
return safeVersionId2.length > 0 ? safeVersionId2 : "unknown";
|
|
5452
5489
|
}
|
|
5453
5490
|
|
|
5454
5491
|
// src/handwritten/commands/drive/scanner.ts
|
|
5455
5492
|
import { createHash } from "crypto";
|
|
5456
5493
|
import { constants as fsConstants } from "fs";
|
|
5457
5494
|
import { open as open2, readdir, lstat } from "fs/promises";
|
|
5458
|
-
import { join as join4 } from "path";
|
|
5495
|
+
import { join as join4, posix as pathPosix2 } from "path";
|
|
5459
5496
|
async function scanDriveFiles(root, options = {}) {
|
|
5460
5497
|
const candidates = [];
|
|
5461
5498
|
const files = {};
|
|
5462
|
-
const
|
|
5463
|
-
await walk(
|
|
5499
|
+
const startDrivePath = options.startDrivePath ?? "";
|
|
5500
|
+
await walk(startDrivePath === "" ? root : join4(root, ...startDrivePath.split("/")), startDrivePath);
|
|
5464
5501
|
await addNonCollidingFiles(candidates);
|
|
5465
5502
|
return files;
|
|
5466
5503
|
async function walk(currentPath, currentDrivePath) {
|
|
5467
|
-
|
|
5504
|
+
let entries;
|
|
5505
|
+
try {
|
|
5506
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
5507
|
+
} catch (error) {
|
|
5508
|
+
if (currentDrivePath === startDrivePath && startDrivePath !== "" && isNotFoundError(error)) return;
|
|
5509
|
+
if (currentDrivePath === "" || !isTransientScanError(error) || !options.onPathError) throw error;
|
|
5510
|
+
await options.onPathError(currentDrivePath, error);
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5468
5513
|
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
5469
5514
|
for (const entry of entries) {
|
|
5470
5515
|
if (isExcludedRootEntry(currentDrivePath, entry)) {
|
|
@@ -5482,22 +5527,34 @@ async function scanDriveFiles(root, options = {}) {
|
|
|
5482
5527
|
continue;
|
|
5483
5528
|
}
|
|
5484
5529
|
const nextPath = join4(currentPath, entry.name);
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5530
|
+
try {
|
|
5531
|
+
const stats = await lstat(nextPath);
|
|
5532
|
+
if (stats.isSymbolicLink()) {
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
if (stats.isDirectory()) {
|
|
5536
|
+
await walk(nextPath, nextDrivePath);
|
|
5537
|
+
continue;
|
|
5538
|
+
}
|
|
5539
|
+
if (!stats.isFile()) {
|
|
5540
|
+
continue;
|
|
5541
|
+
}
|
|
5542
|
+
const cached = options.cache?.[nextDrivePath];
|
|
5543
|
+
if (cached !== void 0 && cached.mtime_ms === stats.mtimeMs && cached.size_bytes === stats.size) {
|
|
5544
|
+
options.onCacheUpdate?.(nextDrivePath, cached);
|
|
5545
|
+
candidates.push({ path: nextDrivePath, entry: { sha256: cached.sha256, size_bytes: cached.size_bytes } });
|
|
5546
|
+
continue;
|
|
5547
|
+
}
|
|
5548
|
+
const digest = await hashDriveFile(nextPath);
|
|
5549
|
+
if (!digest) {
|
|
5550
|
+
continue;
|
|
5551
|
+
}
|
|
5552
|
+
options.onCacheUpdate?.(nextDrivePath, { mtime_ms: stats.mtimeMs, size_bytes: digest.sizeBytes, sha256: digest.sha256 });
|
|
5553
|
+
candidates.push({ path: nextDrivePath, entry: { sha256: digest.sha256, size_bytes: digest.sizeBytes } });
|
|
5554
|
+
} catch (error) {
|
|
5555
|
+
if (!isTransientScanError(error) || !options.onPathError) throw error;
|
|
5556
|
+
await options.onPathError(nextDrivePath, error);
|
|
5499
5557
|
}
|
|
5500
|
-
candidates.push({ path: nextDrivePath, entry: { sha256: digest.sha256, size_bytes: digest.sizeBytes } });
|
|
5501
5558
|
}
|
|
5502
5559
|
}
|
|
5503
5560
|
async function addNonCollidingFiles(candidates2) {
|
|
@@ -5526,6 +5583,75 @@ async function scanDriveFiles(root, options = {}) {
|
|
|
5526
5583
|
return currentDrivePath === "" && entry.name === DRIVE_DIR;
|
|
5527
5584
|
}
|
|
5528
5585
|
}
|
|
5586
|
+
async function rescanDriveFiles(root, dirtyPaths, options = {}) {
|
|
5587
|
+
const kept = { ...options.cache ?? {} };
|
|
5588
|
+
for (const dirtyPath of new Set(dirtyPaths)) {
|
|
5589
|
+
if (dirtyPath === "" || isInternalSyncArtifactName(pathPosix2.basename(dirtyPath))) continue;
|
|
5590
|
+
try {
|
|
5591
|
+
validateDrivePath(dirtyPath);
|
|
5592
|
+
} catch (error) {
|
|
5593
|
+
if (!options.onPathError) throw error;
|
|
5594
|
+
await options.onPathError(dirtyPath, error);
|
|
5595
|
+
continue;
|
|
5596
|
+
}
|
|
5597
|
+
removePathAndChildren(kept, dirtyPath);
|
|
5598
|
+
const absPath = join4(root, ...dirtyPath.split("/"));
|
|
5599
|
+
let stats;
|
|
5600
|
+
try {
|
|
5601
|
+
stats = await lstat(absPath);
|
|
5602
|
+
} catch (error) {
|
|
5603
|
+
if (isNotFoundError(error)) continue;
|
|
5604
|
+
if (!isTransientScanError(error) || !options.onPathError) throw error;
|
|
5605
|
+
await options.onPathError(dirtyPath, error);
|
|
5606
|
+
continue;
|
|
5607
|
+
}
|
|
5608
|
+
if (stats.isSymbolicLink()) continue;
|
|
5609
|
+
if (stats.isDirectory()) {
|
|
5610
|
+
await scanDriveFiles(root, {
|
|
5611
|
+
...options,
|
|
5612
|
+
startDrivePath: dirtyPath,
|
|
5613
|
+
onCacheUpdate: (path, entry) => {
|
|
5614
|
+
kept[path] = entry;
|
|
5615
|
+
}
|
|
5616
|
+
});
|
|
5617
|
+
continue;
|
|
5618
|
+
}
|
|
5619
|
+
if (!stats.isFile()) continue;
|
|
5620
|
+
try {
|
|
5621
|
+
const cached = options.cache?.[dirtyPath];
|
|
5622
|
+
if (cached !== void 0 && cached.mtime_ms === stats.mtimeMs && cached.size_bytes === stats.size) {
|
|
5623
|
+
kept[dirtyPath] = cached;
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
const digest = await hashDriveFile(absPath);
|
|
5627
|
+
if (!digest) continue;
|
|
5628
|
+
kept[dirtyPath] = { mtime_ms: stats.mtimeMs, size_bytes: digest.sizeBytes, sha256: digest.sha256 };
|
|
5629
|
+
} catch (error) {
|
|
5630
|
+
if (!isTransientScanError(error) || !options.onPathError) throw error;
|
|
5631
|
+
await options.onPathError(dirtyPath, error);
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
const files = {};
|
|
5635
|
+
for (const [path, entry] of Object.entries(kept)) {
|
|
5636
|
+
options.onCacheUpdate?.(path, entry);
|
|
5637
|
+
files[path] = { sha256: entry.sha256, size_bytes: entry.size_bytes };
|
|
5638
|
+
}
|
|
5639
|
+
return files;
|
|
5640
|
+
}
|
|
5641
|
+
function removePathAndChildren(view, path) {
|
|
5642
|
+
delete view[path];
|
|
5643
|
+
const prefix = `${path}/`;
|
|
5644
|
+
for (const key of Object.keys(view)) {
|
|
5645
|
+
if (key.startsWith(prefix)) delete view[key];
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
function isNotFoundError(error) {
|
|
5649
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
5650
|
+
}
|
|
5651
|
+
var TRANSIENT_SCAN_ERROR_CODES = /* @__PURE__ */ new Set(["ENOENT", "EPERM", "EBUSY"]);
|
|
5652
|
+
function isTransientScanError(error) {
|
|
5653
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && TRANSIENT_SCAN_ERROR_CODES.has(error.code);
|
|
5654
|
+
}
|
|
5529
5655
|
function isInternalSyncArtifactName(name) {
|
|
5530
5656
|
if (!name.startsWith(".") || !name.endsWith(".tmp")) return false;
|
|
5531
5657
|
return name.includes(".wspc-download-") || name.includes(".wspc-backup-") || name.includes(".wspc-conflict-") || name.includes(".wspc-merge-");
|
|
@@ -5580,7 +5706,7 @@ import { Readable as Readable2, Transform } from "stream";
|
|
|
5580
5706
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
5581
5707
|
async function assertLocalStillScanned(localPath, scanned) {
|
|
5582
5708
|
const snapshot = await hashDriveFile(localPath).catch((error) => {
|
|
5583
|
-
if (
|
|
5709
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5584
5710
|
throw error;
|
|
5585
5711
|
});
|
|
5586
5712
|
if (!snapshot || snapshot.sha256 !== scanned.sha256 || snapshot.sizeBytes !== scanned.size_bytes) {
|
|
@@ -5608,7 +5734,7 @@ async function installMergedLocalFile(root, path, tmp, scanned, mergedSha256, me
|
|
|
5608
5734
|
await rename2(target, backup);
|
|
5609
5735
|
onLocalMutation();
|
|
5610
5736
|
} catch (error) {
|
|
5611
|
-
if (
|
|
5737
|
+
if (isNotFoundError2(error)) {
|
|
5612
5738
|
throw new Error("local file changed after scan");
|
|
5613
5739
|
}
|
|
5614
5740
|
throw error;
|
|
@@ -5646,10 +5772,10 @@ async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBy
|
|
|
5646
5772
|
try {
|
|
5647
5773
|
await rename2(target, quarantine);
|
|
5648
5774
|
} catch (error) {
|
|
5649
|
-
if (!
|
|
5775
|
+
if (!isNotFoundError2(error)) throw error;
|
|
5650
5776
|
}
|
|
5651
5777
|
const quarantineDigest = await hashDriveFile(quarantine).catch((error) => {
|
|
5652
|
-
if (
|
|
5778
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5653
5779
|
throw error;
|
|
5654
5780
|
});
|
|
5655
5781
|
if (quarantineDigest !== void 0 && (quarantineDigest.sha256 !== mergedSha256 || quarantineDigest.sizeBytes !== mergedSizeBytes)) {
|
|
@@ -5711,7 +5837,7 @@ async function installDownloadedFile(root, path, tmp, entry, onLocalMutation) {
|
|
|
5711
5837
|
await rename2(target, backup);
|
|
5712
5838
|
onLocalMutation();
|
|
5713
5839
|
} catch (error) {
|
|
5714
|
-
if (!
|
|
5840
|
+
if (!isNotFoundError2(error)) throw error;
|
|
5715
5841
|
await installNoOverwrite(tmp, target, onLocalMutation);
|
|
5716
5842
|
return;
|
|
5717
5843
|
}
|
|
@@ -5756,7 +5882,7 @@ async function removeLocalIfStillBase(root, path, entry, onLocalMutation) {
|
|
|
5756
5882
|
await rename2(target, backup);
|
|
5757
5883
|
onLocalMutation();
|
|
5758
5884
|
} catch (error) {
|
|
5759
|
-
if (
|
|
5885
|
+
if (isNotFoundError2(error)) {
|
|
5760
5886
|
throw new Error("local file changed before delete");
|
|
5761
5887
|
}
|
|
5762
5888
|
throw error;
|
|
@@ -5794,13 +5920,13 @@ async function restoreBackupWhenPossible(backup, target) {
|
|
|
5794
5920
|
return true;
|
|
5795
5921
|
} catch (error) {
|
|
5796
5922
|
if (isAlreadyExistsError(error)) return false;
|
|
5797
|
-
if (
|
|
5923
|
+
if (isNotFoundError2(error)) return true;
|
|
5798
5924
|
return false;
|
|
5799
5925
|
}
|
|
5800
5926
|
}
|
|
5801
5927
|
async function localFileExists(path) {
|
|
5802
5928
|
const digest = await hashDriveFile(path).catch((error) => {
|
|
5803
|
-
if (
|
|
5929
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5804
5930
|
throw error;
|
|
5805
5931
|
});
|
|
5806
5932
|
return digest !== void 0;
|
|
@@ -5816,14 +5942,14 @@ async function readStableUploadBody(localPath, scanned) {
|
|
|
5816
5942
|
throw new Error("local file missing from scan");
|
|
5817
5943
|
}
|
|
5818
5944
|
const snapshot = await hashDriveFile(localPath).catch((error) => {
|
|
5819
|
-
if (
|
|
5945
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5820
5946
|
throw error;
|
|
5821
5947
|
});
|
|
5822
5948
|
if (!snapshot || snapshot.sha256 !== scanned.sha256 || snapshot.sizeBytes !== scanned.size_bytes) {
|
|
5823
5949
|
throw new Error("local file changed after scan");
|
|
5824
5950
|
}
|
|
5825
5951
|
const body = await readFile3(localPath).catch((error) => {
|
|
5826
|
-
if (
|
|
5952
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5827
5953
|
throw error;
|
|
5828
5954
|
});
|
|
5829
5955
|
if (!body) {
|
|
@@ -5840,7 +5966,7 @@ async function readStableUploadBody(localPath, scanned) {
|
|
|
5840
5966
|
async function assertLocalSafeForDownload(root, path, entry) {
|
|
5841
5967
|
const target = resolveInsideRoot(root, path);
|
|
5842
5968
|
const digest = await hashDriveFile(target).catch((error) => {
|
|
5843
|
-
if (
|
|
5969
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5844
5970
|
throw error;
|
|
5845
5971
|
});
|
|
5846
5972
|
if (!digest) return;
|
|
@@ -5853,14 +5979,14 @@ async function assertLocalSafeForDownload(root, path, entry) {
|
|
|
5853
5979
|
}
|
|
5854
5980
|
async function assertLocalAbsentBeforeRemoteDelete(root, path) {
|
|
5855
5981
|
const digest = await hashDriveFile(resolveInsideRoot(root, path)).catch((error) => {
|
|
5856
|
-
if (
|
|
5982
|
+
if (isNotFoundError2(error)) return void 0;
|
|
5857
5983
|
throw error;
|
|
5858
5984
|
});
|
|
5859
5985
|
if (digest) {
|
|
5860
5986
|
throw new Error("local file appeared before remote delete");
|
|
5861
5987
|
}
|
|
5862
5988
|
}
|
|
5863
|
-
function
|
|
5989
|
+
function isNotFoundError2(error) {
|
|
5864
5990
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
5865
5991
|
}
|
|
5866
5992
|
function isAlreadyExistsError(error) {
|
|
@@ -5884,32 +6010,63 @@ function emptySummary() {
|
|
|
5884
6010
|
function isActionableAction(action) {
|
|
5885
6011
|
return action.type !== "unchanged" && action.type !== "state_only" && action.type !== "remove_state";
|
|
5886
6012
|
}
|
|
5887
|
-
async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress, debug = noopDriveDebugLogger) {
|
|
6013
|
+
async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress, debug = noopDriveDebugLogger, options = {}) {
|
|
5888
6014
|
return withDriveLock(root, async () => {
|
|
5889
6015
|
let state = await readDriveState(root);
|
|
5890
|
-
const syncApi = api ?? await createDriveApi();
|
|
6016
|
+
const syncApi = api ?? await createDriveApi({ clientId: state.realtime?.client_id });
|
|
5891
6017
|
const summary = emptySummary();
|
|
5892
6018
|
const blockedPaths = /* @__PURE__ */ new Set();
|
|
5893
6019
|
const scanStartedMs = Date.now();
|
|
5894
|
-
const
|
|
6020
|
+
const nextScanCache = {};
|
|
6021
|
+
const scanOptions = {
|
|
6022
|
+
cache: state.scan_cache,
|
|
6023
|
+
onCacheUpdate: (path, entry) => {
|
|
6024
|
+
nextScanCache[path] = entry;
|
|
6025
|
+
},
|
|
5895
6026
|
onPathError: async (path, error) => {
|
|
5896
|
-
await recordPathError(summary, blockedPaths, path, error);
|
|
6027
|
+
await recordPathError(summary, blockedPaths, path, error, { debug, op: "scan" });
|
|
5897
6028
|
}
|
|
5898
|
-
}
|
|
6029
|
+
};
|
|
6030
|
+
const useIncrementalScan = options.dirtyPaths !== void 0 && state.scan_cache !== void 0;
|
|
6031
|
+
const localFiles = useIncrementalScan ? await rescanDriveFiles(root, options.dirtyPaths, scanOptions) : await scanDriveFiles(root, scanOptions);
|
|
6032
|
+
if (JSON.stringify(state.scan_cache ?? {}) !== JSON.stringify(nextScanCache)) {
|
|
6033
|
+
state = { ...state, scan_cache: nextScanCache };
|
|
6034
|
+
await writeDriveState(root, state, clock);
|
|
6035
|
+
}
|
|
5899
6036
|
const scanMs = Date.now() - scanStartedMs;
|
|
5900
6037
|
const manifestStartedMs = Date.now();
|
|
5901
|
-
const
|
|
6038
|
+
const manifest = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths, debug);
|
|
6039
|
+
const remoteFiles = manifest.remoteFiles;
|
|
6040
|
+
if (manifest.manifestCursor !== void 0 && manifest.manifestCursor !== state.manifest_cursor) {
|
|
6041
|
+
state = { ...state, manifest_cursor: manifest.manifestCursor };
|
|
6042
|
+
await writeDriveState(root, state, clock);
|
|
6043
|
+
}
|
|
5902
6044
|
const manifestMs = Date.now() - manifestStartedMs;
|
|
5903
6045
|
const paths = Array.from(
|
|
5904
6046
|
/* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
|
|
5905
6047
|
).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
|
|
6048
|
+
const movedPaths = await applyRenamesAsMoves({
|
|
6049
|
+
root,
|
|
6050
|
+
state,
|
|
6051
|
+
api: syncApi,
|
|
6052
|
+
paths,
|
|
6053
|
+
localFiles,
|
|
6054
|
+
remoteFiles,
|
|
6055
|
+
summary,
|
|
6056
|
+
clock,
|
|
6057
|
+
debug,
|
|
6058
|
+
onStateChange: (nextState) => {
|
|
6059
|
+
state = nextState;
|
|
6060
|
+
}
|
|
6061
|
+
});
|
|
5906
6062
|
const total = paths.filter(
|
|
5907
|
-
(path) => isActionableAction(decideDriveAction(state.entries[path], localFiles[path], remoteFiles[path]))
|
|
6063
|
+
(path) => !movedPaths.has(path) && isActionableAction(decideDriveAction(state.entries[path], localFiles[path], remoteFiles[path]))
|
|
5908
6064
|
).length;
|
|
5909
6065
|
let processed = 0;
|
|
5910
6066
|
onProgress?.(processed, total);
|
|
5911
6067
|
const processStartedMs = Date.now();
|
|
5912
6068
|
for (const path of paths) {
|
|
6069
|
+
if (movedPaths.has(path)) continue;
|
|
5913
6070
|
const remote = remoteFiles[path];
|
|
5914
6071
|
const local = localFiles[path];
|
|
5915
6072
|
const action = decideDriveAction(state.entries[path], local, remote);
|
|
@@ -5917,7 +6074,7 @@ async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress,
|
|
|
5917
6074
|
debug.log("decision", decisionFields(path, action, state.entries[path], local, remote));
|
|
5918
6075
|
}
|
|
5919
6076
|
const previousConflict = state.conflicts[path];
|
|
5920
|
-
const result = await processPath({ root, state, api: syncApi, path, action, remote, local, summary, clock });
|
|
6077
|
+
const result = await processPath({ root, state, api: syncApi, path, action, remote, local, summary, clock, debug });
|
|
5921
6078
|
state = result.state;
|
|
5922
6079
|
const recordedConflict = state.conflicts[path];
|
|
5923
6080
|
if (recordedConflict !== void 0 && recordedConflict !== previousConflict) {
|
|
@@ -5961,24 +6118,64 @@ function driveSyncCommand(api) {
|
|
|
5961
6118
|
});
|
|
5962
6119
|
return sync;
|
|
5963
6120
|
}
|
|
5964
|
-
async function fetchRemoteManifest(root, state, api, summary, blockedPaths) {
|
|
6121
|
+
async function fetchRemoteManifest(root, state, api, summary, blockedPaths, debug) {
|
|
6122
|
+
if (state.manifest_cursor !== void 0) {
|
|
6123
|
+
const delta = await api.getManifest(state.library_id, void 0, state.manifest_cursor);
|
|
6124
|
+
if (delta.resync_required !== true) {
|
|
6125
|
+
const remoteFiles = remoteViewFromState(state);
|
|
6126
|
+
const changed = delta.entries.filter((entry) => entry.deleted_at === void 0);
|
|
6127
|
+
const normalized2 = normalizeRemoteManifest(root, changed);
|
|
6128
|
+
for (const pathError of normalized2.pathErrors) {
|
|
6129
|
+
await recordPathError(summary, blockedPaths, pathError.path, pathError.error, {
|
|
6130
|
+
appendPathResult: pathError.appendPathResult,
|
|
6131
|
+
debug,
|
|
6132
|
+
op: "manifest"
|
|
6133
|
+
});
|
|
6134
|
+
}
|
|
6135
|
+
for (const entry of delta.entries) {
|
|
6136
|
+
if (entry.deleted_at !== void 0) delete remoteFiles[entry.path];
|
|
6137
|
+
}
|
|
6138
|
+
Object.assign(remoteFiles, normalized2.remoteFiles);
|
|
6139
|
+
return { remoteFiles, manifestCursor: delta.latest_cursor ?? state.manifest_cursor };
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
5965
6142
|
const entries = [];
|
|
5966
6143
|
let cursor;
|
|
6144
|
+
let latestCursor;
|
|
5967
6145
|
do {
|
|
5968
6146
|
const page = await api.getManifest(state.library_id, cursor);
|
|
5969
6147
|
entries.push(...page.entries);
|
|
6148
|
+
if (page.latest_cursor !== void 0) latestCursor = page.latest_cursor;
|
|
5970
6149
|
cursor = page.next_cursor ?? void 0;
|
|
5971
6150
|
} while (cursor !== void 0);
|
|
5972
6151
|
const normalized = normalizeRemoteManifest(root, entries);
|
|
5973
6152
|
for (const pathError of normalized.pathErrors) {
|
|
5974
6153
|
await recordPathError(summary, blockedPaths, pathError.path, pathError.error, {
|
|
5975
|
-
appendPathResult: pathError.appendPathResult
|
|
6154
|
+
appendPathResult: pathError.appendPathResult,
|
|
6155
|
+
debug,
|
|
6156
|
+
op: "manifest"
|
|
5976
6157
|
});
|
|
5977
6158
|
}
|
|
5978
|
-
return normalized.remoteFiles;
|
|
6159
|
+
return { remoteFiles: normalized.remoteFiles, manifestCursor: latestCursor };
|
|
6160
|
+
}
|
|
6161
|
+
function remoteViewFromState(state) {
|
|
6162
|
+
const remoteFiles = {};
|
|
6163
|
+
for (const [path, entry] of Object.entries(state.entries)) {
|
|
6164
|
+
remoteFiles[path] = {
|
|
6165
|
+
id: entry.entry_id,
|
|
6166
|
+
path,
|
|
6167
|
+
kind: "file",
|
|
6168
|
+
entry_version: entry.entry_version,
|
|
6169
|
+
size_bytes: entry.size_bytes,
|
|
6170
|
+
updated_at: entry.last_synced_at,
|
|
6171
|
+
...entry.current_version_id === void 0 ? {} : { current_version_id: entry.current_version_id },
|
|
6172
|
+
...entry.content_sha256 === void 0 ? {} : { content_sha256: entry.content_sha256 }
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
return remoteFiles;
|
|
5979
6176
|
}
|
|
5980
6177
|
async function processPath(args) {
|
|
5981
|
-
const { root, state, api, path, action, remote, local, summary, clock } = args;
|
|
6178
|
+
const { root, state, api, path, action, remote, local, summary, clock, debug } = args;
|
|
5982
6179
|
summary.paths.push({ path, action: action.type });
|
|
5983
6180
|
let durableStateRequired = false;
|
|
5984
6181
|
try {
|
|
@@ -6079,35 +6276,17 @@ async function processPath(args) {
|
|
|
6079
6276
|
summary.conflicts += 1;
|
|
6080
6277
|
return { state: nextState2, stop: false };
|
|
6081
6278
|
}
|
|
6082
|
-
const
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
path,
|
|
6087
|
-
reason: action.reason,
|
|
6088
|
-
type: "edit_edit",
|
|
6089
|
-
remote,
|
|
6090
|
-
clock
|
|
6091
|
-
});
|
|
6092
|
-
if (conflictCopyState) {
|
|
6093
|
-
summary.conflicts += 1;
|
|
6094
|
-
return { state: conflictCopyState, stop: false };
|
|
6279
|
+
const resolved = await resolveConflictWithLocalAsMain({ root, state, api, path, remote, local, clock });
|
|
6280
|
+
if (resolved) {
|
|
6281
|
+
recordResolvedConflict(summary, debug, path, resolved.copyPath, action.reason, "edit_edit");
|
|
6282
|
+
return { state: resolved.state, stop: false };
|
|
6095
6283
|
}
|
|
6096
6284
|
}
|
|
6097
6285
|
if (action.reason === "local_and_remote_without_base") {
|
|
6098
|
-
const
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
path,
|
|
6103
|
-
reason: action.reason,
|
|
6104
|
-
type: "create_create",
|
|
6105
|
-
remote,
|
|
6106
|
-
clock
|
|
6107
|
-
});
|
|
6108
|
-
if (conflictCopyState) {
|
|
6109
|
-
summary.conflicts += 1;
|
|
6110
|
-
return { state: conflictCopyState, stop: false };
|
|
6286
|
+
const resolved = await resolveConflictWithLocalAsMain({ root, state, api, path, remote, local, clock });
|
|
6287
|
+
if (resolved) {
|
|
6288
|
+
recordResolvedConflict(summary, debug, path, resolved.copyPath, action.reason, "create_create");
|
|
6289
|
+
return { state: resolved.state, stop: false };
|
|
6111
6290
|
}
|
|
6112
6291
|
}
|
|
6113
6292
|
if (action.reason === "local_changed_remote_deleted") {
|
|
@@ -6147,11 +6326,11 @@ async function processPath(args) {
|
|
|
6147
6326
|
summary.paths[summary.paths.length - 1] = { path, action: "conflict" };
|
|
6148
6327
|
return { state: nextState, stop: false };
|
|
6149
6328
|
} catch (writeError) {
|
|
6150
|
-
await recordPathError(summary, void 0, path, writeError);
|
|
6329
|
+
await recordPathError(summary, void 0, path, writeError, { debug, op: "process" });
|
|
6151
6330
|
return { state, stop: durableStateRequired };
|
|
6152
6331
|
}
|
|
6153
6332
|
}
|
|
6154
|
-
await recordPathError(summary, void 0, path, error);
|
|
6333
|
+
await recordPathError(summary, void 0, path, error, { debug, op: "process" });
|
|
6155
6334
|
return { state, stop: durableStateRequired };
|
|
6156
6335
|
}
|
|
6157
6336
|
return { state, stop: false };
|
|
@@ -6213,6 +6392,84 @@ async function downloadBytes(api, libraryId, path, versionId) {
|
|
|
6213
6392
|
const response = await api.downloadFile(libraryId, path, versionId);
|
|
6214
6393
|
return new Uint8Array(await response.arrayBuffer());
|
|
6215
6394
|
}
|
|
6395
|
+
async function applyRenamesAsMoves(args) {
|
|
6396
|
+
const { root, api, paths, localFiles, remoteFiles, summary, clock, debug, onStateChange } = args;
|
|
6397
|
+
const movedPaths = /* @__PURE__ */ new Set();
|
|
6398
|
+
if (api.moveFile === void 0) return movedPaths;
|
|
6399
|
+
let state = args.state;
|
|
6400
|
+
const deletesBySha = /* @__PURE__ */ new Map();
|
|
6401
|
+
const createsBySha = /* @__PURE__ */ new Map();
|
|
6402
|
+
for (const path of paths) {
|
|
6403
|
+
const action = decideDriveAction(state.entries[path], localFiles[path], remoteFiles[path]);
|
|
6404
|
+
if (action.type === "delete_remote") {
|
|
6405
|
+
const sha = state.entries[path]?.last_local_sha256 ?? state.entries[path]?.content_sha256;
|
|
6406
|
+
if (sha !== void 0) deletesBySha.set(sha, [...deletesBySha.get(sha) ?? [], path]);
|
|
6407
|
+
}
|
|
6408
|
+
if (action.type === "upload_create") {
|
|
6409
|
+
const sha = localFiles[path]?.sha256;
|
|
6410
|
+
if (sha !== void 0) createsBySha.set(sha, [...createsBySha.get(sha) ?? [], path]);
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
for (const [sha, fromPaths] of deletesBySha) {
|
|
6414
|
+
const toPaths = createsBySha.get(sha);
|
|
6415
|
+
if (fromPaths.length !== 1 || toPaths === void 0 || toPaths.length !== 1) continue;
|
|
6416
|
+
const fromPath = fromPaths[0];
|
|
6417
|
+
const toPath = toPaths[0];
|
|
6418
|
+
const entry = state.entries[fromPath];
|
|
6419
|
+
const local = localFiles[toPath];
|
|
6420
|
+
if (entry === void 0 || local === void 0) continue;
|
|
6421
|
+
try {
|
|
6422
|
+
const moved = await api.moveFile(state.library_id, fromPath, toPath, entry.entry_version);
|
|
6423
|
+
const nextState = cloneDriveState(state);
|
|
6424
|
+
delete nextState.entries[fromPath];
|
|
6425
|
+
delete nextState.conflicts[fromPath];
|
|
6426
|
+
nextState.entries[toPath] = stateEntryFromRemote(moved.entry, local.sha256, clock);
|
|
6427
|
+
delete nextState.conflicts[toPath];
|
|
6428
|
+
await writeDriveState(root, nextState, clock);
|
|
6429
|
+
state = nextState;
|
|
6430
|
+
onStateChange(nextState);
|
|
6431
|
+
movedPaths.add(fromPath);
|
|
6432
|
+
movedPaths.add(toPath);
|
|
6433
|
+
summary.paths.push({ path: toPath, action: "move" });
|
|
6434
|
+
debug.log("decision", { path: toPath, action: "move", from_path: fromPath });
|
|
6435
|
+
} catch (error) {
|
|
6436
|
+
debug.log("error", { path: toPath, op: "move", message: errorMessage(error) });
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
return movedPaths;
|
|
6440
|
+
}
|
|
6441
|
+
function recordResolvedConflict(summary, debug, path, copyPath, reason, type) {
|
|
6442
|
+
summary.conflicts += 1;
|
|
6443
|
+
summary.conflict_paths.push(copyPath);
|
|
6444
|
+
const lastPath = summary.paths.at(-1);
|
|
6445
|
+
if (lastPath?.path === path) {
|
|
6446
|
+
lastPath.conflict_paths = [copyPath];
|
|
6447
|
+
}
|
|
6448
|
+
debug.log("conflict", { path, reason, type, strategy: "conflict_copy", conflict_paths: [copyPath] });
|
|
6449
|
+
}
|
|
6450
|
+
async function resolveConflictWithLocalAsMain(args) {
|
|
6451
|
+
const { root, state, api, path, remote, local, clock } = args;
|
|
6452
|
+
const remoteVersionId = remote?.current_version_id;
|
|
6453
|
+
if (!remote || remoteVersionId === void 0 || !local) {
|
|
6454
|
+
return void 0;
|
|
6455
|
+
}
|
|
6456
|
+
const previous = state.conflicts[path];
|
|
6457
|
+
let copyPath;
|
|
6458
|
+
if (previous !== void 0 && await canReuseConflictCopy(root, previous, remoteVersionId)) {
|
|
6459
|
+
copyPath = previous.conflict_paths[0];
|
|
6460
|
+
} else {
|
|
6461
|
+
const remoteBytes = await downloadBytes(api, state.library_id, path, remoteVersionId);
|
|
6462
|
+
copyPath = await writeConflictCopy(root, path, "remote", remoteVersionId, remoteBytes, clock);
|
|
6463
|
+
}
|
|
6464
|
+
const localPath = resolveInsideRoot(root, path);
|
|
6465
|
+
const { body, digest } = await readStableUploadBody(localPath, local);
|
|
6466
|
+
const uploaded = await api.uploadFile(state.library_id, path, body, digest, remote.entry_version);
|
|
6467
|
+
const nextState = cloneDriveState(state);
|
|
6468
|
+
nextState.entries[path] = stateEntryFromRemote(uploaded.entry, digest, clock);
|
|
6469
|
+
delete nextState.conflicts[path];
|
|
6470
|
+
await writeDriveState(root, nextState, clock);
|
|
6471
|
+
return { state: nextState, copyPath };
|
|
6472
|
+
}
|
|
6216
6473
|
async function recordRemoteConflictCopy(args) {
|
|
6217
6474
|
const { root, state, api, path, reason, type, remote, clock } = args;
|
|
6218
6475
|
const entry = state.entries[path];
|
|
@@ -6288,12 +6545,12 @@ function conflictCopyPathWithSuffix(path, suffix) {
|
|
|
6288
6545
|
if (suffix === 1) {
|
|
6289
6546
|
return path;
|
|
6290
6547
|
}
|
|
6291
|
-
const parsed =
|
|
6548
|
+
const parsed = pathPosix3.parse(path);
|
|
6292
6549
|
const fileName = `${parsed.name}-${suffix}${parsed.ext}`;
|
|
6293
6550
|
if (parsed.dir === "") {
|
|
6294
6551
|
return fileName;
|
|
6295
6552
|
}
|
|
6296
|
-
return
|
|
6553
|
+
return pathPosix3.join(parsed.dir, fileName);
|
|
6297
6554
|
}
|
|
6298
6555
|
function isExpectedVersionDownloadMissing(error) {
|
|
6299
6556
|
const structured = error;
|
|
@@ -6371,7 +6628,13 @@ async function recordPathError(summary, blockedPaths, path, error, options = {})
|
|
|
6371
6628
|
} else {
|
|
6372
6629
|
summary.paths.push({ path, action: "error" });
|
|
6373
6630
|
}
|
|
6374
|
-
void
|
|
6631
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
6632
|
+
options.debug?.log("error", {
|
|
6633
|
+
path,
|
|
6634
|
+
...options.op === void 0 ? {} : { op: options.op },
|
|
6635
|
+
...code === void 0 ? {} : { code },
|
|
6636
|
+
message: errorMessage(error)
|
|
6637
|
+
});
|
|
6375
6638
|
summary.errors += 1;
|
|
6376
6639
|
}
|
|
6377
6640
|
function conflict(reason, remote, clock) {
|
|
@@ -6407,7 +6670,7 @@ function containsVersionConflict(value) {
|
|
|
6407
6670
|
import { Command as Command81 } from "commander";
|
|
6408
6671
|
import chokidar from "chokidar";
|
|
6409
6672
|
import { watch as fsWatch } from "fs";
|
|
6410
|
-
import { relative as relative2, resolve as resolve4 } from "path";
|
|
6673
|
+
import { basename as basename4, relative as relative2, resolve as resolve4 } from "path";
|
|
6411
6674
|
|
|
6412
6675
|
// src/handwritten/commands/drive/realtime.ts
|
|
6413
6676
|
function createDriveRealtimeSource(args) {
|
|
@@ -6504,12 +6767,15 @@ function createDriveRealtimeSource(args) {
|
|
|
6504
6767
|
return;
|
|
6505
6768
|
}
|
|
6506
6769
|
if (message.type === "library_changed") {
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6770
|
+
const ownEcho = message.origin_client_id !== void 0 && message.origin_client_id === currentRealtime.client_id;
|
|
6771
|
+
if (!ownEcho) {
|
|
6772
|
+
handlers?.onEvent({
|
|
6773
|
+
debounce_ms: 2e3,
|
|
6774
|
+
reason: "library_changed",
|
|
6775
|
+
...message.cursor === void 0 ? {} : { cursor: message.cursor },
|
|
6776
|
+
...message.path === void 0 ? {} : { path: message.path }
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
6513
6779
|
if (message.cursor !== void 0) {
|
|
6514
6780
|
await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor, last_event_at: driveIsoTimestamp(clock) });
|
|
6515
6781
|
}
|
|
@@ -6596,7 +6862,8 @@ function parseDriveRealtimeMessage(raw) {
|
|
|
6596
6862
|
return {
|
|
6597
6863
|
type: "library_changed",
|
|
6598
6864
|
...cursor === void 0 ? {} : { cursor },
|
|
6599
|
-
...typeof value.path === "string" ? { path: value.path } : {}
|
|
6865
|
+
...typeof value.path === "string" ? { path: value.path } : {},
|
|
6866
|
+
...typeof value.origin_client_id === "string" ? { origin_client_id: value.origin_client_id } : {}
|
|
6600
6867
|
};
|
|
6601
6868
|
}
|
|
6602
6869
|
if (messageType === "resync_required") {
|
|
@@ -6706,7 +6973,7 @@ function webSocketError(event) {
|
|
|
6706
6973
|
// src/handwritten/commands/drive/watch.ts
|
|
6707
6974
|
async function runDriveWatch(root, options = {}) {
|
|
6708
6975
|
const dbg = options.debugLogger ?? (options.debug ? createDriveDebugLogger(root) : noopDriveDebugLogger);
|
|
6709
|
-
const runSync = options.runSync ?? ((syncRoot, onProgress) => runDriveSyncOnce(syncRoot, void 0, void 0, onProgress, dbg));
|
|
6976
|
+
const runSync = options.runSync ?? ((syncRoot, onProgress, dirtyPaths2) => runDriveSyncOnce(syncRoot, void 0, void 0, onProgress, dbg, dirtyPaths2 === void 0 ? {} : { dirtyPaths: dirtyPaths2 }));
|
|
6710
6977
|
const debounceMs = options.debounceMs ?? 500;
|
|
6711
6978
|
const remoteDebounceMs = options.remoteDebounceMs ?? 2e3;
|
|
6712
6979
|
const emit = options.onEvent ?? ((event) => {
|
|
@@ -6718,6 +6985,7 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6718
6985
|
render({ kind: "drive_watch", display: { shape: "object" } }, event);
|
|
6719
6986
|
});
|
|
6720
6987
|
let nextTrigger = "initial";
|
|
6988
|
+
const dirtyPaths = /* @__PURE__ */ new Set();
|
|
6721
6989
|
let debounceTimer;
|
|
6722
6990
|
let debounceDeadlineMs;
|
|
6723
6991
|
let retryTimer;
|
|
@@ -6744,9 +7012,15 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6744
7012
|
emit({ kind: "drive_sync_start" });
|
|
6745
7013
|
dbg.log("sync_start", { trigger: nextTrigger });
|
|
6746
7014
|
const startedAtMs = Date.now();
|
|
6747
|
-
const
|
|
6748
|
-
|
|
6749
|
-
|
|
7015
|
+
const dirtySnapshot = nextTrigger === "local" ? [...dirtyPaths] : void 0;
|
|
7016
|
+
dirtyPaths.clear();
|
|
7017
|
+
const summary = await runSync(
|
|
7018
|
+
root,
|
|
7019
|
+
(processed, total) => {
|
|
7020
|
+
emit({ kind: "drive_sync_progress", processed, total });
|
|
7021
|
+
},
|
|
7022
|
+
dirtySnapshot
|
|
7023
|
+
);
|
|
6750
7024
|
emit({ kind: "drive_sync_once", ...summary });
|
|
6751
7025
|
dbg.log("sync_end", {
|
|
6752
7026
|
duration_ms: Date.now() - startedAtMs,
|
|
@@ -6853,7 +7127,10 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6853
7127
|
source = options.source ?? createDefaultWatchSource(root);
|
|
6854
7128
|
source.onChange((path) => {
|
|
6855
7129
|
if (isDriveInternalPath(root, path)) return;
|
|
6856
|
-
|
|
7130
|
+
if (isInternalSyncArtifactName(basename4(path))) return;
|
|
7131
|
+
const drivePath = relative2(root, path).replace(/\\/g, "/");
|
|
7132
|
+
dirtyPaths.add(drivePath);
|
|
7133
|
+
dbg.log("fs_event", { path: drivePath });
|
|
6857
7134
|
scheduleSync(debounceMs, "local");
|
|
6858
7135
|
});
|
|
6859
7136
|
emit({ kind: "drive_watch_started", root, library_id: state.library_id });
|