share-pages 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  Share Pages 的命令行客户端,用于发布和管理 HTML、目录、Markdown、LaTeX、EPUB 与 CSV 内容。
4
4
 
5
+ 默认输出为适合人阅读的摘要;添加 `--json` 可获得稳定的单行 JSON。直接运行 `share-pages`,或在任意命令后添加 `--help`,即可看到按工作流组织的示例。
6
+
5
7
  ## 安装
6
8
 
7
9
  需要 Node.js 22 或更高版本。
8
10
 
9
11
  ```sh
10
12
  npm install --global share-pages
13
+ share-pages --version
14
+ share-pages
11
15
  ```
12
16
 
13
17
  ## 登录
@@ -16,6 +20,7 @@ npm install --global share-pages
16
20
 
17
21
  ```sh
18
22
  share-pages login --server https://pages.example.com
23
+ share-pages status
19
24
  ```
20
25
 
21
26
  自动化环境可通过 stdin 提供 Token:
@@ -35,15 +40,46 @@ share-pages publish ./README.md --id readme --access public
35
40
  share-pages publish ./report.csv --id report --title "Report"
36
41
  ```
37
42
 
43
+ 成功后 CLI 会给出页面地址、管理入口和下一次更新命令。更新已有页面必须显式添加 `--update`,避免误操作:
44
+
45
+ ```sh
46
+ share-pages publish ./README.md --id readme --update
47
+ ```
48
+
38
49
  发布前只做本地转换和校验:
39
50
 
40
51
  ```sh
41
52
  share-pages publish ./site --dry-run --json
42
53
  ```
43
54
 
55
+ ## 查询与恢复
56
+
57
+ ```sh
58
+ share-pages pages list
59
+ share-pages pages get readme
60
+ share-pages versions list readme
61
+ share-pages stats readme
62
+ share-pages download readme --output readme-original.md
63
+ ```
64
+
65
+ 网络中断后,使用首次上传时打印的会话 ID 继续:
66
+
67
+ ```sh
68
+ share-pages publish ./site --id docs --resume <upload-id>
69
+ share-pages uploads <upload-id>
70
+ ```
71
+
72
+ 删除本机凭据不会撤销远端 Token;敏感 Token 仍应在后台撤销:
73
+
74
+ ```sh
75
+ share-pages logout
76
+ ```
77
+
44
78
  查看全部命令:
45
79
 
46
80
  ```sh
47
81
  share-pages --help
48
82
  share-pages publish --help
