@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.
@@ -0,0 +1,198 @@
1
+ // src/commands/web.ts
2
+ import http from "http";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join, extname } from "path";
5
+ import { fileURLToPath } from "url";
6
+ var MIME_TYPES = {
7
+ ".html": "text/html",
8
+ ".js": "application/javascript",
9
+ ".css": "text/css",
10
+ ".json": "application/json",
11
+ ".svg": "image/svg+xml",
12
+ ".ico": "image/x-icon",
13
+ ".woff": "font/woff",
14
+ ".woff2": "font/woff2"
15
+ };
16
+ function findWebDist() {
17
+ const thisFile = fileURLToPath(import.meta.url);
18
+ const candidates = [
19
+ join(thisFile, "..", "..", "..", "..", "..", "web", "dist"),
20
+ // from dist/commands/
21
+ join(thisFile, "..", "..", "..", "web", "dist")
22
+ // from src/commands/
23
+ ];
24
+ for (const p of candidates) {
25
+ if (existsSync(join(p, "index.html"))) return p;
26
+ }
27
+ return null;
28
+ }
29
+ async function getFreePort(preferred) {
30
+ return new Promise((resolve) => {
31
+ const server = http.createServer();
32
+ server.listen(preferred, () => {
33
+ const addr = server.address();
34
+ server.close(() => resolve(addr.port));
35
+ });
36
+ server.on("error", () => resolve(getFreePort(preferred + 1)));
37
+ });
38
+ }
39
+ function packageReportToSickbayReport(pkg, parent) {
40
+ return {
41
+ timestamp: parent.timestamp,
42
+ projectPath: pkg.path,
43
+ projectInfo: {
44
+ name: pkg.name,
45
+ version: "unknown",
46
+ hasTypeScript: false,
47
+ hasESLint: false,
48
+ hasPrettier: false,
49
+ framework: pkg.framework,
50
+ packageManager: parent.packageManager,
51
+ totalDependencies: Object.keys(pkg.dependencies).length + Object.keys(pkg.devDependencies).length,
52
+ dependencies: pkg.dependencies,
53
+ devDependencies: pkg.devDependencies
54
+ },
55
+ checks: pkg.checks,
56
+ overallScore: pkg.score,
57
+ summary: pkg.summary
58
+ };
59
+ }
60
+ async function serveWeb(report, preferredPort = 3030, aiService) {
61
+ const distDir = findWebDist();
62
+ if (!distDir) {
63
+ throw new Error(
64
+ "Web dashboard not built. Run: pnpm --filter @sickbay/web build"
65
+ );
66
+ }
67
+ const reportJson = JSON.stringify(report);
68
+ const port = await getFreePort(preferredPort);
69
+ let aiSummary = null;
70
+ if (aiService && !("isMonorepo" in report)) {
71
+ try {
72
+ aiSummary = await aiService.generateSummary(report);
73
+ } catch (err) {
74
+ console.warn("AI summary generation failed:", err);
75
+ }
76
+ }
77
+ const server = http.createServer(async (req, res) => {
78
+ const url = req.url ?? "/";
79
+ if (url === "/sickbay-report.json") {
80
+ res.writeHead(200, { "Content-Type": "application/json" });
81
+ res.end(reportJson);
82
+ return;
83
+ }
84
+ if (url === "/sickbay-history.json") {
85
+ const basePath = "isMonorepo" in report ? report.rootPath : report.projectPath;
86
+ const historyPath = join(basePath, ".sickbay", "history.json");
87
+ if (existsSync(historyPath)) {
88
+ res.writeHead(200, { "Content-Type": "application/json" });
89
+ res.end(readFileSync(historyPath, "utf-8"));
90
+ } else {
91
+ res.writeHead(404, { "Content-Type": "application/json" });
92
+ res.end("{}");
93
+ }
94
+ return;
95
+ }
96
+ if (url === "/sickbay-dep-tree.json") {
97
+ const basePath = "isMonorepo" in report ? report.rootPath : report.projectPath;
98
+ const treePath = join(basePath, ".sickbay", "dep-tree.json");
99
+ if (existsSync(treePath)) {
100
+ res.writeHead(200, { "Content-Type": "application/json" });
101
+ res.end(readFileSync(treePath, "utf-8"));
102
+ } else {
103
+ res.writeHead(404, { "Content-Type": "application/json" });
104
+ res.end("{}");
105
+ }
106
+ return;
107
+ }
108
+ const parsedUrl = new URL(url, "http://localhost");
109
+ const packageName = parsedUrl.searchParams.get("package");
110
+ if (parsedUrl.pathname === "/ai/summary") {
111
+ if (req.method === "HEAD") {
112
+ if (aiService) {
113
+ res.writeHead(200);
114
+ } else {
115
+ res.writeHead(404);
116
+ }
117
+ res.end();
118
+ return;
119
+ }
120
+ if (packageName && aiService && "isMonorepo" in report) {
121
+ const pkg = report.packages.find((p) => p.name === packageName);
122
+ if (!pkg) {
123
+ res.writeHead(404, { "Content-Type": "application/json" });
124
+ res.end(JSON.stringify({ error: "Package not found" }));
125
+ return;
126
+ }
127
+ try {
128
+ const pkgReport = packageReportToSickbayReport(pkg, report);
129
+ const summary = await aiService.generateSummary(pkgReport);
130
+ res.writeHead(200, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({ summary }));
132
+ } catch (err) {
133
+ res.writeHead(500, { "Content-Type": "application/json" });
134
+ res.end(JSON.stringify({ error: String(err) }));
135
+ }
136
+ return;
137
+ }
138
+ if (aiSummary) {
139
+ res.writeHead(200, { "Content-Type": "application/json" });
140
+ res.end(JSON.stringify({ summary: aiSummary }));
141
+ return;
142
+ }
143
+ res.writeHead(404, { "Content-Type": "application/json" });
144
+ res.end("{}");
145
+ return;
146
+ }
147
+ if (parsedUrl.pathname === "/ai/chat" && req.method === "POST" && aiService) {
148
+ let body = "";
149
+ req.on("data", (chunk) => body += chunk);
150
+ req.on("end", async () => {
151
+ try {
152
+ const { message, history } = JSON.parse(body);
153
+ let chatReport;
154
+ if (packageName && "isMonorepo" in report) {
155
+ const pkg = report.packages.find((p) => p.name === packageName);
156
+ if (!pkg) {
157
+ res.writeHead(404, { "Content-Type": "application/json" });
158
+ res.end(JSON.stringify({ error: "Package not found" }));
159
+ return;
160
+ }
161
+ chatReport = packageReportToSickbayReport(pkg, report);
162
+ } else if (!("isMonorepo" in report)) {
163
+ chatReport = report;
164
+ } else {
165
+ res.writeHead(400, { "Content-Type": "application/json" });
166
+ res.end(JSON.stringify({ error: "Package name required for monorepo" }));
167
+ return;
168
+ }
169
+ const response = await aiService.chat(message, chatReport, history ?? []);
170
+ res.writeHead(200, { "Content-Type": "application/json" });
171
+ res.end(JSON.stringify({ response }));
172
+ } catch (err) {
173
+ res.writeHead(500, { "Content-Type": "application/json" });
174
+ res.end(JSON.stringify({ error: String(err) }));
175
+ }
176
+ });
177
+ return;
178
+ }
179
+ const filePath = join(distDir, url === "/" ? "index.html" : url);
180
+ if (existsSync(filePath)) {
181
+ const ext = extname(filePath);
182
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
183
+ res.writeHead(200, { "Content-Type": mime });
184
+ res.end(readFileSync(filePath));
185
+ } else {
186
+ res.writeHead(200, { "Content-Type": "text/html" });
187
+ res.end(readFileSync(join(distDir, "index.html")));
188
+ }
189
+ });
190
+ return new Promise((resolve) => {
191
+ server.listen(port, () => {
192
+ resolve(`http://localhost:${port}`);
193
+ });
194
+ });
195
+ }
196
+ export {
197
+ serveWeb
198
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nebulord/sickbay",
3
+ "version": "0.1.0",
4
+ "description": "Zero-config health check CLI for JavaScript and TypeScript projects",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/nebulord-dev/sickbay"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "sickbay": "./dist/index.js"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "@anthropic-ai/sdk": "^0.80.0",
28
+ "chokidar": "^5.0.0",
29
+ "commander": "^14.0.3",
30
+ "dotenv": "^17.3.1",
31
+ "ink": "^6.8.0",
32
+ "ink-gradient": "^4.0.0",
33
+ "ink-spinner": "^5.0.0",
34
+ "open": "^11.0.0",
35
+ "react": "^19.2.4",
36
+ "@nebulord/sickbay-core": "0.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@testing-library/jest-dom": "^6.9.1",
40
+ "@testing-library/react": "^16.3.2",
41
+ "@types/react": "^19.2.14",
42
+ "ink-testing-library": "^4.0.0",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.1.2"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "dev": "tsup --watch",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "test:ui": "vitest --ui --coverage",
53
+ "test:coverage": "vitest run --coverage",
54
+ "lint": "eslint src/",
55
+ "lint:fix": "eslint src/ --fix",
56
+ "clean": "rm -rf dist"
57
+ }
58
+ }