@getxflow/cli 0.1.9 → 0.2.0
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 +45 -45
- package/dist/api.js +5 -9
- package/dist/args.js +2 -10
- package/dist/bin.js +3 -6
- package/dist/commands/auth.js +18 -13
- package/dist/commands/db.js +3 -10
- package/dist/commands/deploy.js +40 -18
- package/dist/commands/env.js +6 -15
- package/dist/commands/functions.js +4 -113
- package/dist/commands/logs.js +1 -2
- package/dist/commands/mcp.js +6 -13
- package/dist/commands/projects.js +4 -16
- package/dist/commands/skills.js +18 -42
- package/dist/commands/sources.js +7 -16
- package/dist/config.js +8 -8
- package/dist/credentials.js +9 -10
- package/dist/errors.js +1 -1
- package/dist/help.js +257 -244
- package/dist/limits.js +5 -15
- package/dist/session.js +5 -9
- package/dist/template.js +25 -33
- package/dist/tree.js +5 -18
- package/dist/types.js +1 -8
- package/dist/ui.js +36 -14
- package/dist/version.js +3 -7
- package/dist/zip.js +15 -8
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +21 -16
package/dist/template.js
CHANGED
|
@@ -7,31 +7,31 @@ exports.writeEnvValue = writeEnvValue;
|
|
|
7
7
|
exports.scaffoldFiles = scaffoldFiles;
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
|
-
const GITIGNORE = `node_modules/
|
|
11
|
-
dist/
|
|
12
|
-
.xflow/
|
|
13
|
-
.env
|
|
14
|
-
.env.*
|
|
10
|
+
const GITIGNORE = `node_modules/
|
|
11
|
+
dist/
|
|
12
|
+
.xflow/
|
|
13
|
+
.env
|
|
14
|
+
.env.*
|
|
15
15
|
`;
|
|
16
|
-
const README = `# %NAME%
|
|
17
|
-
|
|
18
|
-
An application on XFlow.
|
|
19
|
-
|
|
20
|
-
\`\`\`bash
|
|
21
|
-
npm install # dependencies
|
|
22
|
-
npm run dev # develop on localhost:3000
|
|
23
|
-
xflow deploy # send the code and build it on the platform
|
|
24
|
-
xflow publish # show the dev version to visitors
|
|
25
|
-
\`\`\`
|
|
26
|
-
|
|
27
|
-
The interface is built on the template components: \`src/components/ui\` (buttons, fields,
|
|
28
|
-
dialogs) and \`src/components/blocks\` (table, form, filters, kanban, charts). Layout built
|
|
29
|
-
on top of them looks like the rest of the platform, while custom colors in place of the
|
|
30
|
-
tokens from \`src/index.css\` break that consistency.
|
|
31
|
-
|
|
32
|
-
Useful: \`xflow status\` for what is on the server, \`xflow deployments\` for version history.
|
|
16
|
+
const README = `# %NAME%
|
|
17
|
+
|
|
18
|
+
An application on XFlow.
|
|
19
|
+
|
|
20
|
+
\`\`\`bash
|
|
21
|
+
npm install # dependencies
|
|
22
|
+
npm run dev # develop on localhost:3000
|
|
23
|
+
xflow deploy # send the code and build it on the platform
|
|
24
|
+
xflow publish # show the dev version to visitors
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
The interface is built on the template components: \`src/components/ui\` (buttons, fields,
|
|
28
|
+
dialogs) and \`src/components/blocks\` (table, form, filters, kanban, charts). Layout built
|
|
29
|
+
on top of them looks like the rest of the platform, while custom colors in place of the
|
|
30
|
+
tokens from \`src/index.css\` break that consistency.
|
|
31
|
+
|
|
32
|
+
Useful: \`xflow status\` for what is on the server, \`xflow deployments\` for version history.
|
|
33
33
|
`;
|
|
34
|
-
/**
|
|
34
|
+
/** Function name → URL map, one JSON line. */
|
|
35
35
|
exports.FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
|
|
36
36
|
function functionsEnvValue(functions) {
|
|
37
37
|
const map = {};
|
|
@@ -41,10 +41,7 @@ function functionsEnvValue(functions) {
|
|
|
41
41
|
}
|
|
42
42
|
return JSON.stringify(map);
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
* `.env` в исходники не уходит (его пропускают и push, и сервер), поэтому в свежем
|
|
46
|
-
* клоне его восстанавливает `xflow link`.
|
|
47
|
-
*/
|
|
44
|
+
/** .env is never pushed; xflow link recreates it in a fresh clone. */
|
|
48
45
|
function envFile(projectToken, apiUrl, functions = []) {
|
|
49
46
|
return [
|
|
50
47
|
`VITE_XFLOW_PROJECT_TOKEN=${projectToken}`,
|
|
@@ -53,12 +50,7 @@ function envFile(projectToken, apiUrl, functions = []) {
|
|
|
53
50
|
'',
|
|
54
51
|
].join('\n');
|
|
55
52
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Обновить одну переменную в `.env`, не трогая остальные.
|
|
58
|
-
*
|
|
59
|
-
* Файл наш, но живёт у разработчика: рядом с нашими тремя строками у него может
|
|
60
|
-
* лежать что угодно своё, и переписывать файл целиком мы не вправе.
|
|
61
|
-
*/
|
|
53
|
+
/** Update one variable in .env without touching the rest of the file. */
|
|
62
54
|
function writeEnvValue(root, key, value) {
|
|
63
55
|
const path = (0, node_path_1.join)(root, '.env');
|
|
64
56
|
const line = `${key}=${value}`;
|
package/dist/tree.js
CHANGED
|
@@ -7,12 +7,7 @@ exports.heaviest = heaviest;
|
|
|
7
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
|
-
/**
|
|
11
|
-
* Не отправляется никогда. Первые пять сервер отклоняет и сам, но узнать об
|
|
12
|
-
* этом лучше до заливки 200 МБ node_modules. Секреты (`.env*`) шире серверного
|
|
13
|
-
* списка сознательно: значения переменных живут в настройках организации,
|
|
14
|
-
* а не в репозитории.
|
|
15
|
-
*/
|
|
10
|
+
/** Never sent. */
|
|
16
11
|
const DEFAULT_IGNORE = [
|
|
17
12
|
'node_modules/',
|
|
18
13
|
'.git/',
|
|
@@ -60,7 +55,7 @@ function compileRule(raw) {
|
|
|
60
55
|
const suffix = dirOnly ? '/' : '($|/)';
|
|
61
56
|
return { re: new RegExp(`${prefix}${source}${suffix}`) };
|
|
62
57
|
}
|
|
63
|
-
/**
|
|
58
|
+
/** Defaults, the .xflowignore file and the ignore field of xflow.json. */
|
|
64
59
|
function loadIgnoreRules(root, extra = []) {
|
|
65
60
|
const lines = [...DEFAULT_IGNORE, ...extra];
|
|
66
61
|
const ignoreFile = (0, node_path_1.join)(root, IGNORE_FILE);
|
|
@@ -73,7 +68,7 @@ function isIgnored(relativePath, rules, isDirectory) {
|
|
|
73
68
|
const probe = isDirectory ? `${relativePath}/` : relativePath;
|
|
74
69
|
return rules.some((rule) => rule.re.test(probe));
|
|
75
70
|
}
|
|
76
|
-
/**
|
|
71
|
+
/** NUL byte in the head, same heuristic as git. */
|
|
77
72
|
function isBinary(content) {
|
|
78
73
|
const limit = Math.min(content.length, 8000);
|
|
79
74
|
for (let i = 0; i < limit; i++) {
|
|
@@ -82,12 +77,7 @@ function isBinary(content) {
|
|
|
82
77
|
}
|
|
83
78
|
return false;
|
|
84
79
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Приводим переводы строк к LF.
|
|
87
|
-
*
|
|
88
|
-
* Иначе одно и то же дерево на Windows и на Linux даёт разные хеши, и статус
|
|
89
|
-
* вечно показывает изменения, которых никто не делал. Двоичные файлы не трогаем.
|
|
90
|
-
*/
|
|
80
|
+
/** CRLF and LF must hash the same on every OS. Binary files stay untouched. */
|
|
91
81
|
function normalizeEol(content) {
|
|
92
82
|
if (isBinary(content) || !content.includes(0x0d))
|
|
93
83
|
return content;
|
|
@@ -100,8 +90,6 @@ function collectFiles(root, rules) {
|
|
|
100
90
|
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
101
91
|
const full = (0, node_path_1.join)(dir, entry.name);
|
|
102
92
|
const rel = (0, node_path_1.relative)(root, full).split(node_path_1.sep).join('/');
|
|
103
|
-
// Ссылки не отправляем: в архиве их не представить, а пойти по ним значит
|
|
104
|
-
// утащить чужое дерево целиком.
|
|
105
93
|
if (entry.isSymbolicLink()) {
|
|
106
94
|
skippedLinks.push(rel);
|
|
107
95
|
continue;
|
|
@@ -123,13 +111,12 @@ function collectFiles(root, rules) {
|
|
|
123
111
|
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
124
112
|
return { files, skippedLinks };
|
|
125
113
|
}
|
|
126
|
-
/**
|
|
114
|
+
/** Computed identically on the server. */
|
|
127
115
|
function treeHash(files) {
|
|
128
116
|
const parts = files.map((file) => `${file.path}\0${(0, node_crypto_1.createHash)('sha256').update(file.content).digest('hex')}`);
|
|
129
117
|
parts.sort();
|
|
130
118
|
return (0, node_crypto_1.createHash)('sha256').update(parts.join('\n')).digest('hex');
|
|
131
119
|
}
|
|
132
|
-
/** Самые тяжёлые файлы: показываем их, когда архив не влез в лимит. */
|
|
133
120
|
function heaviest(files, count = 5) {
|
|
134
121
|
return [...files].sort((a, b) => b.content.length - a.content.length).slice(0, count);
|
|
135
122
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Ответы публичного API, которые разбирает CLI.
|
|
4
|
-
*
|
|
5
|
-
* Адресов собранного приложения здесь нет: платформа их наружу не отдаёт. В них
|
|
6
|
-
* зашит номер версии, и после следующей публикации такая ссылка продолжает
|
|
7
|
-
* отвечать старой копией, а не ломается. Версии называем номерами, ссылка одна
|
|
8
|
-
* и ведёт на страницу проекта.
|
|
9
|
-
*/
|
|
2
|
+
/** Public API responses parsed by the CLI. */
|
|
10
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/ui.js
CHANGED
|
@@ -10,14 +10,13 @@ exports.fail = fail;
|
|
|
10
10
|
exports.table = table;
|
|
11
11
|
exports.formatBytes = formatBytes;
|
|
12
12
|
exports.formatAge = formatAge;
|
|
13
|
+
exports.offerBrowser = offerBrowser;
|
|
13
14
|
exports.confirmWord = confirmWord;
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
16
|
const node_readline_1 = require("node:readline");
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Всё пишется через один модуль, а не через console: человек читает stdout,
|
|
19
|
-
* машина парсит его же, а служебные сообщения (шаги, предупреждения) обязаны
|
|
20
|
-
* уходить в stderr, иначе `xflow pull > file` сложит их в файл вместе с данными.
|
|
18
|
+
* All CLI output. Data goes to stdout, progress and warnings to stderr,
|
|
19
|
+
* so `xflow pull > file` captures data only.
|
|
21
20
|
*/
|
|
22
21
|
const color = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
23
22
|
function paint(code, text) {
|
|
@@ -30,11 +29,9 @@ exports.dim = dim;
|
|
|
30
29
|
const red = (t) => paint('31', t);
|
|
31
30
|
const green = (t) => paint('32', t);
|
|
32
31
|
const yellow = (t) => paint('33', t);
|
|
33
|
-
/** Данные: то, ради чего команду запускали. */
|
|
34
32
|
function out(line = '') {
|
|
35
33
|
process.stdout.write(`${line}\n`);
|
|
36
34
|
}
|
|
37
|
-
/** Ход работы: шаги, предупреждения, ошибки. */
|
|
38
35
|
function note(line = '') {
|
|
39
36
|
process.stderr.write(`${line}\n`);
|
|
40
37
|
}
|
|
@@ -50,7 +47,7 @@ function warn(message) {
|
|
|
50
47
|
function fail(message) {
|
|
51
48
|
note(red(`✗ ${message}`));
|
|
52
49
|
}
|
|
53
|
-
/**
|
|
50
|
+
/** Column-aligned rows, no header. */
|
|
54
51
|
function table(rows) {
|
|
55
52
|
if (rows.length === 0)
|
|
56
53
|
return;
|
|
@@ -87,17 +84,42 @@ function formatAge(iso) {
|
|
|
87
84
|
return `${hours} h ago`;
|
|
88
85
|
return `${Math.floor(hours / 24)} d ago`;
|
|
89
86
|
}
|
|
87
|
+
/** Open a link with the system handler. Failures are ignored: the link is already printed. */
|
|
88
|
+
function openUrl(url) {
|
|
89
|
+
// The URL reaches a shell on Windows: allow only plain http(s) links.
|
|
90
|
+
if (!/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._~/?=#-]*)?$/.test(url))
|
|
91
|
+
return;
|
|
92
|
+
const [command, args] = process.platform === 'win32'
|
|
93
|
+
? ['cmd', ['/c', 'start', '', url]]
|
|
94
|
+
: process.platform === 'darwin'
|
|
95
|
+
? ['open', [url]]
|
|
96
|
+
: ['xdg-open', [url]];
|
|
97
|
+
try {
|
|
98
|
+
(0, node_child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true }).unref();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// No browser at all (ssh session): the user opens the link elsewhere.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
90
104
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* Требуем ввести конкретное слово, а не «y»: `--force` перетирает чужую работу,
|
|
94
|
-
* и подтверждение должно стоить осознанного действия. В неинтерактивном режиме
|
|
95
|
-
* (CI, запуск из агента) подтвердить нельзя вовсе — это сознательно (D23).
|
|
105
|
+
* Offer to open the link on Enter. TTY only, so agent-driven runs are unaffected.
|
|
106
|
+
* Non-blocking: polling continues, Enter is just a shortcut. Returns a cleanup.
|
|
96
107
|
*/
|
|
108
|
+
function offerBrowser(url) {
|
|
109
|
+
if (!process.stdin.isTTY)
|
|
110
|
+
return () => { };
|
|
111
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
|
112
|
+
rl.on('line', () => {
|
|
113
|
+
openUrl(url);
|
|
114
|
+
rl.close();
|
|
115
|
+
});
|
|
116
|
+
return () => rl.close();
|
|
117
|
+
}
|
|
118
|
+
/** Dangerous-action confirmation: type the exact word. Unavailable without a TTY. */
|
|
97
119
|
async function confirmWord(question, expected) {
|
|
98
120
|
if (!process.stdin.isTTY) {
|
|
99
121
|
fail('Confirmation is only possible in an interactive terminal');
|
|
100
|
-
note((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite
|
|
122
|
+
note((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite work that is not yours'));
|
|
101
123
|
return false;
|
|
102
124
|
}
|
|
103
125
|
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
package/dist/version.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* попадает.
|
|
8
|
-
*/
|
|
9
|
-
exports.CLI_VERSION = '0.1.9';
|
|
10
|
-
/** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
|
|
4
|
+
/** Keep in sync with cli/package.json. */
|
|
5
|
+
exports.CLI_VERSION = '0.2.0';
|
|
6
|
+
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
11
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/dist/zip.js
CHANGED
|
@@ -10,9 +10,8 @@ exports.ZipError = ZipError;
|
|
|
10
10
|
const LOCAL_SIG = 0x04034b50;
|
|
11
11
|
const CENTRAL_SIG = 0x02014b50;
|
|
12
12
|
const EOCD_SIG = 0x06054b50;
|
|
13
|
-
/** Флаг «имена в UTF-8»: без него кириллица в путях читается как мохибейк. */
|
|
14
13
|
const FLAG_UTF8 = 0x0800;
|
|
15
|
-
/**
|
|
14
|
+
/** Fixed timestamp keeps archives reproducible. */
|
|
16
15
|
const DOS_DATE = 0x0021;
|
|
17
16
|
const DOS_TIME = 0x0000;
|
|
18
17
|
const CRC_TABLE = (() => {
|
|
@@ -47,13 +46,11 @@ function zipCreate(entries) {
|
|
|
47
46
|
const locals = [];
|
|
48
47
|
const centrals = [];
|
|
49
48
|
let offset = 0;
|
|
50
|
-
//
|
|
49
|
+
// Same tree, same archive: fixed order.
|
|
51
50
|
const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
52
51
|
for (const entry of sorted) {
|
|
53
52
|
const name = Buffer.from(entry.path, 'utf-8');
|
|
54
53
|
const crc = crc32(entry.content);
|
|
55
|
-
// Сжимаем, но только если это что-то даёт: у уже сжатых файлов (png, zip)
|
|
56
|
-
// deflate добавляет байты, и хранение как есть честнее.
|
|
57
54
|
const deflated = (0, node_zlib_1.deflateRawSync)(entry.content, { level: 6 });
|
|
58
55
|
const compressed = deflated.length < entry.content.length;
|
|
59
56
|
const method = compressed ? 8 : 0;
|
|
@@ -110,7 +107,7 @@ function zipCreate(entries) {
|
|
|
110
107
|
return Buffer.concat([...locals, central, eocd]);
|
|
111
108
|
}
|
|
112
109
|
function findEocd(buffer) {
|
|
113
|
-
//
|
|
110
|
+
// The archive comment can be up to 64 KB long: scan from the end.
|
|
114
111
|
const from = Math.max(0, buffer.length - 66_000);
|
|
115
112
|
for (let i = buffer.length - 22; i >= from; i--) {
|
|
116
113
|
if (buffer.readUInt32LE(i) === EOCD_SIG)
|
|
@@ -118,6 +115,13 @@ function findEocd(buffer) {
|
|
|
118
115
|
}
|
|
119
116
|
return -1;
|
|
120
117
|
}
|
|
118
|
+
/** Entries are written to disk by the caller: reject paths that escape the target. */
|
|
119
|
+
function isSafeEntryPath(path) {
|
|
120
|
+
if (path.length === 0 || path.startsWith('/') || /^[A-Za-z]:/.test(path) || path.includes('\0')) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return path.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
124
|
+
}
|
|
121
125
|
function zipRead(buffer) {
|
|
122
126
|
if (buffer.length < 22)
|
|
123
127
|
throw new ZipError('The file is too small to be a zip');
|
|
@@ -145,7 +149,7 @@ function zipRead(buffer) {
|
|
|
145
149
|
if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff) {
|
|
146
150
|
throw new ZipError(`The zip64 format is not supported: ${name}`);
|
|
147
151
|
}
|
|
148
|
-
if (buffer.readUInt32LE(localOffset) !== LOCAL_SIG) {
|
|
152
|
+
if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIG) {
|
|
149
153
|
throw new ZipError(`A damaged archive entry: ${name}`);
|
|
150
154
|
}
|
|
151
155
|
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
@@ -167,7 +171,10 @@ function zipRead(buffer) {
|
|
|
167
171
|
else {
|
|
168
172
|
throw new ZipError(`Unknown compression method (${method}) on ${name}`);
|
|
169
173
|
}
|
|
170
|
-
|
|
174
|
+
const path = name.replace(/\\/g, '/');
|
|
175
|
+
if (!isSafeEntryPath(path))
|
|
176
|
+
throw new ZipError(`Unsafe path in the archive: ${name}`);
|
|
177
|
+
entries.push({ path, content });
|
|
171
178
|
}
|
|
172
179
|
return entries;
|
|
173
180
|
}
|
package/package.json
CHANGED
package/skills/xflow/SKILL.md
CHANGED
|
@@ -40,8 +40,8 @@ restored within an hour of the data going back under the limit.
|
|
|
40
40
|
2. `npm run typecheck` for a two-second type check (older projects may not have the
|
|
41
41
|
script, then `npx tsc --noEmit`).
|
|
42
42
|
3. `npm run build` if the change is substantial, before deploying.
|
|
43
|
-
4. `xflow deploy` sends the sources
|
|
44
|
-
phase and the six-digit number of the version it built.
|
|
43
|
+
4. `xflow deploy` sends the sources, ships the cloud functions and builds the application
|
|
44
|
+
on the platform, printing each phase and the six-digit number of the version it built.
|
|
45
45
|
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
46
46
|
browser for them.
|
|
47
47
|
6. `xflow publish` makes that same version visible to visitors.
|
|
@@ -105,11 +105,16 @@ Interface rules, checked outside `src/components/ui` and `src/components/blocks`
|
|
|
105
105
|
|
|
106
106
|
## Cloud functions
|
|
107
107
|
|
|
108
|
-
Server-side code lives in `functions/<name>/index.ts` and exports `handler`.
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
Server-side code lives in `functions/<name>/index.ts` and exports `handler`. There is no
|
|
109
|
+
separate deploy command: `xflow deploy` ships the functions and then builds the application,
|
|
110
|
+
in that order. List what is live with `xflow functions list`. The handler returns
|
|
111
111
|
`{ statusCode, body }` where `body` is a JSON string.
|
|
112
112
|
|
|
113
|
+
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
114
|
+
next deploy deletes the function from the cloud, schedules included, and that cannot be
|
|
115
|
+
undone: a function created again later gets a different address. So never remove a function
|
|
116
|
+
directory to "clean up" unless the user asked for the function to go.
|
|
117
|
+
|
|
113
118
|
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
114
119
|
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
115
120
|
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
@@ -118,9 +123,9 @@ never crashed, not that logging is broken.
|
|
|
118
123
|
|
|
119
124
|
From the app, call a function through `src/lib/xflow.ts`:
|
|
120
125
|
`await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the project
|
|
121
|
-
token for you. Addresses are baked into the build
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
token for you. Addresses are baked into the build, which is why the functions go out first:
|
|
127
|
+
by the time the bundle is built they already exist, and a new function is never missing
|
|
128
|
+
from the application that calls it.
|
|
124
129
|
|
|
125
130
|
Treat a function as a public API: the token ships inside the frontend bundle, so anyone
|
|
126
131
|
who opens the app can call it.
|
|
@@ -131,8 +136,8 @@ functions read but the platform does not have. Values never come back out — th
|
|
|
131
136
|
they exist is inside the running function.
|
|
132
137
|
|
|
133
138
|
A function receives only the variables it mentions by name via `process.env.NAME`, so never
|
|
134
|
-
assemble a variable name from an expression. New values arrive on the next
|
|
135
|
-
|
|
139
|
+
assemble a variable name from an expression. New values arrive on the next `xflow deploy`,
|
|
140
|
+
not at the moment they are written.
|
|
136
141
|
|
|
137
142
|
To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
|
|
138
143
|
Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
|
|
@@ -184,12 +189,12 @@ read-only queries, migrations, function logs and invocations, schedules, environ
|
|
|
184
189
|
variables, versions, publish and rollback. They answer with aggregates and say explicitly
|
|
185
190
|
when a result is truncated, which parsing terminal output does not.
|
|
186
191
|
|
|
187
|
-
Code never travels through those tools. Sending sources
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
never expect the starting call to return a finished version.
|
|
192
|
+
Code never travels through those tools. Sending sources stays in the CLI (`xflow push`):
|
|
193
|
+
pulling a repository through tool calls burns the user's tokens for nothing. Building is
|
|
194
|
+
available through the tools, because it runs from the revision already stored on the
|
|
195
|
+
server: `deployments action=build` starts it and answers immediately, `action=status`
|
|
196
|
+
reports the phase. It ships the functions too, from that same revision. A build takes
|
|
197
|
+
minutes, so never expect the starting call to return a finished version.
|
|
193
198
|
|
|
194
199
|
## Do not
|
|
195
200
|
|