layero 0.10.1 → 0.10.2

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 CHANGED
@@ -74,7 +74,7 @@ The first `layero deploy` in a directory creates a project and links it via
74
74
  | `layero login` / `logout` / `whoami` | Browser device-flow sign-in, sign-out, current account. |
75
75
  | `layero deploy` | Pack the current directory, build on the platform, publish. `--claim` — without an account. |
76
76
  | `layero projects list` | Projects on your account with addresses. |
77
- | `layero projects create --repo <provider>:<owner/repo>` | Create a project from a repository of a connected provider. Events: `project_created`, `source_connected`, `webhook_installed` \| `webhook_unavailable`. |
77
+ | `layero projects create --repo <provider>:<owner/repo>` | Create a project from a repository of a connected provider, apply the detected settings and start the first build (what the dashboard's "Start deploy" button does); `--no-deploy` leaves it in the wizard. Events: `project_created`, `source_connected`, `webhook_installed` \| `webhook_unavailable`, then `setup_applied` + `deploy_started` \| `setup_pending` \| `setup_failed`. |
78
78
  | `layero projects delete <slug> --yes` | Delete a project. Irreversible; needs a token with scope `admin`. |
79
79
  | `layero sources list` | Git providers the platform supports and the organization's connections. |
80
80
  | `layero sources connect <provider> --token-stdin` | Connect a provider by personal access token (read from stdin so it never lands in shell history). |
@@ -196,6 +196,14 @@ SourceCraft, which has no outgoing webhooks), the CLI says so with
196
196
  `webhook_unavailable` and the URL to register by hand — the repository is
197
197
  connected either way, only push-triggered builds wait for the webhook.
198
198
 
199
+ Once linked, the command finishes the setup wizard itself: it takes the
200
+ detection hint (framework, build command, output directory), applies it and
201
+ starts the first build — exactly what the dashboard's "Start deploy" button
202
+ does (`setup_applied`, `deploy_started`). `--no-deploy` leaves the project in
203
+ the wizard (`setup_pending` with the dashboard URL). If detection or setup
204
+ fails, the project is still created and the exit code is 0: `setup_failed`
205
+ carries the reason and the wizard URL.
206
+
199
207
  ## Deploy hooks — webhook URLs that trigger builds
200
208
 
