@wspc/cli 0.1.14 → 0.1.15
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 +111 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1520,9 +1520,9 @@ function createConsistencyFetch(opts) {
|
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
1522
|
// src/version.ts
|
|
1523
|
-
var VERSION = "0.1.
|
|
1523
|
+
var VERSION = "0.1.15";
|
|
1524
1524
|
var SPEC_SHA = "bec760cd";
|
|
1525
|
-
var SPEC_FETCHED_AT = "2026-07-
|
|
1525
|
+
var SPEC_FETCHED_AT = "2026-07-13T04:21:22.596Z";
|
|
1526
1526
|
var API_BASE = "https://api.wspc.ai";
|
|
1527
1527
|
|
|
1528
1528
|
// src/index.ts
|
|
@@ -4851,6 +4851,7 @@ async function expectJsonResult(result) {
|
|
|
4851
4851
|
if (result.data == null) throw new Error("empty response");
|
|
4852
4852
|
return result.data;
|
|
4853
4853
|
}
|
|
4854
|
+
var SYNC_CLIENT_HEADERS = { "x-wspc-client": "drive-sync" };
|
|
4854
4855
|
function driveContentUrl(baseUrl, id) {
|
|
4855
4856
|
const baseWithTrailingSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
4856
4857
|
return new URL(`drive/libraries/${encodeURIComponent(id)}/files/content`, baseWithTrailingSlash);
|
|
@@ -4878,6 +4879,7 @@ async function createDriveApi(opts = {}) {
|
|
|
4878
4879
|
const result = await driveFileDelete({
|
|
4879
4880
|
client: rawClient,
|
|
4880
4881
|
path: { id },
|
|
4882
|
+
headers: SYNC_CLIENT_HEADERS,
|
|
4881
4883
|
body: {
|
|
4882
4884
|
path,
|
|
4883
4885
|
expected_entry_version: expectedEntryVersion
|
|
@@ -4895,7 +4897,8 @@ async function createDriveApi(opts = {}) {
|
|
|
4895
4897
|
method: "PUT",
|
|
4896
4898
|
headers: {
|
|
4897
4899
|
"content-type": "application/octet-stream",
|
|
4898
|
-
"x-drive-content-sha256": sha256
|
|
4900
|
+
"x-drive-content-sha256": sha256,
|
|
4901
|
+
...SYNC_CLIENT_HEADERS
|
|
4899
4902
|
},
|
|
4900
4903
|
body
|
|
4901
4904
|
});
|
|
@@ -5155,9 +5158,44 @@ function driveBindCommand() {
|
|
|
5155
5158
|
// src/handwritten/commands/drive/sync.ts
|
|
5156
5159
|
import { Command as Command80 } from "commander";
|
|
5157
5160
|
import { mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
5158
|
-
import { basename as basename3, dirname as dirname2, join as
|
|
5161
|
+
import { basename as basename3, dirname as dirname2, join as join6, posix as pathPosix2, resolve as resolve3 } from "path";
|
|
5159
5162
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
5160
5163
|
|
|
5164
|
+
// src/handwritten/commands/drive/debug-log.ts
|
|
5165
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
5166
|
+
import { join as join3 } from "path";
|
|
5167
|
+
var noopDriveDebugLogger = { log() {
|
|
5168
|
+
} };
|
|
5169
|
+
var DEBUG_LOG_FILE = "debug.log";
|
|
5170
|
+
var MAX_DEBUG_LOG_BYTES = 10 * 1024 * 1024;
|
|
5171
|
+
function createDriveDebugLogger(root, options = {}) {
|
|
5172
|
+
const clock = options.clock ?? systemDriveClock;
|
|
5173
|
+
const maxBytes = options.maxBytes ?? MAX_DEBUG_LOG_BYTES;
|
|
5174
|
+
const dir = join3(root, DRIVE_DIR);
|
|
5175
|
+
const file = join3(dir, DEBUG_LOG_FILE);
|
|
5176
|
+
let warned = false;
|
|
5177
|
+
return {
|
|
5178
|
+
log(event, fields = {}) {
|
|
5179
|
+
try {
|
|
5180
|
+
mkdirSync(dir, { recursive: true });
|
|
5181
|
+
try {
|
|
5182
|
+
if (statSync(file).size > maxBytes) renameSync(file, `${file}.old`);
|
|
5183
|
+
} catch {
|
|
5184
|
+
}
|
|
5185
|
+
appendFileSync(file, `${JSON.stringify({ ts: driveIsoTimestamp(clock), event, ...fields })}
|
|
5186
|
+
`);
|
|
5187
|
+
} catch (error) {
|
|
5188
|
+
if (!warned) {
|
|
5189
|
+
warned = true;
|
|
5190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5191
|
+
process.stderr.write(`wspc drive: debug log write failed: ${message}
|
|
5192
|
+
`);
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5161
5199
|
// src/handwritten/commands/drive/decision.ts
|
|
5162
5200
|
function decideDriveAction(entry, local, remote) {
|
|
5163
5201
|
if (!entry) return decideWithoutBase(local, remote);
|
|
@@ -5417,7 +5455,7 @@ function safeShortVersionId(versionId) {
|
|
|
5417
5455
|
import { createHash } from "crypto";
|
|
5418
5456
|
import { constants as fsConstants } from "fs";
|
|
5419
5457
|
import { open as open2, readdir, lstat } from "fs/promises";
|
|
5420
|
-
import { join as
|
|
5458
|
+
import { join as join4 } from "path";
|
|
5421
5459
|
async function scanDriveFiles(root, options = {}) {
|
|
5422
5460
|
const candidates = [];
|
|
5423
5461
|
const files = {};
|
|
@@ -5443,7 +5481,7 @@ async function scanDriveFiles(root, options = {}) {
|
|
|
5443
5481
|
await options.onPathError(nextDrivePath, error);
|
|
5444
5482
|
continue;
|
|
5445
5483
|
}
|
|
5446
|
-
const nextPath =
|
|
5484
|
+
const nextPath = join4(currentPath, entry.name);
|
|
5447
5485
|
const stats = await lstat(nextPath);
|
|
5448
5486
|
if (stats.isSymbolicLink()) {
|
|
5449
5487
|
continue;
|
|
@@ -5536,7 +5574,7 @@ async function hashDriveFile(path) {
|
|
|
5536
5574
|
// src/handwritten/commands/drive/local-mutations.ts
|
|
5537
5575
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
5538
5576
|
import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
5539
|
-
import { basename as basename2, dirname, join as
|
|
5577
|
+
import { basename as basename2, dirname, join as join5 } from "path";
|
|
5540
5578
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5541
5579
|
import { Readable as Readable2, Transform } from "stream";
|
|
5542
5580
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
@@ -5552,7 +5590,7 @@ async function assertLocalStillScanned(localPath, scanned) {
|
|
|
5552
5590
|
async function writeMergedLocalFile(root, path, bytes, digest, scanned, onLocalMutation) {
|
|
5553
5591
|
const target = resolveInsideRoot(root, path);
|
|
5554
5592
|
await mkdir2(dirname(target), { recursive: true });
|
|
5555
|
-
const tmp =
|
|
5593
|
+
const tmp = join5(dirname(target), `.${basename2(target)}.wspc-merge-${randomUUID3()}.tmp`);
|
|
5556
5594
|
try {
|
|
5557
5595
|
await writeFile2(tmp, bytes, { flag: "wx" });
|
|
5558
5596
|
return await installMergedLocalFile(root, path, tmp, scanned, digest, bytes.byteLength, onLocalMutation);
|
|
@@ -5604,7 +5642,7 @@ async function installMergedLocalFile(root, path, tmp, scanned, mergedSha256, me
|
|
|
5604
5642
|
}
|
|
5605
5643
|
}
|
|
5606
5644
|
async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBytes, mergedSha256, mergedSizeBytes) {
|
|
5607
|
-
const quarantine =
|
|
5645
|
+
const quarantine = join5(dirname(target), `.${basename2(target)}.wspc-merge-restore-${randomUUID3()}.tmp`);
|
|
5608
5646
|
try {
|
|
5609
5647
|
await rename2(target, quarantine);
|
|
5610
5648
|
} catch (error) {
|
|
@@ -5634,7 +5672,7 @@ async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBy
|
|
|
5634
5672
|
async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
|
|
5635
5673
|
const target = resolveInsideRoot(root, path);
|
|
5636
5674
|
await mkdir2(dirname(target), { recursive: true });
|
|
5637
|
-
const tmp =
|
|
5675
|
+
const tmp = join5(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID3()}.tmp`);
|
|
5638
5676
|
try {
|
|
5639
5677
|
const response = await api.downloadFile(libraryId, path);
|
|
5640
5678
|
if (!response.body) {
|
|
@@ -5768,7 +5806,7 @@ async function localFileExists(path) {
|
|
|
5768
5806
|
return digest !== void 0;
|
|
5769
5807
|
}
|
|
5770
5808
|
function localMutationBackupPath(target) {
|
|
5771
|
-
return
|
|
5809
|
+
return join5(dirname(target), `.${basename2(target)}.wspc-backup-${randomUUID3()}.tmp`);
|
|
5772
5810
|
}
|
|
5773
5811
|
function expectedLocalBaseSha256(entry) {
|
|
5774
5812
|
return entry?.last_local_sha256 ?? entry?.content_sha256;
|
|
@@ -5846,18 +5884,22 @@ function emptySummary() {
|
|
|
5846
5884
|
function isActionableAction(action) {
|
|
5847
5885
|
return action.type !== "unchanged" && action.type !== "state_only" && action.type !== "remove_state";
|
|
5848
5886
|
}
|
|
5849
|
-
async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress) {
|
|
5887
|
+
async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress, debug = noopDriveDebugLogger) {
|
|
5850
5888
|
return withDriveLock(root, async () => {
|
|
5851
5889
|
let state = await readDriveState(root);
|
|
5852
5890
|
const syncApi = api ?? await createDriveApi();
|
|
5853
5891
|
const summary = emptySummary();
|
|
5854
5892
|
const blockedPaths = /* @__PURE__ */ new Set();
|
|
5893
|
+
const scanStartedMs = Date.now();
|
|
5855
5894
|
const localFiles = await scanDriveFiles(root, {
|
|
5856
5895
|
onPathError: async (path, error) => {
|
|
5857
5896
|
await recordPathError(summary, blockedPaths, path, error);
|
|
5858
5897
|
}
|
|
5859
5898
|
});
|
|
5899
|
+
const scanMs = Date.now() - scanStartedMs;
|
|
5900
|
+
const manifestStartedMs = Date.now();
|
|
5860
5901
|
const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
|
|
5902
|
+
const manifestMs = Date.now() - manifestStartedMs;
|
|
5861
5903
|
const paths = Array.from(
|
|
5862
5904
|
/* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
|
|
5863
5905
|
).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
|
|
@@ -5866,11 +5908,27 @@ async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress)
|
|
|
5866
5908
|
).length;
|
|
5867
5909
|
let processed = 0;
|
|
5868
5910
|
onProgress?.(processed, total);
|
|
5911
|
+
const processStartedMs = Date.now();
|
|
5869
5912
|
for (const path of paths) {
|
|
5870
5913
|
const remote = remoteFiles[path];
|
|
5871
|
-
const
|
|
5872
|
-
const
|
|
5914
|
+
const local = localFiles[path];
|
|
5915
|
+
const action = decideDriveAction(state.entries[path], local, remote);
|
|
5916
|
+
if (isActionableAction(action)) {
|
|
5917
|
+
debug.log("decision", decisionFields(path, action, state.entries[path], local, remote));
|
|
5918
|
+
}
|
|
5919
|
+
const previousConflict = state.conflicts[path];
|
|
5920
|
+
const result = await processPath({ root, state, api: syncApi, path, action, remote, local, summary, clock });
|
|
5873
5921
|
state = result.state;
|
|
5922
|
+
const recordedConflict = state.conflicts[path];
|
|
5923
|
+
if (recordedConflict !== void 0 && recordedConflict !== previousConflict) {
|
|
5924
|
+
debug.log("conflict", {
|
|
5925
|
+
path,
|
|
5926
|
+
reason: recordedConflict.reason,
|
|
5927
|
+
...recordedConflict.type === void 0 ? {} : { type: recordedConflict.type },
|
|
5928
|
+
...recordedConflict.strategy === void 0 ? {} : { strategy: recordedConflict.strategy },
|
|
5929
|
+
...recordedConflict.conflict_paths === void 0 ? {} : { conflict_paths: recordedConflict.conflict_paths }
|
|
5930
|
+
});
|
|
5931
|
+
}
|
|
5874
5932
|
if (isActionableAction(action)) {
|
|
5875
5933
|
processed += 1;
|
|
5876
5934
|
onProgress?.(processed, total);
|
|
@@ -5878,9 +5936,20 @@ async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress)
|
|
|
5878
5936
|
if (result.stop) break;
|
|
5879
5937
|
}
|
|
5880
5938
|
recordUnresolvedConflicts(summary, state);
|
|
5939
|
+
debug.log("sync_phases", { scan_ms: scanMs, manifest_ms: manifestMs, process_ms: Date.now() - processStartedMs });
|
|
5881
5940
|
return summary;
|
|
5882
5941
|
});
|
|
5883
5942
|
}
|
|
5943
|
+
function decisionFields(path, action, entry, local, remote) {
|
|
5944
|
+
return {
|
|
5945
|
+
path,
|
|
5946
|
+
action: action.type,
|
|
5947
|
+
..."reason" in action && action.reason !== void 0 ? { reason: action.reason } : {},
|
|
5948
|
+
...entry === void 0 ? {} : { base_version_id: entry.current_version_id, base_entry_version: entry.entry_version, base_sha256: entry.content_sha256 },
|
|
5949
|
+
...local === void 0 ? {} : { local_sha256: local.sha256, local_size_bytes: local.size_bytes },
|
|
5950
|
+
...remote === void 0 ? {} : { remote_version_id: remote.current_version_id, remote_entry_version: remote.entry_version, remote_sha256: remote.content_sha256 }
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
5884
5953
|
function driveSyncCommand(api) {
|
|
5885
5954
|
const sync = new Command80("sync").description("Drive sync commands");
|
|
5886
5955
|
sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
@@ -6194,7 +6263,7 @@ async function writeConflictCopy(root, path, side, versionId, bytes, clock) {
|
|
|
6194
6263
|
const target = resolveInsideRoot(root, candidate);
|
|
6195
6264
|
await mkdir3(dirname2(target), { recursive: true });
|
|
6196
6265
|
for (; ; ) {
|
|
6197
|
-
const tmp =
|
|
6266
|
+
const tmp = join6(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID4()}.tmp`);
|
|
6198
6267
|
let tmpWritten = false;
|
|
6199
6268
|
try {
|
|
6200
6269
|
await writeFile3(tmp, bytes, { flag: "wx" });
|
|
@@ -6636,7 +6705,8 @@ function webSocketError(event) {
|
|
|
6636
6705
|
|
|
6637
6706
|
// src/handwritten/commands/drive/watch.ts
|
|
6638
6707
|
async function runDriveWatch(root, options = {}) {
|
|
6639
|
-
const
|
|
6708
|
+
const dbg = options.debugLogger ?? (options.debug ? createDriveDebugLogger(root) : noopDriveDebugLogger);
|
|
6709
|
+
const runSync = options.runSync ?? ((syncRoot, onProgress) => runDriveSyncOnce(syncRoot, void 0, void 0, onProgress, dbg));
|
|
6640
6710
|
const debounceMs = options.debounceMs ?? 500;
|
|
6641
6711
|
const remoteDebounceMs = options.remoteDebounceMs ?? 2e3;
|
|
6642
6712
|
const emit = options.onEvent ?? ((event) => {
|
|
@@ -6647,6 +6717,7 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6647
6717
|
}
|
|
6648
6718
|
render({ kind: "drive_watch", display: { shape: "object" } }, event);
|
|
6649
6719
|
});
|
|
6720
|
+
let nextTrigger = "initial";
|
|
6650
6721
|
let debounceTimer;
|
|
6651
6722
|
let debounceDeadlineMs;
|
|
6652
6723
|
let retryTimer;
|
|
@@ -6671,14 +6742,28 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6671
6742
|
rerunRequested = false;
|
|
6672
6743
|
try {
|
|
6673
6744
|
emit({ kind: "drive_sync_start" });
|
|
6745
|
+
dbg.log("sync_start", { trigger: nextTrigger });
|
|
6746
|
+
const startedAtMs = Date.now();
|
|
6674
6747
|
const summary = await runSync(root, (processed, total) => {
|
|
6675
6748
|
emit({ kind: "drive_sync_progress", processed, total });
|
|
6676
6749
|
});
|
|
6677
6750
|
emit({ kind: "drive_sync_once", ...summary });
|
|
6751
|
+
dbg.log("sync_end", {
|
|
6752
|
+
duration_ms: Date.now() - startedAtMs,
|
|
6753
|
+
uploaded: summary.uploaded,
|
|
6754
|
+
downloaded: summary.downloaded,
|
|
6755
|
+
deleted: summary.deleted,
|
|
6756
|
+
merged: summary.merged,
|
|
6757
|
+
unchanged: summary.unchanged,
|
|
6758
|
+
conflicts: summary.conflicts,
|
|
6759
|
+
errors: summary.errors
|
|
6760
|
+
});
|
|
6678
6761
|
backoffMs = 1e3;
|
|
6679
6762
|
} catch (error) {
|
|
6680
6763
|
if (isAuthError(error) || isFatalWatchError(error) || !isRetryableWatchError(error)) throw error;
|
|
6681
6764
|
emit({ kind: "drive_watch_retry", delay_ms: backoffMs, error: errorMessage2(error) });
|
|
6765
|
+
dbg.log("retry", { delay_ms: backoffMs, error: errorMessage2(error) });
|
|
6766
|
+
nextTrigger = "retry";
|
|
6682
6767
|
if (stopped) return;
|
|
6683
6768
|
await waitForManagedTimer(backoffMs);
|
|
6684
6769
|
if (stopped) return;
|
|
@@ -6696,7 +6781,8 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6696
6781
|
debounceTimer = void 0;
|
|
6697
6782
|
debounceDeadlineMs = void 0;
|
|
6698
6783
|
}
|
|
6699
|
-
function scheduleSync(delayMs) {
|
|
6784
|
+
function scheduleSync(delayMs, trigger) {
|
|
6785
|
+
nextTrigger = trigger;
|
|
6700
6786
|
if (running) {
|
|
6701
6787
|
rerunRequested = true;
|
|
6702
6788
|
return;
|
|
@@ -6767,7 +6853,8 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6767
6853
|
source = options.source ?? createDefaultWatchSource(root);
|
|
6768
6854
|
source.onChange((path) => {
|
|
6769
6855
|
if (isDriveInternalPath(root, path)) return;
|
|
6770
|
-
|
|
6856
|
+
dbg.log("fs_event", { path: relative2(root, path) });
|
|
6857
|
+
scheduleSync(debounceMs, "local");
|
|
6771
6858
|
});
|
|
6772
6859
|
emit({ kind: "drive_watch_started", root, library_id: state.library_id });
|
|
6773
6860
|
realtimeSource?.start({
|
|
@@ -6776,11 +6863,12 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6776
6863
|
},
|
|
6777
6864
|
onEvent(event) {
|
|
6778
6865
|
emit(realtimeEvent(event));
|
|
6866
|
+
dbg.log("realtime_event", { ...event });
|
|
6779
6867
|
if (event.immediate) {
|
|
6780
|
-
scheduleSync(0);
|
|
6868
|
+
scheduleSync(0, "remote");
|
|
6781
6869
|
return;
|
|
6782
6870
|
}
|
|
6783
|
-
scheduleSync(event.debounce_ms ?? remoteDebounceMs);
|
|
6871
|
+
scheduleSync(event.debounce_ms ?? remoteDebounceMs, "remote");
|
|
6784
6872
|
},
|
|
6785
6873
|
onReconnect(delayMs, error) {
|
|
6786
6874
|
emit({ kind: "drive_realtime_reconnecting", delay_ms: delayMs, error });
|
|
@@ -6809,8 +6897,9 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6809
6897
|
}
|
|
6810
6898
|
}
|
|
6811
6899
|
function driveWatchCommand(options = {}) {
|
|
6812
|
-
return new Command81("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
6813
|
-
|
|
6900
|
+
return new Command81("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").option("--debug", "append debug NDJSON events to <folder>/.wspc-drive/debug.log").action(async (path, flags) => {
|
|
6901
|
+
const debug = options.debug ?? (flags.debug === true || process.env.WSPC_DRIVE_DEBUG === "1");
|
|
6902
|
+
await runDriveWatch(resolve4(path), { ...options, debug });
|
|
6814
6903
|
});
|
|
6815
6904
|
}
|
|
6816
6905
|
function createDefaultWatchSource(root) {
|