delimit-cli 2.2.0 → 2.3.1
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 +91 -37
- package/bin/delimit-cli.js +300 -130
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# delimit-cli
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Prevent breaking API changes before they reach production.**
|
|
4
|
+
|
|
5
|
+
Deterministic diff engine + policy enforcement + semver classification for OpenAPI specs. The independent successor to Optic.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/delimit-cli)
|
|
6
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -11,46 +13,76 @@
|
|
|
11
13
|
npm install -g delimit-cli
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Quick Start
|
|
16
|
+
## Quick Start (Under 5 Minutes)
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
# Initialize a policy
|
|
20
|
-
delimit init
|
|
19
|
+
# 1. Initialize with a policy preset
|
|
20
|
+
delimit init --preset default
|
|
21
21
|
|
|
22
|
-
# Detect breaking changes
|
|
22
|
+
# 2. Detect breaking changes
|
|
23
23
|
delimit lint api/openapi-old.yaml api/openapi-new.yaml
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Generate a human-readable explanation
|
|
29
|
-
delimit explain api/openapi-old.yaml api/openapi-new.yaml
|
|
25
|
+
# 3. Add the GitHub Action for automated PR checks
|
|
26
|
+
# Copy .github/workflows/api-governance.yml (see CI section below)
|
|
30
27
|
```
|
|
31
28
|
|
|
29
|
+
## What It Catches
|
|
30
|
+
|
|
31
|
+
Delimit deterministically detects 23 types of API changes, including 10 breaking patterns:
|
|
32
|
+
|
|
33
|
+
- Endpoint or method removal
|
|
34
|
+
- Required parameter addition
|
|
35
|
+
- Response field removal
|
|
36
|
+
- Type changes
|
|
37
|
+
- Enum value removal
|
|
38
|
+
- And more
|
|
39
|
+
|
|
40
|
+
Every change is classified as `MAJOR`, `MINOR`, `PATCH`, or `NONE` per semver.
|
|
41
|
+
|
|
32
42
|
## Commands
|
|
33
43
|
|
|
34
44
|
| Command | Description |
|
|
35
45
|
|---------|-------------|
|
|
36
|
-
| `delimit init` | Create `.delimit/policies.yml` with
|
|
37
|
-
| `delimit lint <old> <new>` | Diff + policy check
|
|
46
|
+
| `delimit init` | Create `.delimit/policies.yml` with a policy preset |
|
|
47
|
+
| `delimit lint <old> <new>` | Diff + policy check — returns exit code 1 on violations |
|
|
38
48
|
| `delimit diff <old> <new>` | Raw diff with `[BREAKING]`/`[safe]` tags |
|
|
39
49
|
| `delimit explain <old> <new>` | Human-readable change explanation |
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
## Policy Presets
|
|
52
|
+
|
|
53
|
+
Choose a preset that fits your team:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
delimit init --preset strict # Public APIs, payments — zero tolerance
|
|
57
|
+
delimit init --preset default # Most teams — balanced rules
|
|
58
|
+
delimit init --preset relaxed # Internal APIs, startups — warnings only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Preset | Breaking changes | Type changes | Field removal |
|
|
62
|
+
|--------|-----------------|--------------|---------------|
|
|
63
|
+
| `strict` | Error (blocks) | Error (blocks) | Error (blocks) |
|
|
64
|
+
| `default` | Error (blocks) | Warning | Error (blocks) |
|
|
65
|
+
| `relaxed` | Warning | Warning | Info |
|
|
66
|
+
|
|
67
|
+
Pass a preset directly to lint:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
delimit lint --policy strict old.yaml new.yaml
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Options
|
|
42
74
|
|
|
43
75
|
```bash
|
|
44
|
-
#
|
|
76
|
+
# Semver classification with version bump
|
|
77
|
+
delimit lint old.yaml new.yaml --current-version 1.0.0
|
|
78
|
+
|
|
79
|
+
# Explainer templates
|
|
45
80
|
delimit explain old.yaml new.yaml -t migration
|
|
46
81
|
delimit explain old.yaml new.yaml -t pr_comment
|
|
47
82
|
delimit explain old.yaml new.yaml -t changelog
|
|
48
83
|
|
|
49
|
-
#
|
|
50
|
-
delimit lint old.yaml new.yaml --
|
|
51
|
-
|
|
52
|
-
# Use custom policy file
|
|
53
|
-
delimit lint old.yaml new.yaml -p .delimit/policies.yml
|
|
84
|
+
# JSON output for scripting
|
|
85
|
+
delimit lint old.yaml new.yaml --json
|
|
54
86
|
```
|
|
55
87
|
|
|
56
88
|
### Explainer Templates
|
|
@@ -67,34 +99,56 @@ delimit lint old.yaml new.yaml -p .delimit/policies.yml
|
|
|
67
99
|
|
|
68
100
|
## CI/CD Integration
|
|
69
101
|
|
|
70
|
-
|
|
102
|
+
Add this workflow to `.github/workflows/api-governance.yml`:
|
|
71
103
|
|
|
72
104
|
```yaml
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
name: API Governance
|
|
106
|
+
on:
|
|
107
|
+
pull_request:
|
|
108
|
+
paths:
|
|
109
|
+
- 'path/to/openapi.yaml' # adjust to your spec path
|
|
110
|
+
permissions:
|
|
111
|
+
contents: read
|
|
112
|
+
pull-requests: write
|
|
113
|
+
jobs:
|
|
114
|
+
api-governance:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: actions/checkout@v4
|
|
119
|
+
with:
|
|
120
|
+
ref: ${{ github.event.pull_request.base.sha }}
|
|
121
|
+
path: _base
|
|
122
|
+
- uses: delimit-ai/delimit@v1
|
|
123
|
+
with:
|
|
124
|
+
old_spec: _base/path/to/openapi.yaml
|
|
125
|
+
new_spec: path/to/openapi.yaml
|
|
126
|
+
mode: advisory # or 'enforce' to block PRs
|
|
77
127
|
```
|
|
78
128
|
|
|
129
|
+
The action posts a PR comment with:
|
|
130
|
+
- Semver badge (`MAJOR` / `MINOR` / `PATCH`)
|
|
131
|
+
- Violation table with severity
|
|
132
|
+
- Expandable migration guide for breaking changes
|
|
133
|
+
|
|
79
134
|
See [Delimit API Governance](https://github.com/marketplace/actions/delimit-api-governance) on the GitHub Marketplace.
|
|
80
135
|
|
|
81
136
|
## Custom Policies
|
|
82
137
|
|
|
83
|
-
Create `.delimit/policies.yml
|
|
138
|
+
Create `.delimit/policies.yml` or start from a preset:
|
|
84
139
|
|
|
85
140
|
```yaml
|
|
141
|
+
override_defaults: false
|
|
142
|
+
|
|
86
143
|
rules:
|
|
87
|
-
- id:
|
|
88
|
-
|
|
144
|
+
- id: protect_v1
|
|
145
|
+
name: Protect V1 API
|
|
146
|
+
change_types: [endpoint_removed, method_removed, field_removed]
|
|
89
147
|
severity: error
|
|
90
148
|
action: forbid
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
change_types: [type_changed]
|
|
95
|
-
severity: warning
|
|
96
|
-
action: warn
|
|
97
|
-
message: "Type change may break clients"
|
|
149
|
+
conditions:
|
|
150
|
+
path_pattern: "^/v1/.*"
|
|
151
|
+
message: "V1 API is frozen. Make changes in V2."
|
|
98
152
|
```
|
|
99
153
|
|
|
100
154
|
## Supported Specs
|
|
@@ -105,7 +159,7 @@ rules:
|
|
|
105
159
|
|
|
106
160
|
## Links
|
|
107
161
|
|
|
108
|
-
- [GitHub Action](https://github.com/marketplace/actions/delimit-api-governance) —
|
|
162
|
+
- [GitHub Action](https://github.com/marketplace/actions/delimit-api-governance) — Automated PR checks
|
|
109
163
|
- [GitHub](https://github.com/delimit-ai/delimit) — Source code
|
|
110
164
|
- [Issues](https://github.com/delimit-ai/delimit/issues) — Bug reports and feature requests
|
|
111
165
|
|
package/bin/delimit-cli.js
CHANGED
|
@@ -49,8 +49,8 @@ async function ensureAgent() {
|
|
|
49
49
|
|
|
50
50
|
program
|
|
51
51
|
.name('delimit')
|
|
52
|
-
.description('
|
|
53
|
-
.version('2.
|
|
52
|
+
.description('Prevent breaking API changes before they reach production')
|
|
53
|
+
.version('2.3.0');
|
|
54
54
|
|
|
55
55
|
// Install command with modes
|
|
56
56
|
program
|
|
@@ -158,6 +158,7 @@ program
|
|
|
158
158
|
program
|
|
159
159
|
.command('mode [mode]')
|
|
160
160
|
.description('Switch governance mode (advisory, guarded, enforce, auto)')
|
|
161
|
+
|
|
161
162
|
.action(async (mode) => {
|
|
162
163
|
await ensureAgent();
|
|
163
164
|
|
|
@@ -181,6 +182,7 @@ program
|
|
|
181
182
|
program
|
|
182
183
|
.command('status')
|
|
183
184
|
.description('Show governance status')
|
|
185
|
+
|
|
184
186
|
.option('--verbose', 'Show detailed status')
|
|
185
187
|
.action(async (options) => {
|
|
186
188
|
const agentRunning = await checkAgent();
|
|
@@ -274,6 +276,7 @@ program
|
|
|
274
276
|
program
|
|
275
277
|
.command('policy')
|
|
276
278
|
.description('Manage governance policies')
|
|
279
|
+
|
|
277
280
|
.option('--init', 'Create example policy file')
|
|
278
281
|
.option('--validate', 'Validate policy syntax')
|
|
279
282
|
.action(async (options) => {
|
|
@@ -326,6 +329,7 @@ overrides:
|
|
|
326
329
|
program
|
|
327
330
|
.command('auth')
|
|
328
331
|
.description('Setup authentication and credentials for services')
|
|
332
|
+
|
|
329
333
|
.option('--all', 'Setup all available services')
|
|
330
334
|
.option('--github', 'Setup GitHub authentication')
|
|
331
335
|
.option('--ai', 'Setup AI tools authentication')
|
|
@@ -387,6 +391,7 @@ program
|
|
|
387
391
|
program
|
|
388
392
|
.command('audit')
|
|
389
393
|
.description('View governance audit log')
|
|
394
|
+
|
|
390
395
|
.option('--tail <n>', 'Show last N entries', '10')
|
|
391
396
|
.action(async (options) => {
|
|
392
397
|
await ensureAgent();
|
|
@@ -412,120 +417,14 @@ program
|
|
|
412
417
|
});
|
|
413
418
|
});
|
|
414
419
|
|
|
415
|
-
// Doctor command -
|
|
416
|
-
|
|
417
|
-
.command('doctor')
|
|
418
|
-
.description('Diagnose Delimit configuration and issues')
|
|
419
|
-
.action(async () => {
|
|
420
|
-
console.log(chalk.blue.bold('\n🩺 Delimit Doctor\n'));
|
|
421
|
-
const issues = [];
|
|
422
|
-
const warnings = [];
|
|
423
|
-
const info = [];
|
|
424
|
-
|
|
425
|
-
// Check agent status
|
|
426
|
-
const agentRunning = await checkAgent();
|
|
427
|
-
if (!agentRunning) {
|
|
428
|
-
issues.push('Agent is not running. Run "delimit status" to start it.');
|
|
429
|
-
} else {
|
|
430
|
-
info.push('Agent is running and responsive');
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Check Git hooks
|
|
434
|
-
try {
|
|
435
|
-
const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
|
|
436
|
-
if (hooksPath.includes('.delimit')) {
|
|
437
|
-
info.push('Git hooks are configured correctly');
|
|
438
|
-
|
|
439
|
-
// Check hook files exist
|
|
440
|
-
const hookFiles = ['pre-commit', 'pre-push'];
|
|
441
|
-
hookFiles.forEach(hook => {
|
|
442
|
-
const hookFile = path.join(hooksPath, hook);
|
|
443
|
-
if (!fs.existsSync(hookFile)) {
|
|
444
|
-
warnings.push(`Missing hook file: ${hook}`);
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
} else {
|
|
448
|
-
warnings.push('Git hooks not pointing to Delimit. Run "delimit install" to fix.');
|
|
449
|
-
}
|
|
450
|
-
} catch (e) {
|
|
451
|
-
issues.push('Git hooks not configured. Run "delimit install" to set up.');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Check PATH
|
|
455
|
-
const pathHasDelimit = process.env.PATH.includes('.delimit/shims');
|
|
456
|
-
if (pathHasDelimit) {
|
|
457
|
-
warnings.push('PATH hijacking is active (for AI tool interception)');
|
|
458
|
-
} else {
|
|
459
|
-
info.push('PATH is clean (no AI tool interception)');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Check policy files
|
|
463
|
-
const policies = [];
|
|
464
|
-
if (fs.existsSync('delimit.yml')) {
|
|
465
|
-
policies.push('project');
|
|
466
|
-
// Validate policy
|
|
467
|
-
try {
|
|
468
|
-
const policy = yaml.load(fs.readFileSync('delimit.yml', 'utf8'));
|
|
469
|
-
if (!policy.rules) {
|
|
470
|
-
warnings.push('Project policy has no rules defined');
|
|
471
|
-
}
|
|
472
|
-
} catch (e) {
|
|
473
|
-
issues.push(`Project policy is invalid: ${e.message}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const userPolicyPath = path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml');
|
|
478
|
-
if (fs.existsSync(userPolicyPath)) {
|
|
479
|
-
policies.push('user');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (policies.length === 0) {
|
|
483
|
-
warnings.push('No policy files found. Run "delimit policy --init" to create one.');
|
|
484
|
-
} else {
|
|
485
|
-
info.push(`Policy files loaded: ${policies.join(', ')}`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Check audit log
|
|
489
|
-
const auditDir = path.join(process.env.HOME, '.delimit', 'audit');
|
|
490
|
-
if (fs.existsSync(auditDir)) {
|
|
491
|
-
const files = fs.readdirSync(auditDir);
|
|
492
|
-
info.push(`Audit log has ${files.length} day(s) of history`);
|
|
493
|
-
} else {
|
|
494
|
-
warnings.push('No audit logs found yet');
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Display results
|
|
498
|
-
if (issues.length > 0) {
|
|
499
|
-
console.log(chalk.red.bold('❌ Issues Found:\n'));
|
|
500
|
-
issues.forEach(issue => console.log(chalk.red(` • ${issue}`)));
|
|
501
|
-
console.log();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (warnings.length > 0) {
|
|
505
|
-
console.log(chalk.yellow.bold('⚠️ Warnings:\n'));
|
|
506
|
-
warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`)));
|
|
507
|
-
console.log();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (info.length > 0) {
|
|
511
|
-
console.log(chalk.green.bold('✅ Working Correctly:\n'));
|
|
512
|
-
info.forEach(item => console.log(chalk.green(` • ${item}`)));
|
|
513
|
-
console.log();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Overall status
|
|
517
|
-
if (issues.length === 0) {
|
|
518
|
-
console.log(chalk.green.bold('🎉 Delimit is healthy!'));
|
|
519
|
-
} else {
|
|
520
|
-
console.log(chalk.red.bold('🔧 Please fix the issues above'));
|
|
521
|
-
process.exit(1);
|
|
522
|
-
}
|
|
523
|
-
});
|
|
420
|
+
// Doctor command - verify setup for API governance
|
|
421
|
+
// (legacy doctor replaced with v1-focused checks)
|
|
524
422
|
|
|
525
423
|
// Explain-decision command - show governance decision reasoning
|
|
526
424
|
program
|
|
527
425
|
.command('explain-decision [decision-id]')
|
|
528
426
|
.description('Explain a governance decision')
|
|
427
|
+
|
|
529
428
|
.action(async (decisionId) => {
|
|
530
429
|
await ensureAgent();
|
|
531
430
|
|
|
@@ -545,6 +444,7 @@ program
|
|
|
545
444
|
program
|
|
546
445
|
.command('uninstall')
|
|
547
446
|
.description('Remove Delimit governance')
|
|
447
|
+
|
|
548
448
|
.action(async () => {
|
|
549
449
|
const { confirm } = await inquirer.prompt([{
|
|
550
450
|
type: 'confirm',
|
|
@@ -637,6 +537,7 @@ program
|
|
|
637
537
|
.command('proxy <tool>')
|
|
638
538
|
.allowUnknownOption()
|
|
639
539
|
.description('Proxy AI tool execution with governance')
|
|
540
|
+
|
|
640
541
|
.action(async (tool, options) => {
|
|
641
542
|
const { proxyAITool } = require('../lib/proxy-handler');
|
|
642
543
|
// Get all args after the tool name
|
|
@@ -649,6 +550,7 @@ program
|
|
|
649
550
|
program
|
|
650
551
|
.command('hook <type>')
|
|
651
552
|
.description('Internal hook handler')
|
|
553
|
+
|
|
652
554
|
.action(async (type) => {
|
|
653
555
|
await ensureAgent();
|
|
654
556
|
|
|
@@ -741,11 +643,131 @@ program
|
|
|
741
643
|
|
|
742
644
|
const apiEngine = require('../lib/api-engine');
|
|
743
645
|
|
|
646
|
+
// Policy preset templates
|
|
647
|
+
const POLICY_PRESETS = {
|
|
648
|
+
strict: `# Delimit Policy Preset: strict
|
|
649
|
+
# For public APIs, payment systems, and regulated environments.
|
|
650
|
+
# Zero tolerance for breaking changes.
|
|
651
|
+
|
|
652
|
+
override_defaults: true
|
|
653
|
+
|
|
654
|
+
rules:
|
|
655
|
+
- id: no_endpoint_removal
|
|
656
|
+
name: Forbid Endpoint Removal
|
|
657
|
+
change_types: [endpoint_removed]
|
|
658
|
+
severity: error
|
|
659
|
+
action: forbid
|
|
660
|
+
message: "Endpoint {path} cannot be removed. Deprecate with Sunset header first."
|
|
661
|
+
|
|
662
|
+
- id: no_method_removal
|
|
663
|
+
name: Forbid Method Removal
|
|
664
|
+
change_types: [method_removed]
|
|
665
|
+
severity: error
|
|
666
|
+
action: forbid
|
|
667
|
+
message: "HTTP method removed from {path}. This breaks all clients."
|
|
668
|
+
|
|
669
|
+
- id: no_required_param_addition
|
|
670
|
+
name: Forbid Required Parameter Addition
|
|
671
|
+
change_types: [required_param_added]
|
|
672
|
+
severity: error
|
|
673
|
+
action: forbid
|
|
674
|
+
message: "Cannot add required parameter to {path}. Make it optional."
|
|
675
|
+
|
|
676
|
+
- id: no_field_removal
|
|
677
|
+
name: Forbid Response Field Removal
|
|
678
|
+
change_types: [field_removed]
|
|
679
|
+
severity: error
|
|
680
|
+
action: forbid
|
|
681
|
+
message: "Cannot remove field from {path}. Deprecate it first."
|
|
682
|
+
|
|
683
|
+
- id: no_type_change
|
|
684
|
+
name: Forbid Type Changes
|
|
685
|
+
change_types: [type_changed]
|
|
686
|
+
severity: error
|
|
687
|
+
action: forbid
|
|
688
|
+
message: "Type change at {path} breaks client deserialization."
|
|
689
|
+
|
|
690
|
+
- id: no_enum_removal
|
|
691
|
+
name: Forbid Enum Value Removal
|
|
692
|
+
change_types: [enum_value_removed]
|
|
693
|
+
severity: error
|
|
694
|
+
action: forbid
|
|
695
|
+
message: "Enum value removed at {path}."
|
|
696
|
+
|
|
697
|
+
- id: no_param_removal
|
|
698
|
+
name: Forbid Parameter Removal
|
|
699
|
+
change_types: [param_removed]
|
|
700
|
+
severity: error
|
|
701
|
+
action: forbid
|
|
702
|
+
message: "Parameter removed from {path}."
|
|
703
|
+
`,
|
|
704
|
+
default: `# Delimit Policy Preset: default
|
|
705
|
+
# Balanced rules for most teams. Blocks destructive changes, warns on risky ones.
|
|
706
|
+
# Uses built-in defaults — customize by adding rules below.
|
|
707
|
+
|
|
708
|
+
override_defaults: false
|
|
709
|
+
|
|
710
|
+
rules: []
|
|
711
|
+
# Add custom rules here. Example:
|
|
712
|
+
# - id: protect_v1
|
|
713
|
+
# name: Protect V1 API
|
|
714
|
+
# change_types: [endpoint_removed, method_removed, field_removed]
|
|
715
|
+
# severity: error
|
|
716
|
+
# action: forbid
|
|
717
|
+
# conditions:
|
|
718
|
+
# path_pattern: "^/v1/.*"
|
|
719
|
+
# message: "V1 API is frozen. Make changes in V2."
|
|
720
|
+
`,
|
|
721
|
+
relaxed: `# Delimit Policy Preset: relaxed
|
|
722
|
+
# For internal APIs, early-stage startups, and rapid iteration.
|
|
723
|
+
# Only warns — never blocks CI.
|
|
724
|
+
|
|
725
|
+
override_defaults: true
|
|
726
|
+
|
|
727
|
+
rules:
|
|
728
|
+
- id: warn_endpoint_removal
|
|
729
|
+
name: Warn on Endpoint Removal
|
|
730
|
+
change_types: [endpoint_removed]
|
|
731
|
+
severity: warning
|
|
732
|
+
action: warn
|
|
733
|
+
message: "Endpoint {path} was removed. Check downstream consumers."
|
|
734
|
+
|
|
735
|
+
- id: warn_method_removal
|
|
736
|
+
name: Warn on Method Removal
|
|
737
|
+
change_types: [method_removed]
|
|
738
|
+
severity: warning
|
|
739
|
+
action: warn
|
|
740
|
+
message: "HTTP method removed from {path}."
|
|
741
|
+
|
|
742
|
+
- id: warn_required_param
|
|
743
|
+
name: Warn on Required Parameter Addition
|
|
744
|
+
change_types: [required_param_added]
|
|
745
|
+
severity: warning
|
|
746
|
+
action: warn
|
|
747
|
+
message: "New required parameter at {path}."
|
|
748
|
+
|
|
749
|
+
- id: warn_type_change
|
|
750
|
+
name: Warn on Type Changes
|
|
751
|
+
change_types: [type_changed]
|
|
752
|
+
severity: warning
|
|
753
|
+
action: warn
|
|
754
|
+
message: "Type changed at {path}."
|
|
755
|
+
|
|
756
|
+
- id: allow_field_removal
|
|
757
|
+
name: Allow Field Removal
|
|
758
|
+
change_types: [field_removed]
|
|
759
|
+
severity: info
|
|
760
|
+
action: allow
|
|
761
|
+
message: "Field removed from {path}."
|
|
762
|
+
`,
|
|
763
|
+
};
|
|
764
|
+
|
|
744
765
|
// Init command — scaffold .delimit/ config
|
|
745
766
|
program
|
|
746
767
|
.command('init')
|
|
747
768
|
.description('Initialize Delimit API governance in this project')
|
|
748
|
-
.
|
|
769
|
+
.option('--preset <name>', 'Policy preset: strict, default, or relaxed', 'default')
|
|
770
|
+
.action(async (options) => {
|
|
749
771
|
const configDir = path.join(process.cwd(), '.delimit');
|
|
750
772
|
const policyFile = path.join(configDir, 'policies.yml');
|
|
751
773
|
|
|
@@ -754,34 +776,175 @@ program
|
|
|
754
776
|
return;
|
|
755
777
|
}
|
|
756
778
|
|
|
779
|
+
const preset = options.preset.toLowerCase();
|
|
780
|
+
if (!POLICY_PRESETS[preset]) {
|
|
781
|
+
console.log(chalk.red(`Unknown preset "${preset}". Choose: strict, default, or relaxed`));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
757
785
|
fs.mkdirSync(configDir, { recursive: true });
|
|
786
|
+
fs.writeFileSync(policyFile, POLICY_PRESETS[preset]);
|
|
787
|
+
console.log(chalk.green(`\n Created .delimit/policies.yml (preset: ${preset})\n`));
|
|
758
788
|
|
|
759
|
-
|
|
760
|
-
|
|
789
|
+
// Auto-detect OpenAPI spec files
|
|
790
|
+
const specPatterns = [
|
|
791
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
792
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
793
|
+
'api.yaml', 'api.yml', 'api.json',
|
|
794
|
+
'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
|
|
795
|
+
'spec/openapi.yaml', 'spec/openapi.json',
|
|
796
|
+
'specs/openapi.yaml', 'specs/openapi.json',
|
|
797
|
+
'api/openapi.yaml', 'api/openapi.json',
|
|
798
|
+
'contrib/openapi.json',
|
|
799
|
+
];
|
|
800
|
+
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
|
|
761
801
|
|
|
762
|
-
|
|
763
|
-
|
|
802
|
+
if (foundSpecs.length > 0) {
|
|
803
|
+
const specPath = foundSpecs[0];
|
|
804
|
+
console.log(` Detected spec: ${chalk.bold(specPath)}`);
|
|
805
|
+
console.log('');
|
|
806
|
+
console.log(chalk.bold(' Add this to .github/workflows/api-governance.yml:\n'));
|
|
807
|
+
console.log(chalk.gray(` name: API Governance
|
|
808
|
+
on:
|
|
809
|
+
pull_request:
|
|
810
|
+
paths:
|
|
811
|
+
- '${specPath}'
|
|
812
|
+
permissions:
|
|
813
|
+
contents: read
|
|
814
|
+
pull-requests: write
|
|
815
|
+
jobs:
|
|
816
|
+
api-governance:
|
|
817
|
+
runs-on: ubuntu-latest
|
|
818
|
+
steps:
|
|
819
|
+
- uses: actions/checkout@v4
|
|
820
|
+
- uses: actions/checkout@v4
|
|
821
|
+
with:
|
|
822
|
+
ref: \${{ github.event.pull_request.base.sha }}
|
|
823
|
+
path: _base
|
|
824
|
+
- uses: delimit-ai/delimit@v1
|
|
825
|
+
with:
|
|
826
|
+
old_spec: _base/${specPath}
|
|
827
|
+
new_spec: ${specPath}
|
|
828
|
+
mode: advisory`));
|
|
829
|
+
console.log('');
|
|
830
|
+
} else {
|
|
831
|
+
console.log(' No OpenAPI spec file detected.');
|
|
832
|
+
console.log(` Delimit also supports ${chalk.bold('Zero-Spec Mode')} — run ${chalk.bold('delimit lint')} in a FastAPI/NestJS/Express project.`);
|
|
833
|
+
console.log('');
|
|
834
|
+
}
|
|
764
835
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
# - id: protect_v1
|
|
768
|
-
# name: Protect V1 API
|
|
769
|
-
# change_types: [endpoint_removed, method_removed, field_removed]
|
|
770
|
-
# severity: error
|
|
771
|
-
# action: forbid
|
|
772
|
-
# conditions:
|
|
773
|
-
# path_pattern: "^/v1/.*"
|
|
774
|
-
# message: "V1 API is frozen. Make changes in V2."
|
|
775
|
-
`;
|
|
776
|
-
fs.writeFileSync(policyFile, template);
|
|
777
|
-
console.log(chalk.green('Created .delimit/policies.yml'));
|
|
778
|
-
console.log('');
|
|
836
|
+
console.log(` ${chalk.bold('Presets')}: strict | default | relaxed`);
|
|
837
|
+
console.log(` Switch: ${chalk.bold('delimit init --preset strict')}\n`);
|
|
779
838
|
console.log('Next steps:');
|
|
780
839
|
console.log(` ${chalk.bold('delimit lint')} old.yaml new.yaml — check for breaking changes`);
|
|
781
840
|
console.log(` ${chalk.bold('delimit diff')} old.yaml new.yaml — see all changes`);
|
|
782
841
|
console.log(` ${chalk.bold('delimit explain')} old.yaml new.yaml — human-readable summary`);
|
|
783
842
|
});
|
|
784
843
|
|
|
844
|
+
// Doctor command — verify setup is correct
|
|
845
|
+
program
|
|
846
|
+
.command('doctor')
|
|
847
|
+
.description('Verify Delimit setup and diagnose common issues')
|
|
848
|
+
.action(async () => {
|
|
849
|
+
console.log(chalk.bold('\n Delimit Doctor\n'));
|
|
850
|
+
let ok = 0;
|
|
851
|
+
let warn = 0;
|
|
852
|
+
let fail = 0;
|
|
853
|
+
|
|
854
|
+
// Check policy file
|
|
855
|
+
const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
|
|
856
|
+
if (fs.existsSync(policyPath)) {
|
|
857
|
+
console.log(chalk.green(' ✓ .delimit/policies.yml found'));
|
|
858
|
+
ok++;
|
|
859
|
+
try {
|
|
860
|
+
const yaml = require('js-yaml');
|
|
861
|
+
const policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
|
|
862
|
+
if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
|
|
863
|
+
console.log(chalk.green(' ✓ Policy file is valid YAML'));
|
|
864
|
+
ok++;
|
|
865
|
+
} else {
|
|
866
|
+
console.log(chalk.yellow(' ⚠ Policy file has no rules section'));
|
|
867
|
+
warn++;
|
|
868
|
+
}
|
|
869
|
+
} catch (e) {
|
|
870
|
+
console.log(chalk.red(` ✗ Policy file has invalid YAML: ${e.message}`));
|
|
871
|
+
fail++;
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
console.log(chalk.red(' ✗ No .delimit/policies.yml — run: delimit init'));
|
|
875
|
+
fail++;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Check for OpenAPI spec
|
|
879
|
+
const specPatterns = [
|
|
880
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
881
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
882
|
+
'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
|
|
883
|
+
'spec/openapi.yaml', 'spec/openapi.json',
|
|
884
|
+
'api/openapi.yaml', 'api/openapi.json',
|
|
885
|
+
'contrib/openapi.json',
|
|
886
|
+
];
|
|
887
|
+
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
|
|
888
|
+
if (foundSpecs.length > 0) {
|
|
889
|
+
console.log(chalk.green(` ✓ OpenAPI spec found: ${foundSpecs[0]}`));
|
|
890
|
+
ok++;
|
|
891
|
+
} else {
|
|
892
|
+
// Check for framework (Zero-Spec candidate)
|
|
893
|
+
const pkgJson = path.join(process.cwd(), 'package.json');
|
|
894
|
+
const reqTxt = path.join(process.cwd(), 'requirements.txt');
|
|
895
|
+
if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
|
|
896
|
+
console.log(chalk.yellow(' ⚠ No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project'));
|
|
897
|
+
warn++;
|
|
898
|
+
} else {
|
|
899
|
+
console.log(chalk.red(' ✗ No OpenAPI spec file found'));
|
|
900
|
+
fail++;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Check for GitHub workflow
|
|
905
|
+
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
906
|
+
if (fs.existsSync(workflowDir)) {
|
|
907
|
+
const workflows = fs.readdirSync(workflowDir);
|
|
908
|
+
const hasDelimit = workflows.some(f => {
|
|
909
|
+
try {
|
|
910
|
+
const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
|
|
911
|
+
return content.includes('delimit-ai/delimit') || content.includes('delimit');
|
|
912
|
+
} catch { return false; }
|
|
913
|
+
});
|
|
914
|
+
if (hasDelimit) {
|
|
915
|
+
console.log(chalk.green(' ✓ GitHub Action workflow found'));
|
|
916
|
+
ok++;
|
|
917
|
+
} else {
|
|
918
|
+
console.log(chalk.yellow(' ⚠ No Delimit GitHub Action workflow — run delimit init for setup instructions'));
|
|
919
|
+
warn++;
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
console.log(chalk.yellow(' ⚠ No .github/workflows/ directory'));
|
|
923
|
+
warn++;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Check git
|
|
927
|
+
try {
|
|
928
|
+
const { execSync } = require('child_process');
|
|
929
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
930
|
+
console.log(chalk.green(' ✓ Git repository detected'));
|
|
931
|
+
ok++;
|
|
932
|
+
} catch {
|
|
933
|
+
console.log(chalk.yellow(' ⚠ Not a git repository'));
|
|
934
|
+
warn++;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Summary
|
|
938
|
+
console.log('');
|
|
939
|
+
if (fail === 0 && warn === 0) {
|
|
940
|
+
console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
|
|
941
|
+
} else if (fail === 0) {
|
|
942
|
+
console.log(chalk.yellow.bold(` ${ok} passed, ${warn} warning(s). Setup looks good.\n`));
|
|
943
|
+
} else {
|
|
944
|
+
console.log(chalk.red.bold(` ${ok} passed, ${warn} warning(s), ${fail} error(s). Fix errors above.\n`));
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
785
948
|
// Lint command — diff + policy (primary command)
|
|
786
949
|
// Supports zero-spec mode: `delimit lint` (no args) auto-extracts from FastAPI
|
|
787
950
|
program
|
|
@@ -964,4 +1127,11 @@ program
|
|
|
964
1127
|
}
|
|
965
1128
|
});
|
|
966
1129
|
|
|
1130
|
+
// Hide legacy/internal commands from --help
|
|
1131
|
+
['install', 'mode', 'status', 'policy', 'auth', 'audit',
|
|
1132
|
+
'explain-decision', 'uninstall', 'proxy', 'hook'].forEach(name => {
|
|
1133
|
+
const cmd = program.commands.find(c => c.name() === name);
|
|
1134
|
+
if (cmd) cmd._hidden = true;
|
|
1135
|
+
});
|
|
1136
|
+
|
|
967
1137
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "Prevent breaking API changes before they reach production. Deterministic diff engine + policy enforcement + semver classification.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"delimit": "./bin/delimit-cli.js"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"postinstall": "echo 'Run \"delimit install\" to set up governance'",
|
|
11
11
|
"install": "bash ./hooks/install-hooks.sh install",
|
|
12
|
-
"install-mcp": "bash ./hooks/install-hooks.sh mcp-only",
|
|
12
|
+
"install-mcp": "bash ./hooks/install-hooks.sh mcp-only",
|
|
13
13
|
"test-mcp": "bash ./hooks/install-hooks.sh troubleshoot",
|
|
14
14
|
"fix-mcp": "bash ./hooks/install-hooks.sh fix-mcp",
|
|
15
15
|
"test": "echo 'Governance is context-aware' && exit 0"
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
"url": "https://github.com/delimit-ai/delimit.git"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"commander": "^9.0.0",
|
|
48
47
|
"axios": "^1.0.0",
|
|
49
48
|
"chalk": "^4.1.2",
|
|
50
|
-
"
|
|
49
|
+
"commander": "^12.1.0",
|
|
51
50
|
"express": "^4.18.0",
|
|
51
|
+
"inquirer": "^8.2.0",
|
|
52
52
|
"js-yaml": "^4.1.0",
|
|
53
53
|
"minimatch": "^5.1.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=14.0.0"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|