@novastorm-ai/cli 0.0.1 → 0.0.4
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/LICENSE.md +100 -0
- package/dist/bin/nova.js +4 -2
- package/dist/chunk-4AQQAQBM.js +509 -0
- package/dist/{chunk-NFNZMCLQ.js → chunk-CLQXFM4X.js} +355 -48
- package/dist/chunk-KKTDQOQX.js +7359 -0
- package/dist/{chunk-FYSTZ6K6.js → chunk-QKD6A4EK.js} +6 -6
- package/dist/dist-6FOBVQ63.js +13 -0
- package/dist/dist-EMATXD3M.js +151 -0
- package/dist/index.js +4 -2
- package/dist/{package-3YCVE5UE.js → package-BDGFABJG.js} +12 -5
- package/dist/{setup-3KREUXRO.js → setup-L5TRND4P.js} +2 -1
- package/package.json +21 -12
package/LICENSE.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Novastorm License
|
|
2
|
+
|
|
3
|
+
> **Summary:** Free for individuals and small teams (up to 3 developers). Larger teams require a paid license. After March 20, 2029, the entire codebase becomes MIT-licensed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Business Source License 1.1
|
|
8
|
+
|
|
9
|
+
**Licensor:** Uladzimir Pranevich (NIP: PL8992922668)
|
|
10
|
+
|
|
11
|
+
**Licensed Work:** Novastorm — an ambient development toolkit.
|
|
12
|
+
The Licensed Work is (c) 2026 Uladzimir Pranevich.
|
|
13
|
+
|
|
14
|
+
**Additional Use Grant:** You may use the Licensed Work for any purpose, including production commercial use, provided that you do not use it in a way that competes with the Licensed Work. A competing use is one that offers the Licensed Work to third parties as a commercial development tool or platform.
|
|
15
|
+
|
|
16
|
+
**Change Date:** March 20, 2029
|
|
17
|
+
|
|
18
|
+
**Change License:** MIT
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Free License
|
|
23
|
+
|
|
24
|
+
Copyright (c) 2026 Uladzimir Pranevich
|
|
25
|
+
|
|
26
|
+
Permission is hereby granted, free of charge, to any person or entity meeting the eligibility criteria below ("Eligible User"), to use, copy, modify, and run the Licensed Work, subject to the conditions of this license.
|
|
27
|
+
|
|
28
|
+
### Eligibility
|
|
29
|
+
|
|
30
|
+
You qualify as an Eligible User if **any** of the following apply:
|
|
31
|
+
|
|
32
|
+
- You are an **individual developer** (sole proprietor, freelancer, hobbyist)
|
|
33
|
+
- You are part of a **team of 3 or fewer developers** (measured by unique git commit authors within a 90-day sliding window)
|
|
34
|
+
- You are using Novastorm for an **open-source project** published under an OSI-approved license
|
|
35
|
+
- You are a **student, educator, or academic researcher**
|
|
36
|
+
- You are **evaluating** Novastorm and have not yet used it in production
|
|
37
|
+
|
|
38
|
+
### Permitted Use
|
|
39
|
+
|
|
40
|
+
- Use the Licensed Work to develop, modify, and deploy your own applications
|
|
41
|
+
- Fork and modify the source code for your own use cases
|
|
42
|
+
- Contribute modifications back to the project
|
|
43
|
+
|
|
44
|
+
### Restrictions
|
|
45
|
+
|
|
46
|
+
You may **not**:
|
|
47
|
+
|
|
48
|
+
- Copy or modify the Licensed Work for the purpose of selling, renting, licensing, relicensing, or sublicensing your own derivative of Novastorm as a development tool or platform
|
|
49
|
+
- Remove or alter license validation, license notices, or telemetry attribution
|
|
50
|
+
- Misrepresent the origin of the Licensed Work
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Company License
|
|
55
|
+
|
|
56
|
+
If you do not meet the eligibility criteria above — specifically, if your project has **more than 3 unique human developers** committing within a 90-day window and the project is **not open source** — you must purchase a Company License.
|
|
57
|
+
|
|
58
|
+
Visit [https://cli.novastorm.ai/#pricing](https://cli.novastorm.ai/#pricing) for pricing and license keys.
|
|
59
|
+
|
|
60
|
+
### How Developer Count Works
|
|
61
|
+
|
|
62
|
+
Novastorm counts unique git commit author emails within a 90-day sliding window. Bot accounts (dependabot, renovate, github-actions, etc.) are automatically excluded. Email normalization strips `+tags` and lowercases addresses to avoid counting the same person twice.
|
|
63
|
+
|
|
64
|
+
### What Happens Without a License
|
|
65
|
+
|
|
66
|
+
Novastorm continues to work but displays periodic license nudge messages. It does **not** hard-block usage.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Change Date — March 20, 2029
|
|
71
|
+
|
|
72
|
+
On the Change Date, the Licensed Work will be made available under the MIT License:
|
|
73
|
+
|
|
74
|
+
> MIT License
|
|
75
|
+
>
|
|
76
|
+
> Copyright (c) 2026 Uladzimir Pranevich
|
|
77
|
+
>
|
|
78
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
79
|
+
>
|
|
80
|
+
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Warranty Disclaimer
|
|
85
|
+
|
|
86
|
+
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Contributing
|
|
91
|
+
|
|
92
|
+
By contributing to Novastorm, you agree that your contributions will be licensed under the same Business Source License 1.1 terms, and will transition to MIT on the Change Date along with the rest of the codebase.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Notice
|
|
97
|
+
|
|
98
|
+
This license is based on the Business Source License 1.1, available at [https://mariadb.com/bsl11/](https://mariadb.com/bsl11/).
|
|
99
|
+
|
|
100
|
+
For questions about licensing, see the [License FAQ](docs/license-faq.md).
|
package/dist/bin/nova.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
run
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-CLQXFM4X.js";
|
|
5
|
+
import "../chunk-4AQQAQBM.js";
|
|
6
|
+
import "../chunk-QKD6A4EK.js";
|
|
7
|
+
import "../chunk-KKTDQOQX.js";
|
|
6
8
|
import "../chunk-3RG5ZIWI.js";
|
|
7
9
|
|
|
8
10
|
// bin/nova.ts
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// ../proxy/dist/index.js
|
|
2
|
+
import http from "http";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import zlib from "zlib";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import httpProxy from "http-proxy";
|
|
7
|
+
import { WebSocketServer as WsServer } from "ws";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import http2 from "http";
|
|
10
|
+
import { URL } from "url";
|
|
11
|
+
var SCRIPT_TAG = '<script src="/nova-overlay.js"></script>';
|
|
12
|
+
var ProxyServer = class {
|
|
13
|
+
server = null;
|
|
14
|
+
proxy = null;
|
|
15
|
+
running = false;
|
|
16
|
+
projectMapApi = null;
|
|
17
|
+
/** Returns the underlying http.Server (used by WebSocketServer). */
|
|
18
|
+
getHttpServer() {
|
|
19
|
+
return this.server;
|
|
20
|
+
}
|
|
21
|
+
setProjectMapApi(api) {
|
|
22
|
+
this.projectMapApi = api;
|
|
23
|
+
}
|
|
24
|
+
async start(targetPort, proxyPort, overlayScriptPath) {
|
|
25
|
+
if (this.running) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.proxy = httpProxy.createProxyServer({
|
|
29
|
+
target: `http://127.0.0.1:${targetPort}`,
|
|
30
|
+
selfHandleResponse: true
|
|
31
|
+
});
|
|
32
|
+
this.proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
33
|
+
const headers = { ...proxyRes.headers };
|
|
34
|
+
delete headers["content-security-policy"];
|
|
35
|
+
delete headers["content-security-policy-report-only"];
|
|
36
|
+
const contentType = proxyRes.headers["content-type"] ?? "";
|
|
37
|
+
const isHtml = contentType.includes("text/html");
|
|
38
|
+
if (!isHtml) {
|
|
39
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
40
|
+
if (value !== void 0) {
|
|
41
|
+
res.setHeader(key, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
res.writeHead(proxyRes.statusCode ?? 200);
|
|
45
|
+
proxyRes.pipe(res);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const encoding = proxyRes.headers["content-encoding"];
|
|
49
|
+
let stream = proxyRes;
|
|
50
|
+
if (encoding === "gzip") {
|
|
51
|
+
stream = proxyRes.pipe(zlib.createGunzip());
|
|
52
|
+
} else if (encoding === "br") {
|
|
53
|
+
stream = proxyRes.pipe(zlib.createBrotliDecompress());
|
|
54
|
+
} else if (encoding === "deflate") {
|
|
55
|
+
stream = proxyRes.pipe(zlib.createInflate());
|
|
56
|
+
}
|
|
57
|
+
const chunks = [];
|
|
58
|
+
stream.on("data", (chunk) => {
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
});
|
|
61
|
+
stream.on("error", () => {
|
|
62
|
+
if (!res.headersSent) {
|
|
63
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
64
|
+
res.end("Nova Proxy: failed to decompress response");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
stream.on("end", () => {
|
|
68
|
+
let body = Buffer.concat(chunks).toString("utf-8");
|
|
69
|
+
if (body.includes("</body>")) {
|
|
70
|
+
body = body.replace("</body>", `${SCRIPT_TAG}</body>`);
|
|
71
|
+
} else if (body.includes("</html>")) {
|
|
72
|
+
body = body.replace("</html>", `${SCRIPT_TAG}</html>`);
|
|
73
|
+
} else {
|
|
74
|
+
body += SCRIPT_TAG;
|
|
75
|
+
}
|
|
76
|
+
delete headers["content-length"];
|
|
77
|
+
delete headers["content-encoding"];
|
|
78
|
+
delete headers["transfer-encoding"];
|
|
79
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
80
|
+
if (value !== void 0) {
|
|
81
|
+
res.setHeader(key, value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
res.writeHead(proxyRes.statusCode ?? 200);
|
|
85
|
+
res.end(body);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const proxyRef = this.proxy;
|
|
89
|
+
this.proxy.on("error", (err, req, res) => {
|
|
90
|
+
if (res instanceof http.ServerResponse && !res.headersSent) {
|
|
91
|
+
proxyRef.web(req, res, { target: `http://[::1]:${targetPort}` }, (retryErr) => {
|
|
92
|
+
if (res instanceof http.ServerResponse && !res.headersSent) {
|
|
93
|
+
res.writeHead(502, { "Content-Type": "text/html" });
|
|
94
|
+
res.end(`
|
|
95
|
+
<html><body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
96
|
+
<div style="text-align:center">
|
|
97
|
+
<h2>Nova Proxy</h2>
|
|
98
|
+
<p>Waiting for dev server on port ${targetPort}...</p>
|
|
99
|
+
<p style="color:#888">This page will auto-refresh.</p>
|
|
100
|
+
<script>setTimeout(() => location.reload(), 2000)</script>
|
|
101
|
+
</div>
|
|
102
|
+
</body></html>
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
this.server = http.createServer((req, res) => {
|
|
109
|
+
if (req.url === "/nova-project-map") {
|
|
110
|
+
const mapPath = path.join(import.meta.dirname, "..", "static", "project-map.html");
|
|
111
|
+
fs.readFile(mapPath, (err, data) => {
|
|
112
|
+
if (err) {
|
|
113
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
114
|
+
res.end("project-map.html not found");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.writeHead(200, {
|
|
118
|
+
"Content-Type": "text/html",
|
|
119
|
+
"Cache-Control": "no-cache"
|
|
120
|
+
});
|
|
121
|
+
res.end(data);
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (req.url?.startsWith("/nova-api/") && this.projectMapApi) {
|
|
126
|
+
this.projectMapApi.handleRequest(req, res).catch(() => {
|
|
127
|
+
if (!res.headersSent) {
|
|
128
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
129
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (req.url === "/nova-overlay.js") {
|
|
135
|
+
fs.readFile(overlayScriptPath, (err, data) => {
|
|
136
|
+
if (err) {
|
|
137
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
138
|
+
res.end("nova-overlay.js not found");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.writeHead(200, {
|
|
142
|
+
"Content-Type": "application/javascript",
|
|
143
|
+
"Cache-Control": "no-cache"
|
|
144
|
+
});
|
|
145
|
+
res.end(data);
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.proxy.web(req, res);
|
|
150
|
+
});
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
this.server.on("error", (err) => {
|
|
153
|
+
if (err.code === "EADDRINUSE") {
|
|
154
|
+
reject(new Error(`Port ${proxyPort} is already in use`));
|
|
155
|
+
} else {
|
|
156
|
+
reject(err);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
this.server.listen(proxyPort, () => {
|
|
160
|
+
this.running = true;
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async stop() {
|
|
166
|
+
if (!this.running) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
this.proxy?.close();
|
|
171
|
+
this.server?.close((err) => {
|
|
172
|
+
this.running = false;
|
|
173
|
+
this.server = null;
|
|
174
|
+
this.proxy = null;
|
|
175
|
+
if (err) {
|
|
176
|
+
reject(err);
|
|
177
|
+
} else {
|
|
178
|
+
resolve();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
isRunning() {
|
|
184
|
+
return this.running;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var WebSocketServer = class {
|
|
188
|
+
wss = null;
|
|
189
|
+
observationHandlers = [];
|
|
190
|
+
confirmHandlers = [];
|
|
191
|
+
cancelHandlers = [];
|
|
192
|
+
appendHandlers = [];
|
|
193
|
+
browserErrorHandlers = [];
|
|
194
|
+
secretsSubmitHandlers = [];
|
|
195
|
+
start(httpServer) {
|
|
196
|
+
this.wss = new WsServer({
|
|
197
|
+
server: httpServer,
|
|
198
|
+
path: "/nova-ws"
|
|
199
|
+
});
|
|
200
|
+
this.wss.on("connection", (ws) => {
|
|
201
|
+
ws.on("message", (data) => {
|
|
202
|
+
try {
|
|
203
|
+
const raw = typeof data === "string" ? data : data.toString("utf-8");
|
|
204
|
+
const parsed = JSON.parse(raw);
|
|
205
|
+
if (parsed.type === "confirm") {
|
|
206
|
+
for (const handler of this.confirmHandlers) {
|
|
207
|
+
handler();
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (parsed.type === "cancel") {
|
|
212
|
+
for (const handler of this.cancelHandlers) {
|
|
213
|
+
handler();
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (parsed.type === "append") {
|
|
218
|
+
const text = parsed.data?.text ?? "";
|
|
219
|
+
for (const handler of this.appendHandlers) {
|
|
220
|
+
handler(text);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (parsed.type === "browser_error") {
|
|
225
|
+
const error = parsed.data?.error ?? "";
|
|
226
|
+
for (const handler of this.browserErrorHandlers) {
|
|
227
|
+
handler(error);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (parsed.type === "secrets_submit") {
|
|
232
|
+
const secrets = parsed.data?.secrets ?? {};
|
|
233
|
+
for (const handler of this.secretsSubmitHandlers) {
|
|
234
|
+
handler(secrets);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const obsData = parsed.data ?? parsed;
|
|
239
|
+
const observation = {
|
|
240
|
+
screenshot: obsData.screenshotBase64 ? Buffer.from(obsData.screenshotBase64, "base64") : obsData.screenshot instanceof Buffer ? obsData.screenshot : Buffer.alloc(0),
|
|
241
|
+
clickCoords: obsData.clickCoords,
|
|
242
|
+
domSnapshot: obsData.domSnapshot,
|
|
243
|
+
transcript: obsData.transcript,
|
|
244
|
+
currentUrl: obsData.currentUrl ?? "",
|
|
245
|
+
consoleErrors: obsData.consoleErrors,
|
|
246
|
+
timestamp: obsData.timestamp ?? Date.now(),
|
|
247
|
+
gestureContext: obsData.gestureContext
|
|
248
|
+
};
|
|
249
|
+
const autoExecute = obsData.autoExecute === true;
|
|
250
|
+
for (const handler of this.observationHandlers) {
|
|
251
|
+
handler(observation, autoExecute);
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
onObservation(handler) {
|
|
259
|
+
this.observationHandlers.push(handler);
|
|
260
|
+
}
|
|
261
|
+
onConfirm(handler) {
|
|
262
|
+
this.confirmHandlers.push(handler);
|
|
263
|
+
}
|
|
264
|
+
onCancel(handler) {
|
|
265
|
+
this.cancelHandlers.push(handler);
|
|
266
|
+
}
|
|
267
|
+
onAppend(handler) {
|
|
268
|
+
this.appendHandlers.push(handler);
|
|
269
|
+
}
|
|
270
|
+
onBrowserError(handler) {
|
|
271
|
+
this.browserErrorHandlers.push(handler);
|
|
272
|
+
}
|
|
273
|
+
onSecretsSubmit(handler) {
|
|
274
|
+
this.secretsSubmitHandlers.push(handler);
|
|
275
|
+
}
|
|
276
|
+
sendEvent(event) {
|
|
277
|
+
if (!this.wss) return;
|
|
278
|
+
const payload = JSON.stringify(event);
|
|
279
|
+
for (const client of this.wss.clients) {
|
|
280
|
+
if (client.readyState === 1) {
|
|
281
|
+
client.send(payload);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
getClientCount() {
|
|
286
|
+
return this.wss?.clients.size ?? 0;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var POLL_INTERVAL_MS = 500;
|
|
290
|
+
var MAX_WAIT_MS = 3e4;
|
|
291
|
+
var DevServerRunner = class {
|
|
292
|
+
process = null;
|
|
293
|
+
logs = [];
|
|
294
|
+
running = false;
|
|
295
|
+
readyHandler = null;
|
|
296
|
+
errorHandler = null;
|
|
297
|
+
outputHandlers = [];
|
|
298
|
+
async spawn(command, cwd, port) {
|
|
299
|
+
const [cmd, ...args] = command.split(" ");
|
|
300
|
+
this.process = spawn(cmd, args, {
|
|
301
|
+
cwd,
|
|
302
|
+
shell: true,
|
|
303
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
304
|
+
env: { ...process.env }
|
|
305
|
+
});
|
|
306
|
+
this.running = true;
|
|
307
|
+
this.logs = [];
|
|
308
|
+
this.process.stdout?.on("data", (data) => {
|
|
309
|
+
const text = data.toString();
|
|
310
|
+
this.logs.push(text);
|
|
311
|
+
for (const handler of this.outputHandlers) {
|
|
312
|
+
handler(text);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
this.process.stderr?.on("data", (data) => {
|
|
316
|
+
const text = data.toString();
|
|
317
|
+
this.logs.push(text);
|
|
318
|
+
for (const handler of this.outputHandlers) {
|
|
319
|
+
handler(text);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
this.process.on("exit", (code, signal) => {
|
|
323
|
+
this.running = false;
|
|
324
|
+
if (code !== 0 && code !== null) {
|
|
325
|
+
this.errorHandler?.(
|
|
326
|
+
`Dev server exited with code ${code}${signal ? ` (${signal})` : ""}`
|
|
327
|
+
);
|
|
328
|
+
} else if (signal) {
|
|
329
|
+
this.errorHandler?.(`Dev server killed by signal ${signal}`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
this.process.on("error", (err) => {
|
|
333
|
+
this.running = false;
|
|
334
|
+
this.errorHandler?.(err.message);
|
|
335
|
+
});
|
|
336
|
+
await this.pollUntilReady(port);
|
|
337
|
+
}
|
|
338
|
+
onReady(handler) {
|
|
339
|
+
this.readyHandler = handler;
|
|
340
|
+
}
|
|
341
|
+
onError(handler) {
|
|
342
|
+
this.errorHandler = handler;
|
|
343
|
+
}
|
|
344
|
+
onOutput(handler) {
|
|
345
|
+
this.outputHandlers.push(handler);
|
|
346
|
+
}
|
|
347
|
+
getLogs() {
|
|
348
|
+
return this.logs.join("");
|
|
349
|
+
}
|
|
350
|
+
async kill() {
|
|
351
|
+
if (!this.process || !this.running) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const proc = this.process;
|
|
355
|
+
await new Promise((resolve) => {
|
|
356
|
+
const killTimer = setTimeout(() => {
|
|
357
|
+
proc.kill("SIGKILL");
|
|
358
|
+
}, 5e3);
|
|
359
|
+
proc.on("exit", () => {
|
|
360
|
+
clearTimeout(killTimer);
|
|
361
|
+
this.running = false;
|
|
362
|
+
this.process = null;
|
|
363
|
+
resolve();
|
|
364
|
+
});
|
|
365
|
+
proc.kill("SIGTERM");
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
isRunning() {
|
|
369
|
+
return this.running;
|
|
370
|
+
}
|
|
371
|
+
pollUntilReady(port) {
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const startTime = Date.now();
|
|
374
|
+
const check = () => {
|
|
375
|
+
if (!this.running) {
|
|
376
|
+
reject(
|
|
377
|
+
new Error(
|
|
378
|
+
`Dev server process exited before becoming ready. Logs:
|
|
379
|
+
${this.getLogs()}`
|
|
380
|
+
)
|
|
381
|
+
);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const tryConnect = (host, fallback) => {
|
|
385
|
+
const req = http2.get(
|
|
386
|
+
`http://${host}:${port}`,
|
|
387
|
+
(res) => {
|
|
388
|
+
res.resume();
|
|
389
|
+
this.readyHandler?.();
|
|
390
|
+
resolve();
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
req.on("error", () => {
|
|
394
|
+
if (fallback) {
|
|
395
|
+
tryConnect(fallback);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (Date.now() - startTime >= MAX_WAIT_MS) {
|
|
399
|
+
reject(
|
|
400
|
+
new Error(
|
|
401
|
+
`Dev server did not become ready within ${MAX_WAIT_MS / 1e3}s. Logs:
|
|
402
|
+
${this.getLogs()}`
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
setTimeout(check, POLL_INTERVAL_MS);
|
|
408
|
+
});
|
|
409
|
+
req.end();
|
|
410
|
+
};
|
|
411
|
+
tryConnect("127.0.0.1", "[::1]");
|
|
412
|
+
};
|
|
413
|
+
check();
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
var ProjectMapApi = class {
|
|
418
|
+
graphStore = null;
|
|
419
|
+
searchRouter = null;
|
|
420
|
+
analysis = null;
|
|
421
|
+
activeFiles = [];
|
|
422
|
+
setGraphStore(store) {
|
|
423
|
+
this.graphStore = store;
|
|
424
|
+
}
|
|
425
|
+
setSearchRouter(router) {
|
|
426
|
+
this.searchRouter = router;
|
|
427
|
+
}
|
|
428
|
+
setAnalysis(analysis) {
|
|
429
|
+
this.analysis = analysis;
|
|
430
|
+
}
|
|
431
|
+
setActiveFiles(files) {
|
|
432
|
+
this.activeFiles = files;
|
|
433
|
+
}
|
|
434
|
+
async handleRequest(req, res) {
|
|
435
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
436
|
+
if (url.pathname === "/nova-api/project-map" && req.method === "GET") {
|
|
437
|
+
await this.handleGetMap(res);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (url.pathname === "/nova-api/project-map/search" && req.method === "GET") {
|
|
441
|
+
const query = url.searchParams.get("q") ?? "";
|
|
442
|
+
await this.handleSearch(res, query);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
async handleGetMap(res) {
|
|
448
|
+
if (!this.graphStore) {
|
|
449
|
+
this.sendJson(res, 503, { error: "Graph store not initialized" });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const nodes = await this.graphStore.load();
|
|
453
|
+
const mapNodes = nodes.map((n) => {
|
|
454
|
+
const methods = this.analysis?.methods.filter((m) => m.filePath === n.filePath).map((m) => ({ name: m.name, signature: m.signature, purpose: m.purpose })) ?? [];
|
|
455
|
+
return {
|
|
456
|
+
id: n.filePath,
|
|
457
|
+
label: n.filePath.split("/").pop() ?? n.filePath,
|
|
458
|
+
type: n.type,
|
|
459
|
+
exports: n.exports,
|
|
460
|
+
keywords: n.keywords,
|
|
461
|
+
route: n.route,
|
|
462
|
+
methods
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
const mapEdges = [];
|
|
466
|
+
for (const node of nodes) {
|
|
467
|
+
for (const imp of node.imports) {
|
|
468
|
+
if (nodes.some((n) => n.filePath === imp)) {
|
|
469
|
+
mapEdges.push({ source: node.filePath, target: imp });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const data = {
|
|
474
|
+
nodes: mapNodes,
|
|
475
|
+
edges: mapEdges,
|
|
476
|
+
analysis: this.analysis,
|
|
477
|
+
activeFiles: this.activeFiles
|
|
478
|
+
};
|
|
479
|
+
this.sendJson(res, 200, data);
|
|
480
|
+
}
|
|
481
|
+
async handleSearch(res, query) {
|
|
482
|
+
if (!query) {
|
|
483
|
+
this.sendJson(res, 400, { error: 'Missing query parameter "q"' });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!this.searchRouter) {
|
|
487
|
+
this.sendJson(res, 503, { error: "Search router not initialized" });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const results = await this.searchRouter.search(query, 20);
|
|
491
|
+
this.sendJson(res, 200, { results });
|
|
492
|
+
}
|
|
493
|
+
sendJson(res, status, data) {
|
|
494
|
+
const body = JSON.stringify(data);
|
|
495
|
+
res.writeHead(status, {
|
|
496
|
+
"Content-Type": "application/json",
|
|
497
|
+
"Access-Control-Allow-Origin": "*",
|
|
498
|
+
"Cache-Control": "no-cache"
|
|
499
|
+
});
|
|
500
|
+
res.end(body);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export {
|
|
505
|
+
ProxyServer,
|
|
506
|
+
WebSocketServer,
|
|
507
|
+
DevServerRunner,
|
|
508
|
+
ProjectMapApi
|
|
509
|
+
};
|