micro-contracts 0.15.0 → 0.15.1

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 (92) hide show
  1. package/README.md +62 -0
  2. package/cli-contract.yaml +620 -0
  3. package/dist/agents/context-builder.d.ts +5 -0
  4. package/dist/agents/context-builder.d.ts.map +1 -0
  5. package/dist/agents/context-builder.js +239 -0
  6. package/dist/agents/context-builder.js.map +1 -0
  7. package/dist/agents/formatter.d.ts +9 -0
  8. package/dist/agents/formatter.d.ts.map +1 -0
  9. package/dist/agents/formatter.js +124 -0
  10. package/dist/agents/formatter.js.map +1 -0
  11. package/dist/agents/index.d.ts +5 -0
  12. package/dist/agents/index.d.ts.map +1 -0
  13. package/dist/agents/index.js +3 -0
  14. package/dist/agents/index.js.map +1 -0
  15. package/dist/agents/orchestrator.d.ts +5 -0
  16. package/dist/agents/orchestrator.d.ts.map +1 -0
  17. package/dist/agents/orchestrator.js +105 -0
  18. package/dist/agents/orchestrator.js.map +1 -0
  19. package/dist/agents/types.d.ts +21 -0
  20. package/dist/agents/types.d.ts.map +1 -0
  21. package/dist/agents/types.js +2 -0
  22. package/dist/agents/types.js.map +1 -0
  23. package/dist/cli.d.ts +0 -6
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +814 -1161
  26. package/dist/cli.js.map +1 -1
  27. package/dist/commands/audit-guardrails.d.ts +13 -0
  28. package/dist/commands/audit-guardrails.d.ts.map +1 -0
  29. package/dist/commands/audit-guardrails.js +32 -0
  30. package/dist/commands/audit-guardrails.js.map +1 -0
  31. package/dist/commands/audit-openapi.d.ts +13 -0
  32. package/dist/commands/audit-openapi.d.ts.map +1 -0
  33. package/dist/commands/audit-openapi.js +32 -0
  34. package/dist/commands/audit-openapi.js.map +1 -0
  35. package/dist/commands/propose-overlays.d.ts +13 -0
  36. package/dist/commands/propose-overlays.d.ts.map +1 -0
  37. package/dist/commands/propose-overlays.js +32 -0
  38. package/dist/commands/propose-overlays.js.map +1 -0
  39. package/dist/commands/review-published.d.ts +13 -0
  40. package/dist/commands/review-published.d.ts.map +1 -0
  41. package/dist/commands/review-published.js +32 -0
  42. package/dist/commands/review-published.js.map +1 -0
  43. package/dist/generated/commands.d.ts +2 -0
  44. package/dist/generated/commands.d.ts.map +1 -0
  45. package/dist/generated/commands.js +2 -0
  46. package/dist/generated/commands.js.map +1 -0
  47. package/dist/generated/dsl/agents.d.ts +34 -0
  48. package/dist/generated/dsl/agents.d.ts.map +1 -0
  49. package/dist/generated/dsl/agents.js +98 -0
  50. package/dist/generated/dsl/agents.js.map +1 -0
  51. package/dist/generated/dsl/handoffs.d.ts +1209 -0
  52. package/dist/generated/dsl/handoffs.d.ts.map +1 -0
  53. package/dist/generated/dsl/handoffs.js +205 -0
  54. package/dist/generated/dsl/handoffs.js.map +1 -0
  55. package/dist/generated/dsl/index.d.ts +13 -0
  56. package/dist/generated/dsl/index.d.ts.map +1 -0
  57. package/dist/generated/dsl/index.js +10 -0
  58. package/dist/generated/dsl/index.js.map +1 -0
  59. package/dist/generated/dsl/tasks.d.ts +26 -0
  60. package/dist/generated/dsl/tasks.d.ts.map +1 -0
  61. package/dist/generated/dsl/tasks.js +133 -0
  62. package/dist/generated/dsl/tasks.js.map +1 -0
  63. package/dist/generated/dsl/workflows.d.ts +34 -0
  64. package/dist/generated/dsl/workflows.d.ts.map +1 -0
  65. package/dist/generated/dsl/workflows.js +84 -0
  66. package/dist/generated/dsl/workflows.js.map +1 -0
  67. package/dist/generated/index.d.ts +7 -0
  68. package/dist/generated/index.d.ts.map +1 -0
  69. package/dist/generated/index.js +7 -0
  70. package/dist/generated/index.js.map +1 -0
  71. package/dist/generated/policy-runtime.d.ts +114 -0
  72. package/dist/generated/policy-runtime.d.ts.map +1 -0
  73. package/dist/generated/policy-runtime.js +195 -0
  74. package/dist/generated/policy-runtime.js.map +1 -0
  75. package/dist/generated/policy.d.ts +619 -0
  76. package/dist/generated/policy.d.ts.map +1 -0
  77. package/dist/generated/policy.js +802 -0
  78. package/dist/generated/policy.js.map +1 -0
  79. package/dist/generated/program.d.ts +112 -0
  80. package/dist/generated/program.d.ts.map +1 -0
  81. package/dist/generated/program.js +266 -0
  82. package/dist/generated/program.js.map +1 -0
  83. package/dist/generated/schemas.d.ts +778 -0
  84. package/dist/generated/schemas.d.ts.map +1 -0
  85. package/dist/generated/schemas.js +1005 -0
  86. package/dist/generated/schemas.js.map +1 -0
  87. package/dist/generated/types.d.ts +395 -0
  88. package/dist/generated/types.d.ts.map +1 -0
  89. package/dist/generated/types.js +3 -0
  90. package/dist/generated/types.js.map +1 -0
  91. package/docs/cli-reference.md +300 -0
  92. package/package.json +20 -2
package/dist/cli.js CHANGED
@@ -1,858 +1,811 @@
1
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
2
  import fs from 'fs';
10
3
  import path from 'path';
11
4
  import { createRequire } from 'module';
12
5
  import yaml from 'yaml';
6
+ import { createProgram } from './generated/program.js';
13
7
  import { generate, loadConfig, loadOpenAPISpec, lintSpec, formatLintResults, computeInputHash } from './generator/index.js';
14
8
  import { isMultiModuleConfig } from './types.js';
15
9
  import { getStarterTemplates, getScreenStarterTemplates } from './cli/templates.js';
16
10
  import { runAllChecks, formatCheckResults, formatSingleCheckResult, formatCheckStart, formatCheckSummary, getAvailableChecks, createGuardrailsConfig, generateManifest, writeManifest, canSkipGeneration, GATE_DESCRIPTIONS, loadGuardrailsConfigWithPath, } from './guardrails/index.js';
17
- // Load package.json for version info
11
+ import { commandAuditOpenapi } from './commands/audit-openapi.js';
12
+ import { commandReviewPublished } from './commands/review-published.js';
13
+ import { commandProposeOverlays } from './commands/propose-overlays.js';
14
+ import { commandAuditGuardrails } from './commands/audit-guardrails.js';
18
15
  const require = createRequire(import.meta.url);
19
16
  const pkg = require('../package.json');
