s9n-devops-agent 2.0.10 → 2.0.11-dev.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/bin/cs-devops-agent +28 -0
- package/package.json +4 -3
- package/scripts/contract-automation/README.md +515 -0
- package/scripts/contract-automation/analyze-with-llm.js +497 -0
- package/scripts/contract-automation/check-compliance.js +567 -0
- package/scripts/contract-automation/generate-contracts.js +592 -0
- package/scripts/contract-automation/validate-commit.js +422 -0
- package/src/credentials-manager.js +108 -0
- package/src/session-coordinator.js +7 -3
- package/src/setup-cs-devops-agent.js +175 -3
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* CONTRACT GENERATION SCRIPT
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* This script scans the codebase and generates populated contract files.
|
|
9
|
+
* It uses local file system operations, regex, and AST parsing.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/contract-automation/generate-contracts.js
|
|
13
|
+
* node scripts/contract-automation/generate-contracts.js --contract=features
|
|
14
|
+
* node scripts/contract-automation/generate-contracts.js --validate-only
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --contract=<name> Generate specific contract (features, api, database, sql, integrations, infra)
|
|
18
|
+
* --validate-only Only validate existing contracts, don't generate
|
|
19
|
+
* --output=<path> Output directory (default: House_Rules_Contracts/)
|
|
20
|
+
* --verbose Detailed logging
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import { execSync } from 'child_process';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
|
|
33
|
+
// Configuration
|
|
34
|
+
const CONFIG = {
|
|
35
|
+
rootDir: process.cwd(),
|
|
36
|
+
contractsDir: path.join(process.cwd(), 'House_Rules_Contracts'),
|
|
37
|
+
srcDir: path.join(process.cwd(), 'src'),
|
|
38
|
+
verbose: process.argv.includes('--verbose'),
|
|
39
|
+
validateOnly: process.argv.includes('--validate-only'),
|
|
40
|
+
specificContract: getArgValue('--contract'),
|
|
41
|
+
outputDir: getArgValue('--output') || path.join(process.cwd(), 'House_Rules_Contracts')
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Helper: Get command line argument value
|
|
45
|
+
function getArgValue(argName) {
|
|
46
|
+
const arg = process.argv.find(a => a.startsWith(argName + '='));
|
|
47
|
+
return arg ? arg.split('=')[1] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Helper: Log with optional verbose mode
|
|
51
|
+
function log(message, level = 'info') {
|
|
52
|
+
const prefix = {
|
|
53
|
+
info: '[INFO]',
|
|
54
|
+
warn: '[WARN]',
|
|
55
|
+
error: '[ERROR]',
|
|
56
|
+
success: '[SUCCESS]',
|
|
57
|
+
debug: '[DEBUG]'
|
|
58
|
+
}[level];
|
|
59
|
+
|
|
60
|
+
if (level === 'debug' && !CONFIG.verbose) return;
|
|
61
|
+
|
|
62
|
+
console.log(`${prefix} ${message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Helper: Find files matching glob pattern
|
|
66
|
+
function findFiles(pattern, dir = CONFIG.rootDir) {
|
|
67
|
+
try {
|
|
68
|
+
const result = execSync(`find ${dir} -type f ${pattern}`, { encoding: 'utf8' });
|
|
69
|
+
return result.trim().split('\n').filter(Boolean);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Helper: Read file safely
|
|
76
|
+
function readFileSafe(filePath) {
|
|
77
|
+
try {
|
|
78
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
log(`Failed to read ${filePath}: ${error.message}`, 'warn');
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Helper: Write JSON file
|
|
86
|
+
function writeJSON(filePath, data) {
|
|
87
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// FEATURE SCANNER
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
function scanFeatures() {
|
|
95
|
+
log('Scanning for features...');
|
|
96
|
+
|
|
97
|
+
const features = [];
|
|
98
|
+
|
|
99
|
+
// Find feature directories
|
|
100
|
+
const featureDirs = [
|
|
101
|
+
...findFiles('-path "*/src/features/*" -type d', CONFIG.rootDir),
|
|
102
|
+
...findFiles('-path "*/src/modules/*" -type d', CONFIG.rootDir)
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
log(`Found ${featureDirs.length} potential feature directories`, 'debug');
|
|
106
|
+
|
|
107
|
+
for (const dir of featureDirs) {
|
|
108
|
+
const featureName = path.basename(dir);
|
|
109
|
+
if (featureName === 'features' || featureName === 'modules') continue;
|
|
110
|
+
|
|
111
|
+
const files = findFiles(`-path "${dir}/*" -name "*.js" -o -name "*.ts"`, CONFIG.rootDir);
|
|
112
|
+
|
|
113
|
+
if (files.length > 0) {
|
|
114
|
+
features.push({
|
|
115
|
+
id: `F-${String(features.length + 1).padStart(3, '0')}`,
|
|
116
|
+
name: featureName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
117
|
+
path: dir,
|
|
118
|
+
files: files,
|
|
119
|
+
status: 'active',
|
|
120
|
+
priority: 'medium',
|
|
121
|
+
completion: 100
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(`Discovered ${features.length} features`, 'success');
|
|
127
|
+
return features;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// DATABASE SCHEMA SCANNER
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
function scanDatabaseSchema() {
|
|
135
|
+
log('Scanning for database schema...');
|
|
136
|
+
|
|
137
|
+
const tables = [];
|
|
138
|
+
|
|
139
|
+
// Find migration files
|
|
140
|
+
const migrationFiles = [
|
|
141
|
+
...findFiles('-path "*/migrations/*.sql"', CONFIG.rootDir),
|
|
142
|
+
...findFiles('-path "*/migrations/*.js"', CONFIG.rootDir),
|
|
143
|
+
...findFiles('-path "*/alembic/*.py"', CONFIG.rootDir)
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
log(`Found ${migrationFiles.length} migration files`, 'debug');
|
|
147
|
+
|
|
148
|
+
// Find Prisma schema
|
|
149
|
+
const prismaFiles = findFiles('-name "schema.prisma"', CONFIG.rootDir);
|
|
150
|
+
|
|
151
|
+
// Parse CREATE TABLE statements from SQL files
|
|
152
|
+
for (const file of migrationFiles) {
|
|
153
|
+
if (file.endsWith('.sql')) {
|
|
154
|
+
const content = readFileSafe(file);
|
|
155
|
+
const tableMatches = content.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s*\(([\s\S]*?)\);/gi);
|
|
156
|
+
|
|
157
|
+
for (const match of tableMatches) {
|
|
158
|
+
const tableName = match[1];
|
|
159
|
+
const columns = match[2];
|
|
160
|
+
|
|
161
|
+
// Parse columns
|
|
162
|
+
const columnLines = columns.split(',').map(l => l.trim()).filter(Boolean);
|
|
163
|
+
const parsedColumns = columnLines.map(line => {
|
|
164
|
+
const parts = line.split(/\s+/);
|
|
165
|
+
return {
|
|
166
|
+
name: parts[0],
|
|
167
|
+
type: parts[1] || 'UNKNOWN',
|
|
168
|
+
nullable: !line.includes('NOT NULL'),
|
|
169
|
+
isPrimaryKey: line.includes('PRIMARY KEY')
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
tables.push({
|
|
174
|
+
name: tableName,
|
|
175
|
+
columns: parsedColumns,
|
|
176
|
+
source: file,
|
|
177
|
+
created: fs.statSync(file).birthtime.toISOString().split('T')[0]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Parse Prisma schema
|
|
184
|
+
for (const file of prismaFiles) {
|
|
185
|
+
const content = readFileSafe(file);
|
|
186
|
+
const modelMatches = content.matchAll(/model\s+(\w+)\s*{([\s\S]*?)}/gi);
|
|
187
|
+
|
|
188
|
+
for (const match of modelMatches) {
|
|
189
|
+
const tableName = match[1].toLowerCase();
|
|
190
|
+
const fields = match[2];
|
|
191
|
+
|
|
192
|
+
const fieldLines = fields.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('//'));
|
|
193
|
+
const parsedColumns = fieldLines.map(line => {
|
|
194
|
+
const parts = line.split(/\s+/);
|
|
195
|
+
return {
|
|
196
|
+
name: parts[0],
|
|
197
|
+
type: parts[1] || 'UNKNOWN',
|
|
198
|
+
nullable: !line.includes('@default') && line.includes('?'),
|
|
199
|
+
isPrimaryKey: line.includes('@id')
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
tables.push({
|
|
204
|
+
name: tableName,
|
|
205
|
+
columns: parsedColumns,
|
|
206
|
+
source: file,
|
|
207
|
+
created: fs.statSync(file).birthtime.toISOString().split('T')[0]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
log(`Discovered ${tables.length} database tables`, 'success');
|
|
213
|
+
return tables;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// SQL QUERY SCANNER
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
function scanSQLQueries() {
|
|
221
|
+
log('Scanning for SQL queries...');
|
|
222
|
+
|
|
223
|
+
const queries = {};
|
|
224
|
+
let queryCount = 0;
|
|
225
|
+
|
|
226
|
+
// Find SQL files
|
|
227
|
+
const sqlFiles = findFiles('-name "*.sql" -not -path "*/migrations/*"', CONFIG.rootDir);
|
|
228
|
+
|
|
229
|
+
// Find code files with SQL
|
|
230
|
+
const codeFiles = [
|
|
231
|
+
...findFiles('-path "*/src/*.js" -o -path "*/src/*.ts"', CONFIG.rootDir),
|
|
232
|
+
...findFiles('-path "*/src/*.py"', CONFIG.rootDir)
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
log(`Scanning ${sqlFiles.length} SQL files and ${codeFiles.length} code files`, 'debug');
|
|
236
|
+
|
|
237
|
+
// Parse SQL files
|
|
238
|
+
for (const file of sqlFiles) {
|
|
239
|
+
const content = readFileSafe(file);
|
|
240
|
+
const queryName = path.basename(file, path.extname(file));
|
|
241
|
+
|
|
242
|
+
if (content.match(/SELECT|INSERT|UPDATE|DELETE/i)) {
|
|
243
|
+
const queryId = queryName.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
244
|
+
queries[queryId] = {
|
|
245
|
+
id: queryId,
|
|
246
|
+
name: queryName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
247
|
+
sql: content.trim(),
|
|
248
|
+
operation_type: content.match(/^(SELECT|INSERT|UPDATE|DELETE)/i)?.[1]?.toUpperCase() || 'UNKNOWN',
|
|
249
|
+
source: file,
|
|
250
|
+
parameters: extractParameters(content),
|
|
251
|
+
used_by_modules: []
|
|
252
|
+
};
|
|
253
|
+
queryCount++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Parse inline SQL in code files
|
|
258
|
+
for (const file of codeFiles) {
|
|
259
|
+
const content = readFileSafe(file);
|
|
260
|
+
|
|
261
|
+
// Match SQL strings (simple heuristic)
|
|
262
|
+
const sqlMatches = content.matchAll(/['"`](SELECT|INSERT|UPDATE|DELETE)[\s\S]*?['"`]/gi);
|
|
263
|
+
|
|
264
|
+
for (const match of sqlMatches) {
|
|
265
|
+
const sql = match[0].slice(1, -1); // Remove quotes
|
|
266
|
+
const operation = match[1].toUpperCase();
|
|
267
|
+
|
|
268
|
+
// Generate query ID from first table name
|
|
269
|
+
const tableMatch = sql.match(/FROM\s+(\w+)|INTO\s+(\w+)|UPDATE\s+(\w+)/i);
|
|
270
|
+
const tableName = tableMatch ? (tableMatch[1] || tableMatch[2] || tableMatch[3]) : 'unknown';
|
|
271
|
+
|
|
272
|
+
const queryId = `${operation.toLowerCase()}_${tableName}_inline_${queryCount}`;
|
|
273
|
+
|
|
274
|
+
if (!queries[queryId]) {
|
|
275
|
+
queries[queryId] = {
|
|
276
|
+
id: queryId,
|
|
277
|
+
name: `${operation} ${tableName}`,
|
|
278
|
+
sql: sql.trim(),
|
|
279
|
+
operation_type: operation,
|
|
280
|
+
source: file,
|
|
281
|
+
parameters: extractParameters(sql),
|
|
282
|
+
used_by_modules: [{
|
|
283
|
+
module: path.dirname(file).split('/').pop(),
|
|
284
|
+
file: file.replace(CONFIG.rootDir + '/', ''),
|
|
285
|
+
function: 'inline',
|
|
286
|
+
usage: 'Inline SQL query'
|
|
287
|
+
}]
|
|
288
|
+
};
|
|
289
|
+
queryCount++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
log(`Discovered ${Object.keys(queries).length} SQL queries`, 'success');
|
|
295
|
+
return queries;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function extractParameters(sql) {
|
|
299
|
+
const params = [];
|
|
300
|
+
|
|
301
|
+
// Match $1, $2, etc. (PostgreSQL style)
|
|
302
|
+
const pgParams = sql.matchAll(/\$(\d+)/g);
|
|
303
|
+
for (const match of pgParams) {
|
|
304
|
+
params.push({
|
|
305
|
+
name: `param${match[1]}`,
|
|
306
|
+
type: 'unknown',
|
|
307
|
+
required: true,
|
|
308
|
+
position: parseInt(match[1])
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Match ? (MySQL style)
|
|
313
|
+
const mysqlParams = sql.match(/\?/g);
|
|
314
|
+
if (mysqlParams) {
|
|
315
|
+
mysqlParams.forEach((_, i) => {
|
|
316
|
+
params.push({
|
|
317
|
+
name: `param${i + 1}`,
|
|
318
|
+
type: 'unknown',
|
|
319
|
+
required: true,
|
|
320
|
+
position: i + 1
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Match :name (named parameters)
|
|
326
|
+
const namedParams = sql.matchAll(/:(\w+)/g);
|
|
327
|
+
for (const match of namedParams) {
|
|
328
|
+
params.push({
|
|
329
|
+
name: match[1],
|
|
330
|
+
type: 'unknown',
|
|
331
|
+
required: true
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return params;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============================================================================
|
|
339
|
+
// API ENDPOINT SCANNER
|
|
340
|
+
// ============================================================================
|
|
341
|
+
|
|
342
|
+
function scanAPIEndpoints() {
|
|
343
|
+
log('Scanning for API endpoints...');
|
|
344
|
+
|
|
345
|
+
const endpoints = [];
|
|
346
|
+
|
|
347
|
+
// Find route files
|
|
348
|
+
const routeFiles = [
|
|
349
|
+
...findFiles('-path "*/routes/*.js" -o -path "*/routes/*.ts"', CONFIG.rootDir),
|
|
350
|
+
...findFiles('-path "*/api/*.js" -o -path "*/api/*.ts"', CONFIG.rootDir),
|
|
351
|
+
...findFiles('-path "*/controllers/*.js" -o -path "*/controllers/*.ts"', CONFIG.rootDir)
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
log(`Scanning ${routeFiles.length} route/controller files`, 'debug');
|
|
355
|
+
|
|
356
|
+
for (const file of routeFiles) {
|
|
357
|
+
const content = readFileSafe(file);
|
|
358
|
+
|
|
359
|
+
// Match Express routes: app.get('/path', ...)
|
|
360
|
+
const expressRoutes = content.matchAll(/(app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi);
|
|
361
|
+
|
|
362
|
+
for (const match of expressRoutes) {
|
|
363
|
+
const method = match[2].toUpperCase();
|
|
364
|
+
const path = match[3];
|
|
365
|
+
|
|
366
|
+
endpoints.push({
|
|
367
|
+
method,
|
|
368
|
+
path,
|
|
369
|
+
source: file.replace(CONFIG.rootDir + '/', ''),
|
|
370
|
+
controller: path.dirname(file).split('/').pop(),
|
|
371
|
+
status: 'active'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Match FastAPI routes: @app.get('/path')
|
|
376
|
+
const fastAPIRoutes = content.matchAll(/@(app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi);
|
|
377
|
+
|
|
378
|
+
for (const match of fastAPIRoutes) {
|
|
379
|
+
const method = match[2].toUpperCase();
|
|
380
|
+
const path = match[3];
|
|
381
|
+
|
|
382
|
+
endpoints.push({
|
|
383
|
+
method,
|
|
384
|
+
path,
|
|
385
|
+
source: file.replace(CONFIG.rootDir + '/', ''),
|
|
386
|
+
controller: path.dirname(file).split('/').pop(),
|
|
387
|
+
status: 'active'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
log(`Discovered ${endpoints.length} API endpoints`, 'success');
|
|
393
|
+
return endpoints;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ============================================================================
|
|
397
|
+
// THIRD-PARTY INTEGRATION SCANNER
|
|
398
|
+
// ============================================================================
|
|
399
|
+
|
|
400
|
+
function scanThirdPartyIntegrations() {
|
|
401
|
+
log('Scanning for third-party integrations...');
|
|
402
|
+
|
|
403
|
+
const integrations = [];
|
|
404
|
+
|
|
405
|
+
// Check package.json
|
|
406
|
+
const packageJsonPath = path.join(CONFIG.rootDir, 'package.json');
|
|
407
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
408
|
+
const packageJson = JSON.parse(readFileSafe(packageJsonPath));
|
|
409
|
+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
410
|
+
|
|
411
|
+
// Known third-party services
|
|
412
|
+
const knownServices = {
|
|
413
|
+
'stripe': { name: 'Stripe', purpose: 'Payment processing' },
|
|
414
|
+
'@sendgrid/mail': { name: 'SendGrid', purpose: 'Email delivery' },
|
|
415
|
+
'aws-sdk': { name: 'AWS SDK', purpose: 'AWS services' },
|
|
416
|
+
'@aws-sdk/client-s3': { name: 'AWS S3', purpose: 'Object storage' },
|
|
417
|
+
'twilio': { name: 'Twilio', purpose: 'SMS/Voice' },
|
|
418
|
+
'mailgun-js': { name: 'Mailgun', purpose: 'Email delivery' },
|
|
419
|
+
'axios': { name: 'Axios', purpose: 'HTTP client (check for API calls)' }
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
for (const [pkg, info] of Object.entries(knownServices)) {
|
|
423
|
+
if (dependencies[pkg]) {
|
|
424
|
+
integrations.push({
|
|
425
|
+
service: info.name,
|
|
426
|
+
purpose: info.purpose,
|
|
427
|
+
package: pkg,
|
|
428
|
+
version: dependencies[pkg],
|
|
429
|
+
status: 'active'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check for integration folders
|
|
436
|
+
const integrationDirs = findFiles('-path "*/src/integrations/*" -type d', CONFIG.rootDir);
|
|
437
|
+
|
|
438
|
+
for (const dir of integrationDirs) {
|
|
439
|
+
const serviceName = path.basename(dir);
|
|
440
|
+
if (serviceName !== 'integrations' && !integrations.find(i => i.service.toLowerCase() === serviceName)) {
|
|
441
|
+
integrations.push({
|
|
442
|
+
service: serviceName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
443
|
+
purpose: 'Unknown - check code',
|
|
444
|
+
bindingModule: dir.replace(CONFIG.rootDir + '/', ''),
|
|
445
|
+
status: 'active'
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
log(`Discovered ${integrations.length} third-party integrations`, 'success');
|
|
451
|
+
return integrations;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// ENVIRONMENT VARIABLE SCANNER
|
|
456
|
+
// ============================================================================
|
|
457
|
+
|
|
458
|
+
function scanEnvironmentVariables() {
|
|
459
|
+
log('Scanning for environment variables...');
|
|
460
|
+
|
|
461
|
+
const envVars = new Set();
|
|
462
|
+
|
|
463
|
+
// Check .env.example
|
|
464
|
+
const envExamplePath = path.join(CONFIG.rootDir, '.env.example');
|
|
465
|
+
if (fs.existsSync(envExamplePath)) {
|
|
466
|
+
const content = readFileSafe(envExamplePath);
|
|
467
|
+
const matches = content.matchAll(/^([A-Z_][A-Z0-9_]*)=/gm);
|
|
468
|
+
for (const match of matches) {
|
|
469
|
+
envVars.add(match[1]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Scan code for process.env usage
|
|
474
|
+
const codeFiles = findFiles('-path "*/src/*.js" -o -path "*/src/*.ts"', CONFIG.rootDir);
|
|
475
|
+
|
|
476
|
+
for (const file of codeFiles) {
|
|
477
|
+
const content = readFileSafe(file);
|
|
478
|
+
const matches = content.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g);
|
|
479
|
+
for (const match of matches) {
|
|
480
|
+
envVars.add(match[1]);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const envVarsList = Array.from(envVars).sort().map(name => ({
|
|
485
|
+
name,
|
|
486
|
+
type: inferEnvVarType(name),
|
|
487
|
+
category: inferEnvVarCategory(name),
|
|
488
|
+
required: !name.includes('OPTIONAL')
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
log(`Discovered ${envVarsList.length} environment variables`, 'success');
|
|
492
|
+
return envVarsList;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function inferEnvVarType(name) {
|
|
496
|
+
if (name.includes('PORT') || name.includes('TIMEOUT') || name.includes('MAX') || name.includes('LIMIT')) {
|
|
497
|
+
return 'integer';
|
|
498
|
+
}
|
|
499
|
+
if (name.includes('ENABLED') || name.includes('DEBUG') || name.includes('SSL')) {
|
|
500
|
+
return 'boolean';
|
|
501
|
+
}
|
|
502
|
+
return 'string';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function inferEnvVarCategory(name) {
|
|
506
|
+
if (name.startsWith('DATABASE_') || name.startsWith('DB_')) return 'database';
|
|
507
|
+
if (name.startsWith('REDIS_')) return 'cache';
|
|
508
|
+
if (name.startsWith('JWT_') || name.startsWith('AUTH_')) return 'authentication';
|
|
509
|
+
if (name.startsWith('AWS_')) return 'aws';
|
|
510
|
+
if (name.startsWith('FEATURE_')) return 'feature_flags';
|
|
511
|
+
if (name.startsWith('LOG_')) return 'logging';
|
|
512
|
+
return 'application';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ============================================================================
|
|
516
|
+
// MAIN EXECUTION
|
|
517
|
+
// ============================================================================
|
|
518
|
+
|
|
519
|
+
async function main() {
|
|
520
|
+
log('='.repeat(80));
|
|
521
|
+
log('CONTRACT GENERATION SCRIPT');
|
|
522
|
+
log('='.repeat(80));
|
|
523
|
+
|
|
524
|
+
// Ensure contracts directory exists
|
|
525
|
+
if (!fs.existsSync(CONFIG.contractsDir)) {
|
|
526
|
+
log(`Creating contracts directory: ${CONFIG.contractsDir}`);
|
|
527
|
+
fs.mkdirSync(CONFIG.contractsDir, { recursive: true });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const results = {
|
|
531
|
+
features: null,
|
|
532
|
+
database: null,
|
|
533
|
+
sql: null,
|
|
534
|
+
api: null,
|
|
535
|
+
integrations: null,
|
|
536
|
+
envVars: null
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Scan based on options
|
|
540
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'features') {
|
|
541
|
+
results.features = scanFeatures();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'database') {
|
|
545
|
+
results.database = scanDatabaseSchema();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'sql') {
|
|
549
|
+
results.sql = scanSQLQueries();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'api') {
|
|
553
|
+
results.api = scanAPIEndpoints();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'integrations') {
|
|
557
|
+
results.integrations = scanThirdPartyIntegrations();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!CONFIG.specificContract || CONFIG.specificContract === 'infra') {
|
|
561
|
+
results.envVars = scanEnvironmentVariables();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Save results
|
|
565
|
+
if (!CONFIG.validateOnly) {
|
|
566
|
+
const outputPath = path.join(CONFIG.outputDir, 'contract-scan-results.json');
|
|
567
|
+
writeJSON(outputPath, {
|
|
568
|
+
generated: new Date().toISOString(),
|
|
569
|
+
results
|
|
570
|
+
});
|
|
571
|
+
log(`Results saved to: ${outputPath}`, 'success');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Summary
|
|
575
|
+
log('='.repeat(80));
|
|
576
|
+
log('SCAN COMPLETE', 'success');
|
|
577
|
+
log('='.repeat(80));
|
|
578
|
+
if (results.features) log(`Features: ${results.features.length}`);
|
|
579
|
+
if (results.database) log(`Database Tables: ${results.database.length}`);
|
|
580
|
+
if (results.sql) log(`SQL Queries: ${Object.keys(results.sql).length}`);
|
|
581
|
+
if (results.api) log(`API Endpoints: ${results.api.length}`);
|
|
582
|
+
if (results.integrations) log(`Third-party Integrations: ${results.integrations.length}`);
|
|
583
|
+
if (results.envVars) log(`Environment Variables: ${results.envVars.length}`);
|
|
584
|
+
log('='.repeat(80));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Run
|
|
588
|
+
main().catch(error => {
|
|
589
|
+
log(`Fatal error: ${error.message}`, 'error');
|
|
590
|
+
console.error(error);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
});
|