@alimaandev/ripple 0.1.0 → 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/bin.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import { DEFAULT_CONFIG, rippleConfigSchema, formatSchemaIssues } from './chunk-OOERV74D.js';
2
3
  import { CommanderError, Command } from 'commander';
3
4
  import { createJiti } from 'jiti';
4
5
  import path8 from 'path';
5
6
  import fs from 'fs/promises';
6
7
  import fs2 from 'fs';
7
8
  import { Project, ts, SyntaxKind } from 'ts-morph';
8
- import { z } from 'zod';
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 {
@@ -183,66 +185,6 @@ function substituteAlias(specifier, aliases) {
183
185
  return void 0;
184
186
  }
185
187
 
186
- // src/config/defaults.ts
187
- var DEFAULT_CONFIG = {
188
- include: ["**/*.{ts,tsx,js,jsx}"],
189
- ignore: ["node_modules", "dist", "build", "coverage", ".next", "out", ".git"],
190
- aliases: {},
191
- tsconfigPath: "tsconfig.json",
192
- risk: {
193
- weights: {
194
- affectedFiles: 0.3,
195
- entryPoint: 0.15,
196
- sharedUtility: 0.15,
197
- publicExports: 0.1,
198
- tests: 0.1,
199
- routes: 0.1,
200
- cycleMembership: 0.1
201
- },
202
- thresholds: {
203
- medium: 30,
204
- high: 55,
205
- critical: 80
206
- }
207
- }
208
- };
209
-
210
- // src/config/schema.ts
211
- var fraction = z.number().min(0).max(1);
212
- var score = z.number().min(0).max(100);
213
- var riskWeightsSchema = z.object({
214
- affectedFiles: fraction.default(DEFAULT_CONFIG.risk.weights.affectedFiles),
215
- entryPoint: fraction.default(DEFAULT_CONFIG.risk.weights.entryPoint),
216
- sharedUtility: fraction.default(DEFAULT_CONFIG.risk.weights.sharedUtility),
217
- publicExports: fraction.default(DEFAULT_CONFIG.risk.weights.publicExports),
218
- tests: fraction.default(DEFAULT_CONFIG.risk.weights.tests),
219
- routes: fraction.default(DEFAULT_CONFIG.risk.weights.routes),
220
- cycleMembership: fraction.default(DEFAULT_CONFIG.risk.weights.cycleMembership)
221
- }).strict();
222
- var riskThresholdsSchema = z.object({
223
- medium: score.default(DEFAULT_CONFIG.risk.thresholds.medium),
224
- high: score.default(DEFAULT_CONFIG.risk.thresholds.high),
225
- critical: score.default(DEFAULT_CONFIG.risk.thresholds.critical)
226
- }).strict();
227
- var riskConfigSchema = z.object({
228
- weights: riskWeightsSchema.default(DEFAULT_CONFIG.risk.weights),
229
- thresholds: riskThresholdsSchema.default(DEFAULT_CONFIG.risk.thresholds)
230
- }).strict();
231
- var rippleConfigSchema = z.object({
232
- include: z.array(z.string().min(1)).min(1).default(DEFAULT_CONFIG.include),
233
- ignore: z.array(z.string()).default(DEFAULT_CONFIG.ignore),
234
- aliases: z.record(z.string(), z.string()).default(DEFAULT_CONFIG.aliases),
235
- tsconfigPath: z.string().min(1).default(DEFAULT_CONFIG.tsconfigPath),
236
- risk: riskConfigSchema.default(DEFAULT_CONFIG.risk)
237
- }).strict();
238
- function formatSchemaIssues(error) {
239
- return error.issues.map((issue) => {
240
- const path12 = issue.path.join(".");
241
- const location = path12 === "" ? "config" : `config.${path12}`;
242
- return `${location}: ${issue.message}`;
243
- });
244
- }
245
-
246
188
  // src/config/loader.ts
247
189
  var CONFIG_CANDIDATES = [
248
190
  "ripple.config.ts",
@@ -600,10 +542,10 @@ function reverseTraverse(graph, targetKey, options) {
600
542
  }
601
543
 
602
544
  // src/risk/levels.ts
603
- function scoreToLevel(score2, thresholds) {
604
- if (score2 >= thresholds.critical) return "CRITICAL";
605
- if (score2 >= thresholds.high) return "HIGH";
606
- if (score2 >= thresholds.medium) return "MEDIUM";
545
+ function scoreToLevel(score, thresholds) {
546
+ if (score >= thresholds.critical) return "CRITICAL";
547
+ if (score >= thresholds.high) return "HIGH";
548
+ if (score >= thresholds.medium) return "MEDIUM";
607
549
  return "LOW";
608
550
  }
609
551
  function clamp(value, min, max) {
@@ -684,14 +626,14 @@ function scoreRisk(input) {
684
626
  contribution: Math.round(weight * value * 100 * 100) / 100
685
627
  };
686
628
  });
687
- const score2 = clamp(
629
+ const score = clamp(
688
630
  Math.round(factors.reduce((sum, factor) => sum + factor.contribution, 0) * 100) / 100,
689
631
  0,
690
632
  100
691
633
  );
692
634
  return {
693
- score: score2,
694
- level: scoreToLevel(score2, input.riskConfig.thresholds),
635
+ score,
636
+ level: scoreToLevel(score, input.riskConfig.thresholds),
695
637
  factors
696
638
  };
697
639
  }
@@ -733,24 +675,79 @@ function analyzeFile(input) {
733
675
  maxDepth: reachedDepth
734
676
  };
735
677
  }
736
- function createSpinner(enabled) {
737
- if (!enabled || !process.stdout.isTTY) {
738
- return { start: () => {
739
- }, stop: () => {
740
- } };
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: () => {
741
719
  }
742
- 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
+ };
743
731
  return {
744
- start(text) {
745
- instance = ora(text).start();
732
+ next(label) {
733
+ if (current) stamp(current, Date.now() - from);
734
+ current = label;
735
+ from = Date.now();
746
736
  },
747
- stop() {
748
- instance?.stop();
749
- instance = void 0;
737
+ done() {
738
+ if (current) stamp(current, Date.now() - from);
739
+ current = void 0;
750
740
  }
751
741
  };
752
742
  }
753
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
+
754
751
  // src/formatter/table.ts
755
752
  function renderTable(columns, rows) {
756
753
  const widths = columns.map((column, index) => {
@@ -788,50 +785,53 @@ function renderTree(nodes) {
788
785
  }
789
786
  return treeify.asTree(root, false, false).trimEnd();
790
787
  }
791
- function banner(text, style) {
792
- return style.color ? chalk.cyanBright.bold(text) : text;
793
- }
794
- function sectionTitle(text, style) {
795
- return style.color ? chalk.bold.underline(text) : text;
796
- }
797
- function dim(text, style) {
798
- return style.color ? chalk.dim(text) : text;
799
- }
800
- function strong(text, style) {
801
- return style.color ? chalk.bold(text) : text;
802
- }
803
- var RISK_COLORS = {
804
- LOW: (text) => chalk.green(text),
805
- MEDIUM: (text) => chalk.yellow(text),
806
- HIGH: (text) => chalk.magenta(text),
807
- CRITICAL: (text) => chalk.red.bold(text)
808
- };
809
- function riskBadge(level, style) {
810
- return style.color ? RISK_COLORS[level](level) : level;
811
- }
812
- function confidenceText(percent, style) {
813
- const text = `${percent}%`;
814
- if (!style.color) return text;
815
- if (percent >= 90) return chalk.green(text);
816
- if (percent >= 70) return chalk.yellow(text);
817
- 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
+ });
818
801
  }
819
- function formatDuration(ms) {
820
- if (ms < 1e3) return `${ms}ms`;
821
- 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)}`;
822
806
  }
823
- function pluralize(count, noun, plural = `${noun}s`) {
824
- 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));
825
816
  }
826
817
 
827
818
  // src/output/report.ts
828
819
  var DEFAULT_LIST_LIMIT = 20;
829
820
  var textStyle = (options) => ({ color: options.color });
830
821
  function fileLabel(filePath, depth, inCycle, style) {
831
- const relative = dim(filePath, style);
832
- const depthSuffix = dim(`(depth ${depth})`, style);
833
- const cycleSuffix = dim("(cycle)", style);
834
- 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);
835
835
  }
836
836
  function buildAffectedTree(result, cwd, style) {
837
837
  const { affected, graph } = result;
@@ -871,29 +871,32 @@ function renderAnalyzeReport(result, options, writer) {
871
871
  const style = textStyle(options);
872
872
  const { summary, risk } = result;
873
873
  const file = displayPath(result.targetPath, options.cwd);
874
- writer.writeLine(banner("\u{1F30A} Ripple Analysis", style));
874
+ writer.writeLine(brandHeader({ label: "impact analysis", version: options.version, style }));
875
875
  writer.writeLine();
876
- writer.writeLine(
877
- renderKeyValue([
878
- ["File", file],
879
- ["Risk", riskBadge(risk.level, style)],
880
- ["Affected Files", pluralize(summary.affectedFiles, "file")],
881
- ["Components", String(summary.components)],
882
- ["API Routes", String(summary.routes)],
883
- ["Tests", String(summary.tests)],
884
- ["Utilities", String(summary.utilities)],
885
- ["Max Depth", String(summary.maxDepth)],
886
- ["Confidence", confidenceText(summary.confidence, style)],
887
- ["Duration", dim(formatDuration(result.durationMs), style)]
888
- ])
889
- );
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
+ }
890
891
  writer.writeLine();
891
- writer.writeLine(sectionTitle("Top Impact", style));
892
+ writer.writeLine(sectionHeader("Top impact", style));
892
893
  for (const area of summary.topImpact) {
893
- 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
+ );
894
897
  }
895
898
  writer.writeLine();
896
- writer.writeLine(sectionTitle("Circular Dependencies", style));
899
+ writer.writeLine(sectionHeader("Circular dependencies", style));
897
900
  if (result.graph.cycles.length === 0) {
898
901
  writer.writeLine(dim("none", style));
899
902
  } else {
@@ -901,13 +904,13 @@ function renderAnalyzeReport(result, options, writer) {
901
904
  const chain = cycle.path.map((key) => {
902
905
  const node = result.graph.nodes.get(key);
903
906
  return displayPath(node?.path ?? key, options.cwd);
904
- }).join(" \u2192 ");
905
- writer.writeLine(`\u2B55 ${chain}`);
907
+ }).join(` ${icon("arrowRight")} `);
908
+ writer.writeLine(`${icon("circle")} ${chain}`);
906
909
  }
907
910
  }
908
911
  writer.writeLine();
909
912
  if (summary.affectedFiles > 0) {
910
- writer.writeLine(sectionTitle(`Affected Files (${summary.affectedFiles})`, style));
913
+ writer.writeLine(sectionHeader(`Affected files (${summary.affectedFiles})`, style));
911
914
  const tree = buildAffectedTree(result, options.cwd, style);
912
915
  writer.writeLine(renderTree(tree));
913
916
  const limit = options.listLimit ?? DEFAULT_LIST_LIMIT;
@@ -919,22 +922,27 @@ function renderAnalyzeReport(result, options, writer) {
919
922
  writer.writeLine();
920
923
  }
921
924
  if (options.verbose) {
922
- writer.writeLine(sectionTitle("Risk Factors", style));
925
+ writer.writeLine(sectionHeader("Risk factors", style));
923
926
  const columns = [
924
927
  { header: "Factor" },
925
928
  { header: "Weight" },
926
929
  { header: "Signal" },
927
930
  { header: "Points" }
928
931
  ];
929
- const rows = risk.factors.map((factor) => [
932
+ const rowsVerbose = risk.factors.map((factor) => [
930
933
  factor.label,
931
934
  factor.weight.toFixed(2),
932
935
  factor.value.toFixed(2),
933
936
  factor.contribution.toFixed(2)
934
937
  ]);
935
- writer.writeLine(renderTable(columns, rows));
938
+ writer.writeLine(renderTable(columns, rowsVerbose));
936
939
  writer.writeLine();
937
940
  }
941
+ if (style.color) {
942
+ writer.writeLine(
943
+ dim(`${icon("info")} Tip: run "ripple doctor" to check project health`, style)
944
+ );
945
+ }
938
946
  }
939
947
  function buildAnalyzeJsonReport(result, version, cwd) {
940
948
  const realPath = (key) => result.graph.nodes.get(key)?.path ?? key;
@@ -958,7 +966,7 @@ function buildAnalyzeJsonReport(result, version, cwd) {
958
966
  function renderGraphReport(input, options, writer) {
959
967
  const style = textStyle(options);
960
968
  const { stats } = input;
961
- writer.writeLine(banner("\u{1F30A} Dependency Graph", style));
969
+ writer.writeLine(brandHeader({ label: "dependency graph", version: options.version, style }));
962
970
  writer.writeLine();
963
971
  writer.writeLine(
964
972
  renderKeyValue([
@@ -971,26 +979,26 @@ function renderGraphReport(input, options, writer) {
971
979
  );
972
980
  writer.writeLine();
973
981
  if (input.forward) {
974
- writer.writeLine(sectionTitle("Dependants (forward)", style));
982
+ writer.writeLine(sectionHeader("Dependants (forward)", style));
975
983
  writer.writeLine(renderTree(input.forward.map((path12) => ({ label: path12 }))));
976
984
  writer.writeLine();
977
985
  }
978
986
  if (input.reverse) {
979
- writer.writeLine(sectionTitle("Dependents (reverse)", style));
987
+ writer.writeLine(sectionHeader("Dependents (reverse)", style));
980
988
  writer.writeLine(renderTree(input.reverse.map((path12) => ({ label: path12 }))));
981
989
  writer.writeLine();
982
990
  }
983
- if (input.cycles.length > 0) {
984
- 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 {
985
995
  for (const cycle of input.cycles) {
986
996
  const chain = cycle.path.map((key) => {
987
997
  const node = input.nodes.get(key);
988
998
  return displayPath(node?.path ?? key, input.cwd);
989
- }).join(" \u2192 ");
990
- writer.writeLine(`\u2B55 ${chain}`);
999
+ }).join(` ${icon("arrowRight")} `);
1000
+ writer.writeLine(`${icon("circle")} ${chain}`);
991
1001
  }
992
- } else {
993
- writer.writeLine(dim("No circular dependencies found.", style));
994
1002
  }
995
1003
  }
996
1004
  function buildGraphJsonReport(input, version) {
@@ -1473,11 +1481,13 @@ async function analyzeCommand(fileArg, options, ctx2) {
1473
1481
  if (!await pathExists(targetPath)) {
1474
1482
  throw new RippleError(`File not found: ${fileArg}`, ExitCode.NotFound);
1475
1483
  }
1476
- const spinner = createSpinner(!options.json);
1477
- spinner.start("Analyzing project\u2026");
1484
+ const tracker = createStageTracker(!options.json);
1485
+ tracker.next("Loading config");
1478
1486
  const started = Date.now();
1479
1487
  const context = await loadProjectContext(ctx2.cwd, options.config);
1488
+ tracker.next("Building dependency graph");
1480
1489
  const { graph, entryPoints, durationMs: pipelineMs } = await runPipeline(context);
1490
+ tracker.next("Analyzing impact");
1481
1491
  const completed = Date.now();
1482
1492
  const result = analyzeFile({
1483
1493
  graph,
@@ -1488,7 +1498,7 @@ async function analyzeCommand(fileArg, options, ctx2) {
1488
1498
  durationMs: pipelineMs + (completed - started),
1489
1499
  ...options.depth !== void 0 ? { maxDepth: options.depth } : {}
1490
1500
  });
1491
- spinner.stop();
1501
+ tracker.done();
1492
1502
  if (options.json) {
1493
1503
  const report = buildAnalyzeJsonReport(result, ctx2.version, ctx2.cwd);
1494
1504
  ctx2.writer.write(serializeJson(report));
@@ -1498,8 +1508,9 @@ async function analyzeCommand(fileArg, options, ctx2) {
1498
1508
  result,
1499
1509
  {
1500
1510
  cwd: ctx2.cwd,
1501
- color: options.color !== false,
1502
- verbose: options.verbose
1511
+ color: resolveColor(options.color),
1512
+ verbose: options.verbose,
1513
+ version: ctx2.version
1503
1514
  },
1504
1515
  ctx2.writer
1505
1516
  );
@@ -1528,11 +1539,12 @@ function walk(graph, startKey, edges, maxDepth, cwd) {
1528
1539
  return entries.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((entry) => entry.path);
1529
1540
  }
1530
1541
  async function graphCommand(fileArg, options, ctx2) {
1531
- const spinner = createSpinner(!options.json);
1532
- spinner.start(fileArg ? "Building dependency tree\u2026" : "Building graph\u2026");
1542
+ const tracker = createStageTracker(!options.json);
1543
+ tracker.next("Loading config");
1533
1544
  const cwd = ctx2.cwd;
1534
1545
  try {
1535
1546
  const context = await loadProjectContext(cwd, options.config);
1547
+ tracker.next("Building graph");
1536
1548
  const { graph } = await runPipeline(context);
1537
1549
  const input = {
1538
1550
  stats: graph.stats,
@@ -1541,13 +1553,19 @@ async function graphCommand(fileArg, options, ctx2) {
1541
1553
  cwd
1542
1554
  };
1543
1555
  if (!fileArg) {
1556
+ tracker.done();
1544
1557
  if (options.json) {
1545
1558
  const report = buildGraphJsonReport(input, ctx2.version);
1546
1559
  ctx2.writer.write(serializeJson(report));
1547
1560
  } else {
1548
1561
  renderGraphReport(
1549
1562
  input,
1550
- { 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
+ },
1551
1569
  ctx2.writer
1552
1570
  );
1553
1571
  }
@@ -1557,6 +1575,7 @@ async function graphCommand(fileArg, options, ctx2) {
1557
1575
  const { key } = requireNode(graph, targetPath, context.rootDir);
1558
1576
  const dependants = walk(graph, key, graph.forward, options.depth, cwd);
1559
1577
  const dependents = walk(graph, key, graph.reverse, options.depth, cwd);
1578
+ tracker.done();
1560
1579
  if (options.json) {
1561
1580
  const report = buildGraphJsonReport(
1562
1581
  {
@@ -1573,17 +1592,38 @@ async function graphCommand(fileArg, options, ctx2) {
1573
1592
  forward: options.reverse ? void 0 : dependants,
1574
1593
  reverse: options.reverse ? dependents : void 0
1575
1594
  },
1576
- { cwd, color: options.color !== false, verbose: options.verbose },
1595
+ { cwd, color: resolveColor(options.color), verbose: options.verbose, version: ctx2.version },
1577
1596
  ctx2.writer
1578
1597
  );
1579
1598
  }
1580
1599
  return ExitCode.Success;
1581
1600
  } finally {
1582
- spinner.stop();
1601
+ tracker.done();
1602
+ }
1603
+ }
1604
+ function createSpinner(enabled) {
1605
+ if (!enabled || !process.stdout.isTTY) {
1606
+ return { start: () => {
1607
+ }, stop: () => {
1608
+ } };
1583
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
+ };
1584
1620
  }
1621
+
1622
+ // src/commands/doctor.ts
1585
1623
  async function doctorCommand(options, ctx2) {
1586
- const spinner = createSpinner(options.color !== false);
1624
+ const color = resolveColor(options.color);
1625
+ const style = { color };
1626
+ const spinner = createSpinner(color);
1587
1627
  spinner.start("Running diagnostics\u2026");
1588
1628
  const results = [];
1589
1629
  const check = (label, state, detail) => {
@@ -1608,7 +1648,7 @@ async function doctorCommand(options, ctx2) {
1608
1648
  context = await loadProjectContext(ctx2.cwd, options.config);
1609
1649
  } catch (error) {
1610
1650
  check("ripple config", "fail", error instanceof Error ? error.message : String(error));
1611
- renderResults(results, ctx2);
1651
+ renderResults(results, ctx2, color);
1612
1652
  return ExitCode.InvalidConfig;
1613
1653
  }
1614
1654
  if (context.tsconfig) {
@@ -1657,29 +1697,39 @@ async function doctorCommand(options, ctx2) {
1657
1697
  } finally {
1658
1698
  spinner.stop();
1659
1699
  }
1660
- renderResults(results, ctx2);
1700
+ renderResults(results, ctx2, color);
1661
1701
  const failed = results.some((check2) => check2.state === "fail");
1662
1702
  const warned = results.some((check2) => check2.state === "warn");
1663
- ctx2.writer.writeLine(
1664
- failed ? "\u2716 Diagnoses found problems." : warned ? "\u26A0 Diagnoses passed with warnings." : "\u2713 All checks passed."
1665
- );
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}`);
1666
1706
  return failed ? ExitCode.Failure : ExitCode.Success;
1667
1707
  }
