@wspc/cli 0.0.22 → 0.0.23

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
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command63 } from "commander";
4
+ import { Command as Command65 } from "commander";
5
+ import { realpathSync } from "fs";
6
+ import { fileURLToPath } from "url";
5
7
 
6
8
  // src/generated/cli/invite/accept.ts
7
9
  import { Command } from "commander";
@@ -35,7 +37,7 @@ function createSseClient({
35
37
  ...options
36
38
  }) {
37
39
  let lastEventId;
38
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
40
+ const sleep = sseSleepFn ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
39
41
  const createStream = async function* () {
40
42
  let retryDelay = sseDefaultRetryDelay ?? 3e3;
41
43
  let attempt = 0;
@@ -928,6 +930,25 @@ var eventIcsDownload = (options) => (options.client ?? client).get({
928
930
  url: "/calendar/events/{filename}",
929
931
  ...options
930
932
  });
933
+ var driveFileDelete = (options) => (options.client ?? client).post({
934
+ security: [{ scheme: "bearer", type: "http" }],
935
+ url: "/drive/libraries/{id}/files/delete",
936
+ ...options,
937
+ headers: {
938
+ "Content-Type": "application/json",
939
+ ...options.headers
940
+ }
941
+ });
942
+ var driveLibraryGet = (options) => (options.client ?? client).get({
943
+ security: [{ scheme: "bearer", type: "http" }],
944
+ url: "/drive/libraries/{id}",
945
+ ...options
946
+ });
947
+ var driveManifestGet = (options) => (options.client ?? client).get({
948
+ security: [{ scheme: "bearer", type: "http" }],
949
+ url: "/drive/libraries/{id}/manifest",
950
+ ...options
951
+ });
931
952
  var emailAliasList = (options) => (options?.client ?? client).get({
932
953
  security: [{ scheme: "bearer", type: "http" }],
933
954
  url: "/email/aliases",
@@ -1177,7 +1198,7 @@ var V1_CRED_KEYS = [
1177
1198
  "actor",
1178
1199
  "agent_label"
1179
1200
  ];
1180
- var CONSISTENCY_BOOKMARK_SERVICES = ["auth", "todo", "calendar", "email", "push"];
1201
+ var CONSISTENCY_BOOKMARK_SERVICES = ["auth", "todo", "calendar", "drive", "email", "push"];
1181
1202
  function normalizeConsistencyBookmarks(raw) {
1182
1203
  if (typeof raw !== "object" || raw === null) return void 0;
1183
1204
  const out = {};
@@ -1318,6 +1339,7 @@ var SERVICE_HEADERS = {
1318
1339
  auth: "x-cb-auth",
1319
1340
  todo: "x-cb-todo",
1320
1341
  calendar: "x-cb-cal",
1342
+ drive: "x-cb-drive",
1321
1343
  email: "x-cb-email",
1322
1344
  push: "x-cb-push"
1323
1345
  };
@@ -1384,7 +1406,7 @@ function createConsistencyFetch(opts) {
1384
1406
  const value = response.headers.get(header);
1385
1407
  return value ? [[serviceName, value]] : [];
1386
1408
  });
1387
- const shouldCheckInvalidBookmark = injectedBookmarks.length > 0;
1409
+ const shouldCheckInvalidBookmark = injectedBookmarks.length > 0 && !response.ok;
1388
1410
  const invalidBookmark = shouldCheckInvalidBookmark ? await responseHasInvalidBookmark(response) : false;
1389
1411
  if (nextBookmarks.length === 0 && !invalidBookmark) return response;
1390
1412
  await opts.store.update((config) => {
@@ -1410,9 +1432,9 @@ function createConsistencyFetch(opts) {
1410
1432
  }
1411
1433
 
1412
1434
  // src/version.ts
1413
- var VERSION = "0.0.22";
1414
- var SPEC_SHA = "a2dfa511";
1415
- var SPEC_FETCHED_AT = "2026-06-20T14:16:47.131Z";
1435
+ var VERSION = "0.0.23";
1436
+ var SPEC_SHA = "51d0418e";
1437
+ var SPEC_FETCHED_AT = "2026-06-21T04:09:00.455Z";
1416
1438
  var API_BASE = "https://api.wspc.ai";
1417
1439
 
1418
1440
  // src/index.ts
@@ -1559,6 +1581,17 @@ function buildInterceptor(store, resolved, fetchImpl) {
1559
1581
  });
1560
1582
  }
1561
1583
  async function loadSdkClient(opts = {}) {
1584
+ const { _rawClient } = await loadClientParts(opts);
1585
+ return { _rawClient };
1586
+ }
1587
+ async function loadAuthedFetch(opts = {}) {
1588
+ const { fetch: fetch2, baseUrl } = await loadClientParts(opts);
1589
+ return { fetch: fetch2, baseUrl };
1590
+ }
1591
+ async function loadSdkClientWithAuthedFetch(opts = {}) {
1592
+ return loadClientParts(opts);
1593
+ }
1594
+ async function loadClientParts(opts = {}) {
1562
1595
  const store = opts.store ?? new ConfigStore();
1563
1596
  const config = await store.read();
1564
1597
  const resolved = resolveAccount(config, { accountOverride: process.env.WSPC_ACCOUNT });
@@ -1569,27 +1602,14 @@ async function loadSdkClient(opts = {}) {
1569
1602
  fetchImpl: opts.fetchImpl
1570
1603
  });
1571
1604
  const interceptor = buildInterceptor(store, resolved, consistencyFetch);
1605
+ const authedFetch = (input, init) => interceptor.execute(new Request(input, init));
1572
1606
  const rawClient = createClient(
1573
1607
  createConfig({
1574
1608
  baseUrl: resolved.apiBase,
1575
- fetch: ((input, init) => interceptor.execute(new Request(input, init)))
1609
+ fetch: authedFetch
1576
1610
  })
1577
1611
  );
1578
- return { _rawClient: rawClient };
1579
- }
1580
- async function loadAuthedFetch(opts = {}) {
1581
- const store = opts.store ?? new ConfigStore();
1582
- const config = await store.read();
1583
- const resolved = resolveAccount(config, { accountOverride: process.env.WSPC_ACCOUNT });
1584
- const consistencyFetch = createConsistencyFetch({
1585
- store,
1586
- envName: resolved.envName,
1587
- apiBase: resolved.apiBase,
1588
- fetchImpl: opts.fetchImpl
1589
- });
1590
- const interceptor = buildInterceptor(store, resolved, consistencyFetch);
1591
- const authedFetch = (input, init) => interceptor.execute(new Request(input, init));
1592
- return { fetch: authedFetch, baseUrl: resolved.apiBase };
1612
+ return { _rawClient: rawClient, fetch: authedFetch, baseUrl: resolved.apiBase };
1593
1613
  }
1594
1614
 
1595
1615
  // src/handwritten/output/primitives.ts
@@ -1754,12 +1774,12 @@ function wrapToWidth(text, width) {
1754
1774
  out.push(head);
1755
1775
  word = word.slice(head.length);
1756
1776
  }
1757
- const sep = cur ? " " : "";
1758
- if (cur && visibleWidth(cur + sep + word) > limit) {
1777
+ const sep2 = cur ? " " : "";
1778
+ if (cur && visibleWidth(cur + sep2 + word) > limit) {
1759
1779
  out.push(cur);
1760
1780
  cur = word;
1761
1781
  } else {
1762
- cur = cur + sep + word;
1782
+ cur = cur + sep2 + word;
1763
1783
  }
1764
1784
  }
1765
1785
  if (cur) out.push(cur);
@@ -1779,11 +1799,11 @@ function table(headers, rows) {
1779
1799
  }
1780
1800
  return w;
1781
1801
  });
1782
- const sep = " ";
1802
+ const sep2 = " ";
1783
1803
  const lines = [];
1784
- lines.push(headers.map((h, i) => dim(padEndVisible(h, widths[i] ?? 0))).join(sep));
1804
+ lines.push(headers.map((h, i) => dim(padEndVisible(h, widths[i] ?? 0))).join(sep2));
1785
1805
  for (const r of rows) {
1786
- lines.push(r.map((c, i) => padEndVisible(c ?? "", widths[i] ?? 0)).join(sep));
1806
+ lines.push(r.map((c, i) => padEndVisible(c ?? "", widths[i] ?? 0)).join(sep2));
1787
1807
  }
1788
1808
  return lines.join("\n") + "\n";
1789
1809
  }