49
83
  ```
84
+
85
+ 完整参数、约束和示例均可通过 `share-pages help <命令>` 离线查看。
@@ -5079,9 +5079,7 @@ const limitsSchema = object({
5079
5079
  zipRatio: number$1().positive().default(100),
5080
5080
  storageBytes: number$1().int().positive().default(8 * 1024 ** 3),
5081
5081
  uploadHours: number$1().positive().default(24),
5082
- trashDays: number$1().positive().default(7),
5083
- analyticsMonths: number$1().int().positive().default(24),
5084
- uvDays: number$1().int().min(1).default(3)
5082
+ trashDays: number$1().positive().default(7)
5085
5083
  });
5086
5084
  const defaultLimits = limitsSchema.parse({});
5087
5085
  function validateManifestLimits(manifest, limits) {
@@ -5238,11 +5236,7 @@ const settings = object({
5238
5236
  limits: limitsSchema,
5239
5237
  used_bytes: integer,
5240
5238
  reserved_bytes: integer,
5241
- maintenance: integer,
5242
- analytics_error: nullableText,
5243
- last_aggregate: integer.nullable(),
5244
- analytics_started_at: integer,
5245
- analytics_watermark: nullableText
5239
+ maintenance: integer
5246
5240
  });
5247
5241
  const jobSummary = object({
5248
5242
  id: text,
@@ -5388,27 +5382,43 @@ const metrics = object({
5388
5382
  errors: number,
5389
5383
  denied: number,
5390
5384
  cache_hits: number,
5391
- duration: number,
5392
- sampled: number
5385
+ duration: number
5393
5386
  });
5394
5387
  object({
5388
+ source: _enum(["analytics-engine", "local-memory"]),
5395
5389
  scope: string(),
5396
- timezone: string(),
5390
+ timezone: literal("UTC"),
5397
5391
  from: string(),
5398
5392
  to: string(),
5399
- days: array(metrics.partial().extend({
5393
+ queriedAt: number,
5394
+ cacheStatus: _enum([
5395
+ "live",
5396
+ "fresh-cache",
5397
+ "stale-cache"
5398
+ ]),
5399
+ days: array(metrics.extend({
5400
5400
  day: string(),
5401
- uv: number,
5402
- complete: boolean(),
5403
- state: string(),
5404
- ingestError: boolean(),
5405
- updatedAt: number.nullable()
5401
+ uv: number.nullable(),
5402
+ uvStatus: _enum([
5403
+ "exact",
5404
+ "sampled-estimate",
5405
+ "unavailable"
5406
+ ]),
5407
+ partial: boolean(),
5408
+ sampled: boolean()
5406
5409
  })),
5407
5410
  distributions: array(metrics.extend({
5408
- day: string(),
5409
- scope: string(),
5410
- dimension: string(),
5411
- value: string()
5411
+ dimension: _enum([
5412
+ "country",
5413
+ "device",
5414
+ "browser",
5415
+ "referer",
5416
+ "status",
5417
+ "event",
5418
+ "bot"
5419
+ ]),
5420
+ value: string(),
5421
+ sampled: boolean()
5412
5422
  })),
5413
5423
  popular: array(object({
5414
5424
  scope: string(),
@@ -5420,12 +5430,8 @@ object({
5420
5430
  id: string().optional(),
5421
5431
  number: number.optional()
5422
5432
  })),
5423
- visitorDays: number,
5424
- uvLabel: string(),
5425
- lastAggregate: number.nullable(),
5426
- watermark: string().nullable(),
5427
- mode: string(),
5428
- error: string().nullable()
5433
+ visitorDays: number.nullable(),
5434
+ retentionDays: literal(90)
5429
5435
  });
5430
5436
  async function digest(data) {
5431
5437
  const b = typeof data === "string" ? new TextEncoder().encode(data) : data;
@@ -5441,4 +5447,4 @@ function escapeHtml(value) {
5441
5447
  })[c] ?? c);
5442
5448
  }
5443
5449
  //#endregion
5444
- export { digest as a, manifestSchema as c, defaultLimits as i, safePath as l, CHUNK_SIZE as n, escapeHtml as o, assert as r, formats as s, AppError as t, validateManifestLimits as u };
5450
+ export { defaultLimits as a, formats as c, slugSchema as d, validateManifestLimits as f, assert as i, manifestSchema as l, CHUNK_SIZE as n, digest as o, accessSchema as r, escapeHtml as s, AppError as t, safePath as u };
@@ -1,6 +1,6 @@
1
1
  import { t as __commonJSMin } from "./rolldown-runtime-B4iAMlE-.mjs";
2
- import { l as safePath, r as assert } from "./dist-JyXFh6I5.mjs";
3
- import { c as unzipSafe, n as document, o as safeXMLText, r as encoder, s as sanitizeEPUB, t as decodeText } from "./prepare-C56DezIS-DiqwpdkG.mjs";
2
+ import { i as assert, u as safePath } from "./dist-LkxVliJx.mjs";
3
+ import { c as unzipSafe, n as document, o as safeXMLText, r as encoder, s as sanitizeEPUB, t as decodeText } from "./prepare-C56DezIS-pzIt9WD8.mjs";
4
4
  //#region ../../node_modules/.pnpm/@xmldom+xmldom@0.8.15/node_modules/@xmldom/xmldom/lib/conventions.js
5
5
  var require_conventions = /* @__PURE__ */ __commonJSMin(((exports) => {
6
6
  /**
package/dist/index.mjs CHANGED
@@ -1,14 +1,105 @@
1
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";
2
+ import { a as defaultLimits, c as formats, d as slugSchema, i as assert, n as CHUNK_SIZE, o as digest, r as accessSchema, t as AppError } from "./dist-LkxVliJx.mjs";
3
+ import { Command, CommanderError, Help, Option } from "commander";
4
4
  import { randomUUID } from "node:crypto";
5
- import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
5
+ import { chmod, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { createInterface } from "node:readline/promises";
9
+ import { StringDecoder } from "node:string_decoder";
9
10
  import { createWriteStream } from "node:fs";
10
11
  import { Readable } from "node:stream";
11
12
  import { pipeline } from "node:stream/promises";
13
+ //#region src/terminal.ts
14
+ const control = /[\x00-\x1f\x7f-\x9f]/g;
15
+ function terminalText(value, multiline = false) {
16
+ return String(value).replace(control, (character) => multiline && character === "\n" ? "\n" : " ");
17
+ }
18
+ //#endregion
19
+ //#region src/errors.ts
20
+ const hints = {
21
+ LOGIN_REQUIRED: "先运行 share-pages login --server https://你的实例域名",
22
+ SERVER_REQUIRED: "添加 --server https://你的实例域名,或为已有 profile 重新登录",
23
+ INVALID_SERVER: "--server 只填写 origin,例如 https://pages.example.com",
24
+ INSECURE_SERVER: "远程实例必须使用 HTTPS;只有 localhost 可以使用 HTTP",
25
+ TOKEN_REQUIRED: "在后台创建发布 Token,然后重新登录",
26
+ INVALID_PAGE_ID: "页面 ID 使用 3–40 位小写字母、数字或连字符,并避开系统保留名称",
27
+ INVALID_PROFILE: "例如使用 default、production 或 team_docs",
28
+ INVALID_UPLOAD_ID: "复制上传开始时 CLI 输出的完整会话 ID",
29
+ INVALID_DATE: "例如使用 2026-09-10",
30
+ INVALID_DATE_RANGE: "交换起止日期,确保 --from 不晚于 --to",
31
+ INVALID_LIMIT: "将 --limit 设置为 1–100 的整数",
32
+ INVALID_TIMEOUT: "将 --timeout 设置为 1–3600 的整数秒数",
33
+ ID_REQUIRED: "添加 --id <已有页面 ID>,或去掉 --update 创建新页面",
34
+ TYPE_REQUIRED: "stdin 没有文件名;添加 --type markdown、html、latex、epub、csv 或 zip",
35
+ INVALID_INDEX_POLICY: "改用 --access public,或移除 --indexable",
36
+ INPUT_REQUIRED: "自动化环境请使用 --token-stdin、--password-file 或对应环境变量",
37
+ CONFIRMATION_REQUIRED: "确认目标无误后添加 --yes;永久删除无法恢复",
38
+ ID_EXISTS: "若要为已有页面创建新版本,请保持相同 --id 并添加 --update",
39
+ PAGE_NOT_FOUND: "运行 share-pages pages list 查找可用页面 ID",
40
+ VERSION_NOT_FOUND: "运行 share-pages versions list <页面 ID> 查找版本 ID",
41
+ RESUME_MISMATCH: "使用创建该会话时相同的文件和实例,或去掉 --resume 开始新上传",
42
+ INVALID_RESUME_STATE: "去掉 --resume 开始新上传;损坏状态不会影响服务端已有内容",
43
+ NETWORK_ERROR: "检查网络和实例地址后重试;已有上传会话可用 --resume 继续",
44
+ REQUEST_TIMEOUT: "可增大 --timeout,或用 --no-wait 后运行 share-pages uploads <上传 ID>",
45
+ OUTPUT_EXISTS: "换一个 --output 路径;CLI 不会覆盖已有文件",
46
+ INPUT_NOT_FOUND: "检查输入路径、--password-file 或 --resume 的值是否正确",
47
+ PERMISSION_DENIED: "检查文件权限以及当前目录是否可读写",
48
+ CONFIG_ERROR: "修复或移走配置文件后重新登录;默认位置为 ~/.config/share-pages/config.json",
49
+ INSECURE_SECRET_FILE: "秘密文件只能由当前用户读写,建议设置为 chmod 600",
50
+ INVALID_PASSWORD: "换用 8–256 位页面密码后重试",
51
+ PASSWORD_ACCESS_CONFLICT: "使用 --access password,或移除 --password-file",
52
+ EMPTY_PATCH: "运行 share-pages pages edit --help 查看可修改字段",
53
+ OUTPUT_CONFLICT: "只选择一种 stdout 输出格式后重试"
54
+ };
55
+ function systemError(error) {
56
+ const path = error.path ? `:${error.path}` : "";
57
+ if (error.code === "EEXIST") return new AppError("OUTPUT_EXISTS", `目标文件已存在${path}`);
58
+ if (error.code === "ENOENT") return new AppError("INPUT_NOT_FOUND", `找不到文件或目录${path}`);
59
+ if (error.code === "EACCES" || error.code === "EPERM") return new AppError("PERMISSION_DENIED", `没有文件访问权限${path}`, 403);
60
+ }
61
+ function isHelpExit(error) {
62
+ return error instanceof CommanderError && (error.exitCode === 0 || ["commander.help", "commander.helpDisplayed"].includes(error.code));
63
+ }
64
+ function normalizeError(error) {
65
+ if (error instanceof CommanderError) return {
66
+ code: "CLI_USAGE",
67
+ message: error.message.replace(/^error:\s*/i, "").trim().replace(/^unknown command /i, "未知命令 ").replace(/^unknown option /i, "未知选项 ").replace(/^missing required argument /i, "缺少必填参数 ").replace(/^too many arguments\./i, "参数过多。").replace(/\(Did you mean (.+)\?\)/i, "你可能想输入 $1。"),
68
+ status: 400,
69
+ hint: "运行 share-pages --help,或 share-pages <命令> --help 查看示例"
70
+ };
71
+ const resolved = (error instanceof AppError ? error : systemError(error)) ?? new AppError("CLI_ERROR", error instanceof Error ? error.message : "命令执行失败;请重试或使用 --help 检查参数");
72
+ return {
73
+ code: resolved.code,
74
+ message: resolved.message,
75
+ status: resolved.status,
76
+ fields: resolved.fields,
77
+ requestId: resolved.requestId,
78
+ hint: hints[resolved.code]
79
+ };
80
+ }
81
+ function exitCode(error) {
82
+ if (error.status === 401 || error.status === 403) return 3;
83
+ if (error.status === 409) return 4;
84
+ if (error.status >= 500) return 5;
85
+ return 2;
86
+ }
87
+ function printError(error, json) {
88
+ if (json) {
89
+ const { status: _, ...result } = error;
90
+ process.stdout.write(`${JSON.stringify({ error: result })}\n`);
91
+ return;
92
+ }
93
+ process.stderr.write(`错误 [${terminalText(error.code)}]: ${terminalText(error.message, true)}\n`);
94
+ if (error.requestId) process.stderr.write(`请求 ID: ${terminalText(error.requestId)}\n`);
95
+ if (error.hint) process.stderr.write(`下一步: ${terminalText(error.hint)}\n`);
96
+ }
97
+ function wantsJSON(arguments_) {
98
+ const separator = arguments_.indexOf("--");
99
+ const options = separator === -1 ? arguments_ : arguments_.slice(0, separator);
100
+ return options.includes("--json") || options.includes("-j");
101
+ }
102
+ //#endregion
12
103
  //#region ../../packages/sdk/dist/index.js
13
104
  function checkAbort(signal) {
14
105
  if (signal?.aborted) throw new AppError("ABORTED", "操作已取消,上传会话仍可恢复", 499);
@@ -320,6 +411,34 @@ function resumePath(id) {
320
411
  if (!/^[a-z0-9-]+$/i.test(id)) throw new AppError("INVALID_UPLOAD_ID", "非法上传 ID");
321
412
  return join(configDir, `upload-${id}.json`);
322
413
  }
414
+ async function removeResume(id) {
415
+ await unlink(resumePath(id)).catch((error) => {
416
+ if (error.code !== "ENOENT") throw error;
417
+ });
418
+ }
419
+ //#endregion
420
+ //#region src/help.ts
421
+ const headings = /* @__PURE__ */ new Map([
422
+ ["Usage:", "用法:"],
423
+ ["Arguments:", "参数:"],
424
+ ["Options:", "选项:"],
425
+ ["Commands:", "命令:"],
426
+ ["Global Options:", "全局选项:"]
427
+ ]);
428
+ function configureHelp(command) {
429
+ command.helpOption("-h, --help", "显示命令帮助").helpCommand("help [command]", "显示指定命令的帮助").showSuggestionAfterError().showHelpAfterError(false).configureOutput({ writeErr: (value) => {
430
+ if (/^(用法|Usage):|^\n(?:示例|说明):/.test(value)) process.stdout.write(value);
431
+ } }).configureHelp({
432
+ helpWidth: Math.min(process.stdout.columns || 100, 120),
433
+ formatHelp(current, helper) {
434
+ return Help.prototype.formatHelp.call(helper, current, helper).replace(/^(Usage:|Arguments:|Options:|Commands:|Global Options:)/gm, (title) => headings.get(title) ?? title).replace(/\(choices: ([^)]+)\)/g, "(可选值: $1)").replace(/\(default: ([^)]+)\)/g, "(默认: $1)");
435
+ }
436
+ });
437
+ }
438
+ function examples(command, lines, notes = []) {
439
+ command.addHelpText("after", `\n示例:\n${lines.map((line) => ` ${line}`).join("\n")}${notes.length ? `\n\n说明:\n${notes.map((line) => ` ${line}`).join("\n")}` : ""}\n`);
440
+ return command;
441
+ }
323
442
  //#endregion
324
443
  //#region src/input.ts
325
444
  async function stdinBytes(limit = 100 * 1024 ** 2) {
@@ -340,31 +459,57 @@ async function secret(prompt, interactive = true) {
340
459
  process.stdin.resume();
341
460
  return new Promise((resolve, reject) => {
342
461
  let value = "";
462
+ const decoder = new StringDecoder("utf8");
343
463
  const onData = (data) => {
344
- for (const c of data.toString("utf8")) {
464
+ for (const c of decoder.write(data)) {
345
465
  if (c === "\r" || c === "\n") {
346
466
  cleanup();
347
467
  resolve(value);
348
468
  return;
349
469
  }
350
- if (c === "") {
470
+ if (c === "" || c === "") {
351
471
  cleanup();
352
472
  reject(new AppError("CANCELLED", "操作取消"));
353
473
  return;
354
474
  }
355
- if (c === "") value = value.slice(0, -1);
475
+ if (c === "\b" || c === "") value = value.slice(0, -1);
356
476
  else if (c >= " ") value += c;
357
477
  }
358
478
  };
479
+ const onEnd = () => {
480
+ cleanup();
481
+ reject(new AppError("CANCELLED", "输入已结束,操作取消"));
482
+ };
483
+ const onError = (error) => {
484
+ cleanup();
485
+ reject(error);
486
+ };
359
487
  function cleanup() {
360
488
  process.stdin.off("data", onData);
489
+ process.stdin.off("end", onEnd);
490
+ process.stdin.off("error", onError);
361
491
  process.stdin.setRawMode(false);
362
492
  process.stdin.pause();
363
493
  process.stderr.write("\n");
364
494
  }
365
495
  process.stdin.on("data", onData);
496
+ process.stdin.once("end", onEnd);
497
+ process.stdin.once("error", onError);
366
498
  });
367
499
  }
500
+ async function secretFile(path, label = "秘密文件") {
501
+ const file = await open(path, "r");
502
+ try {
503
+ const metadata = await file.stat();
504
+ if (!metadata.isFile()) throw new AppError("INVALID_SECRET_FILE", `${label}必须是普通文件`);
505
+ if (process.platform !== "win32" && (metadata.mode & 63) !== 0) throw new AppError("INSECURE_SECRET_FILE", `${label}权限过宽;请先运行 chmod 600 ${JSON.stringify(path)}`);
506
+ const value = (await file.readFile("utf8")).trimEnd();
507
+ if (!value) throw new AppError("EMPTY_SECRET_FILE", `${label}不能为空`);
508
+ return value;
509
+ } finally {
510
+ await file.close();
511
+ }
512
+ }
368
513
  async function confirm(message, yes, interactive) {
369
514
  if (yes) return;
370
515
  if (!interactive || !process.stdin.isTTY) throw new AppError("CONFIRMATION_REQUIRED", "自动化删除需要显式 --yes");
@@ -380,11 +525,11 @@ async function confirm(message, yes, interactive) {
380
525
  }
381
526
  //#endregion
382
527
  //#region package.json
383
- var version = "0.1.1";
528
+ var version = "0.3.0";
384
529
  //#endregion
385
530
  //#region src/shared.ts
386
531
  function createCommand() {
387
- return new Command().name("share-pages").description("个人静态内容发布工具").version(version).option("--json", "stdout 仅输出 JSON").option("--quiet", "隐藏进度").option("--no-interactive", "禁止交互输入").option("--profile <name>", "实例 profile").option("--server <url>", "覆盖实例地址").exitOverride();
532
+ return new Command().name("share-pages").description("将 HTML、站点目录和文档发布到你自己的 Share Pages 实例").version(version, "-V, --version", "显示 CLI 版本").addOption(new Option("-j, --json", "以单行 JSON 输出结果和错误(适合脚本)")).addOption(new Option("-q, --quiet", "隐藏进度和操作提示")).addOption(new Option("--no-interactive", "禁止密码和删除确认等交互输入")).addOption(new Option("-p, --profile <name>", "使用指定的实例配置")).addOption(new Option("--server <url>", "临时覆盖实例地址")).exitOverride();
388
533
  }
389
534
  function options(command) {
390
535
  const chain = [];
@@ -401,27 +546,75 @@ function stringOptions(options, keys) {
401
546
  }
402
547
  return values;
403
548
  }
404
- function output(value, o) {
549
+ function output(value, o, human) {
405
550
  if (o.json) process.stdout.write(`${JSON.stringify(value)}\n`);
406
- else if (typeof value === "object" && value && "url" in value) process.stdout.write(`${String(value.url)}\n`);
551
+ else if (human) process.stdout.write(`${terminalText(human, true).trimEnd()}\n`);
552
+ else if (typeof value === "object" && value && "url" in value) process.stdout.write(`${terminalText(value.url)}\n`);
407
553
  else process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
408
554
  }
555
+ function note(message, o) {
556
+ if (!o.quiet) process.stderr.write(`${terminalText(message).trimEnd()}\n`);
557
+ }
409
558
  async function client(o) {
410
559
  return new SharePagesClient(await profile(o.profile, o.server));
411
560
  }
412
561
  //#endregion
562
+ //#region src/validation.ts
563
+ const choices = {
564
+ access: accessSchema.options,
565
+ format: formats,
566
+ pageState: [
567
+ "active",
568
+ "trash",
569
+ "purging",
570
+ "purged",
571
+ "all"
572
+ ]
573
+ };
574
+ function pageId(value) {
575
+ const parsed = slugSchema.safeParse(value);
576
+ if (!parsed.success) throw new AppError("INVALID_PAGE_ID", parsed.error.issues[0]?.message ?? "页面 ID 无效");
577
+ return parsed.data;
578
+ }
579
+ function profileName(value) {
580
+ if (!/^[a-z0-9][a-z0-9._-]{0,39}$/i.test(value)) throw new AppError("INVALID_PROFILE", "Profile 名称需为 1–40 位字母、数字、点、下划线或连字符");
581
+ return value;
582
+ }
583
+ function uploadId(value) {
584
+ if (!/^[a-z0-9-]{1,80}$/i.test(value)) throw new AppError("INVALID_UPLOAD_ID", "上传 ID 只能包含字母、数字和连字符");
585
+ return value;
586
+ }
587
+ function positiveSeconds(value) {
588
+ if (!/^\d+$/.test(value)) throw new AppError("INVALID_TIMEOUT", "--timeout 必须是 1–3600 之间的整数秒数");
589
+ const seconds = Number(value);
590
+ if (seconds < 1 || seconds > 3600) throw new AppError("INVALID_TIMEOUT", "--timeout 必须是 1–3600 之间的整数秒数");
591
+ return seconds;
592
+ }
593
+ function pageLimit(value) {
594
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 100) throw new AppError("INVALID_LIMIT", "--limit 必须是 1–100 之间的整数");
595
+ return Number(value);
596
+ }
597
+ function isoDate(value) {
598
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
599
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) throw new AppError("INVALID_DATE", "日期必须使用 YYYY-MM-DD 格式");
600
+ return value;
601
+ }
602
+ function requirePatch(patch) {
603
+ if (!Object.keys(patch).length) throw new AppError("EMPTY_PATCH", "没有要修改的内容;请指定 --title、--access、--indexable 或 --password-file");
604
+ }
605
+ //#endregion
413
606
  //#region src/accounts.ts
414
607
  function registerAccounts(cli) {
415
- cli.command("login").description("保存实例和发布 Token").option("--token-stdin", "从 stdin 读取 Token").action(async (_local, command) => {
416
- const o = options(command), token = o.tokenStdin ? new TextDecoder().decode(await stdinBytes(1024)).trim() : process.env.SHARE_PAGES_TOKEN ?? await secret("发布 Token:", o.interactive);
417
- if (!o.server) throw new AppError("SERVER_REQUIRED", "登录需要 --server <url>");
608
+ examples(cli.command("login").description("验证并安全保存实例地址和发布 Token").helpGroup("开始使用").option("--token-stdin", "从 stdin 读取 Token(推荐用于自动化)").action(async (_local, command) => {
609
+ const o = options(command), config = await readConfig(), name = profileName(o.profile ?? config.active ?? "default"), existing = Object.hasOwn(config.profiles, name) ? config.profiles[name] : void 0, server = o.server ?? process.env.SHARE_PAGES_SERVER ?? existing?.server, token = o.tokenStdin ? new TextDecoder().decode(await stdinBytes(1024)).trim() : process.env.SHARE_PAGES_TOKEN ?? await secret("发布 Token:", o.interactive);
610
+ if (!server) throw new AppError("SERVER_REQUIRED", "首次登录需要 --server <url>");
611
+ if (!token) throw new AppError("TOKEN_REQUIRED", "发布 Token 不能为空", 401);
418
612
  const sdk = new SharePagesClient({
419
- server: o.server,
613
+ server,
420
614
  token
421
615
  });
616
+ note(`正在验证 ${terminalText(sdk.server)}…`, o);
422
617
  await sdk.request("/me");
423
- const config = await readConfig();
424
- const name = o.profile ?? "default";
425
618
  config.profiles = {
426
619
  ...config.profiles,
427
620
  [name]: {
@@ -435,81 +628,226 @@ function registerAccounts(cli) {
435
628
  profile: name,
436
629
  server: sdk.server,
437
630
  status: "logged-in"
438
- }, o);
439
- });
440
- const profiles = cli.command("profiles");
441
- profiles.command("list").action(async (_o, command) => {
631
+ }, o, `已登录并启用 profile “${name}”\n实例: ${sdk.server}`);
632
+ note("下一步: share-pages publish <文件或目录> --id <页面 ID>", o);
633
+ }), [
634
+ "share-pages login --server https://pages.example.com",
635
+ "printf '%s' \"$SHARE_PAGES_TOKEN\" | share-pages login --server \"$SHARE_PAGES_SERVER\" --token-stdin --no-interactive",
636
+ "share-pages login --profile production # 更新已有 profile 的 Token"
637
+ ]);
638
+ const profiles = cli.command("profiles").description("查看和切换本机保存的实例配置(永不显示 Token)").helpGroup("开始使用");
639
+ examples(profiles, [
640
+ "share-pages profiles list",
641
+ "share-pages profiles use production",
642
+ "share-pages status --profile production"
643
+ ]);
644
+ examples(profiles.command("list").description("列出 profile 和当前实例").action(async (_o, command) => {
442
645
  const o = options(command), c = await readConfig();
646
+ const items = Object.entries(c.profiles).map(([name, p]) => ({
647
+ name,
648
+ server: p.server
649
+ }));
443
650
  output({
444
651
  active: c.active,
445
- profiles: Object.entries(c.profiles).map(([name, p]) => ({
446
- name,
447
- server: p.server
448
- }))
449
- }, o);
450
- });
451
- profiles.command("use <name>").action(async (name, _o, command) => {
652
+ profiles: items
653
+ }, o, items.length ? items.map((item) => `${item.name === c.active ? "*" : " "} ${terminalText(item.name)} ${terminalText(item.server)}`).join("\n") : "尚未保存 profile。");
654
+ if (!items.length) note("下一步: share-pages login --server https://你的实例域名", o);
655
+ }), ["share-pages profiles list", "share-pages profiles list --json"]);
656
+ examples(profiles.command("use <name>").description("切换默认 profile").action(async (name, _o, command) => {
452
657
  const o = options(command), c = await readConfig();
453
- if (!Object.hasOwn(c.profiles, name)) throw new AppError("PROFILE_NOT_FOUND", "实例配置不存在");
454
- c.active = name;
658
+ const key = profileName(name);
659
+ if (!Object.hasOwn(c.profiles, key)) throw new AppError("PROFILE_NOT_FOUND", "实例配置不存在");
660
+ c.active = key;
455
661
  await writeConfig(c);
456
- output({ active: name }, o);
662
+ output({ active: key }, o, `已启用 profile “${key}”`);
663
+ }), ["share-pages profiles use production"]);
664
+ examples(cli.command("logout [name]").description("删除本机保存的一个 profile(不会撤销远端 Token)").helpGroup("开始使用").action(async (name, _local, command) => {
665
+ const o = options(command), config = await readConfig(), key = profileName(name ?? o.profile ?? config.active);
666
+ if (!Object.hasOwn(config.profiles, key)) throw new AppError("PROFILE_NOT_FOUND", "实例配置不存在");
667
+ const { [key]: _, ...remaining } = config.profiles;
668
+ config.profiles = remaining;
669
+ if (config.active === key) config.active = Object.keys(remaining)[0] ?? "default";
670
+ await writeConfig(config);
671
+ output({
672
+ profile: key,
673
+ status: "logged-out",
674
+ active: config.active
675
+ }, o, `已删除 profile “${key}”`);
676
+ note("安全提示: 如需让 Token 立即失效,请同时在后台撤销它。", o);
677
+ }), ["share-pages logout", "share-pages logout production"]);
678
+ examples(cli.command("status").description("检查当前 profile、网络、认证和服务端能力").helpGroup("开始使用").action(async (_local, command) => {
679
+ const o = options(command), config = await readConfig(), name = o.profile ?? config.active, sdk = await client(o);
680
+ note(`正在检查 ${terminalText(sdk.server)}…`, o);
681
+ const [actor, capabilities] = await Promise.all([sdk.request("/me"), sdk.capabilities()]);
682
+ const result = {
683
+ status: "ok",
684
+ profile: Object.hasOwn(config.profiles, name) ? name : null,
685
+ server: sdk.server,
686
+ actor: actor.kind,
687
+ scopes: actor.scopes,
688
+ pageIds: actor.pageIds,
689
+ prefixes: actor.prefixes,
690
+ formats: capabilities.formats,
691
+ apiVersion: capabilities.apiVersion,
692
+ contractVersion: capabilities.contractVersion
693
+ };
694
+ output(result, o, [
695
+ "连接与认证正常",
696
+ `Profile: ${terminalText(result.profile ?? "环境变量 / 临时参数")}`,
697
+ `实例: ${terminalText(result.server)}`,
698
+ `身份: ${terminalText(result.actor)}`,
699
+ `权限: ${terminalText(result.scopes.join(", ") || "无")}`,
700
+ `页面范围: ${terminalText(result.pageIds.length || result.prefixes.length ? [...result.pageIds, ...result.prefixes.map((prefix) => `${prefix}*`)].join(", ") : "全部(仍受 scope 限制)")}`,
701
+ `格式: ${terminalText(result.formats.join(", "))}`,
702
+ `API: v${terminalText(result.apiVersion)} · Contract ${terminalText(result.contractVersion)}`
703
+ ].join("\n"));
704
+ }), ["share-pages status", "share-pages status --profile production --json"]);
705
+ }
706
+ //#endregion
707
+ //#region src/presentation.ts
708
+ const date = (value) => {
709
+ if (!value || !Number.isFinite(value)) return "—";
710
+ try {
711
+ return new Date(value).toISOString();
712
+ } catch {
713
+ return "—";
714
+ }
715
+ };
716
+ const bytes = (value) => {
717
+ if (!Number.isFinite(value) || value < 0) return "—";
718
+ if (value < 1024) return `${value} B`;
719
+ if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KiB`;
720
+ return `${(value / 1024 ** 2).toFixed(1)} MiB`;
721
+ };
722
+ function pagesList(result) {
723
+ if (!result.items.length) return "没有符合条件的页面。";
724
+ const rows = result.items.map((page) => `${terminalText(page.slug)} ${terminalText(page.title)}\n ${terminalText(page.format)} · ${terminalText(page.access)} · ${terminalText(page.state)} · ${date(page.updated_at)}`);
725
+ if (result.nextCursor) rows.push(`\n下一页: --cursor ${terminalText(result.nextCursor)}`);
726
+ return rows.join("\n");
727
+ }
728
+ function pageDetail(page) {
729
+ return [
730
+ `${terminalText(page.slug)} ${terminalText(page.title)}`,
731
+ `格式: ${terminalText(page.format)}`,
732
+ `访问: ${terminalText(page.access)}`,
733
+ `状态: ${terminalText(page.state)}`,
734
+ `收录: ${page.indexable ? "允许" : "禁止"}`,
735
+ `当前版本: ${terminalText(page.current_version_id ?? "尚未发布")}`,
736
+ `更新时间: ${date(page.updated_at)}`
737
+ ].join("\n");
738
+ }
739
+ function versionsList(result) {
740
+ if (!result.items.length) return "该页面还没有版本。";
741
+ const rows = result.items.map((version) => `v${version.number} ${terminalText(version.id)}\n ${terminalText(version.state)} · ${bytes(version.bytes)} · ${date(version.created_at)}`);
742
+ if (result.nextCursor) rows.push(`\n下一页: --cursor ${terminalText(result.nextCursor)}`);
743
+ return rows.join("\n");
744
+ }
745
+ function statisticsSummary(result) {
746
+ const total = result.days.reduce((sum, day) => ({
747
+ pv: sum.pv + day.pv,
748
+ downloads: sum.downloads + day.downloads,
749
+ bytes: sum.bytes + day.bytes,
750
+ errors: sum.errors + day.errors
751
+ }), {
752
+ pv: 0,
753
+ downloads: 0,
754
+ bytes: 0,
755
+ errors: 0
756
+ });
757
+ const recent = result.days.slice(-7).map((day) => {
758
+ const uv = day.uv === null ? "不可用" : `${day.uv}${day.uvStatus === "exact" ? "" : "(估算)"}`;
759
+ return ` ${terminalText(day.day)} PV ${day.pv} · UV ${uv} · 下载 ${day.downloads}`;
457
760
  });
761
+ const popular = result.popular.slice(0, 5).map((item) => ` ${terminalText(item.slug ?? item.id ?? item.scope)} PV ${item.pv} · 下载 ${item.downloads}`);
762
+ return [
763
+ `统计范围: ${terminalText(result.scope)}`,
764
+ `UTC 区间: ${terminalText(result.from)} 至 ${terminalText(result.to)}`,
765
+ `来源: ${terminalText(result.source)} · ${terminalText(result.cacheStatus)}`,
766
+ `汇总: PV ${total.pv} · 下载 ${total.downloads} · 流量 ${bytes(total.bytes)} · 错误 ${total.errors}`,
767
+ `访客日: ${result.visitorDays ?? "不可用"}`,
768
+ recent.length ? `最近 ${recent.length} 天:\n${recent.join("\n")}` : "该区间没有每日数据。",
769
+ popular.length ? `热门内容:\n${popular.join("\n")}` : void 0,
770
+ result.days.length > recent.length ? "完整逐日和分布数据请使用 --json 或 --csv。" : void 0
771
+ ].filter(Boolean).join("\n");
458
772
  }
