@alimaandev/ripple 0.4.0 → 0.6.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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG, rippleConfigSchema, formatSchemaIssues } from './chunk-LDCFNORX.js';
2
+ import { rippleConfigSchema, DEFAULT_CONFIG, formatSchemaIssues } from './chunk-CFH4UW2E.js';
3
3
  import { CommanderError, Command } from 'commander';
4
4
  import { createJiti } from 'jiti';
5
5
  import path8 from 'path';
@@ -9,9 +9,9 @@ import { Project, ts, SyntaxKind } from 'ts-morph';
9
9
  import chalk from 'chalk';
10
10
  import figures from 'figures';
11
11
  import treeify from 'treeify';
12
- import boxen from 'boxen';
13
12
  import fg from 'fast-glob';
14
13
  import ora from 'ora';
14
+ import picomatch from 'picomatch';
15
15
  import { execFileSync } from 'child_process';
16
16
 
17
17
  // src/output/writer.ts
@@ -507,7 +507,7 @@ function reverseTraverse(graph, targetKey, options) {
507
507
  const { maxDepth, categorize } = options;
508
508
  const affected = /* @__PURE__ */ new Map();
509
509
  const visited = /* @__PURE__ */ new Set([targetKey]);
510
- const cycleMembers = cycleMemberPaths(graph);
510
+ const cycleMembers2 = cycleMemberPaths(graph);
511
511
  if (!graph.reverse.has(targetKey)) {
512
512
  return { affected, maxDepth: 0 };
513
513
  }
@@ -529,7 +529,7 @@ function reverseTraverse(graph, targetKey, options) {
529
529
  depth: current.depth,
530
530
  direct: current.depth === 1,
531
531
  categories: categorize(filePath),
532
- inCycle: cycleMembers.has(current.key)
532
+ inCycle: cycleMembers2.has(current.key)
533
533
  });
534
534
  if (maxDepth === void 0 || current.depth < maxDepth) {
535
535
  for (const importer of graph.reverse.get(current.key) ?? []) {
@@ -786,24 +786,17 @@ function renderTree(nodes) {
786
786
  }
787
787
  return treeify.asTree(root, false, false).trimEnd();
788
788
  }
789
- var DIVIDER_WIDTH = 38;
790
789
  function brandHeader(options) {
791
790
  const { label, version, style } = options;
792
791
  if (!style.color) {
793
792
  return `ripple \u2014 ${label}${version ? ` \xB7 v${version}` : ""}`;
794
793
  }
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
- });
794
+ const title = chalk.bold.cyan(`ripple \xB7 ${label}`);
795
+ return `${title}${version ? ` ${chalk.dim(`\xB7 v${version}`)}` : ""}`;
802
796
  }
803
797
  function sectionHeader(title, style) {
804
798
  if (!style.color) return title;
805
- return `${chalk.dim("\u2500".repeat(DIVIDER_WIDTH))}
806
- ${chalk.bold(title)}`;
799
+ return `${chalk.dim("\u203A")} ${chalk.bold(title)}`;
807
800
  }
808
801
 
809
802
  // src/ui/gauge.ts
