coderifts 1.3.0 → 1.7.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/package.json CHANGED
@@ -1,17 +1,15 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "1.3.0",
3
+ "version": "1.7.0",
4
4
  "description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
5
5
  "author": "CodeRifts <hello@coderifts.com>",
6
6
  "license": "MIT",
7
7
  "bin": {
8
- "coderifts": "./bin/coderifts.js"
8
+ "coderifts": "./dist/cli.js"
9
9
  },
10
- "main": "src/commands/diff.js",
10
+ "main": "dist/cli.js",
11
11
  "files": [
12
- "bin/",
13
- "src/",
14
- "scripts/",
12
+ "dist/",
15
13
  "README.md"
16
14
  ],
17
15
  "keywords": [
@@ -36,12 +34,10 @@
36
34
  "node": ">=18.0.0"
37
35
  },
38
36
  "scripts": {
39
- "test": "node --test test/*.test.js",
40
- "postinstall": "node scripts/postinstall.js"
41
- },
42
- "overrides": {
43
- "z-schema": "^8.4.0",
44
- "json-schema-ref-parser": "npm:@apidevtools/json-schema-ref-parser@^11.0.0"
37
+ "build": "esbuild bin/coderifts.js --bundle --platform=node --target=node18 --outfile=dist/cli.js",
38
+ "prepublishOnly": "npm run build",
39
+ "postinstall": "node scripts/postinstall.js",
40
+ "test": "node --test test/*.test.js"
45
41
  },
46
42
  "dependencies": {
47
43
  "chalk": "^4.1.2",
@@ -51,5 +47,12 @@
51
47
  "js-yaml": "^4.1.0",
52
48
  "openapi-diff": "^0.24.1",
53
49
  "ora": "^5.4.1"
50
+ },
51
+ "overrides": {
52
+ "z-schema": "^12.0.2",
53
+ "json-schema-ref-parser": "npm:@apidevtools/json-schema-ref-parser@^11.7.3"
54
+ },
55
+ "devDependencies": {
56
+ "esbuild": "^0.27.3"
54
57
  }
55
58
  }
package/bin/coderifts.js DELETED
@@ -1,45 +0,0 @@
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();
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall patch: z-schema v8+ exports { default: ZSchema } instead of ZSchema directly.
4
- * This patches @apidevtools/swagger-parser to handle both export styles.
5
- */
6
- const fs = require('fs');
7
- const path = require('path');
8
-
9
- try {
10
- const schemaPath = require.resolve('@apidevtools/swagger-parser/lib/validators/schema.js');
11
- let content = fs.readFileSync(schemaPath, 'utf8');
12
-
13
- if (!content.includes('_ZSchema')) {
14
- content = content.replace(
15
- 'const ZSchema = require("z-schema");',
16
- 'const _ZSchema = require("z-schema"); const ZSchema = _ZSchema.default || _ZSchema;'
17
- );
18
- fs.writeFileSync(schemaPath, content);
19
- console.log('postinstall: patched z-schema import in swagger-parser');
20
- }
21
- } catch (err) {
22
- console.log('postinstall: skipping z-schema patch:', err.message);
23
- }
24
-
25
- // Patch 2: json-schema-ref-parser v11+ exports { default: $RefParser } instead of $RefParser directly
26
- // json-schema-diff uses `new RefParser()` which breaks with the new export style
27
- try {
28
- const derefPath = require.resolve('json-schema-diff/dist/json-schema-diff/diff-schemas/dereference-schema.js');
29
- let content2 = fs.readFileSync(derefPath, 'utf8');
30
-
31
- if (!content2.includes('_RefParser')) {
32
- content2 = content2.replace(
33
- 'const RefParser = require("json-schema-ref-parser");',
34
- 'const _RefParser = require("json-schema-ref-parser"); const RefParser = _RefParser.default || _RefParser;'
35
- );
36
- fs.writeFileSync(derefPath, content2);
37
- console.log('postinstall: patched json-schema-ref-parser import in json-schema-diff');
38
- }
39
- } catch (err) {
40
- console.log('postinstall: skipping ref-parser patch:', err.message);
41
- }
package/src/cloud.js DELETED
@@ -1,51 +0,0 @@
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 };
@@ -1,237 +0,0 @@
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 };
@@ -1,223 +0,0 @@
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 };
@@ -1,38 +0,0 @@
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 };