@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,226 @@
1
+ import { readdir, readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { parseArgs } from "../args";
4
+
5
+ const METRICS_DIR = join(
6
+ process.env.HOME || "~",
7
+ ".pipeline-workspace",
8
+ "tasks",
9
+ "hermes",
10
+ );
11
+
12
+ interface TaskMetrics {
13
+ task_id: string;
14
+ session_id: string;
15
+ model: string;
16
+ provider: string;
17
+ started_at: string | null;
18
+ ended_at: string | null;
19
+ completed: boolean | null;
20
+ interrupted: boolean | null;
21
+ turns: number;
22
+ api_calls: number;
23
+ tokens: {
24
+ input: number;
25
+ output: number;
26
+ cache_read: number;
27
+ cache_write: number;
28
+ reasoning: number;
29
+ };
30
+ estimated_cost_usd: number;
31
+ api_latency_ms_total: number;
32
+ tool_calls: Record<string, number>;
33
+ tool_calls_total: number;
34
+ }
35
+
36
+ function formatMs(ms: number): string {
37
+ if (ms < 1000) return `${ms}ms`;
38
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
39
+ return `${(ms / 60_000).toFixed(1)}m`;
40
+ }
41
+
42
+ function formatTokens(n: number): string {
43
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
44
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
45
+ return String(n);
46
+ }
47
+
48
+ function renderSingle(m: TaskMetrics): string {
49
+ const lines: string[] = [];
50
+ const status =
51
+ m.ended_at == null
52
+ ? "⏳ running"
53
+ : m.completed
54
+ ? "✅ completed"
55
+ : m.interrupted
56
+ ? "🛑 interrupted"
57
+ : "❓ unknown";
58
+
59
+ lines.push(`Task: ${m.task_id}`);
60
+ lines.push(`Status: ${status}`);
61
+ lines.push(`Model: ${m.model} (${m.provider})`);
62
+ if (m.started_at) lines.push(`Started: ${m.started_at}`);
63
+ if (m.ended_at) lines.push(`Ended: ${m.ended_at}`);
64
+ lines.push("");
65
+ lines.push("Iterations:");
66
+ lines.push(` Turns: ${m.turns}`);
67
+ lines.push(` API calls: ${m.api_calls}`);
68
+ lines.push(` API latency: ${formatMs(m.api_latency_ms_total)}`);
69
+ lines.push("");
70
+ lines.push("Tokens:");
71
+ lines.push(` Input: ${formatTokens(m.tokens.input)}`);
72
+ lines.push(` Output: ${formatTokens(m.tokens.output)}`);
73
+ lines.push(` Cache read: ${formatTokens(m.tokens.cache_read)}`);
74
+ lines.push(` Cache write: ${formatTokens(m.tokens.cache_write)}`);
75
+ lines.push(` Reasoning: ${formatTokens(m.tokens.reasoning)}`);
76
+ const totalTokens =
77
+ m.tokens.input +
78
+ m.tokens.output +
79
+ m.tokens.cache_read +
80
+ m.tokens.cache_write +
81
+ m.tokens.reasoning;
82
+ lines.push(` Total: ${formatTokens(totalTokens)}`);
83
+ lines.push("");
84
+ lines.push(`Cost: $${m.estimated_cost_usd.toFixed(4)}`);
85
+ lines.push("");
86
+
87
+ if (m.tool_calls_total > 0) {
88
+ lines.push(`Tool calls (${m.tool_calls_total}):`);
89
+ const sorted = Object.entries(m.tool_calls).sort((a, b) => b[1] - a[1]);
90
+ for (const [name, count] of sorted) {
91
+ lines.push(` ${name}: ${count}`);
92
+ }
93
+ } else {
94
+ lines.push("Tool calls: none");
95
+ }
96
+
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function renderTable(metricsList: TaskMetrics[]): string {
101
+ if (metricsList.length === 0) return "No metrics found.";
102
+
103
+ const header = [
104
+ "task_id".padEnd(32),
105
+ "status".padEnd(14),
106
+ "turns".padStart(5),
107
+ "api".padStart(4),
108
+ "tools".padStart(5),
109
+ "tokens".padStart(10),
110
+ "cost".padStart(9),
111
+ ].join(" ");
112
+
113
+ const sep = "─".repeat(header.length);
114
+ const lines = [header, sep];
115
+
116
+ for (const m of metricsList) {
117
+ const status =
118
+ m.ended_at == null
119
+ ? "running"
120
+ : m.completed
121
+ ? "done"
122
+ : m.interrupted
123
+ ? "interrupted"
124
+ : "unknown";
125
+
126
+ const totalTokens =
127
+ m.tokens.input +
128
+ m.tokens.output +
129
+ m.tokens.cache_read +
130
+ m.tokens.cache_write +
131
+ m.tokens.reasoning;
132
+
133
+ const row = [
134
+ m.task_id.padEnd(32).slice(0, 32),
135
+ status.padEnd(14),
136
+ String(m.turns).padStart(5),
137
+ String(m.api_calls).padStart(4),
138
+ String(m.tool_calls_total).padStart(5),
139
+ formatTokens(totalTokens).padStart(10),
140
+ `$${m.estimated_cost_usd.toFixed(4)}`.padStart(9),
141
+ ].join(" ");
142
+
143
+ lines.push(row);
144
+ }
145
+
146
+ lines.push("");
147
+ lines.push(`${metricsList.length} task(s)`);
148
+ return lines.join("\n");
149
+ }
150
+
151
+ async function readMetricsFile(filePath: string): Promise<TaskMetrics | null> {
152
+ try {
153
+ const raw = await readFile(filePath, "utf-8");
154
+ return JSON.parse(raw) as TaskMetrics;
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ export async function hermesMetrics(args: string[]): Promise<void> {
161
+ const parsed = parseArgs(args);
162
+ const positional = JSON.parse(String(parsed._ ?? "[]")) as string[];
163
+
164
+ if (parsed.help) {
165
+ console.log(`
166
+ Usage: zzhub-pipeline hermes-metrics [run_id] [--json]
167
+
168
+ Show execution metrics collected by the Hermes pipeline-metrics plugin.
169
+
170
+ Options:
171
+ --json Output raw JSON instead of table
172
+
173
+ Without a run_id, lists all tasks in a summary table.
174
+ With a run_id, shows detailed metrics for that task.
175
+ `.trim());
176
+ return;
177
+ }
178
+
179
+ const asJson = parsed.json === true || parsed.json === "true";
180
+
181
+ // List mode: no positional arg
182
+ if (positional.length === 0) {
183
+ let entries: string[];
184
+ try {
185
+ entries = (await readdir(METRICS_DIR)).filter((f) =>
186
+ f.endsWith(".json"),
187
+ );
188
+ } catch {
189
+ console.log("No metrics found (directory does not exist).");
190
+ return;
191
+ }
192
+
193
+ if (entries.length === 0) {
194
+ console.log("No metrics found.");
195
+ return;
196
+ }
197
+
198
+ const metricsList: TaskMetrics[] = [];
199
+ for (const entry of entries.sort()) {
200
+ const m = await readMetricsFile(join(METRICS_DIR, entry));
201
+ if (m) metricsList.push(m);
202
+ }
203
+
204
+ if (asJson) {
205
+ console.log(JSON.stringify(metricsList, null, 2));
206
+ } else {
207
+ console.log(renderTable(metricsList));
208
+ }
209
+ return;
210
+ }
211
+
212
+ // Single task mode
213
+ const runId = positional[0];
214
+ const filePath = join(METRICS_DIR, `${runId}.json`);
215
+ const m = await readMetricsFile(filePath);
216
+
217
+ if (!m) {
218
+ throw new Error(`No metrics found for run_id: ${runId}`);
219
+ }
220
+
221
+ if (asJson) {
222
+ console.log(JSON.stringify(m, null, 2));
223
+ } else {
224
+ console.log(renderSingle(m));
225
+ }
226
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ runRenderArticleCli,
3
+ runRenderAsciiPortraitCli,
4
+ runRenderCardCli,
5
+ runRenderXLikePostsCli,
6
+ } from "../imgx";
7
+ import { ensureFonts } from "../runtime-paths";
8
+
9
+ type ImgxHandler = (args: string[]) => void;
10
+
11
+ const IMGX_COMMANDS: Record<string, ImgxHandler> = {
12
+ "render-article": runRenderArticleCli,
13
+ "render-ascii-portrait": runRenderAsciiPortraitCli,
14
+ "render-card": runRenderCardCli,
15
+ "render-x-like-posts": runRenderXLikePostsCli,
16
+ };
17
+
18
+ export async function imgxCommand(args: string[]): Promise<void> {
19
+ const subcommand = args[0];
20
+
21
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
22
+ printUsage();
23
+ return;
24
+ }
25
+
26
+ const handler = IMGX_COMMANDS[subcommand];
27
+ if (!handler) {
28
+ throw new Error(`Unknown imgx subcommand: ${subcommand}`);
29
+ }
30
+
31
+ await ensureFonts();
32
+ try {
33
+ handler(args.slice(1));
34
+ } catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ if (msg.includes("@napi-rs/canvas") || msg.includes("Cannot find package")) {
37
+ throw new Error(
38
+ "@napi-rs/canvas not installed. Required for image rendering.\n" +
39
+ "Install it with:\n" +
40
+ " npm install @napi-rs/canvas\n" +
41
+ " # or: bun add @napi-rs/canvas",
42
+ );
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+
48
+ function printUsage(): void {
49
+ console.log(
50
+ [
51
+ "Usage: zzhub-pipeline imgx <subcommand> [options]",
52
+ "",
53
+ "Subcommands:",
54
+ " render-card Render poster-3-4 / wechat-cover-split / tips-3-4",
55
+ " render-article Render longform-3-4 article pages",
56
+ " render-ascii-portrait Render ascii portrait posters",
57
+ " render-x-like-posts Render X-like posts cards",
58
+ ].join("\n"),
59
+ );
60
+ }