layero 0.9.2 → 0.9.4

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
@@ -141,6 +141,17 @@ function renderHuman(event) {
141
141
  case "setup_applied":
142
142
  process.stdout.write(`✓ Setup applied\n`);
143
143
  break;
144
+ case "preview_evicted": {
145
+ // Предупреждение, а не отчёт: перестал отвечать адрес, который кому-то
146
+ // уже отдали ссылкой. Пишем имя ветки — по нему её и возвращают.
147
+ const names = event.evicted
148
+ .map((e) => e.branch ?? e.hostname ?? "")
149
+ .filter(Boolean)
150
+ .join(", ");
151
+ process.stdout.write(`! Превью ${names} приостановлено — этот деплой занял его место\n`);
152
+ process.stdout.write(` Вернуть: задеплойте ту ветку снова или откройте панель\n`);
153
+ break;
154
+ }
144
155
  case "runtime_type_applied":
145
156
  process.stdout.write(`✓ Project type set to ${event.project_type}\n`);
146
157
  break;
package/dist/api.js CHANGED
@@ -153,6 +153,7 @@ export class ApiClient {
153
153
  name: input.name,
154
154
  quota_gb: input.quota_gb ?? null,
155
155
  extensions: input.extensions ?? [],
156
+ preset: input.preset ?? true,
156
157
  });
157
158
  }
158
159
  connectDatabaseToProject(org, dbId, projectId) {
@@ -225,15 +225,32 @@ async function main() {
225
225
  withOrg(db
226
226
  .command("create <name>")
227
227
  .description("Завести базу. Строка подключения печатается ОДИН раз.")
228
- .option("--gb <number>", "объём платной базы в гигабайтах", (v) => parseInt(v, 10))).action(async (name, opts) => dbCreateCmd(name, { ...opts, json: program.opts().json }));
228
+ // 🚨 ФЛАГ ОСТАВЛЕН РАДИ ЧЕСТНОГО ОТКАЗА, А НЕ РАДИ РАБОТЫ. Он обещал
229
+ // «объём платной базы в гигабайтах» и не делал НИЧЕГО: сервер поле не
230
+ // читает ни одним путём создания — у Shared объём задаёт тариф, у
231
+ // выделенного диск ступени. Убрать его совсем значило бы отвечать
232
+ // «неизвестный параметр» тому, кто им пользовался, и оставить человека
233
+ // гадать, куда делся объём. Теперь команда говорит это словами (C9).
234
+ .option("--gb <number>", "БОЛЬШЕ НЕ РАБОТАЕТ: объём задаёт тариф или ступень", (v) => parseInt(v, 10))).action(async (name, opts) => dbCreateCmd(name, { ...opts, json: program.opts().json }));
229
235
  withOrg(db
230
236
  .command("connect <database>")
231
237
  .description("Подключить проект к базе: строка подключения приедет в его переменные.")
232
238
  .option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbConnectCmd(database, { ...opts, json: program.opts().json }));
233
239
  withOrg(db
234
- .command("sql <database>")
240
+ // 🚨 ЗАПРОС МОЖНО ПИСАТЬ ПРОСТО СЛЕДОМ ЗА ИМЕНЕМ БАЗЫ. Раньше `-c` был
241
+ // ОБЯЗАТЕЛЕН, и `layero db sql моя-база "select 1"` отвечал
242
+ // «required option -c, --command not specified» — то есть отказывал на
243
+ // самой очевидной форме записи. Так эта команда и записана в приёмке
244
+ // (C6), и так её пишет всякий, кто помнит psql.
245
+ .command("sql <database> [sql]")
235
246
  .description("Выполнить SQL в базе. Скрипт из нескольких операторов — одной транзакцией.")
236
- .requiredOption("-c, --command <sql>", "запрос или скрипт")).action(async (database, opts) => dbSqlCmd(database, { ...opts, json: program.opts().json }));
247
+ .option("-c, --command <sql>", "запрос или скрипт")).action(async (database, sql, opts) => dbSqlCmd(database, {
248
+ ...opts,
249
+ // Явный `-c` выигрывает: он написан намеренно, а позиционный аргумент
250
+ // мог прилететь из истории команд.
251
+ command: opts.command ?? sql,
252
+ json: program.opts().json,
253
+ }));
237
254
  const analytics = program
