coderifts 1.1.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 +111 -0
- package/bin/coderifts.js +45 -0
- package/package.json +49 -0
- package/src/cloud.js +51 -0
- package/src/commands/diff.js +237 -0
- package/src/commands/init.js +223 -0
- package/src/commands/login.js +38 -0
- package/src/config.js +65 -0
- package/src/output/json.js +10 -0
- package/src/output/terminal.js +156 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# CodeRifts CLI
|
|
2
|
+
|
|
3
|
+
Detect breaking API changes between OpenAPI specs from the command line. Works locally or with the CodeRifts cloud API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g coderifts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx coderifts diff old-api.yaml new-api.yaml
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Compare two specs locally
|
|
21
|
+
coderifts diff old-api.yaml new-api.yaml
|
|
22
|
+
|
|
23
|
+
# Use cloud API for full governance report
|
|
24
|
+
coderifts login
|
|
25
|
+
coderifts diff old-api.yaml new-api.yaml --cloud
|
|
26
|
+
|
|
27
|
+
# CI mode — exit 1 if risk score exceeds threshold
|
|
28
|
+
coderifts diff old-api.yaml new-api.yaml --ci --threshold 50
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
### `coderifts diff <old-spec> <new-spec>`
|
|
34
|
+
|
|
35
|
+
Compare two OpenAPI specs and report breaking changes.
|
|
36
|
+
|
|
37
|
+
| Flag | Description | Default |
|
|
38
|
+
|------|-------------|---------|
|
|
39
|
+
| `-f, --format <format>` | Output format: `terminal`, `json`, `markdown` | `terminal` |
|
|
40
|
+
| `--ci` | CI mode — exit code 1 if breaking changes exceed threshold | `false` |
|
|
41
|
+
| `--threshold <number>` | Risk score threshold for CI mode (0-100) | `50` |
|
|
42
|
+
| `--cloud` | Use CodeRifts cloud API instead of local analysis | `false` |
|
|
43
|
+
| `-c, --config <path>` | Path to `.coderifts.yml` config file | auto-detect |
|
|
44
|
+
|
|
45
|
+
**Output formats:**
|
|
46
|
+
|
|
47
|
+
- **terminal** — Colored tables and risk score box (default)
|
|
48
|
+
- **json** — Full structured report for programmatic use
|
|
49
|
+
- **markdown** — Markdown table for CI comments
|
|
50
|
+
|
|
51
|
+
A `coderifts-report.json` file is always saved to the current directory.
|
|
52
|
+
|
|
53
|
+
### `coderifts init`
|
|
54
|
+
|
|
55
|
+
Interactive configuration generator. Creates a `.coderifts.yml` file with industry-specific presets:
|
|
56
|
+
|
|
57
|
+
- Default (recommended)
|
|
58
|
+
- Fintech / Payments
|
|
59
|
+
- Healthcare / HIPAA
|
|
60
|
+
- Platform / API-First
|
|
61
|
+
- E-commerce
|
|
62
|
+
|
|
63
|
+
### `coderifts login`
|
|
64
|
+
|
|
65
|
+
Save your CodeRifts API key for cloud features. Get a free key at [app.coderifts.com/api/signup](https://app.coderifts.com/api/signup).
|
|
66
|
+
|
|
67
|
+
## CI/CD Integration
|
|
68
|
+
|
|
69
|
+
### GitHub Actions
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- name: Check API breaking changes
|
|
73
|
+
run: npx coderifts diff old-api.yaml new-api.yaml --ci --format json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### GitLab CI
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
api-check:
|
|
80
|
+
script:
|
|
81
|
+
- npx coderifts diff old-api.yaml new-api.yaml --ci --threshold 40
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Jenkins
|
|
85
|
+
|
|
86
|
+
```groovy
|
|
87
|
+
sh 'npx coderifts diff old-api.yaml new-api.yaml --ci'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Environment Variables
|
|
91
|
+
|
|
92
|
+
| Variable | Description |
|
|
93
|
+
|----------|-------------|
|
|
94
|
+
| `CODERIFTS_API_KEY` | API key (alternative to `coderifts login`) |
|
|
95
|
+
| `NO_COLOR` | Disable colored output |
|
|
96
|
+
|
|
97
|
+
## Exit Codes
|
|
98
|
+
|
|
99
|
+
| Code | Meaning |
|
|
100
|
+
|------|---------|
|
|
101
|
+
| `0` | No breaking changes (or below threshold) |
|
|
102
|
+
| `1` | Breaking changes found (in CI mode) or analysis failed |
|
|
103
|
+
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- [Website](https://coderifts.com)
|
|
107
|
+
- [CLI Integration Page](https://coderifts.com/integrations/cli/)
|
|
108
|
+
- [API Docs](https://app.coderifts.com/api/docs)
|
|
109
|
+
- [Get API Key](https://app.coderifts.com/api/signup)
|
|
110
|
+
- [GitHub](https://github.com/coderifts/app)
|
|
111
|
+
- [npm](https://www.npmjs.com/package/coderifts)
|
package/bin/coderifts.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { program } = require('commander');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('coderifts')
|
|
10
|
+
.description('Detect breaking API changes between OpenAPI specs')
|
|
11
|
+
.version(pkg.version, '-v, --version');
|
|
12
|
+
|
|
13
|
+
// ── diff command ──
|
|
14
|
+
program
|
|
15
|
+
.command('diff <old-spec> <new-spec>')
|
|
16
|
+
.description('Compare two OpenAPI specs and report breaking changes')
|
|
17
|
+
.option('-f, --format <format>', 'Output format: terminal (default), json, markdown', 'terminal')
|
|
18
|
+
.option('--ci', 'CI mode — exit with code 1 if breaking changes exceed threshold')
|
|
19
|
+
.option('--threshold <number>', 'Risk score threshold for CI mode (0-100)', '50')
|
|
20
|
+
.option('--cloud', 'Use the CodeRifts cloud API instead of local analysis')
|
|
21
|
+
.option('-c, --config <path>', 'Path to .coderifts.yml config file')
|
|
22
|
+
.action(async (oldSpec, newSpec, options) => {
|
|
23
|
+
const { diff } = require('../src/commands/diff');
|
|
24
|
+
await diff(oldSpec, newSpec, options);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── init command ──
|
|
28
|
+
program
|
|
29
|
+
.command('init')
|
|
30
|
+
.description('Generate a .coderifts.yml configuration file')
|
|
31
|
+
.action(async () => {
|
|
32
|
+
const { init } = require('../src/commands/init');
|
|
33
|
+
await init();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── login command ──
|
|
37
|
+
program
|
|
38
|
+
.command('login')
|
|
39
|
+
.description('Save your API key for cloud features')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
const { login } = require('../src/commands/login');
|
|
42
|
+
await login();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coderifts",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
5
|
+
"author": "CodeRifts <hello@coderifts.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"coderifts": "./bin/coderifts.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/commands/diff.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"src/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openapi",
|
|
18
|
+
"api",
|
|
19
|
+
"breaking-changes",
|
|
20
|
+
"diff",
|
|
21
|
+
"governance",
|
|
22
|
+
"cli",
|
|
23
|
+
"ci-cd",
|
|
24
|
+
"swagger",
|
|
25
|
+
"devtools"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/coderifts/app",
|
|
30
|
+
"directory": "packages/cli"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://coderifts.com",
|
|
33
|
+
"bugs": "https://github.com/coderifts/app/issues",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --test test/*.test.js"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"cli-table3": "^0.6.4",
|
|
44
|
+
"inquirer": "^8.2.6",
|
|
45
|
+
"ora": "^5.4.1",
|
|
46
|
+
"js-yaml": "^4.1.0",
|
|
47
|
+
"openapi-diff": "^0.23.5"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/cloud.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
const API_BASE = 'https://app.coderifts.com';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Send specs to the CodeRifts cloud API for analysis.
|
|
9
|
+
*/
|
|
10
|
+
function cloudDiff(oldSpec, newSpec, apiKey) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const body = JSON.stringify({ old_spec: oldSpec, new_spec: newSpec });
|
|
13
|
+
|
|
14
|
+
const url = new URL('/api/v1/diff', API_BASE);
|
|
15
|
+
const options = {
|
|
16
|
+
hostname: url.hostname,
|
|
17
|
+
port: 443,
|
|
18
|
+
path: url.pathname,
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Content-Length': Buffer.byteLength(body),
|
|
23
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
24
|
+
'User-Agent': '@coderifts/cli',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const req = https.request(options, (res) => {
|
|
29
|
+
let data = '';
|
|
30
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(data);
|
|
34
|
+
if (res.statusCode >= 400) {
|
|
35
|
+
reject(new Error(parsed.message || parsed.error || `HTTP ${res.statusCode}`));
|
|
36
|
+
} else {
|
|
37
|
+
resolve(parsed);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
reject(new Error(`Invalid JSON response (HTTP ${res.statusCode})`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
req.write(body);
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { cloudDiff };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
const { loadProjectConfig, getApiKey } = require('../config');
|
|
9
|
+
const { cloudDiff } = require('../cloud');
|
|
10
|
+
const { renderTerminal } = require('../output/terminal');
|
|
11
|
+
const { renderJson } = require('../output/json');
|
|
12
|
+
|
|
13
|
+
// Respect NO_COLOR
|
|
14
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read and parse an OpenAPI spec file.
|
|
18
|
+
* Returns the raw string content (for cloud mode) and parsed object (for local mode).
|
|
19
|
+
*/
|
|
20
|
+
function readSpec(filePath) {
|
|
21
|
+
const resolved = path.resolve(filePath);
|
|
22
|
+
if (!fs.existsSync(resolved)) {
|
|
23
|
+
console.error(chalk.red(`Error: File not found: ${filePath}`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run local analysis using the bundled core analyzer.
|
|
32
|
+
*/
|
|
33
|
+
async function localAnalyze(oldSpecRaw, newSpecRaw, config) {
|
|
34
|
+
// Parse specs
|
|
35
|
+
let oldSpec, newSpec;
|
|
36
|
+
try {
|
|
37
|
+
oldSpec = yaml.load(oldSpecRaw);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(`Failed to parse old spec: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
newSpec = yaml.load(newSpecRaw);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw new Error(`Failed to parse new spec: ${e.message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Use openapi-diff for the raw diff
|
|
48
|
+
const { diffSpecs } = require('openapi-diff');
|
|
49
|
+
|
|
50
|
+
// Convert specs to JSON strings for openapi-diff
|
|
51
|
+
const oldJson = JSON.stringify(oldSpec);
|
|
52
|
+
const newJson = JSON.stringify(newSpec);
|
|
53
|
+
|
|
54
|
+
let diffResult;
|
|
55
|
+
try {
|
|
56
|
+
diffResult = await diffSpecs({
|
|
57
|
+
sourceSpec: { content: oldJson, location: 'old-spec.json', format: 'openapi3' },
|
|
58
|
+
destinationSpec: { content: newJson, location: 'new-spec.json', format: 'openapi3' },
|
|
59
|
+
});
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw new Error(`Diff engine error: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Classify breaking changes
|
|
65
|
+
const breakingChanges = [];
|
|
66
|
+
const nonBreakingChanges = [];
|
|
67
|
+
|
|
68
|
+
if (diffResult.breakingDifferences) {
|
|
69
|
+
for (const diff of diffResult.breakingDifferences) {
|
|
70
|
+
breakingChanges.push({
|
|
71
|
+
type: diff.code || 'unknown',
|
|
72
|
+
path: diff.sourceSpecEntityDetails?.[0]?.location || diff.entity || '',
|
|
73
|
+
method: '',
|
|
74
|
+
field: '',
|
|
75
|
+
severity: 'high',
|
|
76
|
+
description: diff.action || diff.code || '',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (diffResult.nonBreakingDifferences) {
|
|
82
|
+
for (const diff of diffResult.nonBreakingDifferences) {
|
|
83
|
+
nonBreakingChanges.push({
|
|
84
|
+
type: diff.code || 'unknown',
|
|
85
|
+
path: diff.sourceSpecEntityDetails?.[0]?.location || diff.entity || '',
|
|
86
|
+
method: '',
|
|
87
|
+
field: '',
|
|
88
|
+
severity: 'low',
|
|
89
|
+
description: diff.action || diff.code || '',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Calculate risk score
|
|
95
|
+
const breakingCount = breakingChanges.length;
|
|
96
|
+
const baseScore = Math.min(breakingCount * 15, 80);
|
|
97
|
+
const riskScore = Math.min(baseScore + (breakingCount > 3 ? 20 : 0), 100);
|
|
98
|
+
|
|
99
|
+
let riskLevel = 'minimal';
|
|
100
|
+
if (riskScore >= 80) riskLevel = 'critical';
|
|
101
|
+
else if (riskScore >= 60) riskLevel = 'high';
|
|
102
|
+
else if (riskScore >= 40) riskLevel = 'moderate';
|
|
103
|
+
else if (riskScore >= 20) riskLevel = 'low';
|
|
104
|
+
|
|
105
|
+
// Semver suggestion
|
|
106
|
+
let semverSuggestion = 'patch';
|
|
107
|
+
if (breakingCount > 0) semverSuggestion = 'major';
|
|
108
|
+
else if (nonBreakingChanges.length > 0) semverSuggestion = 'minor';
|
|
109
|
+
|
|
110
|
+
// Policy check
|
|
111
|
+
const policyViolations = [];
|
|
112
|
+
const blockOn = config?.rules?.block_on || [];
|
|
113
|
+
const threshold = config?.rules?.risk_threshold ?? 50;
|
|
114
|
+
|
|
115
|
+
for (const change of breakingChanges) {
|
|
116
|
+
for (const rule of blockOn) {
|
|
117
|
+
if (change.type.includes(rule) || change.description.includes(rule)) {
|
|
118
|
+
policyViolations.push({
|
|
119
|
+
rule: rule,
|
|
120
|
+
message: `Blocked by policy: ${rule} — ${change.path}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const shouldBlock = riskScore >= threshold || policyViolations.length > 0;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
risk_score: riskScore,
|
|
130
|
+
risk_level: riskLevel,
|
|
131
|
+
risk_dimensions: {
|
|
132
|
+
revenue_impact: Math.min(breakingCount * 3, 10),
|
|
133
|
+
blast_radius: Math.min(breakingCount * 2, 10),
|
|
134
|
+
app_compatibility: Math.min(breakingCount * 2, 10),
|
|
135
|
+
security: 0,
|
|
136
|
+
},
|
|
137
|
+
semver_suggestion: semverSuggestion,
|
|
138
|
+
breaking_changes: breakingChanges,
|
|
139
|
+
non_breaking_changes: nonBreakingChanges,
|
|
140
|
+
security_findings: [],
|
|
141
|
+
changelog: {
|
|
142
|
+
breaking: breakingChanges.map(c => `**${c.type}** ${c.path}`),
|
|
143
|
+
added: [],
|
|
144
|
+
changed: nonBreakingChanges.map(c => `${c.type} ${c.path}`),
|
|
145
|
+
deprecated: [],
|
|
146
|
+
},
|
|
147
|
+
policy_violations: policyViolations,
|
|
148
|
+
should_block: shouldBlock,
|
|
149
|
+
stats: {
|
|
150
|
+
total_changes: breakingCount + nonBreakingChanges.length,
|
|
151
|
+
breaking_count: breakingCount,
|
|
152
|
+
non_breaking_count: nonBreakingChanges.length,
|
|
153
|
+
security_count: 0,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Main diff command handler.
|
|
160
|
+
*/
|
|
161
|
+
async function diff(oldSpecPath, newSpecPath, options) {
|
|
162
|
+
const spinner = ora('Analyzing specs...').start();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Read specs
|
|
166
|
+
const oldSpecRaw = readSpec(oldSpecPath);
|
|
167
|
+
const newSpecRaw = readSpec(newSpecPath);
|
|
168
|
+
|
|
169
|
+
// Load config
|
|
170
|
+
const projectConfig = loadProjectConfig(options.config);
|
|
171
|
+
|
|
172
|
+
// Determine mode: cloud or local
|
|
173
|
+
const useCloud = options.cloud || false;
|
|
174
|
+
const apiKey = getApiKey();
|
|
175
|
+
|
|
176
|
+
let result;
|
|
177
|
+
|
|
178
|
+
if (useCloud) {
|
|
179
|
+
if (!apiKey) {
|
|
180
|
+
spinner.fail('Cloud mode requires an API key. Run `coderifts login` first.');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
spinner.text = 'Sending to CodeRifts cloud API...';
|
|
184
|
+
result = await cloudDiff(oldSpecRaw, newSpecRaw, apiKey);
|
|
185
|
+
} else {
|
|
186
|
+
spinner.text = 'Running local analysis...';
|
|
187
|
+
result = await localAnalyze(oldSpecRaw, newSpecRaw, projectConfig);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
spinner.stop();
|
|
191
|
+
|
|
192
|
+
// Output based on format
|
|
193
|
+
const format = options.format || 'terminal';
|
|
194
|
+
|
|
195
|
+
if (format === 'json') {
|
|
196
|
+
console.log(renderJson(result));
|
|
197
|
+
} else if (format === 'markdown') {
|
|
198
|
+
// Simple markdown output
|
|
199
|
+
console.log(`# CodeRifts Report\n`);
|
|
200
|
+
console.log(`**Risk Score:** ${result.risk_score}/100 (${result.risk_level})`);
|
|
201
|
+
console.log(`**Semver:** ${result.semver_suggestion}`);
|
|
202
|
+
console.log(`**Breaking Changes:** ${result.stats?.breaking_count || 0}`);
|
|
203
|
+
console.log(`**Non-Breaking:** ${result.stats?.non_breaking_count || 0}\n`);
|
|
204
|
+
if (result.breaking_changes?.length > 0) {
|
|
205
|
+
console.log('## Breaking Changes\n');
|
|
206
|
+
console.log('| Type | Path | Severity |');
|
|
207
|
+
console.log('|------|------|----------|');
|
|
208
|
+
for (const c of result.breaking_changes) {
|
|
209
|
+
console.log(`| ${c.type} | ${c.path} | ${c.severity} |`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.log(renderTerminal(result));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Save JSON report
|
|
217
|
+
const reportPath = path.join(process.cwd(), 'coderifts-report.json');
|
|
218
|
+
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2) + '\n');
|
|
219
|
+
if (format === 'terminal') {
|
|
220
|
+
console.log(chalk.dim(` Full report: ${reportPath} (saved)`));
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// CI mode: exit with code 1 if should_block
|
|
225
|
+
if (options.ci) {
|
|
226
|
+
const threshold = parseInt(options.threshold, 10) || 50;
|
|
227
|
+
if (result.risk_score >= threshold || result.should_block) {
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
spinner.fail(`Analysis failed: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { diff };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
|
|
8
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
9
|
+
|
|
10
|
+
const TEMPLATES = {
|
|
11
|
+
'Default (recommended)': `# CodeRifts Configuration — Recommended Default
|
|
12
|
+
version: 1
|
|
13
|
+
|
|
14
|
+
rules:
|
|
15
|
+
block_on:
|
|
16
|
+
- removed-endpoint
|
|
17
|
+
- auth-removed
|
|
18
|
+
|
|
19
|
+
risk_threshold: 50
|
|
20
|
+
|
|
21
|
+
semver:
|
|
22
|
+
enforce: false # Suggest but don't block
|
|
23
|
+
|
|
24
|
+
changelog:
|
|
25
|
+
auto_generate: true
|
|
26
|
+
`,
|
|
27
|
+
|
|
28
|
+
'Fintech / Payments': `# CodeRifts Configuration — Fintech / Payment APIs
|
|
29
|
+
version: 1
|
|
30
|
+
preset: fintech
|
|
31
|
+
|
|
32
|
+
rules:
|
|
33
|
+
block_on:
|
|
34
|
+
- removed-endpoint
|
|
35
|
+
- required-field-added
|
|
36
|
+
- field-type-changed
|
|
37
|
+
- auth-scope-removed
|
|
38
|
+
|
|
39
|
+
protected_fields:
|
|
40
|
+
- amount
|
|
41
|
+
- currency
|
|
42
|
+
- transaction_id
|
|
43
|
+
- payment_method
|
|
44
|
+
- card_number
|
|
45
|
+
- account_id
|
|
46
|
+
- routing_number
|
|
47
|
+
- iban
|
|
48
|
+
- swift_code
|
|
49
|
+
|
|
50
|
+
security:
|
|
51
|
+
sensitive_fields:
|
|
52
|
+
- card_number
|
|
53
|
+
- cvv
|
|
54
|
+
- pin
|
|
55
|
+
- account_number
|
|
56
|
+
- ssn
|
|
57
|
+
- tax_id
|
|
58
|
+
alert_on_exposure: true
|
|
59
|
+
|
|
60
|
+
risk_threshold: 30
|
|
61
|
+
|
|
62
|
+
semver:
|
|
63
|
+
enforce: true
|
|
64
|
+
block_on_missing_bump: true
|
|
65
|
+
|
|
66
|
+
deprecation:
|
|
67
|
+
require_sunset_header: true
|
|
68
|
+
minimum_deprecation_period_days: 90
|
|
69
|
+
`,
|
|
70
|
+
|
|
71
|
+
'Healthcare / HIPAA': `# CodeRifts Configuration — Healthcare / HIPAA
|
|
72
|
+
version: 1
|
|
73
|
+
preset: healthcare
|
|
74
|
+
|
|
75
|
+
rules:
|
|
76
|
+
block_on:
|
|
77
|
+
- removed-endpoint
|
|
78
|
+
- required-field-added
|
|
79
|
+
- field-type-changed
|
|
80
|
+
- auth-scope-removed
|
|
81
|
+
- auth-removed
|
|
82
|
+
|
|
83
|
+
protected_fields:
|
|
84
|
+
- patient_id
|
|
85
|
+
- medical_record_number
|
|
86
|
+
- diagnosis_code
|
|
87
|
+
- provider_id
|
|
88
|
+
- insurance_id
|
|
89
|
+
- date_of_birth
|
|
90
|
+
- prescription_id
|
|
91
|
+
|
|
92
|
+
security:
|
|
93
|
+
sensitive_fields:
|
|
94
|
+
- ssn
|
|
95
|
+
- date_of_birth
|
|
96
|
+
- medical_record_number
|
|
97
|
+
- diagnosis
|
|
98
|
+
- medication
|
|
99
|
+
- insurance_num
|
|
100
|
+
alert_on_exposure: true
|
|
101
|
+
require_auth_on_phi_endpoints: true
|
|
102
|
+
|
|
103
|
+
risk_threshold: 20
|
|
104
|
+
|
|
105
|
+
semver:
|
|
106
|
+
enforce: true
|
|
107
|
+
block_on_missing_bump: true
|
|
108
|
+
|
|
109
|
+
deprecation:
|
|
110
|
+
require_sunset_header: true
|
|
111
|
+
minimum_deprecation_period_days: 180
|
|
112
|
+
`,
|
|
113
|
+
|
|
114
|
+
'Platform / API-First': `# CodeRifts Configuration — Platform / API-First Teams
|
|
115
|
+
version: 1
|
|
116
|
+
preset: platform
|
|
117
|
+
|
|
118
|
+
rules:
|
|
119
|
+
block_on:
|
|
120
|
+
- removed-endpoint
|
|
121
|
+
- required-field-added
|
|
122
|
+
- field-type-changed
|
|
123
|
+
- response-field-removed
|
|
124
|
+
|
|
125
|
+
semver:
|
|
126
|
+
enforce: true
|
|
127
|
+
block_on_missing_bump: true
|
|
128
|
+
auto_label_pr: true
|
|
129
|
+
|
|
130
|
+
deprecation:
|
|
131
|
+
require_sunset_header: true
|
|
132
|
+
require_deprecation_notice: true
|
|
133
|
+
minimum_deprecation_period_days: 60
|
|
134
|
+
|
|
135
|
+
changelog:
|
|
136
|
+
auto_generate: true
|
|
137
|
+
format: keep-a-changelog
|
|
138
|
+
|
|
139
|
+
risk_threshold: 40
|
|
140
|
+
|
|
141
|
+
governance:
|
|
142
|
+
require_review_on_breaking: true
|
|
143
|
+
breaking_change_label: "api:breaking"
|
|
144
|
+
notify_channels: []
|
|
145
|
+
`,
|
|
146
|
+
|
|
147
|
+
'E-commerce': `# CodeRifts Configuration — E-commerce
|
|
148
|
+
version: 1
|
|
149
|
+
preset: ecommerce
|
|
150
|
+
|
|
151
|
+
rules:
|
|
152
|
+
block_on:
|
|
153
|
+
- removed-endpoint
|
|
154
|
+
- required-field-added
|
|
155
|
+
- field-type-changed
|
|
156
|
+
|
|
157
|
+
protected_fields:
|
|
158
|
+
- product_id
|
|
159
|
+
- sku
|
|
160
|
+
- price
|
|
161
|
+
- inventory_count
|
|
162
|
+
- cart_id
|
|
163
|
+
- order_id
|
|
164
|
+
- checkout_session
|
|
165
|
+
- shipping_address
|
|
166
|
+
- tracking_number
|
|
167
|
+
|
|
168
|
+
security:
|
|
169
|
+
sensitive_fields:
|
|
170
|
+
- card_number
|
|
171
|
+
- billing_address
|
|
172
|
+
- email
|
|
173
|
+
- phone
|
|
174
|
+
alert_on_exposure: true
|
|
175
|
+
|
|
176
|
+
risk_threshold: 35
|
|
177
|
+
|
|
178
|
+
semver:
|
|
179
|
+
enforce: true
|
|
180
|
+
|
|
181
|
+
deprecation:
|
|
182
|
+
minimum_deprecation_period_days: 30
|
|
183
|
+
`,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
async function init() {
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(chalk.bold(' CodeRifts — Configuration Generator'));
|
|
189
|
+
console.log('');
|
|
190
|
+
|
|
191
|
+
const outputPath = path.join(process.cwd(), '.coderifts.yml');
|
|
192
|
+
|
|
193
|
+
// Check if config already exists
|
|
194
|
+
if (fs.existsSync(outputPath)) {
|
|
195
|
+
const { overwrite } = await inquirer.prompt([{
|
|
196
|
+
type: 'confirm',
|
|
197
|
+
name: 'overwrite',
|
|
198
|
+
message: '.coderifts.yml already exists. Overwrite?',
|
|
199
|
+
default: false,
|
|
200
|
+
}]);
|
|
201
|
+
if (!overwrite) {
|
|
202
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { industry } = await inquirer.prompt([{
|
|
208
|
+
type: 'list',
|
|
209
|
+
name: 'industry',
|
|
210
|
+
message: 'Select your industry:',
|
|
211
|
+
choices: Object.keys(TEMPLATES),
|
|
212
|
+
}]);
|
|
213
|
+
|
|
214
|
+
const template = TEMPLATES[industry];
|
|
215
|
+
fs.writeFileSync(outputPath, template);
|
|
216
|
+
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(chalk.green(` ✓ Created .coderifts.yml with ${industry.toLowerCase()} preset`));
|
|
219
|
+
console.log(chalk.dim(` ${outputPath}`));
|
|
220
|
+
console.log('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { init };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { saveUserConfig, loadUserConfig, CONFIG_FILE } = require('../config');
|
|
6
|
+
|
|
7
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
8
|
+
|
|
9
|
+
async function login() {
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(chalk.bold(' CodeRifts — API Key Setup'));
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(chalk.dim(' Get a free API key at: https://app.coderifts.com/api/signup'));
|
|
14
|
+
console.log('');
|
|
15
|
+
|
|
16
|
+
const { apiKey } = await inquirer.prompt([{
|
|
17
|
+
type: 'password',
|
|
18
|
+
name: 'apiKey',
|
|
19
|
+
message: 'Paste your API key:',
|
|
20
|
+
mask: '*',
|
|
21
|
+
validate: (input) => {
|
|
22
|
+
if (!input || input.trim().length === 0) return 'API key is required';
|
|
23
|
+
if (!input.startsWith('cr_live_')) return 'Invalid key format — keys start with cr_live_';
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
}]);
|
|
27
|
+
|
|
28
|
+
const config = loadUserConfig();
|
|
29
|
+
config.api_key = apiKey.trim();
|
|
30
|
+
saveUserConfig(config);
|
|
31
|
+
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(chalk.green(` ✓ API key saved to ${CONFIG_FILE}`));
|
|
34
|
+
console.log(chalk.dim(' Use --cloud flag with diff command to use the cloud API.'));
|
|
35
|
+
console.log('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { login };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), '.coderifts');
|
|
9
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load the project .coderifts.yml config from the given path or cwd.
|
|
13
|
+
*/
|
|
14
|
+
function loadProjectConfig(configPath) {
|
|
15
|
+
const candidates = configPath
|
|
16
|
+
? [configPath]
|
|
17
|
+
: [
|
|
18
|
+
path.join(process.cwd(), '.coderifts.yml'),
|
|
19
|
+
path.join(process.cwd(), '.coderifts.yaml'),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const p of candidates) {
|
|
23
|
+
if (fs.existsSync(p)) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
26
|
+
return yaml.load(raw) || {};
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load the user's global config (~/.coderifts/config.json).
|
|
37
|
+
*/
|
|
38
|
+
function loadUserConfig() {
|
|
39
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save the user's global config.
|
|
49
|
+
*/
|
|
50
|
+
function saveUserConfig(config) {
|
|
51
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
52
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the stored API key.
|
|
59
|
+
*/
|
|
60
|
+
function getApiKey() {
|
|
61
|
+
const userConfig = loadUserConfig();
|
|
62
|
+
return userConfig.api_key || process.env.CODERIFTS_API_KEY || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { loadProjectConfig, loadUserConfig, saveUserConfig, getApiKey, CONFIG_DIR, CONFIG_FILE };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const Table = require('cli-table3');
|
|
5
|
+
|
|
6
|
+
// Respect NO_COLOR env var
|
|
7
|
+
if (process.env.NO_COLOR) {
|
|
8
|
+
chalk.level = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RISK_COLORS = {
|
|
12
|
+
minimal: chalk.green,
|
|
13
|
+
low: chalk.green,
|
|
14
|
+
moderate: chalk.yellow,
|
|
15
|
+
high: chalk.red,
|
|
16
|
+
critical: chalk.bgRed.white,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SEVERITY_ICONS = {
|
|
20
|
+
high: chalk.red('●'),
|
|
21
|
+
medium: chalk.yellow('●'),
|
|
22
|
+
low: chalk.blue('●'),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function riskEmoji(level) {
|
|
26
|
+
switch (level) {
|
|
27
|
+
case 'minimal': return chalk.green('●');
|
|
28
|
+
case 'low': return chalk.green('●');
|
|
29
|
+
case 'moderate': return chalk.yellow('●');
|
|
30
|
+
case 'high': return chalk.red('●');
|
|
31
|
+
case 'critical': return chalk.bgRed.white(' ● ');
|
|
32
|
+
default: return '●';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function semverEmoji(suggestion) {
|
|
37
|
+
switch (suggestion) {
|
|
38
|
+
case 'major': return chalk.red('● MAJOR');
|
|
39
|
+
case 'minor': return chalk.yellow('● MINOR');
|
|
40
|
+
case 'patch': return chalk.green('● PATCH');
|
|
41
|
+
default: return chalk.dim('none');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render the full terminal report.
|
|
47
|
+
*/
|
|
48
|
+
function renderTerminal(result) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
|
|
51
|
+
// ── Header box ──
|
|
52
|
+
const color = RISK_COLORS[result.risk_level] || chalk.white;
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(chalk.bold(' ╔══════════════════════════════════════════╗'));
|
|
55
|
+
lines.push(chalk.bold(' ║ CodeRifts — API Breaking Change Report ║'));
|
|
56
|
+
lines.push(chalk.bold(' ╠══════════════════════════════════════════╣'));
|
|
57
|
+
lines.push(chalk.bold(` ║ Risk Score: ${color(`${result.risk_score}/100 (${capitalize(result.risk_level)})`).padEnd(32)}║`));
|
|
58
|
+
lines.push(chalk.bold(` ║ Semver: ${semverEmoji(result.semver_suggestion).padEnd(32)}║`));
|
|
59
|
+
lines.push(chalk.bold(` ║ Breaking: ${String(result.stats?.breaking_count || 0).padEnd(22)}║`));
|
|
60
|
+
lines.push(chalk.bold(` ║ Non-Breaking: ${String(result.stats?.non_breaking_count || 0).padEnd(22)}║`));
|
|
61
|
+
lines.push(chalk.bold(` ║ Security: ${String(result.stats?.security_count || 0).padEnd(22)}║`));
|
|
62
|
+
lines.push(chalk.bold(' ╚══════════════════════════════════════════╝'));
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
// ── Risk Dimensions ──
|
|
66
|
+
if (result.risk_dimensions) {
|
|
67
|
+
const dims = result.risk_dimensions;
|
|
68
|
+
lines.push(chalk.bold(' Risk Dimensions:'));
|
|
69
|
+
const dimTable = new Table({
|
|
70
|
+
head: ['Dimension', 'Score'],
|
|
71
|
+
style: { head: ['cyan'], border: ['dim'] },
|
|
72
|
+
colWidths: [25, 10],
|
|
73
|
+
});
|
|
74
|
+
dimTable.push(
|
|
75
|
+
['Revenue Impact', dims.revenue_impact || 0],
|
|
76
|
+
['Blast Radius', dims.blast_radius || 0],
|
|
77
|
+
['App Compatibility', dims.app_compatibility || 0],
|
|
78
|
+
['Security', dims.security || 0],
|
|
79
|
+
);
|
|
80
|
+
lines.push(dimTable.toString());
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Breaking Changes ──
|
|
85
|
+
if (result.breaking_changes && result.breaking_changes.length > 0) {
|
|
86
|
+
lines.push(chalk.bold.red(` Breaking Changes (${result.breaking_changes.length}):`));
|
|
87
|
+
const breakTable = new Table({
|
|
88
|
+
head: ['Type', 'Path', 'Severity'],
|
|
89
|
+
style: { head: ['red'], border: ['dim'] },
|
|
90
|
+
colWidths: [25, 25, 12],
|
|
91
|
+
});
|
|
92
|
+
for (const change of result.breaking_changes) {
|
|
93
|
+
const sev = SEVERITY_ICONS[change.severity] || change.severity;
|
|
94
|
+
breakTable.push([
|
|
95
|
+
change.type || '',
|
|
96
|
+
`${change.method || ''} ${change.path || ''}`.trim(),
|
|
97
|
+
`${sev} ${change.severity || ''}`,
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
lines.push(breakTable.toString());
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Non-Breaking Changes ──
|
|
105
|
+
if (result.non_breaking_changes && result.non_breaking_changes.length > 0) {
|
|
106
|
+
lines.push(chalk.bold.green(` Non-Breaking Changes (${result.non_breaking_changes.length}):`));
|
|
107
|
+
const nbTable = new Table({
|
|
108
|
+
head: ['Type', 'Path', 'Description'],
|
|
109
|
+
style: { head: ['green'], border: ['dim'] },
|
|
110
|
+
colWidths: [25, 25, 25],
|
|
111
|
+
});
|
|
112
|
+
for (const change of result.non_breaking_changes.slice(0, 10)) {
|
|
113
|
+
nbTable.push([
|
|
114
|
+
change.type || '',
|
|
115
|
+
`${change.method || ''} ${change.path || ''}`.trim(),
|
|
116
|
+
(change.description || '').substring(0, 22),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
lines.push(nbTable.toString());
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Security Findings ──
|
|
124
|
+
if (result.security_findings && result.security_findings.length > 0) {
|
|
125
|
+
lines.push(chalk.bold.yellow(` Security Findings (${result.security_findings.length}):`));
|
|
126
|
+
for (const finding of result.security_findings) {
|
|
127
|
+
lines.push(` ${chalk.yellow('⚠')} ${finding.description || finding.type || JSON.stringify(finding)}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Policy Violations ──
|
|
133
|
+
if (result.policy_violations && result.policy_violations.length > 0) {
|
|
134
|
+
lines.push(chalk.bold.red(` Policy Violations (${result.policy_violations.length}):`));
|
|
135
|
+
for (const v of result.policy_violations) {
|
|
136
|
+
lines.push(` ${chalk.red('✗')} ${v.message || v.rule || JSON.stringify(v)}`);
|
|
137
|
+
}
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Should Block ──
|
|
142
|
+
if (result.should_block) {
|
|
143
|
+
lines.push(chalk.bgRed.white.bold(' ✗ BLOCKED — breaking changes exceed threshold '));
|
|
144
|
+
} else {
|
|
145
|
+
lines.push(chalk.green.bold(' ✓ PASSED — within acceptable risk threshold'));
|
|
146
|
+
}
|
|
147
|
+
lines.push('');
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function capitalize(s) {
|
|
153
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { renderTerminal };
|