layero 0.10.4 → 0.11.0

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
@@ -53,7 +53,8 @@ bare `npx layero` call then runs the globally installed copy for years.
53
53
  ```bash
54
54
  layero login # device flow: prints a URL + code, sign in once (email code or Yandex ID)
55
55
  cd my-site
56
- layero deploy # detects the framework, packs, uploads, builds, ships
56
+ layero deploy --dry-run # how the platform will build this folder; uploads nothing, no login needed
57
+ layero deploy # packs, uploads, the platform builds and ships
57
58
  ```
58
59
 
59
60
  The first `layero deploy` in a directory creates a project and links it via
@@ -70,9 +71,9 @@ The first `layero deploy` in a directory creates a project and links it via
70
71
 
71
72
  | Command | Description |
72
73
  |---|---|
73
- | `layero init` | Scaffold `.layero/project.json` and write a Layero block into `AGENTS.md` / `CLAUDE.md` / `.cursorrules` (compact index; full skill in `layero-agents`). |
74
+ | `layero init` | Write a Layero block into `AGENTS.md` / `CLAUDE.md` / `.cursorrules` (compact index; full skill in `layero-agents`) and scaffold `.layero/project.json`. Optional: `deploy` links the folder by itself. It records no guessed settings. |
74
75
  | `layero login` / `logout` / `whoami` | Browser device-flow sign-in, sign-out, current account. |
75
- | `layero deploy` | Pack the current directory, build on the platform, publish. `--claim` — without an account. |
76
+ | `layero deploy` | Pack the current directory, build on the platform, publish. `--dry-run` — show the build plan only. `--claim` — without an account. |
76
77
  | `layero projects list` | Projects on your account with addresses. |
