@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/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
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ resolveProject,
3
+ shortName
4
+ } from "./chunk-BUD5BE6U.js";
5
+ export {
6
+ resolveProject,
7
+ shortName
8
+ };