layero 0.11.6 → 0.11.7

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
@@ -122,8 +122,10 @@ Run `layero <cmd> --help` for full options.
122
122
  separate `cli` environment). A project without a repository is always
123
123
  published live.
124
124
  - `--branch <name>` — **refused** (`branch_unsupported`, exit 4), see above.
125
- - `--claim` — deploy without an account: a temporary project for 72 hours
126
- plus a `claim_url` for a human to take it over. Turns on by itself when
125
+ - `--claim` — deploy without an account: a temporary site for 1 hour
126
+ plus a `claim_url` for a human to take it over. Static sites and SPAs only:
127
+ a server app (SSR, fullstack, container) is refused before anything is
128
+ uploaded (`claim_static_only`, exit 4). Turns on by itself when
127
129
  there is no token, the run is non-interactive (an agent, not CI), `--yes`
128
130
  is passed and the project is new: no `--project`, and the folder is not
129
131
  linked to an account project. An existing project without a token means
@@ -143,11 +145,16 @@ The platform creates a temporary project and a token for it; the CLI deploys
143
145
  with that token and prints, before `ready`:
144
146
 
145
147
  ```
146
- {"event":"claimable","url":"https://swift-fox.layero.app","claim_url":"https://app.layero.ru/claim?code=…","expires_at":"…"}
148
+ {"event":"claimable","url":"https://k3v9q2m8x7w1c4r6t0y5u2ze.layero.app","claim_url":"https://app.layero.ru/claim?code=…","expires_at":"…"}
147
149
  ```
148
150
 
149
- The site lives for 72 hours. A human opens `claim_url`, signs in and takes
150
- the project into their account — the CLI cannot accept a claim by itself.
151
+ The site lives for 1 hour, then stops answering and is deleted. Only static
152
+ sites and SPAs are accepted: a server app needs an account. The address is
153
+ random (the folder name and `--name` do not go into it), and the site is
154
+ closed to search engines: `robots.txt` with `Disallow: /` and
155
+ `X-Robots-Tag: noindex, nofollow`. A human opens `claim_url`, signs in and
156
+ takes the project into their account — the CLI cannot accept a claim by
157
+ itself.
151
158
  The claim code is saved in `.layero/project.json`; the temporary token stays
152
159
  in `~/.layero/config.json`, so `layero deploy` in the same directory keeps
153
160
  updating the same site until the claim expires or is accepted.
