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.
@@ -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
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
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
- // In CI nobody can open a browser, so the device flow can only end one
369
- // way: fifteen minutes of a hung job and then `auth_expired`. Fail
370
- // immediately instead, and say what to do burning a quarter of an hour
371
- // of someone's runner to reach a foregone conclusion is not acceptable.
372
- if (isCiEnv()) {
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
- cliCfg = await runDeviceLogin(cliCfg);
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
- const cwd = process.cwd();
380
- const existing = await loadProjectConfig(cwd);
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
- ? UUID_RE.test(opts.project)
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
+ }
@@ -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
- // Accepts an id directly; UUID format check is server-side.
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 Error("no project linked in cwd pass --project <id> or run `layero deploy` "
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 Error("not logged in. run `layero login` first.");
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
- if (hooks.length === 0) {
29
- console.log("no deploy hooks yet — `layero hooks create <name>` to add one.");
30
- return;
31
- }
32
- for (const h of hooks) {
33
- const targetTag = h.target === "production" ? chalk.red("[prod]") : chalk.cyan("[preview]");
34
- const branchTag = h.branch ? chalk.dim(`branch=${h.branch}`) : chalk.dim("branch=default");
35
- const last = h.last_triggered_at
36
- ? `fired ${new Date(h.last_triggered_at).toISOString()}`
37
- : "never fired";
38
- console.log(`${targetTag} ${chalk.bold(h.name)} ${branchTag} ${chalk.dim("(" + last + ")")}`);
39
- console.log(` ${chalk.dim(h.id)}`);
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 Error("name is required: `layero hooks create <name>`");
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
- const targetTag = hook.target === "production" ? chalk.red("[prod]") : chalk.cyan("[preview]");
55
- console.log(`${chalk.green("")} created ${targetTag} ${chalk.bold(hook.name)}`);
56
- console.log(` ${chalk.dim(hook.id)}`);
57
- console.log(` ${chalk.bold(hook.url)}`);
58
- console.log(chalk.dim("\n Paste this URL into your CMS / cron / external CI as a POST webhook. "
59
- + "Anyone with the URL can fire a build — treat it like a secret. "
60
- + "Rotate by deleting and creating a new one."));
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 Error("hook id is required: `layero hooks delete <id>`");
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
- console.error(chalk.yellow(`no hook ${hookId} on this project (already deleted?)`));
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
- console.log(`${chalk.green("")} hook ${chalk.dim(hookId)} revoked`);
76
+ emit({ event: "hook_deleted", project: projectId, id: hookId });
80
77
  }
@@ -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, detectMode } from "../agent.js";
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
- ## Deploying with Layero
14
+ ## Layero
11
15
 
12
- This project deploys to Layero (https://layero.ru). To ship local changes:
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
- \`\`\`bash
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
- There is no \`not_logged_in\`, \`deploy_error\` or \`deploy_timed_out\` do not
61
- branch on codes that are not listed here.
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
- ### Re-deploys and production
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 --prebuilt out
37
+ npx layero@latest deploy --json
91
38
  \`\`\`
92
39
 
93
- Full reference: https://docs.layero.ru/en/cli/agents
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
- if (mode.interactive) {
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
- if (mode.interactive) {
215
- console.log("");
216
- console.log(chalk.bold("Next:"));
217
- console.log(" $ npx layero login # one-time, in your browser");
218
- console.log(" $ npx layero deploy # ship the current directory");
219
- }
186
+ emit({
187
+ event: "init_done",
188
+ framework: detected.framework_hint,
189
+ agent_docs: agentDocs,
190
+ project_json: pjResult,
191
+ });
220
192
  }
@@ -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
- console.error(chalk.yellow("not logged in. run `layero login` first."));
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
- // The backend accepts UUIDs only on /projects/{id}; for slug we fall back
14
- // to listing this is fine because users typically have <100 projects.
15
- let proj;
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
- console.log(chalk.green(`linked ${proj.slug} (${proj.id}) → ./.layero/project.json`));
36
- if (proj.status === "pending_setup") {
37
- console.log(chalk.yellow(" note: project is in pending_setup. finish the setup wizard in the dashboard, " +
38
- "or run `layero deploy` to upload source and get the wizard URL."));
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
  }
@@ -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
- console.log(chalk.green(`logged out (removed ${configPath()})`));
5
+ emit({ event: "logged_out", config_path: configPath() });
6
6
  }
@@ -1,24 +1,20 @@
1
- import chalk from "chalk";
2
1
  import { ApiClient } from "../api.js";
3
2
  import { loadConfig } from "../config.js";
4
- /** `layero orgs list` show every Layero organization the caller belongs to.
3
+ import { LayeroError, emit } from "../agent.js";
4
+ /** `layero orgs list` — организации аккаунта: личная и команды.
5
5
  *
6
- * Useful before `layero deploy --org=<slug>` so the user can see which
7
- * slugs to pass without leaving the terminal.
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 Error("not logged in. run `layero login` first.");
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
- if (orgs.length === 0) {
16
- console.log(chalk.dim("no organizations on this account."));
17
- return;
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
  }