layero 0.9.5 → 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 +125 -0
- package/dist/bin/layero.js +92 -33
- 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 +2 -2
- package/dist/commands/deploy.js +80 -10
- 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/package.json +10 -5
package/dist/commands/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ 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";
|
|
6
7
|
import { dashboardOrigin } from "../urls.js";
|
|
7
8
|
import { loadConfig } from "../config.js";
|
|
8
9
|
import { loadProjectConfig, persistProjectLinking, } from "../project-config.js";
|
|
@@ -12,7 +13,7 @@ import { detectProject } from "../detect.js";
|
|
|
12
13
|
import { runDeviceLogin } from "../auth.js";
|
|
13
14
|
import { LayeroError, detectMode, emit, isCiEnv } from "../agent.js";
|
|
14
15
|
import { ensureUsername } from "../username.js";
|
|
15
|
-
|
|
16
|
+
import { claimTokenFor, createClaimable } from "./claim.js";
|
|
16
17
|
const VALID_TYPES = new Set([
|
|
17
18
|
"vite",
|
|
18
19
|
"next",
|
|
@@ -363,21 +364,78 @@ export async function deployCmd(opts) {
|
|
|
363
364
|
`runtime kinds: ${[...new Set(Object.values(RUNTIME_TYPES))].join(", ")} ` +
|
|
364
365
|
`(aliases: ${Object.keys(RUNTIME_TYPES).join(", ")})`);
|
|
365
366
|
}
|
|
367
|
+
const cwd = process.cwd();
|
|
368
|
+
let existing = await loadProjectConfig(cwd);
|
|
366
369
|
let cliCfg = await loadConfig();
|
|
370
|
+
// Claimable-проект этого запуска (этап 13): событие `claimable` уходит
|
|
371
|
+
// перед `ready`, когда адрес сайта уже известен.
|
|
372
|
+
let claimable = null;
|
|
367
373
|
if (!cliCfg.token) {
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
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.
|
|
373
403
|
throw new LayeroError("auth_required", "No credentials in CI. Create a token at https://app.layero.ru/settings/cli " +
|
|
374
404
|
"and pass it as the LAYERO_TOKEN environment variable.", "set_layero_token");
|
|
375
405
|
}
|
|
376
|
-
|
|
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`");
|
|
377
412
|
}
|
|
378
413
|
const api = new ApiClient(cliCfg);
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
439
|
// --- Всё, что требует файловой системы, делаем сами. Остальное — сервер.
|
|
382
440
|
const prebuiltDir = await resolvePrebuiltDir(cwd, opts.prebuilt);
|
|
383
441
|
const setup = prebuiltDir
|
|
@@ -436,7 +494,7 @@ export async function deployCmd(opts) {
|
|
|
436
494
|
// Опечатка в слаге при этом обязана дать 404, а не завести лишний
|
|
437
495
|
// проект с похожим именем: `create_if_missing: false`.
|
|
438
496
|
...(opts.project
|
|
439
|
-
?
|
|
497
|
+
? looksLikeId(opts.project)
|
|
440
498
|
? { project_id: opts.project }
|
|
441
499
|
: { name: opts.project, create_if_missing: false }
|
|
442
500
|
: existing?.project_id
|
|
@@ -638,6 +696,18 @@ export async function deployCmd(opts) {
|
|
|
638
696
|
if (evicted.length > 0) {
|
|
639
697
|
emit({ event: "preview_evicted", evicted });
|
|
640
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
|
+
}
|
|
641
711
|
emit({
|
|
642
712
|
event: "ready",
|
|
643
713
|
url: liveUrl,
|
|
@@ -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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,96 +1,73 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import chalk from "chalk";
|
|
4
3
|
import { detectProject } from "../detect.js";
|
|
5
|
-
import { emit
|
|
4
|
+
import { emit } from "../agent.js";
|
|
6
5
|
const AGENT_BLOCK_MARKER_START = "<!-- layero:start -->";
|
|
7
6
|
const AGENT_BLOCK_MARKER_END = "<!-- layero:end -->";
|
|
8
7
|
function agentDocBlock(framework) {
|
|
8
|
+
// Компактный индекс, а не ссылка на навык: по evals Vercel блок в
|
|
9
|
+
// AGENTS.md агент выполняет надёжнее (100 % против 79 % у навыка).
|
|
10
|
+
// Полная версия — навык `layero` в LayeroInfra/layero-agents; здесь —
|
|
11
|
+
// то, без чего первый деплой не случится. Коды ошибок сверяет
|
|
12
|
+
// `check-error-codes.py`: не перечислять здесь то, чего CLI не выдаёт.
|
|
9
13
|
return `${AGENT_BLOCK_MARKER_START}
|
|
10
|
-
##
|
|
14
|
+
## Layero
|
|
11
15
|
|
|
12
|
-
This project deploys to Layero (https://layero.ru)
|
|
16
|
+
This project deploys to Layero (https://layero.ru) — hosting with build servers
|
|
17
|
+
in Russia. Full agent skill: https://github.com/LayeroInfra/layero-agents
|
|
18
|
+
(\`npx skills add LayeroInfra/layero-agents\`, Claude Code:
|
|
19
|
+
\`claude plugin marketplace add LayeroInfra/layero-agents && claude plugin install layero@layero\`).
|
|
20
|
+
Docs for agents: https://docs.layero.ru/agents/
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
npx layero@latest deploy
|
|
16
|
-
\`\`\`
|
|
17
|
-
|
|
18
|
-
Detected framework: **${framework}**. No git repository or GitHub connection
|
|
19
|
-
is required — Layero packs and uploads the local directory directly.
|
|
20
|
-
|
|
21
|
-
### First-time auth (one-click device flow)
|
|
22
|
-
|
|
23
|
-
If you're not logged in yet, \`npx layero@latest deploy\` (or \`… login\`)
|
|
24
|
-
starts the browser device-flow automatically and emits a JSON line:
|
|
25
|
-
|
|
26
|
-
\`\`\`json
|
|
27
|
-
{"event":"auth_required","url":"https://app.layero.ru/cli?code=ABCD-1234","user_code":"ABCD-1234"}
|
|
28
|
-
\`\`\`
|
|
29
|
-
|
|
30
|
-
Render the \`url\` as a clickable link in chat. The user opens it, signs in
|
|
31
|
-
(by an emailed code or with Yandex ID — Layero creates the account on first sign-in),
|
|
32
|
-
clicks "Разрешить доступ", and the CLI's poll loop picks up the token within
|
|
33
|
-
2 seconds. No localhost server is involved — the browser can be on a
|
|
34
|
-
different machine than the CLI.
|
|
35
|
-
|
|
36
|
-
### JSON-lines events
|
|
37
|
-
|
|
38
|
-
When run inside an agent (\`CURSOR_AGENT\`, \`CLAUDECODE\`, or any non-TTY
|
|
39
|
-
stdout), the CLI auto-switches to JSON-lines. Key events to watch:
|
|
40
|
-
|
|
41
|
-
| event | meaning |
|
|
42
|
-
|---|---|
|
|
43
|
-
| \`auth_required\` | render \`url\` as a link, keep waiting |
|
|
44
|
-
| \`detected\` | framework auto-detection result |
|
|
45
|
-
| \`project_created\` / \`project_linked\` | project bound for this directory |
|
|
46
|
-
| \`build_log\` | forward only if it contains errors |
|
|
47
|
-
| \`ready\` | \`url\` = live public site (show to user, stop). \`dashboard_url\` = management page. |
|
|
48
|
-
| \`error\` | follow \`next_action\` field verbatim |
|
|
49
|
-
|
|
50
|
-
Common error codes and remediation:
|
|
51
|
-
|
|
52
|
-
- \`auth_required\` → run \`npx layero@latest login\`, or set \`LAYERO_TOKEN\`
|
|
53
|
-
- \`auth_expired\` / \`auth_timeout\` → user did not approve in time, re-run login
|
|
54
|
-
- \`project_unknown\` → run from the project directory, or pass \`--project\`
|
|
55
|
-
- \`invalid_type\` → drop \`--type\`, rely on auto-detect
|
|
56
|
-
- \`cli_deploys_disabled\` → user must enable CLI deploys in project settings
|
|
57
|
-
- \`deploy_failed\` → check the dashboard URL in the message
|
|
58
|
-
- \`internal\` → unexpected CLI error; re-run with \`--debug\`
|
|
22
|
+
### Three paths — pick by situation
|
|
59
23
|
|
|
60
|
-
|
|
61
|
-
branch
|
|
24
|
+
1. **Repository connected** (GitHub, GitVerse, GitLab, GitFlic, SourceCraft) —
|
|
25
|
+
push to a branch = preview, push to \`main\` = production. Connect one with
|
|
26
|
+
\`npx layero@latest projects create --repo <provider>:<owner/repo>\`.
|
|
27
|
+
2. **A directory with code** (this project, framework: **${framework}**) —
|
|
28
|
+
\`npx layero@latest deploy --json\`. The CLI packs the directory, the
|
|
29
|
+
platform builds it. No git repository is needed for this path.
|
|
30
|
+
3. **A site already on Layero** — \`npx layero@latest diagnose\`, \`logs\`,
|
|
31
|
+
\`rollback\`, \`domains\`, \`env\`, \`envs list\`, or the MCP server
|
|
32
|
+
\`https://mcp.layero.ru/mcp\`.
|
|
62
33
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
A plain \`npx layero deploy\` of a CLI project **publishes to the apex**
|
|
66
|
-
\`https://<project>.layero.app\` — direct uploads auto-promote, so you do
|
|
67
|
-
**not** need \`--prod\` or a separate \`promote\` step. Safe to run repeatedly;
|
|
68
|
-
each run replaces what the apex serves.
|
|
69
|
-
|
|
70
|
-
There is no separate per-deploy preview address: user sites live in the
|
|
71
|
-
\`layero.app\` zone, which has no preview sub-zone and no CDN in front, so the
|
|
72
|
-
apex is reachable the moment the deploy is ready.
|
|
73
|
-
|
|
74
|
-
Hand the user \`ready.url\` and stop — that address is live.
|
|
75
|
-
|
|
76
|
-
There is no way to publish without replacing the live site from the CLI:
|
|
77
|
-
\`--branch\` is accepted and **silently ignored** — archive uploads are always
|
|
78
|
-
filed under the reserved \`cli\` environment. If the user asks for a version
|
|
79
|
-
"just to look at" that leaves the live address alone, tell them it needs a
|
|
80
|
-
connected repository and a push to a branch. (\`--prod\` exists for
|
|
81
|
-
git-connected projects; for direct CLI uploads it's redundant.)
|
|
82
|
-
|
|
83
|
-
### Already built? Skip the server build
|
|
84
|
-
|
|
85
|
-
If the site is already built locally (e.g. a Next.js static export in \`out/\`,
|
|
86
|
-
or a \`dist/\`), ship the artifact directly and skip the server-side
|
|
87
|
-
\`npm install\` + build:
|
|
34
|
+
### Deploy from this directory
|
|
88
35
|
|
|
89
36
|
\`\`\`bash
|
|
90
|
-
npx layero@latest deploy --
|
|
37
|
+
npx layero@latest deploy --json
|
|
91
38
|
\`\`\`
|
|
92
39
|
|
|
93
|
-
|
|
40
|
+
Not logged in? The command starts the browser device flow itself and prints
|
|
41
|
+
\`{"event":"auth_required","url":"…","user_code":"…"}\` — show \`url\` as a
|
|
42
|
+
clickable link and keep waiting; the CLI polls every 2 s. No localhost
|
|
43
|
+
callback: the browser may be on another machine. In CI use
|
|
44
|
+
\`LAYERO_TOKEN=… npx layero@latest deploy --project <slug> --json --yes\`.
|
|
45
|
+
No account at all? \`npx layero@latest deploy --claim\` publishes to a
|
|
46
|
+
temporary project for 72 hours and prints a \`claim_url\` for a human to
|
|
47
|
+
take it over.
|
|
48
|
+
|
|
49
|
+
Key JSON events: \`detected\` (framework), \`project_created\` /
|
|
50
|
+
\`project_linked\`, \`build_log\` (forward only lines with errors),
|
|
51
|
+
\`claimable\` (\`claim_url\`, \`expires_at\`), \`ready\` — \`url\` is the live
|
|
52
|
+
site: show it as-is and stop; \`dashboard_url\` is the panel, not the site.
|
|
53
|
+
\`error\` — follow \`next_action\` verbatim.
|
|
54
|
+
|
|
55
|
+
Exit codes: 0 ok · 2 auth (\`auth_required\`, \`auth_expired\`, \`auth_timeout\`) ·
|
|
56
|
+
3 not found (\`project_unknown\`, \`project_not_found\`) · 4 invalid input
|
|
57
|
+
(\`invalid_type\`, \`prebuilt_no_dir\`, \`branch_unsupported\`) · 5 remote
|
|
58
|
+
(\`deploy_failed\`, \`internal\`). Codes \`not_logged_in\`, \`deploy_error\`,
|
|
59
|
+
\`deploy_timed_out\` do not exist — do not branch on them.
|
|
60
|
+
|
|
61
|
+
### Rules
|
|
62
|
+
|
|
63
|
+
- Re-running \`deploy\` is safe and reuses the project; no commit needed.
|
|
64
|
+
- A plain \`deploy\` of a CLI project **replaces the live site** at
|
|
65
|
+
\`ready.url\`: direct uploads auto-promote. \`--branch\` is refused
|
|
66
|
+
(\`branch_unsupported\`) — isolated previews come from pushing a branch of
|
|
67
|
+
a connected repository, nothing else.
|
|
68
|
+
- Already built locally? \`npx layero@latest deploy --prebuilt <dir>\`.
|
|
69
|
+
- Never \`git init\` just to deploy, never \`npm install -g layero\`, never
|
|
70
|
+
build the site address from a template — only \`ready.url\`.
|
|
94
71
|
${AGENT_BLOCK_MARKER_END}
|
|
95
72
|
`;
|
|
96
73
|
}
|
|
@@ -172,7 +149,6 @@ async function ensureGitignore(cwd) {
|
|
|
172
149
|
}
|
|
173
150
|
export async function initCmd(opts) {
|
|
174
151
|
const cwd = process.cwd();
|
|
175
|
-
const mode = detectMode();
|
|
176
152
|
const detected = await detectProject(cwd);
|
|
177
153
|
emit({
|
|
178
154
|
event: "detected",
|
|
@@ -182,6 +158,7 @@ export async function initCmd(opts) {
|
|
|
182
158
|
confident: detected.confident,
|
|
183
159
|
});
|
|
184
160
|
const block = agentDocBlock(detected.framework_hint);
|
|
161
|
+
const agentDocs = [];
|
|
185
162
|
if (!opts.skipAgentDocs) {
|
|
186
163
|
// Touch every agent-doc convention we know about. If one already
|
|
187
164
|
// exists we update it in-place; otherwise we create only the most
|
|
@@ -201,20 +178,15 @@ export async function initCmd(opts) {
|
|
|
201
178
|
const targets = existing.length > 0 ? existing : ["AGENTS.md"];
|
|
202
179
|
for (const f of targets) {
|
|
203
180
|
const result = await upsertAgentDoc(cwd, f, block);
|
|
204
|
-
|
|
205
|
-
console.log(chalk.green(` ${result === "created" ? "✓ created" : result === "updated" ? "✓ updated" : "= unchanged"} ${f}`));
|
|
206
|
-
}
|
|
181
|
+
agentDocs.push({ file: f, result });
|
|
207
182
|
}
|
|
208
183
|
}
|
|
209
184
|
const pjResult = await ensureProjectJson(cwd, detected.framework_hint, detected.build_cmd, detected.output_dir);
|
|
210
|
-
if (mode.interactive) {
|
|
211
|
-
console.log(chalk.green(` ${pjResult === "created" ? "✓ created" : "= unchanged"} .layero/project.json`));
|
|
212
|
-
}
|
|
213
185
|
await ensureGitignore(cwd);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
186
|
+
emit({
|
|
187
|
+
event: "init_done",
|
|
188
|
+
framework: detected.framework_hint,
|
|
189
|
+
agent_docs: agentDocs,
|
|
190
|
+
project_json: pjResult,
|
|
191
|
+
});
|
|
220
192
|
}
|
package/dist/commands/link.js
CHANGED
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { ApiClient } from "../api.js";
|
|
3
2
|
import { loadConfig } from "../config.js";
|
|
4
3
|
import { persistProjectLinking } from "../project-config.js";
|
|
4
|
+
import { LayeroError, emit } from "../agent.js";
|
|
5
5
|
export async function linkCmd(idOrSlug) {
|
|
6
6
|
const cfg = await loadConfig();
|
|
7
7
|
if (!cfg.token) {
|
|
8
|
-
|
|
9
|
-
process.exitCode = 1;
|
|
10
|
-
return;
|
|
8
|
+
throw new LayeroError("auth_required", "вход не выполнен", "выполните `layero login` или задайте LAYERO_TOKEN");
|
|
11
9
|
}
|
|
12
10
|
const api = new ApiClient(cfg);
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
proj = await api.getProject(idOrSlug);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
const all = await api.listProjects();
|
|
21
|
-
const match = all.find((p) => p.slug === idOrSlug);
|
|
22
|
-
if (!match) {
|
|
23
|
-
console.error(chalk.red(`no project with id/slug "${idOrSlug}"`));
|
|
24
|
-
process.exitCode = 1;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
proj = match;
|
|
28
|
-
}
|
|
11
|
+
// Слаг или id — различает клиент по форме значения (`resolveProject`):
|
|
12
|
+
// UUID, ушедший в поиск по слагу, не нашёлся бы.
|
|
13
|
+
const proj = await api.resolveProject(idOrSlug);
|
|
29
14
|
await persistProjectLinking(process.cwd(), {
|
|
30
15
|
project_id: proj.id,
|
|
31
16
|
slug: proj.slug,
|
|
32
17
|
organization_slug: proj.organization.slug,
|
|
33
18
|
apex_hostname: proj.apex_hostname,
|
|
34
19
|
}, proj.framework_hint ?? null);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
emit({
|
|
21
|
+
event: "project_linked",
|
|
22
|
+
project_id: proj.id,
|
|
23
|
+
slug: proj.slug,
|
|
24
|
+
url: `https://${proj.apex_hostname}`,
|
|
25
|
+
status: proj.status,
|
|
26
|
+
});
|
|
40
27
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { clearConfig, configPath } from "../config.js";
|
|
2
|
+
import { emit } from "../agent.js";
|
|
3
3
|
export async function logoutCmd() {
|
|
4
4
|
await clearConfig();
|
|
5
|
-
|
|
5
|
+
emit({ event: "logged_out", config_path: configPath() });
|
|
6
6
|
}
|
package/dist/commands/orgs.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { ApiClient } from "../api.js";
|
|
3
2
|
import { loadConfig } from "../config.js";
|
|
4
|
-
|
|
3
|
+
import { LayeroError, emit } from "../agent.js";
|
|
4
|
+
/** `layero orgs list` — организации аккаунта: личная и команды.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Нужна перед `layero deploy --org=<slug>`, чтобы увидеть слаги, не выходя
|
|
7
|
+
* из терминала.
|
|
8
8
|
*/
|
|
9
9
|
export async function orgsListCmd() {
|
|
10
10
|
const cfg = await loadConfig();
|
|
11
|
-
if (!cfg.token)
|
|
12
|
-
throw new
|
|
11
|
+
if (!cfg.token) {
|
|
12
|
+
throw new LayeroError("auth_required", "вход не выполнен", "выполните `layero login` или задайте LAYERO_TOKEN");
|
|
13
|
+
}
|
|
13
14
|
const api = new ApiClient(cfg);
|
|
14
15
|
const orgs = await api.listOrganizations();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
for (const o of orgs) {
|
|
20
|
-
const kindBadge = o.kind === "personal" ? chalk.dim("personal") : chalk.cyan("team");
|
|
21
|
-
const roleBadge = chalk.dim(`(${o.my_role})`);
|
|
22
|
-
console.log(` ${chalk.bold(o.slug.padEnd(20))} ${kindBadge} ${roleBadge}`);
|
|
23
|
-
}
|
|
16
|
+
emit({
|
|
17
|
+
event: "organizations",
|
|
18
|
+
organizations: orgs.map((o) => ({ id: o.id, slug: o.slug, kind: o.kind, role: o.my_role })),
|
|
19
|
+
});
|
|
24
20
|
}
|