@nebulord/sickbay 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/dist/DiffApp-YF2PYQZK.js +187 -0
- package/dist/DoctorApp-U465IMK7.js +447 -0
- package/dist/FixApp-RJPCWNXJ.js +344 -0
- package/dist/StatsApp-BI6COY7S.js +375 -0
- package/dist/TrendApp-XL77HKDR.js +118 -0
- package/dist/TuiApp-VJNV4FD3.js +982 -0
- package/dist/ai-7DGOLNJX.js +64 -0
- package/dist/badge-KQ73KEIN.js +41 -0
- package/dist/chunk-5KJOYSVJ.js +95 -0
- package/dist/chunk-BIK4EL4H.js +19 -0
- package/dist/chunk-BUD5BE6U.js +61 -0
- package/dist/chunk-D24FSOW4.js +22 -0
- package/dist/chunk-POUHUMJN.js +21 -0
- package/dist/chunk-SSUXSMGH.js +25 -0
- package/dist/history-DYFJ65XH.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +535 -0
- package/dist/init-J2NPRPDO.js +54 -0
- package/dist/resolve-package-PHPJWOLY.js +8 -0
- package/dist/web-EE2VYPEX.js +198 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
LOADING_MESSAGES
|
|
4
|
+
} from "./chunk-POUHUMJN.js";
|
|
5
|
+
import {
|
|
6
|
+
ProgressList
|
|
7
|
+
} from "./chunk-D24FSOW4.js";
|
|
8
|
+
import {
|
|
9
|
+
Header
|
|
10
|
+
} from "./chunk-BIK4EL4H.js";
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
import { config } from "dotenv";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { Command } from "commander";
|
|
18
|
+
import { render } from "ink";
|
|
19
|
+
import React6 from "react";
|
|
20
|
+
|
|
21
|
+
// src/components/App.tsx
|
|
22
|
+
import React5, { useState, useEffect, useRef } from "react";
|
|
23
|
+
import { Box as Box5, Text as Text5, useApp } from "ink";
|
|
24
|
+
import Spinner from "ink-spinner";
|
|
25
|
+
import Gradient from "ink-gradient";
|
|
26
|
+
import { runSickbay, runSickbayMonorepo } from "@nebulord/sickbay-core";
|
|
27
|
+
|
|
28
|
+
// src/components/CheckResult.tsx
|
|
29
|
+
import React2 from "react";
|
|
30
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
31
|
+
|
|
32
|
+
// src/components/ScoreBar.tsx
|
|
33
|
+
import React from "react";
|
|
34
|
+
import { Box, Text } from "ink";
|
|
35
|
+
function ScoreBar({ score, width = 20 }) {
|
|
36
|
+
const filled = Math.round(score / 100 * width);
|
|
37
|
+
const empty = width - filled;
|
|
38
|
+
const color = score >= 80 ? "green" : score >= 60 ? "yellow" : "red";
|
|
39
|
+
return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color }, "\u2588".repeat(filled)), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2591".repeat(empty)), /* @__PURE__ */ React.createElement(Text, { color }, " ", score, "/100"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/components/CheckResult.tsx
|
|
43
|
+
var STATUS_ICONS = {
|
|
44
|
+
pass: "\u2713",
|
|
45
|
+
warning: "\u26A0",
|
|
46
|
+
fail: "\u2717",
|
|
47
|
+
skipped: "\u25CB"
|
|
48
|
+
};
|
|
49
|
+
var CATEGORY_ICONS = {
|
|
50
|
+
dependencies: "\u{1F4E6}",
|
|
51
|
+
security: "\u2714",
|
|
52
|
+
"code-quality": "\u2714",
|
|
53
|
+
performance: "\u26A1",
|
|
54
|
+
git: "\u2714"
|
|
55
|
+
};
|
|
56
|
+
function CheckResultRow({ result }) {
|
|
57
|
+
const statusColor = result.status === "pass" ? "green" : result.status === "fail" ? "red" : result.status === "skipped" ? "gray" : "yellow";
|
|
58
|
+
const icon = CATEGORY_ICONS[result.category] ?? "\u2022";
|
|
59
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, icon, " "), /* @__PURE__ */ React2.createElement(Text2, { bold: true }, result.name), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " via ", result.toolsUsed.join(", "))), /* @__PURE__ */ React2.createElement(Box2, { marginLeft: 2 }, /* @__PURE__ */ React2.createElement(ScoreBar, { score: result.score, width: 16 }), /* @__PURE__ */ React2.createElement(Text2, { color: statusColor }, " ", STATUS_ICONS[result.status], " ", result.status)), result.issues.slice(0, 3).map((issue) => /* @__PURE__ */ React2.createElement(Box2, { key: `${issue.severity}-${issue.message}`, marginLeft: 2 }, /* @__PURE__ */ React2.createElement(
|
|
60
|
+
Text2,
|
|
61
|
+
{
|
|
62
|
+
color: issue.severity === "critical" ? "red" : issue.severity === "warning" ? "yellow" : "gray"
|
|
63
|
+
},
|
|
64
|
+
issue.severity === "critical" ? " \u2717" : issue.severity === "warning" ? " \u26A0" : " \u2139",
|
|
65
|
+
" ",
|
|
66
|
+
issue.message
|
|
67
|
+
))), result.issues.length > 3 && /* @__PURE__ */ React2.createElement(Box2, { marginLeft: 4 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "... and ", result.issues.length - 3, " more")));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/components/Summary.tsx
|
|
71
|
+
import React3 from "react";
|
|
72
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
73
|
+
import { getScoreEmoji } from "@nebulord/sickbay-core";
|
|
74
|
+
function formatDuration(ms) {
|
|
75
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
76
|
+
const seconds = ms / 1e3;
|
|
77
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
78
|
+
const minutes = Math.floor(seconds / 60);
|
|
79
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
80
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
81
|
+
}
|
|
82
|
+
function Summary({ report, scanDuration }) {
|
|
83
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2501".repeat(52)), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true }, "Overall Health Score: "), /* @__PURE__ */ React3.createElement(ScoreBar, { score: report.overallScore, width: 12 }), /* @__PURE__ */ React3.createElement(Text3, null, " ", getScoreEmoji(report.overallScore)), scanDuration != null && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", formatDuration(scanDuration))), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, " \u2717 ", report.summary.critical, " critical"), /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, " \u26A0 ", report.summary.warnings, " warnings"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, " i ", report.summary.info, " info")), report.quote && /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { italic: true, dimColor: true }, '"', report.quote.text, '"'), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " \u2014 ", report.quote.source)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/components/QuickWins.tsx
|
|
87
|
+
import React4 from "react";
|
|
88
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
89
|
+
function replacePackageManager(cmd, pm) {
|
|
90
|
+
if (pm === "npm") return cmd;
|
|
91
|
+
const install = pm === "pnpm" ? "pnpm add" : pm === "yarn" ? "yarn add" : "bun add";
|
|
92
|
+
const uninstall = pm === "pnpm" ? "pnpm remove" : pm === "yarn" ? "yarn remove" : "bun remove";
|
|
93
|
+
const update = pm === "pnpm" ? "pnpm update" : pm === "yarn" ? "yarn upgrade" : "bun update";
|
|
94
|
+
const auditFix = pm === "pnpm" ? "pnpm audit --fix" : pm === "yarn" ? "yarn npm audit --fix" : "bun audit";
|
|
95
|
+
return cmd.replace(/^npm install(?=\s)/, install).replace(/^npm uninstall(?=\s)/, uninstall).replace(/^npm update(?=\s)/, update).replace(/^npm audit fix/, auditFix);
|
|
96
|
+
}
|
|
97
|
+
function QuickWins({ report }) {
|
|
98
|
+
const pm = report.projectInfo?.packageManager ?? "npm";
|
|
99
|
+
const fixes = report.checks.flatMap((c) => c.issues).filter((i) => i.fix?.command).sort((a, b) => {
|
|
100
|
+
const order = { critical: 0, warning: 1, info: 2 };
|
|
101
|
+
return order[a.severity] - order[b.severity];
|
|
102
|
+
}).slice(0, 5);
|
|
103
|
+
if (fixes.length === 0) return null;
|
|
104
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "\u{1F525} Quick Wins:"), fixes.map((fix) => /* @__PURE__ */ React4.createElement(Box4, { key: `${fix.severity}-${fix.message}`, marginLeft: 2 }, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2192 "), /* @__PURE__ */ React4.createElement(Text4, null, fix.fix.description), fix.fix.command && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, ": ", replacePackageManager(fix.fix.command, pm)))));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/components/App.tsx
|
|
108
|
+
function scoreBar(score, width = 10) {
|
|
109
|
+
const filled = Math.round(score / 100 * width);
|
|
110
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
111
|
+
}
|
|
112
|
+
function formatDuration2(ms) {
|
|
113
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
114
|
+
const seconds = ms / 1e3;
|
|
115
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
116
|
+
const minutes = Math.floor(seconds / 60);
|
|
117
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
118
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
119
|
+
}
|
|
120
|
+
function App({
|
|
121
|
+
projectPath,
|
|
122
|
+
checks,
|
|
123
|
+
openWeb,
|
|
124
|
+
enableAI,
|
|
125
|
+
verbose,
|
|
126
|
+
quotes,
|
|
127
|
+
isMonorepo
|
|
128
|
+
}) {
|
|
129
|
+
const { exit } = useApp();
|
|
130
|
+
const [phase, setPhase] = useState("loading");
|
|
131
|
+
const [report, setReport] = useState(null);
|
|
132
|
+
const [monorepoReport, setMonorepoReport] = useState(null);
|
|
133
|
+
const [error, setError] = useState(null);
|
|
134
|
+
const [progress, setProgress] = useState([]);
|
|
135
|
+
const [webUrl, setWebUrl] = useState(null);
|
|
136
|
+
const [projectName, setProjectName] = useState();
|
|
137
|
+
const [scanningPackage, setScanningPackage] = useState();
|
|
138
|
+
const [loadingMsgIdx, setLoadingMsgIdx] = useState(0);
|
|
139
|
+
const [scanDuration, setScanDuration] = useState(null);
|
|
140
|
+
const hasRun = useRef(false);
|
|
141
|
+
const scanStartTime = useRef(0);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!isMonorepo) return;
|
|
144
|
+
const id = setInterval(() => {
|
|
145
|
+
setLoadingMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length);
|
|
146
|
+
}, 4e3);
|
|
147
|
+
return () => clearInterval(id);
|
|
148
|
+
}, [isMonorepo]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (hasRun.current) return;
|
|
151
|
+
hasRun.current = true;
|
|
152
|
+
scanStartTime.current = Date.now();
|
|
153
|
+
if (isMonorepo) {
|
|
154
|
+
runSickbayMonorepo({
|
|
155
|
+
projectPath,
|
|
156
|
+
checks,
|
|
157
|
+
verbose,
|
|
158
|
+
quotes,
|
|
159
|
+
onPackageStart: (name) => setScanningPackage(name),
|
|
160
|
+
onPackageComplete: () => setScanningPackage(void 0)
|
|
161
|
+
}).then(async (r) => {
|
|
162
|
+
setScanDuration(Date.now() - scanStartTime.current);
|
|
163
|
+
setMonorepoReport(r);
|
|
164
|
+
setProjectName(`monorepo (${r.packages.length} packages)`);
|
|
165
|
+
try {
|
|
166
|
+
const { getDependencyTree } = await import("@nebulord/sickbay-core");
|
|
167
|
+
const { saveDepTree } = await import("./history-DYFJ65XH.js");
|
|
168
|
+
const packages = {};
|
|
169
|
+
for (const pkg of r.packages) {
|
|
170
|
+
packages[pkg.name] = await getDependencyTree(pkg.path, r.packageManager);
|
|
171
|
+
}
|
|
172
|
+
saveDepTree(projectPath, { packages });
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
if (openWeb) {
|
|
176
|
+
setPhase("opening-web");
|
|
177
|
+
try {
|
|
178
|
+
const { serveWeb } = await import("./web-EE2VYPEX.js");
|
|
179
|
+
const { default: openBrowser } = await import("open");
|
|
180
|
+
let aiService;
|
|
181
|
+
if (enableAI && process.env.ANTHROPIC_API_KEY) {
|
|
182
|
+
const { createAIService } = await import("./ai-7DGOLNJX.js");
|
|
183
|
+
aiService = createAIService(process.env.ANTHROPIC_API_KEY);
|
|
184
|
+
}
|
|
185
|
+
const url = await serveWeb(r, 3030, aiService);
|
|
186
|
+
setWebUrl(url);
|
|
187
|
+
await openBrowser(url);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
190
|
+
setPhase("error");
|
|
191
|
+
setTimeout(() => exit(), 100);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
setPhase("results");
|
|
195
|
+
setTimeout(() => exit(), 100);
|
|
196
|
+
}
|
|
197
|
+
}).catch((err) => {
|
|
198
|
+
setError(err.message ?? String(err));
|
|
199
|
+
setPhase("error");
|
|
200
|
+
setTimeout(() => exit(err), 100);
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
runSickbay({
|
|
205
|
+
projectPath,
|
|
206
|
+
checks,
|
|
207
|
+
verbose,
|
|
208
|
+
quotes,
|
|
209
|
+
onRunnersReady: (names) => {
|
|
210
|
+
setProgress(names.map((name) => ({ name, status: "pending" })));
|
|
211
|
+
},
|
|
212
|
+
onCheckStart: (name) => {
|
|
213
|
+
setProgress(
|
|
214
|
+
(prev) => prev.map((p) => p.name === name ? { ...p, status: "running" } : p)
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
onCheckComplete: (result) => {
|
|
218
|
+
if (result.status === "skipped") {
|
|
219
|
+
setProgress((prev) => prev.filter((p) => p.name !== result.id));
|
|
220
|
+
} else {
|
|
221
|
+
setProgress(
|
|
222
|
+
(prev) => prev.map(
|
|
223
|
+
(p) => p.name === result.id ? { ...p, status: "done" } : p
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}).then(async (r) => {
|
|
229
|
+
setScanDuration(Date.now() - scanStartTime.current);
|
|
230
|
+
setProjectName(r.projectInfo.name);
|
|
231
|
+
setReport(r);
|
|
232
|
+
try {
|
|
233
|
+
const { saveEntry, saveLastReport } = await import("./history-DYFJ65XH.js");
|
|
234
|
+
saveEntry(r);
|
|
235
|
+
saveLastReport(r);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const { getDependencyTree } = await import("@nebulord/sickbay-core");
|
|
240
|
+
const { saveDepTree } = await import("./history-DYFJ65XH.js");
|
|
241
|
+
const tree = await getDependencyTree(projectPath, r.projectInfo.packageManager);
|
|
242
|
+
saveDepTree(projectPath, tree);
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
if (openWeb) {
|
|
246
|
+
setPhase("opening-web");
|
|
247
|
+
try {
|
|
248
|
+
const { serveWeb } = await import("./web-EE2VYPEX.js");
|
|
249
|
+
const { default: openBrowser } = await import("open");
|
|
250
|
+
let aiService;
|
|
251
|
+
if (enableAI && process.env.ANTHROPIC_API_KEY) {
|
|
252
|
+
const { createAIService } = await import("./ai-7DGOLNJX.js");
|
|
253
|
+
aiService = createAIService(process.env.ANTHROPIC_API_KEY);
|
|
254
|
+
}
|
|
255
|
+
const url = await serveWeb(r, 3030, aiService);
|
|
256
|
+
setWebUrl(url);
|
|
257
|
+
await openBrowser(url);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
260
|
+
setPhase("error");
|
|
261
|
+
setTimeout(() => exit(), 100);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
setPhase("results");
|
|
266
|
+
setTimeout(() => exit(), 100);
|
|
267
|
+
}
|
|
268
|
+
}).catch((err) => {
|
|
269
|
+
setError(err.message ?? String(err));
|
|
270
|
+
setPhase("error");
|
|
271
|
+
setTimeout(() => exit(err), 100);
|
|
272
|
+
});
|
|
273
|
+
}, []);
|
|
274
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React5.createElement(Header, { projectName }), phase === "loading" && /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, isMonorepo ? /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " ", LOADING_MESSAGES[loadingMsgIdx])), scanningPackage && /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, marginLeft: 2 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2192 "), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, scanningPackage))) : /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Running health checks..."), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, marginLeft: 2 }, /* @__PURE__ */ React5.createElement(ProgressList, { items: progress })))), phase === "error" && /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "red" }, "\u2717 Error: ", error)), phase === "results" && monorepoReport && /* @__PURE__ */ React5.createElement(MonorepoSummaryTable, { report: monorepoReport, scanDuration }), phase === "results" && report && /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, report.checks.filter((c) => c.status !== "skipped").map((check) => /* @__PURE__ */ React5.createElement(CheckResultRow, { key: check.id, result: check })), /* @__PURE__ */ React5.createElement(Summary, { report, scanDuration }), /* @__PURE__ */ React5.createElement(QuickWins, { report }), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "View detailed report: "), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "sickbay --web"))), phase === "opening-web" && (monorepoReport ?? report) && /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, monorepoReport ? /* @__PURE__ */ React5.createElement(MonorepoSummaryTable, { report: monorepoReport, scanDuration }) : report ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, report.checks.filter((c) => c.status !== "skipped").map((check) => /* @__PURE__ */ React5.createElement(CheckResultRow, { key: check.id, result: check })), /* @__PURE__ */ React5.createElement(Summary, { report, scanDuration })) : null, /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, webUrl ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "\u2713 Dashboard running at "), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, webUrl), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (Ctrl+C to stop)")) : /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" })), " ", /* @__PURE__ */ React5.createElement(Gradient, { name: "retro" }, "Launching dashboard with AI insights...")))));
|
|
275
|
+
}
|
|
276
|
+
function MonorepoSummaryTable({ report, scanDuration }) {
|
|
277
|
+
const scoreColor = (score) => score >= 80 ? "green" : score >= 60 ? "yellow" : "red";
|
|
278
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "Monorepo \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, report.monorepoType, " workspaces \xB7 "), /* @__PURE__ */ React5.createElement(Text5, null, report.packages.length, " packages")), report.packages.map((pkg) => /* @__PURE__ */ React5.createElement(Box5, { key: pkg.path, marginLeft: 2, gap: 1 }, /* @__PURE__ */ React5.createElement(Text5, { color: scoreColor(pkg.score) }, scoreBar(pkg.score)), /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: scoreColor(pkg.score) }, String(pkg.score).padStart(3)), /* @__PURE__ */ React5.createElement(Text5, null, pkg.name), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, pkg.framework), pkg.summary.critical > 0 && /* @__PURE__ */ React5.createElement(Text5, { color: "red" }, " ", pkg.summary.critical, " critical"))), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 2 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "Overall: "), /* @__PURE__ */ React5.createElement(Text5, { color: scoreColor(report.overallScore), bold: true }, report.overallScore), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "red" }, report.summary.critical, " critical"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, report.summary.warnings, " warnings"), scanDuration !== null && /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\xB7 "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, formatDuration2(scanDuration)))), report.quote && /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, { italic: true, dimColor: true }, '"', report.quote.text, '"'), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \u2014 ", report.quote.source)), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Per-package details: "), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "sickbay --web")));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/index.ts
|
|
282
|
+
var globalConfigPath = join(homedir(), ".sickbay", ".env");
|
|
283
|
+
if (existsSync(globalConfigPath)) {
|
|
284
|
+
config({ path: globalConfigPath, debug: false, quiet: true });
|
|
285
|
+
}
|
|
286
|
+
config({ debug: false, quiet: true });
|
|
287
|
+
var program = new Command();
|
|
288
|
+
program.name("sickbay").description("React project health check CLI").version("0.0.1").enablePositionalOptions().passThroughOptions().option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single named package (monorepo only)").option("--json", "output raw JSON report").option("--web", "open web dashboard after scan").option("--no-ai", "disable AI features").option("--no-quotes", "suppress personality quotes in output").option("--verbose", "show verbose output").action(async (options) => {
|
|
289
|
+
if (options.path && options.path !== process.cwd()) {
|
|
290
|
+
const projectEnvPath = join(options.path, ".env");
|
|
291
|
+
if (existsSync(projectEnvPath)) {
|
|
292
|
+
config({ path: projectEnvPath, override: true });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
|
|
296
|
+
const { detectMonorepo, runSickbay: runSickbay2, runSickbayMonorepo: runSickbayMonorepo2 } = await import("@nebulord/sickbay-core");
|
|
297
|
+
const monorepoInfo = await detectMonorepo(options.path);
|
|
298
|
+
if (options.package && monorepoInfo.isMonorepo) {
|
|
299
|
+
const { readFileSync } = await import("fs");
|
|
300
|
+
const targetPath = monorepoInfo.packagePaths.find((p) => {
|
|
301
|
+
try {
|
|
302
|
+
const pkg = JSON.parse(readFileSync(join(p, "package.json"), "utf-8"));
|
|
303
|
+
return pkg.name === options.package || pkg.name?.endsWith(`/${options.package}`);
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
if (!targetPath) {
|
|
309
|
+
process.stderr.write(`Package "${options.package}" not found in monorepo
|
|
310
|
+
`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
if (options.json) {
|
|
314
|
+
const report = await runSickbay2({ projectPath: targetPath, checks, verbose: options.verbose, quotes: options.quotes });
|
|
315
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
render(
|
|
319
|
+
React6.createElement(App, {
|
|
320
|
+
projectPath: targetPath,
|
|
321
|
+
checks,
|
|
322
|
+
openWeb: options.web,
|
|
323
|
+
enableAI: options.ai !== false,
|
|
324
|
+
verbose: options.verbose,
|
|
325
|
+
quotes: options.quotes
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (options.json) {
|
|
331
|
+
if (monorepoInfo.isMonorepo) {
|
|
332
|
+
const report2 = await runSickbayMonorepo2({
|
|
333
|
+
projectPath: options.path,
|
|
334
|
+
checks,
|
|
335
|
+
verbose: options.verbose,
|
|
336
|
+
quotes: options.quotes
|
|
337
|
+
});
|
|
338
|
+
process.stdout.write(JSON.stringify(report2, null, 2) + "\n");
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
const report = await runSickbay2({
|
|
342
|
+
projectPath: options.path,
|
|
343
|
+
checks,
|
|
344
|
+
verbose: options.verbose,
|
|
345
|
+
quotes: options.quotes
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
const { saveEntry, saveLastReport } = await import("./history-DYFJ65XH.js");
|
|
349
|
+
saveEntry(report);
|
|
350
|
+
saveLastReport(report);
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const { getDependencyTree } = await import("@nebulord/sickbay-core");
|
|
355
|
+
const { saveDepTree } = await import("./history-DYFJ65XH.js");
|
|
356
|
+
const tree = await getDependencyTree(options.path, report.projectInfo.packageManager);
|
|
357
|
+
saveDepTree(options.path, tree);
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
render(
|
|
364
|
+
React6.createElement(App, {
|
|
365
|
+
projectPath: options.path,
|
|
366
|
+
checks,
|
|
367
|
+
openWeb: options.web,
|
|
368
|
+
enableAI: options.ai !== false,
|
|
369
|
+
verbose: options.verbose,
|
|
370
|
+
quotes: options.quotes,
|
|
371
|
+
isMonorepo: monorepoInfo.isMonorepo
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
program.command("init").description("Initialize .sickbay/ folder and run an initial baseline scan").option("-p, --path <path>", "project path to initialize", process.cwd()).action(async (options) => {
|
|
376
|
+
const { initSickbay } = await import("./init-J2NPRPDO.js");
|
|
377
|
+
await initSickbay(options.path);
|
|
378
|
+
});
|
|
379
|
+
program.command("fix").description("Interactively fix issues found by sickbay scan").option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single package (monorepo only)").option("--all", "apply all available fixes without prompting").option("--dry-run", "show what would be fixed without executing").option("--verbose", "show verbose output").action(async (options) => {
|
|
380
|
+
if (options.path && options.path !== process.cwd()) {
|
|
381
|
+
const projectEnvPath = join(options.path, ".env");
|
|
382
|
+
if (existsSync(projectEnvPath)) {
|
|
383
|
+
config({ path: projectEnvPath, override: true });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const { resolveProject } = await import("./resolve-package-PHPJWOLY.js");
|
|
387
|
+
const resolution = await resolveProject(options.path, options.package);
|
|
388
|
+
const { FixApp } = await import("./FixApp-RJPCWNXJ.js");
|
|
389
|
+
const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
|
|
390
|
+
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
391
|
+
render(
|
|
392
|
+
React6.createElement(FixApp, {
|
|
393
|
+
projectPath,
|
|
394
|
+
checks,
|
|
395
|
+
applyAll: options.all ?? false,
|
|
396
|
+
dryRun: options.dryRun ?? false,
|
|
397
|
+
verbose: options.verbose ?? false,
|
|
398
|
+
isMonorepo: resolution.isMonorepo && !resolution.targetPath,
|
|
399
|
+
packagePaths: resolution.isMonorepo ? resolution.packagePaths : void 0,
|
|
400
|
+
packageNames: resolution.isMonorepo ? resolution.packageNames : void 0
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
program.command("trend").description("Show score history and trends over time").option("-p, --path <path>", "project path to analyze", process.cwd()).option("-n, --last <count>", "number of recent scans to show", "20").option("--package <name>", "scope to a single package (monorepo only)").option("--json", "output trend data as JSON").action(async (options) => {
|
|
405
|
+
if (options.path && options.path !== process.cwd()) {
|
|
406
|
+
const projectEnvPath = join(options.path, ".env");
|
|
407
|
+
if (existsSync(projectEnvPath)) {
|
|
408
|
+
config({ path: projectEnvPath, override: true });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const { resolveProject } = await import("./resolve-package-PHPJWOLY.js");
|
|
412
|
+
const resolution = await resolveProject(options.path, options.package);
|
|
413
|
+
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
414
|
+
const { TrendApp } = await import("./TrendApp-XL77HKDR.js");
|
|
415
|
+
render(
|
|
416
|
+
React6.createElement(TrendApp, {
|
|
417
|
+
projectPath,
|
|
418
|
+
last: parseInt(options.last, 10),
|
|
419
|
+
jsonOutput: options.json ?? false,
|
|
420
|
+
isMonorepo: resolution.isMonorepo && !resolution.targetPath,
|
|
421
|
+
packagePaths: resolution.isMonorepo ? resolution.packagePaths : void 0,
|
|
422
|
+
packageNames: resolution.isMonorepo ? resolution.packageNames : void 0
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
program.command("stats").description("Show a quick codebase overview and project summary").option("-p, --path <path>", "project path to analyze", process.cwd()).option("--package <name>", "scope to a single package (monorepo only)").option("--json", "output stats as JSON").action(async (options) => {
|
|
427
|
+
if (options.path && options.path !== process.cwd()) {
|
|
428
|
+
const projectEnvPath = join(options.path, ".env");
|
|
429
|
+
if (existsSync(projectEnvPath)) {
|
|
430
|
+
config({ path: projectEnvPath, override: true });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const { resolveProject } = await import("./resolve-package-PHPJWOLY.js");
|
|
434
|
+
const resolution = await resolveProject(options.path, options.package);
|
|
435
|
+
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
436
|
+
const { StatsApp } = await import("./StatsApp-BI6COY7S.js");
|
|
437
|
+
render(
|
|
438
|
+
React6.createElement(StatsApp, {
|
|
439
|
+
projectPath,
|
|
440
|
+
jsonOutput: options.json ?? false,
|
|
441
|
+
isMonorepo: resolution.isMonorepo && !resolution.targetPath,
|
|
442
|
+
packagePaths: resolution.isMonorepo ? resolution.packagePaths : void 0,
|
|
443
|
+
packageNames: resolution.isMonorepo ? resolution.packageNames : void 0
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
program.command("tui").description("Launch the persistent developer dashboard").option("-p, --path <path>", "project path to monitor", process.cwd()).option("--no-watch", "disable file-watching auto-refresh").option("--no-quotes", "suppress personality quotes in output").option("--refresh <seconds>", "auto-refresh interval in seconds", "300").option("-c, --checks <checks>", "comma-separated list of checks to run").action(async (options) => {
|
|
448
|
+
const { TuiApp } = await import("./TuiApp-VJNV4FD3.js");
|
|
449
|
+
const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
|
|
450
|
+
render(
|
|
451
|
+
React6.createElement(TuiApp, {
|
|
452
|
+
projectPath: options.path,
|
|
453
|
+
checks,
|
|
454
|
+
watchEnabled: options.watch !== false,
|
|
455
|
+
refreshInterval: parseInt(options.refresh, 10),
|
|
456
|
+
quotes: options.quotes
|
|
457
|
+
}),
|
|
458
|
+
{ exitOnCtrlC: true }
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
program.command("doctor").description("Diagnose project setup and configuration issues").option("-p, --path <path>", "project path to analyze", process.cwd()).option("--package <name>", "scope to a single package (monorepo only)").option("--fix", "auto-scaffold missing configuration files").option("--json", "output diagnostic results as JSON").action(async (options) => {
|
|
462
|
+
if (options.path && options.path !== process.cwd()) {
|
|
463
|
+
const projectEnvPath = join(options.path, ".env");
|
|
464
|
+
if (existsSync(projectEnvPath)) {
|
|
465
|
+
config({ path: projectEnvPath, override: true });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const { resolveProject } = await import("./resolve-package-PHPJWOLY.js");
|
|
469
|
+
const resolution = await resolveProject(options.path, options.package);
|
|
470
|
+
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
471
|
+
const { DoctorApp } = await import("./DoctorApp-U465IMK7.js");
|
|
472
|
+
render(
|
|
473
|
+
React6.createElement(DoctorApp, {
|
|
474
|
+
projectPath,
|
|
475
|
+
autoFix: options.fix ?? false,
|
|
476
|
+
jsonOutput: options.json ?? false,
|
|
477
|
+
isMonorepo: resolution.isMonorepo && !resolution.targetPath,
|
|
478
|
+
packagePaths: resolution.isMonorepo ? resolution.packagePaths : void 0,
|
|
479
|
+
packageNames: resolution.isMonorepo ? resolution.packageNames : void 0
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
program.command("badge").description("Generate a health score badge for your README").option("-p, --path <path>", "project path", process.cwd()).option("--package <name>", "scope to a single package (monorepo only)").option("--html", "output HTML <img> tag instead of markdown").option("--url", "output bare badge URL only").option("--label <text>", "custom badge label", "sickbay").option("--scan", "run a fresh scan instead of using last report").action(async (options) => {
|
|
484
|
+
const { resolveProject } = await import("./resolve-package-PHPJWOLY.js");
|
|
485
|
+
const resolution = await resolveProject(options.path, options.package);
|
|
486
|
+
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
487
|
+
const {
|
|
488
|
+
loadScoreFromLastReport,
|
|
489
|
+
badgeUrl,
|
|
490
|
+
badgeMarkdown,
|
|
491
|
+
badgeHtml
|
|
492
|
+
} = await import("./badge-KQ73KEIN.js");
|
|
493
|
+
let score = options.scan ? null : loadScoreFromLastReport(projectPath);
|
|
494
|
+
if (score === null) {
|
|
495
|
+
const { runSickbay: runSickbay2 } = await import("@nebulord/sickbay-core");
|
|
496
|
+
const report = await runSickbay2({ projectPath, quotes: false });
|
|
497
|
+
try {
|
|
498
|
+
const { saveEntry, saveLastReport } = await import("./history-DYFJ65XH.js");
|
|
499
|
+
saveEntry(report);
|
|
500
|
+
saveLastReport(report);
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
score = report.overallScore;
|
|
504
|
+
}
|
|
505
|
+
let output;
|
|
506
|
+
if (options.url) {
|
|
507
|
+
output = badgeUrl(score, options.label);
|
|
508
|
+
} else if (options.html) {
|
|
509
|
+
output = badgeHtml(score, options.label);
|
|
510
|
+
} else {
|
|
511
|
+
output = badgeMarkdown(score, options.label);
|
|
512
|
+
}
|
|
513
|
+
process.stdout.write(output + "\n");
|
|
514
|
+
process.exit(0);
|
|
515
|
+
});
|
|
516
|
+
program.command("diff <branch>").description("Compare health score against another branch").option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--json", "output diff as JSON").option("--verbose", "show verbose output").action(async (branch, options) => {
|
|
517
|
+
if (options.path && options.path !== process.cwd()) {
|
|
518
|
+
const projectEnvPath = join(options.path, ".env");
|
|
519
|
+
if (existsSync(projectEnvPath)) {
|
|
520
|
+
config({ path: projectEnvPath, override: true });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
|
|
524
|
+
const { DiffApp } = await import("./DiffApp-YF2PYQZK.js");
|
|
525
|
+
render(
|
|
526
|
+
React6.createElement(DiffApp, {
|
|
527
|
+
projectPath: options.path,
|
|
528
|
+
branch,
|
|
529
|
+
jsonOutput: options.json ?? false,
|
|
530
|
+
checks,
|
|
531
|
+
verbose: options.verbose
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
program.parse();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
saveEntry
|
|
3
|
+
} from "./chunk-5KJOYSVJ.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/init.ts
|
|
6
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { runSickbay } from "@nebulord/sickbay-core";
|
|
9
|
+
async function initSickbay(projectPath) {
|
|
10
|
+
const sickbayDir = join(projectPath, ".sickbay");
|
|
11
|
+
mkdirSync(sickbayDir, { recursive: true });
|
|
12
|
+
writeFileSync(
|
|
13
|
+
join(sickbayDir, ".gitignore"),
|
|
14
|
+
"history.json\ncache/\n"
|
|
15
|
+
);
|
|
16
|
+
const rootGitignorePath = join(projectPath, ".gitignore");
|
|
17
|
+
const gitignoreEntries = [".sickbay/history.json", ".sickbay/cache/"];
|
|
18
|
+
const existingGitignore = existsSync(rootGitignorePath) ? readFileSync(rootGitignorePath, "utf-8") : "";
|
|
19
|
+
const toAdd = gitignoreEntries.filter((e) => !existingGitignore.includes(e));
|
|
20
|
+
if (toAdd.length > 0) {
|
|
21
|
+
const prefix = existingGitignore.endsWith("\n") || existingGitignore === "" ? "" : "\n";
|
|
22
|
+
appendFileSync(rootGitignorePath, `${prefix}${toAdd.join("\n")}
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
const baselinePath = join(sickbayDir, "baseline.json");
|
|
26
|
+
if (existsSync(baselinePath)) {
|
|
27
|
+
console.log(
|
|
28
|
+
"\u26A0 .sickbay/baseline.json already exists. Overwriting with new scan."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
console.log("Running initial scan to generate baseline...\n");
|
|
32
|
+
const report = await runSickbay({ projectPath });
|
|
33
|
+
writeFileSync(baselinePath, JSON.stringify(report, null, 2));
|
|
34
|
+
try {
|
|
35
|
+
saveEntry(report);
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
const scoreLabel = report.overallScore >= 80 ? "good" : report.overallScore >= 60 ? "fair" : "needs work";
|
|
39
|
+
console.log(`
|
|
40
|
+
\u2713 Sickbay initialized for ${report.projectInfo.name}`);
|
|
41
|
+
console.log(` Overall score: ${report.overallScore}/100 (${scoreLabel})`);
|
|
42
|
+
console.log(`
|
|
43
|
+
Created:`);
|
|
44
|
+
console.log(` .sickbay/baseline.json \u2014 committed (team baseline)`);
|
|
45
|
+
console.log(` .sickbay/.gitignore \u2014 ignores history.json + cache/`);
|
|
46
|
+
if (toAdd.length > 0) {
|
|
47
|
+
console.log(` .gitignore \u2014 added ${toAdd.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
console.log(`
|
|
50
|
+
Run \`sickbay\` to add history entries over time.`);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
initSickbay
|
|
54
|
+
};
|