@lasso-ai/cli 1.0.10 → 1.0.12

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/README.md CHANGED
@@ -217,6 +217,10 @@ project is served at `http://app.lasso:<port>` or the secure
217
217
  domain (reuse, generate, or change one); never duplicates.
218
218
  - `lasso unregister [domain]` — remove the current directory’s `.lasso`
219
219
  registration, or pass a domain explicitly.
220
+
221
+ For Next.js projects, `lasso init` and `lasso register` automatically add the
222
+ project’s `.lasso` domain to `allowedDevOrigins` in `next.config.*`, allowing
223
+ HMR and development assets to work through the secure Host URL.
220
224
  - `lasso projects` — list registered domains and running state.
221
225
  - `lasso daemon install/uninstall` — attach a macOS LaunchAgent (auto-start on
222
226
  login) and configure the system DNS resolver so bare `app.lasso` works.
@@ -128,6 +128,7 @@ class LassoHost {
128
128
  throw new Error(result.error || `Could not start ${domain}.`);
129
129
  }
130
130
  const runtime = result.runtime;
131
+ this.crashLog.delete(domain);
131
132
  this.running.set(domain, runtime);
132
133
  runtime.child.on("exit", (code, signal) => {
133
134
  if (this.running.get(domain) === runtime) {
@@ -210,10 +211,12 @@ class LassoHost {
210
211
  route: "POST /_host/restart",
211
212
  handler: async (_req, res) => {
212
213
  const stopped = [];
214
+ this.crashLog.clear();
213
215
  for (const [domain, runtime] of this.running) {
214
216
  runtime.child.kill("SIGTERM");
215
217
  this.running.delete(domain);
216
218
  this.cleanupOnStart.add(domain);
219
+ this.crashLog.delete(domain);
217
220
  stopped.push(domain);
218
221
  this.log(`stopped ${domain} for restart`);
219
222
  }
@@ -229,6 +232,7 @@ class LassoHost {
229
232
  return writeJson(res, 400, { ok: false, error: "A project domain is required." });
230
233
  const runtime = this.running.get(domain);
231
234
  this.cleanupOnStart.add(domain);
235
+ this.crashLog.delete(domain);
232
236
  if (runtime) {
233
237
  runtime.child.kill("SIGTERM");
234
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 {
package/dist/cli/index.js CHANGED
@@ -19,6 +19,7 @@ const paths_1 = require("./host/paths");
19
19
  const install_1 = require("./host/install");
20
20
  const client_1 = require("./host/client");
21
21
  const registry_1 = require("./host/registry");
22
+ const next_config_1 = require("./next-config");
22
23
  const package_json_1 = __importDefault(require("../../package.json"));
23
24
  const VERSION = package_json_1.default.version;
24
25
  const program = new commander_1.Command();
@@ -279,6 +280,13 @@ program.command("register [domain]")
279
280
  console.log(chalk_1.default.dim(` Added domain to ${project_1.LASSO_CONFIG_FILE}.`));
280
281
  }
281
282
  }
283
+ if ((0, framework_1.detectFramework)(cwd) === "next") {
284
+ const injected = (0, next_config_1.ensureNextAllowedDevOrigin)(cwd, domain);
285
+ if (!injected.ok)
286
+ console.warn(chalk_1.default.yellow("!") + ` Could not add ${domain} to Next.js allowedDevOrigins: ${injected.reason}`);
287
+ else if (injected.changed)
288
+ console.log(chalk_1.default.green("✓") + ` Added ${chalk_1.default.cyan(domain)} to ${node_path_1.default.basename(injected.file)}`);
289
+ }
282
290
  });
283
291
  // prettier-ignore
284
292
  program.command("unregister [domain]")
@@ -0,0 +1,8 @@
1
+ export type NextConfigInjectionResult = {
2
+ ok: boolean;
3
+ changed: boolean;
4
+ file?: string;
5
+ reason?: string;
6
+ };
7
+ /** Adds the Host domain without replacing existing Next.js configuration. */
8
+ export declare function ensureNextAllowedDevOrigin(directory: string, domain: string): NextConfigInjectionResult;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureNextAllowedDevOrigin = ensureNextAllowedDevOrigin;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const CONFIG_NAMES = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
10
+ function configFile(directory) {
11
+ for (const name of CONFIG_NAMES) {
12
+ const file = node_path_1.default.join(directory, name);
13
+ if (node_fs_1.default.existsSync(file))
14
+ return file;
15
+ }
16
+ return null;
17
+ }
18
+ /** Adds the Host domain without replacing existing Next.js configuration. */
19
+ function ensureNextAllowedDevOrigin(directory, domain) {
20
+ const file = configFile(directory);
21
+ if (!file)
22
+ return { ok: false, changed: false, reason: "No next.config file was found." };
23
+ const source = node_fs_1.default.readFileSync(file, "utf8");
24
+ const quotedDomain = JSON.stringify(domain);
25
+ const existingOrigin = new RegExp(`(?:["']${domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"])`);
26
+ if (existingOrigin.test(source))
27
+ return { ok: true, changed: false, file };
28
+ const originsProperty = /(allowedDevOrigins\s*:\s*\[)([\s\S]*?)(\])/m;
29
+ const propertyMatch = source.match(originsProperty);
30
+ if (propertyMatch && propertyMatch.index !== undefined) {
31
+ const current = propertyMatch[2].trim();
32
+ const replacement = `${propertyMatch[1]}${current ? `${propertyMatch[2].trimEnd()}, ` : ""}${quotedDomain}${propertyMatch[3]}`;
33
+ const nextSource = source.slice(0, propertyMatch.index) + replacement + source.slice(propertyMatch.index + propertyMatch[0].length);
34
+ node_fs_1.default.writeFileSync(file, nextSource);
35
+ return { ok: true, changed: true, file };
36
+ }
37
+ const objectStart = source.search(/(?:const\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*|module\.exports\s*=\s*|export\s+default\s+)\{/m);
38
+ if (objectStart < 0) {
39
+ return { ok: false, changed: false, file, reason: "The Next config does not expose a plain configuration object." };
40
+ }
41
+ const brace = source.indexOf("{", objectStart);
42
+ const insertion = `\n allowedDevOrigins: [${quotedDomain}],`;
43
+ node_fs_1.default.writeFileSync(file, source.slice(0, brace + 1) + insertion + source.slice(brace + 1));
44
+ return { ok: true, changed: true, file };
45
+ }
@@ -21,6 +21,7 @@ const auth_1 = require("./auth");
21
21
  const registry_1 = require("./host/registry");
22
22
  const client_1 = require("./host/client");
23
23
  const paths_1 = require("./host/paths");
24
+ const next_config_1 = require("./next-config");
24
25
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
25
26
  exports.LASSO_CONFIG_FILE = "lasso.config.json";
26
27
  const PROJECT_STATE_FILE = ".lasso-project.json";
@@ -224,6 +225,13 @@ async function initProject(cwd, fileEnv) {
224
225
  const existing = (0, registry_1.findByDirectory)(registry, cwd);
225
226
  const domain = existing ? existing.domain : (0, registry_1.generateUniqueDomain)(cwd, registry);
226
227
  await (0, client_1.registerWithHost)(domain, cwd, projectId, (0, paths_1.hostProxyPort)(fileEnv));
228
+ if (framework === "next") {
229
+ const injected = (0, next_config_1.ensureNextAllowedDevOrigin)(cwd, domain);
230
+ if (!injected.ok)
231
+ console.warn(chalk_1.default.yellow("!") + ` Could not add ${domain} to Next.js allowedDevOrigins: ${injected.reason}`);
232
+ else if (injected.changed)
233
+ console.log(chalk_1.default.green("✓") + ` Added ${chalk_1.default.cyan(domain)} to ${node_path_1.default.basename(injected.file)}`);
234
+ }
227
235
  node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, exports.LASSO_CONFIG_FILE), JSON.stringify({ id: projectId, domain }, null, 2) + "\n");
228
236
  return {
229
237
  ok: true,
@@ -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.10",
3
+ "version": "1.0.12",
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",