@alimaandev/ripple 0.1.1 → 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/README.md CHANGED
@@ -44,13 +44,12 @@ exactly how a change propagates through your codebase:
44
44
  ```text
45
45
  $ ripple analyze src/authentication/login.ts
46
46
 
47
- Risk MEDIUM
48
- Affected Files 7 files
49
- API Routes 4
50
- Components 2
51
- Tests 1
52
- Max Depth 2
53
- Confidence 100%
47
+ File src/authentication/login.ts
48
+ Risk MEDIUM · 34.7/100 ███░░░░░░░
49
+ Impact 4 routes · 2 components · 1 test
50
+ Affected 7 files
51
+ Max depth 2
52
+ Confidence 100%
54
53
  ```
55
54
 
56
55
  No black box.
@@ -117,30 +116,32 @@ ripple analyze src/authentication/login.ts
117
116
  Expected output:
118
117
 
119
118
  ```text
120
- 🌊 Ripple Analysis
121
-
122
- File src/authentication/login.ts
123
- Risk MEDIUM
124
- Affected Files 7 files
125
- Components 2
126
- API Routes 4
127
- Tests 1
128
- Utilities 0
129
- Max Depth 2
130
- Confidence 100%
131
-
132
- Top Impact
133
- • Dashboard (2)
134
- • Admin (1)
135
- • Index TS (1)
136
-
137
- Circular Dependencies
138
- ⭕ src/circular/a.ts → src/circular/c.ts → src/circular/b.ts → src/circular/a.ts
139
-
140
- Affected Files (7)
141
- ├─ src/admin/page.tsx (depth 1)
142
- ├─ src/dashboard/page.tsx (depth 1)
143
- └─ src/main.ts (depth 2)
119
+ ╭ ripple ────────────────────────────╮
120
+ │ ❯ impact analysis · v0.2.0 │
121
+ ╰────────────────────────────────────╯
122
+
123
+ File src/authentication/login.ts
124
+ Risk MEDIUM · 34.7/100 ███░░░░░░░
125
+ Impact 4 routes · 2 components · 1 test
126
+ Affected 7 files
127
+ Max depth 2
128
+ Confidence 100%
129
+
130
+ ────────────────────────────────────
131
+ Top impact
132
+ ● Dashboard (2)
133
+ ● Admin (1)
134
+ ● Index TS (1)
135
+
136
+ ────────────────────────────────────
137
+ Circular dependencies
138
+ ◯ src/circular/a.ts → src/circular/c.ts → src/circular/b.ts → src/circular/a.ts
139
+
140
+ ────────────────────────────────────
141
+ Affected files (7)
142
+ ├─ src/admin/page.tsx · depth 1
143
+ ├─ src/dashboard/page.tsx · depth 1
144
+ └─ src/main.ts · depth 2
144
145
  ```
145
146
 
146
147
  ### See more of the project
@@ -153,7 +154,9 @@ ripple graph
153
154
  ```
154
155
 
155
156
  ```text
156
- 🌊 Dependency Graph
157
+ ╭ ripple ────────────────────────────╮
158
+ │ ❯ dependency graph · v0.2.0 │
159
+ ╰────────────────────────────────────╯
157
160
 
158
161
  Files 25
159
162
  Edges 28
@@ -161,8 +164,9 @@ External 4
161
164
  Unresolved 0
162
165
  Circular groups 1
163
166
 
