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/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
+ }