delimit-cli 2.1.1 → 2.2.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.
@@ -783,20 +783,66 @@ rules: []
783
783
  });
784
784
 
785
785
  // Lint command — diff + policy (primary command)
786
+ // Supports zero-spec mode: `delimit lint` (no args) auto-extracts from FastAPI
786
787
  program
787
- .command('lint <old_spec> <new_spec>')
788
+ .command('lint [old_spec] [new_spec]')
788
789
  .description('Lint API specs for breaking changes and policy violations')
789
790
  .option('-p, --policy <file>', 'Custom policy file')
790
791
  .option('--current-version <ver>', 'Current API version for semver bump')
791
792
  .option('-n, --name <name>', 'API name for context')
792
793
  .option('--json', 'Output raw JSON')
794
+ .option('-d, --dir <path>', 'Project directory for zero-spec mode', '.')
793
795
  .action(async (oldSpec, newSpec, options) => {
794
796
  try {
795
- const result = apiEngine.lint(
796
- path.resolve(oldSpec),
797
- path.resolve(newSpec),
798
- { policy: options.policy, version: options.currentVersion, name: options.name }
799
- );
797
+ let result;
798
+
799
+ if (!oldSpec || !newSpec) {
800
+ // Zero-spec mode: extract from framework source
801
+ console.log(chalk.gray('No spec files provided — detecting framework...'));
802
+ const zeroResult = apiEngine.zeroSpec(path.resolve(options.dir));
803
+
804
+ if (!zeroResult.success) {
805
+ console.error(chalk.red(`\n ${zeroResult.error}\n`));
806
+ if (zeroResult.error_type === 'no_framework') {
807
+ console.log(' Usage: delimit lint <old_spec> <new_spec>');
808
+ console.log(' Or run from a FastAPI project directory.\n');
809
+ }
810
+ process.exit(1);
811
+ return;
812
+ }
813
+
814
+ console.log(chalk.green(` ${zeroResult.message}`));
815
+ console.log(` Extracted: ${zeroResult.paths_count} paths, ${zeroResult.schemas_count} schemas`);
816
+ console.log(` Spec: ${zeroResult.spec_path}\n`);
817
+
818
+ // Check for baseline
819
+ const baselineDir = path.join(path.resolve(options.dir), '.delimit');
820
+ const baselinePath = path.join(baselineDir, 'baseline.yaml');
821
+
822
+ if (!fs.existsSync(baselinePath)) {
823
+ // First run: save baseline
824
+ fs.mkdirSync(baselineDir, { recursive: true });
825
+ const yaml = require('js-yaml');
826
+ fs.writeFileSync(baselinePath, yaml.dump(zeroResult.spec));
827
+ console.log(chalk.green(' Saved baseline to .delimit/baseline.yaml'));
828
+ console.log(' Run again after making changes to see the diff.\n');
829
+ process.exit(0);
830
+ return;
831
+ }
832
+
833
+ // Compare against baseline
834
+ result = apiEngine.lint(
835
+ baselinePath,
836
+ zeroResult.spec_path,
837
+ { policy: options.policy, version: options.currentVersion, name: options.name }
838
+ );
839
+ } else {
840
+ result = apiEngine.lint(
841
+ path.resolve(oldSpec),
842
+ path.resolve(newSpec),
843
+ { policy: options.policy, version: options.currentVersion, name: options.name }
844
+ );
845
+ }
800
846
 
801
847
  if (options.json) {
802
848
  console.log(JSON.stringify(result, null, 2));
package/lib/api-engine.js CHANGED
@@ -153,4 +153,42 @@ function semver(oldSpec, newSpec, currentVersion) {
153
153
  ].join('\n'));
154
154
  }
155
155
 
156
- module.exports = { lint, diff, explain, semver, GATEWAY_ROOT };
156
+ /**
157
+ * delimit zero-spec — extract OpenAPI from framework source code
158
+ */
159
+ function zeroSpec(projectDir, opts = {}) {
160
+ const args = [];
161
+ if (opts.pythonBin) args.push(`python_bin=${pyStr(opts.pythonBin)}`);
162
+
163
+ return runGateway([
164
+ 'import json, sys',
165
+ 'sys.path.insert(0, ".")',
166
+ 'from core.zero_spec.detector import detect_framework, Framework',
167
+ 'from core.zero_spec.fastapi_extractor import extract_fastapi_spec',
168
+ 'from core.zero_spec.nestjs_extractor import extract_nestjs_spec',
169
+ 'from core.zero_spec.express_extractor import extract_express_spec',
170
+ `info = detect_framework(${pyStr(projectDir)})`,
171
+ 'r = {"framework": info.framework.value, "confidence": info.confidence, "message": info.message}',
172
+ 'if info.framework == Framework.FASTAPI:',
173
+ ` ext = extract_fastapi_spec(info, ${pyStr(projectDir)}${opts.pythonBin ? `, python_bin=${pyStr(opts.pythonBin)}` : ''})`,
174
+ ' r.update(ext)',
175
+ ' if ext.get("success") and info.app_locations:',
176
+ ' r["app_file"] = info.app_locations[0].file',
177
+ 'elif info.framework == Framework.NESTJS:',
178
+ ` ext = extract_nestjs_spec(info, ${pyStr(projectDir)})`,
179
+ ' r.update(ext)',
180
+ ' if ext.get("success") and info.app_locations:',
181
+ ' r["app_file"] = info.app_locations[0].file',
182
+ 'elif info.framework == Framework.EXPRESS:',
183
+ ` ext = extract_express_spec(info, ${pyStr(projectDir)})`,
184
+ ' r.update(ext)',
185
+ ' if ext.get("success") and info.app_locations:',
186
+ ' r["app_file"] = info.app_locations[0].file',
187
+ 'else:',
188
+ ' r["success"] = False',
189
+ ' r["error"] = "No supported API framework detected"',
190
+ 'print(json.dumps(r, default=str))',
191
+ ].join('\n'));
192
+ }
193
+
194
+ module.exports = { lint, diff, explain, semver, zeroSpec, GATEWAY_ROOT };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "ESLint for API contracts — detect breaking changes, enforce semver, and generate migration guides for OpenAPI specs",
5
5
  "main": "index.js",
6
6
  "bin": {