1668
- 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));
1669
1713
  for (const check of results) {
1670
- const icon = check.state === "ok" ? "\u2713" : check.state === "warn" ? "\u26A0" : "\u2716";
1671
- const suffix = check.detail ? ` ${check.detail}` : "";
1672
- 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}`);
1673
1719
  }
1674
1720
  ctx2.writer.writeLine();
1675
1721
  }
1676
1722
  var INIT_FILENAME = "ripple.config.json";
1677
1723
  async function initCommand(options, ctx2) {
1724
+ const style = { color: resolveColor(options.color) };
1678
1725
  const target = path8.join(ctx2.cwd, INIT_FILENAME);
1679
1726
  try {
1680
1727
  await fs.access(target);
1681
1728
  if (!options.force) {
1682
- 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
+ );
1683
1733
  return ExitCode.Failure;
1684
1734
  }
1685
1735
  } catch {
@@ -1690,12 +1740,16 @@ async function initCommand(options, ctx2) {
1690
1740
  try {
1691
1741
  await fs.writeFile(target, `${JSON.stringify(payload, null, 2)}
1692
1742
  `, "utf8");
1693
- ctx2.writer.writeLine(`\u2713 Wrote ${INIT_FILENAME}`);
1694
- 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
+ );
1695
1748
  return ExitCode.Success;
1696
1749
  } catch (error) {
1750
+ const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1697
1751
  ctx2.writer.writeLine(
1698
- `\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)}`
1699
1753
  );
1700
1754
  return ExitCode.Failure;
1701
1755
  }
@@ -1751,8 +1805,11 @@ function createProgram(ctx2) {
1751
1805
  throw new ExitError(exitCode2);
1752
1806
  }
1753
1807
  });
1754
- program.command("init").description("Create a ripple.config.json with the default settings").option("-f, --force", "overwrite an existing config file").action(async (options) => {
1755
- 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
+ );
1756
1813
  if (exitCode2 !== ExitCode.Success) {
1757
1814
  throw new ExitError(exitCode2);
1758
1815
  }
@@ -1760,6 +1817,11 @@ function createProgram(ctx2) {
1760
1817
  program.command("version").description("Print the Ripple version").action(() => {
1761
1818
  ctx2.writer.writeLine(ctx2.version);
1762
1819
  });
1820
+ program.addHelpText(
1821
+ "beforeAll",
1822
+ `ripple \u2014 dependency impact analysis for TypeScript and JavaScript
1823
+ `
1824
+ );
1763
1825
  return program;
1764
1826
  }
1765
1827
  var ExitError = class extends Error {
@@ -1814,7 +1876,7 @@ async function run({ argv, ctx: ctx2 }) {
1814
1876
 
1815
1877
  // package.json
1816
1878
  var package_default = {
1817
- version: "0.1.0"};
1879
+ version: "0.2.0"};
1818
1880
 
1819
1881
  // src/cli/bin.ts
1820
1882
  var ctx = {