cerber-core 1.0.4 → 1.1.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/CHANGELOG.md +158 -68
- package/README.md +75 -0
- package/USAGE_GUIDE.md +254 -0
- package/bin/cerber +121 -105
- package/dev/templates/cerber-guardian.mjs.tpl +44 -0
- package/dev/templates/cerber.yml.tpl +53 -0
- package/dev/templates/health-checks.ts.tpl +11 -0
- package/dev/templates/health-route.ts.tpl +50 -0
- package/dev/templates/pre-commit.tpl +4 -0
- package/dist/cli/contract-parser.d.ts +13 -0
- package/dist/cli/contract-parser.d.ts.map +1 -0
- package/dist/cli/contract-parser.js +241 -0
- package/dist/cli/contract-parser.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +241 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/template-generator.d.ts +28 -0
- package/dist/cli/template-generator.d.ts.map +1 -0
- package/dist/cli/template-generator.js +227 -0
- package/dist/cli/template-generator.js.map +1 -0
- package/dist/cli/types.d.ts +70 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +8 -0
- package/dist/cli/types.js.map +1 -0
- package/package.json +106 -104
- package/solo/templates/cerber-guardian.mjs.tpl +44 -0
- package/solo/templates/cerber.yml.tpl +53 -0
- package/solo/templates/health-checks.ts.tpl +29 -0
- package/solo/templates/health-route.ts.tpl +50 -0
- package/solo/templates/pre-commit.tpl +4 -0
- package/team/templates/CODEOWNERS.tpl +6 -0
- package/team/templates/cerber-guardian.mjs.tpl +44 -0
- package/team/templates/cerber.yml.tpl +53 -0
- package/team/templates/health-checks.ts.tpl +10 -0
- package/team/templates/health-route.ts.tpl +50 -0
- package/team/templates/pre-commit.tpl +4 -0
package/bin/cerber
CHANGED
|
@@ -1,105 +1,121 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cerber Core - Unified CLI
|
|
5
|
-
*
|
|
6
|
-
* Main entry point for all Cerber commands
|
|
7
|
-
*
|
|
8
|
-
* @author Stefan Pitek
|
|
9
|
-
* @license MIT
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import { program } from 'commander';
|
|
14
|
-
|
|
15
|
-
program
|
|
16
|
-
.name('cerber')
|
|
17
|
-
.description('Cerber Core - Code quality & health monitoring')
|
|
18
|
-
.version('1.
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.command('
|
|
22
|
-
.description('
|
|
23
|
-
.option('
|
|
24
|
-
.option('
|
|
25
|
-
.option('--
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
execSync
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
execSync(
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
program
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cerber Core - Unified CLI
|
|
5
|
+
*
|
|
6
|
+
* Main entry point for all Cerber commands
|
|
7
|
+
*
|
|
8
|
+
* @author Stefan Pitek
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { program } from 'commander';
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('cerber')
|
|
17
|
+
.description('Cerber Core - Code quality & health monitoring')
|
|
18
|
+
.version('1.1.0');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('init')
|
|
22
|
+
.description('Initialize Cerber in your project')
|
|
23
|
+
.option('--mode <mode>', 'Override mode: solo | dev | team')
|
|
24
|
+
.option('--force', 'Overwrite existing files')
|
|
25
|
+
.option('--dry-run', 'Show what would be generated without creating files')
|
|
26
|
+
.option('--print-template', 'Print CERBER.md template contract to stdout')
|
|
27
|
+
.option('--no-husky', 'Skip Husky hook generation')
|
|
28
|
+
.option('--no-workflow', 'Skip GitHub Actions workflow generation')
|
|
29
|
+
.option('--no-health', 'Skip health check template generation')
|
|
30
|
+
.option('--write-contract', 'Update CERBER.md contract with CLI options')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const { initCommand } = await import('../dist/cli/init.js');
|
|
33
|
+
await initCommand(options);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('guardian')
|
|
38
|
+
.description('Run Guardian pre-commit validation')
|
|
39
|
+
.option('-s, --schema <file>', 'Schema file path')
|
|
40
|
+
.option('-v, --verbose', 'Verbose output')
|
|
41
|
+
.option('--fail-on-warning', 'Exit with error on warnings')
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
const { Guardian } = await import('../dist/guardian/index.js');
|
|
44
|
+
const schema = options.schema ? await import(options.schema) : null;
|
|
45
|
+
|
|
46
|
+
const guardian = new Guardian(schema?.default || {});
|
|
47
|
+
const result = await guardian.validate();
|
|
48
|
+
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
console.error(chalk.red('❌ Guardian validation failed'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.green('✅ Guardian validation passed'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('health')
|
|
59
|
+
.description('Run Cerber health checks')
|
|
60
|
+
.option('-c, --checks <file>', 'Health checks file path')
|
|
61
|
+
.option('-u, --url <url>', 'Fetch health from URL')
|
|
62
|
+
.option('-p, --parallel', 'Run checks in parallel')
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const { Cerber } = await import('../dist/cerber/index.js');
|
|
65
|
+
|
|
66
|
+
if (options.url) {
|
|
67
|
+
const response = await fetch(options.url);
|
|
68
|
+
const result = await response.json();
|
|
69
|
+
console.log(JSON.stringify(result, null, 2));
|
|
70
|
+
process.exit(result.status === 'healthy' ? 0 : 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const checksModule = options.checks ? await import(options.checks) : null;
|
|
74
|
+
const checks = checksModule ? Object.values(checksModule).filter(v => typeof v === 'function') : [];
|
|
75
|
+
|
|
76
|
+
const cerber = new Cerber(checks);
|
|
77
|
+
const result = await cerber.runChecks({ parallel: options.parallel });
|
|
78
|
+
|
|
79
|
+
process.exit(result.status === 'healthy' ? 0 : 1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command('morning')
|
|
84
|
+
.description('Run morning dashboard (SOLO)')
|
|
85
|
+
.action(async () => {
|
|
86
|
+
const { execSync } = await import('child_process');
|
|
87
|
+
execSync('node solo/scripts/cerber-daily-check.js', { stdio: 'inherit' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('repair')
|
|
92
|
+
.description('Auto-repair common issues (SOLO)')
|
|
93
|
+
.option('--dry-run', 'Show what would be fixed without making changes')
|
|
94
|
+
.option('--approve', 'Require approval for each fix')
|
|
95
|
+
.action(async (options) => {
|
|
96
|
+
const { execSync } = await import('child_process');
|
|
97
|
+
const args = [
|
|
98
|
+
options.dryRun && '--dry-run',
|
|
99
|
+
options.approve && '--approve'
|
|
100
|
+
].filter(Boolean).join(' ');
|
|
101
|
+
|
|
102
|
+
execSync(`node solo/scripts/cerber-auto-repair.js ${args}`, { stdio: 'inherit' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command('focus <module>')
|
|
107
|
+
.description('Generate focus context for module (TEAM)')
|
|
108
|
+
.action(async (module) => {
|
|
109
|
+
const { execSync } = await import('child_process');
|
|
110
|
+
execSync(`bash team/scripts/cerber-focus.sh ${module}`, { stdio: 'inherit' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command('dashboard')
|
|
115
|
+
.description('Show system dashboard (SOLO)')
|
|
116
|
+
.action(async () => {
|
|
117
|
+
const { execSync } = await import('child_process');
|
|
118
|
+
execSync('node solo/scripts/cerber-dashboard.js', { stdio: 'inherit' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program.parse();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generated by Cerber init - DO NOT EDIT MANUALLY
|
|
3
|
+
// To regenerate: npx cerber init --force
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
const SCHEMA_FILE = '{{SCHEMA_FILE}}';
|
|
9
|
+
const APPROVALS_TAG = '{{APPROVALS_TAG}}';
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
console.log('🛡️ Cerber Guardian: Validating staged files...');
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(SCHEMA_FILE)) {
|
|
15
|
+
console.error(`❌ Schema file not found: ${SCHEMA_FILE}`);
|
|
16
|
+
console.error('Guardian MVP: schema missing. Add your rules to proceed.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let stagedFiles;
|
|
21
|
+
try {
|
|
22
|
+
stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf-8' })
|
|
23
|
+
.trim()
|
|
24
|
+
.split('\n')
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('❌ Failed to get staged files');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (stagedFiles.length === 0) {
|
|
32
|
+
console.log('✅ No files staged for commit');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`Checking ${stagedFiles.length} file(s)...`);
|
|
37
|
+
console.log('Guardian MVP: schema detected. Add validation rules to enforce imports/forbidden patterns.');
|
|
38
|
+
console.log('✅ All checks passed');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
main().catch(err => {
|
|
42
|
+
console.error('❌ Guardian check failed:', err?.message || err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Generated by Cerber init
|
|
2
|
+
name: Cerber CI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: {{CI_BRANCHES}}
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: {{CI_BRANCHES}}
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
cerber-ci:
|
|
13
|
+
name: Cerber CI
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: '20'
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Build (if script exists)
|
|
30
|
+
run: |
|
|
31
|
+
if npm run | grep -q "build"; then
|
|
32
|
+
npm run build
|
|
33
|
+
else
|
|
34
|
+
echo "No build script defined; skipping"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Test (if script exists)
|
|
38
|
+
run: |
|
|
39
|
+
if npm run | grep -q "test"; then
|
|
40
|
+
npm test
|
|
41
|
+
else
|
|
42
|
+
echo "No test script defined; skipping"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
- name: Run Guardian
|
|
46
|
+
run: |
|
|
47
|
+
if npm run | grep -q "cerber:guardian"; then
|
|
48
|
+
npm run cerber:guardian
|
|
49
|
+
else
|
|
50
|
+
node scripts/cerber-guardian.mjs
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
{{POST_DEPLOY_BLOCK}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Generated by Cerber init - CUSTOMIZE THIS FILE
|
|
2
|
+
// To regenerate template: npx cerber init --force
|
|
3
|
+
|
|
4
|
+
import { CerberCheck, makeIssue } from 'cerber-core';
|
|
5
|
+
|
|
6
|
+
export const checks: Record<string, CerberCheck> = {
|
|
7
|
+
database: async () => {
|
|
8
|
+
// TODO: Implement your database health check
|
|
9
|
+
return [];
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Generated by Cerber init - CUSTOMIZE THIS FILE
|
|
2
|
+
|
|
3
|
+
import { checks } from './health-checks.js';
|
|
4
|
+
|
|
5
|
+
export async function healthHandler(req: any, res: any) {
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const results = await Promise.all(
|
|
10
|
+
Object.entries(checks).map(async ([name, check]) => ({
|
|
11
|
+
name,
|
|
12
|
+
issues: await check()
|
|
13
|
+
}))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const allIssues = results.flatMap(r => r.issues);
|
|
17
|
+
const critical = allIssues.filter(i => i.severity === 'critical').length;
|
|
18
|
+
const errors = allIssues.filter(i => i.severity === 'error').length;
|
|
19
|
+
const warnings = allIssues.filter(i => i.severity === 'warning').length;
|
|
20
|
+
|
|
21
|
+
const status = critical > 0 ? 'unhealthy' :
|
|
22
|
+
errors > 0 ? 'degraded' : 'healthy';
|
|
23
|
+
|
|
24
|
+
const statusCode = status === 'healthy' ? 200 : 503;
|
|
25
|
+
|
|
26
|
+
res.status(statusCode).json({
|
|
27
|
+
status,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
durationMs: Date.now() - startTime,
|
|
30
|
+
summary: {
|
|
31
|
+
totalChecks: results.length,
|
|
32
|
+
failedChecks: results.filter(r => r.issues.length > 0).length,
|
|
33
|
+
criticalIssues: critical,
|
|
34
|
+
errorIssues: errors,
|
|
35
|
+
warningIssues: warnings
|
|
36
|
+
},
|
|
37
|
+
components: allIssues
|
|
38
|
+
});
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
res.status(503).json({
|
|
41
|
+
status: 'error',
|
|
42
|
+
message: 'Health check failed',
|
|
43
|
+
error: err.message
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Usage in your server:
|
|
49
|
+
// import { healthHandler } from './cerber/health-route.js';
|
|
50
|
+
// app.get('{{HEALTH_ENDPOINT}}', healthHandler);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CERBER_CONTRACT parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts and validates YAML contract from CERBER.md
|
|
5
|
+
*
|
|
6
|
+
* @author Stefan Pitek
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import { CerberContract, ContractParseResult } from './types.js';
|
|
10
|
+
export declare function parseCerberContract(projectRoot: string): Promise<ContractParseResult>;
|
|
11
|
+
export declare function extractContract(content: string): ContractParseResult;
|
|
12
|
+
export declare function getDefaultContract(mode?: 'solo' | 'dev' | 'team'): CerberContract;
|
|
13
|
+
//# sourceMappingURL=contract-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-parser.d.ts","sourceRoot":"","sources":["../../src/cli/contract-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAMjE,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAU3F;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA8EpE;AAsHD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAM,GAAG,KAAK,GAAG,MAAc,GAAG,cAAc,CA+BxF"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CERBER_CONTRACT parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts and validates YAML contract from CERBER.md
|
|
5
|
+
*
|
|
6
|
+
* @author Stefan Pitek
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
const YAML_START_MARKER = '## CERBER_CONTRACT';
|
|
12
|
+
const YAML_CODE_BLOCK_START = '```yaml';
|
|
13
|
+
const YAML_CODE_BLOCK_END = '```';
|
|
14
|
+
export async function parseCerberContract(projectRoot) {
|
|
15
|
+
const cerberPath = path.join(projectRoot, 'CERBER.md');
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(cerberPath, 'utf-8');
|
|
18
|
+
return extractContract(content);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
// CERBER.md doesn't exist
|
|
22
|
+
return { success: false, error: { message: 'CERBER.md not found' } };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function extractContract(content) {
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
// Find CERBER_CONTRACT section
|
|
28
|
+
const contractStartIndex = lines.findIndex(line => line.trim() === YAML_START_MARKER);
|
|
29
|
+
if (contractStartIndex === -1) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: {
|
|
33
|
+
message: `Missing "${YAML_START_MARKER}" section header`,
|
|
34
|
+
context: 'Expected format:\n\n## CERBER_CONTRACT\n\`\`\`yaml\n...\n\`\`\`'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Find yaml code block
|
|
39
|
+
let yamlStartIndex = -1;
|
|
40
|
+
let yamlEndIndex = -1;
|
|
41
|
+
for (let i = contractStartIndex; i < lines.length; i++) {
|
|
42
|
+
if (lines[i].trim().startsWith(YAML_CODE_BLOCK_START)) {
|
|
43
|
+
yamlStartIndex = i + 1;
|
|
44
|
+
}
|
|
45
|
+
else if (yamlStartIndex !== -1 && lines[i].trim() === YAML_CODE_BLOCK_END) {
|
|
46
|
+
yamlEndIndex = i;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (yamlStartIndex === -1) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: {
|
|
54
|
+
message: 'Missing YAML code block after CERBER_CONTRACT header',
|
|
55
|
+
line: contractStartIndex + 1,
|
|
56
|
+
context: `Expected \`\`\`yaml after line ${contractStartIndex + 1}`
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (yamlEndIndex === -1) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: {
|
|
64
|
+
message: 'Unclosed YAML code block',
|
|
65
|
+
line: yamlStartIndex,
|
|
66
|
+
context: 'Missing closing \`\`\` for YAML block'
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const yamlContent = lines.slice(yamlStartIndex, yamlEndIndex).join('\n');
|
|
71
|
+
// Simple YAML parser (for our specific structure)
|
|
72
|
+
try {
|
|
73
|
+
const contract = parseSimpleYaml(yamlContent);
|
|
74
|
+
// Validate required fields
|
|
75
|
+
const validation = validateContract(contract);
|
|
76
|
+
if (!validation.valid) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: {
|
|
80
|
+
message: 'Invalid contract structure',
|
|
81
|
+
context: validation.errors.join('\n')
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { success: true, contract };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: {
|
|
91
|
+
message: 'Failed to parse YAML contract',
|
|
92
|
+
context: err.message || 'Invalid YAML structure'
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function parseSimpleYaml(yamlContent) {
|
|
98
|
+
const lines = yamlContent.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'));
|
|
99
|
+
const contract = {
|
|
100
|
+
version: 1,
|
|
101
|
+
mode: 'dev',
|
|
102
|
+
guardian: {},
|
|
103
|
+
health: {},
|
|
104
|
+
ci: {},
|
|
105
|
+
team: {}
|
|
106
|
+
};
|
|
107
|
+
let currentSection = null;
|
|
108
|
+
let currentSubsection = null;
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
const indent = line.length - line.trimStart().length;
|
|
112
|
+
if (indent === 0 && trimmed.endsWith(':')) {
|
|
113
|
+
// Top-level key
|
|
114
|
+
const key = trimmed.slice(0, -1);
|
|
115
|
+
currentSection = key;
|
|
116
|
+
currentSubsection = null;
|
|
117
|
+
}
|
|
118
|
+
else if (indent === 2 && trimmed.endsWith(':')) {
|
|
119
|
+
// Second-level key
|
|
120
|
+
const key = trimmed.slice(0, -1);
|
|
121
|
+
currentSubsection = key;
|
|
122
|
+
if (currentSection && !contract[currentSection][key]) {
|
|
123
|
+
contract[currentSection][key] = {};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (trimmed.includes(':')) {
|
|
127
|
+
// Key-value pair
|
|
128
|
+
const [key, ...valueParts] = trimmed.split(':');
|
|
129
|
+
let value = valueParts.join(':').trim();
|
|
130
|
+
// Strip inline comments (# after value)
|
|
131
|
+
const commentIndex = value.indexOf('#');
|
|
132
|
+
if (commentIndex !== -1) {
|
|
133
|
+
value = value.substring(0, commentIndex).trim();
|
|
134
|
+
}
|
|
135
|
+
// Parse value type
|
|
136
|
+
if (value === 'true')
|
|
137
|
+
value = true;
|
|
138
|
+
else if (value === 'false')
|
|
139
|
+
value = false;
|
|
140
|
+
else if (!isNaN(Number(value)))
|
|
141
|
+
value = Number(value);
|
|
142
|
+
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
143
|
+
value = value.slice(1, -1).split(',').map((v) => {
|
|
144
|
+
const trimmed = v.trim();
|
|
145
|
+
// Strip quotes from array values
|
|
146
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
147
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
148
|
+
return trimmed.slice(1, -1);
|
|
149
|
+
}
|
|
150
|
+
return trimmed;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (currentSubsection && currentSection) {
|
|
154
|
+
contract[currentSection][currentSubsection][key.trim()] = value;
|
|
155
|
+
}
|
|
156
|
+
else if (currentSection) {
|
|
157
|
+
contract[currentSection][key.trim()] = value;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
contract[key.trim()] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return contract;
|
|
165
|
+
}
|
|
166
|
+
function validateContract(contract) {
|
|
167
|
+
const errors = [];
|
|
168
|
+
// Check guardian section
|
|
169
|
+
if (!contract.guardian) {
|
|
170
|
+
errors.push('Missing "guardian" section');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
if (typeof contract.guardian.enabled !== 'boolean') {
|
|
174
|
+
errors.push('guardian.enabled must be true or false');
|
|
175
|
+
}
|
|
176
|
+
if (contract.guardian.enabled && !contract.guardian.schemaFile) {
|
|
177
|
+
errors.push('guardian.schemaFile is required when guardian is enabled');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Check health section
|
|
181
|
+
if (!contract.health) {
|
|
182
|
+
errors.push('Missing "health" section');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (typeof contract.health.enabled !== 'boolean') {
|
|
186
|
+
errors.push('health.enabled must be true or false');
|
|
187
|
+
}
|
|
188
|
+
if (contract.health.enabled && !contract.health.endpoint) {
|
|
189
|
+
errors.push('health.endpoint is required when health is enabled');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Check ci section
|
|
193
|
+
if (!contract.ci) {
|
|
194
|
+
errors.push('Missing "ci" section');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
if (!contract.ci.provider) {
|
|
198
|
+
errors.push('ci.provider is required (e.g., "github")');
|
|
199
|
+
}
|
|
200
|
+
if (!contract.ci.postDeploy) {
|
|
201
|
+
contract.ci.postDeploy = { enabled: false };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
valid: errors.length === 0,
|
|
206
|
+
errors
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export function getDefaultContract(mode = 'dev') {
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
mode,
|
|
213
|
+
guardian: {
|
|
214
|
+
enabled: true,
|
|
215
|
+
schemaFile: 'BACKEND_SCHEMA.ts',
|
|
216
|
+
hook: 'husky',
|
|
217
|
+
approvalsTag: 'ARCHITECT_APPROVED'
|
|
218
|
+
},
|
|
219
|
+
health: {
|
|
220
|
+
enabled: mode !== 'solo',
|
|
221
|
+
endpoint: '/api/health',
|
|
222
|
+
failOn: {
|
|
223
|
+
critical: true,
|
|
224
|
+
error: true,
|
|
225
|
+
warning: false
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
ci: {
|
|
229
|
+
provider: 'github',
|
|
230
|
+
branches: ['main'],
|
|
231
|
+
requiredOnPR: true,
|
|
232
|
+
postDeploy: {
|
|
233
|
+
enabled: mode === 'team',
|
|
234
|
+
waitSeconds: 90,
|
|
235
|
+
healthUrlVar: 'CERBER_HEALTH_URL',
|
|
236
|
+
authHeaderSecret: 'CERBER_HEALTH_AUTH_HEADER'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=contract-parser.js.map
|