layero 0.8.2 → 0.8.4
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 +1 -1
- package/dist/agent.js +12 -0
- package/dist/commands/deploy.js +65 -28
- package/dist/commands/init.js +8 -10
- package/dist/config.js +14 -3
- package/package.json +2 -7
package/README.md
CHANGED
package/dist/agent.js
CHANGED
|
@@ -27,6 +27,18 @@ const AGENT_ENV_VARS = [
|
|
|
27
27
|
"CONTINUE_CLI",
|
|
28
28
|
];
|
|
29
29
|
const CI_ENV_VARS = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"];
|
|
30
|
+
/**
|
|
31
|
+
* True when we're on a build runner.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT the same as `!detectMode().interactive`: an AI agent
|
|
34
|
+
* (Cursor, Claude Code) is also non-interactive, but there the device-auth
|
|
35
|
+
* flow is the documented happy path — the agent renders the link and a human
|
|
36
|
+
* clicks it. On a runner nobody can click, so the same flow just hangs until
|
|
37
|
+
* the code expires.
|
|
38
|
+
*/
|
|
39
|
+
export function isCiEnv() {
|
|
40
|
+
return CI_ENV_VARS.some((k) => process.env[k] && process.env[k] !== "0");
|
|
41
|
+
}
|
|
30
42
|
export function detectMode(argv = process.argv) {
|
|
31
43
|
if (cachedMode)
|
|
32
44
|
return cachedMode;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -9,7 +9,7 @@ import { packCwd, packDirectory } from "../pack.js";
|
|
|
9
9
|
import { streamDeployLogs } from "../logs.js";
|
|
10
10
|
import { detectProject } from "../detect.js";
|
|
11
11
|
import { runDeviceLogin } from "../auth.js";
|
|
12
|
-
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
12
|
+
import { LayeroError, detectMode, emit, isCiEnv } from "../agent.js";
|
|
13
13
|
const VALID_TYPES = new Set([
|
|
14
14
|
"vite",
|
|
15
15
|
"next",
|
|
@@ -226,21 +226,44 @@ const PREBUILT_AUTO_DIRS = [
|
|
|
226
226
|
async function resolvePrebuiltDir(cwd, raw) {
|
|
227
227
|
if (raw === undefined || raw === false)
|
|
228
228
|
return null;
|
|
229
|
+
let dir = null;
|
|
229
230
|
if (typeof raw === "string" && raw.length > 0) {
|
|
230
|
-
|
|
231
|
+
dir = raw;
|
|
231
232
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
else {
|
|
234
|
+
// No explicit dir — pick the first existing common output directory.
|
|
235
|
+
for (const candidate of PREBUILT_AUTO_DIRS) {
|
|
236
|
+
try {
|
|
237
|
+
const stat = await fs.stat(path.join(cwd, candidate));
|
|
238
|
+
if (stat.isDirectory()) {
|
|
239
|
+
dir = candidate;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// try next
|
|
245
|
+
}
|
|
238
246
|
}
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
if (dir === null) {
|
|
248
|
+
throw new LayeroError("prebuilt_no_dir", "could not auto-detect a built artifact directory", `pass it explicitly: --prebuilt ./dist (tried: ${PREBUILT_AUTO_DIRS.join(", ")})`);
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
|
-
|
|
251
|
+
// Servability check (mirrors the builder-side gate, 2026-07-01 audit): a
|
|
252
|
+
// prebuilt dir with no index.html at its root isn't a servable site — the
|
|
253
|
+
// apex "/" would 404. Fail fast here, before packing + uploading, instead of
|
|
254
|
+
// the deploy going "ready" but broken.
|
|
255
|
+
let hasIndex = false;
|
|
256
|
+
try {
|
|
257
|
+
const entries = await fs.readdir(path.join(cwd, dir));
|
|
258
|
+
hasIndex = entries.some((e) => e.toLowerCase() === "index.html");
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
hasIndex = false;
|
|
262
|
+
}
|
|
263
|
+
if (!hasIndex) {
|
|
264
|
+
throw new LayeroError("prebuilt_no_index", `'${dir}' has no index.html — nothing to serve`, `point --prebuilt at the folder that contains your built index.html (e.g. --prebuilt ./dist)`);
|
|
265
|
+
}
|
|
266
|
+
return dir;
|
|
244
267
|
}
|
|
245
268
|
// Resolve the framework / build / output config to send to completeSetup.
|
|
246
269
|
// Precedence: explicit CLI args > .layero/project.json > auto-detect.
|
|
@@ -287,6 +310,14 @@ export async function deployCmd(opts) {
|
|
|
287
310
|
}
|
|
288
311
|
let cliCfg = await loadConfig();
|
|
289
312
|
if (!cliCfg.token) {
|
|
313
|
+
// In CI nobody can open a browser, so the device flow can only end one
|
|
314
|
+
// way: fifteen minutes of a hung job and then `auth_expired`. Fail
|
|
315
|
+
// immediately instead, and say what to do — burning a quarter of an hour
|
|
316
|
+
// of someone's runner to reach a foregone conclusion is not acceptable.
|
|
317
|
+
if (isCiEnv()) {
|
|
318
|
+
throw new LayeroError("auth_required", "No credentials in CI. Create a token at https://app.layero.ru/settings/cli " +
|
|
319
|
+
"and pass it as the LAYERO_TOKEN environment variable.", "set_layero_token");
|
|
320
|
+
}
|
|
290
321
|
// Not authenticated yet. Kick off the browser device-login flow inline
|
|
291
322
|
// and poll, exactly as the docs describe (B5/I4) — no separate `layero
|
|
292
323
|
// login` step required. In JSON/agent mode this emits `auth_required`
|
|
@@ -431,30 +462,36 @@ export async function deployCmd(opts) {
|
|
|
431
462
|
const dashboardUrl = projectUrl(cliCfg.apiUrl, project.id);
|
|
432
463
|
const apexUrl = `https://${project.apex_hostname}`;
|
|
433
464
|
const probe = await resolveReachability(api, deploy.environment_id);
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
465
|
+
// Публичный адрес сайта.
|
|
466
|
+
//
|
|
467
|
+
// Раньше здесь стоял гейт `cdn_ready`: пока YC CDN не подтвердил апекс,
|
|
468
|
+
// отдавали off-CDN превью, потому что свежий апекс 10-15 минут отвечал
|
|
469
|
+
// 404, а runtime-приложения на апексе вообще не принимали POST (CDN резал
|
|
470
|
+
// не-GET). После EDGE-02 CDN перед пользовательскими зонами нет: апекс
|
|
471
|
+
// валиден с момента создания проекта (wildcard-запись + wildcard-серт), а
|
|
472
|
+
// `cdn_ready` бекенд отдаёт false НАВСЕГДА — строки в `cdn_hostnames` у
|
|
473
|
+
// новых проектов не появляется вовсе.
|
|
474
|
+
//
|
|
475
|
+
// Из-за этого условие не выполнялось никогда, и после успешной публикации
|
|
476
|
+
// CLI выдавал ссылку на дашборд вместо адреса сайта. Проверено вживую
|
|
477
|
+
// 2026-07-26: `layero deploy` вернул `url: https://app.layero.ru/projects/…`.
|
|
478
|
+
//
|
|
479
|
+
// Апекс — адрес по умолчанию; превью остаётся для деплоя ветки, которую
|
|
480
|
+
// не промоутили: там апекс ведёт на другую сборку.
|
|
481
|
+
const liveUrl = promoted || !opts.branch
|
|
444
482
|
? probe?.canonical_url ?? apexUrl
|
|
445
|
-
: probe?.preview_url ??
|
|
446
|
-
probe?.canonical_url ??
|
|
447
|
-
(promoted || !opts.branch ? apexUrl : dashboardUrl);
|
|
483
|
+
: probe?.preview_url ?? probe?.canonical_url ?? apexUrl;
|
|
448
484
|
emit({
|
|
449
485
|
event: "ready",
|
|
450
486
|
url: liveUrl,
|
|
451
487
|
deploy_id: deploy.id,
|
|
452
488
|
preview_url: probe?.preview_url ?? undefined,
|
|
453
489
|
dashboard_url: dashboardUrl,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
490
|
+
// `available` от пробы, а НЕ `cdn_ready`: последний после EDGE-02
|
|
491
|
+
// остаётся false навсегда, и потребитель JSON-вывода ждал бы события,
|
|
492
|
+
// которого не будет. ETA пропагации убран по той же причине —
|
|
493
|
+
// распространять больше нечего.
|
|
494
|
+
edge_ready: probe ? probe.available : undefined,
|
|
458
495
|
});
|
|
459
496
|
}
|
|
460
497
|
finally {
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,7 @@ function agentDocBlock(framework) {
|
|
|
9
9
|
return `${AGENT_BLOCK_MARKER_START}
|
|
10
10
|
## Deploying with Layero
|
|
11
11
|
|
|
12
|
-
This project deploys to https://layero.ru. To ship local changes:
|
|
12
|
+
This project deploys to Layero (https://layero.ru). To ship local changes:
|
|
13
13
|
|
|
14
14
|
\`\`\`bash
|
|
15
15
|
npx layero@latest deploy
|
|
@@ -44,7 +44,7 @@ stdout), the CLI auto-switches to JSON-lines. Key events to watch:
|
|
|
44
44
|
| \`detected\` | framework auto-detection result |
|
|
45
45
|
| \`project_created\` / \`project_linked\` | project bound for this directory |
|
|
46
46
|
| \`build_log\` | forward only if it contains errors |
|
|
47
|
-
| \`ready\` | \`url\` = live public site (show to user, stop). \`
|
|
47
|
+
| \`ready\` | \`url\` = live public site (show to user, stop). \`dashboard_url\` = management page. |
|
|
48
48
|
| \`error\` | follow \`next_action\` field verbatim |
|
|
49
49
|
|
|
50
50
|
Common error codes and remediation:
|
|
@@ -58,17 +58,15 @@ Common error codes and remediation:
|
|
|
58
58
|
### Re-deploys and production
|
|
59
59
|
|
|
60
60
|
A plain \`npx layero deploy\` of a CLI project **publishes to the apex**
|
|
61
|
-
\`https://<
|
|
61
|
+
\`https://<project>.layero.app\` — direct uploads auto-promote, so you do
|
|
62
62
|
**not** need \`--prod\` or a separate \`promote\` step. Safe to run repeatedly;
|
|
63
63
|
each run replaces what the apex serves.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
\`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Hand the user \`ready.url\` (the apex); if \`edge_ready\` is false, also offer
|
|
71
|
-
\`preview_url\` so they can see it right away.
|
|
65
|
+
There is no separate per-deploy preview address: user sites live in the
|
|
66
|
+
\`layero.app\` zone, which has no preview sub-zone and no CDN in front, so the
|
|
67
|
+
apex is reachable the moment the deploy is ready.
|
|
68
|
+
|
|
69
|
+
Hand the user \`ready.url\` and stop — that address is live.
|
|
72
70
|
|
|
73
71
|
Use \`--branch <name>\` to deploy to an isolated preview environment that does
|
|
74
72
|
**not** touch the apex. (\`--prod\` exists for git-connected projects; for
|
package/dist/config.js
CHANGED
|
@@ -7,19 +7,30 @@ const DEFAULT_API_URL = process.env.LAYERO_API_URL ?? "https://api.layero.ru";
|
|
|
7
7
|
async function ensureDir() {
|
|
8
8
|
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
9
9
|
}
|
|
10
|
+
// Долгоживущий токен для CI. Выпускается в дашборде, кладётся в секреты
|
|
11
|
+
// репозитория и попадает сюда через окружение — на раннере нет ни `layero
|
|
12
|
+
// login` (некому пройти device flow), ни конфиг-файла.
|
|
13
|
+
//
|
|
14
|
+
// Приоритет выше файла намеренно: если на машине есть и то и другое,
|
|
15
|
+
// окружение выигрывает. Иначе локальный конфиг разработчика молча
|
|
16
|
+
// перебивал бы токен, заданный в CI, и деплой уходил бы не в тот аккаунт.
|
|
17
|
+
const ENV_TOKEN = process.env.LAYERO_TOKEN?.trim() || undefined;
|
|
10
18
|
export async function loadConfig() {
|
|
11
19
|
try {
|
|
12
20
|
const raw = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
13
21
|
const parsed = JSON.parse(raw);
|
|
14
22
|
return {
|
|
15
23
|
apiUrl: parsed.apiUrl ?? DEFAULT_API_URL,
|
|
16
|
-
token: parsed.token,
|
|
17
|
-
|
|
24
|
+
token: ENV_TOKEN ?? parsed.token,
|
|
25
|
+
// Пользователя из файла не подставляем, когда токен пришёл из
|
|
26
|
+
// окружения: файл мог остаться от другого аккаунта, и подпись в
|
|
27
|
+
// выводе врала бы. Кто мы — узнаем у API.
|
|
28
|
+
user: ENV_TOKEN ? undefined : parsed.user,
|
|
18
29
|
};
|
|
19
30
|
}
|
|
20
31
|
catch (err) {
|
|
21
32
|
if (err?.code === "ENOENT") {
|
|
22
|
-
return { apiUrl: DEFAULT_API_URL };
|
|
33
|
+
return { apiUrl: DEFAULT_API_URL, token: ENV_TOKEN };
|
|
23
34
|
}
|
|
24
35
|
throw err;
|
|
25
36
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layero",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Layero CLI — publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://layero.ru",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/LayeroInfra/core.git",
|
|
11
|
-
"directory": "cli"
|
|
12
|
-
},
|
|
13
8
|
"bugs": {
|
|
14
|
-
"url": "https://
|
|
9
|
+
"url": "https://docs.layero.ru/contacts/"
|
|
15
10
|
},
|
|
16
11
|
"keywords": [
|
|
17
12
|
"layero",
|