layero 0.9.3 → 0.9.5
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/README.md +1 -1
- package/dist/api.js +6 -0
- package/dist/bin/layero.js +37 -5
- package/dist/commands/db.js +159 -5
- package/dist/commands/deploy.js +1 -13
- package/dist/commands/env.js +13 -2
- package/dist/urls.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npx layero@latest deploy --json # ships current dir, streams JSON events to st
|
|
|
14
14
|
|
|
15
15
|
`layero deploy` prints `{"event":"ready","url":"https://..."}` on success. Show the URL to the user. That's the whole job.
|
|
16
16
|
|
|
17
|
-
**If the user has no code yet** and wants a landing page built from scratch, this CLI is the wrong tool. Layero runs a remote MCP server for that — `https://mcp.layero.ru/mcp` (Streamable HTTP), registered as `ru.layero/layero` in the official MCP registry. It builds the page from a two-or-three-question brief and deploys it. Install it with `claude plugin marketplace add LayeroInfra/layero-
|
|
17
|
+
**If the user has no code yet** and wants a landing page built from scratch, this CLI is the wrong tool. Layero runs a remote MCP server for that — `https://mcp.layero.ru/mcp` (Streamable HTTP), registered as `ru.layero/layero` in the official MCP registry. It builds the page from a two-or-three-question brief and deploys it. Install it with `claude plugin marketplace add LayeroInfra/layero-agents && claude plugin install layero@layero-claude`, `codex mcp add layero --url https://mcp.layero.ru/mcp --bearer-token-env-var LAYERO_TOKEN`, or one click in Cursor from [land.layero.app](https://land.layero.app). Details: [docs.layero.ru/en/plugin/intro](https://docs.layero.ru/en/plugin/intro).
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
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) {
|
|
@@ -160,6 +161,11 @@ export class ApiClient {
|
|
|
160
161
|
project_id: projectId,
|
|
161
162
|
});
|
|
162
163
|
}
|
|
164
|
+
/** Отвязать проект от базы. Роль проекта при этом удаляется, а переменная
|
|
165
|
+
* уходит из его окружения — не сразу, а следующим деплоем. */
|
|
166
|
+
disconnectDatabaseFromProject(org, dbId, projectId) {
|
|
167
|
+
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/projects/${projectId}`);
|
|
168
|
+
}
|
|
163
169
|
queryDatabase(org, dbId, sql) {
|
|
164
170
|
return this.request("POST", `/organizations/${org}/databases/${dbId}/query`, {
|
|
165
171
|
sql,
|
package/dist/bin/layero.js
CHANGED
|
@@ -16,7 +16,7 @@ import { initCmd } from "../commands/init.js";
|
|
|
16
16
|
import { diagnoseCmd, logsCmd } from "../commands/diagnose.js";
|
|
17
17
|
import { perfCheckCmd, perfShowCmd } from "../commands/perf.js";
|
|
18
18
|
import { dataEnvCmd } from "../commands/data.js";
|
|
19
|
-
import { dbConnectCmd, dbCreateCmd, dbListCmd, dbSqlCmd } from "../commands/db.js";
|
|
19
|
+
import { dbConnectCmd, dbCreateCmd, dbDisconnectCmd, dbListCmd, dbSqlCmd, } from "../commands/db.js";
|
|
20
20
|
import { envListCmd, envSetCmd, envUnsetCmd } from "../commands/env.js";
|
|
21
21
|
import { analyticsConnectCmd, analyticsDisconnectCmd, analyticsStatsCmd, analyticsStatusCmd, } from "../commands/analytics.js";
|
|
22
22
|
import { domainsAddCmd, domainsListCmd, domainsPrimaryCmd, domainsRemoveCmd, domainsVerifyCmd, } from "../commands/domains.js";
|
|
@@ -225,15 +225,47 @@ 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))
|
|
235
|
+
// 🚨 ТЕ ЖЕ ФЛАГИ РАДИ ЧЕСТНОГО ОТКАЗА. Выделенный инстанс из терминала
|
|
236
|
+
// не заказать, и это осознанно: у заказа есть цена и заморозка денег, а
|
|
237
|
+
// карту в терминале не привяжешь и сумму подтвердить негде. Но человек,
|
|
238
|
+
// прочитавший про ступени в панели, попробует `--cpu 2` — и без этих
|
|
239
|
+
// флагов получит «неизвестный параметр», то есть ответ про синтаксис
|
|
240
|
+
// вместо ответа про причину.
|
|
241
|
+
.option("--cpu <number>", "выделенный инстанс: заказывается в панели", (v) => parseInt(v, 10))
|
|
242
|
+
.option("--ram <mb>", "выделенный инстанс: заказывается в панели", (v) => parseInt(v, 10))
|
|
243
|
+
.option("--dedicated", "выделенный инстанс: заказывается в панели")).action(async (name, opts) => dbCreateCmd(name, { ...opts, json: program.opts().json }));
|
|
229
244
|
withOrg(db
|
|
230
245
|
.command("connect <database>")
|
|
231
|
-
.description("Подключить проект к базе: строка подключения приедет в его
|
|
246
|
+
.description("Подключить проект к базе: строка подключения приедет в его "
|
|
247
|
+
+ "переменные, а домены проекта станут разрешёнными для Data API.")
|
|
232
248
|
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbConnectCmd(database, { ...opts, json: program.opts().json }));
|
|
233
249
|
withOrg(db
|
|
234
|
-
.command("
|
|
250
|
+
.command("disconnect <database>")
|
|
251
|
+
.description("Отвязать проект от базы: переменная уйдёт следующим деплоем, "
|
|
252
|
+
+ "роль проекта удалится, домены перестанут быть разрешёнными для Data API.")
|
|
253
|
+
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbDisconnectCmd(database, { ...opts, json: program.opts().json }));
|
|
254
|
+
withOrg(db
|
|
255
|
+
// 🚨 ЗАПРОС МОЖНО ПИСАТЬ ПРОСТО СЛЕДОМ ЗА ИМЕНЕМ БАЗЫ. Раньше `-c` был
|
|
256
|
+
// ОБЯЗАТЕЛЕН, и `layero db sql моя-база "select 1"` отвечал
|
|
257
|
+
// «required option -c, --command not specified» — то есть отказывал на
|
|
258
|
+
// самой очевидной форме записи. Так эта команда и записана в приёмке
|
|
259
|
+
// (C6), и так её пишет всякий, кто помнит psql.
|
|
260
|
+
.command("sql <database> [sql]")
|
|
235
261
|
.description("Выполнить SQL в базе. Скрипт из нескольких операторов — одной транзакцией.")
|
|
236
|
-
.
|
|
262
|
+
.option("-c, --command <sql>", "запрос или скрипт")).action(async (database, sql, opts) => dbSqlCmd(database, {
|
|
263
|
+
...opts,
|
|
264
|
+
// Явный `-c` выигрывает: он написан намеренно, а позиционный аргумент
|
|
265
|
+
// мог прилететь из истории команд.
|
|
266
|
+
command: opts.command ?? sql,
|
|
267
|
+
json: program.opts().json,
|
|
268
|
+
}));
|
|
237
269
|
const analytics = program
|
|
238
270
|
.command("analytics")
|
|
239
271
|
.description("Яндекс.Метрика: подключение и статистика сайта.");
|
package/dist/commands/db.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// командах.
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import { ApiClient } from "../api.js";
|
|
14
|
+
import { dashboardOrigin } from "../urls.js";
|
|
14
15
|
import { loadConfig } from "../config.js";
|
|
15
16
|
import { loadProjectConfig } from "../project-config.js";
|
|
16
17
|
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
@@ -44,6 +45,40 @@ function gb(bytes) {
|
|
|
44
45
|
return "—";
|
|
45
46
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} ГБ`;
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Что сказать про деньги у этой базы. Пусто — платить не за что.
|
|
50
|
+
*
|
|
51
|
+
* 🚨 КАЖДОМУ СОСТОЯНИЮ СВОЯ ФРАЗА, И ЭТО НЕ УКРАШЕНИЕ. У базы с закрытым
|
|
52
|
+
* доступом «спишем 3 октября» — неправда: списывать уже пробовали и не вышло.
|
|
53
|
+
* Одна строка на все состояния была бы верной ровно в одном из них.
|
|
54
|
+
*
|
|
55
|
+
* Даты приходят от сервера уже посчитанными: правила лестницы живут там, и
|
|
56
|
+
* вторая их реализация в CLI разошлась бы с панелью молча.
|
|
57
|
+
*/
|
|
58
|
+
export function billingLine(d) {
|
|
59
|
+
const b = d.billing;
|
|
60
|
+
if (!b)
|
|
61
|
+
return null;
|
|
62
|
+
const rub = (b.price_month_kopecks / 100).toLocaleString("ru-RU");
|
|
63
|
+
const when = (iso) => iso ? new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "long" }) : null;
|
|
64
|
+
if (b.status === "suspended") {
|
|
65
|
+
const gone = when(b.terminate_at);
|
|
66
|
+
return chalk.red(gone
|
|
67
|
+
? `${rub} ₽/мес · доступ закрыт за неоплату, удалим ${gone}`
|
|
68
|
+
: `${rub} ₽/мес · доступ закрыт за неоплату`);
|
|
69
|
+
}
|
|
70
|
+
if (b.status === "past_due") {
|
|
71
|
+
const till = when(b.paid_until);
|
|
72
|
+
return chalk.yellow(till
|
|
73
|
+
? `${rub} ₽/мес · оплата не прошла, работает по ${till}`
|
|
74
|
+
: `${rub} ₽/мес · оплата не прошла`);
|
|
75
|
+
}
|
|
76
|
+
const next = when(b.next_charge_at);
|
|
77
|
+
if (next)
|
|
78
|
+
return chalk.dim(`${rub} ₽/мес · спишем ${next}`);
|
|
79
|
+
const till = when(b.paid_until);
|
|
80
|
+
return chalk.dim(till ? `${rub} ₽/мес · оплачено по ${till}` : `${rub} ₽/мес`);
|
|
81
|
+
}
|
|
47
82
|
export async function dbListCmd(opts) {
|
|
48
83
|
const mode = detectMode();
|
|
49
84
|
const api = new ApiClient(await loadConfig());
|
|
@@ -59,22 +94,107 @@ export async function dbListCmd(opts) {
|
|
|
59
94
|
}
|
|
60
95
|
for (const d of list) {
|
|
61
96
|
const api_on = d.api_enabled ? chalk.green("API") : chalk.dim("без API");
|
|
97
|
+
// 🚨 РАЗМЕЩЕНИЕ И ВЕРСИЯ — В СТРОКЕ, А НЕ ТОЛЬКО В `--json`. Их не было
|
|
98
|
+
// вовсе: по выводу нельзя было отличить базу из тарифа от выделенного
|
|
99
|
+
// инстанса за 7750 ₽ в месяц и узнать, какой там Postgres. Панель обе
|
|
100
|
+
// вещи показывает, а приёмка (C2) спрашивает именно их.
|
|
101
|
+
const place = d.placement === "dedicated"
|
|
102
|
+
? "выделенный"
|
|
103
|
+
: d.placement === "external"
|
|
104
|
+
? "свой сервер"
|
|
105
|
+
: "Shared";
|
|
106
|
+
const version = d.pg_version ? `PG${d.pg_version}` : chalk.dim("PG—");
|
|
62
107
|
console.log(`${chalk.bold(d.name)} ${chalk.dim(d.name_slug ?? "")} ${d.status} ` +
|
|
108
|
+
`${place} ${version} ` +
|
|
63
109
|
`${api_on} проектов: ${d.projects_count} ${gb(d.size_bytes)} из ${gb(d.quota_bytes)}`);
|
|
110
|
+
// 🚨 СРОК ОПЛАТЫ — ОТДЕЛЬНОЙ СТРОКОЙ, А НЕ ХВОСТОМ ПЕРВОЙ. Строка и так
|
|
111
|
+
// на пределе ширины терминала, а это единственное, что решает, будет база
|
|
112
|
+
// работать через неделю. Хвост, уезжающий за край, — то же молчание.
|
|
113
|
+
const money = billingLine(d);
|
|
114
|
+
if (money)
|
|
115
|
+
console.log(` ${money}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Ждёт, пока база станет рабочей. `false` — не дождались за отведённое время.
|
|
119
|
+
*
|
|
120
|
+
* Опрос, а не подписка: ручки событий у нас нет, а держать соединение ради
|
|
121
|
+
* одной строки состояния незачем. Потолок ожидания — своё число, а не «пока
|
|
122
|
+
* не надоест»: выделенный кластер поднимается 7–13 минут, и повесить терминал
|
|
123
|
+
* на всё это время нельзя. Не дождались — говорим об этом прямо.
|
|
124
|
+
*/
|
|
125
|
+
async function waitUntilReady(api, org, name, timeoutMs = 90_000) {
|
|
126
|
+
const until = Date.now() + timeoutMs;
|
|
127
|
+
while (Date.now() < until) {
|
|
128
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
129
|
+
try {
|
|
130
|
+
const all = await api.listDatabases(org);
|
|
131
|
+
const mine = all.find((d) => d.name === name);
|
|
132
|
+
if (mine && mine.status === "active")
|
|
133
|
+
return true;
|
|
134
|
+
if (mine && mine.status === "failed")
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Сеть моргнула — это не повод объявлять базу несозданной.
|
|
139
|
+
}
|
|
64
140
|
}
|
|
141
|
+
return false;
|
|
65
142
|
}
|
|
66
143
|
export async function dbCreateCmd(name, opts) {
|
|
67
144
|
const mode = detectMode();
|
|
68
|
-
|
|
145
|
+
// 🚨 `--gb` НЕ ДЕЛАЛ НИЧЕГО, И МОЛЧАНИЕ БЫЛО ХУЖЕ ОТКАЗА. Поле уезжало на
|
|
146
|
+
// сервер, а тот его не читает ни одним путём создания: у Shared объём задаёт
|
|
147
|
+
// тариф, у выделенного — диск ступени. Человек называл число, получал базу
|
|
148
|
+
// другого размера и не знал почему (приёмка C9).
|
|
149
|
+
if (opts.gb != null) {
|
|
150
|
+
throw new LayeroError("gb_not_supported", "объём базы так не выбирается", "у базы из тарифа объём задан тарифом, у выделенного инстанса — диском " +
|
|
151
|
+
"ступени: закажите нужную ступень в панели");
|
|
152
|
+
}
|
|
153
|
+
// 🚨 ПЛАТНЫЙ ЗАКАЗ ИЗ ТЕРМИНАЛА НЕВОЗМОЖЕН, И СКАЗАТЬ ОБ ЭТОМ НАДО СЛОВАМИ.
|
|
154
|
+
// Дело не в лени: у заказа выделенного инстанса есть цена и заморозка денег
|
|
155
|
+
// на карте. Карту в терминале не привяжешь, сумму подтвердить негде, а
|
|
156
|
+
// «списали, потому что вы набрали команду» — не тот способ брать деньги.
|
|
157
|
+
//
|
|
158
|
+
// Отказ называет причину и ведёт туда, где заказ возможен. Молчаливое
|
|
159
|
+
// создание Shared вместо выделенного было бы хуже отказа: человек получил бы
|
|
160
|
+
// не то, что просил, и узнал бы об этом по нехватке мощности.
|
|
161
|
+
const config = await loadConfig();
|
|
162
|
+
if (opts.dedicated || opts.cpu != null || opts.ram != null) {
|
|
163
|
+
throw new LayeroError("dedicated_needs_panel", "выделенный инстанс из терминала не заказывается", "у него есть цена и заморозка денег на карте, а подтвердить сумму в " +
|
|
164
|
+
"терминале негде. Закажите в панели: " +
|
|
165
|
+
`${dashboardOrigin(config.apiUrl)}/databases?new=1 — там видны ` +
|
|
166
|
+
"ступени, цена и дата следующего списания");
|
|
167
|
+
}
|
|
168
|
+
const api = new ApiClient(config);
|
|
69
169
|
const org = await orgOf(api, opts);
|
|
70
|
-
const created = await api.createDatabase(org, {
|
|
170
|
+
const created = await api.createDatabase(org, {
|
|
171
|
+
name,
|
|
172
|
+
// 🚨 ФЛАГА «БЕЗ НАПОЛНЕНИЯ» БОЛЬШЕ НЕТ. Он существовал, пока пресет
|
|
173
|
+
// заводил три таблицы с примерами: человек со своей схемой получал чужие
|
|
174
|
+
// таблицы молча, и отказаться было нечем. Тестовых таблиц в пресете не
|
|
175
|
+
// осталось — он настраивает роли, функцию состояния и умолчания прав,
|
|
176
|
+
// и вычищать после него нечего. Значит и отказываться не от чего.
|
|
177
|
+
});
|
|
71
178
|
// Пароль отдаётся ОДИН РАЗ — второй раз его не покажет никто, только
|
|
72
179
|
// ротация. Поэтому он и в JSON-режиме, и в терминале.
|
|
73
180
|
if (mode.json || opts.json) {
|
|
74
181
|
emit({ event: "database_created", org, name, ...created });
|
|
75
182
|
return;
|
|
76
183
|
}
|
|
77
|
-
|
|
184
|
+
// 🚨 БАЗА ЕЩЁ НЕ ГОТОВА В ЭТОТ МОМЕНТ. Ручка стала асинхронной: она пишет
|
|
185
|
+
// намерение и возвращается, подготовка идёт минуты. Печатать «заведена» и
|
|
186
|
+
// строку подключения сразу значит отправить человека к `psql`, который
|
|
187
|
+
// ответит «database does not exist», — и он пойдёт искать причину в пароле.
|
|
188
|
+
process.stdout.write(`${chalk.dim("…")} готовим базу «${name}» в организации ${org}`);
|
|
189
|
+
const ready = await waitUntilReady(api, org, name);
|
|
190
|
+
process.stdout.write("\r\u001b[2K");
|
|
191
|
+
if (ready) {
|
|
192
|
+
console.log(`${chalk.green("✓")} база «${name}» готова в организации ${org}`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log(`${chalk.yellow("!")} база «${name}» ещё готовится — строка подключения ниже ` +
|
|
196
|
+
"заработает, как только она поднимется");
|
|
197
|
+
}
|
|
78
198
|
console.log(`\n ${created.connection_string}`);
|
|
79
199
|
console.log(chalk.dim("\n Пароль показывается один раз — сохраните строку подключения.\n" +
|
|
80
200
|
" Ключи Data API для фронтенда: layero data env"));
|
|
@@ -95,14 +215,48 @@ export async function dbConnectCmd(ref, opts) {
|
|
|
95
215
|
emit({ event: "database_connected", org, database: db.name, project: project.slug });
|
|
96
216
|
return;
|
|
97
217
|
}
|
|
218
|
+
// 🚨 СЛЕДСТВИЙ У СВЯЗИ ДВА, и второе стоило отдельного разбора 09.09.2026.
|
|
219
|
+
// Кроме строки подключения, связь проект↔база — ЕДИНСТВЕННЫЙ источник списка
|
|
220
|
+
// разрешённых доменов Data API: представление `data_api_origins` собирает их
|
|
221
|
+
// из `userdb_roles`. Пока здесь говорилось только про переменные, человек,
|
|
222
|
+
// у которого сайт отбивался по CORS, не имел ни одной причины связать это со
|
|
223
|
+
// своей ненажатой командой — а отказ шлюза именно её и советует.
|
|
98
224
|
console.log(`${chalk.green("✓")} проект «${project.slug}» подключён к базе «${db.name}»\n` +
|
|
99
|
-
chalk.dim(" строка подключения приедет в переменные окружения
|
|
225
|
+
chalk.dim(" строка подключения приедет в переменные окружения проекта\n") +
|
|
226
|
+
chalk.dim(` домены проекта разрешены для Data API базы «${db.name}»`));
|
|
227
|
+
}
|
|
228
|
+
export async function dbDisconnectCmd(ref, opts) {
|
|
229
|
+
// 🚨 ОБРАТНОГО ДЕЙСТВИЯ НЕ БЫЛО ВОВСЕ. `db connect` существовал с самого
|
|
230
|
+
// начала, а отвязать проект от базы можно было только в панели — при том
|
|
231
|
+
// что подсказка `env list` прямо звала сделать это командой. Приёмка (A7.6)
|
|
232
|
+
// спрашивает именно её.
|
|
233
|
+
const mode = detectMode();
|
|
234
|
+
const api = new ApiClient(await loadConfig());
|
|
235
|
+
const org = await orgOf(api, opts);
|
|
236
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
237
|
+
const projectRef = opts.project ?? linked?.project_id;
|
|
238
|
+
if (!projectRef) {
|
|
239
|
+
throw new LayeroError("project_unknown", "не понятно, какой проект отвязывать от базы", "запустите из каталога проекта или передайте --project <id|slug>");
|
|
240
|
+
}
|
|
241
|
+
const project = await api.getProject(projectRef);
|
|
242
|
+
const db = await pick(api, org, ref);
|
|
243
|
+
await api.disconnectDatabaseFromProject(org, db.id, project.id);
|
|
244
|
+
if (mode.json || opts.json) {
|
|
245
|
+
emit({ event: "database_disconnected", org, database: db.name, project: project.slug });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Обратная сторона той же связи: домены проекта перестают быть разрешёнными.
|
|
249
|
+
// Молчать об этом — значит отдать человеку сайт, который перестанет отвечать
|
|
250
|
+
// по причине, названной в другой команде.
|
|
251
|
+
console.log(`${chalk.green("✓")} проект «${project.slug}» отвязан от базы «${db.name}»\n` +
|
|
252
|
+
chalk.dim(" переменная уйдёт из окружения следующим деплоем, роль проекта удалена\n") +
|
|
253
|
+
chalk.dim(" домены проекта больше не разрешены для Data API этой базы"));
|
|
100
254
|
}
|
|
101
255
|
export async function dbSqlCmd(ref, opts) {
|
|
102
256
|
const mode = detectMode();
|
|
103
257
|
const sql = (opts.command ?? "").trim();
|
|
104
258
|
if (!sql) {
|
|
105
|
-
throw new LayeroError("sql_missing", "нечего выполнять", 'передайте
|
|
259
|
+
throw new LayeroError("sql_missing", "нечего выполнять", 'передайте запрос следом за именем базы: layero db sql моя-база "select 1"');
|
|
106
260
|
}
|
|
107
261
|
const api = new ApiClient(await loadConfig());
|
|
108
262
|
const org = await orgOf(api, opts);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import readline from "node:readline/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { ApiClient, ApiError, uploadArchive, } from "../api.js";
|
|
6
|
+
import { dashboardOrigin } from "../urls.js";
|
|
6
7
|
import { loadConfig } from "../config.js";
|
|
7
8
|
import { loadProjectConfig, persistProjectLinking, } from "../project-config.js";
|
|
8
9
|
import { packCwd, packDirectory } from "../pack.js";
|
|
@@ -70,19 +71,6 @@ export function runtimeTypeOf(raw) {
|
|
|
70
71
|
return null;
|
|
71
72
|
return RUNTIME_TYPES[raw.trim().toLowerCase()] ?? null;
|
|
72
73
|
}
|
|
73
|
-
function dashboardOrigin(apiUrl) {
|
|
74
|
-
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
75
|
-
if (override)
|
|
76
|
-
return override.replace(/\/+$/, "");
|
|
77
|
-
try {
|
|
78
|
-
const u = new URL(apiUrl);
|
|
79
|
-
u.hostname = u.hostname.replace(/^api\./, "app.");
|
|
80
|
-
return u.origin;
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return "https://app.layero.ru";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
74
|
function projectUrl(apiUrl, projectId) {
|
|
87
75
|
return `${dashboardOrigin(apiUrl)}/projects/${projectId}`;
|
|
88
76
|
}
|
package/dist/commands/env.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function envListCmd(opts) {
|
|
|
24
24
|
emit({
|
|
25
25
|
event: "env_vars",
|
|
26
26
|
project: project.slug,
|
|
27
|
-
vars: rows.map((r) => ({ key: r.key, length: r.length })),
|
|
27
|
+
vars: rows.map((r) => ({ key: r.key, length: r.length, managed: !!r.managed })),
|
|
28
28
|
});
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
@@ -33,10 +33,21 @@ export async function envListCmd(opts) {
|
|
|
33
33
|
console.log(chalk.dim("добавить: layero env set KEY=value"));
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
// 🚨 ПЕРЕМЕННЫЕ ПЛАТФОРМЫ ПОМЕЧЕНЫ, А НЕ ПЕРЕМЕШАНЫ СО СВОИМИ. Список
|
|
37
|
+
// отвечает на вопрос «что получит моё приложение», и строка подключения к
|
|
38
|
+
// базе — часть ответа. Но менять её нельзя: она пересобирается из связи
|
|
39
|
+
// проекта с базой, и предложить правку значило бы пообещать несбыточное.
|
|
36
40
|
for (const r of rows) {
|
|
37
|
-
|
|
41
|
+
const tail = r.managed
|
|
42
|
+
? chalk.dim(` (${r.length} симв., ставит платформа)`)
|
|
43
|
+
: chalk.dim(` (${r.length} симв.)`);
|
|
44
|
+
console.log(`${r.key}${tail}`);
|
|
38
45
|
}
|
|
39
46
|
console.log(chalk.dim("\nзначения не показываются — платформа их не отдаёт"));
|
|
47
|
+
if (rows.some((r) => r.managed)) {
|
|
48
|
+
console.log(chalk.dim("помеченные «ставит платформа» задаются подключением базы к проекту:"));
|
|
49
|
+
console.log(chalk.dim(" layero db connect <база> / layero db disconnect <база>"));
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
function parsePair(raw) {
|
|
42
53
|
const i = raw.indexOf("=");
|
package/dist/urls.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Адреса панели, выведенные из адреса API.
|
|
3
|
+
*
|
|
4
|
+
* 🚨 ОДНО МЕСТО, А НЕ ПО КОПИИ НА КОМАНДУ. Правило простое — `api.` меняется на
|
|
5
|
+
* `app.`, — и ровно поэтому его тянет переписать заново там, где понадобилось.
|
|
6
|
+
* Вторая копия разошлась бы на первом же стенде: переменную `LAYERO_DASHBOARD_URL`
|
|
7
|
+
* читала бы одна команда, а вторая уводила бы человека на боевую панель.
|
|
8
|
+
*/
|
|
9
|
+
export function dashboardOrigin(apiUrl) {
|
|
10
|
+
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
11
|
+
if (override)
|
|
12
|
+
return override.replace(/\/+$/, "");
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(apiUrl);
|
|
15
|
+
u.hostname = u.hostname.replace(/^api\./, "app.");
|
|
16
|
+
return u.origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "https://app.layero.ru";
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED