layero 0.7.0 → 0.7.1

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/agent.js CHANGED
@@ -124,6 +124,13 @@ function renderHuman(event) {
124
124
  case "setup_applied":
125
125
  process.stdout.write(`✓ Setup applied\n`);
126
126
  break;
127
+ case "runtime_type_applied":
128
+ process.stdout.write(`✓ Project type set to ${event.project_type}\n`);
129
+ break;
130
+ case "runtime_type_apply_failed":
131
+ process.stdout.write(`! Failed to set project_type automatically: ${event.error}\n`);
132
+ process.stdout.write(` Build may fail at detect; accept the suggestion in the dashboard if so.\n`);
133
+ break;
127
134
  case "deploy_started":
128
135
  process.stdout.write(`→ Building...\n`);
129
136
  break;
package/dist/api.js CHANGED
@@ -68,6 +68,9 @@ export class ApiClient {
68
68
  completeSetup(projectId, input) {
69
69
  return this.request("POST", `/projects/${projectId}/setup`, input);
70
70
  }
71
+ setRuntimeType(projectId, projectType) {
72
+ return this.request("POST", `/projects/${projectId}/runtime-type`, { project_type: projectType });
73
+ }
71
74
  updateProject(projectId, input) {
72
75
  return this.request("PATCH", `/projects/${projectId}`, input);
73
76
  }
@@ -213,6 +213,7 @@ async function resolveSetupConfig(cwd, opts, existing) {
213
213
  build_cmd: detected.build_cmd,
214
214
  output_dir: detected.output_dir,
215
215
  confident: detected.confident,
216
+ ...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
216
217
  });
217
218
  const framework_hint = opts.type ?? existing?.framework_hint ?? detected.framework_hint;
218
219
  const build_cmd = existing?.build_cmd ?? detected.build_cmd;
@@ -227,7 +228,13 @@ async function resolveSetupConfig(cwd, opts, existing) {
227
228
  else {
228
229
  source = "detected";
229
230
  }
230
- return { framework_hint, build_cmd, output_dir, source };
231
+ return {
232
+ framework_hint,
233
+ build_cmd,
234
+ output_dir,
235
+ source,
236
+ ...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
237
+ };
231
238
  }
232
239
  export async function deployCmd(opts) {
233
240
  const mode = detectMode();
@@ -306,6 +313,26 @@ export async function deployCmd(opts) {
306
313
  root_directory: opts.root ?? null,
307
314
  });
308
315
  emit({ event: "setup_applied" });
316
+ // Newly-created projects default to project_type='spa'. If detect
317
+ // says the repo is SSR (Next.js without `output: 'export'`), flip
318
+ // the type now — otherwise the first build crashes at the detect
319
+ // stage with "looks like ssr_next but configured as spa", forcing
320
+ // the user to open the dashboard and accept the suggestion. Only
321
+ // applied on first setup; an explicitly-configured spa project
322
+ // that drops a next.config later keeps user's choice.
323
+ if (setup.runtime_kind) {
324
+ try {
325
+ project = await api.setRuntimeType(project.id, setup.runtime_kind);
326
+ emit({ event: "runtime_type_applied", project_type: setup.runtime_kind });
327
+ }
328
+ catch (err) {
329
+ // Non-fatal: the build will fall back to the dashboard suggestion
330
+ // flow exactly as it did before this CLI fix. Tell the user what
331
+ // happened so they don't burn a deploy on a surprise crash.
332
+ const msg = err instanceof Error ? err.message : String(err);
333
+ emit({ event: "runtime_type_apply_failed", error: msg });
334
+ }
335
+ }
309
336
  }
310
337
  else if (opts.root !== undefined) {
311
338
  // Active project: --root patches the existing row so the *next*
package/dist/detect.js CHANGED
@@ -26,6 +26,34 @@ async function fileExists(cwd, ...candidates) {
26
26
  }
27
27
  return false;
28
28
  }
29
+ // Mirrors `frameworks/nextjs.py:_NEXT_STATIC_EXPORT_RE` and
30
+ // `next_config_static_export()` so CLI and builder can't disagree on
31
+ // what counts as a static-export Next.js project. The class-of-bug we
32
+ // hit on 2026-05-22 (builder detector v72 fix) and 2026-05-26 (CLI side)
33
+ // was exactly this kind of dual detector drift.
34
+ const NEXT_STATIC_EXPORT_RE = /output\s*:\s*['"]export['"]/m;
35
+ async function readNextConfigText(cwd) {
36
+ for (const name of ["next.config.mjs", "next.config.ts", "next.config.js", "next.config.cjs"]) {
37
+ try {
38
+ return await fs.readFile(path.join(cwd, name), "utf-8");
39
+ }
40
+ catch (err) {
41
+ if (err?.code === "ENOENT")
42
+ continue;
43
+ return null;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ // True -> config has `output: 'export'` (static-export SPA).
49
+ // False -> config exists but no export marker (SSR Next.js).
50
+ // null -> no next.config file at all; caller treats as static-default.
51
+ async function nextConfigStaticExport(cwd) {
52
+ const text = await readNextConfigText(cwd);
53
+ if (text === null)
54
+ return null;
55
+ return NEXT_STATIC_EXPORT_RE.test(text);
56
+ }
29
57
  async function hasAnyHtml(cwd) {
30
58
  // Shallow check — only top-level. Recursive walk is too expensive here
31
59
  // and the static fallback is already permissive enough.
@@ -60,12 +88,20 @@ function buildCmd(pkg, fallback) {
60
88
  export async function detectProject(cwd) {
61
89
  const pkg = await readPkg(cwd);
62
90
  if (pkg) {
63
- if (hasDep(pkg, "next") || (await fileExists(cwd, "next.config.js", "next.config.mjs", "next.config.ts"))) {
91
+ if (hasDep(pkg, "next") || (await fileExists(cwd, "next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"))) {
92
+ // A Next.js repo is SSR unless next.config explicitly declares
93
+ // `output: 'export'`. We only flag ssr_next when the config file
94
+ // exists AND lacks the export marker — same rule the builder uses
95
+ // in `runtime_detect.py:46`. Without a next.config file at all we
96
+ // err on the side of static (legacy `next export` workflow).
97
+ const exportFlag = await nextConfigStaticExport(cwd);
98
+ const isSsr = exportFlag === false;
64
99
  return {
65
100
  framework_hint: "nextjs",
66
101
  build_cmd: buildCmd(pkg, "npx next build"),
67
- output_dir: "out",
102
+ output_dir: isSsr ? ".next" : "out",
68
103
  confident: true,
104
+ ...(isSsr ? { runtime_kind: "ssr_next" } : {}),
69
105
  };
70
106
  }
71
107
  if (hasDep(pkg, "nuxt") || hasDep(pkg, "nuxt3") || (await fileExists(cwd, "nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Layero CLI — publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
5
5
  "license": "MIT",
6
6
  "type": "module",