@ijfw/install 1.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/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/ijfw.js +1316 -0
- package/dist/install.js +279 -0
- package/dist/uninstall.js +259 -0
- package/package.json +58 -0
- package/src/install.ps1 +277 -0
package/dist/ijfw.js
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name12 in all)
|
|
9
|
+
__defProp(target, name12, { get: all[name12], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/preflight/runner.js
|
|
13
|
+
function statusColor(status) {
|
|
14
|
+
if (status === "PASS") return c.green;
|
|
15
|
+
if (status === "FAIL") return c.red;
|
|
16
|
+
if (status === "WARN") return c.yellow;
|
|
17
|
+
if (status === "SKIP") return c.cyan;
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
function pad(s, n) {
|
|
21
|
+
return s.padEnd(n, " ");
|
|
22
|
+
}
|
|
23
|
+
function printGateResult(result, index, total) {
|
|
24
|
+
const col = statusColor(result.status);
|
|
25
|
+
const badge = `${col}[${result.status}]${c.reset}`;
|
|
26
|
+
const ms = `${c.dim}${result.durationMs}ms${c.reset}`;
|
|
27
|
+
const num = `${c.dim}${String(index).padStart(2, " ")}/${total}${c.reset}`;
|
|
28
|
+
console.log(` ${num} ${badge} ${pad(result.name, 20)} ${result.message} ${ms}`);
|
|
29
|
+
if (result.details.length > 0) {
|
|
30
|
+
for (const line of result.details) {
|
|
31
|
+
console.log(` ${c.dim}${line}${c.reset}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function runPreflight(gates, ctx) {
|
|
36
|
+
const t0 = Date.now();
|
|
37
|
+
const results = [];
|
|
38
|
+
if (!ctx.json) {
|
|
39
|
+
console.log(`
|
|
40
|
+
${c.bold}IJFW Preflight${c.reset} -- ${gates.length} gates
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
const parallelGates = gates.filter((g) => g.parallel !== false);
|
|
44
|
+
const serialGates = gates.filter((g) => g.parallel === false);
|
|
45
|
+
if (parallelGates.length > 0) {
|
|
46
|
+
if (!ctx.json) console.log(`${c.dim} Running ${parallelGates.length} gate(s) in parallel...${c.reset}`);
|
|
47
|
+
const parallelResults = await Promise.all(parallelGates.map((g) => g.run(ctx)));
|
|
48
|
+
for (let i = 0; i < parallelResults.length; i++) {
|
|
49
|
+
results.push(parallelResults[i]);
|
|
50
|
+
if (!ctx.json) printGateResult(parallelResults[i], results.length, gates.length);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const gate of serialGates) {
|
|
54
|
+
const result = await gate.run(ctx);
|
|
55
|
+
results.push(result);
|
|
56
|
+
if (!ctx.json) printGateResult(result, results.length, gates.length);
|
|
57
|
+
if (ctx.failFast && result.status === "FAIL" && gate.severity === "blocking") {
|
|
58
|
+
if (!ctx.json) {
|
|
59
|
+
console.log(`
|
|
60
|
+
${c.yellow}Paused at gate: ${gate.name} -- resolve this one first, then rerun.${c.reset}
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const totalMs = Date.now() - t0;
|
|
67
|
+
const blockingFailures = results.filter((r) => {
|
|
68
|
+
const gate = gates.find((g) => g.name === r.name);
|
|
69
|
+
return r.status === "FAIL" && gate && gate.severity === "blocking";
|
|
70
|
+
});
|
|
71
|
+
const outcome = blockingFailures.length > 0 ? "fail" : "pass";
|
|
72
|
+
const report = {
|
|
73
|
+
version: "1.1.0",
|
|
74
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
+
gates: results,
|
|
76
|
+
outcome,
|
|
77
|
+
totalMs
|
|
78
|
+
};
|
|
79
|
+
if (ctx.json) {
|
|
80
|
+
console.log(JSON.stringify(report, null, 2));
|
|
81
|
+
return report;
|
|
82
|
+
}
|
|
83
|
+
const passCount = results.filter((r) => r.status === "PASS").length;
|
|
84
|
+
const warnCount = results.filter((r) => r.status === "WARN").length;
|
|
85
|
+
const skipCount = results.filter((r) => r.status === "SKIP").length;
|
|
86
|
+
const failCount = results.filter((r) => r.status === "FAIL").length;
|
|
87
|
+
const warmSLO = 9e4;
|
|
88
|
+
const coldSLO = 24e4;
|
|
89
|
+
const timeNote = totalMs <= warmSLO ? `${c.green}within warm-cache SLO (<=90s)${c.reset}` : totalMs <= coldSLO ? `${c.yellow}within cold-cache limit (<=240s)${c.reset}` : `${c.red}exceeded cold-cache limit (${Math.round(totalMs / 1e3)}s > 240s)${c.reset}`;
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(` ${c.bold}Summary${c.reset}`);
|
|
92
|
+
console.log(` ${c.green}PASS ${passCount}${c.reset} ${c.yellow}WARN ${warnCount}${c.reset} ${c.cyan}SKIP ${skipCount}${c.reset} ${c.red}FAIL ${failCount}${c.reset}`);
|
|
93
|
+
console.log(` Time: ${Math.round(totalMs / 1e3)}s ${timeNote}`);
|
|
94
|
+
console.log("");
|
|
95
|
+
if (outcome === "pass") {
|
|
96
|
+
console.log(` ${c.bold}${c.green}All blocking gates passed.${c.reset}`);
|
|
97
|
+
if (warnCount > 0) {
|
|
98
|
+
console.log(` ${c.yellow}${warnCount} advisory note(s) worth reviewing.${c.reset}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log(` ${c.bold}${c.red}${blockingFailures.length} item(s) need attention before shipping.${c.reset}`);
|
|
102
|
+
for (const r of blockingFailures) {
|
|
103
|
+
console.log(` ${c.red} HIGH ${r.name}: ${r.message}${c.reset}`);
|
|
104
|
+
}
|
|
105
|
+
console.log(` ${c.yellow}Fix the findings above, then re-run \`ijfw preflight\`.${c.reset}`);
|
|
106
|
+
}
|
|
107
|
+
console.log("");
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
var isTTY, c;
|
|
111
|
+
var init_runner = __esm({
|
|
112
|
+
"src/preflight/runner.js"() {
|
|
113
|
+
isTTY = process.stdout.isTTY;
|
|
114
|
+
c = {
|
|
115
|
+
green: isTTY ? "\x1B[32m" : "",
|
|
116
|
+
yellow: isTTY ? "\x1B[33m" : "",
|
|
117
|
+
red: isTTY ? "\x1B[31m" : "",
|
|
118
|
+
cyan: isTTY ? "\x1B[36m" : "",
|
|
119
|
+
bold: isTTY ? "\x1B[1m" : "",
|
|
120
|
+
reset: isTTY ? "\x1B[0m" : "",
|
|
121
|
+
dim: isTTY ? "\x1B[2m" : ""
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/preflight/gates/shellcheck.js
|
|
127
|
+
var shellcheck_exports = {};
|
|
128
|
+
__export(shellcheck_exports, {
|
|
129
|
+
name: () => name,
|
|
130
|
+
parallel: () => parallel,
|
|
131
|
+
run: () => run,
|
|
132
|
+
severity: () => severity
|
|
133
|
+
});
|
|
134
|
+
import { spawnSync } from "node:child_process";
|
|
135
|
+
import { readdirSync, statSync } from "node:fs";
|
|
136
|
+
import { join } from "node:path";
|
|
137
|
+
function findShFiles(dir, acc = []) {
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = readdirSync(dir);
|
|
141
|
+
} catch {
|
|
142
|
+
return acc;
|
|
143
|
+
}
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (e === "node_modules" || e === ".git") continue;
|
|
146
|
+
const full = join(dir, e);
|
|
147
|
+
let st;
|
|
148
|
+
try {
|
|
149
|
+
st = statSync(full);
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (st.isDirectory()) findShFiles(full, acc);
|
|
154
|
+
else if (e.endsWith(".sh")) acc.push(full);
|
|
155
|
+
}
|
|
156
|
+
return acc;
|
|
157
|
+
}
|
|
158
|
+
async function run(ctx) {
|
|
159
|
+
const t0 = Date.now();
|
|
160
|
+
const which = spawnSync("shellcheck", ["--version"], { encoding: "utf8" });
|
|
161
|
+
if (which.status === null || which.error) {
|
|
162
|
+
return {
|
|
163
|
+
name: "shellcheck",
|
|
164
|
+
status: "SKIP",
|
|
165
|
+
message: "shellcheck not installed -- brew install shellcheck / apt install shellcheck",
|
|
166
|
+
details: ["Gate skipped: install shellcheck to enable shell linting."],
|
|
167
|
+
durationMs: Date.now() - t0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const files = findShFiles(ctx.repoRoot);
|
|
171
|
+
if (files.length === 0) {
|
|
172
|
+
return {
|
|
173
|
+
name: "shellcheck",
|
|
174
|
+
status: "PASS",
|
|
175
|
+
message: "No shell scripts found",
|
|
176
|
+
details: [],
|
|
177
|
+
durationMs: Date.now() - t0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const res = spawnSync("shellcheck", ["--enable=all", "--disable=SC2312", ...files], {
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
cwd: ctx.repoRoot
|
|
183
|
+
});
|
|
184
|
+
const durationMs = Date.now() - t0;
|
|
185
|
+
if (res.status === 0) {
|
|
186
|
+
return {
|
|
187
|
+
name: "shellcheck",
|
|
188
|
+
status: "PASS",
|
|
189
|
+
message: `${files.length} shell script(s) clean`,
|
|
190
|
+
details: [],
|
|
191
|
+
durationMs
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const lines = (res.stdout || "").split("\n").filter(Boolean);
|
|
195
|
+
return {
|
|
196
|
+
name: "shellcheck",
|
|
197
|
+
status: "FAIL",
|
|
198
|
+
message: `shellcheck found issues in ${files.length} script(s)`,
|
|
199
|
+
details: lines.slice(0, 20),
|
|
200
|
+
durationMs
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
var name, severity, parallel;
|
|
204
|
+
var init_shellcheck = __esm({
|
|
205
|
+
"src/preflight/gates/shellcheck.js"() {
|
|
206
|
+
name = "shellcheck";
|
|
207
|
+
severity = "blocking";
|
|
208
|
+
parallel = true;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/preflight/gates/oxlint.js
|
|
213
|
+
var oxlint_exports = {};
|
|
214
|
+
__export(oxlint_exports, {
|
|
215
|
+
name: () => name2,
|
|
216
|
+
parallel: () => parallel2,
|
|
217
|
+
run: () => run2,
|
|
218
|
+
severity: () => severity2
|
|
219
|
+
});
|
|
220
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
221
|
+
async function run2(ctx) {
|
|
222
|
+
const t0 = Date.now();
|
|
223
|
+
const ver = ctx.versions["oxlint"] || "latest";
|
|
224
|
+
const res = spawnSync2(
|
|
225
|
+
"npx",
|
|
226
|
+
["--yes", `oxlint@${ver}`, "--deny-warnings", "installer/src", "mcp-server/src"],
|
|
227
|
+
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 6e4 }
|
|
228
|
+
);
|
|
229
|
+
const durationMs = Date.now() - t0;
|
|
230
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
231
|
+
const lines = output.split("\n").filter(Boolean);
|
|
232
|
+
if (res.status === 0) {
|
|
233
|
+
return {
|
|
234
|
+
name: "oxlint",
|
|
235
|
+
status: "PASS",
|
|
236
|
+
message: "oxlint: no issues",
|
|
237
|
+
details: [],
|
|
238
|
+
durationMs
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
name: "oxlint",
|
|
243
|
+
status: "FAIL",
|
|
244
|
+
message: "oxlint found lint issues",
|
|
245
|
+
details: lines.slice(0, 30),
|
|
246
|
+
durationMs
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
var name2, severity2, parallel2;
|
|
250
|
+
var init_oxlint = __esm({
|
|
251
|
+
"src/preflight/gates/oxlint.js"() {
|
|
252
|
+
name2 = "oxlint";
|
|
253
|
+
severity2 = "blocking";
|
|
254
|
+
parallel2 = true;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// src/preflight/gates/eslint-security.js
|
|
259
|
+
var eslint_security_exports = {};
|
|
260
|
+
__export(eslint_security_exports, {
|
|
261
|
+
name: () => name3,
|
|
262
|
+
parallel: () => parallel3,
|
|
263
|
+
run: () => run3,
|
|
264
|
+
severity: () => severity3
|
|
265
|
+
});
|
|
266
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
267
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
268
|
+
import { join as join2 } from "node:path";
|
|
269
|
+
import { tmpdir } from "node:os";
|
|
270
|
+
async function run3(ctx) {
|
|
271
|
+
const t0 = Date.now();
|
|
272
|
+
const eslintVer = ctx.versions["eslint"] || "latest";
|
|
273
|
+
const pluginVer = ctx.versions["eslint-plugin-security"] || "latest";
|
|
274
|
+
const tmpDir = mkdtempSync(join2(tmpdir(), "ijfw-eslint-security-"));
|
|
275
|
+
try {
|
|
276
|
+
writeFileSync(join2(tmpDir, "package.json"), JSON.stringify({ name: "eslint-security-runner", version: "1.0.0", type: "module" }));
|
|
277
|
+
const install = spawnSync3(
|
|
278
|
+
"npm",
|
|
279
|
+
["install", "--no-save", "--silent", `eslint@${eslintVer}`, `eslint-plugin-security@${pluginVer}`],
|
|
280
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 9e4 }
|
|
281
|
+
);
|
|
282
|
+
if (install.status !== 0) {
|
|
283
|
+
return {
|
|
284
|
+
name: "eslint-security",
|
|
285
|
+
status: "WARN",
|
|
286
|
+
message: "eslint-security: could not install plugin (npm install failed)",
|
|
287
|
+
details: ((install.stdout || "") + (install.stderr || "")).split("\n").filter(Boolean).slice(0, 5),
|
|
288
|
+
durationMs: Date.now() - t0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const configContent = `
|
|
292
|
+
import security from '${join2(tmpDir, "node_modules", "eslint-plugin-security", "index.js").replace(/\\/g, "/")}';
|
|
293
|
+
export default [
|
|
294
|
+
{
|
|
295
|
+
files: ['installer/src/**/*.js', 'mcp-server/src/**/*.js'],
|
|
296
|
+
plugins: { security },
|
|
297
|
+
rules: ${JSON.stringify(RULES)},
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
`;
|
|
301
|
+
const configPath = join2(ctx.repoRoot, ".eslint-security-temp.mjs");
|
|
302
|
+
writeFileSync(configPath, configContent, "utf8");
|
|
303
|
+
const eslintBin = join2(tmpDir, "node_modules", ".bin", "eslint");
|
|
304
|
+
const res = spawnSync3(
|
|
305
|
+
eslintBin,
|
|
306
|
+
["--config", configPath, "installer/src/**/*.js", "mcp-server/src/**/*.js"],
|
|
307
|
+
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 6e4 }
|
|
308
|
+
);
|
|
309
|
+
const durationMs = Date.now() - t0;
|
|
310
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
311
|
+
const lines = output.split("\n").filter(Boolean);
|
|
312
|
+
if (res.status === 0) {
|
|
313
|
+
return {
|
|
314
|
+
name: "eslint-security",
|
|
315
|
+
status: "PASS",
|
|
316
|
+
message: "eslint-security: no security issues",
|
|
317
|
+
details: [],
|
|
318
|
+
durationMs
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (res.status === 1) {
|
|
322
|
+
return {
|
|
323
|
+
name: "eslint-security",
|
|
324
|
+
status: "WARN",
|
|
325
|
+
message: "eslint-security: advisory warnings (review above)",
|
|
326
|
+
details: lines.slice(0, 30),
|
|
327
|
+
durationMs
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
name: "eslint-security",
|
|
332
|
+
status: "FAIL",
|
|
333
|
+
message: "eslint-security: security errors found (exit code 2)",
|
|
334
|
+
details: lines.slice(0, 30),
|
|
335
|
+
durationMs
|
|
336
|
+
};
|
|
337
|
+
} finally {
|
|
338
|
+
try {
|
|
339
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
rmSync(join2(ctx.repoRoot, ".eslint-security-temp.mjs"), { force: true });
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var RULES, name3, severity3, parallel3;
|
|
349
|
+
var init_eslint_security = __esm({
|
|
350
|
+
"src/preflight/gates/eslint-security.js"() {
|
|
351
|
+
RULES = {
|
|
352
|
+
"security/detect-eval-with-expression": "error",
|
|
353
|
+
"security/detect-non-literal-fs-filename": "warn",
|
|
354
|
+
"security/detect-non-literal-regexp": "warn",
|
|
355
|
+
"security/detect-non-literal-require": "warn",
|
|
356
|
+
"security/detect-object-injection": "warn",
|
|
357
|
+
"security/detect-possible-timing-attacks": "warn",
|
|
358
|
+
"security/detect-pseudoRandomBytes": "error",
|
|
359
|
+
"security/detect-unsafe-regex": "error"
|
|
360
|
+
};
|
|
361
|
+
name3 = "eslint-security";
|
|
362
|
+
severity3 = "blocking";
|
|
363
|
+
parallel3 = true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// src/preflight/gates/psscriptanalyzer.js
|
|
368
|
+
var psscriptanalyzer_exports = {};
|
|
369
|
+
__export(psscriptanalyzer_exports, {
|
|
370
|
+
name: () => name4,
|
|
371
|
+
parallel: () => parallel4,
|
|
372
|
+
run: () => run4,
|
|
373
|
+
severity: () => severity4
|
|
374
|
+
});
|
|
375
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
376
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
377
|
+
import { join as join3 } from "node:path";
|
|
378
|
+
import { platform } from "node:os";
|
|
379
|
+
function findPs1Files(dir, acc = []) {
|
|
380
|
+
let entries;
|
|
381
|
+
try {
|
|
382
|
+
entries = readdirSync2(dir);
|
|
383
|
+
} catch {
|
|
384
|
+
return acc;
|
|
385
|
+
}
|
|
386
|
+
for (const e of entries) {
|
|
387
|
+
if (e === "node_modules" || e === ".git") continue;
|
|
388
|
+
const full = join3(dir, e);
|
|
389
|
+
let st;
|
|
390
|
+
try {
|
|
391
|
+
st = statSync2(full);
|
|
392
|
+
} catch {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (st.isDirectory()) findPs1Files(full, acc);
|
|
396
|
+
else if (e.endsWith(".ps1")) acc.push(full);
|
|
397
|
+
}
|
|
398
|
+
return acc;
|
|
399
|
+
}
|
|
400
|
+
async function run4(ctx) {
|
|
401
|
+
const t0 = Date.now();
|
|
402
|
+
const files = findPs1Files(ctx.repoRoot);
|
|
403
|
+
if (files.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
name: "psscriptanalyzer",
|
|
406
|
+
status: "PASS",
|
|
407
|
+
message: "No PowerShell scripts found",
|
|
408
|
+
details: [],
|
|
409
|
+
durationMs: Date.now() - t0
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const which = spawnSync4("pwsh", ["--version"], { encoding: "utf8" });
|
|
413
|
+
if (which.status === null || which.error) {
|
|
414
|
+
const isWin2 = platform() === "win32";
|
|
415
|
+
return {
|
|
416
|
+
name: "psscriptanalyzer",
|
|
417
|
+
status: isWin2 ? "FAIL" : "WARN",
|
|
418
|
+
message: isWin2 ? `pwsh not found -- ${files.length} .ps1 file(s) unchecked (install PowerShell)` : `pwsh not installed -- PSScriptAnalyzer skipped on non-Windows (runs in CI on windows-latest)`,
|
|
419
|
+
details: [`Files that would be checked: ${files.join(", ")}`],
|
|
420
|
+
durationMs: Date.now() - t0
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const script = `
|
|
424
|
+
$files = @(${files.map((f) => `'${f.replace(/'/g, "''")}'`).join(",")})
|
|
425
|
+
$found = $false
|
|
426
|
+
foreach ($f in $files) {
|
|
427
|
+
$results = Invoke-ScriptAnalyzer -Path $f -Severity Warning -ErrorAction SilentlyContinue
|
|
428
|
+
if ($results) {
|
|
429
|
+
$found = $true
|
|
430
|
+
foreach ($r in $results) {
|
|
431
|
+
Write-Output "$($r.ScriptName):$($r.Line): [$($r.Severity)] $($r.RuleName) -- $($r.Message)"
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if ($found) { exit 1 } else { exit 0 }
|
|
436
|
+
`;
|
|
437
|
+
const res = spawnSync4("pwsh", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
cwd: ctx.repoRoot,
|
|
440
|
+
timeout: 3e4
|
|
441
|
+
});
|
|
442
|
+
const durationMs = Date.now() - t0;
|
|
443
|
+
if (res.status === 0) {
|
|
444
|
+
return {
|
|
445
|
+
name: "psscriptanalyzer",
|
|
446
|
+
status: "PASS",
|
|
447
|
+
message: `${files.length} PowerShell script(s) clean`,
|
|
448
|
+
details: [],
|
|
449
|
+
durationMs
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const lines = ((res.stdout || "") + (res.stderr || "")).split("\n").filter(Boolean);
|
|
453
|
+
const isWin = platform() === "win32";
|
|
454
|
+
return {
|
|
455
|
+
name: "psscriptanalyzer",
|
|
456
|
+
status: isWin ? "FAIL" : "WARN",
|
|
457
|
+
message: `PSScriptAnalyzer found issues in ${files.length} script(s)`,
|
|
458
|
+
details: lines.slice(0, 20),
|
|
459
|
+
durationMs
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
var name4, severity4, parallel4;
|
|
463
|
+
var init_psscriptanalyzer = __esm({
|
|
464
|
+
"src/preflight/gates/psscriptanalyzer.js"() {
|
|
465
|
+
name4 = "psscriptanalyzer";
|
|
466
|
+
severity4 = "blocking";
|
|
467
|
+
parallel4 = true;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/preflight/gates/publint.js
|
|
472
|
+
var publint_exports = {};
|
|
473
|
+
__export(publint_exports, {
|
|
474
|
+
name: () => name5,
|
|
475
|
+
parallel: () => parallel5,
|
|
476
|
+
run: () => run5,
|
|
477
|
+
severity: () => severity5
|
|
478
|
+
});
|
|
479
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
480
|
+
async function run5(ctx) {
|
|
481
|
+
const t0 = Date.now();
|
|
482
|
+
const ver = ctx.versions["publint"] || "latest";
|
|
483
|
+
const res = spawnSync5(
|
|
484
|
+
"npx",
|
|
485
|
+
["--yes", `publint@${ver}`, "--strict"],
|
|
486
|
+
{ encoding: "utf8", cwd: ctx.repoRoot + "/installer", timeout: 3e4 }
|
|
487
|
+
);
|
|
488
|
+
const durationMs = Date.now() - t0;
|
|
489
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
490
|
+
const lines = output.split("\n").filter(Boolean);
|
|
491
|
+
if (res.status === 0) {
|
|
492
|
+
return {
|
|
493
|
+
name: "publint",
|
|
494
|
+
status: "PASS",
|
|
495
|
+
message: "publint: package.json integrity verified",
|
|
496
|
+
details: [],
|
|
497
|
+
durationMs
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name: "publint",
|
|
502
|
+
status: "FAIL",
|
|
503
|
+
message: "publint: package.json issues found",
|
|
504
|
+
details: lines.slice(0, 20),
|
|
505
|
+
durationMs
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
var name5, severity5, parallel5;
|
|
509
|
+
var init_publint = __esm({
|
|
510
|
+
"src/preflight/gates/publint.js"() {
|
|
511
|
+
name5 = "publint";
|
|
512
|
+
severity5 = "blocking";
|
|
513
|
+
parallel5 = true;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// src/preflight/gates/gitleaks.js
|
|
518
|
+
var gitleaks_exports = {};
|
|
519
|
+
__export(gitleaks_exports, {
|
|
520
|
+
name: () => name6,
|
|
521
|
+
parallel: () => parallel6,
|
|
522
|
+
run: () => run6,
|
|
523
|
+
severity: () => severity6
|
|
524
|
+
});
|
|
525
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
526
|
+
async function run6(ctx) {
|
|
527
|
+
const t0 = Date.now();
|
|
528
|
+
const which = spawnSync6("gitleaks", ["version"], { encoding: "utf8" });
|
|
529
|
+
if (which.status === null || which.error) {
|
|
530
|
+
return {
|
|
531
|
+
name: "gitleaks",
|
|
532
|
+
status: "WARN",
|
|
533
|
+
message: "gitleaks not installed -- brew install gitleaks / https://github.com/gitleaks/gitleaks",
|
|
534
|
+
details: ["Secret scan skipped. Install gitleaks to enable this gate."],
|
|
535
|
+
durationMs: Date.now() - t0
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const res = spawnSync6(
|
|
539
|
+
"gitleaks",
|
|
540
|
+
["detect", "--no-git", "--source", ctx.repoRoot, "--gitleaks-ignore-path", ".gitleaksignore", "-v", "--exit-code", "1"],
|
|
541
|
+
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 3e4 }
|
|
542
|
+
);
|
|
543
|
+
const durationMs = Date.now() - t0;
|
|
544
|
+
if (res.status === 0) {
|
|
545
|
+
return {
|
|
546
|
+
name: "gitleaks",
|
|
547
|
+
status: "PASS",
|
|
548
|
+
message: "gitleaks: no secrets detected",
|
|
549
|
+
details: [],
|
|
550
|
+
durationMs
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const lines = ((res.stdout || "") + (res.stderr || "")).split("\n").filter(Boolean);
|
|
554
|
+
return {
|
|
555
|
+
name: "gitleaks",
|
|
556
|
+
status: "FAIL",
|
|
557
|
+
message: "gitleaks: potential secrets detected",
|
|
558
|
+
details: lines.slice(0, 30),
|
|
559
|
+
durationMs
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
var name6, severity6, parallel6;
|
|
563
|
+
var init_gitleaks = __esm({
|
|
564
|
+
"src/preflight/gates/gitleaks.js"() {
|
|
565
|
+
name6 = "gitleaks";
|
|
566
|
+
severity6 = "blocking";
|
|
567
|
+
parallel6 = true;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// src/preflight/gates/audit-ci.js
|
|
572
|
+
var audit_ci_exports = {};
|
|
573
|
+
__export(audit_ci_exports, {
|
|
574
|
+
name: () => name7,
|
|
575
|
+
parallel: () => parallel7,
|
|
576
|
+
run: () => run7,
|
|
577
|
+
severity: () => severity7
|
|
578
|
+
});
|
|
579
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
580
|
+
import { join as join4 } from "node:path";
|
|
581
|
+
async function run7(ctx) {
|
|
582
|
+
const t0 = Date.now();
|
|
583
|
+
const ver = ctx.versions["audit-ci"] || "latest";
|
|
584
|
+
const configPath = join4(ctx.repoRoot, ".audit-ci.jsonc");
|
|
585
|
+
const res = spawnSync7(
|
|
586
|
+
"npx",
|
|
587
|
+
["--yes", `audit-ci@${ver}`, "--config", configPath],
|
|
588
|
+
{
|
|
589
|
+
encoding: "utf8",
|
|
590
|
+
cwd: join4(ctx.repoRoot, "installer"),
|
|
591
|
+
timeout: 6e4
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
const durationMs = Date.now() - t0;
|
|
595
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
596
|
+
const lines = output.split("\n").filter(Boolean);
|
|
597
|
+
if (res.status === 0) {
|
|
598
|
+
return {
|
|
599
|
+
name: "audit-ci",
|
|
600
|
+
status: "PASS",
|
|
601
|
+
message: "audit-ci: no high/critical vulnerabilities",
|
|
602
|
+
details: [],
|
|
603
|
+
durationMs
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
name: "audit-ci",
|
|
608
|
+
status: "FAIL",
|
|
609
|
+
message: "audit-ci: high or critical vulnerabilities found",
|
|
610
|
+
details: lines.slice(0, 20),
|
|
611
|
+
durationMs
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
var name7, severity7, parallel7;
|
|
615
|
+
var init_audit_ci = __esm({
|
|
616
|
+
"src/preflight/gates/audit-ci.js"() {
|
|
617
|
+
name7 = "audit-ci";
|
|
618
|
+
severity7 = "blocking";
|
|
619
|
+
parallel7 = true;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// src/preflight/gates/knip.js
|
|
624
|
+
var knip_exports = {};
|
|
625
|
+
__export(knip_exports, {
|
|
626
|
+
name: () => name8,
|
|
627
|
+
parallel: () => parallel8,
|
|
628
|
+
run: () => run8,
|
|
629
|
+
severity: () => severity8
|
|
630
|
+
});
|
|
631
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
632
|
+
async function run8(ctx) {
|
|
633
|
+
const t0 = Date.now();
|
|
634
|
+
const ver = ctx.versions["knip"] || "latest";
|
|
635
|
+
const res = spawnSync8(
|
|
636
|
+
"npx",
|
|
637
|
+
["--yes", `knip@${ver}`, "--production"],
|
|
638
|
+
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 6e4 }
|
|
639
|
+
);
|
|
640
|
+
const durationMs = Date.now() - t0;
|
|
641
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
642
|
+
const lines = output.split("\n").filter(Boolean);
|
|
643
|
+
if (res.status === 0) {
|
|
644
|
+
return {
|
|
645
|
+
name: "knip",
|
|
646
|
+
status: "PASS",
|
|
647
|
+
message: "knip: no unused exports or dead code",
|
|
648
|
+
details: [],
|
|
649
|
+
durationMs
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
name: "knip",
|
|
654
|
+
status: "WARN",
|
|
655
|
+
message: "knip: unused code detected (advisory)",
|
|
656
|
+
details: lines.slice(0, 20),
|
|
657
|
+
durationMs
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
var name8, severity8, parallel8;
|
|
661
|
+
var init_knip = __esm({
|
|
662
|
+
"src/preflight/gates/knip.js"() {
|
|
663
|
+
name8 = "knip";
|
|
664
|
+
severity8 = "warn";
|
|
665
|
+
parallel8 = true;
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// src/preflight/gates/license-check.js
|
|
670
|
+
var license_check_exports = {};
|
|
671
|
+
__export(license_check_exports, {
|
|
672
|
+
name: () => name9,
|
|
673
|
+
parallel: () => parallel9,
|
|
674
|
+
run: () => run9,
|
|
675
|
+
severity: () => severity9
|
|
676
|
+
});
|
|
677
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
678
|
+
import { join as join5 } from "node:path";
|
|
679
|
+
async function run9(ctx) {
|
|
680
|
+
const t0 = Date.now();
|
|
681
|
+
const ver = ctx.versions["license-checker"] || "latest";
|
|
682
|
+
const res = spawnSync9(
|
|
683
|
+
"npx",
|
|
684
|
+
["--yes", `license-checker@${ver}`, "--onlyAllow", ALLOWED, "--production"],
|
|
685
|
+
{
|
|
686
|
+
encoding: "utf8",
|
|
687
|
+
cwd: join5(ctx.repoRoot, "installer"),
|
|
688
|
+
timeout: 3e4
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
const durationMs = Date.now() - t0;
|
|
692
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
693
|
+
const lines = output.split("\n").filter(Boolean);
|
|
694
|
+
if (res.status === 0) {
|
|
695
|
+
return {
|
|
696
|
+
name: "license-check",
|
|
697
|
+
status: "PASS",
|
|
698
|
+
message: "license-check: all production deps use approved licenses",
|
|
699
|
+
details: [],
|
|
700
|
+
durationMs
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
name: "license-check",
|
|
705
|
+
status: "WARN",
|
|
706
|
+
message: "license-check: unexpected license(s) in production deps (advisory)",
|
|
707
|
+
details: lines.slice(0, 20),
|
|
708
|
+
durationMs
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
var ALLOWED, name9, severity9, parallel9;
|
|
712
|
+
var init_license_check = __esm({
|
|
713
|
+
"src/preflight/gates/license-check.js"() {
|
|
714
|
+
ALLOWED = "MIT;ISC;BSD-2-Clause;BSD-3-Clause;Apache-2.0;CC0-1.0;Unlicense;0BSD;Python-2.0;BlueOak-1.0.0";
|
|
715
|
+
name9 = "license-check";
|
|
716
|
+
severity9 = "warn";
|
|
717
|
+
parallel9 = true;
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// src/preflight/gates/pack-smoke.js
|
|
722
|
+
var pack_smoke_exports = {};
|
|
723
|
+
__export(pack_smoke_exports, {
|
|
724
|
+
name: () => name10,
|
|
725
|
+
parallel: () => parallel10,
|
|
726
|
+
run: () => run10,
|
|
727
|
+
severity: () => severity10
|
|
728
|
+
});
|
|
729
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
730
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
731
|
+
import { join as join6, resolve } from "node:path";
|
|
732
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
733
|
+
async function run10(ctx) {
|
|
734
|
+
const t0 = Date.now();
|
|
735
|
+
const installerDir = join6(ctx.repoRoot, "installer");
|
|
736
|
+
const build = spawnSync10("npm", ["run", "build"], {
|
|
737
|
+
encoding: "utf8",
|
|
738
|
+
cwd: installerDir,
|
|
739
|
+
timeout: 6e4
|
|
740
|
+
});
|
|
741
|
+
if (build.status !== 0) {
|
|
742
|
+
return {
|
|
743
|
+
name: "pack-smoke",
|
|
744
|
+
status: "FAIL",
|
|
745
|
+
message: "pack-smoke: build failed before pack",
|
|
746
|
+
details: ((build.stdout || "") + (build.stderr || "")).split("\n").filter(Boolean).slice(0, 10),
|
|
747
|
+
durationMs: Date.now() - t0
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const pack = spawnSync10("npm", ["pack", "--silent"], {
|
|
751
|
+
encoding: "utf8",
|
|
752
|
+
cwd: installerDir,
|
|
753
|
+
timeout: 3e4
|
|
754
|
+
});
|
|
755
|
+
if (pack.status !== 0) {
|
|
756
|
+
return {
|
|
757
|
+
name: "pack-smoke",
|
|
758
|
+
status: "FAIL",
|
|
759
|
+
message: "pack-smoke: npm pack failed",
|
|
760
|
+
details: ((pack.stdout || "") + (pack.stderr || "")).split("\n").filter(Boolean),
|
|
761
|
+
durationMs: Date.now() - t0
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const tarball = pack.stdout.trim();
|
|
765
|
+
if (!tarball) {
|
|
766
|
+
return {
|
|
767
|
+
name: "pack-smoke",
|
|
768
|
+
status: "FAIL",
|
|
769
|
+
message: "pack-smoke: npm pack produced no output",
|
|
770
|
+
details: [],
|
|
771
|
+
durationMs: Date.now() - t0
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const tarballPath = resolve(installerDir, tarball);
|
|
775
|
+
const tmpRoot = mkdtempSync2(join6(tmpdir2(), "ijfw-pack-smoke-"));
|
|
776
|
+
const fakeHome = join6(tmpRoot, "home");
|
|
777
|
+
const installDir = join6(tmpRoot, "install");
|
|
778
|
+
mkdirSync(fakeHome, { recursive: true });
|
|
779
|
+
mkdirSync(installDir, { recursive: true });
|
|
780
|
+
try {
|
|
781
|
+
writeFileSync2(join6(installDir, "package.json"), JSON.stringify({ name: "smoke-test", version: "1.0.0", type: "module" }));
|
|
782
|
+
const install = spawnSync10("npm", ["install", "--no-save", tarballPath], {
|
|
783
|
+
encoding: "utf8",
|
|
784
|
+
cwd: installDir,
|
|
785
|
+
timeout: 6e4,
|
|
786
|
+
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome }
|
|
787
|
+
});
|
|
788
|
+
if (install.status !== 0) {
|
|
789
|
+
return {
|
|
790
|
+
name: "pack-smoke",
|
|
791
|
+
status: "FAIL",
|
|
792
|
+
message: "pack-smoke: tarball install failed",
|
|
793
|
+
details: ((install.stdout || "") + (install.stderr || "")).split("\n").filter(Boolean).slice(0, 15),
|
|
794
|
+
durationMs: Date.now() - t0
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const binCandidates = [
|
|
798
|
+
join6(installDir, "node_modules", ".bin", "ijfw"),
|
|
799
|
+
join6(installDir, "node_modules", ".bin", "ijfw-install")
|
|
800
|
+
];
|
|
801
|
+
let binPath = null;
|
|
802
|
+
for (const c2 of binCandidates) {
|
|
803
|
+
try {
|
|
804
|
+
const r = spawnSync10("ls", [c2], { encoding: "utf8" });
|
|
805
|
+
if (r.status === 0) {
|
|
806
|
+
binPath = c2;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (!binPath) {
|
|
813
|
+
const binDir = join6(installDir, "node_modules", ".bin");
|
|
814
|
+
let entries = [];
|
|
815
|
+
try {
|
|
816
|
+
entries = readdirSync3(binDir);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
const found = entries.find((e) => e.startsWith("ijfw"));
|
|
820
|
+
if (found) binPath = join6(binDir, found);
|
|
821
|
+
}
|
|
822
|
+
if (!binPath) {
|
|
823
|
+
return {
|
|
824
|
+
name: "pack-smoke",
|
|
825
|
+
status: "FAIL",
|
|
826
|
+
message: "pack-smoke: no ijfw* binary found in installed tarball",
|
|
827
|
+
details: [],
|
|
828
|
+
durationMs: Date.now() - t0
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const helpRun = spawnSync10("node", [binPath, "--help"], {
|
|
832
|
+
encoding: "utf8",
|
|
833
|
+
cwd: installDir,
|
|
834
|
+
timeout: 15e3,
|
|
835
|
+
env: { ...process.env, HOME: fakeHome }
|
|
836
|
+
});
|
|
837
|
+
const durationMs = Date.now() - t0;
|
|
838
|
+
if (helpRun.status === 0) {
|
|
839
|
+
return {
|
|
840
|
+
name: "pack-smoke",
|
|
841
|
+
status: "PASS",
|
|
842
|
+
message: `pack-smoke: tarball installs and binary responds to --help`,
|
|
843
|
+
details: [],
|
|
844
|
+
durationMs
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
name: "pack-smoke",
|
|
849
|
+
status: "FAIL",
|
|
850
|
+
message: "pack-smoke: binary --help exited non-zero",
|
|
851
|
+
details: ((helpRun.stdout || "") + (helpRun.stderr || "")).split("\n").filter(Boolean).slice(0, 15),
|
|
852
|
+
durationMs
|
|
853
|
+
};
|
|
854
|
+
} finally {
|
|
855
|
+
try {
|
|
856
|
+
rmSync2(tmpRoot, { recursive: true, force: true });
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
rmSync2(tarballPath, { force: true });
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
var name10, severity10, parallel10;
|
|
866
|
+
var init_pack_smoke = __esm({
|
|
867
|
+
"src/preflight/gates/pack-smoke.js"() {
|
|
868
|
+
name10 = "pack-smoke";
|
|
869
|
+
severity10 = "blocking";
|
|
870
|
+
parallel10 = false;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// src/preflight/gates/upgrade-smoke.js
|
|
875
|
+
var upgrade_smoke_exports = {};
|
|
876
|
+
__export(upgrade_smoke_exports, {
|
|
877
|
+
name: () => name11,
|
|
878
|
+
parallel: () => parallel11,
|
|
879
|
+
run: () => run11,
|
|
880
|
+
severity: () => severity11
|
|
881
|
+
});
|
|
882
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
883
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync, existsSync } from "node:fs";
|
|
884
|
+
import { join as join7, resolve as resolve2 } from "node:path";
|
|
885
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
886
|
+
async function run11(ctx) {
|
|
887
|
+
const t0 = Date.now();
|
|
888
|
+
const installerDir = join7(ctx.repoRoot, "installer");
|
|
889
|
+
const build = spawnSync11("npm", ["run", "build"], {
|
|
890
|
+
encoding: "utf8",
|
|
891
|
+
cwd: installerDir,
|
|
892
|
+
timeout: 6e4
|
|
893
|
+
});
|
|
894
|
+
if (build.status !== 0) {
|
|
895
|
+
return {
|
|
896
|
+
name: "upgrade-smoke",
|
|
897
|
+
status: "FAIL",
|
|
898
|
+
message: "upgrade-smoke: build failed",
|
|
899
|
+
details: ((build.stdout || "") + (build.stderr || "")).split("\n").filter(Boolean).slice(0, 10),
|
|
900
|
+
durationMs: Date.now() - t0
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const pack = spawnSync11("npm", ["pack", "--silent"], {
|
|
904
|
+
encoding: "utf8",
|
|
905
|
+
cwd: installerDir,
|
|
906
|
+
timeout: 3e4
|
|
907
|
+
});
|
|
908
|
+
if (pack.status !== 0) {
|
|
909
|
+
return {
|
|
910
|
+
name: "upgrade-smoke",
|
|
911
|
+
status: "FAIL",
|
|
912
|
+
message: "upgrade-smoke: npm pack failed",
|
|
913
|
+
details: ((pack.stdout || "") + (pack.stderr || "")).split("\n").filter(Boolean),
|
|
914
|
+
durationMs: Date.now() - t0
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const tarball = pack.stdout.trim();
|
|
918
|
+
const tarballPath = resolve2(installerDir, tarball);
|
|
919
|
+
const tmpRoot = mkdtempSync3(join7(tmpdir3(), "ijfw-upgrade-smoke-"));
|
|
920
|
+
const fakeHome = join7(tmpRoot, "home");
|
|
921
|
+
const installDir = join7(tmpRoot, "install");
|
|
922
|
+
mkdirSync2(fakeHome, { recursive: true });
|
|
923
|
+
mkdirSync2(installDir, { recursive: true });
|
|
924
|
+
const claudeDir = join7(fakeHome, ".claude");
|
|
925
|
+
mkdirSync2(claudeDir, { recursive: true });
|
|
926
|
+
try {
|
|
927
|
+
writeFileSync3(join7(installDir, "package.json"), JSON.stringify({ name: "upgrade-smoke", version: "1.0.0", type: "module" }));
|
|
928
|
+
const install = spawnSync11("npm", ["install", "--no-save", tarballPath], {
|
|
929
|
+
encoding: "utf8",
|
|
930
|
+
cwd: installDir,
|
|
931
|
+
timeout: 6e4,
|
|
932
|
+
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome }
|
|
933
|
+
});
|
|
934
|
+
if (install.status !== 0) {
|
|
935
|
+
return {
|
|
936
|
+
name: "upgrade-smoke",
|
|
937
|
+
status: "FAIL",
|
|
938
|
+
message: "upgrade-smoke: tarball install failed",
|
|
939
|
+
details: ((install.stdout || "") + (install.stderr || "")).split("\n").filter(Boolean).slice(0, 15),
|
|
940
|
+
durationMs: Date.now() - t0
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const binCandidates = [
|
|
944
|
+
join7(installDir, "node_modules", ".bin", "ijfw-install"),
|
|
945
|
+
join7(installDir, "node_modules", ".bin", "ijfw")
|
|
946
|
+
];
|
|
947
|
+
let installerBin = null;
|
|
948
|
+
for (const c2 of binCandidates) {
|
|
949
|
+
const check = spawnSync11("ls", [c2], { encoding: "utf8" });
|
|
950
|
+
if (check.status === 0) {
|
|
951
|
+
installerBin = c2;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (!installerBin) {
|
|
956
|
+
return {
|
|
957
|
+
name: "upgrade-smoke",
|
|
958
|
+
status: "FAIL",
|
|
959
|
+
message: "upgrade-smoke: no installer binary found",
|
|
960
|
+
details: [],
|
|
961
|
+
durationMs: Date.now() - t0
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
const settingsPath = join7(claudeDir, "settings.json");
|
|
965
|
+
if (existsSync(settingsPath)) {
|
|
966
|
+
let settings;
|
|
967
|
+
try {
|
|
968
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
969
|
+
} catch (e) {
|
|
970
|
+
return {
|
|
971
|
+
name: "upgrade-smoke",
|
|
972
|
+
status: "FAIL",
|
|
973
|
+
message: "upgrade-smoke: settings.json is not valid JSON",
|
|
974
|
+
details: [e.message],
|
|
975
|
+
durationMs: Date.now() - t0
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const hasWrongKey = JSON.stringify(settings).includes("ijfw-core");
|
|
979
|
+
if (hasWrongKey) {
|
|
980
|
+
return {
|
|
981
|
+
name: "upgrade-smoke",
|
|
982
|
+
status: "FAIL",
|
|
983
|
+
message: 'upgrade-smoke: settings.json still uses deprecated "ijfw-core" key',
|
|
984
|
+
details: [`Found "ijfw-core" in: ${settingsPath}`],
|
|
985
|
+
durationMs: Date.now() - t0
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const marketplaceSrc = join7(installerDir, "src", "marketplace.js");
|
|
990
|
+
if (existsSync(marketplaceSrc)) {
|
|
991
|
+
const src = readFileSync(marketplaceSrc, "utf8");
|
|
992
|
+
const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
|
|
993
|
+
const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
|
|
994
|
+
if (!registersCorrectKey) {
|
|
995
|
+
return {
|
|
996
|
+
name: "upgrade-smoke",
|
|
997
|
+
status: "FAIL",
|
|
998
|
+
message: 'upgrade-smoke: marketplace.js does not register "ijfw@ijfw" plugin key',
|
|
999
|
+
details: ['Fix: add enabledPlugins["ijfw@ijfw"] = true in marketplace.js'],
|
|
1000
|
+
durationMs: Date.now() - t0
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
if (registersWrongKey) {
|
|
1004
|
+
return {
|
|
1005
|
+
name: "upgrade-smoke",
|
|
1006
|
+
status: "FAIL",
|
|
1007
|
+
message: 'upgrade-smoke: marketplace.js still registers deprecated "ijfw-core@ijfw" key as active',
|
|
1008
|
+
details: ['Fix: change active registration to "ijfw@ijfw" in marketplace.js'],
|
|
1009
|
+
durationMs: Date.now() - t0
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const durationMs = Date.now() - t0;
|
|
1014
|
+
return {
|
|
1015
|
+
name: "upgrade-smoke",
|
|
1016
|
+
status: "PASS",
|
|
1017
|
+
message: "upgrade-smoke: plugin key and settings wiring verified",
|
|
1018
|
+
details: [],
|
|
1019
|
+
durationMs
|
|
1020
|
+
};
|
|
1021
|
+
} finally {
|
|
1022
|
+
try {
|
|
1023
|
+
rmSync3(tmpRoot, { recursive: true, force: true });
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
rmSync3(tarballPath, { force: true });
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
var name11, severity11, parallel11;
|
|
1033
|
+
var init_upgrade_smoke = __esm({
|
|
1034
|
+
"src/preflight/gates/upgrade-smoke.js"() {
|
|
1035
|
+
name11 = "upgrade-smoke";
|
|
1036
|
+
severity11 = "blocking";
|
|
1037
|
+
parallel11 = false;
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// src/preflight.js
|
|
1042
|
+
var preflight_exports = {};
|
|
1043
|
+
__export(preflight_exports, {
|
|
1044
|
+
runPreflightCommand: () => runPreflightCommand
|
|
1045
|
+
});
|
|
1046
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
1047
|
+
import { join as join8, dirname } from "node:path";
|
|
1048
|
+
import { fileURLToPath } from "node:url";
|
|
1049
|
+
function printHelp() {
|
|
1050
|
+
console.log(`
|
|
1051
|
+
ijfw preflight -- 11-gate quality pipeline
|
|
1052
|
+
|
|
1053
|
+
USAGE
|
|
1054
|
+
ijfw preflight [options]
|
|
1055
|
+
|
|
1056
|
+
OPTIONS
|
|
1057
|
+
--json Emit machine-readable JSON (for CI consumption)
|
|
1058
|
+
--fail-fast Stop after the first blocking gate failure
|
|
1059
|
+
--help, -h Show this help
|
|
1060
|
+
|
|
1061
|
+
GATES (in execution order)
|
|
1062
|
+
1. shellcheck Shell lint (POSIX, unbound vars) [blocking]
|
|
1063
|
+
2. oxlint JS/TS fast lint [blocking]
|
|
1064
|
+
3. eslint-security Security-focused ESLint rules [blocking]
|
|
1065
|
+
4. psscriptanalyzer PowerShell lint (CI: windows-latest) [blocking]
|
|
1066
|
+
5. publint package.json bin/exports integrity [blocking]
|
|
1067
|
+
6. gitleaks Secret scan [blocking]
|
|
1068
|
+
7. audit-ci npm audit, fails on high+ [blocking]
|
|
1069
|
+
8. knip Dead code detection [advisory]
|
|
1070
|
+
9. license-check Production dep license check [advisory]
|
|
1071
|
+
10. pack-smoke npm pack -> install -> binary --help [blocking]
|
|
1072
|
+
11. upgrade-smoke Plugin-key wiring verification [blocking]
|
|
1073
|
+
|
|
1074
|
+
EXIT CODES
|
|
1075
|
+
0 All blocking gates passed (advisory warnings may exist)
|
|
1076
|
+
1 One or more blocking gates failed
|
|
1077
|
+
|
|
1078
|
+
SLO
|
|
1079
|
+
Warm cache: <=90s Cold cache: <=240s (both printed at end)
|
|
1080
|
+
`);
|
|
1081
|
+
}
|
|
1082
|
+
function loadVersions(repoRoot2) {
|
|
1083
|
+
const candidates = [
|
|
1084
|
+
join8(repoRoot2, "preflight-versions.json"),
|
|
1085
|
+
join8(repoRoot2, ".ijfw", "preflight-versions.json")
|
|
1086
|
+
];
|
|
1087
|
+
for (const f of candidates) {
|
|
1088
|
+
if (existsSync2(f)) {
|
|
1089
|
+
try {
|
|
1090
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return {};
|
|
1096
|
+
}
|
|
1097
|
+
function parseArgs(argv) {
|
|
1098
|
+
const out = { json: false, failFast: false, help: false };
|
|
1099
|
+
for (const a of argv.slice(2)) {
|
|
1100
|
+
if (a === "--json") out.json = true;
|
|
1101
|
+
else if (a === "--fail-fast") out.failFast = true;
|
|
1102
|
+
else if (a === "--help" || a === "-h") out.help = true;
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
async function runPreflightCommand(argv, repoRoot2) {
|
|
1107
|
+
const args = parseArgs(argv);
|
|
1108
|
+
if (args.help) {
|
|
1109
|
+
printHelp();
|
|
1110
|
+
process.exit(0);
|
|
1111
|
+
}
|
|
1112
|
+
const versions = loadVersions(repoRoot2);
|
|
1113
|
+
const ctx = {
|
|
1114
|
+
repoRoot: repoRoot2,
|
|
1115
|
+
versions,
|
|
1116
|
+
json: args.json,
|
|
1117
|
+
failFast: args.failFast
|
|
1118
|
+
};
|
|
1119
|
+
const shellcheck = await Promise.resolve().then(() => (init_shellcheck(), shellcheck_exports));
|
|
1120
|
+
const oxlint = await Promise.resolve().then(() => (init_oxlint(), oxlint_exports));
|
|
1121
|
+
const eslintSecurity = await Promise.resolve().then(() => (init_eslint_security(), eslint_security_exports));
|
|
1122
|
+
const psscriptanalyzer = await Promise.resolve().then(() => (init_psscriptanalyzer(), psscriptanalyzer_exports));
|
|
1123
|
+
const publint = await Promise.resolve().then(() => (init_publint(), publint_exports));
|
|
1124
|
+
const gitleaks = await Promise.resolve().then(() => (init_gitleaks(), gitleaks_exports));
|
|
1125
|
+
const auditCi = await Promise.resolve().then(() => (init_audit_ci(), audit_ci_exports));
|
|
1126
|
+
const knip = await Promise.resolve().then(() => (init_knip(), knip_exports));
|
|
1127
|
+
const licenseCheck = await Promise.resolve().then(() => (init_license_check(), license_check_exports));
|
|
1128
|
+
const packSmoke = await Promise.resolve().then(() => (init_pack_smoke(), pack_smoke_exports));
|
|
1129
|
+
const upgradeSmoke = await Promise.resolve().then(() => (init_upgrade_smoke(), upgrade_smoke_exports));
|
|
1130
|
+
const gates = [
|
|
1131
|
+
shellcheck,
|
|
1132
|
+
oxlint,
|
|
1133
|
+
eslintSecurity,
|
|
1134
|
+
psscriptanalyzer,
|
|
1135
|
+
publint,
|
|
1136
|
+
gitleaks,
|
|
1137
|
+
auditCi,
|
|
1138
|
+
knip,
|
|
1139
|
+
licenseCheck,
|
|
1140
|
+
packSmoke,
|
|
1141
|
+
upgradeSmoke
|
|
1142
|
+
];
|
|
1143
|
+
const report = await runPreflight(gates, ctx);
|
|
1144
|
+
process.exit(report.outcome === "pass" ? 0 : 1);
|
|
1145
|
+
}
|
|
1146
|
+
var __dirname;
|
|
1147
|
+
var init_preflight = __esm({
|
|
1148
|
+
"src/preflight.js"() {
|
|
1149
|
+
init_runner();
|
|
1150
|
+
__dirname = dirname(fileURLToPath(import.meta.url));
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// src/ijfw.js
|
|
1155
|
+
import { dirname as dirname2, join as join9, resolve as resolve3, basename } from "node:path";
|
|
1156
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1157
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
|
|
1158
|
+
import { homedir } from "node:os";
|
|
1159
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
1160
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1161
|
+
function repoRoot() {
|
|
1162
|
+
let dir = __dirname2;
|
|
1163
|
+
for (let i = 0; i < 6; i++) {
|
|
1164
|
+
if (existsSync3(join9(dir, "package.json")) && existsSync3(join9(dir, ".git"))) return dir;
|
|
1165
|
+
dir = resolve3(dir, "..");
|
|
1166
|
+
}
|
|
1167
|
+
return process.cwd();
|
|
1168
|
+
}
|
|
1169
|
+
function printHelp2() {
|
|
1170
|
+
console.log(`
|
|
1171
|
+
ijfw -- the AI efficiency layer
|
|
1172
|
+
|
|
1173
|
+
USAGE
|
|
1174
|
+
ijfw <command> [options]
|
|
1175
|
+
|
|
1176
|
+
COMMANDS
|
|
1177
|
+
install Install IJFW into your AI coding agents
|
|
1178
|
+
uninstall Remove IJFW from your AI coding agents
|
|
1179
|
+
preflight Run 11-gate quality pipeline before publishing
|
|
1180
|
+
dashboard Start / stop / check the local observability dashboard
|
|
1181
|
+
design Manage the visual design companion
|
|
1182
|
+
doctor Diagnose IJFW installation health
|
|
1183
|
+
|
|
1184
|
+
--help, -h Show this help
|
|
1185
|
+
--version Show version
|
|
1186
|
+
`);
|
|
1187
|
+
}
|
|
1188
|
+
function doctorCheck(cmd, args) {
|
|
1189
|
+
const r = spawnSync12(cmd, args, { encoding: "utf8" });
|
|
1190
|
+
return r.status === 0 ? r.stdout.split("\n")[0].trim() : "not found";
|
|
1191
|
+
}
|
|
1192
|
+
async function main() {
|
|
1193
|
+
const argv = process.argv;
|
|
1194
|
+
const sub = argv[2];
|
|
1195
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
1196
|
+
printHelp2();
|
|
1197
|
+
process.exit(0);
|
|
1198
|
+
}
|
|
1199
|
+
if (sub === "--version" || sub === "-v") {
|
|
1200
|
+
try {
|
|
1201
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
1202
|
+
const pkgPath = join9(__dirname2, "..", "package.json");
|
|
1203
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1204
|
+
console.log(pkg.version || "unknown");
|
|
1205
|
+
} catch {
|
|
1206
|
+
console.log("unknown");
|
|
1207
|
+
}
|
|
1208
|
+
process.exit(0);
|
|
1209
|
+
}
|
|
1210
|
+
switch (sub) {
|
|
1211
|
+
case "install": {
|
|
1212
|
+
const installBin = resolve3(__dirname2, "..", "dist", "install.js");
|
|
1213
|
+
const r = spawnSync12("node", [installBin, ...argv.slice(3)], { stdio: "inherit" });
|
|
1214
|
+
process.exit(r.status ?? 1);
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
case "uninstall": {
|
|
1218
|
+
const uninstallBin = resolve3(__dirname2, "..", "dist", "uninstall.js");
|
|
1219
|
+
const r = spawnSync12("node", [uninstallBin, ...argv.slice(3)], { stdio: "inherit" });
|
|
1220
|
+
process.exit(r.status ?? 1);
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
case "preflight": {
|
|
1224
|
+
const { runPreflightCommand: runPreflightCommand2 } = await Promise.resolve().then(() => (init_preflight(), preflight_exports));
|
|
1225
|
+
await runPreflightCommand2([argv[0], argv[1], ...argv.slice(3)], repoRoot());
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
case "dashboard": {
|
|
1229
|
+
const dashSub = argv[3];
|
|
1230
|
+
const root = repoRoot();
|
|
1231
|
+
if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
|
|
1232
|
+
const dashBin = join9(root, "mcp-server", "bin", "ijfw-dashboard");
|
|
1233
|
+
if (existsSync3(dashBin)) {
|
|
1234
|
+
const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
|
|
1235
|
+
process.exit(r.status ?? 0);
|
|
1236
|
+
} else {
|
|
1237
|
+
const serverJs = join9(root, "mcp-server", "src", "dashboard-server.js");
|
|
1238
|
+
if (dashSub === "start" && existsSync3(serverJs)) {
|
|
1239
|
+
const { spawn } = await import("node:child_process");
|
|
1240
|
+
const child = spawn(process.execPath, [serverJs, "--daemon"], {
|
|
1241
|
+
detached: true,
|
|
1242
|
+
stdio: "ignore"
|
|
1243
|
+
});
|
|
1244
|
+
child.unref();
|
|
1245
|
+
console.log("Dashboard starting... (check: ijfw dashboard status)");
|
|
1246
|
+
process.exit(0);
|
|
1247
|
+
}
|
|
1248
|
+
console.log("[ijfw] Dashboard bin not found. Run from the IJFW repo root.");
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
} else if (dashSub === "render" || !dashSub) {
|
|
1252
|
+
const binJs = join9(root, "scripts", "dashboard", "bin.js");
|
|
1253
|
+
if (existsSync3(binJs)) {
|
|
1254
|
+
const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
|
|
1255
|
+
process.exit(r.status ?? 0);
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log("[ijfw] Run `ijfw dashboard start` to launch the web dashboard.");
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
console.log("Usage: ijfw dashboard <start|stop|status|render>");
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
case "design": {
|
|
1267
|
+
const designSub = argv[3];
|
|
1268
|
+
const contentDir = join9(homedir(), ".ijfw", "design-companion", "content");
|
|
1269
|
+
mkdirSync3(contentDir, { recursive: true });
|
|
1270
|
+
if (designSub === "push") {
|
|
1271
|
+
const filePath = argv[4];
|
|
1272
|
+
if (!filePath) {
|
|
1273
|
+
console.error("Usage: ijfw design push <file.html>");
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
const abs = resolve3(filePath);
|
|
1277
|
+
if (!existsSync3(abs)) {
|
|
1278
|
+
console.error(`File not found: ${abs}`);
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
const dest = join9(contentDir, basename(abs));
|
|
1282
|
+
copyFileSync(abs, dest);
|
|
1283
|
+
console.log(`Design pushed: ${dest}`);
|
|
1284
|
+
} else if (designSub === "clear") {
|
|
1285
|
+
const files = readdirSync4(contentDir);
|
|
1286
|
+
for (const f of files) rmSync4(join9(contentDir, f), { force: true });
|
|
1287
|
+
console.log("Design companion content cleared.");
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log("ijfw design -- Manage the visual design companion. Push HTML mockups for live preview.");
|
|
1290
|
+
console.log("");
|
|
1291
|
+
console.log("Usage: ijfw design push <file.html> | ijfw design clear");
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
case "doctor": {
|
|
1297
|
+
console.log("\nijfw doctor\n");
|
|
1298
|
+
console.log(" node: " + doctorCheck("node", ["--version"]));
|
|
1299
|
+
console.log(" git: " + doctorCheck("git", ["--version"]));
|
|
1300
|
+
console.log(" shellcheck: " + doctorCheck("shellcheck", ["--version"]));
|
|
1301
|
+
console.log(" gitleaks: " + doctorCheck("gitleaks", ["version"]));
|
|
1302
|
+
console.log("");
|
|
1303
|
+
process.exit(0);
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
default: {
|
|
1307
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
1308
|
+
printHelp2();
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
main().catch((e) => {
|
|
1314
|
+
console.error(e.message || e);
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
});
|