delimit-cli 4.1.28 → 4.1.30

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
@@ -154,6 +154,10 @@ npx delimit-cli setup --dry-run # Preview changes first
154
154
  npx delimit-cli lint api/openapi.yaml # Check for breaking changes
155
155
  npx delimit-cli diff old.yaml new.yaml # Compare two specs
156
156
  npx delimit-cli explain old.yaml new.yaml # Generate migration guide
157
+ npx delimit-cli check # Pre-commit governance check
158
+ npx delimit-cli check --staged --fix # Check staged files + show guidance
159
+ npx delimit-cli ci # Generate GitHub Action workflow
160
+ npx delimit-cli ci --strict --dry-run # Preview strict workflow
157
161
  npx delimit-cli doctor # Check setup health
158
162
  npx delimit-cli uninstall --dry-run # Preview removal
159
163
  ```
@@ -1914,6 +1914,7 @@ jobs:
1914
1914
  console.log(chalk.bold(`\n Setup complete in ${elapsed}s`));
1915
1915
  console.log(chalk.gray(` Evidence saved to .delimit/evidence/\n`));
1916
1916
  console.log(' Next steps:');
1917
+ console.log(` ${chalk.bold('npx delimit-cli check')} — pre-commit governance check`);
1917
1918
  if (specPath) {
1918
1919
  console.log(` ${chalk.bold('npx delimit-cli lint')} ${specPath} ${specPath} — lint on every PR`);
1919
1920
  } else if (['fastapi', 'nestjs', 'express'].includes(framework)) {
@@ -1921,6 +1922,9 @@ jobs:
1921
1922
  } else {
1922
1923
  console.log(` ${chalk.bold('npx delimit-cli lint')} — add an OpenAPI spec first`);
1923
1924
  }
1925
+ if (ciProvider === 'none') {
1926
+ console.log(` ${chalk.bold('npx delimit-cli ci')} — generate GitHub Action workflow`);
1927
+ }
1924
1928
  console.log(` ${chalk.bold('delimit doctor')} — verify setup`);
1925
1929
  console.log(` ${chalk.bold('delimit explain')} — human-readable report`);
1926
1930
  if (securityFindings.length > 0) {
@@ -1932,7 +1936,7 @@ jobs:
1932
1936
  if (foundSpecs.length > 1) {
1933
1937
  console.log(` ${chalk.gray('Review all ' + foundSpecs.length + ' specs')} — multiple specs detected`);
1934
1938
  }
1935
- if (ciProvider === 'none') {
1939
+ if (ciProvider === 'none' && !specPath) {
1936
1940
  console.log(` ${chalk.gray('Add CI')} — no CI detected; consider GitHub Actions`);
1937
1941
  }
1938
1942
 
@@ -2437,6 +2441,7 @@ program
2437
2441
  choices: [
2438
2442
  { name: `Lint this spec for breaking changes`, value: 'lint' },
2439
2443
  { name: 'Set up governance for this project', value: 'init' },
2444
+ { name: 'Add CI gate (GitHub Action)', value: 'ci' },
2440
2445
  { name: 'Configure AI assistants (Claude, Codex, Gemini)', value: 'setup' },
2441
2446
  { name: 'Exit', value: 'exit' },
2442
2447
  ],
@@ -2445,6 +2450,8 @@ program
2445
2450
  execSync(`npx delimit-cli lint ${target}`, { stdio: 'inherit' });
2446
2451
  } else if (next === 'init') {
2447
2452
  execSync('npx delimit-cli init', { stdio: 'inherit' });
2453
+ } else if (next === 'ci') {
2454
+ execSync('npx delimit-cli ci', { stdio: 'inherit' });
2448
2455
  } else if (next === 'setup') {
2449
2456
  execSync('npx delimit-cli setup', { stdio: 'inherit' });
2450
2457
  }
@@ -2541,6 +2548,7 @@ program
2541
2548
  message: '\n What next?\n',
2542
2549
  choices: [
2543
2550
  { name: 'Set up governance for this project', value: 'init' },
2551
+ { name: 'Add CI gate (GitHub Action)', value: 'ci' },
2544
2552
  { name: 'Configure AI assistants (Claude, Codex, Gemini)', value: 'setup' },
2545
2553
  { name: 'Run a breaking change demo', value: 'try' },
2546
2554
  { name: 'Exit', value: 'exit' },
@@ -2548,6 +2556,8 @@ program
2548
2556
  }]);
2549
2557
  if (next === 'init') {
2550
2558
  execSync('npx delimit-cli init', { stdio: 'inherit' });
2559
+ } else if (next === 'ci') {
2560
+ execSync('npx delimit-cli ci', { stdio: 'inherit' });
2551
2561
  } else if (next === 'setup') {
2552
2562
  execSync('npx delimit-cli setup', { stdio: 'inherit' });
2553
2563
  } else if (next === 'try') {
@@ -3034,6 +3044,274 @@ program
3034
3044
  }
3035
3045
  });
3036
3046
 
3047
+ // Check command — pre-commit/pre-push governance check
3048
+ program
3049
+ .command('check')
3050
+ .description('Run a local governance check on staged or modified API specs')
3051
+ .option('--base <ref>', 'Git ref to compare against (default: HEAD)')
3052
+ .option('--staged', 'Only check staged files')
3053
+ .option('--fix', 'Show migration guidance for violations')
3054
+ .action(async (opts) => {
3055
+ const startTime = Date.now();
3056
+ const projectDir = process.cwd();
3057
+ const configDir = path.join(projectDir, '.delimit');
3058
+ const policyFile = path.join(configDir, 'policies.yml');
3059
+
3060
+ console.log(chalk.bold('\n Delimit Check\n'));
3061
+
3062
+ // Verify governance is initialized
3063
+ if (!fs.existsSync(policyFile)) {
3064
+ console.log(chalk.yellow(' No governance setup found. Run:'));
3065
+ console.log(chalk.bold(' npx delimit-cli init\n'));
3066
+ process.exitCode = 1;
3067
+ return;
3068
+ }
3069
+
3070
+ // Load policy preset
3071
+ let preset = 'default';
3072
+ try {
3073
+ const policyContent = fs.readFileSync(policyFile, 'utf-8');
3074
+ if (policyContent.includes('action: forbid') && !policyContent.includes('action: warn')) preset = 'strict';
3075
+ else if (!policyContent.includes('action: forbid') && policyContent.includes('action: warn')) preset = 'relaxed';
3076
+ } catch {}
3077
+
3078
+ // Find changed spec files via git
3079
+ const base = opts.base || 'HEAD';
3080
+ let changedFiles = [];
3081
+ try {
3082
+ const gitCmd = opts.staged
3083
+ ? 'git diff --cached --name-only'
3084
+ : `git diff --name-only ${base}`;
3085
+ const output = execSync(gitCmd, { cwd: projectDir, encoding: 'utf-8', timeout: 5000 }).trim();
3086
+ if (output) changedFiles = output.split('\n');
3087
+ } catch {
3088
+ // Not a git repo or no changes — fall back to scanning all specs
3089
+ }
3090
+
3091
+ // Filter to spec files
3092
+ const specExtensions = ['.yaml', '.yml', '.json'];
3093
+ const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec', 'api.'];
3094
+ let specFiles = changedFiles.filter(f => {
3095
+ const ext = path.extname(f).toLowerCase();
3096
+ const name = path.basename(f).toLowerCase();
3097
+ if (!specExtensions.includes(ext)) return false;
3098
+ if (specKeywords.some(kw => name.includes(kw))) return true;
3099
+ // Peek inside to confirm it's a spec
3100
+ try {
3101
+ const head = fs.readFileSync(path.join(projectDir, f), 'utf-8').slice(0, 512);
3102
+ return head.includes('"openapi"') || head.includes('openapi:') || head.includes('"swagger"') || head.includes('swagger:');
3103
+ } catch { return false; }
3104
+ });
3105
+
3106
+ // If no changed specs found, scan all known specs
3107
+ if (specFiles.length === 0) {
3108
+ const candidates = [
3109
+ 'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
3110
+ 'openapi.yaml', 'openapi.yml', 'openapi.json',
3111
+ 'swagger.yaml', 'swagger.yml', 'swagger.json',
3112
+ 'spec/api.json', 'spec/openapi.yaml', 'docs/openapi.yaml',
3113
+ ];
3114
+ specFiles = candidates.filter(c => fs.existsSync(path.join(projectDir, c)));
3115
+ }
3116
+
3117
+ if (specFiles.length === 0) {
3118
+ console.log(chalk.gray(' No API spec files found or changed.'));
3119
+ console.log(chalk.gray(' Point at a spec: npx delimit-cli check --base main\n'));
3120
+ return;
3121
+ }
3122
+
3123
+ console.log(chalk.gray(` Policy: ${preset} | Base: ${base} | Specs: ${specFiles.length}\n`));
3124
+
3125
+ let totalBreaking = 0;
3126
+ let totalWarnings = 0;
3127
+ let totalViolations = [];
3128
+ let allPassed = true;
3129
+
3130
+ for (const specFile of specFiles) {
3131
+ const fullPath = path.join(projectDir, specFile);
3132
+
3133
+ // Get the base version from git
3134
+ let baseContent = null;
3135
+ try {
3136
+ baseContent = execSync(`git show ${base}:${specFile}`, {
3137
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
3138
+ });
3139
+ } catch {
3140
+ // File is new — no base version to compare
3141
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.gray('(new file — no base to compare)')}`);
3142
+ continue;
3143
+ }
3144
+
3145
+ // Write base version to temp file for comparison
3146
+ const tmpBase = path.join(os.tmpdir(), `delimit-check-base-${Date.now()}.yaml`);
3147
+ try {
3148
+ fs.writeFileSync(tmpBase, baseContent);
3149
+ const result = apiEngine.lint(tmpBase, fullPath, { policy: preset });
3150
+
3151
+ if (result && result.summary) {
3152
+ const breaking = result.summary.breaking || result.summary.breaking_changes || 0;
3153
+ const warnings = result.summary.warnings || 0;
3154
+ const violations = result.violations || [];
3155
+
3156
+ totalBreaking += breaking;
3157
+ totalWarnings += warnings;
3158
+ totalViolations.push(...violations);
3159
+
3160
+ if (breaking > 0) {
3161
+ allPassed = false;
3162
+ console.log(` ${chalk.red('X')} ${specFile} ${chalk.red(`— ${breaking} breaking, ${warnings} warning(s)`)}`);
3163
+ if (opts.fix) {
3164
+ for (const v of violations) {
3165
+ const icon = v.severity === 'error' ? chalk.red(' BLOCK') : chalk.yellow(' WARN ');
3166
+ console.log(` ${icon} ${v.message}`);
3167
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
3168
+ }
3169
+ }
3170
+ } else if (warnings > 0) {
3171
+ console.log(` ${chalk.yellow('~')} ${specFile} ${chalk.yellow(`— ${warnings} warning(s)`)}`);
3172
+ } else {
3173
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3174
+ }
3175
+
3176
+ // Show semver bump
3177
+ if (result.semver && result.semver.bump && result.semver.bump !== 'none') {
3178
+ const bump = result.semver.bump.toUpperCase();
3179
+ const bumpColor = bump === 'MAJOR' ? chalk.red : bump === 'MINOR' ? chalk.yellow : chalk.green;
3180
+ console.log(` ${chalk.gray('Semver:')} ${bumpColor(bump)}`);
3181
+ }
3182
+ } else {
3183
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3184
+ }
3185
+ } catch (err) {
3186
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3187
+ } finally {
3188
+ try { fs.unlinkSync(tmpBase); } catch {}
3189
+ }
3190
+ }
3191
+
3192
+ // Summary
3193
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3194
+ console.log('');
3195
+ if (totalBreaking > 0) {
3196
+ console.log(chalk.red.bold(` BLOCKED — ${totalBreaking} breaking change(s) across ${specFiles.length} spec(s)`));
3197
+ if (!opts.fix) {
3198
+ console.log(chalk.gray(' Run with --fix to see migration guidance'));
3199
+ }
3200
+ console.log(chalk.gray(` ${elapsed}s\n`));
3201
+ process.exitCode = 1;
3202
+ } else if (totalWarnings > 0) {
3203
+ console.log(chalk.yellow.bold(` PASSED with ${totalWarnings} warning(s)`));
3204
+ console.log(chalk.gray(` ${elapsed}s\n`));
3205
+ } else {
3206
+ console.log(chalk.green.bold(' PASSED — no breaking changes'));
3207
+ console.log(chalk.gray(` ${elapsed}s\n`));
3208
+ }
3209
+ });
3210
+
3211
+ // CI command — generate GitHub Action workflow
3212
+ program
3213
+ .command('ci')
3214
+ .description('Generate a GitHub Action workflow for API governance on every PR')
3215
+ .option('--spec <path>', 'Path to OpenAPI spec (auto-detected if omitted)')
3216
+ .option('--strict', 'Use strict policy preset')
3217
+ .option('--dry-run', 'Print workflow to stdout instead of writing file')
3218
+ .action(async (opts) => {
3219
+ console.log(chalk.bold('\n Delimit CI Setup\n'));
3220
+
3221
+ // Auto-detect spec
3222
+ let specPath = opts.spec;
3223
+ if (!specPath) {
3224
+ const candidates = [
3225
+ 'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
3226
+ 'openapi.yaml', 'openapi.yml', 'openapi.json',
3227
+ 'api/swagger.yaml', 'api/swagger.yml', 'api/swagger.json',
3228
+ 'swagger.yaml', 'swagger.yml', 'swagger.json',
3229
+ 'docs/openapi.yaml', 'docs/openapi.yml',
3230
+ 'specs/openapi.yaml', 'specs/openapi.yml',
3231
+ ];
3232
+ for (const c of candidates) {
3233
+ if (fs.existsSync(path.join(process.cwd(), c))) {
3234
+ specPath = c;
3235
+ break;
3236
+ }
3237
+ }
3238
+ }
3239
+
3240
+ if (specPath) {
3241
+ console.log(chalk.green(` Found spec: ${specPath}`));
3242
+ } else {
3243
+ console.log(chalk.yellow(' No OpenAPI spec found — using auto-detect in workflow'));
3244
+ }
3245
+
3246
+ const policy = opts.strict ? 'strict' : 'default';
3247
+ const specLine = specPath ? `\n spec: ${specPath}` : '';
3248
+ const policyLine = opts.strict ? `\n policy: strict` : '';
3249
+
3250
+ const workflow = `name: API Governance
3251
+ on:
3252
+ pull_request:
3253
+ paths:
3254
+ - '**/*.yaml'
3255
+ - '**/*.yml'
3256
+ - '**/*.json'
3257
+
3258
+ jobs:
3259
+ delimit:
3260
+ name: Breaking Change Check
3261
+ runs-on: ubuntu-latest
3262
+ permissions:
3263
+ pull-requests: write
3264
+ contents: read
3265
+ steps:
3266
+ - uses: actions/checkout@v4
3267
+ with:
3268
+ fetch-depth: 0
3269
+
3270
+ - uses: delimit-ai/delimit-action@v1
3271
+ with:${specLine}${policyLine}
3272
+ comment: true
3273
+ `;
3274
+
3275
+ if (opts.dryRun) {
3276
+ console.log('');
3277
+ console.log(workflow);
3278
+ return;
3279
+ }
3280
+
3281
+ // Write workflow file
3282
+ const workflowDir = path.join(process.cwd(), '.github', 'workflows');
3283
+ const workflowPath = path.join(workflowDir, 'api-governance.yml');
3284
+
3285
+ if (fs.existsSync(workflowPath)) {
3286
+ const ans = await inquirer.prompt([{
3287
+ type: 'confirm',
3288
+ name: 'overwrite',
3289
+ message: chalk.yellow(' .github/workflows/api-governance.yml already exists. Overwrite?'),
3290
+ default: false,
3291
+ }]);
3292
+ if (!ans.overwrite) {
3293
+ console.log(chalk.gray('\n Skipped. Use --dry-run to preview the workflow.\n'));
3294
+ return;
3295
+ }
3296
+ }
3297
+
3298
+ fs.mkdirSync(workflowDir, { recursive: true });
3299
+ fs.writeFileSync(workflowPath, workflow);
3300
+
3301
+ console.log(chalk.green.bold(`\n Created: .github/workflows/api-governance.yml`));
3302
+ console.log(chalk.gray(` Policy: ${policy}`));
3303
+ console.log('');
3304
+ console.log(chalk.bold(' What happens next:'));
3305
+ console.log(` ${chalk.green('1.')} Open a PR that changes an API spec`);
3306
+ console.log(` ${chalk.green('2.')} Delimit detects breaking changes automatically`);
3307
+ console.log(` ${chalk.green('3.')} PR gets a comment with violations + migration guide`);
3308
+ console.log('');
3309
+ console.log(chalk.bold(' Commit it:'));
3310
+ console.log(chalk.gray(` git add .github/workflows/api-governance.yml`));
3311
+ console.log(chalk.gray(` git commit -m "ci: add API governance gate"`));
3312
+ console.log(chalk.gray(` git push\n`));
3313
+ });
3314
+
3037
3315
  // Lint command — diff + policy (primary command)
3038
3316
  // Supports zero-spec mode: `delimit lint` (no args) auto-extracts from FastAPI
3039
3317
  program
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.28",
4
+ "version": "4.1.30",
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": [