layero 0.7.2 → 0.7.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/dist/agent.js CHANGED
@@ -107,7 +107,9 @@ function renderHuman(event) {
107
107
  process.stdout.write(`→ Project ${event.slug}\n`);
108
108
  break;
109
109
  case "detected":
110
- process.stdout.write(`→ Detected ${event.framework} (build: ${event.build_cmd}, output: ${event.output_dir})\n`);
110
+ process.stdout.write(event.runtime_kind
111
+ ? `→ Detected ${event.runtime_kind} runtime app — will deploy as a scale-to-zero container\n`
112
+ : `→ Detected ${event.framework} (build: ${event.build_cmd}, output: ${event.output_dir})\n`);
111
113
  break;
112
114
  case "prebuilt":
113
115
  process.stdout.write(`→ Prebuilt mode: shipping ${event.dir}\n`);
package/dist/detect.js CHANGED
@@ -26,6 +26,25 @@ async function fileExists(cwd, ...candidates) {
26
26
  }
27
27
  return false;
28
28
  }
29
+ // Python runtime: `app.py` entry + a runtime lib in requirements.txt.
30
+ // Lockstep with detect_runtime() (backend) / runtime_detect.py (builder):
31
+ // app.py + requirements.txt mentioning streamlit | gradio | flask.
32
+ async function detectPythonRuntime(cwd) {
33
+ if (!(await fileExists(cwd, "app.py")))
34
+ return null;
35
+ let reqs;
36
+ try {
37
+ reqs = (await fs.readFile(path.join(cwd, "requirements.txt"), "utf-8")).toLowerCase();
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ for (const lib of ["streamlit", "gradio", "flask"]) {
43
+ if (reqs.includes(lib))
44
+ return lib;
45
+ }
46
+ return null;
47
+ }
29
48
  // Mirrors `frameworks/nextjs.py:_NEXT_STATIC_EXPORT_RE` and
30
49
  // `next_config_static_export()` so CLI and builder can't disagree on
31
50
  // what counts as a static-export Next.js project. The class-of-bug we
@@ -137,6 +156,25 @@ export async function detectProject(cwd) {
137
156
  }),
138
157
  };
139
158
  }
159
+ // Remix / React Router v7 before SvelteKit/Vite — RR7 ships Vite
160
+ // internally, so the Vite-dep check would otherwise win. Mirrors the
161
+ // builder's RemixFramework + backend `remix` table entry (output
162
+ // build/client). Was missing from the CLI entirely — same dual-detector
163
+ // gap class as Angular: a Remix repo deployed via the CLI fell through
164
+ // to the static fallback and shipped raw sources.
165
+ if (hasDep(pkg, "@remix-run/dev") ||
166
+ hasDep(pkg, "@remix-run/react") ||
167
+ hasDep(pkg, "@remix-run/node") ||
168
+ hasDep(pkg, "@react-router/dev") ||
169
+ hasDep(pkg, "@react-router/node") ||
170
+ (await fileExists(cwd, "react-router.config.ts", "react-router.config.js"))) {
171
+ return {
172
+ framework_hint: "remix",
173
+ build_cmd: buildCmd(pkg, "npx react-router build"),
174
+ output_dir: "build/client",
175
+ confident: true,
176
+ };
177
+ }
140
178
  if (hasDep(pkg, "@sveltejs/kit") || (await fileExists(cwd, "svelte.config.js"))) {
141
179
  // SvelteKit needs an adapter. adapter-static = SPA path; anything
142
180
  // else (adapter-node, adapter-auto, …) is server-side. Mirrors
@@ -254,6 +292,22 @@ export async function detectProject(cwd) {
254
292
  confident: true,
255
293
  };
256
294
  }