@@ -4274,9 +4294,973 @@ var attachmentCommand = new Command62("attachment").description("Download an inb
4274
4294
  `);
4275
4295
  });
4276
4296
 
4297
+ // src/handwritten/commands/drive/bind.ts
4298
+ import { Command as Command63 } from "commander";
4299
+ import { stat as stat2 } from "fs/promises";
4300
+ import { resolve } from "path";
4301
+
4302
+ // src/handwritten/commands/drive/api.ts
4303
+ function asError(result) {
4304
+ const message = JSON.stringify(result.error ?? "request failed");
4305
+ return new Error(`HTTP ${result.response?.status ?? "?"}: ${message}`);
4306
+ }
4307
+ async function expectJsonResult(result) {
4308
+ if (result.error || !result.response?.ok) throw asError(result);
4309
+ if (result.data == null) throw new Error("empty response");
4310
+ return result.data;
4311
+ }
4312
+ function driveContentUrl(baseUrl, id) {
4313
+ const baseWithTrailingSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4314
+ return new URL(`drive/libraries/${encodeURIComponent(id)}/files/content`, baseWithTrailingSlash);
4315
+ }
4316
+ async function createDriveApi(opts = {}) {
4317
+ const client2 = await loadSdkClientWithAuthedFetch(opts);
4318
+ const rawClient = client2._rawClient;
4319
+ return {
4320
+ async getLibrary(id) {
4321
+ const result = await driveLibraryGet({
4322
+ client: rawClient,
4323
+ path: { id }
4324
+ });
4325
+ return expectJsonResult(result);
4326
+ },
4327
+ async getManifest(id, cursor) {
4328
+ const result = await driveManifestGet({
4329
+ client: rawClient,
4330
+ path: { id },
4331
+ ...cursor ? { query: { cursor } } : {}
4332
+ });
4333
+ return expectJsonResult(result);
4334
+ },
4335
+ async deleteFile(id, path, expectedEntryVersion) {
4336
+ const result = await driveFileDelete({
4337
+ client: rawClient,
4338
+ path: { id },
4339
+ body: {
4340
+ path,
4341
+ expected_entry_version: expectedEntryVersion
4342
+ }
4343
+ });
4344
+ return expectJsonResult(result);
4345
+ },
4346
+ async uploadFile(id, path, body, sha256, expectedEntryVersion) {
4347
+ const url = driveContentUrl(client2.baseUrl, id);
4348
+ url.searchParams.set("path", path);
4349
+ if (expectedEntryVersion !== void 0) {
4350
+ url.searchParams.set("expected_entry_version", String(expectedEntryVersion));
4351
+ }
4352
+ const res = await client2.fetch(url, {
4353
+ method: "PUT",
4354
+ headers: {
4355
+ "content-type": "application/octet-stream",
4356
+ "x-drive-content-sha256": sha256
4357
+ },
4358
+ body
4359
+ });
4360
+ if (!res.ok) {
4361
+ const text = await res.text();
4362
+ throw new Error(`HTTP ${res.status}: ${text}`);
4363
+ }
4364
+ const payload = await res.json();
4365
+ if (payload === void 0 || payload === null) {
4366
+ throw new Error("empty response");
4367
+ }
4368
+ return payload;
4369
+ },
4370
+ async downloadFile(id, path) {
4371
+ const url = driveContentUrl(client2.baseUrl, id);
4372
+ url.searchParams.set("path", path);
4373
+ const res = await client2.fetch(url, { method: "GET" });
4374
+ if (!res.ok) {
4375
+ const text = await res.text();
4376
+ throw new Error(`HTTP ${res.status}: ${text}`);
4377
+ }
4378
+ return res;
4379
+ }
4380
+ };
4381
+ }
4382
+
4383
+ // src/handwritten/commands/drive/state.ts
4384
+ import { mkdir, open, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
4385
+ import { randomUUID } from "crypto";
4386
+ import { join as join2 } from "path";
4387
+ var DRIVE_DIR = ".wspc-drive";
4388
+ var STATE_FILE = "state.json";
4389
+ function statePath(root) {
4390
+ return join2(root, DRIVE_DIR, STATE_FILE);
4391
+ }
4392
+ async function readDriveState(root) {
4393
+ const buf = await readFile2(statePath(root), "utf8");
4394
+ const parsed = JSON.parse(buf);
4395
+ if (!isValidDriveState(parsed)) {
4396
+ throw new Error("unsupported .wspc-drive/state.json schema");
4397
+ }
4398
+ return parsed;
4399
+ }
4400
+ async function writeDriveState(root, state) {
4401
+ await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4402
+ const tmp = join2(root, DRIVE_DIR, `state.json.tmp-${process.pid}-${randomUUID()}`);
4403
+ const snapshot = JSON.stringify(
4404
+ {
4405
+ ...state,
4406
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
4407
+ },
4408
+ null,
4409
+ 2
4410
+ ) + "\n";
4411
+ const fullPath = statePath(root);
4412
+ try {
4413
+ await writeFile(tmp, snapshot, { mode: 384 });
4414
+ const fh = await open(tmp, "r");
4415
+ try {
4416
+ await fh.sync();
4417
+ } finally {
4418
+ await fh.close();
4419
+ }
4420
+ await rename(tmp, fullPath);
4421
+ } finally {
4422
+ await rm(tmp, { force: true });
4423
+ }
4424
+ }
4425
+ async function initDriveState(root, libraryId) {
4426
+ await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4427
+ try {
4428
+ const existing = await readDriveState(root);
4429
+ if (existing.library_id !== libraryId) {
4430
+ throw new Error(`folder already bound to ${existing.library_id}`);
4431
+ }
4432
+ return existing;
4433
+ } catch (err) {
4434
+ if (err.code !== "ENOENT") throw err;
4435
+ }
4436
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4437
+ const state = {
4438
+ schema_version: 1,
4439
+ library_id: libraryId,
4440
+ created_at: now,
4441
+ updated_at: now,
4442
+ entries: {},
4443
+ conflicts: {}
4444
+ };
4445
+ await writeDriveState(root, state);
4446
+ return state;
4447
+ }
4448
+ async function withDriveLock(root, fn) {
4449
+ await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4450
+ const lockFile = join2(root, DRIVE_DIR, "sync.lock");
4451
+ const fh = await open(lockFile, "wx").catch((error) => {
4452
+ if (error.code === "EEXIST") {
4453
+ throw new Error("sync lock already exists");
4454
+ }
4455
+ throw error;
4456
+ });
4457
+ try {
4458
+ return await fn();
4459
+ } finally {
4460
+ await fh.close().catch(() => {
4461
+ });
4462
+ await rm(lockFile, { force: true }).catch(() => {
4463
+ });
4464
+ }
4465
+ }
4466
+ function isRecord(value) {
4467
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4468
+ }
4469
+ function isDriveStateEntry(value) {
4470
+ return isRecord(value) && typeof value.entry_id === "string" && typeof value.entry_version === "number" && typeof value.size_bytes === "number" && typeof value.last_synced_at === "string" && value.status === "synced" && (value.current_version_id === void 0 || typeof value.current_version_id === "string") && (value.content_sha256 === void 0 || typeof value.content_sha256 === "string") && (value.last_local_sha256 === void 0 || typeof value.last_local_sha256 === "string");
4471
+ }
4472
+ function isDriveConflict(value) {
4473
+ return isRecord(value) && typeof value.detected_at === "string" && typeof value.reason === "string" && (value.remote_entry_version === void 0 || typeof value.remote_entry_version === "number") && (value.remote_version_id === void 0 || typeof value.remote_version_id === "string");
4474
+ }
4475
+ function isValidDriveState(value) {
4476
+ if (!isRecord(value)) return false;
4477
+ if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord(value.entries) || !isRecord(value.conflicts)) {
4478
+ return false;
4479
+ }
4480
+ for (const entry of Object.values(value.entries)) {
4481
+ if (!isDriveStateEntry(entry)) return false;
4482
+ }
4483
+ for (const conflict2 of Object.values(value.conflicts)) {
4484
+ if (!isDriveConflict(conflict2)) return false;
4485
+ }
4486
+ return true;
4487
+ }
4488
+
4489
+ // src/handwritten/commands/drive/bind.ts
4490
+ async function assertExistingDirectory(path) {
4491
+ let stats;
4492
+ try {
4493
+ stats = await stat2(path);
4494
+ } catch {
4495
+ throw new Error(`local folder does not exist: ${path}`);
4496
+ }
4497
+ if (!stats.isDirectory()) {
4498
+ throw new Error(`local path is not a folder: ${path}`);
4499
+ }
4500
+ }
4501
+ function driveBindCommand() {
4502
+ return new Command63("bind").description("Bind a local folder to an existing Drive library").requiredOption("--library <id>", "existing Drive library id").argument("[path]", "local folder path", ".").action(async (path, opts) => {
4503
+ const root = resolve(path);
4504
+ await assertExistingDirectory(root);
4505
+ const api = await createDriveApi();
4506
+ const library = await api.getLibrary(opts.library);
4507
+ const state = await initDriveState(root, opts.library);
4508
+ render(
4509
+ { kind: "drive_bind", display: { shape: "object" } },
4510
+ {
4511
+ root,
4512
+ library_id: state.library_id,
4513
+ library_name: library.name
4514
+ }
4515
+ );
4516
+ });
4517
+ }
4518
+
4519
+ // src/handwritten/commands/drive/sync.ts
4520
+ import { Command as Command64 } from "commander";
4521
+ import { createWriteStream as createWriteStream2 } from "fs";
4522
+ import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink } from "fs/promises";
4523
+ import { basename as basename2, dirname, join as join4, resolve as resolve3 } from "path";
4524
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
4525
+ import { Readable as Readable2, Transform } from "stream";
4526
+ import { pipeline as pipeline2 } from "stream/promises";
4527
+
4528
+ // src/handwritten/commands/drive/decision.ts
4529
+ function decideDriveAction(entry, local, remote) {
4530
+ if (!entry) return decideWithoutBase(local, remote);
4531
+ if (!local && !remote) return { type: "remove_state" };
4532
+ const localStatus = getLocalStatus(entry, local);
4533
+ const remoteStatus = getRemoteStatus(entry, remote);
4534
+ if (!remote) return decideRemoteMissing(localStatus);
4535
+ if (!local) return decideLocalMissing(entry, remoteStatus);
4536
+ return decideBothPresent(entry, localStatus, remoteStatus);
4537
+ }
4538
+ function decideWithoutBase(local, remote) {
4539
+ if (local && !remote) return { type: "upload_create", expectedEntryVersion: 0 };
4540
+ if (!local && remote) {
4541
+ return remote.entry_version === void 0 ? { type: "conflict", reason: "remote_missing_entry_version" } : { type: "download" };
4542
+ }
4543
+ if (local && remote) {
4544
+ if (remote.entry_version === void 0) return { type: "conflict", reason: "remote_missing_entry_version" };
4545
+ return remote.content_sha256 !== void 0 && local.sha256 === remote.content_sha256 ? { type: "state_only" } : { type: "conflict", reason: "local_and_remote_without_base" };
4546
+ }
4547
+ return { type: "unchanged" };
4548
+ }
4549
+ function decideRemoteMissing(localStatus) {
4550
+ if (localStatus === "unchanged") return { type: "delete_local" };
4551
+ if (localStatus === "unknown") return { type: "conflict", reason: "unknown_local_base_remote_deleted" };
4552
+ return { type: "conflict", reason: "local_changed_remote_deleted" };
4553
+ }
4554
+ function decideLocalMissing(entry, remoteStatus) {
4555
+ if (remoteStatus === "unchanged") return { type: "delete_remote", expectedEntryVersion: entry.entry_version };
4556
+ return { type: "conflict", reason: "remote_changed_before_delete" };
4557
+ }
4558
+ function decideBothPresent(entry, localStatus, remoteStatus) {
4559
+ if (localStatus === "unchanged" && remoteStatus === "content_same_new_version") {
4560
+ return { type: "state_only" };
4561
+ }
4562
+ if (remoteStatus === "missing_version") {
4563
+ return { type: "conflict", reason: "remote_missing_entry_version" };
4564
+ }
4565
+ if (localStatus === "unknown" && remoteStatus !== "unchanged") {
4566
+ return { type: "conflict", reason: "unknown_local_base_remote_changed" };
4567
+ }
4568
+ if (localStatus === "unknown") return { type: "conflict", reason: "unknown_local_base" };
4569
+ if (localStatus === "unchanged" && remoteStatus === "changed") return { type: "download" };
4570
+ if (localStatus === "changed" && remoteStatus === "unchanged") {
4571
+ return { type: "upload_update", expectedEntryVersion: entry.entry_version };
4572
+ }
4573
+ if (localStatus !== "unchanged" && remoteStatus !== "unchanged") {
4574
+ return { type: "conflict", reason: "local_and_remote_changed" };
4575
+ }
4576
+ return { type: "unchanged" };
4577
+ }
4578
+ function getLocalStatus(entry, local) {
4579
+ if (!local) return "changed";
4580
+ if (entry.last_local_sha256 !== void 0) {
4581
+ return local.sha256 === entry.last_local_sha256 ? "unchanged" : "changed";
4582
+ }
4583
+ if (entry.content_sha256 !== void 0) {
4584
+ return local.sha256 === entry.content_sha256 ? "unchanged" : "changed";
4585
+ }
4586
+ return "unknown";
4587
+ }
4588
+ function getRemoteStatus(entry, remote) {
4589
+ if (!remote) return "changed";
4590
+ if (remote.entry_version !== void 0 && remote.entry_version === entry.entry_version) return "unchanged";
4591
+ if (remote.entry_version === void 0) return "missing_version";
4592
+ if (remote.content_sha256 !== void 0 && entry.content_sha256 !== void 0 && remote.content_sha256 === entry.content_sha256) {
4593
+ return "content_same_new_version";
4594
+ }
4595
+ return "changed";
4596
+ }
4597
+
4598
+ // src/handwritten/commands/drive/path-policy.ts
4599
+ import { isAbsolute, relative, resolve as resolve2, sep } from "path";
4600
+ var UTF8_SEGMENT_LIMIT = 255;
4601
+ var UTF8_PATH_LIMIT = 1024;
4602
+ var CONTROL_CHARS = /[\0-\x1f\x7f]/;
4603
+ var WINDOWS_DRIVE_PREFIX = /^[a-zA-Z]:/;
4604
+ var UNC_PREFIX = /^\\\\/;
4605
+ var ABSOLUTE_POSIX_DOUBLE_SLASH = /^\/\//;
4606
+ function validateDrivePath(drivePath) {
4607
+ if (drivePath.length === 0) {
4608
+ throw new Error("invalid drive path: empty");
4609
+ }
4610
+ if (isAbsolute(drivePath) || ABSOLUTE_POSIX_DOUBLE_SLASH.test(drivePath)) {
4611
+ throw new Error(`invalid drive path: ${drivePath}`);
4612
+ }
4613
+ if (drivePath.includes("\\")) {
4614
+ throw new Error("invalid drive path: backslash");
4615
+ }
4616
+ if (WINDOWS_DRIVE_PREFIX.test(drivePath) || UNC_PREFIX.test(drivePath)) {
4617
+ throw new Error(`invalid drive path: ${drivePath}`);
4618
+ }
4619
+ if (CONTROL_CHARS.test(drivePath)) {
4620
+ throw new Error(`invalid drive path: control character`);
4621
+ }
4622
+ if (Buffer.byteLength(drivePath, "utf8") > UTF8_PATH_LIMIT) {
4623
+ throw new Error(`invalid drive path: exceeds ${UTF8_PATH_LIMIT} bytes`);
4624
+ }
4625
+ const segments = drivePath.split("/");
4626
+ if (segments.some((segment) => segment.length === 0)) {
4627
+ throw new Error("invalid drive path: empty segment");
4628
+ }
4629
+ if (segments.some((segment) => segment === "." || segment === "..")) {
4630
+ throw new Error("invalid drive path: relative segment");
4631
+ }
4632
+ if (segments.some((segment) => Buffer.byteLength(segment, "utf8") > UTF8_SEGMENT_LIMIT)) {
4633
+ throw new Error(`invalid drive path: segment exceeds ${UTF8_SEGMENT_LIMIT} bytes`);
4634
+ }
4635
+ return drivePath;
4636
+ }
4637
+ function resolveInsideRoot(root, drivePath) {
4638
+ const normalizedPath = validateDrivePath(drivePath);
4639
+ const absoluteRoot = resolve2(root);
4640
+ const absolutePath = resolve2(absoluteRoot, normalizedPath);
4641
+ const relativePath = relative(absoluteRoot, absolutePath);
4642
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`)) {
4643
+ throw new Error(`drive path escapes root: ${drivePath}`);
4644
+ }
4645
+ return absolutePath;
4646
+ }
4647
+
4648
+ // src/handwritten/commands/drive/scanner.ts
4649
+ import { createHash } from "crypto";
4650
+ import { constants as fsConstants } from "fs";
4651
+ import { open as open2, readdir, lstat } from "fs/promises";
4652
+ import { join as join3 } from "path";
4653
+ async function scanDriveFiles(root, options = {}) {
4654
+ const candidates = [];
4655
+ const files = {};
4656
+ const absRoot = root;
4657
+ await walk(absRoot, "");
4658
+ await addNonCollidingFiles(candidates);
4659
+ return files;
4660
+ async function walk(currentPath, currentDrivePath) {
4661
+ const entries = await readdir(currentPath, { withFileTypes: true });
4662
+ entries.sort((left, right) => left.name.localeCompare(right.name));
4663
+ for (const entry of entries) {
4664
+ if (isExcludedRootEntry(currentDrivePath, entry)) {
4665
+ continue;
4666
+ }
4667
+ if (isInternalSyncArtifactName(entry.name)) {
4668
+ continue;
4669
+ }
4670
+ const nextDrivePath = currentDrivePath ? `${currentDrivePath}/${entry.name}` : entry.name;
4671
+ try {
4672
+ validateDrivePath(nextDrivePath);
4673
+ } catch (error) {
4674
+ if (!options.onPathError) throw error;
4675
+ await options.onPathError(nextDrivePath, error);
4676
+ continue;
4677
+ }
4678
+ const nextPath = join3(currentPath, entry.name);
4679
+ const stats = await lstat(nextPath);
4680
+ if (stats.isSymbolicLink()) {
4681
+ continue;
4682
+ }
4683
+ if (stats.isDirectory()) {
4684
+ await walk(nextPath, nextDrivePath);
4685
+ continue;
4686
+ }
4687
+ if (!stats.isFile()) {
4688
+ continue;
4689
+ }
4690
+ const digest = await hashDriveFile(nextPath);
4691
+ if (!digest) {
4692
+ continue;
4693
+ }
4694
+ candidates.push({ path: nextDrivePath, entry: { sha256: digest.sha256, size_bytes: digest.sizeBytes } });
4695
+ }
4696
+ }
4697
+ async function addNonCollidingFiles(candidates2) {
4698
+ const byCaseFoldedPath = /* @__PURE__ */ new Map();
4699
+ for (const candidate of candidates2) {
4700
+ const folded = candidate.path.toLowerCase();
4701
+ const group = byCaseFoldedPath.get(folded) ?? [];
4702
+ group.push(candidate);
4703
+ byCaseFoldedPath.set(folded, group);
4704
+ }
4705
+ for (const group of byCaseFoldedPath.values()) {
4706
+ if (group.length > 1) {
4707
+ const sorted = group.sort((left, right) => left.path.localeCompare(right.path));
4708
+ for (const candidate2 of sorted) {
4709
+ const error = new Error(`LOCAL_PATH_CASE_CONFLICT: ${candidate2.path}`);
4710
+ if (!options.onPathError) throw error;
4711
+ await options.onPathError(candidate2.path, error);
4712
+ }
4713
+ continue;
4714
+ }
4715
+ const [candidate] = group;
4716
+ if (candidate) files[candidate.path] = candidate.entry;
4717
+ }
4718
+ }
4719
+ function isExcludedRootEntry(currentDrivePath, entry) {
4720
+ return currentDrivePath === "" && entry.name === DRIVE_DIR;
4721
+ }
4722
+ }
4723
+ function isInternalSyncArtifactName(name) {
4724
+ if (!name.startsWith(".") || !name.endsWith(".tmp")) return false;
4725
+ return name.includes(".wspc-download-") || name.includes(".wspc-backup-");
4726
+ }
4727
+ async function hashDriveFile(path) {
4728
+ const useNoFollow = fsConstants.O_NOFOLLOW !== void 0;
4729
+ const fdFlags = useNoFollow ? fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW : fsConstants.O_RDONLY;
4730
+ const fileHandle = await open2(path, fdFlags);
4731
+ const hash = createHash("sha256");
4732
+ let sizeBytes = 0;
4733
+ try {
4734
+ const stats = await fileHandle.stat();
4735
+ if (!stats.isFile()) {
4736
+ return void 0;
4737
+ }
4738
+ if (!useNoFollow) {
4739
+ const liveStats = await lstat(path);
4740
+ if (!liveStats.isFile()) {
4741
+ return void 0;
4742
+ }
4743
+ if (stats.ino !== void 0 && liveStats.ino !== void 0 && stats.dev !== void 0 && liveStats.dev !== void 0) {
4744
+ if (stats.ino !== liveStats.ino || stats.dev !== liveStats.dev) {
4745
+ return void 0;
4746
+ }
4747
+ }
4748
+ }
4749
+ await new Promise((resolve4, reject) => {
4750
+ const stream = fileHandle.createReadStream();
4751
+ stream.on("error", reject);
4752
+ stream.on("data", (chunk) => {
4753
+ hash.update(chunk);
4754
+ sizeBytes += chunk.length;
4755
+ });
4756
+ stream.on("end", () => resolve4());
4757
+ });
4758
+ return {
4759
+ sizeBytes,
4760
+ sha256: hash.digest("hex")
4761
+ };
4762
+ } finally {
4763
+ await fileHandle.close().catch(() => {
4764
+ });
4765
+ }
4766
+ }
4767
+
4768
+ // src/handwritten/commands/drive/sync.ts
4769
+ function emptySummary() {
4770
+ return {
4771
+ uploaded: 0,
4772
+ downloaded: 0,
4773
+ deleted: 0,
4774
+ unchanged: 0,
4775
+ conflicts: 0,
4776
+ errors: 0,
4777
+ paths: []
4778
+ };
4779
+ }
4780
+ async function runDriveSyncOnce(root, api) {
4781
+ return withDriveLock(root, async () => {
4782
+ let state = await readDriveState(root);
4783
+ const syncApi = api ?? await createDriveApi();
4784
+ const summary = emptySummary();
4785
+ const blockedPaths = /* @__PURE__ */ new Set();
4786
+ const localFiles = await scanDriveFiles(root, {
4787
+ onPathError: async (path, error) => {
4788
+ await recordPathError(summary, blockedPaths, path, error);
4789
+ }
4790
+ });
4791
+ const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
4792
+ const paths = Array.from(
4793
+ /* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
4794
+ ).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
4795
+ for (const path of paths) {
4796
+ const remote = remoteFiles[path];
4797
+ const action = decideDriveAction(state.entries[path], localFiles[path], remote);
4798
+ const result = await processPath({ root, state, api: syncApi, path, action, remote, local: localFiles[path], summary });
4799
+ state = result.state;
4800
+ if (result.stop) break;
4801
+ }
4802
+ recordUnresolvedConflicts(summary, state);
4803
+ return summary;
4804
+ });
4805
+ }
4806
+ function driveSyncCommand(api) {
4807
+ const sync = new Command64("sync").description("Drive sync commands");
4808
+ sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
4809
+ const summary = await runDriveSyncOnce(resolve3(path), api);
4810
+ render({ kind: "drive_sync_once", display: { shape: "object" } }, summary);
4811
+ if (summary.conflicts > 0 || summary.errors > 0) {
4812
+ process.exitCode = 1;
4813
+ }
4814
+ });
4815
+ return sync;
4816
+ }
4817
+ async function fetchRemoteManifest(root, state, api, summary, blockedPaths) {
4818
+ const candidates = [];
4819
+ const remoteFiles = {};
4820
+ let cursor;
4821
+ do {
4822
+ const page = await api.getManifest(state.library_id, cursor);
4823
+ for (const entry of page.entries) {
4824
+ try {
4825
+ validateRemoteEntry(root, entry);
4826
+ candidates.push(entry);
4827
+ } catch (error) {
4828
+ await recordPathError(summary, blockedPaths, entry.path, error);
4829
+ }
4830
+ }
4831
+ cursor = page.next_cursor ?? void 0;
4832
+ } while (cursor !== void 0);
4833
+ const byCaseFoldedPath = /* @__PURE__ */ new Map();
4834
+ for (const entry of candidates) {
4835
+ const folded = entry.path.toLowerCase();
4836
+ const group = byCaseFoldedPath.get(folded) ?? [];
4837
+ group.push(entry);
4838
+ byCaseFoldedPath.set(folded, group);
4839
+ }
4840
+ for (const group of byCaseFoldedPath.values()) {
4841
+ const exactPathCounts = /* @__PURE__ */ new Map();
4842
+ for (const entry2 of group) {
4843
+ exactPathCounts.set(entry2.path, (exactPathCounts.get(entry2.path) ?? 0) + 1);
4844
+ }
4845
+ if (group.length > 1) {
4846
+ const hasExactDuplicate = Array.from(exactPathCounts.values()).some((count) => count > 1);
4847
+ const reason = hasExactDuplicate ? "REMOTE_PATH_DUPLICATE" : "REMOTE_PATH_CASE_CONFLICT";
4848
+ for (const entry2 of group.sort((left, right) => left.path.localeCompare(right.path))) {
4849
+ await recordPathError(summary, blockedPaths, entry2.path, new Error(`${reason}: ${entry2.path}`), {
4850
+ appendPathResult: true
4851
+ });
4852
+ }
4853
+ continue;
4854
+ }
4855
+ const [entry] = group;
4856
+ if (entry) remoteFiles[entry.path] = entry;
4857
+ }
4858
+ return remoteFiles;
4859
+ }
4860
+ function validateRemoteEntry(root, entry) {
4861
+ validateDrivePath(entry.path);
4862
+ resolveInsideRoot(root, entry.path);
4863
+ }
4864
+ async function processPath(args) {
4865
+ const { root, state, api, path, action, remote, local, summary } = args;
4866
+ summary.paths.push({ path, action: action.type });
4867
+ let durableStateRequired = false;
4868
+ try {
4869
+ if (action.type === "upload_create" || action.type === "upload_update") {
4870
+ const localPath = resolveInsideRoot(root, path);
4871
+ const { body, digest: uploadDigest } = await readStableUploadBody(localPath, local);
4872
+ const uploaded = await api.uploadFile(state.library_id, path, body, uploadDigest, action.expectedEntryVersion);
4873
+ durableStateRequired = true;
4874
+ const nextState = cloneDriveState(state);
4875
+ nextState.entries[path] = stateEntryFromRemote(uploaded.entry, uploadDigest);
4876
+ delete nextState.conflicts[path];
4877
+ await commitDriveState(root, nextState);
4878
+ summary.uploaded += 1;
4879
+ return { state: nextState, stop: false };
4880
+ }
4881
+ if (action.type === "download") {
4882
+ if (!remote) throw new Error("remote entry missing for download");
4883
+ await assertLocalSafeForDownload(root, path, state.entries[path]);
4884
+ const digest = await downloadRemote(root, state.library_id, path, api, remote.content_sha256, state.entries[path], () => {
4885
+ durableStateRequired = true;
4886
+ });
4887
+ const nextState = cloneDriveState(state);
4888
+ nextState.entries[path] = stateEntryFromRemote(remote, digest);
4889
+ delete nextState.conflicts[path];
4890
+ await commitDriveState(root, nextState);
4891
+ summary.downloaded += 1;
4892
+ return { state: nextState, stop: false };
4893
+ }
4894
+ if (action.type === "delete_remote") {
4895
+ await assertLocalAbsentBeforeRemoteDelete(root, path);
4896
+ await api.deleteFile(state.library_id, path, action.expectedEntryVersion);
4897
+ durableStateRequired = true;
4898
+ await assertLocalAbsentBeforeRemoteDelete(root, path);
4899
+ const nextState = cloneDriveState(state);
4900
+ delete nextState.entries[path];
4901
+ delete nextState.conflicts[path];
4902
+ await commitDriveState(root, nextState);
4903
+ summary.deleted += 1;
4904
+ return { state: nextState, stop: false };
4905
+ }
4906
+ if (action.type === "delete_local") {
4907
+ await removeLocalIfStillBase(root, path, state.entries[path], () => {
4908
+ durableStateRequired = true;
4909
+ });
4910
+ const nextState = cloneDriveState(state);
4911
+ delete nextState.entries[path];
4912
+ delete nextState.conflicts[path];
4913
+ await commitDriveState(root, nextState);
4914
+ summary.deleted += 1;
4915
+ return { state: nextState, stop: false };
4916
+ }
4917
+ if (action.type === "state_only") {
4918
+ if (!remote) throw new Error("remote entry missing for state update");
4919
+ const nextState = cloneDriveState(state);
4920
+ nextState.entries[path] = stateEntryFromRemote(remote, local?.sha256 ?? remote.content_sha256);
4921
+ delete nextState.conflicts[path];
4922
+ await commitDriveState(root, nextState);
4923
+ summary.unchanged += 1;
4924
+ return { state: nextState, stop: false };
4925
+ }
4926
+ if (action.type === "remove_state") {
4927
+ const nextState = cloneDriveState(state);
4928
+ delete nextState.entries[path];
4929
+ delete nextState.conflicts[path];
4930
+ await commitDriveState(root, nextState);
4931
+ summary.unchanged += 1;
4932
+ return { state: nextState, stop: false };
4933
+ }
4934
+ if (action.type === "conflict") {
4935
+ const nextState = await recordConflict(root, state, path, action.reason, remote);
4936
+ summary.conflicts += 1;
4937
+ return { state: nextState, stop: false };
4938
+ }
4939
+ summary.unchanged += 1;
4940
+ } catch (error) {
4941
+ if (isVersionConflict(error)) {
4942
+ try {
4943
+ const nextState = await recordConflict(root, state, path, "VERSION_CONFLICT", remote);
4944
+ summary.conflicts += 1;
4945
+ summary.paths[summary.paths.length - 1] = { path, action: "conflict" };
4946
+ return { state: nextState, stop: false };
4947
+ } catch (writeError) {
4948
+ await recordPathError(summary, void 0, path, writeError);
4949
+ return { state, stop: durableStateRequired };
4950
+ }
4951
+ }
4952
+ await recordPathError(summary, void 0, path, error);
4953
+ return { state, stop: durableStateRequired };
4954
+ }
4955
+ return { state, stop: false };
4956
+ }
4957
+ function recordUnresolvedConflicts(summary, state) {
4958
+ const newlyRecorded = new Set(summary.paths.filter((result) => result.action === "conflict").map((result) => result.path));
4959
+ const reportedPaths = new Set(summary.paths.map((result) => result.path));
4960
+ for (const path of Object.keys(state.conflicts).sort((left, right) => left.localeCompare(right))) {
4961
+ if (!newlyRecorded.has(path)) {
4962
+ summary.conflicts += 1;
4963
+ }
4964
+ const existingResult = summary.paths.find((result) => result.path === path);
4965
+ if (existingResult?.action === "unchanged") {
4966
+ existingResult.action = "conflict";
4967
+ continue;
4968
+ }
4969
+ if (!reportedPaths.has(path)) {
4970
+ summary.paths.push({ path, action: "conflict" });
4971
+ }
4972
+ }
4973
+ }
4974
+ async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
4975
+ const target = resolveInsideRoot(root, path);
4976
+ await mkdir2(dirname(target), { recursive: true });
4977
+ const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID2()}.tmp`);
4978
+ try {
4979
+ const response = await api.downloadFile(libraryId, path);
4980
+ if (!response.body) {
4981
+ throw new Error("download response body missing");
4982
+ }
4983
+ const hash = createHash2("sha256");
4984
+ const hashingStream = new Transform({
4985
+ transform(chunk, _encoding, callback) {
4986
+ hash.update(chunk);
4987
+ callback(void 0, chunk);
4988
+ }
4989
+ });
4990
+ await pipeline2(
4991
+ Readable2.fromWeb(response.body),
4992
+ hashingStream,
4993
+ createWriteStream2(tmp, { flags: "wx" })
4994
+ );
4995
+ const digest = hash.digest("hex");
4996
+ if (expectedSha256 !== void 0 && digest !== expectedSha256) {
4997
+ throw new Error(`download hash mismatch: expected ${expectedSha256}, got ${digest}`);
4998
+ }
4999
+ await installDownloadedFile(root, path, tmp, entry, onLocalMutation);
5000
+ return digest;
5001
+ } finally {
5002
+ await rm2(tmp, { force: true }).catch(() => {
5003
+ });
5004
+ }
5005
+ }
5006
+ async function installDownloadedFile(root, path, tmp, entry, onLocalMutation) {
5007
+ const target = resolveInsideRoot(root, path);
5008
+ const backup = localMutationBackupPath(target);
5009
+ const expectedSha256 = expectedLocalBaseSha256(entry);
5010
+ let backupIsExpectedBase = false;
5011
+ try {
5012
+ try {
5013
+ await rename2(target, backup);
5014
+ onLocalMutation();
5015
+ } catch (error) {
5016
+ if (!isNotFoundError(error)) throw error;
5017
+ await installNoOverwrite(tmp, target, onLocalMutation);
5018
+ return;
5019
+ }
5020
+ const backupDigest = await hashDriveFile(backup);
5021
+ if (!backupDigest) {
5022
+ await restoreBackupWhenPossible(backup, target);
5023
+ throw new Error("local file changed before download");
5024
+ }
5025
+ if (!expectedSha256 || backupDigest.sha256 !== expectedSha256) {
5026
+ await restoreBackupWhenPossible(backup, target);
5027
+ throw new Error("local file changed before download");
5028
+ }
5029
+ backupIsExpectedBase = true;
5030
+ try {
5031
+ await installNoOverwrite(tmp, target, onLocalMutation);
5032
+ } catch (error) {
5033
+ const restored = await restoreBackupWhenPossible(backup, target);
5034
+ if (!restored && backupIsExpectedBase) {
5035
+ await unlink(backup).catch(() => {
5036
+ });
5037
+ }
5038
+ throw error;
5039
+ }
5040
+ await unlink(backup);
5041
+ } catch (error) {
5042
+ if (!backupIsExpectedBase) {
5043
+ await restoreBackupWhenPossible(backup, target);
5044
+ }
5045
+ throw error;
5046
+ }
5047
+ }
5048
+ async function removeLocalIfStillBase(root, path, entry, onLocalMutation) {
5049
+ const target = resolveInsideRoot(root, path);
5050
+ const backup = localMutationBackupPath(target);
5051
+ const expectedSha256 = expectedLocalBaseSha256(entry);
5052
+ if (!expectedSha256) {
5053
+ throw new Error("local file has no sync base");
5054
+ }
5055
+ let backupIsExpectedBase = false;
5056
+ try {
5057
+ try {
5058
+ await rename2(target, backup);
5059
+ onLocalMutation();
5060
+ } catch (error) {
5061
+ if (isNotFoundError(error)) {
5062
+ throw new Error("local file changed before delete");
5063
+ }
5064
+ throw error;
5065
+ }
5066
+ const backupDigest = await hashDriveFile(backup);
5067
+ if (!backupDigest || backupDigest.sha256 !== expectedSha256) {
5068
+ await restoreBackupWhenPossible(backup, target);
5069
+ throw new Error("local file changed before delete");
5070
+ }
5071
+ backupIsExpectedBase = true;
5072
+ if (await localFileExists(target)) {
5073
+ await unlink(backup).catch(() => {
5074
+ });
5075
+ throw new Error("local file reappeared during delete");
5076
+ }
5077
+ await unlink(backup);
5078
+ if (await localFileExists(target)) {
5079
+ throw new Error("local file reappeared during delete");
5080
+ }
5081
+ } catch (error) {
5082
+ if (!backupIsExpectedBase) {
5083
+ await restoreBackupWhenPossible(backup, target);
5084
+ }
5085
+ throw error;
5086
+ }
5087
+ }
5088
+ async function installNoOverwrite(source, target, onLinked) {
5089
+ await link(source, target);
5090
+ onLinked?.();
5091
+ await unlink(source);
5092
+ }
5093
+ async function restoreBackupWhenPossible(backup, target) {
5094
+ try {
5095
+ await installNoOverwrite(backup, target);
5096
+ return true;
5097
+ } catch (error) {
5098
+ if (isAlreadyExistsError(error)) return false;
5099
+ if (isNotFoundError(error)) return true;
5100
+ return false;
5101
+ }
5102
+ }
5103
+ async function localFileExists(path) {
5104
+ const digest = await hashDriveFile(path).catch((error) => {
5105
+ if (isNotFoundError(error)) return void 0;
5106
+ throw error;
5107
+ });
5108
+ return digest !== void 0;
5109
+ }
5110
+ function localMutationBackupPath(target) {
5111
+ return join4(dirname(target), `.${basename2(target)}.wspc-backup-${randomUUID2()}.tmp`);
5112
+ }
5113
+ function expectedLocalBaseSha256(entry) {
5114
+ return entry?.last_local_sha256 ?? entry?.content_sha256;
5115
+ }
5116
+ async function readStableUploadBody(localPath, scanned) {
5117
+ if (!scanned) {
5118
+ throw new Error("local file missing from scan");
5119
+ }
5120
+ const snapshot = await hashDriveFile(localPath).catch((error) => {
5121
+ if (isNotFoundError(error)) return void 0;
5122
+ throw error;
5123
+ });
5124
+ if (!snapshot || snapshot.sha256 !== scanned.sha256 || snapshot.sizeBytes !== scanned.size_bytes) {
5125
+ throw new Error("local file changed after scan");
5126
+ }
5127
+ const body = await readFile3(localPath).catch((error) => {
5128
+ if (isNotFoundError(error)) return void 0;
5129
+ throw error;
5130
+ });
5131
+ if (!body) {
5132
+ throw new Error("local file changed after scan");
5133
+ }
5134
+ const uploadBytes = new Uint8Array(body.byteLength);
5135
+ uploadBytes.set(body);
5136
+ const digest = createHash2("sha256").update(uploadBytes).digest("hex");
5137
+ if (digest !== scanned.sha256 || uploadBytes.byteLength !== scanned.size_bytes) {
5138
+ throw new Error("local file changed after scan");
5139
+ }
5140
+ return { body: uploadBytes.buffer, digest };
5141
+ }
5142
+ async function assertLocalSafeForDownload(root, path, entry) {
5143
+ const target = resolveInsideRoot(root, path);
5144
+ const digest = await hashDriveFile(target).catch((error) => {
5145
+ if (isNotFoundError(error)) return void 0;
5146
+ throw error;
5147
+ });
5148
+ if (!digest) return;
5149
+ if (!entry?.last_local_sha256) {
5150
+ throw new Error("local file appeared before download");
5151
+ }
5152
+ if (digest.sha256 !== entry.last_local_sha256) {
5153
+ throw new Error("local file changed before download");
5154
+ }
5155
+ }
5156
+ async function assertLocalAbsentBeforeRemoteDelete(root, path) {
5157
+ const digest = await hashDriveFile(resolveInsideRoot(root, path)).catch((error) => {
5158
+ if (isNotFoundError(error)) return void 0;
5159
+ throw error;
5160
+ });
5161
+ if (digest) {
5162
+ throw new Error("local file appeared before remote delete");
5163
+ }
5164
+ }
5165
+ function stateEntryFromRemote(remote, localSha256) {
5166
+ return {
5167
+ entry_id: remote.id,
5168
+ entry_version: remote.entry_version,
5169
+ current_version_id: remote.current_version_id,
5170
+ content_sha256: remote.content_sha256,
5171
+ size_bytes: remote.size_bytes,
5172
+ last_local_sha256: localSha256,
5173
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
5174
+ status: "synced"
5175
+ };
5176
+ }
5177
+ async function commitDriveState(root, nextState) {
5178
+ await writeDriveState(root, nextState);
5179
+ }
5180
+ function cloneDriveState(state) {
5181
+ return {
5182
+ ...state,
5183
+ entries: { ...state.entries },
5184
+ conflicts: { ...state.conflicts }
5185
+ };
5186
+ }
5187
+ async function recordConflict(root, state, path, reason, remote) {
5188
+ const nextState = cloneDriveState(state);
5189
+ nextState.conflicts[path] = conflict(reason, remote);
5190
+ await commitDriveState(root, nextState);
5191
+ return nextState;
5192
+ }
5193
+ async function recordPathError(summary, blockedPaths, path, error, options = {}) {
5194
+ blockedPaths?.add(path);
5195
+ const lastPath = summary.paths.at(-1);
5196
+ if (!options.appendPathResult && lastPath?.path === path) {
5197
+ lastPath.action = "error";
5198
+ } else {
5199
+ summary.paths.push({ path, action: "error" });
5200
+ }
5201
+ void errorMessage(error);
5202
+ summary.errors += 1;
5203
+ }
5204
+ function conflict(reason, remote) {
5205
+ return {
5206
+ detected_at: (/* @__PURE__ */ new Date()).toISOString(),
5207
+ reason,
5208
+ remote_entry_version: remote?.entry_version,
5209
+ remote_version_id: remote?.current_version_id
5210
+ };
5211
+ }
5212
+ function errorMessage(error) {
5213
+ return error instanceof Error ? error.message : String(error);
5214
+ }
5215
+ function isVersionConflict(error) {
5216
+ const structured = error;
5217
+ if (structured?.code === "VERSION_CONFLICT") return true;
5218
+ return [errorMessage(error), structured?.body, structured?.response?.body].some(containsVersionConflict);
5219
+ }
5220
+ function containsVersionConflict(value) {
5221
+ if (value === void 0) return false;
5222
+ if (typeof value === "string") return value.includes("VERSION_CONFLICT");
5223
+ try {
5224
+ return JSON.stringify(value).includes("VERSION_CONFLICT");
5225
+ } catch {
5226
+ return false;
5227
+ }
5228
+ }
5229
+ function isNotFoundError(error) {
5230
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
5231
+ }
5232
+ function isAlreadyExistsError(error) {
5233
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
5234
+ }
5235
+
4277
5236
  // src/cli.ts
5237
+ function mountDriveCommands(program) {
5238
+ let drive = program.commands.find((c) => c.name() === "drive");
5239
+ if (!drive) {
5240
+ drive = new Command65("drive").description("Drive commands");
5241
+ program.addCommand(drive);
5242
+ }
5243
+ if (!drive.commands.some((c) => c.name() === "bind")) {
5244
+ drive.addCommand(driveBindCommand());
5245
+ }
5246
+ const sync = drive.commands.find((c) => c.name() === "sync");
5247
+ if (!sync) {
5248
+ drive.addCommand(driveSyncCommand());
5249
+ } else if (!sync.commands.some((c) => c.name() === "once")) {
5250
+ const once = driveSyncCommand().commands.find((c) => c.name() === "once");
5251
+ if (once) sync.addCommand(once);
5252
+ }
5253
+ }
5254
+ function isCliEntrypoint(argv = process.argv, metaUrl = import.meta.url) {
5255
+ if (!argv[1]) return false;
5256
+ try {
5257
+ return realpathSync(argv[1]) === realpathSync(fileURLToPath(metaUrl));
5258
+ } catch {
5259
+ return false;
5260
+ }
5261
+ }
4278
5262
  function buildProgram() {
4279
- const program = new Command63().name("wspc").description("Official CLI for wspc.ai").version(`wspc ${VERSION} (spec ${SPEC_SHA}, fetched ${SPEC_FETCHED_AT})`).option("--json", "Output raw JSON (machine-readable)").option("--account <email>", "Run as a specific account (overrides the active account)").hook("preAction", (_thisCommand, actionCommand) => {
5263
+ const program = new Command65().name("wspc").description("Official CLI for wspc.ai").version(`wspc ${VERSION} (spec ${SPEC_SHA}, fetched ${SPEC_FETCHED_AT})`).option("--json", "Output raw JSON (machine-readable)").option("--account <email>", "Run as a specific account (overrides the active account)").hook("preAction", (_thisCommand, actionCommand) => {
4280
5264
  const globals = actionCommand.optsWithGlobals();
4281
5265
  if (globals.json) process.env.WSPC_OUTPUT = "json";
4282
5266
  if (globals.account) process.env.WSPC_ACCOUNT = String(globals.account);
@@ -4287,6 +5271,7 @@ function buildProgram() {
4287
5271
  program.addCommand(configCommand);
4288
5272
  program.addCommand(accountCommand);
4289
5273
  registerGeneratedCommands(program);
5274
+ mountDriveCommands(program);
4290
5275
  const todo = program.commands.find((c) => c.name() === "todo");
4291
5276
  if (todo) todo.addCommand(todoDoneCommand);
4292
5277
  const email = program.commands.find((c) => c.name() === "email");
@@ -4319,8 +5304,12 @@ async function dispatch(argv, { allowRetry = true } = {}) {
4319
5304
  process.exitCode = 1;
4320
5305
  }
4321
5306
  }
4322
- dispatch(process.argv);
5307
+ if (isCliEntrypoint()) {
5308
+ dispatch(process.argv);
5309
+ }
4323
5310
  export {
4324
- dispatch
5311
+ dispatch,
5312
+ isCliEntrypoint,
5313
+ mountDriveCommands
4325
5314
  };
4326
5315
  //# sourceMappingURL=cli.js.map