@crafter/cli-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/archaeology/cache.d.ts +11 -0
  4. package/dist/archaeology/delegate.d.ts +43 -0
  5. package/dist/archaeology/index.d.ts +12 -0
  6. package/dist/archaeology/index.js +61 -0
  7. package/dist/archaeology/index.js.map +9 -0
  8. package/dist/archaeology/llm.d.ts +1 -0
  9. package/dist/archaeology/merge.d.ts +3 -0
  10. package/dist/archaeology/orchestrator.d.ts +25 -0
  11. package/dist/archaeology/prompts.d.ts +13 -0
  12. package/dist/archaeology/types.d.ts +101 -0
  13. package/dist/archaeology/validate.d.ts +18 -0
  14. package/dist/chunk-57gtsvhb.js +434 -0
  15. package/dist/chunk-57gtsvhb.js.map +16 -0
  16. package/dist/chunk-5aahbfr2.js +293 -0
  17. package/dist/chunk-5aahbfr2.js.map +10 -0
  18. package/dist/chunk-pkfpaae1.js +678 -0
  19. package/dist/chunk-pkfpaae1.js.map +15 -0
  20. package/dist/chunk-q4se2rwe.js +181 -0
  21. package/dist/chunk-q4se2rwe.js.map +14 -0
  22. package/dist/chunk-v5w3w6bd.js +168 -0
  23. package/dist/chunk-v5w3w6bd.js.map +11 -0
  24. package/dist/chunk-ykze151b.js +770 -0
  25. package/dist/chunk-ykze151b.js.map +16 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +433 -0
  28. package/dist/cli.js.map +10 -0
  29. package/dist/encoders/ansi.d.ts +2 -0
  30. package/dist/encoders/html.d.ts +10 -0
  31. package/dist/encoders/string.d.ts +2 -0
  32. package/dist/flow/encode.d.ts +5 -0
  33. package/dist/flow/index.d.ts +8 -0
  34. package/dist/flow/index.js +25 -0
  35. package/dist/flow/index.js.map +9 -0
  36. package/dist/flow/layout.d.ts +30 -0
  37. package/dist/flow/parse.d.ts +2 -0
  38. package/dist/flow/render.d.ts +3 -0
  39. package/dist/flow/types.d.ts +42 -0
  40. package/dist/flow/validate.d.ts +3 -0
  41. package/dist/flow/yaml.d.ts +4 -0
  42. package/dist/grid.d.ts +14 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.js +21 -0
  45. package/dist/index.js.map +9 -0
  46. package/dist/miner/history.d.ts +6 -0
  47. package/dist/miner/index.d.ts +18 -0
  48. package/dist/miner/index.js +38 -0
  49. package/dist/miner/index.js.map +9 -0
  50. package/dist/miner/sessions.d.ts +3 -0
  51. package/dist/miner/stats.d.ts +2 -0
  52. package/dist/miner/suggest.d.ts +11 -0
  53. package/dist/miner/transitions.d.ts +6 -0
  54. package/dist/miner/types.d.ts +46 -0
  55. package/dist/miner/workflows.d.ts +11 -0
  56. package/dist/parse.d.ts +3 -0
  57. package/dist/render.d.ts +3 -0
  58. package/dist/types.d.ts +39 -0
  59. package/package.json +85 -0
  60. package/skill/SKILL.md +263 -0
  61. package/skill/evals/evals.json +26 -0
  62. package/skill/install.sh +38 -0
  63. package/skill/references/archaeology-guide.md +157 -0
  64. package/skill/references/skill-template.md +120 -0
  65. package/src/archaeology/cache.ts +107 -0
  66. package/src/archaeology/delegate.ts +113 -0
  67. package/src/archaeology/index.ts +48 -0
  68. package/src/archaeology/llm.ts +10 -0
  69. package/src/archaeology/merge.ts +155 -0
  70. package/src/archaeology/orchestrator.ts +185 -0
  71. package/src/archaeology/prompts.ts +178 -0
  72. package/src/archaeology/types.ts +139 -0
  73. package/src/archaeology/validate.ts +157 -0
  74. package/src/cli.ts +451 -0
  75. package/src/encoders/ansi.ts +32 -0
  76. package/src/encoders/html.ts +78 -0
  77. package/src/encoders/string.ts +20 -0
  78. package/src/flow/encode.ts +21 -0
  79. package/src/flow/index.ts +15 -0
  80. package/src/flow/layout.ts +150 -0
  81. package/src/flow/parse.ts +100 -0
  82. package/src/flow/render.ts +186 -0
  83. package/src/flow/types.ts +45 -0
  84. package/src/flow/validate.ts +111 -0
  85. package/src/flow/yaml.ts +235 -0
  86. package/src/grid.ts +59 -0
  87. package/src/index.ts +24 -0
  88. package/src/miner/history.ts +156 -0
  89. package/src/miner/index.ts +76 -0
  90. package/src/miner/sessions.ts +39 -0
  91. package/src/miner/stats.ts +43 -0
  92. package/src/miner/suggest.ts +101 -0
  93. package/src/miner/transitions.ts +62 -0
  94. package/src/miner/types.ts +45 -0
  95. package/src/miner/workflows.ts +96 -0
  96. package/src/parse.ts +321 -0
  97. package/src/render.ts +182 -0
  98. package/src/types.ts +62 -0
  99. package/workflows/docker-deploy.yml +42 -0
  100. package/workflows/docker-parallel.yml +36 -0
  101. package/workflows/gh-issue-to-pr.yml +48 -0
  102. package/workflows/git-pr-flow.yml +36 -0
  103. package/workflows/kubectl-rollout.yml +37 -0
  104. package/workflows/npm-publish.yml +42 -0
