stackaudit 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jandro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # stackAudit 🕵️‍♂️
2
+
3
+ > **Elimina el "funciona en mi máquina" para siempre.**
4
+ > Una CLI Open Source para validar tu entorno de desarrollo en segundos.
5
+
6
+ `stackAudit` es una herramienta de línea de comandos diseñada para auditar el entorno local de un desarrollador frente a un archivo de configuración declarativo (`stackAudit.config.json`). Asegura que todas las dependencias, puertos, versiones y variables de entorno estén listas **antes** de que intentes iniciar tu aplicación.
7
+
8
+ ---
9
+
10
+ ## 🚀 Filosofía
11
+
12
+ * **Fail Efficiently:** No más "Whack-a-Mole" de errores. `stackAudit` ejecuta verificaciones en paralelo y te reporta *todos* los problemas de golpe.
13
+ * **Cero Configuración Oculta:** Si tu proyecto lo necesita, debe estar en `stackAudit.config.json`.
14
+ * **Local-First:** Todo ocurre en tu máquina. Tus secretos (`.env`) nunca salen de tu ordenador.
15
+ * **CI/CD Ready:** Diseñado para funcionar igual en tu laptop y en tus pipelines de GitHub Actions.
16
+
17
+ ## 📦 Instalación
18
+
19
+ ### Vía NPM (Recomendado para Node.js)
20
+
21
+ ```bash
22
+ npm install -g stackAudit
23
+ # O ejecútalo directamente con npx
24
+ npx stackAudit check
25
+ ```
26
+
27
+ ### Binarios Standalone (Próximamente)
28
+
29
+ Para desarrolladores de Go, Python, PHP, etc., ofreceremos binarios compilados (Single Executable Applications) que no requieren instalar Node.js globalmente.
30
+
31
+ ---
32
+
33
+ ## 🛠 Guía de Uso
34
+
35
+ ### 1. Inicializar
36
+
37
+ Genera un archivo de configuración base en tu proyecto:
38
+
39
+ ```bash
40
+ stackAudit init
41
+ ```
42
+
43
+ Esto creará un archivo `stackAudit.config.json` en la raíz de tu proyecto.
44
+
45
+ ### 2. Configurar
46
+
47
+ Edita `stackAudit.config.json` para definir los requisitos de tu proyecto. Ejemplo:
48
+
49
+ ```json
50
+ {
51
+ "projectName": "Mi Super SaaS",
52
+ "version": "1.0.0",
53
+ "checks": {
54
+ "node": ">=18.0.0",
55
+ "npm": ">=9.0.0",
56
+ "env": {
57
+ "required": ["DATABASE_URL", "STRIPE_SECRET_KEY"]
58
+ },
59
+ "ports": [
60
+ { "port": 5432, "name": "PostgreSQL", "type": "tcp" },
61
+ { "port": 6379, "name": "Redis" }
62
+ ],
63
+ "commands": [
64
+ {
65
+ "cmd": "docker info",
66
+ "match": "Server Version",
67
+ "errorMsg": "Docker Daemon no está corriendo."
68
+ }
69
+ ]
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### 3. Auditar
75
+
76
+ Ejecuta el comando check antes de trabajar:
77
+
78
+ ```bash
79
+ stackAudit check
80
+ ```
81
+
82
+ ✅ Si todo está bien, verás un mensaje de éxito.
83
+ ❌ Si algo falla, recibirás un reporte detallado de qué falta (ej: puerto 5432 ocupado, falta variable STRIPE_KEY, versión de Node incorrecta).
84
+
85
+ ---
86
+
87
+ ## 🏗 Arquitectura y Tecnología
88
+
89
+ `stackAudit` está construido con tecnologías modernas y pensado para ser robusto:
90
+
91
+ * **Core:** Node.js (>=18) + TypeScript 5.x (ESModules).
92
+ * **Ejecución:** Paralela (`Promise.allSettled`) para máxima velocidad.
93
+ * **Validación:** Zod para esquemas estrictos.
94
+ * **UX:** `commander`, `chalk`, `ora` para una experiencia de terminal premium.
95
+ * **Checks Inteligentes:**
96
+ * **Puertos:** Estrategia "Wait-for" con backoff exponencial (evita falsos negativos si la DB está arrancando).
97
+ * **Env:** Valida contra `process.env` (compatible con Docker/K8s/CI), no solo archivos de texto.
98
+
99
+ ## 🗺 Roadmap
100
+
101
+ - [ ] **Fase 1 (MVP):** Validación de Node, Archivos y CLI básica.
102
+ - [ ] **Fase 2 (Robustez):** Checks de Puertos (Wait-for), Variables de Entorno, Comandos custom.
103
+ - [ ] **Fase 3 (DevEx):** Comando `init`, UI mejorada, Distribución de binarios nativos (SEA).
104
+
105
+ ## 🤝 Contribuyendo
106
+
107
+ ¡Las contribuciones son bienvenidas!
108
+
109
+ 1. Haz un Fork del repositorio.
110
+ 2. Crea tu rama de feature (`git checkout -b feature/AmazingFeature`).
111
+ 3. Haz Commit de tus cambios (`git commit -m 'Add some AmazingFeature'`).
112
+ 4. Push a la rama (`git push origin feature/AmazingFeature`).
113
+ 5. Abre un Pull Request.
114
+
115
+ ## 📄 Licencia
116
+
117
+ Distribuido bajo la licencia MIT. Ver `LICENSE` para más información.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import("../dist/index.js");
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,777 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/check.ts
7
+ import ora from "ora";
8
+
9
+ // src/core/configLoader.ts
10
+ import { readFile } from "fs/promises";
11
+ import { resolve } from "path";
12
+
13
+ // src/schemas/config.schema.ts
14
+ import { z } from "zod";
15
+ var portSchema = z.object({
16
+ port: z.number().int().min(1).max(65535),
17
+ name: z.string().min(1),
18
+ type: z.enum(["tcp"]).optional().default("tcp")
19
+ });
20
+ var envSchema = z.object({
21
+ target: z.string().min(1),
22
+ example: z.string().min(1),
23
+ required: z.array(z.string().min(1)).min(1, "At least one required env var must be specified")
24
+ });
25
+ var commandSchema = z.object({
26
+ cmd: z.string().min(1),
27
+ match: z.string().optional(),
28
+ errorMsg: z.string().optional()
29
+ });
30
+ var semverRangeString = z.string().min(1, "Semver range cannot be empty");
31
+ var checksSchema = z.object({
32
+ node: semverRangeString.optional(),
33
+ npm: semverRangeString.optional(),
34
+ env: envSchema.optional(),
35
+ ports: z.array(portSchema).min(1, "Ports array cannot be empty").optional(),
36
+ files: z.array(z.string().min(1)).min(1, "Files array cannot be empty").optional(),
37
+ commands: z.array(commandSchema).min(1, "Commands array cannot be empty").optional()
38
+ }).refine(
39
+ (checks) => {
40
+ const hasNode = checks.node !== void 0;
41
+ const hasNpm = checks.npm !== void 0;
42
+ const hasEnv = checks.env !== void 0;
43
+ const hasPorts = checks.ports !== void 0 && checks.ports.length > 0;
44
+ const hasFiles = checks.files !== void 0 && checks.files.length > 0;
45
+ const hasCommands = checks.commands !== void 0 && checks.commands.length > 0;
46
+ return hasNode || hasNpm || hasEnv || hasPorts || hasFiles || hasCommands;
47
+ },
48
+ { message: "At least one check must be configured" }
49
+ );
50
+ var configSchema = z.object({
51
+ projectName: z.string().min(1, "projectName is required"),
52
+ version: z.string().min(1, "version is required"),
53
+ checks: checksSchema
54
+ });
55
+
56
+ // src/core/configLoader.ts
57
+ var DEFAULT_CONFIG_FILE = "stackAudit.config.json";
58
+ async function loadConfig(configPath) {
59
+ const filePath = resolve(process.cwd(), configPath ?? DEFAULT_CONFIG_FILE);
60
+ let raw;
61
+ try {
62
+ raw = await readFile(filePath, "utf-8");
63
+ } catch {
64
+ throw new Error(
65
+ `Config file not found: ${filePath}
66
+ Run "stackaudit init" to create one, or use --config to specify a path.`
67
+ );
68
+ }
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ } catch {
73
+ throw new Error(
74
+ `Invalid JSON in config file: ${filePath}
75
+ Check for trailing commas or syntax errors.`
76
+ );
77
+ }
78
+ const result = configSchema.safeParse(parsed);
79
+ if (!result.success) {
80
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
81
+ throw new Error(
82
+ `Invalid configuration in ${filePath}:
83
+ ${issues}`
84
+ );
85
+ }
86
+ return result.data;
87
+ }
88
+
89
+ // src/checks/nodeVersion.ts
90
+ import semver from "semver";
91
+
92
+ // src/utils/system.ts
93
+ import { execa } from "execa";
94
+ var SAFE_COMMAND_PREFIXES = [
95
+ "node --version",
96
+ "node -v",
97
+ "npm --version",
98
+ "npm -v",
99
+ "docker info",
100
+ "docker --version",
101
+ "docker compose version",
102
+ "git --version",
103
+ "python --version",
104
+ "python3 --version",
105
+ "ruby --version",
106
+ "java --version",
107
+ "javac --version",
108
+ "go version",
109
+ "rustc --version",
110
+ "cargo --version"
111
+ ];
112
+ function isCommandAllowed(command) {
113
+ const normalized = command.trim();
114
+ return SAFE_COMMAND_PREFIXES.some((safeCmd) => normalized === safeCmd);
115
+ }
116
+ function parseCommand(command) {
117
+ const tokens = [];
118
+ let current = "";
119
+ let inSingle = false;
120
+ let inDouble = false;
121
+ let escaped = false;
122
+ for (const char of command) {
123
+ if (escaped) {
124
+ current += char;
125
+ escaped = false;
126
+ continue;
127
+ }
128
+ if (char === "\\" && !inSingle) {
129
+ escaped = true;
130
+ continue;
131
+ }
132
+ if (char === "'" && !inDouble) {
133
+ inSingle = !inSingle;
134
+ continue;
135
+ }
136
+ if (char === '"' && !inSingle) {
137
+ inDouble = !inDouble;
138
+ continue;
139
+ }
140
+ if (/\s/.test(char) && !inSingle && !inDouble) {
141
+ if (current.length > 0) {
142
+ tokens.push(current);
143
+ current = "";
144
+ }
145
+ continue;
146
+ }
147
+ current += char;
148
+ }
149
+ if (current.length > 0) {
150
+ tokens.push(current);
151
+ }
152
+ return tokens;
153
+ }
154
+ async function execCommand(command) {
155
+ const tokens = parseCommand(command);
156
+ if (tokens.length === 0) {
157
+ throw new Error("Empty command");
158
+ }
159
+ const [cmd, ...args] = tokens;
160
+ const result = await execa(cmd, args, {
161
+ timeout: 1e4,
162
+ reject: false
163
+ });
164
+ if (result.failed) {
165
+ throw new Error(result.stderr || `Command "${command}" failed`);
166
+ }
167
+ return result.stdout;
168
+ }
169
+ async function timedCheck(name, fn) {
170
+ const start = performance.now();
171
+ try {
172
+ const partial = await fn();
173
+ return {
174
+ name,
175
+ ...partial,
176
+ duration: Math.round(performance.now() - start)
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ name,
181
+ status: "fail",
182
+ message: error instanceof Error ? error.message : String(error),
183
+ duration: Math.round(performance.now() - start)
184
+ };
185
+ }
186
+ }
187
+
188
+ // src/checks/nodeVersion.ts
189
+ function checkNodeVersion(requiredRange) {
190
+ return async () => {
191
+ const result = await timedCheck("Node.js Version", async () => {
192
+ const current = process.version;
193
+ const cleanCurrent = semver.clean(current);
194
+ if (!cleanCurrent) {
195
+ return {
196
+ status: "fail",
197
+ message: `Could not parse current Node.js version: ${current}`
198
+ };
199
+ }
200
+ if (!semver.validRange(requiredRange)) {
201
+ return {
202
+ status: "fail",
203
+ message: `Invalid semver range in config: "${requiredRange}"`
204
+ };
205
+ }
206
+ if (semver.satisfies(cleanCurrent, requiredRange)) {
207
+ return {
208
+ status: "pass",
209
+ message: `v${cleanCurrent} satisfies ${requiredRange}`
210
+ };
211
+ }
212
+ return {
213
+ status: "fail",
214
+ message: `v${cleanCurrent} does not satisfy ${requiredRange}`
215
+ };
216
+ });
217
+ return [result];
218
+ };
219
+ }
220
+
221
+ // src/checks/npmVersion.ts
222
+ import semver2 from "semver";
223
+ function checkNpmVersion(requiredRange) {
224
+ return async () => {
225
+ const result = await timedCheck("npm Version", async () => {
226
+ const stdout = await execCommand("npm --version");
227
+ const current = semver2.clean(stdout.trim());
228
+ if (!current) {
229
+ return {
230
+ status: "fail",
231
+ message: `Could not parse npm version from output: "${stdout.trim()}"`
232
+ };
233
+ }
234
+ if (!semver2.validRange(requiredRange)) {
235
+ return {
236
+ status: "fail",
237
+ message: `Invalid semver range in config: "${requiredRange}"`
238
+ };
239
+ }
240
+ if (semver2.satisfies(current, requiredRange)) {
241
+ return {
242
+ status: "pass",
243
+ message: `v${current} satisfies ${requiredRange}`
244
+ };
245
+ }
246
+ return {
247
+ status: "fail",
248
+ message: `v${current} does not satisfy ${requiredRange}`
249
+ };
250
+ });
251
+ return [result];
252
+ };
253
+ }
254
+
255
+ // src/checks/envVars.ts
256
+ import { readFile as readFile2 } from "fs/promises";
257
+ import { resolve as resolve2, relative, isAbsolute } from "path";
258
+ import { parse as dotenvParse } from "dotenv";
259
+ function checkEnvVars(envConfig) {
260
+ return async () => {
261
+ const results = [];
262
+ const cwd = process.cwd();
263
+ if (isAbsolute(envConfig.target)) {
264
+ results.push({
265
+ name: `Env File (${envConfig.target})`,
266
+ status: "fail",
267
+ message: `Absolute paths are not allowed for env.target: "${envConfig.target}"`,
268
+ duration: 0
269
+ });
270
+ return results;
271
+ }
272
+ const targetPath = resolve2(cwd, envConfig.target);
273
+ const rel = relative(cwd, targetPath);
274
+ if (rel.startsWith("..")) {
275
+ results.push({
276
+ name: `Env File (${envConfig.target})`,
277
+ status: "fail",
278
+ message: `Path traversal detected: "${envConfig.target}" resolves outside the project directory`,
279
+ duration: 0
280
+ });
281
+ return results;
282
+ }
283
+ let envKeys = {};
284
+ const fileCheck = await timedCheck(`Env File (${envConfig.target})`, async () => {
285
+ let content;
286
+ try {
287
+ content = await readFile2(targetPath, "utf-8");
288
+ } catch {
289
+ return {
290
+ status: "pass",
291
+ message: `File not found: ${envConfig.target} (Using process.env)`
292
+ };
293
+ }
294
+ envKeys = dotenvParse(content);
295
+ return {
296
+ status: "pass",
297
+ message: `${envConfig.target} loaded successfully`
298
+ };
299
+ });
300
+ results.push(fileCheck);
301
+ for (const key of envConfig.required) {
302
+ const keyResult = await timedCheck(`Env: ${key}`, async () => {
303
+ const value = envKeys[key] ?? process.env[key];
304
+ if (value === void 0) {
305
+ return {
306
+ status: "fail",
307
+ message: `Missing required variable "${key}" in ${envConfig.target} and process.env`
308
+ };
309
+ }
310
+ if (value.trim() === "") {
311
+ return {
312
+ status: "fail",
313
+ message: `Variable "${key}" exists but is empty`
314
+ };
315
+ }
316
+ return {
317
+ status: "pass",
318
+ message: `"${key}" is set (value hidden)`
319
+ };
320
+ });
321
+ results.push(keyResult);
322
+ }
323
+ return results;
324
+ };
325
+ }
326
+
327
+ // src/checks/ports.ts
328
+ import { createConnection } from "net";
329
+ var DEFAULT_TIMEOUT_MS = 3e3;
330
+ function probePort(port, timeoutMs = DEFAULT_TIMEOUT_MS) {
331
+ return new Promise((resolve5) => {
332
+ const socket = createConnection({ port, host: "127.0.0.1" });
333
+ const timer = setTimeout(() => {
334
+ socket.destroy();
335
+ resolve5(false);
336
+ }, timeoutMs);
337
+ socket.on("connect", () => {
338
+ clearTimeout(timer);
339
+ socket.destroy();
340
+ resolve5(true);
341
+ });
342
+ socket.on("error", () => {
343
+ clearTimeout(timer);
344
+ socket.destroy();
345
+ resolve5(false);
346
+ });
347
+ });
348
+ }
349
+ function checkPorts(portsConfig) {
350
+ return async () => {
351
+ const results = [];
352
+ for (const portDef of portsConfig) {
353
+ const result = await timedCheck(
354
+ `Port ${portDef.port} (${portDef.name})`,
355
+ async () => {
356
+ const isOpen = await probePort(portDef.port);
357
+ if (isOpen) {
358
+ return {
359
+ status: "pass",
360
+ message: `${portDef.name} is accepting connections on port ${portDef.port}`
361
+ };
362
+ }
363
+ return {
364
+ status: "fail",
365
+ message: `Nothing listening on port ${portDef.port}. Is ${portDef.name} running?`
366
+ };
367
+ }
368
+ );
369
+ results.push(result);
370
+ }
371
+ return results;
372
+ };
373
+ }
374
+
375
+ // src/checks/files.ts
376
+ import * as fs from "fs/promises";
377
+ import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2 } from "path";
378
+ function checkFiles(filesList) {
379
+ return async () => {
380
+ const results = [];
381
+ const cwd = process.cwd();
382
+ for (const file of filesList) {
383
+ const result = await timedCheck(`File: ${file}`, async () => {
384
+ if (isAbsolute2(file)) {
385
+ return {
386
+ status: "fail",
387
+ message: `Absolute paths are not allowed in file checks: "${file}"`
388
+ };
389
+ }
390
+ const filePath = resolve3(cwd, file);
391
+ const realPath = await fs.realpath(filePath);
392
+ const rel = relative2(cwd, realPath);
393
+ if (rel.startsWith("..")) {
394
+ return {
395
+ status: "fail",
396
+ message: `Path traversal detected: "${file}" resolves outside the project directory`
397
+ };
398
+ }
399
+ try {
400
+ await fs.access(realPath);
401
+ } catch {
402
+ return {
403
+ status: "fail",
404
+ message: `Required file not found: ${file}`
405
+ };
406
+ }
407
+ return {
408
+ status: "pass",
409
+ message: `${file} exists`
410
+ };
411
+ });
412
+ results.push(result);
413
+ }
414
+ return results;
415
+ };
416
+ }
417
+
418
+ // src/checks/commands.ts
419
+ function checkCommands(commandsConfig, trustCommands = false) {
420
+ return async () => {
421
+ const results = [];
422
+ for (const cmdDef of commandsConfig) {
423
+ const label = cmdDef.cmd.length > 40 ? cmdDef.cmd.slice(0, 37) + "..." : cmdDef.cmd;
424
+ if (!trustCommands && !isCommandAllowed(cmdDef.cmd)) {
425
+ results.push({
426
+ name: `Command: ${label}`,
427
+ status: "skip",
428
+ message: `Skipped untrusted command: "${cmdDef.cmd}". Use --trust-commands to allow execution of custom commands.`,
429
+ duration: 0
430
+ });
431
+ continue;
432
+ }
433
+ const result = await timedCheck(`Command: ${label}`, async () => {
434
+ let stdout;
435
+ try {
436
+ stdout = await execCommand(cmdDef.cmd);
437
+ } catch (error) {
438
+ const msg = cmdDef.errorMsg ?? `Command failed: "${cmdDef.cmd}" \u2014 ${error instanceof Error ? error.message : String(error)}`;
439
+ return { status: "fail", message: msg };
440
+ }
441
+ if (cmdDef.match && !stdout.includes(cmdDef.match)) {
442
+ const msg = cmdDef.errorMsg ?? `Output of "${cmdDef.cmd}" does not contain "${cmdDef.match}"`;
443
+ return { status: "fail", message: msg };
444
+ }
445
+ return {
446
+ status: "pass",
447
+ message: cmdDef.match ? `"${cmdDef.cmd}" output contains "${cmdDef.match}"` : `"${cmdDef.cmd}" executed successfully`
448
+ };
449
+ });
450
+ results.push(result);
451
+ }
452
+ return results;
453
+ };
454
+ }
455
+
456
+ // src/checks/index.ts
457
+ function buildCheckerPipeline(checks, options = { trustCommands: false }) {
458
+ const pipeline = [];
459
+ if (checks.node) {
460
+ pipeline.push(checkNodeVersion(checks.node));
461
+ }
462
+ if (checks.npm) {
463
+ pipeline.push(checkNpmVersion(checks.npm));
464
+ }
465
+ if (checks.files && checks.files.length > 0) {
466
+ pipeline.push(checkFiles(checks.files));
467
+ }
468
+ if (checks.env) {
469
+ pipeline.push(checkEnvVars(checks.env));
470
+ }
471
+ if (checks.ports && checks.ports.length > 0) {
472
+ pipeline.push(checkPorts(checks.ports));
473
+ }
474
+ if (checks.commands && checks.commands.length > 0) {
475
+ pipeline.push(checkCommands(checks.commands, options.trustCommands));
476
+ }
477
+ return pipeline;
478
+ }
479
+
480
+ // src/core/runner.ts
481
+ async function runAudit(config, options = { trustCommands: false }) {
482
+ const checkers = buildCheckerPipeline(config.checks, options);
483
+ const settled = await Promise.allSettled(
484
+ checkers.map((checker) => checker())
485
+ );
486
+ const results = [];
487
+ for (const outcome of settled) {
488
+ if (outcome.status === "fulfilled") {
489
+ results.push(...outcome.value);
490
+ } else {
491
+ results.push({
492
+ name: "Unknown Check",
493
+ status: "fail",
494
+ message: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
495
+ duration: 0
496
+ });
497
+ }
498
+ }
499
+ const summary = { passed: 0, failed: 0, warned: 0, skipped: 0, total: results.length };
500
+ for (const r of results) {
501
+ switch (r.status) {
502
+ case "pass":
503
+ summary.passed++;
504
+ break;
505
+ case "fail":
506
+ summary.failed++;
507
+ break;
508
+ case "warn":
509
+ summary.warned++;
510
+ break;
511
+ case "skip":
512
+ summary.skipped++;
513
+ break;
514
+ }
515
+ }
516
+ return {
517
+ projectName: config.projectName,
518
+ timestamp: /* @__PURE__ */ new Date(),
519
+ results,
520
+ summary
521
+ };
522
+ }
523
+
524
+ // src/utils/logger.ts
525
+ import chalk from "chalk";
526
+ import boxen from "boxen";
527
+ var STATUS_ICONS = {
528
+ pass: chalk.green("\u2714"),
529
+ fail: chalk.red("\u2716"),
530
+ warn: chalk.yellow("\u26A0"),
531
+ skip: chalk.gray("\u25CB")
532
+ };
533
+ function formatCheckResult(result, verbose) {
534
+ const icon = STATUS_ICONS[result.status];
535
+ const duration = chalk.gray(`(${result.duration}ms)`);
536
+ const name = result.status === "fail" ? chalk.red(result.name) : result.name;
537
+ if (!verbose && result.status === "pass") {
538
+ return ` ${icon} ${name} ${duration}`;
539
+ }
540
+ return ` ${icon} ${name} ${duration}
541
+ ${chalk.gray(result.message)}`;
542
+ }
543
+ function formatReport(report, options) {
544
+ if (options.json) {
545
+ return JSON.stringify({
546
+ projectName: report.projectName,
547
+ timestamp: report.timestamp.toISOString(),
548
+ results: report.results,
549
+ summary: report.summary
550
+ }, null, 2);
551
+ }
552
+ const lines = [];
553
+ lines.push("");
554
+ for (const result of report.results) {
555
+ lines.push(formatCheckResult(result, options.verbose));
556
+ }
557
+ lines.push("");
558
+ const { passed, failed, warned, skipped, total } = report.summary;
559
+ const summaryParts = [
560
+ chalk.green(`${passed} passed`),
561
+ failed > 0 ? chalk.red(`${failed} failed`) : null,
562
+ warned > 0 ? chalk.yellow(`${warned} warnings`) : null,
563
+ skipped > 0 ? chalk.gray(`${skipped} skipped`) : null
564
+ ].filter(Boolean).join(chalk.gray(" \xB7 "));
565
+ const summaryLine = `${summaryParts} ${chalk.gray(`(${total} checks)`)}`;
566
+ const elapsed = report.results.reduce((sum, r) => sum + r.duration, 0);
567
+ const header = failed > 0 ? chalk.red.bold("stackAudit \u2014 FAIL") : chalk.green.bold("stackAudit \u2014 PASS");
568
+ const boxContent = [
569
+ header,
570
+ chalk.gray(`Project: ${report.projectName}`),
571
+ "",
572
+ summaryLine,
573
+ chalk.gray(`Done in ${elapsed}ms`)
574
+ ].join("\n");
575
+ lines.push(
576
+ boxen(boxContent, {
577
+ padding: 1,
578
+ margin: { top: 0, bottom: 0, left: 1, right: 1 },
579
+ borderColor: failed > 0 ? "red" : "green",
580
+ borderStyle: "round"
581
+ })
582
+ );
583
+ return lines.join("\n");
584
+ }
585
+ function logError(message) {
586
+ console.error(chalk.red.bold("Error:"), message);
587
+ }
588
+ function logInfo(message) {
589
+ console.log(chalk.blue("\u2139"), message);
590
+ }
591
+ function logSuccess(message) {
592
+ console.log(chalk.green("\u2714"), message);
593
+ }
594
+ function logWarn(message) {
595
+ console.log(chalk.yellow("\u26A0"), message);
596
+ }
597
+
598
+ // src/commands/check.ts
599
+ async function checkCommand(options) {
600
+ const silent = options.ci || options.json;
601
+ const spinner = silent ? null : ora("Loading configuration...").start();
602
+ let config;
603
+ try {
604
+ config = await loadConfig(options.config);
605
+ spinner?.succeed(`Loaded config for "${config.projectName}"`);
606
+ } catch (error) {
607
+ spinner?.fail("Configuration error");
608
+ logError(error instanceof Error ? error.message : String(error));
609
+ process.exitCode = 2;
610
+ return;
611
+ }
612
+ spinner?.start("Running checks...");
613
+ const report = await runAudit(config, { trustCommands: options.trustCommands });
614
+ spinner?.stop();
615
+ console.log(formatReport(report, { verbose: options.verbose, json: options.json }));
616
+ if (report.summary.failed > 0) {
617
+ process.exitCode = 1;
618
+ }
619
+ }
620
+
621
+ // src/commands/init.ts
622
+ import { access as access2, readFile as readFile3, writeFile } from "fs/promises";
623
+ import { basename, resolve as resolve4 } from "path";
624
+ var CONFIG_FILENAME = "stackAudit.config.json";
625
+ async function probeToolVersion(cmd) {
626
+ try {
627
+ const output = await execCommand(cmd);
628
+ return output.trim();
629
+ } catch {
630
+ return null;
631
+ }
632
+ }
633
+ async function detectFiles(candidates) {
634
+ const found = [];
635
+ for (const file of candidates) {
636
+ try {
637
+ await access2(resolve4(process.cwd(), file));
638
+ found.push(file);
639
+ } catch {
640
+ }
641
+ }
642
+ return found;
643
+ }
644
+ async function detectProjectName() {
645
+ try {
646
+ const raw = await readFile3(resolve4(process.cwd(), "package.json"), "utf-8");
647
+ const pkg = JSON.parse(raw);
648
+ if (typeof pkg.name === "string" && pkg.name.length > 0) {
649
+ return pkg.name;
650
+ }
651
+ } catch {
652
+ }
653
+ return basename(process.cwd());
654
+ }
655
+ async function buildDetectedConfig() {
656
+ const checks = {};
657
+ const nodeVersion = await probeToolVersion("node --version");
658
+ if (nodeVersion) {
659
+ const major = nodeVersion.replace(/^v/, "").split(".")[0];
660
+ checks.node = `>=${major}.0.0`;
661
+ logSuccess(`Detected Node.js ${nodeVersion} \u2192 requiring >=${major}.0.0`);
662
+ }
663
+ const npmVersion = await probeToolVersion("npm --version");
664
+ if (npmVersion) {
665
+ const major = npmVersion.split(".")[0];
666
+ checks.npm = `>=${major}.0.0`;
667
+ logSuccess(`Detected npm ${npmVersion} \u2192 requiring >=${major}.0.0`);
668
+ }
669
+ const commonFiles = [
670
+ "package.json",
671
+ "docker-compose.yml",
672
+ "docker-compose.yaml",
673
+ "Dockerfile",
674
+ "Makefile",
675
+ ".env.example",
676
+ "tsconfig.json"
677
+ ];
678
+ const foundFiles = await detectFiles(commonFiles);
679
+ if (foundFiles.length > 0) {
680
+ checks.files = foundFiles;
681
+ logSuccess(`Detected ${foundFiles.length} project files: ${foundFiles.join(", ")}`);
682
+ }
683
+ const envExamplePath = resolve4(process.cwd(), ".env.example");
684
+ try {
685
+ const content = await readFile3(envExamplePath, "utf-8");
686
+ const keys = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter((key) => key.length > 0);
687
+ if (keys.length > 0) {
688
+ checks.env = {
689
+ target: ".env",
690
+ example: ".env.example",
691
+ required: keys
692
+ };
693
+ logSuccess(`Detected ${keys.length} env vars from .env.example: ${keys.join(", ")}`);
694
+ }
695
+ } catch {
696
+ }
697
+ const dockerVersion = await probeToolVersion("docker --version");
698
+ if (dockerVersion) {
699
+ checks.commands = [
700
+ {
701
+ cmd: "docker info",
702
+ match: "Server Version",
703
+ errorMsg: "Docker daemon is not running. Start Docker Desktop or the Docker service."
704
+ }
705
+ ];
706
+ logSuccess(`Detected Docker \u2192 adding daemon check`);
707
+ }
708
+ const projectName = await detectProjectName();
709
+ return {
710
+ projectName,
711
+ version: "1.0.0",
712
+ checks
713
+ };
714
+ }
715
+ async function initCommand(options) {
716
+ const targetPath = resolve4(process.cwd(), CONFIG_FILENAME);
717
+ try {
718
+ await access2(targetPath);
719
+ throw new Error(
720
+ `${CONFIG_FILENAME} already exists in this directory. Delete it first to re-initialize.`
721
+ );
722
+ } catch {
723
+ }
724
+ let config;
725
+ if (options.detect) {
726
+ logInfo("Scanning environment...\n");
727
+ config = await buildDetectedConfig();
728
+ if (Object.keys(config.checks).length === 0) {
729
+ logWarn("No tools detected. Generating a minimal config.");
730
+ config.checks = { node: ">=18.0.0" };
731
+ }
732
+ console.log("");
733
+ } else {
734
+ config = {
735
+ projectName: await detectProjectName(),
736
+ version: "1.0.0",
737
+ checks: {
738
+ node: ">=18.0.0",
739
+ npm: ">=9.0.0",
740
+ files: ["package.json"]
741
+ }
742
+ };
743
+ }
744
+ try {
745
+ const content = JSON.stringify(config, null, 2) + "\n";
746
+ await writeFile(targetPath, content, "utf-8");
747
+ logSuccess(`Created ${CONFIG_FILENAME} \u2014 customize it for your project.`);
748
+ } catch (error) {
749
+ throw new Error(
750
+ `Failed to write ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
751
+ );
752
+ }
753
+ }
754
+
755
+ // src/index.ts
756
+ var program = new Command();
757
+ program.name("stackaudit").description(
758
+ "Audit your development environment against a declarative configuration file."
759
+ ).version("0.1.0");
760
+ program.command("check").description("Run all environment checks defined in stackAudit.config.json").option("-c, --config <path>", "Path to config file", "stackAudit.config.json").option("-v, --verbose", "Show detailed output for all checks", false).option("--ci", "CI mode \u2014 no spinners, plain output", false).option("--json", "Output results as JSON (implies --ci)", false).option(
761
+ "--trust-commands",
762
+ "Allow execution of custom commands not on the safe allowlist",
763
+ false
764
+ ).action(async (opts) => {
765
+ await checkCommand({
766
+ config: opts.config,
767
+ verbose: opts.verbose,
768
+ ci: opts.ci || opts.json,
769
+ json: opts.json,
770
+ trustCommands: opts.trustCommands
771
+ });
772
+ });
773
+ program.command("init").description("Generate a starter stackAudit.config.json in the current directory").option("-d, --detect", "Auto-detect installed tools and populate config", false).action(async (opts) => {
774
+ await initCommand({ detect: opts.detect });
775
+ });
776
+ program.parse();
777
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/check.ts","../src/core/configLoader.ts","../src/schemas/config.schema.ts","../src/checks/nodeVersion.ts","../src/utils/system.ts","../src/checks/npmVersion.ts","../src/checks/envVars.ts","../src/checks/ports.ts","../src/checks/files.ts","../src/checks/commands.ts","../src/checks/index.ts","../src/core/runner.ts","../src/utils/logger.ts","../src/commands/init.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport { checkCommand } from \"./commands/check.js\";\nimport { initCommand } from \"./commands/init.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"stackaudit\")\n .description(\n \"Audit your development environment against a declarative configuration file.\",\n )\n .version(\"0.1.0\");\n\nprogram\n .command(\"check\")\n .description(\"Run all environment checks defined in stackAudit.config.json\")\n .option(\"-c, --config <path>\", \"Path to config file\", \"stackAudit.config.json\")\n .option(\"-v, --verbose\", \"Show detailed output for all checks\", false)\n .option(\"--ci\", \"CI mode — no spinners, plain output\", false)\n .option(\"--json\", \"Output results as JSON (implies --ci)\", false)\n .option(\n \"--trust-commands\",\n \"Allow execution of custom commands not on the safe allowlist\",\n false,\n )\n .action(async (opts) => {\n await checkCommand({\n config: opts.config,\n verbose: opts.verbose,\n ci: opts.ci || opts.json,\n json: opts.json,\n trustCommands: opts.trustCommands,\n });\n });\n\nprogram\n .command(\"init\")\n .description(\"Generate a starter stackAudit.config.json in the current directory\")\n .option(\"-d, --detect\", \"Auto-detect installed tools and populate config\", false)\n .action(async (opts) => {\n await initCommand({ detect: opts.detect });\n });\n\nprogram.parse();\n","import ora from \"ora\";\nimport { loadConfig } from \"../core/configLoader.js\";\nimport { runAudit } from \"../core/runner.js\";\nimport { formatReport, logError } from \"../utils/logger.js\";\nimport type { CLIOptions } from \"../types.js\";\n\n/**\n * The \"check\" command — main entry point for environment auditing.\n *\n * Flow:\n * 1. Load and validate config\n * 2. Run all checkers in parallel\n * 3. Render report (text, verbose, or JSON)\n * 4. Exit with appropriate code (using process.exitCode for clean flush)\n */\nexport async function checkCommand(options: CLIOptions): Promise<void> {\n const silent = options.ci || options.json;\n const spinner = silent ? null : ora(\"Loading configuration...\").start();\n\n let config;\n try {\n config = await loadConfig(options.config);\n spinner?.succeed(`Loaded config for \"${config.projectName}\"`);\n } catch (error) {\n spinner?.fail(\"Configuration error\");\n logError(error instanceof Error ? error.message : String(error));\n process.exitCode = 2;\n return;\n }\n\n spinner?.start(\"Running checks...\");\n const report = await runAudit(config, { trustCommands: options.trustCommands });\n spinner?.stop();\n\n console.log(formatReport(report, { verbose: options.verbose, json: options.json }));\n\n if (report.summary.failed > 0) {\n process.exitCode = 1;\n }\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { configSchema } from \"../schemas/config.schema.js\";\nimport type { StackAuditConfig } from \"../types.js\";\n\nconst DEFAULT_CONFIG_FILE = \"stackAudit.config.json\";\n\n/**\n * Loads and validates the stackAudit configuration file.\n *\n * Flow:\n * 1. Resolve the config file path relative to CWD\n * 2. Read and parse as JSON\n * 3. Validate against the Zod schema\n * 4. Return the typed config or throw with actionable errors\n */\nexport async function loadConfig(\n configPath?: string,\n): Promise<StackAuditConfig> {\n const filePath = resolve(process.cwd(), configPath ?? DEFAULT_CONFIG_FILE);\n\n let raw: string;\n try {\n raw = await readFile(filePath, \"utf-8\");\n } catch {\n throw new Error(\n `Config file not found: ${filePath}\\n` +\n `Run \"stackaudit init\" to create one, or use --config to specify a path.`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `Invalid JSON in config file: ${filePath}\\n` +\n `Check for trailing commas or syntax errors.`,\n );\n }\n\n const result = configSchema.safeParse(parsed);\n\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => ` - ${issue.path.join(\".\")}: ${issue.message}`)\n .join(\"\\n\");\n\n throw new Error(\n `Invalid configuration in ${filePath}:\\n${issues}`,\n );\n }\n\n return result.data as StackAuditConfig;\n}\n","import { z } from \"zod\";\n\nconst portSchema = z.object({\n port: z.number().int().min(1).max(65535),\n name: z.string().min(1),\n type: z.enum([\"tcp\"]).optional().default(\"tcp\"),\n});\n\nconst envSchema = z.object({\n target: z.string().min(1),\n example: z.string().min(1),\n required: z.array(z.string().min(1)).min(1, \"At least one required env var must be specified\"),\n});\n\nconst commandSchema = z.object({\n cmd: z.string().min(1),\n match: z.string().optional(),\n errorMsg: z.string().optional(),\n});\n\n/**\n * Semver range string — validates that the value is a non-empty string\n * that semver can interpret. Prevents empty strings from bypassing\n * the \"at least one check\" refinement while producing zero actual checks.\n */\nconst semverRangeString = z.string().min(1, \"Semver range cannot be empty\");\n\nconst checksSchema = z\n .object({\n node: semverRangeString.optional(),\n npm: semverRangeString.optional(),\n env: envSchema.optional(),\n ports: z.array(portSchema).min(1, \"Ports array cannot be empty\").optional(),\n files: z.array(z.string().min(1)).min(1, \"Files array cannot be empty\").optional(),\n commands: z.array(commandSchema).min(1, \"Commands array cannot be empty\").optional(),\n })\n .refine(\n (checks) => {\n // Count checks that actually produce work — excludes undefined keys\n // and explicitly guards against the \"empty config = exit 0\" false positive.\n const hasNode = checks.node !== undefined;\n const hasNpm = checks.npm !== undefined;\n const hasEnv = checks.env !== undefined;\n const hasPorts = checks.ports !== undefined && checks.ports.length > 0;\n const hasFiles = checks.files !== undefined && checks.files.length > 0;\n const hasCommands = checks.commands !== undefined && checks.commands.length > 0;\n\n return hasNode || hasNpm || hasEnv || hasPorts || hasFiles || hasCommands;\n },\n { message: \"At least one check must be configured\" },\n );\n\nexport const configSchema = z.object({\n projectName: z.string().min(1, \"projectName is required\"),\n version: z.string().min(1, \"version is required\"),\n checks: checksSchema,\n});\n\nexport type ConfigSchemaInput = z.input<typeof configSchema>;\nexport type ConfigSchemaOutput = z.output<typeof configSchema>;\n","import semver from \"semver\";\nimport { timedCheck } from \"../utils/system.js\";\nimport type { CheckResult } from \"../types.js\";\n\n/**\n * Validates that the current Node.js version satisfies\n * the semver range specified in the config.\n *\n * Uses process.version directly — no subprocess needed.\n */\nexport function checkNodeVersion(requiredRange: string): () => Promise<CheckResult[]> {\n return async () => {\n const result = await timedCheck(\"Node.js Version\", async () => {\n const current = process.version;\n const cleanCurrent = semver.clean(current);\n\n if (!cleanCurrent) {\n return {\n status: \"fail\" as const,\n message: `Could not parse current Node.js version: ${current}`,\n };\n }\n\n if (!semver.validRange(requiredRange)) {\n return {\n status: \"fail\" as const,\n message: `Invalid semver range in config: \"${requiredRange}\"`,\n };\n }\n\n if (semver.satisfies(cleanCurrent, requiredRange)) {\n return {\n status: \"pass\" as const,\n message: `v${cleanCurrent} satisfies ${requiredRange}`,\n };\n }\n\n return {\n status: \"fail\" as const,\n message: `v${cleanCurrent} does not satisfy ${requiredRange}`,\n };\n });\n\n return [result];\n };\n}\n","import { execa } from \"execa\";\nimport type { CheckResult } from \"../types.js\";\n\n/**\n * Allowlist of commands considered safe for automatic execution.\n * Commands not on this list require explicit user consent via --trust-commands.\n *\n * Only commands that read system state (version checks, service status)\n * belong here. Never add commands that modify state (rm, mv, curl, wget, etc).\n */\n/**\n * Only EXACT read-only commands belong here.\n * NEVER add bare tool names like \"node\", \"npm\", \"npx\" — they allow\n * arbitrary code execution (e.g. `node -e`, `npx evil-pkg`, `npm exec`).\n */\nconst SAFE_COMMAND_PREFIXES = [\n \"node --version\",\n \"node -v\",\n \"npm --version\",\n \"npm -v\",\n \"docker info\",\n \"docker --version\",\n \"docker compose version\",\n \"git --version\",\n \"python --version\",\n \"python3 --version\",\n \"ruby --version\",\n \"java --version\",\n \"javac --version\",\n \"go version\",\n \"rustc --version\",\n \"cargo --version\",\n];\n\n/**\n * Checks if a command is on the safe allowlist.\n */\nexport function isCommandAllowed(command: string): boolean {\n const normalized = command.trim();\n return SAFE_COMMAND_PREFIXES.some((safeCmd) => normalized === safeCmd);\n}\n\n/**\n * Parses a command string into [executable, ...args] respecting\n * single and double quotes.\n *\n * \"grep 'Server Version' file.log\" → [\"grep\", \"Server Version\", \"file.log\"]\n * \"echo \\\"hello world\\\"\" → [\"echo\", \"hello world\"]\n *\n * This avoids the naive split(/\\s+/) which breaks quoted arguments.\n */\nexport function parseCommand(command: string): string[] {\n const tokens: string[] = [];\n let current = \"\";\n let inSingle = false;\n let inDouble = false;\n let escaped = false;\n\n for (const char of command) {\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n\n if (char === \"\\\\\" && !inSingle) {\n escaped = true;\n continue;\n }\n\n if (char === \"'\" && !inDouble) {\n inSingle = !inSingle;\n continue;\n }\n\n if (char === '\"' && !inSingle) {\n inDouble = !inDouble;\n continue;\n }\n\n if (/\\s/.test(char) && !inSingle && !inDouble) {\n if (current.length > 0) {\n tokens.push(current);\n current = \"\";\n }\n continue;\n }\n\n current += char;\n }\n\n if (current.length > 0) {\n tokens.push(current);\n }\n\n return tokens;\n}\n\n/**\n * Executes a shell command and returns its stdout.\n * Throws if the command fails or is not found.\n *\n * Uses a proper token parser instead of naive split, so quoted\n * arguments like \"grep 'Server Version' file\" work correctly.\n *\n * Shell operators (|, >, &&, ;) are NOT interpreted because execa\n * runs without shell: true. This is a security feature.\n */\nexport async function execCommand(command: string): Promise<string> {\n const tokens = parseCommand(command);\n\n if (tokens.length === 0) {\n throw new Error(\"Empty command\");\n }\n\n const [cmd, ...args] = tokens;\n\n const result = await execa(cmd, args, {\n timeout: 10_000,\n reject: false,\n });\n\n if (result.failed) {\n throw new Error(result.stderr || `Command \"${command}\" failed`);\n }\n\n return result.stdout;\n}\n\n/**\n * Measures the execution time of an async check function.\n * Wraps exceptions into a fail CheckResult so the runner never crashes.\n */\nexport async function timedCheck(\n name: string,\n fn: () => Promise<Omit<CheckResult, \"name\" | \"duration\">>,\n): Promise<CheckResult> {\n const start = performance.now();\n try {\n const partial = await fn();\n return {\n name,\n ...partial,\n duration: Math.round(performance.now() - start),\n };\n } catch (error) {\n return {\n name,\n status: \"fail\",\n message: error instanceof Error ? error.message : String(error),\n duration: Math.round(performance.now() - start),\n };\n }\n}\n","import semver from \"semver\";\nimport { execCommand, timedCheck } from \"../utils/system.js\";\nimport type { CheckResult } from \"../types.js\";\n\n/**\n * Validates that the installed npm version satisfies\n * the semver range from config. Requires shelling out\n * since npm version is not available on process.\n */\nexport function checkNpmVersion(requiredRange: string): () => Promise<CheckResult[]> {\n return async () => {\n const result = await timedCheck(\"npm Version\", async () => {\n const stdout = await execCommand(\"npm --version\");\n const current = semver.clean(stdout.trim());\n\n if (!current) {\n return {\n status: \"fail\" as const,\n message: `Could not parse npm version from output: \"${stdout.trim()}\"`,\n };\n }\n\n if (!semver.validRange(requiredRange)) {\n return {\n status: \"fail\" as const,\n message: `Invalid semver range in config: \"${requiredRange}\"`,\n };\n }\n\n if (semver.satisfies(current, requiredRange)) {\n return {\n status: \"pass\" as const,\n message: `v${current} satisfies ${requiredRange}`,\n };\n }\n\n return {\n status: \"fail\" as const,\n message: `v${current} does not satisfy ${requiredRange}`,\n };\n });\n\n return [result];\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve, relative, isAbsolute } from \"node:path\";\nimport { parse as dotenvParse } from \"dotenv\";\nimport { timedCheck } from \"../utils/system.js\";\nimport type { CheckResult, EnvConfig } from \"../types.js\";\n\n/**\n * Validates environment variables following the \"Closed Eyes\" principle:\n * - Checks if required keys EXIST in the target .env file\n * - Checks if values are NON-EMPTY\n * - NEVER logs or exposes the actual secret values\n *\n * Security: Rejects env.target paths that resolve outside CWD.\n * Uses dotenv.parse() instead of dotenv.config() to avoid\n * injecting values into process.env as a side-effect.\n */\nexport function checkEnvVars(envConfig: EnvConfig): () => Promise<CheckResult[]> {\n return async () => {\n const results: CheckResult[] = [];\n const cwd = process.cwd();\n\n // Path traversal guard\n if (isAbsolute(envConfig.target)) {\n results.push({\n name: `Env File (${envConfig.target})`,\n status: \"fail\",\n message: `Absolute paths are not allowed for env.target: \"${envConfig.target}\"`,\n duration: 0,\n });\n return results;\n }\n\n const targetPath = resolve(cwd, envConfig.target);\n const rel = relative(cwd, targetPath);\n if (rel.startsWith(\"..\")) {\n results.push({\n name: `Env File (${envConfig.target})`,\n status: \"fail\",\n message: `Path traversal detected: \"${envConfig.target}\" resolves outside the project directory`,\n duration: 0,\n });\n return results;\n }\n\n let envKeys: Record<string, string> = {};\n\n const fileCheck = await timedCheck(`Env File (${envConfig.target})`, async () => {\n let content: string;\n try {\n content = await readFile(targetPath, \"utf-8\");\n } catch {\n // CI/CD Support: It's okay if .env is missing, as long as variables are in process.env\n return {\n status: \"pass\",\n message: `File not found: ${envConfig.target} (Using process.env)`,\n };\n }\n\n envKeys = dotenvParse(content);\n\n return {\n status: \"pass\" as const,\n message: `${envConfig.target} loaded successfully`,\n };\n });\n\n results.push(fileCheck);\n\n // Proceed to check keys regardless of file existence\n\n\n for (const key of envConfig.required) {\n const keyResult = await timedCheck(`Env: ${key}`, async () => {\n const value = envKeys[key] ?? process.env[key];\n\n if (value === undefined) {\n return {\n status: \"fail\" as const,\n message: `Missing required variable \"${key}\" in ${envConfig.target} and process.env`,\n };\n }\n\n if (value.trim() === \"\") {\n return {\n status: \"fail\" as const,\n message: `Variable \"${key}\" exists but is empty`,\n };\n }\n\n return {\n status: \"pass\" as const,\n message: `\"${key}\" is set (value hidden)`,\n };\n });\n\n results.push(keyResult);\n }\n\n return results;\n };\n}\n","import { createConnection } from \"node:net\";\nimport { timedCheck } from \"../utils/system.js\";\nimport type { CheckResult, PortConfig } from \"../types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 3000;\n\n/**\n * Checks if a TCP port is accepting connections on localhost.\n * Uses a raw TCP socket with a timeout — no data is sent.\n */\nfunction probePort(port: number, timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ port, host: \"127.0.0.1\" });\n\n const timer = setTimeout(() => {\n socket.destroy();\n resolve(false);\n }, timeoutMs);\n\n socket.on(\"connect\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(true);\n });\n\n socket.on(\"error\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\nexport function checkPorts(portsConfig: PortConfig[]): () => Promise<CheckResult[]> {\n return async () => {\n const results: CheckResult[] = [];\n\n for (const portDef of portsConfig) {\n const result = await timedCheck(\n `Port ${portDef.port} (${portDef.name})`,\n async () => {\n const isOpen = await probePort(portDef.port);\n\n if (isOpen) {\n return {\n status: \"pass\" as const,\n message: `${portDef.name} is accepting connections on port ${portDef.port}`,\n };\n }\n\n return {\n status: \"fail\" as const,\n message: `Nothing listening on port ${portDef.port}. Is ${portDef.name} running?`,\n };\n },\n );\n\n results.push(result);\n }\n\n return results;\n };\n}\n","import * as fs from \"node:fs/promises\";\nimport { resolve, relative, isAbsolute } from \"node:path\";\nimport { timedCheck } from \"../utils/system.js\";\nimport type { CheckResult } from \"../types.js\";\n\n/**\n * Checks that all required files exist in the project directory.\n *\n * Security: Rejects paths that resolve outside the CWD to prevent\n * a malicious config from probing the filesystem (e.g. \"../../etc/passwd\").\n */\nexport function checkFiles(filesList: string[]): () => Promise<CheckResult[]> {\n return async () => {\n const results: CheckResult[] = [];\n const cwd = process.cwd();\n\n for (const file of filesList) {\n const result = await timedCheck(`File: ${file}`, async () => {\n // Reject absolute paths outright\n if (isAbsolute(file)) {\n return {\n status: \"fail\" as const,\n message: `Absolute paths are not allowed in file checks: \"${file}\"`,\n };\n }\n\n const filePath = resolve(cwd, file);\n const realPath = await fs.realpath(filePath);\n const rel = relative(cwd, realPath);\n if (rel.startsWith(\"..\")) {\n return {\n status: \"fail\" as const,\n message: `Path traversal detected: \"${file}\" resolves outside the project directory`,\n };\n }\n\n try {\n await fs.access(realPath);\n } catch {\n return {\n status: \"fail\" as const,\n message: `Required file not found: ${file}`,\n };\n }\n\n return {\n status: \"pass\" as const,\n message: `${file} exists`,\n };\n });\n\n results.push(result);\n }\n\n return results;\n };\n}\n","import { execCommand, isCommandAllowed, timedCheck } from \"../utils/system.js\";\nimport type { CheckResult, CommandConfig } from \"../types.js\";\n\n/**\n * Executes custom shell commands and optionally matches output\n * against an expected string.\n *\n * Security: Commands not on the safe allowlist are SKIPPED unless\n * the user explicitly passes --trust-commands. This prevents a\n * malicious config file from executing arbitrary code on a\n * developer's machine during onboarding (clone + npx stackaudit check).\n */\nexport function checkCommands(\n commandsConfig: CommandConfig[],\n trustCommands: boolean = false,\n): () => Promise<CheckResult[]> {\n return async () => {\n const results: CheckResult[] = [];\n\n for (const cmdDef of commandsConfig) {\n const label = cmdDef.cmd.length > 40\n ? cmdDef.cmd.slice(0, 37) + \"...\"\n : cmdDef.cmd;\n\n // Gate: refuse to execute untrusted commands without explicit consent\n if (!trustCommands && !isCommandAllowed(cmdDef.cmd)) {\n results.push({\n name: `Command: ${label}`,\n status: \"skip\",\n message:\n `Skipped untrusted command: \"${cmdDef.cmd}\". ` +\n `Use --trust-commands to allow execution of custom commands.`,\n duration: 0,\n });\n continue;\n }\n\n const result = await timedCheck(`Command: ${label}`, async () => {\n let stdout: string;\n try {\n stdout = await execCommand(cmdDef.cmd);\n } catch (error) {\n const msg =\n cmdDef.errorMsg ??\n `Command failed: \"${cmdDef.cmd}\" — ${error instanceof Error ? error.message : String(error)}`;\n return { status: \"fail\" as const, message: msg };\n }\n\n if (cmdDef.match && !stdout.includes(cmdDef.match)) {\n const msg =\n cmdDef.errorMsg ??\n `Output of \"${cmdDef.cmd}\" does not contain \"${cmdDef.match}\"`;\n return { status: \"fail\" as const, message: msg };\n }\n\n return {\n status: \"pass\" as const,\n message: cmdDef.match\n ? `\"${cmdDef.cmd}\" output contains \"${cmdDef.match}\"`\n : `\"${cmdDef.cmd}\" executed successfully`,\n };\n });\n\n results.push(result);\n }\n\n return results;\n };\n}\n","import type { CheckerFn, ChecksConfig } from \"../types.js\";\nimport { checkNodeVersion } from \"./nodeVersion.js\";\nimport { checkNpmVersion } from \"./npmVersion.js\";\nimport { checkEnvVars } from \"./envVars.js\";\nimport { checkPorts } from \"./ports.js\";\nimport { checkFiles } from \"./files.js\";\nimport { checkCommands } from \"./commands.js\";\n\nexport interface PipelineOptions {\n trustCommands: boolean;\n}\n\n/**\n * Builds the checker pipeline based on the config.\n * Only registers checkers for keys that are present in the config,\n * so users only run what they declare.\n */\nexport function buildCheckerPipeline(\n checks: ChecksConfig,\n options: PipelineOptions = { trustCommands: false },\n): CheckerFn[] {\n const pipeline: CheckerFn[] = [];\n\n if (checks.node) {\n pipeline.push(checkNodeVersion(checks.node));\n }\n\n if (checks.npm) {\n pipeline.push(checkNpmVersion(checks.npm));\n }\n\n if (checks.files && checks.files.length > 0) {\n pipeline.push(checkFiles(checks.files));\n }\n\n if (checks.env) {\n pipeline.push(checkEnvVars(checks.env));\n }\n\n if (checks.ports && checks.ports.length > 0) {\n pipeline.push(checkPorts(checks.ports));\n }\n\n if (checks.commands && checks.commands.length > 0) {\n pipeline.push(checkCommands(checks.commands, options.trustCommands));\n }\n\n return pipeline;\n}\n","import type {\n AuditReport,\n CheckerFn,\n CheckResult,\n StackAuditConfig,\n} from \"../types.js\";\nimport { buildCheckerPipeline, type PipelineOptions } from \"../checks/index.js\";\n\n/**\n * Executes all configured checkers in parallel and aggregates results.\n *\n * Uses Promise.allSettled to ensure every checker runs to completion\n * regardless of individual failures — the \"Fail Efficiently\" principle.\n */\nexport async function runAudit(\n config: StackAuditConfig,\n options: PipelineOptions = { trustCommands: false },\n): Promise<AuditReport> {\n const checkers: CheckerFn[] = buildCheckerPipeline(config.checks, options);\n\n const settled = await Promise.allSettled(\n checkers.map((checker) => checker()),\n );\n\n const results: CheckResult[] = [];\n\n for (const outcome of settled) {\n if (outcome.status === \"fulfilled\") {\n results.push(...outcome.value);\n } else {\n results.push({\n name: \"Unknown Check\",\n status: \"fail\",\n message:\n outcome.reason instanceof Error\n ? outcome.reason.message\n : String(outcome.reason),\n duration: 0,\n });\n }\n }\n\n // Single-pass summary instead of 4x .filter() iterations\n const summary = { passed: 0, failed: 0, warned: 0, skipped: 0, total: results.length };\n for (const r of results) {\n switch (r.status) {\n case \"pass\": summary.passed++; break;\n case \"fail\": summary.failed++; break;\n case \"warn\": summary.warned++; break;\n case \"skip\": summary.skipped++; break;\n }\n }\n\n return {\n projectName: config.projectName,\n timestamp: new Date(),\n results,\n summary,\n };\n}\n","import chalk from \"chalk\";\nimport boxen from \"boxen\";\nimport type { AuditReport, CheckResult, CLIOptions } from \"../types.js\";\n\nconst STATUS_ICONS: Record<string, string> = {\n pass: chalk.green(\"✔\"),\n fail: chalk.red(\"✖\"),\n warn: chalk.yellow(\"⚠\"),\n skip: chalk.gray(\"○\"),\n};\n\nexport function formatCheckResult(result: CheckResult, verbose: boolean): string {\n const icon = STATUS_ICONS[result.status];\n const duration = chalk.gray(`(${result.duration}ms)`);\n const name = result.status === \"fail\" ? chalk.red(result.name) : result.name;\n\n // In non-verbose mode, hide passing checks' messages for a cleaner output\n if (!verbose && result.status === \"pass\") {\n return ` ${icon} ${name} ${duration}`;\n }\n\n return ` ${icon} ${name} ${duration}\\n ${chalk.gray(result.message)}`;\n}\n\nexport function formatReport(report: AuditReport, options: Pick<CLIOptions, \"verbose\" | \"json\">): string {\n if (options.json) {\n return JSON.stringify({\n projectName: report.projectName,\n timestamp: report.timestamp.toISOString(),\n results: report.results,\n summary: report.summary,\n }, null, 2);\n }\n\n const lines: string[] = [];\n\n lines.push(\"\");\n for (const result of report.results) {\n lines.push(formatCheckResult(result, options.verbose));\n }\n lines.push(\"\");\n\n const { passed, failed, warned, skipped, total } = report.summary;\n\n const summaryParts = [\n chalk.green(`${passed} passed`),\n failed > 0 ? chalk.red(`${failed} failed`) : null,\n warned > 0 ? chalk.yellow(`${warned} warnings`) : null,\n skipped > 0 ? chalk.gray(`${skipped} skipped`) : null,\n ]\n .filter(Boolean)\n .join(chalk.gray(\" · \"));\n\n const summaryLine = `${summaryParts} ${chalk.gray(`(${total} checks)`)}`;\n\n const elapsed = report.results.reduce((sum, r) => sum + r.duration, 0);\n\n const header =\n failed > 0\n ? chalk.red.bold(\"stackAudit — FAIL\")\n : chalk.green.bold(\"stackAudit — PASS\");\n\n const boxContent = [\n header,\n chalk.gray(`Project: ${report.projectName}`),\n \"\",\n summaryLine,\n chalk.gray(`Done in ${elapsed}ms`),\n ].join(\"\\n\");\n\n lines.push(\n boxen(boxContent, {\n padding: 1,\n margin: { top: 0, bottom: 0, left: 1, right: 1 },\n borderColor: failed > 0 ? \"red\" : \"green\",\n borderStyle: \"round\",\n }),\n );\n\n return lines.join(\"\\n\");\n}\n\nexport function logError(message: string): void {\n console.error(chalk.red.bold(\"Error:\"), message);\n}\n\nexport function logInfo(message: string): void {\n console.log(chalk.blue(\"ℹ\"), message);\n}\n\nexport function logSuccess(message: string): void {\n console.log(chalk.green(\"✔\"), message);\n}\n\nexport function logWarn(message: string): void {\n console.log(chalk.yellow(\"⚠\"), message);\n}\n","import { access, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, resolve } from \"node:path\";\nimport { logError, logInfo, logSuccess, logWarn } from \"../utils/logger.js\";\nimport { execCommand } from \"../utils/system.js\";\nimport type { ChecksConfig } from \"../types.js\";\n\nconst CONFIG_FILENAME = \"stackAudit.config.json\";\n\ninterface InitOptions {\n detect: boolean;\n}\n\n/**\n * Probes for a command and returns its version, or null if not found.\n */\nasync function probeToolVersion(cmd: string): Promise<string | null> {\n try {\n const output = await execCommand(cmd);\n return output.trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Detects common project files in the current directory.\n */\nasync function detectFiles(candidates: string[]): Promise<string[]> {\n const found: string[] = [];\n for (const file of candidates) {\n try {\n await access(resolve(process.cwd(), file));\n found.push(file);\n } catch {\n // not present\n }\n }\n return found;\n}\n\n/**\n * Tries to read the project name from package.json.\n */\nasync function detectProjectName(): Promise<string> {\n try {\n const raw = await readFile(resolve(process.cwd(), \"package.json\"), \"utf-8\");\n const pkg = JSON.parse(raw);\n if (typeof pkg.name === \"string\" && pkg.name.length > 0) {\n return pkg.name;\n }\n } catch {\n // ignore\n }\n return basename(process.cwd());\n}\n\n/**\n * Builds a smart config by probing the local environment.\n */\nasync function buildDetectedConfig(): Promise<{ projectName: string; version: string; checks: ChecksConfig }> {\n const checks: ChecksConfig = {};\n\n // Detect Node.js\n const nodeVersion = await probeToolVersion(\"node --version\");\n if (nodeVersion) {\n const major = nodeVersion.replace(/^v/, \"\").split(\".\")[0];\n checks.node = `>=${major}.0.0`;\n logSuccess(`Detected Node.js ${nodeVersion} → requiring >=${major}.0.0`);\n }\n\n // Detect npm\n const npmVersion = await probeToolVersion(\"npm --version\");\n if (npmVersion) {\n const major = npmVersion.split(\".\")[0];\n checks.npm = `>=${major}.0.0`;\n logSuccess(`Detected npm ${npmVersion} → requiring >=${major}.0.0`);\n }\n\n // Detect common project files\n const commonFiles = [\n \"package.json\",\n \"docker-compose.yml\",\n \"docker-compose.yaml\",\n \"Dockerfile\",\n \"Makefile\",\n \".env.example\",\n \"tsconfig.json\",\n ];\n\n const foundFiles = await detectFiles(commonFiles);\n if (foundFiles.length > 0) {\n checks.files = foundFiles;\n logSuccess(`Detected ${foundFiles.length} project files: ${foundFiles.join(\", \")}`);\n }\n\n // Detect .env.example and extract required keys\n const envExamplePath = resolve(process.cwd(), \".env.example\");\n try {\n const content = await readFile(envExamplePath, \"utf-8\");\n const keys = content\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0].trim())\n .filter((key) => key.length > 0);\n\n if (keys.length > 0) {\n checks.env = {\n target: \".env\",\n example: \".env.example\",\n required: keys,\n };\n logSuccess(`Detected ${keys.length} env vars from .env.example: ${keys.join(\", \")}`);\n }\n } catch {\n // No .env.example\n }\n\n // Detect Docker\n const dockerVersion = await probeToolVersion(\"docker --version\");\n if (dockerVersion) {\n checks.commands = [\n {\n cmd: \"docker info\",\n match: \"Server Version\",\n errorMsg: \"Docker daemon is not running. Start Docker Desktop or the Docker service.\",\n },\n ];\n logSuccess(`Detected Docker → adding daemon check`);\n }\n\n const projectName = await detectProjectName();\n\n return {\n projectName,\n version: \"1.0.0\",\n checks,\n };\n}\n\n/**\n * Generates a stackAudit.config.json in the current directory.\n * With --detect, probes the local environment to auto-populate config.\n */\nexport async function initCommand(options: InitOptions): Promise<void> {\n const targetPath = resolve(process.cwd(), CONFIG_FILENAME);\n\n try {\n await access(targetPath);\n throw new Error(\n `${CONFIG_FILENAME} already exists in this directory. Delete it first to re-initialize.`,\n );\n } catch {\n // File does not exist — proceed\n }\n\n let config;\n\n if (options.detect) {\n logInfo(\"Scanning environment...\\n\");\n config = await buildDetectedConfig();\n\n if (Object.keys(config.checks).length === 0) {\n logWarn(\"No tools detected. Generating a minimal config.\");\n config.checks = { node: \">=18.0.0\" };\n }\n\n console.log(\"\");\n } else {\n config = {\n projectName: await detectProjectName(),\n version: \"1.0.0\",\n checks: {\n node: \">=18.0.0\",\n npm: \">=9.0.0\",\n files: [\"package.json\"],\n },\n };\n }\n\n try {\n const content = JSON.stringify(config, null, 2) + \"\\n\";\n await writeFile(targetPath, content, \"utf-8\");\n logSuccess(`Created ${CONFIG_FILENAME} — customize it for your project.`);\n } catch (error) {\n throw new Error(\n `Failed to write ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n"],"mappings":"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","names":["semver","semver","readFile","resolve","resolve","readFile","resolve","resolve","relative","isAbsolute","isAbsolute","resolve","relative","access","readFile","resolve","access","resolve","readFile"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "stackaudit",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to audit developer environments against a declarative configuration file",
5
+ "type": "module",
6
+ "bin": {
7
+ "stackaudit": "./bin/stackAudit.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "bin"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "start": "node bin/stackAudit.js",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "lint": "tsc --noEmit",
22
+ "build:sea": "npm run build && node scripts/build-sea.mjs",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "cli",
27
+ "developer-experience",
28
+ "devex",
29
+ "audit",
30
+ "environment",
31
+ "validation",
32
+ "onboarding"
33
+ ],
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {
39
+ "boxen": "^8.0.1",
40
+ "chalk": "^5.4.1",
41
+ "commander": "^13.1.0",
42
+ "dotenv": "^16.4.7",
43
+ "execa": "^9.5.2",
44
+ "ora": "^8.2.0",
45
+ "semver": "^7.6.3",
46
+ "zod": "^3.24.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.12.0",
50
+ "@types/semver": "^7.5.8",
51
+ "tsup": "^8.3.6",
52
+ "typescript": "^5.7.3",
53
+ "vitest": "^3.0.5"
54
+ }
55
+ }