@alimaandev/ripple 0.2.0 → 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/README.md +125 -23
- package/dist/bin.js +260 -11
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,10 @@ Change less. Break less. Know the blast radius of any file before you touch it.
|
|
|
16
16
|
<a href="package.json"><img alt="Node.js >= 22" src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?logo=nodedotjs&logoColor=white&style=for-the-badge"/></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
<pre><code>npm install -g @alimaandev/ripple
|
|
19
|
+
<pre><code>npm install -g @alimaandev/ripple
|
|
20
|
+
|
|
21
|
+
# or try it without installing anything:
|
|
22
|
+
npx @alimaandev/ripple analyze src/your-file.ts</code></pre>
|
|
20
23
|
|
|
21
24
|
<img src="https://raw.githubusercontent.com/alimaandev/ripple/main/assets/hero.svg" alt="Terminal output of `ripple analyze` — risk, affected files, routes, components, tests, and confidence score" title="ripple analyze" width="100%" />
|
|
22
25
|
|
|
@@ -76,6 +79,7 @@ Just deterministic analysis against the code you actually have.
|
|
|
76
79
|
| **Categorized results** | Routes, layouts, components, utilities, and tests detected by path conventions — not a raw string of file names. |
|
|
77
80
|
| **Entry-point intelligence** | `package.json` `main`/`bin` plus conventional root and `src` entry files, flagged in the report. |
|
|
78
81
|
| **Stable JSON contract** | A machine-readable report for CI gates, dashboards, and tooling — additive-only compatibility. |
|
|
82
|
+
| **Change-set gating** | `ripple diff` analyzes every changed file since a base ref and blocks the merge on risky code. |
|
|
79
83
|
| **Error-tolerant parsing** | A broken file lowers confidence instead of aborting the run. No false precision. |
|
|
80
84
|
| **Deterministic & offline** | Same input, same output, every time. No hidden network dependency. |
|
|
81
85
|
|
|
@@ -108,8 +112,17 @@ npm link # exposes the global `ripple` binary
|
|
|
108
112
|
|
|
109
113
|
### Run your first analysis
|
|
110
114
|
|
|
115
|
+
No install required — `npx` pulls the package from the registry on the fly:
|
|
116
|
+
|
|
111
117
|
```bash
|
|
112
118
|
cd tests/fixtures/basic
|
|
119
|
+
npx @alimaandev/ripple analyze src/authentication/login.ts
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Once you're sold, install it globally:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install -g @alimaandev/ripple
|
|
113
126
|
ripple analyze src/authentication/login.ts
|
|
114
127
|
```
|
|
115
128
|
|
|
@@ -192,28 +205,35 @@ ripple <command> [options]
|
|
|
192
205
|
| ---------------- | ------------------------------------------------- |
|
|
193
206
|
| `analyze <file>` | Impact analysis for a target file |
|
|
194
207
|
| `graph [file]` | Project stats, or a single file's dependency tree |
|
|
208
|
+
| `diff` | Gate all files changed since a git base ref |
|
|
195
209
|
| `doctor` | Project and environment health check |
|
|
196
210
|
| `init` | Scaffold a `ripple.config.json` |
|
|
197
211
|
| `version` | Print the current version |
|
|
198
212
|
|
|
199
213
|
### Options
|
|
200
214
|
|
|
201
|
-
| Command | Flag | Description
|
|
202
|
-
| --------- | --------------------- |
|
|
203
|
-
| `analyze` | `-j, --json` | Emit the JSON report instead of the terminal report
|
|
204
|
-
| `analyze` | `-v, --verbose` | Include the risk-factor point breakdown
|
|
205
|
-
| `analyze` | `-d, --depth <n>` | Cap the reverse traversal at `n` levels
|
|
206
|
-
| `analyze` | `-c, --config <path>` | Use a specific config file
|
|
207
|
-
| `analyze` | `--no-color` | Disable ANSI colors
|
|
208
|
-
| `graph` | `-j, --json` | Emit the JSON report
|
|
209
|
-
| `graph` | `-r, --reverse` | Show dependents (what imports this file) instead of dependencies
|
|
210
|
-
| `graph` | `-d, --depth <n>` | Cap tree depth
|
|
211
|
-
| `graph` | `-c, --config <path>` | Use a specific config file
|
|
212
|
-
| `graph` | `--no-color` | Disable ANSI colors
|
|
213
|
-
| `doctor` | `-c, --config <path>` | Use a specific config file
|
|
214
|
-
| `doctor` | `-v, --verbose` | Verbose output
|
|
215
|
-
| `doctor` | `--no-color` | Disable ANSI colors
|
|
216
|
-
| `init` | `-f, --force` | Overwrite an existing config
|
|
215
|
+
| Command | Flag | Description |
|
|
216
|
+
| --------- | --------------------- | ------------------------------------------------------------------ |
|
|
217
|
+
| `analyze` | `-j, --json` | Emit the JSON report instead of the terminal report |
|
|
218
|
+
| `analyze` | `-v, --verbose` | Include the risk-factor point breakdown |
|
|
219
|
+
| `analyze` | `-d, --depth <n>` | Cap the reverse traversal at `n` levels |
|
|
220
|
+
| `analyze` | `-c, --config <path>` | Use a specific config file |
|
|
221
|
+
| `analyze` | `--no-color` | Disable ANSI colors |
|
|
222
|
+
| `graph` | `-j, --json` | Emit the JSON report |
|
|
223
|
+
| `graph` | `-r, --reverse` | Show dependents (what imports this file) instead of dependencies |
|
|
224
|
+
| `graph` | `-d, --depth <n>` | Cap tree depth |
|
|
225
|
+
| `graph` | `-c, --config <path>` | Use a specific config file |
|
|
226
|
+
| `graph` | `--no-color` | Disable ANSI colors |
|
|
227
|
+
| `doctor` | `-c, --config <path>` | Use a specific config file |
|
|
228
|
+
| `doctor` | `-v, --verbose` | Verbose output |
|
|
229
|
+
| `doctor` | `--no-color` | Disable ANSI colors |
|
|
230
|
+
| `init` | `-f, --force` | Overwrite an existing config |
|
|
231
|
+
| `diff` | `-j, --json` | Emit the JSON report instead of the terminal report |
|
|
232
|
+
| `diff` | `-b, --base <ref>` | Git ref to diff against (default: `origin/main`, `main`, `HEAD~1`) |
|
|
233
|
+
| `diff` | `-g, --gate <level>` | Blocking level: `medium`, `high`, or `critical` (default: `high`) |
|
|
234
|
+
| `diff` | `-d, --depth <n>` | Cap reverse traversal per file |
|
|
235
|
+
| `diff` | `-c, --config <path>` | Use a specific config file |
|
|
236
|
+
| `diff` | `--no-color` | Disable ANSI colors |
|
|
217
237
|
|
|
218
238
|
### `ripple doctor`
|
|
219
239
|
|
|
@@ -243,6 +263,41 @@ $ ripple doctor
|
|
|
243
263
|
|
|
244
264
|
Writes `ripple.config.json` with the recommended defaults, ready to edit.
|
|
245
265
|
|
|
266
|
+
### `ripple diff`
|
|
267
|
+
|
|
268
|
+
Analyzes every file that changed since a base git ref, scores each with the
|
|
269
|
+
same risk model as `analyze`, and gates the change set: any file reaching
|
|
270
|
+
the gate level (HIGH by default) makes the command exit `1`. Untracked
|
|
271
|
+
files are included; deleted files and non-source changes are skipped.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
cd my-project
|
|
275
|
+
ripple diff # vs origin/main (then main, then HEAD~1)
|
|
276
|
+
ripple diff --base HEAD~1 --json # machine-readable gate report
|
|
277
|
+
ripple diff --gate critical # only CRITICAL blocks the merge
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
╭ ripple ─────────────────────╮
|
|
282
|
+
│ ❯ diff vs origin/main │
|
|
283
|
+
╰─────────────────────────────╯
|
|
284
|
+
|
|
285
|
+
Changed 2 files
|
|
286
|
+
Source 2 files
|
|
287
|
+
Duration 118ms
|
|
288
|
+
|
|
289
|
+
✖ Gate blocked — 1 CRITICAL (no HIGH or CRITICAL)
|
|
290
|
+
|
|
291
|
+
─────────────────────────────────────
|
|
292
|
+
Risk analysis (2 files)
|
|
293
|
+
✖ src/auth/token.ts CRITICAL · 88.2/100 ████████░░
|
|
294
|
+
● src/auth/session.ts LOW · 12.4/100 █░░░░░░░░░
|
|
295
|
+
… src/api/legacy.js (not a source file)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Run `ripple diff` locally before opening a PR, and in CI as the same gate —
|
|
299
|
+
identical code, identical verdict.
|
|
300
|
+
|
|
246
301
|
## Configuration
|
|
247
302
|
|
|
248
303
|
Ripple discovers `ripple.config.ts`, `.js`, `.cjs`, `.mjs`, or `.json` in the
|
|
@@ -379,6 +434,39 @@ error-tolerant recovery, `graph/` resolves specifiers and detects cycles, and
|
|
|
379
434
|
> change without a major version bump. `graph --json` follows the same
|
|
380
435
|
> convention.
|
|
381
436
|
|
|
437
|
+
### `ripple diff --json`
|
|
438
|
+
|
|
439
|
+
The diff report wraps a per-file view of the same risk model, plus the gate
|
|
440
|
+
verdict:
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"tool": "ripple",
|
|
445
|
+
"version": "0.3.0",
|
|
446
|
+
"command": "diff",
|
|
447
|
+
"base": "origin/main",
|
|
448
|
+
"changedFiles": 2,
|
|
449
|
+
"files": [
|
|
450
|
+
{
|
|
451
|
+
"file": "src/auth/token.ts",
|
|
452
|
+
"analyzed": true,
|
|
453
|
+
"risk": { "score": 88.2, "level": "CRITICAL", "factors": [] },
|
|
454
|
+
"affectedFiles": 14,
|
|
455
|
+
"targetInCycle": false
|
|
456
|
+
},
|
|
457
|
+
{ "file": "src/api/legacy.js", "analyzed": false }
|
|
458
|
+
],
|
|
459
|
+
"gate": {
|
|
460
|
+
"level": "high",
|
|
461
|
+
"blocked": true,
|
|
462
|
+
"counts": { "low": 0, "medium": 0, "high": 0, "critical": 1 }
|
|
463
|
+
},
|
|
464
|
+
"durationMs": 118
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`blocked` tells CI everything: exit code `1` if `true`, `0` otherwise.
|
|
469
|
+
|
|
382
470
|
### Use it in CI
|
|
383
471
|
|
|
384
472
|
```bash
|
|
@@ -386,6 +474,16 @@ ripple analyze src/authentication/login.ts --json | jq -r .risk.level
|
|
|
386
474
|
# MEDIUM
|
|
387
475
|
```
|
|
388
476
|
|
|
477
|
+
Gate every changed file in one call:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
#!/usr/bin/env bash
|
|
481
|
+
set -euo pipefail
|
|
482
|
+
|
|
483
|
+
ripple diff --json --gate high
|
|
484
|
+
# exits 1 when any changed file is HIGH or CRITICAL
|
|
485
|
+
```
|
|
486
|
+
|
|
389
487
|
```bash
|
|
390
488
|
#!/usr/bin/env bash
|
|
391
489
|
set -euo pipefail
|
|
@@ -430,12 +528,12 @@ fi
|
|
|
430
528
|
|
|
431
529
|
## Exit codes
|
|
432
530
|
|
|
433
|
-
| Code | Meaning
|
|
434
|
-
| ---- |
|
|
435
|
-
| `0` | Success
|
|
436
|
-
| `1` | General failure
|
|
437
|
-
| `2` | Target file not found
|
|
438
|
-
| `3` | Config missing or invalid
|
|
531
|
+
| Code | Meaning |
|
|
532
|
+
| ---- | ------------------------------------- |
|
|
533
|
+
| `0` | Success |
|
|
534
|
+
| `1` | General failure / gate blocked |
|
|
535
|
+
| `2` | Target file or git base ref not found |
|
|
536
|
+
| `3` | Config missing or invalid |
|
|
439
537
|
|
|
440
538
|
## Development
|
|
441
539
|
|
|
@@ -461,6 +559,10 @@ npm install -g @alimaandev/ripple
|
|
|
461
559
|
ripple analyze src/your-file.ts --json | jq -r .risk.level # MEDIUM, HIGH, ...
|
|
462
560
|
```
|
|
463
561
|
|
|
562
|
+
If Ripple has ever kept you from an accidental breaking change, a
|
|
563
|
+
[little star](https://github.com/alimaandev/ripple) goes a long way — it
|
|
564
|
+
helps other developers find it.
|
|
565
|
+
|
|
464
566
|
## License
|
|
465
567
|
|
|
466
568
|
MIT — see [LICENSE](LICENSE).
|
package/dist/bin.js
CHANGED
|
@@ -12,6 +12,7 @@ import treeify from 'treeify';
|
|
|
12
12
|
import boxen from 'boxen';
|
|
13
13
|
import fg from 'fast-glob';
|
|
14
14
|
import ora from 'ora';
|
|
15
|
+
import { execFileSync } from 'child_process';
|
|
15
16
|
|
|
16
17
|
// src/output/writer.ts
|
|
17
18
|
var TerminalWriter = class {
|
|
@@ -268,8 +269,8 @@ async function loadProjectContext(cwd, explicitConfigPath) {
|
|
|
268
269
|
function cycleMemberPaths(graph) {
|
|
269
270
|
const members = /* @__PURE__ */ new Set();
|
|
270
271
|
for (const cycle of graph.cycles) {
|
|
271
|
-
for (const
|
|
272
|
-
members.add(
|
|
272
|
+
for (const path13 of cycle.members) {
|
|
273
|
+
members.add(path13);
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
return members;
|
|
@@ -980,12 +981,12 @@ function renderGraphReport(input, options, writer) {
|
|
|
980
981
|
writer.writeLine();
|
|
981
982
|
if (input.forward) {
|
|
982
983
|
writer.writeLine(sectionHeader("Dependants (forward)", style));
|
|
983
|
-
writer.writeLine(renderTree(input.forward.map((
|
|
984
|
+
writer.writeLine(renderTree(input.forward.map((path13) => ({ label: path13 }))));
|
|
984
985
|
writer.writeLine();
|
|
985
986
|
}
|
|
986
987
|
if (input.reverse) {
|
|
987
988
|
writer.writeLine(sectionHeader("Dependents (reverse)", style));
|
|
988
|
-
writer.writeLine(renderTree(input.reverse.map((
|
|
989
|
+
writer.writeLine(renderTree(input.reverse.map((path13) => ({ label: path13 }))));
|
|
989
990
|
writer.writeLine();
|
|
990
991
|
}
|
|
991
992
|
writer.writeLine(sectionHeader("Circular dependencies", style));
|
|
@@ -1277,8 +1278,8 @@ function findCycles(graph) {
|
|
|
1277
1278
|
}
|
|
1278
1279
|
const members = [...component].sort();
|
|
1279
1280
|
const componentSet = new Set(component);
|
|
1280
|
-
const
|
|
1281
|
-
cycles.push({ members, path:
|
|
1281
|
+
const path13 = representativeCyclePath(graph, component, componentSet);
|
|
1282
|
+
cycles.push({ members, path: path13 ?? [members[0], members[0]] });
|
|
1282
1283
|
}
|
|
1283
1284
|
cycles.sort(
|
|
1284
1285
|
(a, b) => a.members.length - b.members.length || a.members[0].localeCompare(b.members[0])
|
|
@@ -1287,14 +1288,14 @@ function findCycles(graph) {
|
|
|
1287
1288
|
}
|
|
1288
1289
|
function representativeCyclePath(graph, component, componentSet) {
|
|
1289
1290
|
const inPath = /* @__PURE__ */ new Set();
|
|
1290
|
-
const
|
|
1291
|
+
const path13 = [];
|
|
1291
1292
|
let current = component[0];
|
|
1292
1293
|
for (let steps = 0; steps <= component.length * 2 && current !== void 0; steps++) {
|
|
1293
1294
|
const firstOccurrence = inPath.has(current);
|
|
1294
1295
|
if (firstOccurrence) {
|
|
1295
|
-
const start =
|
|
1296
|
+
const start = path13.indexOf(current);
|
|
1296
1297
|
if (start === -1) return void 0;
|
|
1297
|
-
const cycle =
|
|
1298
|
+
const cycle = path13.slice(start);
|
|
1298
1299
|
if (cycle.length > 1 || graph.forward.get(current)?.has(current)) {
|
|
1299
1300
|
cycle.push(current);
|
|
1300
1301
|
return cycle;
|
|
@@ -1302,7 +1303,7 @@ function representativeCyclePath(graph, component, componentSet) {
|
|
|
1302
1303
|
return void 0;
|
|
1303
1304
|
}
|
|
1304
1305
|
inPath.add(current);
|
|
1305
|
-
|
|
1306
|
+
path13.push(current);
|
|
1306
1307
|
const next = [...graph.forward.get(current) ?? []].find(
|
|
1307
1308
|
(candidate) => componentSet.has(candidate)
|
|
1308
1309
|
);
|
|
@@ -1754,6 +1755,223 @@ async function initCommand(options, ctx2) {
|
|
|
1754
1755
|
return ExitCode.Failure;
|
|
1755
1756
|
}
|
|
1756
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
|
+
}
|
|
1757
1975
|
|
|
1758
1976
|
// src/cli/program.ts
|
|
1759
1977
|
var PROGRAM_NAME = "ripple";
|
|
@@ -1792,6 +2010,27 @@ function createProgram(ctx2) {
|
|
|
1792
2010
|
throw new ExitError(exitCode2);
|
|
1793
2011
|
}
|
|
1794
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
|
+
});
|
|
1795
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) => {
|
|
1796
2035
|
const exitCode2 = await doctorCommand(
|
|
1797
2036
|
{
|
|
@@ -1843,6 +2082,16 @@ function parsePositiveInt(value) {
|
|
|
1843
2082
|
}
|
|
1844
2083
|
return parsed;
|
|
1845
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
|
+
}
|
|
1846
2095
|
function printError(error, ctx2) {
|
|
1847
2096
|
if (error instanceof RippleError) {
|
|
1848
2097
|
ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
|
|
@@ -1876,7 +2125,7 @@ async function run({ argv, ctx: ctx2 }) {
|
|
|
1876
2125
|
|
|
1877
2126
|
// package.json
|
|
1878
2127
|
var package_default = {
|
|
1879
|
-
version: "0.
|
|
2128
|
+
version: "0.3.0"};
|
|
1880
2129
|
|
|
1881
2130
|
// src/cli/bin.ts
|
|
1882
2131
|
var ctx = {
|