package/src/cli.ts ADDED
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env bun
2
+ import { parseHelpRecursive, parseHelp } from "./parse";
3
+ import { printTree, treeToString, treeToHtml } from "./index";
4
+ import type { TreeOptions } from "./types";
5
+ import { parseWorkflow, validateWorkflow, flowToAnsi, flowToString, flowToHtml } from "./flow";
6
+ import type { FlowRenderOptions } from "./flow/types";
7
+ import { mineCli } from "./miner";
8
+ import { runArchaeology, NullDelegate } from "./archaeology";
9
+
10
+ const args = process.argv.slice(2);
11
+ const subcommand = args[0];
12
+
13
+ const HELP_SUBCOMMANDS = new Set(["flow", "mine", "archaeology"]);
14
+ const isHelpFlag = args.includes("--help") || args.includes("-h");
15
+
16
+ if (!subcommand || (isHelpFlag && !HELP_SUBCOMMANDS.has(subcommand ?? ""))) {
17
+ printMainHelp();
18
+ process.exit(0);
19
+ }
20
+
21
+ if (isHelpFlag) {
22
+ if (subcommand === "flow") printFlowHelp();
23
+ else if (subcommand === "mine") printMineHelp();
24
+ else if (subcommand === "archaeology") printArchaeologyHelp();
25
+ else printMainHelp();
26
+ process.exit(0);
27
+ }
28
+
29
+ if (subcommand === "flow") {
30
+ await runFlow(args.slice(1));
31
+ } else if (subcommand === "mine") {
32
+ await runMine(args.slice(1));
33
+ } else if (subcommand === "archaeology") {
34
+ await runArchaeologyCmd(args.slice(1));
35
+ } else {
36
+ await runTree(args);
37
+ }
38
+
39
+ function printMainHelp() {
40
+ console.log(`
41
+ clitree — Visualize and explore any CLI
42
+
43
+ Usage:
44
+ clitree <binary> [options] Render command tree from --help
45
+ clitree flow <file> [options] Render a workflow YAML as a DAG
46
+ clitree mine <binary> [options] Mine shell history for workflows
47
+ clitree archaeology <binary> [options] Full analysis: tree + mining + (LLM)
48
+
49
+ Examples:
50
+ clitree docker # basic tree
51
+ clitree mine git # "what workflows do I repeat with git?"
52
+ clitree archaeology bun # tree + mining + LLM archaeology
53
+
54
+ Subcommands:
55
+ tree Parse --help output (default; passing a binary name invokes this)
56
+ flow Render a workflow YAML file as an ASCII DAG
57
+ mine Discover workflows from your shell history
58
+ archaeology Run full analysis (tree + mine + LLM proposals when available)
59
+
60
+ Help for a subcommand: clitree <subcommand> --help
61
+ `);
62
+ }
63
+
64
+ function printMineHelp() {
65
+ console.log(`
66
+ cli-tree mine — Mine your shell history for CLI workflows
67
+
68
+ Usage:
69
+ cli-tree mine <binary> [options]
70
+
71
+ Examples:
72
+ cli-tree mine git
73
+ cli-tree mine docker --min-support 5
74
+ cli-tree mine bun --format json > bun-flows.json
75
+
76
+ Options:
77
+ --min-support <n> Minimum occurrences to count as a workflow (default: 3)
78
+ --history-path <p> Custom shell history path (default: ~/.zsh_history)
79
+ --format <fmt> Output format: ansi (default), json
80
+ --max-paths <n> Max workflows to show (default: 10)
81
+ -h, --help Show this help
82
+ `);
83
+ }
84
+
85
+ function printArchaeologyHelp() {
86
+ console.log(`
87
+ cli-tree archaeology — Full CLI analysis: tree + mining + LLM archaeology
88
+
89
+ Usage:
90
+ cli-tree archaeology <binary> [options]
91
+
92
+ When run from the command line without a delegate, only the deterministic
93
+ phases run (help parsing + shell history mining). LLM archaeology requires
94
+ running inside the clitree skill (see ./skill/SKILL.md).
95
+
96
+ Examples:
97
+ cli-tree archaeology git # help + history
98
+ cli-tree archaeology docker --no-cache # bypass cache
99
+ cli-tree archaeology bun --format json # JSON output
100
+
101
+ Options:
102
+ --no-cache Skip cache, always re-run
103
+ --max-depth <n> Max --help recursion depth (default: 2)
104
+ --format <fmt> Output format: ansi (default), json
105
+ -h, --help Show this help
106
+ `);
107
+ }
108
+
109
+ function printFlowHelp() {
110
+ console.log(`
111
+ cli-tree flow — Render a CLI workflow YAML as a DAG
112
+
113
+ Usage:
114
+ cli-tree flow <file> [options]
115
+
116
+ Examples:
117
+ cli-tree flow workflows/git-pr-flow.yml
118
+ cli-tree flow my-workflow.yml --format html > flow.html
119
+ cli-tree flow workflows/docker-deploy.yml --compact --no-color
120
+
121
+ Options:
122
+ --compact Hide description and legend
123
+ --no-color Disable colors
124
+ --format <fmt> Output format: ansi (default), string, html
125
+ --no-validate Skip validation
126
+ -h, --help Show this help
127
+ `);
128
+ }
129
+
130
+ async function runTree(args: string[]) {
131
+ const opts: TreeOptions = {
132
+ color: true,
133
+ showFlags: true,
134
+ showArgs: true,
135
+ showDescriptions: true,
136
+ showTypes: true,
137
+ showDefaults: true,
138
+ compact: false,
139
+ };
140
+
141
+ let depth = 2;
142
+ let format: "ansi" | "string" | "html" | "json" = "ansi";
143
+ let stdinName: string | null = null;
144
+ let binary: string | null = null;
145
+
146
+ for (let i = 0; i < args.length; i++) {
147
+ const arg = args[i]!;
148
+ if (arg === "--depth" && args[i + 1]) {
149
+ depth = Number.parseInt(args[++i]!, 10);
150
+ } else if (arg === "--compact") {
151
+ opts.compact = true;
152
+ opts.showDescriptions = false;
153
+ } else if (arg === "--no-flags") {
154
+ opts.showFlags = false;
155
+ } else if (arg === "--no-args") {
156
+ opts.showArgs = false;
157
+ } else if (arg === "--no-color") {
158
+ opts.color = false;
159
+ } else if (arg === "--format" && args[i + 1]) {
160
+ format = args[++i] as "ansi" | "string" | "html" | "json";
161
+ } else if (arg === "--stdin" && args[i + 1]) {
162
+ stdinName = args[++i]!;
163
+ } else if (!arg.startsWith("-")) {
164
+ binary = arg;
165
+ }
166
+ }
167
+
168
+ try {
169
+ let tree;
170
+ if (stdinName) {
171
+ const input = await readStdin();
172
+ tree = parseHelp(stdinName, input);
173
+ } else if (binary) {
174
+ tree = await parseHelpRecursive(binary, [], depth);
175
+ } else {
176
+ console.error("Error: provide a binary name or use --stdin <name>");
177
+ process.exit(1);
178
+ }
179
+
180
+ if (format === "json") {
181
+ console.log(JSON.stringify(tree, null, 2));
182
+ } else if (format === "html") {
183
+ console.log(treeToHtml(tree, opts));
184
+ } else if (format === "string") {
185
+ console.log(treeToString(tree, opts));
186
+ } else {
187
+ printTree(tree, opts);
188
+ }
189
+ } catch (err: any) {
190
+ console.error(`Error: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ async function runFlow(args: string[]) {
196
+ const opts: FlowRenderOptions = {
197
+ color: true,
198
+ showDescriptions: true,
199
+ showCommands: true,
200
+ compact: false,
201
+ };
202
+
203
+ let format: "ansi" | "string" | "html" = "ansi";
204
+ let file: string | null = null;
205
+ let skipValidate = false;
206
+
207
+ for (let i = 0; i < args.length; i++) {
208
+ const arg = args[i]!;
209
+ if (arg === "--compact") {
210
+ opts.compact = true;
211
+ opts.showDescriptions = false;
212
+ } else if (arg === "--no-color") {
213
+ opts.color = false;
214
+ } else if (arg === "--no-validate") {
215
+ skipValidate = true;
216
+ } else if (arg === "--format" && args[i + 1]) {
217
+ format = args[++i] as "ansi" | "string" | "html";
218
+ } else if (!arg.startsWith("-")) {
219
+ file = arg;
220
+ }
221
+ }
222
+
223
+ if (!file) {
224
+ console.error("Error: provide a workflow YAML file");
225
+ console.error("Run 'cli-tree flow --help' for usage");
226
+ process.exit(1);
227
+ }
228
+
229
+ try {
230
+ const text = await Bun.file(file).text();
231
+ const workflow = parseWorkflow(text);
232
+
233
+ if (!skipValidate) {
234
+ const result = validateWorkflow(workflow);
235
+ for (const err of result.errors) {
236
+ const prefix = err.severity === "error" ? "\x1b[31m✗" : "\x1b[33m⚠";
237
+ const reset = "\x1b[0m";
238
+ const location = err.node ? ` [${err.node}]` : err.edge ? ` [${err.edge.from}→${err.edge.to}]` : "";
239
+ console.error(`${prefix}${location} ${err.message}${reset}`);
240
+ }
241
+ if (!result.valid) process.exit(1);
242
+ }
243
+
244
+ if (format === "html") {
245
+ console.log(flowToHtml(workflow, opts));
246
+ } else if (format === "string") {
247
+ console.log(flowToString(workflow, opts));
248
+ } else {
249
+ console.log(flowToAnsi(workflow, opts));
250
+ }
251
+ } catch (err: any) {
252
+ console.error(`Error: ${err.message}`);
253
+ process.exit(1);
254
+ }
255
+ }
256
+
257
+ async function readStdin(): Promise<string> {
258
+ const chunks: Buffer[] = [];
259
+ for await (const chunk of process.stdin) {
260
+ chunks.push(chunk as Buffer);
261
+ }
262
+ return Buffer.concat(chunks).toString("utf-8");
263
+ }
264
+
265
+ async function runMine(args: string[]) {
266
+ let binary: string | null = null;
267
+ let minSupport = 3;
268
+ let historyPath: string | undefined;
269
+ let format: "ansi" | "json" = "ansi";
270
+ let maxPaths = 10;
271
+
272
+ for (let i = 0; i < args.length; i++) {
273
+ const arg = args[i]!;
274
+ if (arg === "--min-support" && args[i + 1]) {
275
+ minSupport = Number.parseInt(args[++i]!, 10);
276
+ } else if (arg === "--history-path" && args[i + 1]) {
277
+ historyPath = args[++i]!;
278
+ } else if (arg === "--format" && args[i + 1]) {
279
+ format = args[++i] as "ansi" | "json";
280
+ } else if (arg === "--max-paths" && args[i + 1]) {
281
+ maxPaths = Number.parseInt(args[++i]!, 10);
282
+ } else if (!arg.startsWith("-")) {
283
+ binary = arg;
284
+ }
285
+ }
286
+
287
+ if (!binary) {
288
+ console.error("Error: provide a binary name");
289
+ console.error("Run 'cli-tree mine --help' for usage");
290
+ process.exit(1);
291
+ }
292
+
293
+ try {
294
+ const result = await mineCli(binary, { minSupport, historyPath });
295
+
296
+ if (format === "json") {
297
+ console.log(JSON.stringify(result, null, 2));
298
+ return;
299
+ }
300
+
301
+ const C = {
302
+ reset: "\x1b[0m",
303
+ bold: "\x1b[1m",
304
+ dim: "\x1b[2m",
305
+ cyan: "\x1b[36m",
306
+ green: "\x1b[32m",
307
+ yellow: "\x1b[33m",
308
+ magenta: "\x1b[35m",
309
+ gray: "\x1b[90m",
310
+ };
311
+
312
+ console.log(`\n${C.bold}${C.magenta}${binary}${C.reset} ${C.gray}— shell history analysis${C.reset}\n`);
313
+ console.log(`${C.bold}Usage:${C.reset}`);
314
+ console.log(` ${C.dim}Invocations:${C.reset} ${C.cyan}${result.stats.totalInvocations}${C.reset}`);
315
+ console.log(` ${C.dim}Subcommands:${C.reset} ${C.cyan}${result.stats.uniqueSubcommands}${C.reset}`);
316
+ console.log(` ${C.dim}Sessions:${C.reset} ${C.cyan}${result.sessionsAnalyzed}${C.reset}\n`);
317
+
318
+ if (result.stats.topSubcommands.length > 0) {
319
+ console.log(`${C.bold}Top subcommands:${C.reset}`);
320
+ const max = result.stats.topSubcommands[0]!.count;
321
+ for (const { subcommand, count } of result.stats.topSubcommands.slice(0, 5)) {
322
+ const bar = "█".repeat(Math.max(1, Math.round((count / max) * 30)));
323
+ console.log(` ${C.green}${subcommand.padEnd(20)}${C.reset} ${C.cyan}${bar}${C.reset} ${C.dim}${count}${C.reset}`);
324
+ }
325
+ console.log();
326
+ }
327
+
328
+ if (result.workflows.length > 0) {
329
+ console.log(`${C.bold}Discovered workflows:${C.reset}`);
330
+ for (const wf of result.workflows.slice(0, maxPaths)) {
331
+ const chain = wf.path[0]!.map(p => `${C.green}${p}${C.reset}`).join(` ${C.gray}→${C.reset} `);
332
+ console.log(` ${chain} ${C.dim}(${wf.support}×)${C.reset}`);
333
+ }
334
+ console.log();
335
+ }
336
+
337
+ if (result.suggestions.length > 0) {
338
+ console.log(`${C.bold}${C.yellow}💡 Skill suggestions:${C.reset}`);
339
+ for (const s of result.suggestions.slice(0, 3)) {
340
+ const badge =
341
+ s.priority === "high"
342
+ ? `${C.green}[HIGH]${C.reset}`
343
+ : s.priority === "medium"
344
+ ? `${C.yellow}[MED]${C.reset}`
345
+ : `${C.dim}[LOW]${C.reset}`;
346
+ console.log(`\n ${badge} ${C.bold}/${s.name}${C.reset} — ${s.description}`);
347
+ console.log(` ${C.dim}${s.reason}${C.reset}`);
348
+ console.log(` ${C.dim}${s.commands.join(" && ")}${C.reset}`);
349
+ }
350
+ console.log();
351
+ }
352
+ } catch (err: any) {
353
+ console.error(`Error: ${err.message}`);
354
+ process.exit(1);
355
+ }
356
+ }
357
+
358
+ async function runArchaeologyCmd(args: string[]) {
359
+ let binary: string | null = null;
360
+ let skipCache = false;
361
+ let maxDepth = 2;
362
+ let format: "ansi" | "json" = "ansi";
363
+
364
+ for (let i = 0; i < args.length; i++) {
365
+ const arg = args[i]!;
366
+ if (arg === "--no-cache") {
367
+ skipCache = true;
368
+ } else if (arg === "--max-depth" && args[i + 1]) {
369
+ maxDepth = Number.parseInt(args[++i]!, 10);
370
+ } else if (arg === "--format" && args[i + 1]) {
371
+ format = args[++i] as "ansi" | "json";
372
+ } else if (!arg.startsWith("-")) {
373
+ binary = arg;
374
+ }
375
+ }
376
+
377
+ if (!binary) {
378
+ console.error("Error: provide a binary name");
379
+ console.error("Run 'cli-tree archaeology --help' for usage");
380
+ process.exit(1);
381
+ }
382
+
383
+ const C = {
384
+ reset: "\x1b[0m",
385
+ bold: "\x1b[1m",
386
+ dim: "\x1b[2m",
387
+ cyan: "\x1b[36m",
388
+ green: "\x1b[32m",
389
+ yellow: "\x1b[33m",
390
+ red: "\x1b[31m",
391
+ magenta: "\x1b[35m",
392
+ gray: "\x1b[90m",
393
+ };
394
+
395
+ try {
396
+ console.error(`${C.dim}⚠ LLM archaeology requires running inside the clitree skill.${C.reset}`);
397
+ console.error(`${C.dim} Running deterministic phases only (help + history mining).${C.reset}`);
398
+ console.error("");
399
+
400
+ const [arch, mine] = await Promise.all([
401
+ runArchaeology(binary, new NullDelegate(), { skipCache, maxHelpDepth: maxDepth }),
402
+ mineCli(binary, { minSupport: 3 }).catch(() => null),
403
+ ]);
404
+
405
+ if (format === "json") {
406
+ console.log(JSON.stringify({ archaeology: arch, mining: mine }, null, 2));
407
+ return;
408
+ }
409
+
410
+ console.log(`${C.bold}${C.magenta}${binary}${C.reset}`);
411
+ if (arch.tree.description) {
412
+ console.log(`${C.gray}${arch.tree.description}${C.reset}`);
413
+ }
414
+ console.log();
415
+
416
+ console.log(`${C.bold}Tree:${C.reset}`);
417
+ console.log(` ${C.dim}Commands:${C.reset} ${C.cyan}${arch.stats.helpCommands}${C.reset}`);
418
+ console.log(` ${C.dim}Cached:${C.reset} ${arch.fromCache ? `${C.green}yes${C.reset}` : `${C.dim}no${C.reset}`}`);
419
+ console.log();
420
+
421
+ if (mine) {
422
+ console.log(`${C.bold}Usage from shell history:${C.reset}`);
423
+ console.log(` ${C.dim}Invocations:${C.reset} ${C.cyan}${mine.stats.totalInvocations}${C.reset}`);
424
+ console.log(` ${C.dim}Subcommands:${C.reset} ${C.cyan}${mine.stats.uniqueSubcommands}${C.reset}`);
425
+
426
+ if (mine.workflows.length > 0) {
427
+ console.log(`\n${C.bold}Workflows you repeat:${C.reset}`);
428
+ for (const wf of mine.workflows.slice(0, 5)) {
429
+ const chain = wf.path[0]!.map(p => `${C.green}${p}${C.reset}`).join(` ${C.gray}→${C.reset} `);
430
+ console.log(` ${chain} ${C.dim}(${wf.support}×)${C.reset}`);
431
+ }
432
+ }
433
+
434
+ if (mine.suggestions.length > 0) {
435
+ console.log(`\n${C.bold}${C.yellow}💡 Skill suggestions:${C.reset}`);
436
+ for (const s of mine.suggestions.slice(0, 3)) {
437
+ console.log(` ${C.bold}/${s.name}${C.reset} — ${s.description}`);
438
+ console.log(` ${C.dim}${s.reason}${C.reset}`);
439
+ }
440
+ }
441
+ }
442
+
443
+ console.log();
444
+ console.log(`${C.dim}Install the clitree skill for full LLM archaeology:${C.reset}`);
445
+ console.log(`${C.dim} bash ./skill/install.sh${C.reset}`);
446
+ console.log();
447
+ } catch (err: any) {
448
+ console.error(`${C.red}Error: ${err.message}${C.reset}`);
449
+ process.exit(1);
450
+ }
451
+ }
@@ -0,0 +1,32 @@
1
+ import type { Grid } from "../grid";
2
+ import { ANSI_COLORS, ANSI_RESET, type AnsiColor } from "../types";
3
+
4
+ export function encodeAnsi(grid: Grid): string {
5
+ const lines: string[] = [];
6
+
7
+ for (let y = 0; y < grid.height; y++) {
8
+ let line = "";
9
+ let currentFg: string | null = null;
10
+
11
+ for (let x = 0; x < grid.width; x++) {
12
+ const cell = grid.get(x, y);
13
+ if (!cell) continue;
14
+
15
+ if (cell.fg !== currentFg) {
16
+ if (currentFg !== null) line += ANSI_RESET;
17
+ if (cell.fg !== null) line += ANSI_COLORS[cell.fg as AnsiColor] ?? "";
18
+ currentFg = cell.fg;
19
+ }
20
+ line += cell.char;
21
+ }
22
+
23
+ if (currentFg !== null) line += ANSI_RESET;
24
+ lines.push(line.trimEnd());
25
+ }
26
+
27
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
28
+ lines.pop();
29
+ }
30
+
31
+ return lines.join("\n");
32
+ }
@@ -0,0 +1,78 @@
1
+ import type { Grid } from "../grid";
2
+
3
+ const COLOR_TO_CSS: Record<string, string> = {
4
+ red: "#ef4444",
5
+ green: "#22c55e",
6
+ yellow: "#eab308",
7
+ blue: "#3b82f6",
8
+ magenta: "#d946ef",
9
+ cyan: "#06b6d4",
10
+ white: "#e8e0d4",
11
+ gray: "#6b7c72",
12
+ };
13
+
14
+ export interface HtmlEncodeOptions {
15
+ wrap?: boolean;
16
+ background?: string;
17
+ foreground?: string;
18
+ fontFamily?: string;
19
+ fontSize?: string;
20
+ padding?: string;
21
+ }
22
+
23
+ export function encodeHtml(grid: Grid, opts: HtmlEncodeOptions = {}): string {
24
+ const wrap = opts.wrap ?? true;
25
+ const lines: string[] = [];
26
+
27
+ for (let y = 0; y < grid.height; y++) {
28
+ let line = "";
29
+ let currentFg: string | null = null;
30
+
31
+ for (let x = 0; x < grid.width; x++) {
32
+ const cell = grid.get(x, y);
33
+ if (!cell) continue;
34
+
35
+ if (cell.fg !== currentFg) {
36
+ if (currentFg !== null) line += "</span>";
37
+ if (cell.fg !== null) {
38
+ const css = COLOR_TO_CSS[cell.fg] ?? cell.fg;
39
+ line += `<span style="color:${css}">`;
40
+ }
41
+ currentFg = cell.fg;
42
+ }
43
+
44
+ const ch = cell.char;
45
+ if (ch === "<") line += "&lt;";
46
+ else if (ch === ">") line += "&gt;";
47
+ else if (ch === "&") line += "&amp;";
48
+ else line += ch;
49
+ }
50
+
51
+ if (currentFg !== null) line += "</span>";
52
+ lines.push(line);
53
+ }
54
+
55
+ const body = lines.join("\n");
56
+ if (!wrap) return body;
57
+
58
+ const bg = opts.background ?? "#0a0a0a";
59
+ const fg = opts.foreground ?? "#e8e0d4";
60
+ const font = opts.fontFamily ?? '"JetBrains Mono", "Fira Code", "SF Mono", Menlo, Monaco, Consolas, monospace';
61
+ const size = opts.fontSize ?? "14px";
62
+ const pad = opts.padding ?? "24px";
63
+
64
+ const style = [
65
+ `background: ${bg}`,
66
+ `color: ${fg}`,
67
+ `font-family: ${font}`,
68
+ `font-size: ${size}`,
69
+ `line-height: 1.3`,
70
+ `padding: ${pad}`,
71
+ `margin: 0`,
72
+ `white-space: pre`,
73
+ `overflow-x: auto`,
74
+ `border-radius: 8px`,
75
+ ].join("; ");
76
+
77
+ return `<pre style="${style}">${body}</pre>`;
78
+ }
@@ -0,0 +1,20 @@
1
+ import type { Grid } from "../grid";
2
+
3
+ export function encodeString(grid: Grid): string {
4
+ const lines: string[] = [];
5
+
6
+ for (let y = 0; y < grid.height; y++) {
7
+ let line = "";
8
+ for (let x = 0; x < grid.width; x++) {
9
+ const cell = grid.get(x, y);
10
+ line += cell?.char ?? " ";
11
+ }
12
+ lines.push(line.trimEnd());
13
+ }
14
+
15
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
16
+ lines.pop();
17
+ }
18
+
19
+ return lines.join("\n");
20
+ }
@@ -0,0 +1,21 @@
1
+ import { encodeAnsi } from "../encoders/ansi";
2
+ import { encodeString } from "../encoders/string";
3
+ import { encodeHtml } from "../encoders/html";
4
+ import { renderFlow } from "./render";
5
+ import type { Workflow, FlowRenderOptions } from "./types";
6
+
7
+ export function flowToAnsi(workflow: Workflow, opts?: FlowRenderOptions): string {
8
+ return encodeAnsi(renderFlow(workflow, opts));
9
+ }
10
+
11
+ export function flowToString(workflow: Workflow, opts?: FlowRenderOptions): string {
12
+ return encodeString(renderFlow(workflow, opts));
13
+ }
14
+
15
+ export function flowToHtml(workflow: Workflow, opts?: FlowRenderOptions): string {
16
+ return encodeHtml(renderFlow(workflow, opts));
17
+ }
18
+
19
+ export function printFlow(workflow: Workflow, opts?: FlowRenderOptions): void {
20
+ console.log(flowToAnsi(workflow, opts));
21
+ }
@@ -0,0 +1,15 @@
1
+ export { parseWorkflow } from "./parse";
2
+ export { parseYaml } from "./yaml";
3
+ export { validateWorkflow } from "./validate";
4
+ export { computeLayout } from "./layout";
5
+ export { renderFlow } from "./render";
6
+ export { flowToAnsi, flowToString, flowToHtml, printFlow } from "./encode";
7
+ export type {
8
+ Workflow,
9
+ WorkflowNode,
10
+ WorkflowEdge,
11
+ FlowRenderOptions,
12
+ ValidationError,
13
+ ValidationResult,
14
+ } from "./types";
15
+ export type { Layout, LayoutNode, LayoutEdge, LayoutOptions } from "./layout";