459
773
  //#endregion
460
774
  //#region src/pages.ts
461
775
  function registerPages(cli) {
462
- const pages = cli.command("pages");
463
- pages.command("list").option("--state <state>").option("--q <text>").option("--format <format>").option("--access <mode>").option("--cursor <cursor>").action(async (_local, command) => {
776
+ const pages = cli.command("pages").description("查询、修改、删除和恢复页面").helpGroup("页面管理");
777
+ examples(pages, [
778
+ "share-pages pages list",
779
+ "share-pages pages get handbook",
780
+ "share-pages pages edit handbook --title \"团队手册\""
781
+ ]);
782
+ examples(pages.command("list").description("列出页面;默认显示 active 页面").addOption(new Option("--state <state>", "按页面状态筛选").choices(choices.pageState)).option("--q <text>", "按页面 ID 或标题搜索").addOption(new Option("--format <format>", "按内容格式筛选").choices(choices.format)).addOption(new Option("--access <mode>", "按访问方式筛选").choices(choices.access)).option("--limit <count>", "每页返回 1–100 条", pageLimit, 30).option("--cursor <cursor>", "继续读取服务端返回的下一页").action(async (_local, command) => {
464
783
  const o = options(command);
465
- output(await (await client(o)).listPages(stringOptions(o, [
784
+ const params = stringOptions(o, [
466
785
  "state",
467
786
  "q",
468
787
  "format",
469
788
  "access",
470
789
  "cursor"
471
- ])), o);
472
- });
473
- pages.command("get <id>").action(async (id, _local, command) => {
790
+ ]);
791
+ if (o.limit) params.limit = String(o.limit);
792
+ const result = await (await client(o)).listPages(params);
793
+ output(result, o, pagesList(result));
794
+ if (!result.items.length) note("提示: 使用 --state all 查看包括回收站在内的所有页面。", o);
795
+ }), [
796
+ "share-pages pages list",
797
+ "share-pages pages list --q handbook --format markdown",
798
+ "share-pages pages list --state trash --json"
799
+ ]);
800
+ examples(pages.command("get").description("查看一个页面的元数据和当前版本").argument("<id>", "页面 ID", pageId).action(async (id, _local, command) => {
474
801
  const o = options(command);
475
- output(await (await client(o)).getPage(id), o);
476
- });
477
- pages.command("edit <id>").option("--title <title>").option("--access <mode>").option("--indexable <boolean>").option("--password-file <path>").action(async (id, _local, command) => {
802
+ const result = await (await client(o)).getPage(id);
803
+ output(result, o, pageDetail(result));
804
+ }), ["share-pages pages get handbook", "share-pages pages get handbook --json"]);
805
+ examples(pages.command("edit").description("修改页面标题、访问方式或收录策略").argument("<id>", "页面 ID", pageId).option("--title <title>", "设置新标题").addOption(new Option("--access <mode>", "设置访问方式").choices(choices.access)).addOption(new Option("--indexable <boolean>", "是否允许搜索引擎收录").choices(["true", "false"])).addOption(new Option("--password-file <path>", "从权限为 600 的文件读取新密码").implies({ access: "password" })).action(async (id, _local, command) => {
478
806
  const o = options(command);
479
807
  const patch = {};
480
- if (o.title) patch.title = o.title;
808
+ if (o.passwordFile && o.access !== "password") throw new AppError("PASSWORD_ACCESS_CONFLICT", "--password-file 只能用于 password 访问方式");
809
+ if (o.title !== void 0) patch.title = o.title;
481
810
  if (o.access) patch.access = o.access;
482
- if (o.indexable !== void 0) {
483
- if (typeof o.indexable !== "string" || !["true", "false"].includes(o.indexable)) throw new AppError("VALIDATION_ERROR", "--indexable 使用 true/false");
484
- patch.indexable = o.indexable === "true";
485
- }
486
- if (o.access === "password") patch.password = o.passwordFile ? (await readFile(o.passwordFile, "utf8")).trimEnd() : await secret("页面密码:", o.interactive);
487
- output(await (await client(o)).patchPage(id, patch), o);
488
- });
489
- pages.command("delete <id>").option("--purge").option("--yes").action(async (id, _local, command) => {
811
+ if (o.indexable !== void 0) patch.indexable = o.indexable === "true";
812
+ if (o.access === "password") patch.password = o.passwordFile ? await secretFile(o.passwordFile, "页面密码文件") : await secret("页面密码(至少 8 位):", o.interactive);
813
+ if (typeof patch.password === "string" && (patch.password.length < 8 || patch.password.length > 256)) throw new AppError("INVALID_PASSWORD", "页面密码长度必须为 8–256 位");
814
+ requirePatch(patch);
815
+ const result = await (await client(o)).patchPage(id, patch);
816
+ output(result, o, pageDetail(result));
817
+ note(`已更新页面 ${id}`, o);
818
+ }), [
819
+ "share-pages pages edit handbook --title \"团队手册\"",
820
+ "share-pages pages edit handbook --access admin --indexable false",
821
+ "share-pages pages edit handbook --password-file ./private/password.txt"
822
+ ]);
823
+ examples(pages.command("delete").description("将页面移入回收站,或永久删除").argument("<id>", "页面 ID", pageId).option("--purge", "永久删除页面和全部版本(不可恢复)").option("--yes", "跳过交互确认(自动化环境必需)").action(async (id, _local, command) => {
490
824
  const o = options(command), sdk = await client(o);
491
825
  const page = await sdk.getPage(id);
492
- await confirm(`${o.purge ? "永久删除" : "移入回收站"}页面 ${id}(${page.title})及全部版本。`, !!o.yes, !!o.interactive);
493
- output(await sdk.deletePage(id, o.purge), o);
494
- });
495
- pages.command("restore <id>").option("--replace-with <version>").action(async (id, _local, command) => {
826
+ await confirm(`${o.purge ? "永久删除" : "移入回收站"}页面 ${id}(${terminalText(page.title)})及全部版本。`, !!o.yes, !!o.interactive);
827
+ output(await sdk.deletePage(id, o.purge), o, o.purge ? `页面 ${id} 已进入永久清理队列。` : `页面 ${id} 已移入回收站。`);
828
+ }), ["share-pages pages delete handbook", "share-pages pages delete handbook --purge --yes --no-interactive"]);
829
+ examples(pages.command("restore").description("从回收站恢复页面").argument("<id>", "页面 ID", pageId).option("--replace-with <version>", "当前版本不可用时指定接替版本 ID").action(async (id, _local, command) => {
496
830
  const o = options(command);
497
- output(await (await client(o)).restorePage(id, o.replaceWith), o);
498
- });
831
+ output(await (await client(o)).restorePage(id, o.replaceWith), o, `页面 ${id} 已恢复。`);
832
+ }), ["share-pages pages restore handbook", "share-pages pages restore handbook --replace-with <版本 UUID>"]);
499
833
  }
