share-pages 0.1.0

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.
Files changed (146) hide show
  1. package/README.md +49 -0
  2. package/dist/dist-JyXFh6I5.mjs +5444 -0
  3. package/dist/epub-BXQPqTFk-CGD2NLS1.mjs +4915 -0
  4. package/dist/index.mjs +679 -0
  5. package/dist/latex-CybgCSnr-CvP5kdPz.mjs +33421 -0
  6. package/dist/latex-DMo_3IW8-B6ogeBvw.mjs +20 -0
  7. package/dist/markdown-BWlCFeLJ-OwSvKM8l.mjs +68089 -0
  8. package/dist/node-Lh4CQjZ4.mjs +77 -0
  9. package/dist/papaparse-BJnSo-VR.mjs +1139 -0
  10. package/dist/prepare-C56DezIS-DiqwpdkG.mjs +8563 -0
  11. package/dist/resources/assets/csv-worker-Bilq-sDm.js +17 -0
  12. package/dist/resources/chunks/viewer-Bz6Cwh4W.js +9561 -0
  13. package/dist/resources/csv.js +2444 -0
  14. package/dist/resources/epub.js +16493 -0
  15. package/dist/resources/fonts/KaTeX_AMS-Regular.ttf +0 -0
  16. package/dist/resources/fonts/KaTeX_AMS-Regular.woff +0 -0
  17. package/dist/resources/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  18. package/dist/resources/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  19. package/dist/resources/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  20. package/dist/resources/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  21. package/dist/resources/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  22. package/dist/resources/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  23. package/dist/resources/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  24. package/dist/resources/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  25. package/dist/resources/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  26. package/dist/resources/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  27. package/dist/resources/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  28. package/dist/resources/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  29. package/dist/resources/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  30. package/dist/resources/fonts/KaTeX_Main-Bold.ttf +0 -0
  31. package/dist/resources/fonts/KaTeX_Main-Bold.woff +0 -0
  32. package/dist/resources/fonts/KaTeX_Main-Bold.woff2 +0 -0
  33. package/dist/resources/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  34. package/dist/resources/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  35. package/dist/resources/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  36. package/dist/resources/fonts/KaTeX_Main-Italic.ttf +0 -0
  37. package/dist/resources/fonts/KaTeX_Main-Italic.woff +0 -0
  38. package/dist/resources/fonts/KaTeX_Main-Italic.woff2 +0 -0
  39. package/dist/resources/fonts/KaTeX_Main-Regular.ttf +0 -0
  40. package/dist/resources/fonts/KaTeX_Main-Regular.woff +0 -0
  41. package/dist/resources/fonts/KaTeX_Main-Regular.woff2 +0 -0
  42. package/dist/resources/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  43. package/dist/resources/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  44. package/dist/resources/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  45. package/dist/resources/fonts/KaTeX_Math-Italic.ttf +0 -0
  46. package/dist/resources/fonts/KaTeX_Math-Italic.woff +0 -0
  47. package/dist/resources/fonts/KaTeX_Math-Italic.woff2 +0 -0
  48. package/dist/resources/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  49. package/dist/resources/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  50. package/dist/resources/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  51. package/dist/resources/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  52. package/dist/resources/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  53. package/dist/resources/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  54. package/dist/resources/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  55. package/dist/resources/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  56. package/dist/resources/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  57. package/dist/resources/fonts/KaTeX_Script-Regular.ttf +0 -0
  58. package/dist/resources/fonts/KaTeX_Script-Regular.woff +0 -0
  59. package/dist/resources/fonts/KaTeX_Script-Regular.woff2 +0 -0
  60. package/dist/resources/fonts/KaTeX_Size1-Regular.ttf +0 -0
  61. package/dist/resources/fonts/KaTeX_Size1-Regular.woff +0 -0
  62. package/dist/resources/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  63. package/dist/resources/fonts/KaTeX_Size2-Regular.ttf +0 -0
  64. package/dist/resources/fonts/KaTeX_Size2-Regular.woff +0 -0
  65. package/dist/resources/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  66. package/dist/resources/fonts/KaTeX_Size3-Regular.ttf +0 -0
  67. package/dist/resources/fonts/KaTeX_Size3-Regular.woff +0 -0
  68. package/dist/resources/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  69. package/dist/resources/fonts/KaTeX_Size4-Regular.ttf +0 -0
  70. package/dist/resources/fonts/KaTeX_Size4-Regular.woff +0 -0
  71. package/dist/resources/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  72. package/dist/resources/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  73. package/dist/resources/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  74. package/dist/resources/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  75. package/dist/resources/katex.css +1 -0
  76. package/dist/resources/latex-article.css +33 -0
  77. package/dist/resources/latex-base.css +816 -0
  78. package/dist/resources/latex-book.css +43 -0
  79. package/dist/resources/latex-fonts/KaTeX_AMS-Regular.woff +0 -0
  80. package/dist/resources/latex-fonts/KaTeX_AMS-Regular.woff2 +0 -0
  81. package/dist/resources/latex-fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  82. package/dist/resources/latex-fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  83. package/dist/resources/latex-fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  84. package/dist/resources/latex-fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  85. package/dist/resources/latex-fonts/KaTeX_Fraktur-Bold.woff +0 -0
  86. package/dist/resources/latex-fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  87. package/dist/resources/latex-fonts/KaTeX_Fraktur-Regular.woff +0 -0
  88. package/dist/resources/latex-fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  89. package/dist/resources/latex-fonts/KaTeX_Main-Bold.woff +0 -0
  90. package/dist/resources/latex-fonts/KaTeX_Main-Bold.woff2 +0 -0
  91. package/dist/resources/latex-fonts/KaTeX_Main-BoldItalic.woff +0 -0
  92. package/dist/resources/latex-fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  93. package/dist/resources/latex-fonts/KaTeX_Main-Italic.woff +0 -0
  94. package/dist/resources/latex-fonts/KaTeX_Main-Italic.woff2 +0 -0
  95. package/dist/resources/latex-fonts/KaTeX_Main-Regular.woff +0 -0
  96. package/dist/resources/latex-fonts/KaTeX_Main-Regular.woff2 +0 -0
  97. package/dist/resources/latex-fonts/KaTeX_Math-BoldItalic.woff +0 -0
  98. package/dist/resources/latex-fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  99. package/dist/resources/latex-fonts/KaTeX_Math-Italic.woff +0 -0
  100. package/dist/resources/latex-fonts/KaTeX_Math-Italic.woff2 +0 -0
  101. package/dist/resources/latex-fonts/KaTeX_SansSerif-Bold.woff +0 -0
  102. package/dist/resources/latex-fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  103. package/dist/resources/latex-fonts/KaTeX_SansSerif-Italic.woff +0 -0
  104. package/dist/resources/latex-fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  105. package/dist/resources/latex-fonts/KaTeX_SansSerif-Regular.woff +0 -0
  106. package/dist/resources/latex-fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  107. package/dist/resources/latex-fonts/KaTeX_Script-Regular.woff +0 -0
  108. package/dist/resources/latex-fonts/KaTeX_Script-Regular.woff2 +0 -0
  109. package/dist/resources/latex-fonts/KaTeX_Size1-Regular.woff +0 -0
  110. package/dist/resources/latex-fonts/KaTeX_Size1-Regular.woff2 +0 -0
  111. package/dist/resources/latex-fonts/KaTeX_Size2-Regular.woff +0 -0
  112. package/dist/resources/latex-fonts/KaTeX_Size2-Regular.woff2 +0 -0
  113. package/dist/resources/latex-fonts/KaTeX_Size3-Regular.woff +0 -0
  114. package/dist/resources/latex-fonts/KaTeX_Size3-Regular.woff2 +0 -0
  115. package/dist/resources/latex-fonts/KaTeX_Size4-Regular.woff +0 -0
  116. package/dist/resources/latex-fonts/KaTeX_Size4-Regular.woff2 +0 -0
  117. package/dist/resources/latex-fonts/KaTeX_Typewriter-Regular.woff +0 -0
  118. package/dist/resources/latex-fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  119. package/dist/resources/latex-fonts/OFL.txt +102 -0
  120. package/dist/resources/latex-fonts/Sans/cmun-sans.css +30 -0
  121. package/dist/resources/latex-fonts/Sans/cmunsi.woff +0 -0
  122. package/dist/resources/latex-fonts/Sans/cmunso.woff +0 -0
  123. package/dist/resources/latex-fonts/Sans/cmunss.woff +0 -0
  124. package/dist/resources/latex-fonts/Sans/cmunsx.woff +0 -0
  125. package/dist/resources/latex-fonts/Serif/cmun-serif.css +30 -0
  126. package/dist/resources/latex-fonts/Serif/cmunbi.woff +0 -0
  127. package/dist/resources/latex-fonts/Serif/cmunbx.woff +0 -0
  128. package/dist/resources/latex-fonts/Serif/cmunrm.woff +0 -0
  129. package/dist/resources/latex-fonts/Serif/cmunti.woff +0 -0
  130. package/dist/resources/latex-fonts/Serif Slanted/cmun-serif-slanted.css +14 -0
  131. package/dist/resources/latex-fonts/Serif Slanted/cmunbl.woff +0 -0
  132. package/dist/resources/latex-fonts/Serif Slanted/cmunsl.woff +0 -0
  133. package/dist/resources/latex-fonts/Typewriter/cmun-typewriter.css +30 -0
  134. package/dist/resources/latex-fonts/Typewriter/cmunit.woff +0 -0
  135. package/dist/resources/latex-fonts/Typewriter/cmuntb.woff +0 -0
  136. package/dist/resources/latex-fonts/Typewriter/cmuntt.woff +0 -0
  137. package/dist/resources/latex-fonts/Typewriter/cmuntx.woff +0 -0
  138. package/dist/resources/latex-fonts/Typewriter Slanted/cmun-typewriter-slanted.css +6 -0
  139. package/dist/resources/latex-fonts/Typewriter Slanted/cmunst.woff +0 -0
  140. package/dist/resources/latex-fonts/cmu.css +13 -0
  141. package/dist/resources/latex-katex.css +1079 -0
  142. package/dist/resources/licenses.txt +131 -0
  143. package/dist/resources/reader.css +244 -0
  144. package/dist/resources/viewers.css +3 -0
  145. package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
  146. package/package.json +40 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env node
