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/agent.js
CHANGED
|
@@ -116,10 +116,15 @@ function renderHuman(event) {
|
|
|
116
116
|
process.stdout.write(`✓ Имя аккаунта: ${event.username} (организация ${event.organization})\n`);
|
|
117
117
|
break;
|
|
118
118
|
case "project_created":
|
|
119
|
-
process.stdout.write(`✓ Created project ${event.slug} (org: ${event.organization})
|
|
119
|
+
process.stdout.write(`✓ Created project ${event.slug} (org: ${event.organization})` +
|
|
120
|
+
`${event.repo ? ` from ${event.repo}@${event.branch ?? "main"}` : ""}` +
|
|
121
|
+
`${event.url ? ` → ${event.url}` : ""}\n`);
|
|
120
122
|
break;
|
|
121
123
|
case "project_linked":
|
|
122
|
-
process.stdout.write(`→ Project ${event.slug}\n`);
|
|
124
|
+
process.stdout.write(`→ Project ${event.slug}${event.url ? ` ${event.url}` : ""}\n`);
|
|
125
|
+
if (event.status === "pending_setup") {
|
|
126
|
+
process.stdout.write(" проект в pending_setup: `layero deploy` загрузит код и применит настройку\n");
|
|
127
|
+
}
|
|
123
128
|
break;
|
|
124
129
|
case "detected":
|
|
125
130
|
process.stdout.write(event.runtime_kind
|
|
@@ -193,6 +198,129 @@ function renderHuman(event) {
|
|
|
193
198
|
case "promoted":
|
|
194
199
|
process.stdout.write(`✓ Published apex → ${event.url}\n`);
|
|
195
200
|
break;
|
|
201
|
+
case "me":
|
|
202
|
+
process.stdout.write(`id: ${event.id}\n`);
|
|
203
|
+
process.stdout.write(`username: ${event.username ?? "(не задано)"}\n`);
|
|
204
|
+
process.stdout.write(`email: ${event.email ?? "(нет)"}\n`);
|
|
205
|
+
if (event.github_login)
|
|
206
|
+
process.stdout.write(`github: ${event.github_login}\n`);
|
|
207
|
+
break;
|
|
208
|
+
case "logged_out":
|
|
209
|
+
process.stdout.write(`✓ Выход выполнен (удалён ${event.config_path})\n`);
|
|
210
|
+
break;
|
|
211
|
+
case "projects":
|
|
212
|
+
if (event.projects.length === 0) {
|
|
213
|
+
process.stdout.write("Проектов пока нет — `layero deploy` создаст первый.\n");
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
for (const p of event.projects) {
|
|
217
|
+
const tag = p.source_type === "cli" ? "[cli]" : "[git]";
|
|
218
|
+
process.stdout.write(`${tag} ${p.slug} ${p.id} ${p.url}\n`);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case "organizations":
|
|
222
|
+
if (event.organizations.length === 0) {
|
|
223
|
+
process.stdout.write("У аккаунта нет организаций.\n");
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
for (const o of event.organizations) {
|
|
227
|
+
process.stdout.write(` ${o.slug.padEnd(20)} ${o.kind} (${o.role})\n`);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case "hooks":
|
|
231
|
+
if (event.hooks.length === 0) {
|
|
232
|
+
process.stdout.write("Хуков пока нет — `layero hooks create <имя>` заведёт первый.\n");
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
for (const h of event.hooks) {
|
|
236
|
+
const last = h.last_triggered_at ? `срабатывал ${h.last_triggered_at}` : "не срабатывал";
|
|
237
|
+
process.stdout.write(`[${h.target}] ${h.name} branch=${h.branch ?? "default"} (${last})\n` +
|
|
238
|
+
` ${h.id}\n ${h.url}\n`);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case "hook_created":
|
|
242
|
+
process.stdout.write(`✓ Хук [${event.target}] ${event.name}\n ${event.id}\n ${event.url}\n`);
|
|
243
|
+
process.stdout.write("\n Вставьте адрес в CMS / cron / внешний CI как POST-вебхук. " +
|
|
244
|
+
"Любой, у кого он есть, запускает сборку — храните как секрет; " +
|
|
245
|
+
"ротация: delete + create.\n");
|
|
246
|
+
break;
|
|
247
|
+
case "hook_deleted":
|
|
248
|
+
process.stdout.write(`✓ Хук ${event.id} отозван\n`);
|
|
249
|
+
break;
|
|
250
|
+
case "init_done":
|
|
251
|
+
for (const d of event.agent_docs) {
|
|
252
|
+
const mark = d.result === "created" ? "✓ создан" : d.result === "updated" ? "✓ обновлён" : "= без изменений";
|
|
253
|
+
process.stdout.write(` ${mark} ${d.file}\n`);
|
|
254
|
+
}
|
|
255
|
+
process.stdout.write(` ${event.project_json === "created" ? "✓ создан" : "= без изменений"} .layero/project.json\n`);
|
|
256
|
+
process.stdout.write("\nДальше:\n $ npx layero@latest login # один раз, в браузере\n $ npx layero@latest deploy # выложить текущую папку\n");
|
|
257
|
+
break;
|
|
258
|
+
case "sources":
|
|
259
|
+
process.stdout.write(`Провайдеры (${event.org}):\n`);
|
|
260
|
+
for (const p of event.providers) {
|
|
261
|
+
const flags = [p.self_hosted ? "свой инстанс" : null, p.webhook_supported ? null : "без вебхуков"]
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join(", ");
|
|
264
|
+
process.stdout.write(` ${p.id.padEnd(12)} ${p.title}${flags ? ` (${flags})` : ""}\n`);
|
|
265
|
+
}
|
|
266
|
+
process.stdout.write("Подключения:\n");
|
|
267
|
+
if (event.connections.length === 0) {
|
|
268
|
+
process.stdout.write(" нет — `layero sources connect <provider> --token-stdin`\n");
|
|
269
|
+
}
|
|
270
|
+
for (const c of event.connections) {
|
|
271
|
+
process.stdout.write(` ${c.id} ${c.provider.padEnd(12)} ${c.account ?? "—"} ${c.status}` +
|
|
272
|
+
` проектов: ${c.projects_count}${c.last_error ? ` ! ${c.last_error}` : ""}\n`);
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
case "source_connected":
|
|
276
|
+
process.stdout.write(`✓ Подключён ${event.provider}${event.account ? ` (${event.account})` : ""}: ${event.connection_id}\n`);
|
|
277
|
+
break;
|
|
278
|
+
case "source_repos":
|
|
279
|
+
if (event.repos.length === 0) {
|
|
280
|
+
process.stdout.write("Токену не видно ни одного репозитория.\n");
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
for (const r of event.repos) {
|
|
284
|
+
process.stdout.write(` ${r.path.padEnd(40)} ${r.default_branch.padEnd(10)} ${r.private ? "private" : "public"}\n`);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case "webhook_installed":
|
|
288
|
+
process.stdout.write(`✓ Вебхук установлен — push в репозиторий запускает сборку\n`);
|
|
289
|
+
break;
|
|
290
|
+
case "webhook_unavailable":
|
|
291
|
+
process.stdout.write(`! Вебхук не установлен: ${event.hint}\n Адрес для ручной настройки: ${event.url}\n`);
|
|
292
|
+
break;
|
|
293
|
+
case "environments":
|
|
294
|
+
if (event.environments.length === 0) {
|
|
295
|
+
process.stdout.write("Окружений пока нет — сначала `layero deploy` или push в репозиторий.\n");
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
for (const e of event.environments) {
|
|
299
|
+
const mark = e.production ? "★" : " ";
|
|
300
|
+
const when = e.active_deploy_at ? ` ${e.active_deploy_at}` : " (нет активного деплоя)";
|
|
301
|
+
process.stdout.write(`${mark} ${e.branch.padEnd(24)} ${e.url}${when}\n`);
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
case "project_deleted":
|
|
305
|
+
process.stdout.write(`✓ Проект ${event.slug} удалён (очистка ресурсов идёт в фоне)\n`);
|
|
306
|
+
break;
|
|
307
|
+
case "claimable":
|
|
308
|
+
process.stdout.write(`→ Временный проект ${event.slug}: ${event.url}\n`);
|
|
309
|
+
process.stdout.write(` Сайт живёт до ${event.expires_at} (72 часа). Забрать в аккаунт: ${event.claim_url}\n`);
|
|
310
|
+
break;
|
|
311
|
+
case "claim_status":
|
|
312
|
+
process.stdout.write(`Код ${event.code}: ${event.status}${event.claimed ? " (забран)" : ""}` +
|
|
313
|
+
`${event.expires_at ? `, действует до ${event.expires_at}` : ""}\n`);
|
|
314
|
+
if (event.url)
|
|
315
|
+
process.stdout.write(` сайт: ${event.url}\n`);
|
|
316
|
+
if (event.claim_url && !event.claimed)
|
|
317
|
+
process.stdout.write(` забрать: ${event.claim_url}\n`);
|
|
318
|
+
break;
|
|
319
|
+
case "claim_accept":
|
|
320
|
+
process.stdout.write(event.opened
|
|
321
|
+
? `→ Открыл ${event.claim_url} — подтвердите в панели\n`
|
|
322
|
+
: `→ Откройте в браузере и подтвердите в панели: ${event.claim_url}\n`);
|
|
323
|
+
break;
|
|
196
324
|
case "error":
|
|
197
325
|
process.stderr.write(`Error: ${event.code}\n`);
|
|
198
326
|
process.stderr.write(` ${event.message}\n`);
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { LayeroError } from "./agent.js";
|
|
1
2
|
import { CLI_VERSION, userAgent } from "./version.js";
|
|
3
|
+
import { looksLikeId } from "./project-ref.js";
|
|
2
4
|
export class ApiError extends Error {
|
|
3
5
|
status;
|
|
4
6
|
body;
|
|
@@ -57,6 +59,31 @@ export class ApiClient {
|
|
|
57
59
|
getProject(idOrSlug) {
|
|
58
60
|
return this.request("GET", `/projects/${idOrSlug}`);
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Проект по идентификатору ИЛИ по слагу — так, как обещают справки команд.
|
|
64
|
+
*
|
|
65
|
+
* 🚨 РЕЗОЛВ ЖИВЁТ ЗДЕСЬ, А НЕ НА СЕРВЕРЕ, и это не лень. Слаг проекта
|
|
66
|
+
* уникален В ПРЕДЕЛАХ ОРГАНИЗАЦИИ (`projects_owner_slug_uniq` — пара
|
|
67
|
+
* organization_id + slug), то есть один и тот же слаг законно существует у
|
|
68
|
+
* двух организаций сразу. Ручка `/projects/{id}` организации не знает и
|
|
69
|
+
* выбирать между ними не имеет права; клиент знает, за кого он ходит.
|
|
70
|
+
*
|
|
71
|
+
* Отличаем по ФОРМЕ значения: UUID, ушедший в поиск по слагу, не нашёлся бы.
|
|
72
|
+
* Раньше отличия не было вовсе — слаг уезжал в путь как есть, и ручка
|
|
73
|
+
* отвечала 422 `uuid_parsing`, а CLI показывал его как «internal, сообщите
|
|
74
|
+
* об ошибке» (T-20260909-32).
|
|
75
|
+
*/
|
|
76
|
+
async resolveProject(idOrSlug) {
|
|
77
|
+
if (looksLikeId(idOrSlug))
|
|
78
|
+
return this.getProject(idOrSlug);
|
|
79
|
+
const projects = await this.listProjects();
|
|
80
|
+
const found = projects.find((p) => p.slug === idOrSlug);
|
|
81
|
+
if (!found) {
|
|
82
|
+
const known = projects.map((p) => p.slug).slice(0, 8).join(", ");
|
|
83
|
+
throw new LayeroError("project_unknown", `проекта «${idOrSlug}» нет среди доступных`, known ? `есть такие: ${known}` : "заведите проект: layero deploy");
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
60
87
|
createCliProject(input) {
|
|
61
88
|
return this.request("POST", "/projects", {
|
|
62
89
|
name: input.name,
|
|
@@ -161,6 +188,11 @@ export class ApiClient {
|
|
|
161
188
|
project_id: projectId,
|
|
162
189
|
});
|
|
163
190
|
}
|
|
191
|
+
/** Отвязать проект от базы. Роль проекта при этом удаляется, а переменная
|
|
192
|
+
* уходит из его окружения — не сразу, а следующим деплоем. */
|
|
193
|
+
disconnectDatabaseFromProject(org, dbId, projectId) {
|
|
194
|
+
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/projects/${projectId}`);
|
|
195
|
+
}
|
|
164
196
|
queryDatabase(org, dbId, sql) {
|
|
165
197
|
return this.request("POST", `/organizations/${org}/databases/${dbId}/query`, {
|
|
166
198
|
sql,
|
|
@@ -189,6 +221,39 @@ export class ApiClient {
|
|
|
189
221
|
dataEnv(projectId) {
|
|
190
222
|
return this.request("GET", `/projects/${projectId}/data-env`);
|
|
191
223
|
}
|
|
224
|
+
// ── Data API базы: ключи, сайты, методы и уровни (T-20260911-9) ─────────
|
|
225
|
+
//
|
|
226
|
+
// Те же ручки, что у раздела «API» в панели. Ответы у них без модели в
|
|
227
|
+
// схеме, поэтому их форма описана здесь; тела запросов — из схемы.
|
|
228
|
+
listDataKeys(org, dbId) {
|
|
229
|
+
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/keys`);
|
|
230
|
+
}
|
|
231
|
+
issueDataKey(org, dbId, input) {
|
|
232
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/keys`, input);
|
|
233
|
+
}
|
|
234
|
+
revokeDataKey(org, dbId, keyId) {
|
|
235
|
+
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/keys/${keyId}`);
|
|
236
|
+
}
|
|
237
|
+
listDataOrigins(org, dbId) {
|
|
238
|
+
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/origins`);
|
|
239
|
+
}
|
|
240
|
+
addDataOrigin(org, dbId, origin, note) {
|
|
241
|
+
const body = { origin, note };
|
|
242
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/origins`, body);
|
|
243
|
+
}
|
|
244
|
+
/** Источник — параметром запроса: в нём `://` и точки, путём он стал бы чужим маршрутом. */
|
|
245
|
+
removeDataOrigin(org, dbId, origin) {
|
|
246
|
+
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/origins?origin=${encodeURIComponent(origin)}`);
|
|
247
|
+
}
|
|
248
|
+
listDataMethods(org, dbId) {
|
|
249
|
+
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/methods`);
|
|
250
|
+
}
|
|
251
|
+
setDataLevels(org, dbId, input) {
|
|
252
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/levels`, input);
|
|
253
|
+
}
|
|
254
|
+
enableDataApi(org, dbId, withSecret) {
|
|
255
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/enable?with_secret=${withSecret ? "true" : "false"}`);
|
|
256
|
+
}
|
|
192
257
|
/**
|
|
193
258
|
* Значение `null` = «оставить как есть». Благодаря этому добавить одну
|
|
194
259
|
* переменную можно, не читая остальные, — то есть не имея доступа к
|
|
@@ -287,6 +352,71 @@ export class ApiClient {
|
|
|
287
352
|
checkUsername(value) {
|
|
288
353
|
return this.request("GET", `/auth/me/username/check?value=${encodeURIComponent(value)}`);
|
|
289
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Проба метода Data API настоящим запросом через шлюз; запись откатывается
|
|
357
|
+
* (T-20260911-1). Ответ шлюза, в том числе его отказ, приходит телом 200.
|
|
358
|
+
*/
|
|
359
|
+
probeDataApi(org, dbId, input) {
|
|
360
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/probe-http`, input);
|
|
361
|
+
}
|
|
362
|
+
// --- Источники кода: провайдеры, подключения, репозитории (этап 6). ------
|
|
363
|
+
listSourceProviders(org) {
|
|
364
|
+
return this.request("GET", `/organizations/${org}/source-providers`);
|
|
365
|
+
}
|
|
366
|
+
listSourceConnections(org) {
|
|
367
|
+
return this.request("GET", `/organizations/${org}/source-connections`);
|
|
368
|
+
}
|
|
369
|
+
/** Токен уходит на сервер и НЕ возвращается: ответ — подключение без него. */
|
|
370
|
+
createSourceConnection(org, input) {
|
|
371
|
+
return this.request("POST", `/organizations/${org}/source-connections`, input);
|
|
372
|
+
}
|
|
373
|
+
listSourceRepos(org, connectionId) {
|
|
374
|
+
return this.request("GET", `/organizations/${org}/source-connections/${connectionId}/repos`);
|
|
375
|
+
}
|
|
376
|
+
/** Аккаунты, из которых организация может импортировать: GitHub App и токеновые подключения. */
|
|
377
|
+
listImportAccounts(org) {
|
|
378
|
+
return this.request("GET", `/organizations/${org}/import/accounts`);
|
|
379
|
+
}
|
|
380
|
+
listImportRepos(org, accountKey) {
|
|
381
|
+
return this.request("GET", `/organizations/${org}/import/repos?account=${encodeURIComponent(accountKey)}`);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Проект из репозитория GitHub через ключ аккаунта (`github:<installation>`):
|
|
385
|
+
* сервер сам заводит проект и вебхук — у GitHub App иного режима нет.
|
|
386
|
+
*/
|
|
387
|
+
createProjectFromAccount(input) {
|
|
388
|
+
return this.request("POST", "/projects", {
|
|
389
|
+
name: input.name,
|
|
390
|
+
source_type: "github",
|
|
391
|
+
organization_slug: input.organization_slug,
|
|
392
|
+
source_account_key: input.source_account_key,
|
|
393
|
+
repo_path: input.repo_path,
|
|
394
|
+
default_branch: input.default_branch ?? "main",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Привязать репозиторий внешнего провайдера к существующему проекту.
|
|
399
|
+
* Возвращает и итог вебхука: у GitVerse/GitLab/GitFlic его может не дать
|
|
400
|
+
* токен, у SourceCraft вебхуков нет вовсе — привязка при этом остаётся.
|
|
401
|
+
*/
|
|
402
|
+
connectSource(projectId, input) {
|
|
403
|
+
return this.request("POST", `/projects/${projectId}/connect-source`, input);
|
|
404
|
+
}
|
|
405
|
+
/** Окружения проекта: ветки с адресами. Архивные и снятые с раздачи не входят. */
|
|
406
|
+
listBranches(projectId) {
|
|
407
|
+
return this.request("GET", `/projects/${projectId}/branches`);
|
|
408
|
+
}
|
|
409
|
+
/** Требует scope `admin` у токена: удаление необратимо. */
|
|
410
|
+
deleteProject(projectId) {
|
|
411
|
+
return this.request("DELETE", `/projects/${projectId}`);
|
|
412
|
+
}
|
|
413
|
+
// --- Claimable (этап 13). Без токена: заявку создаёт кто угодно. ----------
|
|
414
|
+
createClaimableProject(input) {
|
|
415
|
+
return this.request("POST", "/claimable/projects", input);
|
|
416
|
+
}
|
|
417
|
+
getClaimStatus(code) {
|
|
418
|
+
return this.request("GET", `/claimable/status?code=${encodeURIComponent(code)}`);
|
|
419
|
+
}
|
|
290
420
|
}
|
|
291
421
|
export async function uploadArchive(init, filePath) {
|
|
292
422
|
const fs = await import("node:fs");
|
package/dist/bin/layero.js
CHANGED
|
@@ -3,7 +3,11 @@ import { Command } from "commander";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { whoamiCmd } from "../commands/whoami.js";
|
|
5
5
|
import { logoutCmd } from "../commands/logout.js";
|
|
6
|
-
import { projectsListCmd } from "../commands/projects.js";
|
|
6
|
+
import { projectsCreateCmd, projectsDeleteCmd, projectsListCmd } from "../commands/projects.js";
|
|
7
|
+
import { sourcesConnectCmd, sourcesListCmd, sourcesReposCmd } from "../commands/sources.js";
|
|
8
|
+
import { envsListCmd } from "../commands/envs.js";
|
|
9
|
+
import { claimAcceptCmd, claimStatusCmd } from "../commands/claim.js";
|
|
10
|
+
import { exitCodeFor } from "../exit-codes.js";
|
|
7
11
|
import { linkCmd } from "../commands/link.js";
|
|
8
12
|
import { tokenCreateCmd, tokenListCmd, tokenRevokeCmd, tokenSetCmd } from "../commands/token.js";
|
|
9
13
|
import { deployCmd } from "../commands/deploy.js";
|
|
@@ -16,7 +20,8 @@ import { initCmd } from "../commands/init.js";
|
|
|
16
20
|
import { diagnoseCmd, logsCmd } from "../commands/diagnose.js";
|
|
17
21
|
import { perfCheckCmd, perfShowCmd } from "../commands/perf.js";
|
|
18
22
|
import { dataEnvCmd } from "../commands/data.js";
|
|
19
|
-
import {
|
|
23
|
+
import { registerDataApiCommands } from "../commands/data-api.js";
|
|
24
|
+
import { dbConnectCmd, dbCreateCmd, dbDisconnectCmd, dbListCmd, dbSqlCmd, } from "../commands/db.js";
|
|
20
25
|
import { envListCmd, envSetCmd, envUnsetCmd } from "../commands/env.js";
|
|
21
26
|
import { analyticsConnectCmd, analyticsDisconnectCmd, analyticsStatsCmd, analyticsStatusCmd, } from "../commands/analytics.js";
|
|
22
27
|
import { domainsAddCmd, domainsListCmd, domainsPrimaryCmd, domainsRemoveCmd, domainsVerifyCmd, } from "../commands/domains.js";
|
|
@@ -82,17 +87,82 @@ async function main() {
|
|
|
82
87
|
});
|
|
83
88
|
const projects = program
|
|
84
89
|
.command("projects")
|
|
85
|
-
.description("
|
|
90
|
+
.description("Проекты аккаунта: список, создание из репозитория, удаление.");
|
|
86
91
|
projects
|
|
87
92
|
.command("list")
|
|
88
|
-
.description("
|
|
93
|
+
.description("Список ваших проектов.")
|
|
89
94
|
.action(projectsListCmd);
|
|
95
|
+
projects
|
|
96
|
+
.command("create")
|
|
97
|
+
.description("Создать проект из репозитория подключённого провайдера — путь (a): push в ветку = превью, push в main = прод. " +
|
|
98
|
+
"GitHub — через установленное App, остальные — через `layero sources connect`.")
|
|
99
|
+
.requiredOption("--repo <provider:owner/repo>", "репозиторий: github:acme/site, gitverse:acme/site, gitlab:group/sub/project")
|
|
100
|
+
.option("--branch <name>", "основная ветка (по умолчанию — ветка репозитория по умолчанию)")
|
|
101
|
+
.option("--name <name>", "имя проекта (по умолчанию — имя репозитория)")
|
|
102
|
+
.option("--org <slug>", "организация (по умолчанию единственная)")
|
|
103
|
+
.addHelpText("after", "\nПримеры:\n" +
|
|
104
|
+
" $ layero projects create --repo github:acme/site\n" +
|
|
105
|
+
" $ layero projects create --repo gitverse:acme/site --branch develop --json\n" +
|
|
106
|
+
"\nСобытия в --json: project_created, source_connected, webhook_installed | webhook_unavailable.\n" +
|
|
107
|
+
"Без вебхука push не собирается — заведите его вручную по адресу из webhook_unavailable.")
|
|
108
|
+
.action(async (opts) => projectsCreateCmd({ ...opts, json: program.opts().json }));
|
|
109
|
+
projects
|
|
110
|
+
.command("delete <id_or_slug>")
|
|
111
|
+
.description("Удалить проект. НЕОБРАТИМО; требует токена со scope admin (`layero token create <имя> --scope admin`).")
|
|
112
|
+
.option("-y, --yes", "не спрашивать подтверждение (вне терминала — обязателен)")
|
|
113
|
+
.action(async (ref, opts) => projectsDeleteCmd(ref, { ...opts, json: program.opts().json }));
|
|
114
|
+
const sources = program
|
|
115
|
+
.command("sources")
|
|
116
|
+
.description("Git-провайдеры организации: какие есть, подключить по токену, посмотреть репозитории.");
|
|
117
|
+
sources
|
|
118
|
+
.command("list")
|
|
119
|
+
.description("Провайдеры платформы и подключения организации.")
|
|
120
|
+
.option("--org <slug>", "организация (по умолчанию единственная)")
|
|
121
|
+
.action(async (opts) => sourcesListCmd({ ...opts, json: program.opts().json }));
|
|
122
|
+
sources
|
|
123
|
+
.command("connect <provider>")
|
|
124
|
+
.description("Подключить провайдера по персональному токену (PAT). Токен проверяется до записи и наружу не возвращается.")
|
|
125
|
+
.option("--token <pat>", "токен провайдера (остаётся в истории shell — предпочтительнее --token-stdin)")
|
|
126
|
+
.option("--token-stdin", "прочитать токен из stdin: echo \"$PAT\" | layero sources connect gitverse --token-stdin")
|
|
127
|
+
.option("--base-url <url>", "адрес собственного инстанса (GitLab, GitFlic)")
|
|
128
|
+
.option("--name <label>", "подпись подключения")
|
|
129
|
+
.option("--org <slug>", "организация (по умолчанию единственная)")
|
|
130
|
+
.addHelpText("after", "\nПримеры:\n" +
|
|
131
|
+
" $ echo \"$GITVERSE_TOKEN\" | layero sources connect gitverse --token-stdin\n" +
|
|
132
|
+
" $ layero sources connect gitlab --token-stdin --base-url https://git.example.com < token.txt\n" +
|
|
133
|
+
"\nПровайдеры — `layero sources list`. GitHub подключается установкой App в панели.")
|
|
134
|
+
.action(async (provider, opts) => sourcesConnectCmd(provider, { ...opts, json: program.opts().json }));
|
|
135
|
+
sources
|
|
136
|
+
.command("repos <connection_id>")
|
|
137
|
+
.description("Репозитории, видимые токену подключения.")
|
|
138
|
+
.option("--org <slug>", "организация (по умолчанию единственная)")
|
|
139
|
+
.action(async (id, opts) => sourcesReposCmd(id, { ...opts, json: program.opts().json }));
|
|
140
|
+
const envs = program
|
|
141
|
+
.command("envs")
|
|
142
|
+
.description("Окружения проекта: ветки и их адреса.");
|
|
143
|
+
envs
|
|
144
|
+
.command("list")
|
|
145
|
+
.description("Окружения проекта с адресами. У CLI-проекта одно — `cli`; у проекта с репозиторием — по ветке.")
|
|
146
|
+
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")
|
|
147
|
+
.action(async (opts) => envsListCmd({ ...opts, json: program.opts().json }));
|
|
148
|
+
const claim = program
|
|
149
|
+
.command("claim")
|
|
150
|
+
.description("Проект без аккаунта (`layero deploy --claim`): статус заявки и ссылка, чтобы забрать сайт.");
|
|
151
|
+
claim
|
|
152
|
+
.command("status [code]")
|
|
153
|
+
.description("Состояние заявки: жива, забрана, истекла. Без кода — из .layero/project.json.")
|
|
154
|
+
.action(async (code) => claimStatusCmd(code, { json: program.opts().json }));
|
|
155
|
+
claim
|
|
156
|
+
.command("accept [code]")
|
|
157
|
+
.description("Открыть страницу заявки в панели. Принять её может только человек, вошедший в панель, — CLI лишь открывает или печатает ссылку.")
|
|
158
|
+
.option("--no-browser", "не открывать браузер — только напечатать ссылку")
|
|
159
|
+
.action(async (code, opts) => claimAcceptCmd(code, { ...opts, json: program.opts().json }));
|
|
90
160
|
const orgs = program
|
|
91
161
|
.command("orgs")
|
|
92
|
-
.description("
|
|
162
|
+
.description("Организации аккаунта: личная и команды.");
|
|
93
163
|
orgs
|
|
94
164
|
.command("list")
|
|
95
|
-
.description("
|
|
165
|
+
.description("Все организации, в которых вы состоите.")
|
|
96
166
|
.action(orgsListCmd);
|
|
97
167
|
const deploys = program
|
|
98
168
|
.command("deploys")
|
|
@@ -114,13 +184,7 @@ async function main() {
|
|
|
114
184
|
.description("List deploy hooks for the linked project.")
|
|
115
185
|
.option("--project <id>", "target project id (default: linked .layero/project.json)")
|
|
116
186
|
.action(async (opts) => {
|
|
117
|
-
|
|
118
|
-
await hooksListCmd(opts);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
console.error(String(err?.message ?? err));
|
|
122
|
-
process.exitCode = 1;
|
|
123
|
-
}
|
|
187
|
+
await hooksListCmd(opts);
|
|
124
188
|
});
|
|
125
189
|
hooks
|
|
126
190
|
.command("create <name>")
|
|
@@ -134,26 +198,14 @@ async function main() {
|
|
|
134
198
|
+ " $ layero hooks create publish --prod # production-target hook for default branch\n"
|
|
135
199
|
+ " $ layero hooks create staging --branch=dev # any branch, preview environment")
|
|
136
200
|
.action(async (name, opts) => {
|
|
137
|
-
|
|
138
|
-
await hooksCreateCmd(name, opts);
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
console.error(String(err?.message ?? err));
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
}
|
|
201
|
+
await hooksCreateCmd(name, opts);
|
|
144
202
|
});
|
|
145
203
|
hooks
|
|
146
204
|
.command("delete <id>")
|
|
147
205
|
.description("Revoke a deploy hook. The URL stops working immediately.")
|
|
148
206
|
.option("--project <id>", "target project id (default: linked .layero/project.json)")
|
|
149
207
|
.action(async (id, opts) => {
|
|
150
|
-
|
|
151
|
-
await hooksDeleteCmd(id, opts);
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
console.error(String(err?.message ?? err));
|
|
155
|
-
process.exitCode = 1;
|
|
156
|
-
}
|
|
208
|
+
await hooksDeleteCmd(id, opts);
|
|
157
209
|
});
|
|
158
210
|
program
|
|
159
211
|
.command("promote [deploy]")
|
|
@@ -203,7 +255,7 @@ async function main() {
|
|
|
203
255
|
.option("-y, --yes", "не спрашивать подтверждение")).action(async (keys, opts) => envUnsetCmd(keys, { ...opts, json: program.opts().json }));
|
|
204
256
|
const data = program
|
|
205
257
|
.command("data")
|
|
206
|
-
.description("Data API
|
|
258
|
+
.description("Data API базы: ключи, сайты, методы и доступ; адрес и ключ для фронтенда.");
|
|
207
259
|
data
|
|
208
260
|
.command("env")
|
|
209
261
|
.description("Показать VITE_/NEXT_PUBLIC_ переменные Data API; --write кладёт их в .env.local.")
|
|
@@ -216,6 +268,7 @@ async function main() {
|
|
|
216
268
|
"\nОтдаётся только ПУБЛИЧНЫЙ ключ — тот, что и так уезжает в бандл.\n" +
|
|
217
269
|
"Секретный ключ платформа не хранит и не отдаёт: он для сервера.")
|
|
218
270
|
.action(async (opts) => dataEnvCmd({ ...opts, json: program.opts().json }));
|
|
271
|
+
registerDataApiCommands(data, program);
|
|
219
272
|
const db = program
|
|
220
273
|
.command("db")
|
|
221
274
|
.description("Базы организации: завести, посмотреть, подключить к проекту, выполнить SQL.");
|
|
@@ -231,11 +284,26 @@ async function main() {
|
|
|
231
284
|
// выделенного диск ступени. Убрать его совсем значило бы отвечать
|
|
232
285
|
// «неизвестный параметр» тому, кто им пользовался, и оставить человека
|
|
233
286
|
// гадать, куда делся объём. Теперь команда говорит это словами (C9).
|
|
234
|
-
.option("--gb <number>", "БОЛЬШЕ НЕ РАБОТАЕТ: объём задаёт тариф или ступень", (v) => parseInt(v, 10))
|
|
287
|
+
.option("--gb <number>", "БОЛЬШЕ НЕ РАБОТАЕТ: объём задаёт тариф или ступень", (v) => parseInt(v, 10))
|
|
288
|
+
// 🚨 ТЕ ЖЕ ФЛАГИ РАДИ ЧЕСТНОГО ОТКАЗА. Выделенный инстанс из терминала
|
|
289
|
+
// не заказать, и это осознанно: у заказа есть цена и заморозка денег, а
|
|
290
|
+
// карту в терминале не привяжешь и сумму подтвердить негде. Но человек,
|
|
291
|
+
// прочитавший про ступени в панели, попробует `--cpu 2` — и без этих
|
|
292
|
+
// флагов получит «неизвестный параметр», то есть ответ про синтаксис
|
|
293
|
+
// вместо ответа про причину.
|
|
294
|
+
.option("--cpu <number>", "выделенный инстанс: заказывается в панели", (v) => parseInt(v, 10))
|
|
295
|
+
.option("--ram <mb>", "выделенный инстанс: заказывается в панели", (v) => parseInt(v, 10))
|
|
296
|
+
.option("--dedicated", "выделенный инстанс: заказывается в панели")).action(async (name, opts) => dbCreateCmd(name, { ...opts, json: program.opts().json }));
|
|
235
297
|
withOrg(db
|
|
236
298
|
.command("connect <database>")
|
|
237
|
-
.description("Подключить проект к базе: строка подключения приедет в его
|
|
299
|
+
.description("Подключить проект к базе: строка подключения приедет в его "
|
|
300
|
+
+ "переменные, а домены проекта станут разрешёнными для Data API.")
|
|
238
301
|
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbConnectCmd(database, { ...opts, json: program.opts().json }));
|
|
302
|
+
withOrg(db
|
|
303
|
+
.command("disconnect <database>")
|
|
304
|
+
.description("Отвязать проект от базы: переменная уйдёт следующим деплоем, "
|
|
305
|
+
+ "роль проекта удалится, домены перестанут быть разрешёнными для Data API.")
|
|
306
|
+
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")).action(async (database, opts) => dbDisconnectCmd(database, { ...opts, json: program.opts().json }));
|
|
239
307
|
withOrg(db
|
|
240
308
|
// 🚨 ЗАПРОС МОЖНО ПИСАТЬ ПРОСТО СЛЕДОМ ЗА ИМЕНЕМ БАЗЫ. Раньше `-c` был
|
|
241
309
|
// ОБЯЗАТЕЛЕН, и `layero db sql моя-база "select 1"` отвечал
|
|
@@ -363,10 +431,11 @@ async function main() {
|
|
|
363
431
|
.option("--root <dir>", "monorepo: subdirectory inside the repo that the builder treats as the app root (saved on the project; future GitHub-push and hook triggers use the same value)")
|
|
364
432
|
.option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
|
|
365
433
|
.option("--promote", "pin the project apex to this deploy after a successful build (V071). Works for any branch — e.g. `--promote` without --prod publishes a CLI preview straight to production.")
|
|
366
|
-
.option("--branch <name>", "
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"
|
|
434
|
+
.option("--branch <name>", "REFUSED (branch_unsupported, exit 4): archive uploads always land in the " +
|
|
435
|
+
"reserved `cli` environment, so this flag cannot give you a preview. " +
|
|
436
|
+
"Branch previews come from pushing to a connected repository.")
|
|
437
|
+
.option("--claim", "deploy without an account: a temporary project for 72 hours plus a claim_url for a human to take it over. " +
|
|
438
|
+
"Turns on by itself when there is no token, the run is non-interactive (agent, not CI) and --yes is passed.")
|
|
370
439
|
.option("--org <slug>", "Layero organization slug for first-time project creation. Defaults to personal; required when you're a member of multiple orgs and want a non-personal home.")
|
|
371
440
|
// Осознанное подтверждение выкатки, когда предыдущие сборки подряд падают
|
|
372
441
|
// с ОДНОЙ И ТОЙ ЖЕ ошибкой. Намеренно НЕ покрывается `--yes`: смысл стопа
|
|
@@ -378,7 +447,7 @@ async function main() {
|
|
|
378
447
|
" $ layero deploy --prod # production deploy (interactive confirm)\n" +
|
|
379
448
|
" $ layero deploy --prod --yes # production deploy, no prompt (CI)\n" +
|
|
380
449
|
" $ layero deploy --promote # preview deploy + pin apex (one-shot publish)\n" +
|
|
381
|
-
" $ layero deploy --
|
|
450
|
+
" $ layero deploy --claim # no account: temporary site + claim link\n" +
|
|
382
451
|
" $ layero deploy --type vite # force a framework preset\n" +
|
|
383
452
|
" $ layero deploy --type express # Node backend: platform RUNS it, not serves files\n" +
|
|
384
453
|
" $ layero deploy --json # machine-readable output for agents")
|
|
@@ -491,7 +560,11 @@ main().catch((err) => {
|
|
|
491
560
|
if (debug && err instanceof Error && err.stack) {
|
|
492
561
|
console.error(chalk.dim(err.stack));
|
|
493
562
|
}
|
|
563
|
+
// Класс выхода — по коду ошибки (`exit-codes.ts`): 2 вход, 3 не найдено,
|
|
564
|
+
// 4 неверный ввод, 5 удалённая ошибка, 1 прочее.
|
|
565
|
+
let exitCode = 1;
|
|
494
566
|
if (err instanceof LayeroError) {
|
|
567
|
+
exitCode = exitCodeFor(err.code);
|
|
495
568
|
emit({
|
|
496
569
|
event: "error",
|
|
497
570
|
code: err.code,
|
|
@@ -500,6 +573,7 @@ main().catch((err) => {
|
|
|
500
573
|
});
|
|
501
574
|
}
|
|
502
575
|
else {
|
|
576
|
+
exitCode = exitCodeFor("internal");
|
|
503
577
|
// Anything else (network error, unexpected exception) — still emit
|
|
504
578
|
// a structured error so agents have a consistent format to parse.
|
|
505
579
|
const message = err?.message ?? String(err);
|
|
@@ -520,5 +594,5 @@ main().catch((err) => {
|
|
|
520
594
|
}
|
|
521
595
|
}
|
|
522
596
|
}
|
|
523
|
-
process.exit(
|
|
597
|
+
process.exit(exitCode);
|
|
524
598
|
});
|