genbox 1.0.3 → 1.0.5

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,424 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-Layer Project Scanner
4
+ *
5
+ * Industry-standard approach for detecting:
6
+ * - Project structure (single-app, monorepo, microservices)
7
+ * - Runtime/language versions
8
+ * - Frameworks
9
+ * - Services from docker-compose
10
+ * - Environment variables
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
24
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ProjectScanner = void 0;
28
+ __exportStar(require("./types"), exports);
29
+ __exportStar(require("./structure-detector"), exports);
30
+ __exportStar(require("./runtime-detector"), exports);
31
+ __exportStar(require("./framework-detector"), exports);
32
+ __exportStar(require("./compose-parser"), exports);
33
+ __exportStar(require("./env-analyzer"), exports);
34
+ __exportStar(require("./config-generator"), exports);
35
+ const structure_detector_1 = require("./structure-detector");
36
+ const runtime_detector_1 = require("./runtime-detector");
37
+ const framework_detector_1 = require("./framework-detector");
38
+ const compose_parser_1 = require("./compose-parser");
39
+ const env_analyzer_1 = require("./env-analyzer");
40
+ class ProjectScanner {
41
+ structureDetector;
42
+ runtimeDetector;
43
+ frameworkDetector;
44
+ composeParser;
45
+ envAnalyzer;
46
+ constructor() {
47
+ this.structureDetector = new structure_detector_1.StructureDetector();
48
+ this.runtimeDetector = new runtime_detector_1.RuntimeDetector();
49
+ this.frameworkDetector = new framework_detector_1.FrameworkDetector();
50
+ this.composeParser = new compose_parser_1.ComposeParser();
51
+ this.envAnalyzer = new env_analyzer_1.EnvAnalyzer();
52
+ }
53
+ /**
54
+ * Perform a comprehensive project scan
55
+ */
56
+ async scan(root, options = {}) {
57
+ // Layer 1: Detect project structure
58
+ const structure = await this.structureDetector.detect(root, options.exclude);
59
+ // Layer 2: Detect runtimes and versions
60
+ const runtimes = await this.runtimeDetector.detect(root, structure);
61
+ // Layer 3: Detect frameworks
62
+ const frameworks = await this.frameworkDetector.detect(root, runtimes);
63
+ // Layer 4: Parse docker-compose if present (skip if option set)
64
+ const compose = options.skipCompose ? null : await this.composeParser.parse(root);
65
+ // Layer 5: Discover apps (different approach based on structure)
66
+ const apps = await this.discoverApps(root, structure, compose, options.exclude);
67
+ // Layer 6: Analyze environment variables (skip if option set)
68
+ const envAnalysis = options.skipEnv
69
+ ? { required: [], optional: [], secrets: [], references: [], sources: [] }
70
+ : await this.envAnalyzer.analyze(root, apps, compose);
71
+ // Layer 7: Detect git info
72
+ const git = await this.detectGit(root);
73
+ // Layer 8: Find scripts (skip if option set)
74
+ const scripts = options.skipScripts ? [] : await this.findScripts(root);
75
+ return {
76
+ projectName: this.getProjectName(root),
77
+ root,
78
+ structure,
79
+ runtimes,
80
+ frameworks,
81
+ compose,
82
+ apps,
83
+ envAnalysis,
84
+ git,
85
+ scripts,
86
+ };
87
+ }
88
+ getProjectName(root) {
89
+ const path = require('path');
90
+ return path.basename(root);
91
+ }
92
+ async discoverApps(root, structure, compose, exclude) {
93
+ const fs = require('fs').promises;
94
+ const path = require('path');
95
+ const apps = [];
96
+ if (structure.type === 'hybrid') {
97
+ // Multi-repo workspace - scan all subdirectories with package.json/etc.
98
+ const subApps = await this.discoverMultiRepoApps(root, exclude);
99
+ apps.push(...subApps);
100
+ }
101
+ else if (structure.type === 'microservices' && compose) {
102
+ // Apps from docker-compose services
103
+ for (const service of compose.applications) {
104
+ apps.push({
105
+ name: service.name,
106
+ path: service.build?.context || root,
107
+ type: this.inferAppType(service.name),
108
+ port: service.ports[0]?.host,
109
+ dependencies: service.dependsOn,
110
+ scripts: {},
111
+ });
112
+ }
113
+ }
114
+ else if (structure.type.startsWith('monorepo')) {
115
+ // Discover workspace apps
116
+ const workspaceApps = await this.discoverWorkspaceApps(root, structure);
117
+ apps.push(...workspaceApps);
118
+ }
119
+ else {
120
+ // Single app - the root itself
121
+ const pkgPath = path.join(root, 'package.json');
122
+ let scripts = {};
123
+ try {
124
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
125
+ scripts = pkg.scripts || {};
126
+ }
127
+ catch { }
128
+ apps.push({
129
+ name: this.getProjectName(root),
130
+ path: root,
131
+ type: 'backend', // Will be refined by framework detection
132
+ scripts,
133
+ });
134
+ }
135
+ return apps;
136
+ }
137
+ /**
138
+ * Discover apps in a multi-repo workspace (each subdir is independent project)
139
+ */
140
+ async discoverMultiRepoApps(root, exclude) {
141
+ const fs = require('fs').promises;
142
+ const fsSync = require('fs');
143
+ const path = require('path');
144
+ const apps = [];
145
+ // Default exclusions
146
+ const defaultExclusions = ['node_modules', 'dist', 'build', '.git', 'backup', 'backups', 'logs'];
147
+ const exclusions = new Set([...defaultExclusions, ...(exclude || [])]);
148
+ try {
149
+ const entries = fsSync.readdirSync(root, { withFileTypes: true });
150
+ for (const entry of entries) {
151
+ if (!entry.isDirectory())
152
+ continue;
153
+ if (entry.name.startsWith('.'))
154
+ continue;
155
+ if (exclusions.has(entry.name))
156
+ continue;
157
+ const subPath = path.join(root, entry.name);
158
+ const pkgPath = path.join(subPath, 'package.json');
159
+ // Check for Node.js project
160
+ if (fsSync.existsSync(pkgPath)) {
161
+ try {
162
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
163
+ const hasStartScript = !!(pkg.scripts?.start || pkg.scripts?.dev || pkg.scripts?.serve);
164
+ const type = this.inferAppTypeFromPackage(entry.name, pkg, hasStartScript);
165
+ // Try to detect framework
166
+ let framework;
167
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
168
+ if (deps['next'])
169
+ framework = 'nextjs';
170
+ else if (deps['@nestjs/core'])
171
+ framework = 'nestjs';
172
+ else if (deps['react-admin'])
173
+ framework = 'react-admin';
174
+ else if (deps['react'])
175
+ framework = 'react';
176
+ else if (deps['vue'])
177
+ framework = 'vue';
178
+ else if (deps['express'])
179
+ framework = 'express';
180
+ // Try to detect port from package.json scripts
181
+ let port;
182
+ const devScript = pkg.scripts?.dev || pkg.scripts?.start || '';
183
+ const portMatch = devScript.match(/--port[=\s](\d+)|PORT[=\s](\d+)|-p[=\s](\d+)/);
184
+ if (portMatch) {
185
+ port = parseInt(portMatch[1] || portMatch[2] || portMatch[3], 10);
186
+ }
187
+ apps.push({
188
+ name: entry.name,
189
+ path: entry.name,
190
+ type,
191
+ framework: framework,
192
+ port,
193
+ scripts: pkg.scripts || {},
194
+ dependencies: Object.keys(pkg.dependencies || {}),
195
+ });
196
+ }
197
+ catch {
198
+ // Invalid package.json, skip
199
+ }
200
+ }
201
+ // Check for Go project
202
+ const goModPath = path.join(subPath, 'go.mod');
203
+ if (fsSync.existsSync(goModPath)) {
204
+ apps.push({
205
+ name: entry.name,
206
+ path: entry.name,
207
+ type: 'backend',
208
+ scripts: {},
209
+ });
210
+ }
211
+ // Check for Python project
212
+ const pyprojectPath = path.join(subPath, 'pyproject.toml');
213
+ const requirementsPath = path.join(subPath, 'requirements.txt');
214
+ if (fsSync.existsSync(pyprojectPath) || fsSync.existsSync(requirementsPath)) {
215
+ apps.push({
216
+ name: entry.name,
217
+ path: entry.name,
218
+ type: 'backend',
219
+ scripts: {},
220
+ });
221
+ }
222
+ }
223
+ }
224
+ catch {
225
+ // Error reading directory
226
+ }
227
+ return apps;
228
+ }
229
+ async discoverWorkspaceApps(root, structure) {
230
+ const fs = require('fs').promises;
231
+ const path = require('path');
232
+ const glob = require('fast-glob');
233
+ const apps = [];
234
+ // Get workspace patterns based on type
235
+ let patterns = [];
236
+ switch (structure.type) {
237
+ case 'monorepo-pnpm': {
238
+ // Read pnpm-workspace.yaml
239
+ try {
240
+ const yaml = require('js-yaml');
241
+ const content = await fs.readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8');
242
+ const workspace = yaml.load(content);
243
+ patterns = workspace.packages || [];
244
+ }
245
+ catch { }
246
+ break;
247
+ }
248
+ case 'monorepo-npm':
249
+ case 'monorepo-yarn': {
250
+ // Read package.json workspaces
251
+ try {
252
+ const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
253
+ const workspaces = pkg.workspaces;
254
+ if (Array.isArray(workspaces)) {
255
+ patterns = workspaces;
256
+ }
257
+ else if (workspaces?.packages) {
258
+ patterns = workspaces.packages;
259
+ }
260
+ }
261
+ catch { }
262
+ break;
263
+ }
264
+ case 'monorepo-nx': {
265
+ // Check apps/ and libs/ directories
266
+ patterns = ['apps/*', 'libs/*', 'packages/*'];
267
+ break;
268
+ }
269
+ case 'monorepo-turborepo': {
270
+ // Check apps/ and packages/ directories
271
+ patterns = ['apps/*', 'packages/*'];
272
+ break;
273
+ }
274
+ default:
275
+ patterns = ['packages/*', 'apps/*'];
276
+ }
277
+ // Find all matching directories with package.json
278
+ for (const pattern of patterns) {
279
+ const dirs = await glob(pattern, {
280
+ cwd: root,
281
+ onlyDirectories: true,
282
+ absolute: false,
283
+ });
284
+ for (const dir of dirs) {
285
+ const pkgPath = path.join(root, dir, 'package.json');
286
+ try {
287
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
288
+ // Determine if it's an app or library
289
+ const hasStartScript = !!(pkg.scripts?.start || pkg.scripts?.dev || pkg.scripts?.serve);
290
+ const type = this.inferAppTypeFromPackage(dir, pkg, hasStartScript);
291
+ apps.push({
292
+ name: pkg.name || path.basename(dir),
293
+ path: dir,
294
+ type,
295
+ scripts: pkg.scripts || {},
296
+ dependencies: Object.keys(pkg.dependencies || {}),
297
+ });
298
+ }
299
+ catch {
300
+ // No package.json, skip
301
+ }
302
+ }
303
+ }
304
+ return apps;
305
+ }
306
+ inferAppType(name) {
307
+ const lowerName = name.toLowerCase();
308
+ if (/gateway|proxy|ingress/.test(lowerName))
309
+ return 'gateway';
310
+ if (/frontend|web|ui|client|app/.test(lowerName))
311
+ return 'frontend';
312
+ if (/worker|queue|consumer|job/.test(lowerName))
313
+ return 'worker';
314
+ if (/backend|server|service|api/.test(lowerName))
315
+ return 'backend';
316
+ return 'backend';
317
+ }
318
+ inferAppTypeFromPackage(dir, pkg, hasStartScript) {
319
+ const deps = pkg.dependencies || {};
320
+ const name = (pkg.name || dir).toLowerCase();
321
+ // Check dependencies for type hints
322
+ if (deps.react || deps.vue || deps.svelte || deps['@angular/core']) {
323
+ return 'frontend';
324
+ }
325
+ if (deps['@nestjs/core'] || deps.express || deps.fastify || deps.koa) {
326
+ return 'backend';
327
+ }
328
+ if (deps['bull'] || deps['bullmq'] || deps['bee-queue']) {
329
+ return 'worker';
330
+ }
331
+ // Path-based inference
332
+ if (dir.includes('libs/') || dir.includes('packages/shared')) {
333
+ return 'library';
334
+ }
335
+ if (/web|frontend|ui|client/.test(name))
336
+ return 'frontend';
337
+ if (/api|backend|server/.test(name))
338
+ return 'backend';
339
+ if (/worker|queue/.test(name))
340
+ return 'worker';
341
+ // If it has a start script, it's probably an app
342
+ return hasStartScript ? 'backend' : 'library';
343
+ }
344
+ async detectGit(root) {
345
+ const { execSync } = require('child_process');
346
+ try {
347
+ const remote = execSync('git remote get-url origin', {
348
+ cwd: root,
349
+ stdio: 'pipe',
350
+ encoding: 'utf8',
351
+ }).trim();
352
+ if (!remote)
353
+ return undefined;
354
+ const isSSH = remote.startsWith('git@') || remote.startsWith('ssh://');
355
+ // Extract provider
356
+ let provider = 'other';
357
+ if (remote.includes('github.com'))
358
+ provider = 'github';
359
+ else if (remote.includes('gitlab.com'))
360
+ provider = 'gitlab';
361
+ else if (remote.includes('bitbucket.org'))
362
+ provider = 'bitbucket';
363
+ // Get current branch
364
+ let branch = 'main';
365
+ try {
366
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
367
+ cwd: root,
368
+ stdio: 'pipe',
369
+ encoding: 'utf8',
370
+ }).trim();
371
+ }
372
+ catch { }
373
+ return {
374
+ remote,
375
+ type: isSSH ? 'ssh' : 'https',
376
+ provider,
377
+ branch,
378
+ };
379
+ }
380
+ catch {
381
+ return undefined;
382
+ }
383
+ }
384
+ async findScripts(root) {
385
+ const fs = require('fs').promises;
386
+ const path = require('path');
387
+ const glob = require('fast-glob');
388
+ const scripts = [];
389
+ // Find shell scripts in common locations
390
+ const scriptPatterns = [
391
+ 'scripts/**/*.sh',
392
+ 'bin/*.sh',
393
+ '*.sh',
394
+ 'Makefile',
395
+ ];
396
+ const files = await glob(scriptPatterns, {
397
+ cwd: root,
398
+ ignore: ['node_modules/**', '.git/**', 'vendor/**'],
399
+ });
400
+ for (const file of files) {
401
+ const fullPath = path.join(root, file);
402
+ const stat = await fs.stat(fullPath);
403
+ // Determine script type based on location/name
404
+ let stage = 'setup';
405
+ const lowerFile = file.toLowerCase();
406
+ if (/setup|init|install/.test(lowerFile))
407
+ stage = 'setup';
408
+ else if (/build|compile/.test(lowerFile))
409
+ stage = 'build';
410
+ else if (/start|run|serve/.test(lowerFile))
411
+ stage = 'start';
412
+ else if (/deploy|release/.test(lowerFile))
413
+ stage = 'post-start';
414
+ scripts.push({
415
+ name: path.basename(file),
416
+ path: file,
417
+ stage,
418
+ isExecutable: (stat.mode & 0o111) !== 0,
419
+ });
420
+ }
421
+ return scripts;
422
+ }
423
+ }
424
+ exports.ProjectScanner = ProjectScanner;