238
255
  .command("analytics")
239
256
  .description("Яндекс.Метрика: подключение и статистика сайта.");
@@ -59,22 +59,86 @@ export async function dbListCmd(opts) {
59
59
  }
60
60
  for (const d of list) {
61
61
  const api_on = d.api_enabled ? chalk.green("API") : chalk.dim("без API");
62
+ // 🚨 РАЗМЕЩЕНИЕ И ВЕРСИЯ — В СТРОКЕ, А НЕ ТОЛЬКО В `--json`. Их не было
63
+ // вовсе: по выводу нельзя было отличить базу из тарифа от выделенного
64
+ // инстанса за 7750 ₽ в месяц и узнать, какой там Postgres. Панель обе
65
+ // вещи показывает, а приёмка (C2) спрашивает именно их.
66
+ const place = d.placement === "dedicated"
67
+ ? "выделенный"
68
+ : d.placement === "external"
69
+ ? "свой сервер"
70
+ : "Shared";
71
+ const version = d.pg_version ? `PG${d.pg_version}` : chalk.dim("PG—");
62
72
  console.log(`${chalk.bold(d.name)} ${chalk.dim(d.name_slug ?? "")} ${d.status} ` +
73
+ `${place} ${version} ` +
63
74
  `${api_on} проектов: ${d.projects_count} ${gb(d.size_bytes)} из ${gb(d.quota_bytes)}`);
64
75
  }
65
76
  }
77
+ /** Ждёт, пока база станет рабочей. `false` — не дождались за отведённое время.
78
+ *
79
+ * Опрос, а не подписка: ручки событий у нас нет, а держать соединение ради
80
+ * одной строки состояния незачем. Потолок ожидания — своё число, а не «пока
81
+ * не надоест»: выделенный кластер поднимается 7–13 минут, и повесить терминал
82
+ * на всё это время нельзя. Не дождались — говорим об этом прямо.
83
+ */
84
+ async function waitUntilReady(api, org, name, timeoutMs = 90_000) {
85
+ const until = Date.now() + timeoutMs;
86
+ while (Date.now() < until) {
87
+ await new Promise((r) => setTimeout(r, 2000));
88
+ try {
89
+ const all = await api.listDatabases(org);
90
+ const mine = all.find((d) => d.name === name);
91
+ if (mine && mine.status === "active")
92
+ return true;
93
+ if (mine && mine.status === "failed")
94
+ return false;
95
+ }
96
+ catch {
97
+ // Сеть моргнула — это не повод объявлять базу несозданной.
98
+ }
99
+ }
100
+ return false;
101
+ }
66
102
  export async function dbCreateCmd(name, opts) {
67
103
  const mode = detectMode();
104
+ // 🚨 `--gb` НЕ ДЕЛАЛ НИЧЕГО, И МОЛЧАНИЕ БЫЛО ХУЖЕ ОТКАЗА. Поле уезжало на
105
+ // сервер, а тот его не читает ни одним путём создания: у Shared объём задаёт
106
+ // тариф, у выделенного — диск ступени. Человек называл число, получал базу
107
+ // другого размера и не знал почему (приёмка C9).
108
+ if (opts.gb != null) {
109
+ throw new LayeroError("gb_not_supported", "объём базы так не выбирается", "у базы из тарифа объём задан тарифом, у выделенного инстанса — диском " +
110
+ "ступени: закажите нужную ступень в панели");
111
+ }
68
112
  const api = new ApiClient(await loadConfig());
69
113
  const org = await orgOf(api, opts);
70
- const created = await api.createDatabase(org, { name, quota_gb: opts.gb });
114
+ const created = await api.createDatabase(org, {
115
+ name,
116
+ // 🚨 ФЛАГА «БЕЗ НАПОЛНЕНИЯ» БОЛЬШЕ НЕТ. Он существовал, пока пресет
117
+ // заводил три таблицы с примерами: человек со своей схемой получал чужие
118
+ // таблицы молча, и отказаться было нечем. Тестовых таблиц в пресете не
119
+ // осталось — он настраивает роли, функцию состояния и умолчания прав,
120
+ // и вычищать после него нечего. Значит и отказываться не от чего.
121
+ });
71
122
  // Пароль отдаётся ОДИН РАЗ — второй раз его не покажет никто, только
72
123
  // ротация. Поэтому он и в JSON-режиме, и в терминале.
73
124
  if (mode.json || opts.json) {
74
125
  emit({ event: "database_created", org, name, ...created });
75
126
  return;
76
127
  }
77
- console.log(`${chalk.green("✓")} база «${name}» заведена в организации ${org}`);
128
+ // 🚨 БАЗА ЕЩЁ НЕ ГОТОВА В ЭТОТ МОМЕНТ. Ручка стала асинхронной: она пишет
129
+ // намерение и возвращается, подготовка идёт минуты. Печатать «заведена» и
130
+ // строку подключения сразу значит отправить человека к `psql`, который
131
+ // ответит «database does not exist», — и он пойдёт искать причину в пароле.
132
+ process.stdout.write(`${chalk.dim("…")} готовим базу «${name}» в организации ${org}`);
133
+ const ready = await waitUntilReady(api, org, name);
134
+ process.stdout.write("\r\u001b[2K");
135
+ if (ready) {
136
+ console.log(`${chalk.green("✓")} база «${name}» готова в организации ${org}`);
137
+ }
138
+ else {
139
+ console.log(`${chalk.yellow("!")} база «${name}» ещё готовится — строка подключения ниже ` +
140
+ "заработает, как только она поднимется");
141
+ }
78
142
  console.log(`\n ${created.connection_string}`);
79
143
  console.log(chalk.dim("\n Пароль показывается один раз — сохраните строку подключения.\n" +
80
144
  " Ключи Data API для фронтенда: layero data env"));
@@ -102,7 +166,7 @@ export async function dbSqlCmd(ref, opts) {
102
166
  const mode = detectMode();
103
167
  const sql = (opts.command ?? "").trim();
104
168
  if (!sql) {
105
- throw new LayeroError("sql_missing", "нечего выполнять", 'передайте запрос: -c "SELECT 1"');
169
+ throw new LayeroError("sql_missing", "нечего выполнять", 'передайте запрос следом за именем базы: layero db sql моя-база "select 1"');
106
170
  }
107
171
  const api = new ApiClient(await loadConfig());
108
172
  const org = await orgOf(api, opts);
@@ -638,6 +638,18 @@ export async function deployCmd(opts) {
638
638
  const liveUrl = promoted || !opts.branch
639
639
  ? (probe?.canonical_url ?? apexUrl)
640
640
  : (probe?.preview_url ?? probe?.canonical_url ?? apexUrl);
641
+ // V263: деплой мог занять слот превью и снять с раздачи чужую ветку.
642
+ // Говорим об этом ДО `ready`: агент, увидевший итог, дальше не читает, а
643
+ // здесь изменился чужой работающий адрес — тот, кому ссылку уже отдали,
644
+ // узнает об этом иначе только открыв её.
645
+ //
646
+ // Пустой список — обычный случай и молчит. Поля может не быть вовсе, если
647
+ // платформа старше него: `?? []` и никаких предупреждений о том, чего не
648
+ // знаем.
649
+ const evicted = deployRow.preview_evicted ?? [];
650
+ if (evicted.length > 0) {
651
+ emit({ event: "preview_evicted", evicted });
652
+ }
641
653
  emit({
642
654
  event: "ready",
643
655
  url: liveUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Layero CLI \u2014 publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
5
5
  "license": "MIT",
6
6
  "type": "module",