layero 0.8.13 → 0.8.14
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/_detection/detect_core.js +393 -0
- package/dist/_detection/spec.js +664 -0
- package/dist/api.js +93 -0
- package/dist/auth.js +1 -1
- package/dist/bin/layero.js +92 -3
- package/dist/commands/analytics.js +167 -0
- package/dist/commands/deploy.js +154 -233
- package/dist/commands/diagnose.js +121 -0
- package/dist/commands/domains.js +220 -0
- package/dist/commands/env.js +113 -0
- package/dist/commands/perf.js +99 -0
- package/dist/commands/token.js +1 -1
- package/dist/generated/api-types.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiClient, ApiError } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
5
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
|
+
async function resolveProjectId(api, opts, cwd) {
|
|
7
|
+
const linked = await loadProjectConfig(cwd);
|
|
8
|
+
const ref = opts.project ?? linked?.project_id;
|
|
9
|
+
if (!ref) {
|
|
10
|
+
throw new LayeroError("project_unknown", "не понятно, к какому проекту привязывать домен", "запусти из каталога проекта или передай --project <id|slug>");
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const p = await api.getProject(ref);
|
|
14
|
+
return { id: p.id, slug: p.slug };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
18
|
+
throw new LayeroError("project_not_found", `нет проекта с id/slug "${ref}"`, "посмотри список: layero projects list");
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function stateOf(d) {
|
|
24
|
+
if (d.verified && d.ssl_status === "active")
|
|
25
|
+
return chalk.green("работает");
|
|
26
|
+
if (d.ssl_status === "dns_pending")
|
|
27
|
+
return chalk.yellow("ждёт DNS");
|
|
28
|
+
if (d.ssl_status === "issuing")
|
|
29
|
+
return chalk.yellow("выпускается сертификат");
|
|
30
|
+
if (d.ssl_status === "failed")
|
|
31
|
+
return chalk.red("ошибка");
|
|
32
|
+
return d.ssl_status;
|
|
33
|
+
}
|
|
34
|
+
export async function domainsListCmd(opts) {
|
|
35
|
+
const mode = detectMode();
|
|
36
|
+
const api = new ApiClient(await loadConfig());
|
|
37
|
+
const project = await resolveProjectId(api, opts, process.cwd());
|
|
38
|
+
const rows = await api.listDomains(project.id);
|
|
39
|
+
if (mode.json) {
|
|
40
|
+
emit({ event: "domains", project: project.slug, domains: rows });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!rows.length) {
|
|
44
|
+
console.log(chalk.dim(`у проекта "${project.slug}" нет своих доменов`));
|
|
45
|
+
console.log(chalk.dim("добавить: layero domains add example.com"));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const d of rows) {
|
|
49
|
+
const primary = d.is_primary ? chalk.cyan(" [основной]") : "";
|
|
50
|
+
console.log(`${d.domain}${primary} ${stateOf(d)}`);
|
|
51
|
+
if (d.last_verification_error) {
|
|
52
|
+
console.log(chalk.dim(` ${d.last_verification_error}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function domainsAddCmd(domain, opts) {
|
|
57
|
+
const mode = detectMode();
|
|
58
|
+
const api = new ApiClient(await loadConfig());
|
|
59
|
+
const project = await resolveProjectId(api, opts, process.cwd());
|
|
60
|
+
let created;
|
|
61
|
+
try {
|
|
62
|
+
created = await api.addDomain(project.id, domain);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Отказы здесь — почти всегда про сам домен, и сырое «API POST … → 400»
|
|
66
|
+
// не говорит пользователю ничего. Разворачиваем в объяснение.
|
|
67
|
+
if (err instanceof ApiError && (err.status === 400 || err.status === 409 || err.status === 422)) {
|
|
68
|
+
let detail = err.body;
|
|
69
|
+
try {
|
|
70
|
+
detail = JSON.parse(err.body)?.detail ?? err.body;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* тело не JSON — покажем как есть */
|
|
74
|
+
}
|
|
75
|
+
throw new LayeroError("domain_rejected", `домен не принят: ${String(detail).slice(0, 300)}`, err.status === 409
|
|
76
|
+
? "домен уже привязан — к этому или другому проекту"
|
|
77
|
+
: "проверь написание; платформенные адреса (*.layero.ru, *.layero.app) привязать нельзя — они и так твои");
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
const instr = await api.getDomainInstructions(project.id, created.id);
|
|
82
|
+
if (mode.json) {
|
|
83
|
+
emit({
|
|
84
|
+
event: "domain_added",
|
|
85
|
+
domain: created.domain,
|
|
86
|
+
domain_id: created.id,
|
|
87
|
+
ssl_status: created.ssl_status,
|
|
88
|
+
records: instr.records,
|
|
89
|
+
// Ждать готовности здесь бессмысленно: DNS распространяется минутами,
|
|
90
|
+
// а платформа сама перепроверяет фоном.
|
|
91
|
+
next_action: "ask_user_to_add_dns_then_verify",
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk.green(`домен ${created.domain} добавлен к проекту "${project.slug}"`));
|
|
96
|
+
console.log(`\n${chalk.bold("впиши эти записи у своего регистратора:")}\n`);
|
|
97
|
+
for (const r of instr.records) {
|
|
98
|
+
const opt = r.optional ? chalk.dim(" (необязательно)") : "";
|
|
99
|
+
console.log(` ${chalk.bold(r.type)} ${r.name}${opt}`);
|
|
100
|
+
for (const v of r.values)
|
|
101
|
+
console.log(` → ${v}`);
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.dim("\nDNS расходится от нескольких минут до часа. Проверить: layero domains verify " +
|
|
104
|
+
created.domain +
|
|
105
|
+
"\nМожно и не проверять вручную — платформа перепроверяет сама."));
|
|
106
|
+
}
|
|
107
|
+
export async function domainsVerifyCmd(domain, opts) {
|
|
108
|
+
const mode = detectMode();
|
|
109
|
+
const api = new ApiClient(await loadConfig());
|
|
110
|
+
const project = await resolveProjectId(api, opts, process.cwd());
|
|
111
|
+
const rows = await api.listDomains(project.id);
|
|
112
|
+
const found = rows.find((d) => d.domain === domain.toLowerCase().replace(/^https?:\/\//, ""));
|
|
113
|
+
if (!found) {
|
|
114
|
+
throw new LayeroError("domain_not_found", `домен "${domain}" не привязан к проекту "${project.slug}"`, "список: layero domains list");
|
|
115
|
+
}
|
|
116
|
+
// Неразошедшийся DNS — ОЖИДАЕМОЕ промежуточное состояние, а не сбой:
|
|
117
|
+
// между привязкой и работающим доменом стоит человек, правящий записи у
|
|
118
|
+
// регистратора. API отвечает на это 400 (для него «подтвердить не
|
|
119
|
+
// удалось» — отказ), но показывать пользователю ошибку там, где всё идёт
|
|
120
|
+
// по плану, — значит пугать его на ровном месте. Разворачиваем в отчёт
|
|
121
|
+
// о состоянии.
|
|
122
|
+
let after;
|
|
123
|
+
try {
|
|
124
|
+
after = await api.verifyDomain(project.id, found.id);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
128
|
+
const fresh = (await api.listDomains(project.id)).find((d) => d.id === found.id);
|
|
129
|
+
after = fresh ?? found;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const instr = await api.getDomainInstructions(project.id, found.id);
|
|
136
|
+
if (mode.json) {
|
|
137
|
+
emit({
|
|
138
|
+
event: "domain_verified",
|
|
139
|
+
domain: after.domain,
|
|
140
|
+
verified: after.verified,
|
|
141
|
+
ssl_status: after.ssl_status,
|
|
142
|
+
checks: instr.checks,
|
|
143
|
+
next_check_at: after.next_check_at ?? undefined,
|
|
144
|
+
error: after.last_verification_error ?? undefined,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (after.verified && after.ssl_status === "active") {
|
|
149
|
+
console.log(chalk.green(`${after.domain} — работает`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log(`${after.domain} — ${stateOf(after)}`);
|
|
153
|
+
const c = instr.checks;
|
|
154
|
+
const mark = (v) => v === true ? chalk.green("✓") : v === false ? chalk.red("✗") : chalk.dim("—");
|
|
155
|
+
console.log(` A ${mark(c.a_ok)} CNAME ${mark(c.cname_ok)} TXT ${mark(c.txt_ok)} владение ${mark(c.ownership_ok)}`);
|
|
156
|
+
if (after.last_verification_error) {
|
|
157
|
+
console.log(chalk.dim(` ${after.last_verification_error}`));
|
|
158
|
+
}
|
|
159
|
+
if (after.next_check_at) {
|
|
160
|
+
console.log(chalk.dim(` платформа перепроверит сама: ${after.next_check_at}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function domainsPrimaryCmd(domain, opts) {
|
|
164
|
+
const api = new ApiClient(await loadConfig());
|
|
165
|
+
const project = await resolveProjectId(api, opts, process.cwd());
|
|
166
|
+
const rows = await api.listDomains(project.id);
|
|
167
|
+
const found = rows.find((d) => d.domain === domain.toLowerCase());
|
|
168
|
+
if (!found) {
|
|
169
|
+
throw new LayeroError("domain_not_found", `домен "${domain}" не привязан`, "layero domains list");
|
|
170
|
+
}
|
|
171
|
+
const out = await api.makeDomainPrimary(project.id, found.id);
|
|
172
|
+
if (detectMode().json) {
|
|
173
|
+
emit({ event: "domain_primary", domain: out.domain });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(chalk.green(`${out.domain} — теперь основной домен проекта`));
|
|
177
|
+
console.log(chalk.dim("платформенный адрес будет 301-редиректить на него"));
|
|
178
|
+
}
|
|
179
|
+
export async function domainsRemoveCmd(domain, opts) {
|
|
180
|
+
const mode = detectMode();
|
|
181
|
+
const api = new ApiClient(await loadConfig());
|
|
182
|
+
const project = await resolveProjectId(api, opts, process.cwd());
|
|
183
|
+
const rows = await api.listDomains(project.id);
|
|
184
|
+
const found = rows.find((d) => d.domain === domain.toLowerCase());
|
|
185
|
+
if (!found) {
|
|
186
|
+
throw new LayeroError("domain_not_found", `домен "${domain}" не привязан`, "layero domains list");
|
|
187
|
+
}
|
|
188
|
+
// Снятие домена рвёт живой трафик и требует scope `admin` на стороне API.
|
|
189
|
+
// Спрашиваем подтверждение — но только когда есть кому отвечать.
|
|
190
|
+
if (!opts.yes && mode.interactive) {
|
|
191
|
+
const readline = await import("node:readline/promises");
|
|
192
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
try {
|
|
194
|
+
const a = (await rl.question(`снять ${found.domain} с проекта "${project.slug}"? [y/N]: `))
|
|
195
|
+
.trim()
|
|
196
|
+
.toLowerCase();
|
|
197
|
+
if (a !== "y" && a !== "yes") {
|
|
198
|
+
console.log(chalk.yellow("отменено."));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
rl.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await api.removeDomain(project.id, found.id);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (err instanceof ApiError && err.status === 403) {
|
|
211
|
+
throw new LayeroError("forbidden", "у токена нет прав на удаление домена", "удаление необратимо и требует токена со scope `admin` — выпусти такой в настройках");
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
if (mode.json) {
|
|
216
|
+
emit({ event: "domain_removed", domain: found.domain });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(chalk.green(`${found.domain} снят с проекта`));
|
|
220
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiClient } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
5
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
|
+
async function projectRef(api, opts) {
|
|
7
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
8
|
+
const ref = opts.project ?? linked?.project_id;
|
|
9
|
+
if (!ref) {
|
|
10
|
+
throw new LayeroError("project_unknown", "не понятно, у какого проекта смотреть переменные", "запусти из каталога проекта или передай --project <id|slug>");
|
|
11
|
+
}
|
|
12
|
+
const p = await api.getProject(ref);
|
|
13
|
+
return { id: p.id, slug: p.slug };
|
|
14
|
+
}
|
|
15
|
+
export async function envListCmd(opts) {
|
|
16
|
+
const mode = detectMode();
|
|
17
|
+
const api = new ApiClient(await loadConfig());
|
|
18
|
+
const project = await projectRef(api, opts);
|
|
19
|
+
const rows = await api.listEnvVars(project.id);
|
|
20
|
+
if (mode.json) {
|
|
21
|
+
// Ни значения, ни префикса: только имя и длина. Длину отдаём, потому
|
|
22
|
+
// что она отвечает на реальный вопрос «а он вообще заполнен» и при
|
|
23
|
+
// этом ничего не выдаёт.
|
|
24
|
+
emit({
|
|
25
|
+
event: "env_vars",
|
|
26
|
+
project: project.slug,
|
|
27
|
+
vars: rows.map((r) => ({ key: r.key, length: r.length })),
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
console.log(chalk.dim(`у проекта "${project.slug}" нет переменных окружения`));
|
|
33
|
+
console.log(chalk.dim("добавить: layero env set KEY=value"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const r of rows) {
|
|
37
|
+
console.log(`${r.key}${chalk.dim(` (${r.length} симв.)`)}`);
|
|
38
|
+
}
|
|
39
|
+
console.log(chalk.dim("\nзначения не показываются — платформа их не отдаёт"));
|
|
40
|
+
}
|
|
41
|
+
function parsePair(raw) {
|
|
42
|
+
const i = raw.indexOf("=");
|
|
43
|
+
if (i <= 0) {
|
|
44
|
+
throw new LayeroError("bad_format", `ожидается KEY=value, получено "${raw.slice(0, 40)}"`, "например: layero env set DATABASE_URL=postgres://…");
|
|
45
|
+
}
|
|
46
|
+
return { key: raw.slice(0, i).trim(), value: raw.slice(i + 1) };
|
|
47
|
+
}
|
|
48
|
+
export async function envSetCmd(pairs, opts) {
|
|
49
|
+
const mode = detectMode();
|
|
50
|
+
const api = new ApiClient(await loadConfig());
|
|
51
|
+
const project = await projectRef(api, opts);
|
|
52
|
+
const incoming = pairs.map(parsePair);
|
|
53
|
+
if (!incoming.length) {
|
|
54
|
+
throw new LayeroError("nothing_to_set", "не передано ни одной переменной", "layero env set KEY=value");
|
|
55
|
+
}
|
|
56
|
+
// Остальные ключи переносим как есть — сентинел `null` говорит платформе
|
|
57
|
+
// «оставь значение». Читать чужие секреты для добавления одной своей не
|
|
58
|
+
// нужно, и это принципиально: иначе команда требовала бы прав, которых
|
|
59
|
+
// у неё быть не должно.
|
|
60
|
+
const existing = await api.listEnvVars(project.id);
|
|
61
|
+
const payload = {};
|
|
62
|
+
for (const r of existing)
|
|
63
|
+
payload[r.key] = null;
|
|
64
|
+
for (const { key, value } of incoming)
|
|
65
|
+
payload[key] = value;
|
|
66
|
+
const after = await api.replaceEnvVars(project.id, payload);
|
|
67
|
+
const names = incoming.map((x) => x.key);
|
|
68
|
+
if (mode.json) {
|
|
69
|
+
emit({ event: "env_set", project: project.slug, keys: names, total: after.length });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.green(`сохранено: ${names.join(", ")}`));
|
|
73
|
+
console.log(chalk.yellow("переменные подхватятся при следующей сборке — задеплой проект, чтобы применить"));
|
|
74
|
+
}
|
|
75
|
+
export async function envUnsetCmd(keys, opts) {
|
|
76
|
+
const mode = detectMode();
|
|
77
|
+
const api = new ApiClient(await loadConfig());
|
|
78
|
+
const project = await projectRef(api, opts);
|
|
79
|
+
const existing = await api.listEnvVars(project.id);
|
|
80
|
+
const known = new Set(existing.map((r) => r.key));
|
|
81
|
+
const missing = keys.filter((k) => !known.has(k));
|
|
82
|
+
if (missing.length) {
|
|
83
|
+
throw new LayeroError("env_not_found", `нет таких переменных: ${missing.join(", ")}`, "посмотри список: layero env list");
|
|
84
|
+
}
|
|
85
|
+
if (!opts.yes && mode.interactive) {
|
|
86
|
+
const readline = await import("node:readline/promises");
|
|
87
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
+
try {
|
|
89
|
+
const a = (await rl.question(`удалить ${keys.join(", ")} из "${project.slug}"? [y/N]: `))
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase();
|
|
92
|
+
if (a !== "y" && a !== "yes") {
|
|
93
|
+
console.log(chalk.yellow("отменено."));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
rl.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const drop = new Set(keys);
|
|
102
|
+
const payload = {};
|
|
103
|
+
for (const r of existing)
|
|
104
|
+
if (!drop.has(r.key))
|
|
105
|
+
payload[r.key] = null;
|
|
106
|
+
await api.replaceEnvVars(project.id, payload);
|
|
107
|
+
if (mode.json) {
|
|
108
|
+
emit({ event: "env_unset", project: project.slug, keys });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk.green(`удалено: ${keys.join(", ")}`));
|
|
112
|
+
console.log(chalk.yellow("изменение применится при следующей сборке"));
|
|
113
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiClient, ApiError } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
5
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
|
+
async function projectId(api, opts) {
|
|
7
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
8
|
+
const ref = opts.project ?? linked?.project_id;
|
|
9
|
+
if (!ref) {
|
|
10
|
+
throw new LayeroError("project_unknown", "не понятно, какой проект мерить", "запусти из каталога проекта или передай --project <id|slug>");
|
|
11
|
+
}
|
|
12
|
+
const p = await api.getProject(ref);
|
|
13
|
+
return p.id;
|
|
14
|
+
}
|
|
15
|
+
function render(d) {
|
|
16
|
+
const colour = d.verdict === "regressed" ? chalk.red : d.verdict === "improved" ? chalk.green : chalk.dim;
|
|
17
|
+
console.log(colour(d.message));
|
|
18
|
+
if (d.score !== null && d.score !== undefined) {
|
|
19
|
+
const t = d.timings ?? {};
|
|
20
|
+
const bits = [
|
|
21
|
+
`балл ${d.score}`,
|
|
22
|
+
t.lcp_ms ? `LCP ${t.lcp_ms} мс` : null,
|
|
23
|
+
t.ttfb_ms ? `TTFB ${t.ttfb_ms} мс` : null,
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
console.log(chalk.dim(" " + bits.join(" · ")));
|
|
26
|
+
console.log(chalk.dim(" тайминги справочно: на одном и том же коде они гуляют на десятки процентов"));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function perfCheckCmd(opts) {
|
|
30
|
+
const mode = detectMode();
|
|
31
|
+
const api = new ApiClient(await loadConfig());
|
|
32
|
+
const id = await projectId(api, opts);
|
|
33
|
+
let started;
|
|
34
|
+
try {
|
|
35
|
+
started = await api.startPerfCheck(id);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof ApiError && err.status === 429) {
|
|
39
|
+
// Замер уже идёт — это не ошибка, а ответ на вопрос «когда будет».
|
|
40
|
+
const current = await api.getPerfCheck(id).catch(() => null);
|
|
41
|
+
if (current) {
|
|
42
|
+
if (mode.json) {
|
|
43
|
+
emit({ event: "perf_check", ...current });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.dim("замер уже идёт — вот его состояние:"));
|
|
47
|
+
render(current);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
52
|
+
throw new LayeroError("no_deploy", "у проекта нет успешного деплоя — мерить нечего", "сначала `layero deploy`");
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
if (mode.json && !opts.wait) {
|
|
57
|
+
emit({ event: "perf_check_started", ...started });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!opts.wait) {
|
|
61
|
+
console.log(chalk.dim("замер запущен; результат — `layero perf show` через минуту-две"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// --wait: опрашиваем, но с потолком. Прогон занимает десятки секунд;
|
|
65
|
+
// висеть дольше нескольких минут смысла нет — лучше вернуть управление.
|
|
66
|
+
const deadline = Date.now() + 240_000;
|
|
67
|
+
let last = started;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
await new Promise((r) => setTimeout(r, 8000));
|
|
70
|
+
last = await api.getPerfCheck(id);
|
|
71
|
+
if (last.verdict !== "pending")
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (mode.json) {
|
|
75
|
+
emit({ event: "perf_check", ...last });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
render(last);
|
|
79
|
+
}
|
|
80
|
+
export async function perfShowCmd(opts) {
|
|
81
|
+
const mode = detectMode();
|
|
82
|
+
const api = new ApiClient(await loadConfig());
|
|
83
|
+
const id = await projectId(api, opts);
|
|
84
|
+
let out;
|
|
85
|
+
try {
|
|
86
|
+
out = await api.getPerfCheck(id);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
90
|
+
throw new LayeroError("no_runs", "замеров по этому проекту ещё не было", "запусти: layero perf check");
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
if (mode.json) {
|
|
95
|
+
emit({ event: "perf_check", ...out });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
render(out);
|
|
99
|
+
}
|
package/dist/commands/token.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function tokenSetCmd(jwt) {
|
|
|
8
8
|
const probe = new ApiClient(cfg);
|
|
9
9
|
try {
|
|
10
10
|
const me = await probe.me();
|
|
11
|
-
cfg.user = { id: me.id, username: me.username, email: me.email };
|
|
11
|
+
cfg.user = { id: me.id, username: me.username ?? null, email: me.email };
|
|
12
12
|
}
|
|
13
13
|
catch (err) {
|
|
14
14
|
console.error(chalk.red(`token rejected by API: ${err.message}`));
|
package/package.json
CHANGED