20
- const program = new Command();
21
- program
22
- .name('micro-contracts')
23
- .description('Contract-first OpenAPI toolchain for TypeScript')
24
- .version(pkg.version);
25
- // Generate command
26
- program
27
- .command('generate')
28
- .description('Generate code from OpenAPI specification')
29
- .option('-c, --config <path>', 'Path to config file (micro-contracts.config.yaml)')
30
- .option('-m, --module <names>', 'Module names, comma-separated (default: all)')
31
- .option('--contracts-only', 'Generate contract packages only')
32
- .option('--server-only', 'Generate server routes only')
33
- .option('--frontend-only', 'Generate frontend clients only')
34
- .option('--docs-only', 'Generate documentation only')
35
- .option('--skip-lint', 'Skip linting before generation')
36
- .option('--no-manifest', 'Skip manifest generation even if guardrails are configured')
37
- .option('--manifest-dir <path>', 'Directory for manifest (default: packages/)')
38
- .option('--force', 'Bypass input hash cache and always regenerate')
39
- .option('--no-cache', 'Run without reading or writing input hash cache')
40
- .action(async (options) => {
41
- try {
42
- let config;
43
- // Load from config file
44
- const configPath = options.config
45
- ? path.resolve(options.config)
46
- : findConfigFile();
47
- if (!configPath) {
48
- console.error('Error: No config file found.');
49
- console.error('Create micro-contracts.config.yaml or use --config <path>');
50
- process.exit(1);
51
- }
52
- if (!fs.existsSync(configPath)) {
53
- console.error(`Config file not found: ${configPath}`);
54
- process.exit(1);
55
- }
56
- console.log(`Using config: ${configPath}`);
57
- config = loadConfig(configPath);
58
- // Compute input hash for skip logic (multi-module config only)
59
- const useCache = options.cache !== false && !options.force;
60
- let inputHash;
61
- if (isMultiModuleConfig(config)) {
62
- inputHash = computeInputHash(config, configPath, pkg.version);
63
- if (useCache) {
64
- const manifestDir = options.manifestDir || 'packages/';
65
- const skipCheck = await canSkipGeneration(manifestDir, inputHash);
66
- if (skipCheck.skip) {
67
- console.log(`No input changes detected, skipping generation (${skipCheck.reason})`);
68
- return;
69
- }
17
+ function findConfigFile() {
18
+ const candidates = [
19
+ 'micro-contracts.config.yaml',
20
+ 'micro-contracts.config.yml',
21
+ 'api-framework.config.yaml',
22
+ 'api-framework.config.yml',
23
+ ];
24
+ for (const candidate of candidates) {
25
+ const configPath = path.resolve(candidate);
26
+ if (fs.existsSync(configPath))
27
+ return configPath;
28
+ }
29
+ return null;
30
+ }
31
+ const handlers = {
32
+ // ── generate ──────────────────────────────────────────
33
+ generate: async (opts) => {
34
+ try {
35
+ let config;
36
+ const configPath = opts.config
37
+ ? path.resolve(opts.config)
38
+ : findConfigFile();
39
+ if (!configPath) {
40
+ console.error('Error: No config file found.');
41
+ console.error('Create micro-contracts.config.yaml or use --config <path>');
42
+ process.exit(1);
70
43
  }
71
- }
72
- // Run generation
73
- await generate(config, {
74
- contractsOnly: options.contractsOnly,
75
- serverOnly: options.serverOnly,
76
- frontendOnly: options.frontendOnly,
77
- docsOnly: options.docsOnly,
78
- skipLint: options.skipLint,
79
- modules: options.module,
80
- });
81
- // Generate manifest if guardrails config has 'generated' section
82
- // (unless --no-manifest is specified)
83
- if (options.manifest !== false) {
84
- const { config: guardrailsConfig } = loadGuardrailsConfigWithPath();
85
- // Check if guardrails config has generated patterns defined
86
- if (guardrailsConfig?.generated && guardrailsConfig.generated.length > 0) {
87
- const manifestDir = options.manifestDir || 'packages/';
88
- if (fs.existsSync(manifestDir)) {
89
- const manifestInputHash = (options.cache !== false) ? inputHash : undefined;
90
- const { manifest, changed } = await generateManifest(manifestDir, {
91
- generatorVersion: pkg.version,
92
- inputHash: manifestInputHash,
93
- });
94
- const fileCount = Object.keys(manifest.files).length;
95
- if (changed) {
96
- const manifestPath = writeManifest(manifest, manifestDir);
97
- console.log(`\nManifest updated: ${manifestPath} (${fileCount} files)`);
44
+ if (!fs.existsSync(configPath)) {
45
+ console.error(`Config file not found: ${configPath}`);
46
+ process.exit(1);
47
+ }
48
+ console.log(`Using config: ${configPath}`);
49
+ config = loadConfig(configPath);
50
+ const useCache = opts.cache !== false && !opts.force;
51
+ let inputHash;
52
+ if (isMultiModuleConfig(config)) {
53
+ inputHash = computeInputHash(config, configPath, pkg.version);
54
+ if (useCache) {
55
+ const manifestDir = opts.manifestDir || 'packages/';
56
+ const skipCheck = await canSkipGeneration(manifestDir, inputHash);
57
+ if (skipCheck.skip) {
58
+ console.log(`No input changes detected, skipping generation (${skipCheck.reason})`);
59
+ return;
98
60
  }
99
- else {
100
- console.log(`\nManifest unchanged (${fileCount} files)`);
61
+ }
62
+ }
63
+ await generate(config, {
64
+ contractsOnly: opts.contractsOnly,
65
+ serverOnly: opts.serverOnly,
66
+ frontendOnly: opts.frontendOnly,
67
+ docsOnly: opts.docsOnly,
68
+ skipLint: opts.skipLint,
69
+ modules: opts.module,
70
+ });
71
+ if (opts.manifest !== false) {
72
+ const { config: guardrailsConfig } = loadGuardrailsConfigWithPath();
73
+ if (guardrailsConfig?.generated && guardrailsConfig.generated.length > 0) {
74
+ const manifestDir = opts.manifestDir || 'packages/';
75
+ if (fs.existsSync(manifestDir)) {
76
+ const manifestInputHash = (opts.cache !== false) ? inputHash : undefined;
77
+ const { manifest, changed } = await generateManifest(manifestDir, {
78
+ generatorVersion: pkg.version,
79
+ inputHash: manifestInputHash,
80
+ });
81
+ const fileCount = Object.keys(manifest.files).length;
82
+ if (changed) {
83
+ const manifestPath = writeManifest(manifest, manifestDir);
84
+ console.log(`\nManifest updated: ${manifestPath} (${fileCount} files)`);
85
+ }
86
+ else {
87
+ console.log(`\nManifest unchanged (${fileCount} files)`);
88
+ }
101
89
  }
102
90
  }
103
91
  }
104
92
  }
105
- }
106
- catch (error) {
107
- console.error('Generation failed:', error instanceof Error ? error.message : error);
108
- process.exit(1);
109
- }
110
- });
111
- // Lint command
112
- program
113
- .command('lint')
114
- .description('Lint OpenAPI specification for x-public/x-private violations')
115
- .argument('<input>', 'Path to OpenAPI spec file')
116
- .option('--strict', 'Treat warnings as errors')
117
- .action(async (input, options) => {
118
- try {
119
- const specPath = path.resolve(input);
120
- if (!fs.existsSync(specPath)) {
121
- console.error(`OpenAPI spec not found: ${specPath}`);
93
+ catch (error) {
94
+ console.error('Generation failed:', error instanceof Error ? error.message : error);
122
95
  process.exit(1);
123
96
  }
124
- console.log(`Linting: ${specPath}\n`);
125
- const spec = loadOpenAPISpec(specPath);
126
- const result = lintSpec(spec, { strict: options.strict });
127
- console.log(formatLintResults(result));
128
- if (!result.valid) {
129
- process.exit(1);
97
+ },
98
+ // ── lint ───────────────────────────────────────────────
99
+ lint: async (input) => {
100
+ try {
101
+ const specPath = path.resolve(input);
102
+ if (!fs.existsSync(specPath)) {
103
+ console.error(`OpenAPI spec not found: ${specPath}`);
104
+ process.exit(1);
105
+ }
106
+ console.log(`Linting: ${specPath}\n`);
107
+ const spec = loadOpenAPISpec(specPath);
108
+ const result = lintSpec(spec, { strict: false });
109
+ console.log(formatLintResults(result));
110
+ if (!result.valid) {
111
+ process.exit(1);
112
+ }
130
113
  }
131
- }
132
- catch (error) {
133
- console.error('Lint failed:', error instanceof Error ? error.message : error);
134
- process.exit(1);
135
- }
136
- });
137
- // Init command
138
- program
139
- .command('init')
140
- .description('Initialize a new module structure with starter templates')
141
- .argument('<name>', 'Module name (e.g., core, users)')
142
- .option('-d, --dir <path>', 'Base directory', 'src')
143
- .option('-i, --openapi <path>', 'OpenAPI spec to process (auto-adds x-micro-contracts-service/method)')
144
- .option('-o, --output <path>', 'Output path for processed OpenAPI')
145
- .option('--skip-templates', 'Skip creating starter templates')
146
- .option('--screens', 'Initialize as screen spec module (generates screen templates and starter spec)')
147
- .action(async (name, options) => {
148
- console.log(`Initializing module "${name}"${options.screens ? ' (screen spec)' : ''}...\n`);
149
- // Create spec directory structure
150
- const specDirs = [
151
- 'spec',
152
- 'spec/default/templates',
153
- 'spec/_shared/openapi',
154
- 'spec/_shared/overlays',
155
- `spec/${name}/openapi`,
156
- ];
157
- for (const dir of specDirs) {
158
- if (!fs.existsSync(dir)) {
159
- fs.mkdirSync(dir, { recursive: true });
160
- console.log(`Created: ${dir}/`);
114
+ catch (error) {
115
+ console.error('Lint failed:', error instanceof Error ? error.message : error);
116
+ process.exit(1);
161
117
  }
162
- }
163
- // Create starter templates (unless skipped)
164
- if (!options.skipTemplates) {
165
- const starterTemplates = getStarterTemplates();
166
- for (const [filename, content] of Object.entries(starterTemplates)) {
167
- const templatePath = path.join('spec/default/templates', filename);
168
- if (!fs.existsSync(templatePath)) {
169
- fs.writeFileSync(templatePath, content);
170
- console.log(`Created: ${templatePath}`);
118
+ },
119
+ // ── init ───────────────────────────────────────────────
120
+ init: async (name, opts) => {
121
+ console.log(`Initializing module "${name}"${opts.screens ? ' (screen spec)' : ''}...\n`);
122
+ const specDirs = [
123
+ 'spec',
124
+ 'spec/default/templates',
125
+ 'spec/_shared/openapi',
126
+ 'spec/_shared/overlays',
127
+ `spec/${name}/openapi`,
128
+ ];
129
+ for (const dir of specDirs) {
130
+ if (!fs.existsSync(dir)) {
131
+ fs.mkdirSync(dir, { recursive: true });
132
+ console.log(`Created: ${dir}/`);
171
133
  }
172
134
  }
173
- // Also create screen templates when --screens is specified
174
- if (options.screens) {
175
- const screenTemplates = getScreenStarterTemplates();
176
- for (const [filename, content] of Object.entries(screenTemplates)) {
135
+ if (!opts.skipTemplates) {
136
+ const starterTemplates = getStarterTemplates();
137
+ for (const [filename, content] of Object.entries(starterTemplates)) {
177
138
  const templatePath = path.join('spec/default/templates', filename);
178
139
  if (!fs.existsSync(templatePath)) {
179
140
  fs.writeFileSync(templatePath, content);
180
141
  console.log(`Created: ${templatePath}`);
181
142
  }
182
143
  }
144
+ if (opts.screens) {
145
+ const screenTemplates = getScreenStarterTemplates();
146
+ for (const [filename, content] of Object.entries(screenTemplates)) {
147
+ const templatePath = path.join('spec/default/templates', filename);
148
+ if (!fs.existsSync(templatePath)) {
149
+ fs.writeFileSync(templatePath, content);
150
+ console.log(`Created: ${templatePath}`);
151
+ }
152
+ }
153
+ }
183
154
  }
184
- }
185
- // Create shared schemas
186
- const problemDetailsPath = 'spec/_shared/openapi/problem-details.yaml';
187
- if (!fs.existsSync(problemDetailsPath)) {
188
- fs.writeFileSync(problemDetailsPath, generateProblemDetailsSchema());
189
- console.log(`Created: ${problemDetailsPath}`);
190
- }
191
- // Create Spectral rules
192
- const spectralPath = 'spec/spectral.yaml';
193
- if (!fs.existsSync(spectralPath)) {
194
- fs.writeFileSync(spectralPath, generateSpectralRules());
195
- console.log(`Created: ${spectralPath}`);
196
- }
197
- // Create starter screen spec YAML when --screens is specified
198
- if (options.screens) {
199
- const screenSpecDir = `spec/${name}/openapi`;
200
- fs.mkdirSync(screenSpecDir, { recursive: true });
201
- const screenSpecPath = path.join(screenSpecDir, `${name}.yaml`);
202
- if (!fs.existsSync(screenSpecPath)) {
203
- fs.writeFileSync(screenSpecPath, generateScreenSpecTemplate(name));
204
- console.log(`Created: ${screenSpecPath}`);
205
- }
206
- }
207
- // Create server/frontend directories
208
- const baseDir = path.resolve(options.dir, name);
209
- const dirs = [
210
- baseDir,
211
- path.join(baseDir, 'services'),
212
- path.join(baseDir, 'models'),
213
- ];
214
- for (const dir of dirs) {
215
- if (!fs.existsSync(dir)) {
216
- fs.mkdirSync(dir, { recursive: true });
217
- console.log(`Created: ${dir}/`);
155
+ const problemDetailsPath = 'spec/_shared/openapi/problem-details.yaml';
156
+ if (!fs.existsSync(problemDetailsPath)) {
157
+ fs.writeFileSync(problemDetailsPath, generateProblemDetailsSchema());
158
+ console.log(`Created: ${problemDetailsPath}`);
218
159
  }
219
- }
220
- // Create placeholder files
221
- const files = [
222
- [path.join(baseDir, 'db.ts'), generateDbTemplate()],
223
- [path.join(baseDir, 'container.ts'), generateContainerTemplate(name)],
224
- [path.join(baseDir, 'services', 'index.ts'), '// Export service classes\n'],
225
- [path.join(baseDir, 'models', 'index.ts'), '// Export models\n'],
226
- ];
227
- for (const [filePath, content] of files) {
228
- if (!fs.existsSync(filePath)) {
229
- fs.writeFileSync(filePath, content);
230
- console.log(`Created: ${filePath}`);
160
+ const spectralPath = 'spec/spectral.yaml';
161
+ if (!fs.existsSync(spectralPath)) {
162
+ fs.writeFileSync(spectralPath, generateSpectralRules());
163
+ console.log(`Created: ${spectralPath}`);
231
164
  }
232
- }
233
- // Create config template if not exists
234
- const configPath = path.resolve('micro-contracts.config.yaml');
235
- if (!fs.existsSync(configPath)) {
236
- const configContent = options.screens
237
- ? generateScreenConfigTemplate(name)
238
- : generateConfigTemplate(name);
239
- fs.writeFileSync(configPath, configContent);
240
- console.log(`Created: ${configPath}`);
241
- }
242
- // Process OpenAPI file if provided
243
- if (options.openapi) {
244
- const openapiPath = path.resolve(options.openapi);
245
- if (!fs.existsSync(openapiPath)) {
246
- console.error(`OpenAPI file not found: ${openapiPath}`);
247
- process.exit(1);
165
+ if (opts.screens) {
166
+ const screenSpecDir = `spec/${name}/openapi`;
167
+ fs.mkdirSync(screenSpecDir, { recursive: true });
168
+ const screenSpecPath = path.join(screenSpecDir, `${name}.yaml`);
169
+ if (!fs.existsSync(screenSpecPath)) {
170
+ fs.writeFileSync(screenSpecPath, generateScreenSpecTemplate(name));
171
+ console.log(`Created: ${screenSpecPath}`);
172
+ }
248
173
  }
249
- const outputPath = options.output
250
- ? path.resolve(options.output)
251
- : path.resolve(`spec/${name}/openapi/${name}.yaml`);
252
- // Ensure output directory exists
253
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
254
- console.log(`\nProcessing OpenAPI: ${openapiPath}`);
255
- const processed = processOpenAPIWithExtensions(openapiPath);
256
- fs.writeFileSync(outputPath, processed.yaml);
257
- console.log(`Created: ${outputPath}`);
258
- console.log(` - Added x-micro-contracts-service to ${processed.stats.servicesAdded} operations`);
259
- console.log(` - Added x-micro-contracts-method to ${processed.stats.methodsAdded} operations`);
260
- if (processed.stats.services.length > 0) {
261
- console.log(` - Detected services: ${processed.stats.services.join(', ')}`);
174
+ const baseDir = path.resolve(opts.dir ?? 'src', name);
175
+ const dirs = [
176
+ baseDir,
177
+ path.join(baseDir, 'services'),
178
+ path.join(baseDir, 'models'),
179
+ ];
180
+ for (const dir of dirs) {
181
+ if (!fs.existsSync(dir)) {
182
+ fs.mkdirSync(dir, { recursive: true });
183
+ console.log(`Created: ${dir}/`);
184
+ }
262
185
  }
263
- }
264
- console.log(`\nModule "${name}" initialized!`);
265
- if (options.screens) {
266
- console.log(`\nNext steps:`);
267
- console.log(` 1. Edit spec/${name}/openapi/${name}.yaml with your screen definitions`);
268
- console.log(` 2. Define ViewModels in components/schemas`);
269
- console.log(` 3. Add navigation links and x-events`);
270
- console.log(` 4. Run: npx micro-contracts generate`);
271
- }
272
- else if (!options.openapi) {
273
- console.log(`\nNext steps:`);
274
- console.log(` 1. Create spec/${name}/openapi/${name}.yaml with your API spec`);
275
- console.log(` 2. Add x-micro-contracts-service and x-micro-contracts-method to operations`);
276
- console.log(` 3. Run: npx micro-contracts generate`);
277
- console.log(`\nTip: Use --openapi to auto-add extensions:`);
278
- console.log(` npx micro-contracts init ${name} --openapi path/to/spec.yaml`);
279
- }
280
- else {
281
- console.log(`\nNext steps:`);
282
- console.log(` 1. Review the generated extensions in spec/${name}/openapi/${name}.yaml`);
283
- console.log(` 2. Run: npx micro-contracts generate`);
284
- }
285
- });
286
- // Deps command (dependency analysis)
287
- program
288
- .command('deps')
289
- .description('Analyze module dependencies')
290
- .option('-c, --config <path>', 'Path to config file')
291
- .option('-m, --module <name>', 'Module to analyze')
292
- .option('--graph', 'Output dependency graph in Mermaid format')
293
- .option('--impact <ref>', 'Analyze impact of changing a specific API (e.g., core.User.getUsers)')
294
- .option('--who-depends-on <ref>', 'Find modules that depend on a specific API')
295
- .option('--validate', 'Validate dependencies against OpenAPI declarations')
296
- .action(async (options) => {
297
- try {
298
- const configPath = options.config
299
- ? path.resolve(options.config)
300
- : findConfigFile();
301
- if (!configPath) {
302
- console.error('Error: No config file found.');
303
- process.exit(1);
186
+ const files = [
187
+ [path.join(baseDir, 'db.ts'), generateDbTemplate()],
188
+ [path.join(baseDir, 'container.ts'), generateContainerTemplate(name)],
189
+ [path.join(baseDir, 'services', 'index.ts'), '// Export service classes\n'],
190
+ [path.join(baseDir, 'models', 'index.ts'), '// Export models\n'],
191
+ ];
192
+ for (const [filePath, content] of files) {
193
+ if (!fs.existsSync(filePath)) {
194
+ fs.writeFileSync(filePath, content);
195
+ console.log(`Created: ${filePath}`);
196
+ }
304
197
  }
305
- const config = loadConfig(configPath);
306
- if (!config.modules) {
307
- console.error('Error: Config must have modules defined.');
308
- process.exit(1);
198
+ const configPath = path.resolve('micro-contracts.config.yaml');
199
+ if (!fs.existsSync(configPath)) {
200
+ const configContent = opts.screens
201
+ ? generateScreenConfigTemplate(name)
202
+ : generateConfigTemplate(name);
203
+ fs.writeFileSync(configPath, configContent);
204
+ console.log(`Created: ${configPath}`);
309
205
  }
310
- // Collect dependencies from all modules
311
- const moduleDeps = new Map();
312
- for (const [moduleName, moduleConfig] of Object.entries(config.modules)) {
313
- if (options.module && moduleName !== options.module)
314
- continue;
315
- const openapiPath = path.resolve(path.dirname(configPath), moduleConfig.openapi);
206
+ if (opts.openapi) {
207
+ const openapiPath = path.resolve(opts.openapi);
316
208
  if (!fs.existsSync(openapiPath)) {
317
- console.warn(`Warning: OpenAPI spec not found: ${openapiPath}`);
318
- continue;
209
+ console.error(`OpenAPI file not found: ${openapiPath}`);
210
+ process.exit(1);
211
+ }
212
+ const outputPath = opts.output
213
+ ? path.resolve(opts.output)
214
+ : path.resolve(`spec/${name}/openapi/${name}.yaml`);
215
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
216
+ console.log(`\nProcessing OpenAPI: ${openapiPath}`);
217
+ const processed = processOpenAPIWithExtensions(openapiPath);
218
+ fs.writeFileSync(outputPath, processed.yaml);
219
+ console.log(`Created: ${outputPath}`);
220
+ console.log(` - Added x-micro-contracts-service to ${processed.stats.servicesAdded} operations`);
221
+ console.log(` - Added x-micro-contracts-method to ${processed.stats.methodsAdded} operations`);
222
+ if (processed.stats.services.length > 0) {
223
+ console.log(` - Detected services: ${processed.stats.services.join(', ')}`);
319
224
  }
320
- const spec = loadOpenAPISpec(openapiPath);
321
- const openApiDeps = spec.info['x-micro-contracts-depend-on'] || [];
322
- const configDeps = moduleConfig.dependsOn || [];
323
- moduleDeps.set(moduleName, {
324
- deps: openApiDeps,
325
- openApiDeps,
326
- configDeps,
327
- });
328
- }
329
- // Handle different options
330
- if (options.graph) {
331
- outputDependencyGraph(moduleDeps);
332
- }
333
- else if (options.impact) {
334
- outputImpactAnalysis(moduleDeps, options.impact);
335
225
  }
336
- else if (options.whoDependsOn) {
337
- outputWhoDependsOn(moduleDeps, options.whoDependsOn);
226
+ console.log(`\nModule "${name}" initialized!`);
227
+ if (opts.screens) {
228
+ console.log(`\nNext steps:`);
229
+ console.log(` 1. Edit spec/${name}/openapi/${name}.yaml with your screen definitions`);
230
+ console.log(` 2. Define ViewModels in components/schemas`);
231
+ console.log(` 3. Add navigation links and x-events`);
232
+ console.log(` 4. Run: npx micro-contracts generate`);
338
233
  }
339
- else if (options.validate) {
340
- validateDependencies(moduleDeps);
234
+ else if (!opts.openapi) {
235
+ console.log(`\nNext steps:`);
236
+ console.log(` 1. Create spec/${name}/openapi/${name}.yaml with your API spec`);
237
+ console.log(` 2. Add x-micro-contracts-service and x-micro-contracts-method to operations`);
238
+ console.log(` 3. Run: npx micro-contracts generate`);
239
+ console.log(`\nTip: Use --openapi to auto-add extensions:`);
240
+ console.log(` npx micro-contracts init ${name} --openapi path/to/spec.yaml`);
341
241
  }
342
242
  else {
343
- // Default: show all dependencies
344
- outputAllDependencies(moduleDeps);
243
+ console.log(`\nNext steps:`);
244
+ console.log(` 1. Review the generated extensions in spec/${name}/openapi/${name}.yaml`);
245
+ console.log(` 2. Run: npx micro-contracts generate`);
345
246
  }
346
- }
347
- catch (error) {
348
- console.error('Deps analysis failed:', error instanceof Error ? error.message : error);
349
- process.exit(1);
350
- }
351
- });
352
- // Check command (guardrails)
353
- program
354
- .command('check')
355
- .description('Run AI guardrail checks')
356
- .option('--only <checks>', 'Run only specific checks (comma-separated)')
357
- .option('--skip <checks>', 'Skip specific checks (comma-separated)')
358
- .option('--gate <gates>', 'Run checks for specific gates only (comma-separated, 1-5)')
359
- .option('-v, --verbose', 'Enable verbose output')
360
- .option('--fix', 'Auto-fix issues where possible')
361
- .option('-g, --guardrails <path>', 'Path to guardrails.yaml')
362
- .option('-d, --generated-dir <path>', 'Path to generated files directory', 'packages/')
363
- .option('--changed-files <path>', 'Path to file containing list of changed files (for CI)')
364
- .option('--list', 'List available checks')
365
- .option('--list-gates', 'List available gates')
366
- .action(async (options) => {
367
- try {
368
- // List gates
369
- if (options.listGates) {
370
- console.log('\nAvailable gates:\n');
371
- for (const [gate, description] of Object.entries(GATE_DESCRIPTIONS)) {
372
- console.log(` Gate ${gate}: ${description}`);
247
+ },
248
+ // ── check ─────────────────────────────────────────────
249
+ check: async (opts) => {
250
+ try {
251
+ if (opts.listGates) {
252
+ console.log('\nAvailable gates:\n');
253
+ for (const [gate, description] of Object.entries(GATE_DESCRIPTIONS)) {
254
+ console.log(` Gate ${gate}: ${description}`);
255
+ }
256
+ console.log('\nUsage: micro-contracts check --gate 1,2,3');
257
+ console.log('');
258
+ return;
373
259
  }
374
- console.log('\nUsage: micro-contracts check --gate 1,2,3');
375
- console.log('');
376
- return;
377
- }
378
- // List checks
379
- if (options.list) {
380
- console.log('\nAvailable checks:\n');
381
- for (const check of getAvailableChecks({ guardrailsPath: options.guardrails })) {
382
- const gateStr = check.gate !== undefined ? `[G${check.gate}]` : ' ';
383
- console.log(` ${gateStr} ${check.name.padEnd(20)} - ${check.description}`);
260
+ if (opts.list) {
261
+ console.log('\nAvailable checks:\n');
262
+ for (const check of getAvailableChecks({ guardrailsPath: opts.guardrails })) {
263
+ const gateStr = check.gate !== undefined ? `[G${check.gate}]` : ' ';
264
+ console.log(` ${gateStr} ${check.name.padEnd(20)} - ${check.description}`);
265
+ }
266
+ console.log('');
267
+ return;
268
+ }
269
+ let gates;
270
+ if (opts.gate) {
271
+ gates = opts.gate.split(',').map((s) => {
272
+ const num = parseInt(s.trim(), 10);
273
+ if (num < 1 || num > 5 || isNaN(num)) {
274
+ throw new Error(`Invalid gate number: ${s}. Must be 1-5.`);
275
+ }
276
+ return num;
277
+ });
384
278
  }
385
279
  console.log('');
386
- return;
387
- }
388
- // Parse gate option
389
- let gates;
390
- if (options.gate) {
391
- gates = options.gate.split(',').map((s) => {
392
- const num = parseInt(s.trim(), 10);
393
- if (num < 1 || num > 5 || isNaN(num)) {
394
- throw new Error(`Invalid gate number: ${s}. Must be 1-5.`);
395
- }
396
- return num;
397
- });
398
- }
399
- // Print header immediately
400
- console.log('');
401
- console.log('🔍 AI Guardrail Check Results');
402
- console.log('═'.repeat(50));
403
- console.log('');
404
- // Track whether we're using streaming output (default: yes for TTY)
405
- const isStreaming = process.stdout.isTTY !== false;
406
- // Parse options with streaming callbacks
407
- const checkOptions = {
408
- only: options.only?.split(',').map((s) => s.trim()),
409
- skip: options.skip?.split(',').map((s) => s.trim()),
410
- gates,
411
- verbose: options.verbose,
412
- fix: options.fix,
413
- guardrailsPath: options.guardrails,
414
- generatedDir: options.generatedDir,
415
- changedFilesPath: options.changedFiles,
416
- // Streaming output callbacks
417
- onCheckStart: isStreaming ? (check) => {
418
- // Clear line and show "running..." message
419
- if (process.stdout.isTTY) {
420
- process.stdout.write(formatCheckStart(check) + '\r');
421
- }
422
- } : undefined,
423
- onCheckComplete: isStreaming ? (result, check) => {
424
- // Clear the "running..." line and print result
425
- if (process.stdout.isTTY) {
426
- process.stdout.clearLine?.(0);
427
- process.stdout.cursorTo?.(0);
428
- }
429
- console.log(formatSingleCheckResult(result, check, options.verbose));
430
- } : undefined,
431
- };
432
- // Run checks
433
- const summary = await runAllChecks(checkOptions);
434
- // Output results
435
- if (isStreaming) {
436
- // Only output summary (individual results already printed)
437
- console.log(formatCheckSummary(summary, summary.checks));
438
- }
439
- else {
440
- // Non-streaming: output everything at once
441
- console.log(formatCheckResults(summary, options.verbose, summary.checks));
280
+ console.log('🔍 AI Guardrail Check Results');
281
+ console.log('═'.repeat(50));
282
+ console.log('');
283
+ const isStreaming = process.stdout.isTTY !== false;
284
+ const checkOptions = {
285
+ only: opts.only?.split(',').map((s) => s.trim()),
286
+ skip: opts.skip?.split(',').map((s) => s.trim()),
287
+ gates,
288
+ verbose: opts.verbose,
289
+ fix: opts.fix,
290
+ guardrailsPath: opts.guardrails,
291
+ generatedDir: opts.generatedDir,
292
+ changedFilesPath: opts.changedFiles,
293
+ onCheckStart: isStreaming ? (check) => {
294
+ if (process.stdout.isTTY) {
295
+ process.stdout.write(formatCheckStart(check) + '\r');
296
+ }
297
+ } : undefined,
298
+ onCheckComplete: isStreaming ? (result, check) => {
299
+ if (process.stdout.isTTY) {
300
+ process.stdout.clearLine?.(0);
301
+ process.stdout.cursorTo?.(0);
302
+ }
303
+ console.log(formatSingleCheckResult(result, check, opts.verbose));
304
+ } : undefined,
305
+ };
306
+ const summary = await runAllChecks(checkOptions);
307
+ if (isStreaming) {
308
+ console.log(formatCheckSummary(summary, summary.checks));
309
+ }
310
+ else {
311
+ console.log(formatCheckResults(summary, opts.verbose, summary.checks));
312
+ }
313
+ if (summary.failed > 0) {
314
+ process.exit(1);
315
+ }
442
316
  }
443
- // Exit with error if any checks failed
444
- if (summary.failed > 0) {
317
+ catch (error) {
318
+ console.error('Check failed:', error instanceof Error ? error.message : error);
445
319
  process.exit(1);
446
320
  }
447
- }
448
- catch (error) {
449
- console.error('Check failed:', error instanceof Error ? error.message : error);
450
- process.exit(1);
451
- }
452
- });
453
- // Pipeline command (Gate 1,2 → Generate → Gate 3,4,5)
454
- program
455
- .command('pipeline')
456
- .description('Run full guardrails pipeline: Gate 1,2 → Generate → Gate 3,4,5')
457
- .option('-v, --verbose', 'Enable verbose output (show detailed logs)')
458
- .option('--skip <checks>', 'Skip specific checks (comma-separated)')
459
- .option('--continue-on-error', 'Continue running even if a step fails')
460
- .option('-g, --guardrails <path>', 'Path to guardrails.yaml')
461
- .option('-c, --config <path>', 'Path to config file (micro-contracts.config.yaml)')
462
- .option('-d, --generated-dir <path>', 'Path to generated files directory', 'packages/')
463
- .option('--no-manifest', 'Skip manifest generation')
464
- .option('--skip-lint', 'Skip linting before generation')
465
- .option('--contracts-only', 'Generate contract packages only')
466
- .option('--server-only', 'Generate server routes only')
467
- .option('--frontend-only', 'Generate frontend clients only')
468
- .option('--docs-only', 'Generate documentation only')
469
- .option('--force', 'Bypass input hash cache and always regenerate')
470
- .option('--no-cache', 'Run without reading or writing input hash cache')
471
- .action(async (options) => {
472
- try {
473
- const startTime = Date.now();
474
- const verbose = options.verbose;
475
- let hasFailure = false;
476
- let generatePassed = false;
477
- let generateDuration = 0;
478
- let generateSkipped = false;
479
- let generateError = null;
480
- // All results for final summary
481
- const allResults = [];
482
- console.log('');
483
- console.log('🚀 Running AI Guardrails Pipeline');
484
- console.log('═'.repeat(50));
485
- console.log('');
486
- // Parse skip option
487
- const skipChecks = options.skip?.split(',').map((s) => s.trim()) || [];
488
- // Common check options
489
- const isStreaming = process.stdout.isTTY !== false;
490
- const baseCheckOptions = {
491
- verbose,
492
- skip: skipChecks,
493
- guardrailsPath: options.guardrails,
494
- generatedDir: options.generatedDir,
495
- onCheckStart: isStreaming ? (check) => {
496
- if (process.stdout.isTTY) {
497
- process.stdout.write(formatCheckStart(check) + '\r');
498
- }
499
- } : undefined,
500
- onCheckComplete: isStreaming ? (result, check) => {
501
- if (process.stdout.isTTY) {
502
- process.stdout.clearLine?.(0);
503
- process.stdout.cursorTo?.(0);
504
- }
505
- console.log(formatSingleCheckResult(result, check, verbose));
506
- } : undefined,
507
- };
508
- // ========================================
509
- // Step 1: Gate 1,2 (Pre-generation checks)
510
- // ========================================
511
- if (verbose) {
512
- console.log('┌─────────────────────────────────────────────────┐');
513
- console.log('│ Step 1: Pre-generation checks (Gate 1, 2) │');
514
- console.log('└─────────────────────────────────────────────────┘');
321
+ },
322
+ // ── pipeline ──────────────────────────────────────────
323
+ pipeline: async (opts) => {
324
+ try {
325
+ const startTime = Date.now();
326
+ const verbose = opts.verbose;
327
+ let hasFailure = false;
328
+ let generatePassed = false;
329
+ let generateDuration = 0;
330
+ let generateSkipped = false;
331
+ let generateError = null;
332
+ const allResults = [];
515
333
  console.log('');
516
- }
517
- const gate12Summary = await runAllChecks({
518
- ...baseCheckOptions,
519
- gates: [1, 2],
520
- });
521
- // Collect results (only non-skipped for display count)
522
- allResults.push(...gate12Summary.results);
523
- if (verbose) {
524
- console.log(formatCheckSummary(gate12Summary, gate12Summary.checks));
525
- }
526
- if (gate12Summary.failed > 0) {
527
- hasFailure = true;
528
- if (!options.continueOnError) {
334
+ console.log('🚀 Running AI Guardrails Pipeline');
335
+ console.log('═'.repeat(50));
336
+ console.log('');
337
+ const skipChecks = opts.skip?.split(',').map((s) => s.trim()) || [];
338
+ const isStreaming = process.stdout.isTTY !== false;
339
+ const baseCheckOptions = {
340
+ verbose,
341
+ skip: skipChecks,
342
+ guardrailsPath: opts.guardrails,
343
+ generatedDir: opts.generatedDir,
344
+ onCheckStart: isStreaming ? (check) => {
345
+ if (process.stdout.isTTY) {
346
+ process.stdout.write(formatCheckStart(check) + '\r');
347
+ }
348
+ } : undefined,
349
+ onCheckComplete: isStreaming ? (result, check) => {
350
+ if (process.stdout.isTTY) {
351
+ process.stdout.clearLine?.(0);
352
+ process.stdout.cursorTo?.(0);
353
+ }
354
+ console.log(formatSingleCheckResult(result, check, verbose));
355
+ } : undefined,
356
+ };
357
+ // Step 1: Gate 1,2
358
+ if (verbose) {
359
+ console.log('┌─────────────────────────────────────────────────┐');
360
+ console.log('│ Step 1: Pre-generation checks (Gate 1, 2) │');
361
+ console.log('└─────────────────────────────────────────────────┘');
529
362
  console.log('');
530
- console.log('❌ Gate 1,2 failed. Stopping pipeline.');
531
- console.log(' Use --continue-on-error to continue despite failures.');
532
- process.exit(1);
533
363
  }
364
+ const gate12Summary = await runAllChecks({ ...baseCheckOptions, gates: [1, 2] });
365
+ allResults.push(...gate12Summary.results);
366
+ if (verbose)
367
+ console.log(formatCheckSummary(gate12Summary, gate12Summary.checks));
368
+ if (gate12Summary.failed > 0) {
369
+ hasFailure = true;
370
+ if (!opts.continueOnError) {
371
+ console.log('');
372
+ console.log('❌ Gate 1,2 failed. Stopping pipeline.');
373
+ console.log(' Use --continue-on-error to continue despite failures.');
374
+ process.exit(1);
375
+ }
376
+ if (verbose)
377
+ console.log('⚠️ Gate 1,2 had failures. Continuing due to --continue-on-error.\n');
378
+ }
379
+ // Step 2: Generate
380
+ let generationCacheSkipped = false;
534
381
  if (verbose) {
535
- console.log('⚠️ Gate 1,2 had failures. Continuing due to --continue-on-error.');
382
+ console.log('┌─────────────────────────────────────────────────┐');
383
+ console.log('│ Step 2: Generate contracts │');
384
+ console.log('└─────────────────────────────────────────────────┘');
536
385
  console.log('');
537
386
  }
538
- }
539
- // ========================================
540
- // Step 2: Generate
541
- // ========================================
542
- let generationCacheSkipped = false;
543
- if (verbose) {
544
- console.log('┌─────────────────────────────────────────────────┐');
545
- console.log('│ Step 2: Generate contracts │');
546
- console.log('└─────────────────────────────────────────────────┘');
547
- console.log('');
548
- }
549
- const generateStartTime = Date.now();
550
- // Show "running..." indicator for generate
551
- if (isStreaming && process.stdout.isTTY) {
552
- process.stdout.write(' ⋯ Generate running...\r');
553
- }
554
- try {
555
- // Load config
556
- const configPath = options.config
557
- ? path.resolve(options.config)
558
- : findConfigFile();
559
- if (!configPath) {
560
- generateSkipped = true;
561
- generateDuration = Date.now() - generateStartTime;
562
- // Clear the running indicator
563
- if (isStreaming && process.stdout.isTTY) {
564
- process.stdout.clearLine?.(0);
565
- process.stdout.cursorTo?.(0);
566
- }
567
- console.log(' ○ Generate SKIP (no config file)');
387
+ const generateStartTime = Date.now();
388
+ if (isStreaming && process.stdout.isTTY) {
389
+ process.stdout.write(' ⋯ Generate running...\r');
568
390
  }
569
- else {
570
- if (verbose) {
571
- console.log(` Using config: ${configPath}`);
391
+ try {
392
+ const configPath = opts.config ? path.resolve(opts.config) : findConfigFile();
393
+ if (!configPath) {
394
+ generateSkipped = true;
395
+ generateDuration = Date.now() - generateStartTime;
396
+ if (isStreaming && process.stdout.isTTY) {
397
+ process.stdout.clearLine?.(0);
398
+ process.stdout.cursorTo?.(0);
399
+ }
400
+ console.log(' ○ Generate SKIP (no config file)');
572
401
  }
573
- const config = loadConfig(configPath);
574
- // Compute input hash for cache (multi-module only)
575
- const useCache = options.cache !== false && !options.force;
576
- let inputHash;
577
- if (isMultiModuleConfig(config)) {
578
- inputHash = computeInputHash(config, configPath, pkg.version);
579
- if (useCache) {
580
- const manifestDir = options.generatedDir || 'packages/';
581
- const skipCheck = await canSkipGeneration(manifestDir, inputHash);
582
- if (skipCheck.skip) {
583
- generationCacheSkipped = true;
584
- generateSkipped = true;
585
- generateDuration = Date.now() - generateStartTime;
586
- if (isStreaming && process.stdout.isTTY) {
587
- process.stdout.clearLine?.(0);
588
- process.stdout.cursorTo?.(0);
402
+ else {
403
+ if (verbose)
404
+ console.log(` Using config: ${configPath}`);
405
+ const config = loadConfig(configPath);
406
+ const useCache = opts.cache !== false && !opts.force;
407
+ let inputHash;
408
+ if (isMultiModuleConfig(config)) {
409
+ inputHash = computeInputHash(config, configPath, pkg.version);
410
+ if (useCache) {
411
+ const manifestDir = opts.generatedDir || 'packages/';
412
+ const skipCheck = await canSkipGeneration(manifestDir, inputHash);
413
+ if (skipCheck.skip) {
414
+ generationCacheSkipped = true;
415
+ generateSkipped = true;
416
+ generateDuration = Date.now() - generateStartTime;
417
+ if (isStreaming && process.stdout.isTTY) {
418
+ process.stdout.clearLine?.(0);
419
+ process.stdout.cursorTo?.(0);
420
+ }
421
+ console.log(` ○ Generate SKIP (${skipCheck.reason})`);
589
422
  }
590
- console.log(` ○ Generate SKIP (${skipCheck.reason})`);
591
423
  }
592
424
  }
593
- }
594
- if (!generationCacheSkipped) {
595
- // Suppress console output during generation (unless verbose)
596
- const originalLog = console.log;
597
- if (!verbose) {
598
- console.log = () => { };
599
- }
600
- try {
601
- // Run generation
602
- await generate(config, {
603
- skipLint: options.skipLint,
604
- contractsOnly: options.contractsOnly,
605
- serverOnly: options.serverOnly,
606
- frontendOnly: options.frontendOnly,
607
- docsOnly: options.docsOnly,
608
- });
609
- // Generate manifest if enabled
610
- if (options.manifest !== false) {
611
- const { config: guardrailsConfig } = loadGuardrailsConfigWithPath(options.guardrails);
612
- if (guardrailsConfig?.generated && guardrailsConfig.generated.length > 0) {
613
- const manifestDir = options.generatedDir || 'packages/';
614
- if (fs.existsSync(manifestDir)) {
615
- const manifestInputHash = (options.cache !== false) ? inputHash : undefined;
616
- const { manifest, changed } = await generateManifest(manifestDir, {
617
- generatorVersion: pkg.version,
618
- inputHash: manifestInputHash,
619
- });
620
- // Only log in verbose mode
621
- if (verbose) {
622
- const fileCount = Object.keys(manifest.files).length;
623
- if (changed) {
624
- const manifestPath = writeManifest(manifest, manifestDir);
625
- originalLog(` Manifest updated: ${manifestPath} (${fileCount} files)`);
425
+ if (!generationCacheSkipped) {
426
+ const originalLog = console.log;
427
+ if (!verbose)
428
+ console.log = () => { };
429
+ try {
430
+ await generate(config, {
431
+ skipLint: opts.skipLint,
432
+ contractsOnly: opts.contractsOnly,
433
+ serverOnly: opts.serverOnly,
434
+ frontendOnly: opts.frontendOnly,
435
+ docsOnly: opts.docsOnly,
436
+ });
437
+ if (opts.manifest !== false) {
438
+ const { config: guardrailsConfig } = loadGuardrailsConfigWithPath(opts.guardrails);
439
+ if (guardrailsConfig?.generated && guardrailsConfig.generated.length > 0) {
440
+ const manifestDir = opts.generatedDir || 'packages/';
441
+ if (fs.existsSync(manifestDir)) {
442
+ const manifestInputHash = (opts.cache !== false) ? inputHash : undefined;
443
+ const { manifest, changed } = await generateManifest(manifestDir, {
444
+ generatorVersion: pkg.version,
445
+ inputHash: manifestInputHash,
446
+ });
447
+ if (verbose) {
448
+ const fileCount = Object.keys(manifest.files).length;
449
+ if (changed) {
450
+ const mPath = writeManifest(manifest, manifestDir);
451
+ originalLog(` Manifest updated: ${mPath} (${fileCount} files)`);
452
+ }
453
+ else {
454
+ originalLog(` Manifest unchanged (${fileCount} files)`);
455
+ }
626
456
  }
627
- else {
628
- originalLog(` Manifest unchanged (${fileCount} files)`);
457
+ else if (changed) {
458
+ writeManifest(manifest, manifestDir);
629
459
  }
630
460
  }
631
- else if (changed) {
632
- // Still write the manifest even in non-verbose mode
633
- writeManifest(manifest, manifestDir);
634
- }
635
461
  }
636
462
  }
463
+ generatePassed = true;
637
464
  }
638
- generatePassed = true;
639
- }
640
- finally {
641
- // Restore console.log
642
- if (!verbose) {
643
- console.log = originalLog;
465
+ finally {
466
+ if (!verbose)
467
+ console.log = originalLog;
644
468
  }
645
- }
646
- generateDuration = Date.now() - generateStartTime;
647
- // Clear the running indicator
648
- if (isStreaming && process.stdout.isTTY) {
649
- process.stdout.clearLine?.(0);
650
- process.stdout.cursorTo?.(0);
651
- }
652
- console.log(` ✓ Generate PASS (${generateDuration}ms)`);
653
- if (verbose) {
654
- console.log('');
469
+ generateDuration = Date.now() - generateStartTime;
470
+ if (isStreaming && process.stdout.isTTY) {
471
+ process.stdout.clearLine?.(0);
472
+ process.stdout.cursorTo?.(0);
473
+ }
474
+ console.log(` ✓ Generate PASS (${generateDuration}ms)`);
475
+ if (verbose)
476
+ console.log('');
655
477
  }
656
478
  }
657
479
  }
658
- }
659
- catch (error) {
660
- generateDuration = Date.now() - generateStartTime;
661
- hasFailure = true;
662
- generateError = error instanceof Error ? error.message : String(error);
663
- // Clear the running indicator
664
- if (isStreaming && process.stdout.isTTY) {
665
- process.stdout.clearLine?.(0);
666
- process.stdout.cursorTo?.(0);
667
- }
668
- console.log(` ✗ Generate FAIL (${generateDuration}ms)`);
669
- if (verbose || true) { // Always show error message
480
+ catch (error) {
481
+ generateDuration = Date.now() - generateStartTime;
482
+ hasFailure = true;
483
+ generateError = error instanceof Error ? error.message : String(error);
484
+ if (isStreaming && process.stdout.isTTY) {
485
+ process.stdout.clearLine?.(0);
486
+ process.stdout.cursorTo?.(0);
487
+ }
488
+ console.log(` ✗ Generate FAIL (${generateDuration}ms)`);
670
489
  console.log(` ${generateError}`);
490
+ if (!opts.continueOnError) {
491
+ console.log('');
492
+ console.log('❌ Generation failed. Stopping pipeline.');
493
+ process.exit(1);
494
+ }
495
+ if (verbose)
496
+ console.log('⚠️ Continuing due to --continue-on-error.');
671
497
  }
672
- if (!options.continueOnError) {
673
- console.log('');
674
- console.log('❌ Generation failed. Stopping pipeline.');
675
- process.exit(1);
676
- }
498
+ // Step 3: Gate 3,4,5
677
499
  if (verbose) {
678
- console.log('⚠️ Continuing due to --continue-on-error.');
500
+ console.log('');
501
+ console.log('┌─────────────────────────────────────────────────┐');
502
+ console.log('│ Step 3: Post-generation checks (Gate 3, 4, 5) │');
503
+ console.log('└─────────────────────────────────────────────────┘');
504
+ console.log('');
679
505
  }
680
- }
681
- // ========================================
682
- // Step 3: Gate 3,4,5 (Post-generation checks)
683
- // ========================================
684
- if (verbose) {
506
+ const gate3Skips = generationCacheSkipped
507
+ ? [...skipChecks, 'drift', 'manifest']
508
+ : skipChecks;
509
+ const gate345Summary = await runAllChecks({
510
+ ...baseCheckOptions,
511
+ skip: gate3Skips,
512
+ gates: [3, 4, 5],
513
+ });
514
+ allResults.push(...gate345Summary.results);
515
+ if (verbose)
516
+ console.log(formatCheckSummary(gate345Summary, gate345Summary.checks));
517
+ if (gate345Summary.failed > 0)
518
+ hasFailure = true;
519
+ // Final Summary
520
+ const totalDuration = Date.now() - startTime;
521
+ const totalPassed = gate12Summary.passed + gate345Summary.passed + (generatePassed ? 1 : 0);
522
+ const totalFailed = gate12Summary.failed + gate345Summary.failed + (generateError ? 1 : 0);
523
+ const totalSkipped = gate12Summary.skipped + gate345Summary.skipped + (generateSkipped ? 1 : 0);
685
524
  console.log('');
686
- console.log('┌─────────────────────────────────────────────────┐');
687
- console.log(' Step 3: Post-generation checks (Gate 3, 4, 5) │');
688
- console.log('└─────────────────────────────────────────────────┘');
525
+ console.log(''.repeat(50));
526
+ console.log('📊 Pipeline Summary');
527
+ console.log(''.repeat(50));
689
528
  console.log('');
690
- }
691
- // When generation was skipped via cache, drift/manifest are
692
- // already verified — skip them to avoid redundant work.
693
- const gate3Skips = generationCacheSkipped
694
- ? [...skipChecks, 'drift', 'manifest']
695
- : skipChecks;
696
- const gate345Summary = await runAllChecks({
697
- ...baseCheckOptions,
698
- skip: gate3Skips,
699
- gates: [3, 4, 5],
700
- });
701
- // Collect results
702
- allResults.push(...gate345Summary.results);
703
- if (verbose) {
704
- console.log(formatCheckSummary(gate345Summary, gate345Summary.checks));
705
- }
706
- if (gate345Summary.failed > 0) {
707
- hasFailure = true;
708
- }
709
- // ========================================
710
- // Final Summary
711
- // ========================================
712
- const totalDuration = Date.now() - startTime;
713
- const totalPassed = gate12Summary.passed + gate345Summary.passed + (generatePassed ? 1 : 0);
714
- const totalFailed = gate12Summary.failed + gate345Summary.failed + (generateError ? 1 : 0);
715
- const totalSkipped = gate12Summary.skipped + gate345Summary.skipped + (generateSkipped ? 1 : 0);
716
- console.log('');
717
- console.log('━'.repeat(50));
718
- console.log('📊 Pipeline Summary');
719
- console.log('━'.repeat(50));
720
- console.log('');
721
- console.log(` Total Passed: ${totalPassed}`);
722
- console.log(` Total Failed: ${totalFailed}`);
723
- if (totalSkipped > 0) {
724
- console.log(` Total Skipped: ${totalSkipped}`);
725
- }
726
- console.log(` Duration: ${totalDuration}ms`);
727
- console.log('');
728
- if (hasFailure) {
729
- console.log('❌ Pipeline completed with failures.');
730
- // Show failed check details
731
- const failedResults = allResults.filter(r => r.status === 'fail' && r.details && r.details.length > 0);
732
- if (failedResults.length > 0) {
733
- console.log('');
734
- console.log('📋 Failed Check Details:');
735
- for (const result of failedResults) {
736
- console.log(` ▶ ${result.name}`);
737
- for (const detail of result.details) {
738
- console.log(` ${detail}`);
529
+ console.log(` Total Passed: ${totalPassed}`);
530
+ console.log(` Total Failed: ${totalFailed}`);
531
+ if (totalSkipped > 0)
532
+ console.log(` Total Skipped: ${totalSkipped}`);
533
+ console.log(` Duration: ${totalDuration}ms`);
534
+ console.log('');
535
+ if (hasFailure) {
536
+ console.log('❌ Pipeline completed with failures.');
537
+ const failedResults = allResults.filter(r => r.status === 'fail' && r.details && r.details.length > 0);
538
+ if (failedResults.length > 0) {
539
+ console.log('');
540
+ console.log('📋 Failed Check Details:');
541
+ for (const result of failedResults) {
542
+ console.log(` ▶ ${result.name}`);
543
+ for (const detail of result.details) {
544
+ console.log(` ${detail}`);
545
+ }
739
546
  }
740
547
  }
548
+ process.exit(1);
549
+ }
550
+ else {
551
+ console.log('✅ Pipeline completed successfully!');
741
552
  }
553
+ console.log('');
554
+ }
555
+ catch (error) {
556
+ console.error('Pipeline failed:', error instanceof Error ? error.message : error);
742
557
  process.exit(1);
743
558
  }
744
- else {
745
- console.log('✅ Pipeline completed successfully!');
559
+ },
560
+ // ── deps ──────────────────────────────────────────────
561
+ deps: async (opts) => {
562
+ try {
563
+ const configPath = opts.config
564
+ ? path.resolve(opts.config)
565
+ : findConfigFile();
566
+ if (!configPath) {
567
+ console.error('Error: No config file found.');
568
+ process.exit(1);
569
+ }
570
+ const config = loadConfig(configPath);
571
+ if (!config.modules) {
572
+ console.error('Error: Config must have modules defined.');
573
+ process.exit(1);
574
+ }
575
+ const moduleDeps = new Map();
576
+ for (const [moduleName, moduleConfig] of Object.entries(config.modules)) {
577
+ if (opts.module && moduleName !== opts.module)
578
+ continue;
579
+ const openapiPath = path.resolve(path.dirname(configPath), moduleConfig.openapi);
580
+ if (!fs.existsSync(openapiPath)) {
581
+ console.warn(`Warning: OpenAPI spec not found: ${openapiPath}`);
582
+ continue;
583
+ }
584
+ const spec = loadOpenAPISpec(openapiPath);
585
+ const openApiDeps = spec.info['x-micro-contracts-depend-on'] || [];
586
+ const configDeps = moduleConfig.dependsOn || [];
587
+ moduleDeps.set(moduleName, { deps: openApiDeps, openApiDeps, configDeps });
588
+ }
589
+ if (opts.graph) {
590
+ outputDependencyGraph(moduleDeps);
591
+ }
592
+ else if (opts.impact) {
593
+ outputImpactAnalysis(moduleDeps, opts.impact);
594
+ }
595
+ else if (opts.whoDependsOn) {
596
+ outputWhoDependsOn(moduleDeps, opts.whoDependsOn);
597
+ }
598
+ else if (opts.validate) {
599
+ validateDependencies(moduleDeps);
600
+ }
601
+ else {
602
+ outputAllDependencies(moduleDeps);
603
+ }
746
604
  }
747
- console.log('');
748
- }
749
- catch (error) {
750
- console.error('Pipeline failed:', error instanceof Error ? error.message : error);
751
- process.exit(1);
752
- }
753
- });
754
- // Guardrails init command
755
- program
756
- .command('guardrails-init')
757
- .description('Create a guardrails.yaml configuration file')
758
- .option('-o, --output <path>', 'Output path', 'guardrails.yaml')
759
- .action((options) => {
760
- try {
761
- const outputPath = options.output;
762
- if (fs.existsSync(outputPath)) {
763
- console.error(`File already exists: ${outputPath}`);
764
- console.error('Use --output to specify a different path.');
605
+ catch (error) {
606
+ console.error('Deps analysis failed:', error instanceof Error ? error.message : error);
765
607
  process.exit(1);
766
608
  }
767
- createGuardrailsConfig(outputPath);
768
- console.log(`Created: ${outputPath}`);
769
- console.log('\nNext steps:');
770
- console.log(' 1. Review and customize the guardrails configuration');
771
- console.log(' 2. Run: micro-contracts check');
772
- }
773
- catch (error) {
774
- console.error('Failed to create guardrails config:', error instanceof Error ? error.message : error);
775
- process.exit(1);
776
- }
777
- });
778
- // Manifest command
779
- program
780
- .command('manifest')
781
- .description('Generate or verify manifest for generated artifacts')
782
- .option('-d, --dir <path>', 'Directory to scan', 'packages/')
783
- .option('--verify', 'Verify existing manifest')
784
- .option('-o, --output <path>', 'Output manifest path (default: {dir}/.generated-manifest.json)')
785
- .action(async (options) => {
786
- try {
787
- const baseDir = options.dir;
788
- if (!fs.existsSync(baseDir)) {
789
- console.error(`Directory not found: ${baseDir}`);
609
+ },
610
+ // ── guardrails-init ───────────────────────────────────
611
+ guardrailsInit: async (opts) => {
612
+ try {
613
+ const outputPath = opts.output ?? 'guardrails.yaml';
614
+ if (fs.existsSync(outputPath)) {
615
+ console.error(`File already exists: ${outputPath}`);
616
+ console.error('Use --output to specify a different path.');
617
+ process.exit(1);
618
+ }
619
+ createGuardrailsConfig(outputPath);
620
+ console.log(`Created: ${outputPath}`);
621
+ console.log('\nNext steps:');
622
+ console.log(' 1. Review and customize the guardrails configuration');
623
+ console.log(' 2. Run: micro-contracts check');
624
+ }
625
+ catch (error) {
626
+ console.error('Failed to create guardrails config:', error instanceof Error ? error.message : error);
790
627
  process.exit(1);
791
628
  }
792
- if (options.verify) {
793
- // Verify mode
794
- const { verifyManifest, formatManifestResult } = await import('./guardrails/index.js');
795
- const result = await verifyManifest(baseDir);
796
- console.log(formatManifestResult(result));
797
- if (!result.valid) {
629
+ },
630
+ // ── manifest ──────────────────────────────────────────
631
+ manifest: async (opts) => {
632
+ try {
633
+ const baseDir = opts.dir ?? 'packages/';
634
+ if (!fs.existsSync(baseDir)) {
635
+ console.error(`Directory not found: ${baseDir}`);
798
636
  process.exit(1);
799
637
  }
800
- }
801
- else {
802
- // Generate mode
803
- console.log(`Generating manifest for: ${baseDir}`);
804
- const { manifest, changed } = await generateManifest(baseDir, {
805
- generatorVersion: pkg.version,
806
- });
807
- const fileCount = Object.keys(manifest.files).length;
808
- if (changed) {
809
- const manifestPath = writeManifest(manifest, baseDir);
810
- console.log(`Manifest updated: ${manifestPath} (${fileCount} files)`);
638
+ if (opts.verify) {
639
+ const { verifyManifest, formatManifestResult } = await import('./guardrails/index.js');
640
+ const result = await verifyManifest(baseDir);
641
+ console.log(formatManifestResult(result));
642
+ if (!result.valid) {
643
+ process.exit(1);
644
+ }
811
645
  }
812
646
  else {
813
- console.log(`Manifest unchanged (${fileCount} files)`);
647
+ console.log(`Generating manifest for: ${baseDir}`);
648
+ const { manifest, changed } = await generateManifest(baseDir, {
649
+ generatorVersion: pkg.version,
650
+ });
651
+ const fileCount = Object.keys(manifest.files).length;
652
+ if (changed) {
653
+ const manifestPath = writeManifest(manifest, baseDir);
654
+ console.log(`Manifest updated: ${manifestPath} (${fileCount} files)`);
655
+ }
656
+ else {
657
+ console.log(`Manifest unchanged (${fileCount} files)`);
658
+ }
659
+ }
660
+ }
661
+ catch (error) {
662
+ console.error('Manifest operation failed:', error instanceof Error ? error.message : error);
663
+ process.exit(1);
664
+ }
665
+ },
666
+ // ── audit-openapi (LLM) ──────────────────────────────
667
+ auditOpenapi: async (opts) => {
668
+ return commandAuditOpenapi({
669
+ config: opts.config,
670
+ module: opts.module,
671
+ adapter: opts.adapter,
672
+ model: opts.model,
673
+ showPrompt: opts.showPrompt,
674
+ failOn: opts.failOn,
675
+ output: opts.output,
676
+ reportFormat: opts.reportFormat,
677
+ });
678
+ },
679
+ // ── review-published (LLM) ───────────────────────────
680
+ reviewPublished: async (opts) => {
681
+ return commandReviewPublished({
682
+ config: opts.config,
683
+ module: opts.module,
684
+ adapter: opts.adapter,
685
+ model: opts.model,
686
+ showPrompt: opts.showPrompt,
687
+ failOn: opts.failOn,
688
+ output: opts.output,
689
+ reportFormat: opts.reportFormat,
690
+ });
691
+ },
692
+ // ── propose-overlays (LLM) ───────────────────────────
693
+ proposeOverlays: async (opts) => {
694
+ return commandProposeOverlays({
695
+ config: opts.config,
696
+ module: opts.module,
697
+ adapter: opts.adapter,
698
+ model: opts.model,
699
+ showPrompt: opts.showPrompt,
700
+ failOn: opts.failOn,
701
+ output: opts.output,
702
+ reportFormat: opts.reportFormat,
703
+ });
704
+ },
705
+ // ── audit-guardrails (LLM) ───────────────────────────
706
+ auditGuardrails: async (opts) => {
707
+ return commandAuditGuardrails({
708
+ config: opts.config,
709
+ guardrails: opts.guardrails,
710
+ adapter: opts.adapter,
711
+ model: opts.model,
712
+ showPrompt: opts.showPrompt,
713
+ failOn: opts.failOn,
714
+ output: opts.output,
715
+ reportFormat: opts.reportFormat,
716
+ });
717
+ },
718
+ };
719
+ createProgram(handlers, pkg.version).parse();
720
+ // =============================================================================
721
+ // Helper functions (used by init, deps commands)
722
+ // =============================================================================
723
+ function processOpenAPIWithExtensions(openapiPath) {
724
+ const content = fs.readFileSync(openapiPath, 'utf-8');
725
+ const spec = yaml.parse(content);
726
+ const stats = { servicesAdded: 0, methodsAdded: 0, services: new Set() };
727
+ const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
728
+ if (spec.paths) {
729
+ for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
730
+ if (!pathItem || typeof pathItem !== 'object')
731
+ continue;
732
+ const service = inferServiceFromPath(pathKey);
733
+ for (const method of httpMethods) {
734
+ const operation = pathItem[method];
735
+ if (!operation || typeof operation !== 'object')
736
+ continue;
737
+ const op = operation;
738
+ if (!op['x-micro-contracts-service'] && service) {
739
+ op['x-micro-contracts-service'] = service;
740
+ stats.servicesAdded++;
741
+ stats.services.add(service);
742
+ }
743
+ if (!op['x-micro-contracts-method']) {
744
+ const methodName = op.operationId
745
+ ? String(op.operationId)
746
+ : inferMethodName(method, pathKey);
747
+ op['x-micro-contracts-method'] = methodName;
748
+ stats.methodsAdded++;
749
+ }
814
750
  }
815
751
  }
816
752
  }
817
- catch (error) {
818
- console.error('Manifest operation failed:', error instanceof Error ? error.message : error);
819
- process.exit(1);
753
+ const output = yaml.stringify(spec, { indent: 2 });
754
+ return { yaml: output, stats: { ...stats, services: Array.from(stats.services) } };
755
+ }
756
+ function inferServiceFromPath(pathKey) {
757
+ const normalized = pathKey.replace(/^\/api\//, '/').replace(/^\/v\d+\//, '/');
758
+ const segments = normalized.split('/').filter(Boolean);
759
+ if (segments.length === 0)
760
+ return null;
761
+ const firstSegment = segments[0];
762
+ if (firstSegment.startsWith('{'))
763
+ return null;
764
+ const words = firstSegment.replace(/-/g, '_').split('_');
765
+ const pascalCase = words
766
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
767
+ .join('');
768
+ if (pascalCase.endsWith('s') && !pascalCase.endsWith('ss')) {
769
+ return pascalCase.slice(0, -1);
820
770
  }
821
- });
822
- program.parse();
823
- /**
824
- * Find config file in current directory
825
- */
826
- function findConfigFile() {
827
- const candidates = [
828
- 'micro-contracts.config.yaml',
829
- 'micro-contracts.config.yml',
830
- 'api-framework.config.yaml', // Legacy name
831
- 'api-framework.config.yml',
832
- ];
833
- for (const candidate of candidates) {
834
- const configPath = path.resolve(candidate);
835
- if (fs.existsSync(configPath)) {
836
- return configPath;
837
- }
771
+ return pascalCase;
772
+ }
773
+ function inferMethodName(httpMethod, pathKey) {
774
+ const segments = pathKey
775
+ .replace(/^\/api\//, '/')
776
+ .replace(/^\/v\d+\//, '/')
777
+ .split('/')
778
+ .filter(Boolean);
779
+ const hasIdParam = segments.some(s => s.startsWith('{'));
780
+ const resourceSegments = segments.filter(s => !s.startsWith('{'));
781
+ const resourceName = resourceSegments
782
+ .map((seg) => {
783
+ const words = seg.replace(/-/g, '_').split('_');
784
+ return words
785
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
786
+ .join('');
787
+ })
788
+ .join('');
789
+ const singularName = resourceName.endsWith('s') && !resourceName.endsWith('ss')
790
+ ? resourceName.slice(0, -1)
791
+ : resourceName;
792
+ switch (httpMethod.toLowerCase()) {
793
+ case 'get': return hasIdParam ? `get${singularName}ById` : `get${resourceName}`;
794
+ case 'post': return `create${singularName}`;
795
+ case 'put': return `update${singularName}`;
796
+ case 'patch': return `patch${singularName}`;
797
+ case 'delete': return `delete${singularName}`;
798
+ default: return `${httpMethod.toLowerCase()}${resourceName}`;
838
799
  }
839
- return null;
840
800
  }
841
801
  function generateDbTemplate() {
842
- return `/**
843
- * Database connection for this module
844
- */
845
-
846
- import pg from 'pg';
802
+ return `import pg from 'pg';
847
803
  import { DBModel, PostgresDriver } from 'litedbmodel';
848
804
 
849
805
  const { Pool } = pg;
850
806
 
851
807
  let pool: pg.Pool | null = null;
852
808
 
853
- /**
854
- * Get database connection pool
855
- */
856
809
  export function getPool(): pg.Pool {
857
810
  if (!pool) {
858
811
  pool = new Pool({
@@ -865,16 +818,9 @@ export function getPool(): pg.Pool {
865
818
  return pool;
866
819
  }
867
820
 
868
- /**
869
- * Initialize database connection
870
- */
871
821
  export async function initializeDb(): Promise<void> {
872
822
  const p = getPool();
873
-
874
- // Set litedbmodel driver
875
823
  DBModel.setDriver(new PostgresDriver(p));
876
-
877
- // Test connection
878
824
  const client = await p.connect();
879
825
  try {
880
826
  await client.query('SELECT 1');
@@ -883,9 +829,6 @@ export async function initializeDb(): Promise<void> {
883
829
  }
884
830
  }
885
831
 
886
- /**
887
- * Close database connection
888
- */
889
832
  export async function closeDb(): Promise<void> {
890
833
  if (pool) {
891
834
  await pool.end();
@@ -893,9 +836,6 @@ export async function closeDb(): Promise<void> {
893
836
  }
894
837
  }
895
838
 
896
- /**
897
- * Test database connection
898
- */
899
839
  export async function testConnection(): Promise<boolean> {
900
840
  try {
901
841
  const p = getPool();
@@ -914,17 +854,7 @@ export async function testConnection(): Promise<boolean> {
914
854
  }
915
855
  function generateContainerTemplate(moduleName) {
916
856
  const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
917
- return `/**
918
- * Module container (Dependency Injection)
919
- */
920
-
921
- import { testConnection, closeDb } from './db.js';
922
-
923
- // Import service implementations
924
- // import { ExampleService } from './services/ExampleService.js';
925
-
926
- // Import service interfaces from contract
927
- // import type { ExampleServiceApi } from '@project/contract/${moduleName}/services';
857
+ return `import { testConnection, closeDb } from './db.js';
928
858
 
929
859
  export interface ${pascalName}Services {
930
860
  // example: ExampleServiceApi;
@@ -937,9 +867,7 @@ export interface ${pascalName}ModuleContainer {
937
867
  }
938
868
 
939
869
  export async function initialize${pascalName}Module(): Promise<${pascalName}ModuleContainer> {
940
- const services: ${pascalName}Services = {
941
- // example: new ExampleService(),
942
- };
870
+ const services: ${pascalName}Services = {};
943
871
 
944
872
  return {
945
873
  services,
@@ -949,206 +877,39 @@ export async function initialize${pascalName}Module(): Promise<${pascalName}Modu
949
877
  }
950
878
  `;
951
879
  }
952
- /**
953
- * Process OpenAPI file and auto-add x-micro-contracts-service/method extensions
954
- */
955
- function processOpenAPIWithExtensions(openapiPath) {
956
- const content = fs.readFileSync(openapiPath, 'utf-8');
957
- const spec = yaml.parse(content);
958
- const stats = {
959
- servicesAdded: 0,
960
- methodsAdded: 0,
961
- services: new Set(),
962
- };
963
- const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
964
- if (spec.paths) {
965
- for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
966
- if (!pathItem || typeof pathItem !== 'object')
967
- continue;
968
- // Infer service from path: /api/users/{id} → User
969
- const service = inferServiceFromPath(pathKey);
970
- for (const method of httpMethods) {
971
- const operation = pathItem[method];
972
- if (!operation || typeof operation !== 'object')
973
- continue;
974
- const op = operation;
975
- // Add x-micro-contracts-service if not present
976
- if (!op['x-micro-contracts-service'] && service) {
977
- op['x-micro-contracts-service'] = service;
978
- stats.servicesAdded++;
979
- stats.services.add(service);
980
- }
981
- // Add x-micro-contracts-method if not present
982
- if (!op['x-micro-contracts-method']) {
983
- // Use operationId if available, otherwise generate
984
- const methodName = op.operationId
985
- ? String(op.operationId)
986
- : inferMethodName(method, pathKey);
987
- op['x-micro-contracts-method'] = methodName;
988
- stats.methodsAdded++;
989
- }
990
- }
991
- }
992
- }
993
- // Convert back to YAML with proper formatting
994
- const output = yaml.stringify(spec, {
995
- indent: 2,
996
- });
997
- return {
998
- yaml: output,
999
- stats: {
1000
- servicesAdded: stats.servicesAdded,
1001
- methodsAdded: stats.methodsAdded,
1002
- services: Array.from(stats.services),
1003
- },
1004
- };
1005
- }
1006
- /**
1007
- * Infer service name from API path
1008
- * /api/users → User
1009
- * /api/users/{id} → User
1010
- * /api/user-profiles → UserProfile
1011
- * /api/v1/accounts → Account
1012
- */
1013
- function inferServiceFromPath(pathKey) {
1014
- // Remove /api prefix and version prefix
1015
- const normalized = pathKey
1016
- .replace(/^\/api\//, '/')
1017
- .replace(/^\/v\d+\//, '/');
1018
- // Get first path segment
1019
- const segments = normalized.split('/').filter(Boolean);
1020
- if (segments.length === 0)
1021
- return null;
1022
- const firstSegment = segments[0];
1023
- // Skip path parameters
1024
- if (firstSegment.startsWith('{'))
1025
- return null;
1026
- // Convert to PascalCase singular
1027
- // users → User
1028
- // user-profiles → UserProfile
1029
- // accounts → Account
1030
- const words = firstSegment
1031
- .replace(/-/g, '_')
1032
- .split('_');
1033
- const pascalCase = words
1034
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1035
- .join('');
1036
- // Remove trailing 's' for plural (simple heuristic)
1037
- if (pascalCase.endsWith('s') && !pascalCase.endsWith('ss')) {
1038
- return pascalCase.slice(0, -1);
1039
- }
1040
- return pascalCase;
1041
- }
1042
- /**
1043
- * Infer method name from HTTP method and path
1044
- * GET /users → getUsers
1045
- * GET /users/{id} → getUserById
1046
- * POST /users → createUser
1047
- * PUT /users/{id} → updateUser
1048
- * DELETE /users/{id} → deleteUser
1049
- */
1050
- function inferMethodName(httpMethod, pathKey) {
1051
- // Get path segments without parameters
1052
- const segments = pathKey
1053
- .replace(/^\/api\//, '/')
1054
- .replace(/^\/v\d+\//, '/')
1055
- .split('/')
1056
- .filter(Boolean);
1057
- const hasIdParam = segments.some(s => s.startsWith('{'));
1058
- const resourceSegments = segments.filter(s => !s.startsWith('{'));
1059
- // Build resource name from segments
1060
- const resourceName = resourceSegments
1061
- .map((seg, i) => {
1062
- const words = seg.replace(/-/g, '_').split('_');
1063
- return words
1064
- .map((word, j) => {
1065
- // First word of first segment: lowercase for method prefix
1066
- if (i === 0 && j === 0) {
1067
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
1068
- }
1069
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
1070
- })
1071
- .join('');
1072
- })
1073
- .join('');
1074
- // Make singular for single-resource operations
1075
- const singularName = resourceName.endsWith('s') && !resourceName.endsWith('ss')
1076
- ? resourceName.slice(0, -1)
1077
- : resourceName;
1078
- // Map HTTP method to action
1079
- switch (httpMethod.toLowerCase()) {
1080
- case 'get':
1081
- return hasIdParam
1082
- ? `get${singularName}ById`
1083
- : `get${resourceName}`;
1084
- case 'post':
1085
- return `create${singularName}`;
1086
- case 'put':
1087
- return `update${singularName}`;
1088
- case 'patch':
1089
- return `patch${singularName}`;
1090
- case 'delete':
1091
- return `delete${singularName}`;
1092
- default:
1093
- return `${httpMethod.toLowerCase()}${resourceName}`;
1094
- }
1095
- }
1096
880
  function generateConfigTemplate(moduleName) {
1097
- // Note: Using explicit string concatenation to ensure correct YAML indentation
1098
- const yaml = [
1099
- '# micro-contracts Configuration',
1100
- '',
1101
- '# Common settings (defaults for all modules)',
1102
- 'defaults:',
1103
- ' contract:',
1104
- ' output: packages/contract/{module}',
1105
- '',
1106
- ' contractPublic:',
1107
- ' output: packages/contract-published/{module}',
1108
- '',
1109
- ' # Template-based outputs',
1110
- ' outputs:',
1111
- ' server-routes:',
1112
- ' output: server/src/{module}/routes.generated.ts',
1113
- ' template: fastify-routes.hbs',
1114
- ' config:',
1115
- ' servicesPath: fastify.services.{module}',
1116
- '',
1117
- ' frontend-api:',
1118
- ' output: frontend/src/{module}/api.generated.ts',
1119
- ' template: fetch-client.hbs',
1120
- '',
1121
- ' shared-client:',
1122
- ' output: frontend/src/shared/{module}.api.generated.ts',
1123
- ' template: fetch-client.hbs',
1124
- ' condition: hasPublicEndpoints',
1125
- ' config:',
1126
- ' contractPackage: "@project/contract-published/{module}"',
1127
- '',
1128
- ' # Overlay configuration',
1129
- ' overlays:',
1130
- ' shared:',
1131
- ' - spec/_shared/overlays/middleware.overlay.yaml',
1132
- ' collision: error',
1133
- '',
1134
- ' docs:',
1135
- ' enabled: true',
1136
- '',
1137
- '# Module definitions',
1138
- 'modules:',
1139
- ` ${moduleName}:`,
1140
- ` openapi: spec/${moduleName}/openapi/${moduleName}.yaml`,
1141
- '',
881
+ return [
882
+ '# micro-contracts Configuration', '',
883
+ 'defaults:', ' contract:', ' output: packages/contract/{module}', '',
884
+ ' contractPublic:', ' output: packages/contract-published/{module}', '',
885
+ ' outputs:', ' server-routes:', ' output: server/src/{module}/routes.generated.ts',
886
+ ' template: fastify-routes.hbs', ' config:',
887
+ ' servicesPath: fastify.services.{module}', '',
888
+ ' frontend-api:', ' output: frontend/src/{module}/api.generated.ts',
889
+ ' template: fetch-client.hbs', '',
890
+ ' shared-client:', ' output: frontend/src/shared/{module}.api.generated.ts',
891
+ ' template: fetch-client.hbs', ' condition: hasPublicEndpoints', ' config:',
892
+ ' contractPackage: "@project/contract-published/{module}"', '',
893
+ ' overlays:', ' shared:', ' - spec/_shared/overlays/middleware.overlay.yaml',
894
+ ' collision: error', '', ' docs:', ' enabled: true', '',
895
+ 'modules:', ` ${moduleName}:`, ` openapi: spec/${moduleName}/openapi/${moduleName}.yaml`, '',
896
+ ].join('\n');
897
+ }
898
+ function generateScreenConfigTemplate(moduleName) {
899
+ return [
900
+ '# micro-contracts Configuration (Screen Spec)', '',
901
+ 'defaults:', ' contract:', ' output: packages/contract/{module}', '',
902
+ ' docs:', ' enabled: false', '',
903
+ 'modules:', ` ${moduleName}:`, ` openapi: spec/${moduleName}/openapi/${moduleName}.yaml`,
904
+ ' screen: true', ' outputs:', ' screen-navigation:',
905
+ ` output: frontend/src/screens/navigation.generated.ts`,
906
+ ' template: screen-navigation.hbs', ' screen-events:',
907
+ ` output: frontend/src/screens/events.generated.ts`,
908
+ ' template: screen-events.hbs', '',
1142
909
  ].join('\n');
1143
- return yaml;
1144
910
  }
1145
- /**
1146
- * Generate ProblemDetails schema
1147
- */
1148
911
  function generateProblemDetailsSchema() {
1149
912
  return `# RFC 9457 Problem Details
1150
- # https://www.rfc-editor.org/rfc/rfc9457.html
1151
-
1152
913
  components:
1153
914
  schemas:
1154
915
  ProblemDetails:
@@ -1158,29 +919,21 @@ components:
1158
919
  type:
1159
920
  type: string
1160
921
  format: uri
1161
- description: "Error type URI (e.g., /errors/validation)"
1162
922
  title:
1163
923
  type: string
1164
- description: "Short human-readable summary"
1165
924
  status:
1166
925
  type: integer
1167
- description: "HTTP status code"
1168
926
  detail:
1169
927
  type: string
1170
- description: "Detailed explanation"
1171
928
  instance:
1172
929
  type: string
1173
930
  format: uri
1174
- description: "URI of the specific occurrence"
1175
931
  code:
1176
932
  type: string
1177
- description: "Machine-readable error code (SCREAMING_SNAKE)"
1178
933
  traceId:
1179
934
  type: string
1180
- description: "Correlation ID for tracing"
1181
935
  errors:
1182
936
  type: array
1183
- description: "Validation errors (field-level)"
1184
937
  items:
1185
938
  type: object
1186
939
  properties:
@@ -1188,54 +941,12 @@ components:
1188
941
  type: string
1189
942
  message:
1190
943
  type: string
1191
-
1192
- responses:
1193
- BadRequest:
1194
- description: Bad Request
1195
- content:
1196
- application/json:
1197
- schema:
1198
- $ref: '#/components/schemas/ProblemDetails'
1199
-
1200
- Unauthorized:
1201
- description: Unauthorized
1202
- content:
1203
- application/json:
1204
- schema:
1205
- $ref: '#/components/schemas/ProblemDetails'
1206
-
1207
- Forbidden:
1208
- description: Forbidden
1209
- content:
1210
- application/json:
1211
- schema:
1212
- $ref: '#/components/schemas/ProblemDetails'
1213
-
1214
- NotFound:
1215
- description: Not Found
1216
- content:
1217
- application/json:
1218
- schema:
1219
- $ref: '#/components/schemas/ProblemDetails'
1220
-
1221
- InternalError:
1222
- description: Internal Server Error
1223
- content:
1224
- application/json:
1225
- schema:
1226
- $ref: '#/components/schemas/ProblemDetails'
1227
944
  `;
1228
945
  }
1229
- /**
1230
- * Generate Spectral lint rules
1231
- */
1232
946
  function generateSpectralRules() {
1233
- return `# micro-contracts Spectral Rules
1234
-
1235
- extends: ["spectral:oas"]
947
+ return `extends: ["spectral:oas"]
1236
948
 
1237
949
  rules:
1238
- # Require x-micro-contracts-service on all operations
1239
950
  operation-service:
1240
951
  description: "Operations must have x-micro-contracts-service"
1241
952
  severity: error
@@ -1244,7 +955,6 @@ rules:
1244
955
  field: x-micro-contracts-service
1245
956
  function: truthy
1246
957
 
1247
- # Require x-micro-contracts-method on all operations
1248
958
  operation-method:
1249
959
  description: "Operations must have x-micro-contracts-method"
1250
960
  severity: error
@@ -1253,7 +963,6 @@ rules:
1253
963
  field: x-micro-contracts-method
1254
964
  function: truthy
1255
965
 
1256
- # Require error responses
1257
966
  operation-error-responses:
1258
967
  description: "Operations should have 5XX or default error response"
1259
968
  severity: warn
@@ -1267,7 +976,6 @@ rules:
1267
976
  - required: ["5XX"]
1268
977
  - required: ["default"]
1269
978
 
1270
- # Enforce canonical extension names
1271
979
  canonical-extension-prefix:
1272
980
  description: "Use canonical x-micro-contracts-* extensions"
1273
981
  severity: warn
@@ -1275,29 +983,93 @@ rules:
1275
983
  then:
1276
984
  - field: x-service
1277
985
  function: falsy
1278
- description: "Use x-micro-contracts-service instead of x-service"
1279
986
  - field: x-method
1280
987
  function: falsy
1281
- description: "Use x-micro-contracts-method instead of x-method"
1282
988
  - field: x-public
1283
989
  function: falsy
1284
- description: "Use x-micro-contracts-published instead of x-public"
1285
990
  `;
1286
991
  }
1287
- // =============================================================================
1288
- // Dependency Analysis Helpers
1289
- // =============================================================================
992
+ function generateScreenSpecTemplate(moduleName) {
993
+ const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
994
+ return `openapi: '3.1.0'
995
+ info:
996
+ title: ${pascalName} Screen Specification
997
+ version: '0.1.0'
998
+ description: |
999
+ Screen contract for ${moduleName} domain.
1000
+ servers:
1001
+ - url: /
1002
+ description: Screen routes (client-side)
1003
+ paths:
1004
+ /home:
1005
+ get:
1006
+ operationId: renderHomePage
1007
+ security: [{session: []}]
1008
+ x-screen-const: HOME
1009
+ x-screen-id: SCR-001
1010
+ x-screen-name: HomePage
1011
+ x-back-navigation: false
1012
+ summary: Render home page
1013
+ responses:
1014
+ '200':
1015
+ description: Home page ViewModel
1016
+ content:
1017
+ application/json:
1018
+ schema:
1019
+ \$ref: '#/components/schemas/HomePageViewModel'
1020
+ links:
1021
+ goToSettings:
1022
+ operationId: renderSettingsPage
1023
+ /settings:
1024
+ get:
1025
+ operationId: renderSettingsPage
1026
+ security: [{session: []}]
1027
+ x-screen-const: SETTINGS
1028
+ x-screen-id: SCR-002
1029
+ x-screen-name: SettingsPage
1030
+ x-back-navigation: true
1031
+ summary: Render settings page
1032
+ responses:
1033
+ '200':
1034
+ description: Settings page ViewModel
1035
+ content:
1036
+ application/json:
1037
+ schema:
1038
+ \$ref: '#/components/schemas/SettingsPageViewModel'
1039
+ links:
1040
+ goToHome:
1041
+ operationId: renderHomePage
1042
+ components:
1043
+ securitySchemes:
1044
+ session:
1045
+ type: apiKey
1046
+ in: cookie
1047
+ name: session
1048
+ schemas:
1049
+ HomePageViewModel:
1050
+ type: object
1051
+ required: [greeting]
1052
+ properties:
1053
+ greeting:
1054
+ type: string
1055
+ SettingsPageViewModel:
1056
+ type: object
1057
+ required: [theme]
1058
+ properties:
1059
+ theme:
1060
+ type: string
1061
+ enum: [light, dark]
1062
+ `;
1063
+ }
1290
1064
  function outputDependencyGraph(moduleDeps) {
1291
1065
  console.log('```mermaid');
1292
1066
  console.log('graph LR');
1293
1067
  for (const [moduleName, { deps }] of moduleDeps) {
1294
- // Group deps by target module
1295
1068
  const moduleTargets = new Set();
1296
1069
  for (const dep of deps) {
1297
1070
  const parts = dep.split('.');
1298
- if (parts.length >= 1) {
1071
+ if (parts.length >= 1)
1299
1072
  moduleTargets.add(parts[0]);
1300
- }
1301
1073
  }
1302
1074
  for (const target of moduleTargets) {
1303
1075
  console.log(` ${moduleName} --> ${target}`);
@@ -1309,40 +1081,35 @@ function outputImpactAnalysis(moduleDeps, ref) {
1309
1081
  console.log(`Impacted by changes to ${ref}:\n`);
1310
1082
  const impacted = [];
1311
1083
  for (const [moduleName, { deps }] of moduleDeps) {
1312
- if (deps.includes(ref)) {
1084
+ if (deps.includes(ref))
1313
1085
  impacted.push(moduleName);
1314
- }
1315
1086
  }
1316
1087
  if (impacted.length === 0) {
1317
1088
  console.log(' No modules depend on this API.');
1318
1089
  }
1319
1090
  else {
1320
- for (const m of impacted) {
1091
+ for (const m of impacted)
1321
1092
  console.log(` - ${m}`);
1322
- }
1323
1093
  }
1324
1094
  }
1325
1095
  function outputWhoDependsOn(moduleDeps, ref) {
1326
1096
  console.log(`Modules that depend on ${ref}:\n`);
1327
1097
  const dependent = [];
1328
1098
  for (const [moduleName, { deps }] of moduleDeps) {
1329
- if (deps.some(d => d.startsWith(ref))) {
1099
+ if (deps.some(d => d.startsWith(ref)))
1330
1100
  dependent.push(moduleName);
1331
- }
1332
1101
  }
1333
1102
  if (dependent.length === 0) {
1334
1103
  console.log(' None found.');
1335
1104
  }
1336
1105
  else {
1337
- for (const m of dependent) {
1106
+ for (const m of dependent)
1338
1107
  console.log(` - ${m}`);
1339
- }
1340
1108
  }
1341
1109
  }
1342
1110
  function validateDependencies(moduleDeps) {
1343
1111
  let hasErrors = false;
1344
1112
  for (const [moduleName, { openApiDeps, configDeps }] of moduleDeps) {
1345
- // Check that configDeps is subset of openApiDeps
1346
1113
  for (const dep of configDeps) {
1347
1114
  if (!openApiDeps.includes(dep)) {
1348
1115
  console.error(`ERROR: ${moduleName}.dependsOn includes '${dep}'`);
@@ -1366,124 +1133,10 @@ function outputAllDependencies(moduleDeps) {
1366
1133
  console.log(' (no dependencies)');
1367
1134
  }
1368
1135
  else {
1369
- for (const dep of deps) {
1136
+ for (const dep of deps)
1370
1137
  console.log(` - ${dep}`);
1371
- }
1372
1138
  }
1373
1139
  console.log();
1374
1140
  }
1375
1141
  }
1376
- /**
1377
- * Generate starter screen spec YAML
1378
- */
1379
- function generateScreenSpecTemplate(moduleName) {
1380
- const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
1381
- return `openapi: '3.1.0'
1382
- info:
1383
- title: ${pascalName} Screen Specification
1384
- version: '0.1.0'
1385
- description: |
1386
- Screen contract for ${moduleName} domain. This is a screen specification, not a server API.
1387
- Defines ViewModels, navigation links, and analytics events per screen.
1388
- servers:
1389
- - url: /
1390
- description: Screen routes (client-side)
1391
- paths:
1392
- /home:
1393
- get:
1394
- operationId: renderHomePage
1395
- security: [{session: []}]
1396
- x-screen-const: HOME
1397
- x-screen-id: SCR-001
1398
- x-screen-name: HomePage
1399
- x-back-navigation: false
1400
- x-events:
1401
- - name: home_view
1402
- type: screen_view
1403
- method: trackView
1404
- summary: Render home page
1405
- responses:
1406
- '200':
1407
- description: Home page ViewModel
1408
- content:
1409
- application/json:
1410
- schema:
1411
- $ref: '#/components/schemas/HomePageViewModel'
1412
- links:
1413
- goToSettings:
1414
- operationId: renderSettingsPage
1415
- /settings:
1416
- get:
1417
- operationId: renderSettingsPage
1418
- security: [{session: []}]
1419
- x-screen-const: SETTINGS
1420
- x-screen-id: SCR-002
1421
- x-screen-name: SettingsPage
1422
- x-back-navigation: true
1423
- x-events:
1424
- - name: settings_view
1425
- type: screen_view
1426
- method: trackView
1427
- summary: Render settings page
1428
- responses:
1429
- '200':
1430
- description: Settings page ViewModel
1431
- content:
1432
- application/json:
1433
- schema:
1434
- $ref: '#/components/schemas/SettingsPageViewModel'
1435
- links:
1436
- goToHome:
1437
- operationId: renderHomePage
1438
- components:
1439
- securitySchemes:
1440
- session:
1441
- type: apiKey
1442
- in: cookie
1443
- name: session
1444
- schemas:
1445
- HomePageViewModel:
1446
- type: object
1447
- required: [greeting]
1448
- properties:
1449
- greeting:
1450
- type: string
1451
- SettingsPageViewModel:
1452
- type: object
1453
- required: [theme]
1454
- properties:
1455
- theme:
1456
- type: string
1457
- enum: [light, dark]
1458
- `;
1459
- }
1460
- /**
1461
- * Generate config template for screen module
1462
- */
1463
- function generateScreenConfigTemplate(moduleName) {
1464
- return [
1465
- '# micro-contracts Configuration (Screen Spec)',
1466
- '',
1467
- 'defaults:',
1468
- ' contract:',
1469
- ' output: packages/contract/{module}',
1470
- '',
1471
- ' docs:',
1472
- ' enabled: false',
1473
- '',
1474
- 'modules:',
1475
- ` ${moduleName}:`,
1476
- ` openapi: spec/${moduleName}/openapi/${moduleName}.yaml`,
1477
- ' screen: true',
1478
- ' outputs:',
1479
- ' screen-navigation:',
1480
- ` output: frontend/src/screens/navigation.generated.ts`,
1481
- ' template: screen-navigation.hbs',
1482
- ' screen-events:',
1483
- ` output: frontend/src/screens/events.generated.ts`,
1484
- ' template: screen-events.hbs',
1485
- '',
1486
- ].join('\n');
1487
- }
1488
- // Starter templates are imported from ./cli/templates.js
1489
1142
  //# sourceMappingURL=cli.js.map