micro-contracts 0.16.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-contract.yaml +0 -32
- package/dist/micro-contracts.bundle.mjs +4682 -0
- package/dist/micro-contracts.bundle.mjs.map +7 -0
- package/docs/cli-reference.md +0 -28
- package/package.json +20 -23
- package/dist/agents/context-builder.d.ts +0 -5
- package/dist/agents/context-builder.d.ts.map +0 -1
- package/dist/agents/context-builder.js +0 -239
- package/dist/agents/context-builder.js.map +0 -1
- package/dist/agents/formatter.d.ts +0 -9
- package/dist/agents/formatter.d.ts.map +0 -1
- package/dist/agents/formatter.js +0 -124
- package/dist/agents/formatter.js.map +0 -1
- package/dist/agents/index.d.ts +0 -5
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/index.js +0 -3
- package/dist/agents/index.js.map +0 -1
- package/dist/agents/orchestrator.d.ts +0 -5
- package/dist/agents/orchestrator.d.ts.map +0 -1
- package/dist/agents/orchestrator.js +0 -105
- package/dist/agents/orchestrator.js.map +0 -1
- package/dist/agents/types.d.ts +0 -21
- package/dist/agents/types.d.ts.map +0 -1
- package/dist/agents/types.js +0 -2
- package/dist/agents/types.js.map +0 -1
- package/dist/cli/templates.d.ts +0 -22
- package/dist/cli/templates.d.ts.map +0 -1
- package/dist/cli/templates.js +0 -445
- package/dist/cli/templates.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -1142
- package/dist/cli.js.map +0 -1
- package/dist/commands/audit-guardrails.d.ts +0 -13
- package/dist/commands/audit-guardrails.d.ts.map +0 -1
- package/dist/commands/audit-guardrails.js +0 -32
- package/dist/commands/audit-guardrails.js.map +0 -1
- package/dist/commands/audit-openapi.d.ts +0 -13
- package/dist/commands/audit-openapi.d.ts.map +0 -1
- package/dist/commands/audit-openapi.js +0 -32
- package/dist/commands/audit-openapi.js.map +0 -1
- package/dist/commands/propose-overlays.d.ts +0 -13
- package/dist/commands/propose-overlays.d.ts.map +0 -1
- package/dist/commands/propose-overlays.js +0 -32
- package/dist/commands/propose-overlays.js.map +0 -1
- package/dist/commands/review-published.d.ts +0 -13
- package/dist/commands/review-published.d.ts.map +0 -1
- package/dist/commands/review-published.js +0 -32
- package/dist/commands/review-published.js.map +0 -1
- package/dist/generated/commands.d.ts +0 -2
- package/dist/generated/commands.d.ts.map +0 -1
- package/dist/generated/commands.js +0 -2
- package/dist/generated/commands.js.map +0 -1
- package/dist/generated/dsl/agents.d.ts +0 -34
- package/dist/generated/dsl/agents.d.ts.map +0 -1
- package/dist/generated/dsl/agents.js +0 -98
- package/dist/generated/dsl/agents.js.map +0 -1
- package/dist/generated/dsl/handoffs.d.ts +0 -457
- package/dist/generated/dsl/handoffs.d.ts.map +0 -1
- package/dist/generated/dsl/handoffs.js +0 -205
- package/dist/generated/dsl/handoffs.js.map +0 -1
- package/dist/generated/dsl/index.d.ts +0 -13
- package/dist/generated/dsl/index.d.ts.map +0 -1
- package/dist/generated/dsl/index.js +0 -10
- package/dist/generated/dsl/index.js.map +0 -1
- package/dist/generated/dsl/tasks.d.ts +0 -26
- package/dist/generated/dsl/tasks.d.ts.map +0 -1
- package/dist/generated/dsl/tasks.js +0 -133
- package/dist/generated/dsl/tasks.js.map +0 -1
- package/dist/generated/dsl/workflows.d.ts +0 -34
- package/dist/generated/dsl/workflows.d.ts.map +0 -1
- package/dist/generated/dsl/workflows.js +0 -84
- package/dist/generated/dsl/workflows.js.map +0 -1
- package/dist/generated/index.d.ts +0 -7
- package/dist/generated/index.d.ts.map +0 -1
- package/dist/generated/index.js +0 -7
- package/dist/generated/index.js.map +0 -1
- package/dist/generated/policy-runtime.d.ts +0 -114
- package/dist/generated/policy-runtime.d.ts.map +0 -1
- package/dist/generated/policy-runtime.js +0 -195
- package/dist/generated/policy-runtime.js.map +0 -1
- package/dist/generated/policy.d.ts +0 -619
- package/dist/generated/policy.d.ts.map +0 -1
- package/dist/generated/policy.js +0 -802
- package/dist/generated/policy.js.map +0 -1
- package/dist/generated/program.d.ts +0 -112
- package/dist/generated/program.d.ts.map +0 -1
- package/dist/generated/program.js +0 -266
- package/dist/generated/program.js.map +0 -1
- package/dist/generated/schemas.d.ts +0 -778
- package/dist/generated/schemas.d.ts.map +0 -1
- package/dist/generated/schemas.js +0 -1005
- package/dist/generated/schemas.js.map +0 -1
- package/dist/generated/types.d.ts +0 -395
- package/dist/generated/types.d.ts.map +0 -1
- package/dist/generated/types.js +0 -3
- package/dist/generated/types.js.map +0 -1
- package/dist/generator/dependencyGenerator.d.ts +0 -43
- package/dist/generator/dependencyGenerator.d.ts.map +0 -1
- package/dist/generator/dependencyGenerator.js +0 -159
- package/dist/generator/dependencyGenerator.js.map +0 -1
- package/dist/generator/index.d.ts +0 -39
- package/dist/generator/index.d.ts.map +0 -1
- package/dist/generator/index.js +0 -720
- package/dist/generator/index.js.map +0 -1
- package/dist/generator/inputHash.d.ts +0 -23
- package/dist/generator/inputHash.d.ts.map +0 -1
- package/dist/generator/inputHash.js +0 -145
- package/dist/generator/inputHash.js.map +0 -1
- package/dist/generator/linter.d.ts +0 -26
- package/dist/generator/linter.d.ts.map +0 -1
- package/dist/generator/linter.js +0 -352
- package/dist/generator/linter.js.map +0 -1
- package/dist/generator/overlayProcessor.d.ts +0 -90
- package/dist/generator/overlayProcessor.d.ts.map +0 -1
- package/dist/generator/overlayProcessor.js +0 -541
- package/dist/generator/overlayProcessor.js.map +0 -1
- package/dist/generator/schemaGenerator.d.ts +0 -10
- package/dist/generator/schemaGenerator.d.ts.map +0 -1
- package/dist/generator/schemaGenerator.js +0 -299
- package/dist/generator/schemaGenerator.js.map +0 -1
- package/dist/generator/serviceGenerator.d.ts +0 -41
- package/dist/generator/serviceGenerator.d.ts.map +0 -1
- package/dist/generator/serviceGenerator.js +0 -296
- package/dist/generator/serviceGenerator.js.map +0 -1
- package/dist/generator/templateProcessor.d.ts +0 -245
- package/dist/generator/templateProcessor.d.ts.map +0 -1
- package/dist/generator/templateProcessor.js +0 -821
- package/dist/generator/templateProcessor.js.map +0 -1
- package/dist/generator/typeGenerator.d.ts +0 -9
- package/dist/generator/typeGenerator.d.ts.map +0 -1
- package/dist/generator/typeGenerator.js +0 -424
- package/dist/generator/typeGenerator.js.map +0 -1
- package/dist/generator/typeGenerator.test.d.ts +0 -2
- package/dist/generator/typeGenerator.test.d.ts.map +0 -1
- package/dist/generator/typeGenerator.test.js +0 -122
- package/dist/generator/typeGenerator.test.js.map +0 -1
- package/dist/guardrails/allowlist.d.ts +0 -45
- package/dist/guardrails/allowlist.d.ts.map +0 -1
- package/dist/guardrails/allowlist.js +0 -261
- package/dist/guardrails/allowlist.js.map +0 -1
- package/dist/guardrails/config.d.ts +0 -40
- package/dist/guardrails/config.d.ts.map +0 -1
- package/dist/guardrails/config.js +0 -174
- package/dist/guardrails/config.js.map +0 -1
- package/dist/guardrails/docs.d.ts +0 -24
- package/dist/guardrails/docs.d.ts.map +0 -1
- package/dist/guardrails/docs.js +0 -138
- package/dist/guardrails/docs.js.map +0 -1
- package/dist/guardrails/drift.d.ts +0 -23
- package/dist/guardrails/drift.d.ts.map +0 -1
- package/dist/guardrails/drift.js +0 -127
- package/dist/guardrails/drift.js.map +0 -1
- package/dist/guardrails/index.d.ts +0 -19
- package/dist/guardrails/index.d.ts.map +0 -1
- package/dist/guardrails/index.js +0 -23
- package/dist/guardrails/index.js.map +0 -1
- package/dist/guardrails/lint.d.ts +0 -20
- package/dist/guardrails/lint.d.ts.map +0 -1
- package/dist/guardrails/lint.js +0 -274
- package/dist/guardrails/lint.js.map +0 -1
- package/dist/guardrails/manifest.d.ts +0 -63
- package/dist/guardrails/manifest.d.ts.map +0 -1
- package/dist/guardrails/manifest.js +0 -299
- package/dist/guardrails/manifest.js.map +0 -1
- package/dist/guardrails/runner.d.ts +0 -43
- package/dist/guardrails/runner.d.ts.map +0 -1
- package/dist/guardrails/runner.js +0 -365
- package/dist/guardrails/runner.js.map +0 -1
- package/dist/guardrails/typecheck.d.ts +0 -15
- package/dist/guardrails/typecheck.d.ts.map +0 -1
- package/dist/guardrails/typecheck.js +0 -104
- package/dist/guardrails/typecheck.js.map +0 -1
- package/dist/guardrails/types.d.ts +0 -202
- package/dist/guardrails/types.d.ts.map +0 -1
- package/dist/guardrails/types.js +0 -8
- package/dist/guardrails/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -583
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -305
- package/dist/types.js.map +0 -1
package/dist/cli.js
DELETED
|
@@ -1,1142 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { createRequire } from 'module';
|
|
5
|
-
import yaml from 'yaml';
|
|
6
|
-
import { createProgram } from './generated/program.js';
|
|
7
|
-
import { generate, loadConfig, loadOpenAPISpec, lintSpec, formatLintResults, computeInputHash } from './generator/index.js';
|
|
8
|
-
import { isMultiModuleConfig } from './types.js';
|
|
9
|
-
import { getStarterTemplates, getScreenStarterTemplates } from './cli/templates.js';
|
|
10
|
-
import { runAllChecks, formatCheckResults, formatSingleCheckResult, formatCheckStart, formatCheckSummary, getAvailableChecks, createGuardrailsConfig, generateManifest, writeManifest, canSkipGeneration, GATE_DESCRIPTIONS, loadGuardrailsConfigWithPath, } from './guardrails/index.js';
|
|
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';
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const pkg = require('../package.json');
|
|
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);
|
|
43
|
-
}
|
|
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;
|
|
60
|
-
}
|
|
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
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
console.error('Generation failed:', error instanceof Error ? error.message : error);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
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
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
console.error('Lint failed:', error instanceof Error ? error.message : error);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
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}/`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!opts.skipTemplates) {
|
|
136
|
-
const starterTemplates = getStarterTemplates();
|
|
137
|
-
for (const [filename, content] of Object.entries(starterTemplates)) {
|
|
138
|
-
const templatePath = path.join('spec/default/templates', filename);
|
|
139
|
-
if (!fs.existsSync(templatePath)) {
|
|
140
|
-
fs.writeFileSync(templatePath, content);
|
|
141
|
-
console.log(`Created: ${templatePath}`);
|
|
142
|
-
}
|
|
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
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const problemDetailsPath = 'spec/_shared/openapi/problem-details.yaml';
|
|
156
|
-
if (!fs.existsSync(problemDetailsPath)) {
|
|
157
|
-
fs.writeFileSync(problemDetailsPath, generateProblemDetailsSchema());
|
|
158
|
-
console.log(`Created: ${problemDetailsPath}`);
|
|
159
|
-
}
|
|
160
|
-
const spectralPath = 'spec/spectral.yaml';
|
|
161
|
-
if (!fs.existsSync(spectralPath)) {
|
|
162
|
-
fs.writeFileSync(spectralPath, generateSpectralRules());
|
|
163
|
-
console.log(`Created: ${spectralPath}`);
|
|
164
|
-
}
|
|
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
|
-
}
|
|
173
|
-
}
|
|
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
|
-
}
|
|
185
|
-
}
|
|
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
|
-
}
|
|
197
|
-
}
|
|
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}`);
|
|
205
|
-
}
|
|
206
|
-
if (opts.openapi) {
|
|
207
|
-
const openapiPath = path.resolve(opts.openapi);
|
|
208
|
-
if (!fs.existsSync(openapiPath)) {
|
|
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(', ')}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
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`);
|
|
233
|
-
}
|
|
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`);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
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`);
|
|
246
|
-
}
|
|
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;
|
|
259
|
-
}
|
|
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
|
-
});
|
|
278
|
-
}
|
|
279
|
-
console.log('');
|
|
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
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
console.error('Check failed:', error instanceof Error ? error.message : error);
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
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 = [];
|
|
333
|
-
console.log('');
|
|
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('└─────────────────────────────────────────────────┘');
|
|
362
|
-
console.log('');
|
|
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;
|
|
381
|
-
if (verbose) {
|
|
382
|
-
console.log('┌─────────────────────────────────────────────────┐');
|
|
383
|
-
console.log('│ Step 2: Generate contracts │');
|
|
384
|
-
console.log('└─────────────────────────────────────────────────┘');
|
|
385
|
-
console.log('');
|
|
386
|
-
}
|
|
387
|
-
const generateStartTime = Date.now();
|
|
388
|
-
if (isStreaming && process.stdout.isTTY) {
|
|
389
|
-
process.stdout.write(' ⋯ Generate running...\r');
|
|
390
|
-
}
|
|
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)');
|
|
401
|
-
}
|
|
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})`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
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
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else if (changed) {
|
|
458
|
-
writeManifest(manifest, manifestDir);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
generatePassed = true;
|
|
464
|
-
}
|
|
465
|
-
finally {
|
|
466
|
-
if (!verbose)
|
|
467
|
-
console.log = originalLog;
|
|
468
|
-
}
|
|
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('');
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
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)`);
|
|
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.');
|
|
497
|
-
}
|
|
498
|
-
// Step 3: Gate 3,4,5
|
|
499
|
-
if (verbose) {
|
|
500
|
-
console.log('');
|
|
501
|
-
console.log('┌─────────────────────────────────────────────────┐');
|
|
502
|
-
console.log('│ Step 3: Post-generation checks (Gate 3, 4, 5) │');
|
|
503
|
-
console.log('└─────────────────────────────────────────────────┘');
|
|
504
|
-
console.log('');
|
|
505
|
-
}
|
|
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);
|
|
524
|
-
console.log('');
|
|
525
|
-
console.log('━'.repeat(50));
|
|
526
|
-
console.log('📊 Pipeline Summary');
|
|
527
|
-
console.log('━'.repeat(50));
|
|
528
|
-
console.log('');
|
|
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
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
console.log('✅ Pipeline completed successfully!');
|
|
552
|
-
}
|
|
553
|
-
console.log('');
|
|
554
|
-
}
|
|
555
|
-
catch (error) {
|
|
556
|
-
console.error('Pipeline failed:', error instanceof Error ? error.message : error);
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
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
|
-
}
|
|
604
|
-
}
|
|
605
|
-
catch (error) {
|
|
606
|
-
console.error('Deps analysis failed:', error instanceof Error ? error.message : error);
|
|
607
|
-
process.exit(1);
|
|
608
|
-
}
|
|
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);
|
|
627
|
-
process.exit(1);
|
|
628
|
-
}
|
|
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}`);
|
|
636
|
-
process.exit(1);
|
|
637
|
-
}
|
|
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
|
-
}
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
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
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
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);
|
|
770
|
-
}
|
|
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}`;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
function generateDbTemplate() {
|
|
802
|
-
return `import pg from 'pg';
|
|
803
|
-
import { DBModel, PostgresDriver } from 'litedbmodel';
|
|
804
|
-
|
|
805
|
-
const { Pool } = pg;
|
|
806
|
-
|
|
807
|
-
let pool: pg.Pool | null = null;
|
|
808
|
-
|
|
809
|
-
export function getPool(): pg.Pool {
|
|
810
|
-
if (!pool) {
|
|
811
|
-
pool = new Pool({
|
|
812
|
-
connectionString: process.env.DATABASE_URL,
|
|
813
|
-
max: 20,
|
|
814
|
-
idleTimeoutMillis: 30000,
|
|
815
|
-
connectionTimeoutMillis: 2000,
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
return pool;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
export async function initializeDb(): Promise<void> {
|
|
822
|
-
const p = getPool();
|
|
823
|
-
DBModel.setDriver(new PostgresDriver(p));
|
|
824
|
-
const client = await p.connect();
|
|
825
|
-
try {
|
|
826
|
-
await client.query('SELECT 1');
|
|
827
|
-
} finally {
|
|
828
|
-
client.release();
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export async function closeDb(): Promise<void> {
|
|
833
|
-
if (pool) {
|
|
834
|
-
await pool.end();
|
|
835
|
-
pool = null;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
export async function testConnection(): Promise<boolean> {
|
|
840
|
-
try {
|
|
841
|
-
const p = getPool();
|
|
842
|
-
const client = await p.connect();
|
|
843
|
-
try {
|
|
844
|
-
await client.query('SELECT 1');
|
|
845
|
-
return true;
|
|
846
|
-
} finally {
|
|
847
|
-
client.release();
|
|
848
|
-
}
|
|
849
|
-
} catch {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
`;
|
|
854
|
-
}
|
|
855
|
-
function generateContainerTemplate(moduleName) {
|
|
856
|
-
const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
|
|
857
|
-
return `import { testConnection, closeDb } from './db.js';
|
|
858
|
-
|
|
859
|
-
export interface ${pascalName}Services {
|
|
860
|
-
// example: ExampleServiceApi;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
export interface ${pascalName}ModuleContainer {
|
|
864
|
-
services: ${pascalName}Services;
|
|
865
|
-
testConnection: () => Promise<boolean>;
|
|
866
|
-
close: () => Promise<void>;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export async function initialize${pascalName}Module(): Promise<${pascalName}ModuleContainer> {
|
|
870
|
-
const services: ${pascalName}Services = {};
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
services,
|
|
874
|
-
testConnection,
|
|
875
|
-
close: closeDb,
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
`;
|
|
879
|
-
}
|
|
880
|
-
function generateConfigTemplate(moduleName) {
|
|
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', '',
|
|
909
|
-
].join('\n');
|
|
910
|
-
}
|
|
911
|
-
function generateProblemDetailsSchema() {
|
|
912
|
-
return `# RFC 9457 Problem Details
|
|
913
|
-
components:
|
|
914
|
-
schemas:
|
|
915
|
-
ProblemDetails:
|
|
916
|
-
type: object
|
|
917
|
-
required: [type, title, status]
|
|
918
|
-
properties:
|
|
919
|
-
type:
|
|
920
|
-
type: string
|
|
921
|
-
format: uri
|
|
922
|
-
title:
|
|
923
|
-
type: string
|
|
924
|
-
status:
|
|
925
|
-
type: integer
|
|
926
|
-
detail:
|
|
927
|
-
type: string
|
|
928
|
-
instance:
|
|
929
|
-
type: string
|
|
930
|
-
format: uri
|
|
931
|
-
code:
|
|
932
|
-
type: string
|
|
933
|
-
traceId:
|
|
934
|
-
type: string
|
|
935
|
-
errors:
|
|
936
|
-
type: array
|
|
937
|
-
items:
|
|
938
|
-
type: object
|
|
939
|
-
properties:
|
|
940
|
-
field:
|
|
941
|
-
type: string
|
|
942
|
-
message:
|
|
943
|
-
type: string
|
|
944
|
-
`;
|
|
945
|
-
}
|
|
946
|
-
function generateSpectralRules() {
|
|
947
|
-
return `extends: ["spectral:oas"]
|
|
948
|
-
|
|
949
|
-
rules:
|
|
950
|
-
operation-service:
|
|
951
|
-
description: "Operations must have x-micro-contracts-service"
|
|
952
|
-
severity: error
|
|
953
|
-
given: "$.paths[*][get,post,put,patch,delete]"
|
|
954
|
-
then:
|
|
955
|
-
field: x-micro-contracts-service
|
|
956
|
-
function: truthy
|
|
957
|
-
|
|
958
|
-
operation-method:
|
|
959
|
-
description: "Operations must have x-micro-contracts-method"
|
|
960
|
-
severity: error
|
|
961
|
-
given: "$.paths[*][get,post,put,patch,delete]"
|
|
962
|
-
then:
|
|
963
|
-
field: x-micro-contracts-method
|
|
964
|
-
function: truthy
|
|
965
|
-
|
|
966
|
-
operation-error-responses:
|
|
967
|
-
description: "Operations should have 5XX or default error response"
|
|
968
|
-
severity: warn
|
|
969
|
-
given: "$.paths[*][get,post,put,patch,delete].responses"
|
|
970
|
-
then:
|
|
971
|
-
function: schema
|
|
972
|
-
functionOptions:
|
|
973
|
-
schema:
|
|
974
|
-
anyOf:
|
|
975
|
-
- required: ["500"]
|
|
976
|
-
- required: ["5XX"]
|
|
977
|
-
- required: ["default"]
|
|
978
|
-
|
|
979
|
-
canonical-extension-prefix:
|
|
980
|
-
description: "Use canonical x-micro-contracts-* extensions"
|
|
981
|
-
severity: warn
|
|
982
|
-
given: "$.paths[*][get,post,put,patch,delete]"
|
|
983
|
-
then:
|
|
984
|
-
- field: x-service
|
|
985
|
-
function: falsy
|
|
986
|
-
- field: x-method
|
|
987
|
-
function: falsy
|
|
988
|
-
- field: x-public
|
|
989
|
-
function: falsy
|
|
990
|
-
`;
|
|
991
|
-
}
|
|
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
|
-
}
|
|
1064
|
-
function outputDependencyGraph(moduleDeps) {
|
|
1065
|
-
console.log('```mermaid');
|
|
1066
|
-
console.log('graph LR');
|
|
1067
|
-
for (const [moduleName, { deps }] of moduleDeps) {
|
|
1068
|
-
const moduleTargets = new Set();
|
|
1069
|
-
for (const dep of deps) {
|
|
1070
|
-
const parts = dep.split('.');
|
|
1071
|
-
if (parts.length >= 1)
|
|
1072
|
-
moduleTargets.add(parts[0]);
|
|
1073
|
-
}
|
|
1074
|
-
for (const target of moduleTargets) {
|
|
1075
|
-
console.log(` ${moduleName} --> ${target}`);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
console.log('```');
|
|
1079
|
-
}
|
|
1080
|
-
function outputImpactAnalysis(moduleDeps, ref) {
|
|
1081
|
-
console.log(`Impacted by changes to ${ref}:\n`);
|
|
1082
|
-
const impacted = [];
|
|
1083
|
-
for (const [moduleName, { deps }] of moduleDeps) {
|
|
1084
|
-
if (deps.includes(ref))
|
|
1085
|
-
impacted.push(moduleName);
|
|
1086
|
-
}
|
|
1087
|
-
if (impacted.length === 0) {
|
|
1088
|
-
console.log(' No modules depend on this API.');
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
for (const m of impacted)
|
|
1092
|
-
console.log(` - ${m}`);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
function outputWhoDependsOn(moduleDeps, ref) {
|
|
1096
|
-
console.log(`Modules that depend on ${ref}:\n`);
|
|
1097
|
-
const dependent = [];
|
|
1098
|
-
for (const [moduleName, { deps }] of moduleDeps) {
|
|
1099
|
-
if (deps.some(d => d.startsWith(ref)))
|
|
1100
|
-
dependent.push(moduleName);
|
|
1101
|
-
}
|
|
1102
|
-
if (dependent.length === 0) {
|
|
1103
|
-
console.log(' None found.');
|
|
1104
|
-
}
|
|
1105
|
-
else {
|
|
1106
|
-
for (const m of dependent)
|
|
1107
|
-
console.log(` - ${m}`);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
function validateDependencies(moduleDeps) {
|
|
1111
|
-
let hasErrors = false;
|
|
1112
|
-
for (const [moduleName, { openApiDeps, configDeps }] of moduleDeps) {
|
|
1113
|
-
for (const dep of configDeps) {
|
|
1114
|
-
if (!openApiDeps.includes(dep)) {
|
|
1115
|
-
console.error(`ERROR: ${moduleName}.dependsOn includes '${dep}'`);
|
|
1116
|
-
console.error(` but it's not declared in OpenAPI x-micro-contracts-depend-on`);
|
|
1117
|
-
hasErrors = true;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (!hasErrors) {
|
|
1122
|
-
console.log('✓ All dependencies are valid');
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
process.exit(1);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
function outputAllDependencies(moduleDeps) {
|
|
1129
|
-
console.log('Module Dependencies:\n');
|
|
1130
|
-
for (const [moduleName, { deps }] of moduleDeps) {
|
|
1131
|
-
console.log(`${moduleName}:`);
|
|
1132
|
-
if (deps.length === 0) {
|
|
1133
|
-
console.log(' (no dependencies)');
|
|
1134
|
-
}
|
|
1135
|
-
else {
|
|
1136
|
-
for (const dep of deps)
|
|
1137
|
-
console.log(` - ${dep}`);
|
|
1138
|
-
}
|
|
1139
|
-
console.log();
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
//# sourceMappingURL=cli.js.map
|