heicat-cli 0.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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +43 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +95 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +70 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/test.d.ts +4 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +166 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +65 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/watch.d.ts +5 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +656 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +7 -0
- package/package.json +37 -0
- package/src/cli.ts +49 -0
- package/src/commands/init.ts +103 -0
- package/src/commands/status.ts +75 -0
- package/src/commands/test.ts +188 -0
- package/src/commands/validate.ts +73 -0
- package/src/commands/watch.ts +655 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heicat-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tools for Heicat",
|
|
5
|
+
"bin": {
|
|
6
|
+
"heicat": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"lint": "eslint src/**/*.ts",
|
|
13
|
+
"dev": "tsc --watch"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"api",
|
|
17
|
+
"contract",
|
|
18
|
+
"validation",
|
|
19
|
+
"cli",
|
|
20
|
+
"nodejs"
|
|
21
|
+
],
|
|
22
|
+
"author": "Backend Contract Studio",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"heicat-core": "^0.1.0",
|
|
26
|
+
"chalk": "^5.0.0",
|
|
27
|
+
"commander": "^11.0.0",
|
|
28
|
+
"inquirer": "^9.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^29.0.0",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"jest": "^29.0.0",
|
|
34
|
+
"ts-jest": "^29.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init';
|
|
5
|
+
import { validateCommand } from './commands/validate';
|
|
6
|
+
import { statusCommand } from './commands/status';
|
|
7
|
+
import { watchCommand } from './commands/watch';
|
|
8
|
+
import { testCommand } from './commands/test';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('heicat')
|
|
14
|
+
.description('Runtime-enforced API contract system for Node.js')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Initialize contract studio in your project')
|
|
20
|
+
.option('--contracts-path <path>', 'Path to contracts directory', './contracts')
|
|
21
|
+
.action(initCommand);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('validate')
|
|
25
|
+
.description('Validate contracts in your project')
|
|
26
|
+
.option('--contracts-path <path>', 'Path to contracts directory', './contracts')
|
|
27
|
+
.action(validateCommand);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('status')
|
|
31
|
+
.description('Show contract validation status')
|
|
32
|
+
.option('--contracts-path <path>', 'Path to contracts directory', './contracts')
|
|
33
|
+
.action(statusCommand);
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('watch')
|
|
37
|
+
.description('Watch contracts and start local GUI server')
|
|
38
|
+
.option('--contracts-path <path>', 'Path to contracts directory', './contracts')
|
|
39
|
+
.option('--port <port>', 'Port for GUI server', '3333')
|
|
40
|
+
.action(watchCommand);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('test')
|
|
44
|
+
.description('Test contracts against a running server')
|
|
45
|
+
.argument('<url>', 'Server URL to test against')
|
|
46
|
+
.option('--contracts-path <path>', 'Path to contracts directory', './contracts')
|
|
47
|
+
.action(testCommand);
|
|
48
|
+
|
|
49
|
+
program.parse();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export function initCommand(options: { contractsPath: string }) {
|
|
7
|
+
const contractsPath = resolve(process.cwd(), options.contractsPath);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Create contracts directory
|
|
11
|
+
mkdirSync(contractsPath, { recursive: true });
|
|
12
|
+
console.log(chalk.green(`✅ Created contracts directory: ${contractsPath}`));
|
|
13
|
+
|
|
14
|
+
// Create example contracts
|
|
15
|
+
const usersContract = {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
path: '/users',
|
|
18
|
+
auth: 'jwt',
|
|
19
|
+
request: {
|
|
20
|
+
body: {
|
|
21
|
+
email: { type: 'string', required: true },
|
|
22
|
+
password: { type: 'string', minLength: 8, required: true },
|
|
23
|
+
name: { type: 'string' }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
response: {
|
|
27
|
+
201: {
|
|
28
|
+
id: { type: 'string' },
|
|
29
|
+
email: { type: 'string' },
|
|
30
|
+
name: { type: 'string' },
|
|
31
|
+
createdAt: { type: 'string' }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
errors: {
|
|
35
|
+
400: {
|
|
36
|
+
message: { type: 'string' }
|
|
37
|
+
},
|
|
38
|
+
409: {
|
|
39
|
+
message: { type: 'string' }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const authContract = {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
path: '/auth/login',
|
|
47
|
+
request: {
|
|
48
|
+
body: {
|
|
49
|
+
email: { type: 'string', required: true },
|
|
50
|
+
password: { type: 'string', required: true }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
response: {
|
|
54
|
+
200: {
|
|
55
|
+
token: { type: 'string' },
|
|
56
|
+
user: {
|
|
57
|
+
id: { type: 'string' },
|
|
58
|
+
email: { type: 'string' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
errors: {
|
|
63
|
+
401: {
|
|
64
|
+
message: { type: 'string' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
writeFileSync(
|
|
70
|
+
resolve(contractsPath, 'users.contract.json'),
|
|
71
|
+
JSON.stringify(usersContract, null, 2)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
writeFileSync(
|
|
75
|
+
resolve(contractsPath, 'auth.contract.json'),
|
|
76
|
+
JSON.stringify(authContract, null, 2)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green('✅ Created example contracts:'));
|
|
80
|
+
console.log(chalk.gray(' - users.contract.json'));
|
|
81
|
+
console.log(chalk.gray(' - auth.contract.json'));
|
|
82
|
+
|
|
83
|
+
// Check if package.json exists and suggest middleware setup
|
|
84
|
+
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
85
|
+
let hasPackageJson = false;
|
|
86
|
+
try {
|
|
87
|
+
require(packageJsonPath);
|
|
88
|
+
hasPackageJson = true;
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
91
|
+
if (hasPackageJson) {
|
|
92
|
+
console.log('\n' + chalk.blue('📝 Next steps:'));
|
|
93
|
+
console.log(chalk.gray('1. Install the core package: npm install @heicat/core'));
|
|
94
|
+
console.log(chalk.gray('2. Add middleware to your Express app:'));
|
|
95
|
+
console.log(chalk.gray(' const { contractMiddleware } = require("@heicat/core");'));
|
|
96
|
+
console.log(chalk.gray(' app.use(contractMiddleware({ contractsPath: "./contracts", mode: "dev" }));'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(chalk.red('❌ Failed to initialize contract studio:'), error);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { resolve, extname } from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Contract } from 'heicat-core';
|
|
6
|
+
|
|
7
|
+
export function statusCommand(options: { contractsPath: string }) {
|
|
8
|
+
const contractsPath = resolve(process.cwd(), options.contractsPath);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const files = readdirSync(contractsPath)
|
|
12
|
+
.filter(file => extname(file) === '.json' && file.endsWith('.contract.json'));
|
|
13
|
+
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log(chalk.yellow('⚠️ No contract files found in'), contractsPath);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const contracts: Contract[] = [];
|
|
20
|
+
const methods = new Set<string>();
|
|
21
|
+
const paths = new Set<string>();
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const filePath = resolve(contractsPath, file);
|
|
25
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const contract = JSON.parse(content) as Contract;
|
|
29
|
+
contracts.push(contract);
|
|
30
|
+
|
|
31
|
+
if (contract.method) methods.add(contract.method.toUpperCase());
|
|
32
|
+
if (contract.path) paths.add(contract.path);
|
|
33
|
+
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Skip invalid contracts
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.blue('📊 Heicat Status'));
|
|
40
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
41
|
+
|
|
42
|
+
console.log(chalk.green(`📁 Contracts: ${contracts.length}`));
|
|
43
|
+
console.log(chalk.green(`🔗 Endpoints: ${contracts.length}`));
|
|
44
|
+
console.log(chalk.green(`📋 Methods: ${Array.from(methods).join(', ')}`));
|
|
45
|
+
|
|
46
|
+
// Group by method
|
|
47
|
+
const byMethod = contracts.reduce((acc, contract) => {
|
|
48
|
+
const method = contract.method.toUpperCase();
|
|
49
|
+
if (!acc[method]) acc[method] = [];
|
|
50
|
+
acc[method].push(contract.path);
|
|
51
|
+
return acc;
|
|
52
|
+
}, {} as Record<string, string[]>);
|
|
53
|
+
|
|
54
|
+
console.log('\n' + chalk.blue('📋 Endpoints by Method:'));
|
|
55
|
+
for (const [method, paths] of Object.entries(byMethod)) {
|
|
56
|
+
console.log(chalk.gray(` ${method}: ${paths.length} endpoint${paths.length !== 1 ? 's' : ''}`));
|
|
57
|
+
paths.forEach(path => console.log(chalk.gray(` ${path}`)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for potential issues
|
|
61
|
+
const duplicatePaths = contracts
|
|
62
|
+
.map(c => `${c.method.toUpperCase()} ${c.path}`)
|
|
63
|
+
.filter((item, index, arr) => arr.indexOf(item) !== index);
|
|
64
|
+
|
|
65
|
+
if (duplicatePaths.length > 0) {
|
|
66
|
+
console.log('\n' + chalk.yellow('⚠️ Potential Issues:'));
|
|
67
|
+
console.log(chalk.yellow(' Duplicate endpoints found:'));
|
|
68
|
+
duplicatePaths.forEach(path => console.log(chalk.yellow(` ${path}`)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('❌ Failed to read contract status:'), error);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { resolve, extname } from 'path';
|
|
5
|
+
import { Contract } from 'heicat-core';
|
|
6
|
+
|
|
7
|
+
interface TestResult {
|
|
8
|
+
contract: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
responseTime?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function testCommand(url: string, options: { contractsPath: string }) {
|
|
16
|
+
const contractsPath = resolve(process.cwd(), options.contractsPath);
|
|
17
|
+
const baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
|
|
18
|
+
|
|
19
|
+
console.log(chalk.blue('🧪 Testing Contracts Against Server'));
|
|
20
|
+
console.log(chalk.gray(`📍 Server: ${baseUrl}`));
|
|
21
|
+
console.log(chalk.gray(`📁 Contracts: ${contractsPath}`));
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Load contracts
|
|
26
|
+
const files = readdirSync(contractsPath)
|
|
27
|
+
.filter(file => extname(file) === '.json' && file.endsWith('.contract.json'));
|
|
28
|
+
|
|
29
|
+
if (files.length === 0) {
|
|
30
|
+
console.log(chalk.yellow('⚠️ No contract files found'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const contracts: Contract[] = [];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const filePath = resolve(contractsPath, file);
|
|
37
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
38
|
+
contracts.push(JSON.parse(content));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(chalk.blue(`📋 Testing ${contracts.length} contract${contracts.length !== 1 ? 's' : ''}`));
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
const results: TestResult[] = [];
|
|
45
|
+
|
|
46
|
+
for (const contract of contracts) {
|
|
47
|
+
const endpoint = `${contract.method} ${contract.path}`;
|
|
48
|
+
console.log(chalk.gray(`Testing: ${endpoint}`));
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await testContract(baseUrl, contract);
|
|
52
|
+
results.push(result);
|
|
53
|
+
|
|
54
|
+
if (result.success) {
|
|
55
|
+
console.log(chalk.green(` ✅ ${endpoint} (${result.responseTime}ms)`));
|
|
56
|
+
} else {
|
|
57
|
+
console.log(chalk.red(` ❌ ${endpoint}: ${result.error}`));
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
61
|
+
results.push({
|
|
62
|
+
contract: contract.path,
|
|
63
|
+
endpoint,
|
|
64
|
+
success: false,
|
|
65
|
+
error: errorMsg
|
|
66
|
+
});
|
|
67
|
+
console.log(chalk.red(` ❌ ${endpoint}: ${errorMsg}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Summary
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(chalk.blue('📊 Test Results'));
|
|
74
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
75
|
+
|
|
76
|
+
const passed = results.filter(r => r.success).length;
|
|
77
|
+
const failed = results.filter(r => !r.success).length;
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green(`✅ Passed: ${passed}`));
|
|
80
|
+
console.log(chalk.red(`❌ Failed: ${failed}`));
|
|
81
|
+
|
|
82
|
+
if (failed > 0) {
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(chalk.red('❌ Failed Tests:'));
|
|
85
|
+
results.filter(r => !r.success).forEach(result => {
|
|
86
|
+
console.log(chalk.red(` • ${result.endpoint}: ${result.error}`));
|
|
87
|
+
});
|
|
88
|
+
process.exit(1);
|
|
89
|
+
} else {
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.green('🎉 All contract tests passed!'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(chalk.red('❌ Test failed:'), error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function testContract(baseUrl: string, contract: Contract): Promise<TestResult> {
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
const endpoint = `${contract.method} ${contract.path}`;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// For now, just test that the endpoint exists and returns valid JSON
|
|
106
|
+
// In a real implementation, this would test the full contract validation
|
|
107
|
+
const url = `${baseUrl}${contract.path}`;
|
|
108
|
+
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
method: contract.method,
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
},
|
|
114
|
+
// For POST requests, send minimal valid body if defined
|
|
115
|
+
body: contract.method === 'POST' && contract.request?.body
|
|
116
|
+
? JSON.stringify(createMinimalBody(contract.request.body))
|
|
117
|
+
: undefined
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const responseTime = Date.now() - startTime;
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
return {
|
|
124
|
+
contract: contract.path,
|
|
125
|
+
endpoint,
|
|
126
|
+
success: false,
|
|
127
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
128
|
+
responseTime
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try to parse JSON response
|
|
133
|
+
try {
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
return {
|
|
136
|
+
contract: contract.path,
|
|
137
|
+
endpoint,
|
|
138
|
+
success: true,
|
|
139
|
+
responseTime
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
return {
|
|
143
|
+
contract: contract.path,
|
|
144
|
+
endpoint,
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'Response is not valid JSON',
|
|
147
|
+
responseTime
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const responseTime = Date.now() - startTime;
|
|
153
|
+
const errorMsg = error instanceof Error ? error.message : 'Network error';
|
|
154
|
+
return {
|
|
155
|
+
contract: contract.path,
|
|
156
|
+
endpoint,
|
|
157
|
+
success: false,
|
|
158
|
+
error: errorMsg,
|
|
159
|
+
responseTime
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createMinimalBody(schema: Record<string, any>): Record<string, any> {
|
|
165
|
+
const body: Record<string, any> = {};
|
|
166
|
+
|
|
167
|
+
for (const [key, fieldSchema] of Object.entries(schema)) {
|
|
168
|
+
if (typeof fieldSchema === 'object' && fieldSchema.type) {
|
|
169
|
+
if (fieldSchema.required === false) continue;
|
|
170
|
+
|
|
171
|
+
switch (fieldSchema.type) {
|
|
172
|
+
case 'string':
|
|
173
|
+
body[key] = fieldSchema.minLength ? 'a'.repeat(fieldSchema.minLength) : 'test';
|
|
174
|
+
break;
|
|
175
|
+
case 'number':
|
|
176
|
+
body[key] = fieldSchema.minimum || 1;
|
|
177
|
+
break;
|
|
178
|
+
case 'boolean':
|
|
179
|
+
body[key] = true;
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
body[key] = 'test';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return body;
|
|
188
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { resolve, extname } from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Contract } from 'heicat-core';
|
|
6
|
+
|
|
7
|
+
export function validateCommand(options: { contractsPath: string }) {
|
|
8
|
+
const contractsPath = resolve(process.cwd(), options.contractsPath);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const files = readdirSync(contractsPath)
|
|
12
|
+
.filter(file => extname(file) === '.json' && file.endsWith('.contract.json'));
|
|
13
|
+
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log(chalk.yellow('⚠️ No contract files found in'), contractsPath);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let validCount = 0;
|
|
20
|
+
let invalidCount = 0;
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const filePath = resolve(contractsPath, file);
|
|
24
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const contract = JSON.parse(content) as Contract;
|
|
28
|
+
|
|
29
|
+
// Basic validation
|
|
30
|
+
const errors: string[] = [];
|
|
31
|
+
|
|
32
|
+
if (!contract.method) {
|
|
33
|
+
errors.push('Missing required field: method');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!contract.path) {
|
|
37
|
+
errors.push('Missing required field: path');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (contract.method && typeof contract.method !== 'string') {
|
|
41
|
+
errors.push('Method must be a string');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (contract.path && typeof contract.path !== 'string') {
|
|
45
|
+
errors.push('Path must be a string');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (errors.length > 0) {
|
|
49
|
+
console.log(chalk.red(`❌ ${file}:`));
|
|
50
|
+
errors.forEach(error => console.log(chalk.red(` ${error}`)));
|
|
51
|
+
invalidCount++;
|
|
52
|
+
} else {
|
|
53
|
+
console.log(chalk.green(`✅ ${file}`));
|
|
54
|
+
validCount++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.log(chalk.red(`❌ ${file}: Invalid JSON`));
|
|
59
|
+
invalidCount++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('\n' + chalk.blue(`📊 Summary: ${validCount} valid, ${invalidCount} invalid`));
|
|
64
|
+
|
|
65
|
+
if (invalidCount > 0) {
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(chalk.red('❌ Failed to validate contracts:'), error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|