devsurface 0.1.0 → 0.2.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 +8 -0
- package/README.md +62 -13
- package/SECURITY.md +9 -0
- package/dist/cli/index.js +167 -36
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/src/web/dist/assets/index-CdzG3b92.js +10 -0
- package/src/web/dist/assets/index-l7i8vzTo.css +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BPOLPimA.js +0 -10
- package/src/web/dist/assets/index-Ch_lsiJZ.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Added retained process logs through `GET /api/logs` so the dashboard can recover session output without relying only on WebSocket state.
|
|
6
|
+
- Added dashboard keyboard shortcuts for refresh, section navigation, settings, sidebar collapse, and drawer close.
|
|
7
|
+
- Added exit-code-aware process status labels in the dashboard.
|
|
8
|
+
- Kept dashboard settings in memory to avoid browser storage.
|
|
9
|
+
- Documented `bunx devsurface` as a no-install launch command.
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
- Initial DevSurface MVP scaffold.
|
package/README.md
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<a id="readme-top"></a>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<div align="center">
|
|
6
|
+
|
|
7
|
+
<h1>DevSurface</h1>
|
|
8
|
+
|
|
9
|
+
<p><strong>Turn any Node.js repository into a local developer control panel.</strong></p>
|
|
10
|
+
|
|
11
|
+
<p>
|
|
12
|
+
<a href="#quick-start">Quick Start</a>
|
|
13
|
+
·
|
|
14
|
+
<a href="#commands">Commands</a>
|
|
15
|
+
·
|
|
16
|
+
<a href="#dashboard">Dashboard</a>
|
|
17
|
+
·
|
|
18
|
+
<a href="https://github.com/mrfandu1/devsurface/issues">Report an issue</a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p>
|
|
22
|
+
<a href="https://github.com/mrfandu1/devsurface">
|
|
23
|
+
<img alt="DevSurface ready" src="docs/devsurface-badge.svg">
|
|
24
|
+
</a>
|
|
25
|
+
<a href="LICENSE">
|
|
26
|
+
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-informational">
|
|
27
|
+
</a>
|
|
28
|
+
<img alt="Built with TypeScript" src="https://img.shields.io/badge/Built%20with-TypeScript-3178c6">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
</div>
|
|
6
32
|
|
|
7
33
|
DevSurface scans a project, starts a local dashboard, and shows the things contributors
|
|
8
34
|
usually need before a project will run: package scripts, environment files, ports,
|
|
@@ -14,6 +40,12 @@ No config file is required.
|
|
|
14
40
|
npx devsurface
|
|
15
41
|
```
|
|
16
42
|
|
|
43
|
+
With Bun:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bunx devsurface
|
|
47
|
+
```
|
|
48
|
+
|
|
17
49
|

