layero 0.11.18 → 0.11.20
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/agent.js +5 -0
- package/dist/api.js +19 -23
- package/dist/commands/data-api.js +38 -4
- package/dist/commands/data-probe.js +4 -1
- package/dist/commands/data-secrets.js +116 -0
- package/dist/commands/db.js +9 -2
- package/dist/commands/env.js +10 -2
- package/dist/detect.js +4 -0
- package/dist/logs.js +25 -4
- package/package.json +1 -1
- package/dist/commands/data-calls.js +0 -286
package/dist/agent.js
CHANGED
|
@@ -142,6 +142,11 @@ function renderHuman(event) {
|
|
|
142
142
|
process.stdout.write(` → ${event.next_action}\n`);
|
|
143
143
|
for (const w of event.layero_warnings ?? [])
|
|
144
144
|
process.stdout.write(`! ${w}\n`);
|
|
145
|
+
if (event.skipped_build_script) {
|
|
146
|
+
process.stderr.write(`\n⚠ Это статика: скрипт сборки «${event.skipped_build_script}» из package.json НЕ запустится,\n` +
|
|
147
|
+
" файлы уйдут как есть. Если сайт надо собрать, добавьте layero.json:\n" +
|
|
148
|
+
' {"framework":"generic","buildCommand":"npm run build","outputDirectory":"<папка с index.html после сборки>"}\n\n');
|
|
149
|
+
}
|
|
145
150
|
break;
|
|
146
151
|
case "plan": {
|
|
147
152
|
const src = event.sources;
|
package/dist/api.js
CHANGED
|
@@ -74,7 +74,17 @@ export class ApiClient {
|
|
|
74
74
|
listOrganizations() {
|
|
75
75
|
return this.request("GET", "/organizations");
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Проект по идентификатору; слаг уходит в `resolveProject`.
|
|
79
|
+
*
|
|
80
|
+
* 🚨 Справки `env`, `domains`, `perf`, `analytics`, `db` обещают
|
|
81
|
+
* `--project <id|slug>`, а слаг уезжал в путь как есть: сырой 422 «Input
|
|
82
|
+
* should be a valid UUID», и деплой следом собирался без переменных (G-10,
|
|
83
|
+
* 26.10). Резолв по форме — здесь, один раз для всех команд.
|
|
84
|
+
*/
|
|
77
85
|
getProject(idOrSlug) {
|
|
86
|
+
if (!looksLikeId(idOrSlug))
|
|
87
|
+
return this.resolveProject(idOrSlug);
|
|
78
88
|
return this.request("GET", `/projects/${idOrSlug}`);
|
|
79
89
|
}
|
|
80
90
|
/**
|
|
@@ -266,7 +276,7 @@ export class ApiClient {
|
|
|
266
276
|
removeDataOrigin(org, dbId, origin) {
|
|
267
277
|
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/origins?origin=${encodeURIComponent(origin)}`);
|
|
268
278
|
}
|
|
269
|
-
// ──
|
|
279
|
+
// ── секреты (HTTP из SQL и вход) и журнал запросов ─────────────────────
|
|
270
280
|
listDataSecrets(org, dbId) {
|
|
271
281
|
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/secrets`);
|
|
272
282
|
}
|
|
@@ -277,34 +287,20 @@ export class ApiClient {
|
|
|
277
287
|
deleteDataSecret(org, dbId, name) {
|
|
278
288
|
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/secrets/${encodeURIComponent(name)}`);
|
|
279
289
|
}
|
|
280
|
-
listDataHosts(org, dbId) {
|
|
281
|
-
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/hosts`);
|
|
282
|
-
}
|
|
283
|
-
addDataHost(org, dbId, host) {
|
|
284
|
-
const body = { host };
|
|
285
|
-
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/hosts`, body);
|
|
286
|
-
}
|
|
287
|
-
deleteDataHost(org, dbId, host) {
|
|
288
|
-
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/hosts/${encodeURIComponent(host)}`);
|
|
289
|
-
}
|
|
290
|
-
listDataCalls(org, dbId) {
|
|
291
|
-
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/calls`);
|
|
292
|
-
}
|
|
293
|
-
saveDataCall(org, dbId, call) {
|
|
294
|
-
return this.request("PUT", `/organizations/${org}/databases/${dbId}/api/calls`, call);
|
|
295
|
-
}
|
|
296
|
-
deleteDataCall(org, dbId, name) {
|
|
297
|
-
return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/calls/${encodeURIComponent(name)}`);
|
|
298
|
-
}
|
|
299
|
-
listDataCallLog(org, dbId, limit = 20) {
|
|
300
|
-
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/call-log?limit=${limit}`);
|
|
301
|
-
}
|
|
302
290
|
listDataMethods(org, dbId) {
|
|
303
291
|
return this.request("GET", `/organizations/${org}/databases/${dbId}/api/methods`);
|
|
304
292
|
}
|
|
305
293
|
setDataLevels(org, dbId, input) {
|
|
306
294
|
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/levels`, input);
|
|
307
295
|
}
|
|
296
|
+
/** Вход (схема `auth`, `auth.uid()`) — та же ручка, что у кнопки панели (G-05). */
|
|
297
|
+
enableDataAuth(org, dbId) {
|
|
298
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/auth/enable`, {});
|
|
299
|
+
}
|
|
300
|
+
/** Файлы (схема `storage`) — та же ручка, что у кнопки панели (G-05). */
|
|
301
|
+
enableDataStorage(org, dbId) {
|
|
302
|
+
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/storage/enable`);
|
|
303
|
+
}
|
|
308
304
|
enableDataApi(org, dbId, withSecret) {
|
|
309
305
|
return this.request("POST", `/organizations/${org}/databases/${dbId}/api/enable?with_secret=${withSecret ? "true" : "false"}`);
|
|
310
306
|
}
|
|
@@ -20,7 +20,7 @@ import { loadConfig } from "../config.js";
|
|
|
20
20
|
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
21
21
|
import { orgOf, pick } from "./db.js";
|
|
22
22
|
import { registerDataProbeCommand } from "./data-probe.js";
|
|
23
|
-
import {
|
|
23
|
+
import { registerDataSecretsCommands } from "./data-secrets.js";
|
|
24
24
|
const LEVELS = ["closed", "visitor", "user", "server"];
|
|
25
25
|
const LEVEL_WORD = {
|
|
26
26
|
closed: "закрыто",
|
|
@@ -305,8 +305,12 @@ export async function dataMethodsCmd(opts) {
|
|
|
305
305
|
if (res.tables.length)
|
|
306
306
|
console.log(chalk.bold("REST"));
|
|
307
307
|
for (const t of res.tables) {
|
|
308
|
+
// Право на часть колонок — рядом с уровнем (G-09): уровень честный, но
|
|
309
|
+
// писать можно не всё; панель зовёт это «частичный доступ».
|
|
310
|
+
const partly = new Set(t.column_only ?? []);
|
|
308
311
|
const levels = Object.entries(t.levels)
|
|
309
|
-
.map(([m, l]) => `${m} ${l === "closed" ? chalk.dim(LEVEL_WORD[l]) : LEVEL_WORD[l] ?? l}`
|
|
312
|
+
.map(([m, l]) => `${m} ${l === "closed" ? chalk.dim(LEVEL_WORD[l]) : LEVEL_WORD[l] ?? l}` +
|
|
313
|
+
(partly.has(m) ? chalk.dim(" (часть колонок)") : ""))
|
|
310
314
|
.join(" · ");
|
|
311
315
|
const view = t.kind === "view" ? chalk.dim(" (представление)") : "";
|
|
312
316
|
const profile = t.shadowed_by
|
|
@@ -465,11 +469,36 @@ const REPAIR_LOSS = "роли Data API и права по умолчанию б
|
|
|
465
469
|
* именно его советуют отказы «роли Data API в базе нет — включите API заново». Он
|
|
466
470
|
* требует явного `--db` и подтверждения с предупреждением о `public`.
|
|
467
471
|
*/
|
|
472
|
+
/** Вход и файлы базы — ручками панели. `quiet` — итог печатает включение API. */
|
|
473
|
+
async function enableExtras(api, org, db, ref, opts, quiet = false) {
|
|
474
|
+
if (opts.auth)
|
|
475
|
+
await api.enableDataAuth(org, db.id);
|
|
476
|
+
if (opts.storage)
|
|
477
|
+
await api.enableDataStorage(org, db.id);
|
|
478
|
+
if (asJson(opts)) {
|
|
479
|
+
emit({ event: "data_services_enabled", org, database: ref, auth: Boolean(opts.auth), storage: Boolean(opts.storage) });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (quiet && !opts.auth && !opts.storage)
|
|
483
|
+
return;
|
|
484
|
+
if (opts.auth) {
|
|
485
|
+
console.log(`${chalk.green("✓")} вход включён у базы «${db.name}»: схема auth и auth.uid() появились — миграции с политиками можно гнать`);
|
|
486
|
+
}
|
|
487
|
+
if (opts.storage) {
|
|
488
|
+
console.log(`${chalk.green("✓")} файлы включены у базы «${db.name}»: схема storage, бакеты — в панели или SQL (storage.buckets)`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
468
491
|
export async function dataEnableCmd(opts) {
|
|
469
492
|
if (opts.repair && !opts.db) {
|
|
470
493
|
throw new LayeroError("database_unknown", `переприменение Data API меняет умолчания прав у ролей базы — базу нужно указать явно`, "layero data enable --db <база> --repair");
|
|
471
494
|
}
|
|
472
495
|
const { api, org, db, ref } = await target(opts, "enable");
|
|
496
|
+
// Вход и файлы — тем же шагом (G-05, 26.10): миграция с `auth.uid()` в
|
|
497
|
+
// политиках иначе требовала кликов в панели, и стенд не описывался кодом.
|
|
498
|
+
if (db.api_enabled && !opts.repair && (opts.auth || opts.storage)) {
|
|
499
|
+
await enableExtras(api, org, db, ref, opts);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
473
502
|
if (db.api_enabled && !opts.repair) {
|
|
474
503
|
throw new LayeroError("data_api_already_enabled", `у базы «${db.name}» Data API уже включён — повторное включение ничего не выпускает, а ${REPAIR_LOSS}`,
|
|
475
504
|
// `--with-secret` здесь ничего не выпустит: секретный ключ выпускается отдельной командой.
|
|
@@ -491,6 +520,8 @@ export async function dataEnableCmd(opts) {
|
|
|
491
520
|
}
|
|
492
521
|
}
|
|
493
522
|
const res = await api.enableDataApi(org, db.id, Boolean(opts.withSecret));
|
|
523
|
+
if (opts.auth || opts.storage)
|
|
524
|
+
await enableExtras(api, org, db, ref, opts, true);
|
|
494
525
|
const publicKey = res.key?.key ?? null;
|
|
495
526
|
const secretKey = res.secret_key?.key ?? null;
|
|
496
527
|
const reapplied = Boolean(opts.repair);
|
|
@@ -541,7 +572,8 @@ export async function dataEnableCmd(opts) {
|
|
|
541
572
|
console.log(chalk.dim(` ключи уже были — список: layero data keys list --db ${ref}`));
|
|
542
573
|
if (!reapplied) {
|
|
543
574
|
console.log(chalk.dim("\n Как в Supabase: новые таблицы public открыты ролям, строки решает RLS — включайте его у " +
|
|
544
|
-
"каждой таблицы. Что открыто: layero data methods"
|
|
575
|
+
"каждой таблицы. Что открыто: layero data methods\n" +
|
|
576
|
+
` Вход (auth.uid() в политиках) и файлы: layero data enable --db ${shellArg(ref)} --auth --storage`));
|
|
545
577
|
}
|
|
546
578
|
}
|
|
547
579
|
/**
|
|
@@ -604,8 +636,10 @@ export function registerDataApiCommands(data, program) {
|
|
|
604
636
|
.description("Enable the Data API for a database: sets up the api schema and roles, issues a public key. " +
|
|
605
637
|
"As in Supabase, new tables, sequences and functions in public are open to the API roles; rows are decided by RLS.")
|
|
606
638
|
.option("--with-secret", "also issue a secret key for the server")
|
|
639
|
+
.option("--auth", "also enable sign-in: the auth schema and auth.uid() for RLS policies")
|
|
640
|
+
.option("--storage", "also enable files: the storage schema (buckets, storage.objects)")
|
|
607
641
|
.option("--repair", "re-apply the roles and the api schema for a database whose Data API is already enabled; table grants are kept, default privileges for new tables are removed")
|
|
608
642
|
.option("-y, --yes", "do not ask for confirmation for --repair")).action(async (opts) => dataEnableCmd(json(opts)));
|
|
609
643
|
registerDataProbeCommand(data, withDb, json);
|
|
610
|
-
|
|
644
|
+
registerDataSecretsCommands(data, withDb, json);
|
|
611
645
|
}
|
|
@@ -525,7 +525,9 @@ export function registerDataProbeCommand(data, withDb, json) {
|
|
|
525
525
|
.option("--body-file <file>", "body from a JSON file")
|
|
526
526
|
.option("--schema <schema>", "table schema: api, public or app; without the flag the gateway looks in api, then public, then app")
|
|
527
527
|
.option("--expect <status>", "expected status: 200, 2xx or a comma-separated list; a mismatch is an error")
|
|
528
|
-
.addHelpText("after", "\nPaths: /rest/v1/<table>, /rest/v1/rpc/<function> (GET or POST), /whoami (GET only)
|
|
528
|
+
.addHelpText("after", "\nPaths: /rest/v1/<table>, /rest/v1/rpc/<function> (GET or POST), /whoami (GET only),\n" +
|
|
529
|
+
"files without writing: POST /storage/v1/object/list/<bucket>, GET /storage/v1/object/info/<bucket>/<path>,\n" +
|
|
530
|
+
"POST /storage/v1/object/sign/<bucket>/<path> (read), POST /storage/v1/object/upload/sign/<bucket>/<path> (may upload?).\n" +
|
|
529
531
|
"Pass parameters as --query flags, not with a \"?\" in the path.\n" +
|
|
530
532
|
"\nExamples:\n" +
|
|
531
533
|
" $ layero data probe GET /rest/v1/products --query select=id,title --query limit=5\n" +
|
|
@@ -533,6 +535,7 @@ export function registerDataProbeCommand(data, withDb, json) {
|
|
|
533
535
|
" $ layero data probe POST /rest/v1/rpc/order_create --body-file order.json --as server\n" +
|
|
534
536
|
" $ layero data probe GET /rest/v1/orders --expect 401,403 # is it closed to visitors?\n" +
|
|
535
537
|
" $ layero data probe GET /whoami\n" +
|
|
538
|
+
" $ layero data probe POST /storage/v1/object/list/avatars --as user --user <id> # whose files are visible\n" +
|
|
536
539
|
`\nWrites are rolled back. Not rolled back: ${NOT_UNDONE_HELP}.\n` +
|
|
537
540
|
"\nExit code, decided in this order:\n" +
|
|
538
541
|
" 1. the write rollback was not confirmed — error data_probe_not_rolled_back;\n" +
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
4
|
+
import { target } from "./data-api.js";
|
|
5
|
+
function asJson(opts) {
|
|
6
|
+
return detectMode().json || Boolean(opts.json);
|
|
7
|
+
}
|
|
8
|
+
async function readStdin() {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for await (const chunk of process.stdin)
|
|
11
|
+
chunks.push(chunk);
|
|
12
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
13
|
+
}
|
|
14
|
+
/** Ввод без эха: символы не печатаются, как у `sudo`. */
|
|
15
|
+
async function askHidden(question) {
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
17
|
+
const out = rl;
|
|
18
|
+
let prompted = false;
|
|
19
|
+
out._writeToOutput = (s) => {
|
|
20
|
+
if (!prompted) {
|
|
21
|
+
process.stdout.write(s);
|
|
22
|
+
prompted = true;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
return await rl.question(question);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
rl.close();
|
|
30
|
+
process.stdout.write("\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function confirmed(question, opts) {
|
|
34
|
+
if (opts.yes)
|
|
35
|
+
return true;
|
|
36
|
+
const mode = detectMode();
|
|
37
|
+
if (!mode.interactive || asJson(opts))
|
|
38
|
+
return null;
|
|
39
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
+
try {
|
|
41
|
+
const answer = (await rl.question(`${question} [y/N]: `)).trim().toLowerCase();
|
|
42
|
+
return answer === "y" || answer === "yes";
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function notConfirmed(what, command) {
|
|
49
|
+
return new LayeroError("confirmation_required", `${what} не подтверждено`, `повторите с --yes: ${command} --yes`);
|
|
50
|
+
}
|
|
51
|
+
// ── секреты ─────────────────────────────────────────────────────────────────
|
|
52
|
+
export async function dataSecretsListCmd(opts) {
|
|
53
|
+
const { api, org, db, ref } = await target(opts);
|
|
54
|
+
const names = (await api.listDataSecrets(org, db.id)).map((s) => s.name);
|
|
55
|
+
if (asJson(opts)) {
|
|
56
|
+
emit({ event: "data_secrets", org, database: ref, secrets: names });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.bold("Секреты") + chalk.dim(" — только имена, значения не показываются никогда"));
|
|
60
|
+
if (names.length)
|
|
61
|
+
for (const n of names)
|
|
62
|
+
console.log(` ${n}`);
|
|
63
|
+
else
|
|
64
|
+
console.log(chalk.dim(` нет — задать: layero data secrets set TELEGRAM_BOT_TOKEN --db ${ref}`));
|
|
65
|
+
}
|
|
66
|
+
export async function dataSecretsSetCmd(name, opts) {
|
|
67
|
+
let value;
|
|
68
|
+
if (opts.fromEnv) {
|
|
69
|
+
value = process.env[opts.fromEnv];
|
|
70
|
+
if (!value)
|
|
71
|
+
throw new LayeroError("bad_format", `переменная окружения ${opts.fromEnv} пуста или не задана`, "задайте её или передайте значение через stdin");
|
|
72
|
+
}
|
|
73
|
+
else if (!process.stdin.isTTY) {
|
|
74
|
+
value = await readStdin();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
value = await askHidden(`значение ${name}: `);
|
|
78
|
+
}
|
|
79
|
+
if (!value) {
|
|
80
|
+
throw new LayeroError("bad_format", "пустое значение секрета", `передайте значение через stdin: printf %s "$TOKEN" | layero data secrets set ${name}`);
|
|
81
|
+
}
|
|
82
|
+
const { api, org, db, ref } = await target(opts);
|
|
83
|
+
await api.setDataSecret(org, db.id, name, value);
|
|
84
|
+
if (asJson(opts)) {
|
|
85
|
+
emit({ event: "data_secret_set", org, database: ref, name });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(`${chalk.green("✓")} секрет ${name} сохранён — в вызове: {{secrets.${name}}}`);
|
|
89
|
+
}
|
|
90
|
+
export async function dataSecretsRemoveCmd(name, opts) {
|
|
91
|
+
const { api, org, db, ref } = await target(opts);
|
|
92
|
+
const ok = await confirmed(`удалить секрет ${name}? Вызовы, которые его подставляют, начнут отказывать`, opts);
|
|
93
|
+
if (ok === null)
|
|
94
|
+
throw notConfirmed(`удаление секрета ${name}`, `layero data secrets remove ${name} --db ${ref}`);
|
|
95
|
+
if (!ok) {
|
|
96
|
+
console.log(chalk.yellow("отменено."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await api.deleteDataSecret(org, db.id, name);
|
|
100
|
+
if (asJson(opts)) {
|
|
101
|
+
emit({ event: "data_secret_removed", org, database: ref, name });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
console.log(`${chalk.green("✓")} секрет ${name} удалён`);
|
|
105
|
+
}
|
|
106
|
+
export function registerDataSecretsCommands(data, withDb, json) {
|
|
107
|
+
const secrets = data.command("secrets").description("Secrets substituted into net.http_* requests as {{secrets.NAME}}. Values are never shown.");
|
|
108
|
+
withDb(secrets.command("list").description("Secret names.")).action(async (opts) => dataSecretsListCmd(json(opts)));
|
|
109
|
+
withDb(secrets
|
|
110
|
+
.command("set <name>")
|
|
111
|
+
.description("Set a secret. The value is read from stdin, from --from-env, or typed without echo.")
|
|
112
|
+
.option("--from-env <var>", "take the value from this environment variable")
|
|
113
|
+
.addHelpText("after", '\nExample:\n $ printf %s "$BOT_TOKEN" | layero data secrets set TELEGRAM_BOT_TOKEN')).action(async (name, opts) => dataSecretsSetCmd(name, json(opts)));
|
|
114
|
+
withDb(secrets.command("remove <name>").description("Delete a secret.").option("-y, --yes", "do not ask for confirmation"))
|
|
115
|
+
.action(async (name, opts) => dataSecretsRemoveCmd(name, json(opts)));
|
|
116
|
+
}
|
package/dist/commands/db.js
CHANGED
|
@@ -247,9 +247,16 @@ export async function dbConnectCmd(ref, opts) {
|
|
|
247
247
|
// из `userdb_roles`. Пока здесь говорилось только про переменные, человек,
|
|
248
248
|
// у которого сайт отбивался по CORS, не имел ни одной причины связать это со
|
|
249
249
|
// своей ненажатой командой — а отказ шлюза именно её и советует.
|
|
250
|
+
// 🚨 Что именно приедет и куда (G-11, 26.10). Клиент со статическим сайтом
|
|
251
|
+
// увидел в переменных строку с паролем, не увидел ключей Data API и положил
|
|
252
|
+
// их руками. А строка подключения в сборку не попадает вовсе — только в
|
|
253
|
+
// контейнер серверного приложения, — адрес и ключ платформа кладёт в сборку сама.
|
|
250
254
|
console.log(`${chalk.green("✓")} проект «${project.slug}» подключён к базе «${db.name}»\n` +
|
|
251
|
-
chalk.dim(
|
|
252
|
-
chalk.dim(
|
|
255
|
+
chalk.dim(` домены проекта разрешены для Data API базы «${db.name}»\n`) +
|
|
256
|
+
chalk.dim(" в сборку сайта платформа сама положит адрес и публичный ключ Data API:\n") +
|
|
257
|
+
chalk.dim(" VITE_LAYERO_DATA_URL / VITE_LAYERO_DATA_KEY (для Next.js — NEXT_PUBLIC_LAYERO_DATA_…);\n") +
|
|
258
|
+
chalk.dim(" у сайта без сборки — файл /layero-data.js. Класть их через env set не нужно\n") +
|
|
259
|
+
chalk.dim(" строка подключения с паролем — только серверному коду, в сборку и в браузер не попадает"));
|
|
253
260
|
}
|
|
254
261
|
export async function dbDisconnectCmd(ref, opts) {
|
|
255
262
|
// 🚨 ОБРАТНОГО ДЕЙСТВИЯ НЕ БЫЛО ВОВСЕ. `db connect` существовал с самого
|
package/dist/commands/env.js
CHANGED
|
@@ -24,7 +24,14 @@ export async function envListCmd(opts) {
|
|
|
24
24
|
emit({
|
|
25
25
|
event: "env_vars",
|
|
26
26
|
project: project.slug,
|
|
27
|
-
vars: rows.map((r) => ({
|
|
27
|
+
vars: rows.map((r) => ({
|
|
28
|
+
key: r.key,
|
|
29
|
+
length: r.length,
|
|
30
|
+
managed: !!r.managed,
|
|
31
|
+
// Куда платформа кладёт свою переменную (G-11): `runtime` — только в
|
|
32
|
+
// контейнер сервера, в сборку не попадает; `build` — в сборку сайта.
|
|
33
|
+
...(r.managed ? { scope: r.scope ?? null } : {}),
|
|
34
|
+
})),
|
|
28
35
|
});
|
|
29
36
|
return;
|
|
30
37
|
}
|
|
@@ -38,8 +45,9 @@ export async function envListCmd(opts) {
|
|
|
38
45
|
// базе — часть ответа. Но менять её нельзя: она пересобирается из связи
|
|
39
46
|
// проекта с базой, и предложить правку значило бы пообещать несбыточное.
|
|
40
47
|
for (const r of rows) {
|
|
48
|
+
const where = r.scope === "build" ? ", в сборку сайта" : r.scope === "runtime" ? ", только серверу — в сборку не попадает" : "";
|
|
41
49
|
const tail = r.managed
|
|
42
|
-
? chalk.dim(` (${r.length} симв., ставит
|
|
50
|
+
? chalk.dim(` (${r.length} симв., ставит платформа${where})`)
|
|
43
51
|
: chalk.dim(` (${r.length} симв.)`);
|
|
44
52
|
console.log(`${r.key}${tail}`);
|
|
45
53
|
}
|
package/dist/detect.js
CHANGED
|
@@ -665,6 +665,7 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
665
665
|
let confident = declared || plan.confident;
|
|
666
666
|
let shape = null;
|
|
667
667
|
const notes = [];
|
|
668
|
+
let skippedBuild = null;
|
|
668
669
|
let flagNextAction;
|
|
669
670
|
if (framework === "static") {
|
|
670
671
|
// Статика не собирается НИКОГДА: команда сборки у неё не «true», а
|
|
@@ -678,6 +679,7 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
678
679
|
if (await rootHasIndexHtml(cwd)) {
|
|
679
680
|
confident = true;
|
|
680
681
|
if (buildScript && !declared) {
|
|
682
|
+
skippedBuild = buildScript;
|
|
681
683
|
notes.push(`package.json has a build script (\`${buildScript}\`), but a static site is served as it is and the script is not run. ` +
|
|
682
684
|
'If it must run: layero.json {"framework":"generic","buildCommand":"npm run build","outputDirectory":"<folder>"}.');
|
|
683
685
|
}
|
|
@@ -761,6 +763,7 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
761
763
|
...(shape?.next_action ?? flagNextAction ? { next_action: shape?.next_action ?? flagNextAction } : {}),
|
|
762
764
|
...(shape?.candidates?.length ? { candidates: shape.candidates } : {}),
|
|
763
765
|
...(warning ? { ssr_warning: warning } : {}),
|
|
766
|
+
...(skippedBuild ? { skipped_build_script: skippedBuild } : {}),
|
|
764
767
|
};
|
|
765
768
|
}
|
|
766
769
|
/** Событие `detected` — одно на `init`, `deploy` и `deploy --dry-run`. */
|
|
@@ -778,5 +781,6 @@ export function detectedEvent(d) {
|
|
|
778
781
|
...(d.candidates?.length ? { candidates: d.candidates } : {}),
|
|
779
782
|
...(d.ssr_warning ? { ssr_warning: d.ssr_warning } : {}),
|
|
780
783
|
...(d.layero_warnings?.length ? { layero_warnings: d.layero_warnings } : {}),
|
|
784
|
+
...(d.skipped_build_script ? { skipped_build_script: d.skipped_build_script } : {}),
|
|
781
785
|
};
|
|
782
786
|
}
|
package/dist/logs.js
CHANGED
|
@@ -60,6 +60,7 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
60
60
|
const announced = new Set();
|
|
61
61
|
const startedAt = Date.now();
|
|
62
62
|
let lastQueuedAt = startedAt;
|
|
63
|
+
let queueShown = false;
|
|
63
64
|
const sleep = (ms) => new Promise((res) => setTimeout(res, retryDelayScale() * ms));
|
|
64
65
|
const announce = (name) => {
|
|
65
66
|
if (name === lastStage || announced.has(name))
|
|
@@ -120,12 +121,32 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
120
121
|
}
|
|
121
122
|
if (poll.current_stage)
|
|
122
123
|
announce(poll.current_stage);
|
|
123
|
-
if (
|
|
124
|
-
poll.status === "queued" &&
|
|
124
|
+
if (poll.status === "queued" &&
|
|
125
125
|
announced.size === 0 &&
|
|
126
|
-
Date.now() - lastQueuedAt >= QUEUED_EVERY_MS) {
|
|
126
|
+
(Date.now() - lastQueuedAt >= QUEUED_EVERY_MS || (!queueShown && poll.queue_message))) {
|
|
127
127
|
lastQueuedAt = Date.now();
|
|
128
|
-
|
|
128
|
+
// 🚨 Место и оценка — от сервера (G-14, 26.10). Раньше человек видел
|
|
129
|
+
// тишину, а агент — один `waited_s`: восемь минут без причины читались
|
|
130
|
+
// как зависание, и деплой запускали ещё раз.
|
|
131
|
+
const q = (poll.queue ?? null);
|
|
132
|
+
const waited = Math.round((Date.now() - startedAt) / 1000);
|
|
133
|
+
if (json) {
|
|
134
|
+
emit({
|
|
135
|
+
event: "queued",
|
|
136
|
+
waited_s: waited,
|
|
137
|
+
...(q ? {
|
|
138
|
+
position: q.position,
|
|
139
|
+
ahead: q.ahead,
|
|
140
|
+
running: q.running,
|
|
141
|
+
estimated_wait_s: q.estimated_wait_s ?? null,
|
|
142
|
+
} : {}),
|
|
143
|
+
...(poll.queue_message ? { message: poll.queue_message } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (poll.queue_message) {
|
|
147
|
+
process.stderr.write(chalk.dim(`… ${poll.queue_message}\n`));
|
|
148
|
+
}
|
|
149
|
+
queueShown = true;
|
|
129
150
|
}
|
|
130
151
|
if (poll.terminal) {
|
|
131
152
|
return poll;
|
package/package.json
CHANGED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
// `layero data hosts | secrets | calls` — исходящие вызовы базы из терминала
|
|
2
|
-
// (G-12, 26.10.2026).
|
|
3
|
-
//
|
|
4
|
-
// 🚨 Зачем. Механизм был только у сервера: документация звала в раздел панели,
|
|
5
|
-
// которого не было, а CLI и MCP о нём не знали. Строки `api.outbox` у клиента
|
|
6
|
-
// ложились в `failed: вызов не описан`, и обещанные уведомления в Telegram без
|
|
7
|
-
// своего сервера не запускались. Теперь всё описывается кодом:
|
|
8
|
-
//
|
|
9
|
-
// layero data hosts add api.telegram.org
|
|
10
|
-
// layero data secrets set TELEGRAM_BOT_TOKEN # значение — со stdin
|
|
11
|
-
// layero data calls set --file telegram_notify.json
|
|
12
|
-
//
|
|
13
|
-
// ⚠️ Значение секрета НЕ принимается аргументом: аргумент оседает в истории
|
|
14
|
-
// shell и в списке процессов. Только stdin, переменная окружения или ввод без
|
|
15
|
-
// эха в терминале.
|
|
16
|
-
import { readFile } from "node:fs/promises";
|
|
17
|
-
import readline from "node:readline/promises";
|
|
18
|
-
import chalk from "chalk";
|
|
19
|
-
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
20
|
-
import { target } from "./data-api.js";
|
|
21
|
-
function asJson(opts) {
|
|
22
|
-
return detectMode().json || Boolean(opts.json);
|
|
23
|
-
}
|
|
24
|
-
async function readStdin() {
|
|
25
|
-
const chunks = [];
|
|
26
|
-
for await (const chunk of process.stdin)
|
|
27
|
-
chunks.push(chunk);
|
|
28
|
-
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
29
|
-
}
|
|
30
|
-
/** Ввод без эха: символы не печатаются, как у `sudo`. */
|
|
31
|
-
async function askHidden(question) {
|
|
32
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
33
|
-
const out = rl;
|
|
34
|
-
let prompted = false;
|
|
35
|
-
out._writeToOutput = (s) => {
|
|
36
|
-
if (!prompted) {
|
|
37
|
-
process.stdout.write(s);
|
|
38
|
-
prompted = true;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
return await rl.question(question);
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
rl.close();
|
|
46
|
-
process.stdout.write("\n");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function confirmed(question, opts) {
|
|
50
|
-
if (opts.yes)
|
|
51
|
-
return true;
|
|
52
|
-
const mode = detectMode();
|
|
53
|
-
if (!mode.interactive || asJson(opts))
|
|
54
|
-
return null;
|
|
55
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
-
try {
|
|
57
|
-
const answer = (await rl.question(`${question} [y/N]: `)).trim().toLowerCase();
|
|
58
|
-
return answer === "y" || answer === "yes";
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
rl.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function notConfirmed(what, command) {
|
|
65
|
-
return new LayeroError("confirmation_required", `${what} не подтверждено`, `повторите с --yes: ${command} --yes`);
|
|
66
|
-
}
|
|
67
|
-
// ── хосты ───────────────────────────────────────────────────────────────────
|
|
68
|
-
export async function dataHostsListCmd(opts) {
|
|
69
|
-
const { api, org, db, ref } = await target(opts);
|
|
70
|
-
const hosts = (await api.listDataHosts(org, db.id)).map((h) => h.host);
|
|
71
|
-
if (asJson(opts)) {
|
|
72
|
-
emit({ event: "data_hosts", org, database: ref, hosts });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
console.log(chalk.bold("Разрешённые хосты") + chalk.dim(" — куда шлюзу можно ходить"));
|
|
76
|
-
if (hosts.length)
|
|
77
|
-
for (const h of hosts)
|
|
78
|
-
console.log(` ${h}`);
|
|
79
|
-
else
|
|
80
|
-
console.log(chalk.dim(` нет — добавить: layero data hosts add api.telegram.org --db ${ref}`));
|
|
81
|
-
}
|
|
82
|
-
export async function dataHostsAddCmd(host, opts) {
|
|
83
|
-
const { api, org, db, ref } = await target(opts);
|
|
84
|
-
await api.addDataHost(org, db.id, host);
|
|
85
|
-
if (asJson(opts)) {
|
|
86
|
-
emit({ event: "data_host_added", org, database: ref, host });
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
console.log(`${chalk.green("✓")} шлюз базы «${db.name}» может ходить на ${host}`);
|
|
90
|
-
}
|
|
91
|
-
export async function dataHostsRemoveCmd(host, opts) {
|
|
92
|
-
const { api, org, db, ref } = await target(opts);
|
|
93
|
-
const ok = await confirmed(`убрать ${host}? Вызовы на этот хост начнут отказывать`, opts);
|
|
94
|
-
if (ok === null)
|
|
95
|
-
throw notConfirmed(`удаление хоста ${host}`, `layero data hosts remove ${host} --db ${ref}`);
|
|
96
|
-
if (!ok) {
|
|
97
|
-
console.log(chalk.yellow("отменено."));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
await api.deleteDataHost(org, db.id, host);
|
|
101
|
-
if (asJson(opts)) {
|
|
102
|
-
emit({ event: "data_host_removed", org, database: ref, host });
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
console.log(`${chalk.green("✓")} ${host} убран`);
|
|
106
|
-
}
|
|
107
|
-
// ── секреты ─────────────────────────────────────────────────────────────────
|
|
108
|
-
export async function dataSecretsListCmd(opts) {
|
|
109
|
-
const { api, org, db, ref } = await target(opts);
|
|
110
|
-
const names = (await api.listDataSecrets(org, db.id)).map((s) => s.name);
|
|
111
|
-
if (asJson(opts)) {
|
|
112
|
-
emit({ event: "data_secrets", org, database: ref, secrets: names });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
console.log(chalk.bold("Секреты") + chalk.dim(" — только имена, значения не показываются никогда"));
|
|
116
|
-
if (names.length)
|
|
117
|
-
for (const n of names)
|
|
118
|
-
console.log(` ${n}`);
|
|
119
|
-
else
|
|
120
|
-
console.log(chalk.dim(` нет — задать: layero data secrets set TELEGRAM_BOT_TOKEN --db ${ref}`));
|
|
121
|
-
}
|
|
122
|
-
export async function dataSecretsSetCmd(name, opts) {
|
|
123
|
-
let value;
|
|
124
|
-
if (opts.fromEnv) {
|
|
125
|
-
value = process.env[opts.fromEnv];
|
|
126
|
-
if (!value)
|
|
127
|
-
throw new LayeroError("bad_format", `переменная окружения ${opts.fromEnv} пуста или не задана`, "задайте её или передайте значение через stdin");
|
|
128
|
-
}
|
|
129
|
-
else if (!process.stdin.isTTY) {
|
|
130
|
-
value = await readStdin();
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
value = await askHidden(`значение ${name}: `);
|
|
134
|
-
}
|
|
135
|
-
if (!value) {
|
|
136
|
-
throw new LayeroError("bad_format", "пустое значение секрета", `передайте значение через stdin: printf %s "$TOKEN" | layero data secrets set ${name}`);
|
|
137
|
-
}
|
|
138
|
-
const { api, org, db, ref } = await target(opts);
|
|
139
|
-
await api.setDataSecret(org, db.id, name, value);
|
|
140
|
-
if (asJson(opts)) {
|
|
141
|
-
emit({ event: "data_secret_set", org, database: ref, name });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
console.log(`${chalk.green("✓")} секрет ${name} сохранён — в вызове: {{secrets.${name}}}`);
|
|
145
|
-
}
|
|
146
|
-
export async function dataSecretsRemoveCmd(name, opts) {
|
|
147
|
-
const { api, org, db, ref } = await target(opts);
|
|
148
|
-
const ok = await confirmed(`удалить секрет ${name}? Вызовы, которые его подставляют, начнут отказывать`, opts);
|
|
149
|
-
if (ok === null)
|
|
150
|
-
throw notConfirmed(`удаление секрета ${name}`, `layero data secrets remove ${name} --db ${ref}`);
|
|
151
|
-
if (!ok) {
|
|
152
|
-
console.log(chalk.yellow("отменено."));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
await api.deleteDataSecret(org, db.id, name);
|
|
156
|
-
if (asJson(opts)) {
|
|
157
|
-
emit({ event: "data_secret_removed", org, database: ref, name });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
console.log(`${chalk.green("✓")} секрет ${name} удалён`);
|
|
161
|
-
}
|
|
162
|
-
// ── вызовы ──────────────────────────────────────────────────────────────────
|
|
163
|
-
/** Описание из файла → тело запроса. Те же поля, что на странице документации. */
|
|
164
|
-
export function parseCallSpec(text, source) {
|
|
165
|
-
let raw;
|
|
166
|
-
try {
|
|
167
|
-
raw = JSON.parse(text);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
throw new LayeroError("bad_format", `${source}: не JSON`, "описание вызова — JSON-объект с полями name и url_template");
|
|
171
|
-
}
|
|
172
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
173
|
-
throw new LayeroError("bad_format", `${source}: ожидался JSON-объект`, "описание вызова — JSON-объект с полями name и url_template");
|
|
174
|
-
}
|
|
175
|
-
for (const field of ["name", "url_template"]) {
|
|
176
|
-
if (typeof raw[field] !== "string" || !raw[field]) {
|
|
177
|
-
throw new LayeroError("bad_format", `${source}: нет поля ${field}`, "описание вызова — JSON-объект с полями name и url_template");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
181
|
-
name: raw.name,
|
|
182
|
-
method: typeof raw.method === "string" ? raw.method.toUpperCase() : "POST",
|
|
183
|
-
url_template: raw.url_template,
|
|
184
|
-
headers: raw.headers ?? null,
|
|
185
|
-
body_template: raw.body_template ?? null,
|
|
186
|
-
allow_anon: raw.allow_anon === true,
|
|
187
|
-
hourly_limit: Number.isInteger(raw.hourly_limit) ? raw.hourly_limit : 600,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
export async function dataCallsListCmd(opts) {
|
|
191
|
-
const { api, org, db, ref } = await target(opts);
|
|
192
|
-
const calls = await api.listDataCalls(org, db.id);
|
|
193
|
-
if (asJson(opts)) {
|
|
194
|
-
emit({ event: "data_calls", org, database: ref, calls });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (!calls.length) {
|
|
198
|
-
console.log(chalk.dim(`вызовов нет — описать: layero data calls set --file call.json --db ${ref}`));
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
for (const c of calls) {
|
|
202
|
-
console.log(`${chalk.bold(c.name)} ${c.method} ${c.url_template}${c.allow_anon ? chalk.yellow(" публичный ключ") : ""}`);
|
|
203
|
-
console.log(chalk.dim(` из базы: INSERT INTO api.outbox (call_name, args) VALUES ('${c.name}', …); с сайта: POST /call/${c.name}`));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
export async function dataCallsSetCmd(opts) {
|
|
207
|
-
if (!opts.file) {
|
|
208
|
-
throw new LayeroError("bad_format", "не указан файл описания", "layero data calls set --file telegram_notify.json");
|
|
209
|
-
}
|
|
210
|
-
const text = opts.file === "-" ? await readStdin() : await readFile(opts.file, "utf8");
|
|
211
|
-
const call = parseCallSpec(text, opts.file === "-" ? "stdin" : opts.file);
|
|
212
|
-
const { api, org, db, ref } = await target(opts);
|
|
213
|
-
await api.saveDataCall(org, db.id, call);
|
|
214
|
-
if (asJson(opts)) {
|
|
215
|
-
emit({ event: "data_call_saved", org, database: ref, name: call.name });
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
console.log(`${chalk.green("✓")} вызов ${call.name} описан у базы «${db.name}»\n` +
|
|
219
|
-
chalk.dim(` из базы: INSERT INTO api.outbox (call_name, args) VALUES ('${call.name}', '{…}'::jsonb)\n`) +
|
|
220
|
-
chalk.dim(` журнал: layero data calls log --db ${ref}`));
|
|
221
|
-
}
|
|
222
|
-
export async function dataCallsRemoveCmd(name, opts) {
|
|
223
|
-
const { api, org, db, ref } = await target(opts);
|
|
224
|
-
const ok = await confirmed(`удалить вызов ${name}? Новые строки api.outbox с этим именем будут отклонены`, opts);
|
|
225
|
-
if (ok === null)
|
|
226
|
-
throw notConfirmed(`удаление вызова ${name}`, `layero data calls remove ${name} --db ${ref}`);
|
|
227
|
-
if (!ok) {
|
|
228
|
-
console.log(chalk.yellow("отменено."));
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
await api.deleteDataCall(org, db.id, name);
|
|
232
|
-
if (asJson(opts)) {
|
|
233
|
-
emit({ event: "data_call_removed", org, database: ref, name });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
console.log(`${chalk.green("✓")} вызов ${name} удалён`);
|
|
237
|
-
}
|
|
238
|
-
export async function dataCallsLogCmd(opts) {
|
|
239
|
-
const { api, org, db, ref } = await target(opts);
|
|
240
|
-
const limit = Math.min(Math.max(Number(opts.limit ?? 20) || 20, 1), 200);
|
|
241
|
-
const rows = await api.listDataCallLog(org, db.id, limit);
|
|
242
|
-
if (asJson(opts)) {
|
|
243
|
-
emit({ event: "data_call_log", org, database: ref, calls: rows });
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (!rows.length) {
|
|
247
|
-
console.log(chalk.dim("вызовов ещё не было"));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
for (const r of rows) {
|
|
251
|
-
const verdict = r.error ? chalk.red(r.error) : r.status_code && r.status_code < 400 ? chalk.green(String(r.status_code)) : chalk.yellow(String(r.status_code ?? "—"));
|
|
252
|
-
console.log(`${r.at} ${r.call_name} ${r.mode === "queue" ? "очередь" : "синхронно"} ${verdict}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
export function registerDataCallsCommands(data, withDb, json) {
|
|
256
|
-
const hosts = data.command("hosts").description("Hosts the gateway may call for the database's outbound calls.");
|
|
257
|
-
withDb(hosts.command("list").description("Allowed hosts.")).action(async (opts) => dataHostsListCmd(json(opts)));
|
|
258
|
-
withDb(hosts.command("add <host>").description("Allow a host, e.g. api.telegram.org."))
|
|
259
|
-
.action(async (host, opts) => dataHostsAddCmd(host, json(opts)));
|
|
260
|
-
withDb(hosts.command("remove <host>").description("Remove a host.").option("-y, --yes", "do not ask for confirmation"))
|
|
261
|
-
.action(async (host, opts) => dataHostsRemoveCmd(host, json(opts)));
|
|
262
|
-
const secrets = data.command("secrets").description("Secrets substituted into outbound calls as {{secrets.NAME}}. Values are never shown.");
|
|
263
|
-
withDb(secrets.command("list").description("Secret names.")).action(async (opts) => dataSecretsListCmd(json(opts)));
|
|
264
|
-
withDb(secrets
|
|
265
|
-
.command("set <name>")
|
|
266
|
-
.description("Set a secret. The value is read from stdin, from --from-env, or typed without echo.")
|
|
267
|
-
.option("--from-env <var>", "take the value from this environment variable")
|
|
268
|
-
.addHelpText("after", '\nExample:\n $ printf %s "$BOT_TOKEN" | layero data secrets set TELEGRAM_BOT_TOKEN')).action(async (name, opts) => dataSecretsSetCmd(name, json(opts)));
|
|
269
|
-
withDb(secrets.command("remove <name>").description("Delete a secret.").option("-y, --yes", "do not ask for confirmation"))
|
|
270
|
-
.action(async (name, opts) => dataSecretsRemoveCmd(name, json(opts)));
|
|
271
|
-
const calls = data.command("calls").description("Outbound calls: webhooks and Telegram from the database (api.outbox) or the site (POST /call/<name>).");
|
|
272
|
-
withDb(calls.command("list").description("Described calls.")).action(async (opts) => dataCallsListCmd(json(opts)));
|
|
273
|
-
withDb(calls
|
|
274
|
-
.command("set")
|
|
275
|
-
.description("Create or replace a call from a JSON description: name, method, url_template, headers, body_template, allow_anon.")
|
|
276
|
-
.option("--file <path>", "JSON file with the description, - for stdin")
|
|
277
|
-
.addHelpText("after", "\nThe host of url_template must be allowed first: layero data hosts add <host>.\n" +
|
|
278
|
-
"\nExample (telegram_notify.json):\n" +
|
|
279
|
-
' {"name": "telegram_notify", "method": "POST",\n' +
|
|
280
|
-
' "url_template": "https://api.telegram.org/bot{{secrets.TELEGRAM_BOT_TOKEN}}/sendMessage",\n' +
|
|
281
|
-
' "body_template": {"chat_id": "{{args.chat_id}}", "text": "{{args.text}}"}}')).action(async (opts) => dataCallsSetCmd(json(opts)));
|
|
282
|
-
withDb(calls.command("remove <name>").description("Delete a call.").option("-y, --yes", "do not ask for confirmation"))
|
|
283
|
-
.action(async (name, opts) => dataCallsRemoveCmd(name, json(opts)));
|
|
284
|
-
withDb(calls.command("log").description("Recent calls: status code and refusal reason.").option("--limit <n>", "how many, up to 200", "20"))
|
|
285
|
-
.action(async (opts) => dataCallsLogCmd(json(opts)));
|
|
286
|
-
}
|