layero 0.11.11 → 0.11.13
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/api.js +14 -4
- package/dist/bin/layero.js +15 -3
- package/dist/commands/data-api.js +5 -0
- package/dist/commands/db.js +26 -11
- package/dist/commands/perf.js +5 -5
- package/dist/next-action.js +35 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -4,6 +4,16 @@ import { looksLikeId } from "./project-ref.js";
|
|
|
4
4
|
// Один опрос логов отвечает за доли секунды. 30 с — заведомо «запрос завис»,
|
|
5
5
|
// а не «сборка идёт долго»: длительность сборки на ответ этой ручки не влияет.
|
|
6
6
|
export const POLL_TIMEOUT_MS = 30_000;
|
|
7
|
+
/** Устройство и страница замера — одинаковые у запуска и у результата. */
|
|
8
|
+
function perfQuery(opts) {
|
|
9
|
+
const q = new URLSearchParams();
|
|
10
|
+
if (opts.device)
|
|
11
|
+
q.set("device", opts.device);
|
|
12
|
+
if (opts.path)
|
|
13
|
+
q.set("path", opts.path);
|
|
14
|
+
const s = q.toString();
|
|
15
|
+
return s ? `?${s}` : "";
|
|
16
|
+
}
|
|
7
17
|
export class ApiError extends Error {
|
|
8
18
|
status;
|
|
9
19
|
body;
|
|
@@ -292,12 +302,12 @@ export class ApiClient {
|
|
|
292
302
|
}
|
|
293
303
|
// --- Замеры (AGENT-11) ----------------------------------------------
|
|
294
304
|
/** Запустить замер активного деплоя. Не ждёт: прогон асинхронный. */
|
|
295
|
-
startPerfCheck(projectId) {
|
|
296
|
-
return this.request("POST", `/projects/${projectId}/perf-check`);
|
|
305
|
+
startPerfCheck(projectId, opts = {}) {
|
|
306
|
+
return this.request("POST", `/projects/${projectId}/perf-check${perfQuery(opts)}`);
|
|
297
307
|
}
|
|
298
308
|
/** Результат последнего замера со сравнением с предыдущим деплоем. */
|
|
299
|
-
getPerfCheck(projectId) {
|
|
300
|
-
return this.request("GET", `/projects/${projectId}/perf-check`);
|
|
309
|
+
getPerfCheck(projectId, opts = {}) {
|
|
310
|
+
return this.request("GET", `/projects/${projectId}/perf-check${perfQuery(opts)}`);
|
|
301
311
|
}
|
|
302
312
|
// --- Домены (AGENT-09) ---------------------------------------------
|
|
303
313
|
listDomains(projectId) {
|
package/dist/bin/layero.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { whoamiCmd } from "../commands/whoami.js";
|
|
5
|
+
import { nextActionFor } from "../next-action.js";
|
|
5
6
|
import { logoutCmd } from "../commands/logout.js";
|
|
6
7
|
import { projectsCreateCmd, projectsDeleteCmd, projectsListCmd } from "../commands/projects.js";
|
|
7
8
|
import { sourcesConnectCmd, sourcesListCmd, sourcesReposCmd } from "../commands/sources.js";
|
|
@@ -352,14 +353,18 @@ async function main() {
|
|
|
352
353
|
.description("Measure site performance and compare it with the previous deploy.");
|
|
353
354
|
perf
|
|
354
355
|
.command("check")
|
|
355
|
-
.description("
|
|
356
|
+
.description("Measure the main branch (Lighthouse score, verdict as in the dashboard). The run is asynchronous — with --wait the command waits for the result.")
|
|
356
357
|
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
357
358
|
.option("--wait", "wait for the result (up to 4 minutes)")
|
|
359
|
+
.option("--device <desktop|mobile>", "device profile (default: desktop)")
|
|
360
|
+
.option("--path <path>", "page on the main branch, e.g. /pricing (default: /)")
|
|
358
361
|
.action(async (opts) => perfCheckCmd({ ...opts, json: program.opts().json }));
|
|
359
362
|
perf
|
|
360
363
|
.command("show")
|
|
361
364
|
.description("Show the latest measurement and the comparison with the previous deploy.")
|
|
362
365
|
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
366
|
+
.option("--device <desktop|mobile>", "device profile (default: desktop)")
|
|
367
|
+
.option("--path <path>", "page on the main branch, e.g. /pricing (default: /)")
|
|
363
368
|
.action(async (opts) => perfShowCmd({ ...opts, json: program.opts().json }));
|
|
364
369
|
const domains = program
|
|
365
370
|
.command("domains")
|
|
@@ -527,7 +532,11 @@ function asAccountStateError(err) {
|
|
|
527
532
|
: "";
|
|
528
533
|
return new LayeroError("plan_limit", what
|
|
529
534
|
? `Тариф не позволяет больше ${what}${counts}.`
|
|
530
|
-
|
|
535
|
+
// Сервер прислал готовую причину («Замер скорости доступен на тарифе
|
|
536
|
+
// Pro.») — она точнее общего текста.
|
|
537
|
+
: typeof detail.message === "string" && detail.message
|
|
538
|
+
? detail.message
|
|
539
|
+
: "Действие недоступно на текущем тарифе.", "смените тариф на app.layero.ru/billing или освободите место, удалив ненужное");
|
|
531
540
|
}
|
|
532
541
|
// 🚨 Понятный отказ сервера не имеет права превращаться в нашу поломку.
|
|
533
542
|
// 409 с `reason: reserved` и текстом «имя зарезервировано платформой»
|
|
@@ -544,7 +553,8 @@ function asAccountStateError(err) {
|
|
|
544
553
|
// Общий совет — последний: причина и лекарство чаще всего уже в тексте
|
|
545
554
|
// отказа (подсказки SQL про роли и Авторизацию, T-20261025-9), и
|
|
546
555
|
// «исправьте запрос» уводил агента от них.
|
|
547
|
-
detail.next_action ??
|
|
556
|
+
detail.next_action ?? detail.hint ?? nextActionFor(err.status, detail.message) ??
|
|
557
|
+
"прочитайте message: в нём причина отказа сервера; поправьте команду и повторите");
|
|
548
558
|
}
|
|
549
559
|
}
|
|
550
560
|
return null;
|
|
@@ -573,6 +583,8 @@ function detailOf(body) {
|
|
|
573
583
|
code: typeof detail.code === "string" ? detail.code : undefined,
|
|
574
584
|
message,
|
|
575
585
|
next_action: typeof detail.next_action === "string" ? detail.next_action : undefined,
|
|
586
|
+
// Шлюз данных кладёт подсказку в `hint` (формат PostgREST).
|
|
587
|
+
hint: typeof detail.hint === "string" && detail.hint ? detail.hint : undefined,
|
|
576
588
|
};
|
|
577
589
|
}
|
|
578
590
|
}
|
|
@@ -495,6 +495,11 @@ export async function dataEnableCmd(opts) {
|
|
|
495
495
|
public_key: publicKey,
|
|
496
496
|
secret_key: secretKey,
|
|
497
497
|
reapplied,
|
|
498
|
+
...(publicKey || secretKey
|
|
499
|
+
? { next_action: "ключи показаны один раз — сохраните их сейчас. Публичный потом: layero data env " +
|
|
500
|
+
`(у подключённого проекта) или новый: layero data keys issue --db ${shellArg(ref)}; ` +
|
|
501
|
+
"секретный больше не покажут — только выпуск нового" }
|
|
502
|
+
: {}),
|
|
498
503
|
});
|
|
499
504
|
return;
|
|
500
505
|
}
|
package/dist/commands/db.js
CHANGED
|
@@ -122,23 +122,23 @@ export async function dbListCmd(opts) {
|
|
|
122
122
|
* не надоест»: выделенный кластер поднимается 7–13 минут, и повесить терминал
|
|
123
123
|
* на всё это время нельзя. Не дождались — говорим об этом прямо.
|
|
124
124
|
*/
|
|
125
|
-
async function waitUntilReady(api, org, name, timeoutMs = 90_000) {
|
|
125
|
+
export async function waitUntilReady(api, org, name, timeoutMs = 90_000, pollMs = 2000) {
|
|
126
126
|
const until = Date.now() + timeoutMs;
|
|
127
127
|
while (Date.now() < until) {
|
|
128
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
129
128
|
try {
|
|
130
129
|
const all = await api.listDatabases(org);
|
|
131
130
|
const mine = all.find((d) => d.name === name);
|
|
132
131
|
if (mine && mine.status === "active")
|
|
133
|
-
return
|
|
132
|
+
return "active";
|
|
134
133
|
if (mine && mine.status === "failed")
|
|
135
|
-
return
|
|
134
|
+
return "failed";
|
|
136
135
|
}
|
|
137
136
|
catch {
|
|
138
137
|
// Сеть моргнула — это не повод объявлять базу несозданной.
|
|
139
138
|
}
|
|
139
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
140
140
|
}
|
|
141
|
-
return
|
|
141
|
+
return "creating";
|
|
142
142
|
}
|
|
143
143
|
export async function dbCreateCmd(name, opts) {
|
|
144
144
|
const mode = detectMode();
|
|
@@ -181,25 +181,40 @@ export async function dbCreateCmd(name, opts) {
|
|
|
181
181
|
}
|
|
182
182
|
throw err;
|
|
183
183
|
});
|
|
184
|
+
// 🚨 ЖДЁМ ГОТОВНОСТИ И В JSON (T-20261025-33, п. 1). Раньше событие уходило
|
|
185
|
+
// сразу, пока база `creating`, и следующая команда агента (`data enable`,
|
|
186
|
+
// `db sql`) получала 404 «база не найдена или не активна» — агент искал
|
|
187
|
+
// ошибку в имени. Теперь событие несёт `status`, а `creating` после 90 с —
|
|
188
|
+
// с подсказкой, что делать.
|
|
189
|
+
const json = mode.json || opts.json;
|
|
190
|
+
if (!json)
|
|
191
|
+
process.stdout.write(`${chalk.dim("…")} готовим базу «${name}» в организации ${org}`);
|
|
192
|
+
const state = await waitUntilReady(api, org, name);
|
|
193
|
+
if (!json)
|
|
194
|
+
process.stdout.write("\r\u001b[2K");
|
|
184
195
|
// Пароль отдаётся ОДИН РАЗ — второй раз его не покажет никто, только
|
|
185
196
|
// ротация. Поэтому он и в JSON-режиме, и в терминале.
|
|
186
|
-
if (
|
|
197
|
+
if (json) {
|
|
187
198
|
// 🚨 Пароль — в ОДНОМ поле (T-20261025-12). Раньше он шёл ещё и отдельным
|
|
188
199
|
// `password`: агент маскировал строку подключения и не замечал второе
|
|
189
200
|
// поле, и пароль уезжал в вывод инструмента. `secrets` называет, что
|
|
190
201
|
// маскировать.
|
|
191
202
|
emit({ event: "database_created", org, name, connection_string: created.connection_string,
|
|
192
|
-
secrets: ["connection_string"]
|
|
203
|
+
secrets: ["connection_string"], status: state,
|
|
204
|
+
...(state === "creating"
|
|
205
|
+
? { next_action: `база ещё готовится — дождитесь status active в layero db list --org ${org} ` +
|
|
206
|
+
"(обычно до 2 минут), затем data enable / db sql" }
|
|
207
|
+
: state === "failed"
|
|
208
|
+
? { next_action: "подготовка базы не удалась — удалите её в панели и создайте заново; " +
|
|
209
|
+
"повторится — напишите в поддержку" }
|
|
210
|
+
: {}) });
|
|
193
211
|
return;
|
|
194
212
|
}
|
|
195
213
|
// 🚨 БАЗА ЕЩЁ НЕ ГОТОВА В ЭТОТ МОМЕНТ. Ручка стала асинхронной: она пишет
|
|
196
214
|
// намерение и возвращается, подготовка идёт минуты. Печатать «заведена» и
|
|
197
215
|
// строку подключения сразу значит отправить человека к `psql`, который
|
|
198
216
|
// ответит «database does not exist», — и он пойдёт искать причину в пароле.
|
|
199
|
-
|
|
200
|
-
const ready = await waitUntilReady(api, org, name);
|
|
201
|
-
process.stdout.write("\r\u001b[2K");
|
|
202
|
-
if (ready) {
|
|
217
|
+
if (state === "active") {
|
|
203
218
|
console.log(`${chalk.green("✓")} база «${name}» готова в организации ${org}`);
|
|
204
219
|
}
|
|
205
220
|
else {
|
package/dist/commands/perf.js
CHANGED
|
@@ -18,7 +18,7 @@ function render(d) {
|
|
|
18
18
|
if (d.score !== null && d.score !== undefined) {
|
|
19
19
|
const t = d.timings ?? {};
|
|
20
20
|
const bits = [
|
|
21
|
-
`балл ${d.score}`,
|
|
21
|
+
`балл Lighthouse ${d.score}`,
|
|
22
22
|
t.lcp_ms ? `LCP ${t.lcp_ms} мс` : null,
|
|
23
23
|
t.ttfb_ms ? `TTFB ${t.ttfb_ms} мс` : null,
|
|
24
24
|
].filter(Boolean);
|
|
@@ -32,12 +32,12 @@ export async function perfCheckCmd(opts) {
|
|
|
32
32
|
const id = await projectId(api, opts);
|
|
33
33
|
let started;
|
|
34
34
|
try {
|
|
35
|
-
started = await api.startPerfCheck(id);
|
|
35
|
+
started = await api.startPerfCheck(id, opts);
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
38
|
if (err instanceof ApiError && err.status === 429) {
|
|
39
39
|
// Замер уже идёт — это не ошибка, а ответ на вопрос «когда будет».
|
|
40
|
-
const current = await api.getPerfCheck(id).catch(() => null);
|
|
40
|
+
const current = await api.getPerfCheck(id, opts).catch(() => null);
|
|
41
41
|
if (current) {
|
|
42
42
|
if (mode.json) {
|
|
43
43
|
emit({ event: "perf_check", ...current });
|
|
@@ -67,7 +67,7 @@ export async function perfCheckCmd(opts) {
|
|
|
67
67
|
let last = started;
|
|
68
68
|
while (Date.now() < deadline) {
|
|
69
69
|
await new Promise((r) => setTimeout(r, 8000));
|
|
70
|
-
last = await api.getPerfCheck(id);
|
|
70
|
+
last = await api.getPerfCheck(id, opts);
|
|
71
71
|
if (last.verdict !== "pending")
|
|
72
72
|
break;
|
|
73
73
|
}
|
|
@@ -83,7 +83,7 @@ export async function perfShowCmd(opts) {
|
|
|
83
83
|
const id = await projectId(api, opts);
|
|
84
84
|
let out;
|
|
85
85
|
try {
|
|
86
|
-
out = await api.getPerfCheck(id);
|
|
86
|
+
out = await api.getPerfCheck(id, opts);
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
89
89
|
if (err instanceof ApiError && err.status === 404) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Что делать дальше — по известным отказам сервера (T-20261025-33, п. 2).
|
|
3
|
+
*
|
|
4
|
+
* Сервер отдаёт эти отказы строкой без подсказки, а агент читает именно
|
|
5
|
+
* `next_action`: общий совет «исправьте и повторите» уводил его искать
|
|
6
|
+
* опечатку там, где база ещё создаётся. Таблица — по тексту отказа, потому
|
|
7
|
+
* что кода у этих отказов нет; новый текст на сервере без строки здесь
|
|
8
|
+
* получит общий совет, а не неверный.
|
|
9
|
+
*/
|
|
10
|
+
export function nextActionFor(status, message) {
|
|
11
|
+
const m = message.toLowerCase();
|
|
12
|
+
const rules = [
|
|
13
|
+
[/не найдена или не активна/,
|
|
14
|
+
"если базу только что создали — она ещё готовится: дождитесь status active в layero db list " +
|
|
15
|
+
"и повторите; иначе проверьте имя базы там же"],
|
|
16
|
+
[/у организации уже есть/,
|
|
17
|
+
"общая база одна на организацию: используйте её (layero db list) или заведите выделенный " +
|
|
18
|
+
"инстанс в панели"],
|
|
19
|
+
[/relation .* does not exist|таблиц[аы] .* не найден/,
|
|
20
|
+
"таблицы нет: проверьте имя и схему — SELECT schemaname, tablename FROM pg_tables " +
|
|
21
|
+
"WHERE schemaname NOT IN ('pg_catalog', 'information_schema')"],
|
|
22
|
+
[/schema .* does not exist/,
|
|
23
|
+
"схемы нет: создайте её (CREATE SCHEMA …) или уберите имя схемы из запроса"],
|
|
24
|
+
[/permission denied/,
|
|
25
|
+
"у роли запроса нет прав: выполните его владельцем базы (layero db sql) или выдайте GRANT"],
|
|
26
|
+
[/syntax error/,
|
|
27
|
+
"синтаксическая ошибка в SQL — поправьте запрос по месту, указанному в сообщении"],
|
|
28
|
+
[/(лимит|тариф)/,
|
|
29
|
+
"смените тариф на app.layero.ru/billing или освободите место, удалив ненужное"],
|
|
30
|
+
];
|
|
31
|
+
for (const [re, action] of rules)
|
|
32
|
+
if (re.test(m))
|
|
33
|
+
return action;
|
|
34
|
+
return status === 404 ? "проверьте имя или идентификатор: layero db list / layero list покажут, что есть" : undefined;
|
|
35
|
+
}
|
package/package.json
CHANGED