proj-pulse 1.0.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/Readme.md ADDED
@@ -0,0 +1,95 @@
1
+ # ๐Ÿฉบ proj-pulse
2
+
3
+ > A health checkup for your project โ€” one command to catch what's quietly rotting.
4
+
5
+ ```bash
6
+ npx proj-pulse
7
+ ```
8
+
9
+ ---
10
+
11
+ ## What it checks
12
+
13
+ | Check | What it finds |
14
+ |---|---|
15
+ | ๐Ÿ“ฆ **Unused Dependencies** | npm packages installed but never imported |
16
+ | ๐Ÿ” **Env Var Health** | Missing keys, weak secrets, .env not in .gitignore |
17
+ | ๐Ÿ“ **Stale TODOs** | TODO/FIXME comments older than 30 days |
18
+ | ๐Ÿ“ **Large Files** | Source files over 500 KB that shouldn't be in git |
19
+ | ๐Ÿ›ก๏ธ **.gitignore Safety** | Missing critical entries (node_modules, .env, etc.) |
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ # Run instantly with npx (no install needed)
27
+ npx proj-pulse
28
+
29
+ # Or install globally
30
+ npm install -g proj-pulse
31
+ proj-pulse
32
+
33
+ # Or scan a specific folder
34
+ proj-pulse /path/to/my/project
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Example Output
40
+
41
+ ```
42
+ ๐Ÿฉบ proj-pulse Project Health Check
43
+
44
+ Scanning: /Users/dev/my-app
45
+
46
+ โœ” Unused Dependencies
47
+ All 12 dependencies appear to be used.
48
+
49
+ โš  Env Var Health
50
+ 2 env issues detected:
51
+ ยท .env is NOT listed in .gitignore โ€” risk of leaking secrets
52
+ ยท Line 4: DB_PASSWORD โ€” weak/short secret
53
+ โ†’ Fix: Review your .env file and update insecure or missing values.
54
+
55
+ โœ– Stale TODOs
56
+ 5 TODO/FIXME found, 3 older than 30 days:
57
+ ยท [TODO] src/api/auth.js:42 โ€” "refactor token handling" (87d old)
58
+ ยท [FIXME] src/utils/parser.js:11 โ€” "edge case for empty array" (63d old)
59
+ ยท [HACK] src/db/index.js:7 โ€” "replace with proper pool" (45d old)
60
+ โ†’ Fix: Address or remove comments older than 30 days.
61
+
62
+ โœ” Large Files
63
+ No unexpectedly large source files found (threshold: 500 KB).
64
+
65
+ โš  .gitignore Safety
66
+ 1 .gitignore issue found:
67
+ ยท Missing: ".env" (secrets / API keys)
68
+ โ†’ Fix: Add missing entries to your .gitignore file.
69
+
70
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
71
+ Summary
72
+
73
+ โœ” Passed 2/5
74
+ โš  Warnings 2/5
75
+ โœ– Failed 1/5
76
+
77
+ Health Score: 40%
78
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Roadmap
84
+
85
+ - [ ] `--fix` flag to auto-resolve safe issues
86
+ - [ ] JSON output for CI integration (`--json`)
87
+ - [ ] Config file (`.proj-pulse.json`) for custom thresholds
88
+ - [ ] Git history analysis for churn / hotspot detection
89
+ - [ ] Dead code detection
90
+
91
+ ---
92
+
93
+ ## License
94
+
95
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const path = require("path");
6
+ const { runPulse } = require("../src/index");
7
+
8
+ const projectRoot = process.argv[2]
9
+ ? path.resolve(process.argv[2])
10
+ : process.cwd();
11
+
12
+ runPulse(projectRoot);
13
+
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "proj-pulse",
3
+ "version": "1.0.0",
4
+ "description": "A health checkup for your project โ€” dead deps, stale TODOs, unused env vars, and more.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "proj-pulse": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.js",
11
+ "test": "node bin/cli.js"
12
+ },
13
+ "keywords": [
14
+ "developer-tools",
15
+ "cli",
16
+ "health-check",
17
+ "project",
18
+ "dependencies",
19
+ "audit"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^4.1.2",
25
+ "glob": "^8.1.0",
26
+ "ora": "^5.4.1"
27
+ }
28
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ const INSECURE_PATTERNS = [
6
+ { pattern: /^(password|pass|pwd|secret|key|token|api_key)=.{1,7}$/i, label: "weak/short secret" },
7
+ { pattern: /=password$/i, label: "default password value" },
8
+ { pattern: /=secret$/i, label: "default secret value" },
9
+ { pattern: /=changeme$/i, label: "placeholder value (changeme)" },
10
+ { pattern: /=12345/, label: "weak numeric value" },
11
+ { pattern: /=true$/i, label: "boolean flag โ€” confirm intentional" },
12
+ ];
13
+
14
+ async function checkEnvVars(projectRoot) {
15
+ const envPath = path.join(projectRoot, ".env");
16
+ const examplePath = path.join(projectRoot, ".env.example");
17
+ const templatePath = path.join(projectRoot, ".env.template");
18
+
19
+ const hasEnv = fs.existsSync(envPath);
20
+ const hasExample = fs.existsSync(examplePath) || fs.existsSync(templatePath);
21
+
22
+ if (!hasEnv && !hasExample) {
23
+ return { status: "skip", message: "No .env or .env.example file found." };
24
+ }
25
+
26
+ const issues = [];
27
+
28
+ // Check .env is NOT committed (check .gitignore)
29
+ const gitignorePath = path.join(projectRoot, ".gitignore");
30
+ if (hasEnv && fs.existsSync(gitignorePath)) {
31
+ const gi = fs.readFileSync(gitignorePath, "utf8");
32
+ if (!gi.includes(".env")) {
33
+ issues.push(".env is NOT listed in .gitignore โ€” risk of leaking secrets");
34
+ }
35
+ }
36
+
37
+ // Parse .env and check for insecure patterns
38
+ if (hasEnv) {
39
+ const lines = fs.readFileSync(envPath, "utf8").split("\n");
40
+ lines.forEach((line, i) => {
41
+ const trimmed = line.trim();
42
+ if (!trimmed || trimmed.startsWith("#")) return;
43
+
44
+ INSECURE_PATTERNS.forEach(({ pattern, label }) => {
45
+ if (pattern.test(trimmed)) {
46
+ const key = trimmed.split("=")[0];
47
+ issues.push(`Line ${i + 1}: ${key} โ€” ${label}`);
48
+ }
49
+ });
50
+ });
51
+
52
+ // Cross-check .env vs .env.example for missing keys
53
+ if (hasExample) {
54
+ const exPath = fs.existsSync(examplePath) ? examplePath : templatePath;
55
+ const exampleKeys = fs.readFileSync(exPath, "utf8")
56
+ .split("\n")
57
+ .filter(l => l.includes("=") && !l.startsWith("#"))
58
+ .map(l => l.split("=")[0].trim());
59
+
60
+ const envKeys = fs.readFileSync(envPath, "utf8")
61
+ .split("\n")
62
+ .filter(l => l.includes("=") && !l.startsWith("#"))
63
+ .map(l => l.split("=")[0].trim());
64
+
65
+ const missing = exampleKeys.filter(k => !envKeys.includes(k));
66
+ if (missing.length > 0) {
67
+ missing.forEach(k => issues.push(`Missing key from .env.example: ${k}`));
68
+ }
69
+ }
70
+ }
71
+
72
+ if (issues.length === 0) {
73
+ return { status: "pass", message: ".env looks healthy โ€” no obvious issues found." };
74
+ }
75
+
76
+ return {
77
+ status: "warn",
78
+ message: `${issues.length} env ${issues.length === 1 ? "issue" : "issues"} detected:`,
79
+ items: issues,
80
+ fix: "Review your .env file and update insecure or missing values.",
81
+ };
82
+ }
83
+
84
+ module.exports = checkEnvVars;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const MUST_IGNORE = [
7
+ { entry: ".env", reason: "secrets / API keys" },
8
+ { entry: "node_modules", reason: "dependencies (bloats repo)" },
9
+ { entry: ".DS_Store", reason: "macOS metadata noise" },
10
+ { entry: "*.log", reason: "log files" },
11
+ { entry: "dist", reason: "build output" },
12
+ { entry: "coverage", reason: "test coverage output" },
13
+ ];
14
+
15
+ const SENSITIVE_FILES = [
16
+ ".env", ".env.local", ".env.production",
17
+ "*.pem", "*.key", "*.p12", "*.pfx",
18
+ "secrets.json", "credentials.json",
19
+ ];
20
+
21
+ async function checkGitignore(projectRoot) {
22
+ const gitignorePath = path.join(projectRoot, ".gitignore");
23
+
24
+ if (!fs.existsSync(gitignorePath)) {
25
+ return {
26
+ status: "fail",
27
+ message: "No .gitignore file found! This is risky for any project.",
28
+ fix: "Run: npx gitignore node (or create one manually)",
29
+ };
30
+ }
31
+
32
+ const content = fs.readFileSync(gitignorePath, "utf8");
33
+ const lines = content.split("\n").map(l => l.trim()).filter(Boolean);
34
+
35
+ const isIgnored = (entry) =>
36
+ lines.some(l => l === entry || l === `/${entry}` || l === `${entry}/`);
37
+
38
+ const missing = MUST_IGNORE.filter(({ entry }) => !isIgnored(entry));
39
+
40
+ // Check if any sensitive files actually exist but are NOT gitignored
41
+ const exposedSecrets = SENSITIVE_FILES.filter(pattern => {
42
+ const cleanPattern = pattern.replace(/\*/g, "");
43
+ const exists = fs.existsSync(path.join(projectRoot, cleanPattern.startsWith(".") ? cleanPattern : `.${cleanPattern}`))
44
+ || fs.existsSync(path.join(projectRoot, cleanPattern));
45
+ const ignored = lines.some(l => l.includes(cleanPattern) || l === pattern);
46
+ return exists && !ignored;
47
+ });
48
+
49
+ const issues = [
50
+ ...missing.map(({ entry, reason }) => `Missing: "${entry}" (${reason})`),
51
+ ...exposedSecrets.map(f => `โš  Sensitive file exists and is NOT ignored: ${f}`),
52
+ ];
53
+
54
+ if (issues.length === 0) {
55
+ return {
56
+ status: "pass",
57
+ message: ".gitignore covers all common dangerous entries.",
58
+ };
59
+ }
60
+
61
+ const hasCritical = exposedSecrets.length > 0;
62
+
63
+ return {
64
+ status: hasCritical ? "fail" : "warn",
65
+ message: `${issues.length} .gitignore ${issues.length === 1 ? "issue" : "issues"} found:`,
66
+ items: issues,
67
+ fix: `Add missing entries to your .gitignore file.`,
68
+ };
69
+ }
70
+
71
+ module.exports = checkGitignore;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const glob = require('glob')
6
+
7
+ const SIZE_WARN_MB = 0.5; // 500 KB
8
+ const SIZE_FAIL_MB = 2; // 2 MB
9
+
10
+ const IGNORED_EXTENSIONS = [
11
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico",
12
+ ".mp4", ".mov", ".avi", ".mp3", ".wav",
13
+ ".zip", ".tar", ".gz", ".rar",
14
+ ".pdf", ".woff", ".woff2", ".ttf", ".eot",
15
+ ];
16
+
17
+ function formatSize(bytes) {
18
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
19
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
20
+ return `${bytes} B`;
21
+ }
22
+
23
+ async function checkLargeFiles(projectRoot) {
24
+ const allFiles = glob.sync("**/*", {
25
+ cwd: projectRoot,
26
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**", ".git/**"],
27
+ absolute: true,
28
+ nodir: true,
29
+ });
30
+
31
+ const large = [];
32
+
33
+ for (const filePath of allFiles) {
34
+ const ext = path.extname(filePath).toLowerCase();
35
+ if (IGNORED_EXTENSIONS.includes(ext)) continue;
36
+
37
+ let stat;
38
+ try { stat = fs.statSync(filePath); } catch { continue; }
39
+
40
+ const mb = stat.size / (1024 * 1024);
41
+ if (mb >= SIZE_WARN_MB) {
42
+ const relPath = path.relative(projectRoot, filePath);
43
+ large.push({ relPath, size: stat.size, mb });
44
+ }
45
+ }
46
+
47
+ if (large.length === 0) {
48
+ return { status: "pass", message: `No unexpectedly large source files found (threshold: ${SIZE_WARN_MB * 1000} KB).` };
49
+ }
50
+
51
+ large.sort((a, b) => b.size - a.size);
52
+
53
+ const hasFail = large.some(f => f.mb >= SIZE_FAIL_MB);
54
+ const status = hasFail ? "fail" : "warn";
55
+
56
+ const items = large.map(f => `${f.relPath} (${formatSize(f.size)})`);
57
+
58
+ return {
59
+ status,
60
+ message: `${large.length} large ${large.length === 1 ? "file" : "files"} found in source:`,
61
+ items,
62
+ fix: "Consider moving large files to a CDN or adding them to .gitignore.",
63
+ };
64
+ }
65
+
66
+ module.exports = checkLargeFiles;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const glob = require("glob");
6
+ const { execSync } = require("child_process");
7
+
8
+ const TODO_PATTERN = /\/\/\s*(TODO|FIXME|HACK|XXX|BUG):?\s*(.+)/i;
9
+
10
+ function getFileAge(filePath) {
11
+ try {
12
+ const out = execSync(`git log -1 --format="%ci" -- "${filePath}" 2>/dev/null`, {
13
+ encoding: "utf8",
14
+ stdio: ["pipe", "pipe", "ignore"],
15
+ }).trim();
16
+ if (!out) return null;
17
+ return new Date(out);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function daysSince(date) {
24
+ return Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));
25
+ }
26
+
27
+ async function checkStaleTodos(projectRoot) {
28
+ const sourceFiles = glob.sync("**/*.{js,ts,jsx,tsx,mjs,cjs,py,rb,go,java,cs}", {
29
+ cwd: projectRoot,
30
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
31
+ absolute: true,
32
+ });
33
+
34
+ if (sourceFiles.length === 0) {
35
+ return { status: "skip", message: "No source files found." };
36
+ }
37
+
38
+ const todos = [];
39
+
40
+ for (const filePath of sourceFiles) {
41
+ let content;
42
+ try { content = fs.readFileSync(filePath, "utf8"); } catch { continue; }
43
+
44
+ const lines = content.split("\n");
45
+ lines.forEach((line, i) => {
46
+ const match = line.match(TODO_PATTERN);
47
+ if (!match) return;
48
+
49
+ const relPath = path.relative(projectRoot, filePath);
50
+ const age = getFileAge(filePath);
51
+ const days = age ? daysSince(age) : null;
52
+ const label = match[1].toUpperCase();
53
+ const text = match[2].trim().slice(0, 60);
54
+ const ageStr = days !== null ? `${days}d old` : "age unknown";
55
+
56
+ todos.push({ relPath, line: i + 1, label, text, days, ageStr });
57
+ });
58
+ }
59
+
60
+ if (todos.length === 0) {
61
+ return { status: "pass", message: "No TODO/FIXME comments found. Clean codebase!" };
62
+ }
63
+
64
+ // Sort by oldest first
65
+ todos.sort((a, b) => (b.days || 0) - (a.days || 0));
66
+
67
+ const stale = todos.filter(t => t.days !== null && t.days > 30);
68
+ const status = stale.length > 5 ? "fail" : stale.length > 0 ? "warn" : "pass";
69
+
70
+ const items = todos.map(t =>
71
+ `[${t.label}] ${t.relPath}:${t.line} โ€” "${t.text}" (${t.ageStr})`
72
+ );
73
+
74
+ return {
75
+ status,
76
+ message: `${todos.length} TODO/FIXME found, ${stale.length} older than 30 days:`,
77
+ items,
78
+ fix: stale.length > 0 ? "Address or remove comments older than 30 days." : undefined,
79
+ };
80
+ }
81
+
82
+ module.exports = checkStaleTodos;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const glob = require("glob");
5
+
6
+ async function checkUnusedDependencies(projectRoot) {
7
+ const pkgPath = path.join(projectRoot, "package.json");
8
+
9
+ if (!fs.existsSync(pkgPath)) {
10
+ return { status: "skip", message: "No package.json found." };
11
+ }
12
+
13
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
14
+ const deps = Object.keys(pkg.dependencies || {});
15
+
16
+ if (deps.length === 0) {
17
+ return { status: "pass", message: "No dependencies declared." };
18
+ }
19
+
20
+ // Gather all source files
21
+ const sourceFiles = glob.sync("**/*.{js,ts,jsx,tsx,mjs,cjs}", {
22
+ cwd: projectRoot,
23
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
24
+ absolute: true,
25
+ });
26
+
27
+ if (sourceFiles.length === 0) {
28
+ return { status: "skip", message: "No source files found to scan." };
29
+ }
30
+
31
+ // Read all source content once
32
+ const allSource = sourceFiles.map(f => {
33
+ try { return fs.readFileSync(f, "utf8"); } catch { return ""; }
34
+ }).join("\n");
35
+
36
+ const unused = deps.filter(dep => {
37
+ // Match require("dep"), require('dep'), from "dep", from 'dep'
38
+ const escaped = dep.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
39
+ const pattern = new RegExp(
40
+ `require\\(['"](${escaped})['"](\\/.+)?['"]\\)|from\\s+['"](${escaped})(\\/.+)?['"]`,
41
+ "m"
42
+ );
43
+ return !pattern.test(allSource);
44
+ });
45
+
46
+ if (unused.length === 0) {
47
+ return {
48
+ status: "pass",
49
+ message: `All ${deps.length} dependencies appear to be used.`,
50
+ };
51
+ }
52
+
53
+ return {
54
+ status: "warn",
55
+ message: `${unused.length} potentially unused ${unused.length === 1 ? "dependency" : "dependencies"} found:`,
56
+ items: unused.map(d => `${d}`),
57
+ fix: `npm uninstall ${unused.slice(0, 3).join(" ")}${unused.length > 3 ? " ..." : ""}`,
58
+ };
59
+ }
60
+
61
+ module.exports = checkUnusedDependencies;
package/src/index.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+
3
+ const chalk = require("chalk");
4
+ const ora = require("ora");
5
+ const path = require("path");
6
+
7
+ const checkUnusedDependencies = require("./checks/unusedDeps");
8
+ const checkEnvVars = require("./checks/envVars");
9
+ const checkStaleTodos = require("./checks/staleTodos");
10
+ const checkLargeFiles = require("./checks/largeFiles");
11
+ const checkGitignore = require("./checks/gitignore");
12
+
13
+ const CHECKS = [
14
+ { name: "Unused Dependencies", fn: checkUnusedDependencies },
15
+ { name: "Env Var Health", fn: checkEnvVars },
16
+ { name: "Stale TODOs", fn: checkStaleTodos },
17
+ { name: "Large Files", fn: checkLargeFiles },
18
+ { name: ".gitignore Safety", fn: checkGitignore },
19
+ ];
20
+
21
+ function printHeader() {
22
+ console.log("\n" + chalk.bgGreen.black.bold(" ๐Ÿฉบ proj-pulse ") + chalk.gray(" Project Health Check\n"));
23
+ }
24
+
25
+ function printSummary(results) {
26
+ const total = results.length;
27
+ const passed = results.filter(r => r.status === "pass").length;
28
+ const warned = results.filter(r => r.status === "warn").length;
29
+ const failed = results.filter(r => r.status === "fail").length;
30
+
31
+ console.log("\n" + chalk.bold("โ”€".repeat(50)));
32
+ console.log(chalk.bold(" Summary\n"));
33
+ console.log(` ${chalk.green("โœ”")} Passed ${chalk.green.bold(passed)}/${total}`);
34
+ if (warned) console.log(` ${chalk.yellow("โš ")} Warnings ${chalk.yellow.bold(warned)}/${total}`);
35
+ if (failed) console.log(` ${chalk.red("โœ–")} Failed ${chalk.red.bold(failed)}/${total}`);
36
+
37
+ const score = Math.round((passed / total) * 100);
38
+ const scoreColor = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
39
+ console.log("\n " + chalk.bold("Health Score: ") + scoreColor.bold(`${score}%`));
40
+ console.log(chalk.bold("โ”€".repeat(50)) + "\n");
41
+ }
42
+
43
+ function printCheckResult(result) {
44
+ const icons = { pass: chalk.green("โœ”"), warn: chalk.yellow("โš "), fail: chalk.red("โœ–"), skip: chalk.gray("โ€“") };
45
+ const colors = { pass: chalk.green, warn: chalk.yellow, fail: chalk.red, skip: chalk.gray };
46
+
47
+ const icon = icons[result.status] || chalk.gray("โ€“");
48
+ const color = colors[result.status] || chalk.gray;
49
+
50
+ console.log(`\n ${icon} ${chalk.bold(result.name)}`);
51
+
52
+ if (result.message) {
53
+ console.log(` ${color(result.message)}`);
54
+ }
55
+
56
+ if (result.items && result.items.length > 0) {
57
+ result.items.slice(0, 8).forEach(item => {
58
+ console.log(` ${chalk.gray("ยท")} ${item}`);
59
+ });
60
+ if (result.items.length > 8) {
61
+ console.log(` ${chalk.gray(`... and ${result.items.length - 8} more`)}`);
62
+ }
63
+ }
64
+
65
+ if (result.fix) {
66
+ console.log(` ${chalk.cyan("โ†’ Fix:")} ${chalk.cyan(result.fix)}`);
67
+ }
68
+ }
69
+
70
+ async function runPulse(projectRoot) {
71
+ printHeader();
72
+ console.log(chalk.gray(` Scanning: ${projectRoot}\n`));
73
+
74
+ const results = [];
75
+
76
+ for (const check of CHECKS) {
77
+ const spinner = ora({ text: chalk.gray(`Checking ${check.name}...`), spinner: "dots" }).start();
78
+ try {
79
+ const result = await check.fn(projectRoot);
80
+ result.name = check.name;
81
+ results.push(result);
82
+ spinner.stop();
83
+ printCheckResult(result);
84
+ } catch (err) {
85
+ spinner.stop();
86
+ const errResult = {
87
+ name: check.name,
88
+ status: "skip",
89
+ message: `Skipped โ€” ${err.message}`,
90
+ };
91
+ results.push(errResult);
92
+ printCheckResult(errResult);
93
+ }
94
+ }
95
+
96
+ printSummary(results);
97
+ }
98
+
99
+ module.exports = { runPulse };