delimit-cli 4.1.26 → 4.1.28

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 CHANGED
@@ -31,7 +31,7 @@ Works across any configuration — from a single model on a budget to an enterpr
31
31
 
32
32
  ```bash
33
33
  npx delimit-cli scan # Instant health grade for your API spec
34
- npx delimit-cli demo # See governance in action no setup needed
34
+ npx delimit-cli pr owner/repo#123 # Review any GitHub PR for breaking changes
35
35
  npx delimit-cli setup && source ~/.bashrc # Configure AI assistants + activate
36
36
  ```
37
37
 
@@ -143,6 +143,7 @@ That's it. Delimit auto-fetches the base branch spec, diffs it, and posts a PR c
143
143
 
144
144
  ```bash
145
145
  npx delimit-cli scan # Instant spec health grade + recommendations
146
+ npx delimit-cli pr owner/repo#123 # Review any GitHub PR for breaking changes
146
147
  npx delimit-cli quickstart # Clone demo project + guided walkthrough
147
148
  npx delimit-cli try # Zero-risk demo — saves governance report
148
149
  npx delimit-cli demo # Self-contained governance demo
@@ -2560,6 +2560,183 @@ program
2560
2560
  }
2561
2561
  });
2562
2562
 
2563
+ // PR review command — review any GitHub PR for breaking API changes
2564
+ program
2565
+ .command('pr <url>')
2566
+ .description('Review a GitHub PR for breaking API changes')
2567
+ .action(async (url) => {
2568
+ console.log(chalk.bold('\n Delimit PR Review\n'));
2569
+
2570
+ // Parse GitHub PR URL: owner/repo#number or full URL
2571
+ let owner, repo, prNumber;
2572
+ const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
2573
+ const shortMatch = url.match(/^([^/]+)\/([^#]+)#(\d+)$/);
2574
+ if (urlMatch) {
2575
+ [, owner, repo, prNumber] = urlMatch;
2576
+ } else if (shortMatch) {
2577
+ [, owner, repo, prNumber] = shortMatch;
2578
+ } else if (/^\d+$/.test(url)) {
2579
+ // Just a number — try current repo
2580
+ try {
2581
+ const remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8' }).trim();
2582
+ const remoteMatch = remote.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2583
+ if (remoteMatch) {
2584
+ [, owner, repo] = remoteMatch;
2585
+ prNumber = url;
2586
+ }
2587
+ } catch {}
2588
+ }
2589
+
2590
+ if (!owner || !repo || !prNumber) {
2591
+ console.log(chalk.red(' Could not parse PR URL.'));
2592
+ console.log(chalk.gray(' Usage: npx delimit-cli pr owner/repo#123'));
2593
+ console.log(chalk.gray(' npx delimit-cli pr https://github.com/owner/repo/pull/123'));
2594
+ console.log(chalk.gray(' npx delimit-cli pr 123 (in a git repo)\n'));
2595
+ return;
2596
+ }
2597
+
2598
+ console.log(chalk.gray(` Reviewing ${owner}/${repo}#${prNumber}...\n`));
2599
+
2600
+ // Get PR changed files
2601
+ try {
2602
+ const filesJson = execSync(
2603
+ `gh api repos/${owner}/${repo}/pulls/${prNumber}/files --paginate 2>/dev/null`,
2604
+ { encoding: 'utf-8', timeout: 15000 }
2605
+ );
2606
+ const files = JSON.parse(filesJson);
2607
+ const specPatterns = ['openapi', 'swagger', 'api-spec', 'api_spec'];
2608
+ const specExts = ['.yaml', '.yml', '.json'];
2609
+ const specFiles = files.filter(f => {
2610
+ const name = f.filename.toLowerCase();
2611
+ return specExts.some(ext => name.endsWith(ext)) &&
2612
+ specPatterns.some(p => name.includes(p));
2613
+ });
2614
+
2615
+ if (specFiles.length === 0) {
2616
+ console.log(chalk.gray(' No OpenAPI/Swagger spec changes found in this PR.'));
2617
+ console.log(chalk.gray(' Delimit reviews PRs that modify API spec files.\n'));
2618
+ // Show what files were changed
2619
+ const apiFiles = files.filter(f => f.filename.includes('api') || f.filename.includes('spec'));
2620
+ if (apiFiles.length > 0) {
2621
+ console.log(chalk.gray(' API-related files changed:'));
2622
+ apiFiles.slice(0, 5).forEach(f => console.log(chalk.gray(` ${f.filename} (+${f.additions}/-${f.deletions})`)));
2623
+ console.log('');
2624
+ }
2625
+ return;
2626
+ }
2627
+
2628
+ console.log(` ${chalk.green('✓')} Found ${specFiles.length} spec file(s) changed:\n`);
2629
+ specFiles.forEach(f => {
2630
+ console.log(` ${chalk.cyan(f.filename)} (+${f.additions}/-${f.deletions})`);
2631
+ });
2632
+ console.log('');
2633
+
2634
+ // Fetch base and head versions of the first spec
2635
+ const specFile = specFiles[0];
2636
+ const tmpDir = path.join(os.tmpdir(), `delimit-pr-${Date.now()}`);
2637
+ fs.mkdirSync(tmpDir, { recursive: true });
2638
+
2639
+ // Get PR details for base/head refs
2640
+ const prJson = execSync(
2641
+ `gh api repos/${owner}/${repo}/pulls/${prNumber} 2>/dev/null`,
2642
+ { encoding: 'utf-8', timeout: 10000 }
2643
+ );
2644
+ const pr = JSON.parse(prJson);
2645
+ const baseSha = pr.base.sha;
2646
+ const headSha = pr.head.sha;
2647
+
2648
+ // Fetch both versions
2649
+ const basePath = path.join(tmpDir, 'base.yaml');
2650
+ const headPath = path.join(tmpDir, 'head.yaml');
2651
+
2652
+ try {
2653
+ const baseContent = execSync(
2654
+ `gh api repos/${owner}/${repo}/contents/${specFile.filename}?ref=${baseSha} -q '.content' 2>/dev/null`,
2655
+ { encoding: 'utf-8', timeout: 10000 }
2656
+ );
2657
+ fs.writeFileSync(basePath, Buffer.from(baseContent.trim(), 'base64').toString());
2658
+ } catch {
2659
+ console.log(chalk.yellow(' New spec file (no base version). Cannot diff.\n'));
2660
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2661
+ return;
2662
+ }
2663
+
2664
+ try {
2665
+ const headContent = execSync(
2666
+ `gh api repos/${owner}/${repo}/contents/${specFile.filename}?ref=${headSha} -q '.content' 2>/dev/null`,
2667
+ { encoding: 'utf-8', timeout: 10000 }
2668
+ );
2669
+ fs.writeFileSync(headPath, Buffer.from(headContent.trim(), 'base64').toString());
2670
+ } catch {
2671
+ console.log(chalk.red(' Could not fetch head version of spec.\n'));
2672
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2673
+ return;
2674
+ }
2675
+
2676
+ // Run lint
2677
+ console.log(chalk.gray(' Running governance pipeline...\n'));
2678
+ const bundledGateway = path.join(__dirname, '..', 'gateway');
2679
+ const serverDir = (continuityContext.serverDir && fs.existsSync(continuityContext.serverDir))
2680
+ ? continuityContext.serverDir
2681
+ : fs.existsSync(bundledGateway) ? bundledGateway : null;
2682
+
2683
+ if (!serverDir) {
2684
+ console.log(chalk.yellow(' Gateway not available. Run: npx delimit-cli setup\n'));
2685
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2686
+ return;
2687
+ }
2688
+
2689
+ try {
2690
+ const result = execSync(
2691
+ `python3 -c "
2692
+ import sys,json,yaml
2693
+ sys.path.insert(0,'${serverDir}')
2694
+ from core.diff_engine_v2 import OpenAPIDiffEngine
2695
+ old=yaml.safe_load(open('${basePath}'))
2696
+ new=yaml.safe_load(open('${headPath}'))
2697
+ engine=OpenAPIDiffEngine()
2698
+ changes=engine.compare(old,new)
2699
+ print(json.dumps({'changes':[{'type':c.type.value if hasattr(c.type,'value') else str(c.type),'path':c.path,'breaking':c.severity in ('high','critical','error'),'detail':c.message or c.details or ''} for c in changes]}))
2700
+ "`,
2701
+ { encoding: 'utf-8', timeout: 15000, cwd: serverDir }
2702
+ );
2703
+ const diff = JSON.parse(result);
2704
+ const breaking = (diff.changes || []).filter(c => c.breaking);
2705
+ const nonBreaking = (diff.changes || []).filter(c => !c.breaking);
2706
+
2707
+ if (breaking.length > 0) {
2708
+ console.log(chalk.red.bold(` ${breaking.length} BREAKING change(s) detected\n`));
2709
+ breaking.forEach(c => {
2710
+ console.log(` ${chalk.red('BREAK')} ${c.type}: ${c.path || ''}`);
2711
+ if (c.detail) console.log(chalk.gray(` ${c.detail}`));
2712
+ });
2713
+ } else {
2714
+ console.log(chalk.green.bold(' No breaking changes detected'));
2715
+ }
2716
+
2717
+ if (nonBreaking.length > 0) {
2718
+ console.log(chalk.gray(`\n ${nonBreaking.length} non-breaking change(s)`));
2719
+ }
2720
+
2721
+ // Semver classification
2722
+ const bump = breaking.length > 0 ? 'MAJOR' : nonBreaking.length > 0 ? 'MINOR' : 'NONE';
2723
+ console.log(`\n Semver: ${chalk.bold(bump)}`);
2724
+ console.log(` Total: ${(diff.changes || []).length} changes (${breaking.length} breaking, ${nonBreaking.length} compatible)\n`);
2725
+
2726
+ console.log(chalk.bold(' Add to your repo:'));
2727
+ console.log(chalk.green(` npx delimit-cli init`));
2728
+ console.log(chalk.gray(' Catches this on every PR automatically.\n'));
2729
+ } catch (e) {
2730
+ console.log(chalk.red(` Diff failed: ${e.message.split('\n')[0]}\n`));
2731
+ }
2732
+
2733
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2734
+ } catch (e) {
2735
+ console.log(chalk.red(` Error: ${e.message.split('\n')[0]}`));
2736
+ console.log(chalk.gray(' Make sure gh CLI is authenticated: gh auth login\n'));
2737
+ }
2738
+ });
2739
+
2563
2740
  // Try command — zero-risk demo with Markdown report artifact (LED-264)
2564
2741
  program
2565
2742
  .command('try')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.1.26",
4
+ "version": "4.1.28",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [