layero 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js 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})\n`);
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,
@@ -194,6 +221,39 @@ export class ApiClient {
194
221
  dataEnv(projectId) {
195
222
  return this.request("GET", `/projects/${projectId}/data-env`);
196
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
+ }
197
257
  /**
198
258
  * Значение `null` = «оставить как есть». Благодаря этому добавить одну
199
259
  * переменную можно, не читая остальные, — то есть не имея доступа к
@@ -292,6 +352,71 @@ export class ApiClient {
292
352
  checkUsername(value) {
293
353
  return this.request("GET", `/auth/me/username/check?value=${encodeURIComponent(value)}`);
294
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
+ }
295
420
  }
296
421
  export async function uploadArchive(init, filePath) {
297
422
  const fs = await import("node:fs");
@@ -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,6 +20,7 @@ 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";
23
+ import { registerDataApiCommands } from "../commands/data-api.js";
19
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";
@@ -82,17 +87,82 @@ async function main() {
82
87
  });
83
88
  const projects = program
84
89
  .command("projects")
85
- .description("Inspect projects on your account.");
90
+ .description("Проекты аккаунта: список, создание из репозитория, удаление.");
86
91
  projects
87
92
  .command("list")
88
- .description("List your projects.")
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("Layero organizations on your account (personal + teams).");
162
+ .description("Организации аккаунта: личная и команды.");
93
163
  orgs
94
164
  .command("list")
95
- .description("Show every Layero organization you belong to.")
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
- try {
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
- try {
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
- try {
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.");
@@ -378,10 +431,11 @@ async function main() {
378
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)")
379
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.")
380
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.")
381
- .option("--branch <name>", "ACCEPTED BUT IGNORED for direct uploads: the backend files every " +
382
- "archive deploy under the reserved `cli` environment. Branch " +
383
- "environments come from pushes to a connected repository, not from " +
384
- "this flag. Kept for backwards compatibility.")
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.")
385
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.")
386
440
  // Осознанное подтверждение выкатки, когда предыдущие сборки подряд падают
387
441
  // с ОДНОЙ И ТОЙ ЖЕ ошибкой. Намеренно НЕ покрывается `--yes`: смысл стопа
@@ -393,7 +447,7 @@ async function main() {
393
447
  " $ layero deploy --prod # production deploy (interactive confirm)\n" +
394
448
  " $ layero deploy --prod --yes # production deploy, no prompt (CI)\n" +
395
449
  " $ layero deploy --promote # preview deploy + pin apex (one-shot publish)\n" +
396
- " $ layero deploy --branch=staging # preview on a specific branch\n" +
450
+ " $ layero deploy --claim # no account: temporary site + claim link\n" +
397
451
  " $ layero deploy --type vite # force a framework preset\n" +
398
452
  " $ layero deploy --type express # Node backend: platform RUNS it, not serves files\n" +
399
453
  " $ layero deploy --json # machine-readable output for agents")
@@ -506,7 +560,11 @@ main().catch((err) => {
506
560
  if (debug && err instanceof Error && err.stack) {
507
561
  console.error(chalk.dim(err.stack));
508
562
  }
563
+ // Класс выхода — по коду ошибки (`exit-codes.ts`): 2 вход, 3 не найдено,
564
+ // 4 неверный ввод, 5 удалённая ошибка, 1 прочее.
565
+ let exitCode = 1;
509
566
  if (err instanceof LayeroError) {
567
+ exitCode = exitCodeFor(err.code);
510
568
  emit({
511
569
  event: "error",
512
570
  code: err.code,
@@ -515,6 +573,7 @@ main().catch((err) => {
515
573
  });
516
574
  }
517
575
  else {
576
+ exitCode = exitCodeFor("internal");
518
577
  // Anything else (network error, unexpected exception) — still emit
519
578
  // a structured error so agents have a consistent format to parse.
520
579
  const message = err?.message ?? String(err);
@@ -535,5 +594,5 @@ main().catch((err) => {
535
594
  }
536
595
  }
537
596
  }
538
- process.exit(1);
597
+ process.exit(exitCode);
539
598
  });