@zzclub/pipeline 0.3.0 → 0.4.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 (194) hide show
  1. package/README.md +366 -170
  2. package/package.json +4 -7
  3. package/src/adapter-loader.ts +138 -0
  4. package/src/adapter-types.ts +172 -0
  5. package/src/adapters/builtin-image-renderer.ts +353 -0
  6. package/src/adapters/builtin-markdown-renderer.ts +63 -0
  7. package/src/adapters/index.ts +2 -0
  8. package/src/args.test.ts +118 -0
  9. package/src/args.ts +90 -0
  10. package/src/cli.ts +51 -0
  11. package/src/commands/abandon.test.ts +104 -0
  12. package/src/commands/abandon.ts +202 -0
  13. package/src/commands/attach-body-images.ts +88 -0
  14. package/src/commands/attach-body.ts +86 -0
  15. package/src/commands/attach-newspic-spec.ts +41 -0
  16. package/src/commands/checkpoint.test.ts +91 -0
  17. package/src/commands/checkpoint.ts +66 -0
  18. package/src/commands/config.ts +106 -0
  19. package/src/commands/cos-upload.ts +62 -0
  20. package/src/commands/doctor.ts +66 -0
  21. package/src/commands/find-run.ts +55 -0
  22. package/src/commands/hermes-metrics.ts +226 -0
  23. package/src/commands/imgx.ts +60 -0
  24. package/src/commands/ingest-handoff.ts +509 -0
  25. package/src/commands/init.ts +126 -0
  26. package/src/commands/prepare-finalize.ts +348 -0
  27. package/src/commands/prepare.ts +310 -0
  28. package/src/commands/publish.ts +147 -0
  29. package/src/commands/reconcile.ts +36 -0
  30. package/src/commands/render.ts +244 -0
  31. package/src/commands/reset.test.ts +151 -0
  32. package/src/commands/reset.ts +159 -0
  33. package/src/commands/review.ts +77 -0
  34. package/src/commands/status.ts +68 -0
  35. package/src/commands/sync-blog.ts +39 -0
  36. package/src/commands/tasks.ts +50 -0
  37. package/src/commands/wechat-export.ts +68 -0
  38. package/src/config.ts +0 -0
  39. package/src/imgx/ascii-portrait.ts +77 -0
  40. package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
  41. package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
  42. package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  43. package/src/imgx/assets/icons/logo.png +0 -0
  44. package/src/imgx/assets/icons/logo.svg +8 -0
  45. package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
  46. package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
  47. package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
  48. package/src/imgx/cli.ts +57 -0
  49. package/src/imgx/geometry.ts +68 -0
  50. package/src/imgx/index.ts +5 -0
  51. package/src/imgx/longform-theme.ts +243 -0
  52. package/src/imgx/poster-recipe.ts +81 -0
  53. package/src/imgx/pretext-adapter.ts +14 -0
  54. package/src/imgx/pretext-runtime.ts +61 -0
  55. package/src/imgx/references/ascii-portrait-3-4.md +59 -0
  56. package/src/imgx/references/longform-3-4.md +173 -0
  57. package/src/imgx/references/poster-3-4.md +87 -0
  58. package/src/imgx/references/tips-3-4.md +69 -0
  59. package/src/imgx/references/wechat-cover-split.md +89 -0
  60. package/src/imgx/references/x-like-posts.md +139 -0
  61. package/src/imgx/render-article.test.ts +53 -0
  62. package/src/imgx/render-article.ts +1364 -0
  63. package/src/imgx/render-ascii-portrait.ts +53 -0
  64. package/src/imgx/render-card.ts +232 -0
  65. package/src/imgx/render-x-like-posts.ts +235 -0
  66. package/src/imgx/runtime.ts +403 -0
  67. package/src/imgx/typographic-scale.test.ts +56 -0
  68. package/src/imgx/typographic-scale.ts +44 -0
  69. package/src/output.ts +632 -0
  70. package/src/plugins.ts +101 -0
  71. package/src/profiles.ts +149 -0
  72. package/src/providers/blog.test.ts +58 -0
  73. package/src/providers/blog.ts +154 -0
  74. package/src/providers/cos.ts +183 -0
  75. package/src/providers/index.test.ts +26 -0
  76. package/src/providers/index.ts +185 -0
  77. package/src/providers/wechat.test.ts +215 -0
  78. package/src/providers/wechat.ts +548 -0
  79. package/src/routes.ts +287 -0
  80. package/src/runtime-paths.ts +179 -0
  81. package/src/schema/config.ts +133 -0
  82. package/src/schema/state.ts +376 -0
  83. package/src/spawn.ts +103 -0
  84. package/src/state.ts +265 -0
  85. package/src/task-manager.ts +657 -0
  86. package/src/task-views.ts +222 -0
  87. package/src/text.test.ts +689 -0
  88. package/src/text.ts +683 -0
  89. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  90. package/src/wechat-preview/browser/editor-export.css +241 -0
  91. package/src/wechat-preview/browser/editor-export.ts +103 -0
  92. package/src/wechat-preview/frontmatter-handler.ts +73 -0
  93. package/src/wechat-preview/index.test.ts +154 -0
  94. package/src/wechat-preview/index.ts +294 -0
  95. package/src/wechat-preview/themes.ts +141 -0
  96. package/src/wechat-preview/wechat-formatter.test.ts +288 -0
  97. package/src/wechat-preview/wechat-formatter.ts +1506 -0
  98. package/src/wechat-preview.test.ts +73 -0
  99. package/src/workflow-materials.test.ts +225 -0
  100. package/src/workflow-materials.ts +217 -0
  101. package/src/workflow.test.ts +2087 -0
  102. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  103. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  104. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  105. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  106. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
  107. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  108. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
  109. package/dist/cli.js +0 -431
  110. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
  111. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
  112. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
  113. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
  114. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
  115. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
  116. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
  117. package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
  118. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
  119. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
  120. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
  121. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
  122. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
  123. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
  124. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
  125. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
  126. /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
  127. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
  128. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
  129. /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
  130. /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
  131. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
  132. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  133. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  134. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  135. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  136. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  137. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  138. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  139. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  140. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  141. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  142. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  143. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  144. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  145. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  146. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  147. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  148. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  149. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  150. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  151. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  152. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  153. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  154. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  155. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  156. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  157. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  158. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  159. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  160. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  161. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  162. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  163. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  164. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  165. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  166. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  167. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  168. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  169. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  170. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  171. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  172. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  173. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  174. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  175. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  176. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  177. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  178. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  179. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  180. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  181. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  182. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  183. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  184. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  185. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  186. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  187. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  188. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  189. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  190. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  191. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  192. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
  193. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
  194. /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