package/dist/agent.js CHANGED
@@ -343,7 +343,7 @@ function renderHuman(event) {
343
343
  break;
344
344
  case "claimable":
345
345
  process.stdout.write(`→ Временный проект ${event.slug}: ${event.url}\n`);
346
- process.stdout.write(` Сайт живёт до ${event.expires_at} (72 часа). Забрать в аккаунт: ${event.claim_url}\n`);
346
+ process.stdout.write(` Сайт живёт до ${event.expires_at} (час). Забрать в аккаунт: ${event.claim_url}\n`);
347
347
  break;
348
348
  case "claim_status":
349
349
  process.stdout.write(`Код ${event.code}: ${event.status}${event.claimed ? " (забран)" : ""}` +
@@ -453,7 +453,9 @@ async function main() {
453
453
  .option("--branch <name>", "REFUSED (branch_unsupported, exit 4): archive uploads always land in the " +
454
454
  "reserved `cli` environment, so this flag cannot give you a preview. " +
455
455
  "Branch previews come from pushing to a connected repository.")
456
- .option("--claim", "deploy without an account: a temporary project for 72 hours plus a claim_url for a human to take it over. " +
456
+ .option("--claim", "deploy without an account: a temporary site for 1 hour plus a claim_url for a human to take it over. " +
457
+ "Static sites and SPAs only — a server app (SSR, fullstack, container) is refused (claim_static_only, exit 4). " +
458
+ "The address is random and closed to search engines. " +
457
459
  "Turns on by itself when there is no token, the run is non-interactive (agent, not CI), --yes is passed " +
458
460
  "and the project is new (no --project, folder not linked to an account project). " +
459
461
  "Refused together with --project (claim_with_project, exit 4).")
@@ -9,7 +9,7 @@ import { dashboardOrigin } from "../urls.js";
9
9
  *
10
10
  * Сценарий: агент работает без аккаунта Layero — токена нет, браузера нет,
11
11
  * человек не рядом. Платформа заводит временный проект и выдаёт токен на
12
- * него; сайт живёт 72 часа; человек забирает его в свой аккаунт по ссылке
12
+ * него; сайт живёт час; человек забирает его в свой аккаунт по ссылке
13
13
  * `claim_url`. Принять заявку можно ТОЛЬКО в панели: CLI и агент не
14
14
  * подтверждают её ни при каких флагах.
15
15
  */
@@ -193,7 +193,43 @@ function isLegacyInitGuess(cfg) {
193
193
  cfg.build_cmd === "true" &&
194
194
  cfg.output_dir === ".");
195
195
  }
196
- async function resolveSetupConfig(cwd, opts, existing, projectFramework) {
196
+ /**
197
+ * 🚨 ПЕСОЧНИЦА — ТОЛЬКО СТАТИКА И SPA (T-20260919-10, решение владельца
198
+ * 19.09.2026). Серверное приложение (SSR, fullstack, контейнер) без аккаунта
199
+ * не выкладывается: это анонимный процесс с выходом в интернет на общем узле.
200
+ * Проверка ДО того, как заведена песочница и уехал архив; платформа и сборщик
201
+ * откажут и сами, но уже потратив чужую минуту.
202
+ */
203
+ async function assertSandboxServesFiles(cwd, opts, existing, detectFolder) {
204
+ if (opts.prebuilt)
205
+ return; // готовая сборка — это файлы по определению
206
+ const h = hintsFor(cwd, opts, existing);
207
+ const kind = h.asRuntime ?? (await detectFolder(h.detectCwd, h.hint, h.hintSource)).runtime_kind ?? null;
208
+ if (!kind)
209
+ return;
210
+ throw new LayeroError("claim_static_only", `деплой без аккаунта (--claim) выкладывает только статику и SPA, а эта папка — серверное приложение (${kind})`, "sign in and deploy from the account: `npx layero@latest login`, then run the same deploy without --claim");
211
+ }
212
+ /**
213
+ * 🚨 ДЕТЕКТ — ОДИН НА ВЫКАТКУ. Гейт core «входов анализа не становится
214
+ * больше» считает запуски детекта в этом файле: каждый лишний — ещё один
215
+ * снимок папки со своей полнотой, и два снимка рано или поздно разойдутся.
216
+ * Проверка песочницы (до заявки) и план сборки (после входа) берут один
217
+ * результат по одинаковым входам.
218
+ */
219
+ function oneDetectionPerRun() {
220
+ const memo = new Map();
221
+ return (detectCwd, hint, hintSource) => {
222
+ const key = `${detectCwd}\u0000${hint ?? ""}\u0000${hintSource}`;
223
+ let run = memo.get(key);
224
+ if (!run) {
225
+ run = detectProject(detectCwd, { frameworkHint: hint, hintSource });
226
+ memo.set(key, run);
227
+ }
228
+ return run;
229
+ };
230
+ }
231
+ /** Входы детекта: папка (с `--root`) и подсказка фреймворка с её источником. */
232
+ function hintsFor(cwd, opts, existing, projectFramework) {
197
233
  // Honour --root when auto-detecting: the framework signals live in the
198
234
  // monorepo subdir, not the repo root.
199
235
  const detectCwd = opts.root ? path.join(cwd, opts.root) : cwd;
@@ -206,10 +242,12 @@ async function resolveSetupConfig(cwd, opts, existing, projectFramework) {
206
242
  const own = isLegacyInitGuess(existing) ? null : existing;
207
243
  const fileHint = own?.framework_hint ?? null;
208
244
  const hint = typeHint ?? fileHint ?? projectFramework ?? null;
209
- const detected = await detectProject(detectCwd, {
210
- frameworkHint: hint,
211
- hintSource: typeHint ? "--type" : fileHint ? ".layero/project.json" : "project settings",
212
- });
245
+ const hintSource = typeHint ? "--type" : fileHint ? ".layero/project.json" : "project settings";
246
+ return { detectCwd, asRuntime, typeHint, own, fileHint, hint, hintSource };
247
+ }
248
+ async function resolveSetupConfig(cwd, opts, existing, projectFramework, detectFolder) {
249
+ const { detectCwd, asRuntime, typeHint, own, fileHint, hint, hintSource } = hintsFor(cwd, opts, existing, projectFramework);
250
+ const detected = await detectFolder(detectCwd, hint, hintSource);
213
251
  emit(detectedEvent(detected));
214
252
  return {
215
253
  detected,
@@ -405,7 +443,7 @@ async function dryRun(cwd, opts, existing, cliCfg) {
405
443
  });
406
444
  return;
407
445
  }
408
- const setup = await resolveSetupConfig(cwd, { ...opts, root: root ?? undefined }, existing, project?.framework_hint ?? null);
446
+ const setup = await resolveSetupConfig(cwd, { ...opts, root: root ?? undefined }, existing, project?.framework_hint ?? null, oneDetectionPerRun());
409
447
  const d = setup.detected;
410
448
  let buildCmd = d.build_cmd;
411
449
  let outputDir = d.output_dir;
@@ -485,6 +523,7 @@ export async function waitUntilServing(url, budgetMs = SERVE_WAIT_MS) {
485
523
  }
486
524
  export async function deployCmd(opts) {
487
525
  const mode = detectMode();
526
+ const detectFolder = oneDetectionPerRun();
488
527
  if (opts.type &&
489
528
  !VALID_TYPES.has(opts.type.toLowerCase()) &&
490
529
  !runtimeTypeOf(opts.type)) {
@@ -533,6 +572,7 @@ export async function deployCmd(opts) {
533
572
  ? claimTokenFor(cliCfg, existing?.project_id)
534
573
  : undefined;
535
574
  if (reuse) {
575
+ await assertSandboxServesFiles(cwd, opts, existing, detectFolder);
536
576
  cliCfg = { ...cliCfg, token: reuse };
537
577
  // Повторная выкатка песочницы: ссылка «забрать» нужна агенту и здесь —
538
578
  // без неё он искал её в `.layero/project.json` (симуляция 18.09.2026).
@@ -546,8 +586,9 @@ export async function deployCmd(opts) {
546
586
  (!boundToAccountProject && !mode.interactive && !isCiEnv() && opts.yes)) {
547
587
  // 🚨 CI СЮДА НЕ ПОПАДАЕТ НАМЕРЕННО. Раннер без LAYERO_TOKEN — это
548
588
  // забытый секрет, и правильный ответ ему — отказ, а не сайт на
549
- // временном адресе, который через 72 часа исчезнет вместе с
550
- // «зелёным» прогоном. Явный `--claim` в CI работает.
589
+ // временном адресе, который через час исчезнет вместе с «зелёным»
590
+ // прогоном. Явный `--claim` в CI работает.
591
+ await assertSandboxServesFiles(cwd, opts, existing, detectFolder);
551
592
  const r = await createClaimable(cliCfg, cwd, {
552
593
  name: opts.name ?? path.basename(cwd),
553
594
  });
@@ -614,7 +655,7 @@ export async function deployCmd(opts) {
614
655
  // выкатку из исходников.
615
656
  const setup = prebuiltDir
616
657
  ? { framework_hint: null, build_cmd: null, output_dir: null }
617
- : await resolveSetupConfig(cwd, opts, existing);
658
+ : await resolveSetupConfig(cwd, opts, existing, undefined, detectFolder);
618
659
  if (prebuiltDir) {
619
660
  emit({ event: "prebuilt", dir: prebuiltDir });
620
661
  }
@@ -49,6 +49,7 @@ const INVALID_INPUT = new Set([
49
49
  "sql_missing",
50
50
  "branch_unsupported",
51
51
  "claim_with_project",
52
+ "claim_static_only",
52
53
  "provider_unknown",
53
54
  "repo_format",
54
55
  "token_missing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.11.6",
3
+ "version": "0.11.7",
4
4
  "description": "Layero CLI — deploy a directory or a repo to hosting with build servers in Russia. JSON events and exit codes for AI agents and CI.",
5
5
  "license": "MIT",
6
6
  "type": "module",