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 +12 -2
- package/dist/agent.js +7 -1
- package/dist/api.js +23 -0
- package/dist/bin/layero.js +5 -1
- package/dist/commands/projects.js +109 -1
- package/package.json +1 -1
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":"…"
|
|
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`);
|
package/dist/bin/layero.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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