2
+ import { a as digest, i as defaultLimits, n as CHUNK_SIZE, r as assert, s as formats, t as AppError } from "./dist-JyXFh6I5.mjs";
3
+ import { Command, CommanderError, Option } from "commander";
4
+ import { randomUUID } from "node:crypto";
5
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { createInterface } from "node:readline/promises";
9
+ import { createWriteStream } from "node:fs";
10
+ import { Readable } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
12
+ //#region ../../packages/sdk/dist/index.js
13
+ function checkAbort(signal) {
14
+ if (signal?.aborted) throw new AppError("ABORTED", "操作已取消,上传会话仍可恢复", 499);
15
+ }
16
+ function abortScope(parent, timeoutMs) {
17
+ const controller = new AbortController();
18
+ const abort = () => controller.abort(parent?.reason);
19
+ if (parent?.aborted) abort();
20
+ else parent?.addEventListener("abort", abort, { once: true });
21
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(), timeoutMs);
22
+ return {
23
+ controller,
24
+ signal: controller.signal,
25
+ dispose() {
26
+ if (timer !== void 0) clearTimeout(timer);
27
+ parent?.removeEventListener("abort", abort);
28
+ }
29
+ };
30
+ }
31
+ function delay(ms, signal) {
32
+ checkAbort(signal);
33
+ return new Promise((resolve, reject) => {
34
+ const finish = () => {
35
+ signal?.removeEventListener("abort", cancel);
36
+ resolve();
37
+ };
38
+ const timer = setTimeout(finish, ms);
39
+ const cancel = () => {
40
+ clearTimeout(timer);
41
+ signal?.removeEventListener("abort", cancel);
42
+ reject(new AppError("ABORTED", "操作已取消,上传会话仍可恢复", 499));
43
+ };
44
+ signal?.addEventListener("abort", cancel, { once: true });
45
+ });
46
+ }
47
+ function result(session) {
48
+ assert(session.result, "INVALID_RESPONSE", "已提交会话缺少发布结果", 502);
49
+ return session.result;
50
+ }
51
+ async function waitForUpload(client, id, timeoutMs = 12e4, signal) {
52
+ assert(Number.isFinite(timeoutMs) && timeoutMs >= 0, "INVALID_TIMEOUT", "等待时间必须为非负有限数");
53
+ const end = Date.now() + timeoutMs;
54
+ while (true) {
55
+ checkAbort(signal);
56
+ const status = await client.request(`/uploads/${encodeURIComponent(id)}`, { signal }, true);
57
+ if (status.status === "committed") return result(status);
58
+ if ([
59
+ "failed",
60
+ "cancelled",
61
+ "expired"
62
+ ].includes(status.status)) throw new AppError(status.error?.code ?? "PUBLISH_FAILED", status.error?.message ?? `发布 ${status.status}`, 409);
63
+ if (Date.now() >= end) return status;
64
+ await delay(Math.min(500, Math.max(0, end - Date.now())), signal);
65
+ }
66
+ }
67
+ async function publish(client, prepared, options = {}) {
68
+ const { idempotencyKey = crypto.randomUUID(), uploadId, wait = true, timeoutMs = 12e4, signal, uploadConcurrency = 3, onProgress, onSession, ...input } = options;
69
+ assert(Number.isFinite(timeoutMs) && timeoutMs >= 0, "INVALID_TIMEOUT", "等待时间必须为非负有限数");
70
+ assert(Number.isInteger(uploadConcurrency) && uploadConcurrency >= 1 && uploadConcurrency <= 4, "INVALID_CONCURRENCY", "上传并发数必须为 1–4");
71
+ checkAbort(signal);
72
+ let session = uploadId ? await client.request(`/uploads/${encodeURIComponent(uploadId)}`, { signal }, true) : await client.json("/uploads", "POST", {
73
+ ...input,
74
+ title: options.title ?? prepared.title,
75
+ manifest: prepared.manifest
76
+ }, { "Idempotency-Key": idempotencyKey }, true, signal);
77
+ assert(session.manifestDigest === await digest(JSON.stringify(prepared.manifest)), "RESUME_MISMATCH", "上传会话与本地内容清单不同");
78
+ await onSession?.(session);
79
+ if (session.status === "committed") return result(session);
80
+ if (session.status === "uploading") {
81
+ const files = new Map(prepared.manifest.files.map((file) => [file.id, file]));
82
+ const tasks = [];
83
+ const total = prepared.manifest.files.reduce((sum, file) => sum + file.size, 0);
84
+ let completed = total;
85
+ for (const missing of session.missing) {
86
+ const file = files.get(missing.fileId);
87
+ const data = prepared.files.get(missing.fileId);
88
+ assert(file && data, "RESUME_MISMATCH", "断点会话与本地文件不同");
89
+ assert(data.byteLength === file.size, "RESUME_MISMATCH", "本地文件大小与清单不同");
90
+ assert(new Set(missing.parts).size === missing.parts.length, "INVALID_RESPONSE", "服务端返回重复分片", 502);
91
+ for (const part of missing.parts) {
92
+ assert(Number.isInteger(part) && part >= 0 && (part === 0 || part * 8388608 < file.size), "INVALID_RESPONSE", "服务端分片编号无效", 502);
93
+ const bytes = data.subarray(part * CHUNK_SIZE, (part + 1) * CHUNK_SIZE);
94
+ completed -= bytes.byteLength;
95
+ tasks.push({
96
+ fileId: missing.fileId,
97
+ part,
98
+ bytes
99
+ });
100
+ }
101
+ }
102
+ onProgress?.(completed, total);
103
+ const scope = abortScope(signal);
104
+ let next = 0;
105
+ let failure;
106
+ try {
107
+ await Promise.all(Array.from({ length: Math.min(uploadConcurrency, tasks.length) }, async () => {
108
+ try {
109
+ while (next < tasks.length) {
110
+ checkAbort(scope.signal);
111
+ const task = tasks[next++];
112
+ if (!task) break;
113
+ await client.request(`/uploads/${session.id}/files/${task.fileId}?part=${task.part}`, {
114
+ method: "PUT",
115
+ headers: { "X-Chunk-SHA256": await digest(task.bytes) },
116
+ body: task.bytes,
117
+ signal: scope.signal
118
+ }, true);
119
+ completed += task.bytes.byteLength;
120
+ onProgress?.(completed, total);
121
+ }
122
+ } catch (error) {
123
+ failure ??= error;
124
+ scope.controller.abort();
125
+ }
126
+ }));
127
+ if (failure) throw failure;
128
+ } finally {
129
+ scope.dispose();
130
+ }
131
+ session = await client.json(`/uploads/${session.id}/complete`, "POST", {}, {}, true, signal);
132
+ }
133
+ return wait ? waitForUpload(client, session.id, timeoutMs, signal) : session;
134
+ }
135
+ var HTTPClient = class {
136
+ server;
137
+ transport;
138
+ token;
139
+ timeout;
140
+ constructor(options) {
141
+ this.server = options.server.replace(/\/$/, "");
142
+ let url;
143
+ try {
144
+ url = new URL(this.server);
145
+ } catch {
146
+ throw new AppError("INVALID_SERVER", "实例地址需要是 HTTP(S) origin");
147
+ }
148
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new AppError("INVALID_SERVER", "实例地址需要是 HTTP(S) origin");
149
+ if (url.protocol !== "https:" && ![
150
+ "localhost",
151
+ "127.0.0.1",
152
+ "[::1]"
153
+ ].includes(url.hostname)) throw new AppError("INSECURE_SERVER", "远端实例必须使用 HTTPS");
154
+ this.timeout = options.requestTimeoutMs ?? 3e4;
155
+ if (!Number.isFinite(this.timeout) || this.timeout <= 0) throw new AppError("INVALID_TIMEOUT", "请求超时必须是有限正数");
156
+ this.token = options.token;
157
+ this.transport = options.fetch ?? globalThis.fetch.bind(globalThis);
158
+ }
159
+ async request(path, init = {}, retry = false) {
160
+ const headers = new Headers(init.headers);
161
+ if (this.token) headers.set("Authorization", `Bearer ${this.token}`);
162
+ for (let attempt = 0;; attempt++) {
163
+ checkAbort(init.signal);
164
+ const scope = abortScope(init.signal, this.timeout);
165
+ let retryAfter = 250 * 2 ** attempt;
166
+ try {
167
+ const response = await this.transport(`${this.server}/api/v1${path}`, {
168
+ ...init,
169
+ headers,
170
+ credentials: "include",
171
+ signal: init.signal ? AbortSignal.any([scope.signal, init.signal]) : scope.signal
172
+ });
173
+ if (retry && [
174
+ 429,
175
+ 502,
176
+ 503,
177
+ 504
178
+ ].includes(response.status) && attempt < 3) {
179
+ const value = response.headers.get("Retry-After");
180
+ if (value) retryAfter = Math.min(5e3, Math.max(0, /^\d+$/.test(value) ? Number(value) * 1e3 : Date.parse(value) - Date.now()) || retryAfter);
181
+ await response.body?.cancel();
182
+ } else {
183
+ if (!response.ok) {
184
+ let error = {};
185
+ try {
186
+ const value = await response.json();
187
+ if (value && typeof value === "object") error = value;
188
+ } catch {}
189
+ throw new AppError(typeof error.code === "string" ? error.code : "HTTP_ERROR", typeof error.message === "string" ? error.message : `请求失败 (${response.status})`, response.status, error.fields, error.requestId ?? response.headers.get("X-Request-Id") ?? void 0);
190
+ }
191
+ if (response.headers.get("Content-Type")?.includes("application/json")) try {
192
+ return await response.json();
193
+ } catch {
194
+ throw new AppError("INVALID_RESPONSE", "服务端返回了无效 JSON", 502);
195
+ }
196
+ return response;
197
+ }
198
+ } catch (error) {
199
+ checkAbort(init.signal);
200
+ if (error instanceof AppError) throw error;
201
+ if (!retry || attempt >= 3) throw new AppError(scope.signal.aborted ? "REQUEST_TIMEOUT" : "NETWORK_ERROR", "网络请求失败,可使用相同上传会话继续", 503);
202
+ } finally {
203
+ scope.dispose();
204
+ }
205
+ await delay(retryAfter, init.signal);
206
+ }
207
+ }
208
+ json(path, method, body, headers = {}, retry = false, signal) {
209
+ return this.request(path, {
210
+ method,
211
+ headers: {
212
+ "Content-Type": "application/json",
213
+ ...headers
214
+ },
215
+ body: JSON.stringify(body),
216
+ signal
217
+ }, retry);
218
+ }
219
+ };
220
+ var SharePagesClient = class extends HTTPClient {
221
+ publish(prepared, options = {}) {
222
+ return publish(this, prepared, options);
223
+ }
224
+ waitForUpload(id, timeoutMs = 12e4, signal) {
225
+ return waitForUpload(this, id, timeoutMs, signal);
226
+ }
227
+ capabilities() {
228
+ return this.request("/capabilities", {}, true);
229
+ }
230
+ listPages(params = {}) {
231
+ return this.request(`/pages?${new URLSearchParams(params)}`);
232
+ }
233
+ getPage(id) {
234
+ return this.request(`/pages/${encodeURIComponent(id)}`);
235
+ }
236
+ listVersions(id, cursor) {
237
+ return this.request(`/pages/${encodeURIComponent(id)}/versions` + (cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""));
238
+ }
239
+ patchPage(id, patch) {
240
+ return this.json(`/pages/${encodeURIComponent(id)}`, "PATCH", patch);
241
+ }
242
+ async rollback(id, versionId, options = {}) {
243
+ const s = await this.json(`/pages/${encodeURIComponent(id)}/rollback`, "POST", { versionId }, { "Idempotency-Key": options.idempotencyKey ?? crypto.randomUUID() }, true);
244
+ return options.wait === false ? s : this.waitForUpload(s.id, options.timeoutMs);
245
+ }
246
+ deletePage(id, purge = false) {
247
+ return this.request(`/pages/${encodeURIComponent(id)}?purge=${purge}`, { method: "DELETE" });
248
+ }
249
+ restorePage(id, replaceWith) {
250
+ return this.json(`/pages/${encodeURIComponent(id)}/restore`, "POST", { replaceWith });
251
+ }
252
+ deleteVersion(id, version, options = {}) {
253
+ const params = new URLSearchParams(Object.entries(options).filter(([, v]) => v !== void 0).map(([k, v]) => [k, String(v)]));
254
+ return this.request(`/pages/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}?${params}`, { method: "DELETE" });
255
+ }
256
+ restoreVersion(id, version) {
257
+ return this.json(`/pages/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/restore`, "POST", {});
258
+ }
259
+ download(id, version) {
260
+ return this.request(`/pages/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/download`);
261
+ }
262
+ stats(params = {}) {
263
+ return this.request(`/stats?${new URLSearchParams(params)}`);
264
+ }
265
+ };
266
+ //#endregion
267
+ //#region src/config.ts
268
+ const configDir = process.env.SHARE_PAGES_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "share-pages");
269
+ const configPath = join(configDir, "config.json");
270
+ async function readConfig() {
271
+ try {
272
+ const value = JSON.parse(await readFile(configPath, "utf8"));
273
+ if (!validConfig(value)) throw new Error("Invalid configuration");
274
+ return value;
275
+ } catch (e) {
276
+ if (e.code === "ENOENT") return {
277
+ active: "default",
278
+ profiles: {}
279
+ };
280
+ throw new AppError("CONFIG_ERROR", "配置文件读取失败");
281
+ }
282
+ }
283
+ function validConfig(value) {
284
+ if (!value || typeof value !== "object" || !("active" in value) || !("profiles" in value)) return false;
285
+ if (typeof value.active !== "string" || !value.profiles || typeof value.profiles !== "object" || Array.isArray(value.profiles)) return false;
286
+ return Object.values(value.profiles).every((profile) => profile && typeof profile === "object" && typeof profile.server === "string" && typeof profile.token === "string");
287
+ }
288
+ async function savePrivate(path, data) {
289
+ await mkdir(configDir, {
290
+ recursive: true,
291
+ mode: 448
292
+ });
293
+ const temp = `${path}.${randomUUID()}.tmp`;
294
+ try {
295
+ await writeFile(temp, `${JSON.stringify(data, null, 2)}\n`, {
296
+ mode: 384,
297
+ flag: "wx"
298
+ });
299
+ await rename(temp, path);
300
+ await chmod(path, 384);
301
+ } finally {
302
+ await unlink(temp).catch((error) => {
303
+ if (error.code !== "ENOENT") throw error;
304
+ });
305
+ }
306
+ }
307
+ async function writeConfig(config) {
308
+ await savePrivate(configPath, config);
309
+ }
310
+ async function profile(name, server) {
311
+ const config = await readConfig(), key = name ?? config.active, p = Object.hasOwn(config.profiles, key) ? config.profiles[key] : void 0;
312
+ const value = {
313
+ server: server ?? process.env.SHARE_PAGES_SERVER ?? p?.server,
314
+ token: process.env.SHARE_PAGES_TOKEN ?? p?.token
315
+ };
316
+ if (!value.server || !value.token) throw new AppError("LOGIN_REQUIRED", "请先登录,或设置 SHARE_PAGES_SERVER 和 SHARE_PAGES_TOKEN", 401);
317
+ return value;
318
+ }
319
+ function resumePath(id) {
320
+ if (!/^[a-z0-9-]+$/i.test(id)) throw new AppError("INVALID_UPLOAD_ID", "非法上传 ID");
321
+ return join(configDir, `upload-${id}.json`);
322
+ }
323
+ //#endregion
324
+ //#region src/input.ts
325
+ async function stdinBytes(limit = 100 * 1024 ** 2) {
326
+ const chunks = [];
327
+ let length = 0;
328
+ for await (const chunk of process.stdin) {
329
+ const b = Buffer.from(chunk);
330
+ length += b.length;
331
+ if (length > limit) throw new AppError("INPUT_TOO_LARGE", "stdin 内容超限", 413);
332
+ chunks.push(b);
333
+ }
334
+ return new Uint8Array(Buffer.concat(chunks));
335
+ }
336
+ async function secret(prompt, interactive = true) {
337
+ if (!interactive || !process.stdin.isTTY) throw new AppError("INPUT_REQUIRED", "非交互模式需要通过 stdin 文件或环境变量提供秘密");
338
+ process.stderr.write(prompt);
339
+ process.stdin.setRawMode(true);
340
+ process.stdin.resume();
341
+ return new Promise((resolve, reject) => {
342
+ let value = "";
343
+ const onData = (data) => {
344
+ for (const c of data.toString("utf8")) {
345
+ if (c === "\r" || c === "\n") {
346
+ cleanup();
347
+ resolve(value);
348
+ return;
349
+ }
350
+ if (c === "") {
351
+ cleanup();
352
+ reject(new AppError("CANCELLED", "操作取消"));
353
+ return;
354
+ }
355
+ if (c === "") value = value.slice(0, -1);
356
+ else if (c >= " ") value += c;
357
+ }
358
+ };
359
+ function cleanup() {
360
+ process.stdin.off("data", onData);
361
+ process.stdin.setRawMode(false);
362
+ process.stdin.pause();
363
+ process.stderr.write("\n");
364
+ }
365
+ process.stdin.on("data", onData);
366
+ });
367
+ }
368
+ async function confirm(message, yes, interactive) {
369
+ if (yes) return;
370
+ if (!interactive || !process.stdin.isTTY) throw new AppError("CONFIRMATION_REQUIRED", "自动化删除需要显式 --yes");
371
+ const rl = createInterface({
372
+ input: process.stdin,
373
+ output: process.stderr
374
+ });
375
+ try {
376
+ if (await rl.question(`${message} 输入 yes 继续:`) !== "yes") throw new AppError("CANCELLED", "操作取消");
377
+ } finally {
378
+ rl.close();
379
+ }
380
+ }
381
+ //#endregion
382
+ //#region src/shared.ts
383
+ function createCommand() {
384
+ return new Command().name("share-pages").description("个人静态内容发布工具").version("0.1.0").option("--json", "stdout 仅输出 JSON").option("--quiet", "隐藏进度").option("--no-interactive", "禁止交互输入").option("--profile <name>", "实例 profile").option("--server <url>", "覆盖实例地址").exitOverride();
385
+ }
386
+ function options(command) {
387
+ const chain = [];
388
+ for (let current = command; current; current = current.parent) chain.unshift(current);
389
+ const values = {};
390
+ for (const current of chain) for (const [key, value] of Object.entries(current.opts())) if (!(key in values) || current.getOptionValueSource(key) !== "default") values[key] = value;
391
+ return values;
392
+ }
393
+ function stringOptions(options, keys) {
394
+ const values = {};
395
+ for (const key of keys) {
396
+ const value = options[key];
397
+ if (typeof value === "string") values[key] = value;
398
+ }
399
+ return values;
400
+ }
401
+ function output(value, o) {
402
+ if (o.json) process.stdout.write(`${JSON.stringify(value)}\n`);
403
+ else if (typeof value === "object" && value && "url" in value) process.stdout.write(`${String(value.url)}\n`);
404
+ else process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
405
+ }
406
+ async function client(o) {
407
+ return new SharePagesClient(await profile(o.profile, o.server));
408
+ }
409
+ //#endregion
410
+ //#region src/accounts.ts
411
+ function registerAccounts(cli) {
412
+ cli.command("login").description("保存实例和发布 Token").option("--token-stdin", "从 stdin 读取 Token").action(async (_local, command) => {
413
+ const o = options(command), token = o.tokenStdin ? new TextDecoder().decode(await stdinBytes(1024)).trim() : process.env.SHARE_PAGES_TOKEN ?? await secret("发布 Token:", o.interactive);
414
+ if (!o.server) throw new AppError("SERVER_REQUIRED", "登录需要 --server <url>");
415
+ const sdk = new SharePagesClient({
416
+ server: o.server,
417
+ token
418
+ });
419
+ await sdk.request("/me");
420
+ const config = await readConfig();
421
+ const name = o.profile ?? "default";
422
+ config.profiles = {
423
+ ...config.profiles,
424
+ [name]: {
425
+ server: sdk.server,
426
+ token
427
+ }
428
+ };
429
+ config.active = name;
430
+ await writeConfig(config);
431
+ output({
432
+ profile: name,
433
+ server: sdk.server,
434
+ status: "logged-in"
435
+ }, o);
436
+ });
437
+ const profiles = cli.command("profiles");
438
+ profiles.command("list").action(async (_o, command) => {
439
+ const o = options(command), c = await readConfig();
440
+ output({
441
+ active: c.active,
442
+ profiles: Object.entries(c.profiles).map(([name, p]) => ({
443
+ name,
444
+ server: p.server
445
+ }))
446
+ }, o);
447
+ });
448
+ profiles.command("use <name>").action(async (name, _o, command) => {
449
+ const o = options(command), c = await readConfig();
450
+ if (!Object.hasOwn(c.profiles, name)) throw new AppError("PROFILE_NOT_FOUND", "实例配置不存在");
451
+ c.active = name;
452
+ await writeConfig(c);
453
+ output({ active: name }, o);
454
+ });
455
+ }
456
+ //#endregion
457
+ //#region src/pages.ts
458
+ function registerPages(cli) {
459
+ const pages = cli.command("pages");
460
+ pages.command("list").option("--state <state>").option("--q <text>").option("--format <format>").option("--access <mode>").option("--cursor <cursor>").action(async (_local, command) => {
461
+ const o = options(command);
462
+ output(await (await client(o)).listPages(stringOptions(o, [
463
+ "state",
464
+ "q",
465
+ "format",
466
+ "access",
467
+ "cursor"
468
+ ])), o);
469
+ });
470
+ pages.command("get <id>").action(async (id, _local, command) => {
471
+ const o = options(command);
472
+ output(await (await client(o)).getPage(id), o);
473
+ });
474
+ pages.command("edit <id>").option("--title <title>").option("--access <mode>").option("--indexable <boolean>").option("--password-file <path>").action(async (id, _local, command) => {
475
+ const o = options(command);
476
+ const patch = {};
477
+ if (o.title) patch.title = o.title;
478
+ if (o.access) patch.access = o.access;
479
+ if (o.indexable !== void 0) {
480
+ if (typeof o.indexable !== "string" || !["true", "false"].includes(o.indexable)) throw new AppError("VALIDATION_ERROR", "--indexable 使用 true/false");
481
+ patch.indexable = o.indexable === "true";
482
+ }
483
+ if (o.access === "password") patch.password = o.passwordFile ? (await readFile(o.passwordFile, "utf8")).trimEnd() : await secret("页面密码:", o.interactive);
484
+ output(await (await client(o)).patchPage(id, patch), o);
485
+ });
486
+ pages.command("delete <id>").option("--purge").option("--yes").action(async (id, _local, command) => {
487
+ const o = options(command), sdk = await client(o);
488
+ const page = await sdk.getPage(id);
489
+ await confirm(`${o.purge ? "永久删除" : "移入回收站"}页面 ${id}(${page.title})及全部版本。`, !!o.yes, !!o.interactive);
490
+ output(await sdk.deletePage(id, o.purge), o);
491
+ });
492
+ pages.command("restore <id>").option("--replace-with <version>").action(async (id, _local, command) => {
493
+ const o = options(command);
494
+ output(await (await client(o)).restorePage(id, o.replaceWith), o);
495
+ });
496
+ }
497
+ //#endregion
498
+ //#region src/publish.ts
499
+ function registerPublish(cli) {
500
+ cli.command("publish <input>").description("发布文件、ZIP 或目录;使用 - 从 stdin 读取内容").option("--id <id>").option("--update", "更新已有 ID").option("--type <format>").option("--title <title>").option("--entry <path>", "HTML 入口", "index.html").option("--spa", "启用 HTML 导航回退").option("--access <access>", "public / password / admin").option("--indexable", "允许公开页面收录").option("--encoding <encoding>", "文本编码", "utf-8").option("--delimiter <delimiter>", "CSV 分隔符").option("--no-header", "CSV 没有表头").option("--password-file <path>", "从权限受限文件读取页面密码").option("--dry-run", "仅检查转换和 manifest").option("--resume <uploadId>", "继续上传会话").option("--idempotency-key <key>").option("--no-wait", "提交后返回处理中状态").option("--timeout <seconds>", "等待秒数", "120").action(async (input, _local, command) => {
501
+ const o = options(command);
502
+ const { prepareNode, prepareBytes, ensureDOM } = await import("./node-Lh4CQjZ4.mjs");
503
+ if (o.type && !formats.includes(o.type)) throw new AppError("UNSUPPORTED_FORMAT", `--type 必须是 ${formats.join(", ")}`);
504
+ if (input === "-" && !o.type) throw new AppError("TYPE_REQUIRED", "stdin 需要显式 --type");
505
+ let sdk, limits = defaultLimits;
506
+ if (!o.dryRun) {
507
+ sdk = await client(o);
508
+ limits = (await sdk.capabilities()).limits;
509
+ }
510
+ const prepareOptions = {
511
+ type: o.type,
512
+ title: o.title,
513
+ entry: o.entry,
514
+ spa: o.spa,
515
+ encoding: o.encoding,
516
+ delimiter: o.delimiter,
517
+ header: o.header,
518
+ limits
519
+ };
520
+ let prepared;
521
+ if (input === "-") {
522
+ if (o.type === "latex") await ensureDOM();
523
+ prepared = await prepareBytes(`stdin.${o.type === "markdown" ? "md" : o.type === "latex" ? "tex" : o.type}`, await stdinBytes(limits.inputBytes), prepareOptions);
524
+ } else prepared = await prepareNode(input, prepareOptions);
525
+ if (o.dryRun) {
526
+ output({
527
+ status: "valid",
528
+ title: prepared.title,
529
+ manifest: prepared.manifest,
530
+ warnings: prepared.warnings
531
+ }, o);
532
+ return;
533
+ }
534
+ const manifestHash = await digest(JSON.stringify(prepared.manifest));
535
+ if (!sdk) throw new AppError("MISSING_CLIENT", "发布客户端尚未初始化");
536
+ if (o.resume) {
537
+ const saved = JSON.parse(await readFile(resumePath(o.resume), "utf8"));
538
+ if (saved.manifestHash !== manifestHash || saved.server !== sdk?.server) throw new AppError("RESUME_MISMATCH", "文件内容或实例与断点会话不一致");
539
+ }
540
+ const password = o.access === "password" ? o.passwordFile ? (await readFile(o.passwordFile, "utf8")).trimEnd() : await secret("页面密码(至少 8 位):", o.interactive) : void 0;
541
+ const result = await sdk.publish(prepared, {
542
+ id: o.id,
543
+ update: !!o.update,
544
+ title: o.title,
545
+ access: o.access,
546
+ password,
547
+ indexable: typeof o.indexable === "boolean" ? o.indexable : void 0,
548
+ idempotencyKey: o.idempotencyKey,
549
+ uploadId: o.resume,
550
+ wait: o.wait,
551
+ timeoutMs: Number(o.timeout) * 1e3,
552
+ onSession: async (s) => {
553
+ await savePrivate(resumePath(s.id), {
554
+ server: sdk?.server,
555
+ uploadId: s.id,
556
+ manifestHash
557
+ });
558
+ if (!o.quiet) process.stderr.write(`上传会话 ${s.id}\n`);
559
+ },
560
+ onProgress: (done, total) => {
561
+ if (!o.quiet && process.stderr.isTTY) process.stderr.write(`\r上传 ${total ? Math.floor(done / total * 100) : 100}%`);
562
+ }
563
+ });
564
+ if (!o.quiet && process.stderr.isTTY) process.stderr.write("\n");
565
+ output(result, o);
566
+ if (result.status !== "ready") process.exitCode = 6;
567
+ });
568
+ }
569
+ //#endregion
570
+ //#region src/stats.ts
571
+ function registerStats(cli) {
572
+ cli.command("stats [id]").option("--version <version>").option("--from <date>").option("--to <date>").option("--csv").action(async (id, _local, command) => {
573
+ const o = options(command), params = {};
574
+ if (id) params.page = id;
575
+ for (const k of [
576
+ "version",
577
+ "from",
578
+ "to"
579
+ ]) if (o[k]) params[k] = o[k];
580
+ if (o.csv) params.format = "csv";
581
+ const data = await (await client(o)).stats(params);
582
+ if (data instanceof Response) process.stdout.write(await data.text());
583
+ else output(data, o);
584
+ });
585
+ cli.command("uploads <id>").action(async (id, _local, command) => {
586
+ const o = options(command);
587
+ output(await (await client(o)).request(`/uploads/${encodeURIComponent(id)}`), o);
588
+ });
589
+ }
590
+ //#endregion
591
+ //#region src/transfer.ts
592
+ function registerTransfer(cli) {
593
+ cli.command("download <id>").option("--version <version>").option("-o, --output <file>").action(async (id, _local, command) => {
594
+ const o = options(command), sdk = await client(o);
595
+ const version = o.version ?? (await sdk.getPage(id)).current_version_id;
596
+ if (!version) throw new AppError("NO_CURRENT_VERSION", "页面没有当前版");
597
+ const response = await sdk.download(id, String(version));
598
+ const filename = o.output ?? `${id}-${version}.source`;
599
+ if (!response.body) throw new AppError("EMPTY_DOWNLOAD", "下载内容为空");
600
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(filename, {
601
+ flags: "wx",
602
+ mode: 384
603
+ }));
604
+ output({
605
+ file: filename,
606
+ versionId: version
607
+ }, o);
608
+ });
609
+ }
610
+ //#endregion
611
+ //#region src/versions.ts
612
+ function registerVersions(cli) {
613
+ const versions = cli.command("versions");
614
+ versions.command("list <id>").option("--cursor <cursor>").action(async (id, _local, command) => {
615
+ const o = options(command);
616
+ output(await (await client(o)).listVersions(id, o.cursor), o);
617
+ });
618
+ versions.command("delete <id> <version>").option("--replace-with <version>").option("--unpublish").option("--purge").option("--yes").action(async (id, version, _local, command) => {
619
+ const o = options(command), sdk = await client(o);
620
+ await sdk.getPage(id);
621
+ await confirm(`${o.purge ? "永久删除" : "移入回收站"} ${id} 的版本 ${version}。`, !!o.yes, !!o.interactive);
622
+ output(await sdk.deleteVersion(id, version, {
623
+ replaceWith: o.replaceWith,
624
+ unpublish: o.unpublish,
625
+ purge: o.purge
626
+ }), o);
627
+ });
628
+ versions.command("restore <id> <version>").action(async (id, version, _local, command) => {
629
+ const o = options(command);
630
+ output(await (await client(o)).restoreVersion(id, version), o);
631
+ });
632
+ cli.command("rollback <id>").requiredOption("--to <version>").option("--no-wait").action(async (id, _local, command) => {
633
+ const o = options(command), result = await (await client(o)).rollback(id, o.to ?? "", { wait: o.wait });
634
+ output(result, o);
635
+ if (result.status !== "ready") process.exitCode = 6;
636
+ });
637
+ }
638
+ //#endregion
639
+ //#region src/program.ts
640
+ function buildCLI() {
641
+ const cli = createCommand();
642
+ registerAccounts(cli);
643
+ registerPublish(cli);
644
+ registerPages(cli);
645
+ registerVersions(cli);
646
+ registerTransfer(cli);
647
+ registerStats(cli);
648
+ const configure = (command) => {
649
+ command.enablePositionalOptions();
650
+ for (const option of cli.options) {
651
+ if (option.long === "--version" || command.options.some((local) => local.long === option.long)) continue;
652
+ command.addOption(new Option(option.flags, option.description));
653
+ }
654
+ for (const child of command.commands) configure(child);
655
+ };
656
+ configure(cli);
657
+ return cli;
658
+ }
659
+ //#endregion
660
+ //#region src/index.ts
661
+ const cli = buildCLI();
662
+ try {
663
+ await cli.parseAsync();
664
+ } catch (error) {
665
+ if (error instanceof CommanderError && error.exitCode === 0) process.exitCode = 0;
666
+ else {
667
+ const e = error instanceof AppError ? error : new AppError("CLI_ERROR", error instanceof Error ? error.message : "命令执行失败");
668
+ const result = {
669
+ code: e.code,
670
+ message: e.message,
671
+ fields: e.fields
672
+ };
673
+ if (process.argv.includes("--json")) process.stdout.write(`${JSON.stringify({ error: result })}\n`);
674
+ else process.stderr.write(`${e.code}: ${e.message}\n`);
675
+ process.exitCode = e.status === 401 || e.status === 403 ? 3 : e.status === 409 ? 4 : e.status >= 500 ? 5 : 2;
676
+ }
677
+ }
678
+ //#endregion
679
+ export {};