layero 0.8.24 → 0.8.25
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/dist/api.js +19 -2
- package/dist/bin/layero.js +19 -1
- package/dist/commands/data.js +97 -0
- package/dist/commands/deploy.js +91 -4
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -89,8 +89,15 @@ export class ApiClient {
|
|
|
89
89
|
completeSetup(projectId, input) {
|
|
90
90
|
return this.request("POST", `/projects/${projectId}/setup`, input);
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Сменить тип проекта.
|
|
94
|
+
*
|
|
95
|
+
* `force` — записать вопреки возражению платформы (409). Владелец имеет на
|
|
96
|
+
* это право: детект ошибается, и репозиторий, который эвристика считает «не
|
|
97
|
+
* бэкендом», прекрасно работает одним сервисом.
|
|
98
|
+
*/
|
|
99
|
+
setRuntimeType(projectId, projectType, force = false) {
|
|
100
|
+
return this.request("POST", `/projects/${projectId}/runtime-type`, { project_type: projectType, force });
|
|
94
101
|
}
|
|
95
102
|
updateProject(projectId, input) {
|
|
96
103
|
return this.request("PATCH", `/projects/${projectId}`, input);
|
|
@@ -126,6 +133,16 @@ export class ApiClient {
|
|
|
126
133
|
listEnvVars(projectId) {
|
|
127
134
|
return this.request("GET", `/projects/${projectId}/env`);
|
|
128
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Адрес и ПУБЛИЧНЫЙ ключ Data API — те же, что платформа кладёт в сборку.
|
|
138
|
+
*
|
|
139
|
+
* Единственное место, где значение приезжает открытым, и это не исключение
|
|
140
|
+
* из правила выше, а другая природа: публичный ключ уезжает в бандл и виден
|
|
141
|
+
* любому посетителю сайта. Секретного здесь не бывает.
|
|
142
|
+
*/
|
|
143
|
+
dataEnv(projectId) {
|
|
144
|
+
return this.request("GET", `/projects/${projectId}/data-env`);
|
|
145
|
+
}
|
|
129
146
|
/**
|
|
130
147
|
* Значение `null` = «оставить как есть». Благодаря этому добавить одну
|
|
131
148
|
* переменную можно, не читая остальные, — то есть не имея доступа к
|
package/dist/bin/layero.js
CHANGED
|
@@ -15,6 +15,7 @@ import { orgsListCmd } from "../commands/orgs.js";
|
|
|
15
15
|
import { initCmd } from "../commands/init.js";
|
|
16
16
|
import { diagnoseCmd, logsCmd } from "../commands/diagnose.js";
|
|
17
17
|
import { perfCheckCmd, perfShowCmd } from "../commands/perf.js";
|
|
18
|
+
import { dataEnvCmd } from "../commands/data.js";
|
|
18
19
|
import { envListCmd, envSetCmd, envUnsetCmd } from "../commands/env.js";
|
|
19
20
|
import { analyticsConnectCmd, analyticsDisconnectCmd, analyticsStatsCmd, analyticsStatusCmd, } from "../commands/analytics.js";
|
|
20
21
|
import { domainsAddCmd, domainsListCmd, domainsPrimaryCmd, domainsRemoveCmd, domainsVerifyCmd, } from "../commands/domains.js";
|
|
@@ -193,6 +194,21 @@ async function main() {
|
|
|
193
194
|
.command("unset <keys...>")
|
|
194
195
|
.description("Удалить переменные.")
|
|
195
196
|
.option("-y, --yes", "не спрашивать подтверждение")).action(async (keys, opts) => envUnsetCmd(keys, { ...opts, json: program.opts().json }));
|
|
197
|
+
const data = program
|
|
198
|
+
.command("data")
|
|
199
|
+
.description("Data API: адрес и публичный ключ базы для фронтенда.");
|
|
200
|
+
data
|
|
201
|
+
.command("env")
|
|
202
|
+
.description("Показать VITE_/NEXT_PUBLIC_ переменные Data API; --write кладёт их в .env.local.")
|
|
203
|
+
.option("--project <id_or_slug>", "проект (по умолчанию — залинкованный)")
|
|
204
|
+
.option("-w, --write", "записать в файл, а не печатать")
|
|
205
|
+
.option("--file <path>", "имя файла (по умолчанию .env.local)")
|
|
206
|
+
.addHelpText("after", "\nПримеры:\n" +
|
|
207
|
+
" $ layero data env # посмотреть\n" +
|
|
208
|
+
" $ layero data env --write # положить в .env.local\n" +
|
|
209
|
+
"\nОтдаётся только ПУБЛИЧНЫЙ ключ — тот, что и так уезжает в бандл.\n" +
|
|
210
|
+
"Секретный ключ платформа не хранит и не отдаёт: он для сервера.")
|
|
211
|
+
.action(async (opts) => dataEnvCmd({ ...opts, json: program.opts().json }));
|
|
196
212
|
const analytics = program
|
|
197
213
|
.command("analytics")
|
|
198
214
|
.description("Яндекс.Метрика: подключение и статистика сайта.");
|
|
@@ -278,7 +294,8 @@ async function main() {
|
|
|
278
294
|
program
|
|
279
295
|
.command("deploy")
|
|
280
296
|
.description("Pack the current directory and deploy it. Framework, build command and output directory are auto-detected.")
|
|
281
|
-
.option("-t, --type <preset>", "
|
|
297
|
+
.option("-t, --type <preset>", "type override — static preset (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | storybook | eleventy | hugo | static) " +
|
|
298
|
+
"or runtime kind for apps the platform RUNS (node_web | python_web | flask | streamlit | gradio | ssr_next; aliases: express, fastapi, django, node, python)")
|
|
282
299
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
283
300
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
284
301
|
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
@@ -304,6 +321,7 @@ async function main() {
|
|
|
304
321
|
" $ layero deploy --promote # preview deploy + pin apex (one-shot publish)\n" +
|
|
305
322
|
" $ layero deploy --branch=staging # preview on a specific branch\n" +
|
|
306
323
|
" $ layero deploy --type vite # force a framework preset\n" +
|
|
324
|
+
" $ layero deploy --type express # Node backend: platform RUNS it, not serves files\n" +
|
|
307
325
|
" $ layero deploy --json # machine-readable output for agents")
|
|
308
326
|
.action(async (opts) => {
|
|
309
327
|
await deployCmd(opts);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { ApiClient } from "../api.js";
|
|
5
|
+
import { loadConfig } from "../config.js";
|
|
6
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
7
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
8
|
+
const DEFAULT_FILE = ".env.local";
|
|
9
|
+
async function projectRef(api, opts) {
|
|
10
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
11
|
+
const ref = opts.project ?? linked?.project_id;
|
|
12
|
+
if (!ref) {
|
|
13
|
+
throw new LayeroError("project_unknown", "не понятно, у какого проекта брать переменные Data API", "запусти из каталога проекта или передай --project <id|slug>");
|
|
14
|
+
}
|
|
15
|
+
const p = await api.getProject(ref);
|
|
16
|
+
return { id: p.id, slug: p.slug };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Дописывает переменные в файл, не трогая чужие строки.
|
|
20
|
+
*
|
|
21
|
+
* Перезапись файла целиком была бы проще и однажды стёрла бы чужую строку,
|
|
22
|
+
* которую человек добавил руками. Свои ключи узнаём по имени и обновляем на
|
|
23
|
+
* месте — остальное остаётся как было.
|
|
24
|
+
*/
|
|
25
|
+
function mergeEnvFile(previous, vars) {
|
|
26
|
+
const lines = previous ? previous.split("\n") : [];
|
|
27
|
+
const remaining = new Map(Object.entries(vars));
|
|
28
|
+
const out = lines.map((line) => {
|
|
29
|
+
const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
30
|
+
const name = match?.[1];
|
|
31
|
+
if (!name)
|
|
32
|
+
return line;
|
|
33
|
+
const value = remaining.get(name);
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return line;
|
|
36
|
+
remaining.delete(name);
|
|
37
|
+
return `${name}=${value}`;
|
|
38
|
+
});
|
|
39
|
+
if (remaining.size) {
|
|
40
|
+
if (out.length && (out[out.length - 1] ?? "").trim() !== "")
|
|
41
|
+
out.push("");
|
|
42
|
+
out.push("# Layero Data API — адрес и публичный ключ (layero data env)");
|
|
43
|
+
for (const [key, value] of remaining)
|
|
44
|
+
out.push(`${key}=${value}`);
|
|
45
|
+
out.push("");
|
|
46
|
+
}
|
|
47
|
+
return out.join("\n");
|
|
48
|
+
}
|
|
49
|
+
/** Прикрыт ли файл от коммита. Проверяем буквально то имя, которое пишем. */
|
|
50
|
+
function ignored(cwd, file) {
|
|
51
|
+
const gitignore = join(cwd, ".gitignore");
|
|
52
|
+
if (!existsSync(gitignore))
|
|
53
|
+
return false;
|
|
54
|
+
const patterns = readFileSync(gitignore, "utf8")
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((l) => l.trim())
|
|
57
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
58
|
+
return patterns.some((p) => p === file || p === `/${file}` || p === "*.local" || p === ".env*" || p === ".env.*");
|
|
59
|
+
}
|
|
60
|
+
export async function dataEnvCmd(opts) {
|
|
61
|
+
const mode = detectMode();
|
|
62
|
+
const api = new ApiClient(await loadConfig());
|
|
63
|
+
const project = await projectRef(api, opts);
|
|
64
|
+
const vars = await api.dataEnv(project.id);
|
|
65
|
+
if (!Object.keys(vars).length) {
|
|
66
|
+
if (mode.json) {
|
|
67
|
+
emit({ event: "data_env", project: project.slug, vars: {} });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(chalk.dim(`у проекта "${project.slug}" нет базы с включённым Data API\n` +
|
|
71
|
+
"включить: панель → база → «API» → «Включить API»"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (mode.json) {
|
|
75
|
+
emit({ event: "data_env", project: project.slug, vars });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const file = opts.file ?? DEFAULT_FILE;
|
|
79
|
+
if (!opts.write) {
|
|
80
|
+
// По умолчанию печатаем, а не пишем: команда, которая молча правит файлы в
|
|
81
|
+
// каталоге, — неприятный сюрприз, особенно у агента.
|
|
82
|
+
for (const [key, value] of Object.entries(vars))
|
|
83
|
+
console.log(`${key}=${value}`);
|
|
84
|
+
console.log(chalk.dim(`\n записать в ${file}: layero data env --write`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const path = join(process.cwd(), file);
|
|
88
|
+
const previous = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
89
|
+
writeFileSync(path, mergeEnvFile(previous, vars), "utf8");
|
|
90
|
+
console.log(`${chalk.green("✓")} ${file}: ${Object.keys(vars).length} переменных`);
|
|
91
|
+
if (!ignored(process.cwd(), file)) {
|
|
92
|
+
console.log(chalk.yellow(`\n⚠ ${file} не закрыт .gitignore.`) +
|
|
93
|
+
chalk.dim(`\n Публичный ключ не секрет — он и так уезжает в бандл. Но файл с таким` +
|
|
94
|
+
`\n именем однажды примет настоящий секрет, и тогда будет поздно.` +
|
|
95
|
+
`\n Добавьте строку: echo "${file}" >> .gitignore`));
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -31,6 +31,45 @@ const VALID_TYPES = new Set([
|
|
|
31
31
|
"vitepress",
|
|
32
32
|
"storybook",
|
|
33
33
|
]);
|
|
34
|
+
/**
|
|
35
|
+
* Runtime kinds — приложения, которые платформа ЗАПУСКАЕТ, а не раздаёт
|
|
36
|
+
* файлами.
|
|
37
|
+
*
|
|
38
|
+
* 🚨 Их здесь не было вовсе, и это стоило переноса. `--type` принимал только
|
|
39
|
+
* статические пресеты, а Express-репозиторий детект уверенно опознавал как
|
|
40
|
+
* `vite` — из-за devDependency `vite`, которую тянет `vitest`. Первый деплой
|
|
41
|
+
* умирал на «собранный сайт не содержит index.html», и единственным выходом
|
|
42
|
+
* оставался curl в недокументированную ручку `/projects/<id>/runtime-type`.
|
|
43
|
+
* Найдено переносом настоящего приложения 16.08.2026.
|
|
44
|
+
*
|
|
45
|
+
* Слева — то, что человек напишет, справа — то, что понимает платформа.
|
|
46
|
+
* Синонимы не украшение: `--type express` пишут чаще, чем `--type node_web`,
|
|
47
|
+
* и отказ на нём отправляет читать исходники, а не деплоить.
|
|
48
|
+
*/
|
|
49
|
+
const RUNTIME_TYPES = {
|
|
50
|
+
node_web: "node_web",
|
|
51
|
+
node: "node_web",
|
|
52
|
+
express: "node_web",
|
|
53
|
+
fastify: "node_web",
|
|
54
|
+
nest: "node_web",
|
|
55
|
+
nestjs: "node_web",
|
|
56
|
+
koa: "node_web",
|
|
57
|
+
python_web: "python_web",
|
|
58
|
+
python: "python_web",
|
|
59
|
+
fastapi: "python_web",
|
|
60
|
+
django: "python_web",
|
|
61
|
+
flask: "flask",
|
|
62
|
+
streamlit: "streamlit",
|
|
63
|
+
gradio: "gradio",
|
|
64
|
+
ssr_next: "ssr_next",
|
|
65
|
+
"next-ssr": "ssr_next",
|
|
66
|
+
};
|
|
67
|
+
/** Каноничный runtime-kind по тому, что написал человек, либо `null`. */
|
|
68
|
+
export function runtimeTypeOf(raw) {
|
|
69
|
+
if (!raw)
|
|
70
|
+
return null;
|
|
71
|
+
return RUNTIME_TYPES[raw.trim().toLowerCase()] ?? null;
|
|
72
|
+
}
|
|
34
73
|
function dashboardOrigin(apiUrl) {
|
|
35
74
|
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
36
75
|
if (override)
|
|
@@ -177,7 +216,14 @@ async function resolveSetupConfig(cwd, opts, existing) {
|
|
|
177
216
|
...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
|
|
178
217
|
...(detected.ssr_warning ? { ssr_warning: detected.ssr_warning } : {}),
|
|
179
218
|
});
|
|
180
|
-
|
|
219
|
+
// ⚠️ Runtime-тип в `framework_hint` не уходит: это разные вопросы. Хинт
|
|
220
|
+
// отвечает «чем собирать» (vite, next, …), а runtime-kind — «запускать или
|
|
221
|
+
// раздавать файлами». Положи мы сюда `node_web`, сборщик получил бы имя
|
|
222
|
+
// фреймворка, которого не существует.
|
|
223
|
+
const asRuntime = runtimeTypeOf(opts.type);
|
|
224
|
+
const framework_hint = (asRuntime ? undefined : opts.type) ??
|
|
225
|
+
existing?.framework_hint ??
|
|
226
|
+
detected.framework_hint;
|
|
181
227
|
const build_cmd = existing?.build_cmd ?? detected.build_cmd;
|
|
182
228
|
const output_dir = existing?.output_dir ?? detected.output_dir;
|
|
183
229
|
let source;
|
|
@@ -195,7 +241,13 @@ async function resolveSetupConfig(cwd, opts, existing) {
|
|
|
195
241
|
build_cmd,
|
|
196
242
|
output_dir,
|
|
197
243
|
source,
|
|
198
|
-
|
|
244
|
+
// Явный `--type` сильнее детекта: человек уже видел, как детект ошибся,
|
|
245
|
+
// иначе бы не писал флаг.
|
|
246
|
+
...(asRuntime
|
|
247
|
+
? { runtime_kind: asRuntime }
|
|
248
|
+
: detected.runtime_kind
|
|
249
|
+
? { runtime_kind: detected.runtime_kind }
|
|
250
|
+
: {}),
|
|
199
251
|
};
|
|
200
252
|
}
|
|
201
253
|
/**
|
|
@@ -313,8 +365,15 @@ async function startWithRepeatedFailureGuard(api, sessionId, commitSha, opts) {
|
|
|
313
365
|
}
|
|
314
366
|
export async function deployCmd(opts) {
|
|
315
367
|
const mode = detectMode();
|
|
316
|
-
if (opts.type &&
|
|
317
|
-
|
|
368
|
+
if (opts.type &&
|
|
369
|
+
!VALID_TYPES.has(opts.type.toLowerCase()) &&
|
|
370
|
+
!runtimeTypeOf(opts.type)) {
|
|
371
|
+
throw new LayeroError("invalid_type", `unknown --type "${opts.type}"`,
|
|
372
|
+
// Две группы, а не один список: у них разная судьба. Статический пресет
|
|
373
|
+
// говорит, ЧЕМ собирать; runtime-тип — что приложение надо ЗАПУСКАТЬ.
|
|
374
|
+
`static presets: ${[...VALID_TYPES].join(", ")}\n` +
|
|
375
|
+
`runtime kinds: ${[...new Set(Object.values(RUNTIME_TYPES))].join(", ")} ` +
|
|
376
|
+
`(aliases: ${Object.keys(RUNTIME_TYPES).join(", ")})`);
|
|
318
377
|
}
|
|
319
378
|
let cliCfg = await loadConfig();
|
|
320
379
|
if (!cliCfg.token) {
|
|
@@ -453,6 +512,34 @@ export async function deployCmd(opts) {
|
|
|
453
512
|
slug: project.slug,
|
|
454
513
|
});
|
|
455
514
|
emit({ event: "setup_applied" });
|
|
515
|
+
// 🚨 На ПЕРВОЙ настройке хватает `runtime_kind` в сессии, а у уже
|
|
516
|
+
// существующего проекта побеждает его `project_type` — и `--type node_web`
|
|
517
|
+
// молча не делал ничего. Ровно здесь человек и упирался: тип приходилось
|
|
518
|
+
// менять curl'ом в `/projects/<id>/runtime-type`.
|
|
519
|
+
//
|
|
520
|
+
// Момент выбран не случайно: сессия создана (значит, id проекта известен),
|
|
521
|
+
// но архив ещё не упакован и сборка не запущена — флип успевает повлиять на
|
|
522
|
+
// ту же выкатку.
|
|
523
|
+
const wantRuntime = runtimeTypeOf(opts.type);
|
|
524
|
+
if (wantRuntime && project.project_type !== wantRuntime) {
|
|
525
|
+
try {
|
|
526
|
+
await api.setRuntimeType(project.id, wantRuntime);
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
// 409 — платформа возражает: репозиторий не похож на этот тип. Возражение
|
|
530
|
+
// обязано быть заметным, но не запирающим: человек написал флаг явно, и
|
|
531
|
+
// спорить с ним мы перестали намеренно (та же логика, что у панели).
|
|
532
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
533
|
+
process.stderr.write(chalk.yellow(`! platform disagrees with --type ${wantRuntime}: ${err.body.slice(0, 200)}\n` +
|
|
534
|
+
` applying anyway because you asked explicitly\n`));
|
|
535
|
+
await api.setRuntimeType(project.id, wantRuntime, true);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
emit({ event: "runtime_type_applied", project_type: wantRuntime });
|
|
542
|
+
}
|
|
456
543
|
await persistProjectLinking(cwd, {
|
|
457
544
|
project_id: project.id,
|
|
458
545
|
slug: project.slug,
|