layero 0.8.23 → 0.8.24

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
@@ -148,6 +148,11 @@ function renderHuman(event) {
148
148
  process.stdout.write(`! Failed to set project_type automatically: ${event.error}\n`);
149
149
  process.stdout.write(` Build may fail at detect; accept the suggestion in the dashboard if so.\n`);
150
150
  break;
151
+ case "repeated_failure_guard":
152
+ // В человеческом режиме подробности печатает сама команда: там они с
153
+ // цветом, в stderr и с вопросом. Дублировать их здесь значило бы
154
+ // показать одно и то же дважды.
155
+ break;
151
156
  case "deploy_started":
152
157
  process.stdout.write(`→ Building...\n`);
153
158
  break;
@@ -292,6 +292,11 @@ async function main() {
292
292
  "environments come from pushes to a connected repository, not from " +
293
293
  "this flag. Kept for backwards compatibility.")
294
294
  .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.")
295
+ // Осознанное подтверждение выкатки, когда предыдущие сборки подряд падают
296
+ // с ОДНОЙ И ТОЙ ЖЕ ошибкой. Намеренно НЕ покрывается `--yes`: смысл стопа
297
+ // в том, чтобы прервать автоматический цикл, а `--yes` в скриптах уже
298
+ // стоит по умолчанию и снял бы стоп, ничего не остановив.
299
+ .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)")
295
300
  .addHelpText("after", "\nExamples:\n" +
296
301
  " $ layero deploy # preview deploy (CLI pseudo-branch), auto-detect framework\n" +
297
302
  " $ layero deploy --prod # production deploy (interactive confirm)\n" +
@@ -198,6 +198,119 @@ async function resolveSetupConfig(cwd, opts, existing) {
198
198
  ...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
199
199
  };
200
200
  }
