@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 CHANGED
@@ -1520,9 +1520,9 @@ function createConsistencyFetch(opts) {
1520
1520
  }
1521
1521
 
1522
1522
  // src/version.ts
1523
- var VERSION = "0.1.14";
1523
+ var VERSION = "0.1.15";
1524
1524
  var SPEC_SHA = "bec760cd";
1525
- var SPEC_FETCHED_AT = "2026-07-08T06:11:14.456Z";
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 join5, posix as pathPosix2, resolve as resolve3 } from "path";
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 join3 } from "path";
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 = join3(currentPath, entry.name);
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 join4 } from "path";
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 = join4(dirname(target), `.${basename2(target)}.wspc-merge-${randomUUID3()}.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 = join4(dirname(target), `.${basename2(target)}.wspc-merge-restore-${randomUUID3()}.tmp`);
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 = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID3()}.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 join4(dirname(target), `.${basename2(target)}.wspc-backup-${randomUUID3()}.tmp`);
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 action = decideDriveAction(state.entries[path], localFiles[path], remote);
5872
- const result = await processPath({ root, state, api: syncApi, path, action, remote, local: localFiles[path], summary, clock });
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 = join5(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID4()}.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 runSync = options.runSync ?? ((syncRoot, onProgress) => runDriveSyncOnce(syncRoot, void 0, void 0, onProgress));
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
- scheduleSync(debounceMs);
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
- await runDriveWatch(resolve4(path), options);
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) {