layero 0.11.10 → 0.11.12

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 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) {
@@ -352,14 +352,18 @@ async function main() {
352
352
  .description("Measure site performance and compare it with the previous deploy.");
353
353
  perf
354
354
  .command("check")
355
- .description("Start a measurement of the active deploy. The run is asynchronous — with --wait the command waits for the result.")
355
+ .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
356
  .option("--project <id_or_slug>", "project (default: the linked project)")
357
357
  .option("--wait", "wait for the result (up to 4 minutes)")
358
+ .option("--device <desktop|mobile>", "device profile (default: desktop)")
359
+ .option("--path <path>", "page on the main branch, e.g. /pricing (default: /)")
358
360
  .action(async (opts) => perfCheckCmd({ ...opts, json: program.opts().json }));
359
361
  perf
360
362
  .command("show")
361
363
  .description("Show the latest measurement and the comparison with the previous deploy.")
362
364
  .option("--project <id_or_slug>", "project (default: the linked project)")
365
+ .option("--device <desktop|mobile>", "device profile (default: desktop)")
366
+ .option("--path <path>", "page on the main branch, e.g. /pricing (default: /)")
363
367
  .action(async (opts) => perfShowCmd({ ...opts, json: program.opts().json }));
364
368
  const domains = program
365
369
  .command("domains")
@@ -527,7 +531,11 @@ function asAccountStateError(err) {
527
531
  : "";
528
532
  return new LayeroError("plan_limit", what
529
533
  ? `Тариф не позволяет больше ${what}${counts}.`
530
- : "Действие недоступно на текущем тарифе.", "смените тариф на app.layero.ru/billing или освободите место, удалив ненужное");
534
+ // Сервер прислал готовую причину («Замер скорости доступен на тарифе
535
+ // Pro.») — она точнее общего текста.
536
+ : typeof detail.message === "string" && detail.message
537
+ ? detail.message
538
+ : "Действие недоступно на текущем тарифе.", "смените тариф на app.layero.ru/billing или освободите место, удалив ненужное");
531
539
  }
532
540
  // 🚨 Понятный отказ сервера не имеет права превращаться в нашу поломку.
533
541
  // 409 с `reason: reserved` и текстом «имя зарезервировано платформой»
@@ -540,7 +548,11 @@ function asAccountStateError(err) {
540
548
  if (err.status >= 400 && err.status < 500) {
541
549
  const detail = detailOf(err.body);
542
550
  if (detail) {
543
- return new LayeroError(detail.code ?? `http_${err.status}`, detail.message, detail.next_action ?? "исправьте запрос и повторите");
551
+ return new LayeroError(detail.code ?? `http_${err.status}`, detail.message,
552
+ // Общий совет — последний: причина и лекарство чаще всего уже в тексте
553
+ // отказа (подсказки SQL про роли и Авторизацию, T-20261025-9), и
554
+ // «исправьте запрос» уводил агента от них.
555
+ detail.next_action ?? "причина и что сделать — в тексте отказа; исправьте и повторите");
544
556
  }
545
557
  }
546
558
  return null;
@@ -438,9 +438,12 @@ export async function dataGrantCmd(object, opts) {
438
438
  console.log(planLines(done).join("\n"));
439
439
  console.log(`\n${chalk.green("✓")} доступ изменён`);
440
440
  }
441
- // Что снимает переприменение: агент узла при каждом включении API отзывает у
442
- // ролей Data API всё в схеме `public` (`runtime/userdb-agent/agent.py`, `api_enable`).
443
- const REPAIR_LOSS = "роли Data API потеряют права на схему public (USAGE) и на все её таблицы";
441
+ // Что снимает переприменение (сверено с агентом узла 25.10.2026,
442
+ // `runtime/userdb-agent/agent.py`, `api_enable`): права на таблицы и USAGE на
443
+ // `public` у существующих ролей СОХРАНЯЮТСЯ (с T-20260914-3 снимается только у
444
+ // роли, созданной этим же включением; USAGE на `public` выдаётся с 25.10).
445
+ // Снимаются только умолчания, которые сами открывали ролям НОВЫЕ таблицы.
446
+ const REPAIR_LOSS = "умолчания, которые сами открывали ролям Data API новые таблицы, будут сняты; уже выданные права на таблицы сохранятся";
444
447
  /**
445
448
  * Включение Data API.
446
449
  *
@@ -456,7 +459,7 @@ const REPAIR_LOSS = "роли Data API потеряют права на схем
456
459
  */
457
460
  export async function dataEnableCmd(opts) {
458
461
  if (opts.repair && !opts.db) {
459
- throw new LayeroError("database_unknown", `переприменение Data API снимает права ролей на схему public — базу нужно указать явно`, "layero data enable --db <база> --repair");
462
+ throw new LayeroError("database_unknown", `переприменение Data API меняет умолчания прав у ролей базы — базу нужно указать явно`, "layero data enable --db <база> --repair");
460
463
  }
461
464
  const { api, org, db, ref } = await target(opts, "enable");
462
465
  if (db.api_enabled && !opts.repair) {
@@ -499,9 +502,8 @@ export async function dataEnableCmd(opts) {
499
502
  // агент снимает права только в `public`, уровни в `api` и `app` остаются.
500
503
  console.log(reapplied
501
504
  ? `${chalk.green("✓")} Data API переприменён у базы «${db.name}»\n` +
502
- chalk.yellow(" Роли Data API лишились прав на схему public (USAGE) и на все её таблицы. " +
503
- "Уровни таблиц в схемах api и app сохранились; таблицы public, если они были открыты, " +
504
- "откройте заново: layero data methods, затем layero data grant")
505
+ chalk.yellow(" Выданные права на таблицы сохранились. Сняты только умолчания, которые сами открывали " +
506
+ "новые таблицы ролям Data API: новые таблицы открывайте явно — layero data grant")
505
507
  : `${chalk.green("✓")} Data API включён у базы «${db.name}»`);
506
508
  if (publicKey)
507
509
  console.log(`\n публичный ключ: ${publicKey}`);
@@ -572,7 +574,7 @@ export function registerDataApiCommands(data, program) {
572
574
  .command("enable")
573
575
  .description("Enable the Data API for a database: sets up the api schema and roles, issues a public key. All methods start closed.")
574
576
  .option("--with-secret", "also issue a secret key for the server")
575
- .option("--repair", "re-apply the roles and the api schema for a database whose Data API is already enabled; revokes the roles' privileges on schema public")
577
+ .option("--repair", "re-apply the roles and the api schema for a database whose Data API is already enabled; table grants are kept, default privileges for new tables are removed")
576
578
  .option("-y, --yes", "do not ask for confirmation for --repair")).action(async (opts) => dataEnableCmd(json(opts)));
577
579
  registerDataProbeCommand(data, withDb, json);
578
580
  }
@@ -66,12 +66,16 @@ export async function dataEnvCmd(opts) {
66
66
  const project = await projectRef(api, opts);
67
67
  const vars = await api.dataEnv(project.id);
68
68
  if (!Object.keys(vars).length) {
69
+ // 🚨 Пустой ответ — не «всё в порядке» (T-20261025-9): агент видел
70
+ // `vars: {}` без подсказки и решал, что писать нечего. Говорим, почему
71
+ // пусто и что сделать, тем же `next_action`, что у отказов.
72
+ const next = `подключите к проекту базу с Data API: layero db connect <база> --project ${project.slug}; ` +
73
+ "Data API у базы: layero data enable --db <база> (или панель → Базы данных → база → Data API → «Включить Data API»)";
69
74
  if (mode.json) {
70
- emit({ event: "data_env", project: project.slug, vars: {} });
75
+ emit({ event: "data_env", project: project.slug, vars: {}, reason: "no_data_api_database", next_action: next });
71
76
  return;
72
77
  }
73
- console.log(chalk.dim(`у проекта "${project.slug}" нет базы с включённым Data API\n` +
74
- "включить: панель → база → «API» → «Включить API»"));
78
+ console.log(chalk.dim(`у проекта "${project.slug}" нет подключённой базы с включённым Data API\n${next}`));
75
79
  return;
76
80
  }
77
81
  if (mode.json) {
@@ -167,18 +167,29 @@ export async function dbCreateCmd(name, opts) {
167
167
  }
168
168
  const api = new ApiClient(config);
169
169
  const org = await orgOf(api, opts);
170
- const created = await api.createDatabase(org, {
171
- name,
172
- // 🚨 ФЛАГА «БЕЗ НАПОЛНЕНИЯ» БОЛЬШЕ НЕТ. Он существовал, пока пресет
173
- // заводил три таблицы с примерами: человек со своей схемой получал чужие
174
- // таблицы молча, и отказаться было нечем. Тестовых таблиц в пресете не
175
- // осталось — он настраивает роли, функцию состояния и умолчания прав,
176
- // и вычищать после него нечего. Значит и отказываться не от чего.
170
+ // 🚨 ФЛАГА «БЕЗ НАПОЛНЕНИЯ» БОЛЬШЕ НЕТ. Он существовал, пока пресет
171
+ // заводил три таблицы с примерами: человек со своей схемой получал чужие
172
+ // таблицы молча, и отказаться было нечем. Тестовых таблиц в пресете не
173
+ // осталось — он настраивает роли, функцию состояния и умолчания прав,
174
+ // и вычищать после него нечего. Значит и отказываться не от чего.
175
+ const created = await api.createDatabase(org, { name }).catch((err) => {
176
+ // 409 — место бесплатной базы занято или лимит тарифа (T-20261025-9):
177
+ // общий совет «исправьте запрос» здесь ничего не говорил.
178
+ if (err instanceof LayeroError && err.code === "http_409") {
179
+ throw new LayeroError(err.code, err.message, "удалите ненужную базу (layero db list; удаление — в панели: Базы данных → база → Настройки) " +
180
+ "или заведите базу со своей конфигурацией в панели");
181
+ }
182
+ throw err;
177
183
  });
178
184
  // Пароль отдаётся ОДИН РАЗ — второй раз его не покажет никто, только
179
185
  // ротация. Поэтому он и в JSON-режиме, и в терминале.
180
186
  if (mode.json || opts.json) {
181
- emit({ event: "database_created", org, name, ...created });
187
+ // 🚨 Пароль — в ОДНОМ поле (T-20261025-12). Раньше он шёл ещё и отдельным
188
+ // `password`: агент маскировал строку подключения и не замечал второе
189
+ // поле, и пароль уезжал в вывод инструмента. `secrets` называет, что
190
+ // маскировать.
191
+ emit({ event: "database_created", org, name, connection_string: created.connection_string,
192
+ secrets: ["connection_string"] });
182
193
  return;
183
194
  }
184
195
  // 🚨 БАЗА ЕЩЁ НЕ ГОТОВА В ЭТОТ МОМЕНТ. Ручка стала асинхронной: она пишет
@@ -209,13 +209,6 @@ async function assertSandboxServesFiles(cwd, opts, existing, detectFolder) {
209
209
  return;
210
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
211
  }
212
- /** Обёртка временного сайта по его адресу: `<панель>/preview/<метка>`.
213
- * Считается здесь для повторной выкатки — платформа отдаёт поле только при
214
- * создании песочницы. */
215
- function sandboxPreviewUrl(siteUrl) {
216
- const label = new URL(siteUrl).hostname.split(".")[0];
217
- return `https://app.layero.ru/preview/${label}`;
218
- }
219
212
  /**
220
213
  * 🚨 ДЕТЕКТ — ОДИН НА ВЫКАТКУ. Гейт core «входов анализа не становится
221
214
  * больше» считает запуски детекта в этом файле: каждый лишний — ещё один
@@ -942,15 +935,15 @@ export async function deployCmd(opts) {
942
935
  }
943
936
  // Claimable: ссылка «забрать» — ДО `ready`, потому что после `ready`
944
937
  // агент не читает, а без этой ссылки сайт исчезнет через час.
945
- // `preview_url` — обёртка панели: пока сайт временный, человеку
946
- // открывается только она; `url` остаётся для проверок и агентов.
938
+ // `preview_url` — ссылка для людей. С 22.10.2026 это сам сайт
939
+ // `<метка>.preview.layero.app`: обёртки панели больше нет.
947
940
  if (claimable) {
948
941
  emit({
949
942
  event: "claimable",
950
943
  project_id: claimable.project_id,
951
944
  slug: claimable.slug,
952
945
  url: liveUrl,
953
- preview_url: claimable.preview_url ?? sandboxPreviewUrl(liveUrl),
946
+ preview_url: claimable.preview_url ?? liveUrl,
954
947
  claim_url: claimable.claim_url,
955
948
  expires_at: claimable.expires_at,
956
949
  });
@@ -961,7 +954,7 @@ export async function deployCmd(opts) {
961
954
  project_id: project.id,
962
955
  slug: project.slug,
963
956
  url: liveUrl,
964
- preview_url: sandboxPreviewUrl(liveUrl),
957
+ preview_url: liveUrl,
965
958
  claim_url: reusedClaim.claim_url,
966
959
  expires_at: reusedClaim.expires_at,
967
960
  });
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.11.10",
3
+ "version": "0.11.12",
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",