77
78
  | `layero projects create --repo <provider>:<owner/repo>` | Create a project from a repository of a connected provider, apply the detected settings and start the first build (what the dashboard's "Start deploy" button does); `--no-deploy` leaves it in the wizard. Events: `project_created`, `source_connected`, `webhook_installed` \| `webhook_unavailable`, then `setup_applied` + `deploy_started` \| `setup_pending` \| `setup_failed`. |
78
79
  | `layero projects delete <slug> --yes` | Delete a project. Irreversible; needs a token with scope `admin`. |
@@ -93,11 +94,21 @@ Run `layero <cmd> --help` for full options.
93
94
 
94
95
  ## `layero deploy` flags
95
96
 
97
+ - `--dry-run` — print how the platform will build this folder and exit:
98
+ framework, build command, output folder, where each value comes from
99
+ (`layero.json`, project settings, `package.json`, a framework default), and
100
+ a `hint` / `next_action` when the folder is a monorepo, a frontend +
101
+ backend pair, a custom build script or a server. Uploads nothing, needs no
102
+ login (with a login it also reads the settings of a linked project).
96
103
  - `--type <preset>` — framework override: `vite`, `vitepress`, `next`,
97
104
  `astro`, `cra`, `sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `eleventy`
98
- (alias `11ty`), `hugo`, `static`; runtime kinds `node_web`, `python_web`,
99
- `flask`, `streamlit`, `gradio`, `ssr_next` (aliases `express`, `fastapi`,
100
- `django`, …). **Optional** — auto-detected when omitted.
105
+ (alias `11ty`), `hugo`, `static`, `generic`; runtime kinds `node_web`,
106
+ `python_web`, `flask`, `streamlit`, `gradio`, `ssr_next` (aliases
107
+ `express`, `fastapi`, `django`, …). **Optional** — detected by the
108
+ platform when omitted. `static` serves the files as they are and never runs
109
+ a build; `generic` runs your own build command (`layero.json`
110
+ `buildCommand` or the `build` script) and serves the folder with
111
+ `index.html`.
101
112
  - `--prebuilt [dir]` — ship an already-built artifact instead of building
102
113
  remotely. Without an argument, picks the first existing of `dist/`,
103
114
  `build/`, `public/`, `out/`, `_site/`, `.output/public/`,
@@ -106,13 +117,18 @@ Run `layero <cmd> --help` for full options.
106
117
  - `--name <name>` — project name (only on first deploy).
107
118
  - `--project <id_or_slug>` — deploy into an existing project, ignoring
108
119
  `./.layero/project.json` (use this in CI, not `--name`).
109
- - `--prod` — target the production environment of a repository-linked
110
- project. Redundant for CLI projects: they auto-promote on every deploy.
120
+ - `--prod` — only for a project **with a connected repository**: publish this
121
+ upload at the live address (without it the upload lands in the project's
122
+ separate `cli` environment). A project without a repository is always
123
+ published live.
111
124
  - `--branch <name>` — **refused** (`branch_unsupported`, exit 4), see above.
112
125
  - `--claim` — deploy without an account: a temporary project for 72 hours
113
126
  plus a `claim_url` for a human to take it over. Turns on by itself when
114
- there is no token, the run is non-interactive (an agent, not CI) and
115
- `--yes` is passed. In CI a missing `LAYERO_TOKEN` stays an error.
127
+ there is no token, the run is non-interactive (an agent, not CI), `--yes`
128
+ is passed and the project is new: no `--project`, and the folder is not
129
+ linked to an account project. An existing project without a token means
130
+ signing in (`auth_required`). With `--project` it is refused
131
+ (`claim_with_project`, exit 4). In CI a missing `LAYERO_TOKEN` stays an error.
116
132
  - `--org <slug>` — organization for first-time project creation.
117
133
  - `--yes` / `-y` — non-interactive mode.
118
134
  - `--json` — JSON-lines events on stdout (for agents and CI).
@@ -140,7 +156,15 @@ the page in a browser (in agent mode it prints the link).
140
156
 
141
157
  ## Framework auto-detection
142
158
 
143
- `layero deploy` (and `layero init`) read your project on disk and pick sane defaults:
159
+ `layero deploy --dry-run` (and `layero init`, and the `detected` event of every
160
+ deploy) read your project on disk with the same rules the platform uses. This
161
+ is **advice, not a decision**: the platform detects again on the uploaded
162
+ files, and nothing the CLI guesses is saved to the project — only what you name
163
+ (`--type`, `--root`, fields you write into `.layero/project.json` or
164
+ `layero.json`). `confident: false` means the folder was not recognised; read
165
+ `hint` and `next_action` (an app in a subfolder → `--root <dir>`, frontend +
166
+ backend → `layero.json` with both halves, a custom build script →
167
+ `"framework": "generic"`).
144
168
 
145
169
  | Signal | Framework | `build_cmd` | `output_dir` |
146
170
  |---|---|---|---|
@@ -156,7 +180,8 @@ the page in a browser (in agent mode it prints the link).
156
180
  | `react-scripts` dep | cra | `npm run build` | `build` |
157
181
  | `@11ty/eleventy` dep / `.eleventy.js` / `eleventy.config.*` | eleventy | `npm run build` (or `npx @11ty/eleventy`) | `_site` |
158
182
  | `hugo.{toml,yaml,json}` or `config.*` with Hugo markers (`baseURL`, `[markup]`, …) | hugo | `hugo --gc --minify` (no install needed) | `public` |
159
- | any `.html` at root, no `package.json` | static | `true` (no-op) | `.` |
183
+ | `index.html` at the root, no framework | static | none — served as is | `.` |
184
+ | `package.json` with a `build` script, no known framework | generic | `npm run build` | the folder with `index.html` after the build |
160
185
 
161
186
  ## `layero.json` — pin the settings in the repository
162
187
 
@@ -284,7 +309,8 @@ Every command emits events — one JSON object per line on stdout:
284
309
  {"event":"sources","org":"…","providers":[…],"connections":[…]}
285
310
  {"event":"source_repos","org":"…","connection_id":"…","repos":[…]}
286
311
  {"event":"environments","project":"…","environments":[{"branch":"main","url":"https://…","production":true,…}]}
287
- {"event":"detected","framework":"…","build_cmd":"…","output_dir":"…","confident":true}
312
+ {"event":"detected","framework":"…","build_cmd":"…"|null,"output_dir":"…"|null,"confident":true,"sources":{…},"hint":"…","next_action":"…"}
313
+ {"event":"plan","framework":"…","build_cmd":"…"|null,"output_dir":"…"|null,"sources":{…},"creates_project":true,"replaces_live_site":true,…} (--dry-run)
288
314
  {"event":"packing","files":N,"bytes":N,"sha256":"…"}
289
315
  {"event":"uploading"} {"event":"uploaded","archive_key":"…"}
290
316
  {"event":"deploy_started","deploy_id":"…"}
@@ -301,9 +327,11 @@ Every command emits events — one JSON object per line on stdout:
301
327
  ```
302
328
 
303
329
  On `ready`, `url` is the **live public site** — show it as-is and never
304
- rebuild the hostname from a template. `dashboard_url` is the management page,
305
- not the site. `preview_url`, `edge_ready` and `edge_eta_seconds` are legacy
306
- fields; do not wait for them.
330
+ rebuild the hostname from a template. `ready` comes once the address answers
331
+ with the site itself (`edge_ready: true`); `edge_ready: false` with `screen`
332
+ means the platform's own page still answered after the wait — the app did not
333
+ come up, read `layero logs --runtime`. `dashboard_url` is the management page,
334
+ not the site. `preview_url` and `edge_eta_seconds` are legacy fields.
307
335
 
308
336
  Errors carry a stable `code` (`auth_required`, `auth_expired`, `auth_timeout`,
309
337
  `project_unknown`, `project_not_found`, `cli_deploys_disabled`, `invalid_type`,
@@ -338,7 +366,8 @@ the rule files themselves. Maximum archive size is 200 MB.
338
366
  - Per-project link: `./.layero/project.json` — `project_id`, `slug`,
339
367
  `organization_slug`, `apex_hostname`, `claim` are managed by the CLI;
340
368
  `framework_hint`, `build_cmd`, `output_dir`, `analytics_enabled`,
341
- `env_vars` are user-editable and override auto-detection.
369
+ `env_vars` are yours: the CLI never writes guesses there, and what you put
370
+ there is applied to a new project as your choice.
342
371
 
343
372
  ## Contributing
344
373
 
package/dist/agent.js CHANGED
@@ -127,10 +127,39 @@ function renderHuman(event) {
127
127
  }
128
128
  break;
129
129
  case "detected":
130
- process.stdout.write(event.runtime_kind
131
- ? `→ Detected ${event.runtime_kind} runtime app — will deploy as a scale-to-zero container\n`
132
- : `→ Detected ${event.framework} (build: ${event.build_cmd}, output: ${event.output_dir})\n`);
130
+ if (event.runtime_kind) {
131
+ process.stdout.write(`→ Detected ${event.runtime_kind} runtime app — will deploy as a scale-to-zero container\n`);
132
+ }
133
+ else if (event.confident) {
134
+ process.stdout.write(`→ Detected ${event.framework} (build: ${event.build_cmd ?? "none"}, output: ${event.output_dir ?? "found after the build"})\n`);
135
+ }
136
+ else {
137
+ process.stdout.write(`! Не удалось уверенно определить приложение в этой папке\n`);
138
+ }
139
+ if (event.hint)
140
+ process.stdout.write(` ${event.hint}\n`);
141
+ if (event.next_action)
142
+ process.stdout.write(` → ${event.next_action}\n`);
143
+ break;
144
+ case "plan": {
145
+ const src = event.sources;
146
+ process.stdout.write(`План сборки (ничего не выгружено):\n`);
147
+ process.stdout.write(` фреймворк: ${event.framework} (${src.framework})\n`);
148
+ if (event.runtime_kind)
149
+ process.stdout.write(` запуск: контейнер ${event.runtime_kind}\n`);
150
+ process.stdout.write(` сборка: ${event.build_cmd ?? "не запускается"} (${src.build_cmd})\n`);
151
+ process.stdout.write(` результат: ${event.output_dir ?? "определится после сборки"} (${src.output_dir})\n`);
152
+ if (event.root)
153
+ process.stdout.write(` папка: ${event.root}\n`);
154
+ process.stdout.write(event.replaces_live_site
155
+ ? ` выкатка заменит живой сайт\n`
156
+ : ` выкатка ляжет в окружение «cli», живой сайт не изменится\n`);
157
+ if (event.hint)
158
+ process.stdout.write(` ${event.hint}\n`);
159
+ if (event.next_action)
160
+ process.stdout.write(` → ${event.next_action}\n`);
133
161
  break;
162
+ }
134
163
  case "prebuilt":
135
164
  process.stdout.write(`→ Prebuilt mode: shipping ${event.dir}\n`);
136
165
  break;
@@ -186,16 +215,10 @@ function renderHuman(event) {
186
215
  break;
187
216
  case "ready":
188
217
  if (event.edge_ready === false) {
189
- // Built & published, but the CDN edge for the apex is still
190
- // propagating (first deploy of a new hostname can take a few
191
- // minutes on YC CDN). The preview host is reachable immediately.
192
- const eta = typeof event.edge_eta_seconds === "number" && event.edge_eta_seconds > 0
193
- ? ` (edge propagating, ~${event.edge_eta_seconds}s)`
194
- : " (edge propagating)";
195
- process.stdout.write(`✓ Built. Live at ${event.url}${eta}\n`);
196
- if (event.preview_url) {
197
- process.stdout.write(` Reachable now: ${event.preview_url}\n`);
198
- }
218
+ // Сборка готова, но по адресу всё ещё отвечает экран платформы, а не
219
+ // приложение: оно не поднялось за время ожидания.
220
+ process.stdout.write(`✓ Built. ${event.url} still shows the platform screen` +
221
+ `${event.screen ? ` (${event.screen})` : ""} — check \`layero logs --runtime\`\n`);
199
222
  }
200
223
  else {
201
224
  process.stdout.write(`✓ Live at ${event.url}\n`);
@@ -424,22 +424,33 @@ async function main() {
424
424
  .action(tokenSetCmd);
425
425
  program
426
426
  .command("deploy")
427
- .description("Pack the current directory and deploy it. Framework, build command and output directory are auto-detected.")
427
+ .description("Pack the current directory and deploy it: the platform builds it and publishes it. " +
428
+ "A project without a connected repository is published live — every deploy replaces the site at ready.url. " +
429
+ "Framework, build command and output directory are detected by the platform; --dry-run shows the plan first.")
430
+ .option("--dry-run", "show how the platform will build this folder (framework, build command, output folder, where each came from, " +
431
+ "hints for monorepos / frontend+backend / custom build scripts) and exit; uploads nothing, needs no login")
428
432
  .option("-t, --type <preset>", "type override — static preset (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | storybook | eleventy | hugo | static) " +
429
- "or runtime kind for apps the platform RUNS (node_web | python_web | flask | streamlit | gradio | ssr_next; aliases: express, fastapi, django, node, python)")
433
+ "or runtime kind for apps the platform RUNS (node_web | python_web | flask | streamlit | gradio | ssr_next; aliases: express, fastapi, django, node, python). " +
434
+ "`static` serves the files as they are and never runs a build; `generic` runs your own build command " +
435
+ "(layero.json buildCommand or the package.json build script) and serves the folder with index.html")
430
436
  .option("--name <name>", "project name (only used on first deploy)")
431
437
  .option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
432
- .option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
438
+ .option("-y, --yes", "non-interactive: accept defaults and skip the --prod confirmation")
433
439
  .option("--config", "(legacy alias of the default behaviour — auto-detect + .layero/project.json values)")
434
440
  .option("--prebuilt [dir]", "ship an already-built artifact directory (default auto-pick: dist/build/public/out/_site/...)")
435
- .option("--root <dir>", "monorepo: subdirectory inside the repo that the builder treats as the app root (saved on the project; future GitHub-push and hook triggers use the same value)")
436
- .option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
437
- .option("--promote", "pin the project apex to this deploy after a successful build (V071). Works for any branch — e.g. `--promote` without --prod publishes a CLI preview straight to production.")
441
+ .option("--root <dir>", "monorepo: the app's subfolder (e.g. apps/web) that the builder treats as the app root; the whole folder is still uploaded. " +
442
+ "Saved on the project, so later deploys and git pushes use it too")
443
+ .option("--prod", "only for a project WITH a connected repository: publish this upload at the live address. Without it such an upload " +
444
+ "lands in the project's separate `cli` environment. A project without a repository is always published live")
445
+ .option("--promote", "after a successful build, point the live address at this deploy (done by the CLI; same result as --prod). " +
446
+ "Not needed for a project without a repository — it is published live anyway")
438
447
  .option("--branch <name>", "REFUSED (branch_unsupported, exit 4): archive uploads always land in the " +
439
448
  "reserved `cli` environment, so this flag cannot give you a preview. " +
440
449
  "Branch previews come from pushing to a connected repository.")
441
450
  .option("--claim", "deploy without an account: a temporary project for 72 hours plus a claim_url for a human to take it over. " +
442
- "Turns on by itself when there is no token, the run is non-interactive (agent, not CI) and --yes is passed.")
451
+ "Turns on by itself when there is no token, the run is non-interactive (agent, not CI), --yes is passed " +
452
+ "and the project is new (no --project, folder not linked to an account project). " +
453
+ "Refused together with --project (claim_with_project, exit 4).")
443
454
  .option("--org <slug>", "Layero organization slug for first-time project creation. Defaults to personal; required when you're a member of multiple orgs and want a non-personal home.")
444
455
  // Осознанное подтверждение выкатки, когда предыдущие сборки подряд падают
445
456
  // с ОДНОЙ И ТОЙ ЖЕ ошибкой. Намеренно НЕ покрывается `--yes`: смысл стопа
@@ -447,14 +458,15 @@ async function main() {
447
458
  // стоит по умолчанию и снял бы стоп, ничего не остановив.
448
459
  .option("--confirm-repeated-failure", "proceed even though recent deploys keep failing with the SAME error (the platform stops repeat deploys until you confirm you know what changed)")
449
460
  .addHelpText("after", "\nExamples:\n" +
450
- " $ layero deploy # preview deploy (CLI pseudo-branch), auto-detect framework\n" +
451
- " $ layero deploy --prod # production deploy (interactive confirm)\n" +
452
- " $ layero deploy --prod --yes # production deploy, no prompt (CI)\n" +
453
- " $ layero deploy --promote # preview deploy + pin apex (one-shot publish)\n" +
454
- " $ layero deploy --claim # no account: temporary site + claim link\n" +
455
- " $ layero deploy --type vite # force a framework preset\n" +
461
+ " $ layero deploy --dry-run # how the platform will build this folder; nothing is uploaded\n" +
462
+ " $ layero deploy # build and publish (no repository: replaces the live site)\n" +
463
+ " $ layero deploy --root apps/web # monorepo: the app lives in a subfolder\n" +
464
+ " $ layero deploy --type generic # own build script, no known framework: run it, serve the result\n" +
456
465
  " $ layero deploy --type express # Node backend: platform RUNS it, not serves files\n" +
457
- " $ layero deploy --json # machine-readable output for agents")
466
+ " $ layero deploy --claim # no account: temporary site + claim link\n" +
467
+ " $ layero deploy --prod --yes # project with a connected repository: publish this upload live (CI)\n" +
468
+ " $ layero deploy --json # machine-readable output for agents\n" +
469
+ "\nIsolated previews come only from pushing a branch of a connected repository (layero projects create --repo).")
458
470
  .action(async (opts) => {
459
471
  await deployCmd(opts);
460
472
  });