@wspc/cli 0.1.1 → 0.1.3
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/README.md +26 -9
- package/dist/cli.js +1468 -398
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/spec/openapi.json +124 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command73 } from "commander";
|
|
5
5
|
import { realpathSync } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
@@ -930,6 +930,20 @@ var eventIcsDownload = (options) => (options.client ?? client).get({
|
|
|
930
930
|
url: "/calendar/events/{filename}",
|
|
931
931
|
...options
|
|
932
932
|
});
|
|
933
|
+
var driveLibraryList = (options) => (options?.client ?? client).get({
|
|
934
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
935
|
+
url: "/drive/libraries",
|
|
936
|
+
...options
|
|
937
|
+
});
|
|
938
|
+
var driveLibraryCreate = (options) => (options?.client ?? client).post({
|
|
939
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
940
|
+
url: "/drive/libraries",
|
|
941
|
+
...options,
|
|
942
|
+
headers: {
|
|
943
|
+
"Content-Type": "application/json",
|
|
944
|
+
...options?.headers
|
|
945
|
+
}
|
|
946
|
+
});
|
|
933
947
|
var driveFileDelete = (options) => (options.client ?? client).post({
|
|
934
948
|
security: [{ scheme: "bearer", type: "http" }],
|
|
935
949
|
url: "/drive/libraries/{id}/files/delete",
|
|
@@ -939,11 +953,29 @@ var driveFileDelete = (options) => (options.client ?? client).post({
|
|
|
939
953
|
...options.headers
|
|
940
954
|
}
|
|
941
955
|
});
|
|
956
|
+
var driveLibraryDelete = (options) => (options.client ?? client).delete({
|
|
957
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
958
|
+
url: "/drive/libraries/{id}",
|
|
959
|
+
...options,
|
|
960
|
+
headers: {
|
|
961
|
+
"Content-Type": "application/json",
|
|
962
|
+
...options.headers
|
|
963
|
+
}
|
|
964
|
+
});
|
|
942
965
|
var driveLibraryGet = (options) => (options.client ?? client).get({
|
|
943
966
|
security: [{ scheme: "bearer", type: "http" }],
|
|
944
967
|
url: "/drive/libraries/{id}",
|
|
945
968
|
...options
|
|
946
969
|
});
|
|
970
|
+
var driveLibraryUpdate = (options) => (options.client ?? client).patch({
|
|
971
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
972
|
+
url: "/drive/libraries/{id}",
|
|
973
|
+
...options,
|
|
974
|
+
headers: {
|
|
975
|
+
"Content-Type": "application/json",
|
|
976
|
+
...options.headers
|
|
977
|
+
}
|
|
978
|
+
});
|
|
947
979
|
var driveManifestGet = (options) => (options.client ?? client).get({
|
|
948
980
|
security: [{ scheme: "bearer", type: "http" }],
|
|
949
981
|
url: "/drive/libraries/{id}/manifest",
|
|
@@ -1432,9 +1464,9 @@ function createConsistencyFetch(opts) {
|
|
|
1432
1464
|
}
|
|
1433
1465
|
|
|
1434
1466
|
// src/version.ts
|
|
1435
|
-
var VERSION = "0.1.
|
|
1436
|
-
var SPEC_SHA = "
|
|
1437
|
-
var SPEC_FETCHED_AT = "2026-06-
|
|
1467
|
+
var VERSION = "0.1.3";
|
|
1468
|
+
var SPEC_SHA = "e0bbc1e3";
|
|
1469
|
+
var SPEC_FETCHED_AT = "2026-06-22T01:23:52.693Z";
|
|
1438
1470
|
var API_BASE = "https://api.wspc.ai";
|
|
1439
1471
|
|
|
1440
1472
|
// src/index.ts
|
|
@@ -1588,8 +1620,19 @@ async function loadAuthedFetch(opts = {}) {
|
|
|
1588
1620
|
const { fetch: fetch2, baseUrl } = await loadClientParts(opts);
|
|
1589
1621
|
return { fetch: fetch2, baseUrl };
|
|
1590
1622
|
}
|
|
1623
|
+
async function loadRealtimeAuthHeaders(opts = {}) {
|
|
1624
|
+
const { baseUrl, authInterceptor } = await loadClientParts(opts);
|
|
1625
|
+
const verifyUrl = new URL(opts.verifyPath ?? "/auth/me", `${baseUrl.replace(/\/$/, "")}/`);
|
|
1626
|
+
const verifyResponse = await authInterceptor.execute(new Request(verifyUrl));
|
|
1627
|
+
if (!verifyResponse.ok) {
|
|
1628
|
+
throw new Error(`realtime auth failed: HTTP ${verifyResponse.status}`);
|
|
1629
|
+
}
|
|
1630
|
+
const request = await authInterceptor.onRequest(new Request(baseUrl));
|
|
1631
|
+
return { baseUrl, headers: request.headers };
|
|
1632
|
+
}
|
|
1591
1633
|
async function loadSdkClientWithAuthedFetch(opts = {}) {
|
|
1592
|
-
|
|
1634
|
+
const { _rawClient, fetch: fetch2, baseUrl } = await loadClientParts(opts);
|
|
1635
|
+
return { _rawClient, fetch: fetch2, baseUrl };
|
|
1593
1636
|
}
|
|
1594
1637
|
async function loadClientParts(opts = {}) {
|
|
1595
1638
|
const store = opts.store ?? new ConfigStore();
|
|
@@ -1609,7 +1652,7 @@ async function loadClientParts(opts = {}) {
|
|
|
1609
1652
|
fetch: authedFetch
|
|
1610
1653
|
})
|
|
1611
1654
|
);
|
|
1612
|
-
return { _rawClient: rawClient, fetch: authedFetch, baseUrl: resolved.apiBase };
|
|
1655
|
+
return { _rawClient: rawClient, fetch: authedFetch, baseUrl: resolved.apiBase, authInterceptor: interceptor };
|
|
1613
1656
|
}
|
|
1614
1657
|
|
|
1615
1658
|
// src/handwritten/output/primitives.ts
|
|
@@ -1835,22 +1878,24 @@ function render(ctx, data) {
|
|
|
1835
1878
|
renderPaginationFooter(data);
|
|
1836
1879
|
}
|
|
1837
1880
|
function renderPaginationFooter(data) {
|
|
1838
|
-
if (data
|
|
1839
|
-
const
|
|
1840
|
-
if (
|
|
1841
|
-
process.stdout.write(dim(` \u2026 more results \u2014 re-run with --cursor ${
|
|
1842
|
-
}
|
|
1843
|
-
const id =
|
|
1844
|
-
|
|
1881
|
+
if (!isRecord(data)) return;
|
|
1882
|
+
const nextCursor = getString(data, "next_cursor");
|
|
1883
|
+
if (nextCursor !== void 0 && nextCursor.length > 0) {
|
|
1884
|
+
process.stdout.write(dim(` \u2026 more results \u2014 re-run with --cursor ${nextCursor}`) + "\n");
|
|
1885
|
+
}
|
|
1886
|
+
const id = getString(data, "id") ?? "<id>";
|
|
1887
|
+
const childrenNextCursor = getString(data, "children_next_cursor");
|
|
1888
|
+
if (childrenNextCursor !== void 0 && childrenNextCursor.length > 0) {
|
|
1845
1889
|
process.stdout.write(dim(` \u2026 more children \u2014 wspc todo ls --parent ${id}`) + "\n");
|
|
1846
1890
|
}
|
|
1847
|
-
|
|
1891
|
+
const commentsNextCursor = getString(data, "comments_next_cursor");
|
|
1892
|
+
if (commentsNextCursor !== void 0 && commentsNextCursor.length > 0) {
|
|
1848
1893
|
process.stdout.write(dim(` \u2026 more comments \u2014 wspc todo comment ls ${id}`) + "\n");
|
|
1849
1894
|
}
|
|
1850
1895
|
}
|
|
1851
1896
|
function drillDataPath(data, dataPath) {
|
|
1852
1897
|
if (!dataPath) return data;
|
|
1853
|
-
if (data
|
|
1898
|
+
if (!isRecord(data)) return data;
|
|
1854
1899
|
const value = data[dataPath];
|
|
1855
1900
|
return value === void 0 ? data : value;
|
|
1856
1901
|
}
|
|
@@ -1878,9 +1923,9 @@ function renderGeneric(data, hints) {
|
|
|
1878
1923
|
}
|
|
1879
1924
|
function detectShape(data) {
|
|
1880
1925
|
if (Array.isArray(data)) return "list";
|
|
1881
|
-
if (
|
|
1926
|
+
if (isRecord(data)) {
|
|
1882
1927
|
const keys = Object.keys(data);
|
|
1883
|
-
const arrayKeys = keys.filter((k) =>
|
|
1928
|
+
const arrayKeys = keys.filter((k) => getArray(data, k) !== void 0);
|
|
1884
1929
|
if (arrayKeys.length === 1) return "list";
|
|
1885
1930
|
return "object";
|
|
1886
1931
|
}
|
|
@@ -1888,9 +1933,10 @@ function detectShape(data) {
|
|
|
1888
1933
|
}
|
|
1889
1934
|
function extractItems(data) {
|
|
1890
1935
|
if (Array.isArray(data)) return data;
|
|
1891
|
-
if (
|
|
1892
|
-
for (const
|
|
1893
|
-
|
|
1936
|
+
if (isRecord(data)) {
|
|
1937
|
+
for (const key of Object.keys(data)) {
|
|
1938
|
+
const value = getArray(data, key);
|
|
1939
|
+
if (value !== void 0) return value;
|
|
1894
1940
|
}
|
|
1895
1941
|
}
|
|
1896
1942
|
return [];
|
|
@@ -1901,13 +1947,13 @@ function renderList(data, hints) {
|
|
|
1901
1947
|
process.stdout.write(dim(" " + (hints?.emptyMessage ?? "no items")) + "\n");
|
|
1902
1948
|
return;
|
|
1903
1949
|
}
|
|
1904
|
-
const first = items[0];
|
|
1950
|
+
const first = isRecord(items[0]) ? items[0] : {};
|
|
1905
1951
|
const columns = pickColumns(first, hints?.columns);
|
|
1906
1952
|
const format = hints?.format ?? {};
|
|
1907
1953
|
const headers = columns.map((c) => c.toUpperCase());
|
|
1908
1954
|
const rows = items.map(
|
|
1909
1955
|
(item) => columns.map(
|
|
1910
|
-
(col) => formatCell(item[col], format[col], hints?.enumColorMap?.[col])
|
|
1956
|
+
(col) => formatCell(isRecord(item) ? item[col] : void 0, format[col], hints?.enumColorMap?.[col])
|
|
1911
1957
|
)
|
|
1912
1958
|
);
|
|
1913
1959
|
process.stdout.write(table(headers, rows));
|
|
@@ -1925,7 +1971,7 @@ function pickColumns(first, hint) {
|
|
|
1925
1971
|
return ordered.slice(0, 5);
|
|
1926
1972
|
}
|
|
1927
1973
|
function renderObject(data, hints) {
|
|
1928
|
-
if (
|
|
1974
|
+
if (!isRecord(data)) {
|
|
1929
1975
|
renderScalar(data);
|
|
1930
1976
|
return;
|
|
1931
1977
|
}
|
|
@@ -1934,7 +1980,7 @@ function renderObject(data, hints) {
|
|
|
1934
1980
|
if (topKeys.length === 1) {
|
|
1935
1981
|
const onlyKey = topKeys[0];
|
|
1936
1982
|
const inner = obj[onlyKey];
|
|
1937
|
-
if (
|
|
1983
|
+
if (isRecord(inner)) {
|
|
1938
1984
|
obj = inner;
|
|
1939
1985
|
}
|
|
1940
1986
|
}
|
|
@@ -1945,7 +1991,10 @@ function renderObject(data, hints) {
|
|
|
1945
1991
|
}
|
|
1946
1992
|
const format = hints?.format ?? {};
|
|
1947
1993
|
const arrayFields = hints?.fields ? [] : Object.keys(obj).filter(
|
|
1948
|
-
(k) =>
|
|
1994
|
+
(k) => {
|
|
1995
|
+
const value = getArray(obj, k);
|
|
1996
|
+
return value !== void 0 && value.length > 0;
|
|
1997
|
+
}
|
|
1949
1998
|
);
|
|
1950
1999
|
const formatted = fields.map((f) => [
|
|
1951
2000
|
f,
|
|
@@ -1974,7 +2023,8 @@ function renderObject(data, hints) {
|
|
|
1974
2023
|
for (const f of arrayFields) {
|
|
1975
2024
|
const uncapped = f === "children" || f === "comments";
|
|
1976
2025
|
const max = uncapped ? Number.POSITIVE_INFINITY : ARRAY_FIELD_MAX_ITEMS;
|
|
1977
|
-
|
|
2026
|
+
const items = getArray(obj, f);
|
|
2027
|
+
if (items !== void 0) renderArrayField(f, items, labelWidth, max);
|
|
1978
2028
|
}
|
|
1979
2029
|
const hadAbove = inlineFinal.length > 0 || arrayFields.length > 0;
|
|
1980
2030
|
blocks.forEach(([f, value], i) => {
|
|
@@ -2026,28 +2076,31 @@ function formatArrayItem(item) {
|
|
|
2026
2076
|
return JSON.stringify(item);
|
|
2027
2077
|
}
|
|
2028
2078
|
function formatTodoLike(item) {
|
|
2029
|
-
if (!
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
const status =
|
|
2034
|
-
|
|
2079
|
+
if (!isRecord(item)) return null;
|
|
2080
|
+
const idValue = getString(item, "id");
|
|
2081
|
+
const title = getString(item, "title");
|
|
2082
|
+
if (idValue === void 0 || title === void 0) return null;
|
|
2083
|
+
const status = getString(item, "status");
|
|
2084
|
+
const id = idShort(idValue);
|
|
2085
|
+
const statusText = status === void 0 ? "" : statusBadge(status);
|
|
2086
|
+
return statusText ? `${id} ${statusText} ${title}` : `${id} ${title}`;
|
|
2035
2087
|
}
|
|
2036
2088
|
function formatCommentLike(item) {
|
|
2037
|
-
if (!
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
const
|
|
2042
|
-
const
|
|
2089
|
+
if (!isRecord(item)) return null;
|
|
2090
|
+
const idValue = getString(item, "id");
|
|
2091
|
+
const content = getString(item, "content");
|
|
2092
|
+
if (idValue === void 0 || content === void 0) return null;
|
|
2093
|
+
const id = idShort(idValue);
|
|
2094
|
+
const when = item.created_at !== void 0 ? `${relativeTime(item.created_at)} ` : "";
|
|
2095
|
+
const snippet = truncate(content, 60);
|
|
2043
2096
|
return `${id} ${when}${snippet}`;
|
|
2044
2097
|
}
|
|
2045
2098
|
function formatAttendeeLike(item) {
|
|
2046
|
-
if (!
|
|
2047
|
-
const
|
|
2048
|
-
const email = typeof rec.email === "string" ? rec.email : null;
|
|
2099
|
+
if (!isRecord(item)) return null;
|
|
2100
|
+
const email = getString(item, "email");
|
|
2049
2101
|
if (!email) return null;
|
|
2050
|
-
const
|
|
2102
|
+
const displayName = getString(item, "display_name");
|
|
2103
|
+
const name = displayName !== void 0 && displayName.length > 0 ? displayName : void 0;
|
|
2051
2104
|
return name ? `${name} <${email}>` : `<${email}>`;
|
|
2052
2105
|
}
|
|
2053
2106
|
function renderScalar(data) {
|
|
@@ -2068,6 +2121,17 @@ function renderScalar(data) {
|
|
|
2068
2121
|
function isScalar(v) {
|
|
2069
2122
|
return v === null || typeof v !== "object" && typeof v !== "function";
|
|
2070
2123
|
}
|
|
2124
|
+
function isRecord(value) {
|
|
2125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2126
|
+
}
|
|
2127
|
+
function getString(record, key) {
|
|
2128
|
+
const value = record[key];
|
|
2129
|
+
return typeof value === "string" ? value : void 0;
|
|
2130
|
+
}
|
|
2131
|
+
function getArray(record, key) {
|
|
2132
|
+
const value = record[key];
|
|
2133
|
+
return Array.isArray(value) ? value : void 0;
|
|
2134
|
+
}
|
|
2071
2135
|
function formatCell(value, fmt, colorMap, opts) {
|
|
2072
2136
|
if (fmt !== "enum-badge" && (value === void 0 || value === null)) return dim("\u2014");
|
|
2073
2137
|
switch (fmt) {
|
|
@@ -2697,9 +2761,174 @@ var eventIcsDownloadCommand = new Command21("ics").description("Download event a
|
|
|
2697
2761
|
render({ kind: "event_ics_download", display: { "shape": "raw" } }, result.data);
|
|
2698
2762
|
});
|
|
2699
2763
|
|
|
2700
|
-
// src/generated/cli/
|
|
2764
|
+
// src/generated/cli/drive/library/add.ts
|
|
2701
2765
|
import { Command as Command22 } from "commander";
|
|
2702
|
-
var
|
|
2766
|
+
var driveLibraryCreateCommand = new Command22("add").description("Create a drive library").argument("<name>", "name").action(async (name, opts) => {
|
|
2767
|
+
const client2 = await loadSdkClient();
|
|
2768
|
+
const result = await driveLibraryCreate({
|
|
2769
|
+
client: client2._rawClient,
|
|
2770
|
+
body: {
|
|
2771
|
+
name
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
if (result.error || !result.response?.ok) {
|
|
2775
|
+
process.stderr.write(
|
|
2776
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2777
|
+
`
|
|
2778
|
+
);
|
|
2779
|
+
process.exitCode = 1;
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
render({ kind: "drive_library_create", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
// src/generated/cli/drive/library/ls.ts
|
|
2786
|
+
import { Command as Command23 } from "commander";
|
|
2787
|
+
var driveLibraryListCommand = new Command23("ls").description("List drive libraries").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").action(async (opts) => {
|
|
2788
|
+
const client2 = await loadSdkClient();
|
|
2789
|
+
const result = await driveLibraryList({
|
|
2790
|
+
client: client2._rawClient,
|
|
2791
|
+
query: {
|
|
2792
|
+
limit: opts.limit,
|
|
2793
|
+
cursor: opts.cursor,
|
|
2794
|
+
include_deleted: opts.includeDeleted
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
if (result.error || !result.response?.ok) {
|
|
2798
|
+
process.stderr.write(
|
|
2799
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2800
|
+
`
|
|
2801
|
+
);
|
|
2802
|
+
process.exitCode = 1;
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
render({ kind: "drive_library_list", display: { "shape": "list", "dataPath": "libraries", "columns": ["id", "name", "file_count", "storage_bytes", "updated_at"], "emptyMessage": "no drive libraries" } }, result.data);
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
// src/generated/cli/drive/file/rm.ts
|
|
2809
|
+
import { Command as Command24 } from "commander";
|
|
2810
|
+
var driveFileDeleteCommand = new Command24("rm").description("Delete a drive file").argument("<id>", "id").argument("<path>", "path").option("--expected-entry-version <value>", "expected_entry_version").action(async (id, path, opts) => {
|
|
2811
|
+
const client2 = await loadSdkClient();
|
|
2812
|
+
const result = await driveFileDelete({
|
|
2813
|
+
client: client2._rawClient,
|
|
2814
|
+
path: {
|
|
2815
|
+
id
|
|
2816
|
+
},
|
|
2817
|
+
body: {
|
|
2818
|
+
path,
|
|
2819
|
+
expected_entry_version: opts.expectedEntryVersion
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
if (result.error || !result.response?.ok) {
|
|
2823
|
+
process.stderr.write(
|
|
2824
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2825
|
+
`
|
|
2826
|
+
);
|
|
2827
|
+
process.exitCode = 1;
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
render({ kind: "drive_file_delete", display: { "shape": "object", "dataPath": "entry", "columns": ["path", "entry_version", "deleted_at"] } }, result.data);
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
// src/generated/cli/drive/library/rm.ts
|
|
2834
|
+
import { Command as Command25 } from "commander";
|
|
2835
|
+
var driveLibraryDeleteCommand = new Command25("rm").description("Delete a drive library").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
|
|
2836
|
+
const client2 = await loadSdkClient();
|
|
2837
|
+
const result = await driveLibraryDelete({
|
|
2838
|
+
client: client2._rawClient,
|
|
2839
|
+
path: {
|
|
2840
|
+
id
|
|
2841
|
+
},
|
|
2842
|
+
body: {
|
|
2843
|
+
expected_version: opts.expectedVersion
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
if (result.error || !result.response?.ok) {
|
|
2847
|
+
process.stderr.write(
|
|
2848
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2849
|
+
`
|
|
2850
|
+
);
|
|
2851
|
+
process.exitCode = 1;
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
render({ kind: "drive_library_delete", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
|
|
2855
|
+
});
|
|
2856
|
+
|
|
2857
|
+
// src/generated/cli/drive/library/show.ts
|
|
2858
|
+
import { Command as Command26 } from "commander";
|
|
2859
|
+
var driveLibraryGetCommand = new Command26("show").description("Get a drive library").argument("<id>", "id").action(async (id, opts) => {
|
|
2860
|
+
const client2 = await loadSdkClient();
|
|
2861
|
+
const result = await driveLibraryGet({
|
|
2862
|
+
client: client2._rawClient,
|
|
2863
|
+
path: {
|
|
2864
|
+
id
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
if (result.error || !result.response?.ok) {
|
|
2868
|
+
process.stderr.write(
|
|
2869
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2870
|
+
`
|
|
2871
|
+
);
|
|
2872
|
+
process.exitCode = 1;
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
render({ kind: "drive_library_get", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
// src/generated/cli/drive/library/update.ts
|
|
2879
|
+
import { Command as Command27 } from "commander";
|
|
2880
|
+
var driveLibraryUpdateCommand = new Command27("update").description("Update a drive library").argument("<id>", "id").option("--name <value>", "name").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
|
|
2881
|
+
const client2 = await loadSdkClient();
|
|
2882
|
+
const result = await driveLibraryUpdate({
|
|
2883
|
+
client: client2._rawClient,
|
|
2884
|
+
path: {
|
|
2885
|
+
id
|
|
2886
|
+
},
|
|
2887
|
+
body: {
|
|
2888
|
+
name: opts.name,
|
|
2889
|
+
expected_version: opts.expectedVersion
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
if (result.error || !result.response?.ok) {
|
|
2893
|
+
process.stderr.write(
|
|
2894
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2895
|
+
`
|
|
2896
|
+
);
|
|
2897
|
+
process.exitCode = 1;
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
render({ kind: "drive_library_update", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
|
|
2901
|
+
});
|
|
2902
|
+
|
|
2903
|
+
// src/generated/cli/drive/manifest/get.ts
|
|
2904
|
+
import { Command as Command28 } from "commander";
|
|
2905
|
+
var driveManifestGetCommand = new Command28("get").description("Get a drive library manifest").argument("<id>", "id").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").action(async (id, opts) => {
|
|
2906
|
+
const client2 = await loadSdkClient();
|
|
2907
|
+
const result = await driveManifestGet({
|
|
2908
|
+
client: client2._rawClient,
|
|
2909
|
+
path: {
|
|
2910
|
+
id
|
|
2911
|
+
},
|
|
2912
|
+
query: {
|
|
2913
|
+
limit: opts.limit,
|
|
2914
|
+
cursor: opts.cursor,
|
|
2915
|
+
include_deleted: opts.includeDeleted
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
if (result.error || !result.response?.ok) {
|
|
2919
|
+
process.stderr.write(
|
|
2920
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
2921
|
+
`
|
|
2922
|
+
);
|
|
2923
|
+
process.exitCode = 1;
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
render({ kind: "drive_manifest_get", display: { "shape": "list", "dataPath": "entries", "columns": ["path", "entry_version", "size_bytes", "updated_at"], "emptyMessage": "no drive files" } }, result.data);
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
// src/generated/cli/alias/add.ts
|
|
2930
|
+
import { Command as Command29 } from "commander";
|
|
2931
|
+
var emailAliasCreateCommand = new Command29("add").description("Create a receiving alias").argument("<email>", "email").action(async (email, opts) => {
|
|
2703
2932
|
const client2 = await loadSdkClient();
|
|
2704
2933
|
const result = await emailAliasCreate({
|
|
2705
2934
|
client: client2._rawClient,
|
|
@@ -2719,8 +2948,8 @@ var emailAliasCreateCommand = new Command22("add").description("Create a receivi
|
|
|
2719
2948
|
});
|
|
2720
2949
|
|
|
2721
2950
|
// src/generated/cli/alias/ls.ts
|
|
2722
|
-
import { Command as
|
|
2723
|
-
var emailAliasListCommand = new
|
|
2951
|
+
import { Command as Command30 } from "commander";
|
|
2952
|
+
var emailAliasListCommand = new Command30("ls").description("List the caller's aliases").option("--include-deleted <value>", "When `true`, include soft-deleted aliases (with `deleted_at` set) alongside active ones. Defaults to `false`.").action(async (opts) => {
|
|
2724
2953
|
const client2 = await loadSdkClient();
|
|
2725
2954
|
const result = await emailAliasList({
|
|
2726
2955
|
client: client2._rawClient,
|
|
@@ -2740,8 +2969,8 @@ var emailAliasListCommand = new Command23("ls").description("List the caller's a
|
|
|
2740
2969
|
});
|
|
2741
2970
|
|
|
2742
2971
|
// src/generated/cli/domain/add.ts
|
|
2743
|
-
import { Command as
|
|
2744
|
-
var emailDomainCreateCommand = new
|
|
2972
|
+
import { Command as Command31 } from "commander";
|
|
2973
|
+
var emailDomainCreateCommand = new Command31("add").description("Register a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
|
|
2745
2974
|
const client2 = await loadSdkClient();
|
|
2746
2975
|
const result = await emailDomainCreate({
|
|
2747
2976
|
client: client2._rawClient,
|
|
@@ -2761,8 +2990,8 @@ var emailDomainCreateCommand = new Command24("add").description("Register a cust
|
|
|
2761
2990
|
});
|
|
2762
2991
|
|
|
2763
2992
|
// src/generated/cli/domain/ls.ts
|
|
2764
|
-
import { Command as
|
|
2765
|
-
var emailDomainListCommand = new
|
|
2993
|
+
import { Command as Command32 } from "commander";
|
|
2994
|
+
var emailDomainListCommand = new Command32("ls").description("List cached custom domains").action(async (opts) => {
|
|
2766
2995
|
const client2 = await loadSdkClient();
|
|
2767
2996
|
const result = await emailDomainList({
|
|
2768
2997
|
client: client2._rawClient
|
|
@@ -2779,8 +3008,8 @@ var emailDomainListCommand = new Command25("ls").description("List cached custom
|
|
|
2779
3008
|
});
|
|
2780
3009
|
|
|
2781
3010
|
// src/generated/cli/alias/rm.ts
|
|
2782
|
-
import { Command as
|
|
2783
|
-
var emailAliasDeleteCommand = new
|
|
3011
|
+
import { Command as Command33 } from "commander";
|
|
3012
|
+
var emailAliasDeleteCommand = new Command33("rm").description("Soft-delete an alias").argument("<email>", "email").action(async (email, opts) => {
|
|
2784
3013
|
const client2 = await loadSdkClient();
|
|
2785
3014
|
const result = await emailAliasDelete({
|
|
2786
3015
|
client: client2._rawClient,
|
|
@@ -2800,8 +3029,8 @@ var emailAliasDeleteCommand = new Command26("rm").description("Soft-delete an al
|
|
|
2800
3029
|
});
|
|
2801
3030
|
|
|
2802
3031
|
// src/generated/cli/email/rm.ts
|
|
2803
|
-
import { Command as
|
|
2804
|
-
var emailDeleteCommand = new
|
|
3032
|
+
import { Command as Command34 } from "commander";
|
|
3033
|
+
var emailDeleteCommand = new Command34("rm").description("Soft-delete inbound emails").argument("<id...>", "id").action(async (id, opts) => {
|
|
2805
3034
|
const idRaw = id;
|
|
2806
3035
|
const ids = idRaw.length > 0 ? idRaw : void 0;
|
|
2807
3036
|
const client2 = await loadSdkClient();
|
|
@@ -2823,8 +3052,8 @@ var emailDeleteCommand = new Command27("rm").description("Soft-delete inbound em
|
|
|
2823
3052
|
});
|
|
2824
3053
|
|
|
2825
3054
|
// src/generated/cli/domain/rm.ts
|
|
2826
|
-
import { Command as
|
|
2827
|
-
var emailDomainDeleteCommand = new
|
|
3055
|
+
import { Command as Command35 } from "commander";
|
|
3056
|
+
var emailDomainDeleteCommand = new Command35("rm").description("Delete a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
|
|
2828
3057
|
const client2 = await loadSdkClient();
|
|
2829
3058
|
const result = await emailDomainDelete({
|
|
2830
3059
|
client: client2._rawClient,
|
|
@@ -2844,8 +3073,8 @@ var emailDomainDeleteCommand = new Command28("rm").description("Delete a custom
|
|
|
2844
3073
|
});
|
|
2845
3074
|
|
|
2846
3075
|
// src/generated/cli/domain/show.ts
|
|
2847
|
-
import { Command as
|
|
2848
|
-
var emailDomainGetCommand = new
|
|
3076
|
+
import { Command as Command36 } from "commander";
|
|
3077
|
+
var emailDomainGetCommand = new Command36("show").description("Get one cached custom domain").argument("<domain>", "domain").action(async (domain, opts) => {
|
|
2849
3078
|
const client2 = await loadSdkClient();
|
|
2850
3079
|
const result = await emailDomainGet({
|
|
2851
3080
|
client: client2._rawClient,
|
|
@@ -2865,8 +3094,8 @@ var emailDomainGetCommand = new Command29("show").description("Get one cached cu
|
|
|
2865
3094
|
});
|
|
2866
3095
|
|
|
2867
3096
|
// src/generated/cli/email/show.ts
|
|
2868
|
-
import { Command as
|
|
2869
|
-
var emailGetCommand = new
|
|
3097
|
+
import { Command as Command37 } from "commander";
|
|
3098
|
+
var emailGetCommand = new Command37("show").description("Get an inbound email by id").argument("<id>", "id").option("--include-html <value>", "When `true`, fetch the HTML body from R2 and include it as `html_body` in the response. Costs an extra R2 read; omit if you only need text.").option("--include-deleted <value>", "When `true`, allow fetching a soft-deleted email. Defaults to `false` (returns 404 for soft-deleted rows).").action(async (id, opts) => {
|
|
2870
3099
|
const client2 = await loadSdkClient();
|
|
2871
3100
|
const result = await emailGet({
|
|
2872
3101
|
client: client2._rawClient,
|
|
@@ -2890,8 +3119,8 @@ var emailGetCommand = new Command30("show").description("Get an inbound email by
|
|
|
2890
3119
|
});
|
|
2891
3120
|
|
|
2892
3121
|
// src/generated/cli/email/ls.ts
|
|
2893
|
-
import { Command as
|
|
2894
|
-
var emailListCommand = new
|
|
3122
|
+
import { Command as Command38 } from "commander";
|
|
3123
|
+
var emailListCommand = new Command38("ls").description("List inbound emails").option("--limit <value>", "Max items to return (clamped to 1-100). Defaults to 20 server-side.").option("--alias-email <value>", "If set, only return emails received on this full alias email address.").option("--unread-only <value>", "When `true`, only return emails with `is_read=false`.").option("--since <value>", "Unix epoch milliseconds \u2014 only return emails with `received_at >= since`. Useful for incremental sync.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").option("--include-deleted <value>", "When `true`, also return soft-deleted emails. Defaults to `false`.").action(async (opts) => {
|
|
2895
3124
|
const client2 = await loadSdkClient();
|
|
2896
3125
|
const result = await emailList({
|
|
2897
3126
|
client: client2._rawClient,
|
|
@@ -2916,8 +3145,8 @@ var emailListCommand = new Command31("ls").description("List inbound emails").op
|
|
|
2916
3145
|
});
|
|
2917
3146
|
|
|
2918
3147
|
// src/generated/cli/email/read.ts
|
|
2919
|
-
import { Command as
|
|
2920
|
-
var emailMarkReadCommand = new
|
|
3148
|
+
import { Command as Command39 } from "commander";
|
|
3149
|
+
var emailMarkReadCommand = new Command39("read").description("Mark inbound emails as read").argument("<id...>", "id").action(async (id, opts) => {
|
|
2921
3150
|
const idRaw = id;
|
|
2922
3151
|
const ids = idRaw.length > 0 ? idRaw : void 0;
|
|
2923
3152
|
const client2 = await loadSdkClient();
|
|
@@ -2939,8 +3168,8 @@ var emailMarkReadCommand = new Command32("read").description("Mark inbound email
|
|
|
2939
3168
|
});
|
|
2940
3169
|
|
|
2941
3170
|
// src/generated/cli/email/unread.ts
|
|
2942
|
-
import { Command as
|
|
2943
|
-
var emailMarkUnreadCommand = new
|
|
3171
|
+
import { Command as Command40 } from "commander";
|
|
3172
|
+
var emailMarkUnreadCommand = new Command40("unread").description("Mark inbound emails as unread").argument("<id...>", "id").action(async (id, opts) => {
|
|
2944
3173
|
const idRaw = id;
|
|
2945
3174
|
const ids = idRaw.length > 0 ? idRaw : void 0;
|
|
2946
3175
|
const client2 = await loadSdkClient();
|
|
@@ -2962,8 +3191,8 @@ var emailMarkUnreadCommand = new Command33("unread").description("Mark inbound e
|
|
|
2962
3191
|
});
|
|
2963
3192
|
|
|
2964
3193
|
// src/generated/cli/domain/verify.ts
|
|
2965
|
-
import { Command as
|
|
2966
|
-
var emailDomainVerifyCommand = new
|
|
3194
|
+
import { Command as Command41 } from "commander";
|
|
3195
|
+
var emailDomainVerifyCommand = new Command41("verify").description("Verify a custom domain with the provider").argument("<domain>", "domain").action(async (domain, opts) => {
|
|
2967
3196
|
const client2 = await loadSdkClient();
|
|
2968
3197
|
const result = await emailDomainVerify({
|
|
2969
3198
|
client: client2._rawClient,
|
|
@@ -2983,8 +3212,8 @@ var emailDomainVerifyCommand = new Command34("verify").description("Verify a cus
|
|
|
2983
3212
|
});
|
|
2984
3213
|
|
|
2985
3214
|
// src/generated/cli/push/config/rm.ts
|
|
2986
|
-
import { Command as
|
|
2987
|
-
var pushConfigDeleteCommand = new
|
|
3215
|
+
import { Command as Command42 } from "commander";
|
|
3216
|
+
var pushConfigDeleteCommand = new Command42("rm").description("Remove a push transport").argument("<transport>", "transport").action(async (transport, opts) => {
|
|
2988
3217
|
const client2 = await loadSdkClient();
|
|
2989
3218
|
const result = await pushConfigDelete({
|
|
2990
3219
|
client: client2._rawClient,
|
|
@@ -3004,8 +3233,8 @@ var pushConfigDeleteCommand = new Command35("rm").description("Remove a push tra
|
|
|
3004
3233
|
});
|
|
3005
3234
|
|
|
3006
3235
|
// src/generated/cli/push/config/set.ts
|
|
3007
|
-
import { Command as
|
|
3008
|
-
var pushConfigSetCommand = new
|
|
3236
|
+
import { Command as Command43 } from "commander";
|
|
3237
|
+
var pushConfigSetCommand = new Command43("set").description("Register or update a push transport").option("--transport <value>", "Transport discriminator. `telegram` is the only supported value today \u2014 push delivers via a Telegram bot DM. Future transports (web push, iOS/Android, generic webhook) will be added as additional discriminator values.").option("--target-bot-username <value>", "Telegram bot username (with leading `@`, 5\u201332 alphanumeric/underscore characters). This is the bot the user has already started a chat with \u2014 wspc DMs notifications to it via the Telegram Bot API.").action(async (opts) => {
|
|
3009
3238
|
const client2 = await loadSdkClient();
|
|
3010
3239
|
const result = await pushConfigSet({
|
|
3011
3240
|
client: client2._rawClient,
|
|
@@ -3028,8 +3257,8 @@ var pushConfigSetCommand = new Command36("set").description("Register or update
|
|
|
3028
3257
|
});
|
|
3029
3258
|
|
|
3030
3259
|
// src/generated/cli/push/config/show.ts
|
|
3031
|
-
import { Command as
|
|
3032
|
-
var pushConfigGetCommand = new
|
|
3260
|
+
import { Command as Command44 } from "commander";
|
|
3261
|
+
var pushConfigGetCommand = new Command44("show").description("List the caller's push transports").action(async (opts) => {
|
|
3033
3262
|
const client2 = await loadSdkClient();
|
|
3034
3263
|
const result = await pushConfigGet({
|
|
3035
3264
|
client: client2._rawClient
|
|
@@ -3046,8 +3275,8 @@ var pushConfigGetCommand = new Command37("show").description("List the caller's
|
|
|
3046
3275
|
});
|
|
3047
3276
|
|
|
3048
3277
|
// src/generated/cli/push/test.ts
|
|
3049
|
-
import { Command as
|
|
3050
|
-
var pushTestCommand = new
|
|
3278
|
+
import { Command as Command45 } from "commander";
|
|
3279
|
+
var pushTestCommand = new Command45("test").description("Send a test push notification").option("--transport <value>", "Which transport to send the test message through. Must match a transport the caller has already registered via `POST /push/config`; today only `telegram` is supported.").action(async (opts) => {
|
|
3051
3280
|
const client2 = await loadSdkClient();
|
|
3052
3281
|
const result = await pushTest({
|
|
3053
3282
|
client: client2._rawClient,
|
|
@@ -3070,8 +3299,8 @@ var pushTestCommand = new Command38("test").description("Send a test push notifi
|
|
|
3070
3299
|
});
|
|
3071
3300
|
|
|
3072
3301
|
// src/generated/cli/todo/comment/add.ts
|
|
3073
|
-
import { Command as
|
|
3074
|
-
var todoCommentCreateCommand = new
|
|
3302
|
+
import { Command as Command46 } from "commander";
|
|
3303
|
+
var todoCommentCreateCommand = new Command46("add").description("Add a comment to a todo").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
|
|
3075
3304
|
const client2 = await loadSdkClient();
|
|
3076
3305
|
const result = await todoCommentCreate({
|
|
3077
3306
|
client: client2._rawClient,
|
|
@@ -3094,8 +3323,8 @@ var todoCommentCreateCommand = new Command39("add").description("Add a comment t
|
|
|
3094
3323
|
});
|
|
3095
3324
|
|
|
3096
3325
|
// src/generated/cli/todo/comment/ls.ts
|
|
3097
|
-
import { Command as
|
|
3098
|
-
var todoCommentListCommand = new
|
|
3326
|
+
import { Command as Command47 } from "commander";
|
|
3327
|
+
var todoCommentListCommand = new Command47("ls").description("List comments on a todo").argument("<id>", "id").option("--order <value>", "order").option("--include-deleted <value>", "include_deleted").option("--limit <value>", "Max comments to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (id, opts) => {
|
|
3099
3328
|
const client2 = await loadSdkClient();
|
|
3100
3329
|
const result = await todoCommentList({
|
|
3101
3330
|
client: client2._rawClient,
|
|
@@ -3121,8 +3350,8 @@ var todoCommentListCommand = new Command40("ls").description("List comments on a
|
|
|
3121
3350
|
});
|
|
3122
3351
|
|
|
3123
3352
|
// src/generated/cli/todo/project/add.ts
|
|
3124
|
-
import { Command as
|
|
3125
|
-
var projectCreateCommand = new
|
|
3353
|
+
import { Command as Command48 } from "commander";
|
|
3354
|
+
var projectCreateCommand = new Command48("add").description("Create a project").argument("<name>", "name").option("--default-todo-type-id <value>", "default_todo_type_id").action(async (name, opts) => {
|
|
3126
3355
|
const client2 = await loadSdkClient();
|
|
3127
3356
|
const result = await projectCreate({
|
|
3128
3357
|
client: client2._rawClient,
|
|
@@ -3143,8 +3372,8 @@ var projectCreateCommand = new Command41("add").description("Create a project").
|
|
|
3143
3372
|
});
|
|
3144
3373
|
|
|
3145
3374
|
// src/generated/cli/todo/project/ls.ts
|
|
3146
|
-
import { Command as
|
|
3147
|
-
var projectListCommand = new
|
|
3375
|
+
import { Command as Command49 } from "commander";
|
|
3376
|
+
var projectListCommand = new Command49("ls").description("List projects").option("--include-deleted <value>", "Set to `true` to include soft-deleted projects in the response.").action(async (opts) => {
|
|
3148
3377
|
const client2 = await loadSdkClient();
|
|
3149
3378
|
const result = await projectList({
|
|
3150
3379
|
client: client2._rawClient,
|
|
@@ -3164,8 +3393,8 @@ var projectListCommand = new Command42("ls").description("List projects").option
|
|
|
3164
3393
|
});
|
|
3165
3394
|
|
|
3166
3395
|
// src/generated/cli/todo/rule/add.ts
|
|
3167
|
-
import { Command as
|
|
3168
|
-
var recurrenceRuleCreateCommand = new
|
|
3396
|
+
import { Command as Command50 } from "commander";
|
|
3397
|
+
var recurrenceRuleCreateCommand = new Command50("add").description("Create a recurring todo rule").argument("<title>", "title").option("--rrule <value>", "rrule").option("--dtstart <value>", "dtstart").option("--description <value>", "description").option("--parent-id <value>", "parent_id").option("-p, --project <value>", "Project for the recurrence rule, its template todo, and all materialized instances. Must be an active project in the caller's organization.").option("-t, --type <value>", "type_id").action(async (title, opts) => {
|
|
3169
3398
|
const client2 = await loadSdkClient();
|
|
3170
3399
|
const result = await recurrenceRuleCreate({
|
|
3171
3400
|
client: client2._rawClient,
|
|
@@ -3191,8 +3420,8 @@ var recurrenceRuleCreateCommand = new Command43("add").description("Create a rec
|
|
|
3191
3420
|
});
|
|
3192
3421
|
|
|
3193
3422
|
// src/generated/cli/todo/rule/ls.ts
|
|
3194
|
-
import { Command as
|
|
3195
|
-
var recurrenceRuleListCommand = new
|
|
3423
|
+
import { Command as Command51 } from "commander";
|
|
3424
|
+
var recurrenceRuleListCommand = new Command51("ls").description("List recurring todo rules").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").action(async (opts) => {
|
|
3196
3425
|
const client2 = await loadSdkClient();
|
|
3197
3426
|
const result = await recurrenceRuleList({
|
|
3198
3427
|
client: client2._rawClient,
|
|
@@ -3213,7 +3442,7 @@ var recurrenceRuleListCommand = new Command44("ls").description("List recurring
|
|
|
3213
3442
|
});
|
|
3214
3443
|
|
|
3215
3444
|
// src/generated/cli/todo/add.ts
|
|
3216
|
-
import { Command as
|
|
3445
|
+
import { Command as Command52 } from "commander";
|
|
3217
3446
|
|
|
3218
3447
|
// src/handwritten/utils/parse-json-field.ts
|
|
3219
3448
|
function parseJsonField(raw, flag) {
|
|
@@ -3228,7 +3457,7 @@ function parseJsonField(raw, flag) {
|
|
|
3228
3457
|
}
|
|
3229
3458
|
|
|
3230
3459
|
// src/generated/cli/todo/add.ts
|
|
3231
|
-
var todoCreateCommand = new
|
|
3460
|
+
var todoCreateCommand = new Command52("add").description("Create a todo").argument("<title>", "title").option("-p, --project <value>", "Project id to assign this todo to. It must be an active project in the caller's organization.").option("--description <value>", "Free-form details about the todo. Fully supports GFM Markdown (tables, strikethrough, task lists). Stored verbatim; client applications are responsible for rendering. Optional. Passing `null` is strictly rejected.").option("--parent-id <value>", "Parent todo ID (`tod_<ULID>`) to attach this todo as a child under another todo. Omit or pass `null` to create a root-level todo. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`. To make a subtask appear on every occurrence of a recurring rule, set this to that rule's template todo id (the template id returned when the rule is created); the server re-materializes future occurrences so each carries the subtask.").option("--status <value>", "Initial status of the todo. Omit to default to `open`. Allowed values: `open`, `in_progress`, `done`, `cancelled`.").option("--due-at <value>", 'Optional calendar due date in ISO date-only format (`YYYY-MM-DD`). Stored without timezone offsets to represent the same local calendar day globally. Pass `""` or omit the field to skip setting a due date. Passing `null` is strictly rejected.').option("--idempotency-key <value>", "idempotency_key").option("--type-id <value>", "Type id this todo belongs to. Omit to use the project's default type. When project_id is also supplied, the type must belong to the same project. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "Custom field values keyed by the field's immutable `key` (not the human `label`). Each value must match the declared field type: string fields require string values, and string_array fields require string arrays. Providing a key that is not declared on the resolved todo type is strictly rejected with `UNDECLARED_FIELD`. Missing required fields that lack a default value are rejected with `FIELD_REQUIRED`. Defaults declared on the type are auto-applied at create time.").action(async (title, opts) => {
|
|
3232
3461
|
const client2 = await loadSdkClient();
|
|
3233
3462
|
const result = await todoCreate({
|
|
3234
3463
|
client: client2._rawClient,
|
|
@@ -3256,8 +3485,8 @@ var todoCreateCommand = new Command45("add").description("Create a todo").argume
|
|
|
3256
3485
|
});
|
|
3257
3486
|
|
|
3258
3487
|
// src/generated/cli/todo/ls.ts
|
|
3259
|
-
import { Command as
|
|
3260
|
-
var todoListCommand = new
|
|
3488
|
+
import { Command as Command53 } from "commander";
|
|
3489
|
+
var todoListCommand = new Command53("ls").description("List todos with filters").option("-p, --project <value>", "Filter by project. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--parent-id <value>", "parent_id").option("-s, --status <value>", "status").option("--include-deleted <value>", "include_deleted").option("--include-templates <value>", "include_templates").option("--due-after <value>", "due_after").option("--due-before <value>", "due_before").option("--type-id <value>", "type_id").option("--sort-by <value>", "sort_by").option("--order <value>", "order").option("--include-orphan-fields <value>", "include_orphan_fields").option("--limit <value>", "Max todos to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (opts) => {
|
|
3261
3490
|
const client2 = await loadSdkClient();
|
|
3262
3491
|
const result = await todoList({
|
|
3263
3492
|
client: client2._rawClient,
|
|
@@ -3290,8 +3519,8 @@ var todoListCommand = new Command46("ls").description("List todos with filters")
|
|
|
3290
3519
|
});
|
|
3291
3520
|
|
|
3292
3521
|
// src/generated/cli/todo/type/ls.ts
|
|
3293
|
-
import { Command as
|
|
3294
|
-
var todoTypeListCommand = new
|
|
3522
|
+
import { Command as Command54 } from "commander";
|
|
3523
|
+
var todoTypeListCommand = new Command54("ls").description("List todo types").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--include-deleted <value>", "include_deleted").action(async (opts) => {
|
|
3295
3524
|
const client2 = await loadSdkClient();
|
|
3296
3525
|
const result = await todoTypeList({
|
|
3297
3526
|
client: client2._rawClient,
|
|
@@ -3313,8 +3542,8 @@ var todoTypeListCommand = new Command47("ls").description("List todo types").opt
|
|
|
3313
3542
|
});
|
|
3314
3543
|
|
|
3315
3544
|
// src/generated/cli/todo/comment/rm.ts
|
|
3316
|
-
import { Command as
|
|
3317
|
-
var todoCommentDeleteCommand = new
|
|
3545
|
+
import { Command as Command55 } from "commander";
|
|
3546
|
+
var todoCommentDeleteCommand = new Command55("rm").description("Soft-delete a comment").argument("<id>", "id").action(async (id, opts) => {
|
|
3318
3547
|
const client2 = await loadSdkClient();
|
|
3319
3548
|
const result = await todoCommentDelete({
|
|
3320
3549
|
client: client2._rawClient,
|
|
@@ -3334,8 +3563,8 @@ var todoCommentDeleteCommand = new Command48("rm").description("Soft-delete a co
|
|
|
3334
3563
|
});
|
|
3335
3564
|
|
|
3336
3565
|
// src/generated/cli/todo/comment/edit.ts
|
|
3337
|
-
import { Command as
|
|
3338
|
-
var todoCommentUpdateCommand = new
|
|
3566
|
+
import { Command as Command56 } from "commander";
|
|
3567
|
+
var todoCommentUpdateCommand = new Command56("edit").description("Edit a comment").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
|
|
3339
3568
|
const client2 = await loadSdkClient();
|
|
3340
3569
|
const result = await todoCommentUpdate({
|
|
3341
3570
|
client: client2._rawClient,
|
|
@@ -3358,8 +3587,8 @@ var todoCommentUpdateCommand = new Command49("edit").description("Edit a comment
|
|
|
3358
3587
|
});
|
|
3359
3588
|
|
|
3360
3589
|
// src/generated/cli/todo/rule/rm.ts
|
|
3361
|
-
import { Command as
|
|
3362
|
-
var recurrenceRuleDeleteCommand = new
|
|
3590
|
+
import { Command as Command57 } from "commander";
|
|
3591
|
+
var recurrenceRuleDeleteCommand = new Command57("rm").description("Delete a recurring todo rule").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
|
|
3363
3592
|
const client2 = await loadSdkClient();
|
|
3364
3593
|
const result = await recurrenceRuleDelete({
|
|
3365
3594
|
client: client2._rawClient,
|
|
@@ -3382,8 +3611,8 @@ var recurrenceRuleDeleteCommand = new Command50("rm").description("Delete a recu
|
|
|
3382
3611
|
});
|
|
3383
3612
|
|
|
3384
3613
|
// src/generated/cli/todo/rule/show.ts
|
|
3385
|
-
import { Command as
|
|
3386
|
-
var recurrenceRuleGetCommand = new
|
|
3614
|
+
import { Command as Command58 } from "commander";
|
|
3615
|
+
var recurrenceRuleGetCommand = new Command58("show").description("Get a recurring todo rule").argument("<id>", "id").action(async (id, opts) => {
|
|
3387
3616
|
const client2 = await loadSdkClient();
|
|
3388
3617
|
const result = await recurrenceRuleGet({
|
|
3389
3618
|
client: client2._rawClient,
|
|
@@ -3403,8 +3632,8 @@ var recurrenceRuleGetCommand = new Command51("show").description("Get a recurrin
|
|
|
3403
3632
|
});
|
|
3404
3633
|
|
|
3405
3634
|
// src/generated/cli/todo/rm.ts
|
|
3406
|
-
import { Command as
|
|
3407
|
-
var todoDeleteCommand = new
|
|
3635
|
+
import { Command as Command59 } from "commander";
|
|
3636
|
+
var todoDeleteCommand = new Command59("rm").description("Soft-delete a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--cascade <value>", "cascade").action(async (id, opts) => {
|
|
3408
3637
|
const client2 = await loadSdkClient();
|
|
3409
3638
|
const result = await todoDelete({
|
|
3410
3639
|
client: client2._rawClient,
|
|
@@ -3428,8 +3657,8 @@ var todoDeleteCommand = new Command52("rm").description("Soft-delete a todo").ar
|
|
|
3428
3657
|
});
|
|
3429
3658
|
|
|
3430
3659
|
// src/generated/cli/todo/show.ts
|
|
3431
|
-
import { Command as
|
|
3432
|
-
var todoGetCommand = new
|
|
3660
|
+
import { Command as Command60 } from "commander";
|
|
3661
|
+
var todoGetCommand = new Command60("show").description("Get a todo by id").argument("<id>", "id").option("--include-deleted <value>", "include_deleted").option("--include-orphan-fields <value>", "include_orphan_fields").action(async (id, opts) => {
|
|
3433
3662
|
const client2 = await loadSdkClient();
|
|
3434
3663
|
const result = await todoGet({
|
|
3435
3664
|
client: client2._rawClient,
|
|
@@ -3454,8 +3683,8 @@ var todoGetCommand = new Command53("show").description("Get a todo by id").argum
|
|
|
3454
3683
|
});
|
|
3455
3684
|
|
|
3456
3685
|
// src/generated/cli/todo/update.ts
|
|
3457
|
-
import { Command as
|
|
3458
|
-
var todoUpdateCommand = new
|
|
3686
|
+
import { Command as Command61 } from "commander";
|
|
3687
|
+
var todoUpdateCommand = new Command61("update").description("Update a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--title <value>", "New title. Omit to leave the existing title unchanged. Must be non-empty when supplied.").option("--description <value>", 'New description. Markdown formatted (CommonMark + GFM tables, strikethrough, task lists). Pass empty string `""` explicitly to clear an existing description, or omit to leave unchanged. Passing `null` is strictly rejected.').option("--parent-id <value>", "Re-parent the todo. Pass a valid parent ID to attach under another todo, pass `null` to move it back to the root level, or omit to leave unchanged. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`.").option("--status <value>", "New status of the todo. Allowed transitions: `open` \u2794 `in_progress` \u2794 `done`. `cancelled` represents a terminal state. Transitioning to `done` automatically emits a `captureTodoCompleted` analytics event. Omit to leave the existing status unchanged.").option("--due-at <value>", 'Update calendar due date in ISO date-only format (`YYYY-MM-DD`). Pass `""` explicitly to clear an existing due date, or omit to leave it unchanged. Passing `null` is strictly rejected.').option("--type-id <value>", "Re-assign this todo to a different active type. The new type must belong to the todo's same project; otherwise the request fails with TYPE_PROJECT_MISMATCH. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "PATCH semantics: only the keys present in this map change. Pass `null` for a key (e.g. `custom_fields: { priority: null }`) to explicitly delete that custom field value. Array values are replaced wholesale with no element-level diff. Providing a key that is not declared on the effective todo type is rejected with `UNDECLARED_FIELD`.").option("--user-id <value>", "Reassign the owner (assignee) user ID of this todo. Target user must belong to the same organization.").action(async (id, opts) => {
|
|
3459
3688
|
const client2 = await loadSdkClient();
|
|
3460
3689
|
const result = await todoUpdate({
|
|
3461
3690
|
client: client2._rawClient,
|
|
@@ -3514,6 +3743,17 @@ function registerGeneratedCommands(root) {
|
|
|
3514
3743
|
root_event.addCommand(eventGetCommand);
|
|
3515
3744
|
root_event.addCommand(eventUpdateCommand);
|
|
3516
3745
|
root_event.addCommand(eventIcsDownloadCommand);
|
|
3746
|
+
const root_drive = root.command("drive").description("drive commands");
|
|
3747
|
+
const root_drive_library = root_drive.command("library").description("library commands");
|
|
3748
|
+
root_drive_library.addCommand(driveLibraryCreateCommand);
|
|
3749
|
+
root_drive_library.addCommand(driveLibraryListCommand);
|
|
3750
|
+
root_drive_library.addCommand(driveLibraryDeleteCommand);
|
|
3751
|
+
root_drive_library.addCommand(driveLibraryGetCommand);
|
|
3752
|
+
root_drive_library.addCommand(driveLibraryUpdateCommand);
|
|
3753
|
+
const root_drive_file = root_drive.command("file").description("file commands");
|
|
3754
|
+
root_drive_file.addCommand(driveFileDeleteCommand);
|
|
3755
|
+
const root_drive_manifest = root_drive.command("manifest").description("manifest commands");
|
|
3756
|
+
root_drive_manifest.addCommand(driveManifestGetCommand);
|
|
3517
3757
|
const root_alias = root.command("alias").description("alias commands");
|
|
3518
3758
|
root_alias.addCommand(emailAliasCreateCommand);
|
|
3519
3759
|
root_alias.addCommand(emailAliasListCommand);
|
|
@@ -3560,7 +3800,7 @@ function registerGeneratedCommands(root) {
|
|
|
3560
3800
|
}
|
|
3561
3801
|
|
|
3562
3802
|
// src/handwritten/commands/login.ts
|
|
3563
|
-
import { Command as
|
|
3803
|
+
import { Command as Command62 } from "commander";
|
|
3564
3804
|
|
|
3565
3805
|
// src/handwritten/auth/device-flow.ts
|
|
3566
3806
|
var DEFAULT_SLEEP = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3794,7 +4034,7 @@ function resolveLoginTarget(opts, env) {
|
|
|
3794
4034
|
function wantsJson(opts, env) {
|
|
3795
4035
|
return opts.json === true || env.WSPC_OUTPUT === "json";
|
|
3796
4036
|
}
|
|
3797
|
-
var loginCommand = new
|
|
4037
|
+
var loginCommand = new Command62("login").description("Log in via OAuth device flow (default) or API key").option("--api-key <key>", "Log in with a wspc API key (escape hatch)").option("--api-base <url>", "Target API base URL (default: production)").option("--env <name>", "Config env name to store credentials under").option("--json", "Emit machine-readable events to stdout").action(async (opts) => {
|
|
3798
4038
|
const store = new ConfigStore();
|
|
3799
4039
|
const { baseUrl, envName } = resolveLoginTarget(opts, process.env);
|
|
3800
4040
|
const output = wantsJson(opts, process.env) ? { write: () => {
|
|
@@ -3813,7 +4053,7 @@ var loginCommand = new Command55("login").description("Log in via OAuth device f
|
|
|
3813
4053
|
});
|
|
3814
4054
|
|
|
3815
4055
|
// src/handwritten/commands/logout.ts
|
|
3816
|
-
import { Command as
|
|
4056
|
+
import { Command as Command63 } from "commander";
|
|
3817
4057
|
|
|
3818
4058
|
// src/handwritten/auth/logout.ts
|
|
3819
4059
|
async function runLogout(opts) {
|
|
@@ -3841,7 +4081,7 @@ async function runLogout(opts) {
|
|
|
3841
4081
|
}
|
|
3842
4082
|
|
|
3843
4083
|
// src/handwritten/commands/logout.ts
|
|
3844
|
-
var logoutCommand = new
|
|
4084
|
+
var logoutCommand = new Command63("logout").description("Log out an account (default: the active account in the current env)").argument("[email]", "Email of the account to log out").option("--all", "Log out every account in the current env").action(async (email, opts) => {
|
|
3845
4085
|
const res = await runLogout({ store: new ConfigStore(), email, all: opts.all });
|
|
3846
4086
|
if (res.removed.length === 0) {
|
|
3847
4087
|
process.stdout.write("nothing to log out\n");
|
|
@@ -3854,7 +4094,7 @@ var logoutCommand = new Command56("logout").description("Log out an account (def
|
|
|
3854
4094
|
});
|
|
3855
4095
|
|
|
3856
4096
|
// src/handwritten/commands/whoami.ts
|
|
3857
|
-
import { Command as
|
|
4097
|
+
import { Command as Command64 } from "commander";
|
|
3858
4098
|
var ENV_DISPLAY = {
|
|
3859
4099
|
shape: "object",
|
|
3860
4100
|
fields: ["name", "api_base", "account", "actor", "agent_label"]
|
|
@@ -3886,7 +4126,7 @@ async function backfillActiveEmail(store, envName, email, userId) {
|
|
|
3886
4126
|
await store.write(cfg);
|
|
3887
4127
|
}
|
|
3888
4128
|
}
|
|
3889
|
-
var whoamiCommand = new
|
|
4129
|
+
var whoamiCommand = new Command64("whoami").description("Show the active env, signed-in account, and organization").action(async () => {
|
|
3890
4130
|
const store = new ConfigStore();
|
|
3891
4131
|
const config = await store.read();
|
|
3892
4132
|
let resolved;
|
|
@@ -3939,8 +4179,8 @@ function printLoggedOut() {
|
|
|
3939
4179
|
}
|
|
3940
4180
|
|
|
3941
4181
|
// src/handwritten/commands/config.ts
|
|
3942
|
-
import { Command as
|
|
3943
|
-
var configCommand = new
|
|
4182
|
+
import { Command as Command65 } from "commander";
|
|
4183
|
+
var configCommand = new Command65("config").description("Manage wspc local config");
|
|
3944
4184
|
registerRenderer("config_show", (data) => {
|
|
3945
4185
|
const d = data;
|
|
3946
4186
|
if (d.envs.length === 0) {
|
|
@@ -4011,7 +4251,7 @@ configCommand.command("use <env>").description("Switch current_env").action(asyn
|
|
|
4011
4251
|
});
|
|
4012
4252
|
|
|
4013
4253
|
// src/handwritten/commands/account.ts
|
|
4014
|
-
import { Command as
|
|
4254
|
+
import { Command as Command66 } from "commander";
|
|
4015
4255
|
async function listAccounts(store) {
|
|
4016
4256
|
const c = await store.read();
|
|
4017
4257
|
const envName = c.current_env;
|
|
@@ -4052,7 +4292,7 @@ registerRenderer("account_ls", (data) => {
|
|
|
4052
4292
|
]);
|
|
4053
4293
|
process.stdout.write(table(headers, body));
|
|
4054
4294
|
});
|
|
4055
|
-
var accountCommand = new
|
|
4295
|
+
var accountCommand = new Command66("account").description("Manage logged-in accounts");
|
|
4056
4296
|
accountCommand.command("ls").description("List accounts in the current env (active marked with \u2713)").action(async () => {
|
|
4057
4297
|
const accounts = await listAccounts(new ConfigStore());
|
|
4058
4298
|
render({ kind: "account_ls" }, { accounts });
|
|
@@ -4064,7 +4304,7 @@ accountCommand.command("switch <email>").description("Set the active account for
|
|
|
4064
4304
|
});
|
|
4065
4305
|
|
|
4066
4306
|
// src/handwritten/commands/todo-done.ts
|
|
4067
|
-
import { Command as
|
|
4307
|
+
import { Command as Command67 } from "commander";
|
|
4068
4308
|
var TODO_UPDATE_DISPLAY = {
|
|
4069
4309
|
shape: "object",
|
|
4070
4310
|
format: {
|
|
@@ -4082,7 +4322,7 @@ var TODO_UPDATE_DISPLAY = {
|
|
|
4082
4322
|
deleted_at: "relative-time"
|
|
4083
4323
|
}
|
|
4084
4324
|
};
|
|
4085
|
-
var todoDoneCommand = new
|
|
4325
|
+
var todoDoneCommand = new Command67("done").description("Mark a todo done (sugar for `update <id> --status done`)").argument("<id>", "Todo id").action(async (id) => {
|
|
4086
4326
|
const client2 = await loadSdkClient();
|
|
4087
4327
|
const result = await todoUpdate({
|
|
4088
4328
|
client: client2._rawClient,
|
|
@@ -4101,7 +4341,7 @@ var todoDoneCommand = new Command60("done").description("Mark a todo done (sugar
|
|
|
4101
4341
|
});
|
|
4102
4342
|
|
|
4103
4343
|
// src/handwritten/commands/email/send.ts
|
|
4104
|
-
import { Command as
|
|
4344
|
+
import { Command as Command68 } from "commander";
|
|
4105
4345
|
import { readFile, stat } from "fs/promises";
|
|
4106
4346
|
import { basename } from "path";
|
|
4107
4347
|
|
|
@@ -4159,7 +4399,7 @@ async function resolveAttachment(input) {
|
|
|
4159
4399
|
`--attach ${input}: neither a readable file nor a valid <prefix>_<ulid>:<idx> reference.`
|
|
4160
4400
|
);
|
|
4161
4401
|
}
|
|
4162
|
-
var sendCommand = new
|
|
4402
|
+
var sendCommand = new Command68("send").description("Send an outbound email").requiredOption("--from <alias-email>", "alias email to send from").option("--to <addr...>", "recipient address (repeatable)", []).option("--subject <text>", "subject").option("--text <body>", "plain-text body").option("--text-file <path>", "read text body from file").option("--reply <id>", "inbound email id to reply to").option("--attach <path-or-ref...>", "attachment (file path or eml_xxx:idx)", []).requiredOption("--idempotency-key <key>", "idempotency key").action(async (opts) => {
|
|
4163
4403
|
const isReply = Boolean(opts.reply);
|
|
4164
4404
|
const to = opts.to;
|
|
4165
4405
|
const attachInputs = opts.attach;
|
|
@@ -4246,7 +4486,7 @@ var sendCommand = new Command61("send").description("Send an outbound email").re
|
|
|
4246
4486
|
});
|
|
4247
4487
|
|
|
4248
4488
|
// src/handwritten/commands/email/attachment.ts
|
|
4249
|
-
import { Command as
|
|
4489
|
+
import { Command as Command69 } from "commander";
|
|
4250
4490
|
import { createWriteStream } from "fs";
|
|
4251
4491
|
import { Readable } from "stream";
|
|
4252
4492
|
import { pipeline } from "stream/promises";
|
|
@@ -4263,7 +4503,7 @@ function parseContentDispositionFilename(header) {
|
|
|
4263
4503
|
}
|
|
4264
4504
|
|
|
4265
4505
|
// src/handwritten/commands/email/attachment.ts
|
|
4266
|
-
var attachmentCommand = new
|
|
4506
|
+
var attachmentCommand = new Command69("attachment").description("Download an inbound email attachment by index").argument("<email-id>").argument("<idx>").option("--output <path>", "output file path").option("--include-deleted", "allow downloads from soft-deleted parent emails").action(async (emailId, idxArg, opts) => {
|
|
4267
4507
|
const idx = Number(idxArg);
|
|
4268
4508
|
if (!Number.isInteger(idx) || idx < 0) {
|
|
4269
4509
|
process.stderr.write(`<idx> must be a non-negative integer (got "${idxArg}")
|
|
@@ -4295,7 +4535,7 @@ var attachmentCommand = new Command62("attachment").description("Download an inb
|
|
|
4295
4535
|
});
|
|
4296
4536
|
|
|
4297
4537
|
// src/handwritten/commands/drive/bind.ts
|
|
4298
|
-
import { Command as
|
|
4538
|
+
import { Command as Command70 } from "commander";
|
|
4299
4539
|
import { stat as stat2 } from "fs/promises";
|
|
4300
4540
|
import { resolve } from "path";
|
|
4301
4541
|
|
|
@@ -4367,9 +4607,12 @@ async function createDriveApi(opts = {}) {
|
|
|
4367
4607
|
}
|
|
4368
4608
|
return payload;
|
|
4369
4609
|
},
|
|
4370
|
-
async downloadFile(id, path) {
|
|
4610
|
+
async downloadFile(id, path, versionId) {
|
|
4371
4611
|
const url = driveContentUrl(client2.baseUrl, id);
|
|
4372
4612
|
url.searchParams.set("path", path);
|
|
4613
|
+
if (versionId !== void 0) {
|
|
4614
|
+
url.searchParams.set("version_id", versionId);
|
|
4615
|
+
}
|
|
4373
4616
|
const res = await client2.fetch(url, { method: "GET" });
|
|
4374
4617
|
if (!res.ok) {
|
|
4375
4618
|
const text = await res.text();
|
|
@@ -4384,11 +4627,34 @@ async function createDriveApi(opts = {}) {
|
|
|
4384
4627
|
import { mkdir, open, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
4385
4628
|
import { randomUUID } from "crypto";
|
|
4386
4629
|
import { join as join2 } from "path";
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4630
|
+
import { DateTime as DateTime4 } from "luxon";
|
|
4631
|
+
|
|
4632
|
+
// src/handwritten/commands/drive/clock.ts
|
|
4633
|
+
import { DateTime as DateTime3 } from "luxon";
|
|
4634
|
+
var systemDriveClock = {
|
|
4635
|
+
now: () => DateTime3.utc()
|
|
4636
|
+
};
|
|
4637
|
+
function driveIsoTimestamp(clock = systemDriveClock) {
|
|
4638
|
+
const timestamp = clock.now().toISO();
|
|
4639
|
+
if (timestamp === null) {
|
|
4640
|
+
throw new Error("invalid drive clock timestamp");
|
|
4641
|
+
}
|
|
4642
|
+
return timestamp;
|
|
4643
|
+
}
|
|
4644
|
+
function driveConflictTimestamp(clock = systemDriveClock) {
|
|
4645
|
+
const timestamp = clock.now().toUTC();
|
|
4646
|
+
if (!timestamp.isValid) {
|
|
4647
|
+
throw new Error("invalid drive clock timestamp");
|
|
4648
|
+
}
|
|
4649
|
+
return timestamp.toFormat("yyyyLLdd'T'HHmmss'Z'");
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
// src/handwritten/commands/drive/state.ts
|
|
4653
|
+
var DRIVE_DIR = ".wspc-drive";
|
|
4654
|
+
var STATE_FILE = "state.json";
|
|
4655
|
+
function statePath(root) {
|
|
4656
|
+
return join2(root, DRIVE_DIR, STATE_FILE);
|
|
4657
|
+
}
|
|
4392
4658
|
async function readDriveState(root) {
|
|
4393
4659
|
const buf = await readFile2(statePath(root), "utf8");
|
|
4394
4660
|
const parsed = JSON.parse(buf);
|
|
@@ -4397,13 +4663,13 @@ async function readDriveState(root) {
|
|
|
4397
4663
|
}
|
|
4398
4664
|
return parsed;
|
|
4399
4665
|
}
|
|
4400
|
-
async function writeDriveState(root, state) {
|
|
4666
|
+
async function writeDriveState(root, state, clock) {
|
|
4401
4667
|
await mkdir(join2(root, DRIVE_DIR), { recursive: true });
|
|
4402
4668
|
const tmp = join2(root, DRIVE_DIR, `state.json.tmp-${process.pid}-${randomUUID()}`);
|
|
4403
4669
|
const snapshot = JSON.stringify(
|
|
4404
4670
|
{
|
|
4405
4671
|
...state,
|
|
4406
|
-
updated_at: (
|
|
4672
|
+
updated_at: driveIsoTimestamp(clock)
|
|
4407
4673
|
},
|
|
4408
4674
|
null,
|
|
4409
4675
|
2
|
|
@@ -4422,7 +4688,7 @@ async function writeDriveState(root, state) {
|
|
|
4422
4688
|
await rm(tmp, { force: true });
|
|
4423
4689
|
}
|
|
4424
4690
|
}
|
|
4425
|
-
async function initDriveState(root, libraryId) {
|
|
4691
|
+
async function initDriveState(root, libraryId, clock) {
|
|
4426
4692
|
await mkdir(join2(root, DRIVE_DIR), { recursive: true });
|
|
4427
4693
|
try {
|
|
4428
4694
|
const existing = await readDriveState(root);
|
|
@@ -4433,7 +4699,7 @@ async function initDriveState(root, libraryId) {
|
|
|
4433
4699
|
} catch (err) {
|
|
4434
4700
|
if (err.code !== "ENOENT") throw err;
|
|
4435
4701
|
}
|
|
4436
|
-
const now = (
|
|
4702
|
+
const now = driveIsoTimestamp(clock);
|
|
4437
4703
|
const state = {
|
|
4438
4704
|
schema_version: 1,
|
|
4439
4705
|
library_id: libraryId,
|
|
@@ -4442,9 +4708,32 @@ async function initDriveState(root, libraryId) {
|
|
|
4442
4708
|
entries: {},
|
|
4443
4709
|
conflicts: {}
|
|
4444
4710
|
};
|
|
4445
|
-
await writeDriveState(root, state);
|
|
4711
|
+
await writeDriveState(root, state, clock);
|
|
4446
4712
|
return state;
|
|
4447
4713
|
}
|
|
4714
|
+
async function ensureDriveRealtimeState(root) {
|
|
4715
|
+
return withDriveLock(root, async () => {
|
|
4716
|
+
const state = await readDriveState(root);
|
|
4717
|
+
if (state.realtime?.client_id !== void 0) {
|
|
4718
|
+
return state;
|
|
4719
|
+
}
|
|
4720
|
+
const next = {
|
|
4721
|
+
...state,
|
|
4722
|
+
realtime: {
|
|
4723
|
+
...state.realtime,
|
|
4724
|
+
client_id: `drvcli_${randomUUID().replace(/-/g, "")}`
|
|
4725
|
+
}
|
|
4726
|
+
};
|
|
4727
|
+
await writeDriveState(root, next);
|
|
4728
|
+
return next;
|
|
4729
|
+
});
|
|
4730
|
+
}
|
|
4731
|
+
async function writeDriveRealtimeState(root, realtime) {
|
|
4732
|
+
await withDriveLock(root, async () => {
|
|
4733
|
+
const current = await readDriveState(root);
|
|
4734
|
+
await writeDriveState(root, { ...current, realtime });
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4448
4737
|
async function withDriveLock(root, fn) {
|
|
4449
4738
|
await mkdir(join2(root, DRIVE_DIR), { recursive: true });
|
|
4450
4739
|
const lockFile = join2(root, DRIVE_DIR, "sync.lock");
|
|
@@ -4463,18 +4752,28 @@ async function withDriveLock(root, fn) {
|
|
|
4463
4752
|
});
|
|
4464
4753
|
}
|
|
4465
4754
|
}
|
|
4466
|
-
function
|
|
4755
|
+
function isRecord2(value) {
|
|
4467
4756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4468
4757
|
}
|
|
4469
4758
|
function isDriveStateEntry(value) {
|
|
4470
|
-
return
|
|
4759
|
+
return isRecord2(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
4760
|
}
|
|
4472
4761
|
function isDriveConflict(value) {
|
|
4473
|
-
return
|
|
4762
|
+
return isRecord2(value) && typeof value.detected_at === "string" && typeof value.reason === "string" && (value.type === void 0 || value.type === "edit_edit" || value.type === "create_create" || value.type === "delete_edit" || value.type === "edit_delete") && (value.strategy === void 0 || value.strategy === "clean_merge" || value.strategy === "conflict_copy" || value.strategy === "record_only") && (value.base_version_id === void 0 || typeof value.base_version_id === "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") && (value.conflict_paths === void 0 || Array.isArray(value.conflict_paths) && value.conflict_paths.every((path) => typeof path === "string"));
|
|
4763
|
+
}
|
|
4764
|
+
function isDriveRealtimeState(value) {
|
|
4765
|
+
const allowedKeys = /* @__PURE__ */ new Set(["client_id", "last_cursor", "last_connected_at", "last_event_at"]);
|
|
4766
|
+
return isRecord2(value) && Object.keys(value).every((key) => allowedKeys.has(key)) && (value.client_id === void 0 || typeof value.client_id === "string" && /^drvcli_[A-Za-z0-9_-]+$/.test(value.client_id)) && (value.last_cursor === void 0 || typeof value.last_cursor === "string") && isOptionalIsoTimestamp(value.last_connected_at) && isOptionalIsoTimestamp(value.last_event_at);
|
|
4767
|
+
}
|
|
4768
|
+
function isOptionalIsoTimestamp(value) {
|
|
4769
|
+
if (value === void 0) return true;
|
|
4770
|
+
if (typeof value !== "string") return false;
|
|
4771
|
+
if (!/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value)) return false;
|
|
4772
|
+
return DateTime4.fromISO(value, { setZone: true }).isValid;
|
|
4474
4773
|
}
|
|
4475
4774
|
function isValidDriveState(value) {
|
|
4476
|
-
if (!
|
|
4477
|
-
if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !
|
|
4775
|
+
if (!isRecord2(value)) return false;
|
|
4776
|
+
if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord2(value.entries) || !isRecord2(value.conflicts) || value.realtime !== void 0 && !isDriveRealtimeState(value.realtime)) {
|
|
4478
4777
|
return false;
|
|
4479
4778
|
}
|
|
4480
4779
|
for (const entry of Object.values(value.entries)) {
|
|
@@ -4499,7 +4798,7 @@ async function assertExistingDirectory(path) {
|
|
|
4499
4798
|
}
|
|
4500
4799
|
}
|
|
4501
4800
|
function driveBindCommand() {
|
|
4502
|
-
return new
|
|
4801
|
+
return new Command70("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
4802
|
const root = resolve(path);
|
|
4504
4803
|
await assertExistingDirectory(root);
|
|
4505
4804
|
const api = await createDriveApi();
|
|
@@ -4517,13 +4816,10 @@ function driveBindCommand() {
|
|
|
4517
4816
|
}
|
|
4518
4817
|
|
|
4519
4818
|
// src/handwritten/commands/drive/sync.ts
|
|
4520
|
-
import { Command as
|
|
4521
|
-
import {
|
|
4522
|
-
import {
|
|
4523
|
-
import {
|
|
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";
|
|
4819
|
+
import { Command as Command71 } from "commander";
|
|
4820
|
+
import { mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4821
|
+
import { basename as basename3, dirname as dirname2, join as join5, posix as pathPosix2, resolve as resolve3 } from "path";
|
|
4822
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
4527
4823
|
|
|
4528
4824
|
// src/handwritten/commands/drive/decision.ts
|
|
4529
4825
|
function decideDriveAction(entry, local, remote) {
|
|
@@ -4645,6 +4941,141 @@ function resolveInsideRoot(root, drivePath) {
|
|
|
4645
4941
|
return absolutePath;
|
|
4646
4942
|
}
|
|
4647
4943
|
|
|
4944
|
+
// src/handwritten/commands/drive/manifest.ts
|
|
4945
|
+
function normalizeRemoteManifest(root, entries) {
|
|
4946
|
+
const candidates = [];
|
|
4947
|
+
const remoteFiles = {};
|
|
4948
|
+
const pathErrors = [];
|
|
4949
|
+
for (const entry of entries) {
|
|
4950
|
+
try {
|
|
4951
|
+
validateDrivePath(entry.path);
|
|
4952
|
+
resolveInsideRoot(root, entry.path);
|
|
4953
|
+
candidates.push(entry);
|
|
4954
|
+
} catch (error) {
|
|
4955
|
+
pathErrors.push({ path: entry.path, error: error instanceof Error ? error : new Error(String(error)) });
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
const byCaseFoldedPath = /* @__PURE__ */ new Map();
|
|
4959
|
+
for (const entry of candidates) {
|
|
4960
|
+
const folded = entry.path.toLowerCase();
|
|
4961
|
+
const group = byCaseFoldedPath.get(folded) ?? [];
|
|
4962
|
+
group.push(entry);
|
|
4963
|
+
byCaseFoldedPath.set(folded, group);
|
|
4964
|
+
}
|
|
4965
|
+
for (const group of byCaseFoldedPath.values()) {
|
|
4966
|
+
if (group.length > 1) {
|
|
4967
|
+
const hasExactDuplicate = new Set(group.map((entry2) => entry2.path)).size < group.length;
|
|
4968
|
+
const reason = hasExactDuplicate ? "REMOTE_PATH_DUPLICATE" : "REMOTE_PATH_CASE_CONFLICT";
|
|
4969
|
+
for (const entry2 of group.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
4970
|
+
pathErrors.push({
|
|
4971
|
+
path: entry2.path,
|
|
4972
|
+
error: new Error(`${reason}: ${entry2.path}`),
|
|
4973
|
+
appendPathResult: true
|
|
4974
|
+
});
|
|
4975
|
+
}
|
|
4976
|
+
continue;
|
|
4977
|
+
}
|
|
4978
|
+
const [entry] = group;
|
|
4979
|
+
if (entry) remoteFiles[entry.path] = entry;
|
|
4980
|
+
}
|
|
4981
|
+
return { remoteFiles, pathErrors };
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
// src/handwritten/commands/drive/merge.ts
|
|
4985
|
+
import { posix as pathPosix } from "path";
|
|
4986
|
+
import { TextDecoder } from "util";
|
|
4987
|
+
import { diff3Merge } from "node-diff3";
|
|
4988
|
+
var MAX_MERGE_TEXT_BYTES = 1024 * 1024;
|
|
4989
|
+
var SNIFF_BYTES = 8192;
|
|
4990
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4991
|
+
".txt",
|
|
4992
|
+
".md",
|
|
4993
|
+
".markdown",
|
|
4994
|
+
".json",
|
|
4995
|
+
".yaml",
|
|
4996
|
+
".yml",
|
|
4997
|
+
".csv",
|
|
4998
|
+
".tsv",
|
|
4999
|
+
".html",
|
|
5000
|
+
".htm",
|
|
5001
|
+
".css",
|
|
5002
|
+
".js",
|
|
5003
|
+
".jsx",
|
|
5004
|
+
".ts",
|
|
5005
|
+
".tsx",
|
|
5006
|
+
".xml",
|
|
5007
|
+
".svg"
|
|
5008
|
+
]);
|
|
5009
|
+
var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
5010
|
+
function classifyMergeText(path, bytes, mimeType) {
|
|
5011
|
+
if (bytes.byteLength > MAX_MERGE_TEXT_BYTES) {
|
|
5012
|
+
return { mergeable: false, reason: "too_large" };
|
|
5013
|
+
}
|
|
5014
|
+
if (hasBinaryControlBytes(bytes)) {
|
|
5015
|
+
return { mergeable: false, reason: "binary" };
|
|
5016
|
+
}
|
|
5017
|
+
let text;
|
|
5018
|
+
try {
|
|
5019
|
+
text = UTF8_DECODER.decode(bytes);
|
|
5020
|
+
} catch {
|
|
5021
|
+
return { mergeable: false, reason: "invalid_utf8" };
|
|
5022
|
+
}
|
|
5023
|
+
if (!hasTextHint(path, mimeType)) {
|
|
5024
|
+
return { mergeable: false, reason: "not_text" };
|
|
5025
|
+
}
|
|
5026
|
+
return { mergeable: true, text };
|
|
5027
|
+
}
|
|
5028
|
+
function mergeText3(base, local, remote) {
|
|
5029
|
+
const localNewline = local.includes("\r\n") ? "\r\n" : "\n";
|
|
5030
|
+
const regions = diff3Merge(normalizeLines(local), normalizeLines(base), normalizeLines(remote));
|
|
5031
|
+
const mergedLines = [];
|
|
5032
|
+
for (const region of regions) {
|
|
5033
|
+
if (region.conflict !== void 0) {
|
|
5034
|
+
return { clean: false };
|
|
5035
|
+
}
|
|
5036
|
+
if (region.ok !== void 0) {
|
|
5037
|
+
mergedLines.push(...region.ok);
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
return { clean: true, text: mergedLines.join(localNewline) };
|
|
5041
|
+
}
|
|
5042
|
+
function conflictCopyPath(path, side, timestamp, versionId) {
|
|
5043
|
+
const parsed = pathPosix.parse(path);
|
|
5044
|
+
const shortVersionId = safeShortVersionId(versionId);
|
|
5045
|
+
const fileName = `${parsed.name}.${side}-conflict-${timestamp}-${shortVersionId}${parsed.ext}`;
|
|
5046
|
+
if (parsed.dir === "") {
|
|
5047
|
+
return fileName;
|
|
5048
|
+
}
|
|
5049
|
+
return pathPosix.join(parsed.dir, fileName);
|
|
5050
|
+
}
|
|
5051
|
+
function hasBinaryControlBytes(bytes) {
|
|
5052
|
+
const sniffLength = Math.min(bytes.byteLength, SNIFF_BYTES);
|
|
5053
|
+
for (let index = 0; index < sniffLength; index += 1) {
|
|
5054
|
+
const byte = bytes[index];
|
|
5055
|
+
if (byte === void 0) {
|
|
5056
|
+
continue;
|
|
5057
|
+
}
|
|
5058
|
+
if (byte === 0) {
|
|
5059
|
+
return true;
|
|
5060
|
+
}
|
|
5061
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
5062
|
+
return true;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
return false;
|
|
5066
|
+
}
|
|
5067
|
+
function hasTextHint(path, mimeType) {
|
|
5068
|
+
const extension = pathPosix.extname(path).toLowerCase();
|
|
5069
|
+
return TEXT_EXTENSIONS.has(extension) || mimeType?.toLowerCase().startsWith("text/") === true;
|
|
5070
|
+
}
|
|
5071
|
+
function normalizeLines(text) {
|
|
5072
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
5073
|
+
}
|
|
5074
|
+
function safeShortVersionId(versionId) {
|
|
5075
|
+
const safeVersionId = versionId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
5076
|
+
return (safeVersionId.length > 0 ? safeVersionId : "unknown").slice(0, 8);
|
|
5077
|
+
}
|
|
5078
|
+
|
|
4648
5079
|
// src/handwritten/commands/drive/scanner.ts
|
|
4649
5080
|
import { createHash } from "crypto";
|
|
4650
5081
|
import { constants as fsConstants } from "fs";
|
|
@@ -4722,7 +5153,7 @@ async function scanDriveFiles(root, options = {}) {
|
|
|
4722
5153
|
}
|
|
4723
5154
|
function isInternalSyncArtifactName(name) {
|
|
4724
5155
|
if (!name.startsWith(".") || !name.endsWith(".tmp")) return false;
|
|
4725
|
-
return name.includes(".wspc-download-") || name.includes(".wspc-backup-");
|
|
5156
|
+
return name.includes(".wspc-download-") || name.includes(".wspc-backup-") || name.includes(".wspc-conflict-") || name.includes(".wspc-merge-");
|
|
4726
5157
|
}
|
|
4727
5158
|
async function hashDriveFile(path) {
|
|
4728
5159
|
const useNoFollow = fsConstants.O_NOFOLLOW !== void 0;
|
|
@@ -4765,236 +5196,128 @@ async function hashDriveFile(path) {
|
|
|
4765
5196
|
}
|
|
4766
5197
|
}
|
|
4767
5198
|
|
|
4768
|
-
// src/handwritten/commands/drive/
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
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;
|
|
5199
|
+
// src/handwritten/commands/drive/local-mutations.ts
|
|
5200
|
+
import { createWriteStream as createWriteStream2 } from "fs";
|
|
5201
|
+
import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
5202
|
+
import { basename as basename2, dirname, join as join4 } from "path";
|
|
5203
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
5204
|
+
import { Readable as Readable2, Transform } from "stream";
|
|
5205
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
5206
|
+
async function assertLocalStillScanned(localPath, scanned) {
|
|
5207
|
+
const snapshot = await hashDriveFile(localPath).catch((error) => {
|
|
5208
|
+
if (isNotFoundError(error)) return void 0;
|
|
5209
|
+
throw error;
|
|
4804
5210
|
});
|
|
5211
|
+
if (!snapshot || snapshot.sha256 !== scanned.sha256 || snapshot.sizeBytes !== scanned.size_bytes) {
|
|
5212
|
+
throw new Error("local file changed after scan");
|
|
5213
|
+
}
|
|
4805
5214
|
}
|
|
4806
|
-
function
|
|
4807
|
-
const
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5215
|
+
async function writeMergedLocalFile(root, path, bytes, digest, scanned, onLocalMutation) {
|
|
5216
|
+
const target = resolveInsideRoot(root, path);
|
|
5217
|
+
await mkdir2(dirname(target), { recursive: true });
|
|
5218
|
+
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-merge-${randomUUID2()}.tmp`);
|
|
5219
|
+
try {
|
|
5220
|
+
await writeFile2(tmp, bytes, { flag: "wx" });
|
|
5221
|
+
return await installMergedLocalFile(root, path, tmp, scanned, digest, bytes.byteLength, onLocalMutation);
|
|
5222
|
+
} finally {
|
|
5223
|
+
await rm2(tmp, { force: true }).catch(() => {
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
4816
5226
|
}
|
|
4817
|
-
async function
|
|
4818
|
-
const
|
|
4819
|
-
const
|
|
4820
|
-
let
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
await recordPathError(summary, blockedPaths, entry.path, error);
|
|
5227
|
+
async function installMergedLocalFile(root, path, tmp, scanned, mergedSha256, mergedSizeBytes, onLocalMutation) {
|
|
5228
|
+
const target = resolveInsideRoot(root, path);
|
|
5229
|
+
const backup = localMutationBackupPath(target);
|
|
5230
|
+
let backupIsScannedLocal = false;
|
|
5231
|
+
try {
|
|
5232
|
+
try {
|
|
5233
|
+
await rename2(target, backup);
|
|
5234
|
+
onLocalMutation();
|
|
5235
|
+
} catch (error) {
|
|
5236
|
+
if (isNotFoundError(error)) {
|
|
5237
|
+
throw new Error("local file changed after scan");
|
|
4829
5238
|
}
|
|
5239
|
+
throw error;
|
|
4830
5240
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
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);
|
|
5241
|
+
const backupDigest = await hashDriveFile(backup);
|
|
5242
|
+
if (!backupDigest || backupDigest.sha256 !== scanned.sha256 || backupDigest.sizeBytes !== scanned.size_bytes) {
|
|
5243
|
+
await restoreBackupWhenPossible(backup, target);
|
|
5244
|
+
throw new Error("local file changed after scan");
|
|
4844
5245
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
5246
|
+
backupIsScannedLocal = true;
|
|
5247
|
+
try {
|
|
5248
|
+
await installNoOverwrite(tmp, target);
|
|
5249
|
+
} catch (error) {
|
|
5250
|
+
await restoreBackupWhenPossible(backup, target);
|
|
5251
|
+
throw error;
|
|
5252
|
+
}
|
|
5253
|
+
return {
|
|
5254
|
+
finalize: async () => {
|
|
5255
|
+
await rm2(backup, { force: true }).catch(() => {
|
|
4851
5256
|
});
|
|
5257
|
+
},
|
|
5258
|
+
restore: async () => {
|
|
5259
|
+
await restoreMergedLocalFile(target, backup, scanned.sha256, scanned.size_bytes, mergedSha256, mergedSizeBytes);
|
|
4852
5260
|
}
|
|
4853
|
-
|
|
5261
|
+
};
|
|
5262
|
+
} catch (error) {
|
|
5263
|
+
if (!backupIsScannedLocal) {
|
|
5264
|
+
await restoreBackupWhenPossible(backup, target);
|
|
4854
5265
|
}
|
|
4855
|
-
|
|
4856
|
-
if (entry) remoteFiles[entry.path] = entry;
|
|
5266
|
+
throw error;
|
|
4857
5267
|
}
|
|
4858
|
-
return remoteFiles;
|
|
4859
5268
|
}
|
|
4860
|
-
function
|
|
4861
|
-
|
|
4862
|
-
|
|
5269
|
+
async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBytes, mergedSha256, mergedSizeBytes) {
|
|
5270
|
+
const quarantine = join4(dirname(target), `.${basename2(target)}.wspc-merge-restore-${randomUUID2()}.tmp`);
|
|
5271
|
+
try {
|
|
5272
|
+
await rename2(target, quarantine);
|
|
5273
|
+
} catch (error) {
|
|
5274
|
+
if (!isNotFoundError(error)) throw error;
|
|
5275
|
+
}
|
|
5276
|
+
const quarantineDigest = await hashDriveFile(quarantine).catch((error) => {
|
|
5277
|
+
if (isNotFoundError(error)) return void 0;
|
|
5278
|
+
throw error;
|
|
5279
|
+
});
|
|
5280
|
+
if (quarantineDigest !== void 0 && (quarantineDigest.sha256 !== mergedSha256 || quarantineDigest.sizeBytes !== mergedSizeBytes)) {
|
|
5281
|
+
await restoreBackupWhenPossible(quarantine, target);
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
const restored = await restoreBackupWhenPossible(backup, target);
|
|
5285
|
+
if (!restored) {
|
|
5286
|
+
await rm2(quarantine, { force: true }).catch(() => {
|
|
5287
|
+
});
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
await unlink(quarantine).catch(() => {
|
|
5291
|
+
});
|
|
5292
|
+
const restoredDigest = await hashDriveFile(target);
|
|
5293
|
+
if (!restoredDigest || restoredDigest.sha256 !== backupSha256 || restoredDigest.sizeBytes !== backupSizeBytes) {
|
|
5294
|
+
throw new Error("local file restore failed");
|
|
5295
|
+
}
|
|
4863
5296
|
}
|
|
4864
|
-
async function
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
|
|
5297
|
+
async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
|
|
5298
|
+
const target = resolveInsideRoot(root, path);
|
|
5299
|
+
await mkdir2(dirname(target), { recursive: true });
|
|
5300
|
+
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID2()}.tmp`);
|
|
4868
5301
|
try {
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
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 };
|
|
5302
|
+
const response = await api.downloadFile(libraryId, path);
|
|
5303
|
+
if (!response.body) {
|
|
5304
|
+
throw new Error("download response body missing");
|
|
4893
5305
|
}
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
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}`);
|
|
5306
|
+
const hash = createHash2("sha256");
|
|
5307
|
+
const hashingStream = new Transform({
|
|
5308
|
+
transform(chunk, _encoding, callback) {
|
|
5309
|
+
hash.update(chunk);
|
|
5310
|
+
callback(void 0, chunk);
|
|
5311
|
+
}
|
|
5312
|
+
});
|
|
5313
|
+
await pipeline2(
|
|
5314
|
+
Readable2.fromWeb(response.body),
|
|
5315
|
+
hashingStream,
|
|
5316
|
+
createWriteStream2(tmp, { flags: "wx" })
|
|
5317
|
+
);
|
|
5318
|
+
const digest = hash.digest("hex");
|
|
5319
|
+
if (expectedSha256 !== void 0 && digest !== expectedSha256) {
|
|
5320
|
+
throw new Error(`download hash mismatch: expected ${expectedSha256}, got ${digest}`);
|
|
4998
5321
|
}
|
|
4999
5322
|
await installDownloadedFile(root, path, tmp, entry, onLocalMutation);
|
|
5000
5323
|
return digest;
|
|
@@ -5162,7 +5485,431 @@ async function assertLocalAbsentBeforeRemoteDelete(root, path) {
|
|
|
5162
5485
|
throw new Error("local file appeared before remote delete");
|
|
5163
5486
|
}
|
|
5164
5487
|
}
|
|
5165
|
-
function
|
|
5488
|
+
function isNotFoundError(error) {
|
|
5489
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
5490
|
+
}
|
|
5491
|
+
function isAlreadyExistsError(error) {
|
|
5492
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
// src/handwritten/commands/drive/sync.ts
|
|
5496
|
+
function emptySummary() {
|
|
5497
|
+
return {
|
|
5498
|
+
uploaded: 0,
|
|
5499
|
+
downloaded: 0,
|
|
5500
|
+
deleted: 0,
|
|
5501
|
+
unchanged: 0,
|
|
5502
|
+
merged: 0,
|
|
5503
|
+
conflicts: 0,
|
|
5504
|
+
errors: 0,
|
|
5505
|
+
conflict_paths: [],
|
|
5506
|
+
paths: []
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
async function runDriveSyncOnce(root, api, clock = systemDriveClock) {
|
|
5510
|
+
return withDriveLock(root, async () => {
|
|
5511
|
+
let state = await readDriveState(root);
|
|
5512
|
+
const syncApi = api ?? await createDriveApi();
|
|
5513
|
+
const summary = emptySummary();
|
|
5514
|
+
const blockedPaths = /* @__PURE__ */ new Set();
|
|
5515
|
+
const localFiles = await scanDriveFiles(root, {
|
|
5516
|
+
onPathError: async (path, error) => {
|
|
5517
|
+
await recordPathError(summary, blockedPaths, path, error);
|
|
5518
|
+
}
|
|
5519
|
+
});
|
|
5520
|
+
const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
|
|
5521
|
+
const paths = Array.from(
|
|
5522
|
+
/* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
|
|
5523
|
+
).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
|
|
5524
|
+
for (const path of paths) {
|
|
5525
|
+
const remote = remoteFiles[path];
|
|
5526
|
+
const action = decideDriveAction(state.entries[path], localFiles[path], remote);
|
|
5527
|
+
const result = await processPath({ root, state, api: syncApi, path, action, remote, local: localFiles[path], summary, clock });
|
|
5528
|
+
state = result.state;
|
|
5529
|
+
if (result.stop) break;
|
|
5530
|
+
}
|
|
5531
|
+
recordUnresolvedConflicts(summary, state);
|
|
5532
|
+
return summary;
|
|
5533
|
+
});
|
|
5534
|
+
}
|
|
5535
|
+
function driveSyncCommand(api) {
|
|
5536
|
+
const sync = new Command71("sync").description("Drive sync commands");
|
|
5537
|
+
sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
5538
|
+
const summary = await runDriveSyncOnce(resolve3(path), api);
|
|
5539
|
+
render({ kind: "drive_sync_once", display: { shape: "object" } }, summary);
|
|
5540
|
+
if (summary.conflicts > 0 || summary.errors > 0) {
|
|
5541
|
+
process.exitCode = 1;
|
|
5542
|
+
}
|
|
5543
|
+
});
|
|
5544
|
+
return sync;
|
|
5545
|
+
}
|
|
5546
|
+
async function fetchRemoteManifest(root, state, api, summary, blockedPaths) {
|
|
5547
|
+
const entries = [];
|
|
5548
|
+
let cursor;
|
|
5549
|
+
do {
|
|
5550
|
+
const page = await api.getManifest(state.library_id, cursor);
|
|
5551
|
+
entries.push(...page.entries);
|
|
5552
|
+
cursor = page.next_cursor ?? void 0;
|
|
5553
|
+
} while (cursor !== void 0);
|
|
5554
|
+
const normalized = normalizeRemoteManifest(root, entries);
|
|
5555
|
+
for (const pathError of normalized.pathErrors) {
|
|
5556
|
+
await recordPathError(summary, blockedPaths, pathError.path, pathError.error, {
|
|
5557
|
+
appendPathResult: pathError.appendPathResult
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
return normalized.remoteFiles;
|
|
5561
|
+
}
|
|
5562
|
+
async function processPath(args) {
|
|
5563
|
+
const { root, state, api, path, action, remote, local, summary, clock } = args;
|
|
5564
|
+
summary.paths.push({ path, action: action.type });
|
|
5565
|
+
let durableStateRequired = false;
|
|
5566
|
+
try {
|
|
5567
|
+
if (action.type === "upload_create" || action.type === "upload_update") {
|
|
5568
|
+
const localPath = resolveInsideRoot(root, path);
|
|
5569
|
+
const { body, digest: uploadDigest } = await readStableUploadBody(localPath, local);
|
|
5570
|
+
const uploaded = await api.uploadFile(state.library_id, path, body, uploadDigest, action.expectedEntryVersion);
|
|
5571
|
+
durableStateRequired = true;
|
|
5572
|
+
const nextState = cloneDriveState(state);
|
|
5573
|
+
nextState.entries[path] = stateEntryFromRemote(uploaded.entry, uploadDigest, clock);
|
|
5574
|
+
delete nextState.conflicts[path];
|
|
5575
|
+
await writeDriveState(root, nextState, clock);
|
|
5576
|
+
summary.uploaded += 1;
|
|
5577
|
+
return { state: nextState, stop: false };
|
|
5578
|
+
}
|
|
5579
|
+
if (action.type === "download") {
|
|
5580
|
+
if (!remote) throw new Error("remote entry missing for download");
|
|
5581
|
+
await assertLocalSafeForDownload(root, path, state.entries[path]);
|
|
5582
|
+
const digest = await downloadRemote(root, state.library_id, path, api, remote.content_sha256, state.entries[path], () => {
|
|
5583
|
+
durableStateRequired = true;
|
|
5584
|
+
});
|
|
5585
|
+
const nextState = cloneDriveState(state);
|
|
5586
|
+
nextState.entries[path] = stateEntryFromRemote(remote, digest, clock);
|
|
5587
|
+
delete nextState.conflicts[path];
|
|
5588
|
+
await writeDriveState(root, nextState, clock);
|
|
5589
|
+
summary.downloaded += 1;
|
|
5590
|
+
return { state: nextState, stop: false };
|
|
5591
|
+
}
|
|
5592
|
+
if (action.type === "delete_remote") {
|
|
5593
|
+
await assertLocalAbsentBeforeRemoteDelete(root, path);
|
|
5594
|
+
await api.deleteFile(state.library_id, path, action.expectedEntryVersion);
|
|
5595
|
+
durableStateRequired = true;
|
|
5596
|
+
await assertLocalAbsentBeforeRemoteDelete(root, path);
|
|
5597
|
+
const nextState = cloneDriveState(state);
|
|
5598
|
+
delete nextState.entries[path];
|
|
5599
|
+
delete nextState.conflicts[path];
|
|
5600
|
+
await writeDriveState(root, nextState, clock);
|
|
5601
|
+
summary.deleted += 1;
|
|
5602
|
+
return { state: nextState, stop: false };
|
|
5603
|
+
}
|
|
5604
|
+
if (action.type === "delete_local") {
|
|
5605
|
+
await removeLocalIfStillBase(root, path, state.entries[path], () => {
|
|
5606
|
+
durableStateRequired = true;
|
|
5607
|
+
});
|
|
5608
|
+
const nextState = cloneDriveState(state);
|
|
5609
|
+
delete nextState.entries[path];
|
|
5610
|
+
delete nextState.conflicts[path];
|
|
5611
|
+
await writeDriveState(root, nextState, clock);
|
|
5612
|
+
summary.deleted += 1;
|
|
5613
|
+
return { state: nextState, stop: false };
|
|
5614
|
+
}
|
|
5615
|
+
if (action.type === "state_only") {
|
|
5616
|
+
if (!remote) throw new Error("remote entry missing for state update");
|
|
5617
|
+
const nextState = cloneDriveState(state);
|
|
5618
|
+
nextState.entries[path] = stateEntryFromRemote(remote, local?.sha256 ?? remote.content_sha256, clock);
|
|
5619
|
+
delete nextState.conflicts[path];
|
|
5620
|
+
await writeDriveState(root, nextState, clock);
|
|
5621
|
+
summary.unchanged += 1;
|
|
5622
|
+
return { state: nextState, stop: false };
|
|
5623
|
+
}
|
|
5624
|
+
if (action.type === "remove_state") {
|
|
5625
|
+
const nextState = cloneDriveState(state);
|
|
5626
|
+
delete nextState.entries[path];
|
|
5627
|
+
delete nextState.conflicts[path];
|
|
5628
|
+
await writeDriveState(root, nextState, clock);
|
|
5629
|
+
summary.unchanged += 1;
|
|
5630
|
+
return { state: nextState, stop: false };
|
|
5631
|
+
}
|
|
5632
|
+
if (action.type === "conflict") {
|
|
5633
|
+
if (action.reason === "local_and_remote_changed") {
|
|
5634
|
+
try {
|
|
5635
|
+
const mergedState = await tryResolveConflict({
|
|
5636
|
+
root,
|
|
5637
|
+
state,
|
|
5638
|
+
api,
|
|
5639
|
+
path,
|
|
5640
|
+
remote,
|
|
5641
|
+
local,
|
|
5642
|
+
clock,
|
|
5643
|
+
onLocalMutation: () => {
|
|
5644
|
+
durableStateRequired = true;
|
|
5645
|
+
}
|
|
5646
|
+
});
|
|
5647
|
+
if (mergedState) {
|
|
5648
|
+
summary.merged += 1;
|
|
5649
|
+
summary.paths[summary.paths.length - 1] = { path, action: "merged" };
|
|
5650
|
+
return { state: mergedState, stop: false };
|
|
5651
|
+
}
|
|
5652
|
+
} catch (error) {
|
|
5653
|
+
if (!isLocalChangedDuringMerge(error)) {
|
|
5654
|
+
throw error;
|
|
5655
|
+
}
|
|
5656
|
+
const nextState2 = await recordTypedConflict(root, state, path, action.reason, remote, clock, {
|
|
5657
|
+
type: "edit_edit",
|
|
5658
|
+
strategy: "record_only",
|
|
5659
|
+
reason: "local_changed_during_merge"
|
|
5660
|
+
});
|
|
5661
|
+
summary.conflicts += 1;
|
|
5662
|
+
return { state: nextState2, stop: false };
|
|
5663
|
+
}
|
|
5664
|
+
const conflictCopyState = await recordRemoteConflictCopy({
|
|
5665
|
+
root,
|
|
5666
|
+
state,
|
|
5667
|
+
api,
|
|
5668
|
+
path,
|
|
5669
|
+
reason: action.reason,
|
|
5670
|
+
type: "edit_edit",
|
|
5671
|
+
remote,
|
|
5672
|
+
clock
|
|
5673
|
+
});
|
|
5674
|
+
if (conflictCopyState) {
|
|
5675
|
+
summary.conflicts += 1;
|
|
5676
|
+
return { state: conflictCopyState, stop: false };
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
if (action.reason === "local_and_remote_without_base") {
|
|
5680
|
+
const conflictCopyState = await recordRemoteConflictCopy({
|
|
5681
|
+
root,
|
|
5682
|
+
state,
|
|
5683
|
+
api,
|
|
5684
|
+
path,
|
|
5685
|
+
reason: action.reason,
|
|
5686
|
+
type: "create_create",
|
|
5687
|
+
remote,
|
|
5688
|
+
clock
|
|
5689
|
+
});
|
|
5690
|
+
if (conflictCopyState) {
|
|
5691
|
+
summary.conflicts += 1;
|
|
5692
|
+
return { state: conflictCopyState, stop: false };
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
if (action.reason === "local_changed_remote_deleted") {
|
|
5696
|
+
const nextState2 = await recordTypedConflict(root, state, path, action.reason, remote, clock, {
|
|
5697
|
+
type: "edit_delete",
|
|
5698
|
+
strategy: "record_only"
|
|
5699
|
+
});
|
|
5700
|
+
summary.conflicts += 1;
|
|
5701
|
+
return { state: nextState2, stop: false };
|
|
5702
|
+
}
|
|
5703
|
+
if (action.reason === "remote_changed_before_delete") {
|
|
5704
|
+
const conflictCopyState = await recordRemoteConflictCopy({
|
|
5705
|
+
root,
|
|
5706
|
+
state,
|
|
5707
|
+
api,
|
|
5708
|
+
path,
|
|
5709
|
+
reason: action.reason,
|
|
5710
|
+
type: "delete_edit",
|
|
5711
|
+
remote,
|
|
5712
|
+
clock
|
|
5713
|
+
});
|
|
5714
|
+
if (conflictCopyState) {
|
|
5715
|
+
summary.conflicts += 1;
|
|
5716
|
+
return { state: conflictCopyState, stop: false };
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
const nextState = await recordConflict(root, state, path, action.reason, remote, clock);
|
|
5720
|
+
summary.conflicts += 1;
|
|
5721
|
+
return { state: nextState, stop: false };
|
|
5722
|
+
}
|
|
5723
|
+
summary.unchanged += 1;
|
|
5724
|
+
} catch (error) {
|
|
5725
|
+
if (isVersionConflict(error)) {
|
|
5726
|
+
try {
|
|
5727
|
+
const nextState = await recordConflict(root, state, path, "VERSION_CONFLICT", remote, clock);
|
|
5728
|
+
summary.conflicts += 1;
|
|
5729
|
+
summary.paths[summary.paths.length - 1] = { path, action: "conflict" };
|
|
5730
|
+
return { state: nextState, stop: false };
|
|
5731
|
+
} catch (writeError) {
|
|
5732
|
+
await recordPathError(summary, void 0, path, writeError);
|
|
5733
|
+
return { state, stop: durableStateRequired };
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
await recordPathError(summary, void 0, path, error);
|
|
5737
|
+
return { state, stop: durableStateRequired };
|
|
5738
|
+
}
|
|
5739
|
+
return { state, stop: false };
|
|
5740
|
+
}
|
|
5741
|
+
async function tryResolveConflict(args) {
|
|
5742
|
+
const { root, state, api, path, remote, local, clock, onLocalMutation } = args;
|
|
5743
|
+
const entry = state.entries[path];
|
|
5744
|
+
const baseVersionId = entry?.current_version_id;
|
|
5745
|
+
const remoteVersionId = remote?.current_version_id;
|
|
5746
|
+
if (!entry || !remote || !local || baseVersionId === void 0 || remoteVersionId === void 0) {
|
|
5747
|
+
return void 0;
|
|
5748
|
+
}
|
|
5749
|
+
const localPath = resolveInsideRoot(root, path);
|
|
5750
|
+
let baseBytes;
|
|
5751
|
+
let remoteBytes;
|
|
5752
|
+
try {
|
|
5753
|
+
const [downloadedBaseBytes, downloadedRemoteBytes] = await Promise.all([
|
|
5754
|
+
downloadBytes(api, state.library_id, path, baseVersionId),
|
|
5755
|
+
downloadBytes(api, state.library_id, path, remoteVersionId)
|
|
5756
|
+
]);
|
|
5757
|
+
baseBytes = downloadedBaseBytes;
|
|
5758
|
+
remoteBytes = downloadedRemoteBytes;
|
|
5759
|
+
} catch (error) {
|
|
5760
|
+
if (isExpectedVersionDownloadMissing(error)) {
|
|
5761
|
+
return void 0;
|
|
5762
|
+
}
|
|
5763
|
+
throw error;
|
|
5764
|
+
}
|
|
5765
|
+
const localBytes = await readFile4(localPath);
|
|
5766
|
+
const baseText = classifyMergeText(path, baseBytes, void 0);
|
|
5767
|
+
const localText = classifyMergeText(path, localBytes, void 0);
|
|
5768
|
+
const remoteText = classifyMergeText(path, remoteBytes, void 0);
|
|
5769
|
+
if (!baseText.mergeable || !localText.mergeable || !remoteText.mergeable) {
|
|
5770
|
+
return void 0;
|
|
5771
|
+
}
|
|
5772
|
+
const merged = mergeText3(baseText.text, localText.text, remoteText.text);
|
|
5773
|
+
if (!merged.clean) {
|
|
5774
|
+
return void 0;
|
|
5775
|
+
}
|
|
5776
|
+
await assertLocalStillScanned(localPath, local);
|
|
5777
|
+
const mergedBytes = new TextEncoder().encode(merged.text);
|
|
5778
|
+
const mergedDigest = createHash3("sha256").update(mergedBytes).digest("hex");
|
|
5779
|
+
const install = await writeMergedLocalFile(root, path, mergedBytes, mergedDigest, local, onLocalMutation);
|
|
5780
|
+
let uploaded;
|
|
5781
|
+
try {
|
|
5782
|
+
uploaded = await api.uploadFile(state.library_id, path, mergedBytes, mergedDigest, remote.entry_version);
|
|
5783
|
+
} catch (error) {
|
|
5784
|
+
await install.restore();
|
|
5785
|
+
throw error;
|
|
5786
|
+
}
|
|
5787
|
+
await install.finalize();
|
|
5788
|
+
const nextState = cloneDriveState(state);
|
|
5789
|
+
nextState.entries[path] = stateEntryFromRemote(uploaded.entry, mergedDigest, clock);
|
|
5790
|
+
delete nextState.conflicts[path];
|
|
5791
|
+
await writeDriveState(root, nextState, clock);
|
|
5792
|
+
return nextState;
|
|
5793
|
+
}
|
|
5794
|
+
async function downloadBytes(api, libraryId, path, versionId) {
|
|
5795
|
+
const response = await api.downloadFile(libraryId, path, versionId);
|
|
5796
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
5797
|
+
}
|
|
5798
|
+
async function recordRemoteConflictCopy(args) {
|
|
5799
|
+
const { root, state, api, path, reason, type, remote, clock } = args;
|
|
5800
|
+
const entry = state.entries[path];
|
|
5801
|
+
const remoteVersionId = remote?.current_version_id;
|
|
5802
|
+
if (!remote || remoteVersionId === void 0) {
|
|
5803
|
+
return void 0;
|
|
5804
|
+
}
|
|
5805
|
+
if (await canReuseConflictCopy(root, state.conflicts[path], remoteVersionId)) {
|
|
5806
|
+
return state;
|
|
5807
|
+
}
|
|
5808
|
+
const remoteBytes = await downloadBytes(api, state.library_id, path, remoteVersionId);
|
|
5809
|
+
const copyPath = await writeConflictCopy(root, path, "remote", remoteVersionId, remoteBytes, clock);
|
|
5810
|
+
const nextState = cloneDriveState(state);
|
|
5811
|
+
nextState.conflicts[path] = {
|
|
5812
|
+
detected_at: driveIsoTimestamp(clock),
|
|
5813
|
+
reason,
|
|
5814
|
+
type,
|
|
5815
|
+
strategy: "conflict_copy",
|
|
5816
|
+
base_version_id: entry?.current_version_id,
|
|
5817
|
+
remote_version_id: remoteVersionId,
|
|
5818
|
+
remote_entry_version: remote.entry_version,
|
|
5819
|
+
conflict_paths: [copyPath]
|
|
5820
|
+
};
|
|
5821
|
+
await writeDriveState(root, nextState, clock);
|
|
5822
|
+
return nextState;
|
|
5823
|
+
}
|
|
5824
|
+
async function canReuseConflictCopy(root, conflict2, remoteVersionId) {
|
|
5825
|
+
if (conflict2?.strategy !== "conflict_copy" || conflict2.remote_version_id !== remoteVersionId || !Array.isArray(conflict2.conflict_paths) || conflict2.conflict_paths.length === 0) {
|
|
5826
|
+
return false;
|
|
5827
|
+
}
|
|
5828
|
+
for (const conflictPath of conflict2.conflict_paths) {
|
|
5829
|
+
try {
|
|
5830
|
+
validateDrivePath(conflictPath);
|
|
5831
|
+
if (!await localFileExists(resolveInsideRoot(root, conflictPath))) {
|
|
5832
|
+
return false;
|
|
5833
|
+
}
|
|
5834
|
+
} catch {
|
|
5835
|
+
return false;
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
return true;
|
|
5839
|
+
}
|
|
5840
|
+
async function writeConflictCopy(root, path, side, versionId, bytes, clock) {
|
|
5841
|
+
const baseCopyPath = conflictCopyPath(path, side, driveConflictTimestamp(clock), versionId);
|
|
5842
|
+
for (let suffix = 1; ; suffix += 1) {
|
|
5843
|
+
const candidate = conflictCopyPathWithSuffix(baseCopyPath, suffix);
|
|
5844
|
+
validateDrivePath(candidate);
|
|
5845
|
+
const target = resolveInsideRoot(root, candidate);
|
|
5846
|
+
await mkdir3(dirname2(target), { recursive: true });
|
|
5847
|
+
for (; ; ) {
|
|
5848
|
+
const tmp = join5(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID3()}.tmp`);
|
|
5849
|
+
let tmpWritten = false;
|
|
5850
|
+
try {
|
|
5851
|
+
await writeFile3(tmp, bytes, { flag: "wx" });
|
|
5852
|
+
tmpWritten = true;
|
|
5853
|
+
await installNoOverwrite(tmp, target);
|
|
5854
|
+
return candidate;
|
|
5855
|
+
} catch (error) {
|
|
5856
|
+
if (tmpWritten) {
|
|
5857
|
+
await rm3(tmp, { force: true }).catch(() => {
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
5861
|
+
if (tmpWritten) break;
|
|
5862
|
+
continue;
|
|
5863
|
+
}
|
|
5864
|
+
throw error;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
function conflictCopyPathWithSuffix(path, suffix) {
|
|
5870
|
+
if (suffix === 1) {
|
|
5871
|
+
return path;
|
|
5872
|
+
}
|
|
5873
|
+
const parsed = pathPosix2.parse(path);
|
|
5874
|
+
const fileName = `${parsed.name}-${suffix}${parsed.ext}`;
|
|
5875
|
+
if (parsed.dir === "") {
|
|
5876
|
+
return fileName;
|
|
5877
|
+
}
|
|
5878
|
+
return pathPosix2.join(parsed.dir, fileName);
|
|
5879
|
+
}
|
|
5880
|
+
function isExpectedVersionDownloadMissing(error) {
|
|
5881
|
+
const structured = error;
|
|
5882
|
+
if (structured?.status === 404 || structured?.status === 410) return true;
|
|
5883
|
+
if (structured?.response?.status === 404 || structured?.response?.status === 410) return true;
|
|
5884
|
+
if (structured?.code === "VERSION_NOT_FOUND" || structured?.code === "NOT_FOUND") return true;
|
|
5885
|
+
return /\b(?:HTTP 40[410]|missing version|version not found|not found)\b/i.test(errorMessage(error));
|
|
5886
|
+
}
|
|
5887
|
+
function recordUnresolvedConflicts(summary, state) {
|
|
5888
|
+
const newlyRecorded = new Set(summary.paths.filter((result) => result.action === "conflict").map((result) => result.path));
|
|
5889
|
+
const reportedPaths = new Set(summary.paths.map((result) => result.path));
|
|
5890
|
+
for (const path of Object.keys(state.conflicts).sort((left, right) => left.localeCompare(right))) {
|
|
5891
|
+
const conflictPaths = state.conflicts[path]?.conflict_paths;
|
|
5892
|
+
if (conflictPaths) {
|
|
5893
|
+
summary.conflict_paths.push(...conflictPaths);
|
|
5894
|
+
}
|
|
5895
|
+
if (!newlyRecorded.has(path)) {
|
|
5896
|
+
summary.conflicts += 1;
|
|
5897
|
+
}
|
|
5898
|
+
const existingResult = summary.paths.find((result) => result.path === path);
|
|
5899
|
+
if (existingResult?.action === "unchanged") {
|
|
5900
|
+
existingResult.action = "conflict";
|
|
5901
|
+
if (conflictPaths) existingResult.conflict_paths = conflictPaths;
|
|
5902
|
+
continue;
|
|
5903
|
+
}
|
|
5904
|
+
if (existingResult?.action === "conflict" && conflictPaths) {
|
|
5905
|
+
existingResult.conflict_paths = conflictPaths;
|
|
5906
|
+
}
|
|
5907
|
+
if (!reportedPaths.has(path)) {
|
|
5908
|
+
summary.paths.push({ path, action: "conflict", ...conflictPaths ? { conflict_paths: conflictPaths } : {} });
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
function stateEntryFromRemote(remote, localSha256, clock) {
|
|
5166
5913
|
return {
|
|
5167
5914
|
entry_id: remote.id,
|
|
5168
5915
|
entry_version: remote.entry_version,
|
|
@@ -5170,13 +5917,10 @@ function stateEntryFromRemote(remote, localSha256) {
|
|
|
5170
5917
|
content_sha256: remote.content_sha256,
|
|
5171
5918
|
size_bytes: remote.size_bytes,
|
|
5172
5919
|
last_local_sha256: localSha256,
|
|
5173
|
-
last_synced_at: (
|
|
5920
|
+
last_synced_at: driveIsoTimestamp(clock),
|
|
5174
5921
|
status: "synced"
|
|
5175
5922
|
};
|
|
5176
5923
|
}
|
|
5177
|
-
async function commitDriveState(root, nextState) {
|
|
5178
|
-
await writeDriveState(root, nextState);
|
|
5179
|
-
}
|
|
5180
5924
|
function cloneDriveState(state) {
|
|
5181
5925
|
return {
|
|
5182
5926
|
...state,
|
|
@@ -5184,10 +5928,21 @@ function cloneDriveState(state) {
|
|
|
5184
5928
|
conflicts: { ...state.conflicts }
|
|
5185
5929
|
};
|
|
5186
5930
|
}
|
|
5187
|
-
async function recordConflict(root, state, path, reason, remote) {
|
|
5931
|
+
async function recordConflict(root, state, path, reason, remote, clock) {
|
|
5188
5932
|
const nextState = cloneDriveState(state);
|
|
5189
|
-
nextState.conflicts[path] = conflict(reason, remote);
|
|
5190
|
-
await
|
|
5933
|
+
nextState.conflicts[path] = conflict(reason, remote, clock);
|
|
5934
|
+
await writeDriveState(root, nextState, clock);
|
|
5935
|
+
return nextState;
|
|
5936
|
+
}
|
|
5937
|
+
async function recordTypedConflict(root, state, path, reason, remote, clock, metadata) {
|
|
5938
|
+
const nextState = cloneDriveState(state);
|
|
5939
|
+
nextState.conflicts[path] = {
|
|
5940
|
+
...conflict(metadata.reason ?? reason, remote, clock),
|
|
5941
|
+
type: metadata.type,
|
|
5942
|
+
strategy: metadata.strategy,
|
|
5943
|
+
base_version_id: state.entries[path]?.current_version_id
|
|
5944
|
+
};
|
|
5945
|
+
await writeDriveState(root, nextState, clock);
|
|
5191
5946
|
return nextState;
|
|
5192
5947
|
}
|
|
5193
5948
|
async function recordPathError(summary, blockedPaths, path, error, options = {}) {
|
|
@@ -5201,9 +5956,9 @@ async function recordPathError(summary, blockedPaths, path, error, options = {})
|
|
|
5201
5956
|
void errorMessage(error);
|
|
5202
5957
|
summary.errors += 1;
|
|
5203
5958
|
}
|
|
5204
|
-
function conflict(reason, remote) {
|
|
5959
|
+
function conflict(reason, remote, clock) {
|
|
5205
5960
|
return {
|
|
5206
|
-
detected_at: (
|
|
5961
|
+
detected_at: driveIsoTimestamp(clock),
|
|
5207
5962
|
reason,
|
|
5208
5963
|
remote_entry_version: remote?.entry_version,
|
|
5209
5964
|
remote_version_id: remote?.current_version_id
|
|
@@ -5217,6 +5972,9 @@ function isVersionConflict(error) {
|
|
|
5217
5972
|
if (structured?.code === "VERSION_CONFLICT") return true;
|
|
5218
5973
|
return [errorMessage(error), structured?.body, structured?.response?.body].some(containsVersionConflict);
|
|
5219
5974
|
}
|
|
5975
|
+
function isLocalChangedDuringMerge(error) {
|
|
5976
|
+
return errorMessage(error) === "local file changed after scan";
|
|
5977
|
+
}
|
|
5220
5978
|
function containsVersionConflict(value) {
|
|
5221
5979
|
if (value === void 0) return false;
|
|
5222
5980
|
if (typeof value === "string") return value.includes("VERSION_CONFLICT");
|
|
@@ -5226,22 +5984,268 @@ function containsVersionConflict(value) {
|
|
|
5226
5984
|
return false;
|
|
5227
5985
|
}
|
|
5228
5986
|
}
|
|
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
5987
|
|
|
5236
5988
|
// src/handwritten/commands/drive/watch.ts
|
|
5237
|
-
import { Command as
|
|
5989
|
+
import { Command as Command72 } from "commander";
|
|
5238
5990
|
import chokidar from "chokidar";
|
|
5239
5991
|
import { relative as relative2, resolve as resolve4 } from "path";
|
|
5992
|
+
|
|
5993
|
+
// src/handwritten/commands/drive/realtime.ts
|
|
5994
|
+
function createDriveRealtimeSource(args) {
|
|
5995
|
+
const connect = args.connect ?? nativeWebSocketConnector;
|
|
5996
|
+
const clock = args.clock ?? systemDriveClock;
|
|
5997
|
+
const scheduleTimeout = args.setTimeout ?? setTimeout;
|
|
5998
|
+
const cancelTimeout = args.clearTimeout ?? clearTimeout;
|
|
5999
|
+
let currentRealtime = { ...args.realtime };
|
|
6000
|
+
let handlers;
|
|
6001
|
+
let activeSocket;
|
|
6002
|
+
let reconnectTimer;
|
|
6003
|
+
let reconnectDelayMs = 1e3;
|
|
6004
|
+
let stopped = false;
|
|
6005
|
+
let authFailed = false;
|
|
6006
|
+
let connectionId = 0;
|
|
6007
|
+
function clearReconnectTimer() {
|
|
6008
|
+
if (reconnectTimer === void 0) return;
|
|
6009
|
+
cancelTimeout(reconnectTimer);
|
|
6010
|
+
reconnectTimer = void 0;
|
|
6011
|
+
}
|
|
6012
|
+
async function persist(next) {
|
|
6013
|
+
currentRealtime = next;
|
|
6014
|
+
await args.writeRealtimeState(next);
|
|
6015
|
+
}
|
|
6016
|
+
async function persistBestEffort(next) {
|
|
6017
|
+
try {
|
|
6018
|
+
await persist(next);
|
|
6019
|
+
} catch (error) {
|
|
6020
|
+
handlers?.onWarning?.(redactedRealtimeError(error));
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
function connectNow() {
|
|
6024
|
+
if (stopped || authFailed) return;
|
|
6025
|
+
clearReconnectTimer();
|
|
6026
|
+
const id = ++connectionId;
|
|
6027
|
+
const url = buildDriveRealtimeUrl(args.baseUrl, args.libraryId, currentRealtime);
|
|
6028
|
+
activeSocket = connect(url, {
|
|
6029
|
+
open() {
|
|
6030
|
+
if (id !== connectionId || stopped || authFailed) return;
|
|
6031
|
+
reconnectDelayMs = 1e3;
|
|
6032
|
+
void persistBestEffort({ ...currentRealtime, last_connected_at: driveIsoTimestamp(clock) }).then(() => handlers?.onConnected());
|
|
6033
|
+
},
|
|
6034
|
+
message(data) {
|
|
6035
|
+
if (id !== connectionId || stopped || authFailed) return;
|
|
6036
|
+
void handleMessage(data).catch((error) => handlers?.onWarning?.(redactedRealtimeError(error)));
|
|
6037
|
+
},
|
|
6038
|
+
close(error) {
|
|
6039
|
+
closeConnection(id, error ?? "close");
|
|
6040
|
+
}
|
|
6041
|
+
}, args.headers === void 0 ? void 0 : { headers: args.headers });
|
|
6042
|
+
}
|
|
6043
|
+
function closeConnection(id, error) {
|
|
6044
|
+
if (id !== connectionId || stopped || authFailed) return;
|
|
6045
|
+
connectionId += 1;
|
|
6046
|
+
const socket = activeSocket;
|
|
6047
|
+
activeSocket = void 0;
|
|
6048
|
+
clearReconnectTimer();
|
|
6049
|
+
socket?.close();
|
|
6050
|
+
if (isRealtimeAuthError(error)) {
|
|
6051
|
+
authFailed = true;
|
|
6052
|
+
handlers?.onAuthFailed(redactedRealtimeError(error));
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
const delayMs = reconnectDelayMs;
|
|
6056
|
+
handlers?.onReconnect(delayMs, redactedRealtimeError(error));
|
|
6057
|
+
reconnectTimer = scheduleTimeout(() => {
|
|
6058
|
+
reconnectTimer = void 0;
|
|
6059
|
+
connectNow();
|
|
6060
|
+
}, delayMs);
|
|
6061
|
+
reconnectDelayMs = Math.min(delayMs * 2, 6e4);
|
|
6062
|
+
}
|
|
6063
|
+
async function handleMessage(data) {
|
|
6064
|
+
const message = parseDriveRealtimeMessage(data);
|
|
6065
|
+
if (message.type === "ready") {
|
|
6066
|
+
if (message.replayed > 0) {
|
|
6067
|
+
handlers?.onEvent(optionalString({ debounce_ms: 2e3, reason: "ready_replay" }, "cursor", message.cursor));
|
|
6068
|
+
}
|
|
6069
|
+
if (message.cursor !== void 0) {
|
|
6070
|
+
await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor });
|
|
6071
|
+
}
|
|
6072
|
+
return;
|
|
6073
|
+
}
|
|
6074
|
+
if (message.type === "library_changed") {
|
|
6075
|
+
handlers?.onEvent(optionalString(optionalString({
|
|
6076
|
+
debounce_ms: 2e3,
|
|
6077
|
+
reason: "library_changed"
|
|
6078
|
+
}, "cursor", message.cursor), "path", message.path));
|
|
6079
|
+
if (message.cursor !== void 0) {
|
|
6080
|
+
await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor, last_event_at: driveIsoTimestamp(clock) });
|
|
6081
|
+
}
|
|
6082
|
+
return;
|
|
6083
|
+
}
|
|
6084
|
+
if (message.type === "resync_required") {
|
|
6085
|
+
const reason = message.reason ?? "resync_required";
|
|
6086
|
+
handlers?.onEvent(optionalString({ immediate: true, reason }, "cursor", message.cursor));
|
|
6087
|
+
if (message.cursor !== void 0 || isInvalidCursorReason(reason)) {
|
|
6088
|
+
await persistBestEffort(resyncRealtimeState(currentRealtime, message.cursor, reason, driveIsoTimestamp(clock)));
|
|
6089
|
+
}
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
6092
|
+
if (message.type === "error") {
|
|
6093
|
+
const error = message.message ?? message.code ?? "realtime error";
|
|
6094
|
+
if (isRealtimeAuthError(error) || isRealtimeAuthError(message.code)) {
|
|
6095
|
+
authFailed = true;
|
|
6096
|
+
connectionId += 1;
|
|
6097
|
+
clearReconnectTimer();
|
|
6098
|
+
handlers?.onAuthFailed(redactedRealtimeError(error));
|
|
6099
|
+
activeSocket?.close();
|
|
6100
|
+
activeSocket = void 0;
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
handlers?.onWarning?.(redactedRealtimeError(error));
|
|
6104
|
+
return;
|
|
6105
|
+
}
|
|
6106
|
+
handlers?.onWarning?.("unknown realtime message");
|
|
6107
|
+
}
|
|
6108
|
+
return {
|
|
6109
|
+
async start(nextHandlers) {
|
|
6110
|
+
handlers = nextHandlers;
|
|
6111
|
+
stopped = false;
|
|
6112
|
+
authFailed = false;
|
|
6113
|
+
connectNow();
|
|
6114
|
+
},
|
|
6115
|
+
async close() {
|
|
6116
|
+
stopped = true;
|
|
6117
|
+
clearReconnectTimer();
|
|
6118
|
+
activeSocket?.close();
|
|
6119
|
+
activeSocket = void 0;
|
|
6120
|
+
}
|
|
6121
|
+
};
|
|
6122
|
+
}
|
|
6123
|
+
function buildDriveRealtimeUrl(baseUrl, libraryId, realtime) {
|
|
6124
|
+
if (realtime.client_id.length === 0) {
|
|
6125
|
+
throw new Error("drive realtime client_id is required");
|
|
6126
|
+
}
|
|
6127
|
+
const url = new URL(baseUrl);
|
|
6128
|
+
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
|
6129
|
+
url.pathname = `/drive/libraries/${encodeURIComponent(libraryId)}/realtime`;
|
|
6130
|
+
url.search = "";
|
|
6131
|
+
url.hash = "";
|
|
6132
|
+
if (realtime.last_cursor !== void 0) {
|
|
6133
|
+
url.searchParams.set("cursor", realtime.last_cursor);
|
|
6134
|
+
}
|
|
6135
|
+
url.searchParams.set("client_id", realtime.client_id);
|
|
6136
|
+
return url;
|
|
6137
|
+
}
|
|
6138
|
+
function parseDriveRealtimeMessage(raw) {
|
|
6139
|
+
let value;
|
|
6140
|
+
try {
|
|
6141
|
+
value = JSON.parse(raw);
|
|
6142
|
+
} catch {
|
|
6143
|
+
return { type: "unknown" };
|
|
6144
|
+
}
|
|
6145
|
+
if (!isRecord3(value)) {
|
|
6146
|
+
return { type: "unknown" };
|
|
6147
|
+
}
|
|
6148
|
+
const messageType = typeof value.type === "string" ? value.type : void 0;
|
|
6149
|
+
const cursor = typeof value.cursor === "string" ? value.cursor : void 0;
|
|
6150
|
+
if (messageType === "ready") {
|
|
6151
|
+
return optionalString({ type: "ready", replayed: typeof value.replayed === "number" ? value.replayed : 0 }, "cursor", cursor);
|
|
6152
|
+
}
|
|
6153
|
+
if (messageType === "library_changed") {
|
|
6154
|
+
return optionalString(optionalString({ type: "library_changed" }, "cursor", cursor), "path", value.path);
|
|
6155
|
+
}
|
|
6156
|
+
if (messageType === "resync_required") {
|
|
6157
|
+
return optionalString(optionalString({ type: "resync_required" }, "cursor", cursor), "reason", value.reason);
|
|
6158
|
+
}
|
|
6159
|
+
if (messageType === "error") {
|
|
6160
|
+
return optionalString(
|
|
6161
|
+
optionalString({ type: "error" }, "code", value.code),
|
|
6162
|
+
"message",
|
|
6163
|
+
typeof value.message === "string" ? redactedRealtimeError(value.message) : void 0
|
|
6164
|
+
);
|
|
6165
|
+
}
|
|
6166
|
+
return optionalString({ type: "unknown" }, "message_type", messageType);
|
|
6167
|
+
}
|
|
6168
|
+
function redactedRealtimeError(error) {
|
|
6169
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
6170
|
+
const status = text.match(/\bHTTP\s+(401|403|429|5\d\d)\b/i);
|
|
6171
|
+
if (status?.[1] !== void 0) {
|
|
6172
|
+
return `HTTP ${status[1]}`;
|
|
6173
|
+
}
|
|
6174
|
+
if (/\bauth|authorization\b/i.test(text)) {
|
|
6175
|
+
return "auth failed";
|
|
6176
|
+
}
|
|
6177
|
+
if (/\bnetwork|fetch|close\b/i.test(text)) {
|
|
6178
|
+
return "network error";
|
|
6179
|
+
}
|
|
6180
|
+
return "realtime error";
|
|
6181
|
+
}
|
|
6182
|
+
function isRecord3(value) {
|
|
6183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6184
|
+
}
|
|
6185
|
+
function resyncRealtimeState(realtime, cursor, reason, lastEventAt) {
|
|
6186
|
+
if (isInvalidCursorReason(reason)) {
|
|
6187
|
+
const { last_cursor: _lastCursor, ...next } = realtime;
|
|
6188
|
+
return { ...next, last_event_at: lastEventAt };
|
|
6189
|
+
}
|
|
6190
|
+
return optionalString({ ...realtime, last_event_at: lastEventAt }, "last_cursor", cursor);
|
|
6191
|
+
}
|
|
6192
|
+
function isInvalidCursorReason(reason) {
|
|
6193
|
+
return /\bcursor[_ -]?(invalid|expired|gone|missing|not[_ -]?found)\b|\binvalid[_ -]?cursor\b/i.test(reason);
|
|
6194
|
+
}
|
|
6195
|
+
function isRealtimeAuthError(error) {
|
|
6196
|
+
return /\b(401|403|auth|authorization|unauthorized|forbidden)\b/i.test(String(error));
|
|
6197
|
+
}
|
|
6198
|
+
function optionalString(target, key, value) {
|
|
6199
|
+
if (typeof value !== "string") {
|
|
6200
|
+
return target;
|
|
6201
|
+
}
|
|
6202
|
+
return { ...target, [key]: value };
|
|
6203
|
+
}
|
|
6204
|
+
function nativeWebSocketConnector(url, handlers, init) {
|
|
6205
|
+
const WebSocketWithInit = WebSocket;
|
|
6206
|
+
const ws = new WebSocketWithInit(url.toString(), init);
|
|
6207
|
+
let closed = false;
|
|
6208
|
+
let pendingError;
|
|
6209
|
+
const closeOnce = (error) => {
|
|
6210
|
+
if (closed) return;
|
|
6211
|
+
closed = true;
|
|
6212
|
+
handlers.close(error);
|
|
6213
|
+
};
|
|
6214
|
+
ws.addEventListener("open", () => handlers.open());
|
|
6215
|
+
ws.addEventListener("message", (event) => handlers.message(String(event.data)));
|
|
6216
|
+
ws.addEventListener("close", (event) => closeOnce(webSocketCloseError(event) ?? pendingError));
|
|
6217
|
+
ws.addEventListener("error", (event) => {
|
|
6218
|
+
pendingError = webSocketError(event);
|
|
6219
|
+
if (isRealtimeAuthError(pendingError)) {
|
|
6220
|
+
closeOnce(pendingError);
|
|
6221
|
+
}
|
|
6222
|
+
});
|
|
6223
|
+
return { close: () => ws.close() };
|
|
6224
|
+
}
|
|
6225
|
+
function webSocketCloseError(event) {
|
|
6226
|
+
if (event.code === 1e3) return void 0;
|
|
6227
|
+
if (event.code === 4001 || event.code === 4401) return new Error("HTTP 401");
|
|
6228
|
+
if (event.code === 4003 || event.code === 4403) return new Error("HTTP 403");
|
|
6229
|
+
if (event.reason && isRealtimeAuthError(event.reason)) return new Error(event.reason);
|
|
6230
|
+
if (event.reason) return new Error(event.reason);
|
|
6231
|
+
return new Error(`WebSocket close ${event.code}`);
|
|
6232
|
+
}
|
|
6233
|
+
function webSocketError(event) {
|
|
6234
|
+
if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
|
|
6235
|
+
const detail = event.error instanceof Error ? event.error.message : event.message;
|
|
6236
|
+
if (detail) return new Error(detail);
|
|
6237
|
+
}
|
|
6238
|
+
return new Error("network error");
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
// src/handwritten/commands/drive/watch.ts
|
|
5240
6242
|
async function runDriveWatch(root, options = {}) {
|
|
5241
6243
|
const runSync = options.runSync ?? runDriveSyncOnce;
|
|
5242
6244
|
const debounceMs = options.debounceMs ?? 500;
|
|
6245
|
+
const remoteDebounceMs = options.remoteDebounceMs ?? 2e3;
|
|
5243
6246
|
const emit = options.onEvent ?? ((event) => render({ kind: "drive_watch", display: { shape: "object" } }, event));
|
|
5244
6247
|
let debounceTimer;
|
|
6248
|
+
let debounceDeadlineMs;
|
|
5245
6249
|
let retryTimer;
|
|
5246
6250
|
let resolveRetryTimer;
|
|
5247
6251
|
let running = false;
|
|
@@ -5269,6 +6273,7 @@ async function runDriveWatch(root, options = {}) {
|
|
|
5269
6273
|
} catch (error) {
|
|
5270
6274
|
if (isAuthError(error) || isFatalWatchError(error) || !isRetryableWatchError(error)) throw error;
|
|
5271
6275
|
emit({ kind: "drive_watch_retry", delay_ms: backoffMs, error: errorMessage2(error) });
|
|
6276
|
+
if (stopped) return;
|
|
5272
6277
|
await waitForManagedTimer(backoffMs);
|
|
5273
6278
|
if (stopped) return;
|
|
5274
6279
|
backoffMs = Math.min(backoffMs * 2, 6e4);
|
|
@@ -5283,6 +6288,27 @@ async function runDriveWatch(root, options = {}) {
|
|
|
5283
6288
|
if (debounceTimer === void 0) return;
|
|
5284
6289
|
clearTimeout(debounceTimer);
|
|
5285
6290
|
debounceTimer = void 0;
|
|
6291
|
+
debounceDeadlineMs = void 0;
|
|
6292
|
+
}
|
|
6293
|
+
function scheduleSync(delayMs) {
|
|
6294
|
+
if (running) {
|
|
6295
|
+
rerunRequested = true;
|
|
6296
|
+
return;
|
|
6297
|
+
}
|
|
6298
|
+
if (delayMs <= 0) {
|
|
6299
|
+
clearDebounceTimer();
|
|
6300
|
+
requestSync().catch(stopWithError);
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
const deadlineMs = Date.now() + delayMs;
|
|
6304
|
+
if (debounceTimer !== void 0 && debounceDeadlineMs !== void 0 && debounceDeadlineMs <= deadlineMs) return;
|
|
6305
|
+
clearDebounceTimer();
|
|
6306
|
+
debounceDeadlineMs = deadlineMs;
|
|
6307
|
+
debounceTimer = setTimeout(() => {
|
|
6308
|
+
debounceTimer = void 0;
|
|
6309
|
+
debounceDeadlineMs = void 0;
|
|
6310
|
+
requestSync().catch(stopWithError);
|
|
6311
|
+
}, delayMs);
|
|
5286
6312
|
}
|
|
5287
6313
|
function clearRetryTimer() {
|
|
5288
6314
|
if (retryTimer === void 0) return;
|
|
@@ -5304,26 +6330,61 @@ async function runDriveWatch(root, options = {}) {
|
|
|
5304
6330
|
}
|
|
5305
6331
|
function stopWithError(error) {
|
|
5306
6332
|
stopError = error;
|
|
6333
|
+
stopped = true;
|
|
5307
6334
|
clearDebounceTimer();
|
|
5308
6335
|
clearRetryTimer();
|
|
5309
6336
|
stopWatch?.();
|
|
5310
6337
|
}
|
|
5311
|
-
|
|
5312
|
-
|
|
6338
|
+
let source;
|
|
6339
|
+
let realtimeSource;
|
|
5313
6340
|
try {
|
|
6341
|
+
let state = await (options.readState ?? readDriveState)(root);
|
|
6342
|
+
realtimeSource = options.once ? void 0 : options.realtimeSource;
|
|
6343
|
+
if (!options.once && realtimeSource === void 0) {
|
|
6344
|
+
state = await ensureDriveRealtimeState(root);
|
|
6345
|
+
const { baseUrl, headers } = await loadRealtimeAuthHeaders({
|
|
6346
|
+
verifyPath: `/drive/libraries/${encodeURIComponent(state.library_id)}`
|
|
6347
|
+
});
|
|
6348
|
+
const realtime = state.realtime;
|
|
6349
|
+
if (realtime === void 0) {
|
|
6350
|
+
throw new Error("drive realtime state is required");
|
|
6351
|
+
}
|
|
6352
|
+
realtimeSource = createDriveRealtimeSource({
|
|
6353
|
+
baseUrl,
|
|
6354
|
+
headers,
|
|
6355
|
+
libraryId: state.library_id,
|
|
6356
|
+
realtime,
|
|
6357
|
+
writeRealtimeState: (next) => writeDriveRealtimeState(root, next)
|
|
6358
|
+
});
|
|
6359
|
+
}
|
|
6360
|
+
source = options.source ?? createChokidarSource(root);
|
|
5314
6361
|
source.onChange((path) => {
|
|
5315
6362
|
if (isDriveInternalPath(root, path)) return;
|
|
5316
|
-
|
|
5317
|
-
rerunRequested = true;
|
|
5318
|
-
return;
|
|
5319
|
-
}
|
|
5320
|
-
clearDebounceTimer();
|
|
5321
|
-
debounceTimer = setTimeout(() => {
|
|
5322
|
-
debounceTimer = void 0;
|
|
5323
|
-
requestSync().catch(stopWithError);
|
|
5324
|
-
}, debounceMs);
|
|
6363
|
+
scheduleSync(debounceMs);
|
|
5325
6364
|
});
|
|
5326
6365
|
emit({ kind: "drive_watch_started", root, library_id: state.library_id });
|
|
6366
|
+
realtimeSource?.start({
|
|
6367
|
+
onConnected() {
|
|
6368
|
+
emit({ kind: "drive_realtime_connected", library_id: state.library_id });
|
|
6369
|
+
},
|
|
6370
|
+
onEvent(event) {
|
|
6371
|
+
emit(realtimeEvent(event));
|
|
6372
|
+
if (event.immediate) {
|
|
6373
|
+
scheduleSync(0);
|
|
6374
|
+
return;
|
|
6375
|
+
}
|
|
6376
|
+
scheduleSync(event.debounce_ms ?? remoteDebounceMs);
|
|
6377
|
+
},
|
|
6378
|
+
onReconnect(delayMs, error) {
|
|
6379
|
+
emit({ kind: "drive_realtime_reconnecting", delay_ms: delayMs, error });
|
|
6380
|
+
},
|
|
6381
|
+
onAuthFailed(error) {
|
|
6382
|
+
emit({ kind: "drive_realtime_auth_failed", error: error ?? "auth failed" });
|
|
6383
|
+
},
|
|
6384
|
+
onWarning(warning) {
|
|
6385
|
+
emit({ kind: "drive_realtime_warning", warning });
|
|
6386
|
+
}
|
|
6387
|
+
}).catch(stopWithError);
|
|
5327
6388
|
await requestSync();
|
|
5328
6389
|
if (options.once) return;
|
|
5329
6390
|
if (stopError !== void 0) throw stopError;
|
|
@@ -5337,11 +6398,11 @@ async function runDriveWatch(root, options = {}) {
|
|
|
5337
6398
|
clearDebounceTimer();
|
|
5338
6399
|
clearRetryTimer();
|
|
5339
6400
|
cleanupSignalListeners();
|
|
5340
|
-
await source
|
|
6401
|
+
await Promise.all([source?.close(), realtimeSource?.close()]);
|
|
5341
6402
|
}
|
|
5342
6403
|
}
|
|
5343
6404
|
function driveWatchCommand(options = {}) {
|
|
5344
|
-
return new
|
|
6405
|
+
return new Command72("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
5345
6406
|
await runDriveWatch(resolve4(path), options);
|
|
5346
6407
|
});
|
|
5347
6408
|
}
|
|
@@ -5379,6 +6440,15 @@ function isRetryableWatchError(error) {
|
|
|
5379
6440
|
function errorMessage2(error) {
|
|
5380
6441
|
return error instanceof Error ? error.message : String(error);
|
|
5381
6442
|
}
|
|
6443
|
+
function realtimeEvent(event) {
|
|
6444
|
+
return {
|
|
6445
|
+
kind: "drive_realtime_event",
|
|
6446
|
+
message: "remote update received; syncing",
|
|
6447
|
+
...event.path === void 0 ? {} : { path: event.path },
|
|
6448
|
+
...event.reason === void 0 ? {} : { reason: event.reason },
|
|
6449
|
+
...event.cursor === void 0 ? {} : { cursor: event.cursor }
|
|
6450
|
+
};
|
|
6451
|
+
}
|
|
5382
6452
|
function waitForStopSignal(onRegistered) {
|
|
5383
6453
|
return new Promise((resolveStop) => {
|
|
5384
6454
|
const stop = () => resolveStop();
|
|
@@ -5396,7 +6466,7 @@ function waitForStopSignal(onRegistered) {
|
|
|
5396
6466
|
function mountDriveCommands(program) {
|
|
5397
6467
|
let drive = program.commands.find((c) => c.name() === "drive");
|
|
5398
6468
|
if (!drive) {
|
|
5399
|
-
drive = new
|
|
6469
|
+
drive = new Command73("drive").description("Drive commands");
|
|
5400
6470
|
program.addCommand(drive);
|
|
5401
6471
|
}
|
|
5402
6472
|
if (!drive.commands.some((c) => c.name() === "bind")) {
|
|
@@ -5422,7 +6492,7 @@ function isCliEntrypoint(argv = process.argv, metaUrl = import.meta.url) {
|
|
|
5422
6492
|
}
|
|
5423
6493
|
}
|
|
5424
6494
|
function buildProgram() {
|
|
5425
|
-
const program = new
|
|
6495
|
+
const program = new Command73().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) => {
|
|
5426
6496
|
const globals = actionCommand.optsWithGlobals();
|
|
5427
6497
|
if (globals.json) process.env.WSPC_OUTPUT = "json";
|
|
5428
6498
|
if (globals.account) process.env.WSPC_ACCOUNT = String(globals.account);
|