500
834
  //#endregion
501
835
  //#region src/publish.ts
502
836
  function registerPublish(cli) {
503
- 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) => {
837
+ examples(cli.command("publish").description("检查、转换并发布文件、ZIP 或目录").helpGroup("发布与传输").argument("<input>", "文件、ZIP、目录,或 -(从 stdin 读取)").option("--id <id>", "设置 3–40 位稳定页面 ID", pageId).option("--update", "更新已有 ID").addOption(new Option("--type <format>", "覆盖自动识别的内容格式").choices(choices.format)).option("--title <title>", "覆盖从内容推导的页面标题").option("--entry <path>", "HTML/站点入口文件", "index.html").option("--spa", "找不到站点路径时回退到入口文件").addOption(new Option("--access <mode>", "访问方式(新页面默认 public)").choices(choices.access)).option("--indexable", "允许公开页面被搜索引擎收录").option("--encoding <encoding>", "Markdown/LaTeX/CSV 文本编码", "utf-8").option("--delimiter <delimiter>", "覆盖 CSV 分隔符自动识别").option("--no-header", "将 CSV 第一行视为数据而非表头").addOption(new Option("--password-file <path>", "从权限为 600 的文件读取页面密码").implies({ access: "password" })).addOption(new Option("--dry-run", "仅在本地转换和校验,不访问服务器").conflicts("resume")).addOption(new Option("--resume <upload-id>", "继续先前中断的上传").conflicts("dryRun")).option("--idempotency-key <key>", "为自动化重试提供稳定幂等键").option("--no-wait", "提交后返回处理中状态").option("--timeout <seconds>", "等待发布完成的秒数(1–3600)", positiveSeconds, 120).action(async (input, _local, command) => {
504
838
  const o = options(command);
505
- const { prepareNode, prepareBytes, ensureDOM } = await import("./node-Lh4CQjZ4.mjs");
506
- if (o.type && !formats.includes(o.type)) throw new AppError("UNSUPPORTED_FORMAT", `--type 必须是 ${formats.join(", ")}`);
839
+ const { prepareNode, prepareBytes, ensureDOM } = await import("./node-DVMkHtVp.mjs");
840
+ if (o.update && !o.id) throw new AppError("ID_REQUIRED", "--update 必须同时指定 --id");
841
+ if (o.passwordFile && o.access !== "password") throw new AppError("PASSWORD_ACCESS_CONFLICT", "--password-file 只能用于 password 访问方式");
842
+ if (o.indexable && o.access && o.access !== "public") throw new AppError("INVALID_INDEX_POLICY", "--indexable 只适用于 public 页面");
507
843
  if (input === "-" && !o.type) throw new AppError("TYPE_REQUIRED", "stdin 需要显式 --type");
508
844
  let sdk, limits = defaultLimits;
509
845
  if (!o.dryRun) {
510
846
  sdk = await client(o);
847
+ note(`正在检查实例限制: ${terminalText(sdk.server)}…`, o);
511
848
  limits = (await sdk.capabilities()).limits;
512
849
  }
850
+ note(o.dryRun ? "正在本地检查并转换内容…" : "正在检查并转换内容…", o);
513
851
  const prepareOptions = {
514
852
  type: o.type,
515
853
  title: o.title,
@@ -526,21 +864,38 @@ function registerPublish(cli) {
526
864
  prepared = await prepareBytes(`stdin.${o.type === "markdown" ? "md" : o.type === "latex" ? "tex" : o.type}`, await stdinBytes(limits.inputBytes), prepareOptions);
527
865
  } else prepared = await prepareNode(input, prepareOptions);
528
866
  if (o.dryRun) {
867
+ const totalBytes = prepared.manifest.files.reduce((sum, file) => sum + file.size, 0);
529
868
  output({
530
869
  status: "valid",
531
870
  title: prepared.title,
532
871
  manifest: prepared.manifest,
533
872
  warnings: prepared.warnings
534
- }, o);
873
+ }, o, [
874
+ "本地检查通过(未访问服务器)",
875
+ `标题: ${terminalText(prepared.title)}`,
876
+ `格式: ${prepared.manifest.format}`,
877
+ `文件: ${prepared.manifest.files.length}`,
878
+ `总大小: ${totalBytes} bytes`,
879
+ `警告: ${prepared.warnings.length ? prepared.warnings.join(";") : "无"}`
880
+ ].join("\n"));
535
881
  return;
536
882
  }
537
883
  const manifestHash = await digest(JSON.stringify(prepared.manifest));
538
884
  if (!sdk) throw new AppError("MISSING_CLIENT", "发布客户端尚未初始化");
539
885
  if (o.resume) {
540
- const saved = JSON.parse(await readFile(resumePath(o.resume), "utf8"));
541
- if (saved.manifestHash !== manifestHash || saved.server !== sdk?.server) throw new AppError("RESUME_MISMATCH", "文件内容或实例与断点会话不一致");
886
+ let saved;
887
+ try {
888
+ saved = JSON.parse(await readFile(resumePath(o.resume), "utf8"));
889
+ } catch (error) {
890
+ if (error.code) throw error;
891
+ throw new AppError("INVALID_RESUME_STATE", "续传状态文件已损坏,请开始新的上传");
892
+ }
893
+ if (!saved || typeof saved !== "object" || !("manifestHash" in saved) || !("server" in saved) || saved.manifestHash !== manifestHash || saved.server !== sdk.server) throw new AppError("RESUME_MISMATCH", "文件内容或实例与断点会话不一致");
542
894
  }
543
- const password = o.access === "password" ? o.passwordFile ? (await readFile(o.passwordFile, "utf8")).trimEnd() : await secret("页面密码(至少 8 位):", o.interactive) : void 0;
895
+ const password = o.access === "password" ? o.passwordFile ? await secretFile(o.passwordFile, "页面密码文件") : await secret("页面密码(至少 8 位):", o.interactive) : void 0;
896
+ if (password && (password.length < 8 || password.length > 256)) throw new AppError("INVALID_PASSWORD", "页面密码长度必须为 8–256 位");
897
+ let sessionId = o.resume;
898
+ note(o.resume ? `正在恢复上传 ${o.resume}…` : "正在创建上传会话…", o);
544
899
  const result = await sdk.publish(prepared, {
545
900
  id: o.id,
546
901
  update: !!o.update,
@@ -551,29 +906,47 @@ function registerPublish(cli) {
551
906
  idempotencyKey: o.idempotencyKey,
552
907
  uploadId: o.resume,
553
908
  wait: o.wait,
554
- timeoutMs: Number(o.timeout) * 1e3,
909
+ timeoutMs: (o.timeout ?? 120) * 1e3,
555
910
  onSession: async (s) => {
911
+ sessionId = s.id;
556
912
  await savePrivate(resumePath(s.id), {
557
913
  server: sdk?.server,
558
914
  uploadId: s.id,
559
915
  manifestHash
560
916
  });
561
- if (!o.quiet) process.stderr.write(`上传会话 ${s.id}\n`);
917
+ note(`上传会话: ${s.id}(网络中断后可使用 --resume ${s.id})`, o);
562
918
  },
563
919
  onProgress: (done, total) => {
564
920
  if (!o.quiet && process.stderr.isTTY) process.stderr.write(`\r上传 ${total ? Math.floor(done / total * 100) : 100}%`);
565
921
  }
566
922
  });
923
+ if (result.status === "ready" && sessionId) await removeResume(sessionId);
567
924
  if (!o.quiet && process.stderr.isTTY) process.stderr.write("\n");
568
- output(result, o);
925
+ output(result, o, result.status === "ready" ? terminalText(result.url) : `上传 ${terminalText(result.id)}: ${terminalText(result.status)}`);
926
+ if (result.status === "ready") {
927
+ note(`已发布 ${terminalText(result.pageId)} · 版本 ${result.versionNumber}`, o);
928
+ note(`管理: ${terminalText(result.manageUrl)}`, o);
929
+ note(`下一次更新: share-pages publish ${terminalText(input)} --id ${result.pageId} --update`, o);
930
+ } else note(`下一步: share-pages uploads ${result.id}`, o);
569
931
  if (result.status !== "ready") process.exitCode = 6;
570
- });
932
+ }), [
933
+ "share-pages publish ./dist --id my-site",
934
+ "share-pages publish ./README.md --id project-readme --dry-run",
935
+ "share-pages publish ./README.md --id project-readme --update",
936
+ "printf '# Hello' | share-pages publish - --type markdown --id hello"
937
+ ], [
938
+ "支持 html、zip、markdown、latex、epub、csv;目录按静态站点发布。",
939
+ "新建已有 ID 会失败;更新必须显式使用 --update,以避免误覆盖。",
940
+ "JSON 模式下结果只写 stdout,进度和提示写 stderr;添加 --quiet 可隐藏提示。"
941
+ ]);
571
942
  }
