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 +11 -0
- package/dist/api.js +1 -0
- package/dist/bin/layero.js +20 -3
- package/dist/commands/db.js +67 -3
- package/dist/commands/deploy.js +12 -0
- package/package.json +1 -1
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
package/dist/bin/layero.js
CHANGED
|
@@ -225,15 +225,32 @@ async function main() {
|
|
|
225
225
|
withOrg(db
|
|
226
226
|
.command("create <name>")
|
|
227
227
|
.description("Завести базу. Строка подключения печатается ОДИН раз.")
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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("Яндекс.Метрика: подключение и статистика сайта.");
|
package/dist/commands/db.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
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", "нечего выполнять", 'передайте
|
|
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);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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