layero 0.11.1 → 0.11.3
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 +1 -1
- package/dist/api.js +10 -2
- package/dist/bin/layero.js +2 -2
- package/dist/commands/claim.js +21 -0
- package/dist/commands/deploy.js +7 -1
- package/dist/commands/deploys.js +5 -3
- package/dist/commands/diagnose.js +1 -22
- package/dist/detect.js +9 -2
- package/dist/exit-codes.js +1 -0
- package/dist/logs.js +41 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -351,7 +351,7 @@ Full reference: <https://docs.layero.ru/cli/json-events>
|
|
|
351
351
|
| 2 | sign-in needed | `auth_required`, `auth_expired`, `auth_timeout` |
|
|
352
352
|
| 3 | not found | `project_unknown`, `project_not_found`, `org_unknown`, `hook_not_found`, `connection_not_found`, `claim_unknown`, … |
|
|
353
353
|
| 4 | invalid input | `invalid_type`, `prebuilt_no_dir`, `prebuilt_no_index`, `branch_unsupported`, `repo_format`, `token_missing`, `bad_format`, … |
|
|
354
|
-
| 5 | remote failure | `deploy_failed`, `deploy_cancelled`, `deploy_not_started`, `internal`, 5xx from the platform |
|
|
354
|
+
| 5 | remote failure | `deploy_failed`, `deploy_cancelled`, `deploy_not_started`, `deploy_watch_lost`, `internal`, 5xx from the platform |
|
|
355
355
|
|
|
356
356
|
## Ignore rules
|
|
357
357
|
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { LayeroError } from "./agent.js";
|
|
2
2
|
import { CLI_VERSION, userAgent } from "./version.js";
|
|
3
3
|
import { looksLikeId } from "./project-ref.js";
|
|
4
|
+
// Один опрос логов отвечает за доли секунды. 30 с — заведомо «запрос завис»,
|
|
5
|
+
// а не «сборка идёт долго»: длительность сборки на ответ этой ручки не влияет.
|
|
6
|
+
export const POLL_TIMEOUT_MS = 30_000;
|
|
4
7
|
export class ApiError extends Error {
|
|
5
8
|
status;
|
|
6
9
|
body;
|
|
@@ -28,10 +31,15 @@ export class ApiClient {
|
|
|
28
31
|
h["X-Layero-Cli-Version"] = CLI_VERSION;
|
|
29
32
|
return h;
|
|
30
33
|
}
|
|
31
|
-
async request(method, path, body
|
|
34
|
+
async request(method, path, body,
|
|
35
|
+
// Потолок ожидания ответа. По умолчанию его нет: загрузка архива и
|
|
36
|
+
// длинные ручки живут дольше любого разумного числа. Задаётся там, где
|
|
37
|
+
// запрос короткий и повторяемый (опрос логов сборки).
|
|
38
|
+
timeoutMs) {
|
|
32
39
|
const url = `${this.cfg.apiUrl.replace(/\/+$/, "")}${path}`;
|
|
33
40
|
const init = {
|
|
34
41
|
method,
|
|
42
|
+
...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
35
43
|
headers: this.headers(body !== undefined ? { "Content-Type": "application/json" } : undefined),
|
|
36
44
|
};
|
|
37
45
|
if (body !== undefined) {
|
|
@@ -312,7 +320,7 @@ export class ApiClient {
|
|
|
312
320
|
return this.request("DELETE", `/projects/${projectId}/domains/${domainId}`);
|
|
313
321
|
}
|
|
314
322
|
pollLogs(deployId, afterId) {
|
|
315
|
-
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}
|
|
323
|
+
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`, undefined, POLL_TIMEOUT_MS);
|
|
316
324
|
}
|
|
317
325
|
listProjectDeploys(projectId, branch) {
|
|
318
326
|
const qs = branch ? `?branch=${encodeURIComponent(branch)}` : "";
|
package/dist/bin/layero.js
CHANGED
|
@@ -79,7 +79,7 @@ async function main() {
|
|
|
79
79
|
});
|
|
80
80
|
program
|
|
81
81
|
.command("init")
|
|
82
|
-
.description("
|
|
82
|
+
.description("Optional. Write a Layero deployment block into your agent-instructions file so future chat sessions know how to deploy (updates whichever of AGENTS.md / CLAUDE.md / .cursorrules already exist; if none do, creates AGENTS.md), and create .layero/project.json — this folder's link to its Layero project, filled in by the first deploy. No detected settings are recorded: build settings live in layero.json (committed with the code) or in the project settings. `deploy` links the folder by itself, so init is never required.")
|
|
83
83
|
.option("-y, --yes", "non-interactive: accept all defaults")
|
|
84
84
|
.option("--skip-agent-docs", "do not touch AGENTS.md / CLAUDE.md / .cursorrules")
|
|
85
85
|
.action(async (opts) => {
|
|
@@ -429,7 +429,7 @@ async function main() {
|
|
|
429
429
|
"Framework, build command and output directory are detected by the platform; --dry-run shows the plan first.")
|
|
430
430
|
.option("--dry-run", "show how the platform will build this folder (framework, build command, output folder, where each came from, " +
|
|
431
431
|
"hints for monorepos / frontend+backend / custom build scripts) and exit; uploads nothing, needs no login")
|
|
432
|
-
.option("-t, --type <preset>", "type override — static preset (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | storybook | eleventy | hugo | static) " +
|
|
432
|
+
.option("-t, --type <preset>", "type override — static preset (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | storybook | eleventy | hugo | static | generic) " +
|
|
433
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
434
|
"`static` serves the files as they are and never runs a build; `generic` runs your own build command " +
|
|
435
435
|
"(layero.json buildCommand or the package.json build script) and serves the folder with index.html")
|
package/dist/commands/claim.js
CHANGED
|
@@ -76,6 +76,27 @@ export function claimTokenFor(cfg, projectId) {
|
|
|
76
76
|
return undefined;
|
|
77
77
|
return cfg.claim_tokens?.[projectId];
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Конфиг с токеном, которым можно ЧИТАТЬ проект этой папки.
|
|
81
|
+
*
|
|
82
|
+
* 🚨 Песочница (`deploy --claim`) — тоже вход. Её токен лежит в
|
|
83
|
+
* `~/.layero/config.json` по id проекта, и прав на чтение хватает; без этой
|
|
84
|
+
* ветки агент без аккаунта получал на упавшей сборке совет «выполни
|
|
85
|
+
* `layero login`» и не мог узнать причину отказа ничем, кроме панели, которая
|
|
86
|
+
* ему недоступна (T-20260918-8). Для `diagnose`, `logs`, `deploys list` — то,
|
|
87
|
+
* что читает; действия, меняющие сайт, этим путём не ходят.
|
|
88
|
+
*/
|
|
89
|
+
export async function configForFolder(opts, cwd) {
|
|
90
|
+
const cfg = await loadConfig();
|
|
91
|
+
if (cfg.token)
|
|
92
|
+
return cfg;
|
|
93
|
+
const linked = await loadProjectConfig(cwd);
|
|
94
|
+
const sameProject = !opts.project || opts.project === linked?.project_id || opts.project === linked?.slug;
|
|
95
|
+
const claim = sameProject ? claimTokenFor(cfg, linked?.project_id) : undefined;
|
|
96
|
+
if (claim)
|
|
97
|
+
return { ...cfg, token: claim };
|
|
98
|
+
throw new LayeroError("auth_required", "нужен вход", "выполни `layero login` или задай LAYERO_TOKEN");
|
|
99
|
+
}
|
|
79
100
|
function claimUrlFor(cfg, code) {
|
|
80
101
|
return `${dashboardOrigin(cfg.apiUrl)}/claim?code=${encodeURIComponent(code)}`;
|
|
81
102
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -572,7 +572,13 @@ export async function deployCmd(opts) {
|
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
574
|
else if (opts.claim) {
|
|
575
|
-
|
|
575
|
+
// 🚨 Совет НЕ предлагает `logout`. Агенты исполняют `next_action`
|
|
576
|
+
// дословно, а `logout` стирает сохранённый вход и токены песочниц — войти
|
|
577
|
+
// заново может только человек. Прогон evals 19.09: агент с чужим входом на
|
|
578
|
+
// машине сделал ровно это и оставил владельца без CLI.
|
|
579
|
+
throw new LayeroError("bad_format", "--claim — деплой без аккаунта, а на этой машине уже выполнен вход", "run the same command without --claim: the project is created in the signed-in account " +
|
|
580
|
+
"(`npx layero@latest whoami` shows which). Do not run `layero logout` for someone else — " +
|
|
581
|
+
"it deletes the saved login, and only a person can sign in again");
|
|
576
582
|
}
|
|
577
583
|
const api = new ApiClient(cliCfg);
|
|
578
584
|
// 🚨 `--branch` У АРХИВНОЙ ЗАГРУЗКИ НЕ РАБОТАЕТ, И МОЛЧАТЬ ОБ ЭТОМ НЕЛЬЗЯ.
|
package/dist/commands/deploys.js
CHANGED
|
@@ -2,6 +2,7 @@ import readline from "node:readline/promises";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { ApiClient, ApiError } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
+
import { configForFolder } from "./claim.js";
|
|
5
6
|
import { loadProjectConfig } from "../project-config.js";
|
|
6
7
|
import { detectMode, LayeroError } from "../agent.js";
|
|
7
8
|
async function resolveProjectId(api, cwd, override) {
|
|
@@ -52,9 +53,10 @@ function fmtSource(d) {
|
|
|
52
53
|
return chalk.dim("(push)");
|
|
53
54
|
}
|
|
54
55
|
export async function deploysListCmd(opts) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Токеном песочницы тоже: совет при отменённой выкатке ведёт сюда, и агент
|
|
57
|
+
// без аккаунта получал «not logged in» там, где `logs` и `diagnose` уже
|
|
58
|
+
// работали (прогон evals 19.09).
|
|
59
|
+
const cliCfg = await configForFolder(opts, process.cwd());
|
|
58
60
|
const api = new ApiClient(cliCfg);
|
|
59
61
|
const projectId = await resolveProjectId(api, process.cwd(), opts.project);
|
|
60
62
|
const deploys = await api.listProjectDeploys(projectId, opts.branch);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ApiClient, ApiError } from "../api.js";
|
|
3
|
-
import { loadConfig } from "../config.js";
|
|
4
3
|
import { loadProjectConfig } from "../project-config.js";
|
|
5
4
|
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
|
-
import {
|
|
5
|
+
import { configForFolder } from "./claim.js";
|
|
7
6
|
async function resolveDeployId(api, opts, cwd) {
|
|
8
7
|
if (opts.deploy) {
|
|
9
8
|
// Явно указанный деплой: проект знать не обязательно, ручка работает
|
|
@@ -36,26 +35,6 @@ async function resolveDeployId(api, opts, cwd) {
|
|
|
36
35
|
const target = deploys[0];
|
|
37
36
|
return { deployId: target.id, projectId: project.id };
|
|
38
37
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Конфиг с токеном, которым можно читать деплои этой папки.
|
|
41
|
-
*
|
|
42
|
-
* 🚨 Песочница (`deploy --claim`) — тоже вход. Её токен лежит в
|
|
43
|
-
* `~/.layero/config.json` по id проекта и прав на чтение хватает; без этой
|
|
44
|
-
* ветки агент без аккаунта получал на упавшей сборке совет «выполни
|
|
45
|
-
* `layero login`» — то есть не мог узнать причину отказа ничем, кроме панели,
|
|
46
|
-
* которая ему недоступна (T-20260918-8).
|
|
47
|
-
*/
|
|
48
|
-
async function configForFolder(opts, cwd) {
|
|
49
|
-
const cfg = await loadConfig();
|
|
50
|
-
if (cfg.token)
|
|
51
|
-
return cfg;
|
|
52
|
-
const linked = await loadProjectConfig(cwd);
|
|
53
|
-
const sameProject = !opts.project || opts.project === linked?.project_id || opts.project === linked?.slug;
|
|
54
|
-
const claim = sameProject ? claimTokenFor(cfg, linked?.project_id) : undefined;
|
|
55
|
-
if (claim)
|
|
56
|
-
return { ...cfg, token: claim };
|
|
57
|
-
throw new LayeroError("auth_required", "нужен вход", "выполни `layero login` или задай LAYERO_TOKEN");
|
|
58
|
-
}
|
|
59
38
|
export async function diagnoseCmd(opts) {
|
|
60
39
|
const mode = detectMode();
|
|
61
40
|
const cfg = await configForFolder(opts, process.cwd());
|
package/dist/detect.js
CHANGED
|
@@ -531,11 +531,18 @@ export async function detectProject(cwd, opts = {}) {
|
|
|
531
531
|
? (opts.hintSource ?? "--type")
|
|
532
532
|
: "detected";
|
|
533
533
|
if (plan.runtimeKind !== null) {
|
|
534
|
-
|
|
534
|
+
// Файл объявил, КАК запускать (`runtime`, фуллстек-блоки), — это не то же
|
|
535
|
+
// самое, что назвать фреймворк: имя (`fastapi`, `express`) детект берёт из
|
|
536
|
+
// зависимостей. Без этого различия `sources.framework` приписывал файлу
|
|
537
|
+
// ключ, которого в нём нет (прогон evals 19.09, кейс b3).
|
|
538
|
+
const kindFromFile = layero !== null && (str(layero.runtime) !== null || plan.projectKind === "fullstack");
|
|
539
|
+
const nameFromFile = fileFramework !== null ||
|
|
540
|
+
(plan.projectKind === "fullstack" && str(layero?.backend?.framework) !== null);
|
|
535
541
|
const sources = {
|
|
536
|
-
framework: (
|
|
542
|
+
framework: (nameFromFile ? "layero.json" : frameworkSource),
|
|
537
543
|
build_cmd: "none",
|
|
538
544
|
output_dir: "none",
|
|
545
|
+
runtime_kind: (kindFromFile ? "layero.json" : "detected"),
|
|
539
546
|
};
|
|
540
547
|
if (plan.projectType === "ssr_next") {
|
|
541
548
|
// SSR Next builds in the runtime-builder; surface the framework + the
|
package/dist/exit-codes.js
CHANGED
package/dist/logs.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { ApiError } from "./api.js";
|
|
3
|
+
import { detectMode, emit, LayeroError } from "./agent.js";
|
|
3
4
|
const POLL_INTERVAL_MS = 1500;
|
|
5
|
+
// 🚨 ОДИН СБОЙ ОПРОСА — НЕ КОНЕЦ НАБЛЮДЕНИЯ. До 0.11.3 `fetch` шёл без
|
|
6
|
+
// таймаута, а цикл не переживал ни одной сетевой ошибки: 18.09.2026 деплой с
|
|
7
|
+
// токеном дважды подряд молчал 3 и 15 минут и падал с `internal: fetch failed`,
|
|
8
|
+
// хотя обе сборки на платформе дошли до ready (T-20260918-16). Для агента это
|
|
9
|
+
// худший исход: он считает деплой проваленным и запускает его заново.
|
|
10
|
+
// Теперь: таймаут на запрос (api.ts), до MAX_POLL_FAILURES сбоев ПОДРЯД с
|
|
11
|
+
// растущей паузой, и свой код ошибки с подсказкой, где смотреть итог.
|
|
12
|
+
const MAX_POLL_FAILURES = 8;
|
|
13
|
+
const RETRY_BASE_MS = 1500;
|
|
14
|
+
const RETRY_CAP_MS = 10_000;
|
|
15
|
+
/** Сбой, который повторять бессмысленно: сервер ответил отказом по существу. */
|
|
16
|
+
/** Тестам не нужно ждать настоящие паузы. */
|
|
17
|
+
let delayScale = 1;
|
|
18
|
+
export function setRetryDelayScaleForTests(scale) {
|
|
19
|
+
delayScale = scale;
|
|
20
|
+
}
|
|
21
|
+
function retryDelayScale() {
|
|
22
|
+
return delayScale;
|
|
23
|
+
}
|
|
24
|
+
function isFatal(err) {
|
|
25
|
+
return err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;
|
|
26
|
+
}
|
|
4
27
|
function colorForStream(stream) {
|
|
5
28
|
switch (stream) {
|
|
6
29
|
case "meta":
|
|
@@ -29,6 +52,8 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
29
52
|
let afterId = 0;
|
|
30
53
|
let lastStage = null;
|
|
31
54
|
let hiddenNoted = false;
|
|
55
|
+
let failures = 0;
|
|
56
|
+
const sleep = (ms) => new Promise((res) => setTimeout(res, retryDelayScale() * ms));
|
|
32
57
|
const announce = (name) => {
|
|
33
58
|
if (name === lastStage)
|
|
34
59
|
return;
|
|
@@ -43,7 +68,21 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
43
68
|
// Loop until terminal — the server's per-stage timeouts bound duration.
|
|
44
69
|
// eslint-disable-next-line no-constant-condition
|
|
45
70
|
while (true) {
|
|
46
|
-
|
|
71
|
+
let poll;
|
|
72
|
+
try {
|
|
73
|
+
poll = await api.pollLogs(deployId, afterId);
|
|
74
|
+
failures = 0;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (isFatal(err))
|
|
78
|
+
throw err;
|
|
79
|
+
failures += 1;
|
|
80
|
+
if (failures >= MAX_POLL_FAILURES) {
|
|
81
|
+
throw new LayeroError("deploy_watch_lost", `lost connection to the build log after ${failures} attempts (${err.message}); the build itself keeps running on the platform`, `do NOT deploy again — check the result: \`npx layero@latest deploys list --json\`, then \`npx layero@latest logs --deploy ${deployId}\``);
|
|
82
|
+
}
|
|
83
|
+
await sleep(Math.min(RETRY_BASE_MS * 2 ** (failures - 1), RETRY_CAP_MS));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
47
86
|
// 🚨 Этап объявляем по СТРОКЕ, а не по текущему этапу деплоя. Опрос
|
|
48
87
|
// приносит пачку строк, где хвост прошлого этапа идёт вперемешку с
|
|
49
88
|
// началом нового, а `current_stage` — это уже новый: объявленный до
|
package/package.json
CHANGED