572
943
  //#endregion
573
944
  //#region src/stats.ts
574
945
  function registerStats(cli) {
575
- cli.command("stats [id]").option("--version <version>").option("--from <date>").option("--to <date>").option("--csv").action(async (id, _local, command) => {
946
+ examples(cli.command("stats").description("查询全站、页面或版本的访问统计").helpGroup("观测与诊断").argument("[id]", "可选的页面 ID", pageId).option("--version <version>", "只查询指定版本 UUID").option("--from <date>", "UTC 起始日期(YYYY-MM-DD)", isoDate).option("--to <date>", "UTC 结束日期(YYYY-MM-DD)", isoDate).option("--csv", "以 CSV 输出到 stdout").action(async (id, _local, command) => {
576
947
  const o = options(command), params = {};
948
+ if (o.csv && o.json) throw new AppError("OUTPUT_CONFLICT", "--csv 和 --json 不能同时使用");
949
+ if (o.from && o.to && o.from > o.to) throw new AppError("INVALID_DATE_RANGE", "--from 不能晚于 --to");
577
950
  if (id) params.page = id;
578
951
  for (const k of [
579
952
  "version",
@@ -582,24 +955,45 @@ function registerStats(cli) {
582
955
  ]) if (o[k]) params[k] = o[k];
583
956
  if (o.csv) params.format = "csv";
584
957
  const data = await (await client(o)).stats(params);
585
- if (data instanceof Response) process.stdout.write(await data.text());
586
- else output(data, o);
587
- });
588
- cli.command("uploads <id>").action(async (id, _local, command) => {
958
+ if (data instanceof Response) {
959
+ const csv = await data.text();
960
+ process.stdout.write(csv.endsWith("\n") ? csv : `${csv}\n`);
961
+ } else output(data, o, statisticsSummary(data));
962
+ }), [
963
+ "share-pages stats --from 2026-09-01 --to 2026-09-07",
964
+ "share-pages stats handbook --json",
965
+ "share-pages stats handbook --csv > stats.csv"
966
+ ]);
967
+ examples(cli.command("uploads").description("查询断点上传或异步发布任务的状态").helpGroup("观测与诊断").argument("<id>", "上传会话 ID", uploadId).action(async (id, _local, command) => {
589
968
  const o = options(command);
590
- output(await (await client(o)).request(`/uploads/${encodeURIComponent(id)}`), o);
591
- });
969
+ const result = await (await client(o)).request(`/uploads/${encodeURIComponent(id)}`);
970
+ output(result, o, [
971
+ `上传 ${terminalText(result.id)}: ${terminalText(result.status)}`,
972
+ `版本: ${terminalText(result.versionId)}`,
973
+ result.result?.url ? `页面: ${terminalText(result.result.url)}` : void 0,
974
+ result.error ? `错误: ${terminalText(result.error.code)} · ${terminalText(result.error.message)}` : void 0,
975
+ result.missing.length ? `待上传分片文件: ${result.missing.length}` : void 0
976
+ ].filter(Boolean).join("\n"));
977
+ }), ["share-pages uploads <上传会话 ID>", "share-pages uploads <上传会话 ID> --json"]);
592
978
  }
593
979
  //#endregion
594
980
  //#region src/transfer.ts
595
981
  function registerTransfer(cli) {
596
- cli.command("download <id>").option("--version <version>").option("-o, --output <file>").action(async (id, _local, command) => {
597
- const o = options(command), sdk = await client(o);
982
+ examples(cli.command("download").description("下载页面当前版本或指定版本的原件").helpGroup("发布与传输").argument("<id>", "页面 ID", pageId).option("--version <version>", "下载指定版本 UUID(默认当前版本)").option("-o, --output <file>", "输出文件;使用 - 写入 stdout").action(async (id, _local, command) => {
983
+ const o = options(command);
984
+ if (o.output === "-" && o.json) throw new AppError("OUTPUT_CONFLICT", "--output - 输出原件字节,不能与 --json 同时使用");
985
+ const sdk = await client(o);
986
+ note(`正在下载 ${id}${o.version ? ` 的版本 ${terminalText(o.version)}` : ""}…`, o);
598
987
  const version = o.version ?? (await sdk.getPage(id)).current_version_id;
599
988
  if (!version) throw new AppError("NO_CURRENT_VERSION", "页面没有当前版");
600
989
  const response = await sdk.download(id, String(version));
601
990
  const filename = o.output ?? `${id}-${version}.source`;
602
991
  if (!response.body) throw new AppError("EMPTY_DOWNLOAD", "下载内容为空");
992
+ if (filename === "-") {
993
+ note(`正在将 ${id} 的原件写入 stdout…`, o);
994
+ await pipeline(Readable.fromWeb(response.body), process.stdout);
995
+ return;
996
+ }
603
997
  await pipeline(Readable.fromWeb(response.body), createWriteStream(filename, {
604
998
  flags: "wx",
605
999
  mode: 384
@@ -607,18 +1001,24 @@ function registerTransfer(cli) {
607
1001
  output({
608
1002
  file: filename,
609
1003
  versionId: version
610
- }, o);
611
- });
1004
+ }, o, `原件已保存: ${terminalText(filename)}\n版本: ${terminalText(version)}`);
1005
+ }), [
1006
+ "share-pages download handbook --output handbook.md",
1007
+ "share-pages download handbook --version <版本 UUID> --output handbook-v1.md",
1008
+ "share-pages download handbook --output - > handbook.source"
1009
+ ], ["CLI 使用独占创建模式,不会覆盖已有文件。"]);
612
1010
  }
613
1011
  //#endregion
614
1012
  //#region src/versions.ts
615
1013
  function registerVersions(cli) {
616
- const versions = cli.command("versions");
617
- versions.command("list <id>").option("--cursor <cursor>").action(async (id, _local, command) => {
1014
+ const versions = cli.command("versions").description("查询、删除和恢复页面历史版本").helpGroup("页面管理");
1015
+ examples(versions, ["share-pages versions list handbook", "share-pages rollback handbook --to <版本 UUID>"]);
1016
+ examples(versions.command("list").description("按版本号倒序列出历史版本").argument("<id>", "页面 ID", pageId).option("--cursor <cursor>", "继续读取服务端返回的下一页").action(async (id, _local, command) => {
618
1017
  const o = options(command);
619
- output(await (await client(o)).listVersions(id, o.cursor), o);
620
- });
621
- versions.command("delete <id> <version>").option("--replace-with <version>").option("--unpublish").option("--purge").option("--yes").action(async (id, version, _local, command) => {
1018
+ const result = await (await client(o)).listVersions(id, o.cursor);
1019
+ output(result, o, versionsList(result));
1020
+ }), ["share-pages versions list handbook", "share-pages versions list handbook --json"]);
1021
+ examples(versions.command("delete").description("将历史版本移入回收站,或永久删除").argument("<id>", "页面 ID", pageId).argument("<version>", "版本 UUID").option("--replace-with <version>", "删除当前版时指定接替版本 UUID").option("--unpublish", "删除当前版且不指定接替版本时下线页面").option("--purge", "永久删除版本(不可恢复)").option("--yes", "跳过交互确认(自动化环境必需)").action(async (id, version, _local, command) => {
622
1022
  const o = options(command), sdk = await client(o);
623
1023
  await sdk.getPage(id);
624
1024
  await confirm(`${o.purge ? "永久删除" : "移入回收站"} ${id} 的版本 ${version}。`, !!o.yes, !!o.interactive);
@@ -626,33 +1026,54 @@ function registerVersions(cli) {
626
1026
  replaceWith: o.replaceWith,
627
1027
  unpublish: o.unpublish,
628
1028
  purge: o.purge
629
- }), o);
630
- });
631
- versions.command("restore <id> <version>").action(async (id, version, _local, command) => {
1029
+ }), o, o.purge ? `版本 ${version} 已进入永久清理队列。` : `版本 ${version} 已移入回收站。`);
1030
+ }), ["share-pages versions delete handbook <版本 UUID>", "share-pages versions delete handbook <版本 UUID> --purge --yes --no-interactive"]);
1031
+ examples(versions.command("restore").description("恢复回收站中的历史版本(不切换当前版本)").argument("<id>", "页面 ID", pageId).argument("<version>", "版本 UUID").action(async (id, version, _local, command) => {
632
1032
  const o = options(command);
633
- output(await (await client(o)).restoreVersion(id, version), o);
634
- });
635
- cli.command("rollback <id>").requiredOption("--to <version>").option("--no-wait").action(async (id, _local, command) => {
636
- const o = options(command), result = await (await client(o)).rollback(id, o.to ?? "", { wait: o.wait });
637
- output(result, o);
1033
+ output(await (await client(o)).restoreVersion(id, version), o, `版本 ${version} 已恢复。`);
1034
+ }), ["share-pages versions restore handbook <版本 UUID>"]);
1035
+ examples(cli.command("rollback").description("以历史版本内容创建一个新的当前版本").helpGroup("页面管理").argument("<id>", "页面 ID", pageId).requiredOption("--to <version>", "要回滚到的版本 UUID").option("--idempotency-key <key>", "为自动化重试提供稳定幂等键").option("--no-wait", "提交后返回处理中状态").option("--timeout <seconds>", "等待完成的秒数(1–3600)", positiveSeconds, 120).action(async (id, _local, command) => {
1036
+ const o = options(command), result = await (await client(o)).rollback(id, o.to ?? "", {
1037
+ wait: o.wait,
1038
+ idempotencyKey: o.idempotencyKey,
1039
+ timeoutMs: (o.timeout ?? 120) * 1e3
1040
+ });
1041
+ output(result, o, result.status === "ready" ? result.url : `上传 ${result.id}: ${result.status}`);
1042
+ if (result.status === "ready") note(`已创建版本 ${result.versionNumber},当前页面已切换。`, o);
1043
+ else note(`下一步: share-pages uploads ${result.id}`, o);
638
1044
  if (result.status !== "ready") process.exitCode = 6;
639
- });
1045
+ }), [
1046
+ "share-pages versions list handbook",
1047
+ "share-pages rollback handbook --to <版本 UUID>",
1048
+ "share-pages rollback handbook --to <版本 UUID> --no-wait --json"
1049
+ ]);
640
1050
  }
