@lasso-ai/cli 1.0.9 → 1.0.11
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/cli/host/daemon.js
CHANGED
|
@@ -51,6 +51,7 @@ class LassoHost {
|
|
|
51
51
|
dnsHandle = null;
|
|
52
52
|
resolver;
|
|
53
53
|
registry;
|
|
54
|
+
cleanupOnStart = new Set();
|
|
54
55
|
running = new Map();
|
|
55
56
|
starting = new Map();
|
|
56
57
|
crashLog = new Map();
|
|
@@ -63,6 +64,8 @@ class LassoHost {
|
|
|
63
64
|
this.secureServer.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
64
65
|
this.resolver = (0, dns_1.createDomainResolver)();
|
|
65
66
|
this.registry = (0, registry_1.loadRegistry)();
|
|
67
|
+
for (const domain of Object.keys(this.registry))
|
|
68
|
+
this.cleanupOnStart.add(domain);
|
|
66
69
|
this.log(`host starting (version ${opts.version || "unknown"})`);
|
|
67
70
|
}
|
|
68
71
|
log(message) {
|
|
@@ -119,12 +122,13 @@ class LassoHost {
|
|
|
119
122
|
}
|
|
120
123
|
this.crashLog.set(domain, crashes);
|
|
121
124
|
const startup = (async () => {
|
|
122
|
-
const result = await (0, runtime_1.startProjectRuntime)(project.directory);
|
|
125
|
+
const result = await (0, runtime_1.startProjectRuntime)(project.directory, { cleanupExisting: this.cleanupOnStart.delete(domain) });
|
|
123
126
|
if (!result.ok || !result.runtime) {
|
|
124
127
|
this.crashLog.set(domain, [...crashes, Date.now()]);
|
|
125
128
|
throw new Error(result.error || `Could not start ${domain}.`);
|
|
126
129
|
}
|
|
127
130
|
const runtime = result.runtime;
|
|
131
|
+
this.crashLog.delete(domain);
|
|
128
132
|
this.running.set(domain, runtime);
|
|
129
133
|
runtime.child.on("exit", (code, signal) => {
|
|
130
134
|
if (this.running.get(domain) === runtime) {
|
|
@@ -207,9 +211,12 @@ class LassoHost {
|
|
|
207
211
|
route: "POST /_host/restart",
|
|
208
212
|
handler: async (_req, res) => {
|
|
209
213
|
const stopped = [];
|
|
214
|
+
this.crashLog.clear();
|
|
210
215
|
for (const [domain, runtime] of this.running) {
|
|
211
216
|
runtime.child.kill("SIGTERM");
|
|
212
217
|
this.running.delete(domain);
|
|
218
|
+
this.cleanupOnStart.add(domain);
|
|
219
|
+
this.crashLog.delete(domain);
|
|
213
220
|
stopped.push(domain);
|
|
214
221
|
this.log(`stopped ${domain} for restart`);
|
|
215
222
|
}
|
|
@@ -224,6 +231,8 @@ class LassoHost {
|
|
|
224
231
|
if (!domain)
|
|
225
232
|
return writeJson(res, 400, { ok: false, error: "A project domain is required." });
|
|
226
233
|
const runtime = this.running.get(domain);
|
|
234
|
+
this.cleanupOnStart.add(domain);
|
|
235
|
+
this.crashLog.delete(domain);
|
|
227
236
|
if (runtime) {
|
|
228
237
|
runtime.child.kill("SIGTERM");
|
|
229
238
|
this.running.delete(domain);
|
|
@@ -87,15 +87,18 @@ void (async () => {
|
|
|
87
87
|
const chunks = [];
|
|
88
88
|
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
89
89
|
proxyRes.on("end", () => {
|
|
90
|
-
const body = Buffer.concat(chunks)
|
|
90
|
+
const body = Buffer.concat(chunks);
|
|
91
91
|
const contentType = String(proxyRes.headers["content-type"] || "");
|
|
92
92
|
const headers = { ...proxyRes.headers };
|
|
93
93
|
delete headers["content-length"];
|
|
94
94
|
delete headers["content-encoding"];
|
|
95
95
|
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
if (contentType.includes("text/html")) {
|
|
97
|
+
res.end(body.toString("utf8").replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=${bridgePort}"></script></head>`));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
res.end(body);
|
|
101
|
+
}
|
|
99
102
|
});
|
|
100
103
|
});
|
|
101
104
|
const server = node_http_1.default.createServer((req, res) => {
|
|
@@ -104,12 +107,23 @@ void (async () => {
|
|
|
104
107
|
res.end(node_fs_1.default.readFileSync(bundlePath, "utf8"));
|
|
105
108
|
return;
|
|
106
109
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const forward = (attempt = 0) => {
|
|
111
|
+
proxy.web(req, res, { headers: { "accept-encoding": "identity" } }, (error) => {
|
|
112
|
+
const retryable = error.code === "ECONNREFUSED" ||
|
|
113
|
+
error.code === "ECONNRESET" ||
|
|
114
|
+
error.code === "EPIPE";
|
|
115
|
+
if (!res.headersSent && req.method !== "POST" && retryable && attempt < 12) {
|
|
116
|
+
setTimeout(() => forward(attempt + 1), 250);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.error(`[lasso] Next proxy error for ${req.url}: ${error.message}`);
|
|
120
|
+
if (!res.headersSent) {
|
|
121
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
122
|
+
res.end(`Next.js is still starting: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
forward();
|
|
113
127
|
});
|
|
114
128
|
server.on("upgrade", (req, socket, head) => proxy.ws(req, socket, head));
|
|
115
129
|
try {
|
|
@@ -20,4 +20,6 @@ export interface StartRuntimeResult {
|
|
|
20
20
|
* answers HTTP before returning. Reuses Lasso's existing framework detection —
|
|
21
21
|
* the Host never guesses a package manager or hardcodes a runtime.
|
|
22
22
|
*/
|
|
23
|
-
export declare function startProjectRuntime(directory: string
|
|
23
|
+
export declare function startProjectRuntime(directory: string, options?: {
|
|
24
|
+
cleanupExisting?: boolean;
|
|
25
|
+
}): Promise<StartRuntimeResult>;
|
package/dist/cli/host/runtime.js
CHANGED
|
@@ -11,10 +11,12 @@ const node_net_1 = __importDefault(require("node:net"));
|
|
|
11
11
|
const node_http_1 = __importDefault(require("node:http"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
13
|
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_util_1 = require("node:util");
|
|
14
15
|
const framework_1 = require("../utils/framework");
|
|
15
16
|
const paths_1 = require("./paths");
|
|
16
17
|
const READY_TIMEOUT_MS = 60_000;
|
|
17
18
|
const READY_POLL_MS = 500;
|
|
19
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
18
20
|
function pickFreePort() {
|
|
19
21
|
return new Promise((resolve, reject) => {
|
|
20
22
|
const server = node_net_1.default.createServer();
|
|
@@ -88,18 +90,57 @@ async function waitUntilReady(port) {
|
|
|
88
90
|
}
|
|
89
91
|
return false;
|
|
90
92
|
}
|
|
93
|
+
async function stopExistingNextDev(directory) {
|
|
94
|
+
if (process.platform === "win32")
|
|
95
|
+
return;
|
|
96
|
+
const lockPath = node_path_1.default.join(directory, ".next", "dev", "lock");
|
|
97
|
+
let pid = 0;
|
|
98
|
+
try {
|
|
99
|
+
const lock = JSON.parse(node_fs_1.default.readFileSync(lockPath, "utf8"));
|
|
100
|
+
pid = Number(lock.pid);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 0);
|
|
109
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "command="], { maxBuffer: 16 * 1024 });
|
|
110
|
+
if (!/\bnext(?:-dev)?\b|next.*\bdev\b/i.test(stdout))
|
|
111
|
+
return;
|
|
112
|
+
process.kill(pid, "SIGTERM");
|
|
113
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, 0);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
log(directory, `stopped previous Next.js process ${pid}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
process.kill(pid, "SIGKILL");
|
|
124
|
+
log(directory, `force-stopped previous Next.js process ${pid}`);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The lock may be stale or the process may have exited between checks.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
91
130
|
/**
|
|
92
131
|
* Starts the project's development server on a free port and waits until it
|
|
93
132
|
* answers HTTP before returning. Reuses Lasso's existing framework detection —
|
|
94
133
|
* the Host never guesses a package manager or hardcodes a runtime.
|
|
95
134
|
*/
|
|
96
|
-
async function startProjectRuntime(directory) {
|
|
135
|
+
async function startProjectRuntime(directory, options = {}) {
|
|
97
136
|
const port = await pickFreePort();
|
|
98
137
|
const bridgePort = await pickFreePort();
|
|
99
138
|
const plan = spawnCommand(directory, port, bridgePort);
|
|
100
139
|
if (!plan) {
|
|
101
140
|
return { ok: false, error: `${node_path_1.default.basename(directory)} is not a Vite or Next.js project, so Lasso Host can't start it automatically. Run its dev server yourself.` };
|
|
102
141
|
}
|
|
142
|
+
if (options.cleanupExisting && plan.framework === "next")
|
|
143
|
+
await stopExistingNextDev(directory);
|
|
103
144
|
const child = (0, node_child_process_1.spawn)(plan.command, plan.args, {
|
|
104
145
|
cwd: directory,
|
|
105
146
|
env: { ...process.env, PORT: String(port) },
|
package/dist/cli/server/next.js
CHANGED
|
@@ -37,14 +37,14 @@ async function startNextServer(cwd) {
|
|
|
37
37
|
const chunks = [];
|
|
38
38
|
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
39
39
|
proxyRes.on("end", () => {
|
|
40
|
-
const body = Buffer.concat(chunks)
|
|
40
|
+
const body = Buffer.concat(chunks);
|
|
41
41
|
const contentType = proxyRes.headers["content-type"] || "";
|
|
42
42
|
const headers = { ...proxyRes.headers };
|
|
43
43
|
delete headers["content-length"];
|
|
44
44
|
delete headers["content-encoding"];
|
|
45
45
|
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
46
46
|
if (contentType.includes("text/html")) {
|
|
47
|
-
res.end(body.replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=3056"></script></head>`));
|
|
47
|
+
res.end(body.toString("utf-8").replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=3056"></script></head>`));
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
res.end(body);
|
package/package.json
CHANGED