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 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)
@@ -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,10 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Render the analysis result as formatted JSON.
5
+ */
6
+ function renderJson(result) {
7
+ return JSON.stringify(result, null, 2);
8
+ }
9
+
10
+ module.exports = { renderJson };
@@ -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 };