@zzclub/pipeline 0.2.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 -432
  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
@@ -0,0 +1,88 @@
1
+ import { readFile } from "fs/promises";
2
+
3
+ import { parseArgs, optionalArg, requireArg } from "../args";
4
+ import { printResult, renderTaskShape } from "../output";
5
+ import { readState, writeState, type BodyInputReceived } from "../state";
6
+ import { getTaskByStatePath } from "../task-manager";
7
+ import { reconcileStateArtifacts } from "../workflow-materials";
8
+
9
+ function parseReceivedImages(raw: unknown): BodyInputReceived[] {
10
+ if (Array.isArray(raw)) {
11
+ return raw.flatMap((item) => {
12
+ if (!item || typeof item !== "object") {
13
+ return [];
14
+ }
15
+ const input = item as Record<string, unknown>;
16
+ const marker = typeof input.marker === "string" ? input.marker.trim() : "";
17
+ const path = typeof input.path === "string" ? input.path.trim() : "";
18
+ return marker && path ? [{ marker, path }] : [];
19
+ });
20
+ }
21
+
22
+ if (raw && typeof raw === "object") {
23
+ const input = raw as Record<string, unknown>;
24
+ if (Array.isArray(input.images)) {
25
+ return parseReceivedImages(input.images);
26
+ }
27
+ return Object.entries(input).flatMap(([marker, path]) => {
28
+ const normalizedMarker = marker.trim();
29
+ const normalizedPath = typeof path === "string" ? path.trim() : "";
30
+ return normalizedMarker && normalizedPath
31
+ ? [{ marker: normalizedMarker, path: normalizedPath }]
32
+ : [];
33
+ });
34
+ }
35
+
36
+ return [];
37
+ }
38
+
39
+ export async function attachBodyImages(args: string[]): Promise<void> {
40
+ const parsed = parseArgs(args);
41
+
42
+ if (parsed.help) {
43
+ console.log(`
44
+ Usage: zzhub-pipeline attach-body-images [options]
45
+
46
+ Options:
47
+ --state Path to state JSON (required)
48
+ --images-file JSON file mapping marker -> path or [{ marker, path }] (required)
49
+ --scope article | newspic-longform (optional)
50
+ --layout Layout hint such as staggered/editorial (optional)
51
+ `.trim());
52
+ return;
53
+ }
54
+
55
+ const statePath = requireArg(parsed, "state", "state JSON path");
56
+ const imagesFile = requireArg(parsed, "images-file", "JSON file with marker/path data");
57
+ const scope = optionalArg(parsed, "scope");
58
+ const layout = optionalArg(parsed, "layout");
59
+
60
+ const state = await readState(statePath);
61
+ const raw = JSON.parse(await readFile(imagesFile, "utf-8")) as unknown;
62
+ const incoming = parseReceivedImages(raw);
63
+ const merged = new Map(state.images.body_inputs.received.map((item) => [item.marker, item.path]));
64
+ for (const item of incoming) {
65
+ merged.set(item.marker, item.path);
66
+ }
67
+
68
+ state.images.body_inputs.received = [...merged.entries()].map(([marker, path]) => ({ marker, path }));
69
+ if (scope === "article" || scope === "newspic-longform") {
70
+ state.images.body_inputs.scope = scope;
71
+ }
72
+ if (layout) {
73
+ state.images.body_inputs.layout = layout;
74
+ }
75
+
76
+ await reconcileStateArtifacts(state);
77
+ await writeState(statePath, state);
78
+
79
+ const task = await getTaskByStatePath(statePath);
80
+ printResult(
81
+ {
82
+ summary: task.summary,
83
+ blockers: task.blockers,
84
+ next_action: task.next_action,
85
+ },
86
+ renderTaskShape,
87
+ );
88
+ }
@@ -0,0 +1,86 @@
1
+ import { copyFile, mkdir, writeFile } from "fs/promises";
2
+ import { extname, join, resolve } from "path";
3
+
4
+ import { parseArgs, optionalArg, requireArg } from "../args";
5
+ import { printResult, renderTaskShape } from "../output";
6
+ import { resolveWorkspacePaths } from "../config";
7
+ import { readState, writeState } from "../state";
8
+ import { getTaskByStatePath } from "../task-manager";
9
+ import { reconcileStateArtifacts } from "../workflow-materials";
10
+
11
+ async function stageManagedBodyFile(
12
+ workspaceRoot: string,
13
+ runId: string,
14
+ sourcePath: string | null,
15
+ bodyText: string | null,
16
+ ): Promise<string> {
17
+ const tempRoot = resolveWorkspacePaths(workspaceRoot).tempRoot;
18
+ const extension = sourcePath ? extname(sourcePath) || ".md" : ".md";
19
+ const managedDir = join(tempRoot, runId);
20
+ const managedPath = join(managedDir, `source-body${extension}`);
21
+ await mkdir(managedDir, { recursive: true });
22
+
23
+ if (bodyText !== null) {
24
+ await writeFile(managedPath, bodyText, "utf-8");
25
+ } else if (sourcePath) {
26
+ if (resolve(sourcePath) !== resolve(managedPath)) {
27
+ await copyFile(sourcePath, managedPath);
28
+ }
29
+ } else {
30
+ throw new Error("Either sourcePath or bodyText must be provided");
31
+ }
32
+
33
+ return managedPath;
34
+ }
35
+
36
+ export async function attachBody(args: string[]): Promise<void> {
37
+ const parsed = parseArgs(args);
38
+
39
+ if (parsed.help) {
40
+ console.log(`
41
+ Usage: zzhub-pipeline attach-body [options]
42
+
43
+ Options:
44
+ --state Path to state JSON (required)
45
+ --body Path to markdown body file (optional)
46
+ --body-text Inline markdown body text (optional)
47
+ `.trim());
48
+ return;
49
+ }
50
+
51
+ const statePath = requireArg(parsed, "state", "state JSON path");
52
+ const bodyPath = optionalArg(parsed, "body");
53
+ const bodyText = optionalArg(parsed, "body-text");
54
+ if (!bodyPath && bodyText === undefined) {
55
+ throw new Error("Missing required argument: --body or --body-text");
56
+ }
57
+ if (bodyPath && bodyText !== undefined) {
58
+ throw new Error("Use either --body or --body-text, not both");
59
+ }
60
+
61
+ const state = await readState(statePath);
62
+ state.source_body_path = await stageManagedBodyFile(
63
+ state.workspace_root,
64
+ state.run_id,
65
+ bodyPath ?? null,
66
+ bodyText ?? null,
67
+ );
68
+ await reconcileStateArtifacts(state);
69
+ await writeState(statePath, state);
70
+
71
+ const task = await getTaskByStatePath(statePath);
72
+ printResult(
73
+ {
74
+ summary: {
75
+ run_id: task.summary.run_id,
76
+ mode: task.summary.mode,
77
+ phase: task.summary.phase,
78
+ route: task.summary.route,
79
+ metadata: task.summary.metadata,
80
+ },
81
+ blockers: task.blockers,
82
+ next_action: task.next_action,
83
+ },
84
+ renderTaskShape,
85
+ );
86
+ }
@@ -0,0 +1,41 @@
1
+ import { readFile } from "fs/promises";
2
+
3
+ import { parseArgs, requireArg } from "../args";
4
+ import { printResult, renderTaskShape } from "../output";
5
+ import { normalizeNewspicRenderSpec, readState, writeState } from "../state";
6
+ import { getTaskByStatePath } from "../task-manager";
7
+ import { reconcileStateArtifacts } from "../workflow-materials";
8
+
9
+ export async function attachNewspicSpec(args: string[]): Promise<void> {
10
+ const parsed = parseArgs(args);
11
+
12
+ if (parsed.help) {
13
+ console.log(`
14
+ Usage: zzhub-pipeline attach-newspic-spec [options]
15
+
16
+ Options:
17
+ --state Path to state JSON (required)
18
+ --file Path to newspic render spec JSON (required)
19
+ `.trim());
20
+ return;
21
+ }
22
+
23
+ const statePath = requireArg(parsed, "state", "state JSON path");
24
+ const specPath = requireArg(parsed, "file", "newspic render spec file");
25
+
26
+ const state = await readState(statePath);
27
+ const spec = JSON.parse(await readFile(specPath, "utf-8")) as unknown;
28
+ state.intent.newspic_render = normalizeNewspicRenderSpec(spec);
29
+ await reconcileStateArtifacts(state);
30
+ await writeState(statePath, state);
31
+
32
+ const task = await getTaskByStatePath(statePath);
33
+ printResult(
34
+ {
35
+ summary: task.summary,
36
+ blockers: task.blockers,
37
+ next_action: task.next_action,
38
+ },
39
+ renderTaskShape,
40
+ );
41
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtemp, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+
6
+ import { checkpoint } from "./checkpoint";
7
+ import { defaultState, writeState } from "../state";
8
+
9
+ async function makeTempDir(prefix: string): Promise<string> {
10
+ return mkdtemp(join(tmpdir(), prefix));
11
+ }
12
+
13
+ async function captureJsonOutput<T>(fn: () => Promise<void>): Promise<T> {
14
+ const original = console.log;
15
+ const lines: string[] = [];
16
+ console.log = (...args: unknown[]) => {
17
+ lines.push(args.map((value) => String(value)).join(" "));
18
+ };
19
+ try {
20
+ await fn();
21
+ } finally {
22
+ console.log = original;
23
+ }
24
+ return JSON.parse(lines.join("\n").trim()) as T;
25
+ }
26
+
27
+ async function captureTextOutput(fn: () => Promise<void>): Promise<string> {
28
+ const original = console.log;
29
+ const lines: string[] = [];
30
+ console.log = (...args: unknown[]) => {
31
+ lines.push(args.map((value) => String(value)).join(" "));
32
+ };
33
+ try {
34
+ await fn();
35
+ } finally {
36
+ console.log = original;
37
+ }
38
+ return lines.join("\n").trim();
39
+ }
40
+
41
+ describe("checkpoint", () => {
42
+ test("reports validation passing for a well-formed prepare state", async () => {
43
+ const configPath = join(tmpdir(), `zzhub-test-checkpoint-${process.pid}-${Date.now()}.json`);
44
+ await writeFile(configPath, "{}", "utf-8");
45
+ process.env.ZZHUB_PIPELINE_CONFIG = configPath;
46
+
47
+ const workspace = await makeTempDir("zzhub-checkpoint-");
48
+ const statePath = join(workspace, "state.json");
49
+
50
+ const state = defaultState();
51
+ state.workspace_root = workspace;
52
+ state.state_path = statePath;
53
+ state.run_id = "run-checkpoint-1";
54
+ state.phase.current = "prepare";
55
+ await writeState(statePath, state);
56
+
57
+ const output = await captureJsonOutput<{
58
+ validation: { phase_checked: string; valid: boolean; errors: unknown[] };
59
+ next_action: { action: string };
60
+ }>(() => checkpoint(["--state", statePath]));
61
+
62
+ expect(output.validation.phase_checked).toBe("prepare");
63
+ expect(output.validation.valid).toBe(true);
64
+ expect(output.validation.errors).toEqual([]);
65
+ expect(output.next_action).toBeDefined();
66
+ });
67
+
68
+ test("agent view renders markdown with next action", async () => {
69
+ const configPath = join(tmpdir(), `zzhub-test-checkpoint-${process.pid}-${Date.now()}.json`);
70
+ await writeFile(configPath, "{}", "utf-8");
71
+ process.env.ZZHUB_PIPELINE_CONFIG = configPath;
72
+
73
+ const workspace = await makeTempDir("zzhub-checkpoint-view-");
74
+ const statePath = join(workspace, "state.json");
75
+
76
+ const state = defaultState();
77
+ state.workspace_root = workspace;
78
+ state.state_path = statePath;
79
+ state.run_id = "run-checkpoint-view";
80
+ state.route.primary = "wechat-article";
81
+ state.metadata.title = "Test Title";
82
+ await writeState(statePath, state);
83
+
84
+ const output = await captureTextOutput(() =>
85
+ checkpoint(["--state", statePath, "--view", "agent"]),
86
+ );
87
+
88
+ expect(output).toContain("# Current Task");
89
+ expect(output).toContain("## Next Action");
90
+ });
91
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * checkpoint — Read and validate state, report phase status.
3
+ *
4
+ * Used by orchestrator to check state at phase boundaries.
5
+ *
6
+ * Usage:
7
+ * zzhub-pipeline checkpoint --state /path/to/state.json [--phase render]
8
+ *
9
+ * Output: JSON with state summary and validation errors.
10
+ */
11
+
12
+ import { parseArgs, requireArg, optionalArg } from "../args";
13
+ import { printResult, renderTaskShape } from "../output";
14
+ import { getTaskByStatePath } from "../task-manager";
15
+ import { parseTaskViewMode, renderTaskStatusMarkdown } from "../task-views";
16
+ import type { PhaseName } from "../state";
17
+ import { validateForPhase } from "../state";
18
+
19
+ export async function checkpoint(args: string[]): Promise<void> {
20
+ const parsed = parseArgs(args);
21
+
22
+ if (parsed.help) {
23
+ console.log(`
24
+ Usage: zzhub-pipeline checkpoint [options]
25
+
26
+ Options:
27
+ --state Path to state JSON (required)
28
+ --phase Phase to validate for (optional; defaults to state.phase.current)
29
+ --view json | markdown | agent (default: json)
30
+ `.trim());
31
+ return;
32
+ }
33
+
34
+ const statePath = requireArg(parsed, "state", "state JSON path");
35
+ const phaseOverride = optionalArg(parsed, "phase") as PhaseName | undefined;
36
+ const view = parseTaskViewMode(optionalArg(parsed, "view"));
37
+
38
+ const task = await getTaskByStatePath(statePath);
39
+ const phase = phaseOverride ?? task.state.phase.current;
40
+ const errors = validateForPhase(task.state, phase);
41
+
42
+ const output = {
43
+ summary: task.summary,
44
+ validation: {
45
+ phase_checked: phase,
46
+ valid: errors.length === 0,
47
+ errors,
48
+ },
49
+ gaps: task.gaps,
50
+ blockers: task.blockers,
51
+ next_action: task.next_action,
52
+ };
53
+
54
+ if (view !== "json") {
55
+ console.log(renderTaskStatusMarkdown({
56
+ ...task,
57
+ validation: output.validation,
58
+ }, view));
59
+ } else {
60
+ printResult(output, renderTaskShape);
61
+ }
62
+
63
+ if (errors.length > 0) {
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,106 @@
1
+ import { readFileSync } from "fs";
2
+ import { parseArgs, optionalArg, flagArg } from "../args";
3
+ import { printResult, renderConfig } from "../output";
4
+ import {
5
+ configSummary,
6
+ getConfigValue,
7
+ loadConfig,
8
+ PipelineConfigSchema,
9
+ redactConfig,
10
+ redactConfigValue,
11
+ saveConfig,
12
+ setConfigValue,
13
+ } from "../config";
14
+
15
+ export async function configCommand(args: string[]): Promise<void> {
16
+ const parsed = parseArgs(args);
17
+
18
+ if (parsed.help) {
19
+ console.log(`
20
+ Usage: zzhub-pipeline config [options]
21
+
22
+ Options:
23
+ --key Dot-path key to read or update
24
+ --value New value for --key
25
+ --json Force JSON output for scalar reads
26
+ --export Print full config as JSON (secrets redacted; use --raw to show all)
27
+ --import Path to a JSON file to merge into current config
28
+ --raw Show secrets unredacted (use with --export)
29
+ `.trim());
30
+ return;
31
+ }
32
+
33
+ const config = loadConfig();
34
+
35
+ // --export
36
+ if (flagArg(parsed, "export")) {
37
+ const raw = flagArg(parsed, "raw");
38
+ const output = raw ? config : redactConfig(config);
39
+ console.log(JSON.stringify(output, null, 2));
40
+ return;
41
+ }
42
+
43
+ // --import
44
+ const importPath = optionalArg(parsed, "import");
45
+ if (importPath) {
46
+ let imported: unknown;
47
+ try {
48
+ imported = JSON.parse(readFileSync(importPath, "utf-8"));
49
+ } catch (err) {
50
+ throw new Error(`Failed to read import file: ${err instanceof Error ? err.message : String(err)}`);
51
+ }
52
+
53
+ // Merge: existing values win, imported fills gaps
54
+ const importedObj = (typeof imported === "object" && imported !== null ? imported : {}) as Record<string, unknown>;
55
+ const mergedWx = {
56
+ ...config.wx,
57
+ ...(importedObj.wx as Record<string, unknown> ?? {}),
58
+ accounts: {
59
+ ...config.wx.accounts,
60
+ ...((importedObj.wx as Record<string, unknown>)?.accounts as Record<string, unknown> ?? {}),
61
+ },
62
+ };
63
+ const raw = {
64
+ paths: { ...config.paths, ...(importedObj.paths ?? {}) },
65
+ services: { ...config.services, ...(importedObj.services ?? {}) },
66
+ commands: { ...config.commands, ...(importedObj.commands ?? {}) },
67
+ wx: mergedWx,
68
+ cos: { ...config.cos, ...(importedObj.cos ?? {}) },
69
+ plugins: { ...config.plugins, ...(importedObj.plugins ?? {}) },
70
+ };
71
+ // Validate through Zod schema — strips unknown fields, applies defaults
72
+ const merged = PipelineConfigSchema.parse(raw);
73
+
74
+ saveConfig(merged);
75
+ printResult(configSummary(merged), renderConfig);
76
+ return;
77
+ }
78
+
79
+ // --key / --value
80
+ const key = optionalArg(parsed, "key");
81
+ const value = optionalArg(parsed, "value");
82
+ const forceJson = flagArg(parsed, "json");
83
+
84
+ if (!key) {
85
+ printResult(configSummary(config), renderConfig);
86
+ return;
87
+ }
88
+
89
+ if (value === undefined) {
90
+ const currentValue = getConfigValue(config, key);
91
+ const redactedValue = redactConfigValue(key, currentValue);
92
+ if (forceJson || typeof currentValue !== "string") {
93
+ printResult({ key, value: redactedValue }, renderConfig);
94
+ return;
95
+ }
96
+ printResult(String(redactedValue), renderConfig);
97
+ return;
98
+ }
99
+
100
+ const nextConfig = setConfigValue(config, key, value);
101
+ saveConfig(nextConfig);
102
+ printResult(
103
+ { key, value: redactConfigValue(key, getConfigValue(nextConfig, key)) },
104
+ renderConfig,
105
+ );
106
+ }
@@ -0,0 +1,62 @@
1
+ import { optionalArg, parseArgs, requireArg } from "../args";
2
+ import { loadConfig } from "../config";
3
+ import { printHelp, printResult } from "../output";
4
+ import { uploadFileToCos, type CosUploadResult } from "../providers/cos";
5
+
6
+ type CosUploader = (input: {
7
+ localPath: string;
8
+ folder: string;
9
+ baseUrl: string;
10
+ cosPat: string;
11
+ publicBaseUrl: string;
12
+ }) => Promise<CosUploadResult>;
13
+
14
+ export function createCosUploadCommand(uploader: CosUploader = uploadFileToCos) {
15
+ return async function cosUploadCommand(args: string[]): Promise<void> {
16
+ const parsed = parseArgs(args);
17
+
18
+ if (parsed.help) {
19
+ printHelp(`
20
+ Usage: zzhub-pipeline cos-upload --file <path> [options]
21
+
22
+ Options:
23
+ --file Local image file path
24
+ --folder COS folder name, e.g. notes/note-id
25
+ --alt Markdown image alt text
26
+ --base-url Hub base URL for COS STS, defaults to wx.baseUrl
27
+ --pat COS PAT, defaults to cos.pat
28
+ --public-base-url Public CDN base URL
29
+ `.trim());
30
+ return;
31
+ }
32
+
33
+ const config = loadConfig();
34
+ const localPath = requireArg(parsed, "file", "local image file path");
35
+ const folder = optionalArg(parsed, "folder") || "uploads";
36
+ const baseUrl = optionalArg(parsed, "base-url") || config.cos.baseUrl || config.wx.baseUrl;
37
+ const cosPat = optionalArg(parsed, "pat") || config.cos.pat;
38
+ const publicBaseUrl = optionalArg(parsed, "public-base-url") || config.cos.publicBaseUrl;
39
+ const alt = optionalArg(parsed, "alt") || "";
40
+
41
+ if (!cosPat) {
42
+ throw new Error("No COS PAT configured. Set cos.pat in config or pass --pat.");
43
+ }
44
+
45
+ const result = await uploader({
46
+ localPath,
47
+ folder,
48
+ baseUrl,
49
+ cosPat,
50
+ publicBaseUrl,
51
+ });
52
+
53
+ printResult({
54
+ local_path: result.localPath,
55
+ key: result.key,
56
+ url: result.url,
57
+ markdown: `![${alt}](${result.url})`,
58
+ });
59
+ };
60
+ }
61
+
62
+ export const cosUpload = createCosUploadCommand();
@@ -0,0 +1,66 @@
1
+ import { existsSync } from "fs";
2
+ import { parseArgs, optionalArg } from "../args";
3
+ import { printResult, renderDoctor } from "../output";
4
+ import {
5
+ configSummary,
6
+ getPipelineConfigPath,
7
+ getLegacyZCliConfigPath,
8
+ loadConfig,
9
+ resolveWorkspaceRoot,
10
+ resolveWorkspacePaths,
11
+ } from "../config";
12
+ import { listPublishProviders } from "../providers";
13
+ import { resolveBunBinary } from "../spawn";
14
+ import { runPluginDoctorChecks } from "../adapter-loader";
15
+
16
+ export async function doctor(args: string[]): Promise<void> {
17
+ const parsed = parseArgs(args);
18
+
19
+ if (parsed.help) {
20
+ console.log(`
21
+ Usage: zzhub-pipeline doctor [options]
22
+
23
+ Options:
24
+ --workspace Workspace root to resolve default paths against
25
+ `.trim());
26
+ return;
27
+ }
28
+
29
+ const config = loadConfig();
30
+ const workspace = resolveWorkspaceRoot(optionalArg(parsed, "workspace"), config);
31
+ const paths = resolveWorkspacePaths(workspace, config);
32
+
33
+ let bunBinary: string | null = null;
34
+ let bunError: string | null = null;
35
+
36
+ try {
37
+ bunBinary = resolveBunBinary();
38
+ } catch (error) {
39
+ bunError = error instanceof Error ? error.message : String(error);
40
+ }
41
+
42
+ const pluginChecks = await runPluginDoctorChecks(config);
43
+
44
+ printResult(
45
+ {
46
+ ...configSummary(config),
47
+ config_path_exists: existsSync(getPipelineConfigPath()),
48
+ legacy_config_path_exists: existsSync(getLegacyZCliConfigPath()),
49
+ workspace,
50
+ resolved_paths: paths,
51
+ resolved_paths_exist: {
52
+ postsRoot: existsSync(paths.postsRoot),
53
+ tempRoot: existsSync(paths.tempRoot),
54
+ blogRoot: existsSync(paths.blogRoot),
55
+ zotepadExportHtml: existsSync(paths.zotepadExportHtml),
56
+ },
57
+ bundled_imgx: true,
58
+ bundled_wechat_preview: true,
59
+ bun_binary: bunBinary,
60
+ bun_error: bunError,
61
+ publish_providers: listPublishProviders(),
62
+ plugin_checks: pluginChecks,
63
+ },
64
+ renderDoctor,
65
+ );
66
+ }
@@ -0,0 +1,55 @@
1
+ import { parseArgs, optionalArg } from "../args";
2
+ import { printResult, renderTaskShape } from "../output";
3
+ import { findTask } from "../task-manager";
4
+ import { buildAgentTaskShape, parseTaskViewMode, renderTaskStatusMarkdown } from "../task-views";
5
+
6
+ export async function findRun(args: string[]): Promise<void> {
7
+ const parsed = parseArgs(args);
8
+
9
+ if (parsed.help) {
10
+ console.log(`
11
+ Usage: zzhub-pipeline find-run [options]
12
+
13
+ Options:
14
+ --workspace Workspace root (optional)
15
+ --run-id Exact run id
16
+ --route Primary route filter
17
+ --account Account filter
18
+ --mode Mode filter
19
+ --phase Current phase filter
20
+ --title-contains Match title substring
21
+ --active Prefer only in-flight tasks
22
+ --full Include the full state payload
23
+ --view json | markdown | agent | agent-json (default: json)
24
+ `.trim());
25
+ return;
26
+ }
27
+
28
+ const includeFull = parsed.full === true || parsed.full === "true";
29
+ const view = parseTaskViewMode(optionalArg(parsed, "view"));
30
+ const task = await findTask(optionalArg(parsed, "workspace"), {
31
+ run_id: optionalArg(parsed, "run-id"),
32
+ route: optionalArg(parsed, "route"),
33
+ account: optionalArg(parsed, "account"),
34
+ mode: optionalArg(parsed, "mode"),
35
+ phase: optionalArg(parsed, "phase"),
36
+ title_contains: optionalArg(parsed, "title-contains"),
37
+ active_only: parsed.active === true || parsed.active === "true",
38
+ });
39
+
40
+ if (!task) {
41
+ throw new Error("No matching task found");
42
+ }
43
+
44
+ if (view === "agent-json") {
45
+ console.log(JSON.stringify(buildAgentTaskShape(task), null, 2));
46
+ return;
47
+ }
48
+
49
+ if (view !== "json") {
50
+ console.log(renderTaskStatusMarkdown(task, view));
51
+ return;
52
+ }
53
+
54
+ printResult(includeFull ? task : task.summary, renderTaskShape);
55
+ }