delimit-cli 4.1.29 → 4.1.31

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 hooks install # Install git pre-commit hook
160
+ npx delimit-cli hooks install --pre-push # Also add pre-push hook
157
161
  npx delimit-cli ci # Generate GitHub Action workflow
158
162
  npx delimit-cli ci --strict --dry-run # Preview strict workflow
159
163
  npx delimit-cli doctor # Check setup health
@@ -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
 
@@ -3040,6 +3044,304 @@ program
3040
3044
  }
3041
3045
  });
3042
3046
 
3047
+ // Hooks command — install/remove git hooks for governance
3048
+ program
3049
+ .command('hooks <action>')
3050
+ .description('Install or remove git hooks (install | remove | status)')
3051
+ .option('--pre-push', 'Also add pre-push hook')
3052
+ .action(async (action, opts) => {
3053
+ const projectDir = process.cwd();
3054
+ const gitDir = path.join(projectDir, '.git');
3055
+
3056
+ if (!fs.existsSync(gitDir)) {
3057
+ console.log(chalk.red('\n Not a git repository. Run git init first.\n'));
3058
+ process.exitCode = 1;
3059
+ return;
3060
+ }
3061
+
3062
+ const hooksDir = path.join(gitDir, 'hooks');
3063
+ fs.mkdirSync(hooksDir, { recursive: true });
3064
+
3065
+ const preCommitPath = path.join(hooksDir, 'pre-commit');
3066
+ const prePushPath = path.join(hooksDir, 'pre-push');
3067
+ const marker = '# delimit-governance-hook';
3068
+
3069
+ const preCommitHook = `#!/bin/sh
3070
+ ${marker}
3071
+ # Delimit API governance gate
3072
+ # Blocks commits with breaking API changes
3073
+ npx delimit-cli check --staged
3074
+ `;
3075
+
3076
+ const prePushHook = `#!/bin/sh
3077
+ ${marker}
3078
+ # Delimit API governance gate
3079
+ # Blocks pushes with breaking API changes
3080
+ npx delimit-cli check --base origin/main
3081
+ `;
3082
+
3083
+ if (action === 'install') {
3084
+ console.log(chalk.bold('\n Delimit Hooks\n'));
3085
+
3086
+ let installed = 0;
3087
+
3088
+ // Pre-commit hook
3089
+ if (fs.existsSync(preCommitPath)) {
3090
+ const existing = fs.readFileSync(preCommitPath, 'utf-8');
3091
+ if (existing.includes(marker)) {
3092
+ console.log(chalk.gray(' pre-commit hook already installed'));
3093
+ } else {
3094
+ // Append to existing hook
3095
+ fs.appendFileSync(preCommitPath, '\n' + preCommitHook.split('\n').slice(1).join('\n'));
3096
+ console.log(chalk.green(' + pre-commit hook added (appended to existing)'));
3097
+ installed++;
3098
+ }
3099
+ } else {
3100
+ fs.writeFileSync(preCommitPath, preCommitHook);
3101
+ fs.chmodSync(preCommitPath, '755');
3102
+ console.log(chalk.green(' + pre-commit hook installed'));
3103
+ installed++;
3104
+ }
3105
+
3106
+ // Pre-push hook (optional)
3107
+ if (opts.prePush) {
3108
+ if (fs.existsSync(prePushPath)) {
3109
+ const existing = fs.readFileSync(prePushPath, 'utf-8');
3110
+ if (existing.includes(marker)) {
3111
+ console.log(chalk.gray(' pre-push hook already installed'));
3112
+ } else {
3113
+ fs.appendFileSync(prePushPath, '\n' + prePushHook.split('\n').slice(1).join('\n'));
3114
+ console.log(chalk.green(' + pre-push hook added (appended to existing)'));
3115
+ installed++;
3116
+ }
3117
+ } else {
3118
+ fs.writeFileSync(prePushPath, prePushHook);
3119
+ fs.chmodSync(prePushPath, '755');
3120
+ console.log(chalk.green(' + pre-push hook installed'));
3121
+ installed++;
3122
+ }
3123
+ }
3124
+
3125
+ if (installed > 0) {
3126
+ console.log(chalk.bold(`\n ${installed} hook(s) installed.`));
3127
+ console.log(chalk.gray(' Commits that introduce breaking API changes will be blocked.'));
3128
+ console.log(chalk.gray(' Override with: git commit --no-verify\n'));
3129
+ } else {
3130
+ console.log(chalk.gray('\n All hooks already installed.\n'));
3131
+ }
3132
+
3133
+ } else if (action === 'remove') {
3134
+ console.log(chalk.bold('\n Delimit Hooks — Remove\n'));
3135
+ let removed = 0;
3136
+
3137
+ for (const [hookPath, name] of [[preCommitPath, 'pre-commit'], [prePushPath, 'pre-push']]) {
3138
+ if (fs.existsSync(hookPath)) {
3139
+ const content = fs.readFileSync(hookPath, 'utf-8');
3140
+ if (content.includes(marker)) {
3141
+ // If the entire hook is ours, remove it
3142
+ const lines = content.split('\n');
3143
+ const delimitStart = lines.findIndex(l => l.includes(marker));
3144
+ if (delimitStart <= 1) {
3145
+ // Whole file is ours
3146
+ fs.unlinkSync(hookPath);
3147
+ console.log(chalk.yellow(` - ${name} hook removed`));
3148
+ } else {
3149
+ // Remove just our section
3150
+ const before = lines.slice(0, delimitStart).join('\n');
3151
+ fs.writeFileSync(hookPath, before + '\n');
3152
+ console.log(chalk.yellow(` - ${name} Delimit section removed`));
3153
+ }
3154
+ removed++;
3155
+ }
3156
+ }
3157
+ }
3158
+ if (removed === 0) {
3159
+ console.log(chalk.gray(' No Delimit hooks found.\n'));
3160
+ } else {
3161
+ console.log(chalk.bold(`\n ${removed} hook(s) removed.\n`));
3162
+ }
3163
+
3164
+ } else if (action === 'status') {
3165
+ console.log(chalk.bold('\n Delimit Hooks — Status\n'));
3166
+ for (const [hookPath, name] of [[preCommitPath, 'pre-commit'], [prePushPath, 'pre-push']]) {
3167
+ if (fs.existsSync(hookPath) && fs.readFileSync(hookPath, 'utf-8').includes(marker)) {
3168
+ console.log(` ${chalk.green('●')} ${name} — installed`);
3169
+ } else {
3170
+ console.log(` ${chalk.gray('○')} ${name} — not installed`);
3171
+ }
3172
+ }
3173
+ console.log('');
3174
+
3175
+ } else {
3176
+ console.log(chalk.red(`\n Unknown action: ${action}`));
3177
+ console.log(chalk.gray(' Usage: delimit hooks install | remove | status\n'));
3178
+ }
3179
+ });
3180
+
3181
+ // Check command — pre-commit/pre-push governance check
3182
+ program
3183
+ .command('check')
3184
+ .description('Run a local governance check on staged or modified API specs')
3185
+ .option('--base <ref>', 'Git ref to compare against (default: HEAD)')
3186
+ .option('--staged', 'Only check staged files')
3187
+ .option('--fix', 'Show migration guidance for violations')
3188
+ .action(async (opts) => {
3189
+ const startTime = Date.now();
3190
+ const projectDir = process.cwd();
3191
+ const configDir = path.join(projectDir, '.delimit');
3192
+ const policyFile = path.join(configDir, 'policies.yml');
3193
+
3194
+ console.log(chalk.bold('\n Delimit Check\n'));
3195
+
3196
+ // Verify governance is initialized
3197
+ if (!fs.existsSync(policyFile)) {
3198
+ console.log(chalk.yellow(' No governance setup found. Run:'));
3199
+ console.log(chalk.bold(' npx delimit-cli init\n'));
3200
+ process.exitCode = 1;
3201
+ return;
3202
+ }
3203
+
3204
+ // Load policy preset
3205
+ let preset = 'default';
3206
+ try {
3207
+ const policyContent = fs.readFileSync(policyFile, 'utf-8');
3208
+ if (policyContent.includes('action: forbid') && !policyContent.includes('action: warn')) preset = 'strict';
3209
+ else if (!policyContent.includes('action: forbid') && policyContent.includes('action: warn')) preset = 'relaxed';
3210
+ } catch {}
3211
+
3212
+ // Find changed spec files via git
3213
+ const base = opts.base || 'HEAD';
3214
+ let changedFiles = [];
3215
+ try {
3216
+ const gitCmd = opts.staged
3217
+ ? 'git diff --cached --name-only'
3218
+ : `git diff --name-only ${base}`;
3219
+ const output = execSync(gitCmd, { cwd: projectDir, encoding: 'utf-8', timeout: 5000 }).trim();
3220
+ if (output) changedFiles = output.split('\n');
3221
+ } catch {
3222
+ // Not a git repo or no changes — fall back to scanning all specs
3223
+ }
3224
+
3225
+ // Filter to spec files
3226
+ const specExtensions = ['.yaml', '.yml', '.json'];
3227
+ const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec', 'api.'];
3228
+ let specFiles = changedFiles.filter(f => {
3229
+ const ext = path.extname(f).toLowerCase();
3230
+ const name = path.basename(f).toLowerCase();
3231
+ if (!specExtensions.includes(ext)) return false;
3232
+ if (specKeywords.some(kw => name.includes(kw))) return true;
3233
+ // Peek inside to confirm it's a spec
3234
+ try {
3235
+ const head = fs.readFileSync(path.join(projectDir, f), 'utf-8').slice(0, 512);
3236
+ return head.includes('"openapi"') || head.includes('openapi:') || head.includes('"swagger"') || head.includes('swagger:');
3237
+ } catch { return false; }
3238
+ });
3239
+
3240
+ // If no changed specs found, scan all known specs
3241
+ if (specFiles.length === 0) {
3242
+ const candidates = [
3243
+ 'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
3244
+ 'openapi.yaml', 'openapi.yml', 'openapi.json',
3245
+ 'swagger.yaml', 'swagger.yml', 'swagger.json',
3246
+ 'spec/api.json', 'spec/openapi.yaml', 'docs/openapi.yaml',
3247
+ ];
3248
+ specFiles = candidates.filter(c => fs.existsSync(path.join(projectDir, c)));
3249
+ }
3250
+
3251
+ if (specFiles.length === 0) {
3252
+ console.log(chalk.gray(' No API spec files found or changed.'));
3253
+ console.log(chalk.gray(' Point at a spec: npx delimit-cli check --base main\n'));
3254
+ return;
3255
+ }
3256
+
3257
+ console.log(chalk.gray(` Policy: ${preset} | Base: ${base} | Specs: ${specFiles.length}\n`));
3258
+
3259
+ let totalBreaking = 0;
3260
+ let totalWarnings = 0;
3261
+ let totalViolations = [];
3262
+ let allPassed = true;
3263
+
3264
+ for (const specFile of specFiles) {
3265
+ const fullPath = path.join(projectDir, specFile);
3266
+
3267
+ // Get the base version from git
3268
+ let baseContent = null;
3269
+ try {
3270
+ baseContent = execSync(`git show ${base}:${specFile}`, {
3271
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
3272
+ });
3273
+ } catch {
3274
+ // File is new — no base version to compare
3275
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.gray('(new file — no base to compare)')}`);
3276
+ continue;
3277
+ }
3278
+
3279
+ // Write base version to temp file for comparison
3280
+ const tmpBase = path.join(os.tmpdir(), `delimit-check-base-${Date.now()}.yaml`);
3281
+ try {
3282
+ fs.writeFileSync(tmpBase, baseContent);
3283
+ const result = apiEngine.lint(tmpBase, fullPath, { policy: preset });
3284
+
3285
+ if (result && result.summary) {
3286
+ const breaking = result.summary.breaking || result.summary.breaking_changes || 0;
3287
+ const warnings = result.summary.warnings || 0;
3288
+ const violations = result.violations || [];
3289
+
3290
+ totalBreaking += breaking;
3291
+ totalWarnings += warnings;
3292
+ totalViolations.push(...violations);
3293
+
3294
+ if (breaking > 0) {
3295
+ allPassed = false;
3296
+ console.log(` ${chalk.red('X')} ${specFile} ${chalk.red(`— ${breaking} breaking, ${warnings} warning(s)`)}`);
3297
+ if (opts.fix) {
3298
+ for (const v of violations) {
3299
+ const icon = v.severity === 'error' ? chalk.red(' BLOCK') : chalk.yellow(' WARN ');
3300
+ console.log(` ${icon} ${v.message}`);
3301
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
3302
+ }
3303
+ }
3304
+ } else if (warnings > 0) {
3305
+ console.log(` ${chalk.yellow('~')} ${specFile} ${chalk.yellow(`— ${warnings} warning(s)`)}`);
3306
+ } else {
3307
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3308
+ }
3309
+
3310
+ // Show semver bump
3311
+ if (result.semver && result.semver.bump && result.semver.bump !== 'none') {
3312
+ const bump = result.semver.bump.toUpperCase();
3313
+ const bumpColor = bump === 'MAJOR' ? chalk.red : bump === 'MINOR' ? chalk.yellow : chalk.green;
3314
+ console.log(` ${chalk.gray('Semver:')} ${bumpColor(bump)}`);
3315
+ }
3316
+ } else {
3317
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3318
+ }
3319
+ } catch (err) {
3320
+ console.log(` ${chalk.green('+')} ${specFile} ${chalk.green('— clean')}`);
3321
+ } finally {
3322
+ try { fs.unlinkSync(tmpBase); } catch {}
3323
+ }
3324
+ }
3325
+
3326
+ // Summary
3327
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3328
+ console.log('');
3329
+ if (totalBreaking > 0) {
3330
+ console.log(chalk.red.bold(` BLOCKED — ${totalBreaking} breaking change(s) across ${specFiles.length} spec(s)`));
3331
+ if (!opts.fix) {
3332
+ console.log(chalk.gray(' Run with --fix to see migration guidance'));
3333
+ }
3334
+ console.log(chalk.gray(` ${elapsed}s\n`));
3335
+ process.exitCode = 1;
3336
+ } else if (totalWarnings > 0) {
3337
+ console.log(chalk.yellow.bold(` PASSED with ${totalWarnings} warning(s)`));
3338
+ console.log(chalk.gray(` ${elapsed}s\n`));
3339
+ } else {
3340
+ console.log(chalk.green.bold(' PASSED — no breaking changes'));
3341
+ console.log(chalk.gray(` ${elapsed}s\n`));
3342
+ }
3343
+ });
3344
+
3043
3345
  // CI command — generate GitHub Action workflow
3044
3346
  program
3045
3347
  .command('ci')
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.29",
4
+ "version": "4.1.31",
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": [