@wspc/cli 0.1.13 → 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 +126 -23
- 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;
|
|
@@ -5843,32 +5881,75 @@ function emptySummary() {
|
|
|
5843
5881
|
paths: []
|
|
5844
5882
|
};
|
|
5845
5883
|
}
|
|
5846
|
-
|
|
5884
|
+
function isActionableAction(action) {
|
|
5885
|
+
return action.type !== "unchanged" && action.type !== "state_only" && action.type !== "remove_state";
|
|
5886
|
+
}
|
|
5887
|
+
async function runDriveSyncOnce(root, api, clock = systemDriveClock, onProgress, debug = noopDriveDebugLogger) {
|
|
5847
5888
|
return withDriveLock(root, async () => {
|
|
5848
5889
|
let state = await readDriveState(root);
|
|
5849
5890
|
const syncApi = api ?? await createDriveApi();
|
|
5850
5891
|
const summary = emptySummary();
|
|
5851
5892
|
const blockedPaths = /* @__PURE__ */ new Set();
|
|
5893
|
+
const scanStartedMs = Date.now();
|
|
5852
5894
|
const localFiles = await scanDriveFiles(root, {
|
|
5853
5895
|
onPathError: async (path, error) => {
|
|
5854
5896
|
await recordPathError(summary, blockedPaths, path, error);
|
|
5855
5897
|
}
|
|
5856
5898
|
});
|
|
5899
|
+
const scanMs = Date.now() - scanStartedMs;
|
|
5900
|
+
const manifestStartedMs = Date.now();
|
|
5857
5901
|
const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
|
|
5902
|
+
const manifestMs = Date.now() - manifestStartedMs;
|
|
5858
5903
|
const paths = Array.from(
|
|
5859
5904
|
/* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
|
|
5860
5905
|
).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
|
|
5906
|
+
const total = paths.filter(
|
|
5907
|
+
(path) => isActionableAction(decideDriveAction(state.entries[path], localFiles[path], remoteFiles[path]))
|
|
5908
|
+
).length;
|
|
5909
|
+
let processed = 0;
|
|
5910
|
+
onProgress?.(processed, total);
|
|
5911
|
+
const processStartedMs = Date.now();
|
|
5861
5912
|
for (const path of paths) {
|
|
5862
5913
|
const remote = remoteFiles[path];
|
|
5863
|
-
const
|
|
5864
|
-
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 });
|
|
5865
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
|
+
}
|
|
5932
|
+
if (isActionableAction(action)) {
|
|
5933
|
+
processed += 1;
|
|
5934
|
+
onProgress?.(processed, total);
|
|
5935
|
+
}
|
|
5866
5936
|
if (result.stop) break;
|
|
5867
5937
|
}
|
|
5868
5938
|
recordUnresolvedConflicts(summary, state);
|
|
5939
|
+
debug.log("sync_phases", { scan_ms: scanMs, manifest_ms: manifestMs, process_ms: Date.now() - processStartedMs });
|
|
5869
5940
|
return summary;
|
|
5870
5941
|
});
|
|
5871
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
|
+
}
|
|
5872
5953
|
function driveSyncCommand(api) {
|
|
5873
5954
|
const sync = new Command80("sync").description("Drive sync commands");
|
|
5874
5955
|
sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
@@ -6182,7 +6263,7 @@ async function writeConflictCopy(root, path, side, versionId, bytes, clock) {
|
|
|
6182
6263
|
const target = resolveInsideRoot(root, candidate);
|
|
6183
6264
|
await mkdir3(dirname2(target), { recursive: true });
|
|
6184
6265
|
for (; ; ) {
|
|
6185
|
-
const tmp =
|
|
6266
|
+
const tmp = join6(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID4()}.tmp`);
|
|
6186
6267
|
let tmpWritten = false;
|
|
6187
6268
|
try {
|
|
6188
6269
|
await writeFile3(tmp, bytes, { flag: "wx" });
|
|
@@ -6624,7 +6705,8 @@ function webSocketError(event) {
|
|
|
6624
6705
|
|
|
6625
6706
|
// src/handwritten/commands/drive/watch.ts
|
|
6626
6707
|
async function runDriveWatch(root, options = {}) {
|
|
6627
|
-
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));
|
|
6628
6710
|
const debounceMs = options.debounceMs ?? 500;
|
|
6629
6711
|
const remoteDebounceMs = options.remoteDebounceMs ?? 2e3;
|
|
6630
6712
|
const emit = options.onEvent ?? ((event) => {
|
|
@@ -6635,6 +6717,7 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6635
6717
|
}
|
|
6636
6718
|
render({ kind: "drive_watch", display: { shape: "object" } }, event);
|
|
6637
6719
|
});
|
|
6720
|
+
let nextTrigger = "initial";
|
|
6638
6721
|
let debounceTimer;
|
|
6639
6722
|
let debounceDeadlineMs;
|
|
6640
6723
|
let retryTimer;
|
|
@@ -6659,12 +6742,28 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6659
6742
|
rerunRequested = false;
|
|
6660
6743
|
try {
|
|
6661
6744
|
emit({ kind: "drive_sync_start" });
|
|
6662
|
-
|
|
6745
|
+
dbg.log("sync_start", { trigger: nextTrigger });
|
|
6746
|
+
const startedAtMs = Date.now();
|
|
6747
|
+
const summary = await runSync(root, (processed, total) => {
|
|
6748
|
+
emit({ kind: "drive_sync_progress", processed, total });
|
|
6749
|
+
});
|
|
6663
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
|
+
});
|
|
6664
6761
|
backoffMs = 1e3;
|
|
6665
6762
|
} catch (error) {
|
|
6666
6763
|
if (isAuthError(error) || isFatalWatchError(error) || !isRetryableWatchError(error)) throw error;
|
|
6667
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";
|
|
6668
6767
|
if (stopped) return;
|
|
6669
6768
|
await waitForManagedTimer(backoffMs);
|
|
6670
6769
|
if (stopped) return;
|
|
@@ -6682,7 +6781,8 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6682
6781
|
debounceTimer = void 0;
|
|
6683
6782
|
debounceDeadlineMs = void 0;
|
|
6684
6783
|
}
|
|
6685
|
-
function scheduleSync(delayMs) {
|
|
6784
|
+
function scheduleSync(delayMs, trigger) {
|
|
6785
|
+
nextTrigger = trigger;
|
|
6686
6786
|
if (running) {
|
|
6687
6787
|
rerunRequested = true;
|
|
6688
6788
|
return;
|
|
@@ -6753,7 +6853,8 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6753
6853
|
source = options.source ?? createDefaultWatchSource(root);
|
|
6754
6854
|
source.onChange((path) => {
|
|
6755
6855
|
if (isDriveInternalPath(root, path)) return;
|
|
6756
|
-
|
|
6856
|
+
dbg.log("fs_event", { path: relative2(root, path) });
|
|
6857
|
+
scheduleSync(debounceMs, "local");
|
|
6757
6858
|
});
|
|
6758
6859
|
emit({ kind: "drive_watch_started", root, library_id: state.library_id });
|
|
6759
6860
|
realtimeSource?.start({
|
|
@@ -6762,11 +6863,12 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6762
6863
|
},
|
|
6763
6864
|
onEvent(event) {
|
|
6764
6865
|
emit(realtimeEvent(event));
|
|
6866
|
+
dbg.log("realtime_event", { ...event });
|
|
6765
6867
|
if (event.immediate) {
|
|
6766
|
-
scheduleSync(0);
|
|
6868
|
+
scheduleSync(0, "remote");
|
|
6767
6869
|
return;
|
|
6768
6870
|
}
|
|
6769
|
-
scheduleSync(event.debounce_ms ?? remoteDebounceMs);
|
|
6871
|
+
scheduleSync(event.debounce_ms ?? remoteDebounceMs, "remote");
|
|
6770
6872
|
},
|
|
6771
6873
|
onReconnect(delayMs, error) {
|
|
6772
6874
|
emit({ kind: "drive_realtime_reconnecting", delay_ms: delayMs, error });
|
|
@@ -6795,8 +6897,9 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6795
6897
|
}
|
|
6796
6898
|
}
|
|
6797
6899
|
function driveWatchCommand(options = {}) {
|
|
6798
|
-
return new Command81("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
6799
|
-
|
|
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 });
|
|
6800
6903
|
});
|
|
6801
6904
|
}
|
|
6802
6905
|
function createDefaultWatchSource(root) {
|