641
1051
  //#endregion
642
1052
  //#region src/program.ts
643
1053
  function buildCLI() {
644
1054
  const cli = createCommand();
1055
+ configureHelp(cli);
645
1056
  registerAccounts(cli);
646
1057
  registerPublish(cli);
647
1058
  registerPages(cli);
648
1059
  registerVersions(cli);
649
1060
  registerTransfer(cli);
650
1061
  registerStats(cli);
1062
+ examples(cli, [
1063
+ "share-pages login --server https://pages.example.com",
1064
+ "share-pages publish ./dist --id my-site",
1065
+ "share-pages pages list",
1066
+ "share-pages status"
1067
+ ], [
1068
+ "全局选项可以写在子命令前后。自动化环境建议添加 --json --no-interactive。",
1069
+ "环境变量:SHARE_PAGES_SERVER、SHARE_PAGES_TOKEN、SHARE_PAGES_CONFIG_DIR。",
1070
+ "完整说明:https://www.npmjs.com/package/share-pages,或运行 share-pages help <命令>。"
1071
+ ]);
651
1072
  const configure = (command) => {
652
1073
  command.enablePositionalOptions();
653
1074
  for (const option of cli.options) {
654
1075
  if (option.long === "--version" || command.options.some((local) => local.long === option.long)) continue;
655
- command.addOption(new Option(option.flags, option.description));
1076
+ command.addOption(new Option(option.flags, option.description).helpGroup("全局选项"));
656
1077
  }
657
1078
  for (const child of command.commands) configure(child);
658
1079
  };
@@ -661,21 +1082,19 @@ function buildCLI() {
661
1082
  }
662
1083
  //#endregion
663
1084
  //#region src/index.ts
1085
+ process.stdout.on("error", (error) => {
1086
+ if (error.code === "EPIPE") process.exitCode = 0;
1087
+ else throw error;
1088
+ });
664
1089
  const cli = buildCLI();
665
1090
  try {
666
1091
  await cli.parseAsync();
667
1092
  } catch (error) {
668
- if (error instanceof CommanderError && error.exitCode === 0) process.exitCode = 0;
1093
+ if (isHelpExit(error)) process.exitCode = 0;
669
1094
  else {
670
- const e = error instanceof AppError ? error : new AppError("CLI_ERROR", error instanceof Error ? error.message : "命令执行失败");
671
- const result = {
672
- code: e.code,
673
- message: e.message,
674
- fields: e.fields
675
- };
676
- if (process.argv.includes("--json")) process.stdout.write(`${JSON.stringify({ error: result })}\n`);
677
- else process.stderr.write(`${e.code}: ${e.message}\n`);
678
- process.exitCode = e.status === 401 || e.status === 403 ? 3 : e.status === 409 ? 4 : e.status >= 500 ? 5 : 2;
1095
+ const resolved = normalizeError(error);
1096
+ printError(resolved, wantsJSON(process.argv.slice(2)));
1097
+ process.exitCode = exitCode(resolved);
679
1098
  }
680
1099
  }
681
1100
  //#endregion
@@ -1,5 +1,5 @@
1
- import { o as escapeHtml, r as assert, t as AppError } from "./dist-JyXFh6I5.mjs";
2
- import { n as document } from "./prepare-C56DezIS-DiqwpdkG.mjs";
1
+ import { i as assert, s as escapeHtml, t as AppError } from "./dist-LkxVliJx.mjs";
2
+ import { n as document } from "./prepare-C56DezIS-pzIt9WD8.mjs";
3
3
  //#region ../../packages/content/dist/latex-DMo_3IW8.js
4
4
  async function latex(source, title) {
5
5
  assert(!/\\(?:usepackage|RequirePackage|input|include|newcommand|renewcommand|def|bibliography|bibliographystyle|includegraphics|write18|catcode)\b/.test(source), "UNSUPPORTED_LATEX", "不支持任意宏包、宏定义、外部文件、图片或参考文献编译");
@@ -1,6 +1,6 @@
1
1
  import { i as __toESM, n as __exportAll, t as __commonJSMin } from "./rolldown-runtime-B4iAMlE-.mjs";
2
- import { o as escapeHtml, r as assert } from "./dist-JyXFh6I5.mjs";
3
- import { d as parse$4, l as walk, n as document$3 } from "./prepare-C56DezIS-DiqwpdkG.mjs";
2
+ import { i as assert, s as escapeHtml } from "./dist-LkxVliJx.mjs";
3
+ import { d as parse$4, l as walk, n as document$3 } from "./prepare-C56DezIS-pzIt9WD8.mjs";
4
4
  import minpath from "node:path";
5
5
  import minproc from "node:process";
6
6
  import { fileURLToPath as urlToPath } from "node:url";
@@ -1,5 +1,5 @@
1
- import { i as defaultLimits, l as safePath, r as assert } from "./dist-JyXFh6I5.mjs";
2
- import { a as prepareBytes, i as finalize, u as zipSync } from "./prepare-C56DezIS-DiqwpdkG.mjs";
1
+ import { a as defaultLimits, i as assert, u as safePath } from "./dist-LkxVliJx.mjs";
2
+ import { a as prepareBytes, i as finalize, u as zipSync } from "./prepare-C56DezIS-pzIt9WD8.mjs";
3
3
  import { lstat, readFile, readdir } from "node:fs/promises";
4
4
  import { basename, join } from "node:path";
5
5
  //#region ../../packages/content/dist/directory-Cz5DdsHB.js
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM } from "./rolldown-runtime-B4iAMlE-.mjs";
2
- import { a as digest, c as manifestSchema, i as defaultLimits, l as safePath, o as escapeHtml, r as assert, t as AppError, u as validateManifestLimits } from "./dist-JyXFh6I5.mjs";
2
+ import { a as defaultLimits, f as validateManifestLimits, i as assert, l as manifestSchema, o as digest, s as escapeHtml, t as AppError, u as safePath } from "./dist-LkxVliJx.mjs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { createRequire } from "module";
5
5
  //#region ../../node_modules/.pnpm/parse5@8.0.1/node_modules/parse5/dist/common/unicode.js
@@ -8514,7 +8514,7 @@ async function prepareBytes(name, data, options = {}) {
8514
8514
  if (type === "markdown" || type === "latex") {
8515
8515
  assert(data.length <= limits.textBytes, "TEXT_TOO_LARGE", "文本文档超过 5 MiB 限额", 413);
8516
8516
  const source = decodeText(data, options.encoding);
8517
- files.set("index.html", encoder.encode(type === "markdown" ? await (await import("./markdown-BWlCFeLJ-OwSvKM8l.mjs")).markdown(source, title) : await (await import("./latex-DMo_3IW8-B6ogeBvw.mjs")).latex(source, title)));
8517
+ files.set("index.html", encoder.encode(type === "markdown" ? await (await import("./markdown-BWlCFeLJ-CCxfGkZu.mjs")).markdown(source, title) : await (await import("./latex-DMo_3IW8-DdNgxe_1.mjs")).latex(source, title)));
8518
8518
  } else if (type === "csv") {
8519
8519
  assert(data.length <= limits.csvBytes, "CSV_TOO_LARGE", "CSV 超过字节限额", 413);
8520
8520
  const source = decodeText(data, options.encoding);
@@ -8549,7 +8549,7 @@ async function prepareBytes(name, data, options = {}) {
8549
8549
  header,
8550
8550
  maxRows: limits.csvRows
8551
8551
  })));
