@crafter/cli-tree 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,9 +5,13 @@ import {
5
5
  treeToString
6
6
  } from "./chunk-v5w3w6bd.js";
7
7
  import {
8
+ crossCliToFlowWorkflow,
9
+ formatTimeRange,
8
10
  mineCli,
9
- minedToFlowWorkflow
10
- } from "./chunk-dnq2rnr7.js";
11
+ mineCrossCli,
12
+ minedToFlowWorkflow,
13
+ sparkline
14
+ } from "./chunk-9pnqbn7b.js";
11
15
  import {
12
16
  NullDelegate,
13
17
  runArchaeology
@@ -28,7 +32,7 @@ import"./chunk-q4se2rwe.js";
28
32
  // src/cli.ts
29
33
  var args = process.argv.slice(2);
30
34
  var subcommand = args[0];
31
- var HELP_SUBCOMMANDS = new Set(["flow", "mine", "archaeology"]);
35
+ var HELP_SUBCOMMANDS = new Set(["flow", "mine", "archaeology", "safe-help", "cross"]);
32
36
  var isHelpFlag = args.includes("--help") || args.includes("-h");
33
37
  if (!subcommand || isHelpFlag && !HELP_SUBCOMMANDS.has(subcommand ?? "")) {
34
38
  printMainHelp();
@@ -41,6 +45,10 @@ if (isHelpFlag) {
41
45
  printMineHelp();
42
46
  else if (subcommand === "archaeology")
43
47
  printArchaeologyHelp();
48
+ else if (subcommand === "safe-help")
49
+ printSafeHelpHelp();
50
+ else if (subcommand === "cross")
51
+ printCrossHelp();
44
52
  else
45
53
  printMainHelp();
46
54
  process.exit(0);
@@ -51,6 +59,10 @@ if (subcommand === "flow") {
51
59
  await runMine(args.slice(1));
52
60
  } else if (subcommand === "archaeology") {
53
61
  await runArchaeologyCmd(args.slice(1));
62
+ } else if (subcommand === "safe-help") {
63
+ await runSafeHelp(args.slice(1));
64
+ } else if (subcommand === "cross") {
65
+ await runCross(args.slice(1));
54
66
  } else {
55
67
  await runTree(args);
56
68
  }
@@ -62,22 +74,98 @@ function printMainHelp() {
62
74
  clitree <binary> [options] Render command tree from --help
63
75
  clitree flow <file> [options] Render a workflow YAML as a DAG
64
76
  clitree mine <binary> [options] Mine shell history for workflows
77
+ clitree cross [options] Detect cross-CLI workflows (git + gh, docker + kubectl)
65
78
  clitree archaeology <binary> [options] Full analysis: tree + mining + (LLM)
79
+ clitree safe-help <binary> [sub...] Fetch clean help for any CLI (no pager, no overstriking)
66
80
 
67
81
  Examples:
68
82
  clitree docker # basic tree
69
83
  clitree mine git # "what workflows do I repeat with git?"
84
+ clitree cross # cross-CLI flows across your history
70
85
  clitree archaeology bun # tree + mining + LLM archaeology
86
+ clitree safe-help git commit # avoid the git man-page pager trap
71
87
 
72
88
  Subcommands:
73
89
  tree Parse --help output (default; passing a binary name invokes this)
74
90
  flow Render a workflow YAML file as an ASCII DAG
75
- mine Discover workflows from your shell history
91
+ mine Discover workflows from your shell history (single CLI)
92
+ cross Discover workflows that span multiple CLIs (e.g. git → gh)
76
93
  archaeology Run full analysis (tree + mine + LLM proposals when available)
94
+ safe-help Return inline help for any CLI, handling pager/overstrike edge cases
77
95
 
78
96
  Help for a subcommand: clitree <subcommand> --help
79
97
  `);
80
98
  }
99
+ function printCrossHelp() {
100
+ console.log(`
101
+ clitree cross — Mine cross-CLI workflows from your shell history
102
+
103
+ Usage:
104
+ clitree cross [options]
105
+
106
+ This is mineCli's bigger sibling. Instead of filtering history to a single
107
+ binary, it looks at every command in a session and finds sequences that weave
108
+ between tools — e.g.:
109
+
110
+ git push → gh pr create
111
+ docker build → docker push → kubectl apply
112
+ bun test → git commit → git push
113
+
114
+ These patterns are invisible to 'clitree mine <cli>' because they cross boundaries.
115
+
116
+ Examples:
117
+ clitree cross # top 10 cross-CLI workflows
118
+ clitree cross --top-k 5 # just the top 5
119
+ clitree cross --only git,gh,docker # restrict to a set of CLIs
120
+ clitree cross --format json # raw data
121
+
122
+ Options:
123
+ --top-k <n> Max workflows to return (default: 10)
124
+ --min-support <n> Minimum occurrences (default: 3)
125
+ --min-path-length <n> Minimum chain length (default: 2)
126
+ --max-path-length <n> Maximum chain length (default: 5)
127
+ --min-distinct-clis <n> Require at least this many distinct CLIs (default: 2)
128
+ --only <csv> Whitelist: only include these CLIs (comma-separated)
129
+ --format <fmt> ansi (default) or json
130
+ --no-color Disable ANSI colors
131
+ --no-flow Skip DAG rendering of the top workflow
132
+ -h, --help Show this help
133
+ `);
134
+ }
135
+ function printSafeHelpHelp() {
136
+ console.log(`
137
+ clitree safe-help — Fetch clean help output for any CLI
138
+
139
+ Usage:
140
+ clitree safe-help <binary> [subcommand...] [options]
141
+
142
+ Why this exists:
143
+ Running '<cli> --help' directly is a minefield on agents and scripts:
144
+ - 'git commit --help' opens a pager in a TTY and hangs
145
+ - Piping 'git <sub> --help' returns nroff with overstrike (ffiixxuupp)
146
+ - Some CLIs use 'help <sub>' instead of '<sub> --help'
147
+ - macOS man pages emit \\b sequences that need col -bx
148
+
149
+ safe-help tries the right variants in order, cleans overstrikes, and
150
+ returns plain text every time.
151
+
152
+ Strategy (in order):
153
+ 1. <cli> <sub> -h — short inline help
154
+ 2. <cli> <sub> --help — long help (captured, overstrike-stripped)
155
+ 3. <cli> help <sub> — alternate syntax
156
+ 4. MANPAGER=cat man <cli>-<sub> | col -bx — final man fallback
157
+
158
+ Examples:
159
+ clitree safe-help git commit
160
+ clitree safe-help docker run
161
+ clitree safe-help kubectl get pods
162
+ clitree safe-help bun install
163
+
164
+ Options:
165
+ --no-color Disable ANSI codes (auto-detects non-TTY)
166
+ -h, --help Show this help
167
+ `);
168
+ }
81
169
  function printMineHelp() {
82
170
  console.log(`
83
171
  clitree mine — Mine your shell history for CLI workflows
@@ -87,22 +175,26 @@ function printMineHelp() {
87
175
 
88
176
  Examples:
89
177
  clitree mine git
178
+ clitree mine git --top-k 3 # render top 3 workflows as DAGs
90
179
  clitree mine docker --min-support 5 --with-flow
91
180
  clitree mine bun --format json > bun-flows.json
92
181
  clitree mine git --no-color | tee report.txt
182
+ clitree mine git --no-activity # skip temporal sparklines
93
183
 
94
184
  Options:
95
185
  --min-support <n> Minimum occurrences to count as a workflow (default: 3)
96
186
  --history-path <p> Custom shell history path (default: ~/.zsh_history)
97
187
  --format <fmt> Output format: ansi (default), json
98
- --max-paths <n> Max workflows to show (default: 10)
99
- --with-flow Always render the top workflow as an ASCII DAG
100
- --no-flow Never render the DAG (overrides auto-detect)
188
+ --max-paths <n> Max workflows to show in text list (default: 10)
189
+ --top-k <n> Render top N workflows as DAGs (default: 1)
190
+ --with-flow Include 2-step workflows in DAG rendering
191
+ --no-flow Skip DAG rendering entirely
192
+ --no-activity Skip hour/day/30-day activity sparklines
101
193
  --no-color Disable ANSI colors (also auto-disabled when stdout is not a TTY)
102
194
  -h, --help Show this help
103
195
 
104
- By default, the top workflow is rendered as a DAG only when it has 3+ steps.
105
- Use --with-flow to force it on even for 2-step chains, or --no-flow to skip.
196
+ By default, the top 3+ step workflow is rendered as a DAG.
197
+ --top-k bumps that to N distinct workflows stacked on top of each other.
106
198
  `);
107
199
  }
108
200
  function printArchaeologyHelp() {
@@ -273,6 +365,117 @@ async function readStdin() {
273
365
  }
274
366
  return Buffer.concat(chunks).toString("utf-8");
275
367
  }
368
+ function stripOverstrike(text) {
369
+ return text.replace(/(.)\b\1/g, "$1").replace(/_\b(.)/g, "$1").replace(/\b/g, "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
370
+ }
371
+ function looksLikeHelp(text) {
372
+ if (!text.trim())
373
+ return false;
374
+ if (text.length < 40)
375
+ return false;
376
+ const firstChunk = text.slice(0, 200).toLowerCase();
377
+ if (/(unknown|invalid|no such|not a \w+ command|flag needs an argument|unrecognized)/.test(firstChunk)) {
378
+ return false;
379
+ }
380
+ if (/run '.+' for more information/.test(firstChunk))
381
+ return false;
382
+ return true;
383
+ }
384
+ async function tryHelpVariant(binary, argv, timeoutMs = 5000) {
385
+ try {
386
+ const proc = Bun.spawn([binary, ...argv], {
387
+ stdout: "pipe",
388
+ stderr: "pipe",
389
+ env: { ...process.env, PAGER: "cat", MANPAGER: "cat", GIT_PAGER: "cat" }
390
+ });
391
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
392
+ const [stdout, stderr] = await Promise.all([
393
+ new Response(proc.stdout).text(),
394
+ new Response(proc.stderr).text()
395
+ ]);
396
+ clearTimeout(timer);
397
+ await proc.exited;
398
+ const combined = stdout || stderr;
399
+ if (!looksLikeHelp(combined))
400
+ return null;
401
+ return stripOverstrike(combined);
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+ async function tryManPage(binary, subPath, timeoutMs = 5000) {
407
+ const candidates = [
408
+ subPath.length > 0 ? [`${binary}-${subPath.join("-")}`] : [binary],
409
+ [binary, ...subPath]
410
+ ];
411
+ for (const args2 of candidates) {
412
+ try {
413
+ const proc = Bun.spawn(["man", ...args2], {
414
+ stdout: "pipe",
415
+ stderr: "pipe",
416
+ env: { ...process.env, MANPAGER: "cat", PAGER: "cat" }
417
+ });
418
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
419
+ const [stdout] = await Promise.all([
420
+ new Response(proc.stdout).text(),
421
+ new Response(proc.stderr).text()
422
+ ]);
423
+ clearTimeout(timer);
424
+ await proc.exited;
425
+ if (looksLikeHelp(stdout))
426
+ return stripOverstrike(stdout);
427
+ } catch {}
428
+ }
429
+ return null;
430
+ }
431
+ async function runSafeHelp(args2) {
432
+ let binary = null;
433
+ const subPath = [];
434
+ for (const arg of args2) {
435
+ if (arg === "--no-color" || arg === "--help" || arg === "-h")
436
+ continue;
437
+ if (!binary) {
438
+ binary = arg;
439
+ } else {
440
+ subPath.push(arg);
441
+ }
442
+ }
443
+ if (!binary) {
444
+ console.error("Error: provide a binary name");
445
+ console.error("Run 'clitree safe-help --help' for usage");
446
+ process.exit(1);
447
+ }
448
+ const variants = [
449
+ { label: "short help (-h)", args: [...subPath, "-h"] },
450
+ { label: "long help (--help)", args: [...subPath, "--help"] }
451
+ ];
452
+ if (subPath.length > 0) {
453
+ variants.push({ label: "help subcommand", args: ["help", ...subPath] });
454
+ }
455
+ for (const variant of variants) {
456
+ const output = await tryHelpVariant(binary, variant.args);
457
+ if (output) {
458
+ process.stdout.write(output);
459
+ if (!output.endsWith(`
460
+ `))
461
+ process.stdout.write(`
462
+ `);
463
+ return;
464
+ }
465
+ }
466
+ const manOutput = await tryManPage(binary, subPath);
467
+ if (manOutput) {
468
+ process.stdout.write(manOutput);
469
+ if (!manOutput.endsWith(`
470
+ `))
471
+ process.stdout.write(`
472
+ `);
473
+ return;
474
+ }
475
+ console.error(`Error: could not fetch help for ${binary} ${subPath.join(" ")}`);
476
+ console.error("Tried: -h, --help, help subcommand, and man page.");
477
+ process.exit(1);
478
+ }
276
479
  function shouldUseColor(args2) {
277
480
  if (args2.includes("--no-color"))
278
481
  return false;
@@ -305,6 +508,8 @@ async function runMine(args2) {
305
508
  let format = "ansi";
306
509
  let maxPaths = 10;
307
510
  let withFlow = "auto";
511
+ let topK = 1;
512
+ let showActivity = true;
308
513
  for (let i = 0;i < args2.length; i++) {
309
514
  const arg = args2[i];
310
515
  if (arg === "--min-support" && args2[i + 1]) {
@@ -315,10 +520,16 @@ async function runMine(args2) {
315
520
  format = args2[++i];
316
521
  } else if (arg === "--max-paths" && args2[i + 1]) {
317
522
  maxPaths = Number.parseInt(args2[++i], 10);
523
+ } else if (arg === "--top-k" && args2[i + 1]) {
524
+ topK = Number.parseInt(args2[++i], 10);
525
+ if (withFlow === "auto")
526
+ withFlow = "on";
318
527
  } else if (arg === "--with-flow" || arg === "--flow") {
319
528
  withFlow = "on";
320
529
  } else if (arg === "--no-flow") {
321
530
  withFlow = "off";
531
+ } else if (arg === "--no-activity") {
532
+ showActivity = false;
322
533
  } else if (arg !== "--no-color" && !arg.startsWith("-")) {
323
534
  binary = arg;
324
535
  }
@@ -352,6 +563,9 @@ ${C.bold}${C.magenta}${binary}${C.reset} ${C.gray}— shell history analysis${C.
352
563
  }
353
564
  console.log();
354
565
  }
566
+ if (showActivity && result.activity.total > 0) {
567
+ renderActivitySection(result.activity, C);
568
+ }
355
569
  if (result.workflows.length > 0) {
356
570
  console.log(`${C.bold}Discovered workflows:${C.reset}`);
357
571
  for (const wf of result.workflows.slice(0, maxPaths)) {
@@ -373,12 +587,18 @@ ${C.bold}${C.magenta}${binary}${C.reset} ${C.gray}— shell history analysis${C.
373
587
  }
374
588
  if (withFlow !== "off") {
375
589
  const minSteps = withFlow === "on" ? 2 : 3;
376
- const topForFlow = pickWorkflowForFlow(result.workflows, minSteps);
377
- if (topForFlow) {
378
- const flowWorkflow = minedToFlowWorkflow(topForFlow);
379
- const rendered = shouldUseColor(args2) ? flowToAnsi(flowWorkflow) : flowToString(flowWorkflow);
380
- console.log(`${C.bold}Top workflow (visualized):${C.reset}`);
381
- console.log(rendered);
590
+ const topK_effective = Math.max(1, topK);
591
+ const candidates = pickWorkflowsForFlow(result.workflows, minSteps, topK_effective);
592
+ if (candidates.length > 0) {
593
+ const header = candidates.length === 1 ? "Top workflow (visualized):" : `Top ${candidates.length} workflows (visualized):`;
594
+ console.log(`${C.bold}${header}${C.reset}`);
595
+ for (let i = 0;i < candidates.length; i++) {
596
+ if (i > 0)
597
+ console.log(`${C.gray}${"─".repeat(60)}${C.reset}`);
598
+ const flowWorkflow = minedToFlowWorkflow(candidates[i]);
599
+ const rendered = shouldUseColor(args2) ? flowToAnsi(flowWorkflow) : flowToString(flowWorkflow);
600
+ console.log(rendered);
601
+ }
382
602
  console.log();
383
603
  }
384
604
  }
@@ -387,13 +607,124 @@ ${C.bold}${C.magenta}${binary}${C.reset} ${C.gray}— shell history analysis${C.
387
607
  process.exit(1);
388
608
  }
389
609
  }
390
- function pickWorkflowForFlow(workflows, minSteps = 3) {
610
+ function renderActivitySection(activity, C) {
611
+ if (activity.firstSeen === 0)
612
+ return;
613
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
614
+ console.log(`${C.bold}Activity:${C.reset}`);
615
+ const range = formatTimeRange(activity.firstSeen, activity.lastSeen);
616
+ console.log(` ${C.dim}Tracked over:${C.reset} ${C.cyan}${range}${C.reset}`);
617
+ const hourSpark = sparkline(activity.hourOfDay);
618
+ console.log(` ${C.dim}Hour of day: ${C.reset}${C.cyan}${hourSpark}${C.reset} ${C.dim}(0h → 23h)${C.reset}`);
619
+ const dowMax = Math.max(...activity.dayOfWeek);
620
+ if (dowMax > 0) {
621
+ console.log(` ${C.dim}Day of week:${C.reset}`);
622
+ for (let i = 0;i < 7; i++) {
623
+ const count = activity.dayOfWeek[i];
624
+ const barLen = dowMax > 0 ? Math.round(count / dowMax * 20) : 0;
625
+ const bar = "█".repeat(Math.max(count > 0 ? 1 : 0, barLen));
626
+ console.log(` ${C.gray}${dayNames[i]}${C.reset} ${C.cyan}${bar}${C.reset} ${C.dim}${count}${C.reset}`);
627
+ }
628
+ }
629
+ if (activity.last30Days.some((v) => v > 0)) {
630
+ const monthSpark = sparkline(activity.last30Days);
631
+ console.log(` ${C.dim}Last 30 days:${C.reset} ${C.cyan}${monthSpark}${C.reset} ${C.dim}(30d ago → today)${C.reset}`);
632
+ }
633
+ console.log();
634
+ }
635
+ function pickWorkflowsForFlow(workflows, minSteps, topK) {
636
+ const result = [];
637
+ const signatures = new Set;
391
638
  for (const wf of workflows) {
392
639
  const len = wf.path[0]?.length ?? 0;
393
- if (len >= minSteps)
394
- return wf;
640
+ if (len < minSteps)
641
+ continue;
642
+ const sig = wf.path[0].join(" → ");
643
+ if (signatures.has(sig))
644
+ continue;
645
+ signatures.add(sig);
646
+ result.push(wf);
647
+ if (result.length >= topK)
648
+ break;
649
+ }
650
+ return result;
651
+ }
652
+ async function runCross(args2) {
653
+ let format = "ansi";
654
+ let topK = 10;
655
+ let minSupport = 3;
656
+ let minPathLength = 2;
657
+ let maxPathLength = 5;
658
+ let minDistinctCLIs = 2;
659
+ let onlyList = [];
660
+ let withFlow = true;
661
+ for (let i = 0;i < args2.length; i++) {
662
+ const arg = args2[i];
663
+ if (arg === "--top-k" && args2[i + 1]) {
664
+ topK = Number.parseInt(args2[++i], 10);
665
+ } else if (arg === "--min-support" && args2[i + 1]) {
666
+ minSupport = Number.parseInt(args2[++i], 10);
667
+ } else if (arg === "--min-path-length" && args2[i + 1]) {
668
+ minPathLength = Number.parseInt(args2[++i], 10);
669
+ } else if (arg === "--max-path-length" && args2[i + 1]) {
670
+ maxPathLength = Number.parseInt(args2[++i], 10);
671
+ } else if (arg === "--min-distinct-clis" && args2[i + 1]) {
672
+ minDistinctCLIs = Number.parseInt(args2[++i], 10);
673
+ } else if (arg === "--only" && args2[i + 1]) {
674
+ onlyList = args2[++i].split(",").map((s) => s.trim()).filter(Boolean);
675
+ } else if (arg === "--format" && args2[i + 1]) {
676
+ format = args2[++i];
677
+ } else if (arg === "--no-flow") {
678
+ withFlow = false;
679
+ }
680
+ }
681
+ try {
682
+ const result = await mineCrossCli({
683
+ topK,
684
+ minSupport,
685
+ minPathLength,
686
+ maxPathLength,
687
+ minDistinctCLIs,
688
+ allowedCLIs: onlyList.length > 0 ? onlyList : undefined
689
+ });
690
+ if (format === "json") {
691
+ console.log(JSON.stringify(result, null, 2));
692
+ return;
693
+ }
694
+ const C = makeColors(shouldUseColor(args2));
695
+ console.log(`
696
+ ${C.bold}${C.magenta}cross-CLI workflow analysis${C.reset}
697
+ `);
698
+ console.log(`${C.bold}Scope:${C.reset}`);
699
+ console.log(` ${C.dim}Sessions analyzed:${C.reset} ${C.cyan}${result.sessionsAnalyzed}${C.reset}`);
700
+ console.log(` ${C.dim}Distinct CLIs:${C.reset} ${C.cyan}${result.distinctCLIs.length}${C.reset}`);
701
+ console.log(` ${C.dim}Transitions:${C.reset} ${C.cyan}${result.totalTransitions}${C.reset}
702
+ `);
703
+ if (result.workflows.length === 0) {
704
+ console.log(`${C.dim}No cross-CLI workflows found. Try lowering --min-support or --min-distinct-clis.${C.reset}
705
+ `);
706
+ return;
707
+ }
708
+ console.log(`${C.bold}Cross-CLI workflows (top ${result.workflows.length}):${C.reset}`);
709
+ for (let i = 0;i < result.workflows.length; i++) {
710
+ const wf = result.workflows[i];
711
+ const chain = wf.path.map((s) => `${C.green}${s.cli}${C.reset} ${C.cyan}${s.subcommand}${C.reset}`).join(` ${C.gray}→${C.reset} `);
712
+ console.log(` ${C.dim}${(i + 1).toString().padStart(2)}.${C.reset} ${chain}`);
713
+ console.log(` ${C.dim}seen ${wf.support}×, ${wf.uniqueCLIs} distinct CLIs${C.reset}`);
714
+ }
715
+ console.log();
716
+ if (withFlow && result.workflows.length > 0) {
717
+ const top = result.workflows[0];
718
+ const flowWorkflow = crossCliToFlowWorkflow(top);
719
+ const rendered = shouldUseColor(args2) ? flowToAnsi(flowWorkflow) : flowToString(flowWorkflow);
720
+ console.log(`${C.bold}Top cross-CLI workflow (visualized):${C.reset}`);
721
+ console.log(rendered);
722
+ console.log();
723
+ }
724
+ } catch (err) {
725
+ console.error(`Error: ${err.message}`);
726
+ process.exit(1);
395
727
  }
396
- return null;
397
728
  }
398
729
  async function runArchaeologyCmd(args2) {
399
730
  let binary = null;
@@ -470,4 +801,4 @@ ${C.bold}${C.yellow}\uD83D\uDCA1 Skill suggestions:${C.reset}`);
470
801
  }
471
802
  }
472
803
 
473
- //# debugId=FADAD3DCBCB5252964756E2164756E21
804
+ //# debugId=36C7EC67BF0C8B9764756E2164756E21