201
+ /**
202
+ * Русская форма числительного. Шесть строк вместо зависимости: в CLI своего
203
+ * плюрализатора нет, а «2 сборок» в стоп-сообщении читается как небрежность
204
+ * ровно там, где нужно доверие. Порог настраивается платформой, поэтому
205
+ * подставить одну форму нельзя — при пороге 2 и при пороге 5 они разные.
206
+ */
207
+ function pluralRu(n, one, few, many) {
208
+ const a = Math.abs(n) % 100;
209
+ if (a >= 11 && a <= 14)
210
+ return many;
211
+ const b = a % 10;
212
+ if (b === 1)
213
+ return one;
214
+ if (b >= 2 && b <= 4)
215
+ return few;
216
+ return many;
217
+ }
218
+ function parseRepeatedFailure(err) {
219
+ if (!(err instanceof ApiError) || err.status !== 409)
220
+ return null;
221
+ try {
222
+ const detail = JSON.parse(err.body)?.detail;
223
+ return detail?.code === "repeated_failure" ? detail : null;
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ }
229
+ /**
230
+ * Запуск сборки со стопом на повторяющейся ошибке.
231
+ *
232
+ * СМЫСЛ СТОПА — ОСТАНОВИТЬ ТОГО, КТО ГОНИТ ВЫКАТКУ ВСЛЕПУЮ. Агент, десятый раз
233
+ * собирающий одно и то же, ошибку обычно НЕ читает: она приходит в конце
234
+ * длинного лога сборки, а он смотрит на код возврата и запускает снова. Поэтому
235
+ * здесь текст ошибки печатается ПЕРВЫМ и отдельно от всего остального, и лишь
236
+ * потом объясняется, что делать.
237
+ *
238
+ * В интерактивном терминале спрашиваем прямо; без TTY (агент, CI) — падаем с
239
+ * отдельным кодом ошибки. Автоматически продолжать нельзя: это ровно тот цикл,
240
+ * который правило и разрывает.
241
+ */
242
+ async function startWithRepeatedFailureGuard(api, sessionId, commitSha, opts) {
243
+ try {
244
+ return await api.startDeploySession(sessionId, {
245
+ commit_sha: commitSha,
246
+ confirm_repeated_failure: opts.confirmRepeatedFailure === true,
247
+ });
248
+ }
249
+ catch (err) {
250
+ const detail = parseRepeatedFailure(err);
251
+ if (!detail)
252
+ throw err;
253
+ // Машиночитаемое событие — для агентов в режиме --json.
254
+ // `scope` отвечает на вопрос, который агент задаст первым: «я собирал в
255
+ // этом проекте три раза, откуда десять?». Серия могла набраться суммой по
256
+ // нескольким его проектам — перенос в новый проект правило не обходит.
257
+ const scope = detail.scope === "owner" ? "owner" : "project";
258
+ emit({
259
+ event: "repeated_failure_guard",
260
+ streak: detail.streak,
261
+ threshold: detail.threshold,
262
+ scope,
263
+ failure_stage: detail.failure_stage ?? undefined,
264
+ error: detail.error ?? undefined,
265
+ });
266
+ const errorText = (detail.error || "").trim();
267
+ console.error("");
268
+ console.error(chalk.red.bold(`Стоп: ${detail.streak} ${pluralRu(detail.streak, "сборка", "сборки", "сборок")} ` +
269
+ (scope === "owner"
270
+ ? "в ваших проектах упали с одной и той же ошибкой."
271
+ : "подряд упали с одной и той же ошибкой.")));
272
+ if (errorText) {
273
+ console.error("");
274
+ console.error(chalk.red(errorText));
275
+ }
276
+ if (detail.failure_stage) {
277
+ console.error(chalk.dim(`Стадия: ${detail.failure_stage}`));
278
+ }
279
+ console.error("");
280
+ if (scope === "owner") {
281
+ console.error("Ошибка повторяется в разных проектах — значит, дело не в конкретном");
282
+ console.error("проекте, а в коде приложения или в настройках сборки.");
283
+ console.error("");
284
+ }
285
+ console.error("Повторная выкатка без изменений даст тот же результат. Исправьте причину —");
286
+ console.error("или подтвердите, что изменили что-то, влияющее на неё.");
287
+ console.error("");
288
+ const mode = detectMode();
289
+ if (mode.interactive) {
290
+ const rl = readline.createInterface({
291
+ input: process.stdin,
292
+ output: process.stdout,
293
+ });
294
+ try {
295
+ const answer = (await rl.question("Всё равно выкатить? [y/N]: ")).trim().toLowerCase();
296
+ if (answer !== "y" && answer !== "yes") {
297
+ throw new LayeroError("repeated_failure_declined", "deploy cancelled: the same error keeps failing the build", "fix the error above, then run `layero deploy` again");
298
+ }
299
+ }
300
+ finally {
301
+ rl.close();
302
+ }
303
+ return await api.startDeploySession(sessionId, {
304
+ commit_sha: commitSha,
305
+ confirm_repeated_failure: true,
306
+ });
307
+ }
308
+ throw new LayeroError("repeated_failure", `deploy stopped: the last ${detail.streak} deploys failed with the SAME error` +
309
+ (errorText ? ` — ${errorText}` : ""), "read the error above and fix its cause. Re-running the same deploy will " +
310
+ "fail the same way. If you changed something that affects it, re-run with " +
311
+ "`--confirm-repeated-failure`.");
312
+ }
313
+ }
201
314
  export async function deployCmd(opts) {
202
315
  const mode = detectMode();
203
316
  if (opts.type && !VALID_TYPES.has(opts.type.toLowerCase())) {
@@ -387,14 +500,20 @@ export async function deployCmd(opts) {
387
500
  expires_in: session.expires_in,
388
501
  }, pack.archivePath);
389
502
  emit({ event: "uploaded", archive_key: session.source_archive_key });
390
- const started = await api.startDeploySession(session.session_id, {
391
- commit_sha: pack.sha256,
392
- });
503
+ const started = await startWithRepeatedFailureGuard(api, session.session_id, pack.sha256, opts);
393
504
  if (!started.deploy_id) {
394
505
  throw new LayeroError("deploy_not_started", `deploy session ended as "${started.status}"${started.error ? `: ${started.error}` : ""}`, "re-run `layero deploy`; if it repeats, check the project in the dashboard");
395
506
  }
396
507
  emit({ event: "deploy_started", deploy_id: started.deploy_id });
397
508
  const final = await streamDeployLogs(api, started.deploy_id);
509
+ // Отмена — не отказ. С выделением 'cancelled' в отдельный статус
510
+ // (08.08.2026) прежняя строка напечатала бы «deploy failed (cancelled)»:
511
+ // деплой одновременно и упал, и отменён. Чаще всего причина — вытеснение
512
+ // более новым пушем, и совет «посмотрите логи» тут не по адресу: смотреть
513
+ // надо на деплой-преемник, а не на этот.
514
+ if (final.status === "cancelled") {
515
+ throw new LayeroError("deploy_cancelled", `deploy cancelled${final.error_message ? `: ${final.error_message}` : ""}`, "a newer deploy superseded this one — check the latest deploy in the dashboard");
516
+ }
398
517
  if (final.status !== "ready") {
399
518
  throw new LayeroError(`deploy_${final.status}`, `deploy failed (${final.status})${final.error_message ? `: ${final.error_message}` : ""}`, `inspect logs at ${projectUrl(cliCfg.apiUrl, project.id)}`);
400
519
  }
@@ -20,8 +20,8 @@ is required — Layero packs and uploads the local directory directly.
20
20
 
21
21
  ### First-time auth (one-click device flow)
22
22
 
23
- If you're not logged in yet, \`deploy\` (or \`login\`) starts the browser
24
- device-flow automatically and emits a JSON line:
23
+ If you're not logged in yet, \`npx layero@latest deploy\` (or \`… login\`)
24
+ starts the browser device-flow automatically and emits a JSON line:
25
25
 
26
26
  \`\`\`json
27
27
  {"event":"auth_required","url":"https://app.layero.ru/cli?code=ABCD-1234","user_code":"ABCD-1234"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
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",