micro-contracts 0.14.4 → 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.
- package/README.md +62 -0
- package/cli-contract.yaml +706 -69
- package/dist/agents/context-builder.d.ts +5 -0
- package/dist/agents/context-builder.d.ts.map +1 -0
- package/dist/agents/context-builder.js +239 -0
- package/dist/agents/context-builder.js.map +1 -0
- package/dist/agents/formatter.d.ts +9 -0
- package/dist/agents/formatter.d.ts.map +1 -0
- package/dist/agents/formatter.js +124 -0
- package/dist/agents/formatter.js.map +1 -0
- package/dist/agents/index.d.ts +5 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +3 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/orchestrator.d.ts +5 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.js +105 -0
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/types.d.ts +21 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cli.d.ts +0 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +814 -1161
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit-guardrails.d.ts +13 -0
- package/dist/commands/audit-guardrails.d.ts.map +1 -0
- package/dist/commands/audit-guardrails.js +32 -0
- package/dist/commands/audit-guardrails.js.map +1 -0
- package/dist/commands/audit-openapi.d.ts +13 -0
- package/dist/commands/audit-openapi.d.ts.map +1 -0
- package/dist/commands/audit-openapi.js +32 -0
- package/dist/commands/audit-openapi.js.map +1 -0
- package/dist/commands/propose-overlays.d.ts +13 -0
- package/dist/commands/propose-overlays.d.ts.map +1 -0
- package/dist/commands/propose-overlays.js +32 -0
- package/dist/commands/propose-overlays.js.map +1 -0
- package/dist/commands/review-published.d.ts +13 -0
- package/dist/commands/review-published.d.ts.map +1 -0
- package/dist/commands/review-published.js +32 -0
- package/dist/commands/review-published.js.map +1 -0
- package/dist/generated/commands.d.ts +2 -0
- package/dist/generated/commands.d.ts.map +1 -0
- package/dist/generated/commands.js +2 -0
- package/dist/generated/commands.js.map +1 -0
- package/dist/generated/dsl/agents.d.ts +34 -0
- package/dist/generated/dsl/agents.d.ts.map +1 -0
- package/dist/generated/dsl/agents.js +98 -0
- package/dist/generated/dsl/agents.js.map +1 -0
- package/dist/generated/dsl/handoffs.d.ts +1209 -0
- package/dist/generated/dsl/handoffs.d.ts.map +1 -0
- package/dist/generated/dsl/handoffs.js +205 -0
- package/dist/generated/dsl/handoffs.js.map +1 -0
- package/dist/generated/dsl/index.d.ts +13 -0
- package/dist/generated/dsl/index.d.ts.map +1 -0
- package/dist/generated/dsl/index.js +10 -0
- package/dist/generated/dsl/index.js.map +1 -0
- package/dist/generated/dsl/tasks.d.ts +26 -0
- package/dist/generated/dsl/tasks.d.ts.map +1 -0
- package/dist/generated/dsl/tasks.js +133 -0
- package/dist/generated/dsl/tasks.js.map +1 -0
- package/dist/generated/dsl/workflows.d.ts +34 -0
- package/dist/generated/dsl/workflows.d.ts.map +1 -0
- package/dist/generated/dsl/workflows.js +84 -0
- package/dist/generated/dsl/workflows.js.map +1 -0
- package/dist/generated/index.d.ts +7 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +7 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/generated/policy-runtime.d.ts +114 -0
- package/dist/generated/policy-runtime.d.ts.map +1 -0
- package/dist/generated/policy-runtime.js +195 -0
- package/dist/generated/policy-runtime.js.map +1 -0
- package/dist/generated/policy.d.ts +619 -0
- package/dist/generated/policy.d.ts.map +1 -0
- package/dist/generated/policy.js +802 -0
- package/dist/generated/policy.js.map +1 -0
- package/dist/generated/program.d.ts +112 -0
- package/dist/generated/program.d.ts.map +1 -0
- package/dist/generated/program.js +266 -0
- package/dist/generated/program.js.map +1 -0
- package/dist/generated/schemas.d.ts +778 -0
- package/dist/generated/schemas.d.ts.map +1 -0
- package/dist/generated/schemas.js +1005 -0
- package/dist/generated/schemas.js.map +1 -0
- package/dist/generated/types.d.ts +395 -0
- package/dist/generated/types.d.ts.map +1 -0
- package/dist/generated/types.js +3 -0
- package/dist/generated/types.js.map +1 -0
- package/docs/cli-reference.md +328 -28
- package/package.json +21 -3
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
306
|
-
if (!
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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.
|
|
318
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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 (
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
444
|
-
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.error('Check failed:', error instanceof Error ? error.message : error);
|
|
445
319
|
process.exit(1);
|
|
446
320
|
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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('
|
|
382
|
+
console.log('┌─────────────────────────────────────────────────┐');
|
|
383
|
+
console.log('│ Step 2: Generate contracts │');
|
|
384
|
+
console.log('└─────────────────────────────────────────────────┘');
|
|
536
385
|
console.log('');
|
|
537
386
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
inputHash
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
process.stdout.
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
// Restore console.log
|
|
642
|
-
if (!verbose) {
|
|
643
|
-
console.log = originalLog;
|
|
465
|
+
finally {
|
|
466
|
+
if (!verbose)
|
|
467
|
+
console.log = originalLog;
|
|
644
468
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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('
|
|
688
|
-
console.log('
|
|
525
|
+
console.log('━'.repeat(50));
|
|
526
|
+
console.log('📊 Pipeline Summary');
|
|
527
|
+
console.log('━'.repeat(50));
|
|
689
528
|
console.log('');
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (!
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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(`
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
'
|
|
1100
|
-
'',
|
|
1101
|
-
'
|
|
1102
|
-
'
|
|
1103
|
-
'
|
|
1104
|
-
' output:
|
|
1105
|
-
'',
|
|
1106
|
-
'
|
|
1107
|
-
'
|
|
1108
|
-
'',
|
|
1109
|
-
'
|
|
1110
|
-
'
|
|
1111
|
-
'
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
'',
|
|
1117
|
-
'
|
|
1118
|
-
'
|
|
1119
|
-
'
|
|
1120
|
-
'',
|
|
1121
|
-
|
|
1122
|
-
'
|
|
1123
|
-
|
|
1124
|
-
'
|
|
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
|
|
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
|
-
|
|
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
|