8552
- } else if (type === "epub") files = await (await import("./epub-BXQPqTFk-CGD2NLS1.mjs")).prepareEPUB(data, title, limits, await digest(data));
8552
+ } else if (type === "epub") files = await (await import("./epub-BXQPqTFk-DV2p5uyW.mjs")).prepareEPUB(data, title, limits, await digest(data));
8553
8553
  for (const [path, bytes] of await loadViewerAssets(type)) files.set(path, bytes);
8554
8554
  }
8555
8555
  return finalize(name, data, files, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "share-pages",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line client for publishing and managing Share Pages content",
5
5
  "keywords": [
6
6
  "share-pages",
@@ -19,11 +19,6 @@
19
19
  "files": [
20
20
  "dist"
21
21
  ],
22
- "scripts": {
23
- "build": "pnpm --filter @share-pages/viewers build && tsdown && node scripts/copy-viewer-resources.mjs",
24
- "prepublishOnly": "node scripts/check-publish-manifest.mjs && pnpm build && pnpm typecheck",
25
- "typecheck": "tsc --noEmit"
26
- },
27
22
  "publishConfig": {
28
23
  "access": "public"
29
24
  },
@@ -32,9 +27,13 @@
32
27
  "jsdom": "26.1.0"
33
28
  },
34
29
  "devDependencies": {
35
- "@share-pages/content": "workspace:*",
36
- "@share-pages/contracts": "workspace:*",
37
- "@share-pages/sdk": "workspace:*",
38
- "tsdown": "catalog:"
30
+ "tsdown": "0.23.0",
31
+ "@share-pages/contracts": "0.1.0",
32
+ "@share-pages/content": "0.1.0",
33
+ "@share-pages/sdk": "0.1.0"
34
+ },
35
+ "scripts": {
36
+ "build": "pnpm --filter @share-pages/viewers build && tsdown && node scripts/copy-viewer-resources.mjs",
37
+ "typecheck": "tsc --noEmit"
39
38
  }
40
- }
39
+ }