201
209
  ```bash
@@ -270,7 +278,9 @@ Every command emits events — one JSON object per line on stdout:
270
278
  {"event":"project_created","project_id":"…","slug":"…","organization":"…","url":"…","repo":"…","branch":"…"}
271
279
  {"event":"project_linked","project_id":"…","slug":"…","url":"…"}
272
280
  {"event":"source_connected","org":"…","connection_id":"…","provider":"…","account":"…"}
273
- {"event":"webhook_installed","project":"…","url":"…"} | {"event":"webhook_unavailable","project":"…","url":"…","hint":"…"}
281
+ {"event":"webhook_installed","project":"…"} | {"event":"webhook_unavailable","project":"…","url":"…","hint":"…"}
282
+ {"event":"setup_applied","project":"…","framework":"…","build_cmd":"…","output_dir":"…","layero_found":false}
283
+ {"event":"setup_pending","project":"…","url":"https://app.layero.ru/projects/…/setup","hint":"…"} | {"event":"setup_failed","project":"…","reason":"…","url":"…","hint":"…"}
274
284
  {"event":"sources","org":"…","providers":[…],"connections":[…]}
275
285
  {"event":"source_repos","org":"…","connection_id":"…","repos":[…]}
276
286
  {"event":"environments","project":"…","environments":[{"branch":"main","url":"https://…","production":true,…}]}
package/dist/agent.js CHANGED
@@ -144,7 +144,13 @@ function renderHuman(event) {
144
144
  // intentionally quiet in human mode — the next stage line takes over
145
145
  break;
146
146
  case "setup_applied":
147
- process.stdout.write(`✓ Setup applied\n`);
147
+ process.stdout.write(event.framework ? `✓ Setup applied (${event.framework})\n` : `✓ Setup applied\n`);
148
+ break;
149
+ case "setup_pending":
150
+ process.stdout.write(`· Проект ждёт настройки в панели: ${event.url}\n`);
151
+ break;
152
+ case "setup_failed":
153
+ process.stdout.write(`! ${event.hint}\n Причина: ${event.reason}\n`);
148
154
  break;
149
155
  case "preview_evicted": {
150
156
  // Предупреждение, а не отчёт: перестал отвечать адрес, который кому-то
package/dist/api.js CHANGED
@@ -402,6 +402,29 @@ export class ApiClient {
402
402
  connectSource(projectId, input) {
403
403
  return this.request("POST", `/projects/${projectId}/connect-source`, input);
404
404
  }
405
+ /**
406
+ * Подсказка детекта для проекта с репозиторием — то, что панель показывает
407
+ * в мастере: фреймворк, команда сборки, папка результата, менеджер пакетов,
408
+ * найден ли `layero.json`.
409
+ */
410
+ detectProject(projectId) {
411
+ return this.request("GET", `/projects/${projectId}/detect`);
412
+ }
413
+ /**
414
+ * Завершить мастер за человека: та же ручка, что у кнопки «Начать деплой»
415
+ * в панели. Обязателен только `framework_hint`; остальное — то, что дал
416
+ * детект. Не заданное здесь не заглушка, а «решит сборщик по репозиторию».
417
+ */
418
+ applySetup(projectId, input) {
419
+ return this.request("POST", `/projects/${projectId}/setup`, input);
420
+ }
421
+ /**
422
+ * Первая сборка проекта с репозиторием: без архива — сервер сам берёт HEAD
423
+ * ветки по умолчанию. Панель после мастера делает ровно этот вызов.
424
+ */
425
+ triggerRepoDeploy(projectId) {
426
+ return this.request("POST", `/projects/${projectId}/deploy`, {});
427
+ }
405
428
  /** Окружения проекта: ветки с адресами. Архивные и снятые с раздачи не входят. */
406
429
  listBranches(projectId) {
407
430
  return this.request("GET", `/projects/${projectId}/branches`);
@@ -100,10 +100,14 @@ async function main() {
100
100
  .option("--branch <name>", "основная ветка (по умолчанию — ветка репозитория по умолчанию)")
101
101
  .option("--name <name>", "имя проекта (по умолчанию — имя репозитория)")
102
102
  .option("--org <slug>", "организация (по умолчанию единственная)")
103
+ .option("--no-deploy", "не применять настройки и не запускать первую сборку — проект останется в мастере панели")
103
104
  .addHelpText("after", "\nПримеры:\n" +
104
105
  " $ layero projects create --repo github:acme/site\n" +
105
106
  " $ layero projects create --repo gitverse:acme/site --branch develop --json\n" +
106
- "\nСобытия в --json: project_created, source_connected, webhook_installed | webhook_unavailable.\n" +
107
+ "\nПосле привязки команда сама применяет настройки из детекта и запускает первую сборку,\n" +
108
+ "как кнопка «Начать деплой» в панели. С --no-deploy проект остаётся в мастере.\n" +
109
+ "\nСобытия в --json: project_created, source_connected, webhook_installed | webhook_unavailable,\n" +
110
+ "затем setup_applied + deploy_started | setup_pending | setup_failed (проект создан, доделать в панели).\n" +
107
111
  "Без вебхука push не собирается — заведите его вручную по адресу из webhook_unavailable.")
108
112
  .action(async (opts) => projectsCreateCmd({ ...opts, json: program.opts().json }));
109
113
  projects
@@ -1,6 +1,7 @@
1
1
  import readline from "node:readline/promises";
2
2
  import { ApiClient, ApiError } from "../api.js";
3
3
  import { loadConfig } from "../config.js";
4
+ import { dashboardOrigin } from "../urls.js";
4
5
  import { LayeroError, detectMode, emit } from "../agent.js";
5
6
  import { orgOf } from "./db.js";
6
7
  async function makeClient() {
@@ -95,7 +96,10 @@ export async function projectsCreateCmd(opts) {
95
96
  emitCreated(project, repo.path, branch);
96
97
  emit({ event: "source_connected", org, connection_id: account.key, provider, account: account.login });
97
98
  // У GitHub App вебхук — часть установки: без него App не существует.
98
- emit({ event: "webhook_installed", project: project.slug, url: "" });
99
+ // Отдельного адреса у него нет, и пустая строка в `url` читалась бы как
100
+ // «адрес потерян» — поля нет вовсе.
101
+ emit({ event: "webhook_installed", project: project.slug });
102
+ await finishSetup(api, project, opts);
99
103
  return;
100
104
  }
101
105
  const connectionId = account.key.replace(/^connection:/, "");
@@ -138,6 +142,110 @@ export async function projectsCreateCmd(opts) {
138
142
  "провайдер не дал создать вебхук этим токеном — заведите его в настройках репозитория вручную",
139
143
  });
140
144
  }
145
+ await finishSetup(api, connected.project, opts);
146
+ }
147
+ /** Адрес мастера проекта в панели — туда идёт человек, если мы не смогли. */
148
+ async function setupUrl(project) {
149
+ const cfg = await loadConfig();
150
+ return `${dashboardOrigin(cfg.apiUrl)}/projects/${project.id}/setup`;
151
+ }
152
+ /** Рантайм, который панель ставит проекту ДО первой сборки; статика — как есть. */
153
+ const RUNTIME_KINDS = new Set(["ssr_next", "streamlit", "gradio", "flask", "python_web", "node_web"]);
154
+ /** Приложения, у которых `framework_hint` — рудимент: панель шлёт им `static`. */
155
+ const HINT_IS_STATIC = new Set(["ssr_next", "streamlit", "gradio"]);
156
+ const PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
157
+ /**
158
+ * Тело `/setup` из подсказки детекта — так же, как его собирает мастер панели.
159
+ *
160
+ * Пишем только то, что детект действительно дал: пустое поле — не заглушка,
161
+ * а «решит сборщик по репозиторию» (правило 3 `core/docs/BUILD-CONFIG.md`).
162
+ * Менеджер пакетов закрепляем только из `layero.json`: это выбор владельца,
163
+ * а вывод по лок-файлу сборщик и так повторит сам.
164
+ */
165
+ export function setupPayloadFromDetect(d) {
166
+ const kind = d.runtime_kind ?? null;
167
+ const payload = {
168
+ framework_hint: kind && HINT_IS_STATIC.has(kind) ? "static" : d.framework,
169
+ };
170
+ if (d.build_cmd)
171
+ payload.build_cmd = d.build_cmd;
172
+ if (d.output_dir)
173
+ payload.output_dir = d.output_dir;
174
+ if (d.layero_found && d.package_manager && PACKAGE_MANAGERS.has(d.package_manager)) {
175
+ payload.package_manager = d.package_manager;
176
+ }
177
+ if (d.suggested_root_directory)
178
+ payload.root_directory = d.suggested_root_directory;
179
+ return payload;
180
+ }
181
+ /**
182
+ * Завершить мастер и запустить первую сборку — то, что панель делает по
183
+ * кнопке «Начать деплой». До 0.10.2 команда на этом останавливалась:
184
+ * проект оставался в `pending_setup`, первая сборка ждала клика в панели, а
185
+ * агент без панели узнавал об этом только по вечному «сборок ещё не было».
186
+ *
187
+ * Сбой детекта или настройки создание НЕ роняет: проект уже есть, адрес
188
+ * занят, и правильный исход — сказать «доделайте в панели» и выйти нулём.
189
+ */
190
+ async function finishSetup(api, project, opts) {
191
+ const panel = await setupUrl(project);
192
+ if (opts.deploy === false) {
193
+ emit({ event: "setup_pending", project: project.slug, url: panel, hint: "проект ждёт настройки в мастере панели" });
194
+ return;
195
+ }
196
+ let detected;
197
+ let payload;
198
+ try {
199
+ detected = await api.detectProject(project.id);
200
+ payload = setupPayloadFromDetect(detected);
201
+ await api.applySetup(project.id, payload);
202
+ }
203
+ catch (err) {
204
+ const reason = err instanceof ApiError ? err.body.slice(0, 300) : String(err);
205
+ emit({
206
+ event: "setup_failed",
207
+ project: project.slug,
208
+ reason,
209
+ url: panel,
210
+ hint: `проект создан, но настроить его не вышло — завершите в панели: ${panel}`,
211
+ });
212
+ return;
213
+ }
214
+ emit({
215
+ event: "setup_applied",
216
+ project: project.slug,
217
+ framework: payload.framework_hint,
218
+ build_cmd: payload.build_cmd ?? null,
219
+ output_dir: payload.output_dir ?? null,
220
+ layero_found: detected.layero_found,
221
+ });
222
+ // Приложение, а не статика: тип — до первой сборки, иначе сборщик падает на
223
+ // детекте («похоже на ssr_next, а настроено как spa»). Возражение сервера
224
+ // (409) сборку не запирает: это подсказка детекта, а не выбор человека.
225
+ const kind = detected.runtime_kind ?? null;
226
+ if (kind && RUNTIME_KINDS.has(kind) && project.project_type !== kind) {
227
+ try {
228
+ await api.setRuntimeType(project.id, kind);
229
+ emit({ event: "runtime_type_applied", project_type: kind });
230
+ }
231
+ catch {
232
+ /* сборщик переставит тип сам, если репозиторий его подтвердит */
233
+ }
234
+ }
235
+ try {
236
+ const deploy = await api.triggerRepoDeploy(project.id);
237
+ emit({ event: "deploy_started", project: project.slug, deploy_id: deploy.id, url: `https://${project.apex_hostname}` });
238
+ }
239
+ catch (err) {
240
+ const reason = err instanceof ApiError ? err.body.slice(0, 300) : String(err);
241
+ emit({
242
+ event: "setup_failed",
243
+ project: project.slug,
244
+ reason,
245
+ url: panel,
246
+ hint: `настройки применены, но первая сборка не запустилась — запустите в панели: ${panel}`,
247
+ });
248
+ }
141
249
  }
142
250
  function emitCreated(project, repo, branch) {
143
251
  emit({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Layero CLI — deploy a directory or a repo to hosting with build servers in Russia. JSON events and exit codes for AI agents and CI.",
5
5
  "license": "MIT",
6
6
  "type": "module",