layero 0.9.4 → 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 +5 -0
- package/dist/bin/layero.js +18 -3
- package/dist/commands/db.js +92 -2
- 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
|
@@ -161,6 +161,11 @@ export class ApiClient {
|
|
|
161
161
|
project_id: projectId,
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
/** Отвязать проект от базы. Роль проекта при этом удаляется, а переменная
|
|
165
|
+
* уходит из его окружения — не сразу, а следующим деплоем. */
|
|
166
|
+
disconnectDatabaseFromProject(org, dbId, projectId) {
|
|
167
|
+
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/projects/${projectId}`);
|
|
168
|
+
}
|
|
164
169
|
queryDatabase(org, dbId, sql) {
|
|
165
170
|
return this.request("POST", `/organizations/${org}/databases/${dbId}/query`, {
|
|
166
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";
|
|
@@ -231,11 +231,26 @@ async function main() {
|
|
|
231
231
|
// выделенного диск ступени. Убрать его совсем значило бы отвечать
|
|
232
232
|
// «неизвестный параметр» тому, кто им пользовался, и оставить человека
|
|
233
233
|
// гадать, куда делся объём. Теперь команда говорит это словами (C9).
|
|
234
|
-
.option("--gb <number>", "БОЛЬШЕ НЕ РАБОТАЕТ: объём задаёт тариф или ступень", (v) => parseInt(v, 10))
|
|
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 }));
|
|
235
244
|
withOrg(db
|
|
236
245
|
.command("connect <database>")
|
|
237
|
-
.description("Подключить проект к базе: строка подключения приедет в его
|
|
246
|
+
.description("Подключить проект к базе: строка подключения приедет в его "
|
|
247
|
+
+ "переменные, а домены проекта станут разрешёнными для Data API.")
|
|
238
248
|
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbConnectCmd(database, { ...opts, json: program.opts().json }));
|
|
249
|
+
withOrg(db
|
|
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 }));
|
|
239
254
|
withOrg(db
|
|
240
255
|
// 🚨 ЗАПРОС МОЖНО ПИСАТЬ ПРОСТО СЛЕДОМ ЗА ИМЕНЕМ БАЗЫ. Раньше `-c` был
|
|
241
256
|
// ОБЯЗАТЕЛЕН, и `layero db sql моя-база "select 1"` отвечал
|
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());
|
|
@@ -72,6 +107,12 @@ export async function dbListCmd(opts) {
|
|
|
72
107
|
console.log(`${chalk.bold(d.name)} ${chalk.dim(d.name_slug ?? "")} ${d.status} ` +
|
|
73
108
|
`${place} ${version} ` +
|
|
74
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}`);
|
|
75
116
|
}
|
|
76
117
|
}
|
|
77
118
|
/** Ждёт, пока база станет рабочей. `false` — не дождались за отведённое время.
|
|
@@ -109,7 +150,22 @@ export async function dbCreateCmd(name, opts) {
|
|
|
109
150
|
throw new LayeroError("gb_not_supported", "объём базы так не выбирается", "у базы из тарифа объём задан тарифом, у выделенного инстанса — диском " +
|
|
110
151
|
"ступени: закажите нужную ступень в панели");
|
|
111
152
|
}
|
|
112
|
-
|
|
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);
|
|
113
169
|
const org = await orgOf(api, opts);
|
|
114
170
|
const created = await api.createDatabase(org, {
|
|
115
171
|
name,
|
|
@@ -159,8 +215,42 @@ export async function dbConnectCmd(ref, opts) {
|
|
|
159
215
|
emit({ event: "database_connected", org, database: db.name, project: project.slug });
|
|
160
216
|
return;
|
|
161
217
|
}
|
|
218
|
+
// 🚨 СЛЕДСТВИЙ У СВЯЗИ ДВА, и второе стоило отдельного разбора 09.09.2026.
|
|
219
|
+
// Кроме строки подключения, связь проект↔база — ЕДИНСТВЕННЫЙ источник списка
|
|
220
|
+
// разрешённых доменов Data API: представление `data_api_origins` собирает их
|
|
221
|
+
// из `userdb_roles`. Пока здесь говорилось только про переменные, человек,
|
|
222
|
+
// у которого сайт отбивался по CORS, не имел ни одной причины связать это со
|
|
223
|
+
// своей ненажатой командой — а отказ шлюза именно её и советует.
|
|
162
224
|
console.log(`${chalk.green("✓")} проект «${project.slug}» подключён к базе «${db.name}»\n` +
|
|
163
|
-
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 этой базы"));
|
|
164
254
|
}
|
|
165
255
|
export async function dbSqlCmd(ref, opts) {
|
|
166
256
|
const mode = detectMode();
|
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