@getxflow/cli 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/args.js +2 -0
- package/dist/bin.js +39 -1
- package/dist/commands/db.js +130 -0
- package/dist/commands/deploy.js +3 -0
- package/dist/commands/functions.js +84 -0
- package/dist/commands/logs.js +60 -0
- package/dist/commands/projects.js +2 -2
- package/dist/commands/schedules.js +60 -0
- package/dist/help.js +194 -108
- package/dist/template.js +41 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +41 -6
package/README.md
CHANGED
|
@@ -20,7 +20,11 @@ xflow publish
|
|
|
20
20
|
| `init` / `link` | новый проект, связать папку с существующим |
|
|
21
21
|
| `status` / `push` / `pull` | состояние, отправка и получение исходников |
|
|
22
22
|
| `deploy` / `publish` / `rollback` / `deployments` | сборка, публикация, откат, история версий |
|
|
23
|
+
| `db status` / `db migrate` | миграции из `migrations/*.sql` с гейтом на разрушающие |
|
|
23
24
|
| `functions list` / `functions deploy` | облачные функции проекта из `functions/<имя>/index.ts` |
|
|
25
|
+
| `functions invoke` / `functions logs` | вызвать функцию, посмотреть её падения со стеком |
|
|
26
|
+
| `schedules list` / `set` / `rm` | запуск функций по времени (таймер-триггеры) |
|
|
27
|
+
| `logs` | ошибки выложенного приложения в браузере |
|
|
24
28
|
| `projects list` / `projects get` / `open` | проекты организации и адреса приложения |
|
|
25
29
|
| `skills` | инструкция о платформе для ИИ-агента (формат [Agent Skills](https://agentskills.io)) |
|
|
26
30
|
|
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -11,7 +11,10 @@ const zip_1 = require("./zip");
|
|
|
11
11
|
const version_1 = require("./version");
|
|
12
12
|
const auth_1 = require("./commands/auth");
|
|
13
13
|
const projects_1 = require("./commands/projects");
|
|
14
|
+
const db_1 = require("./commands/db");
|
|
14
15
|
const functions_1 = require("./commands/functions");
|
|
16
|
+
const logs_1 = require("./commands/logs");
|
|
17
|
+
const schedules_1 = require("./commands/schedules");
|
|
15
18
|
const skills_1 = require("./commands/skills");
|
|
16
19
|
const sources_1 = require("./commands/sources");
|
|
17
20
|
const deploy_1 = require("./commands/deploy");
|
|
@@ -65,11 +68,46 @@ async function run(args) {
|
|
|
65
68
|
await (0, functions_1.functionsDeploy)(rest);
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
71
|
+
if (second === 'invoke') {
|
|
72
|
+
await (0, functions_1.functionsInvoke)(rest);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (second === 'logs') {
|
|
76
|
+
await (0, logs_1.functionsLogs)(rest);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
68
79
|
if (second === undefined || second === 'list') {
|
|
69
80
|
await (0, functions_1.functionsList)();
|
|
70
81
|
return;
|
|
71
82
|
}
|
|
72
|
-
throw new errors_1.CliError(`Неизвестная команда: functions ${second}`, 'Есть list и
|
|
83
|
+
throw new errors_1.CliError(`Неизвестная команда: functions ${second}`, 'Есть list, deploy, invoke и logs');
|
|
84
|
+
case 'logs':
|
|
85
|
+
await (0, logs_1.logs)(rest);
|
|
86
|
+
return;
|
|
87
|
+
case 'schedules':
|
|
88
|
+
if (second === 'set') {
|
|
89
|
+
await (0, schedules_1.schedulesSet)(rest);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (second === 'rm' || second === 'remove') {
|
|
93
|
+
await (0, schedules_1.schedulesRemove)(rest);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (second === undefined || second === 'list') {
|
|
97
|
+
await (0, schedules_1.schedulesList)();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw new errors_1.CliError(`Неизвестная команда: schedules ${second}`, 'Есть list, set и rm');
|
|
101
|
+
case 'db':
|
|
102
|
+
if (second === 'migrate') {
|
|
103
|
+
await (0, db_1.dbMigrate)(rest);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (second === undefined || second === 'status') {
|
|
107
|
+
await (0, db_1.dbStatus)();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
throw new errors_1.CliError(`Неизвестная команда: db ${second}`, 'Есть status и migrate');
|
|
73
111
|
case 'status':
|
|
74
112
|
await (0, sources_1.status)();
|
|
75
113
|
return;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dbStatus = dbStatus;
|
|
4
|
+
exports.dbMigrate = dbMigrate;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const api_1 = require("../api");
|
|
9
|
+
const args_1 = require("../args");
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const errors_1 = require("../errors");
|
|
12
|
+
const session_1 = require("../session");
|
|
13
|
+
const ui_1 = require("../ui");
|
|
14
|
+
/**
|
|
15
|
+
* Миграции базы проекта.
|
|
16
|
+
*
|
|
17
|
+
* Файлы лежат в репозитории (`migrations/0001_init.sql`), историю применённых
|
|
18
|
+
* помнит сама база. Отсюда правило: локально мы только читаем и сортируем, а что
|
|
19
|
+
* из этого новое, решает сервер.
|
|
20
|
+
*/
|
|
21
|
+
const MIGRATIONS_DIR = 'migrations';
|
|
22
|
+
function checksum(sql) {
|
|
23
|
+
return (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex').slice(0, 16);
|
|
24
|
+
}
|
|
25
|
+
/** Порядок по имени файла: `0001_` идёт раньше `0002_`, это и есть очередь. */
|
|
26
|
+
function readMigrations(root) {
|
|
27
|
+
const dir = (0, node_path_1.join)(root, MIGRATIONS_DIR);
|
|
28
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
29
|
+
return [];
|
|
30
|
+
return (0, node_fs_1.readdirSync)(dir)
|
|
31
|
+
.filter((name) => name.toLowerCase().endsWith('.sql'))
|
|
32
|
+
.sort()
|
|
33
|
+
.map((file) => ({
|
|
34
|
+
name: file.replace(/\.sql$/i, ''),
|
|
35
|
+
sql: (0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, file), 'utf-8'),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
function requireMigrations(root) {
|
|
39
|
+
const migrations = readMigrations(root);
|
|
40
|
+
if (migrations.length === 0) {
|
|
41
|
+
throw new errors_1.CliError(`В проекте нет миграций`, `Положите SQL в ${MIGRATIONS_DIR}/0001_init.sql и повторите`);
|
|
42
|
+
}
|
|
43
|
+
return migrations;
|
|
44
|
+
}
|
|
45
|
+
async function dbStatus() {
|
|
46
|
+
const { root, config } = (0, config_1.requireProject)();
|
|
47
|
+
const client = (0, session_1.connect)(config);
|
|
48
|
+
const local = readMigrations(root);
|
|
49
|
+
const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
|
|
50
|
+
const applied = new Map(history.applied.map((row) => [row.name, row]));
|
|
51
|
+
(0, ui_1.out)(`Схема: ${(0, ui_1.bold)(history.schema)}`);
|
|
52
|
+
(0, ui_1.out)('');
|
|
53
|
+
if (local.length === 0 && applied.size === 0) {
|
|
54
|
+
(0, ui_1.note)(`Миграций нет. Первая: ${MIGRATIONS_DIR}/0001_init.sql`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const migration of local) {
|
|
59
|
+
const row = applied.get(migration.name);
|
|
60
|
+
if (!row) {
|
|
61
|
+
rows.push([migration.name, 'ждёт применения', '']);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const changed = row.checksum !== checksum(migration.sql);
|
|
65
|
+
rows.push([
|
|
66
|
+
migration.name,
|
|
67
|
+
changed ? 'применена, файл изменён' : 'применена',
|
|
68
|
+
(0, ui_1.formatAge)(row.applied_at),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
// Применённое, чего нет локально: чаще всего чужая миграция из другого проекта
|
|
72
|
+
// на той же логической базе.
|
|
73
|
+
const localNames = new Set(local.map((migration) => migration.name));
|
|
74
|
+
for (const row of history.applied) {
|
|
75
|
+
if (!localNames.has(row.name))
|
|
76
|
+
rows.push([row.name, 'применена, файла нет', (0, ui_1.formatAge)(row.applied_at)]);
|
|
77
|
+
}
|
|
78
|
+
(0, ui_1.table)(rows);
|
|
79
|
+
if (rows.some((row) => row[1] === 'применена, файл изменён')) {
|
|
80
|
+
(0, ui_1.out)('');
|
|
81
|
+
(0, ui_1.warn)('Изменённый файл повторно не применится: заведите новую миграцию');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function dbMigrate(args) {
|
|
85
|
+
const { root, config } = (0, config_1.requireProject)();
|
|
86
|
+
const client = (0, session_1.connect)(config);
|
|
87
|
+
const migrations = requireMigrations(root);
|
|
88
|
+
const dryRun = (0, args_1.flagBool)(args, 'dry-run');
|
|
89
|
+
(0, ui_1.step)(dryRun ? `Проверяю миграции (${migrations.length} в каталоге)` : `Применяю миграции (${migrations.length} в каталоге)`);
|
|
90
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: {
|
|
93
|
+
migrations,
|
|
94
|
+
dry_run: dryRun,
|
|
95
|
+
allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive'),
|
|
96
|
+
},
|
|
97
|
+
timeoutMs: 300_000,
|
|
98
|
+
});
|
|
99
|
+
if (result.destructive.length > 0) {
|
|
100
|
+
(0, ui_1.warn)('Разрушающие операции:');
|
|
101
|
+
for (const hit of result.destructive)
|
|
102
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${hit}`));
|
|
103
|
+
}
|
|
104
|
+
if (result.dry_run) {
|
|
105
|
+
if (result.pending.length === 0) {
|
|
106
|
+
(0, ui_1.ok)('Применять нечего: все миграции уже в базе');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
(0, ui_1.out)('');
|
|
110
|
+
(0, ui_1.out)(`Будут применены (${result.pending.length}):`);
|
|
111
|
+
for (const name of result.pending)
|
|
112
|
+
(0, ui_1.out)(` ${name}`);
|
|
113
|
+
(0, ui_1.note)((0, ui_1.dim)(' Пробный прогон: база не менялась'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const dump of result.dumps) {
|
|
117
|
+
(0, ui_1.note)((0, ui_1.dim)(` Дамп ${dump.table}: строк ${dump.rows}${dump.truncated ? ' (обрезан по потолку)' : ''}, хранится ${result.dump_retention_days} суток`));
|
|
118
|
+
}
|
|
119
|
+
if (result.applied.length > 0) {
|
|
120
|
+
(0, ui_1.ok)(`Применено: ${result.applied.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
else if (!result.failed) {
|
|
123
|
+
(0, ui_1.ok)('Применять нечего: все миграции уже в базе');
|
|
124
|
+
}
|
|
125
|
+
if (result.failed) {
|
|
126
|
+
throw new errors_1.CliError(`${result.failed.name}: ${result.failed.error}`, result.applied.length > 0
|
|
127
|
+
? `Применённое до неё осталось в базе (${result.applied.join(', ')}), остальное не запускалось`
|
|
128
|
+
: 'База не изменилась: миграция откатилась целиком');
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -14,6 +14,7 @@ const session_1 = require("../session");
|
|
|
14
14
|
const tree_1 = require("../tree");
|
|
15
15
|
const zip_1 = require("../zip");
|
|
16
16
|
const ui_1 = require("../ui");
|
|
17
|
+
const functions_1 = require("./functions");
|
|
17
18
|
const sources_1 = require("./sources");
|
|
18
19
|
/** Потолок артефакта на сервере. */
|
|
19
20
|
const MAX_ARTIFACT_BYTES = 100 * 1024 * 1024;
|
|
@@ -55,6 +56,8 @@ async function deploy(args) {
|
|
|
55
56
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
56
57
|
}
|
|
57
58
|
if (!(0, args_1.flagBool)(args, 'skip-build')) {
|
|
59
|
+
// Перед сборкой, а не после: адреса функций попадают в бандл на этом шаге.
|
|
60
|
+
await (0, functions_1.refreshFunctionsEnv)(root, client, config.projectId);
|
|
58
61
|
(0, ui_1.step)(`Сборка: ${buildCommand}`);
|
|
59
62
|
await runBuild(buildCommand, root);
|
|
60
63
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.functionsList = functionsList;
|
|
4
|
+
exports.refreshFunctionsEnv = refreshFunctionsEnv;
|
|
5
|
+
exports.functionsInvoke = functionsInvoke;
|
|
4
6
|
exports.functionsDeploy = functionsDeploy;
|
|
5
7
|
const node_fs_1 = require("node:fs");
|
|
6
8
|
const node_path_1 = require("node:path");
|
|
7
9
|
const api_1 = require("../api");
|
|
10
|
+
const args_1 = require("../args");
|
|
8
11
|
const config_1 = require("../config");
|
|
9
12
|
const errors_1 = require("../errors");
|
|
10
13
|
const session_1 = require("../session");
|
|
14
|
+
const template_1 = require("../template");
|
|
11
15
|
const ui_1 = require("../ui");
|
|
12
16
|
const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
|
|
13
17
|
/** Папка с функциями внутри проекта. Та же, что была до пивота: менять её незачем. */
|
|
@@ -77,6 +81,84 @@ async function functionsList() {
|
|
|
77
81
|
fn.error_message ?? fn.invoke_url ?? '',
|
|
78
82
|
]));
|
|
79
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Записать адреса функций в `.env`, откуда их заберёт сборщик.
|
|
86
|
+
*
|
|
87
|
+
* Подставляем на сборке, а не спрашиваем у платформы в рантайме: иначе каждый
|
|
88
|
+
* запуск приложения начинался бы с похода к нам, и мы стали бы обязательным
|
|
89
|
+
* участником работы чужого приложения.
|
|
90
|
+
*/
|
|
91
|
+
async function refreshFunctionsEnv(root, client, projectId) {
|
|
92
|
+
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
93
|
+
// Файла может не быть вовсе: в исходники он не уходит, а клон делают из git.
|
|
94
|
+
// Дописывать в такой один адрес функции бессмысленно — без токена вызов всё
|
|
95
|
+
// равно получит 401, поэтому восстанавливаем целиком, как это делает link.
|
|
96
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
|
|
97
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, '.env'), (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions), 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
(0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
|
|
101
|
+
}
|
|
102
|
+
return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
|
|
103
|
+
}
|
|
104
|
+
/** Ответ функции: JSON разворачиваем, прочее отдаём как есть. */
|
|
105
|
+
function prettyBody(text) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Вызвать функцию так же, как её вызывает приложение.
|
|
115
|
+
*
|
|
116
|
+
* Токен проекта берём из карточки на платформе, а не из локального `.env`: в
|
|
117
|
+
* свежем клоне файла нет вовсе, а команда должна работать сразу после link.
|
|
118
|
+
*/
|
|
119
|
+
async function functionsInvoke(args) {
|
|
120
|
+
const { config } = (0, config_1.requireProject)();
|
|
121
|
+
const client = (0, session_1.connect)(config);
|
|
122
|
+
const name = args.words[1];
|
|
123
|
+
if (!name)
|
|
124
|
+
throw new errors_1.CliError('Нужно имя функции', 'Что выложено: xflow functions list');
|
|
125
|
+
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
|
|
126
|
+
const fn = card.functions.find((item) => item.name === name);
|
|
127
|
+
if (!fn || !fn.invoke_url) {
|
|
128
|
+
throw new errors_1.CliError(`Функция ${name} не выложена`, card.functions.length > 0
|
|
129
|
+
? `Выложены: ${card.functions.map((item) => item.name).join(', ')}`
|
|
130
|
+
: `Выкатить: xflow functions deploy ${name}`);
|
|
131
|
+
}
|
|
132
|
+
const data = (0, args_1.flagString)(args, 'data');
|
|
133
|
+
const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
|
|
134
|
+
const sendsBody = method !== 'GET' && method !== 'HEAD';
|
|
135
|
+
const started = Date.now();
|
|
136
|
+
let response;
|
|
137
|
+
try {
|
|
138
|
+
response = await fetch(fn.invoke_url, {
|
|
139
|
+
method,
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
'X-Project-Token': card.project_token ?? '',
|
|
143
|
+
},
|
|
144
|
+
body: sendsBody ? (data ?? '{}') : undefined,
|
|
145
|
+
// У функции свой потолок в 30 секунд: ждём чуть дольше, чтобы увидеть её
|
|
146
|
+
// собственный таймаут, а не свой.
|
|
147
|
+
signal: AbortSignal.timeout(40_000),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
throw new errors_1.CliError(`Функция не ответила: ${e instanceof Error ? e.message : String(e)}`, 'Проверьте, что она выложена: xflow functions list');
|
|
152
|
+
}
|
|
153
|
+
const elapsed = Date.now() - started;
|
|
154
|
+
const text = await response.text();
|
|
155
|
+
(0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} за ${elapsed} мс`));
|
|
156
|
+
if (text)
|
|
157
|
+
(0, ui_1.out)(prettyBody(text));
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new errors_1.CliError(`Функция ответила ${response.status}`, `Стек и вывод консоли: xflow functions logs ${name}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
80
162
|
async function functionsDeploy(args) {
|
|
81
163
|
const { root, config } = (0, config_1.requireProject)();
|
|
82
164
|
const client = (0, session_1.connect)(config);
|
|
@@ -100,4 +182,6 @@ async function functionsDeploy(args) {
|
|
|
100
182
|
(0, ui_1.note)((0, ui_1.dim)(` Секреты организации в окружении: ${result.secrets.join(', ')}`));
|
|
101
183
|
}
|
|
102
184
|
}
|
|
185
|
+
const available = await refreshFunctionsEnv(root, client, config.projectId);
|
|
186
|
+
(0, ui_1.note)((0, ui_1.dim)(` Адреса в .env обновлены (${available.join(', ')}). Во фронтенде: xflow.functions.invoke('${names[0]}')`));
|
|
103
187
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logs = logs;
|
|
4
|
+
exports.functionsLogs = functionsLogs;
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const args_1 = require("../args");
|
|
7
|
+
const config_1 = require("../config");
|
|
8
|
+
const session_1 = require("../session");
|
|
9
|
+
const ui_1 = require("../ui");
|
|
10
|
+
const DEFAULT_LIMIT = 10;
|
|
11
|
+
/** Стек с хвостом консоли бывает длинным: показываем начало, оно и есть причина. */
|
|
12
|
+
const STACK_MAX_LINES = 14;
|
|
13
|
+
function stamp(iso) {
|
|
14
|
+
const date = new Date(iso);
|
|
15
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
16
|
+
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
17
|
+
}
|
|
18
|
+
async function fetchLogs(source, name, args) {
|
|
19
|
+
const { config } = (0, config_1.requireProject)();
|
|
20
|
+
const client = (0, session_1.connect)(config);
|
|
21
|
+
const query = new URLSearchParams({ source, limit: String((0, args_1.flagNumber)(args, 'limit') ?? DEFAULT_LIMIT) });
|
|
22
|
+
if (name)
|
|
23
|
+
query.set('name', name);
|
|
24
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/logs?${query.toString()}`);
|
|
25
|
+
return data.logs;
|
|
26
|
+
}
|
|
27
|
+
/** Свежее — внизу: так последняя ошибка оказывается перед глазами, а не уезжает вверх. */
|
|
28
|
+
function render(rows) {
|
|
29
|
+
for (const row of [...rows].reverse()) {
|
|
30
|
+
const label = row.source === 'function' ? (row.function ?? 'функция') : 'браузер';
|
|
31
|
+
(0, ui_1.out)(`${(0, ui_1.dim)(stamp(row.timestamp))} ${(0, ui_1.bold)(label)} ${row.message}`);
|
|
32
|
+
if (row.stack) {
|
|
33
|
+
const lines = row.stack.split('\n');
|
|
34
|
+
for (const line of lines.slice(0, STACK_MAX_LINES))
|
|
35
|
+
(0, ui_1.out)((0, ui_1.dim)(` ${line}`));
|
|
36
|
+
if (lines.length > STACK_MAX_LINES)
|
|
37
|
+
(0, ui_1.out)((0, ui_1.dim)(` … ещё строк: ${lines.length - STACK_MAX_LINES}`));
|
|
38
|
+
}
|
|
39
|
+
(0, ui_1.out)('');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function logs(args) {
|
|
43
|
+
const rows = await fetchLogs('client', undefined, args);
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
(0, ui_1.note)('Ошибок из браузера нет');
|
|
46
|
+
(0, ui_1.note)((0, ui_1.dim)(' Сюда попадают падения выложенного приложения, а не локального npm run dev'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
render(rows);
|
|
50
|
+
}
|
|
51
|
+
async function functionsLogs(args) {
|
|
52
|
+
const name = args.words[1];
|
|
53
|
+
const rows = await fetchLogs('function', name, args);
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
(0, ui_1.note)(name ? `Функция ${name} не падала` : 'Функции не падали');
|
|
56
|
+
(0, ui_1.note)((0, ui_1.dim)(' Сюда попадают только неудачные вызовы: успешные ничего не пишут'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
render(rows);
|
|
60
|
+
}
|
|
@@ -128,8 +128,8 @@ async function link(args) {
|
|
|
128
128
|
// В исходники .env не уходит, поэтому в свежем клоне его нет вовсе, и приложение
|
|
129
129
|
// молча теряет доступ к облачным функциям. Восстанавливаем, но чужой не трогаем.
|
|
130
130
|
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.env')) && card.project_token) {
|
|
131
|
-
write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl));
|
|
132
|
-
(0, ui_1.note)((0, ui_1.dim)(' Создан .env с токеном проекта'));
|
|
131
|
+
write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
|
|
132
|
+
(0, ui_1.note)((0, ui_1.dim)(' Создан .env с токеном проекта и адресами функций'));
|
|
133
133
|
}
|
|
134
134
|
(0, ui_1.ok)(`Папка связана с проектом «${card.name}»`);
|
|
135
135
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.schedulesList = schedulesList;
|
|
4
|
+
exports.schedulesSet = schedulesSet;
|
|
5
|
+
exports.schedulesRemove = schedulesRemove;
|
|
6
|
+
const api_1 = require("../api");
|
|
7
|
+
const args_1 = require("../args");
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
const errors_1 = require("../errors");
|
|
10
|
+
const session_1 = require("../session");
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
async function schedulesList() {
|
|
13
|
+
const { config } = (0, config_1.requireProject)();
|
|
14
|
+
const client = (0, session_1.connect)(config);
|
|
15
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`);
|
|
16
|
+
if (data.schedules.length === 0) {
|
|
17
|
+
(0, ui_1.note)('Расписаний нет');
|
|
18
|
+
(0, ui_1.note)((0, ui_1.dim)(' Запускать функцию по времени: xflow schedules set <функция> "0 3 ? * * *"'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
(0, ui_1.table)(data.schedules.map((row) => [
|
|
22
|
+
row.function,
|
|
23
|
+
row.cron,
|
|
24
|
+
row.description === row.cron ? '' : row.description,
|
|
25
|
+
row.status === 'active' ? '' : (row.error ?? row.status),
|
|
26
|
+
]));
|
|
27
|
+
}
|
|
28
|
+
async function schedulesSet(args) {
|
|
29
|
+
const { config } = (0, config_1.requireProject)();
|
|
30
|
+
const client = (0, session_1.connect)(config);
|
|
31
|
+
const functionName = args.words[1];
|
|
32
|
+
const cron = args.words[2];
|
|
33
|
+
if (!functionName || !cron) {
|
|
34
|
+
throw new errors_1.CliError('Нужны имя функции и расписание', 'Например: xflow schedules set nightly-report "0 3 ? * * *" — каждый день в 03:00 UTC');
|
|
35
|
+
}
|
|
36
|
+
const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: { function: functionName, cron, payload: (0, args_1.flagString)(args, 'payload') ?? null },
|
|
39
|
+
});
|
|
40
|
+
(0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} запускается ${row.description}`);
|
|
41
|
+
(0, ui_1.note)((0, ui_1.dim)(` Время в UTC. Проверить вручную: xflow functions invoke ${row.function}`));
|
|
42
|
+
}
|
|
43
|
+
async function schedulesRemove(args) {
|
|
44
|
+
const { config } = (0, config_1.requireProject)();
|
|
45
|
+
const client = (0, session_1.connect)(config);
|
|
46
|
+
const functionName = args.words[1];
|
|
47
|
+
if (!functionName) {
|
|
48
|
+
throw new errors_1.CliError('Нужно имя функции', 'Что запускается по времени: xflow schedules list');
|
|
49
|
+
}
|
|
50
|
+
const cron = args.words[2];
|
|
51
|
+
const query = new URLSearchParams({ function: functionName });
|
|
52
|
+
if (cron)
|
|
53
|
+
query.set('cron', cron);
|
|
54
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules?${query.toString()}`, { method: 'DELETE' });
|
|
55
|
+
if (result.removed === 0) {
|
|
56
|
+
(0, ui_1.note)(`У функции ${functionName} нет такого расписания`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
(0, ui_1.ok)(`Снято расписаний: ${result.removed}`);
|
|
60
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -12,121 +12,207 @@ function help(topic) {
|
|
|
12
12
|
(0, ui_1.out)(TOPICS[topic]);
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
(0, ui_1.out)(`${(0, ui_1.bold)('xflow')} ${(0, ui_1.dim)(version_1.CLI_VERSION)} — хостинг приложений XFlow
|
|
16
|
-
|
|
17
|
-
${(0, ui_1.bold)('Начало работы')}
|
|
18
|
-
xflow login вход через браузер
|
|
19
|
-
xflow init [папка] новый проект из шаблона платформы
|
|
20
|
-
xflow templates какие шаблоны доступны
|
|
21
|
-
xflow link <id> связать текущую папку с существующим проектом
|
|
22
|
-
xflow skills [--global] положить инструкцию о платформе для ИИ-агента
|
|
23
|
-
|
|
24
|
-
${(0, ui_1.bold)('Код')}
|
|
25
|
-
xflow status что на сервере и чем отличается локальная копия
|
|
26
|
-
xflow push [--force] отправить исходники новой ревизией
|
|
27
|
-
xflow pull [--into dir] [--revision N]
|
|
28
|
-
забрать исходники (по умолчанию последнюю ревизию)
|
|
29
|
-
|
|
30
|
-
${(0, ui_1.bold)('Выкладка')}
|
|
31
|
-
xflow deploy [--no-push] [--skip-build]
|
|
32
|
-
отправить код, собрать локально и выложить на dev
|
|
33
|
-
xflow publish показать dev-версию посетителям
|
|
34
|
-
xflow rollback <номер версии> вернуть dev-адрес на прошлую версию
|
|
35
|
-
xflow deployments история версий
|
|
36
|
-
|
|
37
|
-
${(0, ui_1.bold)('Функции')}
|
|
38
|
-
xflow functions list что выложено
|
|
39
|
-
xflow functions deploy [имя] собрать и выкатить функцию
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
xflow
|
|
43
|
-
xflow
|
|
44
|
-
xflow
|
|
45
|
-
|
|
46
|
-
xflow
|
|
47
|
-
|
|
48
|
-
${(0, ui_1.bold)('
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
15
|
+
(0, ui_1.out)(`${(0, ui_1.bold)('xflow')} ${(0, ui_1.dim)(version_1.CLI_VERSION)} — хостинг приложений XFlow
|
|
16
|
+
|
|
17
|
+
${(0, ui_1.bold)('Начало работы')}
|
|
18
|
+
xflow login вход через браузер
|
|
19
|
+
xflow init [папка] новый проект из шаблона платформы
|
|
20
|
+
xflow templates какие шаблоны доступны
|
|
21
|
+
xflow link <id> связать текущую папку с существующим проектом
|
|
22
|
+
xflow skills [--global] положить инструкцию о платформе для ИИ-агента
|
|
23
|
+
|
|
24
|
+
${(0, ui_1.bold)('Код')}
|
|
25
|
+
xflow status что на сервере и чем отличается локальная копия
|
|
26
|
+
xflow push [--force] отправить исходники новой ревизией
|
|
27
|
+
xflow pull [--into dir] [--revision N]
|
|
28
|
+
забрать исходники (по умолчанию последнюю ревизию)
|
|
29
|
+
|
|
30
|
+
${(0, ui_1.bold)('Выкладка')}
|
|
31
|
+
xflow deploy [--no-push] [--skip-build]
|
|
32
|
+
отправить код, собрать локально и выложить на dev
|
|
33
|
+
xflow publish показать dev-версию посетителям
|
|
34
|
+
xflow rollback <номер версии> вернуть dev-адрес на прошлую версию
|
|
35
|
+
xflow deployments история версий
|
|
36
|
+
|
|
37
|
+
${(0, ui_1.bold)('Функции')}
|
|
38
|
+
xflow functions list что выложено
|
|
39
|
+
xflow functions deploy [имя] собрать и выкатить функцию
|
|
40
|
+
xflow functions invoke <имя> [--data '{"a":1}']
|
|
41
|
+
вызвать функцию и показать ответ
|
|
42
|
+
xflow functions logs [имя] падения функций: стек и вывод консоли
|
|
43
|
+
xflow schedules list что запускается по времени
|
|
44
|
+
xflow schedules set <имя> "0 3 ? * * *"
|
|
45
|
+
запускать функцию по расписанию (UTC)
|
|
46
|
+
xflow schedules rm <имя> снять расписание
|
|
47
|
+
|
|
48
|
+
${(0, ui_1.bold)('База данных')}
|
|
49
|
+
xflow db status какие миграции применены, а какие ждут
|
|
50
|
+
xflow db migrate [--dry-run] [--allow-destructive]
|
|
51
|
+
применить миграции из migrations/*.sql
|
|
52
|
+
|
|
53
|
+
${(0, ui_1.bold)('Справочное')}
|
|
54
|
+
xflow logs [--limit N] ошибки выложенного приложения в браузере
|
|
55
|
+
xflow projects list проекты организации
|
|
56
|
+
xflow projects get [id] карточка проекта
|
|
57
|
+
xflow open [--live] открыть приложение в браузере
|
|
58
|
+
xflow whoami чей ключ и что он может
|
|
59
|
+
xflow logout забыть ключ
|
|
60
|
+
|
|
61
|
+
${(0, ui_1.bold)('Окружение')}
|
|
62
|
+
XFLOW_TOKEN ключ доступа (для CI: вместо xflow login)
|
|
63
|
+
XFLOW_API_URL адрес платформы, если он не app.getxflow.com
|
|
64
|
+
|
|
52
65
|
Подробнее о команде: xflow help <команда>`);
|
|
53
66
|
}
|
|
54
67
|
const TOPICS = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
db: `${(0, ui_1.bold)('xflow db migrate')} — применить миграции
|
|
69
|
+
|
|
70
|
+
Файлы лежат в репозитории: ${(0, ui_1.bold)('migrations/0001_init.sql')}, ${(0, ui_1.bold)('migrations/0002_orders.sql')} и так
|
|
71
|
+
далее. Порядок — по имени файла, поэтому номер в начале обязателен. Каждая миграция
|
|
72
|
+
применяется своей транзакцией, история хранится в самой базе, и повторно применённое
|
|
73
|
+
не запускается.
|
|
74
|
+
|
|
75
|
+
--dry-run показать, что применится, и не трогать базу
|
|
76
|
+
--allow-destructive разрешить операции, уничтожающие данные
|
|
77
|
+
|
|
78
|
+
Про разрушающие. Истории базы платформа не хранит и бэкапов не делает, поэтому
|
|
79
|
+
${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} и ${(0, ui_1.bold)('DELETE FROM')} без условия
|
|
80
|
+
отклоняются без явного флага. С флагом содержимое затронутых таблиц уезжает в дамп
|
|
81
|
+
и хранится 7 суток — это страховка «сразу заметил», а не резервная копия.
|
|
82
|
+
${(0, ui_1.bold)('DROP DATABASE')} не пропускается никогда: база общая для организации.
|
|
83
|
+
|
|
84
|
+
Изменять уже применённый файл бесполезно: сверка идёт по имени, а не по
|
|
85
|
+
содержимому. ${(0, ui_1.bold)('xflow db status')} такой файл покажет отдельно — заводите новую
|
|
86
|
+
миграцию.
|
|
87
|
+
|
|
88
|
+
Одна логическая база может быть привязана к нескольким проектам, поэтому в истории
|
|
89
|
+
попадаются миграции, которых нет в вашем репозитории. Это нормально, но означает,
|
|
90
|
+
что ваша миграция может сломать чужое приложение.`,
|
|
91
|
+
logs: `${(0, ui_1.bold)('xflow logs')} и ${(0, ui_1.bold)('xflow functions logs')} — ошибки проекта
|
|
92
|
+
|
|
93
|
+
Один поток на проект: падения выложенного приложения в браузере и падения
|
|
94
|
+
облачных функций. Хранятся последние 200 записей, дальше вытесняются.
|
|
95
|
+
|
|
96
|
+
--limit N сколько показать (по умолчанию 10)
|
|
97
|
+
|
|
98
|
+
Функция сообщает о падении сама: свои логи Yandex наружу отдаёт только по gRPC,
|
|
99
|
+
поэтому обёртка ловит исключение, уносит вместе с ним хвост консоли (последние 40
|
|
100
|
+
строк ${(0, ui_1.bold)('console.log')} этого вызова) и отправляет на платформу. Успешные вызовы не
|
|
101
|
+
пишут ничего: иначе каждый запрос платил бы за это задержкой.
|
|
102
|
+
|
|
103
|
+
Не попадёт сюда: падение на старте модуля (функция не успевает дойти до обёртки),
|
|
104
|
+
превышение 30 секунд и нехватка памяти. Такое видно ответом на
|
|
105
|
+
${(0, ui_1.bold)('xflow functions invoke')}.
|
|
106
|
+
|
|
107
|
+
Ошибки браузера собирает ${(0, ui_1.bold)('src/utils/error-logger.ts')} шаблона и только с
|
|
108
|
+
выложенных адресов: локальный ${(0, ui_1.bold)('npm run dev')} сюда не пишет.`,
|
|
109
|
+
schedules: `${(0, ui_1.bold)('xflow schedules')} — запуск функций по времени
|
|
110
|
+
|
|
111
|
+
Расписание это таймер-триггер Yandex: он зовёт функцию сам, без участия
|
|
112
|
+
приложения и браузера.
|
|
113
|
+
|
|
114
|
+
xflow schedules set отчёт "0 3 ? * * *" каждый день в 03:00
|
|
115
|
+
xflow schedules set сводка "0 */4 ? * * *" каждые 4 часа
|
|
116
|
+
xflow schedules rm отчёт снять все расписания функции
|
|
117
|
+
|
|
118
|
+
Формат из шести полей: ${(0, ui_1.bold)('минуты часы день-месяца месяц день-недели год')}. Ровно одно
|
|
119
|
+
из полей «День месяца» и «День недели» должно быть ${(0, ui_1.bold)('?')} — это не наша причуда, так
|
|
120
|
+
устроен Яндекс. ${(0, ui_1.bold)('Время всегда UTC')}, местное он не знает.
|
|
121
|
+
|
|
122
|
+
--payload '{"режим":"полный"}' тело, которое получит функция
|
|
123
|
+
|
|
124
|
+
Запуск по расписанию приходит хендлеру как POST без заголовков, и токен проекта
|
|
125
|
+
у него не проверяется: снаружи такое событие не подделать. Функция при этом должна
|
|
126
|
+
быть уже выложена — расписание ссылается на неё, а не наоборот.
|
|
127
|
+
|
|
128
|
+
Упавший запуск виден в ${(0, ui_1.bold)('xflow functions logs')}: по расписанию за функцией никто не
|
|
129
|
+
смотрит, поэтому обёртка отчитывается о падении так же, как при обычном вызове.`,
|
|
130
|
+
invoke: `${(0, ui_1.bold)('xflow functions invoke')} <имя> — вызвать функцию
|
|
131
|
+
|
|
132
|
+
Вызывает так же, как её вызывает приложение: с заголовком X-Project-Token.
|
|
133
|
+
Токен берётся из карточки проекта на платформе, локальный .env не нужен.
|
|
134
|
+
|
|
135
|
+
--data '{"a":1}' тело запроса (по умолчанию метод становится POST)
|
|
136
|
+
--method GET другой метод
|
|
137
|
+
|
|
138
|
+
Печатает статус, время ответа и тело. Ненулевой код возврата на статусе 4xx и
|
|
139
|
+
5xx: в CI такой вызов должен ронять шаг. Причину падения показывает
|
|
140
|
+
${(0, ui_1.bold)('xflow functions logs <имя>')}.`,
|
|
141
|
+
skills: `${(0, ui_1.bold)('xflow skills')} — инструкция о платформе для ИИ-агента
|
|
142
|
+
|
|
143
|
+
Кладёт файл в формате Agent Skills (agentskills.io): что такое XFlow, как выкладывать
|
|
144
|
+
и публиковать, откуда брать компоненты дизайн-системы, где смотреть ошибки прода.
|
|
145
|
+
Агент подхватывает его сам, когда задача про платформу, и не тратит контекст в
|
|
146
|
+
остальное время.
|
|
147
|
+
|
|
148
|
+
Файл общий для инструментов, а папки у них разные, поэтому кладём в обе:
|
|
149
|
+
${(0, ui_1.bold)('.claude/skills/xflow/')} читает Claude Code, ${(0, ui_1.bold)('.agents/skills/xflow/')} — Codex и OpenClaw.
|
|
150
|
+
|
|
151
|
+
--global положить в домашнюю папку, тогда скилл виден во всех проектах
|
|
152
|
+
|
|
153
|
+
${(0, ui_1.bold)('AGENTS.md')} и ${(0, ui_1.bold)('CLAUDE.md')} команда не трогает: это ваши файлы, в них ваши правила.
|
|
154
|
+
Наше знание живёт в своей папке и переустанавливается поверх без спора за файл.
|
|
155
|
+
|
|
70
156
|
Скилл едет вместе с CLI, поэтому после обновления пакета команду стоит повторить.`,
|
|
71
|
-
push: `${(0, ui_1.bold)('xflow push')} — отправить исходники
|
|
72
|
-
|
|
73
|
-
Уходит вся рабочая копия целиком, одной ревизией. Не отправляются: node_modules,
|
|
74
|
-
.git, dist, build, .next, любые .env, а также всё, что перечислено в .xflowignore
|
|
75
|
-
и в поле ignore файла xflow.json.
|
|
76
|
-
|
|
77
|
-
Если на сервере ревизия новее той, от которой вы работали, push отклоняется.
|
|
78
|
-
Это значит, что кто-то запушил раньше: заберите его правки рядом
|
|
79
|
-
(${(0, ui_1.bold)('xflow pull --into ./server-copy')}), сведите их у себя в git и повторите.
|
|
80
|
-
|
|
81
|
-
--force перетирает серверную ревизию. Перед этим CLI показывает, чью работу вы
|
|
82
|
-
затираете, и просит подтверждение вводом имени проекта. В неинтерактивном режиме
|
|
83
|
-
(CI, запуск агентом) подтвердить нельзя: расхождение версий должно ронять сборку,
|
|
157
|
+
push: `${(0, ui_1.bold)('xflow push')} — отправить исходники
|
|
158
|
+
|
|
159
|
+
Уходит вся рабочая копия целиком, одной ревизией. Не отправляются: node_modules,
|
|
160
|
+
.git, dist, build, .next, любые .env, а также всё, что перечислено в .xflowignore
|
|
161
|
+
и в поле ignore файла xflow.json.
|
|
162
|
+
|
|
163
|
+
Если на сервере ревизия новее той, от которой вы работали, push отклоняется.
|
|
164
|
+
Это значит, что кто-то запушил раньше: заберите его правки рядом
|
|
165
|
+
(${(0, ui_1.bold)('xflow pull --into ./server-copy')}), сведите их у себя в git и повторите.
|
|
166
|
+
|
|
167
|
+
--force перетирает серверную ревизию. Перед этим CLI показывает, чью работу вы
|
|
168
|
+
затираете, и просит подтверждение вводом имени проекта. В неинтерактивном режиме
|
|
169
|
+
(CI, запуск агентом) подтвердить нельзя: расхождение версий должно ронять сборку,
|
|
84
170
|
а не молча уничтожать чужую работу.`,
|
|
85
|
-
pull: `${(0, ui_1.bold)('xflow pull')} — забрать исходники
|
|
86
|
-
|
|
87
|
-
По умолчанию забирает последнюю ревизию в папку проекта и отказывается писать в
|
|
88
|
-
непустую: слить изменения платформа не умеет, это работа git.
|
|
89
|
-
|
|
90
|
-
--into <папка> выгрузить рядом, чтобы сравнить
|
|
91
|
-
--revision <N> конкретная ревизия (список: xflow deployments)
|
|
171
|
+
pull: `${(0, ui_1.bold)('xflow pull')} — забрать исходники
|
|
172
|
+
|
|
173
|
+
По умолчанию забирает последнюю ревизию в папку проекта и отказывается писать в
|
|
174
|
+
непустую: слить изменения платформа не умеет, это работа git.
|
|
175
|
+
|
|
176
|
+
--into <папка> выгрузить рядом, чтобы сравнить
|
|
177
|
+
--revision <N> конкретная ревизия (список: xflow deployments)
|
|
92
178
|
--force перезаписать папку целиком`,
|
|
93
|
-
deploy: `${(0, ui_1.bold)('xflow deploy')} — собрать и выложить
|
|
94
|
-
|
|
95
|
-
Три шага: отправка исходников, локальная сборка, заливка результата. Команда
|
|
96
|
-
сборки и каталог результата берутся из xflow.json (по умолчанию npm run build
|
|
97
|
-
и dist).
|
|
98
|
-
|
|
99
|
-
--no-push не отправлять исходники, выложить из последней серверной ревизии
|
|
100
|
-
--skip-build не собирать, взять готовый каталог сборки
|
|
101
|
-
--force разрешить перезапись серверной ревизии при отправке
|
|
102
|
-
|
|
103
|
-
Сборка идёт на вашей машине, поэтому её результат зависит от вашей версии Node.
|
|
104
|
-
Платформа раздаёт статику: сборка без index.html в корне отклоняется.
|
|
105
|
-
|
|
179
|
+
deploy: `${(0, ui_1.bold)('xflow deploy')} — собрать и выложить
|
|
180
|
+
|
|
181
|
+
Три шага: отправка исходников, локальная сборка, заливка результата. Команда
|
|
182
|
+
сборки и каталог результата берутся из xflow.json (по умолчанию npm run build
|
|
183
|
+
и dist).
|
|
184
|
+
|
|
185
|
+
--no-push не отправлять исходники, выложить из последней серверной ревизии
|
|
186
|
+
--skip-build не собирать, взять готовый каталог сборки
|
|
187
|
+
--force разрешить перезапись серверной ревизии при отправке
|
|
188
|
+
|
|
189
|
+
Сборка идёт на вашей машине, поэтому её результат зависит от вашей версии Node.
|
|
190
|
+
Платформа раздаёт статику: сборка без index.html в корне отклоняется.
|
|
191
|
+
|
|
106
192
|
Выложенное видно только по dev-адресу. Посетителям — после ${(0, ui_1.bold)('xflow publish')}.`,
|
|
107
|
-
init: `${(0, ui_1.bold)('xflow init')} [папка] — новый проект
|
|
108
|
-
|
|
109
|
-
Разворачивает шаблон платформы: React на Vite, Tailwind, набор компонентов
|
|
110
|
-
(${(0, ui_1.bold)('src/components/ui')}) и готовые блоки (${(0, ui_1.bold)('src/components/blocks')}) — таблица,
|
|
111
|
-
форма, фильтры, канбан, графики. Тот же шаблон получают проекты, созданные в вебе,
|
|
112
|
-
поэтому приложения выглядят одинаково.
|
|
113
|
-
|
|
114
|
-
Строить интерфейс поверх этих компонентов и токенов из ${(0, ui_1.bold)('src/index.css')} — не
|
|
115
|
-
формальность: своя палитра поверх них выглядит чужеродно внутри платформы.
|
|
116
|
-
|
|
117
|
-
--name <имя> имя проекта (по умолчанию — имя папки)
|
|
118
|
-
--template <id> другой шаблон (список: xflow templates)
|
|
119
|
-
--database <id> привязать логическую базу организации
|
|
120
|
-
|
|
121
|
-
Порядок: проект заводится на платформе первым, потому что без него неоткуда взять
|
|
122
|
-
токен для .env. Если запись файлов сорвётся, проект останется пустым, и CLI скажет,
|
|
193
|
+
init: `${(0, ui_1.bold)('xflow init')} [папка] — новый проект
|
|
194
|
+
|
|
195
|
+
Разворачивает шаблон платформы: React на Vite, Tailwind, набор компонентов
|
|
196
|
+
(${(0, ui_1.bold)('src/components/ui')}) и готовые блоки (${(0, ui_1.bold)('src/components/blocks')}) — таблица,
|
|
197
|
+
форма, фильтры, канбан, графики. Тот же шаблон получают проекты, созданные в вебе,
|
|
198
|
+
поэтому приложения выглядят одинаково.
|
|
199
|
+
|
|
200
|
+
Строить интерфейс поверх этих компонентов и токенов из ${(0, ui_1.bold)('src/index.css')} — не
|
|
201
|
+
формальность: своя палитра поверх них выглядит чужеродно внутри платформы.
|
|
202
|
+
|
|
203
|
+
--name <имя> имя проекта (по умолчанию — имя папки)
|
|
204
|
+
--template <id> другой шаблон (список: xflow templates)
|
|
205
|
+
--database <id> привязать логическую базу организации
|
|
206
|
+
|
|
207
|
+
Порядок: проект заводится на платформе первым, потому что без него неоткуда взять
|
|
208
|
+
токен для .env. Если запись файлов сорвётся, проект останется пустым, и CLI скажет,
|
|
123
209
|
как подобрать его командой link.`,
|
|
124
|
-
rollback: `${(0, ui_1.bold)('xflow rollback')} <номер версии> — вернуть прошлую сборку
|
|
125
|
-
|
|
126
|
-
Переключает dev-адрес на выбранную версию. Возвращается только сборка: исходники
|
|
127
|
-
остаются на своей ревизии, и вернуть их — отдельное решение
|
|
128
|
-
(${(0, ui_1.bold)('xflow pull --revision N --into ./старая-версия')}).
|
|
129
|
-
|
|
130
|
-
Посетители продолжают видеть опубликованную версию, пока не выполнен
|
|
210
|
+
rollback: `${(0, ui_1.bold)('xflow rollback')} <номер версии> — вернуть прошлую сборку
|
|
211
|
+
|
|
212
|
+
Переключает dev-адрес на выбранную версию. Возвращается только сборка: исходники
|
|
213
|
+
остаются на своей ревизии, и вернуть их — отдельное решение
|
|
214
|
+
(${(0, ui_1.bold)('xflow pull --revision N --into ./старая-версия')}).
|
|
215
|
+
|
|
216
|
+
Посетители продолжают видеть опубликованную версию, пока не выполнен
|
|
131
217
|
${(0, ui_1.bold)('xflow publish')}.`,
|
|
132
218
|
};
|
package/dist/template.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Файлы, которые CLI добавляет поверх шаблона платформы.
|
|
4
|
-
*
|
|
5
|
-
* Сам шаблон приезжает архивом с платформы (`GET /api/v1/templates/{id}/archive`)
|
|
6
|
-
* и здесь не дублируется: своя копия неизбежно разъехалась бы с той, из которой
|
|
7
|
-
* создаются проекты в вебе, и приложения перестали бы выглядеть одинаково.
|
|
8
|
-
*
|
|
9
|
-
* Здесь только то, чего в шаблоне нет и быть не может: `.env` с токеном
|
|
10
|
-
* конкретного проекта и правила для git.
|
|
11
|
-
*/
|
|
12
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FUNCTIONS_ENV_KEY = void 0;
|
|
4
|
+
exports.functionsEnvValue = functionsEnvValue;
|
|
13
5
|
exports.envFile = envFile;
|
|
6
|
+
exports.writeEnvValue = writeEnvValue;
|
|
14
7
|
exports.scaffoldFiles = scaffoldFiles;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
15
10
|
const GITIGNORE = `node_modules/
|
|
16
11
|
dist/
|
|
17
12
|
.xflow/
|
|
@@ -36,12 +31,46 @@ xflow publish # показать dev-версию посетителям
|
|
|
36
31
|
|
|
37
32
|
Полезное: \`xflow status\` — что на сервере, \`xflow deployments\` — история версий.
|
|
38
33
|
`;
|
|
34
|
+
/** Адреса облачных функций для сборки: имя → URL, одной строкой JSON. */
|
|
35
|
+
exports.FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
|
|
36
|
+
function functionsEnvValue(functions) {
|
|
37
|
+
const map = {};
|
|
38
|
+
for (const fn of functions) {
|
|
39
|
+
if (fn.invoke_url)
|
|
40
|
+
map[fn.name] = fn.invoke_url;
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(map);
|
|
43
|
+
}
|
|
39
44
|
/**
|
|
40
45
|
* `.env` в исходники не уходит (его пропускают и push, и сервер), поэтому в свежем
|
|
41
46
|
* клоне его восстанавливает `xflow link`.
|
|
42
47
|
*/
|
|
43
|
-
function envFile(projectToken, apiUrl) {
|
|
44
|
-
return
|
|
48
|
+
function envFile(projectToken, apiUrl, functions = []) {
|
|
49
|
+
return [
|
|
50
|
+
`VITE_XFLOW_PROJECT_TOKEN=${projectToken}`,
|
|
51
|
+
`VITE_XFLOW_API_URL=${apiUrl.replace(/^https?:\/\//, '')}`,
|
|
52
|
+
`${exports.FUNCTIONS_ENV_KEY}=${functionsEnvValue(functions)}`,
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Обновить одну переменную в `.env`, не трогая остальные.
|
|
58
|
+
*
|
|
59
|
+
* Файл наш, но живёт у разработчика: рядом с нашими тремя строками у него может
|
|
60
|
+
* лежать что угодно своё, и переписывать файл целиком мы не вправе.
|
|
61
|
+
*/
|
|
62
|
+
function writeEnvValue(root, key, value) {
|
|
63
|
+
const path = (0, node_path_1.join)(root, '.env');
|
|
64
|
+
const line = `${key}=${value}`;
|
|
65
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
66
|
+
(0, node_fs_1.writeFileSync)(path, `${line}\n`, 'utf-8');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const text = (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
70
|
+
const existing = new RegExp(`^${key}=.*$`, 'm');
|
|
71
|
+
const next = existing.test(text) ? text.replace(existing, line) : `${text.replace(/\n*$/, '\n')}${line}\n`;
|
|
72
|
+
if (next !== text)
|
|
73
|
+
(0, node_fs_1.writeFileSync)(path, next, 'utf-8');
|
|
45
74
|
}
|
|
46
75
|
function scaffoldFiles(projectName) {
|
|
47
76
|
return [
|
package/dist/version.js
CHANGED
|
@@ -6,6 +6,6 @@ exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
|
6
6
|
* рантайме нельзя, после сборки он лежит на уровень выше dist и в бандл не
|
|
7
7
|
* попадает.
|
|
8
8
|
*/
|
|
9
|
-
exports.CLI_VERSION = '0.1.
|
|
9
|
+
exports.CLI_VERSION = '0.1.4';
|
|
10
10
|
/** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
|
|
11
11
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
package/skills/xflow/SKILL.md
CHANGED
|
@@ -43,13 +43,44 @@ with `xflow functions deploy` (one name to deploy a single function, no name for
|
|
|
43
43
|
list what is live with `xflow functions list`. The handler returns
|
|
44
44
|
`{ statusCode, body }` where `body` is a JSON string.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
the app
|
|
48
|
-
|
|
46
|
+
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
47
|
+
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
48
|
+
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
49
|
+
output of that call. Only failed calls are logged, so an empty output means the function
|
|
50
|
+
never crashed, not that logging is broken.
|
|
51
|
+
|
|
52
|
+
From the app, call a function through `src/lib/xflow.ts`:
|
|
53
|
+
`await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the project
|
|
54
|
+
token for you. Addresses are baked into the build: the CLI writes them to `.env` when
|
|
55
|
+
you deploy a function, so a frontend built before the function existed cannot see it,
|
|
56
|
+
and needs `xflow deploy` again.
|
|
57
|
+
|
|
58
|
+
Treat a function as a public API: the token ships inside the frontend bundle, so anyone
|
|
59
|
+
who opens the app can call it.
|
|
49
60
|
|
|
50
61
|
Organization secrets reach a function only if it mentions them via `process.env`, so
|
|
51
62
|
read them by name and do not build variable names dynamically.
|
|
52
63
|
|
|
64
|
+
To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
|
|
65
|
+
Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
|
|
66
|
+
how Yandex wants it. A scheduled run reaches the handler as a POST with no headers.
|
|
67
|
+
|
|
68
|
+
## Database
|
|
69
|
+
|
|
70
|
+
Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql`, applied
|
|
71
|
+
in filename order by `xflow db migrate`. `xflow db status` shows what is applied and what
|
|
72
|
+
waits. History lives in the database itself, so an already applied file is never re-run and
|
|
73
|
+
editing it changes nothing: write a new migration instead.
|
|
74
|
+
|
|
75
|
+
The platform keeps no database history and no backups. Anything that destroys data
|
|
76
|
+
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) is refused
|
|
77
|
+
unless you pass `--allow-destructive`, and with that flag the affected tables are dumped
|
|
78
|
+
first and kept for 7 days. Check with `--dry-run` before applying.
|
|
79
|
+
|
|
80
|
+
One logical database can be shared by several projects, so your migration can break an app
|
|
81
|
+
you do not see. `xflow db status` lists applied migrations that have no file in your
|
|
82
|
+
repository: that is what someone else's project did.
|
|
83
|
+
|
|
53
84
|
## Syncing code
|
|
54
85
|
|
|
55
86
|
`xflow status` shows how the local copy differs from the server revision.
|
|
@@ -89,9 +120,13 @@ list of organization members: that is what `user-picker` is built on.
|
|
|
89
120
|
|
|
90
121
|
## Errors from a deployed app
|
|
91
122
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
`xflow logs` prints what broke in the browser on deployed addresses: unhandled errors,
|
|
124
|
+
rejected promises and 5xx responses. The same stream, functions included, is in the
|
|
125
|
+
platform UI: open the project, section «Облако» (Cloud), tab «Логи» (Logs). The last 200
|
|
126
|
+
records per project are kept.
|
|
127
|
+
|
|
128
|
+
Local `npm run dev` does not report anything: these logs exist for what you cannot open
|
|
129
|
+
in your own devtools.
|
|
95
130
|
|
|
96
131
|
## Environment
|
|
97
132
|
|