295
+ // Angular after Vite — mirrors builder ALL order. Angular ships its
296
+ // own CLI (`ng build`) and writes to `dist/{project}/` (Angular <17)
297
+ // or `dist/{project}/browser/` (17+ `application` builder), NOT bare
298
+ // `dist`. Until this branch existed the CLI fell through to the
299
+ // static fallback (`output_dir='.'`), so the first deploy of an
300
+ // Angular repo shipped the raw sources — the dist/<project> S3 404
301
+ // incident. `angularOutputDir` parses angular.json to pin the path
302
+ // (lockstep with `frameworks/angular.py:extract_output_dir`).
303
+ if (hasDep(pkg, "@angular/core") || (await fileExists(cwd, "angular.json"))) {
304
+ return {
305
+ framework_hint: "angular",
306
+ build_cmd: buildCmd(pkg, "npx ng build"),
307
+ output_dir: await angularOutputDir(cwd),
308
+ confident: true,
309
+ };
310
+ }
257
311
  if (hasDep(pkg, "react-scripts")) {
258
312
  return {
259
313
  framework_hint: "cra",
@@ -277,6 +331,32 @@ export async function detectProject(cwd) {
277
331
  confident: true,
278
332
  };
279
333
  }
334
+ // package.json present but no framework matched → a custom Node build,
335
+ // NOT a static site. Mirrors the backend's detect(): `pkg is None →
336
+ // _STATIC`, otherwise `_GENERIC` (npm run build → dist). Falling through
337
+ // to the static fallback here shipped the unbuilt sources — exactly the
338
+ // `diplomtest` case (Express app + custom build script detected as static
339
+ // by the CLI but `generic` by the backend).
340
+ return {
341
+ framework_hint: "generic",
342
+ build_cmd: buildCmd(pkg, "npm run build"),
343
+ output_dir: "dist",
344
+ confident: false,
345
+ };
346
+ }
347
+ // Python runtimes (Streamlit / Gradio / Flask): an `app.py` entry plus a
348
+ // runtime lib in requirements.txt. Mirrors detect_runtime() / runtime_detect.py.
349
+ // No package.json, so without this they fall to the static fallback and ship
350
+ // `app.py` as a static file (never runs) — the `streamlit-hello` case.
351
+ const pyRuntime = await detectPythonRuntime(cwd);
352
+ if (pyRuntime) {
353
+ return {
354
+ framework_hint: "static",
355
+ build_cmd: "true",
356
+ output_dir: ".",
357
+ confident: true,
358
+ runtime_kind: pyRuntime,
359
+ };
280
360
  }
281
361
  // Non-Node SSGs (Hugo today). Recognise repos without package.json
282
362
  // before we fall back to "static" with a no-op build command — Hugo
@@ -363,3 +443,57 @@ async function hasHugoConfigMarker(cwd) {
363
443
  }
364
444
  return false;
365
445
  }
446
+ // Resolve Angular's real build output directory from angular.json.
447
+ // Lockstep with `core/builder/src/frameworks/angular.py:extract_output_dir`
448
+ // — same dual-detector-drift class as Next.js static-export, so the two
449
+ // implementations must agree (parity fixtures in
450
+ // core/tests/fixtures/framework-detect/angular-*).
451
+ //
452
+ // Resolution:
453
+ // 1. defaultProject if set & present, else first project in `projects`
454
+ // 2. project.architect.build.options.outputPath, else the CLI default
455
+ // `dist/{projectName}`
456
+ // 3. strip leading `./`
457
+ // 4. append `/browser` for the Angular 17+ `application` builder
458
+ // (builder id ends with `:application`) — it writes the served
459
+ // assets one level deeper. We also probe disk in case the repo was
460
+ // built locally before `layero deploy`.
461
+ // Returns the framework default `dist` on any parse error — matches
462
+ // AngularFramework.default_output_dir, and the builder re-derives the
463
+ // exact path at upload time anyway.
464
+ async function angularOutputDir(cwd) {
465
+ const FALLBACK = "dist";
466
+ let cfg;
467
+ try {
468
+ cfg = JSON.parse(await fs.readFile(path.join(cwd, "angular.json"), "utf-8"));
469
+ }
470
+ catch {
471
+ return FALLBACK;
472
+ }
473
+ const projects = cfg?.projects;
474
+ if (!projects || typeof projects !== "object")
475
+ return FALLBACK;
476
+ const firstName = Object.keys(projects)[0];
477
+ if (!firstName)
478
+ return FALLBACK;
479
+ const defaultName = cfg.defaultProject;
480
+ const projectName = defaultName && projects[defaultName] ? defaultName : firstName;
481
+ const buildCfg = projects[projectName]?.architect?.build;
482
+ if (!buildCfg || typeof buildCfg !== "object")
483
+ return FALLBACK;
484
+ const opts = (buildCfg.options ?? {});
485
+ const rawOut = typeof opts === "object" && opts ? opts.outputPath : undefined;
486
+ let out = typeof rawOut === "string" && rawOut.trim() ? rawOut.trim() : `dist/${projectName}`;
487
+ if (out.startsWith("./"))
488
+ out = out.slice(2);
489
+ const builderId = typeof buildCfg.builder === "string" ? buildCfg.builder : "";
490
+ const isApplicationBuilder = builderId.endsWith(":application");
491
+ let browserExists = false;
492
+ try {
493
+ browserExists = (await fs.stat(path.join(cwd, out, "browser"))).isDirectory();
494
+ }
495
+ catch {
496
+ // not built locally — rely on the builder-id signal below
497
+ }
498
+ return browserExists || isApplicationBuilder ? `${out}/browser` : out;
499
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",