@getxflow/cli 0.1.10 → 0.3.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 +1 -1
- package/dist/bin.js +1 -5
- package/dist/commands/deploy.js +37 -0
- package/dist/commands/env.js +1 -1
- package/dist/commands/functions.js +2 -89
- package/dist/commands/projects.js +1 -1
- package/dist/commands/sources.js +2 -2
- package/dist/help.js +23 -7
- package/dist/template.js +1 -1
- package/dist/tree.js +3 -0
- package/dist/version.js +1 -1
- package/dist/zip.js +20 -0
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +47 -19
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ else's code belongs next to it.
|
|
|
21
21
|
| `status` / `push` / `pull` | state, sending and fetching sources |
|
|
22
22
|
| `deploy` / `publish` / `rollback` / `deployments` | build, publish, roll back, version history |
|
|
23
23
|
| `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
|
|
24
|
-
| `functions list`
|
|
24
|
+
| `functions list` | cloud functions of the project from `functions/<name>/index.ts`, shipped by `deploy` |
|
|
25
25
|
| `functions invoke` / `functions logs` | call a function, look at its crashes with the stack |
|
|
26
26
|
| `schedules list` / `set` / `rm` | running functions on a timer (timer triggers) |
|
|
27
27
|
| `env` / `env check` / `env set` | environment variables of the functions, values are never handed back |
|
package/dist/bin.js
CHANGED
|
@@ -69,10 +69,6 @@ async function run(args) {
|
|
|
69
69
|
}
|
|
70
70
|
throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
|
|
71
71
|
case 'functions':
|
|
72
|
-
if (second === 'deploy') {
|
|
73
|
-
await (0, functions_1.functionsDeploy)(rest);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
72
|
if (second === 'invoke') {
|
|
77
73
|
await (0, functions_1.functionsInvoke)(rest);
|
|
78
74
|
return;
|
|
@@ -85,7 +81,7 @@ async function run(args) {
|
|
|
85
81
|
await (0, functions_1.functionsList)();
|
|
86
82
|
return;
|
|
87
83
|
}
|
|
88
|
-
throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list,
|
|
84
|
+
throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, invoke and logs');
|
|
89
85
|
case 'logs':
|
|
90
86
|
await (0, logs_1.logs)(rest);
|
|
91
87
|
return;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,11 +4,14 @@ exports.deploy = deploy;
|
|
|
4
4
|
exports.publish = publish;
|
|
5
5
|
exports.rollback = rollback;
|
|
6
6
|
exports.deployments = deployments;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
7
9
|
const api_1 = require("../api");
|
|
8
10
|
const args_1 = require("../args");
|
|
9
11
|
const config_1 = require("../config");
|
|
10
12
|
const errors_1 = require("../errors");
|
|
11
13
|
const session_1 = require("../session");
|
|
14
|
+
const template_1 = require("../template");
|
|
12
15
|
const ui_1 = require("../ui");
|
|
13
16
|
const sources_1 = require("./sources");
|
|
14
17
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -40,6 +43,24 @@ async function waitForBuild(client, projectId, started) {
|
|
|
40
43
|
}
|
|
41
44
|
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Write the function addresses into the local .env.
|
|
48
|
+
*
|
|
49
|
+
* The build bakes its own copy of the map into the bundle, this one is for
|
|
50
|
+
* `npm run dev`: without it the local run calls a function that it has no
|
|
51
|
+
* address for.
|
|
52
|
+
*/
|
|
53
|
+
async function refreshFunctionsEnv(root, client, projectId) {
|
|
54
|
+
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
55
|
+
// No .env at all (fresh clone): recreate it fully, like link does.
|
|
56
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
|
|
57
|
+
(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');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
(0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
|
|
61
|
+
}
|
|
62
|
+
return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
|
|
63
|
+
}
|
|
43
64
|
async function deploy(args) {
|
|
44
65
|
const { root, config } = (0, config_1.requireProject)();
|
|
45
66
|
const client = (0, session_1.connect)(config);
|
|
@@ -68,6 +89,11 @@ async function deploy(args) {
|
|
|
68
89
|
reportIssues(e);
|
|
69
90
|
throw e;
|
|
70
91
|
}
|
|
92
|
+
// Removing a function cannot be undone: a new one gets a new address. Say it
|
|
93
|
+
// out loud rather than leaving it to be discovered in functions list.
|
|
94
|
+
if (started.removed_functions?.length) {
|
|
95
|
+
(0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
|
|
96
|
+
}
|
|
71
97
|
const build = await waitForBuild(client, config.projectId, started);
|
|
72
98
|
if (build.phase === 'failed') {
|
|
73
99
|
if (build.log_tail) {
|
|
@@ -78,6 +104,17 @@ async function deploy(args) {
|
|
|
78
104
|
}
|
|
79
105
|
(0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
|
|
80
106
|
(0, ui_1.out)(build.project_url);
|
|
107
|
+
// The functions were shipped by the build itself, so their addresses are known
|
|
108
|
+
// only now. A failure here does not undo a finished build: warn and stop there.
|
|
109
|
+
try {
|
|
110
|
+
const functions = await refreshFunctionsEnv(root, client, config.projectId);
|
|
111
|
+
if (functions.length > 0) {
|
|
112
|
+
(0, ui_1.note)((0, ui_1.dim)(` Functions in .env: ${functions.join(', ')}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
(0, ui_1.warn)('Could not refresh the function addresses in .env, the build itself is fine');
|
|
117
|
+
}
|
|
81
118
|
(0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
|
|
82
119
|
}
|
|
83
120
|
async function publish() {
|
package/dist/commands/env.js
CHANGED
|
@@ -126,7 +126,7 @@ async function envSet(args) {
|
|
|
126
126
|
// Values reach a function on its next deploy.
|
|
127
127
|
const users = referencedByFunctions(root).needed.get(name);
|
|
128
128
|
if (users && users.length > 0) {
|
|
129
|
-
(0, ui_1.note)((0, ui_1.dim)(` For the value to arrive,
|
|
129
|
+
(0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, run xflow deploy: it ships ${users.join(', ')} again`));
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
async function envRemove(args) {
|
|
@@ -2,68 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.functionsList = functionsList;
|
|
4
4
|
exports.functionsInvoke = functionsInvoke;
|
|
5
|
-
exports.functionsDeploy = functionsDeploy;
|
|
6
|
-
const node_fs_1 = require("node:fs");
|
|
7
|
-
const node_path_1 = require("node:path");
|
|
8
5
|
const api_1 = require("../api");
|
|
9
6
|
const args_1 = require("../args");
|
|
10
7
|
const config_1 = require("../config");
|
|
11
8
|
const errors_1 = require("../errors");
|
|
12
9
|
const session_1 = require("../session");
|
|
13
|
-
const template_1 = require("../template");
|
|
14
10
|
const ui_1 = require("../ui");
|
|
15
|
-
const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
|
|
16
11
|
const FUNCTIONS_DIR = 'functions';
|
|
17
|
-
function entryFor(root, name) {
|
|
18
|
-
for (const entry of ENTRY_NAMES) {
|
|
19
|
-
const candidate = (0, node_path_1.join)(root, FUNCTIONS_DIR, name, entry);
|
|
20
|
-
if ((0, node_fs_1.existsSync)(candidate))
|
|
21
|
-
return candidate;
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
function discover(root) {
|
|
26
|
-
const dir = (0, node_path_1.join)(root, FUNCTIONS_DIR);
|
|
27
|
-
if (!(0, node_fs_1.existsSync)(dir))
|
|
28
|
-
return [];
|
|
29
|
-
return (0, node_fs_1.readdirSync)(dir)
|
|
30
|
-
.filter((name) => (0, node_fs_1.statSync)((0, node_path_1.join)(dir, name)).isDirectory())
|
|
31
|
-
.filter((name) => entryFor(root, name) !== null)
|
|
32
|
-
.sort();
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
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.
|
|
37
|
-
*/
|
|
38
|
-
function bundle(root, entry) {
|
|
39
|
-
let esbuild;
|
|
40
|
-
try {
|
|
41
|
-
esbuild = require(require.resolve('esbuild', { paths: [root] }));
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
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');
|
|
45
|
-
}
|
|
46
|
-
const result = esbuild.buildSync({
|
|
47
|
-
entryPoints: [entry],
|
|
48
|
-
bundle: true,
|
|
49
|
-
platform: 'node',
|
|
50
|
-
target: 'node20',
|
|
51
|
-
format: 'cjs',
|
|
52
|
-
external: ['pg'],
|
|
53
|
-
write: false,
|
|
54
|
-
logLevel: 'silent',
|
|
55
|
-
});
|
|
56
|
-
const text = result.outputFiles[0]?.text;
|
|
57
|
-
if (!text)
|
|
58
|
-
throw new errors_1.CliError(`Building ${entry} produced nothing`);
|
|
59
|
-
return text;
|
|
60
|
-
}
|
|
61
12
|
async function functionsList() {
|
|
62
13
|
const { config } = (0, config_1.requireProject)();
|
|
63
14
|
const client = (0, session_1.connect)(config);
|
|
64
15
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
|
|
65
16
|
if (data.functions.length === 0) {
|
|
66
|
-
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow
|
|
17
|
+
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
|
|
67
18
|
return;
|
|
68
19
|
}
|
|
69
20
|
(0, ui_1.table)(data.functions.map((fn) => [
|
|
@@ -73,18 +24,6 @@ async function functionsList() {
|
|
|
73
24
|
fn.error_message ?? '',
|
|
74
25
|
]));
|
|
75
26
|
}
|
|
76
|
-
/** Write function URLs into .env, where the build picks them up. */
|
|
77
|
-
async function refreshFunctionsEnv(root, client, projectId) {
|
|
78
|
-
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
79
|
-
// No .env at all (fresh clone): recreate it fully, like link does.
|
|
80
|
-
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
|
|
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');
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
(0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
|
|
85
|
-
}
|
|
86
|
-
return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
|
|
87
|
-
}
|
|
88
27
|
function prettyBody(text) {
|
|
89
28
|
try {
|
|
90
29
|
return JSON.stringify(JSON.parse(text), null, 2);
|
|
@@ -105,7 +44,7 @@ async function functionsInvoke(args) {
|
|
|
105
44
|
if (!fn || !fn.invoke_url) {
|
|
106
45
|
throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
|
|
107
46
|
? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
|
|
108
|
-
:
|
|
47
|
+
: 'Functions ship with the build: xflow deploy');
|
|
109
48
|
}
|
|
110
49
|
const data = (0, args_1.flagString)(args, 'data');
|
|
111
50
|
const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
|
|
@@ -136,29 +75,3 @@ async function functionsInvoke(args) {
|
|
|
136
75
|
throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
|
|
137
76
|
}
|
|
138
77
|
}
|
|
139
|
-
async function functionsDeploy(args) {
|
|
140
|
-
const { root, config } = (0, config_1.requireProject)();
|
|
141
|
-
const client = (0, session_1.connect)(config);
|
|
142
|
-
const wanted = args.words[1];
|
|
143
|
-
const names = wanted ? [wanted] : discover(root);
|
|
144
|
-
if (names.length === 0) {
|
|
145
|
-
throw new errors_1.CliError(`The project has no functions`, `Create ${FUNCTIONS_DIR}/<name>/index.ts exporting handler and try again`);
|
|
146
|
-
}
|
|
147
|
-
for (const name of names) {
|
|
148
|
-
const entry = entryFor(root, name);
|
|
149
|
-
if (!entry) {
|
|
150
|
-
throw new errors_1.CliError(`Could not find ${FUNCTIONS_DIR}/${name}/index.ts`, `Available functions: ${discover(root).join(', ') || 'none at all'}`);
|
|
151
|
-
}
|
|
152
|
-
(0, ui_1.step)(`Building ${name}`);
|
|
153
|
-
const code = bundle(root, entry);
|
|
154
|
-
(0, ui_1.step)(`Deploying ${name}`);
|
|
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' });
|
|
156
|
-
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} deployed`);
|
|
157
|
-
(0, ui_1.out)(result.url);
|
|
158
|
-
if (result.secrets.length > 0) {
|
|
159
|
-
(0, ui_1.note)((0, ui_1.dim)(` Organization secrets in the environment: ${result.secrets.join(', ')}`));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const available = await refreshFunctionsEnv(root, client, config.projectId);
|
|
163
|
-
(0, ui_1.note)((0, ui_1.dim)(` Addresses in .env updated (${available.join(', ')}). In the frontend: xflow.functions.invoke('${names[0]}')`));
|
|
164
|
-
}
|
|
@@ -63,7 +63,7 @@ async function init(args) {
|
|
|
63
63
|
}
|
|
64
64
|
(0, ui_1.step)('Fetching the template');
|
|
65
65
|
const archive = await (0, api_1.apiBinary)(client, `/api/v1/templates/${templateId}/archive`);
|
|
66
|
-
const files = (0, zip_1.zipRead)(archive);
|
|
66
|
+
const files = (0, zip_1.dropDirectoryEntries)((0, zip_1.zipRead)(archive));
|
|
67
67
|
(0, ui_1.step)('Creating the project on the platform');
|
|
68
68
|
const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
|
|
69
69
|
method: 'POST',
|
package/dist/commands/sources.js
CHANGED
|
@@ -47,7 +47,7 @@ async function confirmForce(client, projectId, server, local) {
|
|
|
47
47
|
(0, ui_1.warn)(`The server holds revision ${server.revision} from ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} files`);
|
|
48
48
|
try {
|
|
49
49
|
const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/pull`);
|
|
50
|
-
const serverPaths = new Set((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url)).map((e) => e.path));
|
|
50
|
+
const serverPaths = new Set((0, zip_1.dropDirectoryEntries)((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url))).map((e) => e.path));
|
|
51
51
|
const localPaths = new Set(local.map((f) => f.path));
|
|
52
52
|
const disappearing = [...serverPaths].filter((p) => !localPaths.has(p));
|
|
53
53
|
if (disappearing.length > 0) {
|
|
@@ -132,7 +132,7 @@ async function pull(args) {
|
|
|
132
132
|
(0, ui_1.warn)('The hash of the downloaded tree did not match the server one: the contents may have changed in transit');
|
|
133
133
|
}
|
|
134
134
|
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
135
|
-
for (const entry of entries) {
|
|
135
|
+
for (const entry of (0, zip_1.dropDirectoryEntries)(entries)) {
|
|
136
136
|
const path = (0, node_path_1.join)(target, entry.path);
|
|
137
137
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
138
138
|
(0, node_fs_1.writeFileSync)(path, entry.content);
|
package/dist/help.js
CHANGED
|
@@ -26,14 +26,13 @@ ${(0, ui_1.bold)('Code')}
|
|
|
26
26
|
fetch the sources (the latest revision by default)
|
|
27
27
|
|
|
28
28
|
${(0, ui_1.bold)('Releasing')}
|
|
29
|
-
xflow deploy [--no-push] send the code, build on the platform
|
|
29
|
+
xflow deploy [--no-push] send the code, ship the functions, build on the platform
|
|
30
30
|
xflow publish show the dev version to visitors
|
|
31
31
|
xflow rollback <version number> return the project to an earlier version
|
|
32
32
|
xflow deployments version history
|
|
33
33
|
|
|
34
34
|
${(0, ui_1.bold)('Functions')}
|
|
35
|
-
xflow functions list what is deployed
|
|
36
|
-
xflow functions deploy [name] build and ship a function
|
|
35
|
+
xflow functions list what is deployed (they ship with xflow deploy)
|
|
37
36
|
xflow functions invoke <name> [--data '{"a":1}']
|
|
38
37
|
call a function and print the answer
|
|
39
38
|
xflow functions logs [name] function crashes: stack and console output
|
|
@@ -125,8 +124,9 @@ mentions by name. A name assembled from an expression (${(0, ui_1.bold)("process
|
|
|
125
124
|
never reaches the environment, so read variables literally.
|
|
126
125
|
|
|
127
126
|
The value reaches the function on deploy, not at the moment it is stored: after
|
|
128
|
-
${(0, ui_1.bold)('env set')}
|
|
129
|
-
|
|
127
|
+
${(0, ui_1.bold)('env set')} run ${(0, ui_1.bold)('xflow deploy')}. The build ships a function whose code did not
|
|
128
|
+
change but whose variables did, so nothing is left holding an old value. The same after a
|
|
129
|
+
delete: a function already deployed keeps the old value until its next deploy.`,
|
|
130
130
|
schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
|
|
131
131
|
|
|
132
132
|
A schedule is a Yandex timer trigger: it calls the function itself, with no
|
|
@@ -224,8 +224,9 @@ into a non-empty one: the platform cannot merge changes, that is git's job.
|
|
|
224
224
|
--force overwrite the folder completely`,
|
|
225
225
|
deploy: `${(0, ui_1.bold)('xflow deploy')}: build and release
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
output directory come from xflow.json (npm run build and dist
|
|
227
|
+
Three steps: sending the sources, shipping the cloud functions, building the application.
|
|
228
|
+
The build command and the output directory come from xflow.json (npm run build and dist
|
|
229
|
+
by default).
|
|
229
230
|
|
|
230
231
|
--no-push do not send sources, build from the latest server revision
|
|
231
232
|
--force allow overwriting the server revision while sending
|
|
@@ -235,6 +236,21 @@ worked on my machine" no longer depends on your machine. Before the build the pr
|
|
|
235
236
|
is checked against the template: mismatches are printed as a list and the build does
|
|
236
237
|
not start at all.
|
|
237
238
|
|
|
239
|
+
Everything in ${(0, ui_1.bold)('functions/<name>/index.ts')} is bundled and shipped by the same run,
|
|
240
|
+
before the application is built. That order is not a convention but the only one that
|
|
241
|
+
works: the addresses of the functions are baked into the bundle, so they have to exist
|
|
242
|
+
first. A function whose code and variables did not change is left alone, and a function
|
|
243
|
+
that fails to ship fails the whole build.
|
|
244
|
+
|
|
245
|
+
A function gone from the sources is deleted from the cloud along with its schedules, and
|
|
246
|
+
the CLI names it before the build starts. That one is final: a function created again
|
|
247
|
+
later gets a different address. If the sources hold no functions at all while the cloud
|
|
248
|
+
holds several, nothing is deleted: that looks like a directory which never made it (a
|
|
249
|
+
${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
|
|
250
|
+
|
|
251
|
+
The database is not part of this: migrations change data in ways nothing can undo, so
|
|
252
|
+
they stay their own command (${(0, ui_1.bold)('xflow db migrate')}).
|
|
253
|
+
|
|
238
254
|
The built version is visible on the project page, and the CLI prints the link. Visitors
|
|
239
255
|
see it after ${(0, ui_1.bold)('xflow publish')}. The address of the build itself is not printed:
|
|
240
256
|
it carries the version number, and after the next publish such a link quietly serves an
|
package/dist/template.js
CHANGED
|
@@ -25,7 +25,7 @@ xflow publish # show the dev version to visitors
|
|
|
25
25
|
\`\`\`
|
|
26
26
|
|
|
27
27
|
The interface is built on the template components: \`src/components/ui\` (buttons, fields,
|
|
28
|
-
dialogs) and \`src/components/blocks\` (
|
|
28
|
+
dialogs) and \`src/components/blocks\` (a data table and charts). Layout built
|
|
29
29
|
on top of them looks like the rest of the platform, while custom colors in place of the
|
|
30
30
|
tokens from \`src/index.css\` break that consistency.
|
|
31
31
|
|
package/dist/tree.js
CHANGED
|
@@ -19,6 +19,9 @@ const DEFAULT_IGNORE = [
|
|
|
19
19
|
'.env.*',
|
|
20
20
|
'.DS_Store',
|
|
21
21
|
'Thumbs.db',
|
|
22
|
+
// Compiler cache: rewritten by every local build, so it would make a new
|
|
23
|
+
// revision out of nothing.
|
|
24
|
+
'tsconfig.tsbuildinfo',
|
|
22
25
|
];
|
|
23
26
|
const IGNORE_FILE = '.xflowignore';
|
|
24
27
|
function globToRegExpSource(pattern) {
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.3.0';
|
|
6
6
|
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
7
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/dist/zip.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZipError = void 0;
|
|
4
4
|
exports.zipCreate = zipCreate;
|
|
5
|
+
exports.dropDirectoryEntries = dropDirectoryEntries;
|
|
5
6
|
exports.zipRead = zipRead;
|
|
6
7
|
const node_zlib_1 = require("node:zlib");
|
|
7
8
|
class ZipError extends Error {
|
|
@@ -122,6 +123,25 @@ function isSafeEntryPath(path) {
|
|
|
122
123
|
}
|
|
123
124
|
return path.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
124
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Drop entries whose name is a directory elsewhere in the same archive: old
|
|
128
|
+
* revisions carry folders as empty files, and unpacking them collides with the
|
|
129
|
+
* directory of the same name. Call it before writing to disk, not inside
|
|
130
|
+
* zipRead: the tree hash is checked against the archive as the server made it.
|
|
131
|
+
*/
|
|
132
|
+
function dropDirectoryEntries(entries) {
|
|
133
|
+
const directories = new Set();
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const segments = entry.path.split('/');
|
|
136
|
+
segments.pop();
|
|
137
|
+
let current = '';
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
current = current ? `${current}/${segment}` : segment;
|
|
140
|
+
directories.add(current);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return entries.filter((entry) => !directories.has(entry.path));
|
|
144
|
+
}
|
|
125
145
|
function zipRead(buffer) {
|
|
126
146
|
if (buffer.length < 22)
|
|
127
147
|
throw new ZipError('The file is too small to be a zip');
|
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.
|
|
@@ -103,13 +103,42 @@ Interface rules, checked outside `src/components/ui` and `src/components/blocks`
|
|
|
103
103
|
`react-hot-toast`, `react-dnd`) must have it mounted in `App.tsx`. Missing providers
|
|
104
104
|
build fine and give visitors a white screen.
|
|
105
105
|
|
|
106
|
+
Template integrity. The app grows out of the platform template, and part of that template
|
|
107
|
+
is not yours to change. The reference is a snapshot of the project itself, taken when the
|
|
108
|
+
platform first looked at it, so these rules never argue with work that was already there:
|
|
109
|
+
|
|
110
|
+
- Platform files must stay byte for byte as they arrived: `src/lib/theme-sync.ts`,
|
|
111
|
+
`src/lib/platform-auth.ts`, `src/contexts/platform-auth-context.tsx`,
|
|
112
|
+
`src/hooks/use-platform-auth.ts`, `src/utils/error-logger.ts`, `src/lib/xflow.ts`.
|
|
113
|
+
They wire the app to the platform, and every way they break is a silent one. Build what
|
|
114
|
+
you need around them, never inside them.
|
|
115
|
+
- The entry point keeps calling `initThemeSync()`, `initPlatformAuth()` and
|
|
116
|
+
`initErrorLogger()`, keeps importing `index.css` and keeps mounting `ThemeProvider`.
|
|
117
|
+
How the file is written is up to you.
|
|
118
|
+
- `index.html` keeps the element with `id="root"` and the script that loads `src/main`.
|
|
119
|
+
- Theme token names in `src/index.css` stay declared, in `:root` and in `.dark` alike, and
|
|
120
|
+
the Tailwind config keeps mapping them. Change the values as much as the design needs:
|
|
121
|
+
it is the names that components paint with.
|
|
122
|
+
- The Tailwind `content` globs keep covering `src/**`. Narrow them and Tailwind strips
|
|
123
|
+
every class the app uses.
|
|
124
|
+
- Files under `src/components/ui` and `src/components/blocks` may be edited freely but
|
|
125
|
+
not deleted.
|
|
126
|
+
|
|
127
|
+
A rejection names the file and the revision to take the original from:
|
|
128
|
+
`xflow pull --revision N --into ./original`, then copy the file back.
|
|
129
|
+
|
|
106
130
|
## Cloud functions
|
|
107
131
|
|
|
108
|
-
Server-side code lives in `functions/<name>/index.ts` and exports `handler`.
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
Server-side code lives in `functions/<name>/index.ts` and exports `handler`. There is no
|
|
133
|
+
separate deploy command: `xflow deploy` ships the functions and then builds the application,
|
|
134
|
+
in that order. List what is live with `xflow functions list`. The handler returns
|
|
111
135
|
`{ statusCode, body }` where `body` is a JSON string.
|
|
112
136
|
|
|
137
|
+
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
138
|
+
next deploy deletes the function from the cloud, schedules included, and that cannot be
|
|
139
|
+
undone: a function created again later gets a different address. So never remove a function
|
|
140
|
+
directory to "clean up" unless the user asked for the function to go.
|
|
141
|
+
|
|
113
142
|
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
114
143
|
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
115
144
|
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
@@ -118,9 +147,9 @@ never crashed, not that logging is broken.
|
|
|
118
147
|
|
|
119
148
|
From the app, call a function through `src/lib/xflow.ts`:
|
|
120
149
|
`await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the project
|
|
121
|
-
token for you. Addresses are baked into the build
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
token for you. Addresses are baked into the build, which is why the functions go out first:
|
|
151
|
+
by the time the bundle is built they already exist, and a new function is never missing
|
|
152
|
+
from the application that calls it.
|
|
124
153
|
|
|
125
154
|
Treat a function as a public API: the token ships inside the frontend bundle, so anyone
|
|
126
155
|
who opens the app can call it.
|
|
@@ -131,8 +160,8 @@ functions read but the platform does not have. Values never come back out — th
|
|
|
131
160
|
they exist is inside the running function.
|
|
132
161
|
|
|
133
162
|
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
|
-
|
|
163
|
+
assemble a variable name from an expression. New values arrive on the next `xflow deploy`,
|
|
164
|
+
not at the moment they are written.
|
|
136
165
|
|
|
137
166
|
To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
|
|
138
167
|
Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
|
|
@@ -184,12 +213,12 @@ read-only queries, migrations, function logs and invocations, schedules, environ
|
|
|
184
213
|
variables, versions, publish and rollback. They answer with aggregates and say explicitly
|
|
185
214
|
when a result is truncated, which parsing terminal output does not.
|
|
186
215
|
|
|
187
|
-
Code never travels through those tools. Sending sources
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
never expect the starting call to return a finished version.
|
|
216
|
+
Code never travels through those tools. Sending sources stays in the CLI (`xflow push`):
|
|
217
|
+
pulling a repository through tool calls burns the user's tokens for nothing. Building is
|
|
218
|
+
available through the tools, because it runs from the revision already stored on the
|
|
219
|
+
server: `deployments action=build` starts it and answers immediately, `action=status`
|
|
220
|
+
reports the phase. It ships the functions too, from that same revision. A build takes
|
|
221
|
+
minutes, so never expect the starting call to return a finished version.
|
|
193
222
|
|
|
194
223
|
## Do not
|
|
195
224
|
|
|
@@ -204,8 +233,7 @@ The platform design system already ships inside the project, and the app is supp
|
|
|
204
233
|
to look like a part of the platform:
|
|
205
234
|
|
|
206
235
|
- `src/components/ui` for primitives: buttons, inputs, dialogs, tables, menus
|
|
207
|
-
- `src/components/blocks` for composed blocks: `data-table
|
|
208
|
-
`filter-bar`, `detail-panel`, `kanban`, `charts`, `user-picker`, `relation-picker`
|
|
236
|
+
- `src/components/blocks` for composed blocks: `data-table` and `charts`
|
|
209
237
|
- `src/index.css` for color tokens
|
|
210
238
|
|
|
211
239
|
Before writing your own component, check whether a block already covers it: props are
|
|
@@ -216,7 +244,7 @@ the design system sits in the project.
|
|
|
216
244
|
|
|
217
245
|
The app runs inside the platform in an iframe and receives the theme and the current
|
|
218
246
|
user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the
|
|
219
|
-
list of organization members
|
|
247
|
+
list of organization members.
|
|
220
248
|
|
|
221
249
|
## Errors from a deployed app
|
|
222
250
|
|