micro-contracts 0.9.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/LICENSE +22 -0
- package/README.md +351 -0
- package/dist/cli/templates.d.ts +16 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +377 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +978 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/dependencyGenerator.d.ts +43 -0
- package/dist/generator/dependencyGenerator.d.ts.map +1 -0
- package/dist/generator/dependencyGenerator.js +159 -0
- package/dist/generator/dependencyGenerator.js.map +1 -0
- package/dist/generator/domainGenerator.d.ts +16 -0
- package/dist/generator/domainGenerator.d.ts.map +1 -0
- package/dist/generator/domainGenerator.js +212 -0
- package/dist/generator/domainGenerator.js.map +1 -0
- package/dist/generator/index.d.ts +37 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +747 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/linter.d.ts +24 -0
- package/dist/generator/linter.d.ts.map +1 -0
- package/dist/generator/linter.js +202 -0
- package/dist/generator/linter.js.map +1 -0
- package/dist/generator/overlayProcessor.d.ts +90 -0
- package/dist/generator/overlayProcessor.d.ts.map +1 -0
- package/dist/generator/overlayProcessor.js +532 -0
- package/dist/generator/overlayProcessor.js.map +1 -0
- package/dist/generator/schemaGenerator.d.ts +10 -0
- package/dist/generator/schemaGenerator.d.ts.map +1 -0
- package/dist/generator/schemaGenerator.js +299 -0
- package/dist/generator/schemaGenerator.js.map +1 -0
- package/dist/generator/templateProcessor.d.ts +178 -0
- package/dist/generator/templateProcessor.d.ts.map +1 -0
- package/dist/generator/templateProcessor.js +607 -0
- package/dist/generator/templateProcessor.js.map +1 -0
- package/dist/generator/typeGenerator.d.ts +9 -0
- package/dist/generator/typeGenerator.d.ts.map +1 -0
- package/dist/generator/typeGenerator.js +395 -0
- package/dist/generator/typeGenerator.js.map +1 -0
- package/dist/guardrails/allowlist.d.ts +45 -0
- package/dist/guardrails/allowlist.d.ts.map +1 -0
- package/dist/guardrails/allowlist.js +261 -0
- package/dist/guardrails/allowlist.js.map +1 -0
- package/dist/guardrails/config.d.ts +40 -0
- package/dist/guardrails/config.d.ts.map +1 -0
- package/dist/guardrails/config.js +174 -0
- package/dist/guardrails/config.js.map +1 -0
- package/dist/guardrails/docs.d.ts +24 -0
- package/dist/guardrails/docs.d.ts.map +1 -0
- package/dist/guardrails/docs.js +138 -0
- package/dist/guardrails/docs.js.map +1 -0
- package/dist/guardrails/drift.d.ts +23 -0
- package/dist/guardrails/drift.d.ts.map +1 -0
- package/dist/guardrails/drift.js +127 -0
- package/dist/guardrails/drift.js.map +1 -0
- package/dist/guardrails/index.d.ts +19 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +25 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/lint.d.ts +20 -0
- package/dist/guardrails/lint.d.ts.map +1 -0
- package/dist/guardrails/lint.js +274 -0
- package/dist/guardrails/lint.js.map +1 -0
- package/dist/guardrails/manifest.d.ts +43 -0
- package/dist/guardrails/manifest.d.ts.map +1 -0
- package/dist/guardrails/manifest.js +231 -0
- package/dist/guardrails/manifest.js.map +1 -0
- package/dist/guardrails/runner.d.ts +31 -0
- package/dist/guardrails/runner.d.ts.map +1 -0
- package/dist/guardrails/runner.js +268 -0
- package/dist/guardrails/runner.js.map +1 -0
- package/dist/guardrails/security.d.ts +31 -0
- package/dist/guardrails/security.d.ts.map +1 -0
- package/dist/guardrails/security.js +181 -0
- package/dist/guardrails/security.js.map +1 -0
- package/dist/guardrails/typecheck.d.ts +15 -0
- package/dist/guardrails/typecheck.d.ts.map +1 -0
- package/dist/guardrails/typecheck.js +104 -0
- package/dist/guardrails/typecheck.js.map +1 -0
- package/dist/guardrails/types.d.ts +196 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +8 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +489 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +297 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.svg +226 -0
- package/docs/development-guardrails.md +541 -0
- package/docs/guardrails-concept.svg +252 -0
- package/docs/overlays-deep-dive.md +298 -0
- package/package.json +66 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* micro-contracts CLI
|
|
4
|
+
*
|
|
5
|
+
* A contract-first OpenAPI toolchain that keeps TypeScript UI
|
|
6
|
+
* and microservices aligned via code generation.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import yaml from 'yaml';
|
|
12
|
+
import { generate, loadConfig, loadOpenAPISpec, lintSpec, formatLintResults } from './generator/index.js';
|
|
13
|
+
import { getStarterTemplates } from './cli/templates.js';
|
|
14
|
+
import { runAllChecks, formatCheckResults, getAvailableChecks, createGuardrailsConfig, generateManifest, writeManifest, GATE_DESCRIPTIONS, } from './guardrails/index.js';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('micro-contracts')
|
|
18
|
+
.description('Contract-first OpenAPI toolchain for TypeScript')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
// Generate command
|
|
21
|
+
program
|
|
22
|
+
.command('generate')
|
|
23
|
+
.description('Generate code from OpenAPI specification')
|
|
24
|
+
.option('-c, --config <path>', 'Path to config file (micro-contracts.config.yaml)')
|
|
25
|
+
.option('-m, --module <names>', 'Module names, comma-separated (default: all)')
|
|
26
|
+
.option('--contracts-only', 'Generate contract packages only')
|
|
27
|
+
.option('--server-only', 'Generate server routes only')
|
|
28
|
+
.option('--frontend-only', 'Generate frontend clients only')
|
|
29
|
+
.option('--docs-only', 'Generate documentation only')
|
|
30
|
+
.option('--skip-lint', 'Skip linting before generation')
|
|
31
|
+
.option('--manifest', 'Generate manifest for guardrails')
|
|
32
|
+
.option('--manifest-dir <path>', 'Directory for manifest (default: packages/)')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
let config;
|
|
36
|
+
// Load from config file
|
|
37
|
+
const configPath = options.config
|
|
38
|
+
? path.resolve(options.config)
|
|
39
|
+
: findConfigFile();
|
|
40
|
+
if (!configPath) {
|
|
41
|
+
console.error('Error: No config file found.');
|
|
42
|
+
console.error('Create micro-contracts.config.yaml or use --config <path>');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (!fs.existsSync(configPath)) {
|
|
46
|
+
console.error(`Config file not found: ${configPath}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
console.log(`Using config: ${configPath}`);
|
|
50
|
+
config = loadConfig(configPath);
|
|
51
|
+
// Run generation
|
|
52
|
+
await generate(config, {
|
|
53
|
+
contractsOnly: options.contractsOnly,
|
|
54
|
+
serverOnly: options.serverOnly,
|
|
55
|
+
frontendOnly: options.frontendOnly,
|
|
56
|
+
docsOnly: options.docsOnly,
|
|
57
|
+
skipLint: options.skipLint,
|
|
58
|
+
modules: options.module,
|
|
59
|
+
});
|
|
60
|
+
// Generate manifest if requested
|
|
61
|
+
if (options.manifest) {
|
|
62
|
+
const manifestDir = options.manifestDir || 'packages/';
|
|
63
|
+
if (fs.existsSync(manifestDir)) {
|
|
64
|
+
console.log(`\nGenerating manifest for: ${manifestDir}`);
|
|
65
|
+
const manifest = await generateManifest(manifestDir, {
|
|
66
|
+
generatorVersion: '1.0.0',
|
|
67
|
+
});
|
|
68
|
+
const manifestPath = writeManifest(manifest, manifestDir);
|
|
69
|
+
const fileCount = Object.keys(manifest.files).length;
|
|
70
|
+
console.log(`Written: ${manifestPath} (${fileCount} files)`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('Generation failed:', error instanceof Error ? error.message : error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Lint command
|
|
80
|
+
program
|
|
81
|
+
.command('lint')
|
|
82
|
+
.description('Lint OpenAPI specification for x-public/x-private violations')
|
|
83
|
+
.argument('<input>', 'Path to OpenAPI spec file')
|
|
84
|
+
.option('--strict', 'Treat warnings as errors')
|
|
85
|
+
.action(async (input, options) => {
|
|
86
|
+
try {
|
|
87
|
+
const specPath = path.resolve(input);
|
|
88
|
+
if (!fs.existsSync(specPath)) {
|
|
89
|
+
console.error(`OpenAPI spec not found: ${specPath}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
console.log(`Linting: ${specPath}\n`);
|
|
93
|
+
const spec = loadOpenAPISpec(specPath);
|
|
94
|
+
const result = lintSpec(spec, { strict: options.strict });
|
|
95
|
+
console.log(formatLintResults(result));
|
|
96
|
+
if (!result.valid) {
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error('Lint failed:', error instanceof Error ? error.message : error);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Init command
|
|
106
|
+
program
|
|
107
|
+
.command('init')
|
|
108
|
+
.description('Initialize a new module structure with starter templates')
|
|
109
|
+
.argument('<name>', 'Module name (e.g., core, users)')
|
|
110
|
+
.option('-d, --dir <path>', 'Base directory', 'src')
|
|
111
|
+
.option('-i, --openapi <path>', 'OpenAPI spec to process (auto-adds x-micro-contracts-domain/method)')
|
|
112
|
+
.option('-o, --output <path>', 'Output path for processed OpenAPI')
|
|
113
|
+
.option('--skip-templates', 'Skip creating starter templates')
|
|
114
|
+
.action(async (name, options) => {
|
|
115
|
+
console.log(`Initializing module "${name}"...\n`);
|
|
116
|
+
// Create spec directory structure
|
|
117
|
+
const specDirs = [
|
|
118
|
+
'spec',
|
|
119
|
+
'spec/default/templates',
|
|
120
|
+
'spec/_shared/openapi',
|
|
121
|
+
'spec/_shared/overlays',
|
|
122
|
+
`spec/${name}/openapi`,
|
|
123
|
+
];
|
|
124
|
+
for (const dir of specDirs) {
|
|
125
|
+
if (!fs.existsSync(dir)) {
|
|
126
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
127
|
+
console.log(`Created: ${dir}/`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Create starter templates (unless skipped)
|
|
131
|
+
if (!options.skipTemplates) {
|
|
132
|
+
const starterTemplates = getStarterTemplates();
|
|
133
|
+
for (const [filename, content] of Object.entries(starterTemplates)) {
|
|
134
|
+
const templatePath = path.join('spec/default/templates', filename);
|
|
135
|
+
if (!fs.existsSync(templatePath)) {
|
|
136
|
+
fs.writeFileSync(templatePath, content);
|
|
137
|
+
console.log(`Created: ${templatePath}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Create shared schemas
|
|
142
|
+
const problemDetailsPath = 'spec/_shared/openapi/problem-details.yaml';
|
|
143
|
+
if (!fs.existsSync(problemDetailsPath)) {
|
|
144
|
+
fs.writeFileSync(problemDetailsPath, generateProblemDetailsSchema());
|
|
145
|
+
console.log(`Created: ${problemDetailsPath}`);
|
|
146
|
+
}
|
|
147
|
+
// Create Spectral rules
|
|
148
|
+
const spectralPath = 'spec/spectral.yaml';
|
|
149
|
+
if (!fs.existsSync(spectralPath)) {
|
|
150
|
+
fs.writeFileSync(spectralPath, generateSpectralRules());
|
|
151
|
+
console.log(`Created: ${spectralPath}`);
|
|
152
|
+
}
|
|
153
|
+
// Create server/frontend directories
|
|
154
|
+
const baseDir = path.resolve(options.dir, name);
|
|
155
|
+
const dirs = [
|
|
156
|
+
baseDir,
|
|
157
|
+
path.join(baseDir, 'domains'),
|
|
158
|
+
path.join(baseDir, 'models'),
|
|
159
|
+
];
|
|
160
|
+
for (const dir of dirs) {
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
+
console.log(`Created: ${dir}/`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Create placeholder files
|
|
167
|
+
const files = [
|
|
168
|
+
[path.join(baseDir, 'db.ts'), generateDbTemplate()],
|
|
169
|
+
[path.join(baseDir, 'container.ts'), generateContainerTemplate(name)],
|
|
170
|
+
[path.join(baseDir, 'domains', 'index.ts'), '// Export domain classes\n'],
|
|
171
|
+
[path.join(baseDir, 'models', 'index.ts'), '// Export models\n'],
|
|
172
|
+
];
|
|
173
|
+
for (const [filePath, content] of files) {
|
|
174
|
+
if (!fs.existsSync(filePath)) {
|
|
175
|
+
fs.writeFileSync(filePath, content);
|
|
176
|
+
console.log(`Created: ${filePath}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Create config template if not exists
|
|
180
|
+
const configPath = path.resolve('micro-contracts.config.yaml');
|
|
181
|
+
if (!fs.existsSync(configPath)) {
|
|
182
|
+
fs.writeFileSync(configPath, generateConfigTemplate(name));
|
|
183
|
+
console.log(`Created: ${configPath}`);
|
|
184
|
+
}
|
|
185
|
+
// Process OpenAPI file if provided
|
|
186
|
+
if (options.openapi) {
|
|
187
|
+
const openapiPath = path.resolve(options.openapi);
|
|
188
|
+
if (!fs.existsSync(openapiPath)) {
|
|
189
|
+
console.error(`OpenAPI file not found: ${openapiPath}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const outputPath = options.output
|
|
193
|
+
? path.resolve(options.output)
|
|
194
|
+
: path.resolve(`spec/${name}/openapi/${name}.yaml`);
|
|
195
|
+
// Ensure output directory exists
|
|
196
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
197
|
+
console.log(`\nProcessing OpenAPI: ${openapiPath}`);
|
|
198
|
+
const processed = processOpenAPIWithExtensions(openapiPath);
|
|
199
|
+
fs.writeFileSync(outputPath, processed.yaml);
|
|
200
|
+
console.log(`Created: ${outputPath}`);
|
|
201
|
+
console.log(` - Added x-micro-contracts-domain to ${processed.stats.domainsAdded} operations`);
|
|
202
|
+
console.log(` - Added x-micro-contracts-method to ${processed.stats.methodsAdded} operations`);
|
|
203
|
+
if (processed.stats.domains.length > 0) {
|
|
204
|
+
console.log(` - Detected domains: ${processed.stats.domains.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log(`\nModule "${name}" initialized!`);
|
|
208
|
+
if (!options.openapi) {
|
|
209
|
+
console.log(`\nNext steps:`);
|
|
210
|
+
console.log(` 1. Create spec/${name}/openapi/${name}.yaml with your API spec`);
|
|
211
|
+
console.log(` 2. Add x-micro-contracts-domain and x-micro-contracts-method to operations`);
|
|
212
|
+
console.log(` 3. Run: npx micro-contracts generate`);
|
|
213
|
+
console.log(`\nTip: Use --openapi to auto-add extensions:`);
|
|
214
|
+
console.log(` npx micro-contracts init ${name} --openapi path/to/spec.yaml`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.log(`\nNext steps:`);
|
|
218
|
+
console.log(` 1. Review the generated extensions in spec/${name}/openapi/${name}.yaml`);
|
|
219
|
+
console.log(` 2. Run: npx micro-contracts generate`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// Deps command (dependency analysis)
|
|
223
|
+
program
|
|
224
|
+
.command('deps')
|
|
225
|
+
.description('Analyze module dependencies')
|
|
226
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
227
|
+
.option('-m, --module <name>', 'Module to analyze')
|
|
228
|
+
.option('--graph', 'Output dependency graph in Mermaid format')
|
|
229
|
+
.option('--impact <ref>', 'Analyze impact of changing a specific API (e.g., core.User.getUsers)')
|
|
230
|
+
.option('--who-depends-on <ref>', 'Find modules that depend on a specific API')
|
|
231
|
+
.option('--validate', 'Validate dependencies against OpenAPI declarations')
|
|
232
|
+
.action(async (options) => {
|
|
233
|
+
try {
|
|
234
|
+
const configPath = options.config
|
|
235
|
+
? path.resolve(options.config)
|
|
236
|
+
: findConfigFile();
|
|
237
|
+
if (!configPath) {
|
|
238
|
+
console.error('Error: No config file found.');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const config = loadConfig(configPath);
|
|
242
|
+
if (!config.modules) {
|
|
243
|
+
console.error('Error: Config must have modules defined.');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
// Collect dependencies from all modules
|
|
247
|
+
const moduleDeps = new Map();
|
|
248
|
+
for (const [moduleName, moduleConfig] of Object.entries(config.modules)) {
|
|
249
|
+
if (options.module && moduleName !== options.module)
|
|
250
|
+
continue;
|
|
251
|
+
const openapiPath = path.resolve(path.dirname(configPath), moduleConfig.openapi);
|
|
252
|
+
if (!fs.existsSync(openapiPath)) {
|
|
253
|
+
console.warn(`Warning: OpenAPI spec not found: ${openapiPath}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const spec = loadOpenAPISpec(openapiPath);
|
|
257
|
+
const openApiDeps = spec.info['x-micro-contracts-depend-on'] || [];
|
|
258
|
+
const configDeps = moduleConfig.dependsOn || [];
|
|
259
|
+
moduleDeps.set(moduleName, {
|
|
260
|
+
deps: openApiDeps,
|
|
261
|
+
openApiDeps,
|
|
262
|
+
configDeps,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// Handle different options
|
|
266
|
+
if (options.graph) {
|
|
267
|
+
outputDependencyGraph(moduleDeps);
|
|
268
|
+
}
|
|
269
|
+
else if (options.impact) {
|
|
270
|
+
outputImpactAnalysis(moduleDeps, options.impact);
|
|
271
|
+
}
|
|
272
|
+
else if (options.whoDependsOn) {
|
|
273
|
+
outputWhoDependsOn(moduleDeps, options.whoDependsOn);
|
|
274
|
+
}
|
|
275
|
+
else if (options.validate) {
|
|
276
|
+
validateDependencies(moduleDeps);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Default: show all dependencies
|
|
280
|
+
outputAllDependencies(moduleDeps);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error('Deps analysis failed:', error instanceof Error ? error.message : error);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// Check command (guardrails)
|
|
289
|
+
program
|
|
290
|
+
.command('check')
|
|
291
|
+
.description('Run AI guardrail checks')
|
|
292
|
+
.option('--only <checks>', 'Run only specific checks (comma-separated)')
|
|
293
|
+
.option('--skip <checks>', 'Skip specific checks (comma-separated)')
|
|
294
|
+
.option('--gate <gates>', 'Run checks for specific gates only (comma-separated, 1-5)')
|
|
295
|
+
.option('-v, --verbose', 'Enable verbose output')
|
|
296
|
+
.option('--fix', 'Auto-fix issues where possible')
|
|
297
|
+
.option('-g, --guardrails <path>', 'Path to guardrails.yaml')
|
|
298
|
+
.option('-d, --generated-dir <path>', 'Path to generated files directory', 'packages/')
|
|
299
|
+
.option('--changed-files <path>', 'Path to file containing list of changed files (for CI)')
|
|
300
|
+
.option('--list', 'List available checks')
|
|
301
|
+
.option('--list-gates', 'List available gates')
|
|
302
|
+
.action(async (options) => {
|
|
303
|
+
try {
|
|
304
|
+
// List gates
|
|
305
|
+
if (options.listGates) {
|
|
306
|
+
console.log('\nAvailable gates:\n');
|
|
307
|
+
for (const [gate, description] of Object.entries(GATE_DESCRIPTIONS)) {
|
|
308
|
+
console.log(` Gate ${gate}: ${description}`);
|
|
309
|
+
}
|
|
310
|
+
console.log('\nUsage: micro-contracts check --gate 1,2,3');
|
|
311
|
+
console.log('');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// List checks
|
|
315
|
+
if (options.list) {
|
|
316
|
+
console.log('\nAvailable checks:\n');
|
|
317
|
+
for (const check of getAvailableChecks({ guardrailsPath: options.guardrails })) {
|
|
318
|
+
const gateStr = check.gate !== undefined ? `[G${check.gate}]` : ' ';
|
|
319
|
+
console.log(` ${gateStr} ${check.name.padEnd(20)} - ${check.description}`);
|
|
320
|
+
}
|
|
321
|
+
console.log('');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Parse gate option
|
|
325
|
+
let gates;
|
|
326
|
+
if (options.gate) {
|
|
327
|
+
gates = options.gate.split(',').map((s) => {
|
|
328
|
+
const num = parseInt(s.trim(), 10);
|
|
329
|
+
if (num < 1 || num > 5 || isNaN(num)) {
|
|
330
|
+
throw new Error(`Invalid gate number: ${s}. Must be 1-5.`);
|
|
331
|
+
}
|
|
332
|
+
return num;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// Parse options
|
|
336
|
+
const checkOptions = {
|
|
337
|
+
only: options.only?.split(',').map((s) => s.trim()),
|
|
338
|
+
skip: options.skip?.split(',').map((s) => s.trim()),
|
|
339
|
+
gates,
|
|
340
|
+
verbose: options.verbose,
|
|
341
|
+
fix: options.fix,
|
|
342
|
+
guardrailsPath: options.guardrails,
|
|
343
|
+
generatedDir: options.generatedDir,
|
|
344
|
+
changedFilesPath: options.changedFiles,
|
|
345
|
+
};
|
|
346
|
+
// Run checks
|
|
347
|
+
const summary = await runAllChecks(checkOptions);
|
|
348
|
+
// Output results
|
|
349
|
+
console.log(formatCheckResults(summary, options.verbose, summary.checks));
|
|
350
|
+
// Exit with error if any checks failed
|
|
351
|
+
if (summary.failed > 0) {
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error('Check failed:', error instanceof Error ? error.message : error);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// Guardrails init command
|
|
361
|
+
program
|
|
362
|
+
.command('guardrails-init')
|
|
363
|
+
.description('Create a guardrails.yaml configuration file')
|
|
364
|
+
.option('-o, --output <path>', 'Output path', 'guardrails.yaml')
|
|
365
|
+
.action((options) => {
|
|
366
|
+
try {
|
|
367
|
+
const outputPath = options.output;
|
|
368
|
+
if (fs.existsSync(outputPath)) {
|
|
369
|
+
console.error(`File already exists: ${outputPath}`);
|
|
370
|
+
console.error('Use --output to specify a different path.');
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
createGuardrailsConfig(outputPath);
|
|
374
|
+
console.log(`Created: ${outputPath}`);
|
|
375
|
+
console.log('\nNext steps:');
|
|
376
|
+
console.log(' 1. Review and customize the guardrails configuration');
|
|
377
|
+
console.log(' 2. Run: micro-contracts check');
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.error('Failed to create guardrails config:', error instanceof Error ? error.message : error);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Manifest command
|
|
385
|
+
program
|
|
386
|
+
.command('manifest')
|
|
387
|
+
.description('Generate or verify manifest for generated artifacts')
|
|
388
|
+
.option('-d, --dir <path>', 'Directory to scan', 'packages/')
|
|
389
|
+
.option('--verify', 'Verify existing manifest')
|
|
390
|
+
.option('-o, --output <path>', 'Output manifest path (default: {dir}/.generated-manifest.json)')
|
|
391
|
+
.action(async (options) => {
|
|
392
|
+
try {
|
|
393
|
+
const baseDir = options.dir;
|
|
394
|
+
if (!fs.existsSync(baseDir)) {
|
|
395
|
+
console.error(`Directory not found: ${baseDir}`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
if (options.verify) {
|
|
399
|
+
// Verify mode
|
|
400
|
+
const { verifyManifest, formatManifestResult } = await import('./guardrails/index.js');
|
|
401
|
+
const result = await verifyManifest(baseDir);
|
|
402
|
+
console.log(formatManifestResult(result));
|
|
403
|
+
if (!result.valid) {
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Generate mode
|
|
409
|
+
console.log(`Generating manifest for: ${baseDir}`);
|
|
410
|
+
const manifest = await generateManifest(baseDir, {
|
|
411
|
+
generatorVersion: '1.0.0',
|
|
412
|
+
});
|
|
413
|
+
const manifestPath = writeManifest(manifest, baseDir);
|
|
414
|
+
const fileCount = Object.keys(manifest.files).length;
|
|
415
|
+
console.log(`Written: ${manifestPath} (${fileCount} files)`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.error('Manifest operation failed:', error instanceof Error ? error.message : error);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
program.parse();
|
|
424
|
+
/**
|
|
425
|
+
* Find config file in current directory
|
|
426
|
+
*/
|
|
427
|
+
function findConfigFile() {
|
|
428
|
+
const candidates = [
|
|
429
|
+
'micro-contracts.config.yaml',
|
|
430
|
+
'micro-contracts.config.yml',
|
|
431
|
+
'api-framework.config.yaml', // Legacy name
|
|
432
|
+
'api-framework.config.yml',
|
|
433
|
+
];
|
|
434
|
+
for (const candidate of candidates) {
|
|
435
|
+
const configPath = path.resolve(candidate);
|
|
436
|
+
if (fs.existsSync(configPath)) {
|
|
437
|
+
return configPath;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
function generateDbTemplate() {
|
|
443
|
+
return `/**
|
|
444
|
+
* Database connection for this module
|
|
445
|
+
*/
|
|
446
|
+
|
|
447
|
+
import pg from 'pg';
|
|
448
|
+
import { DBModel, PostgresDriver } from 'litedbmodel';
|
|
449
|
+
|
|
450
|
+
const { Pool } = pg;
|
|
451
|
+
|
|
452
|
+
let pool: pg.Pool | null = null;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get database connection pool
|
|
456
|
+
*/
|
|
457
|
+
export function getPool(): pg.Pool {
|
|
458
|
+
if (!pool) {
|
|
459
|
+
pool = new Pool({
|
|
460
|
+
connectionString: process.env.DATABASE_URL,
|
|
461
|
+
max: 20,
|
|
462
|
+
idleTimeoutMillis: 30000,
|
|
463
|
+
connectionTimeoutMillis: 2000,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return pool;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Initialize database connection
|
|
471
|
+
*/
|
|
472
|
+
export async function initializeDb(): Promise<void> {
|
|
473
|
+
const p = getPool();
|
|
474
|
+
|
|
475
|
+
// Set litedbmodel driver
|
|
476
|
+
DBModel.setDriver(new PostgresDriver(p));
|
|
477
|
+
|
|
478
|
+
// Test connection
|
|
479
|
+
const client = await p.connect();
|
|
480
|
+
try {
|
|
481
|
+
await client.query('SELECT 1');
|
|
482
|
+
} finally {
|
|
483
|
+
client.release();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Close database connection
|
|
489
|
+
*/
|
|
490
|
+
export async function closeDb(): Promise<void> {
|
|
491
|
+
if (pool) {
|
|
492
|
+
await pool.end();
|
|
493
|
+
pool = null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Test database connection
|
|
499
|
+
*/
|
|
500
|
+
export async function testConnection(): Promise<boolean> {
|
|
501
|
+
try {
|
|
502
|
+
const p = getPool();
|
|
503
|
+
const client = await p.connect();
|
|
504
|
+
try {
|
|
505
|
+
await client.query('SELECT 1');
|
|
506
|
+
return true;
|
|
507
|
+
} finally {
|
|
508
|
+
client.release();
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
function generateContainerTemplate(moduleName) {
|
|
517
|
+
const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
|
|
518
|
+
return `/**
|
|
519
|
+
* Module container (Dependency Injection)
|
|
520
|
+
*/
|
|
521
|
+
|
|
522
|
+
import { testConnection, closeDb } from './db.js';
|
|
523
|
+
|
|
524
|
+
// Import domain implementations
|
|
525
|
+
// import { ExampleDomain } from './domains/ExampleDomain.js';
|
|
526
|
+
|
|
527
|
+
// Import domain interfaces from contract
|
|
528
|
+
// import type { ExampleDomainApi } from '@project/contract/${moduleName}/domains';
|
|
529
|
+
|
|
530
|
+
export interface ${pascalName}Domains {
|
|
531
|
+
// example: ExampleDomainApi;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export interface ${pascalName}ModuleContainer {
|
|
535
|
+
domains: ${pascalName}Domains;
|
|
536
|
+
testConnection: () => Promise<boolean>;
|
|
537
|
+
close: () => Promise<void>;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function initialize${pascalName}Module(): Promise<${pascalName}ModuleContainer> {
|
|
541
|
+
const domains: ${pascalName}Domains = {
|
|
542
|
+
// example: new ExampleDomain(),
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
domains,
|
|
547
|
+
testConnection,
|
|
548
|
+
close: closeDb,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Process OpenAPI file and auto-add x-micro-contracts-domain/method extensions
|
|
555
|
+
*/
|
|
556
|
+
function processOpenAPIWithExtensions(openapiPath) {
|
|
557
|
+
const content = fs.readFileSync(openapiPath, 'utf-8');
|
|
558
|
+
const spec = yaml.parse(content);
|
|
559
|
+
const stats = {
|
|
560
|
+
domainsAdded: 0,
|
|
561
|
+
methodsAdded: 0,
|
|
562
|
+
domains: new Set(),
|
|
563
|
+
};
|
|
564
|
+
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
565
|
+
if (spec.paths) {
|
|
566
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
|
|
567
|
+
if (!pathItem || typeof pathItem !== 'object')
|
|
568
|
+
continue;
|
|
569
|
+
// Infer domain from path: /api/users/{id} → User
|
|
570
|
+
const domain = inferDomainFromPath(pathKey);
|
|
571
|
+
for (const method of httpMethods) {
|
|
572
|
+
const operation = pathItem[method];
|
|
573
|
+
if (!operation || typeof operation !== 'object')
|
|
574
|
+
continue;
|
|
575
|
+
const op = operation;
|
|
576
|
+
// Add x-micro-contracts-domain if not present (check both forms)
|
|
577
|
+
if (!op['x-micro-contracts-domain'] && !op['x-domain'] && domain) {
|
|
578
|
+
op['x-micro-contracts-domain'] = domain;
|
|
579
|
+
stats.domainsAdded++;
|
|
580
|
+
stats.domains.add(domain);
|
|
581
|
+
}
|
|
582
|
+
// Add x-micro-contracts-method if not present (check both forms)
|
|
583
|
+
if (!op['x-micro-contracts-method'] && !op['x-method']) {
|
|
584
|
+
// Use operationId if available, otherwise generate
|
|
585
|
+
const methodName = op.operationId
|
|
586
|
+
? String(op.operationId)
|
|
587
|
+
: inferMethodName(method, pathKey);
|
|
588
|
+
op['x-micro-contracts-method'] = methodName;
|
|
589
|
+
stats.methodsAdded++;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Convert back to YAML with proper formatting
|
|
595
|
+
const output = yaml.stringify(spec, {
|
|
596
|
+
indent: 2,
|
|
597
|
+
});
|
|
598
|
+
return {
|
|
599
|
+
yaml: output,
|
|
600
|
+
stats: {
|
|
601
|
+
domainsAdded: stats.domainsAdded,
|
|
602
|
+
methodsAdded: stats.methodsAdded,
|
|
603
|
+
domains: Array.from(stats.domains),
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Infer domain name from API path
|
|
609
|
+
* /api/users → User
|
|
610
|
+
* /api/users/{id} → User
|
|
611
|
+
* /api/user-profiles → UserProfile
|
|
612
|
+
* /api/v1/accounts → Account
|
|
613
|
+
*/
|
|
614
|
+
function inferDomainFromPath(pathKey) {
|
|
615
|
+
// Remove /api prefix and version prefix
|
|
616
|
+
const normalized = pathKey
|
|
617
|
+
.replace(/^\/api\//, '/')
|
|
618
|
+
.replace(/^\/v\d+\//, '/');
|
|
619
|
+
// Get first path segment
|
|
620
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
621
|
+
if (segments.length === 0)
|
|
622
|
+
return null;
|
|
623
|
+
const firstSegment = segments[0];
|
|
624
|
+
// Skip path parameters
|
|
625
|
+
if (firstSegment.startsWith('{'))
|
|
626
|
+
return null;
|
|
627
|
+
// Convert to PascalCase singular
|
|
628
|
+
// users → User
|
|
629
|
+
// user-profiles → UserProfile
|
|
630
|
+
// accounts → Account
|
|
631
|
+
const words = firstSegment
|
|
632
|
+
.replace(/-/g, '_')
|
|
633
|
+
.split('_');
|
|
634
|
+
const pascalCase = words
|
|
635
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
636
|
+
.join('');
|
|
637
|
+
// Remove trailing 's' for plural (simple heuristic)
|
|
638
|
+
if (pascalCase.endsWith('s') && !pascalCase.endsWith('ss')) {
|
|
639
|
+
return pascalCase.slice(0, -1);
|
|
640
|
+
}
|
|
641
|
+
return pascalCase;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Infer method name from HTTP method and path
|
|
645
|
+
* GET /users → getUsers
|
|
646
|
+
* GET /users/{id} → getUserById
|
|
647
|
+
* POST /users → createUser
|
|
648
|
+
* PUT /users/{id} → updateUser
|
|
649
|
+
* DELETE /users/{id} → deleteUser
|
|
650
|
+
*/
|
|
651
|
+
function inferMethodName(httpMethod, pathKey) {
|
|
652
|
+
// Get path segments without parameters
|
|
653
|
+
const segments = pathKey
|
|
654
|
+
.replace(/^\/api\//, '/')
|
|
655
|
+
.replace(/^\/v\d+\//, '/')
|
|
656
|
+
.split('/')
|
|
657
|
+
.filter(Boolean);
|
|
658
|
+
const hasIdParam = segments.some(s => s.startsWith('{'));
|
|
659
|
+
const resourceSegments = segments.filter(s => !s.startsWith('{'));
|
|
660
|
+
// Build resource name from segments
|
|
661
|
+
const resourceName = resourceSegments
|
|
662
|
+
.map((seg, i) => {
|
|
663
|
+
const words = seg.replace(/-/g, '_').split('_');
|
|
664
|
+
return words
|
|
665
|
+
.map((word, j) => {
|
|
666
|
+
// First word of first segment: lowercase for method prefix
|
|
667
|
+
if (i === 0 && j === 0) {
|
|
668
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
669
|
+
}
|
|
670
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
671
|
+
})
|
|
672
|
+
.join('');
|
|
673
|
+
})
|
|
674
|
+
.join('');
|
|
675
|
+
// Make singular for single-resource operations
|
|
676
|
+
const singularName = resourceName.endsWith('s') && !resourceName.endsWith('ss')
|
|
677
|
+
? resourceName.slice(0, -1)
|
|
678
|
+
: resourceName;
|
|
679
|
+
// Map HTTP method to action
|
|
680
|
+
switch (httpMethod.toLowerCase()) {
|
|
681
|
+
case 'get':
|
|
682
|
+
return hasIdParam
|
|
683
|
+
? `get${singularName}ById`
|
|
684
|
+
: `get${resourceName}`;
|
|
685
|
+
case 'post':
|
|
686
|
+
return `create${singularName}`;
|
|
687
|
+
case 'put':
|
|
688
|
+
return `update${singularName}`;
|
|
689
|
+
case 'patch':
|
|
690
|
+
return `patch${singularName}`;
|
|
691
|
+
case 'delete':
|
|
692
|
+
return `delete${singularName}`;
|
|
693
|
+
default:
|
|
694
|
+
return `${httpMethod.toLowerCase()}${resourceName}`;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function generateConfigTemplate(moduleName) {
|
|
698
|
+
// Note: Using explicit string concatenation to ensure correct YAML indentation
|
|
699
|
+
const yaml = [
|
|
700
|
+
'# micro-contracts Configuration',
|
|
701
|
+
'',
|
|
702
|
+
'# Common settings (defaults for all modules)',
|
|
703
|
+
'defaults:',
|
|
704
|
+
' contract:',
|
|
705
|
+
' output: packages/contract/{module}',
|
|
706
|
+
'',
|
|
707
|
+
' contractPublic:',
|
|
708
|
+
' output: packages/contract-published/{module}',
|
|
709
|
+
'',
|
|
710
|
+
' # Template-based outputs',
|
|
711
|
+
' outputs:',
|
|
712
|
+
' server-routes:',
|
|
713
|
+
' output: server/src/{module}/routes.generated.ts',
|
|
714
|
+
' template: fastify-routes.hbs',
|
|
715
|
+
' config:',
|
|
716
|
+
' domainsPath: fastify.domains.{module}',
|
|
717
|
+
'',
|
|
718
|
+
' frontend-api:',
|
|
719
|
+
' output: frontend/src/{module}/api.generated.ts',
|
|
720
|
+
' template: fetch-client.hbs',
|
|
721
|
+
'',
|
|
722
|
+
' shared-client:',
|
|
723
|
+
' output: frontend/src/shared/{module}.api.generated.ts',
|
|
724
|
+
' template: fetch-client.hbs',
|
|
725
|
+
' condition: hasPublicEndpoints',
|
|
726
|
+
' config:',
|
|
727
|
+
' contractPackage: "@project/contract-published/{module}"',
|
|
728
|
+
'',
|
|
729
|
+
' # Overlay configuration',
|
|
730
|
+
' overlays:',
|
|
731
|
+
' shared:',
|
|
732
|
+
' - spec/_shared/overlays/middleware.overlay.yaml',
|
|
733
|
+
' collision: error',
|
|
734
|
+
'',
|
|
735
|
+
' docs:',
|
|
736
|
+
' enabled: true',
|
|
737
|
+
'',
|
|
738
|
+
'# Module definitions',
|
|
739
|
+
'modules:',
|
|
740
|
+
` ${moduleName}:`,
|
|
741
|
+
` openapi: spec/${moduleName}/openapi/${moduleName}.yaml`,
|
|
742
|
+
'',
|
|
743
|
+
].join('\n');
|
|
744
|
+
return yaml;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Generate ProblemDetails schema
|
|
748
|
+
*/
|
|
749
|
+
function generateProblemDetailsSchema() {
|
|
750
|
+
return `# RFC 9457 Problem Details
|
|
751
|
+
# https://www.rfc-editor.org/rfc/rfc9457.html
|
|
752
|
+
|
|
753
|
+
components:
|
|
754
|
+
schemas:
|
|
755
|
+
ProblemDetails:
|
|
756
|
+
type: object
|
|
757
|
+
required: [type, title, status]
|
|
758
|
+
properties:
|
|
759
|
+
type:
|
|
760
|
+
type: string
|
|
761
|
+
format: uri
|
|
762
|
+
description: "Error type URI (e.g., /errors/validation)"
|
|
763
|
+
title:
|
|
764
|
+
type: string
|
|
765
|
+
description: "Short human-readable summary"
|
|
766
|
+
status:
|
|
767
|
+
type: integer
|
|
768
|
+
description: "HTTP status code"
|
|
769
|
+
detail:
|
|
770
|
+
type: string
|
|
771
|
+
description: "Detailed explanation"
|
|
772
|
+
instance:
|
|
773
|
+
type: string
|
|
774
|
+
format: uri
|
|
775
|
+
description: "URI of the specific occurrence"
|
|
776
|
+
code:
|
|
777
|
+
type: string
|
|
778
|
+
description: "Machine-readable error code (SCREAMING_SNAKE)"
|
|
779
|
+
traceId:
|
|
780
|
+
type: string
|
|
781
|
+
description: "Correlation ID for tracing"
|
|
782
|
+
errors:
|
|
783
|
+
type: array
|
|
784
|
+
description: "Validation errors (field-level)"
|
|
785
|
+
items:
|
|
786
|
+
type: object
|
|
787
|
+
properties:
|
|
788
|
+
field:
|
|
789
|
+
type: string
|
|
790
|
+
message:
|
|
791
|
+
type: string
|
|
792
|
+
|
|
793
|
+
responses:
|
|
794
|
+
BadRequest:
|
|
795
|
+
description: Bad Request
|
|
796
|
+
content:
|
|
797
|
+
application/json:
|
|
798
|
+
schema:
|
|
799
|
+
$ref: '#/components/schemas/ProblemDetails'
|
|
800
|
+
|
|
801
|
+
Unauthorized:
|
|
802
|
+
description: Unauthorized
|
|
803
|
+
content:
|
|
804
|
+
application/json:
|
|
805
|
+
schema:
|
|
806
|
+
$ref: '#/components/schemas/ProblemDetails'
|
|
807
|
+
|
|
808
|
+
Forbidden:
|
|
809
|
+
description: Forbidden
|
|
810
|
+
content:
|
|
811
|
+
application/json:
|
|
812
|
+
schema:
|
|
813
|
+
$ref: '#/components/schemas/ProblemDetails'
|
|
814
|
+
|
|
815
|
+
NotFound:
|
|
816
|
+
description: Not Found
|
|
817
|
+
content:
|
|
818
|
+
application/json:
|
|
819
|
+
schema:
|
|
820
|
+
$ref: '#/components/schemas/ProblemDetails'
|
|
821
|
+
|
|
822
|
+
InternalError:
|
|
823
|
+
description: Internal Server Error
|
|
824
|
+
content:
|
|
825
|
+
application/json:
|
|
826
|
+
schema:
|
|
827
|
+
$ref: '#/components/schemas/ProblemDetails'
|
|
828
|
+
`;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Generate Spectral lint rules
|
|
832
|
+
*/
|
|
833
|
+
function generateSpectralRules() {
|
|
834
|
+
return `# micro-contracts Spectral Rules
|
|
835
|
+
|
|
836
|
+
extends: ["spectral:oas"]
|
|
837
|
+
|
|
838
|
+
rules:
|
|
839
|
+
# Require x-micro-contracts-domain on all operations
|
|
840
|
+
operation-domain:
|
|
841
|
+
description: "Operations must have x-micro-contracts-domain"
|
|
842
|
+
severity: error
|
|
843
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
844
|
+
then:
|
|
845
|
+
field: x-micro-contracts-domain
|
|
846
|
+
function: truthy
|
|
847
|
+
|
|
848
|
+
# Require x-micro-contracts-method on all operations
|
|
849
|
+
operation-method:
|
|
850
|
+
description: "Operations must have x-micro-contracts-method"
|
|
851
|
+
severity: error
|
|
852
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
853
|
+
then:
|
|
854
|
+
field: x-micro-contracts-method
|
|
855
|
+
function: truthy
|
|
856
|
+
|
|
857
|
+
# Require error responses
|
|
858
|
+
operation-error-responses:
|
|
859
|
+
description: "Operations should have 5XX or default error response"
|
|
860
|
+
severity: warn
|
|
861
|
+
given: "$.paths[*][get,post,put,patch,delete].responses"
|
|
862
|
+
then:
|
|
863
|
+
function: schema
|
|
864
|
+
functionOptions:
|
|
865
|
+
schema:
|
|
866
|
+
anyOf:
|
|
867
|
+
- required: ["500"]
|
|
868
|
+
- required: ["5XX"]
|
|
869
|
+
- required: ["default"]
|
|
870
|
+
|
|
871
|
+
# Enforce canonical extension names
|
|
872
|
+
canonical-extension-prefix:
|
|
873
|
+
description: "Use canonical x-micro-contracts-* extensions"
|
|
874
|
+
severity: warn
|
|
875
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
876
|
+
then:
|
|
877
|
+
- field: x-domain
|
|
878
|
+
function: falsy
|
|
879
|
+
description: "Use x-micro-contracts-domain instead of x-domain"
|
|
880
|
+
- field: x-method
|
|
881
|
+
function: falsy
|
|
882
|
+
description: "Use x-micro-contracts-method instead of x-method"
|
|
883
|
+
- field: x-public
|
|
884
|
+
function: falsy
|
|
885
|
+
description: "Use x-micro-contracts-published instead of x-public"
|
|
886
|
+
`;
|
|
887
|
+
}
|
|
888
|
+
// =============================================================================
|
|
889
|
+
// Dependency Analysis Helpers
|
|
890
|
+
// =============================================================================
|
|
891
|
+
function outputDependencyGraph(moduleDeps) {
|
|
892
|
+
console.log('```mermaid');
|
|
893
|
+
console.log('graph LR');
|
|
894
|
+
for (const [moduleName, { deps }] of moduleDeps) {
|
|
895
|
+
// Group deps by target module
|
|
896
|
+
const moduleTargets = new Set();
|
|
897
|
+
for (const dep of deps) {
|
|
898
|
+
const parts = dep.split('.');
|
|
899
|
+
if (parts.length >= 1) {
|
|
900
|
+
moduleTargets.add(parts[0]);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const target of moduleTargets) {
|
|
904
|
+
console.log(` ${moduleName} --> ${target}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
console.log('```');
|
|
908
|
+
}
|
|
909
|
+
function outputImpactAnalysis(moduleDeps, ref) {
|
|
910
|
+
console.log(`Impacted by changes to ${ref}:\n`);
|
|
911
|
+
const impacted = [];
|
|
912
|
+
for (const [moduleName, { deps }] of moduleDeps) {
|
|
913
|
+
if (deps.includes(ref)) {
|
|
914
|
+
impacted.push(moduleName);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (impacted.length === 0) {
|
|
918
|
+
console.log(' No modules depend on this API.');
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
for (const m of impacted) {
|
|
922
|
+
console.log(` - ${m}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function outputWhoDependsOn(moduleDeps, ref) {
|
|
927
|
+
console.log(`Modules that depend on ${ref}:\n`);
|
|
928
|
+
const dependent = [];
|
|
929
|
+
for (const [moduleName, { deps }] of moduleDeps) {
|
|
930
|
+
if (deps.some(d => d.startsWith(ref))) {
|
|
931
|
+
dependent.push(moduleName);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (dependent.length === 0) {
|
|
935
|
+
console.log(' None found.');
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
for (const m of dependent) {
|
|
939
|
+
console.log(` - ${m}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function validateDependencies(moduleDeps) {
|
|
944
|
+
let hasErrors = false;
|
|
945
|
+
for (const [moduleName, { openApiDeps, configDeps }] of moduleDeps) {
|
|
946
|
+
// Check that configDeps is subset of openApiDeps
|
|
947
|
+
for (const dep of configDeps) {
|
|
948
|
+
if (!openApiDeps.includes(dep)) {
|
|
949
|
+
console.error(`ERROR: ${moduleName}.dependsOn includes '${dep}'`);
|
|
950
|
+
console.error(` but it's not declared in OpenAPI x-micro-contracts-depend-on`);
|
|
951
|
+
hasErrors = true;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (!hasErrors) {
|
|
956
|
+
console.log('✓ All dependencies are valid');
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function outputAllDependencies(moduleDeps) {
|
|
963
|
+
console.log('Module Dependencies:\n');
|
|
964
|
+
for (const [moduleName, { deps }] of moduleDeps) {
|
|
965
|
+
console.log(`${moduleName}:`);
|
|
966
|
+
if (deps.length === 0) {
|
|
967
|
+
console.log(' (no dependencies)');
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
for (const dep of deps) {
|
|
971
|
+
console.log(` - ${dep}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
console.log();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Starter templates are imported from ./cli/templates.js
|
|
978
|
+
//# sourceMappingURL=cli.js.map
|