layero 0.11.12 → 0.11.14

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.
@@ -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";
@@ -552,7 +553,8 @@ function asAccountStateError(err) {
552
553
  // Общий совет — последний: причина и лекарство чаще всего уже в тексте
553
554
  // отказа (подсказки SQL про роли и Авторизацию, T-20261025-9), и
554
555
  // «исправьте запрос» уводил агента от них.
555
- detail.next_action ?? "причина и что сделать — в тексте отказа; исправьте и повторите");
556
+ detail.next_action ?? detail.hint ?? nextActionFor(err.status, detail.message) ??
557
+ "прочитайте message: в нём причина отказа сервера; поправьте команду и повторите");
556
558
  }
557
559
  }
558
560
  return null;
@@ -581,6 +583,8 @@ function detailOf(body) {
581
583
  code: typeof detail.code === "string" ? detail.code : undefined,
582
584
  message,
583
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,
584
588
  };
585
589
  }
586
590
  }
@@ -438,12 +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
- // Что снимает переприменение (сверено с агентом узла 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 новые таблицы, будут сняты; уже выданные права на таблицы сохранятся";
441
+ // Что делает переприменение (сверено с агентом узла 26.10.2026,
442
+ // `runtime/userdb-agent/agent.py`, `api_enable` и `supabase_parity`): роли
443
+ // Data API и их пароли заводятся заново, права по умолчанию на `public` — как у
444
+ // Supabase (T-20261025-32). Выданные и закрытые владельцем права на таблицы
445
+ // СОХРАНЯЮТСЯ: разовые шаги перехода повторно не выполняются.
446
+ const REPAIR_LOSS = "роли Data API и права по умолчанию будут переприменены (как в Supabase); выданные и закрытые вами права на таблицы сохранятся";
447
447
  /**
448
448
  * Включение Data API.
449
449
  *
@@ -495,6 +495,13 @@ export async function dataEnableCmd(opts) {
495
495
  public_key: publicKey,
496
496
  secret_key: secretKey,
497
497
  reapplied,
498
+ // Итог включения словами: какие таблицы открыты, какие без RLS закрыты.
499
+ warnings: res.warnings ?? [],
500
+ ...(publicKey || secretKey
501
+ ? { next_action: "ключи показаны один раз — сохраните их сейчас. Публичный потом: layero data env " +
502
+ `(у подключённого проекта) или новый: layero data keys issue --db ${shellArg(ref)}; ` +
503
+ "секретный больше не покажут — только выпуск нового" }
504
+ : {}),
498
505
  });
499
506
  return;
500
507
  }
@@ -502,9 +509,10 @@ export async function dataEnableCmd(opts) {
502
509
  // агент снимает права только в `public`, уровни в `api` и `app` остаются.
503
510
  console.log(reapplied
504
511
  ? `${chalk.green("✓")} Data API переприменён у базы «${db.name}»\n` +
505
- chalk.yellow(" Выданные права на таблицы сохранились. Сняты только умолчания, которые сами открывали " +
506
- "новые таблицы ролям Data API: новые таблицы открывайте явно — layero data grant")
512
+ chalk.yellow(" Выданные права на таблицы сохранились, права по умолчанию — как в Supabase")
507
513
  : `${chalk.green("✓")} Data API включён у базы «${db.name}»`);
514
+ for (const w of res.warnings ?? [])
515
+ console.log(chalk.yellow(`\n ⚠ ${w}`));
508
516
  if (publicKey)
509
517
  console.log(`\n публичный ключ: ${publicKey}`);
510
518
  if (secretKey) {
@@ -514,7 +522,8 @@ export async function dataEnableCmd(opts) {
514
522
  if (!publicKey && !secretKey)
515
523
  console.log(chalk.dim(` ключи уже были — список: layero data keys list --db ${ref}`));
516
524
  if (!reapplied) {
517
- console.log(chalk.dim("\n Все методы закрыты, пока вы их не откроете: layero data methods, затем layero data grant"));
525
+ console.log(chalk.dim("\n Как в Supabase: новые таблицы public открыты ролям, строки решает RLS — включайте его у " +
526
+ "каждой таблицы. Что открыто: layero data methods"));
518
527
  }
519
528
  }
520
529
  /**
@@ -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 true;
132
+ return "active";
134
133
  if (mine && mine.status === "failed")
135
- return false;
134
+ return "failed";
136
135
  }
137
136
  catch {
138
137
  // Сеть моргнула — это не повод объявлять базу несозданной.
139
138
  }
139
+ await new Promise((r) => setTimeout(r, pollMs));
140
140
  }
141
- return false;
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 (mode.json || opts.json) {
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
- process.stdout.write(`${chalk.dim("…")} готовим базу «${name}» в организации ${org}`);
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 {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.11.12",
3
+ "version": "0.11.14",
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",