@@ -1392,7 +1385,7 @@ function buildGraph(project, filePaths, context) {
1392
1385
  const reverse = /* @__PURE__ */ new Map();
1393
1386
  const external = /* @__PURE__ */ new Map();
1394
1387
  let parsedCount = 0;
1395
- let internalEdges = 0;
1388
+ let internalEdges2 = 0;
1396
1389
  let unresolvedEdges = 0;
1397
1390
  let externalEdges = 0;
1398
1391
  for (const parsed of parsedFiles) {
@@ -1412,7 +1405,7 @@ function buildGraph(project, filePaths, context) {
1412
1405
  parsed.imports.map((decl) => decl.raw),
1413
1406
  context
1414
1407
  );
1415
- internalEdges += internal.length;
1408
+ internalEdges2 += internal.length;
1416
1409
  unresolvedEdges += unresolved.length;
1417
1410
  externalEdges += externalSpecifiers.length;
1418
1411
  for (const target of internal) {
@@ -1437,7 +1430,7 @@ function buildGraph(project, filePaths, context) {
1437
1430
  stats: {
1438
1431
  files: parsedFiles.length,
1439
1432
  parsed: parsedCount,
1440
- internalEdges,
1433
+ internalEdges: internalEdges2,
1441
1434
  unresolvedEdges,
1442
1435
  externalEdges,
1443
1436
  cycles: 0
@@ -1518,6 +1511,150 @@ async function analyzeCommand(fileArg, options, ctx2) {
1518
1511
  return ExitCode.Success;
1519
1512
  }
1520
1513
 
1514
+ // src/formatter/graph-export.ts
1515
+ var escapeDot = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1516
+ var escapeMermaid = (value) => value.replace(/#/g, "#35;").replace(/"/g, "#quot;");
1517
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1518
+ function sortedKeys(input) {
1519
+ return [...input.nodes.keys()].sort((a, b) => {
1520
+ const left = input.nodes.get(a) ?? a;
1521
+ const right = input.nodes.get(b) ?? b;
1522
+ return left.localeCompare(right);
1523
+ });
1524
+ }
1525
+ var nodeLabel = (input, key) => displayPath(input.nodes.get(key) ?? key, input.cwd);
1526
+ function cycleMembers(input) {
1527
+ const present = /* @__PURE__ */ new Set();
1528
+ for (const cycle of input.cycles) {
1529
+ for (const member of cycle.members) {
1530
+ if (input.nodes.has(member)) present.add(member);
1531
+ }
1532
+ }
1533
+ return [...present].sort((a, b) => nodeLabel(input, a).localeCompare(nodeLabel(input, b)));
1534
+ }
1535
+ function internalEdges(input) {
1536
+ const pairs = [];
1537
+ for (const [source, targets] of input.edges) {
1538
+ if (!input.nodes.has(source)) continue;
1539
+ for (const target of targets) {
1540
+ if (!input.nodes.has(target)) continue;
1541
+ pairs.push([source, target]);
1542
+ }
1543
+ }
1544
+ return pairs.sort((a, b) => {
1545
+ const left = nodeLabel(input, a[0]).localeCompare(nodeLabel(input, b[0]));
1546
+ return left !== 0 ? left : nodeLabel(input, a[1]).localeCompare(nodeLabel(input, b[1]));
1547
+ });
1548
+ }
1549
+ function renderMermaidGraph(input) {
1550
+ const keys = sortedKeys(input);
1551
+ const idByKey = new Map(keys.map((key, index) => [key, `n${index}`]));
1552
+ const lines = ["flowchart LR"];
1553
+ lines.push(" classDef cycle fill:#4a2525,stroke:#f85149,color:#f0b3b3;");
1554
+ for (const key of keys) {
1555
+ lines.push(` ${idByKey.get(key)}["${escapeMermaid(nodeLabel(input, key))}"]`);
1556
+ }
1557
+ for (const [source, target] of internalEdges(input)) {
1558
+ lines.push(` ${idByKey.get(source)} --> ${idByKey.get(target)}`);
1559
+ }
1560
+ const members = cycleMembers(input).map((key) => idByKey.get(key)).filter((id) => id !== void 0);
1561
+ if (members.length > 0) {
1562
+ lines.push(` class ${members.join(",")} cycle;`);
1563
+ }
1564
+ return `${lines.join("\n")}
1565
+ `;
1566
+ }
1567
+ function renderDotGraph(input) {
1568
+ const lines = [
1569
+ "digraph ripple {",
1570
+ " rankdir=LR;",
1571
+ ' node [shape=box, style="rounded,filled", fillcolor="#161b22", color="#30363d", fontcolor="#e6edf3"];',
1572
+ ' edge [color="#8b949e"];'
1573
+ ];
1574
+ for (const key of sortedKeys(input)) {
1575
+ const label = nodeLabel(input, key);
1576
+ lines.push(` "${escapeDot(label)}" [label="${escapeDot(label)}"];`);
1577
+ }
1578
+ for (const [source, target] of internalEdges(input)) {
1579
+ lines.push(
1580
+ ` "${escapeDot(nodeLabel(input, source))}" -> "${escapeDot(nodeLabel(input, target))}";`
1581
+ );
1582
+ }
1583
+ for (const key of cycleMembers(input)) {
1584
+ const label = nodeLabel(input, key);
1585
+ lines.push(` "${escapeDot(label)}" [color="#f85149", penwidth=1.5];`);
1586
+ }
1587
+ lines.push("}");
1588
+ return `${lines.join("\n")}
1589
+ `;
1590
+ }
1591
+ function renderHtmlGraph(input) {
1592
+ const keys = sortedKeys(input);
1593
+ const edges = internalEdges(input);
1594
+ const members = cycleMembers(input);
1595
+ const cycleLines = input.cycles.map(
1596
+ (cycle) => cycle.path.filter((key) => input.nodes.has(key)).map((key) => escapeHtml(nodeLabel(input, key))).join(" &#8594; ")
1597
+ ).filter((line) => line.length > 0);
1598
+ const edgeRows = edges.map(
1599
+ ([source, target]) => ` <tr><td>${escapeHtml(nodeLabel(input, source))}</td><td>&#8594;</td><td>${escapeHtml(nodeLabel(input, target))}</td></tr>`
1600
+ ).join("\n");
1601
+ const cycleRows = cycleLines.length > 0 ? cycleLines.map((line) => ` <li><code>${line}</code></li>`).join("\n") : " <li><em>none</em></li>";
1602
+ return `<!DOCTYPE html>
1603
+ <html lang="en">
1604
+ <head>
1605
+ <meta charset="utf-8">
1606
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1607
+ <title>Ripple \u2014 dependency graph</title>
1608
+ <style>
1609
+ :root { color-scheme: dark; }
1610
+ body { margin: 0; padding: 2rem; background: #0d1117; color: #e6edf3;
1611
+ font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
1612
+ h1 { font-size: 1.4rem; margin: 0 0 .25rem; }
1613
+ h2 { font-size: 1.05rem; margin: 2rem 0 .5rem; color: #79c0ff; }
1614
+ code, pre { font-family: SFMono-Regular, Consolas, monospace; }
1615
+ pre { background: #161b22; border: 1px solid #30363d; border-radius: 8px;
1616
+ padding: 1rem; overflow-x: auto; font-size: .85rem; }
1617
+ table { border-collapse: collapse; width: 100%; font-size: .9rem; }
1618
+ th, td { text-align: left; padding: .35rem .6rem; border-bottom: 1px solid #21262d; }
1619
+ th { color: #8b949e; font-weight: 600; }
1620
+ .stats { display: flex; gap: 1.5rem; margin-top: 1rem; }
1621
+ .stat b { display: block; font-size: 1.6rem; color: #79c0ff; }
1622
+ .stat span { color: #8b949e; font-size: .8rem; }
1623
+ .muted { color: #8b949e; }
1624
+ a { color: #79c0ff; }
1625
+ </style>
1626
+ </head>
1627
+ <body>
1628
+ <h1>Ripple dependency graph</h1>
1629
+ <p class="muted">Exported with <code>ripple graph --format html</code> \u2014 deterministic and offline.</p>
1630
+ <div class="stats">
1631
+ <div class="stat"><b>${keys.length}</b><span>files</span></div>
1632
+ <div class="stat"><b>${edges.length}</b><span>edges</span></div>
1633
+ <div class="stat"><b>${input.cycles.length}</b><span>cycle groups</span></div>
1634
+ </div>
1635
+
1636
+ <h2>Circular dependencies</h2>
1637
+ <ul>${cycleRows}</ul>
1638
+ <p class="muted">Cycle members: ${members.map(escapeHtml).join(", ")}</p>
1639
+
1640
+ <h2>Graph</h2>
1641
+ <pre><code class="language-mermaid">${escapeHtml(renderMermaidGraph(input)).trimEnd()}</code></pre>
1642
+ <p class="muted">Render this diagram with the
1643
+ <a href="https://mermaid.live">Mermaid live editor</a> or a tool that
1644
+ understands Mermaid (GitHub markdown, docs sites, IDEs).</p>
1645
+
1646
+ <h2>Edges (${edges.length})</h2>
1647
+ <table>
1648
+ <thead><tr><th>Source</th><th></th><th>Target</th></tr></thead>
1649
+ <tbody>
1650
+ ${edgeRows}
1651
+ </tbody>
1652
+ </table>
1653
+ </body>
1654
+ </html>
1655
+ `;
1656
+ }
1657
+
1521
1658
  // src/commands/graph.ts
1522
1659
  function walk(graph, startKey, edges, maxDepth, cwd) {
1523
1660
  const seen = /* @__PURE__ */ new Set([startKey]);
@@ -1539,8 +1676,36 @@ function walk(graph, startKey, edges, maxDepth, cwd) {
1539
1676
  }
1540
1677
  return entries.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((entry) => entry.path);
1541
1678
  }
1679
+ function reachableSubgraph(graph, startKey, reverse, maxDepth, cwd) {
1680
+ const edges = reverse ? graph.reverse : graph.forward;
1681
+ const nodes = /* @__PURE__ */ new Map();
1682
+ const visited = /* @__PURE__ */ new Set([startKey]);
1683
+ const queue = [{ key: startKey, depth: 0 }];
1684
+ const collectedEdges = /* @__PURE__ */ new Map();
1685
+ while (queue.length > 0) {
1686
+ const current = queue.shift();
1687
+ if (!current) continue;
1688
+ nodes.set(current.key, graph.nodes.get(current.key)?.path ?? current.key);
1689
+ if (maxDepth !== void 0 && current.depth >= maxDepth) continue;
1690
+ const targets = /* @__PURE__ */ new Set();
1691
+ for (const candidate of edges.get(current.key) ?? []) {
1692
+ targets.add(candidate);
1693
+ if (!visited.has(candidate)) {
1694
+ visited.add(candidate);
1695
+ queue.push({ key: candidate, depth: current.depth + 1 });
1696
+ }
1697
+ }
1698
+ if (targets.size > 0) collectedEdges.set(current.key, targets);
1699
+ }
1700
+ return { nodes, edges: collectedEdges, cycles: graph.cycles, cwd };
1701
+ }
1702
+ function writeExport(format, input, writer) {
1703
+ const rendered = format === "mermaid" ? renderMermaidGraph(input) : format === "dot" ? renderDotGraph(input) : renderHtmlGraph(input);
1704
+ writer.write(rendered);
1705
+ }
1542
1706
  async function graphCommand(fileArg, options, ctx2) {
1543
- const tracker = createStageTracker(!options.json);
1707
+ const format = options.format ?? (options.json ? "json" : "terminal");
1708
+ const tracker = createStageTracker(format === "terminal");
1544
1709
  tracker.next("Loading config");
1545
1710
  const cwd = ctx2.cwd;
1546
1711
  try {
@@ -1553,9 +1718,24 @@ async function graphCommand(fileArg, options, ctx2) {
1553
1718
  nodes: graph.nodes,
1554
1719
  cwd
1555
1720
  };
1721
+ if (format === "mermaid" || format === "dot" || format === "html") {
1722
+ let exportInput;
1723
+ if (fileArg) {
1724
+ const targetPath2 = resolveTargetFile(fileArg, cwd);
1725
+ const { key: key2 } = requireNode(graph, targetPath2, context.rootDir);
1726
+ exportInput = reachableSubgraph(graph, key2, options.reverse, options.depth, cwd);
1727
+ } else {
1728
+ const nodes = /* @__PURE__ */ new Map();
1729
+ for (const [key2, node] of graph.nodes) nodes.set(key2, node.path);
1730
+ exportInput = { nodes, edges: graph.forward, cycles: graph.cycles, cwd };
1731
+ }
1732
+ tracker.done();
1733
+ writeExport(format, exportInput, ctx2.writer);
1734
+ return ExitCode.Success;
1735
+ }
1556
1736
  if (!fileArg) {
1557
1737
  tracker.done();
1558
- if (options.json) {
1738
+ if (format === "json") {
1559
1739
  const report = buildGraphJsonReport(input, ctx2.version);
1560
1740
  ctx2.writer.write(serializeJson(report));
1561
1741
  } else {
@@ -1577,7 +1757,7 @@ async function graphCommand(fileArg, options, ctx2) {
1577
1757
  const dependants = walk(graph, key, graph.forward, options.depth, cwd);
1578
1758
  const dependents = walk(graph, key, graph.reverse, options.depth, cwd);
1579
1759
  tracker.done();
1580
- if (options.json) {
1760
+ if (format === "json") {
1581
1761
  const report = buildGraphJsonReport(
1582
1762
  {
1583
1763
  ...input,
@@ -1720,29 +1900,72 @@ function renderResults(results, ctx2, color) {
1720
1900
  }
1721
1901
  ctx2.writer.writeLine();
1722
1902
  }
1723
- var INIT_FILENAME = "ripple.config.json";
1903
+ var SCHEMA_REF = "./node_modules/@alimaandev/ripple/ripple.schema.json";
1904
+ function jsonPayload(full) {
1905
+ const payload = full ? { ...DEFAULT_CONFIG } : { include: DEFAULT_CONFIG.include };
1906
+ return { ...payload, $schema: SCHEMA_REF };
1907
+ }
1908
+ function tsValue(value, indent) {
1909
+ if (value === void 0) return "undefined";
1910
+ if (Array.isArray(value)) {
1911
+ if (value.length === 0) return "[]";
1912
+ const items = value.map((item) => tsValue(item, `${indent} `)).join(`,
1913
+ ${indent} `);
1914
+ return `[
1915
+ ${indent} ${items}
1916
+ ${indent}]`;
1917
+ }
1918
+ if (typeof value === "object" && value !== null) {
1919
+ const entries = Object.entries(value).filter(
1920
+ ([, entry]) => entry !== void 0
1921
+ );
1922
+ if (entries.length === 0) return "{}";
1923
+ const items = entries.map(([key, entry]) => `${key}: ${tsValue(entry, `${indent} `)}`).join(`,
1924
+ ${indent} `);
1925
+ return `{
1926
+ ${indent} ${items}
1927
+ ${indent}}`;
1928
+ }
1929
+ return JSON.stringify(value);
1930
+ }
1931
+ function tsPayload(full) {
1932
+ if (!full) {
1933
+ return `import type { RippleConfig } from "@alimaandev/ripple";
1934
+
1935
+ const config: RippleConfig = {
1936
+ include: ${JSON.stringify(DEFAULT_CONFIG.include)},
1937
+ };
1938
+
1939
+ export default config;
1940
+ `;
1941
+ }
1942
+ const body = tsValue({ ...DEFAULT_CONFIG }, " ");
1943
+ return `import type { RippleConfig } from "@alimaandev/ripple";
1944
+
1945
+ const config: RippleConfig = ${body};
1946
+
1947
+ export default config;
1948
+ `;
1949
+ }
1724
1950
  async function initCommand(options, ctx2) {
1725
1951
  const style = { color: resolveColor(options.color) };
1726
- const target = path8.join(ctx2.cwd, INIT_FILENAME);
1952
+ const fileName = options.ts ? "ripple.config.ts" : "ripple.config.json";
1953
+ const target = path8.join(ctx2.cwd, fileName);
1727
1954
  try {
1728
1955
  await fs.access(target);
1729
1956
  if (!options.force) {
1730
1957
  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
- );
1958
+ ctx2.writer.writeLine(`${mark} ${fileName} already exists. Pass --force to overwrite it.`);
1734
1959
  return ExitCode.Failure;
1735
1960
  }
1736
1961
  } catch {
1737
1962
  }
1738
- const payload = {
1739
- ...DEFAULT_CONFIG
1740
- };
1963
+ const content = options.ts ? tsPayload(Boolean(options.full)) : `${JSON.stringify(jsonPayload(Boolean(options.full)), null, 2)}
1964
+ `;
1741
1965
  try {
1742
- await fs.writeFile(target, `${JSON.stringify(payload, null, 2)}
1743
- `, "utf8");
1966
+ await fs.writeFile(target, content, "utf8");
1744
1967
  const mark = style.color ? chalk.green(icon("tick")) : icon("tick");
1745
- ctx2.writer.writeLine(`${mark} Wrote ${INIT_FILENAME}`);
1968
+ ctx2.writer.writeLine(`${mark} Wrote ${fileName}`);
1746
1969
  ctx2.writer.writeLine(
1747
1970
  ` ${dim("Edit it to tune include globs, aliases and risk weights.", style)}`
1748
1971
  );
@@ -1750,7 +1973,7 @@ async function initCommand(options, ctx2) {
1750
1973
  } catch (error) {
1751
1974
  const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
1752
1975
  ctx2.writer.writeLine(
1753
- `${mark} Could not write ${INIT_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
1976
+ `${mark} Could not write ${fileName}: ${error instanceof Error ? error.message : String(error)}`
1754
1977
  );
1755
1978
  return ExitCode.Failure;
1756
1979
  }
@@ -1806,9 +2029,48 @@ function changedFiles(cwd, base) {
1806
2029
  return { baseLabel, files: [...files].sort() };
1807
2030
  }
1808
2031
 
1809
- // src/commands/diff.ts
2032
+ // src/formatter/annotations.ts
1810
2033
  var LEVEL_INDEX = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
1811
2034
  var GATE_INDEX = { medium: 1, high: 2, critical: 3 };
2035
+ function escapeProperty(value) {
2036
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
2037
+ }
2038
+ function escapeMessage(value) {
2039
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
2040
+ }
2041
+ function renderAnnotation(annotation) {
2042
+ const props = annotation.file ? ` file=${escapeProperty(annotation.file)},title=${escapeProperty(annotation.title)}` : ` title=${escapeProperty(annotation.title)}`;
2043
+ return `::${annotation.level}${props}::${escapeMessage(annotation.message)}`;
2044
+ }
2045
+ function buildFileAnnotations(files, gate) {
2046
+ return files.map(({ file, result }) => {
2047
+ const blocked = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
2048
+ return {
2049
+ level: blocked ? "error" : "warning",
2050
+ file,
2051
+ title: `Ripple ${result.risk.level}`,
2052
+ message: `${result.risk.level} risk (score ${result.risk.score}) \u2014 ${result.summary.affectedFiles} affected ${result.summary.affectedFiles === 1 ? "file" : "files"}`
2053
+ };
2054
+ });
2055
+ }
2056
+ function buildGateAnnotation(blocked, gate, counts) {
2057
+ const gateIndex = GATE_INDEX[gate];
2058
+ const parts = [];
2059
+ if (LEVEL_INDEX.CRITICAL >= gateIndex && counts.critical > 0)
2060
+ parts.push(`${counts.critical} CRITICAL`);
2061
+ if (LEVEL_INDEX.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
2062
+ if (LEVEL_INDEX.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
2063
+ const atGate = parts.length > 0 ? parts.join(" \xB7 ") : "none";
2064
+ return {
2065
+ level: "notice",
2066
+ title: `Ripple diff ${gate} gate`,
2067
+ message: blocked ? `Gate blocked: ${atGate} at or above ${gate}` : `Gate passed: nothing at ${gate} or above`
2068
+ };
2069
+ }
2070
+
2071
+ // src/commands/diff.ts
2072
+ var LEVEL_INDEX2 = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
2073
+ var GATE_INDEX2 = { medium: 1, high: 2, critical: 3 };
1812
2074
  var VERDICT_NOTE = {
1813
2075
  medium: "no MEDIUM or worse",
1814
2076
  high: "no HIGH or CRITICAL",
@@ -1821,19 +2083,26 @@ function countLevels(results) {
1821
2083
  }
1822
2084
  return counts;
1823
2085
  }
2086
+ function isAllowedByDiffAllowlist(relPath, patterns) {
2087
+ if (patterns.length === 0) return false;
2088
+ const rel = toPosix(relPath);
2089
+ const matcher = picomatch(patterns.map(toPosix), { dot: true });
2090
+ return matcher(rel);
2091
+ }
1824
2092
  function riskySummary(counts, gate) {
1825
- const gateIndex = GATE_INDEX[gate];
2093
+ const gateIndex = GATE_INDEX2[gate];
1826
2094
  const parts = [];
1827
- if (LEVEL_INDEX.CRITICAL >= gateIndex && counts.critical > 0)
2095
+ if (LEVEL_INDEX2.CRITICAL >= gateIndex && counts.critical > 0)
1828
2096
  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`);
2097
+ if (LEVEL_INDEX2.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
2098
+ if (LEVEL_INDEX2.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
2099
+ if (LEVEL_INDEX2.LOW >= gateIndex && counts.low > 0) parts.push(`${counts.low} LOW`);
1832
2100
  return parts.length > 0 ? parts.join(" \xB7 ") : "none";
1833
2101
  }
1834
2102
  async function diffCommand(options, ctx2) {
2103
+ const format = options.format ?? (options.json ? "json" : "terminal");
1835
2104
  const style = { color: resolveColor(options.color) };
1836
- const tracker = createStageTracker(!options.json);
2105
+ const tracker = createStageTracker(format === "terminal");
1837
2106
  const started = Date.now();
1838
2107
  const context = await loadProjectContext(ctx2.cwd, options.config);
1839
2108
  const gate = options.gate ?? context.config.diff.gate ?? "high";
@@ -1865,20 +2134,29 @@ async function diffCommand(options, ctx2) {
1865
2134
  );
1866
2135
  }
1867
2136
  tracker.done();
1868
- const results = [...byRel.values()].sort((a, b) => b.risk.score - a.risk.score);
1869
- const counts = countLevels(results);
1870
- const blocked = results.some((result) => LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate]);
2137
+ const allowPatterns = [...options.allow ?? [], ...context.config.diff.allow ?? []];
2138
+ const allowedByRel = /* @__PURE__ */ new Map();
2139
+ for (const rel of byRel.keys()) {
2140
+ allowedByRel.set(rel, isAllowedByDiffAllowlist(rel, allowPatterns));
2141
+ }
2142
+ const entries = [...byRel.entries()].map(([rel, result]) => ({ rel, result, allowed: allowedByRel.get(rel) ?? false })).sort((a, b) => b.result.risk.score - a.result.risk.score);
2143
+ const gateResults = entries.filter((entry) => !entry.allowed).map((entry) => entry.result);
2144
+ const counts = countLevels(gateResults);
2145
+ const allowedCount = entries.filter((entry) => entry.allowed).length;
2146
+ const blocked = gateResults.some((result) => LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate]);
1871
2147
  const durationMs = Date.now() - started;
1872
- if (options.json) {
2148
+ if (format === "json") {
1873
2149
  ctx2.writer.write(
1874
2150
  serializeJson(
1875
2151
  buildDiffJsonReport(
1876
2152
  changed.baseLabel,
1877
2153
  changed.files,
1878
2154
  byRel,
2155
+ allowedByRel,
1879
2156
  gate,
1880
2157
  blocked,
1881
2158
  counts,
2159
+ allowedCount,
1882
2160
  durationMs,
1883
2161
  ctx2.version
1884
2162
  )
@@ -1886,14 +2164,27 @@ async function diffCommand(options, ctx2) {
1886
2164
  );
1887
2165
  return blocked ? ExitCode.Failure : ExitCode.Success;
1888
2166
  }
2167
+ if (format === "github") {
2168
+ const annotations = [
2169
+ ...buildFileAnnotations(
2170
+ [...byRel.entries()].filter(([rel]) => !(allowedByRel.get(rel) ?? false)).map(([file, result]) => ({ file, result })),
2171
+ gate
2172
+ ),
2173
+ buildGateAnnotation(blocked, gate, counts)
2174
+ ];
2175
+ ctx2.writer.write(`${annotations.map(renderAnnotation).join("\n")}
2176
+ `);
2177
+ return blocked ? ExitCode.Failure : ExitCode.Success;
2178
+ }
1889
2179
  renderDiffReport(
1890
2180
  {
1891
2181
  cwd: ctx2.cwd,
1892
2182
  baseLabel: changed.baseLabel,
1893
2183
  files: changed.files,
1894
- results,
2184
+ entries,
1895
2185
  skipped,
1896
2186
  counts,
2187
+ allowedCount,
1897
2188
  gate,
1898
2189
  blocked,
1899
2190
  durationMs,
@@ -1904,12 +2195,25 @@ async function diffCommand(options, ctx2) {
1904
2195
  return blocked ? ExitCode.Failure : ExitCode.Success;
1905
2196
  }
1906
2197
  function renderDiffReport(input, writer) {
1907
- const { cwd, baseLabel, files, results, skipped, counts, gate, blocked, durationMs, style } = input;
2198
+ const {
2199
+ cwd,
2200
+ baseLabel,
2201
+ files,
2202
+ entries,
2203
+ skipped,
2204
+ counts,
2205
+ allowedCount,
2206
+ gate,
2207
+ blocked,
2208
+ durationMs,
2209
+ style
2210
+ } = input;
1908
2211
  writer.writeLine(brandHeader({ label: `diff vs ${baseLabel}`, style }));
1909
2212
  writer.writeLine();
1910
2213
  const rows = [
1911
2214
  ["Changed", pluralize(files.length, "file")],
1912
- ["Source", pluralize(results.length, "file")],
2215
+ ["Source", pluralize(entries.length, "file")],
2216
+ ["Allowed", pluralize(allowedCount, "file")],
1913
2217
  ["Duration", dim(formatDuration(durationMs), style)]
1914
2218
  ];
1915
2219
  const width = Math.max(...rows.map(([key]) => key.length));
@@ -1926,16 +2230,17 @@ function renderDiffReport(input, writer) {
1926
2230
  `${markText} ${verdictText} \u2014 ${riskySummary(counts, gate)} ${dim(`(${VERDICT_NOTE[gate]})`, style)}`
1927
2231
  );
1928
2232
  writer.writeLine();
1929
- writer.writeLine(sectionHeader(`Risk analysis (${results.length} files)`, style));
1930
- for (const result of results) {
1931
- const risky = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
2233
+ writer.writeLine(sectionHeader(`Risk analysis (${entries.length} files)`, style));
2234
+ for (const { result, allowed } of entries) {
2235
+ const risky = LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate];
1932
2236
  const fileIcon = risky ? icon("cross") : icon("tick");
1933
2237
  const fileIconText = style.color ? risky ? chalk.red(fileIcon) : chalk.green(fileIcon) : fileIcon;
1934
2238
  const score = result.risk.score.toFixed(1);
1935
2239
  const gauge = riskGauge(result.risk.score, result.risk.level, style);
1936
2240
  const file = displayPath(result.targetPath, cwd);
2241
+ const allowedTag = allowed ? ` ${dim("(allowed)", style)}` : "";
1937
2242
  writer.writeLine(
1938
- `${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}`
2243
+ `${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}${allowedTag}`
1939
2244
  );
1940
2245
  }
1941
2246
  for (const rel of skipped) {
@@ -1945,7 +2250,7 @@ function renderDiffReport(input, writer) {
1945
2250
  }
1946
2251
  writer.writeLine();
1947
2252
  }
1948
- function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked, counts, durationMs, version) {
2253
+ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, allowedByRel, gate, blocked, counts, allowedCount, durationMs, version) {
1949
2254
  const files = [];
1950
2255
  for (const rel of changedFileNames) {
1951
2256
  const result = byRel.get(rel);
@@ -1955,7 +2260,8 @@ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked,
1955
2260
  analyzed: true,
1956
2261
  risk: result.risk,
1957
2262
  affectedFiles: result.summary.affectedFiles,
1958
- targetInCycle: result.targetInCycle
2263
+ targetInCycle: result.targetInCycle,
2264
+ allowed: allowedByRel.get(rel) ?? false
1959
2265
  });
1960
2266
  } else {
1961
2267
  files.push({ file: rel, analyzed: false });
@@ -1969,16 +2275,29 @@ function buildDiffJsonReport(baseLabel, changedFileNames, byRel, gate, blocked,
1969
2275
  base: baseLabel,
1970
2276
  changedFiles: changedFileNames.length,
1971
2277
  files,
1972
- gate: { level: gate, blocked, counts },
2278
+ gate: { level: gate, blocked, counts, allowed: allowedCount },
1973
2279
  durationMs
1974
2280
  };
1975
2281
  }
1976
2282
 
1977
2283
  // src/cli/program.ts
1978
2284
  var PROGRAM_NAME = "ripple";
2285
+ function helpExamples(examples) {
2286
+ const lines = examples.map((example) => ` ${example}`);
2287
+ return `
2288
+ Examples:
2289
+ ${lines.join("\n")}
2290
+ `;
2291
+ }
1979
2292
  function createProgram(ctx2) {
1980
2293
  const program = new Command().name(PROGRAM_NAME).description("Dependency impact analysis for TypeScript and JavaScript projects.").version(ctx2.version, "-V, --version", "print the Ripple version").showHelpAfterError();
1981
- program.command("analyze <file>").description("Analyze the impact of changing a file").option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include the risk factor breakdown").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 (file, options) => {
2294
+ program.command("analyze <file>").description("Analyze the impact of changing a file").addHelpText(
2295
+ "after",
2296
+ helpExamples([
2297
+ "ripple analyze src/authentication/login.ts",
2298
+ "ripple analyze src/index.ts --depth 3"
2299
+ ])
2300
+ ).option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include the risk factor breakdown").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 (file, options) => {
1982
2301
  const exitCode2 = await analyzeCommand(
1983
2302
  file,
1984
2303
  {
@@ -1994,11 +2313,23 @@ function createProgram(ctx2) {
1994
2313
  throw new ExitError(exitCode2);
1995
2314
  }
1996
2315
  });
1997
- program.command("graph [file]").description("Show project graph stats, or the dependency tree of one file").option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include extra sections").option("-r, --reverse", "show dependents (what imports the file) instead of dependants").option("-d, --depth <number>", "cap the tree depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (file, options) => {
2316
+ program.command("graph [file]").description("Show project graph stats, or the dependency tree of one file").addHelpText(
2317
+ "after",
2318
+ helpExamples([
2319
+ "ripple graph",
2320
+ "ripple graph src/index.ts --reverse --depth 3",
2321
+ "ripple graph --format mermaid"
2322
+ ])
2323
+ ).option("-j, --json", "emit a machine-readable JSON report").option(
2324
+ "-f, --format <format>",
2325
+ "output format: terminal | json | mermaid | dot | html",
2326
+ parseGraphFormat
2327
+ ).option("-v, --verbose", "include extra sections").option("-r, --reverse", "show dependents (what imports the file) instead of dependants").option("-d, --depth <number>", "cap the tree depth", parsePositiveInt).option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (file, options) => {
1998
2328
  const exitCode2 = await graphCommand(
1999
2329
  file,
2000
2330
  {
2001
2331
  json: Boolean(options.json),
2332
+ format: options.format ?? (options.json ? "json" : "terminal"),
2002
2333
  verbose: Boolean(options.verbose),
2003
2334
  reverse: Boolean(options.reverse),
2004
2335
  ...options.depth !== void 0 ? { depth: options.depth } : {},
@@ -2011,18 +2342,23 @@ function createProgram(ctx2) {
2011
2342
  throw new ExitError(exitCode2);
2012
2343
  }
2013
2344
  });
2014
- 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(
2345
+ program.command("diff").description("Analyze changed files since a base ref and gate risky changes").addHelpText(
2346
+ "after",
2347
+ helpExamples(["ripple diff --base main --gate critical", "ripple diff --format github"])
2348
+ ).option("-j, --json", "emit a machine-readable JSON report").option("-f, --format <format>", "output format: terminal | json | github", parseDiffFormat).option("-v, --verbose", "include extra sections").option("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").option(
2015
2349
  "-g, --gate <level>",
2016
2350
  "risk level that blocks the gate (medium | high | critical)",
2017
2351
  parseGateLevel
2018
- ).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) => {
2352
+ ).option("-d, --depth <number>", "cap the reverse traversal depth", parsePositiveInt).option("-a, --allow <glob...>", "changed files matching these globs never block the gate").option("-c, --config <path>", "path to a ripple config file").option("--no-color", "disable ANSI colors").action(async (options) => {
2019
2353
  const exitCode2 = await diffCommand(
2020
2354
  {
2021
2355
  json: Boolean(options.json),
2356
+ format: options.format ?? (options.json ? "json" : "terminal"),
2022
2357
  verbose: Boolean(options.verbose),
2023
2358
  ...options.base !== void 0 ? { base: options.base } : {},
2024
2359
  ...options.gate !== void 0 ? { gate: options.gate } : {},
2025
2360
  ...options.depth !== void 0 ? { depth: options.depth } : {},
2361
+ ...options.allow !== void 0 ? { allow: options.allow } : {},
2026
2362
  ...options.config !== void 0 ? { config: options.config } : {},
2027
2363
  color: options.color
2028
2364
  },
@@ -2032,7 +2368,7 @@ function createProgram(ctx2) {
2032
2368
  throw new ExitError(exitCode2);
2033
2369
  }
2034
2370
  });
2035
- 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) => {
2371
+ program.command("doctor").description("Check the project and environment for Ripple readiness").addHelpText("after", helpExamples(["ripple doctor", "ripple doctor -v"])).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) => {
2036
2372
  const exitCode2 = await doctorCommand(
2037
2373
  {
2038
2374
  verbose: Boolean(options.verbose),
@@ -2045,16 +2381,21 @@ function createProgram(ctx2) {
2045
2381
  throw new ExitError(exitCode2);
2046
2382
  }
2047
2383
  });
2048
- 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) => {
2384
+ program.command("init").description("Create a ripple.config with the default settings").addHelpText("after", helpExamples(["ripple init --force", "ripple init --ts --full"])).option("-f, --force", "overwrite an existing config file").option("--full", "write every built-in default instead of a minimal config").option("--ts", "write a typed ripple.config.ts instead of JSON").option("--no-color", "disable ANSI colors").action(async (options) => {
2049
2385
  const exitCode2 = await initCommand(
2050
- { force: Boolean(options.force), color: options.color },
2386
+ {
2387
+ force: Boolean(options.force),
2388
+ full: Boolean(options.full),
2389
+ ts: Boolean(options.ts),
2390
+ color: options.color
2391
+ },
2051
2392
  ctx2
2052
2393
  );
2053
2394
  if (exitCode2 !== ExitCode.Success) {
2054
2395
  throw new ExitError(exitCode2);
2055
2396
  }
2056
2397
  });
2057
- program.command("version").description("Print the Ripple version").action(() => {
2398
+ program.command("version").description("Print the Ripple version").addHelpText("after", helpExamples(["ripple version"])).action(() => {
2058
2399
  ctx2.writer.writeLine(ctx2.version);
2059
2400
  });
2060
2401
  program.addHelpText(
@@ -2093,6 +2434,26 @@ function parseGateLevel(value) {
2093
2434
  `expected one of medium | high | critical, got "${value}"`
2094
2435
  );
2095
2436
  }
2437
+ function parseDiffFormat(value) {
2438
+ if (value === "terminal" || value === "json" || value === "github") {
2439
+ return value;
2440
+ }
2441
+ throw new CommanderError(
2442
+ 1,
2443
+ "commander.invalidArgument",
2444
+ `expected one of terminal | json | github, got "${value}"`
2445
+ );
2446
+ }
2447
+ function parseGraphFormat(value) {
2448
+ if (value === "terminal" || value === "json" || value === "mermaid" || value === "dot" || value === "html") {
2449
+ return value;
2450
+ }
2451
+ throw new CommanderError(
2452
+ 1,
2453
+ "commander.invalidArgument",
2454
+ `expected one of terminal | json | mermaid | dot | html, got "${value}"`
2455
+ );
2456
+ }
2096
2457
  function printError(error, ctx2) {
2097
2458
  if (error instanceof RippleError) {
2098
2459
  ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
@@ -2126,7 +2487,7 @@ async function run({ argv, ctx: ctx2 }) {
2126
2487
 
2127
2488
  // package.json
2128
2489
  var package_default = {
2129
- version: "0.4.0"};
2490
+ version: "0.6.0"};
2130
2491
 
2131
2492
  // src/cli/bin.ts
2132
2493
  var ctx = {