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.
Files changed (99) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -0
  3. package/dist/cli/templates.d.ts +16 -0
  4. package/dist/cli/templates.d.ts.map +1 -0
  5. package/dist/cli/templates.js +377 -0
  6. package/dist/cli/templates.js.map +1 -0
  7. package/dist/cli.d.ts +9 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +978 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/generator/dependencyGenerator.d.ts +43 -0
  12. package/dist/generator/dependencyGenerator.d.ts.map +1 -0
  13. package/dist/generator/dependencyGenerator.js +159 -0
  14. package/dist/generator/dependencyGenerator.js.map +1 -0
  15. package/dist/generator/domainGenerator.d.ts +16 -0
  16. package/dist/generator/domainGenerator.d.ts.map +1 -0
  17. package/dist/generator/domainGenerator.js +212 -0
  18. package/dist/generator/domainGenerator.js.map +1 -0
  19. package/dist/generator/index.d.ts +37 -0
  20. package/dist/generator/index.d.ts.map +1 -0
  21. package/dist/generator/index.js +747 -0
  22. package/dist/generator/index.js.map +1 -0
  23. package/dist/generator/linter.d.ts +24 -0
  24. package/dist/generator/linter.d.ts.map +1 -0
  25. package/dist/generator/linter.js +202 -0
  26. package/dist/generator/linter.js.map +1 -0
  27. package/dist/generator/overlayProcessor.d.ts +90 -0
  28. package/dist/generator/overlayProcessor.d.ts.map +1 -0
  29. package/dist/generator/overlayProcessor.js +532 -0
  30. package/dist/generator/overlayProcessor.js.map +1 -0
  31. package/dist/generator/schemaGenerator.d.ts +10 -0
  32. package/dist/generator/schemaGenerator.d.ts.map +1 -0
  33. package/dist/generator/schemaGenerator.js +299 -0
  34. package/dist/generator/schemaGenerator.js.map +1 -0
  35. package/dist/generator/templateProcessor.d.ts +178 -0
  36. package/dist/generator/templateProcessor.d.ts.map +1 -0
  37. package/dist/generator/templateProcessor.js +607 -0
  38. package/dist/generator/templateProcessor.js.map +1 -0
  39. package/dist/generator/typeGenerator.d.ts +9 -0
  40. package/dist/generator/typeGenerator.d.ts.map +1 -0
  41. package/dist/generator/typeGenerator.js +395 -0
  42. package/dist/generator/typeGenerator.js.map +1 -0
  43. package/dist/guardrails/allowlist.d.ts +45 -0
  44. package/dist/guardrails/allowlist.d.ts.map +1 -0
  45. package/dist/guardrails/allowlist.js +261 -0
  46. package/dist/guardrails/allowlist.js.map +1 -0
  47. package/dist/guardrails/config.d.ts +40 -0
  48. package/dist/guardrails/config.d.ts.map +1 -0
  49. package/dist/guardrails/config.js +174 -0
  50. package/dist/guardrails/config.js.map +1 -0
  51. package/dist/guardrails/docs.d.ts +24 -0
  52. package/dist/guardrails/docs.d.ts.map +1 -0
  53. package/dist/guardrails/docs.js +138 -0
  54. package/dist/guardrails/docs.js.map +1 -0
  55. package/dist/guardrails/drift.d.ts +23 -0
  56. package/dist/guardrails/drift.d.ts.map +1 -0
  57. package/dist/guardrails/drift.js +127 -0
  58. package/dist/guardrails/drift.js.map +1 -0
  59. package/dist/guardrails/index.d.ts +19 -0
  60. package/dist/guardrails/index.d.ts.map +1 -0
  61. package/dist/guardrails/index.js +25 -0
  62. package/dist/guardrails/index.js.map +1 -0
  63. package/dist/guardrails/lint.d.ts +20 -0
  64. package/dist/guardrails/lint.d.ts.map +1 -0
  65. package/dist/guardrails/lint.js +274 -0
  66. package/dist/guardrails/lint.js.map +1 -0
  67. package/dist/guardrails/manifest.d.ts +43 -0
  68. package/dist/guardrails/manifest.d.ts.map +1 -0
  69. package/dist/guardrails/manifest.js +231 -0
  70. package/dist/guardrails/manifest.js.map +1 -0
  71. package/dist/guardrails/runner.d.ts +31 -0
  72. package/dist/guardrails/runner.d.ts.map +1 -0
  73. package/dist/guardrails/runner.js +268 -0
  74. package/dist/guardrails/runner.js.map +1 -0
  75. package/dist/guardrails/security.d.ts +31 -0
  76. package/dist/guardrails/security.d.ts.map +1 -0
  77. package/dist/guardrails/security.js +181 -0
  78. package/dist/guardrails/security.js.map +1 -0
  79. package/dist/guardrails/typecheck.d.ts +15 -0
  80. package/dist/guardrails/typecheck.d.ts.map +1 -0
  81. package/dist/guardrails/typecheck.js +104 -0
  82. package/dist/guardrails/typecheck.js.map +1 -0
  83. package/dist/guardrails/types.d.ts +196 -0
  84. package/dist/guardrails/types.d.ts.map +1 -0
  85. package/dist/guardrails/types.js +8 -0
  86. package/dist/guardrails/types.js.map +1 -0
  87. package/dist/index.d.ts +7 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +7 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/types.d.ts +489 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +297 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/architecture.svg +226 -0
  96. package/docs/development-guardrails.md +541 -0
  97. package/docs/guardrails-concept.svg +252 -0
  98. package/docs/overlays-deep-dive.md +298 -0
  99. 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