@numa-tech/numa 1.13.2 → 1.13.3

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,243 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
+ import { basename, join, resolve } from 'node:path';
6
+
7
+ const root = resolve(process.argv[2] ?? process.cwd());
8
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
9
+ process.stderr.write('Project directory does not exist or is not a directory.\n');
10
+ process.exit(2);
11
+ }
12
+
13
+ const excludedDirectories = new Set([
14
+ '.git', '.idea', '.next', '.nuxt', '.output', '.turbo', '.venv',
15
+ '.superpowers', '.worktrees', 'coverage', 'dist', 'node_modules', 'out',
16
+ 'target', 'test-results'
17
+ ]);
18
+ const sensitiveNames = /^(?:\.env(?:\..*)?|.*(?:secret|credential|private[-_.]?key|token).*)$/iu;
19
+ const maximumFiles = 5_000;
20
+ const maximumDepth = 7;
21
+ const files = [];
22
+
23
+ function walk(directory, depth = 0) {
24
+ if (depth > maximumDepth || files.length >= maximumFiles) return;
25
+ let entries = [];
26
+ try {
27
+ entries = readdirSync(directory, { withFileTypes: true });
28
+ } catch {
29
+ return;
30
+ }
31
+ for (const entry of entries) {
32
+ if (files.length >= maximumFiles) break;
33
+ if (entry.isDirectory()) {
34
+ if (!excludedDirectories.has(entry.name) && !sensitiveNames.test(entry.name)) {
35
+ walk(join(directory, entry.name), depth + 1);
36
+ }
37
+ continue;
38
+ }
39
+ if (entry.isFile() && !sensitiveNames.test(entry.name)) files.push(join(directory, entry.name));
40
+ }
41
+ }
42
+
43
+ walk(root);
44
+ const relativeFiles = files.map((file) => file.slice(root.length + 1));
45
+ const fileSet = new Set(relativeFiles);
46
+
47
+ function text(relativePath, maximumBytes = 1_000_000) {
48
+ if (!fileSet.has(relativePath)) return null;
49
+ try {
50
+ const value = readFileSync(join(root, relativePath));
51
+ return value.byteLength <= maximumBytes ? value.toString('utf8') : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function json(relativePath) {
58
+ const value = text(relativePath);
59
+ if (!value) return null;
60
+ try {
61
+ return JSON.parse(value);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function git(args) {
68
+ try {
69
+ return execFileSync('git', args, {
70
+ cwd: root,
71
+ encoding: 'utf8',
72
+ stdio: ['ignore', 'pipe', 'ignore'],
73
+ maxBuffer: 2_000_000
74
+ }).trim();
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function sanitizeRemote(value) {
81
+ if (!value) return null;
82
+ try {
83
+ const parsed = new URL(value);
84
+ parsed.username = '';
85
+ parsed.password = '';
86
+ parsed.search = '';
87
+ parsed.hash = '';
88
+ return parsed.toString();
89
+ } catch {
90
+ return value
91
+ .replace(/^[^@\s]+@([^:]+):/u, '$1:')
92
+ .replace(/([?&](?:access_?token|token|password|secret)=)[^&#\s]+/giu, '$1[REDACTED]');
93
+ }
94
+ }
95
+
96
+ function slug(value) {
97
+ return String(value ?? '')
98
+ .trim()
99
+ .replace(/^@[^/]+\//u, '')
100
+ .replace(/\.git$/u, '')
101
+ .split(/[/:]/u)
102
+ .at(-1)
103
+ ?.toLowerCase()
104
+ .replace(/[^a-z0-9]+/gu, '-')
105
+ .replace(/^-+|-+$/gu, '') || null;
106
+ }
107
+
108
+ const packageFiles = relativeFiles.filter((file) => /(^|\/)package\.json$/u.test(file));
109
+ const manifests = packageFiles.flatMap((file) => {
110
+ const value = json(file);
111
+ return value && typeof value === 'object' ? [{ file, value }] : [];
112
+ });
113
+ const rootPackage = manifests.find((entry) => entry.file === 'package.json')?.value ?? null;
114
+ const dependencies = new Set();
115
+ for (const { value } of manifests) {
116
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) {
117
+ for (const name of Object.keys(value[section] ?? {})) dependencies.add(name);
118
+ }
119
+ }
120
+
121
+ const jenkins = text('Jenkinsfile') ?? '';
122
+ const dockerfile = text('Dockerfile') ?? '';
123
+ const jenkinsVariables = {};
124
+ for (const key of ['APP_NAME', 'APP_HOME', 'BUILD_FLOW', 'TRIGGER_BUILD', 'NODE_VERSION', 'JDK_VERSION', 'SONAR_SCAN']) {
125
+ const match = jenkins.match(new RegExp(`\\b${key}\\s*=\\s*["']([^"']+)["']`, 'u'));
126
+ if (match) jenkinsVariables[key] = match[1];
127
+ }
128
+
129
+ const remoteNames = (git(['remote']) ?? '').split(/\r?\n/u).filter(Boolean);
130
+ const remotes = remoteNames.map((name) => ({
131
+ name,
132
+ url: sanitizeRemote(git(['remote', 'get-url', name]))
133
+ }));
134
+ const primaryRemote = remotes.find((item) => item.name === 'origin') ?? remotes[0] ?? null;
135
+ const statusLines = (git(['status', '--porcelain', '--untracked-files=all']) ?? '')
136
+ .split(/\r?\n/u).filter(Boolean);
137
+ const branch = git(['branch', '--show-current']);
138
+ const head = git(['rev-parse', 'HEAD']);
139
+ const upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
140
+ const isGitRepository = git(['rev-parse', '--is-inside-work-tree']) === 'true';
141
+ const trackedPathCount = (git(['ls-files', '-z']) ?? '').split('\0').filter(Boolean).length;
142
+ const executableTrackedPathCount = (git(['ls-files', '-s', '-z']) ?? '')
143
+ .split('\0')
144
+ .filter((entry) => entry.startsWith('100755 '))
145
+ .length;
146
+ const sourceState = !isGitRepository ? 'UNVERSIONED_DIRECTORY'
147
+ : statusLines.length > 0 ? 'DIRTY_WORKTREE'
148
+ : remoteNames.length === 0 && head ? 'LOCAL_HISTORY_WITHOUT_REMOTE'
149
+ : remoteNames.length > 0 && !upstream ? 'REMOTE_WITHOUT_UPSTREAM'
150
+ : 'REMOTE_TRACKED';
151
+
152
+ const technologies = [];
153
+ function technology(code, evidence) {
154
+ technologies.push({ code, evidence });
155
+ }
156
+ if (fileSet.has('pom.xml')) technology('java-maven', 'pom.xml');
157
+ if (fileSet.has('settings.gradle.kts') || fileSet.has('build.gradle.kts')) technology('java-gradle', 'Gradle Kotlin DSL');
158
+ if (fileSet.has('go.mod')) technology('go', 'go.mod');
159
+ if (fileSet.has('pyproject.toml')) technology('python', 'pyproject.toml');
160
+ if (dependencies.has('@nestjs/core')) technology('nestjs', '@nestjs/core dependency');
161
+ if (dependencies.has('next')) technology('nextjs', 'next dependency');
162
+ if (dependencies.has('nuxt')) technology('nuxt', 'nuxt dependency');
163
+ if (dependencies.has('fastify')) technology('fastify', 'fastify dependency');
164
+ if (dependencies.has('express')) technology('express', 'express dependency');
165
+ if (dependencies.has('vite')) technology('vite', 'vite dependency');
166
+ if (rootPackage && technologies.length === 0) technology('node', 'package.json');
167
+
168
+ const databaseSignals = [];
169
+ const databasePackages = new Map([
170
+ ['pg', 'POSTGRESQL'], ['postgres', 'POSTGRESQL'], ['drizzle-orm', 'SQL'],
171
+ ['@prisma/client', 'SQL'], ['mysql2', 'MYSQL'], ['mariadb', 'MYSQL'],
172
+ ['mongodb', 'MONGODB'], ['mongoose', 'MONGODB'], ['better-sqlite3', 'SQLITE'], ['sqlite3', 'SQLITE']
173
+ ]);
174
+ for (const [name, engine] of databasePackages) {
175
+ if (dependencies.has(name)) databaseSignals.push({ engine, evidence: `dependency:${name}` });
176
+ }
177
+ for (const file of relativeFiles) {
178
+ if (/\/(?:drizzle|prisma|migrations?)\//u.test(`/${file}`)) {
179
+ databaseSignals.push({ engine: 'UNKNOWN_SQL', evidence: `path:${file}` });
180
+ if (databaseSignals.length >= 20) break;
181
+ }
182
+ }
183
+
184
+ const packageManager = fileSet.has('pnpm-lock.yaml') ? 'pnpm'
185
+ : fileSet.has('yarn.lock') ? 'yarn'
186
+ : fileSet.has('package-lock.json') ? 'npm'
187
+ : fileSet.has('bun.lockb') || fileSet.has('bun.lock') ? 'bun' : null;
188
+ const remoteSlug = slug(primaryRemote?.url);
189
+ const packageSlug = slug(rootPackage?.name);
190
+ const appCodeCandidate = slug(jenkinsVariables.APP_NAME) ?? remoteSlug ?? packageSlug ?? slug(basename(root));
191
+
192
+ const result = {
193
+ schemaVersion: 1,
194
+ scan: {
195
+ directoryName: basename(root),
196
+ filesConsidered: relativeFiles.length,
197
+ truncated: files.length >= maximumFiles,
198
+ maximumDepth,
199
+ sensitiveFilesRead: false
200
+ },
201
+ application: {
202
+ codeCandidate: appCodeCandidate,
203
+ evidence: jenkinsVariables.APP_NAME ? 'Jenkinsfile:APP_NAME'
204
+ : remoteSlug ? 'git:primary-remote' : rootPackage?.name ? 'package.json:name' : 'directory-name',
205
+ packageName: rootPackage?.name ?? null
206
+ },
207
+ git: {
208
+ repository: isGitRepository,
209
+ sourceState,
210
+ branch: branch || null,
211
+ head: head || null,
212
+ upstream: upstream || null,
213
+ dirty: statusLines.length > 0,
214
+ changedPathCount: statusLines.length,
215
+ trackedPathCount,
216
+ executableTrackedPathCount,
217
+ remotes
218
+ },
219
+ build: {
220
+ packageManager,
221
+ packageFiles,
222
+ technologies,
223
+ jenkinsfile: fileSet.has('Jenkinsfile'),
224
+ jenkinsVariables,
225
+ buildEntry: /\bbuildEntry\(\)/u.test(jenkins),
226
+ dockerfile: fileSet.has('Dockerfile'),
227
+ dockerExposedPorts: [...dockerfile.matchAll(/^EXPOSE\s+([^\r\n#]+)/gmu)].map((match) => match[1].trim()),
228
+ hasCompose: fileSet.has('compose.yaml') || fileSet.has('docker-compose.yml')
229
+ },
230
+ database: {
231
+ signals: databaseSignals,
232
+ recommendation: databaseSignals.length === 0 ? 'NONE'
233
+ : databaseSignals.some((item) => item.engine === 'POSTGRESQL') ? 'POSTGRESQL'
234
+ : databaseSignals.every((item) => item.engine === databaseSignals[0]?.engine) ? databaseSignals[0]?.engine : 'REVIEW'
235
+ },
236
+ deployment: {
237
+ hasHelm: relativeFiles.some((file) => /(^|\/)Chart\.ya?ml$/u.test(file)),
238
+ hasKubernetesManifests: relativeFiles.some((file) => /(^|\/)(?:k8s|kubernetes|helm)\//u.test(file)),
239
+ hasCloudflareWorker: relativeFiles.some((file) => /(^|\/)wrangler(?:\.[^.]+)?\.(?:toml|jsonc?)$/u.test(file))
240
+ }
241
+ };
242
+
243
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);