@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.
@@ -12,6 +12,7 @@ export declare class LassoHost {
12
12
  private dnsHandle;
13
13
  private readonly resolver;
14
14
  private readonly registry;
15
+ private readonly cleanupOnStart;
15
16
  private readonly running;
16
17
  private readonly starting;
17
18
  private readonly crashLog;
@@ -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).toString("utf8");
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
- res.end(contentType.includes("text/html")
97
- ? body.replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=${bridgePort}"></script></head>`)
98
- : body);
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
- proxy.web(req, res, { headers: { "accept-encoding": "identity" } }, (error) => {
108
- if (!res.headersSent) {
109
- res.writeHead(502, { "content-type": "text/plain" });
110
- res.end(`Next.js is still starting: ${error.message}`);
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): Promise<StartRuntimeResult>;
23
+ export declare function startProjectRuntime(directory: string, options?: {
24
+ cleanupExisting?: boolean;
25
+ }): Promise<StartRuntimeResult>;
@@ -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) },
@@ -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).toString("utf-8");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",