@alimaandev/ripple 0.1.1 → 0.3.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
@@ -6,10 +6,13 @@ 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';
15
+ import { execFileSync } from 'child_process';
13
16
 
14
17
  // src/output/writer.ts
15
18
  var TerminalWriter = class {
@@ -266,8 +269,8 @@ async function loadProjectContext(cwd, explicitConfigPath) {
266
269
  function cycleMemberPaths(graph) {
267
270
  const members = /* @__PURE__ */ new Set();
268
271
  for (const cycle of graph.cycles) {
269
- for (const path12 of cycle.members) {
270
- members.add(path12);
272
+ for (const path13 of cycle.members) {
273
+ members.add(path13);
271
274
  }
272
275
  }
273
276
  return members;
@@ -673,24 +676,79 @@ function analyzeFile(input) {
673
676
  maxDepth: reachedDepth
674
677
  };
675
678
  }
676
- function createSpinner(enabled) {
677
- if (!enabled || !process.stdout.isTTY) {
678
- return { start: () => {
679
- }, stop: () => {
680
- } };
679
+ function dim(text, style) {
680
+ return style.color ? chalk.dim(text) : text;
681
+ }
682
+ function strong(text, style) {
683
+ return style.color ? chalk.bold(text) : text;
684
+ }
685
+ var RISK_COLORS = {
686
+ LOW: (text) => chalk.green(text),
687
+ MEDIUM: (text) => chalk.yellow(text),
688
+ HIGH: (text) => chalk.magenta(text),
689
+ CRITICAL: (text) => chalk.red.bold(text)
690
+ };
691
+ function riskColor(level) {
692
+ return RISK_COLORS[level];
693
+ }
694
+ function riskBadge(level, style) {
695
+ return style.color ? RISK_COLORS[level](level) : level;
696
+ }
697
+ function confidenceText(percent, style) {
698
+ const text = `${percent}%`;
699
+ if (!style.color) return text;
700
+ if (percent >= 90) return chalk.green(text);
701
+ if (percent >= 70) return chalk.yellow(text);
702
+ return chalk.red(text);
703
+ }
704
+ function formatDuration(ms) {
705
+ if (ms < 1e3) return `${ms}ms`;
706
+ return `${(ms / 1e3).toFixed(2)}s`;
707
+ }
708
+ function pluralize(count, noun, plural = `${noun}s`) {
709
+ return `${count} ${count === 1 ? noun : plural}`;
710
+ }
711
+ function icon(name) {
712
+ return figures[name];
713
+ }
714
+
715
+ // src/ui/progress.ts
716
+ var noop = {
717
+ next: () => {
718
+ },
719
+ done: () => {
681
720
  }
682
- let instance;
721
+ };
722
+ function createStageTracker(enabled) {
723
+ if (!enabled || !process.stdout.isTTY) return noop;
724
+ let current;
725
+ let from = 0;
726
+ const stamp = (label, ms) => {
727
+ process.stdout.write(
728
+ `${chalk.green(icon("tick"))} ${label} ${chalk.dim(formatDuration(ms))}
729
+ `
730
+ );
731
+ };
683
732
  return {
684
- start(text) {
685
- instance = ora(text).start();
733
+ next(label) {
734
+ if (current) stamp(current, Date.now() - from);
735
+ current = label;
736
+ from = Date.now();
686
737
  },
687
- stop() {
688
- instance?.stop();
689
- instance = void 0;
738
+ done() {
739
+ if (current) stamp(current, Date.now() - from);
740
+ current = void 0;
690
741
  }
691
742
  };
692
743
  }
693
744
 
745
+ // src/ui/color.ts
746
+ function resolveColor(requested) {
747
+ if (requested === false) return false;
748
+ const noColor = process.env.NO_COLOR;
749
+ return noColor === void 0 || noColor === "";
750
+ }
751
+
694
752
  // src/formatter/table.ts
695
753
  function renderTable(columns, rows) {
696
754
  const widths = columns.map((column, index) => {
@@ -728,50 +786,53 @@ function renderTree(nodes) {
728
786
  }
729
787
  return treeify.asTree(root, false, false).trimEnd();
730
788
  }
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);
789
+ var DIVIDER_WIDTH = 38;
790
+ function brandHeader(options) {
791
+ const { label, version, style } = options;
792
+ if (!style.color) {
793
+ return `ripple \u2014 ${label}${version ? ` \xB7 v${version}` : ""}`;
794
+ }
795
+ const tagline = `${icon("pointer")} ${label}${version ? ` \xB7 v${version}` : ""}`;
796
+ return boxen(` ${tagline}`, {
797
+ padding: { left: 1, right: 1 },
798
+ title: "ripple",
799
+ borderStyle: "round",
800
+ borderColor: "cyan"
801
+ });
758
802
  }
759
- function formatDuration(ms) {
760
- if (ms < 1e3) return `${ms}ms`;
761
- return `${(ms / 1e3).toFixed(2)}s`;
803
+ function sectionHeader(title, style) {
804
+ if (!style.color) return title;
805
+ return `${chalk.dim("\u2500".repeat(DIVIDER_WIDTH))}
806
+ ${chalk.bold(title)}`;
762
807
  }
763
- function pluralize(count, noun, plural = `${noun}s`) {
764
- return `${count} ${count === 1 ? noun : plural}`;
808
+
809
+ // src/ui/gauge.ts
810
+ var CELLS = 10;
811
+ var FILL = "\u2588";
812
+ var EMPTY = "\u2591";
813
+ function riskGauge(score, level, style) {
814
+ if (!style.color) return "";
815
+ const filled = Math.max(0, Math.min(CELLS, Math.round(score / 100 * CELLS)));
816
+ return riskColor(level)(FILL.repeat(filled) + EMPTY.repeat(CELLS - filled));
765
817
  }
766
818
 
767
819
  // src/output/report.ts
768
820
  var DEFAULT_LIST_LIMIT = 20;
769
821
  var textStyle = (options) => ({ color: options.color });
770
822
  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}` : ""}`;
823
+ const parts = [dim(filePath, style)];
824
+ if (depth >= 0) parts.push(dim(`\xB7 depth ${depth}`, style));
825
+ if (inCycle) parts.push(dim("\xB7 cycle", style));
826
+ return parts.join(" ");
827
+ }
828
+ function impactLine(result, style) {
829
+ const { summary } = result;
830
+ const parts = [];
831
+ if (summary.routes > 0) parts.push(pluralize(summary.routes, "route"));
832
+ if (summary.components > 0) parts.push(pluralize(summary.components, "component"));
833
+ if (summary.tests > 0) parts.push(pluralize(summary.tests, "test"));
834
+ if (summary.utilities > 0) parts.push(pluralize(summary.utilities, "utility"));
835
+ return parts.length > 0 ? parts.join(" \xB7 ") : dim("none", style);
775
836
  }
776
837
  function buildAffectedTree(result, cwd, style) {
777
838
  const { affected, graph } = result;
@@ -811,29 +872,32 @@ function renderAnalyzeReport(result, options, writer) {
811
872
  const style = textStyle(options);
812
873
  const { summary, risk } = result;
813
874
  const file = displayPath(result.targetPath, options.cwd);
814
- writer.writeLine(banner("\u{1F30A} Ripple Analysis", style));
875
+ writer.writeLine(brandHeader({ label: "impact analysis", version: options.version, style }));
815
876
  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
- );
877
+ const score = risk.score.toFixed(1);
878
+ const gauge = riskGauge(risk.score, risk.level, style);
879
+ const rows = [
880
+ ["File", file],
881
+ ["Risk", `${riskBadge(risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}`],
882
+ ["Impact", impactLine(result, style)],
883
+ ["Affected", pluralize(summary.affectedFiles, "file")],
884
+ ["Max depth", String(summary.maxDepth)],
885
+ ["Confidence", confidenceText(summary.confidence, style)],
886
+ ["Duration", dim(formatDuration(result.durationMs), style)]
887
+ ];
888
+ const width = Math.max(...rows.map(([key]) => key.length));
889
+ for (const [key, value] of rows) {
890
+ writer.writeLine(`${key.padEnd(width)} ${value}`);
891
+ }
830
892
  writer.writeLine();
831
- writer.writeLine(sectionTitle("Top Impact", style));
893
+ writer.writeLine(sectionHeader("Top impact", style));
832
894
  for (const area of summary.topImpact) {
833
- writer.writeLine(`\u2022 ${strong(area.label, style)} ${dim(`(${area.count})`, style)}`);
895
+ writer.writeLine(
896
+ `${icon("bullet")} ${strong(area.label, style)} ${dim(`(${area.count})`, style)}`
897
+ );
834
898
  }
835
899
  writer.writeLine();
836
- writer.writeLine(sectionTitle("Circular Dependencies", style));
900
+ writer.writeLine(sectionHeader("Circular dependencies", style));
837
901
  if (result.graph.cycles.length === 0) {
838
902
  writer.writeLine(dim("none", style));
839
903
  } else {
@@ -841,13 +905,13 @@ function renderAnalyzeReport(result, options, writer) {
841
905
  const chain = cycle.path.map((key) => {
842
906
  const node = result.graph.nodes.get(key);
843
907
  return displayPath(node?.path ?? key, options.cwd);
844
- }).join(" \u2192 ");
845
- writer.writeLine(`\u2B55 ${chain}`);
908
+ }).join(` ${icon("arrowRight")} `);
909
+ writer.writeLine(`${icon("circle")} ${chain}`);
846
910
  }
847
911
  }
848
912
  writer.writeLine();
849
913
  if (summary.affectedFiles > 0) {
850
- writer.writeLine(sectionTitle(`Affected Files (${summary.affectedFiles})`, style));
914
+ writer.writeLine(sectionHeader(`Affected files (${summary.affectedFiles})`, style));
851
915
  const tree = buildAffectedTree(result, options.cwd, style);
852
916
  writer.writeLine(renderTree(tree));
853
917
  const limit = options.listLimit ?? DEFAULT_LIST_LIMIT;
@@ -859,22 +923,27 @@ function renderAnalyzeReport(result, options, writer) {
859
923
  writer.writeLine();
860
924
  }
861
925
  if (options.verbose) {
862
- writer.writeLine(sectionTitle("Risk Factors", style));
926
+ writer.writeLine(sectionHeader("Risk factors", style));
863
927
  const columns = [
864
928
  { header: "Factor" },
865
929
  { header: "Weight" },
866
930
  { header: "Signal" },
867
931
  { header: "Points" }
868
932
  ];
869
- const rows = risk.factors.map((factor) => [
933
+ const rowsVerbose = risk.factors.map((factor) => [
870
934
  factor.label,
871
935
  factor.weight.toFixed(2),
872
936
  factor.value.toFixed(2),
873
937
  factor.contribution.toFixed(2)
874
938
  ]);
875
- writer.writeLine(renderTable(columns, rows));
939
+ writer.writeLine(renderTable(columns, rowsVerbose));
876
940
  writer.writeLine();
877
941
  }
942
+ if (style.color) {
943
+ writer.writeLine(
944
+ dim(`${icon("info")} Tip: run "ripple doctor" to check project health`, style)
945
+ );
946
+ }
878
947
  }
879
948
  function buildAnalyzeJsonReport(result, version, cwd) {
880
949
  const realPath = (key) => result.graph.nodes.get(key)?.path ?? key;
@@ -898,7 +967,7 @@ function buildAnalyzeJsonReport(result, version, cwd) {
898
967
  function renderGraphReport(input, options, writer) {
899
968
  const style = textStyle(options);
900
969
  const { stats } = input;
901
- writer.writeLine(banner("\u{1F30A} Dependency Graph", style));
970
+ writer.writeLine(brandHeader({ label: "dependency graph", version: options.version, style }));
902
971
  writer.writeLine();
903
972
  writer.writeLine(
904
973
  renderKeyValue([
@@ -911,26 +980,26 @@ function renderGraphReport(input, options, writer) {
911
980
  );
912
981
  writer.writeLine();
913
982
  if (input.forward) {
914
- writer.writeLine(sectionTitle("Dependants (forward)", style));
915
- writer.writeLine(renderTree(input.forward.map((path12) => ({ label: path12 }))));
983
+ writer.writeLine(sectionHeader("Dependants (forward)", style));
984
+ writer.writeLine(renderTree(input.forward.map((path13) => ({ label: path13 }))));
916
985
  writer.writeLine();
917
986
  }
918
987
  if (input.reverse) {
919
- writer.writeLine(sectionTitle("Dependents (reverse)", style));
920
- writer.writeLine(renderTree(input.reverse.map((path12) => ({ label: path12 }))));
988
+ writer.writeLine(sectionHeader("Dependents (reverse)", style));
989
+ writer.writeLine(renderTree(input.reverse.map((path13) => ({ label: path13 }))));
921
990
  writer.writeLine();
922
991
  }
923
- if (input.cycles.length > 0) {
924
- writer.writeLine(sectionTitle("Circular Dependencies", style));
992
+ writer.writeLine(sectionHeader("Circular dependencies", style));
993
+ if (input.cycles.length === 0) {
994
+ writer.writeLine(dim("none", style));
995
+ } else {
925
996
  for (const cycle of input.cycles) {
926
997
  const chain = cycle.path.map((key) => {
927
998
  const node = input.nodes.get(key);
928
999
  return displayPath(node?.path ?? key, input.cwd);
929
- }).join(" \u2192 ");
930
- writer.writeLine(`\u2B55 ${chain}`);
1000
+ }).join(` ${icon("arrowRight")} `);
1001
+ writer.writeLine(`${icon("circle")} ${chain}`);
931
1002
  }
932
- } else {
933
- writer.writeLine(dim("No circular dependencies found.", style));
934
1003
  }
935
1004
  }
936
1005
  function buildGraphJsonReport(input, version) {
@@ -1209,8 +1278,8 @@ function findCycles(graph) {
1209
1278
  }
1210
1279
  const members = [...component].sort();
1211
1280
  const componentSet = new Set(component);
1212
- const path12 = representativeCyclePath(graph, component, componentSet);
1213
- cycles.push({ members, path: path12 ?? [members[0], members[0]] });
1281
+ const path13 = representativeCyclePath(graph, component, componentSet);
1282
+ cycles.push({ members, path: path13 ?? [members[0], members[0]] });
1214
1283
  }
1215
1284
  cycles.sort(
1216
1285
  (a, b) => a.members.length - b.members.length || a.members[0].localeCompare(b.members[0])
@@ -1219,14 +1288,14 @@ function findCycles(graph) {
1219
1288
  }
1220
1289
  function representativeCyclePath(graph, component, componentSet) {
1221
1290
  const inPath = /* @__PURE__ */ new Set();
1222
- const path12 = [];
1291
+ const path13 = [];
1223
1292
  let current = component[0];
1224
1293
  for (let steps = 0; steps <= component.length * 2 && current !== void 0; steps++) {
1225
1294
  const firstOccurrence = inPath.has(current);
1226
1295
  if (firstOccurrence) {
1227
- const start = path12.indexOf(current);
1296
+ const start = path13.indexOf(current);
1228
1297
  if (start === -1) return void 0;
1229
- const cycle = path12.slice(start);
1298
+ const cycle = path13.slice(start);
1230
1299
  if (cycle.length > 1 || graph.forward.get(current)?.has(current)) {
1231
1300
  cycle.push(current);
1232
1301
  return cycle;
@@ -1234,7 +1303,7 @@ function representativeCyclePath(graph, component, componentSet) {
1234
1303
  return void 0;
1235
1304
  }
1236
1305
  inPath.add(current);
1237
- path12.push(current);
1306
+ path13.push(current);
1238
1307
  const next = [...graph.forward.get(current) ?? []].find(
1239
1308
  (candidate) => componentSet.has(candidate)
1240
1309
  );
@@ -1413,11 +1482,13 @@ async function analyzeCommand(fileArg, options, ctx2) {
1413
1482
  if (!await pathExists(targetPath)) {
1414
1483
  throw new RippleError(`File not found: ${fileArg}`, ExitCode.NotFound);
1415
1484
  }
1416
- const spinner = createSpinner(!options.json);
1417
- spinner.start("Analyzing project\u2026");
1485
+ const tracker = createStageTracker(!options.json);
1486
+ tracker.next("Loading config");
1418
1487
  const started = Date.now();
1419
1488
  const context = await loadProjectContext(ctx2.cwd, options.config);
1489
+ tracker.next("Building dependency graph");
1420
1490
  const { graph, entryPoints, durationMs: pipelineMs } = await runPipeline(context);
1491
+ tracker.next("Analyzing impact");
1421
1492
  const completed = Date.now();
1422
1493
  const result = analyzeFile({
1423
1494
  graph,
@@ -1428,7 +1499,7 @@ async function analyzeCommand(fileArg, options, ctx2) {
1428
1499
  durationMs: pipelineMs + (completed - started),
1429
1500
  ...options.depth !== void 0 ? { maxDepth: options.depth } : {}
1430
1501
  });
1431
- spinner.stop();
1502
+ tracker.done();
1432
1503
  if (options.json) {
1433
1504
  const report = buildAnalyzeJsonReport(result, ctx2.version, ctx2.cwd);
1434
1505
  ctx2.writer.write(serializeJson(report));
@@ -1438,8 +1509,9 @@ async function analyzeCommand(fileArg, options, ctx2) {
1438
1509
  result,
1439
1510
  {
1440
1511
  cwd: ctx2.cwd,
1441
- color: options.color !== false,
1442
- verbose: options.verbose
1512
+ color: resolveColor(options.color),
1513
+ verbose: options.verbose,
1514
+ version: ctx2.version
1443
1515
  },
1444
1516
  ctx2.writer
1445
1517
  );
@@ -1468,11 +1540,12 @@ function walk(graph, startKey, edges, maxDepth, cwd) {
1468
1540
  return entries.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((entry) => entry.path);
1469
1541
  }
1470
1542
  async function graphCommand(fileArg, options, ctx2) {
1471
- const spinner = createSpinner(!options.json);
1472
- spinner.start(fileArg ? "Building dependency tree\u2026" : "Building graph\u2026");
1543
+ const tracker = createStageTracker(!options.json);
1544
+ tracker.next("Loading config");
1473
1545
  const cwd = ctx2.cwd;
1474
1546
  try {
1475
1547
  const context = await loadProjectContext(cwd, options.config);
1548
+ tracker.next("Building graph");
1476
1549
  const { graph } = await runPipeline(context);
1477
1550
  const input = {
1478
1551
  stats: graph.stats,
@@ -1481,13 +1554,19 @@ async function graphCommand(fileArg, options, ctx2) {
1481
1554
  cwd
1482
1555
  };
1483
1556
  if (!fileArg) {
1557
+ tracker.done();
1484
1558
  if (options.json) {
1485
1559
  const report = buildGraphJsonReport(input, ctx2.version);
1486
1560
  ctx2.writer.write(serializeJson(report));
1487
1561
  } else {
1488
1562
  renderGraphReport(
1489
1563
  input,
1490
- { cwd, color: options.color !== false, verbose: options.verbose },
1564
+ {
1565
+ cwd,
1566
+ color: resolveColor(options.color),
1567
+ verbose: options.verbose,
1568
+ version: ctx2.version
1569
+ },
1491
1570
  ctx2.writer
1492
1571
  );
1493
1572
  }
@@ -1497,6 +1576,7 @@ async function graphCommand(fileArg, options, ctx2) {
1497
1576
  const { key } = requireNode(graph, targetPath, context.rootDir);
1498
1577
  const dependants = walk(graph, key, graph.forward, options.depth, cwd);
1499
1578
  const dependents = walk(graph, key, graph.reverse, options.depth, cwd);
1579
+ tracker.done();
1500
1580
  if (options.json) {
1501
1581
  const report = buildGraphJsonReport(
1502
1582
  {
@@ -1513,17 +1593,38 @@ async function graphCommand(fileArg, options, ctx2) {
1513
1593
  forward: options.reverse ? void 0 : dependants,
1514
1594
  reverse: options.reverse ? dependents : void 0
1515
1595
  },
1516
- { cwd, color: options.color !== false, verbose: options.verbose },
1596
+ { cwd, color: resolveColor(options.color), verbose: options.verbose, version: ctx2.version },
1517
1597
  ctx2.writer
1518
1598
  );
1519
1599
  }
1520
1600
  return ExitCode.Success;
1521
1601
  } finally {
1522
- spinner.stop();
1602
+ tracker.done();
1523
1603
  }
1524
1604
  }
1605
+ function createSpinner(enabled) {
1606
+ if (!enabled || !process.stdout.isTTY) {
1607
+ return { start: () => {
1608
+ }, stop: () => {
1609
+ } };
1610
+ }
1611
+ let instance;
1612
+ return {
1613
+ start(text) {
1614
+ instance = ora(text).start();
1615
+ },
1616
+ stop() {
1617
+ instance?.stop();
1618
+ instance = void 0;
1619
+ }
1620
+ };
1621
+ }
1622
+
1623
+ // src/commands/doctor.ts
1525
1624
  async function doctorCommand(options, ctx2) {
1526
- const spinner = createSpinner(options.color !== false);
1625
+ const color = resolveColor(options.color);
1626
+ const style = { color };
1627
+ const spinner = createSpinner(color);
1527
1628
  spinner.start("Running diagnostics\u2026");
1528
1629
  const results = [];
1529
1630
  const check = (label, state, detail) => {
@@ -1548,7 +1649,7 @@ async function doctorCommand(options, ctx2) {
1548
1649
  context = await loadProjectContext(ctx2.cwd, options.config);
1549
1650
  } catch (error) {
1550
1651
  check("ripple config", "fail", error instanceof Error ? error.message : String(error));
1551
- renderResults(results, ctx2);
1652
+ renderResults(results, ctx2, color);
1552
1653
  return ExitCode.InvalidConfig;
1553
1654
  }
1554
1655
  if (context.tsconfig) {
@@ -1597,29 +1698,39 @@ async function doctorCommand(options, ctx2) {
1597
1698
  } finally {
1598
1699
  spinner.stop();
1599
1700
  }
1600
- renderResults(results, ctx2);
1701
+ renderResults(results, ctx2, color);
1601
1702
  const failed = results.some((check2) => check2.state === "fail");
1602
1703
  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
- );
1704
+ 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 };
1705
+ const verdictIcon = style.color ? verdict.color(verdict.icon) : verdict.icon;
1706
+ ctx2.writer.writeLine(`${verdictIcon} ${verdict.text}`);
1606
1707
  return failed ? ExitCode.Failure : ExitCode.Success;
1607
1708
  }
1608
- function renderResults(results, ctx2) {
1709
+ function renderResults(results, ctx2, color) {
1710
+ const style = { color };
1711
+ ctx2.writer.writeLine(brandHeader({ label: "project health check", version: ctx2.version, style }));
1712
+ ctx2.writer.writeLine();
1713
+ const width = Math.max(...results.map((check) => check.label.length));
1609
1714
  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}`);
1715
+ const stateIcon = check.state === "ok" ? icon("tick") : check.state === "warn" ? icon("warning") : icon("cross");
1716
+ const colored = check.state === "ok" ? chalk.green : check.state === "warn" ? chalk.yellow : chalk.red;
1717
+ const mark = style.color ? colored(stateIcon) : stateIcon;
1718
+ const detail = check.detail ? ` ${dim(check.detail, style)}` : "";
1719
+ ctx2.writer.writeLine(`${mark} ${check.label.padEnd(width)}${detail}`);
1613
1720
  }
1614
1721
  ctx2.writer.writeLine();
1615
1722
  }
1616
1723
  var INIT_FILENAME = "ripple.config.json";
1617
1724
  async function initCommand(options, ctx2) {
1725
+ const style = { color: resolveColor(options.color) };
1618
1726
  const target = path8.join(ctx2.cwd, INIT_FILENAME);
1619
1727
  try {
1620
1728
  await fs.access(target);
1621
1729
  if (!options.force) {
1622
- ctx2.writer.writeLine(`\u2716 ${INIT_FILENAME} already exists. Pass --force to overwrite it.`);
1730
+ const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1731
+ ctx2.writer.writeLine(
1732
+ `${mark} ${INIT_FILENAME} already exists. Pass --force to overwrite it.`
1733
+ );
1623
1734
  return ExitCode.Failure;
1624
1735
  }
1625
1736
  } catch {
@@ -1630,16 +1741,237 @@ async function initCommand(options, ctx2) {
1630
1741
  try {
1631
1742
  await fs.writeFile(target, `${JSON.stringify(payload, null, 2)}
1632
1743
  `, "utf8");
1633
- ctx2.writer.writeLine(`\u2713 Wrote ${INIT_FILENAME}`);
1634
- ctx2.writer.writeLine(` Edit it to tune include globs, aliases and risk weights.`);
1744
+ const mark = style.color ? chalk.green(icon("tick")) : icon("tick");
1745
+ ctx2.writer.writeLine(`${mark} Wrote ${INIT_FILENAME}`);
1746
+ ctx2.writer.writeLine(
1747
+ ` ${dim("Edit it to tune include globs, aliases and risk weights.", style)}`
1748
+ );
1635
1749
  return ExitCode.Success;
1636
1750
  } catch (error) {
1751
+ const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1637
1752
  ctx2.writer.writeLine(
1638
- `\u2716 Could not write ${INIT_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1753
+ `${mark} Could not write ${INIT_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1639
1754
  );
1640
1755
  return ExitCode.Failure;
1641
1756
  }
1642
1757
  }
1758
+ var DEFAULT_BASES = ["origin/main", "main", "HEAD~1"];
1759
+ function git(args, cwd) {
1760
+ return execFileSync("git", args, {
1761
+ cwd,
1762
+ encoding: "utf8",
1763
+ stdio: ["ignore", "pipe", "pipe"]
1764
+ }).trim();
1765
+ }
1766
+ function refExists(ref, cwd) {
1767
+ try {
1768
+ const out = git(["rev-parse", "--verify", ref], cwd);
1769
+ return out.length > 0;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ }
1774
+ function resolveBase(cwd, requested) {
1775
+ if (requested) {
1776
+ if (!refExists(requested, cwd)) {
1777
+ throw new RippleError(`Git ref not found: ${requested}`, ExitCode.NotFound);
1778
+ }
1779
+ return requested;
1780
+ }
1781
+ for (const candidate of DEFAULT_BASES) {
1782
+ if (refExists(candidate, cwd)) return candidate;
1783
+ }
1784
+ throw new RippleError(
1785
+ "No git base ref found (tried origin/main, main, HEAD~1). Pass --base <ref> explicitly.",
1786
+ ExitCode.NotFound
1787
+ );
1788
+ }
1789
+ function changedFiles(cwd, base) {
1790
+ const baseLabel = resolveBase(cwd, base);
1791
+ let baseCommit = baseLabel;
1792
+ try {
1793
+ const mergeBase = git(["merge-base", baseLabel, "HEAD"], cwd);
1794
+ if (mergeBase) baseCommit = mergeBase;
1795
+ } catch {
1796
+ }
1797
+ const files = /* @__PURE__ */ new Set();
1798
+ const tracked = git(["diff", "--name-only", "--diff-filter=ACMRT", baseCommit], cwd);
1799
+ for (const line of tracked.split("\n")) {
1800
+ if (line.trim()) files.add(line.trim());
1801
+ }
1802
+ const untracked = git(["ls-files", "--others", "--exclude-standard"], cwd);
1803
+ for (const line of untracked.split("\n")) {
1804
+ if (line.trim()) files.add(line.trim());
1805
+ }
1806
+ return { baseLabel, files: [...files].sort() };
1807
+ }
1808
+
1809
+ // src/commands/diff.ts
1810
+ var LEVEL_INDEX = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
1811
+ var GATE_INDEX = { medium: 1, high: 2, critical: 3 };
1812
+ var VERDICT_NOTE = {
1813
+ medium: "no MEDIUM or worse",
1814
+ high: "no HIGH or CRITICAL",
1815
+ critical: "no CRITICAL"
1816
+ };
1817
+ function countLevels(results) {
1818
+ const counts = { low: 0, medium: 0, high: 0, critical: 0 };
1819
+ for (const result of results) {
1820
+ counts[result.risk.level.toLowerCase()] += 1;
1821
+ }
1822
+ return counts;
1823
+ }
1824
+ function riskySummary(counts, gate) {
1825
+ const gateIndex = GATE_INDEX[gate];
1826
+ const parts = [];
1827
+ if (LEVEL_INDEX.CRITICAL >= gateIndex && counts.critical > 0)
1828
+ parts.push(`${counts.critical} CRITICAL`);
1829
+ if (LEVEL_INDEX.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
1830
+ if (LEVEL_INDEX.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
1831
+ if (LEVEL_INDEX.LOW >= gateIndex && counts.low > 0) parts.push(`${counts.low} LOW`);
1832
+ return parts.length > 0 ? parts.join(" \xB7 ") : "none";
1833
+ }
1834
+ async function diffCommand(options, ctx2) {
1835
+ const style = { color: resolveColor(options.color) };
1836
+ const tracker = createStageTracker(!options.json);
1837
+ const gate = options.gate ?? "high";
1838
+ const started = Date.now();
1839
+ tracker.next("Resolving git changes");
1840
+ const changed = changedFiles(ctx2.cwd, options.base);
1841
+ tracker.next("Analyzing changed files");
1842
+ const context = await loadProjectContext(ctx2.cwd, options.config);
1843
+ const { graph, entryPoints } = await runPipeline(context);
1844
+ const byRel = /* @__PURE__ */ new Map();
1845
+ const skipped = [];
1846
+ for (const rel of changed.files) {
1847
+ const abs = path8.resolve(ctx2.cwd, rel);
1848
+ const key = pathKey(abs, context.rootDir);
1849
+ if (!graph.nodes.has(key)) {
1850
+ skipped.push(rel);
1851
+ continue;
1852
+ }
1853
+ byRel.set(
1854
+ rel,
1855
+ analyzeFile({
1856
+ graph,
1857
+ context,
1858
+ entryPoints,
1859
+ targetKey: key,
1860
+ targetPath: abs,
1861
+ durationMs: 0,
1862
+ ...options.depth !== void 0 ? { maxDepth: options.depth } : {}
1863
+ })
1864
+ );
1865
+ }
1866
+ tracker.done();
1867
+ const results = [...byRel.values()].sort((a, b) => b.risk.score - a.risk.score);
1868
+ const counts = countLevels(results);
1869
+ const blocked = results.some((result) => LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate]);
1870
+ const durationMs = Date.now() - started;
1871
+ if (options.json) {
1872
+ ctx2.writer.write(
1873
+ serializeJson(
1874
+ buildDiffJsonReport(
1875
+ changed.baseLabel,
1876
+ changed.files,
1877
+ byRel,
1878
+ gate,
1879
+ blocked,
1880
+ counts,
1881
+ durationMs,
1882
+ ctx2.version
1883
+ )
1884
+ )
1885
+ );
1886
+ return blocked ? ExitCode.Failure : ExitCode.Success;
1887
+ }
1888
+ renderDiffReport(
1889
+ {
1890
+ cwd: ctx2.cwd,
1891
+ baseLabel: changed.baseLabel,
1892
+ files: changed.files,
1893
+ results,
1894
+ skipped,
1895
+ counts,
1896
+ gate,
1897
+ blocked,
1898
+ durationMs,
1899
+ style
1900
+ },
1901
+ ctx2.writer
1902
+ );
1903
+ return blocked ? ExitCode.Failure : ExitCode.Success;
1904
+ }
1905
+ function renderDiffReport(input, writer) {
1906
+ const { cwd, baseLabel, files, results, skipped, counts, gate, blocked, durationMs, style } = input;
1907
+ writer.writeLine(brandHeader({ label: `diff vs ${baseLabel}`, style }));
1908
+ writer.writeLine();
1909
+ const rows = [
1910
+ ["Changed", pluralize(files.length, "file")],
1911
+ ["Source", pluralize(results.length, "file")],
1912
+ ["Duration", dim(formatDuration(durationMs), style)]
1913
+ ];
1914
+ const width = Math.max(...rows.map(([key]) => key.length));
1915
+ for (const [key, value] of rows) {
1916
+ writer.writeLine(`${key.padEnd(width)} ${value}`);
1917
+ }
1918
+ writer.writeLine();
1919
+ const mark = blocked ? icon("cross") : icon("tick");
1920
+ const color = blocked ? chalk.red : chalk.green;
1921
+ const markText = style.color ? color(mark) : mark;
1922
+ const verdict = blocked ? "Gate blocked" : "Gate passed";
1923
+ const verdictText = style.color ? color(strong(verdict, style)) : verdict;
1924
+ writer.writeLine(
1925
+ `${markText} ${verdictText} \u2014 ${riskySummary(counts, gate)} ${dim(`(${VERDICT_NOTE[gate]})`, style)}`
1926
+ );
1927
+ writer.writeLine();
1928
+ writer.writeLine(sectionHeader(`Risk analysis (${results.length} files)`, style));
1929
+ for (const result of results) {
1930
+ const risky = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
1931
+ const fileIcon = risky ? icon("cross") : icon("tick");
1932
+ const fileIconText = style.color ? risky ? chalk.red(fileIcon) : chalk.green(fileIcon) : fileIcon;
1933
+ const score = result.risk.score.toFixed(1);
1934
+ const gauge = riskGauge(result.risk.score, result.risk.level, style);
1935
+ const file = displayPath(result.targetPath, cwd);
1936
+ writer.writeLine(
1937
+ `${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}`
1938
+ );
1939
+ }
1940
+ for (const rel of skipped) {
1941
+ writer.writeLine(
1942
+ `${dim(icon("ellipsis"), style)} ${dim(rel, style)} ${dim("(not a source file)", style)}`
1943
+ );
1944
+ }
1945
+ writer.writeLine();
1946
+ }
1947
+ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked, counts, durationMs, version) {
1948
+ const files = [];
1949
+ for (const rel of changedFileNames) {
1950
+ const result = byRel.get(rel);
1951
+ if (result) {
1952
+ files.push({
1953
+ file: rel,
1954
+ analyzed: true,
1955
+ risk: result.risk,
1956
+ affectedFiles: result.summary.affectedFiles,
1957
+ targetInCycle: result.targetInCycle
1958
+ });
1959
+ } else {
1960
+ files.push({ file: rel, analyzed: false });
1961
+ }
1962
+ }
1963
+ files.sort((a, b) => (b.risk?.score ?? -1) - (a.risk?.score ?? -1));
1964
+ return {
1965
+ tool: "ripple",
1966
+ version,
1967
+ command: "diff",
1968
+ base: baseLabel,
1969
+ changedFiles: changedFileNames.length,
1970
+ files,
1971
+ gate: { level: gate, blocked, counts },
1972
+ durationMs
1973
+ };
1974
+ }
1643
1975
 
1644
1976
  // src/cli/program.ts
1645
1977
  var PROGRAM_NAME = "ripple";
@@ -1678,6 +2010,27 @@ function createProgram(ctx2) {
1678
2010
  throw new ExitError(exitCode2);
1679
2011
  }
1680
2012
  });
2013
+ program.command("diff").description("Analyze changed files since a base ref and gate risky changes").option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include extra sections").option("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").option(
2014
+ "-g, --gate <level>",
2015
+ "risk level that blocks the gate (medium | high | critical)",
2016
+ parseGateLevel
2017
+ ).option("-d, --depth <number>", "cap the reverse traversal depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
2018
+ const exitCode2 = await diffCommand(
2019
+ {
2020
+ json: Boolean(options.json),
2021
+ verbose: Boolean(options.verbose),
2022
+ ...options.base !== void 0 ? { base: options.base } : {},
2023
+ ...options.gate !== void 0 ? { gate: options.gate } : {},
2024
+ ...options.depth !== void 0 ? { depth: options.depth } : {},
2025
+ ...options.config !== void 0 ? { config: options.config } : {},
2026
+ color: options.color
2027
+ },
2028
+ ctx2
2029
+ );
2030
+ if (exitCode2 !== ExitCode.Success) {
2031
+ throw new ExitError(exitCode2);
2032
+ }
2033
+ });
1681
2034
  program.command("doctor").description("Check the project and environment for Ripple readiness").option("-v, --verbose", "print detailed diagnostics").option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
1682
2035
  const exitCode2 = await doctorCommand(
1683
2036
  {
@@ -1691,8 +2044,11 @@ function createProgram(ctx2) {
1691
2044
  throw new ExitError(exitCode2);
1692
2045
  }
1693
2046
  });
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);
2047
+ 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) => {
2048
+ const exitCode2 = await initCommand(
2049
+ { force: Boolean(options.force), color: options.color },
2050
+ ctx2
2051
+ );
1696
2052
  if (exitCode2 !== ExitCode.Success) {
1697
2053
  throw new ExitError(exitCode2);
1698
2054
  }
@@ -1700,6 +2056,11 @@ function createProgram(ctx2) {
1700
2056
  program.command("version").description("Print the Ripple version").action(() => {
1701
2057
  ctx2.writer.writeLine(ctx2.version);
1702
2058
  });
2059
+ program.addHelpText(
2060
+ "beforeAll",
2061
+ `ripple \u2014 dependency impact analysis for TypeScript and JavaScript
2062
+ `
2063
+ );
1703
2064
  return program;
1704
2065
  }
1705
2066
  var ExitError = class extends Error {
@@ -1721,6 +2082,16 @@ function parsePositiveInt(value) {
1721
2082
  }
1722
2083
  return parsed;
1723
2084
  }
2085
+ function parseGateLevel(value) {
2086
+ if (value === "medium" || value === "high" || value === "critical") {
2087
+ return value;
2088
+ }
2089
+ throw new CommanderError(
2090
+ 1,
2091
+ "commander.invalidArgument",
2092
+ `expected one of medium | high | critical, got "${value}"`
2093
+ );
2094
+ }
1724
2095
  function printError(error, ctx2) {
1725
2096
  if (error instanceof RippleError) {
1726
2097
  ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
@@ -1754,7 +2125,7 @@ async function run({ argv, ctx: ctx2 }) {
1754
2125
 
1755
2126
  // package.json
1756
2127
  var package_default = {
1757
- version: "0.1.1"};
2128
+ version: "0.3.0"};
1758
2129
 
1759
2130
  // src/cli/bin.ts
1760
2131
  var ctx = {