package/src/output.ts ADDED
@@ -0,0 +1,632 @@
1
+ /**
2
+ * output.ts — Unified output layer.
3
+ *
4
+ * - isTTY + no NO_COLOR → pretty print (ANSI color, ASCII borders, structured layout)
5
+ * - FORCE_COLOR=1 → force pretty even when not a TTY (e.g. pager)
6
+ * - NO_COLOR=1 / isTTY=false → raw JSON (agent / pipe / redirect friendly)
7
+ *
8
+ * Each command calls `printResult(data, renderer)`.
9
+ * When a `renderer` is provided it is called for pretty mode; otherwise a
10
+ * generic JSON pretty-printer is used as fallback.
11
+ */
12
+
13
+ // ── TTY detection ────────────────────────────────────────────────────────────
14
+
15
+ export function isTTY(): boolean {
16
+ if (process.env.FORCE_COLOR === "1") return true;
17
+ if (process.env.NO_COLOR) return false;
18
+ return Boolean(process.stdout.isTTY);
19
+ }
20
+
21
+ // ── ANSI color primitives ────────────────────────────────────────────────────
22
+
23
+ const ESC = "\x1b[";
24
+ const RESET = "\x1b[0m";
25
+
26
+ function ansi(code: string, text: string): string {
27
+ return isTTY() ? `${ESC}${code}m${text}${RESET}` : text;
28
+ }
29
+
30
+ export const c = {
31
+ green: (t: string) => ansi("32", t),
32
+ red: (t: string) => ansi("31", t),
33
+ yellow: (t: string) => ansi("33", t),
34
+ blue: (t: string) => ansi("34", t),
35
+ cyan: (t: string) => ansi("36", t),
36
+ magenta: (t: string) => ansi("35", t),
37
+ gray: (t: string) => ansi("90", t),
38
+ bold: (t: string) => ansi("1", t),
39
+ dim: (t: string) => ansi("2", t),
40
+ };
41
+
42
+ // ── Layout primitives ────────────────────────────────────────────────────────
43
+
44
+ const LINE_WIDTH = 52;
45
+
46
+ /** "── label ──────────────────────────────" */
47
+ export function divider(label?: string): string {
48
+ if (!label) {
49
+ return c.gray("─".repeat(LINE_WIDTH));
50
+ }
51
+ const prefix = "── ";
52
+ const suffix = " ";
53
+ const remaining = Math.max(0, LINE_WIDTH - prefix.length - label.length - suffix.length);
54
+ return c.gray(prefix) + c.bold(label) + c.gray(suffix + "─".repeat(remaining));
55
+ }
56
+
57
+ /** key-value row with aligned value column */
58
+ export function kv(key: string, val: string, indent = 2): string {
59
+ const pad = Math.max(1, 14 - key.length);
60
+ return " ".repeat(indent) + c.dim(key) + " ".repeat(pad) + val;
61
+ }
62
+
63
+ /** multi-line ASCII box */
64
+ export function box(lines: string[]): string {
65
+ const innerWidth = Math.max(...lines.map(stripAnsi).map((l) => l.length), 20);
66
+ const top = c.gray("┌─ ") + c.gray("─".repeat(innerWidth + 2)) + c.gray(" ─┐");
67
+ const bottom = c.gray("└") + c.gray("─".repeat(innerWidth + 6)) + c.gray("┘");
68
+ const rows = lines.map((l) => {
69
+ const raw = stripAnsi(l);
70
+ const pad = innerWidth - raw.length;
71
+ return c.gray("│") + " " + l + " ".repeat(Math.max(0, pad)) + " " + c.gray("│");
72
+ });
73
+ return [top, ...rows, bottom].join("\n");
74
+ }
75
+
76
+ /** "[label]" badge — color optional */
77
+ export function badge(text: string, color: keyof typeof c = "gray"): string {
78
+ return c[color](`[${text}]`);
79
+ }
80
+
81
+ /** strip ANSI escape codes for length calculation */
82
+ function stripAnsi(str: string): string {
83
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
84
+ }
85
+
86
+ // ── Status helpers ───────────────────────────────────────────────────────────
87
+
88
+ export function statusBadge(status: string): string {
89
+ switch (status) {
90
+ case "done":
91
+ case "success":
92
+ case "passed":
93
+ case "ready":
94
+ return c.green(`✓ ${status}`);
95
+ case "failed":
96
+ case "needs_revision":
97
+ return c.red(`✗ ${status}`);
98
+ case "handoff":
99
+ return c.yellow(`⚡ ${status}`);
100
+ case "pending":
101
+ case "planned":
102
+ return c.dim(status);
103
+ case "skipped":
104
+ return c.gray(`– ${status}`);
105
+ default:
106
+ return status;
107
+ }
108
+ }
109
+
110
+ // ── Main entry point ─────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Print result to stdout.
114
+ *
115
+ * - Non-TTY: always raw JSON.
116
+ * - TTY: call `renderer(data)` if provided; otherwise generic pretty print.
117
+ */
118
+ export function printResult(data: unknown, renderer?: (data: unknown) => string): void {
119
+ if (!isTTY()) {
120
+ console.log(JSON.stringify(data, null, 2));
121
+ return;
122
+ }
123
+ if (renderer) {
124
+ process.stdout.write(renderer(data) + "\n");
125
+ return;
126
+ }
127
+ process.stdout.write(colorizeJson(JSON.stringify(data, null, 2)) + "\n");
128
+ }
129
+
130
+ /** Print help text (always raw, no transformation) */
131
+ export function printHelp(text: string): void {
132
+ process.stdout.write(text + "\n");
133
+ }
134
+
135
+ /** Print to stderr (not affected by TTY mode) */
136
+ export function printError(msg: string): void {
137
+ process.stderr.write(msg + "\n");
138
+ }
139
+
140
+ // ── Generic JSON colorizer (TTY fallback) ────────────────────────────────────
141
+
142
+ function colorizeJson(json: string): string {
143
+ return json
144
+ .replace(/"([^"]+)":/g, (_, k: string) => c.cyan(`"${k}"`) + ":")
145
+ .replace(/: "([^"]*)"/g, (_, v: string) => ": " + c.green(`"${v}"`))
146
+ .replace(/: (true|false)/g, (_, v: string) => ": " + c.yellow(v))
147
+ .replace(/: (null)/g, (_, v: string) => ": " + c.dim(v))
148
+ .replace(/: (-?\d+(\.\d+)?)/g, (_, v: string) => ": " + c.magenta(v));
149
+ }
150
+
151
+ // ── Per-command pretty renderers ─────────────────────────────────────────────
152
+
153
+ // ---------- init -------------------------------------------------------------
154
+
155
+ interface InitOutput {
156
+ run_id: string;
157
+ state_path: string;
158
+ mode: string;
159
+ phase: string;
160
+ }
161
+
162
+ export function renderInit(data: unknown): string {
163
+ const d = data as InitOutput;
164
+ return [
165
+ box([
166
+ c.bold("run created"),
167
+ "",
168
+ kv("id", c.cyan(d.run_id), 0),
169
+ kv("phase", statusBadge(d.phase), 0),
170
+ kv("mode", d.mode, 0),
171
+ ]),
172
+ kv("state", c.dim(d.state_path)),
173
+ ].join("\n");
174
+ }
175
+
176
+ // ---------- prepare ----------------------------------------------------------
177
+
178
+ interface PrepareOutput {
179
+ state_path: string;
180
+ route: { primary: string; extras: string[]; account: string; content_profile: string };
181
+ authoring: { rewrite_allowed: boolean; style_mode: string };
182
+ metadata: { title: string; slug: string; date: string; description: string | null };
183
+ requires_style: boolean;
184
+ body_formatted_path: string;
185
+ illustration_markers: string[];
186
+ }
187
+
188
+ export function renderPrepare(data: unknown): string {
189
+ const d = data as PrepareOutput;
190
+ const extras = d.route.extras.length > 0 ? ` ${c.dim("+")} ${d.route.extras.join(", ")}` : "";
191
+ const lines: string[] = [
192
+ divider("prepare"),
193
+ kv("title", c.bold(d.metadata.title)),
194
+ kv("slug", c.dim(d.metadata.slug)),
195
+ kv("date", d.metadata.date),
196
+ kv("route", `${c.cyan(d.route.primary)}${extras} ${c.dim(`@${d.route.account}`)}`),
197
+ kv("style", d.requires_style ? c.yellow("requested") : c.dim("none")),
198
+ kv("rewrite", d.authoring.rewrite_allowed ? c.yellow("allowed") : c.dim("no")),
199
+ ];
200
+ if (d.illustration_markers.length > 0) {
201
+ lines.push(kv("markers", d.illustration_markers.join(", ")));
202
+ }
203
+ lines.push(kv("body →", c.dim(d.body_formatted_path)));
204
+ return lines.join("\n");
205
+ }
206
+
207
+ // ---------- prepare-finalize -------------------------------------------------
208
+
209
+ interface PrepareFinalizeOutput {
210
+ asset_path: string;
211
+ state_path: string;
212
+ post_path: string;
213
+ highlight_words: string[];
214
+ phase: string;
215
+ content_version: number;
216
+ }
217
+
218
+ export function renderPrepareFinalize(data: unknown): string {
219
+ const d = data as PrepareFinalizeOutput;
220
+ return [
221
+ divider("prepare-finalize"),
222
+ kv("phase", statusBadge(d.phase)),
223
+ kv("version", `content:${d.content_version}`),
224
+ kv("highlight", d.highlight_words.join(", ") || c.dim("(none)")),
225
+ kv("asset →", c.dim(d.asset_path)),
226
+ kv("post →", c.dim(d.post_path)),
227
+ ].join("\n");
228
+ }
229
+
230
+ // ---------- render -----------------------------------------------------------
231
+
232
+ interface RenderPlan {
233
+ needed: boolean;
234
+ template: string | null;
235
+ cover_template: string | null;
236
+ cover_title: string | null;
237
+ output_dir: string | null;
238
+ status: string;
239
+ }
240
+
241
+ interface RenderAsset {
242
+ kind: string;
243
+ route: string;
244
+ path: string;
245
+ index?: number;
246
+ }
247
+
248
+ interface RenderOutput {
249
+ plan: RenderPlan;
250
+ render_assets?: RenderAsset[];
251
+ render_version?: number;
252
+ phase?: string;
253
+ // waiting for user branch
254
+ body_inputs?: unknown;
255
+ waiting_for_user?: boolean;
256
+ message?: string;
257
+ // skip-render branch
258
+ skip_render?: boolean;
259
+ newspic_render?: unknown;
260
+ }
261
+
262
+ export function renderRender(data: unknown): string {
263
+ const d = data as RenderOutput;
264
+ const lines: string[] = [divider("render")];
265
+
266
+ if (d.waiting_for_user) {
267
+ lines.push(kv("status", c.yellow("waiting for user")));
268
+ if (d.message) lines.push(kv("message", d.message));
269
+ return lines.join("\n");
270
+ }
271
+
272
+ if (d.skip_render) {
273
+ lines.push(kv("status", c.dim("plan only (skip-render)")));
274
+ lines.push(kv("template", d.plan.template ?? c.dim("n/a")));
275
+ return lines.join("\n");
276
+ }
277
+
278
+ lines.push(kv("template", d.plan.template ?? c.dim("n/a")));
279
+ lines.push(kv("plan", statusBadge(d.plan.status)));
280
+ if (d.phase) lines.push(kv("phase", statusBadge(d.phase)));
281
+ if (d.render_version !== undefined) lines.push(kv("version", `render:${d.render_version}`));
282
+
283
+ if (d.render_assets && d.render_assets.length > 0) {
284
+ lines.push(c.gray(" ─────"));
285
+ for (const asset of d.render_assets) {
286
+ const label = asset.kind === "page" ? `page ${asset.index ?? ""}` : asset.kind;
287
+ lines.push(kv(label, c.dim(asset.path)));
288
+ }
289
+ }
290
+
291
+ return lines.join("\n");
292
+ }
293
+
294
+ // ---------- publish ----------------------------------------------------------
295
+
296
+ interface PublishResult {
297
+ route: string;
298
+ status: string;
299
+ detail?: string | null;
300
+ published_at?: string | null;
301
+ content_version?: number;
302
+ render_version?: number;
303
+ }
304
+
305
+ interface PublishOutput {
306
+ publish_results: PublishResult[];
307
+ mode: string;
308
+ phase: string;
309
+ }
310
+
311
+ export function renderPublish(data: unknown): string {
312
+ const d = data as PublishOutput;
313
+ const lines: string[] = [divider("publish")];
314
+ for (const r of d.publish_results) {
315
+ const ts = r.published_at ? c.dim(` ${r.published_at}`) : "";
316
+ lines.push(kv(r.route, `${statusBadge(r.status)}${ts}`));
317
+ if (r.detail && r.status === "failed") {
318
+ lines.push(kv("", c.red(r.detail)));
319
+ }
320
+ }
321
+ lines.push(c.gray(" ─────"));
322
+ lines.push(kv("mode", d.mode));
323
+ lines.push(kv("phase", statusBadge(d.phase)));
324
+ return lines.join("\n");
325
+ }
326
+
327
+ // ---------- review -----------------------------------------------------------
328
+
329
+ interface ReviewOutput {
330
+ state_path: string;
331
+ content_review: { status: string; feedback: string | null };
332
+ }
333
+
334
+ export function renderReview(data: unknown): string {
335
+ const d = data as ReviewOutput;
336
+ const lines: string[] = [
337
+ divider("review"),
338
+ kv("status", statusBadge(d.content_review.status)),
339
+ ];
340
+ if (d.content_review.feedback) {
341
+ lines.push(kv("feedback", d.content_review.feedback));
342
+ }
343
+ return lines.join("\n");
344
+ }
345
+
346
+ // ---------- task shape (reconcile / attach-body / attach-body-images / attach-newspic-spec / status / checkpoint) ──
347
+
348
+ interface TaskSummaryNested {
349
+ run_id?: string;
350
+ mode?: string;
351
+ phase?: { current?: string } | string;
352
+ route?: { primary?: string; extras?: string[]; account?: string } | string;
353
+ metadata?: { title?: string } | null;
354
+ content_review?: { status?: string } | string | null;
355
+ [key: string]: unknown;
356
+ }
357
+
358
+ interface TaskNextAction {
359
+ action?: string;
360
+ reason?: string;
361
+ }
362
+
363
+ interface TaskGap {
364
+ code?: string;
365
+ field?: string;
366
+ message?: string;
367
+ }
368
+
369
+ interface TaskShapeOutput {
370
+ summary?: TaskSummaryNested;
371
+ validation?: { phase_checked?: string; valid?: boolean; errors?: string[] };
372
+ gaps?: TaskGap[];
373
+ blockers?: TaskGap[];
374
+ next_action?: TaskNextAction | string;
375
+ [key: string]: unknown;
376
+ }
377
+
378
+ function extractPhase(phase: TaskSummaryNested["phase"]): string | undefined {
379
+ if (!phase) return undefined;
380
+ if (typeof phase === "string") return phase;
381
+ return phase.current;
382
+ }
383
+
384
+ function extractRoute(route: TaskSummaryNested["route"]): { primary?: string; account?: string } {
385
+ if (!route) return {};
386
+ if (typeof route === "string") return { primary: route };
387
+ return { primary: route.primary, account: route.account };
388
+ }
389
+
390
+ function extractTitle(meta: TaskSummaryNested["metadata"]): string | undefined {
391
+ if (!meta) return undefined;
392
+ if (typeof meta === "object" && "title" in meta) return meta.title ?? undefined;
393
+ return undefined;
394
+ }
395
+
396
+ function extractReviewStatus(cr: TaskSummaryNested["content_review"]): string | undefined {
397
+ if (!cr) return undefined;
398
+ if (typeof cr === "string") return cr;
399
+ if (typeof cr === "object" && "status" in cr) return cr.status ?? undefined;
400
+ return undefined;
401
+ }
402
+
403
+ export function renderTaskShape(data: unknown): string {
404
+ const d = data as TaskShapeOutput;
405
+ const s = d.summary ?? (data as TaskSummaryNested);
406
+
407
+ const runId = s.run_id ?? "status";
408
+ const phase = extractPhase(s.phase);
409
+ const { primary: routePrimary, account } = extractRoute(s.route);
410
+ const title = extractTitle(s.metadata);
411
+ const reviewStatus = extractReviewStatus(s.content_review);
412
+
413
+ const lines: string[] = [divider(runId)];
414
+
415
+ if (title) lines.push(kv("title", c.bold(title)));
416
+ if (phase) lines.push(kv("phase", statusBadge(phase)));
417
+ if (s.mode) lines.push(kv("mode", String(s.mode)));
418
+ if (routePrimary) lines.push(kv("route", `${c.cyan(routePrimary)}${account ? c.dim(` @${account}`) : ""}`));
419
+ if (reviewStatus) lines.push(kv("review", statusBadge(reviewStatus)));
420
+
421
+ if (d.validation && !d.validation.valid && d.validation.errors && d.validation.errors.length > 0) {
422
+ lines.push(c.gray(" ─────"));
423
+ lines.push(kv("errors", c.red(`${d.validation.errors.length}`)));
424
+ for (const err of d.validation.errors) {
425
+ lines.push(" " + c.red(`✗ ${err}`));
426
+ }
427
+ }
428
+
429
+ if (d.blockers && d.blockers.length > 0) {
430
+ lines.push(c.gray(" ─────"));
431
+ for (const b of d.blockers) {
432
+ const msg = typeof b === "string" ? b : (b.message ?? b.field ?? String(b));
433
+ lines.push(" " + c.yellow(`⚠ ${msg}`));
434
+ }
435
+ }
436
+
437
+ if (d.gaps && d.gaps.length > 0) {
438
+ lines.push(c.gray(" ─────"));
439
+ for (const g of d.gaps) {
440
+ const msg = typeof g === "string" ? g : (g.message ?? g.field ?? String(g));
441
+ lines.push(" " + c.dim(`· ${msg}`));
442
+ }
443
+ }
444
+
445
+ if (d.next_action) {
446
+ const na = d.next_action;
447
+ const naStr = typeof na === "string" ? na : `${na.action ?? "?"} ${c.dim(na.reason ?? "")}`;
448
+ lines.push(c.gray(" ─────"));
449
+ lines.push(kv("next", naStr));
450
+ }
451
+
452
+ return lines.join("\n");
453
+ }
454
+
455
+ // ---------- tasks (array) ----------------------------------------------------
456
+
457
+ interface TaskEntry {
458
+ summary?: TaskSummaryNested;
459
+ validation?: unknown;
460
+ blockers?: TaskGap[];
461
+ next_action?: TaskNextAction | string;
462
+ }
463
+
464
+ export function renderTasks(data: unknown): string {
465
+ const arr = data as TaskEntry[];
466
+ if (arr.length === 0) {
467
+ return c.dim(" (no tasks)");
468
+ }
469
+ const lines: string[] = [divider(`tasks (${arr.length})`)];
470
+ for (const task of arr) {
471
+ const s = task.summary ?? {};
472
+ const phase = extractPhase(s.phase);
473
+ const title = extractTitle(s.metadata);
474
+ const phaseStr = phase ? ` ${statusBadge(phase)}` : "";
475
+ const titleStr = title ? ` ${c.dim(title.slice(0, 32))}` : "";
476
+ lines.push(` ${c.cyan(s.run_id ?? "?")}${phaseStr}${titleStr}`);
477
+ if (task.next_action) {
478
+ const na = task.next_action;
479
+ const naStr = typeof na === "string" ? na : (na.action ?? "?");
480
+ lines.push(` ${c.dim("→")} ${naStr}`);
481
+ }
482
+ }
483
+ return lines.join("\n");
484
+ }
485
+
486
+ // ---------- sync-blog --------------------------------------------------------
487
+
488
+ interface SyncBlogOutput {
489
+ route: string;
490
+ status: string;
491
+ detail?: string | null;
492
+ published_at?: string | null;
493
+ content_version?: number;
494
+ render_version?: number;
495
+ }
496
+
497
+ export function renderSyncBlog(data: unknown): string {
498
+ const d = data as SyncBlogOutput;
499
+ const lines: string[] = [
500
+ divider("sync-blog"),
501
+ kv("status", statusBadge(d.status)),
502
+ ];
503
+ if (d.published_at) lines.push(kv("published", d.published_at));
504
+ if (d.content_version !== undefined || d.render_version !== undefined) {
505
+ lines.push(kv("versions", `content:${d.content_version ?? "?"} render:${d.render_version ?? "?"}`));
506
+ }
507
+ if (d.detail && d.status !== "success") lines.push(kv("detail", c.yellow(d.detail)));
508
+ return lines.join("\n");
509
+ }
510
+
511
+ // ---------- reset ------------------------------------------------------------
512
+
513
+ interface ResetOutput {
514
+ state_path: string;
515
+ reset_mode: string;
516
+ mode: string;
517
+ phase: { current: string; prepare: string; render: string; publish: string };
518
+ start_step: string | null;
519
+ }
520
+
521
+ export function renderReset(data: unknown): string {
522
+ const d = data as ResetOutput;
523
+ return [
524
+ divider("reset"),
525
+ kv("mode", d.reset_mode),
526
+ kv("result", d.mode),
527
+ kv("phase", `current: ${statusBadge(d.phase.current)}`),
528
+ kv("phases", `prepare:${d.phase.prepare} render:${d.phase.render} publish:${d.phase.publish}`),
529
+ ...(d.start_step ? [kv("start_step", c.yellow(d.start_step))] : []),
530
+ ].join("\n");
531
+ }
532
+
533
+ // ---------- config -----------------------------------------------------------
534
+
535
+ export function renderConfig(data: unknown): string {
536
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") {
537
+ // scalar read
538
+ return String(data);
539
+ }
540
+ if (data && typeof data === "object" && "key" in data) {
541
+ // single key read/write
542
+ const d = data as { key: string; value: unknown };
543
+ return [
544
+ divider("config"),
545
+ kv(d.key, String(d.value ?? c.dim("(unset)"))),
546
+ ].join("\n");
547
+ }
548
+ // full summary
549
+ const lines: string[] = [divider("config")];
550
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
551
+ lines.push(kv(k, typeof v === "object" ? JSON.stringify(v) : String(v ?? c.dim("(unset)"))));
552
+ }
553
+ return lines.join("\n");
554
+ }
555
+
556
+ // ---------- doctor -----------------------------------------------------------
557
+
558
+ export function renderDoctor(data: unknown): string {
559
+ const d = data as Record<string, unknown>;
560
+ const lines: string[] = [divider("doctor")];
561
+
562
+ const configPath = d.config_path as string | undefined;
563
+ const exists = d.config_path_exists as boolean | undefined;
564
+ if (configPath) {
565
+ lines.push(kv("config", `${c.dim(configPath)} ${exists ? c.green("✓") : c.red("✗ missing")}`));
566
+ }
567
+
568
+ const workspace = d.workspace as string | undefined;
569
+ if (workspace) lines.push(kv("workspace", c.dim(workspace)));
570
+
571
+ const bun = d.bun_binary as string | null | undefined;
572
+ const bunErr = d.bun_error as string | null | undefined;
573
+ if (bun) {
574
+ lines.push(kv("bun", `${c.dim(bun)} ${c.green("✓")}`));
575
+ } else if (bunErr) {
576
+ lines.push(kv("bun", c.red(`✗ ${bunErr}`)));
577
+ }
578
+
579
+ const providers = d.publish_providers as string[] | undefined;
580
+ if (providers && providers.length > 0) {
581
+ lines.push(kv("providers", providers.join(" ")));
582
+ }
583
+
584
+ const resolvedPaths = d.resolved_paths as Record<string, string> | undefined;
585
+ const pathExist = d.resolved_paths_exist as Record<string, boolean> | undefined;
586
+ if (resolvedPaths && pathExist) {
587
+ lines.push(c.gray(" ─────"));
588
+ for (const [key, val] of Object.entries(resolvedPaths)) {
589
+ const ok = pathExist[key];
590
+ const indicator = ok === undefined ? "" : ok ? c.green(" ✓") : c.red(" ✗");
591
+ lines.push(kv(key, c.dim(val) + indicator));
592
+ }
593
+ }
594
+
595
+ const pluginChecks = d.plugin_checks as Array<{ name: string; ok: boolean; message?: string }> | undefined;
596
+ if (pluginChecks && pluginChecks.length > 0) {
597
+ lines.push(c.gray(" ─────"));
598
+ for (const check of pluginChecks) {
599
+ const indicator = check.ok ? c.green("✓") : c.red("✗");
600
+ const msg = check.message ? ` ${c.dim(check.message)}` : "";
601
+ lines.push(kv(check.name, `${indicator}${msg}`));
602
+ }
603
+ }
604
+
605
+ return lines.join("\n");
606
+ }
607
+
608
+ // ---------- abandon ----------------------------------------------------------
609
+
610
+ export function renderAbandon(data: unknown): string {
611
+ const results = data as Array<{ run_id: string; state_path: string; ok: boolean; error?: string }>;
612
+ const lines: string[] = [divider("abandon")];
613
+ for (const r of results) {
614
+ const status = r.ok ? c.green("✓ abandoned") : c.red(`✗ ${r.error ?? "failed"}`);
615
+ lines.push(kv(r.run_id, status));
616
+ if (!r.ok) {
617
+ lines.push(kv("path", c.dim(r.state_path)));
618
+ }
619
+ }
620
+ return lines.join("\n");
621
+ }
622
+
623
+ // ---------- wechat-export ----------------------------------------------------
624
+
625
+ export function renderWechatExport(data: unknown): string {
626
+ const d = data as Record<string, unknown>;
627
+ const lines: string[] = [divider("wechat-export")];
628
+ for (const [k, v] of Object.entries(d)) {
629
+ lines.push(kv(k, typeof v === "object" ? JSON.stringify(v) : String(v ?? "")));
630
+ }
631
+ return lines.join("\n");
632
+ }