layero 0.9.4 → 0.10.0
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/LICENSE +21 -0
- package/README.md +203 -167
- package/dist/agent.js +130 -2
- package/dist/api.js +130 -0
- package/dist/bin/layero.js +110 -36
- package/dist/commands/claim.js +135 -0
- package/dist/commands/data-api.js +578 -0
- package/dist/commands/data-probe.js +541 -0
- package/dist/commands/data.js +4 -1
- package/dist/commands/db.js +94 -4
- package/dist/commands/deploy.js +81 -23
- package/dist/commands/env.js +13 -2
- package/dist/commands/envs.js +44 -0
- package/dist/commands/hooks.js +29 -32
- package/dist/commands/init.js +63 -91
- package/dist/commands/link.js +12 -25
- package/dist/commands/logout.js +2 -2
- package/dist/commands/orgs.js +11 -15
- package/dist/commands/projects.js +179 -12
- package/dist/commands/sources.js +127 -0
- package/dist/commands/whoami.js +9 -10
- package/dist/config.js +1 -0
- package/dist/exit-codes.js +102 -0
- package/dist/project-config.js +2 -0
- package/dist/project-ref.js +20 -0
- package/dist/urls.js +21 -0
- package/package.json +10 -5
package/dist/commands/db.js
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
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";
|
|
17
|
-
async function orgOf(api, opts) {
|
|
18
|
+
export async function orgOf(api, opts) {
|
|
18
19
|
if (opts.org)
|
|
19
20
|
return opts.org;
|
|
20
21
|
const orgs = await api.listOrganizations();
|
|
@@ -26,7 +27,7 @@ async function orgOf(api, opts) {
|
|
|
26
27
|
throw new LayeroError("org_unknown", `у вас несколько организаций: ${orgs.map((o) => o.slug).join(", ")}`, "укажите нужную: --org <slug>");
|
|
27
28
|
}
|
|
28
29
|
/** База по имени, слагу или id — как её назвал человек. */
|
|
29
|
-
async function pick(api, org, ref) {
|
|
30
|
+
export async function pick(api, org, ref) {
|
|
30
31
|
const list = await api.listDatabases(org);
|
|
31
32
|
const needle = ref.trim().toLowerCase();
|
|
32
33
|
const found = list.find((d) => d.id === ref ||
|
|
@@ -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,8 @@ 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 { looksLikeId } from "../project-ref.js";
|
|
7
|
+
import { dashboardOrigin } from "../urls.js";
|
|
6
8
|
import { loadConfig } from "../config.js";
|
|
7
9
|
import { loadProjectConfig, persistProjectLinking, } from "../project-config.js";
|
|
8
10
|
import { packCwd, packDirectory } from "../pack.js";
|
|
@@ -11,7 +13,7 @@ import { detectProject } from "../detect.js";
|
|
|
11
13
|
import { runDeviceLogin } from "../auth.js";
|
|
12
14
|
import { LayeroError, detectMode, emit, isCiEnv } from "../agent.js";
|
|
13
15
|
import { ensureUsername } from "../username.js";
|
|
14
|
-
|
|
16
|
+
import { claimTokenFor, createClaimable } from "./claim.js";
|
|
15
17
|
const VALID_TYPES = new Set([
|
|
16
18
|
"vite",
|
|
17
19
|
"next",
|
|
@@ -70,19 +72,6 @@ export function runtimeTypeOf(raw) {
|
|
|
70
72
|
return null;
|
|
71
73
|
return RUNTIME_TYPES[raw.trim().toLowerCase()] ?? null;
|
|
72
74
|
}
|
|
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
75
|
function projectUrl(apiUrl, projectId) {
|
|
87
76
|
return `${dashboardOrigin(apiUrl)}/projects/${projectId}`;
|
|
88
77
|
}
|
|
@@ -375,21 +364,78 @@ export async function deployCmd(opts) {
|
|
|
375
364
|
`runtime kinds: ${[...new Set(Object.values(RUNTIME_TYPES))].join(", ")} ` +
|
|
376
365
|
`(aliases: ${Object.keys(RUNTIME_TYPES).join(", ")})`);
|
|
377
366
|
}
|
|
367
|
+
const cwd = process.cwd();
|
|
368
|
+
let existing = await loadProjectConfig(cwd);
|
|
378
369
|
let cliCfg = await loadConfig();
|
|
370
|
+
// Claimable-проект этого запуска (этап 13): событие `claimable` уходит
|
|
371
|
+
// перед `ready`, когда адрес сайта уже известен.
|
|
372
|
+
let claimable = null;
|
|
379
373
|
if (!cliCfg.token) {
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
if (
|
|
374
|
+
// Папка уже привязана к claimable-проекту — деплоим его же токеном,
|
|
375
|
+
// пока заявка жива. Заявка забрана или истекла — платформа ответит 401,
|
|
376
|
+
// и это станет `auth_expired`: честнее, чем молча завести ещё один.
|
|
377
|
+
const reuse = claimTokenFor(cliCfg, existing?.project_id);
|
|
378
|
+
if (reuse) {
|
|
379
|
+
cliCfg = { ...cliCfg, token: reuse };
|
|
380
|
+
}
|
|
381
|
+
else if (opts.claim || (!mode.interactive && !isCiEnv() && opts.yes)) {
|
|
382
|
+
// 🚨 CI СЮДА НЕ ПОПАДАЕТ НАМЕРЕННО. Раннер без LAYERO_TOKEN — это
|
|
383
|
+
// забытый секрет, и правильный ответ ему — отказ, а не сайт на
|
|
384
|
+
// временном адресе, который через 72 часа исчезнет вместе с
|
|
385
|
+
// «зелёным» прогоном. Явный `--claim` в CI работает.
|
|
386
|
+
const r = await createClaimable(cliCfg, cwd, {
|
|
387
|
+
name: opts.name ?? path.basename(cwd),
|
|
388
|
+
});
|
|
389
|
+
cliCfg = r.cfg;
|
|
390
|
+
claimable = {
|
|
391
|
+
claim_url: r.created.claim_url,
|
|
392
|
+
expires_at: r.created.expires_at,
|
|
393
|
+
project_id: r.created.project_id,
|
|
394
|
+
slug: r.created.slug,
|
|
395
|
+
};
|
|
396
|
+
existing = await loadProjectConfig(cwd);
|
|
397
|
+
}
|
|
398
|
+
else if (isCiEnv()) {
|
|
399
|
+
// In CI nobody can open a browser, so the device flow can only end one
|
|
400
|
+
// way: fifteen minutes of a hung job and then `auth_expired`. Fail
|
|
401
|
+
// immediately instead, and say what to do — burning a quarter of an hour
|
|
402
|
+
// of someone's runner to reach a foregone conclusion is not acceptable.
|
|
385
403
|
throw new LayeroError("auth_required", "No credentials in CI. Create a token at https://app.layero.ru/settings/cli " +
|
|
386
404
|
"and pass it as the LAYERO_TOKEN environment variable.", "set_layero_token");
|
|
387
405
|
}
|
|
388
|
-
|
|
406
|
+
else {
|
|
407
|
+
cliCfg = await runDeviceLogin(cliCfg);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (opts.claim) {
|
|
411
|
+
throw new LayeroError("bad_format", "--claim — деплой без аккаунта, а вход уже выполнен", "уберите --claim: проект создастся в вашем аккаунте; либо `layero logout` перед `--claim`");
|
|
389
412
|
}
|
|
390
413
|
const api = new ApiClient(cliCfg);
|
|
391
|
-
|
|
392
|
-
|
|
414
|
+
// 🚨 `--branch` У АРХИВНОЙ ЗАГРУЗКИ НЕ РАБОТАЕТ, И МОЛЧАТЬ ОБ ЭТОМ НЕЛЬЗЯ.
|
|
415
|
+
// Платформа кладёт каждый архив в зарезервированное окружение `cli`, что
|
|
416
|
+
// бы ни передали (`projects.py`, «Branch targeting»). До 0.10.0 флаг
|
|
417
|
+
// принимался и игнорировался: агент читал в справке «deploy to a specific
|
|
418
|
+
// branch's environment», делал `--branch=probe` и заменял живой сайт,
|
|
419
|
+
// считая, что выложил превью. Теперь — отказ до упаковки, с разной
|
|
420
|
+
// подсказкой для проекта без репозитория и с ним.
|
|
421
|
+
if (opts.branch) {
|
|
422
|
+
let linked = null;
|
|
423
|
+
try {
|
|
424
|
+
if (opts.project)
|
|
425
|
+
linked = await api.resolveProject(opts.project);
|
|
426
|
+
else if (existing?.project_id)
|
|
427
|
+
linked = await api.getProject(existing.project_id);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
linked = null;
|
|
431
|
+
}
|
|
432
|
+
const repo = linked?.repo_full_name && linked.repo_status !== "disconnected" ? linked.repo_full_name : null;
|
|
433
|
+
throw new LayeroError("branch_unsupported", repo
|
|
434
|
+
? `--branch ${opts.branch}: архивная загрузка не попадает в ветку — платформа кладёт её в окружение «cli»`
|
|
435
|
+
: `--branch ${opts.branch}: у проекта нет подключённого репозитория, а превью-ветки есть только у проектов с репозиторием`, repo
|
|
436
|
+
? `изолированное превью — push в ветку «${opts.branch}» репозитория ${repo}; \`layero deploy\` без --branch обновит окружение «cli»`
|
|
437
|
+
: "превью-ветки есть только у проектов с репозиторием: подключите его — `layero projects create --repo <provider>:<owner/repo>` — и пушьте в ветку; папку без репозитория выкладывает `layero deploy` без --branch");
|
|
438
|
+
}
|
|
393
439
|
// --- Всё, что требует файловой системы, делаем сами. Остальное — сервер.
|
|
394
440
|
const prebuiltDir = await resolvePrebuiltDir(cwd, opts.prebuilt);
|
|
395
441
|
const setup = prebuiltDir
|
|
@@ -448,7 +494,7 @@ export async function deployCmd(opts) {
|
|
|
448
494
|
// Опечатка в слаге при этом обязана дать 404, а не завести лишний
|
|
449
495
|
// проект с похожим именем: `create_if_missing: false`.
|
|
450
496
|
...(opts.project
|
|
451
|
-
?
|
|
497
|
+
? looksLikeId(opts.project)
|
|
452
498
|
? { project_id: opts.project }
|
|
453
499
|
: { name: opts.project, create_if_missing: false }
|
|
454
500
|
: existing?.project_id
|
|
@@ -650,6 +696,18 @@ export async function deployCmd(opts) {
|
|
|
650
696
|
if (evicted.length > 0) {
|
|
651
697
|
emit({ event: "preview_evicted", evicted });
|
|
652
698
|
}
|
|
699
|
+
// Claimable: ссылка «забрать» — ДО `ready`, потому что после `ready`
|
|
700
|
+
// агент не читает, а без этой ссылки сайт исчезнет через 72 часа.
|
|
701
|
+
if (claimable) {
|
|
702
|
+
emit({
|
|
703
|
+
event: "claimable",
|
|
704
|
+
project_id: claimable.project_id,
|
|
705
|
+
slug: claimable.slug,
|
|
706
|
+
url: liveUrl,
|
|
707
|
+
claim_url: claimable.claim_url,
|
|
708
|
+
expires_at: claimable.expires_at,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
653
711
|
emit({
|
|
654
712
|
event: "ready",
|
|
655
713
|
url: liveUrl,
|
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("=");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
4
|
+
import { LayeroError, emit } from "../agent.js";
|
|
5
|
+
/**
|
|
6
|
+
* `layero envs list [--project]` — окружения проекта с адресами.
|
|
7
|
+
*
|
|
8
|
+
* Отдельной ручки «environments» в API нет: окружение и ветка — одна
|
|
9
|
+
* сущность (`environments` в базе, `/projects/{id}/branches` наружу), и
|
|
10
|
+
* список приходит оттуда. У CLI-проекта это одно окружение `cli`; у проекта
|
|
11
|
+
* с репозиторием — ветка на окружение. Архивные и снятые с раздачи в список
|
|
12
|
+
* не входят — так же, как в панели.
|
|
13
|
+
*/
|
|
14
|
+
export async function envsListCmd(opts) {
|
|
15
|
+
const cfg = await loadConfig();
|
|
16
|
+
if (!cfg.token) {
|
|
17
|
+
throw new LayeroError("auth_required", "вход не выполнен", "выполните `layero login` или задайте LAYERO_TOKEN");
|
|
18
|
+
}
|
|
19
|
+
const api = new ApiClient(cfg);
|
|
20
|
+
let ref = opts.project;
|
|
21
|
+
if (!ref) {
|
|
22
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
23
|
+
ref = linked?.project_id;
|
|
24
|
+
}
|
|
25
|
+
if (!ref) {
|
|
26
|
+
throw new LayeroError("project_unknown", "в этой папке нет привязанного проекта", "запустите из папки проекта или передайте --project <id|slug>");
|
|
27
|
+
}
|
|
28
|
+
const project = await api.resolveProject(ref);
|
|
29
|
+
const branches = await api.listBranches(project.id);
|
|
30
|
+
const productionBranch = project.production_branch_name ?? project.default_branch;
|
|
31
|
+
emit({
|
|
32
|
+
event: "environments",
|
|
33
|
+
project: project.slug,
|
|
34
|
+
environments: branches.map((b) => ({
|
|
35
|
+
id: b.id,
|
|
36
|
+
branch: b.branch_name,
|
|
37
|
+
url: b.preview_url,
|
|
38
|
+
hostname: b.hostname,
|
|
39
|
+
active_deploy_id: b.active_deploy_id,
|
|
40
|
+
active_deploy_at: b.active_deploy_at ?? null,
|
|
41
|
+
production: b.branch_name === productionBranch,
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
}
|
package/dist/commands/hooks.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { ApiClient, ApiError } from "../api.js";
|
|
3
2
|
import { loadConfig } from "../config.js";
|
|
4
3
|
import { loadProjectConfig } from "../project-config.js";
|
|
4
|
+
import { LayeroError, emit } from "../agent.js";
|
|
5
5
|
async function resolveProjectId(opts) {
|
|
6
6
|
if (opts.project) {
|
|
7
|
-
//
|
|
7
|
+
// Принимает id напрямую; форма UUID проверяется на сервере.
|
|
8
8
|
return opts.project;
|
|
9
9
|
}
|
|
10
10
|
const linked = await loadProjectConfig(process.cwd());
|
|
11
11
|
if (linked?.project_id) {
|
|
12
12
|
return linked.project_id;
|
|
13
13
|
}
|
|
14
|
-
throw new
|
|
15
|
-
+ "from a project directory once to link it.");
|
|
14
|
+
throw new LayeroError("project_unknown", "в этой папке нет привязанного проекта", "передайте --project <id> или запустите `layero deploy` из папки проекта, чтобы привязать её");
|
|
16
15
|
}
|
|
17
16
|
async function makeClient() {
|
|
18
17
|
const cfg = await loadConfig();
|
|
19
18
|
if (!cfg.token) {
|
|
20
|
-
throw new
|
|
19
|
+
throw new LayeroError("auth_required", "вход не выполнен", "выполните `layero login` или задайте LAYERO_TOKEN");
|
|
21
20
|
}
|
|
22
21
|
return new ApiClient(cfg);
|
|
23
22
|
}
|
|
@@ -25,24 +24,22 @@ export async function hooksListCmd(opts) {
|
|
|
25
24
|
const api = await makeClient();
|
|
26
25
|
const projectId = await resolveProjectId(opts);
|
|
27
26
|
const hooks = await api.listDeployHooks(projectId);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log(` ${h.url}`);
|
|
41
|
-
}
|
|
27
|
+
emit({
|
|
28
|
+
event: "hooks",
|
|
29
|
+
project: projectId,
|
|
30
|
+
hooks: hooks.map((h) => ({
|
|
31
|
+
id: h.id,
|
|
32
|
+
name: h.name,
|
|
33
|
+
branch: h.branch,
|
|
34
|
+
target: h.target,
|
|
35
|
+
url: h.url,
|
|
36
|
+
last_triggered_at: h.last_triggered_at,
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
42
39
|
}
|
|
43
40
|
export async function hooksCreateCmd(name, opts) {
|
|
44
41
|
if (!name || !name.trim()) {
|
|
45
|
-
throw new
|
|
42
|
+
throw new LayeroError("bad_format", "нужно имя хука", "`layero hooks create <имя>`");
|
|
46
43
|
}
|
|
47
44
|
const api = await makeClient();
|
|
48
45
|
const projectId = await resolveProjectId(opts);
|
|
@@ -51,17 +48,19 @@ export async function hooksCreateCmd(name, opts) {
|
|
|
51
48
|
branch: opts.branch ?? null,
|
|
52
49
|
target: opts.prod ? "production" : "preview",
|
|
53
50
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
emit({
|
|
52
|
+
event: "hook_created",
|
|
53
|
+
project: projectId,
|
|
54
|
+
id: hook.id,
|
|
55
|
+
name: hook.name,
|
|
56
|
+
branch: hook.branch,
|
|
57
|
+
target: hook.target,
|
|
58
|
+
url: hook.url,
|
|
59
|
+
});
|
|
61
60
|
}
|
|
62
61
|
export async function hooksDeleteCmd(hookId, opts) {
|
|
63
62
|
if (!hookId) {
|
|
64
|
-
throw new
|
|
63
|
+
throw new LayeroError("bad_format", "нужен id хука", "`layero hooks delete <id>`");
|
|
65
64
|
}
|
|
66
65
|
const api = await makeClient();
|
|
67
66
|
const projectId = await resolveProjectId(opts);
|
|
@@ -70,11 +69,9 @@ export async function hooksDeleteCmd(hookId, opts) {
|
|
|
70
69
|
}
|
|
71
70
|
catch (err) {
|
|
72
71
|
if (err instanceof ApiError && err.status === 404) {
|
|
73
|
-
|
|
74
|
-
process.exitCode = 1;
|
|
75
|
-
return;
|
|
72
|
+
throw new LayeroError("hook_not_found", `у проекта нет хука ${hookId} (уже удалён?)`, "`layero hooks list`");
|
|
76
73
|
}
|
|
77
74
|
throw err;
|
|
78
75
|
}
|
|
79
|
-
|
|
76
|
+
emit({ event: "hook_deleted", project: projectId, id: hookId });
|
|
80
77
|
}
|