layero 0.11.6 → 0.11.8
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 +21 -12
- package/dist/agent.js +1 -1
- package/dist/bin/layero.js +4 -3
- package/dist/commands/claim.js +78 -16
- package/dist/commands/deploy.js +63 -17
- package/dist/config.js +52 -0
- package/dist/exit-codes.js +3 -1
- package/dist/project-config.js +30 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,12 +122,12 @@ Run `layero <cmd> --help` for full options.
|
|
|
122
122
|
separate `cli` environment). A project without a repository is always
|
|
123
123
|
published live.
|
|
124
124
|
- `--branch <name>` — **refused** (`branch_unsupported`, exit 4), see above.
|
|
125
|
-
- `--claim` — deploy without an account: a temporary
|
|
126
|
-
plus a `claim_url` for a human to take it over.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
- `--claim` — deploy without an account: a temporary site for 1 hour
|
|
126
|
+
plus a `claim_url` for a human to take it over. Static sites and SPAs only:
|
|
127
|
+
a server app (SSR, fullstack, container) is refused before anything is
|
|
128
|
+
uploaded (`claim_static_only`, exit 4). Only with this flag: without a
|
|
129
|
+
token and without `--claim`, `deploy` asks for a login (`auth_required`),
|
|
130
|
+
in an agent environment too. With `--project` it is refused
|
|
131
131
|
(`claim_with_project`, exit 4). In CI a missing `LAYERO_TOKEN` stays an error.
|
|
132
132
|
- `--org <slug>` — organization for first-time project creation.
|
|
133
133
|
- `--yes` / `-y` — non-interactive mode.
|
|
@@ -143,14 +143,23 @@ The platform creates a temporary project and a token for it; the CLI deploys
|
|
|
143
143
|
with that token and prints, before `ready`:
|
|
144
144
|
|
|
145
145
|
```
|
|
146
|
-
{"event":"claimable","url":"https://
|
|
146
|
+
{"event":"claimable","url":"https://k3v9q2m8x7w1c4r6t0y5u2ze.layero.app","claim_url":"https://app.layero.ru/claim?code=…","expires_at":"…"}
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
The site lives for
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
The site lives for 1 hour, then stops answering and is deleted. Only static
|
|
150
|
+
sites and SPAs are accepted: a server app needs an account. The address is
|
|
151
|
+
random (the folder name and `--name` do not go into it), and the site is
|
|
152
|
+
closed to search engines: `robots.txt` with `Disallow: /` and
|
|
153
|
+
`X-Robots-Tag: noindex, nofollow`. A human opens `claim_url`, signs in and
|
|
154
|
+
takes the project into their account — the CLI cannot accept a claim by
|
|
155
|
+
itself.
|
|
156
|
+
The temporary token, the claim code and the link are saved in
|
|
157
|
+
`~/.layero/config.json` (readable by you only); `.layero/project.json` gets
|
|
158
|
+
only the folder's link to the project, because that file goes to git and a
|
|
159
|
+
claim code there would let anyone take the site. `layero deploy` in the same
|
|
160
|
+
directory keeps updating the same site until the claim expires or is
|
|
161
|
+
accepted. Claiming the site clears what was set up without an account —
|
|
162
|
+
environment variables, deploy hooks and build settings.
|
|
154
163
|
`layero claim status` shows where things stand; `layero claim accept` opens
|
|
155
164
|
the page in a browser (in agent mode it prints the link).
|
|
156
165
|
|
package/dist/agent.js
CHANGED
|
@@ -343,7 +343,7 @@ function renderHuman(event) {
|
|
|
343
343
|
break;
|
|
344
344
|
case "claimable":
|
|
345
345
|
process.stdout.write(`→ Временный проект ${event.slug}: ${event.url}\n`);
|
|
346
|
-
process.stdout.write(` Сайт живёт до ${event.expires_at} (
|
|
346
|
+
process.stdout.write(` Сайт живёт до ${event.expires_at} (час). Забрать в аккаунт: ${event.claim_url}\n`);
|
|
347
347
|
break;
|
|
348
348
|
case "claim_status":
|
|
349
349
|
process.stdout.write(`Код ${event.code}: ${event.status}${event.claimed ? " (забран)" : ""}` +
|
package/dist/bin/layero.js
CHANGED
|
@@ -453,9 +453,10 @@ async function main() {
|
|
|
453
453
|
.option("--branch <name>", "REFUSED (branch_unsupported, exit 4): archive uploads always land in the " +
|
|
454
454
|
"reserved `cli` environment, so this flag cannot give you a preview. " +
|
|
455
455
|
"Branch previews come from pushing to a connected repository.")
|
|
456
|
-
.option("--claim", "deploy without an account: a temporary
|
|
457
|
-
"
|
|
458
|
-
"
|
|
456
|
+
.option("--claim", "deploy without an account: a temporary site for 1 hour plus a claim_url for a human to take it over. " +
|
|
457
|
+
"Static sites and SPAs only — a server app (SSR, fullstack, container) is refused (claim_static_only, exit 4). " +
|
|
458
|
+
"The address is random and closed to search engines. " +
|
|
459
|
+
"Only with this flag: without a token and without --claim, deploy asks for a login. " +
|
|
459
460
|
"Refused together with --project (claim_with_project, exit 4).")
|
|
460
461
|
.option("--org <slug>", "Layero organization slug for first-time project creation. Defaults to personal; required when you're a member of multiple orgs and want a non-personal home.")
|
|
461
462
|
// Осознанное подтверждение выкатки, когда предыдущие сборки подряд падают
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import open from "open";
|
|
2
2
|
import { ApiClient, ApiError } from "../api.js";
|
|
3
|
-
import { loadConfig, saveConfig } from "../config.js";
|
|
4
|
-
import { loadProjectConfig, persistProjectLinking } from "../project-config.js";
|
|
3
|
+
import { forgetSandbox, loadConfig, saveClaim, saveConfig } from "../config.js";
|
|
4
|
+
import { loadProjectConfig, persistProjectLinking, unlinkProject } from "../project-config.js";
|
|
5
5
|
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
6
|
import { dashboardOrigin } from "../urls.js";
|
|
7
7
|
/**
|
|
@@ -9,7 +9,7 @@ import { dashboardOrigin } from "../urls.js";
|
|
|
9
9
|
*
|
|
10
10
|
* Сценарий: агент работает без аккаунта Layero — токена нет, браузера нет,
|
|
11
11
|
* человек не рядом. Платформа заводит временный проект и выдаёт токен на
|
|
12
|
-
* него; сайт живёт
|
|
12
|
+
* него; сайт живёт час; человек забирает его в свой аккаунт по ссылке
|
|
13
13
|
* `claim_url`. Принять заявку можно ТОЛЬКО в панели: CLI и агент не
|
|
14
14
|
* подтверждают её ни при каких флагах.
|
|
15
15
|
*/
|
|
@@ -32,9 +32,10 @@ export function claimCodeOf(created) {
|
|
|
32
32
|
throw new LayeroError("claimable_unavailable", "платформа не вернула код заявки", "повторите позже; если повторяется — сообщите на https://docs.layero.ru/contacts/");
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Создать claimable-проект и запомнить его:
|
|
36
|
-
* (по
|
|
37
|
-
* с токеном заявки, которым идёт
|
|
35
|
+
* Создать claimable-проект и запомнить его: токен, код и ссылку — в
|
|
36
|
+
* `~/.layero/config.json` (по проекту, файл 0600); в `.layero/project.json` —
|
|
37
|
+
* только привязка папки. Возвращает конфиг с токеном заявки, которым идёт
|
|
38
|
+
* деплой.
|
|
38
39
|
*/
|
|
39
40
|
export async function createClaimable(cfg, cwd, input) {
|
|
40
41
|
const api = new ApiClient(cfg);
|
|
@@ -53,23 +54,84 @@ export async function createClaimable(cfg, cwd, input) {
|
|
|
53
54
|
throw err;
|
|
54
55
|
}
|
|
55
56
|
const code = claimCodeOf(created);
|
|
57
|
+
const claim = { code, claim_url: created.claim_url, expires_at: created.expires_at };
|
|
56
58
|
const next = {
|
|
57
59
|
...cfg,
|
|
58
60
|
token: created.token,
|
|
59
61
|
claim_tokens: { ...(cfg.claim_tokens ?? {}), [created.project_id]: created.token },
|
|
62
|
+
claims: { ...(cfg.claims ?? {}), [created.project_id]: claim },
|
|
60
63
|
};
|
|
61
|
-
// В файл —
|
|
62
|
-
//
|
|
63
|
-
await saveConfig({ ...cfg, claim_tokens: next.claim_tokens });
|
|
64
|
+
// В файл — карты заявок, не сам токен как «вход»: иначе следующий
|
|
65
|
+
// `layero whoami` считал бы временный токен аккаунтом.
|
|
66
|
+
await saveConfig({ ...cfg, claim_tokens: next.claim_tokens, claims: next.claims });
|
|
67
|
+
// 🚨 Код забора в project.json НЕ пишется (T-20260921): файл уходит в git.
|
|
64
68
|
await persistProjectLinking(cwd, {
|
|
65
69
|
project_id: created.project_id,
|
|
66
70
|
slug: created.slug,
|
|
67
71
|
organization_slug: created.organization,
|
|
68
72
|
apex_hostname: created.url ? new URL(created.url).hostname : `${created.slug}.layero.app`,
|
|
69
|
-
claim: { code, claim_url: created.claim_url, expires_at: created.expires_at },
|
|
70
73
|
});
|
|
71
74
|
return { cfg: next, created, code };
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Жива ли песочница папки, прежде чем выкатывать в неё её токеном (T-20260919-3).
|
|
78
|
+
*
|
|
79
|
+
* Токен песочницы умирает в двух случаях: сайт забрали в аккаунт (токен
|
|
80
|
+
* отозван при заборе) или вышел срок — час, после чего уборка удаляет проект
|
|
81
|
+
* и держателя. Раньше агент узнавал об этом сырым 401 посреди выкатки и не
|
|
82
|
+
* знал, что делать дальше. Публичный статус по коду заявки отвечает до
|
|
83
|
+
* упаковки: `claimed` → `sandbox_claimed` (войти тем аккаунтом, привязка
|
|
84
|
+
* остаётся — проект тот же), `expired` или кода больше нет → `sandbox_expired`
|
|
85
|
+
* (мёртвые токен, заявка и привязка убираются, `--claim` заведёт новую).
|
|
86
|
+
* Сеть или платформа не ответили — не отказываем: решит сама выкатка.
|
|
87
|
+
*/
|
|
88
|
+
export async function assertSandboxAlive(cfg, cwd, linked) {
|
|
89
|
+
const claim = claimFor(cfg, linked);
|
|
90
|
+
if (!claim?.code || !linked?.project_id)
|
|
91
|
+
return;
|
|
92
|
+
let status;
|
|
93
|
+
try {
|
|
94
|
+
status = (await new ApiClient({ apiUrl: cfg.apiUrl }).getClaimStatus(claim.code)).status;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err instanceof ApiError && (err.status === 404 || err.status === 410))
|
|
98
|
+
status = "expired";
|
|
99
|
+
else
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const site = linked.slug ? ` (${linked.slug})` : "";
|
|
103
|
+
if (status === "claimed") {
|
|
104
|
+
await forgetSandbox(linked.project_id);
|
|
105
|
+
throw new LayeroError("sandbox_claimed", `сайт этой папки${site} забрали в аккаунт — токен песочницы больше не действует`, "the person took the site into their account: sign in with that account " +
|
|
106
|
+
"(`npx layero@latest login`) and run the same deploy — the folder stays linked to the project");
|
|
107
|
+
}
|
|
108
|
+
if (status === "expired") {
|
|
109
|
+
await forgetSandbox(linked.project_id);
|
|
110
|
+
await unlinkProject(cwd);
|
|
111
|
+
throw new LayeroError("sandbox_expired", `песочница этой папки${site} истекла и удалена`, "a new site without an account: `npx layero@latest deploy --claim` (static sites and SPAs, 1 hour); " +
|
|
112
|
+
"to keep a site, sign in and deploy into the account");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Код забора из `.layero/project.json` старого CLI (до 0.11.8) — в конфиг.
|
|
117
|
+
* Файл папки после этого перепишет `persistProjectLinking` уже без кода.
|
|
118
|
+
*/
|
|
119
|
+
export async function keepLegacyClaim(cfg, linked) {
|
|
120
|
+
if (!linked?.project_id || !linked.claim?.code)
|
|
121
|
+
return;
|
|
122
|
+
if (cfg.claims?.[linked.project_id])
|
|
123
|
+
return;
|
|
124
|
+
await saveClaim(linked.project_id, linked.claim);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Заявка песочницы для проекта папки: из `~/.layero/config.json`, а у папок,
|
|
128
|
+
* выложенных CLI до 0.11.8, — из старого поля `claim` в `.layero/project.json`.
|
|
129
|
+
*/
|
|
130
|
+
export function claimFor(cfg, linked) {
|
|
131
|
+
if (!linked?.project_id)
|
|
132
|
+
return undefined;
|
|
133
|
+
return cfg.claims?.[linked.project_id] ?? linked.claim ?? undefined;
|
|
134
|
+
}
|
|
73
135
|
/** Токен заявки для проекта из `.layero/project.json`, если он есть. */
|
|
74
136
|
export function claimTokenFor(cfg, projectId) {
|
|
75
137
|
if (!projectId)
|
|
@@ -100,19 +162,19 @@ export async function configForFolder(opts, cwd) {
|
|
|
100
162
|
function claimUrlFor(cfg, code) {
|
|
101
163
|
return `${dashboardOrigin(cfg.apiUrl)}/claim?code=${encodeURIComponent(code)}`;
|
|
102
164
|
}
|
|
103
|
-
async function resolveCode(cwd, explicit) {
|
|
165
|
+
async function resolveCode(cfg, cwd, explicit) {
|
|
104
166
|
if (explicit)
|
|
105
167
|
return { code: explicit.trim(), claim_url: null };
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
return { code:
|
|
168
|
+
const claim = claimFor(cfg, await loadProjectConfig(cwd));
|
|
169
|
+
if (claim?.code)
|
|
170
|
+
return { code: claim.code, claim_url: claim.claim_url };
|
|
109
171
|
throw new LayeroError("claim_unknown", "в этой папке нет claimable-проекта", "передайте код: `layero claim status <code>` — или создайте проект без аккаунта: `layero deploy --claim`");
|
|
110
172
|
}
|
|
111
173
|
/** `layero claim status [code]` — что с заявкой: жива, забрана, истекла. */
|
|
112
174
|
export async function claimStatusCmd(code, opts) {
|
|
113
175
|
const cwd = process.cwd();
|
|
114
176
|
const cfg = await loadConfig();
|
|
115
|
-
const ref = await resolveCode(cwd, code);
|
|
177
|
+
const ref = await resolveCode(cfg, cwd, code);
|
|
116
178
|
// Без токена намеренно: статус заявки — публичный по коду, как и сама ссылка.
|
|
117
179
|
const api = new ApiClient({ apiUrl: cfg.apiUrl });
|
|
118
180
|
let status;
|
|
@@ -145,7 +207,7 @@ export async function claimStatusCmd(code, opts) {
|
|
|
145
207
|
export async function claimAcceptCmd(code, opts) {
|
|
146
208
|
const cwd = process.cwd();
|
|
147
209
|
const cfg = await loadConfig();
|
|
148
|
-
const ref = await resolveCode(cwd, code);
|
|
210
|
+
const ref = await resolveCode(cfg, cwd, code);
|
|
149
211
|
const claimUrl = ref.claim_url ?? claimUrlFor(cfg, ref.code);
|
|
150
212
|
const mode = detectMode();
|
|
151
213
|
let opened = false;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { detectProject, detectedEvent } from "../detect.js";
|
|
|
13
13
|
import { runDeviceLogin } from "../auth.js";
|
|
14
14
|
import { LayeroError, detectMode, emit, isCiEnv } from "../agent.js";
|
|
15
15
|
import { ensureUsername } from "../username.js";
|
|
16
|
-
import { claimTokenFor, createClaimable } from "./claim.js";
|
|
16
|
+
import { assertSandboxAlive, claimFor, claimTokenFor, createClaimable, keepLegacyClaim } from "./claim.js";
|
|
17
17
|
const VALID_TYPES = new Set([
|
|
18
18
|
"vite",
|
|
19
19
|
"next",
|
|
@@ -193,7 +193,43 @@ function isLegacyInitGuess(cfg) {
|
|
|
193
193
|
cfg.build_cmd === "true" &&
|
|
194
194
|
cfg.output_dir === ".");
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
/**
|
|
197
|
+
* 🚨 ПЕСОЧНИЦА — ТОЛЬКО СТАТИКА И SPA (T-20260919-10, решение владельца
|
|
198
|
+
* 19.09.2026). Серверное приложение (SSR, fullstack, контейнер) без аккаунта
|
|
199
|
+
* не выкладывается: это анонимный процесс с выходом в интернет на общем узле.
|
|
200
|
+
* Проверка ДО того, как заведена песочница и уехал архив; платформа и сборщик
|
|
201
|
+
* откажут и сами, но уже потратив чужую минуту.
|
|
202
|
+
*/
|
|
203
|
+
async function assertSandboxServesFiles(cwd, opts, existing, detectFolder) {
|
|
204
|
+
if (opts.prebuilt)
|
|
205
|
+
return; // готовая сборка — это файлы по определению
|
|
206
|
+
const h = hintsFor(cwd, opts, existing);
|
|
207
|
+
const kind = h.asRuntime ?? (await detectFolder(h.detectCwd, h.hint, h.hintSource)).runtime_kind ?? null;
|
|
208
|
+
if (!kind)
|
|
209
|
+
return;
|
|
210
|
+
throw new LayeroError("claim_static_only", `деплой без аккаунта (--claim) выкладывает только статику и SPA, а эта папка — серверное приложение (${kind})`, "sign in and deploy from the account: `npx layero@latest login`, then run the same deploy without --claim");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 🚨 ДЕТЕКТ — ОДИН НА ВЫКАТКУ. Гейт core «входов анализа не становится
|
|
214
|
+
* больше» считает запуски детекта в этом файле: каждый лишний — ещё один
|
|
215
|
+
* снимок папки со своей полнотой, и два снимка рано или поздно разойдутся.
|
|
216
|
+
* Проверка песочницы (до заявки) и план сборки (после входа) берут один
|
|
217
|
+
* результат по одинаковым входам.
|
|
218
|
+
*/
|
|
219
|
+
function oneDetectionPerRun() {
|
|
220
|
+
const memo = new Map();
|
|
221
|
+
return (detectCwd, hint, hintSource) => {
|
|
222
|
+
const key = `${detectCwd}\u0000${hint ?? ""}\u0000${hintSource}`;
|
|
223
|
+
let run = memo.get(key);
|
|
224
|
+
if (!run) {
|
|
225
|
+
run = detectProject(detectCwd, { frameworkHint: hint, hintSource });
|
|
226
|
+
memo.set(key, run);
|
|
227
|
+
}
|
|
228
|
+
return run;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Входы детекта: папка (с `--root`) и подсказка фреймворка с её источником. */
|
|
232
|
+
function hintsFor(cwd, opts, existing, projectFramework) {
|
|
197
233
|
// Honour --root when auto-detecting: the framework signals live in the
|
|
198
234
|
// monorepo subdir, not the repo root.
|
|
199
235
|
const detectCwd = opts.root ? path.join(cwd, opts.root) : cwd;
|
|
@@ -206,10 +242,12 @@ async function resolveSetupConfig(cwd, opts, existing, projectFramework) {
|
|
|
206
242
|
const own = isLegacyInitGuess(existing) ? null : existing;
|
|
207
243
|
const fileHint = own?.framework_hint ?? null;
|
|
208
244
|
const hint = typeHint ?? fileHint ?? projectFramework ?? null;
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
245
|
+
const hintSource = typeHint ? "--type" : fileHint ? ".layero/project.json" : "project settings";
|
|
246
|
+
return { detectCwd, asRuntime, typeHint, own, fileHint, hint, hintSource };
|
|
247
|
+
}
|
|
248
|
+
async function resolveSetupConfig(cwd, opts, existing, projectFramework, detectFolder) {
|
|
249
|
+
const { detectCwd, asRuntime, typeHint, own, fileHint, hint, hintSource } = hintsFor(cwd, opts, existing, projectFramework);
|
|
250
|
+
const detected = await detectFolder(detectCwd, hint, hintSource);
|
|
213
251
|
emit(detectedEvent(detected));
|
|
214
252
|
return {
|
|
215
253
|
detected,
|
|
@@ -405,7 +443,7 @@ async function dryRun(cwd, opts, existing, cliCfg) {
|
|
|
405
443
|
});
|
|
406
444
|
return;
|
|
407
445
|
}
|
|
408
|
-
const setup = await resolveSetupConfig(cwd, { ...opts, root: root ?? undefined }, existing, project?.framework_hint ?? null);
|
|
446
|
+
const setup = await resolveSetupConfig(cwd, { ...opts, root: root ?? undefined }, existing, project?.framework_hint ?? null, oneDetectionPerRun());
|
|
409
447
|
const d = setup.detected;
|
|
410
448
|
let buildCmd = d.build_cmd;
|
|
411
449
|
let outputDir = d.output_dir;
|
|
@@ -485,6 +523,7 @@ export async function waitUntilServing(url, budgetMs = SERVE_WAIT_MS) {
|
|
|
485
523
|
}
|
|
486
524
|
export async function deployCmd(opts) {
|
|
487
525
|
const mode = detectMode();
|
|
526
|
+
const detectFolder = oneDetectionPerRun();
|
|
488
527
|
if (opts.type &&
|
|
489
528
|
!VALID_TYPES.has(opts.type.toLowerCase()) &&
|
|
490
529
|
!runtimeTypeOf(opts.type)) {
|
|
@@ -521,7 +560,7 @@ export async function deployCmd(opts) {
|
|
|
521
560
|
const projectIsLinkedOne = !opts.project ||
|
|
522
561
|
(existing?.project_id !== undefined &&
|
|
523
562
|
(opts.project === existing.project_id || opts.project === existing.slug));
|
|
524
|
-
const linkedIsClaimable = Boolean(existing?.project_id && existing.
|
|
563
|
+
const linkedIsClaimable = Boolean(existing?.project_id && (claimFor(cliCfg, existing) || claimTokenFor(cliCfg, existing.project_id)));
|
|
525
564
|
const boundToAccountProject = opts.project
|
|
526
565
|
? !(projectIsLinkedOne && linkedIsClaimable)
|
|
527
566
|
: Boolean(existing?.project_id) && !linkedIsClaimable;
|
|
@@ -533,21 +572,25 @@ export async function deployCmd(opts) {
|
|
|
533
572
|
? claimTokenFor(cliCfg, existing?.project_id)
|
|
534
573
|
: undefined;
|
|
535
574
|
if (reuse) {
|
|
575
|
+
await assertSandboxAlive(cliCfg, cwd, existing);
|
|
576
|
+
await assertSandboxServesFiles(cwd, opts, existing, detectFolder);
|
|
536
577
|
cliCfg = { ...cliCfg, token: reuse };
|
|
537
578
|
// Повторная выкатка песочницы: ссылка «забрать» нужна агенту и здесь —
|
|
538
579
|
// без неё он искал её в `.layero/project.json` (симуляция 18.09.2026).
|
|
539
|
-
|
|
540
|
-
reusedClaim = existing.claim;
|
|
580
|
+
reusedClaim = claimFor(cliCfg, existing) ?? null;
|
|
541
581
|
}
|
|
542
582
|
else if (
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
583
|
+
// 🚨 ТОЛЬКО ЯВНЫЙ `--claim` (T-20260921). До 0.11.8 режим включался сам —
|
|
584
|
+
// нет токена, агентская среда, `--yes` — и агент молча публиковал папку
|
|
585
|
+
// в открытый интернет: человек не решал публиковать и не принимал
|
|
586
|
+
// условий. Теперь без флага — обычный вход (`auth_required`). С
|
|
587
|
+
// `--project` флаг отклонён выше: песочница — только новый проект.
|
|
588
|
+
opts.claim) {
|
|
547
589
|
// 🚨 CI СЮДА НЕ ПОПАДАЕТ НАМЕРЕННО. Раннер без LAYERO_TOKEN — это
|
|
548
590
|
// забытый секрет, и правильный ответ ему — отказ, а не сайт на
|
|
549
|
-
// временном адресе, который через
|
|
550
|
-
//
|
|
591
|
+
// временном адресе, который через час исчезнет вместе с «зелёным»
|
|
592
|
+
// прогоном. Явный `--claim` в CI работает.
|
|
593
|
+
await assertSandboxServesFiles(cwd, opts, existing, detectFolder);
|
|
551
594
|
const r = await createClaimable(cliCfg, cwd, {
|
|
552
595
|
name: opts.name ?? path.basename(cwd),
|
|
553
596
|
});
|
|
@@ -614,7 +657,7 @@ export async function deployCmd(opts) {
|
|
|
614
657
|
// выкатку из исходников.
|
|
615
658
|
const setup = prebuiltDir
|
|
616
659
|
? { framework_hint: null, build_cmd: null, output_dir: null }
|
|
617
|
-
: await resolveSetupConfig(cwd, opts, existing);
|
|
660
|
+
: await resolveSetupConfig(cwd, opts, existing, undefined, detectFolder);
|
|
618
661
|
if (prebuiltDir) {
|
|
619
662
|
emit({ event: "prebuilt", dir: prebuiltDir });
|
|
620
663
|
}
|
|
@@ -776,6 +819,9 @@ export async function deployCmd(opts) {
|
|
|
776
819
|
}
|
|
777
820
|
// Только связка папки с проектом. Фреймворк сюда больше не пишется: поле
|
|
778
821
|
// этого файла CLI читает как выбор человека (см. `resolveSetupConfig`).
|
|
822
|
+
// Код забора старого CLI сначала уезжает в конфиг, потом файл
|
|
823
|
+
// переписывается уже без него (T-20260921).
|
|
824
|
+
await keepLegacyClaim(cliCfg, existing);
|
|
779
825
|
await persistProjectLinking(cwd, {
|
|
780
826
|
project_id: project.id,
|
|
781
827
|
slug: project.slug,
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function loadConfig() {
|
|
|
27
27
|
// выводе врала бы. Кто мы — узнаем у API.
|
|
28
28
|
user: ENV_TOKEN ? undefined : parsed.user,
|
|
29
29
|
claim_tokens: parsed.claim_tokens,
|
|
30
|
+
claims: parsed.claims,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
catch (err) {
|
|
@@ -47,6 +48,57 @@ export async function saveConfig(cfg) {
|
|
|
47
48
|
await fs.chmod(CONFIG_FILE, 0o600);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Записать заявку песочницы в файл конфига, не трогая остального.
|
|
53
|
+
*
|
|
54
|
+
* Читает и пишет САМ файл, а не результат `loadConfig`: тот подставляет
|
|
55
|
+
* `LAYERO_TOKEN` из окружения, и запись через него унесла бы токен CI в
|
|
56
|
+
* файл на диске.
|
|
57
|
+
*/
|
|
58
|
+
export async function saveClaim(projectId, claim) {
|
|
59
|
+
let raw = {};
|
|
60
|
+
try {
|
|
61
|
+
raw = JSON.parse(await fs.readFile(CONFIG_FILE, "utf-8"));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err?.code !== "ENOENT")
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const claims = { ...(raw.claims ?? {}), [projectId]: claim };
|
|
68
|
+
await ensureDir();
|
|
69
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify({ ...raw, claims }, null, 2), {
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
});
|
|
73
|
+
if (process.platform !== "win32")
|
|
74
|
+
await fs.chmod(CONFIG_FILE, 0o600);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Забыть песочницу: её токен и заявку (T-20260919-3). Зовётся, когда
|
|
78
|
+
* песочницы больше нет или её забрали, — мёртвый токен иначе подхватывала бы
|
|
79
|
+
* каждая следующая выкатка из этой папки. Пишет сам файл (см. `saveClaim`).
|
|
80
|
+
*/
|
|
81
|
+
export async function forgetSandbox(projectId) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = JSON.parse(await fs.readFile(CONFIG_FILE, "utf-8"));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err?.code === "ENOENT")
|
|
88
|
+
return;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const tokens = { ...(raw.claim_tokens ?? {}) };
|
|
92
|
+
const claims = { ...(raw.claims ?? {}) };
|
|
93
|
+
delete tokens[projectId];
|
|
94
|
+
delete claims[projectId];
|
|
95
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify({ ...raw, claim_tokens: tokens, claims }, null, 2), {
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
mode: 0o600,
|
|
98
|
+
});
|
|
99
|
+
if (process.platform !== "win32")
|
|
100
|
+
await fs.chmod(CONFIG_FILE, 0o600);
|
|
101
|
+
}
|
|
50
102
|
export async function clearConfig() {
|
|
51
103
|
try {
|
|
52
104
|
await fs.unlink(CONFIG_FILE);
|
package/dist/exit-codes.js
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* `check-error-codes.py`), и класс выхода становится его частью. Новый код,
|
|
20
20
|
* не попавший ни в один список, даёт 1 — честно «прочее», а не наугад.
|
|
21
21
|
*/
|
|
22
|
-
const AUTH = new Set(["auth_required", "auth_expired", "auth_timeout"]);
|
|
22
|
+
const AUTH = new Set(["auth_required", "auth_expired", "auth_timeout", "sandbox_claimed"]);
|
|
23
23
|
const NOT_FOUND = new Set([
|
|
24
|
+
"sandbox_expired",
|
|
24
25
|
"project_unknown",
|
|
25
26
|
"project_not_found",
|
|
26
27
|
"org_unknown",
|
|
@@ -49,6 +50,7 @@ const INVALID_INPUT = new Set([
|
|
|
49
50
|
"sql_missing",
|
|
50
51
|
"branch_unsupported",
|
|
51
52
|
"claim_with_project",
|
|
53
|
+
"claim_static_only",
|
|
52
54
|
"provider_unknown",
|
|
53
55
|
"repo_format",
|
|
54
56
|
"token_missing",
|
package/dist/project-config.js
CHANGED
|
@@ -67,8 +67,9 @@ export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
|
67
67
|
};
|
|
68
68
|
if (linking.api_url !== undefined)
|
|
69
69
|
merged.api_url = linking.api_url;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Код забора от CLI до 0.11.8 — вон из файла, который коммитят
|
|
71
|
+
// (T-20260921). Живую заявку вызывающий перед этим переносит в конфиг.
|
|
72
|
+
delete merged.claim;
|
|
72
73
|
// Only seed framework_hint if the user hasn't set one yet — once it's in
|
|
73
74
|
// the file (manually or from a prior --type), leave it alone.
|
|
74
75
|
if (fallbackHint != null &&
|
|
@@ -79,3 +80,30 @@ export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
|
79
80
|
await fs.writeFile(file, JSON.stringify(merged, null, 2), "utf-8");
|
|
80
81
|
return merged;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Отвязать папку от проекта, сохранив поля, которые человек вписал сам.
|
|
85
|
+
*
|
|
86
|
+
* Зовётся, когда проекта больше нет — песочница истекла и удалена
|
|
87
|
+
* (T-20260919-3): мёртвая привязка иначе уводила бы каждую следующую выкатку
|
|
88
|
+
* в несуществующий проект. Пустой после этого файл удаляется.
|
|
89
|
+
*/
|
|
90
|
+
export async function unlinkProject(cwd) {
|
|
91
|
+
const file = projectConfigPath(cwd);
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.parse(await fs.readFile(file, "utf-8"));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err?.code === "ENOENT")
|
|
98
|
+
return;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
for (const key of ["project_id", "slug", "organization_slug", "owner_slug", "apex_hostname", "api_url", "claim"]) {
|
|
102
|
+
delete raw[key];
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(raw).length === 0) {
|
|
105
|
+
await fs.unlink(file);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await fs.writeFile(file, JSON.stringify(raw, null, 2), "utf-8");
|
|
109
|
+
}
|
package/package.json
CHANGED