164
- Circular Dependencies
165
- ⭕ src/circular/a.ts → src/circular/c.ts → src/circular/b.ts → src/circular/a.ts
167
+ ────────────────────────────────────
168
+ Circular dependencies
169
+ ◯ src/circular/a.ts → src/circular/c.ts → src/circular/b.ts → src/circular/a.ts
166
170
  ```
167
171
 
168
172
  Point it at a file to see its dependency tree — forwards (what it imports)
package/dist/bin.js CHANGED
@@ -6,10 +6,12 @@ import path8 from 'path';
6
6
  import fs from 'fs/promises';
7
7
  import fs2 from 'fs';
8
8
  import { Project, ts, SyntaxKind } from 'ts-morph';
9
- import ora from 'ora';
10
- import treeify from 'treeify';
11
9
  import chalk from 'chalk';
10
+ import figures from 'figures';
11
+ import treeify from 'treeify';
12
+ import boxen from 'boxen';
12
13
  import fg from 'fast-glob';
14
+ import ora from 'ora';
13
15
 
14
16
  // src/output/writer.ts
15
17
  var TerminalWriter = class {
@@ -673,24 +675,79 @@ function analyzeFile(input) {
673
675
  maxDepth: reachedDepth
674
676
  };
675
677
  }
676
- function createSpinner(enabled) {
677
- if (!enabled || !process.stdout.isTTY) {
678
- return { start: () => {
679
- }, stop: () => {
680
- } };
678
+ function dim(text, style) {
679
+ return style.color ? chalk.dim(text) : text;
680
+ }
681
+ function strong(text, style) {
682
+ return style.color ? chalk.bold(text) : text;
683
+ }
684
+ var RISK_COLORS = {
685
+ LOW: (text) => chalk.green(text),
686
+ MEDIUM: (text) => chalk.yellow(text),
687
+ HIGH: (text) => chalk.magenta(text),
688
+ CRITICAL: (text) => chalk.red.bold(text)
689
+ };
690
+ function riskColor(level) {
691
+ return RISK_COLORS[level];
692
+ }
693
+ function riskBadge(level, style) {
694
+ return style.color ? RISK_COLORS[level](level) : level;
695
+ }
696
+ function confidenceText(percent, style) {
697
+ const text = `${percent}%`;
698
+ if (!style.color) return text;
699
+ if (percent >= 90) return chalk.green(text);
700
+ if (percent >= 70) return chalk.yellow(text);
701
+ return chalk.red(text);
702
+ }
703
+ function formatDuration(ms) {
704
+ if (ms < 1e3) return `${ms}ms`;
705
+ return `${(ms / 1e3).toFixed(2)}s`;
706
+ }
707
+ function pluralize(count, noun, plural = `${noun}s`) {
708
+ return `${count} ${count === 1 ? noun : plural}`;
709
+ }
710
+ function icon(name) {
711
+ return figures[name];
712
+ }
713
+
714
+ // src/ui/progress.ts
715
+ var noop = {
716
+ next: () => {
717
+ },
718
+ done: () => {
681
719
  }
682
- let instance;
720
+ };
721
+ function createStageTracker(enabled) {
722
+ if (!enabled || !process.stdout.isTTY) return noop;
723
+ let current;
724
+ let from = 0;
725
+ const stamp = (label, ms) => {
726
+ process.stdout.write(
727
+ `${chalk.green(icon("tick"))} ${label} ${chalk.dim(formatDuration(ms))}
728
+ `
729
+ );
730
+ };
683
731
  return {
684
- start(text) {
685
- instance = ora(text).start();
732
+ next(label) {
733
+ if (current) stamp(current, Date.now() - from);
734
+ current = label;
735
+ from = Date.now();
686
736
  },
687
- stop() {
688
- instance?.stop();
689
- instance = void 0;
737
+ done() {
738
+ if (current) stamp(current, Date.now() - from);
739
+ current = void 0;
690
740
  }
691
741
  };
692
742
  }
693
743
 
744
+ // src/ui/color.ts
745
+ function resolveColor(requested) {
746
+ if (requested === false) return false;
747
+ const noColor = process.env.NO_COLOR;
748
+ return noColor === void 0 || noColor === "";
749
+ }
750
+
694
751
  // src/formatter/table.ts
695
752
  function renderTable(columns, rows) {
696
753
  const widths = columns.map((column, index) => {
@@ -728,50 +785,53 @@ function renderTree(nodes) {
728
785
  }
729
786
  return treeify.asTree(root, false, false).trimEnd();
730
787
  }
731
- function banner(text, style) {
732
- return style.color ? chalk.cyanBright.bold(text) : text;
733
- }
734
- function sectionTitle(text, style) {
735
- return style.color ? chalk.bold.underline(text) : text;
736
- }
737
- function dim(text, style) {
738
- return style.color ? chalk.dim(text) : text;
739
- }
740
- function strong(text, style) {
741
- return style.color ? chalk.bold(text) : text;
742
- }
743
- var RISK_COLORS = {
744
- LOW: (text) => chalk.green(text),
745
- MEDIUM: (text) => chalk.yellow(text),
746
- HIGH: (text) => chalk.magenta(text),
747
- CRITICAL: (text) => chalk.red.bold(text)
748
- };
749
- function riskBadge(level, style) {
750
- return style.color ? RISK_COLORS[level](level) : level;
751
- }
752
- function confidenceText(percent, style) {
753
- const text = `${percent}%`;
754
- if (!style.color) return text;
755
- if (percent >= 90) return chalk.green(text);
756
- if (percent >= 70) return chalk.yellow(text);
757
- return chalk.red(text);
788
+ var DIVIDER_WIDTH = 38;
789
+ function brandHeader(options) {
790
+ const { label, version, style } = options;
791
+ if (!style.color) {
792
+ return `ripple \u2014 ${label}${version ? ` \xB7 v${version}` : ""}`;
793
+ }
794
+ const tagline = `${icon("pointer")} ${label}${version ? ` \xB7 v${version}` : ""}`;
795
+ return boxen(` ${tagline}`, {
796
+ padding: { left: 1, right: 1 },
797
+ title: "ripple",
798
+ borderStyle: "round",
799
+ borderColor: "cyan"
800
+ });
758
801
  }
759
- function formatDuration(ms) {
760
- if (ms < 1e3) return `${ms}ms`;
761
- return `${(ms / 1e3).toFixed(2)}s`;
802
+ function sectionHeader(title, style) {
803
+ if (!style.color) return title;
804
+ return `${chalk.dim("\u2500".repeat(DIVIDER_WIDTH))}
805
+ ${chalk.bold(title)}`;
762
806
  }
763
- function pluralize(count, noun, plural = `${noun}s`) {
764
- return `${count} ${count === 1 ? noun : plural}`;
807
+
808
+ // src/ui/gauge.ts
809
+ var CELLS = 10;
810
+ var FILL = "\u2588";
811
+ var EMPTY = "\u2591";
812
+ function riskGauge(score, level, style) {
813
+ if (!style.color) return "";
814
+ const filled = Math.max(0, Math.min(CELLS, Math.round(score / 100 * CELLS)));
815
+ return riskColor(level)(FILL.repeat(filled) + EMPTY.repeat(CELLS - filled));
765
816
  }
766
817
 
767
818
  // src/output/report.ts
768
819
  var DEFAULT_LIST_LIMIT = 20;
769
820
  var textStyle = (options) => ({ color: options.color });
770
821
  function fileLabel(filePath, depth, inCycle, style) {
771
- const relative = dim(filePath, style);
772
- const depthSuffix = dim(`(depth ${depth})`, style);
773
- const cycleSuffix = dim("(cycle)", style);
774
- return `${relative} ${depthSuffix}${inCycle ? ` ${cycleSuffix}` : ""}`;
822
+ const parts = [dim(filePath, style)];
823
+ if (depth >= 0) parts.push(dim(`\xB7 depth ${depth}`, style));
824
+ if (inCycle) parts.push(dim("\xB7 cycle", style));
825
+ return parts.join(" ");
826
+ }
827
+ function impactLine(result, style) {
828
+ const { summary } = result;
829
+ const parts = [];
830
+ if (summary.routes > 0) parts.push(pluralize(summary.routes, "route"));
831
+ if (summary.components > 0) parts.push(pluralize(summary.components, "component"));
832
+ if (summary.tests > 0) parts.push(pluralize(summary.tests, "test"));
833
+ if (summary.utilities > 0) parts.push(pluralize(summary.utilities, "utility"));
834
+ return parts.length > 0 ? parts.join(" \xB7 ") : dim("none", style);
775
835
  }
776
836
  function buildAffectedTree(result, cwd, style) {
777
837
  const { affected, graph } = result;
@@ -811,29 +871,32 @@ function renderAnalyzeReport(result, options, writer) {
811
871
  const style = textStyle(options);
812
872
  const { summary, risk } = result;
813
873
  const file = displayPath(result.targetPath, options.cwd);
814
- writer.writeLine(banner("\u{1F30A} Ripple Analysis", style));
874
+ writer.writeLine(brandHeader({ label: "impact analysis", version: options.version, style }));
815
875
  writer.writeLine();
816
- writer.writeLine(
817
- renderKeyValue([
818
- ["File", file],
819
- ["Risk", riskBadge(risk.level, style)],
820
- ["Affected Files", pluralize(summary.affectedFiles, "file")],
821
- ["Components", String(summary.components)],
822
- ["API Routes", String(summary.routes)],
823
- ["Tests", String(summary.tests)],
824
- ["Utilities", String(summary.utilities)],
825
- ["Max Depth", String(summary.maxDepth)],
826
- ["Confidence", confidenceText(summary.confidence, style)],
827
- ["Duration", dim(formatDuration(result.durationMs), style)]
828
- ])
829
- );
876
+ const score = risk.score.toFixed(1);
877
+ const gauge = riskGauge(risk.score, risk.level, style);
878
+ const rows = [
879
+ ["File", file],
880
+ ["Risk", `${riskBadge(risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}`],
881
+ ["Impact", impactLine(result, style)],
882
+ ["Affected", pluralize(summary.affectedFiles, "file")],
883
+ ["Max depth", String(summary.maxDepth)],
884
+ ["Confidence", confidenceText(summary.confidence, style)],
885
+ ["Duration", dim(formatDuration(result.durationMs), style)]
886
+ ];
887
+ const width = Math.max(...rows.map(([key]) => key.length));
888
+ for (const [key, value] of rows) {
889
+ writer.writeLine(`${key.padEnd(width)} ${value}`);
890
+ }
830
891
  writer.writeLine();
831
- writer.writeLine(sectionTitle("Top Impact", style));
892
+ writer.writeLine(sectionHeader("Top impact", style));
832
893
  for (const area of summary.topImpact) {
833
- writer.writeLine(`\u2022 ${strong(area.label, style)} ${dim(`(${area.count})`, style)}`);
894
+ writer.writeLine(
895
+ `${icon("bullet")} ${strong(area.label, style)} ${dim(`(${area.count})`, style)}`
896
+ );
834
897
  }
835
898
  writer.writeLine();
836
- writer.writeLine(sectionTitle("Circular Dependencies", style));
899
+ writer.writeLine(sectionHeader("Circular dependencies", style));
837
900
  if (result.graph.cycles.length === 0) {
838
901
  writer.writeLine(dim("none", style));
839
902
  } else {
@@ -841,13 +904,13 @@ function renderAnalyzeReport(result, options, writer) {
841
904
  const chain = cycle.path.map((key) => {
842
905
  const node = result.graph.nodes.get(key);
843
906
  return displayPath(node?.path ?? key, options.cwd);
844
- }).join(" \u2192 ");
845
- writer.writeLine(`\u2B55 ${chain}`);
907
+ }).join(` ${icon("arrowRight")} `);
908
+ writer.writeLine(`${icon("circle")} ${chain}`);
846
909
  }
847
910
  }
848
911
  writer.writeLine();
849
912
  if (summary.affectedFiles > 0) {
850
- writer.writeLine(sectionTitle(`Affected Files (${summary.affectedFiles})`, style));
913
+ writer.writeLine(sectionHeader(`Affected files (${summary.affectedFiles})`, style));
851
914
  const tree = buildAffectedTree(result, options.cwd, style);
852
915
  writer.writeLine(renderTree(tree));
853
916
  const limit = options.listLimit ?? DEFAULT_LIST_LIMIT;
@@ -859,22 +922,27 @@ function renderAnalyzeReport(result, options, writer) {
859
922
  writer.writeLine();
860
923
  }
861
924
  if (options.verbose) {
862
- writer.writeLine(sectionTitle("Risk Factors", style));
925
+ writer.writeLine(sectionHeader("Risk factors", style));
863
926
  const columns = [
864
927
  { header: "Factor" },
865
928
  { header: "Weight" },
866
929
  { header: "Signal" },
867
930
  { header: "Points" }
868
931
  ];
869
- const rows = risk.factors.map((factor) => [
932
+ const rowsVerbose = risk.factors.map((factor) => [
870
933
  factor.label,
871
934
  factor.weight.toFixed(2),
872
935
  factor.value.toFixed(2),
873
936
  factor.contribution.toFixed(2)
874
937
  ]);
875
- writer.writeLine(renderTable(columns, rows));
938
+ writer.writeLine(renderTable(columns, rowsVerbose));
876
939
  writer.writeLine();
877
940
  }
941
+ if (style.color) {
942
+ writer.writeLine(
943
+ dim(`${icon("info")} Tip: run "ripple doctor" to check project health`, style)
944
+ );
945
+ }
878
946
  }
879
947
  function buildAnalyzeJsonReport(result, version, cwd) {
880
948
  const realPath = (key) => result.graph.nodes.get(key)?.path ?? key;
@@ -898,7 +966,7 @@ function buildAnalyzeJsonReport(result, version, cwd) {
898
966
  function renderGraphReport(input, options, writer) {
899
967
  const style = textStyle(options);
900
968
  const { stats } = input;
901
- writer.writeLine(banner("\u{1F30A} Dependency Graph", style));
969
+ writer.writeLine(brandHeader({ label: "dependency graph", version: options.version, style }));
902
970
  writer.writeLine();
903
971
  writer.writeLine(
904
972
  renderKeyValue([
@@ -911,26 +979,26 @@ function renderGraphReport(input, options, writer) {
911
979
  );
912
980
  writer.writeLine();
913
981
  if (input.forward) {
914
- writer.writeLine(sectionTitle("Dependants (forward)", style));
982
+ writer.writeLine(sectionHeader("Dependants (forward)", style));
915
983
  writer.writeLine(renderTree(input.forward.map((path12) => ({ label: path12 }))));
916
984
  writer.writeLine();
917
985
  }
918
986
  if (input.reverse) {
919
- writer.writeLine(sectionTitle("Dependents (reverse)", style));
987
+ writer.writeLine(sectionHeader("Dependents (reverse)", style));
920
988
  writer.writeLine(renderTree(input.reverse.map((path12) => ({ label: path12 }))));
921
989
  writer.writeLine();
922
990
  }
923
- if (input.cycles.length > 0) {
924
- writer.writeLine(sectionTitle("Circular Dependencies", style));
991
+ writer.writeLine(sectionHeader("Circular dependencies", style));
992
+ if (input.cycles.length === 0) {
993
+ writer.writeLine(dim("none", style));
994
+ } else {
925
995
  for (const cycle of input.cycles) {
926
996
  const chain = cycle.path.map((key) => {
927
997
  const node = input.nodes.get(key);
928
998
  return displayPath(node?.path ?? key, input.cwd);
929
- }).join(" \u2192 ");
930
- writer.writeLine(`\u2B55 ${chain}`);
999
+ }).join(` ${icon("arrowRight")} `);
1000
+ writer.writeLine(`${icon("circle")} ${chain}`);
931
1001
  }
932
- } else {
933
- writer.writeLine(dim("No circular dependencies found.", style));
934
1002
  }
935
1003
  }
936
1004
  function buildGraphJsonReport(input, version) {
@@ -1413,11 +1481,13 @@ async function analyzeCommand(fileArg, options, ctx2) {
1413
1481
  if (!await pathExists(targetPath)) {
1414
1482
  throw new RippleError(`File not found: ${fileArg}`, ExitCode.NotFound);
1415
1483
  }
1416
- const spinner = createSpinner(!options.json);
1417
- spinner.start("Analyzing project\u2026");
1484
+ const tracker = createStageTracker(!options.json);
1485
+ tracker.next("Loading config");
1418
1486
  const started = Date.now();
1419
1487
  const context = await loadProjectContext(ctx2.cwd, options.config);
1488
+ tracker.next("Building dependency graph");
1420
1489
  const { graph, entryPoints, durationMs: pipelineMs } = await runPipeline(context);
1490
+ tracker.next("Analyzing impact");
1421
1491
  const completed = Date.now();
1422
1492
  const result = analyzeFile({
1423
1493
  graph,
@@ -1428,7 +1498,7 @@ async function analyzeCommand(fileArg, options, ctx2) {
1428
1498
  durationMs: pipelineMs + (completed - started),
1429
1499
  ...options.depth !== void 0 ? { maxDepth: options.depth } : {}
1430
1500
  });
1431
- spinner.stop();
1501
+ tracker.done();
1432
1502
  if (options.json) {
1433
1503
  const report = buildAnalyzeJsonReport(result, ctx2.version, ctx2.cwd);
1434
1504
  ctx2.writer.write(serializeJson(report));
@@ -1438,8 +1508,9 @@ async function analyzeCommand(fileArg, options, ctx2) {
1438
1508
  result,
1439
1509
  {
1440
1510
  cwd: ctx2.cwd,
1441
- color: options.color !== false,
1442
- verbose: options.verbose
1511
+ color: resolveColor(options.color),
1512
+ verbose: options.verbose,
1513
+ version: ctx2.version
1443
1514
  },
1444
1515
  ctx2.writer
1445
1516
  );
@@ -1468,11 +1539,12 @@ function walk(graph, startKey, edges, maxDepth, cwd) {
1468
1539
  return entries.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((entry) => entry.path);
1469
1540
  }
1470
1541
  async function graphCommand(fileArg, options, ctx2) {
1471
- const spinner = createSpinner(!options.json);
1472
- spinner.start(fileArg ? "Building dependency tree\u2026" : "Building graph\u2026");
1542
+ const tracker = createStageTracker(!options.json);
1543
+ tracker.next("Loading config");
1473
1544
  const cwd = ctx2.cwd;
1474
1545
  try {
1475
1546
  const context = await loadProjectContext(cwd, options.config);
1547
+ tracker.next("Building graph");
1476
1548
  const { graph } = await runPipeline(context);
1477
1549
  const input = {
1478
1550
  stats: graph.stats,
@@ -1481,13 +1553,19 @@ async function graphCommand(fileArg, options, ctx2) {
1481
1553
  cwd
1482
1554
  };
1483
1555
  if (!fileArg) {
1556
+ tracker.done();
1484
1557
  if (options.json) {
1485
1558
  const report = buildGraphJsonReport(input, ctx2.version);
1486
1559
  ctx2.writer.write(serializeJson(report));
1487
1560
  } else {
1488
1561
  renderGraphReport(
1489
1562
  input,
1490
- { cwd, color: options.color !== false, verbose: options.verbose },
1563
+ {
1564
+ cwd,
1565
+ color: resolveColor(options.color),
1566
+ verbose: options.verbose,
1567
+ version: ctx2.version
1568
+ },
1491
1569
  ctx2.writer
1492
1570
  );
1493
1571
  }
@@ -1497,6 +1575,7 @@ async function graphCommand(fileArg, options, ctx2) {
1497
1575
  const { key } = requireNode(graph, targetPath, context.rootDir);
1498
1576
  const dependants = walk(graph, key, graph.forward, options.depth, cwd);
1499
1577
  const dependents = walk(graph, key, graph.reverse, options.depth, cwd);
1578
+ tracker.done();
1500
1579
  if (options.json) {
1501
1580
  const report = buildGraphJsonReport(
1502
1581
  {
@@ -1513,17 +1592,38 @@ async function graphCommand(fileArg, options, ctx2) {
1513
1592
  forward: options.reverse ? void 0 : dependants,
1514
1593
  reverse: options.reverse ? dependents : void 0
1515
1594
  },
1516
- { cwd, color: options.color !== false, verbose: options.verbose },
1595
+ { cwd, color: resolveColor(options.color), verbose: options.verbose, version: ctx2.version },
1517
1596
  ctx2.writer
1518
1597
  );
1519
1598
  }
1520
1599
  return ExitCode.Success;
1521
1600
  } finally {
1522
- spinner.stop();
1601
+ tracker.done();
1523
1602
  }
1524
1603
  }
1604
+ function createSpinner(enabled) {
1605
+ if (!enabled || !process.stdout.isTTY) {
1606
+ return { start: () => {
1607
+ }, stop: () => {
1608
+ } };
1609
+ }
1610
+ let instance;
1611
+ return {
1612
+ start(text) {
1613
+ instance = ora(text).start();
1614
+ },
1615
+ stop() {
1616
+ instance?.stop();
1617
+ instance = void 0;
1618
+ }
1619
+ };
1620
+ }
1621
+
1622
+ // src/commands/doctor.ts
1525
1623
  async function doctorCommand(options, ctx2) {
1526
- const spinner = createSpinner(options.color !== false);
1624
+ const color = resolveColor(options.color);
1625
+ const style = { color };
1626
+ const spinner = createSpinner(color);
1527
1627
  spinner.start("Running diagnostics\u2026");
1528
1628
  const results = [];
1529
1629
  const check = (label, state, detail) => {
@@ -1548,7 +1648,7 @@ async function doctorCommand(options, ctx2) {
1548
1648
  context = await loadProjectContext(ctx2.cwd, options.config);
1549
1649
  } catch (error) {
1550
1650
  check("ripple config", "fail", error instanceof Error ? error.message : String(error));
1551
- renderResults(results, ctx2);
1651
+ renderResults(results, ctx2, color);
1552
1652
  return ExitCode.InvalidConfig;
1553
1653
  }
1554
1654
  if (context.tsconfig) {
@@ -1597,29 +1697,39 @@ async function doctorCommand(options, ctx2) {
1597
1697
  } finally {
1598
1698
  spinner.stop();
1599
1699
  }
1600
- renderResults(results, ctx2);
1700
+ renderResults(results, ctx2, color);
1601
1701
  const failed = results.some((check2) => check2.state === "fail");
1602
1702
  const warned = results.some((check2) => check2.state === "warn");
1603
- ctx2.writer.writeLine(
1604
- failed ? "\u2716 Diagnoses found problems." : warned ? "\u26A0 Diagnoses passed with warnings." : "\u2713 All checks passed."
1605
- );
1703
+ const verdict = failed ? { icon: icon("cross"), text: "Diagnoses found problems.", color: chalk.red } : warned ? { icon: icon("warning"), text: "Diagnoses passed with warnings.", color: chalk.yellow } : { icon: icon("tick"), text: "All checks passed.", color: chalk.green };
1704
+ const verdictIcon = style.color ? verdict.color(verdict.icon) : verdict.icon;
1705
+ ctx2.writer.writeLine(`${verdictIcon} ${verdict.text}`);
1606
1706
  return failed ? ExitCode.Failure : ExitCode.Success;
1607
1707
  }
1608
- function renderResults(results, ctx2) {
1708
+ function renderResults(results, ctx2, color) {
1709
+ const style = { color };
1710
+ ctx2.writer.writeLine(brandHeader({ label: "project health check", version: ctx2.version, style }));
1711
+ ctx2.writer.writeLine();
1712
+ const width = Math.max(...results.map((check) => check.label.length));
1609
1713
  for (const check of results) {
1610
- const icon = check.state === "ok" ? "\u2713" : check.state === "warn" ? "\u26A0" : "\u2716";
1611
- const suffix = check.detail ? ` ${check.detail}` : "";
1612
- ctx2.writer.writeLine(`${icon} ${check.label}${suffix}`);
1714
+ const stateIcon = check.state === "ok" ? icon("tick") : check.state === "warn" ? icon("warning") : icon("cross");
1715
+ const colored = check.state === "ok" ? chalk.green : check.state === "warn" ? chalk.yellow : chalk.red;
1716
+ const mark = style.color ? colored(stateIcon) : stateIcon;
1717
+ const detail = check.detail ? ` ${dim(check.detail, style)}` : "";
1718
+ ctx2.writer.writeLine(`${mark} ${check.label.padEnd(width)}${detail}`);
1613
1719
  }
1614
1720
  ctx2.writer.writeLine();
1615
1721
  }
1616
1722
  var INIT_FILENAME = "ripple.config.json";
1617
1723
  async function initCommand(options, ctx2) {
1724
+ const style = { color: resolveColor(options.color) };
1618
1725
  const target = path8.join(ctx2.cwd, INIT_FILENAME);
1619
1726
  try {
1620
1727
  await fs.access(target);
1621
1728
  if (!options.force) {
1622
- ctx2.writer.writeLine(`\u2716 ${INIT_FILENAME} already exists. Pass --force to overwrite it.`);
1729
+ const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1730
+ ctx2.writer.writeLine(
1731
+ `${mark} ${INIT_FILENAME} already exists. Pass --force to overwrite it.`
1732
+ );
1623
1733
  return ExitCode.Failure;
1624
1734
  }
1625
1735
  } catch {
@@ -1630,12 +1740,16 @@ async function initCommand(options, ctx2) {
1630
1740
  try {
1631
1741
  await fs.writeFile(target, `${JSON.stringify(payload, null, 2)}
1632
1742
  `, "utf8");
1633
- ctx2.writer.writeLine(`\u2713 Wrote ${INIT_FILENAME}`);
1634
- ctx2.writer.writeLine(` Edit it to tune include globs, aliases and risk weights.`);
1743
+ const mark = style.color ? chalk.green(icon("tick")) : icon("tick");
1744
+ ctx2.writer.writeLine(`${mark} Wrote ${INIT_FILENAME}`);
1745
+ ctx2.writer.writeLine(
1746
+ ` ${dim("Edit it to tune include globs, aliases and risk weights.", style)}`
1747
+ );
1635
1748
  return ExitCode.Success;
1636
1749
  } catch (error) {
1750
+ const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1637
1751
  ctx2.writer.writeLine(
1638
- `\u2716 Could not write ${INIT_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1752
+ `${mark} Could not write ${INIT_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1639
1753
  );
1640
1754
  return ExitCode.Failure;
1641
1755
  }
@@ -1691,8 +1805,11 @@ function createProgram(ctx2) {
1691
1805
  throw new ExitError(exitCode2);
1692
1806
  }
1693
1807
  });
1694
- program.command("init").description("Create a ripple.config.json with the default settings").option("-f, --force", "overwrite an existing config file").action(async (options) => {
1695
- const exitCode2 = await initCommand({ force: Boolean(options.force) }, ctx2);
1808
+ program.command("init").description("Create a ripple.config.json with the default settings").option("-f, --force", "overwrite an existing config file").option("--no-color", "disable ANSI colors").action(async (options) => {
1809
+ const exitCode2 = await initCommand(
1810
+ { force: Boolean(options.force), color: options.color },
1811
+ ctx2
1812
+ );
1696
1813
  if (exitCode2 !== ExitCode.Success) {
1697
1814
  throw new ExitError(exitCode2);
1698
1815
  }
@@ -1700,6 +1817,11 @@ function createProgram(ctx2) {
1700
1817
  program.command("version").description("Print the Ripple version").action(() => {
1701
1818
  ctx2.writer.writeLine(ctx2.version);
1702
1819
  });
1820
+ program.addHelpText(
1821
+ "beforeAll",
1822
+ `ripple \u2014 dependency impact analysis for TypeScript and JavaScript
1823
+ `
1824
+ );
1703
1825
  return program;
1704
1826
  }
1705
1827
  var ExitError = class extends Error {
@@ -1754,7 +1876,7 @@ async function run({ argv, ctx: ctx2 }) {
1754
1876
 
1755
1877
  // package.json
1756
1878
  var package_default = {
1757
- version: "0.1.1"};
1879
+ version: "0.2.0"};
1758
1880
 
1759
1881
  // src/cli/bin.ts
1760
1882
  var ctx = {