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.
- package/bin/delimit-cli.js +52 -6
- package/lib/api-engine.js +39 -1
- package/package.json +1 -1
package/bin/delimit-cli.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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