s9n-devops-agent 2.0.10 → 2.0.11-dev.1

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.
@@ -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
+ });