|
|
18
50
|
|
|
19
51
|
## Why DevSurface
|
|
@@ -52,6 +84,13 @@ cd my-node-project
|
|
|
52
84
|
npx devsurface
|
|
53
85
|
```
|
|
54
86
|
|
|
87
|
+
Or, if you use Bun:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd my-node-project
|
|
91
|
+
bunx devsurface
|
|
92
|
+
```
|
|
93
|
+
|
|
55
94
|
The dashboard opens at:
|
|
56
95
|
|
|
57
96
|
```text
|
|
@@ -63,6 +102,13 @@ terminal.
|
|
|
63
102
|
|
|
64
103
|
## Commands
|
|
65
104
|
|
|
105
|
+
Run DevSurface without installing it globally:
|
|
106
|
+
|
|
107
|
+
| Runtime | Command |
|
|
108
|
+
| ------- | ----------------- |
|
|
109
|
+
| npm | `npx devsurface` |
|
|
110
|
+
| Bun | `bunx devsurface` |
|
|
111
|
+
|
|
66
112
|
| Command | Description |
|
|
67
113
|
| ------------------------- | -------------------------------------------------------------------- |
|
|
68
114
|
| `devsurface` | Scan the current project, start the dashboard, and open the browser. |
|
|
@@ -205,18 +251,21 @@ npm test
|
|
|
205
251
|
npm run build
|
|
206
252
|
```
|
|
207
253
|
|
|
208
|
-
##
|
|
254
|
+
## Contributing
|
|
209
255
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
Contributions of every kind are welcome: code, documentation, bug reports,
|
|
257
|
+
examples, and reviews. Start with [CONTRIBUTING.md](CONTRIBUTING.md) for the
|
|
258
|
+
development workflow.
|
|
213
259
|
|
|
214
|
-
|
|
260
|
+
## License
|
|
215
261
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```
|
|
262
|
+
DevSurface is released under the MIT License. See [LICENSE](LICENSE) for the full
|
|
263
|
+
text. Copyright (c) 2026 DevSurface contributors.
|
|
219
264
|
|
|
220
|
-
##
|
|
265
|
+
## Contact and community
|
|
266
|
+
|
|
267
|
+
- GitHub Issues: report bugs and request features through
|
|
268
|
+
[GitHub Issues](https://github.com/mrfandu1/devsurface/issues).
|
|
269
|
+
- Security: report vulnerabilities through [SECURITY.md](SECURITY.md).
|
|
221
270
|
|
|
222
|
-
|
|
271
|
+
[(back to top)](#readme-top)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security issues privately by opening a GitHub security advisory or by
|
|
6
|
+
emailing the maintainer listed in the repository profile.
|
|
7
|
+
|
|
8
|
+
DevSurface runs local commands from the project it scans. It never binds outside
|
|
9
|
+
`127.0.0.1`, never sends telemetry, and never exposes `.env` values.
|
package/dist/cli/index.js
CHANGED
|
@@ -48,6 +48,7 @@ var defaultConfig = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
// src/core/config/load.ts
|
|
51
|
+
var MAX_CONFIGURED_PORTS = 32;
|
|
51
52
|
function isRecord(value) {
|
|
52
53
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
54
|
}
|
|
@@ -101,7 +102,10 @@ function toPorts(value, warnings) {
|
|
|
101
102
|
if (ports.length !== value.length) {
|
|
102
103
|
warnings.push("ports may only contain integers between 1 and 65535.");
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
+
if (ports.length > MAX_CONFIGURED_PORTS) {
|
|
106
|
+
warnings.push(`ports may contain at most ${MAX_CONFIGURED_PORTS} entries.`);
|
|
107
|
+
}
|
|
108
|
+
return ports.slice(0, MAX_CONFIGURED_PORTS);
|
|
105
109
|
}
|
|
106
110
|
function validateConfig(raw) {
|
|
107
111
|
const warnings = [];
|
|
@@ -609,12 +613,23 @@ async function detectPackageManager(root) {
|
|
|
609
613
|
// src/core/scanner/packageJson.ts
|
|
610
614
|
import { promises as fs7 } from "fs";
|
|
611
615
|
import path7 from "path";
|
|
616
|
+
function isWithinRoot4(root, target) {
|
|
617
|
+
const relative = path7.relative(root, target);
|
|
618
|
+
return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
|
|
619
|
+
}
|
|
612
620
|
async function readPackageJson(root) {
|
|
613
621
|
const packageJsonPath = path7.join(root, "package.json");
|
|
614
622
|
try {
|
|
615
|
-
const
|
|
623
|
+
const [realRoot, realPackageJsonPath] = await Promise.all([
|
|
624
|
+
fs7.realpath(root),
|
|
625
|
+
fs7.realpath(packageJsonPath)
|
|
626
|
+
]);
|
|
627
|
+
if (!isWithinRoot4(realRoot, realPackageJsonPath)) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const content = await fs7.readFile(realPackageJsonPath, "utf8");
|
|
616
631
|
const data = JSON.parse(content);
|
|
617
|
-
return { path:
|
|
632
|
+
return { path: realPackageJsonPath, data };
|
|
618
633
|
} catch {
|
|
619
634
|
return null;
|
|
620
635
|
}
|
|
@@ -622,6 +637,8 @@ async function readPackageJson(root) {
|
|
|
622
637
|
|
|
623
638
|
// src/core/scanner/ports.ts
|
|
624
639
|
import net from "net";
|
|
640
|
+
var MAX_PORT_PROBES = 64;
|
|
641
|
+
var PORT_PROBE_CONCURRENCY = 16;
|
|
625
642
|
function uniquePorts(ports) {
|
|
626
643
|
return Array.from(
|
|
627
644
|
new Set(ports.filter((port) => Number.isInteger(port) && port > 0 && port < 65536))
|
|
@@ -675,11 +692,25 @@ async function probePort(port) {
|
|
|
675
692
|
});
|
|
676
693
|
}
|
|
677
694
|
async function detectPorts(ports) {
|
|
678
|
-
const normalized = uniquePorts(ports);
|
|
695
|
+
const normalized = uniquePorts(ports).slice(0, MAX_PORT_PROBES);
|
|
679
696
|
if (normalized.length === 0) {
|
|
680
697
|
return null;
|
|
681
698
|
}
|
|
682
|
-
|
|
699
|
+
const results = [];
|
|
700
|
+
let nextIndex = 0;
|
|
701
|
+
async function worker() {
|
|
702
|
+
while (nextIndex < normalized.length) {
|
|
703
|
+
const port = normalized[nextIndex];
|
|
704
|
+
nextIndex += 1;
|
|
705
|
+
results.push(await probePort(port));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
await Promise.all(
|
|
709
|
+
Array.from({ length: Math.min(PORT_PROBE_CONCURRENCY, normalized.length) }, () => worker())
|
|
710
|
+
);
|
|
711
|
+
return results.sort(
|
|
712
|
+
(left, right) => normalized.indexOf(left.port) - normalized.indexOf(right.port)
|
|
713
|
+
);
|
|
683
714
|
}
|
|
684
715
|
|
|
685
716
|
// src/core/scanner/scripts.ts
|
|
@@ -917,6 +948,31 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
917
948
|
return warnings;
|
|
918
949
|
}
|
|
919
950
|
|
|
951
|
+
// src/cli/terminal.ts
|
|
952
|
+
var ESC = String.fromCharCode(27);
|
|
953
|
+
var BEL = String.fromCharCode(7);
|
|
954
|
+
var OSC_SEQUENCE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, "g");
|
|
955
|
+
var CSI_SEQUENCE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
956
|
+
var ESCAPE_SEQUENCE = new RegExp(`${ESC}[@-Z\\\\-_]`, "g");
|
|
957
|
+
function stripControlCharacters(value) {
|
|
958
|
+
let result = "";
|
|
959
|
+
for (const character of value) {
|
|
960
|
+
const code = character.charCodeAt(0);
|
|
961
|
+
if (code > 31 && code < 127 || code > 159) {
|
|
962
|
+
result += character;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
function safeTerminalText(value) {
|
|
968
|
+
return stripControlCharacters(
|
|
969
|
+
String(value).replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESCAPE_SEQUENCE, "")
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
function safeTerminalList(values) {
|
|
973
|
+
return values.length > 0 ? values.map((value) => safeTerminalText(value)).join(", ") : "none";
|
|
974
|
+
}
|
|
975
|
+
|
|
920
976
|
// src/cli/commands/doctor.ts
|
|
921
977
|
function colorSeverity(severity) {
|
|
922
978
|
if (severity === "error") {
|
|
@@ -934,8 +990,8 @@ async function doctorCommand(cwd = process.cwd()) {
|
|
|
934
990
|
return;
|
|
935
991
|
}
|
|
936
992
|
for (const item of warnings) {
|
|
937
|
-
console.log(`${colorSeverity(item.severity)} ${pc.bold(item.title)}`);
|
|
938
|
-
console.log(` ${item.message}`);
|
|
993
|
+
console.log(`${colorSeverity(item.severity)} ${pc.bold(safeTerminalText(item.title))}`);
|
|
994
|
+
console.log(` ${safeTerminalText(item.message)}`);
|
|
939
995
|
}
|
|
940
996
|
}
|
|
941
997
|
|
|
@@ -1065,14 +1121,14 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
1065
1121
|
// src/cli/commands/scan.ts
|
|
1066
1122
|
import pc4 from "picocolors";
|
|
1067
1123
|
function formatList(values) {
|
|
1068
|
-
return values
|
|
1124
|
+
return safeTerminalList(values);
|
|
1069
1125
|
}
|
|
1070
1126
|
function printScanResult(scan) {
|
|
1071
|
-
console.log(pc4.bold(`Project: ${scan.projectName}`));
|
|
1072
|
-
console.log(`Type: ${scan.framework?.type ?? "Unknown"}`);
|
|
1073
|
-
console.log(`Manager: ${scan.packageManager ?? "unknown"}`);
|
|
1127
|
+
console.log(pc4.bold(`Project: ${safeTerminalText(scan.projectName)}`));
|
|
1128
|
+
console.log(`Type: ${safeTerminalText(scan.framework?.type ?? "Unknown")}`);
|
|
1129
|
+
console.log(`Manager: ${safeTerminalText(scan.packageManager ?? "unknown")}`);
|
|
1074
1130
|
console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
|
|
1075
|
-
console.log(`Git: ${scan.git?.branch ?? "not detected"}`);
|
|
1131
|
+
console.log(`Git: ${safeTerminalText(scan.git?.branch ?? "not detected")}`);
|
|
1076
1132
|
console.log(`README: ${scan.readme.exists ? "found" : "missing"}`);
|
|
1077
1133
|
console.log(`LICENSE: ${scan.license.exists ? "found" : "missing"}`);
|
|
1078
1134
|
if (scan.env !== null) {
|
|
@@ -1099,7 +1155,7 @@ import pc5 from "picocolors";
|
|
|
1099
1155
|
import { promises as fs12 } from "fs";
|
|
1100
1156
|
import path12 from "path";
|
|
1101
1157
|
import { fileURLToPath } from "url";
|
|
1102
|
-
import {
|
|
1158
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
1103
1159
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
1104
1160
|
import { Hono } from "hono";
|
|
1105
1161
|
import open2 from "open";
|
|
@@ -1107,6 +1163,23 @@ import open2 from "open";
|
|
|
1107
1163
|
// src/core/process/manager.ts
|
|
1108
1164
|
import { EventEmitter } from "events";
|
|
1109
1165
|
import spawn3 from "cross-spawn";
|
|
1166
|
+
function killChildProcessTree(child) {
|
|
1167
|
+
if (child.pid === void 0) {
|
|
1168
|
+
child.kill();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (process.platform === "win32") {
|
|
1172
|
+
const result = spawn3.sync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
1173
|
+
stdio: "ignore",
|
|
1174
|
+
windowsHide: true
|
|
1175
|
+
});
|
|
1176
|
+
if (result.error) {
|
|
1177
|
+
child.kill();
|
|
1178
|
+
}
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
child.kill();
|
|
1182
|
+
}
|
|
1110
1183
|
var ProcessManager = class extends EventEmitter {
|
|
1111
1184
|
processes = /* @__PURE__ */ new Map();
|
|
1112
1185
|
logs = [];
|
|
@@ -1163,7 +1236,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1163
1236
|
}
|
|
1164
1237
|
record.status = "stopped";
|
|
1165
1238
|
record.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1166
|
-
record.child
|
|
1239
|
+
killChildProcessTree(record.child);
|
|
1167
1240
|
this.emitSystem(record, "Stopped by DevSurface");
|
|
1168
1241
|
this.emit("process", this.snapshot(record));
|
|
1169
1242
|
return true;
|
|
@@ -1179,7 +1252,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1179
1252
|
if (record.status === "running") {
|
|
1180
1253
|
record.status = "stopped";
|
|
1181
1254
|
record.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1182
|
-
record.child
|
|
1255
|
+
killChildProcessTree(record.child);
|
|
1183
1256
|
}
|
|
1184
1257
|
}
|
|
1185
1258
|
}
|
|
@@ -1273,7 +1346,7 @@ function isSameOrigin(requestUrl, origin) {
|
|
|
1273
1346
|
}
|
|
1274
1347
|
|
|
1275
1348
|
// src/server/routes/api.ts
|
|
1276
|
-
function
|
|
1349
|
+
function isWithinRoot5(root, target) {
|
|
1277
1350
|
const relative = path11.relative(path11.resolve(root), path11.resolve(target));
|
|
1278
1351
|
return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
|
|
1279
1352
|
}
|
|
@@ -1290,18 +1363,18 @@ function hasMutationIntent(intent) {
|
|
|
1290
1363
|
return intent === "dashboard";
|
|
1291
1364
|
}
|
|
1292
1365
|
async function realPathWithinRoot(root, target) {
|
|
1293
|
-
if (!
|
|
1366
|
+
if (!isWithinRoot5(root, target)) {
|
|
1294
1367
|
return false;
|
|
1295
1368
|
}
|
|
1296
1369
|
try {
|
|
1297
1370
|
const [realRoot, realTarget] = await Promise.all([fs11.realpath(root), fs11.realpath(target)]);
|
|
1298
|
-
return
|
|
1371
|
+
return isWithinRoot5(realRoot, realTarget);
|
|
1299
1372
|
} catch {
|
|
1300
1373
|
return false;
|
|
1301
1374
|
}
|
|
1302
1375
|
}
|
|
1303
1376
|
async function writableDestinationWithinRoot(root, destination) {
|
|
1304
|
-
if (!
|
|
1377
|
+
if (!isWithinRoot5(root, destination)) {
|
|
1305
1378
|
return false;
|
|
1306
1379
|
}
|
|
1307
1380
|
try {
|
|
@@ -1309,7 +1382,7 @@ async function writableDestinationWithinRoot(root, destination) {
|
|
|
1309
1382
|
fs11.realpath(root),
|
|
1310
1383
|
fs11.realpath(path11.dirname(destination))
|
|
1311
1384
|
]);
|
|
1312
|
-
return
|
|
1385
|
+
return isWithinRoot5(realRoot, realParent);
|
|
1313
1386
|
} catch {
|
|
1314
1387
|
return false;
|
|
1315
1388
|
}
|
|
@@ -1444,6 +1517,9 @@ function registerApiRoutes(app, options) {
|
|
|
1444
1517
|
app.get("/api/processes", (context) => {
|
|
1445
1518
|
return context.json(options.processManager.list());
|
|
1446
1519
|
});
|
|
1520
|
+
app.get("/api/logs", (context) => {
|
|
1521
|
+
return context.json(options.processManager.listLogs());
|
|
1522
|
+
});
|
|
1447
1523
|
app.post("/api/run/:script", async (context) => {
|
|
1448
1524
|
const script = decodeURIComponent(context.req.param("script"));
|
|
1449
1525
|
const scan = await scanProject(options.projectRoot);
|
|
@@ -1635,6 +1711,70 @@ async function findWebDistDir() {
|
|
|
1635
1711
|
}
|
|
1636
1712
|
return null;
|
|
1637
1713
|
}
|
|
1714
|
+
function toListenError(error, port) {
|
|
1715
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
1716
|
+
if (code === "EADDRINUSE") {
|
|
1717
|
+
return new Error(
|
|
1718
|
+
`Port ${port} is already in use on ${HOST}. Stop the other process or run DevSurface with --port ${port + 1}.`,
|
|
1719
|
+
{ cause: error }
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
if (code === "EACCES") {
|
|
1723
|
+
return new Error(`DevSurface does not have permission to bind to ${HOST}:${port}.`, {
|
|
1724
|
+
cause: error
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1728
|
+
}
|
|
1729
|
+
async function listenOnLocalHost(server, wss, port) {
|
|
1730
|
+
await new Promise((resolve, reject) => {
|
|
1731
|
+
let settled = false;
|
|
1732
|
+
const cleanup = () => {
|
|
1733
|
+
server.off("error", onError);
|
|
1734
|
+
server.off("listening", onListening);
|
|
1735
|
+
wss.off("error", onError);
|
|
1736
|
+
};
|
|
1737
|
+
const onError = (error) => {
|
|
1738
|
+
if (settled) {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
settled = true;
|
|
1742
|
+
cleanup();
|
|
1743
|
+
reject(toListenError(error, port));
|
|
1744
|
+
};
|
|
1745
|
+
const onListening = () => {
|
|
1746
|
+
if (settled) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
settled = true;
|
|
1750
|
+
cleanup();
|
|
1751
|
+
resolve();
|
|
1752
|
+
};
|
|
1753
|
+
wss.once("error", onError);
|
|
1754
|
+
server.once("error", onError);
|
|
1755
|
+
server.once("listening", onListening);
|
|
1756
|
+
server.listen(port, HOST);
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
async function closeWebSocketServer(wss) {
|
|
1760
|
+
await new Promise((resolve) => {
|
|
1761
|
+
wss.close(() => resolve());
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
async function closeHttpServer(server) {
|
|
1765
|
+
if (!server.listening) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
await new Promise((resolve, reject) => {
|
|
1769
|
+
server.close((error) => {
|
|
1770
|
+
if (error) {
|
|
1771
|
+
reject(error);
|
|
1772
|
+
} else {
|
|
1773
|
+
resolve();
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1638
1778
|
async function createApp(options) {
|
|
1639
1779
|
const app = new Hono();
|
|
1640
1780
|
registerApiRoutes(app, options);
|
|
@@ -1666,12 +1806,13 @@ async function startDevSurfaceServer(options) {
|
|
|
1666
1806
|
projectRoot: options.projectRoot,
|
|
1667
1807
|
processManager
|
|
1668
1808
|
});
|
|
1669
|
-
const server =
|
|
1809
|
+
const server = createAdaptorServer({
|
|
1670
1810
|
fetch: app.fetch,
|
|
1671
|
-
port,
|
|
1672
1811
|
hostname: HOST
|
|
1673
1812
|
});
|
|
1674
1813
|
const wss = setupWebSocket(server, processManager);
|
|
1814
|
+
await listenOnLocalHost(server, wss, port);
|
|
1815
|
+
processManager.attachCleanupHandlers();
|
|
1675
1816
|
const url = `http://${HOST}:${port}`;
|
|
1676
1817
|
if (options.openBrowser !== false) {
|
|
1677
1818
|
await open2(url);
|
|
@@ -1682,18 +1823,8 @@ async function startDevSurfaceServer(options) {
|
|
|
1682
1823
|
processManager,
|
|
1683
1824
|
close: async () => {
|
|
1684
1825
|
processManager.killAll();
|
|
1685
|
-
await
|
|
1686
|
-
|
|
1687
|
-
});
|
|
1688
|
-
await new Promise((resolve, reject) => {
|
|
1689
|
-
server.close((error) => {
|
|
1690
|
-
if (error) {
|
|
1691
|
-
reject(error);
|
|
1692
|
-
} else {
|
|
1693
|
-
resolve();
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
});
|
|
1826
|
+
await closeWebSocketServer(wss);
|
|
1827
|
+
await closeHttpServer(server);
|
|
1697
1828
|
}
|
|
1698
1829
|
};
|
|
1699
1830
|
}
|
|
@@ -1701,7 +1832,7 @@ async function startDevSurfaceServer(options) {
|
|
|
1701
1832
|
// src/cli/commands/start.ts
|
|
1702
1833
|
async function startCommand(options) {
|
|
1703
1834
|
const cwd = options.cwd ?? process.cwd();
|
|
1704
|
-
console.log(pc5.bold(`DevSurface v0.
|
|
1835
|
+
console.log(pc5.bold(`DevSurface v0.2.0`));
|
|
1705
1836
|
console.log("Scanning project...\n");
|
|
1706
1837
|
const scan = await scanProject(cwd);
|
|
1707
1838
|
printScanResult(scan);
|
|
@@ -1738,7 +1869,7 @@ function handle(command) {
|
|
|
1738
1869
|
process.exitCode = 1;
|
|
1739
1870
|
});
|
|
1740
1871
|
}
|
|
1741
|
-
program.name("devsurface").description("Turn any Node.js repository into a local developer control panel.").version("0.
|
|
1872
|
+
program.name("devsurface").description("Turn any Node.js repository into a local developer control panel.").version("0.2.0").option("-p, --port <port>", "dashboard port", toPort, 4567).option("--no-open", "do not open the browser automatically").action((options) => {
|
|
1742
1873
|
handle(
|
|
1743
1874
|
startCommand({
|
|
1744
1875
|
cwd: process.cwd(),
|