@getxflow/cli 0.1.8 → 0.1.10
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 +31 -30
- package/dist/api.js +14 -23
- package/dist/args.js +3 -11
- package/dist/bin.js +10 -9
- package/dist/commands/auth.js +48 -41
- package/dist/commands/db.js +21 -28
- package/dist/commands/deploy.js +21 -36
- package/dist/commands/env.js +22 -31
- package/dist/commands/functions.js +26 -48
- package/dist/commands/logs.js +7 -8
- package/dist/commands/mcp.js +20 -27
- package/dist/commands/projects.js +27 -39
- package/dist/commands/schedules.js +8 -8
- package/dist/commands/skills.js +24 -48
- package/dist/commands/sources.js +38 -47
- package/dist/config.js +13 -13
- package/dist/credentials.js +9 -10
- package/dist/errors.js +1 -1
- package/dist/help.js +215 -216
- package/dist/limits.js +68 -0
- package/dist/session.js +6 -10
- package/dist/template.js +13 -21
- package/dist/tree.js +5 -18
- package/dist/types.js +1 -8
- package/dist/ui.js +47 -25
- package/dist/version.js +3 -7
- package/dist/zip.js +22 -15
- package/package.json +22 -22
- package/skills/xflow/SKILL.md +19 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -12,18 +12,9 @@ const session_1 = require("../session");
|
|
|
12
12
|
const ui_1 = require("../ui");
|
|
13
13
|
const sources_1 = require("./sources");
|
|
14
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
-
/**
|
|
16
|
-
* Потолок ожидания сборки в терминале. Чуть больше, чем срок, после которого
|
|
17
|
-
* платформа закрывает молчащую сборку: так разработчик увидит внятный отказ, а
|
|
18
|
-
* не «идёт дольше ожидаемого» на сборке, которую уже похоронили.
|
|
19
|
-
*/
|
|
15
|
+
/** Slightly longer than the platform's own build timeout. */
|
|
20
16
|
const WAIT_LIMIT_MS = 16 * 60_000;
|
|
21
|
-
/**
|
|
22
|
-
* Показать, чем проект не подошёл платформе.
|
|
23
|
-
*
|
|
24
|
-
* Печатаем весь список: правила известны заранее (скилл xflow), а узнавать их по
|
|
25
|
-
* одному через повторные запуски значит терять круг на каждое нарушение.
|
|
26
|
-
*/
|
|
17
|
+
/** Print every check violation at once. */
|
|
27
18
|
function reportIssues(e) {
|
|
28
19
|
const issues = e.issues ?? [];
|
|
29
20
|
for (const issue of issues) {
|
|
@@ -33,13 +24,7 @@ function reportIssues(e) {
|
|
|
33
24
|
}
|
|
34
25
|
throw new errors_1.CliError(e.message, e.hint);
|
|
35
26
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Дождаться конца сборки, печатая фазы.
|
|
38
|
-
*
|
|
39
|
-
* Платформа отвечает на запуск сразу и не держит соединение: сборка живёт в
|
|
40
|
-
* песочнице и переживает перезапуск платформы, поэтому её состояние забирается
|
|
41
|
-
* опросом, а не потоком.
|
|
42
|
-
*/
|
|
27
|
+
/** Poll the build until it finishes, printing phases. */
|
|
43
28
|
async function waitForBuild(client, projectId, started) {
|
|
44
29
|
const deadline = Date.now() + WAIT_LIMIT_MS;
|
|
45
30
|
let shown = '';
|
|
@@ -53,7 +38,7 @@ async function waitForBuild(client, projectId, started) {
|
|
|
53
38
|
if (build.phase === 'done' || build.phase === 'failed')
|
|
54
39
|
return build;
|
|
55
40
|
}
|
|
56
|
-
throw new errors_1.CliError('
|
|
41
|
+
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
|
|
57
42
|
}
|
|
58
43
|
async function deploy(args) {
|
|
59
44
|
const { root, config } = (0, config_1.requireProject)();
|
|
@@ -62,15 +47,15 @@ async function deploy(args) {
|
|
|
62
47
|
if ((0, args_1.flagBool)(args, 'no-push')) {
|
|
63
48
|
const server = await (0, sources_1.latestRevision)(client, config.projectId);
|
|
64
49
|
if (!server) {
|
|
65
|
-
throw new errors_1.CliError('
|
|
50
|
+
throw new errors_1.CliError('The server holds no sources', 'Send them: xflow push');
|
|
66
51
|
}
|
|
67
52
|
revision = server.revision;
|
|
68
|
-
(0, ui_1.warn)(
|
|
53
|
+
(0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
|
|
69
54
|
}
|
|
70
55
|
else {
|
|
71
56
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
72
57
|
}
|
|
73
|
-
(0, ui_1.step)(
|
|
58
|
+
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
74
59
|
let started;
|
|
75
60
|
try {
|
|
76
61
|
started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
|
|
@@ -89,22 +74,22 @@ async function deploy(args) {
|
|
|
89
74
|
(0, ui_1.out)();
|
|
90
75
|
(0, ui_1.out)(build.log_tail);
|
|
91
76
|
}
|
|
92
|
-
throw new errors_1.CliError(build.error || '
|
|
77
|
+
throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: xflow deploy. Version ${build.deploy_id}`);
|
|
93
78
|
}
|
|
94
|
-
(0, ui_1.ok)(
|
|
79
|
+
(0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
|
|
95
80
|
(0, ui_1.out)(build.project_url);
|
|
96
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
81
|
+
(0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
|
|
97
82
|
}
|
|
98
83
|
async function publish() {
|
|
99
84
|
const { config } = (0, config_1.requireProject)();
|
|
100
85
|
const client = (0, session_1.connect)(config);
|
|
101
|
-
(0, ui_1.step)('
|
|
86
|
+
(0, ui_1.step)('Publishing the current dev version');
|
|
102
87
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
|
|
103
88
|
if (result.already_published) {
|
|
104
|
-
(0, ui_1.ok)('
|
|
89
|
+
(0, ui_1.ok)('This version is already published');
|
|
105
90
|
}
|
|
106
91
|
else {
|
|
107
|
-
(0, ui_1.ok)(
|
|
92
|
+
(0, ui_1.ok)(`Version ${result.deploy_id} published: visitors see it now`);
|
|
108
93
|
}
|
|
109
94
|
(0, ui_1.out)(result.project_url);
|
|
110
95
|
}
|
|
@@ -112,29 +97,29 @@ async function rollback(args) {
|
|
|
112
97
|
const { config } = (0, config_1.requireProject)();
|
|
113
98
|
const deployId = args.words[0];
|
|
114
99
|
if (!deployId) {
|
|
115
|
-
throw new errors_1.CliError('
|
|
100
|
+
throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
|
|
116
101
|
}
|
|
117
102
|
const client = (0, session_1.connect)(config);
|
|
118
103
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
|
|
119
|
-
(0, ui_1.ok)(`dev
|
|
104
|
+
(0, ui_1.ok)(`The dev version of the project is switched to ${result.deploy_id}`);
|
|
120
105
|
(0, ui_1.out)(result.project_url);
|
|
121
106
|
if (result.revision !== null) {
|
|
122
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
107
|
+
(0, ui_1.note)((0, ui_1.dim)(` The code of this version: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
|
|
123
108
|
}
|
|
124
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
109
|
+
(0, ui_1.note)((0, ui_1.dim)(' Visitors see it only after xflow publish'));
|
|
125
110
|
}
|
|
126
111
|
async function deployments() {
|
|
127
112
|
const { config } = (0, config_1.requireProject)();
|
|
128
113
|
const client = (0, session_1.connect)(config);
|
|
129
114
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/deployments`);
|
|
130
115
|
if (data.deployments.length === 0) {
|
|
131
|
-
(0, ui_1.note)('
|
|
116
|
+
(0, ui_1.note)('No versions yet. To build and release: xflow deploy');
|
|
132
117
|
return;
|
|
133
118
|
}
|
|
134
119
|
(0, ui_1.table)(data.deployments.map((d) => [
|
|
135
|
-
d.deploy_id ?? '
|
|
136
|
-
d.revision !== null ?
|
|
137
|
-
d.status === 'deployed' ? '
|
|
120
|
+
d.deploy_id ?? '-',
|
|
121
|
+
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
122
|
+
d.status === 'deployed' ? 'ready' : d.status,
|
|
138
123
|
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
139
124
|
[d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
|
|
140
125
|
]));
|
package/dist/commands/env.js
CHANGED
|
@@ -13,9 +13,9 @@ const errors_1 = require("../errors");
|
|
|
13
13
|
const session_1 = require("../session");
|
|
14
14
|
const ui_1 = require("../ui");
|
|
15
15
|
const FUNCTIONS_DIR = 'functions';
|
|
16
|
-
/**
|
|
16
|
+
/** Matches `process.env.NAME` and `process.env['NAME']`. */
|
|
17
17
|
const ENV_REFERENCE = /process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g;
|
|
18
|
-
/**
|
|
18
|
+
/** Set by the platform itself. */
|
|
19
19
|
const PROVIDED = new Set([
|
|
20
20
|
'XFLOW_PROJECT_ID',
|
|
21
21
|
'XFLOW_PROJECT_TOKEN',
|
|
@@ -37,7 +37,7 @@ function sourceFiles(dir, found = []) {
|
|
|
37
37
|
}
|
|
38
38
|
return found;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/** Which variables the project's functions read, and in which of them. */
|
|
41
41
|
function referencedByFunctions(root) {
|
|
42
42
|
const needed = new Map();
|
|
43
43
|
const provided = new Map();
|
|
@@ -66,31 +66,23 @@ async function fetchVariables() {
|
|
|
66
66
|
async function envList() {
|
|
67
67
|
const { rows } = await fetchVariables();
|
|
68
68
|
if (rows.length === 0) {
|
|
69
|
-
(0, ui_1.note)('
|
|
70
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
69
|
+
(0, ui_1.note)('No variables');
|
|
70
|
+
(0, ui_1.note)((0, ui_1.dim)(' To store one: xflow env set SMTP_PASSWORD=secret'));
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
(0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? '
|
|
74
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
73
|
+
(0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? 'this project only' : 'the whole organization']));
|
|
74
|
+
(0, ui_1.note)((0, ui_1.dim)(' The platform never returns values: they are visible only inside the function'));
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Сверить, хватает ли функциям переменных.
|
|
78
|
-
*
|
|
79
|
-
* Это ответ на самый частый способ потерять полчаса: функция выкатилась, а падает
|
|
80
|
-
* на пустом process.env, потому что переменную забыли записать наверх.
|
|
81
|
-
*/
|
|
76
|
+
/** Check that every referenced variable is stored. */
|
|
82
77
|
async function envCheck() {
|
|
83
78
|
const { names, root } = await fetchVariables();
|
|
84
79
|
const { needed, provided } = referencedByFunctions(root);
|
|
85
80
|
if (needed.size === 0 && provided.size === 0) {
|
|
86
|
-
(0, ui_1.note)('
|
|
81
|
+
(0, ui_1.note)('The functions of this project read no environment variables');
|
|
87
82
|
return;
|
|
88
83
|
}
|
|
89
|
-
// Платформенные показываем отдельной таблицей, а не молчим о них: раньше
|
|
90
|
-
// функция, читающая только DATABASE_URL, получала ответ «переменных не
|
|
91
|
-
// читают», то есть команда делала ложное утверждение о собственном коде.
|
|
92
84
|
if (provided.size > 0) {
|
|
93
|
-
(0, ui_1.out)((0, ui_1.bold)('
|
|
85
|
+
(0, ui_1.out)((0, ui_1.bold)('Provided by the platform:'));
|
|
94
86
|
(0, ui_1.table)([...provided.entries()].sort().map(([name, users]) => [name, users.join(', ')]));
|
|
95
87
|
if (needed.size > 0)
|
|
96
88
|
(0, ui_1.out)('');
|
|
@@ -105,24 +97,24 @@ async function envCheck() {
|
|
|
105
97
|
missing.push(row);
|
|
106
98
|
}
|
|
107
99
|
if (present.length > 0) {
|
|
108
|
-
(0, ui_1.out)((0, ui_1.bold)('
|
|
100
|
+
(0, ui_1.out)((0, ui_1.bold)('Stored on the platform:'));
|
|
109
101
|
(0, ui_1.table)(present);
|
|
110
102
|
}
|
|
111
103
|
if (missing.length === 0) {
|
|
112
|
-
(0, ui_1.ok)('
|
|
104
|
+
(0, ui_1.ok)('Every function has the variables it needs');
|
|
113
105
|
return;
|
|
114
106
|
}
|
|
115
107
|
(0, ui_1.out)('');
|
|
116
|
-
(0, ui_1.fail)('
|
|
108
|
+
(0, ui_1.fail)('Missing on the platform:');
|
|
117
109
|
(0, ui_1.table)(missing);
|
|
118
|
-
throw new errors_1.CliError(
|
|
110
|
+
throw new errors_1.CliError(`Missing variables: ${missing.length}`, 'To store them: xflow env set NAME=value. Until then the function receives undefined');
|
|
119
111
|
}
|
|
120
112
|
async function envSet(args) {
|
|
121
113
|
const { root, config } = (0, config_1.requireProject)();
|
|
122
114
|
const client = (0, session_1.connect)(config);
|
|
123
115
|
const pair = args.words[1];
|
|
124
116
|
if (!pair || !pair.includes('=')) {
|
|
125
|
-
throw new errors_1.CliError('
|
|
117
|
+
throw new errors_1.CliError('A NAME=value pair is required', 'For example: xflow env set SMTP_PASSWORD=secret');
|
|
126
118
|
}
|
|
127
119
|
const name = pair.slice(0, pair.indexOf('=')).trim();
|
|
128
120
|
const value = pair.slice(pair.indexOf('=') + 1);
|
|
@@ -130,12 +122,11 @@ async function envSet(args) {
|
|
|
130
122
|
method: 'POST',
|
|
131
123
|
body: { name, value, scope: (0, args_1.flagString)(args, 'scope') === 'project' ? 'project' : 'organization' },
|
|
132
124
|
});
|
|
133
|
-
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)}
|
|
134
|
-
//
|
|
135
|
-
// не передеплоили, в её окружении лежит прежнее.
|
|
125
|
+
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored (${result.scope === 'project' ? 'this project only' : 'the whole organization'})`);
|
|
126
|
+
// Values reach a function on its next deploy.
|
|
136
127
|
const users = referencedByFunctions(root).needed.get(name);
|
|
137
128
|
if (users && users.length > 0) {
|
|
138
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
129
|
+
(0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, redeploy: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
|
|
139
130
|
}
|
|
140
131
|
}
|
|
141
132
|
async function envRemove(args) {
|
|
@@ -143,12 +134,12 @@ async function envRemove(args) {
|
|
|
143
134
|
const client = (0, session_1.connect)(config);
|
|
144
135
|
const name = args.words[1];
|
|
145
136
|
if (!name)
|
|
146
|
-
throw new errors_1.CliError('
|
|
137
|
+
throw new errors_1.CliError('A variable name is required', 'What is stored: xflow env');
|
|
147
138
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
148
139
|
if (!result.removed) {
|
|
149
|
-
(0, ui_1.note)(
|
|
140
|
+
(0, ui_1.note)(`There is no variable ${name}`);
|
|
150
141
|
return;
|
|
151
142
|
}
|
|
152
|
-
(0, ui_1.ok)(`${name}
|
|
153
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
143
|
+
(0, ui_1.ok)(`${name} deleted`);
|
|
144
|
+
(0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the value until their next deploy'));
|
|
154
145
|
}
|
|
@@ -13,7 +13,6 @@ const session_1 = require("../session");
|
|
|
13
13
|
const template_1 = require("../template");
|
|
14
14
|
const ui_1 = require("../ui");
|
|
15
15
|
const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
|
|
16
|
-
/** Папка с функциями внутри проекта. Та же, что была до пивота: менять её незачем. */
|
|
17
16
|
const FUNCTIONS_DIR = 'functions';
|
|
18
17
|
function entryFor(root, name) {
|
|
19
18
|
for (const entry of ENTRY_NAMES) {
|
|
@@ -33,14 +32,8 @@ function discover(root) {
|
|
|
33
32
|
.sort();
|
|
34
33
|
}
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* esbuild берём из node_modules проекта, а не тащим зависимостью в CLI: он и так
|
|
39
|
-
* есть в каждом приложении на платформе (внутри Vite), а пакет без зависимостей
|
|
40
|
-
* ставится быстрее и не требует доверия к нашему списку.
|
|
41
|
-
*
|
|
42
|
-
* `pg` оставляем снаружи бандла сознательно: серверная обёртка подставляет схему
|
|
43
|
-
* проекта в каждое соединение, а сделать это можно только с общим драйвером.
|
|
35
|
+
* Bundle a function into one file. esbuild comes from the project's node_modules;
|
|
36
|
+
* `pg` stays external so the server wrapper can inject the project schema.
|
|
44
37
|
*/
|
|
45
38
|
function bundle(root, entry) {
|
|
46
39
|
let esbuild;
|
|
@@ -48,7 +41,7 @@ function bundle(root, entry) {
|
|
|
48
41
|
esbuild = require(require.resolve('esbuild', { paths: [root] }));
|
|
49
42
|
}
|
|
50
43
|
catch {
|
|
51
|
-
throw new errors_1.CliError('
|
|
44
|
+
throw new errors_1.CliError('Could not find esbuild in the project', 'Install it: npm i -D esbuild. It usually comes with Vite already');
|
|
52
45
|
}
|
|
53
46
|
const result = esbuild.buildSync({
|
|
54
47
|
entryPoints: [entry],
|
|
@@ -62,7 +55,7 @@ function bundle(root, entry) {
|
|
|
62
55
|
});
|
|
63
56
|
const text = result.outputFiles[0]?.text;
|
|
64
57
|
if (!text)
|
|
65
|
-
throw new errors_1.CliError(
|
|
58
|
+
throw new errors_1.CliError(`Building ${entry} produced nothing`);
|
|
66
59
|
return text;
|
|
67
60
|
}
|
|
68
61
|
async function functionsList() {
|
|
@@ -70,28 +63,20 @@ async function functionsList() {
|
|
|
70
63
|
const client = (0, session_1.connect)(config);
|
|
71
64
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
|
|
72
65
|
if (data.functions.length === 0) {
|
|
73
|
-
(0, ui_1.note)(
|
|
66
|
+
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow functions deploy`);
|
|
74
67
|
return;
|
|
75
68
|
}
|
|
76
69
|
(0, ui_1.table)(data.functions.map((fn) => [
|
|
77
70
|
fn.name,
|
|
78
|
-
fn.status === 'deployed' ? '
|
|
79
|
-
fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '
|
|
71
|
+
fn.status === 'deployed' ? 'deployed' : fn.status === 'failed' ? 'failed' : fn.status,
|
|
72
|
+
fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '-',
|
|
80
73
|
fn.error_message ?? '',
|
|
81
74
|
]));
|
|
82
75
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Записать адреса функций в `.env`, откуда их заберёт сборщик.
|
|
85
|
-
*
|
|
86
|
-
* Подставляем на сборке, а не спрашиваем у платформы в рантайме: иначе каждый
|
|
87
|
-
* запуск приложения начинался бы с похода к нам, и мы стали бы обязательным
|
|
88
|
-
* участником работы чужого приложения.
|
|
89
|
-
*/
|
|
76
|
+
/** Write function URLs into .env, where the build picks them up. */
|
|
90
77
|
async function refreshFunctionsEnv(root, client, projectId) {
|
|
91
78
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
92
|
-
//
|
|
93
|
-
// Дописывать в такой один адрес функции бессмысленно — без токена вызов всё
|
|
94
|
-
// равно получит 401, поэтому восстанавливаем целиком, как это делает link.
|
|
79
|
+
// No .env at all (fresh clone): recreate it fully, like link does.
|
|
95
80
|
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
|
|
96
81
|
(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');
|
|
97
82
|
}
|
|
@@ -100,7 +85,6 @@ async function refreshFunctionsEnv(root, client, projectId) {
|
|
|
100
85
|
}
|
|
101
86
|
return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
|
|
102
87
|
}
|
|
103
|
-
/** Ответ функции: JSON разворачиваем, прочее отдаём как есть. */
|
|
104
88
|
function prettyBody(text) {
|
|
105
89
|
try {
|
|
106
90
|
return JSON.stringify(JSON.parse(text), null, 2);
|
|
@@ -109,24 +93,19 @@ function prettyBody(text) {
|
|
|
109
93
|
return text;
|
|
110
94
|
}
|
|
111
95
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Вызвать функцию так же, как её вызывает приложение.
|
|
114
|
-
*
|
|
115
|
-
* Токен проекта берём из карточки на платформе, а не из локального `.env`: в
|
|
116
|
-
* свежем клоне файла нет вовсе, а команда должна работать сразу после link.
|
|
117
|
-
*/
|
|
96
|
+
/** Call a function the same way the application does. */
|
|
118
97
|
async function functionsInvoke(args) {
|
|
119
98
|
const { config } = (0, config_1.requireProject)();
|
|
120
99
|
const client = (0, session_1.connect)(config);
|
|
121
100
|
const name = args.words[1];
|
|
122
101
|
if (!name)
|
|
123
|
-
throw new errors_1.CliError('
|
|
102
|
+
throw new errors_1.CliError('A function name is required', 'What is deployed: xflow functions list');
|
|
124
103
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
|
|
125
104
|
const fn = card.functions.find((item) => item.name === name);
|
|
126
105
|
if (!fn || !fn.invoke_url) {
|
|
127
|
-
throw new errors_1.CliError(
|
|
128
|
-
?
|
|
129
|
-
:
|
|
106
|
+
throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
|
|
107
|
+
? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
|
|
108
|
+
: `To deploy it: xflow functions deploy ${name}`);
|
|
130
109
|
}
|
|
131
110
|
const data = (0, args_1.flagString)(args, 'data');
|
|
132
111
|
const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
|
|
@@ -141,21 +120,20 @@ async function functionsInvoke(args) {
|
|
|
141
120
|
'X-Project-Token': card.project_token ?? '',
|
|
142
121
|
},
|
|
143
122
|
body: sendsBody ? (data ?? '{}') : undefined,
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
signal: AbortSignal.timeout(40_000),
|
|
123
|
+
// Wait past the function's own 90 s cap to see its timeout, not ours.
|
|
124
|
+
signal: AbortSignal.timeout(100_000),
|
|
147
125
|
});
|
|
148
126
|
}
|
|
149
127
|
catch (e) {
|
|
150
|
-
throw new errors_1.CliError(
|
|
128
|
+
throw new errors_1.CliError(`The function did not answer: ${e instanceof Error ? e.message : String(e)}`, 'Check that it is deployed: xflow functions list');
|
|
151
129
|
}
|
|
152
130
|
const elapsed = Date.now() - started;
|
|
153
131
|
const text = await response.text();
|
|
154
|
-
(0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText}
|
|
132
|
+
(0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} in ${elapsed} ms`));
|
|
155
133
|
if (text)
|
|
156
134
|
(0, ui_1.out)(prettyBody(text));
|
|
157
135
|
if (!response.ok) {
|
|
158
|
-
throw new errors_1.CliError(
|
|
136
|
+
throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
|
|
159
137
|
}
|
|
160
138
|
}
|
|
161
139
|
async function functionsDeploy(args) {
|
|
@@ -164,23 +142,23 @@ async function functionsDeploy(args) {
|
|
|
164
142
|
const wanted = args.words[1];
|
|
165
143
|
const names = wanted ? [wanted] : discover(root);
|
|
166
144
|
if (names.length === 0) {
|
|
167
|
-
throw new errors_1.CliError(
|
|
145
|
+
throw new errors_1.CliError(`The project has no functions`, `Create ${FUNCTIONS_DIR}/<name>/index.ts exporting handler and try again`);
|
|
168
146
|
}
|
|
169
147
|
for (const name of names) {
|
|
170
148
|
const entry = entryFor(root, name);
|
|
171
149
|
if (!entry) {
|
|
172
|
-
throw new errors_1.CliError(
|
|
150
|
+
throw new errors_1.CliError(`Could not find ${FUNCTIONS_DIR}/${name}/index.ts`, `Available functions: ${discover(root).join(', ') || 'none at all'}`);
|
|
173
151
|
}
|
|
174
|
-
(0, ui_1.step)(
|
|
152
|
+
(0, ui_1.step)(`Building ${name}`);
|
|
175
153
|
const code = bundle(root, entry);
|
|
176
|
-
(0, ui_1.step)(
|
|
154
|
+
(0, ui_1.step)(`Deploying ${name}`);
|
|
177
155
|
const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/functions?name=${encodeURIComponent(name)}`, Buffer.from(code, 'utf-8'), { 'Content-Type': 'application/javascript' });
|
|
178
|
-
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)}
|
|
156
|
+
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} deployed`);
|
|
179
157
|
(0, ui_1.out)(result.url);
|
|
180
158
|
if (result.secrets.length > 0) {
|
|
181
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
159
|
+
(0, ui_1.note)((0, ui_1.dim)(` Organization secrets in the environment: ${result.secrets.join(', ')}`));
|
|
182
160
|
}
|
|
183
161
|
}
|
|
184
162
|
const available = await refreshFunctionsEnv(root, client, config.projectId);
|
|
185
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
163
|
+
(0, ui_1.note)((0, ui_1.dim)(` Addresses in .env updated (${available.join(', ')}). In the frontend: xflow.functions.invoke('${names[0]}')`));
|
|
186
164
|
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -8,7 +8,6 @@ const config_1 = require("../config");
|
|
|
8
8
|
const session_1 = require("../session");
|
|
9
9
|
const ui_1 = require("../ui");
|
|
10
10
|
const DEFAULT_LIMIT = 10;
|
|
11
|
-
/** Стек с хвостом консоли бывает длинным: показываем начало, оно и есть причина. */
|
|
12
11
|
const STACK_MAX_LINES = 14;
|
|
13
12
|
function stamp(iso) {
|
|
14
13
|
const date = new Date(iso);
|
|
@@ -24,17 +23,17 @@ async function fetchLogs(source, name, args) {
|
|
|
24
23
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/logs?${query.toString()}`);
|
|
25
24
|
return data.logs;
|
|
26
25
|
}
|
|
27
|
-
/**
|
|
26
|
+
/** Newest at the bottom. */
|
|
28
27
|
function render(rows) {
|
|
29
28
|
for (const row of [...rows].reverse()) {
|
|
30
|
-
const label = row.source === 'function' ? (row.function ?? '
|
|
29
|
+
const label = row.source === 'function' ? (row.function ?? 'function') : 'browser';
|
|
31
30
|
(0, ui_1.out)(`${(0, ui_1.dim)(stamp(row.timestamp))} ${(0, ui_1.bold)(label)} ${row.message}`);
|
|
32
31
|
if (row.stack) {
|
|
33
32
|
const lines = row.stack.split('\n');
|
|
34
33
|
for (const line of lines.slice(0, STACK_MAX_LINES))
|
|
35
34
|
(0, ui_1.out)((0, ui_1.dim)(` ${line}`));
|
|
36
35
|
if (lines.length > STACK_MAX_LINES)
|
|
37
|
-
(0, ui_1.out)((0, ui_1.dim)(` …
|
|
36
|
+
(0, ui_1.out)((0, ui_1.dim)(` … ${lines.length - STACK_MAX_LINES} more lines`));
|
|
38
37
|
}
|
|
39
38
|
(0, ui_1.out)('');
|
|
40
39
|
}
|
|
@@ -42,8 +41,8 @@ function render(rows) {
|
|
|
42
41
|
async function logs(args) {
|
|
43
42
|
const rows = await fetchLogs('client', undefined, args);
|
|
44
43
|
if (rows.length === 0) {
|
|
45
|
-
(0, ui_1.note)('
|
|
46
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
44
|
+
(0, ui_1.note)('No browser errors');
|
|
45
|
+
(0, ui_1.note)((0, ui_1.dim)(' This collects crashes of the released application, not of a local npm run dev'));
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
48
|
render(rows);
|
|
@@ -52,8 +51,8 @@ async function functionsLogs(args) {
|
|
|
52
51
|
const name = args.words[1];
|
|
53
52
|
const rows = await fetchLogs('function', name, args);
|
|
54
53
|
if (rows.length === 0) {
|
|
55
|
-
(0, ui_1.note)(name ?
|
|
56
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
54
|
+
(0, ui_1.note)(name ? `Function ${name} has not crashed` : 'No function has crashed');
|
|
55
|
+
(0, ui_1.note)((0, ui_1.dim)(' Only failed calls land here: successful ones write nothing'));
|
|
57
56
|
return;
|
|
58
57
|
}
|
|
59
58
|
render(rows);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ const args_1 = require("../args");
|
|
|
9
9
|
const config_1 = require("../config");
|
|
10
10
|
const session_1 = require("../session");
|
|
11
11
|
const ui_1 = require("../ui");
|
|
12
|
-
/**
|
|
12
|
+
/** Clients with their own server-add command. */
|
|
13
13
|
const CLIENTS = [
|
|
14
14
|
{
|
|
15
15
|
binary: 'claude',
|
|
@@ -24,26 +24,20 @@ const CLIENTS = [
|
|
|
24
24
|
'--header',
|
|
25
25
|
`Authorization: Bearer ${token}`,
|
|
26
26
|
],
|
|
27
|
-
//
|
|
28
|
-
// выполняют и после смены ключа, и после обновления пакета.
|
|
27
|
+
// Re-running must update the entry, not fail on it.
|
|
29
28
|
reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
|
|
30
29
|
},
|
|
31
30
|
];
|
|
32
31
|
const WINDOWS = process.platform === 'win32';
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* На Windows без оболочки не найти `.cmd`-обёртки, которыми ставятся все
|
|
37
|
-
* консольные пакеты npm. А оболочка не экранирует аргументы, а склеивает их:
|
|
38
|
-
* заголовок «Authorization: Bearer …» разваливается по пробелу. Поэтому на
|
|
39
|
-
* Windows собираем строку сами и сами же расставляем кавычки.
|
|
33
|
+
* Windows needs a shell to resolve npm's .cmd shims, and the shell joins
|
|
34
|
+
* arguments instead of escaping them: quote by hand.
|
|
40
35
|
*/
|
|
41
36
|
function quote(value) {
|
|
42
37
|
return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
|
|
43
38
|
}
|
|
44
39
|
function execute(binary, args) {
|
|
45
|
-
//
|
|
46
|
-
// напечатать добавленные заголовки целиком, вместе с ключом.
|
|
40
|
+
// Capture output: clients may echo the added header together with the key.
|
|
47
41
|
const options = { encoding: 'utf-8' };
|
|
48
42
|
return WINDOWS
|
|
49
43
|
? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...options, shell: true })
|
|
@@ -53,47 +47,46 @@ function hasBinary(binary) {
|
|
|
53
47
|
return execute(binary, ['--version']).status === 0;
|
|
54
48
|
}
|
|
55
49
|
async function mcpInstall(args) {
|
|
56
|
-
//
|
|
50
|
+
// The server covers the organization: the command works outside a project folder too.
|
|
57
51
|
const root = (0, config_1.findProjectRoot)();
|
|
58
52
|
const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
|
|
59
53
|
const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
|
|
60
54
|
const identity = await (0, api_1.apiJson)(client, '/api/v1/me');
|
|
61
|
-
(0, ui_1.out)(`${(0, ui_1.bold)('
|
|
62
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
63
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
55
|
+
(0, ui_1.out)(`${(0, ui_1.bold)('MCP server')} ${url}`);
|
|
56
|
+
(0, ui_1.note)((0, ui_1.dim)(` Organization: ${identity.organization.name ?? 'unnamed'}`));
|
|
57
|
+
(0, ui_1.note)((0, ui_1.dim)(' The key covers the whole organization: the agent names the project itself'));
|
|
64
58
|
let found = 0;
|
|
65
59
|
let installed = 0;
|
|
66
60
|
for (const target of CLIENTS) {
|
|
67
61
|
if (!hasBinary(target.binary))
|
|
68
62
|
continue;
|
|
69
63
|
found++;
|
|
70
|
-
(0, ui_1.step)(
|
|
64
|
+
(0, ui_1.step)(`Writing into ${target.label}`);
|
|
71
65
|
execute(target.binary, [...target.reset]);
|
|
72
66
|
const result = execute(target.binary, target.args(url, client.token));
|
|
73
67
|
if (result.status === 0) {
|
|
74
68
|
installed++;
|
|
75
|
-
(0, ui_1.ok)(`${target.label}:
|
|
69
|
+
(0, ui_1.ok)(`${target.label}: the xflow server is connected`);
|
|
76
70
|
}
|
|
77
71
|
else {
|
|
78
72
|
const reason = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
|
|
79
|
-
(0, ui_1.note)(`${target.label}:
|
|
73
|
+
(0, ui_1.note)(`${target.label}: the command exited with an error`);
|
|
80
74
|
if (reason)
|
|
81
|
-
(0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('
|
|
75
|
+
(0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('<key>').split('\n').slice(0, 5).join('\n ')}`));
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
78
|
if (installed === 0) {
|
|
85
|
-
(0, ui_1.note)(found === 0 ? '
|
|
86
|
-
(0, ui_1.out)(`
|
|
87
|
-
(0, ui_1.out)(`
|
|
88
|
-
(0, ui_1.out)(`
|
|
79
|
+
(0, ui_1.note)(found === 0 ? 'Found no client with its own connect command.' : 'Add the server by hand:');
|
|
80
|
+
(0, ui_1.out)(` type: HTTP (streamable)`);
|
|
81
|
+
(0, ui_1.out)(` address: ${url}`);
|
|
82
|
+
(0, ui_1.out)(` header: Authorization: Bearer <key>`);
|
|
89
83
|
}
|
|
90
|
-
// Ключ печатаем только по явной просьбе: см. комментарий в шапке файла.
|
|
91
84
|
if ((0, args_1.flagBool)(args, 'show-token')) {
|
|
92
85
|
(0, ui_1.out)('');
|
|
93
|
-
(0, ui_1.out)(`${(0, ui_1.bold)('
|
|
94
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
86
|
+
(0, ui_1.out)(`${(0, ui_1.bold)('Key')} ${client.token}`);
|
|
87
|
+
(0, ui_1.note)((0, ui_1.dim)(' Do not leave it in the chat with the agent'));
|
|
95
88
|
}
|
|
96
89
|
else {
|
|
97
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
90
|
+
(0, ui_1.note)((0, ui_1.dim)(` The key lives in ${(0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow', 'credentials.json')} and is not printed`));
|
|
98
91
|
}
|
|
99
92
|
}
|