bridgepreflight 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 +123 -0
- package/dist/analyzers/build.js +126 -0
- package/dist/analyzers/env.js +88 -0
- package/dist/analyzers/network.js +65 -0
- package/dist/analyzers/runtime.js +78 -0
- package/dist/cli.js +231 -0
- package/dist/core/scan.js +23 -0
- package/dist/output/console.js +1 -0
- package/dist/types/index.js +8 -0
- package/dist/utils/ci.js +18 -0
- package/dist/utils/projectUtils.js +38 -0
- package/dist/utils/severity.js +15 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# BridgePreflight
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
<!------------------------------ ------------------------------------>
|
|
7
|
+
BridgePreflight
|
|
8
|
+
|
|
9
|
+
AI-native infrastructure readiness scanner for Node & TypeScript projects.
|
|
10
|
+
|
|
11
|
+
BridgePreflight analyzes your repository before code merges and assigns a Preflight Readiness Score (0–100) — automatically blocking pull requests that introduce risk.
|
|
12
|
+
<!------------------------------ ------------------------------------>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<!------------------------------ ------------------------------------>
|
|
16
|
+
The Problem
|
|
17
|
+
Teams merge code that:
|
|
18
|
+
• Has no tests
|
|
19
|
+
• Breaks the build
|
|
20
|
+
• Lacks documentation
|
|
21
|
+
• Has weak repository hygiene
|
|
22
|
+
Issues are discovered too late — during staging or production.
|
|
23
|
+
|
|
24
|
+
BridgePreflight moves detection earlier — directly into CI.
|
|
25
|
+
<!------------------------------ ------------------------------------>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
<!------------------------------ ------------------------------------>
|
|
30
|
+
What BridgePreflight Does
|
|
31
|
+
BridgePreflight scans your repository and:
|
|
32
|
+
• Generates an Infrastructure Readiness Score
|
|
33
|
+
• Classifies readiness (Ready / Caution / Critical)
|
|
34
|
+
• Identifies top risk factors
|
|
35
|
+
• Automatically fails PRs if score < 70
|
|
36
|
+
• Posts a structured risk summary comment on pull requests
|
|
37
|
+
It integrates directly into GitHub Actions for seamless enforcement.
|
|
38
|
+
<!------------------------------ ------------------------------------>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<!------------------------------ ------------------------------------>
|
|
43
|
+
How It Works
|
|
44
|
+
BridgePreflight evaluates:
|
|
45
|
+
• Test presence and structure
|
|
46
|
+
• Build configuration
|
|
47
|
+
• CI workflow setup
|
|
48
|
+
• Documentation presence
|
|
49
|
+
• Repository hygiene
|
|
50
|
+
Each category contributes weighted points to the final score.
|
|
51
|
+
|
|
52
|
+
If the score falls below the threshold:
|
|
53
|
+
• The CI pipeline fails
|
|
54
|
+
• The merge button is blocked
|
|
55
|
+
• A risk summary is posted on the PR
|
|
56
|
+
<!------------------------------ ------------------------------------>
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
<!------------------------------ ------------------------------------>
|
|
60
|
+
Installation (Local Usage)
|
|
61
|
+
npm install
|
|
62
|
+
npm run build
|
|
63
|
+
node dist/cli.js scan
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
For machine-readable output:
|
|
67
|
+
node dist/cli.js scan --json
|
|
68
|
+
<!------------------------------ ------------------------------------>
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
<!------------------------------ ------------------------------------>
|
|
72
|
+
GitHub Actions Integration
|
|
73
|
+
Place this file in:
|
|
74
|
+
.github/workflows/bridgepreflight.yml
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
BridgePreflight will:
|
|
78
|
+
• Run on every push to main
|
|
79
|
+
• Run on every pull request
|
|
80
|
+
• Automatically fail if readiness < 70
|
|
81
|
+
• Comment with risk breakdown
|
|
82
|
+
<!------------------------------ ------------------------------------>
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
<!------------------------------ ------------------------------------>
|
|
86
|
+
Example Output
|
|
87
|
+
{
|
|
88
|
+
"totalScore": 82,
|
|
89
|
+
"readiness": "Ready",
|
|
90
|
+
"results": [
|
|
91
|
+
{ "name": "Test Check", "severity": "healthy" },
|
|
92
|
+
{ "name": "Build Check", "severity": "warning" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
<!------------------------------ ------------------------------------>
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
<!------------------------------ ------------------------------------>
|
|
99
|
+
Why BridgePreflight Matters
|
|
100
|
+
BridgePreflight transforms infrastructure quality into a measurable metric.
|
|
101
|
+
|
|
102
|
+
It acts as:
|
|
103
|
+
• A DevOps gatekeeper
|
|
104
|
+
• A pre-merge risk detection layer
|
|
105
|
+
• A credibility signal for repositories
|
|
106
|
+
Instead of hoping code is safe — you measure it.
|
|
107
|
+
<!------------------------------ ------------------------------------>
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
<!------------------------------ ------------------------------------>
|
|
111
|
+
Roadmap
|
|
112
|
+
• Configurable scoring weights
|
|
113
|
+
• Plugin-based analyzer system
|
|
114
|
+
• Historical score tracking
|
|
115
|
+
• SaaS dashboard
|
|
116
|
+
• AI-based remediation suggestions
|
|
117
|
+
<!------------------------------ ------------------------------------>
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
<!------------------------------ ------------------------------------>
|
|
121
|
+
📄 License
|
|
122
|
+
MIT
|
|
123
|
+
<!------------------------------ ------------------------------------>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BuildAnalyzer = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const execa_1 = require("execa");
|
|
10
|
+
const severity_1 = require("../utils/severity");
|
|
11
|
+
class BuildAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.name = "Build Check";
|
|
14
|
+
this.weight = 1.5;
|
|
15
|
+
this.supports = ["node"];
|
|
16
|
+
this.MAX_SCORE = 50;
|
|
17
|
+
this.BUILD_TIMEOUT_MS = 60000; // 60s safety timeout
|
|
18
|
+
}
|
|
19
|
+
async run() {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const packageJsonPath = path_1.default.join(projectRoot, "package.json");
|
|
22
|
+
const tsconfigPath = path_1.default.join(projectRoot, "tsconfig.json");
|
|
23
|
+
let score = this.MAX_SCORE;
|
|
24
|
+
const findings = [];
|
|
25
|
+
// 1. Ensure package.json exists
|
|
26
|
+
if (!fs_1.default.existsSync(packageJsonPath)) {
|
|
27
|
+
return {
|
|
28
|
+
name: this.name,
|
|
29
|
+
success: false,
|
|
30
|
+
findings: [
|
|
31
|
+
{
|
|
32
|
+
type: "error",
|
|
33
|
+
message: "package.json file is missing.",
|
|
34
|
+
suggestion: "Ensure this is a valid Node.js project with a package.json file.",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
scoreImpact: 0,
|
|
38
|
+
maxScore: this.MAX_SCORE,
|
|
39
|
+
severity: "critical",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
|
|
43
|
+
const hasBuildScript = Boolean(pkg.scripts?.build);
|
|
44
|
+
const usesTypeScript = fs_1.default.existsSync(tsconfigPath);
|
|
45
|
+
// 2. TypeScript but no build script
|
|
46
|
+
if (usesTypeScript && !hasBuildScript) {
|
|
47
|
+
score = 10;
|
|
48
|
+
findings.push({
|
|
49
|
+
type: "error",
|
|
50
|
+
message: "TypeScript project detected but no build script found in package.json.",
|
|
51
|
+
evidence: "tsconfig.json exists but scripts.build is undefined",
|
|
52
|
+
suggestion: 'Add a build script (e.g., `"build": "tsc"`) to compile TypeScript before deployment.',
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
name: this.name,
|
|
56
|
+
success: false,
|
|
57
|
+
findings,
|
|
58
|
+
scoreImpact: score,
|
|
59
|
+
maxScore: this.MAX_SCORE,
|
|
60
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// 3. No build script at all
|
|
64
|
+
if (!hasBuildScript) {
|
|
65
|
+
findings.push({
|
|
66
|
+
type: "warning",
|
|
67
|
+
message: "No build script found in package.json.",
|
|
68
|
+
suggestion: "Consider defining a build step to ensure consistent production builds.",
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
name: this.name,
|
|
72
|
+
success: true,
|
|
73
|
+
findings,
|
|
74
|
+
scoreImpact: score,
|
|
75
|
+
maxScore: this.MAX_SCORE,
|
|
76
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Execute build safely
|
|
80
|
+
try {
|
|
81
|
+
const { stdout, stderr } = await (0, execa_1.execa)("npm", ["run", "build"], {
|
|
82
|
+
cwd: projectRoot,
|
|
83
|
+
timeout: this.BUILD_TIMEOUT_MS,
|
|
84
|
+
});
|
|
85
|
+
const combineOutput = `${stdout}\n${stderr}`;
|
|
86
|
+
const warningCount = (combineOutput.match(/warning/gi) || []).length;
|
|
87
|
+
if (warningCount > 0) {
|
|
88
|
+
const deduction = Math.min(warningCount * 2, 20);
|
|
89
|
+
score -= deduction;
|
|
90
|
+
findings.push({
|
|
91
|
+
type: "warning",
|
|
92
|
+
message: `${warningCount} build warning(s) detected.`,
|
|
93
|
+
suggestion: "Resolve build warnings to improve production stability and reduce hidden runtime issues."
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (score < 0)
|
|
97
|
+
score = 0;
|
|
98
|
+
return {
|
|
99
|
+
name: this.name,
|
|
100
|
+
success: score === this.MAX_SCORE,
|
|
101
|
+
findings,
|
|
102
|
+
scoreImpact: score,
|
|
103
|
+
maxScore: this.MAX_SCORE,
|
|
104
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
score = 0;
|
|
109
|
+
findings.push({
|
|
110
|
+
type: "error",
|
|
111
|
+
message: "Build execution failed.",
|
|
112
|
+
evidence: err?.stderr || err?.message,
|
|
113
|
+
suggestion: "Fix build errors before deployment. Ensure the project builds successfully in a clean environment."
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
name: this.name,
|
|
117
|
+
success: false,
|
|
118
|
+
findings,
|
|
119
|
+
scoreImpact: score,
|
|
120
|
+
maxScore: this.MAX_SCORE,
|
|
121
|
+
severity: "critical"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.BuildAnalyzer = BuildAnalyzer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EnvAnalyzer = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const glob_1 = require("glob");
|
|
10
|
+
const severity_1 = require("../utils/severity");
|
|
11
|
+
class EnvAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.name = "Environment Variables Check";
|
|
14
|
+
this.weight = 1;
|
|
15
|
+
this.supports = ["all"];
|
|
16
|
+
this.MAX_SCORE = 30;
|
|
17
|
+
}
|
|
18
|
+
async run() {
|
|
19
|
+
const projectRoot = process.cwd();
|
|
20
|
+
const baseDir = fs_1.default.existsSync(path_1.default.join(projectRoot, "src"))
|
|
21
|
+
? path_1.default.join(projectRoot, "src")
|
|
22
|
+
: projectRoot;
|
|
23
|
+
const files = (0, glob_1.globSync)("**/*.{ts,js,tsx,jsx,html,css}", {
|
|
24
|
+
cwd: baseDir,
|
|
25
|
+
ignore: ["node_modules/**", "dist/**"],
|
|
26
|
+
nodir: true
|
|
27
|
+
});
|
|
28
|
+
let score = this.MAX_SCORE;
|
|
29
|
+
const findings = [];
|
|
30
|
+
const envUsageFiles = [];
|
|
31
|
+
// Scan up to 300 files for performance safety
|
|
32
|
+
for (const file of files.slice(0, 300)) {
|
|
33
|
+
const content = fs_1.default.readFileSync(path_1.default.join(baseDir, file), "utf-8");
|
|
34
|
+
if (content.includes("process.env"))
|
|
35
|
+
envUsageFiles.push(file);
|
|
36
|
+
}
|
|
37
|
+
const usesEnv = envUsageFiles.length > 0;
|
|
38
|
+
// Check for any environment file variants
|
|
39
|
+
const envFiles = [
|
|
40
|
+
".env",
|
|
41
|
+
".env.local",
|
|
42
|
+
".env.development",
|
|
43
|
+
".env.production"
|
|
44
|
+
];
|
|
45
|
+
const existingEnvFile = envFiles.find((f) => fs_1.default.existsSync(path_1.default.join(projectRoot, f)));
|
|
46
|
+
// Detect static HTML/CSS/JS projects
|
|
47
|
+
const isStaticProject = files.every((f) => /\.(html|css|js)$/.test(f));
|
|
48
|
+
// ----------- 1. Static projects with no env usage -----------
|
|
49
|
+
if (isStaticProject && !usesEnv) {
|
|
50
|
+
findings.push({
|
|
51
|
+
type: "info",
|
|
52
|
+
message: "No .env file and no process.env usage detected.",
|
|
53
|
+
suggestion: "If your project will need API calls or secrets in the future, consider adding a .env file."
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// ----------- 2. Process.env used but no env file -----------
|
|
58
|
+
if (usesEnv && !existingEnvFile) {
|
|
59
|
+
score -= 20;
|
|
60
|
+
findings.push({
|
|
61
|
+
type: "error",
|
|
62
|
+
message: ".env file is missing but process.env usage was detected.",
|
|
63
|
+
evidence: `process.env found in ${envUsageFiles.length} file(s). Example: ${envUsageFiles.slice(0, 3).join(", ")}`,
|
|
64
|
+
suggestion: "Create a .env file (or .env.local/.env.development) for local development or configure environment variables in your deployment platform."
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ----------- 3. Recommend .env.example even if env file exists -----------
|
|
69
|
+
if (!fs_1.default.existsSync(path_1.default.join(projectRoot, ".env.example"))) {
|
|
70
|
+
findings.push({
|
|
71
|
+
type: "info",
|
|
72
|
+
message: ".env.example file is missing.",
|
|
73
|
+
suggestion: "Add a .env.example file to document required environment variables for your team and CI/CD environments."
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (score < 0)
|
|
77
|
+
score = 0;
|
|
78
|
+
return {
|
|
79
|
+
name: this.name,
|
|
80
|
+
success: score === this.MAX_SCORE,
|
|
81
|
+
findings,
|
|
82
|
+
scoreImpact: score,
|
|
83
|
+
maxScore: this.MAX_SCORE,
|
|
84
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.EnvAnalyzer = EnvAnalyzer;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NetworkAnalyzer = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const glob_1 = require("glob");
|
|
10
|
+
const severity_1 = require("../utils/severity");
|
|
11
|
+
class NetworkAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.name = "Localhost Leak Check";
|
|
14
|
+
this.weight = 1.2;
|
|
15
|
+
this.supports = ["all"];
|
|
16
|
+
this.MAX_SCORE = 20;
|
|
17
|
+
this.MAX_FILES_TO_SCAN = 500;
|
|
18
|
+
}
|
|
19
|
+
async run() {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const baseDir = fs_1.default.existsSync(path_1.default.join(projectRoot, "src"))
|
|
22
|
+
? path_1.default.join(projectRoot, "src")
|
|
23
|
+
: projectRoot;
|
|
24
|
+
const files = (0, glob_1.globSync)("**/*.{ts,js,tsx,jsx}", {
|
|
25
|
+
cwd: baseDir,
|
|
26
|
+
ignore: ["node_modules/**", "dist/**"],
|
|
27
|
+
nodir: true
|
|
28
|
+
});
|
|
29
|
+
let score = this.MAX_SCORE;
|
|
30
|
+
const findings = [];
|
|
31
|
+
const leakFiles = [];
|
|
32
|
+
for (const file of files.slice(0, this.MAX_FILES_TO_SCAN)) {
|
|
33
|
+
const fullPath = path_1.default.join(baseDir, file);
|
|
34
|
+
const content = fs_1.default.readFileSync(fullPath, "utf-8");
|
|
35
|
+
// Basic but effective detection
|
|
36
|
+
if (content.includes("localhost") ||
|
|
37
|
+
content.includes("127.0.0.1")) {
|
|
38
|
+
leakFiles.push(file);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const leakCount = leakFiles.length;
|
|
42
|
+
if (leakCount > 0) {
|
|
43
|
+
// Deduct up to a max of 12 points (prevent total collapse)
|
|
44
|
+
const deduction = Math.min(leakCount * 2, 12);
|
|
45
|
+
score -= deduction;
|
|
46
|
+
findings.push({
|
|
47
|
+
type: leakCount > 5 ? "error" : "warning",
|
|
48
|
+
message: `${leakCount} hardcoded localhost reference(s) detected.`,
|
|
49
|
+
evidence: `Examples: ${leakFiles.slice(0, 5).join(", ")}`,
|
|
50
|
+
suggestion: "Replace hardcoded localhost or 127.0.0.1 URLs with environment-based configuration (e.g., process.env.API_BASE_URL).",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (score < 0)
|
|
54
|
+
score = 0;
|
|
55
|
+
return {
|
|
56
|
+
name: this.name,
|
|
57
|
+
success: leakCount === 0,
|
|
58
|
+
findings,
|
|
59
|
+
scoreImpact: score,
|
|
60
|
+
maxScore: this.MAX_SCORE,
|
|
61
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.NetworkAnalyzer = NetworkAnalyzer;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RuntimeAnalyzer = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const severity_1 = require("../utils/severity");
|
|
10
|
+
class RuntimeAnalyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.name = "Runtime Compatibility Check";
|
|
13
|
+
this.weight = 2;
|
|
14
|
+
this.supports = ["node"];
|
|
15
|
+
this.MAX_SCORE = 20;
|
|
16
|
+
}
|
|
17
|
+
async run() {
|
|
18
|
+
const projectRoot = process.cwd();
|
|
19
|
+
const packageJsonPath = path_1.default.join(projectRoot, "package.json");
|
|
20
|
+
let score = 0;
|
|
21
|
+
const findings = [];
|
|
22
|
+
// Check Node engine declaration
|
|
23
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
24
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
|
|
25
|
+
if (!pkg.engines?.node) {
|
|
26
|
+
score -= 8;
|
|
27
|
+
findings.push({
|
|
28
|
+
type: "error",
|
|
29
|
+
message: "Node.js version is not specified in package.json (engines.node missing).",
|
|
30
|
+
evidence: "package.json does not define engines.node",
|
|
31
|
+
suggestion: 'Add `"engines": { "node": ">=18.x" }` to package.json to enforce runtime consistency.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
score = 0;
|
|
37
|
+
findings.push({
|
|
38
|
+
type: "error",
|
|
39
|
+
message: "package.json file is missing.",
|
|
40
|
+
suggestion: "Ensure this project is a valid Node.js project with a package.json file.",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Check version manager config (.nvmrc / .node-version)
|
|
44
|
+
const hasNodeVersionFile = fs_1.default.existsSync(path_1.default.join(projectRoot, ".nvmrc")) ||
|
|
45
|
+
fs_1.default.existsSync(path_1.default.join(projectRoot, ".node-version"));
|
|
46
|
+
if (!hasNodeVersionFile) {
|
|
47
|
+
score -= 6;
|
|
48
|
+
findings.push({
|
|
49
|
+
type: "warning",
|
|
50
|
+
message: "No Node version manager file found (.nvmrc or .node-version).",
|
|
51
|
+
suggestion: "Add a .nvmrc or .node-version file to standardize Node.js versions across environments.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Check dependency lockfile
|
|
55
|
+
const lockfiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"];
|
|
56
|
+
const detectedLockfile = lockfiles.find(file => fs_1.default.existsSync(path_1.default.join(projectRoot, file)));
|
|
57
|
+
if (!detectedLockfile) {
|
|
58
|
+
score -= 6;
|
|
59
|
+
findings.push({
|
|
60
|
+
type: "error",
|
|
61
|
+
message: "No dependency lockfile detected.",
|
|
62
|
+
evidence: "Missing package-lock.json, yarn.lock, or pnpm-lock.yaml",
|
|
63
|
+
suggestion: "Commit a lockfile to ensure deterministic dependency installation across environments.",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (score < 0)
|
|
67
|
+
score = 0;
|
|
68
|
+
return {
|
|
69
|
+
name: this.name,
|
|
70
|
+
success: score === this.MAX_SCORE,
|
|
71
|
+
findings,
|
|
72
|
+
scoreImpact: score,
|
|
73
|
+
maxScore: this.MAX_SCORE,
|
|
74
|
+
severity: (0, severity_1.calculateSeverity)(score, this.MAX_SCORE),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.RuntimeAnalyzer = RuntimeAnalyzer;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const scan_1 = require("./core/scan");
|
|
43
|
+
const build_1 = require("./analyzers/build");
|
|
44
|
+
const env_1 = require("./analyzers/env");
|
|
45
|
+
const network_1 = require("./analyzers/network");
|
|
46
|
+
const runtime_1 = require("./analyzers/runtime");
|
|
47
|
+
const projectUtils_1 = require("./utils/projectUtils");
|
|
48
|
+
const ci_1 = require("./utils/ci");
|
|
49
|
+
const program = new commander_1.Command();
|
|
50
|
+
function renderProgressBar(percentage, length = 20) {
|
|
51
|
+
const safe = Math.max(0, Math.min(percentage, 100));
|
|
52
|
+
const filled = Math.round((safe / 100) * length);
|
|
53
|
+
const bar = "█".repeat(filled) + "░".repeat(length - filled);
|
|
54
|
+
return `[${bar}] ${safe.toFixed(1)}%`;
|
|
55
|
+
}
|
|
56
|
+
program
|
|
57
|
+
.name("bridgepreflight")
|
|
58
|
+
.description("AI-native production safety scanner")
|
|
59
|
+
.version("0.0.1");
|
|
60
|
+
program
|
|
61
|
+
.command("scan")
|
|
62
|
+
.option("--json", "Output results as JSON (for CI pipelines)")
|
|
63
|
+
.description("Run production safety checks")
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
console.log(chalk_1.default.blue("\nWelcome to BridgePreflight — AI-native infrastructure readiness scanner\n"));
|
|
66
|
+
const ciMode = (0, ci_1.isCI)();
|
|
67
|
+
if (!ciMode) {
|
|
68
|
+
const accessGranted = await (0, projectUtils_1.askProjectAccess)();
|
|
69
|
+
if (!accessGranted) {
|
|
70
|
+
console.log(chalk_1.default.blue("\nThank you for using BridgePreflight"));
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(chalk_1.default.gray("CI environment detected — interactive prompts disabled."));
|
|
76
|
+
}
|
|
77
|
+
const projectType = (0, projectUtils_1.detectProjectType)();
|
|
78
|
+
if (projectType === "static") {
|
|
79
|
+
console.log(chalk_1.default.yellow("Detected static HTML/CSS/JS project. Node-specific checks will be skipped."));
|
|
80
|
+
}
|
|
81
|
+
let spinner;
|
|
82
|
+
try {
|
|
83
|
+
const ora = (await Promise.resolve().then(() => __importStar(require("ora")))).default;
|
|
84
|
+
spinner = ora("Running analyzers...").start();
|
|
85
|
+
const allAnalyzers = [
|
|
86
|
+
new build_1.BuildAnalyzer(),
|
|
87
|
+
new env_1.EnvAnalyzer(),
|
|
88
|
+
new network_1.NetworkAnalyzer(),
|
|
89
|
+
new runtime_1.RuntimeAnalyzer(),
|
|
90
|
+
];
|
|
91
|
+
const analyzers = allAnalyzers.filter((a) => a.supports.includes(projectType) ||
|
|
92
|
+
a.supports.includes("all"));
|
|
93
|
+
const skippedAnalyzers = allAnalyzers.filter((a) => !analyzers.includes(a));
|
|
94
|
+
const engine = new scan_1.ScanEngine(analyzers);
|
|
95
|
+
const results = await engine.runAll();
|
|
96
|
+
spinner.stop();
|
|
97
|
+
// =============================
|
|
98
|
+
// 1. Scoring System
|
|
99
|
+
// =============================
|
|
100
|
+
let totalScore = 0;
|
|
101
|
+
let totalMaxScore = 0;
|
|
102
|
+
let weightedScore = 0;
|
|
103
|
+
let weightedMaxScore = 0;
|
|
104
|
+
results.forEach(({ analyzer, result }) => {
|
|
105
|
+
const weight = analyzer.weight ?? 1;
|
|
106
|
+
totalScore += result.scoreImpact;
|
|
107
|
+
totalMaxScore += result.maxScore;
|
|
108
|
+
weightedScore += result.scoreImpact * weight;
|
|
109
|
+
weightedMaxScore += result.maxScore * weight;
|
|
110
|
+
});
|
|
111
|
+
const percentage = weightedMaxScore === 0
|
|
112
|
+
? 100
|
|
113
|
+
: (weightedScore / weightedMaxScore) * 100;
|
|
114
|
+
let readinessLabel;
|
|
115
|
+
let readinessColor;
|
|
116
|
+
if (percentage >= 90) {
|
|
117
|
+
readinessLabel = "Safe to Deploy";
|
|
118
|
+
readinessColor = chalk_1.default.green;
|
|
119
|
+
}
|
|
120
|
+
else if (percentage >= 70) {
|
|
121
|
+
readinessLabel = "Deploy with Caution";
|
|
122
|
+
readinessColor = chalk_1.default.yellow;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
readinessLabel = "High Risk";
|
|
126
|
+
readinessColor = chalk_1.default.red;
|
|
127
|
+
}
|
|
128
|
+
// =============================
|
|
129
|
+
// 2. Top Risk Detection
|
|
130
|
+
// =============================
|
|
131
|
+
let topRisk;
|
|
132
|
+
let lowestHealth = 101;
|
|
133
|
+
for (const { result } of results) {
|
|
134
|
+
if (result.maxScore === 0)
|
|
135
|
+
continue;
|
|
136
|
+
const health = (result.scoreImpact / result.maxScore) * 100;
|
|
137
|
+
if (health < lowestHealth) {
|
|
138
|
+
lowestHealth = health;
|
|
139
|
+
topRisk = {
|
|
140
|
+
name: result.name,
|
|
141
|
+
health,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// =============================
|
|
146
|
+
// JSON Output Mode
|
|
147
|
+
// =============================
|
|
148
|
+
if (options.json) {
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
totalScore,
|
|
151
|
+
maxScore: totalMaxScore,
|
|
152
|
+
percentage,
|
|
153
|
+
readiness: readinessLabel,
|
|
154
|
+
results,
|
|
155
|
+
}, null, 2));
|
|
156
|
+
process.exit(percentage < 70 ? 1 : 0);
|
|
157
|
+
}
|
|
158
|
+
// =============================
|
|
159
|
+
// 3. Analyzer Overview
|
|
160
|
+
// =============================
|
|
161
|
+
console.log(chalk_1.default.blue("\n--- BridgePreflight Scan ---\n"));
|
|
162
|
+
results.forEach(({ result, duration }) => {
|
|
163
|
+
const health = result.maxScore === 0
|
|
164
|
+
? 100
|
|
165
|
+
: (result.scoreImpact / result.maxScore) * 100;
|
|
166
|
+
const icon = health >= 90 ? "✅" :
|
|
167
|
+
health >= 70 ? "⚠" : "❌";
|
|
168
|
+
const color = health >= 90
|
|
169
|
+
? chalk_1.default.green
|
|
170
|
+
: health >= 70
|
|
171
|
+
? chalk_1.default.yellow
|
|
172
|
+
: chalk_1.default.red;
|
|
173
|
+
console.log(color(`${icon} ${result.name} (${result.scoreImpact}/${result.maxScore}) - ${duration}ms`));
|
|
174
|
+
console.log(color(` ${renderProgressBar(health)}`));
|
|
175
|
+
});
|
|
176
|
+
// Show skipped analyzers clearly
|
|
177
|
+
skippedAnalyzers.forEach((a) => {
|
|
178
|
+
console.log(chalk_1.default.gray(`➖ ${a.name} skipped for ${projectType} project.`));
|
|
179
|
+
});
|
|
180
|
+
// =============================
|
|
181
|
+
// 4. Total Summary
|
|
182
|
+
// =============================
|
|
183
|
+
console.log(chalk_1.default.blue("\n-----------------------------"));
|
|
184
|
+
console.log(chalk_1.default.magenta.bold(`Total Score: ${totalScore}/${totalMaxScore} (${percentage.toFixed(1)}%)`));
|
|
185
|
+
console.log(chalk_1.default.gray("Weighted scoring applied based on risk category"));
|
|
186
|
+
console.log(readinessColor.bold(`Readiness: ${readinessLabel}`));
|
|
187
|
+
if (topRisk && topRisk.health < 100) {
|
|
188
|
+
const riskColor = topRisk.health >= 70
|
|
189
|
+
? chalk_1.default.yellow
|
|
190
|
+
: chalk_1.default.red;
|
|
191
|
+
console.log(riskColor.bold(`Top Risk Area: ${topRisk.name} (${topRisk.health.toFixed(1)}% health)`));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(chalk_1.default.gray("Top Risk Area: None (all checks passed)"));
|
|
195
|
+
}
|
|
196
|
+
console.log(chalk_1.default.blue("-----------------------------\n"));
|
|
197
|
+
// =============================
|
|
198
|
+
// 5. Detailed Findings
|
|
199
|
+
// =============================
|
|
200
|
+
console.log(chalk_1.default.cyan.bold("--- Detailed Findings ---\n"));
|
|
201
|
+
results.forEach(({ analyzer, result }) => {
|
|
202
|
+
if (result.findings.length === 0)
|
|
203
|
+
return;
|
|
204
|
+
console.log(chalk_1.default.yellow.bold(`${analyzer.name} Findings:`));
|
|
205
|
+
result.findings.forEach((f) => {
|
|
206
|
+
const typeColor = f.type === "error"
|
|
207
|
+
? chalk_1.default.red.bold
|
|
208
|
+
: f.type === "warning"
|
|
209
|
+
? chalk_1.default.yellow.bold
|
|
210
|
+
: chalk_1.default.blue;
|
|
211
|
+
console.log(typeColor(` • ${f.type.toUpperCase()}: ${f.message}`));
|
|
212
|
+
if (f.evidence) {
|
|
213
|
+
console.log(chalk_1.default.gray(` • Evidence: ${f.evidence}`));
|
|
214
|
+
}
|
|
215
|
+
if (f.suggestion) {
|
|
216
|
+
console.log(chalk_1.default.cyan(` → Suggested action: ${f.suggestion}`));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
console.log("");
|
|
220
|
+
});
|
|
221
|
+
if (percentage < 70)
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (spinner)
|
|
226
|
+
spinner.stop();
|
|
227
|
+
console.error(chalk_1.default.red("Error running scan:"), error);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScanEngine = void 0;
|
|
4
|
+
class ScanEngine {
|
|
5
|
+
constructor(analyzers) {
|
|
6
|
+
this.analyzers = analyzers;
|
|
7
|
+
}
|
|
8
|
+
async runAll() {
|
|
9
|
+
const results = [];
|
|
10
|
+
for (const analyzer of this.analyzers) {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const result = await analyzer.run();
|
|
13
|
+
const duration = Date.now() - start;
|
|
14
|
+
results.push({
|
|
15
|
+
analyzer,
|
|
16
|
+
result,
|
|
17
|
+
duration,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.ScanEngine = ScanEngine;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/utils/ci.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Detects whether BridgePreflight is running in a CI environment.
|
|
3
|
+
// Supports major CI providers and generic CI flag.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.isCI = isCI;
|
|
6
|
+
function isCI() {
|
|
7
|
+
const env = process.env;
|
|
8
|
+
return Boolean(env.CI ||
|
|
9
|
+
env.GITHUB_ACTIONS ||
|
|
10
|
+
env.GITLAB_CI ||
|
|
11
|
+
env.BITBUCKET_BUILD_NUMBER ||
|
|
12
|
+
env.CIRCLECI ||
|
|
13
|
+
env.BUILDKITE ||
|
|
14
|
+
env.TRAVIS ||
|
|
15
|
+
env.TEAMCITY_VERSION ||
|
|
16
|
+
env.AZURE_HTTP_USER_AGENT ||
|
|
17
|
+
env.JENKINS_URL);
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.prompt = prompt;
|
|
7
|
+
exports.askProjectAccess = askProjectAccess;
|
|
8
|
+
exports.detectProjectType = detectProjectType;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const readline_1 = __importDefault(require("readline"));
|
|
12
|
+
const ci_1 = require("./ci");
|
|
13
|
+
// Prompt user with a question in CLI
|
|
14
|
+
async function prompt(question) {
|
|
15
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
rl.question(question, answer => {
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(answer.trim().toLowerCase());
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Ask the developer for permission to scan the project
|
|
24
|
+
async function askProjectAccess() {
|
|
25
|
+
if ((0, ci_1.isCI)())
|
|
26
|
+
return true;
|
|
27
|
+
const answer = await prompt(chalk_1.default.blackBright("BridgePreflight needs access to scan your entire project. Allow? (y/n): "));
|
|
28
|
+
return answer === "y";
|
|
29
|
+
}
|
|
30
|
+
// Detect project type: Node, Frontend, or Static HTML/CSS/JS
|
|
31
|
+
function detectProjectType() {
|
|
32
|
+
const files = fs_1.default.readdirSync(process.cwd());
|
|
33
|
+
if (files.includes("package.json"))
|
|
34
|
+
return "node";
|
|
35
|
+
if (files.some(f => f.endsWith(".html")))
|
|
36
|
+
return "static";
|
|
37
|
+
return "frontend";
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateSeverity = calculateSeverity;
|
|
4
|
+
function calculateSeverity(score, max) {
|
|
5
|
+
const percent = max === 0 ? 100 : (score / max) * 100;
|
|
6
|
+
if (percent === 100)
|
|
7
|
+
return "healthy";
|
|
8
|
+
if (percent >= 75)
|
|
9
|
+
return "low";
|
|
10
|
+
if (percent >= 50)
|
|
11
|
+
return "medium";
|
|
12
|
+
if (percent >= 25)
|
|
13
|
+
return "high";
|
|
14
|
+
return "critical";
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bridgepreflight",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-native production readiness scanner for developers",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bridgepreflight": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "ts-node src/cli.ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"devtools",
|
|
18
|
+
"ci",
|
|
19
|
+
"production",
|
|
20
|
+
"deployment",
|
|
21
|
+
"node",
|
|
22
|
+
"scanner"
|
|
23
|
+
],
|
|
24
|
+
"author": "Godsfavour Jesse",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^5.6.2",
|
|
29
|
+
"commander": "^14.0.3",
|
|
30
|
+
"execa": "^9.6.1",
|
|
31
|
+
"glob": "^13.0.2",
|
|
32
|
+
"ora": "^9.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/glob": "^8.1.0",
|
|
36
|
+
"@types/node": "^25.2.3",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
}
|
|
43
|
+
}
|