@getxflow/cli 1.1.0 → 1.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 +0 -5
- package/dist/args.js +0 -2
- package/dist/bin.js +0 -7
- package/dist/commands/connections.js +2 -2
- package/dist/commands/db.js +6 -4
- package/dist/commands/deploy.js +8 -32
- package/dist/commands/functions.js +1 -1
- package/dist/commands/org.js +0 -7
- package/dist/commands/pages.js +0 -1
- package/dist/commands/skills.js +3 -35
- package/dist/commands/sources.js +12 -33
- package/dist/flags.js +3 -6
- package/dist/help.js +26 -56
- package/dist/limits.js +3 -0
- package/dist/template.js +1 -1
- package/dist/ui.js +0 -19
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/skills/xflow/SKILL.md +718 -729
- package/dist/commands/mcp.js +0 -100
- package/dist/prompt.js +0 -113
package/README.md
CHANGED
|
@@ -31,7 +31,6 @@ else's code belongs next to it.
|
|
|
31
31
|
| `logs` | browser errors from the released application |
|
|
32
32
|
| `projects list` / `projects get` | projects of the organization and the application addresses |
|
|
33
33
|
| `skills` | platform instructions for an AI agent ([Agent Skills](https://agentskills.io) format) |
|
|
34
|
-
| `mcp install` | connect the agent to the platform directly, without a terminal |
|
|
35
34
|
| `update` | update the CLI itself, and the skill that ships inside it |
|
|
36
35
|
|
|
37
36
|
Help for one command: `xflow help <command>`.
|
|
@@ -42,9 +41,5 @@ OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `C
|
|
|
42
41
|
left alone. The description ships inside the package and shares its version, so `xflow update`
|
|
43
42
|
brings both.
|
|
44
43
|
|
|
45
|
-
`xflow mcp install` connects the agent to the platform directly: the database, migrations,
|
|
46
|
-
functions, schedules, variables and versions become its tools, with no command output to parse.
|
|
47
|
-
Code, builds and releases stay in the CLI.
|
|
48
|
-
|
|
49
44
|
The key is stored in `~/.xflow/credentials.json`, the project configuration in `xflow.json`.
|
|
50
45
|
For CI, pass the key in the `XFLOW_TOKEN` variable.
|
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -20,7 +20,6 @@ const db_1 = require("./commands/db");
|
|
|
20
20
|
const env_1 = require("./commands/env");
|
|
21
21
|
const functions_1 = require("./commands/functions");
|
|
22
22
|
const logs_1 = require("./commands/logs");
|
|
23
|
-
const mcp_1 = require("./commands/mcp");
|
|
24
23
|
const org_1 = require("./commands/org");
|
|
25
24
|
const schedules_1 = require("./commands/schedules");
|
|
26
25
|
const skills_1 = require("./commands/skills");
|
|
@@ -93,12 +92,6 @@ async function run(args) {
|
|
|
93
92
|
case 'skills':
|
|
94
93
|
await (0, skills_1.skills)(rest);
|
|
95
94
|
return;
|
|
96
|
-
case 'mcp':
|
|
97
|
-
if (second === undefined || second === 'install') {
|
|
98
|
-
await (0, mcp_1.mcpInstall)(rest);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
|
|
102
95
|
case 'functions':
|
|
103
96
|
if (second === 'invoke') {
|
|
104
97
|
await (0, functions_1.functionsInvoke)(rest);
|
|
@@ -143,10 +143,10 @@ async function connectionsUnlink(args) {
|
|
|
143
143
|
const inUse = row.env
|
|
144
144
|
.map((name) => ({ name, users: needed.get(name) ?? [] }))
|
|
145
145
|
.filter((entry) => entry.users.length > 0);
|
|
146
|
-
if (inUse.length > 0 && !(0, args_1.flagBool)(args, '
|
|
146
|
+
if (inUse.length > 0 && !(0, args_1.flagBool)(args, 'yes')) {
|
|
147
147
|
(0, ui_1.fail)(`The functions of this project read the variables of ${row.alias}:`);
|
|
148
148
|
(0, ui_1.table)(inUse.map((entry) => [entry.name, entry.users.join(', ')]));
|
|
149
|
-
throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --
|
|
149
|
+
throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --yes if that is intended');
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
const result = await (0, api_1.apiJson)(client, `${endpoint(projectId)}?connection_id=${encodeURIComponent(row.id)}`, { method: 'DELETE' });
|
package/dist/commands/db.js
CHANGED
|
@@ -296,19 +296,21 @@ async function dbMove(args) {
|
|
|
296
296
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
297
297
|
const path = `/api/v1/projects/${projectId}/db/move`;
|
|
298
298
|
let place;
|
|
299
|
+
// The platform takes the project name as the yes; --yes looks it up.
|
|
300
|
+
const confirm = (0, args_1.flagBool)(args, 'yes')
|
|
301
|
+
? (await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`)).name
|
|
302
|
+
: undefined;
|
|
299
303
|
try {
|
|
300
304
|
place = await (0, api_1.apiJson)(client, path, {
|
|
301
305
|
method: 'POST',
|
|
302
|
-
body: { to, confirm
|
|
306
|
+
body: { to, confirm },
|
|
303
307
|
timeoutMs: 60_000,
|
|
304
308
|
});
|
|
305
309
|
}
|
|
306
310
|
catch (e) {
|
|
307
311
|
// Said in the words of this command: the flag, and the name that goes in it.
|
|
308
312
|
if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
|
|
309
|
-
|
|
310
|
-
const name = found?.database?.name ?? '<project name>';
|
|
311
|
-
throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --confirm "${name}"`);
|
|
313
|
+
throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --yes`);
|
|
312
314
|
}
|
|
313
315
|
throw e;
|
|
314
316
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -10,7 +10,6 @@ const api_1 = require("../api");
|
|
|
10
10
|
const args_1 = require("../args");
|
|
11
11
|
const config_1 = require("../config");
|
|
12
12
|
const errors_1 = require("../errors");
|
|
13
|
-
const prompt_1 = require("../prompt");
|
|
14
13
|
const session_1 = require("../session");
|
|
15
14
|
const tree_1 = require("../tree");
|
|
16
15
|
const ui_1 = require("../ui");
|
|
@@ -49,9 +48,8 @@ async function waitForBuild(client, base, started) {
|
|
|
49
48
|
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Build ${started.deploy_id}`);
|
|
50
49
|
}
|
|
51
50
|
/**
|
|
52
|
-
* Start the build
|
|
53
|
-
*
|
|
54
|
-
* and the question is asked here, before it starts.
|
|
51
|
+
* Start the build. Removing a cloud function cannot be undone, so the platform
|
|
52
|
+
* refuses such a deploy until --allow-removals says yes.
|
|
55
53
|
*/
|
|
56
54
|
async function startBuild(client, base, revision, allowRemovals, comment) {
|
|
57
55
|
const path = `${base}/builds`;
|
|
@@ -63,22 +61,9 @@ async function startBuild(client, base, revision, allowRemovals, comment) {
|
|
|
63
61
|
reportIssues(e);
|
|
64
62
|
if (!(e instanceof api_1.ApiError) || e.code !== 'confirm_required')
|
|
65
63
|
throw e;
|
|
66
|
-
|
|
67
|
-
if (removing.length === 0)
|
|
64
|
+
if (!e.removing?.length)
|
|
68
65
|
throw e;
|
|
69
|
-
|
|
70
|
-
if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) {
|
|
71
|
-
throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. If it is not, bring the functions/<name>/index.ts directories back`);
|
|
72
|
-
}
|
|
73
|
-
(0, ui_1.warn)(`Gone from the sources, the deploy would remove them: ${removing.join(', ')}`);
|
|
74
|
-
(0, ui_1.note)((0, ui_1.dim)(` ${REMOVAL_IS_FINAL}`));
|
|
75
|
-
const answer = await (0, prompt_1.select)('Remove them from the cloud?', [
|
|
76
|
-
{ label: 'No, stop here', hint: 'bring the functions/<name>/index.ts directories back' },
|
|
77
|
-
{ label: 'Yes, remove and deploy', hint: removing.join(', ') },
|
|
78
|
-
]);
|
|
79
|
-
if (answer !== 1)
|
|
80
|
-
throw new errors_1.CliError('The deploy did not start', 'Nothing was removed');
|
|
81
|
-
return (0, api_1.apiJson)(client, path, { method: 'POST', body: { revision, allow_removals: true, comment } });
|
|
66
|
+
throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. If it is not, bring the functions/<name>/index.ts directories back`);
|
|
82
67
|
}
|
|
83
68
|
}
|
|
84
69
|
/** Keep in sync with lib/version-comment.ts on the platform. */
|
|
@@ -187,19 +172,10 @@ async function deployTarget(target, args, comment, revision) {
|
|
|
187
172
|
await (0, api_1.apiJson)(client, `${base}/builds?check=plan`);
|
|
188
173
|
if (target.kind === 'app' && target.projectRoot)
|
|
189
174
|
await warnUndeployedRoot(client, target.projectRoot, target.projectId);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
revision = server.revision;
|
|
195
|
-
(0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const stream = target.kind === 'app'
|
|
199
|
-
? { kind: 'app', root: target.root, appId: target.appId }
|
|
200
|
-
: { kind: 'project', root: target.root, projectId: target.projectId };
|
|
201
|
-
revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
202
|
-
}
|
|
175
|
+
const stream = target.kind === 'app'
|
|
176
|
+
? { kind: 'app', root: target.root, appId: target.appId }
|
|
177
|
+
: { kind: 'project', root: target.root, projectId: target.projectId };
|
|
178
|
+
revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
203
179
|
if (target.kind === 'project')
|
|
204
180
|
await (0, db_1.migrateBeforeDeploy)(client, target.root, target.projectId);
|
|
205
181
|
}
|
|
@@ -24,7 +24,7 @@ async function functionsList(args) {
|
|
|
24
24
|
fn.last_called_at ? `called ${(0, ui_1.formatAge)(fn.last_called_at)}` : 'never called',
|
|
25
25
|
fn.last_error_at ? `failed ${(0, ui_1.formatAge)(fn.last_error_at)}` : '',
|
|
26
26
|
]));
|
|
27
|
-
(0, ui_1.note)((0, ui_1.dim)(' callers: the app folders
|
|
27
|
+
(0, ui_1.note)((0, ui_1.dim)(' callers: the app folders allowed to call it (functions in the root xflow.json)'));
|
|
28
28
|
(0, ui_1.note)((0, ui_1.dim)(' open: a caller is public, so anyone on the internet can call it; no callers: a workflow'));
|
|
29
29
|
(0, ui_1.note)((0, ui_1.dim)(` Webhooks and other functions call ${functions[0].url.replace(/\/[^/]+$/, '/<name>')} with a key`));
|
|
30
30
|
}
|
package/dist/commands/org.js
CHANGED
|
@@ -9,7 +9,6 @@ const credentials_1 = require("../credentials");
|
|
|
9
9
|
const errors_1 = require("../errors");
|
|
10
10
|
const session_1 = require("../session");
|
|
11
11
|
const ui_1 = require("../ui");
|
|
12
|
-
const mcp_1 = require("./mcp");
|
|
13
12
|
function label(org) {
|
|
14
13
|
return org.name ?? org.organizationId;
|
|
15
14
|
}
|
|
@@ -75,16 +74,10 @@ function orgSwitch(args) {
|
|
|
75
74
|
(0, ui_1.ok)(`Active organization: "${label(target)}"`);
|
|
76
75
|
const root = (0, config_1.findProjectRoot)();
|
|
77
76
|
if (root) {
|
|
78
|
-
// The agent entry of a project folder belongs to the folder's organization,
|
|
79
|
-
// so the switch leaves it alone.
|
|
80
77
|
const boundId = (0, config_1.readState)(root).organizationId;
|
|
81
78
|
if (boundId && boundId !== target.organizationId) {
|
|
82
79
|
const bound = orgs.find((org) => org.organizationId === boundId);
|
|
83
80
|
(0, ui_1.note)((0, ui_1.dim)(` This folder is bound to "${bound ? label(bound) : boundId}": commands here stay in it`));
|
|
84
81
|
}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if ((0, mcp_1.refreshAgentKey)({ apiUrl, token: target.token })) {
|
|
88
|
-
(0, ui_1.note)((0, ui_1.dim)(' The agent key here is rewritten: restart the agent session to pick it up'));
|
|
89
82
|
}
|
|
90
83
|
}
|
package/dist/commands/pages.js
CHANGED
|
@@ -99,7 +99,6 @@ async function pagesCreate(args) {
|
|
|
99
99
|
(0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
100
100
|
(0, ui_1.out)(result.page.url);
|
|
101
101
|
(0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The next version: xflow pages deploy ${result.page.slug} <path>`));
|
|
102
|
-
(0, ui_1.note)((0, ui_1.dim)(` A backend for it: name "${result.page.slug}" in functions.<name>.callers of a project, xflow deploy functions, then fetch /_fn/<name>`));
|
|
103
102
|
}
|
|
104
103
|
function contentTypeOf(upload) {
|
|
105
104
|
return upload.kind === 'zip' ? 'application/zip' : 'text/html';
|
package/dist/commands/skills.js
CHANGED
|
@@ -13,7 +13,6 @@ const node_os_1 = require("node:os");
|
|
|
13
13
|
const node_path_1 = require("node:path");
|
|
14
14
|
const args_1 = require("../args");
|
|
15
15
|
const errors_1 = require("../errors");
|
|
16
|
-
const prompt_1 = require("../prompt");
|
|
17
16
|
const update_1 = require("./update");
|
|
18
17
|
const ui_1 = require("../ui");
|
|
19
18
|
const AGENTS = [
|
|
@@ -322,30 +321,6 @@ function listAgents(base) {
|
|
|
322
321
|
(0, ui_1.note)((0, ui_1.dim)(' * installed. Columns: id, agent, in the project, in the home folder'));
|
|
323
322
|
(0, ui_1.note)((0, ui_1.dim)(' Install without questions: xflow skills --agent <id,id> [--global]'));
|
|
324
323
|
}
|
|
325
|
-
async function pick(globalFlag) {
|
|
326
|
-
let global = globalFlag;
|
|
327
|
-
if (!globalFlag) {
|
|
328
|
-
const scope = await (0, prompt_1.select)('Where to install the xflow skill', [
|
|
329
|
-
{ label: 'This project', hint: process.cwd() },
|
|
330
|
-
{ label: 'Globally', hint: 'home folder, visible to every project' },
|
|
331
|
-
]);
|
|
332
|
-
if (scope === null)
|
|
333
|
-
return null;
|
|
334
|
-
global = scope === 1;
|
|
335
|
-
}
|
|
336
|
-
const base = process.cwd();
|
|
337
|
-
const present = AGENTS.map((agent) => installed(base, agent, global));
|
|
338
|
-
const anyPresent = present.includes(true);
|
|
339
|
-
const defaults = global ? DEFAULT_GLOBAL : DEFAULT_PROJECT;
|
|
340
|
-
const picked = await (0, prompt_1.multiSelect)('Which agents get the skill', AGENTS.map((agent, i) => ({
|
|
341
|
-
label: agent.label,
|
|
342
|
-
hint: pathLabel(agent, global) + (present[i] ? ', installed' : ''),
|
|
343
|
-
checked: anyPresent ? present[i] : defaults.includes(agent.id),
|
|
344
|
-
})));
|
|
345
|
-
if (picked === null || picked.length === 0)
|
|
346
|
-
return null;
|
|
347
|
-
return { ids: picked.map((i) => AGENTS[i].id), global };
|
|
348
|
-
}
|
|
349
324
|
async function skills(args) {
|
|
350
325
|
const sub = args.words[0];
|
|
351
326
|
if (sub === 'list') {
|
|
@@ -365,7 +340,9 @@ async function skills(args) {
|
|
|
365
340
|
if (sub !== undefined) {
|
|
366
341
|
throw new errors_1.CliError(`Unknown command: skills ${sub}`, 'Available: xflow skills [list]');
|
|
367
342
|
}
|
|
368
|
-
|
|
343
|
+
// --yes stays in older install lines; there is nothing left to say yes to.
|
|
344
|
+
(0, args_1.flagBool)(args, 'yes');
|
|
345
|
+
const global = (0, args_1.flagBool)(args, 'global');
|
|
369
346
|
const agentFlag = (0, args_1.flagString)(args, 'agent');
|
|
370
347
|
if (args.flags.agent !== undefined && agentFlag === undefined) {
|
|
371
348
|
throw new errors_1.CliError('The --agent flag needs a value', 'For example: --agent claude,cursor. The ids: xflow skills list');
|
|
@@ -382,15 +359,6 @@ async function skills(args) {
|
|
|
382
359
|
throw new errors_1.CliError(unknown.length > 0 ? `Unknown agent: ${unknown.join(', ')}` : 'No agents named', 'The ids: xflow skills list');
|
|
383
360
|
}
|
|
384
361
|
}
|
|
385
|
-
else if (process.stdin.isTTY === true && process.stderr.isTTY === true && !(0, args_1.flagBool)(args, 'yes')) {
|
|
386
|
-
const picked = await pick(global);
|
|
387
|
-
if (picked === null) {
|
|
388
|
-
(0, ui_1.note)((0, ui_1.dim)(' nothing chosen, nothing changed'));
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
ids = picked.ids;
|
|
392
|
-
global = picked.global;
|
|
393
|
-
}
|
|
394
362
|
else {
|
|
395
363
|
ids = defaultIds(process.cwd(), global);
|
|
396
364
|
}
|
package/dist/commands/sources.js
CHANGED
|
@@ -22,6 +22,8 @@ const zip_1 = require("../zip");
|
|
|
22
22
|
const ui_1 = require("../ui");
|
|
23
23
|
/** Server cap, checked locally to fail before uploading. Keep in sync with the server. */
|
|
24
24
|
const MAX_ARCHIVE_BYTES = 10 * 1024 * 1024;
|
|
25
|
+
/** Keep in sync with KEEP_REVISIONS in lib/project-sources/revisions.ts. */
|
|
26
|
+
const KEPT_REVISIONS = 10;
|
|
25
27
|
/** Where a stream lives on the platform: the same routes under two prefixes. */
|
|
26
28
|
function streamPath(target) {
|
|
27
29
|
return target.kind === 'app' ? `/api/v1/apps/${target.appId}` : `/api/v1/projects/${target.projectId}`;
|
|
@@ -68,34 +70,9 @@ async function latestRevision(client, base) {
|
|
|
68
70
|
const { revisions } = await (0, api_1.apiJson)(client, `${base}/revisions`);
|
|
69
71
|
return revisions[0] ?? null;
|
|
70
72
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
(0, ui_1.note)('');
|
|
75
|
-
(0, ui_1.warn)(`The server holds revision ${server.revision} from ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} files`);
|
|
76
|
-
try {
|
|
77
|
-
const info = await (0, api_1.apiJson)(client, `${base}/pull`);
|
|
78
|
-
const serverPaths = new Set((0, zip_1.dropDirectoryEntries)((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url))).map((e) => e.path));
|
|
79
|
-
const localPaths = new Set(local.map((f) => f.path));
|
|
80
|
-
const disappearing = [...serverPaths].filter((p) => !localPaths.has(p));
|
|
81
|
-
if (disappearing.length > 0) {
|
|
82
|
-
(0, ui_1.note)(` Files that will disappear (${disappearing.length}):`);
|
|
83
|
-
for (const path of disappearing.slice(0, 20))
|
|
84
|
-
(0, ui_1.note)((0, ui_1.dim)(` ${path}`));
|
|
85
|
-
if (disappearing.length > 20)
|
|
86
|
-
(0, ui_1.note)((0, ui_1.dim)(` …and ${disappearing.length - 20} more`));
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
(0, ui_1.note)((0, ui_1.dim)(' No files disappear, but the contents of the server revision are replaced by yours'));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
(0, ui_1.warn)('Could not read the server copy: the list of disappearing files is unavailable');
|
|
94
|
-
}
|
|
95
|
-
(0, ui_1.note)('');
|
|
96
|
-
const confirmed = await (0, ui_1.confirmWord)('The server revision will be replaced by the contents of this folder. Anything newer on the platform goes away, and it cannot be recovered from here.', name);
|
|
97
|
-
if (!confirmed)
|
|
98
|
-
throw new errors_1.CliError('Cancelled');
|
|
73
|
+
/** How the stream is named on the command line: `xflow pull functions`, `xflow deploy apps/shop`. */
|
|
74
|
+
function streamWord(stream) {
|
|
75
|
+
return stream.kind === 'app' ? `${config_1.APPS_DIR}/${(0, node_path_1.basename)(stream.root)}` : 'functions';
|
|
99
76
|
}
|
|
100
77
|
/** The first step of deploy: sources of one stream go up as a revision before the build. */
|
|
101
78
|
async function pushSources(stream, client, options) {
|
|
@@ -113,12 +90,10 @@ async function pushSources(stream, client, options) {
|
|
|
113
90
|
// Asked before the upload, so a doomed archive never leaves the machine.
|
|
114
91
|
if (relation === 'diverged' && !options.force) {
|
|
115
92
|
throw new errors_1.CliError(`The server holds revision ${server.revision}, and this folder does not stand on it`, 'Someone deployed before you, or this folder worked on a different project. ' +
|
|
116
|
-
'Fetch the server copy alongside and compare: xflow pull --into ./server-copy'
|
|
93
|
+
'Fetch the server copy alongside and compare: xflow pull --into ./server-copy. ' +
|
|
94
|
+
`To build what the server holds as it is: xflow deploy ${streamWord(stream)} --revision ${server.revision} -m "..."`);
|
|
117
95
|
}
|
|
118
|
-
if (options.force) {
|
|
119
|
-
await confirmForce(client, base, server, tree.files);
|
|
120
|
-
}
|
|
121
|
-
else if (relation === 'unrecorded') {
|
|
96
|
+
if (!options.force && relation === 'unrecorded') {
|
|
122
97
|
(0, ui_1.warn)(`This folder has no record of the server revision: the code goes up on top of revision ${server.revision}`);
|
|
123
98
|
}
|
|
124
99
|
}
|
|
@@ -134,6 +109,10 @@ async function pushSources(stream, client, options) {
|
|
|
134
109
|
(0, ui_1.ok)(`No changes, revision ${result.revision}`);
|
|
135
110
|
else
|
|
136
111
|
(0, ui_1.ok)(`Revision ${result.revision}: ${result.file_count} files, ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
112
|
+
if (options.force && server && result.status === 'created') {
|
|
113
|
+
(0, ui_1.warn)(`Revision ${server.revision} is no longer the latest; it is kept while it is among the last ${KEPT_REVISIONS}`);
|
|
114
|
+
(0, ui_1.note)((0, ui_1.dim)(` Get it back: xflow pull ${streamWord(stream)} --revision ${server.revision} --into ./server-copy`));
|
|
115
|
+
}
|
|
137
116
|
return { revision: result.revision, status: result.status };
|
|
138
117
|
}
|
|
139
118
|
/**
|
package/dist/flags.js
CHANGED
|
@@ -53,8 +53,6 @@ exports.KNOWN = {
|
|
|
53
53
|
'apps list': ['json', 'project'],
|
|
54
54
|
'apps add': ['name', 'color'],
|
|
55
55
|
skills: ['refresh', 'global', 'agent', 'yes'],
|
|
56
|
-
mcp: ['show-token'],
|
|
57
|
-
'mcp install': ['show-token'],
|
|
58
56
|
functions: ['project'],
|
|
59
57
|
'functions list': ['project'],
|
|
60
58
|
'functions invoke': ['data', 'data-file', 'method', 'project'],
|
|
@@ -69,7 +67,7 @@ exports.KNOWN = {
|
|
|
69
67
|
connections: ['project'],
|
|
70
68
|
'connections list': ['project'],
|
|
71
69
|
'connections link': ['as', 'project'],
|
|
72
|
-
'connections unlink': ['
|
|
70
|
+
'connections unlink': ['yes', 'project'],
|
|
73
71
|
'connections call': ['method', 'data', 'data-file', 'header', 'json', 'project'],
|
|
74
72
|
schedules: ['project'],
|
|
75
73
|
'schedules list': ['project'],
|
|
@@ -82,7 +80,7 @@ exports.KNOWN = {
|
|
|
82
80
|
'db schema': ['project'],
|
|
83
81
|
'db query': ['json', 'project'],
|
|
84
82
|
'db migrate': ['dry-run', 'allow-destructive'],
|
|
85
|
-
'db move': ['to', '
|
|
83
|
+
'db move': ['to', 'yes', 'project'],
|
|
86
84
|
storage: ['json', 'project'],
|
|
87
85
|
'storage ls': ['json', 'project'],
|
|
88
86
|
'storage list': ['json', 'project'],
|
|
@@ -100,7 +98,7 @@ exports.KNOWN = {
|
|
|
100
98
|
status: [],
|
|
101
99
|
dev: [],
|
|
102
100
|
pull: ['into', 'force', 'revision'],
|
|
103
|
-
deploy: ['
|
|
101
|
+
deploy: ['force', 'allow-removals', 'message', 'revision', 'project', 'app'],
|
|
104
102
|
publish: ['app'],
|
|
105
103
|
rollback: ['app'],
|
|
106
104
|
deployments: ['project', 'app'],
|
|
@@ -147,7 +145,6 @@ const NAMESPACES = new Set([
|
|
|
147
145
|
'db',
|
|
148
146
|
'storage',
|
|
149
147
|
'pages',
|
|
150
|
-
'mcp',
|
|
151
148
|
]);
|
|
152
149
|
/** The command these words name, as far as the table knows it. */
|
|
153
150
|
function commandOf(words) {
|
package/dist/help.js
CHANGED
|
@@ -48,7 +48,6 @@ ${(0, ui_1.bold)('Getting started')}
|
|
|
48
48
|
xflow link <id> link this folder to a project, an empty one gets the whole project
|
|
49
49
|
xflow dev in an app folder: run it on 127.0.0.1:3000, functions included
|
|
50
50
|
xflow skills pick the agents that get the platform instructions
|
|
51
|
-
xflow mcp install give the agent platform access without a terminal
|
|
52
51
|
|
|
53
52
|
${(0, ui_1.bold)('Sources')}
|
|
54
53
|
xflow status what is on the server and how the local copies differ
|
|
@@ -87,7 +86,7 @@ ${(0, ui_1.bold)('Functions')}
|
|
|
87
86
|
xflow connections connected accounts the project can use, and their variables
|
|
88
87
|
xflow connections link <name> --as ALIAS
|
|
89
88
|
give the functions the credentials of an account
|
|
90
|
-
xflow connections unlink <ALIAS> [--
|
|
89
|
+
xflow connections unlink <ALIAS> [--yes]
|
|
91
90
|
take them away again
|
|
92
91
|
|
|
93
92
|
${(0, ui_1.bold)('Database')}
|
|
@@ -97,7 +96,7 @@ ${(0, ui_1.bold)('Database')}
|
|
|
97
96
|
xflow db query 'select ...' read data, in a read-only transaction
|
|
98
97
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
99
98
|
apply the migrations from migrations/*.sql
|
|
100
|
-
xflow db move --to <server> --
|
|
99
|
+
xflow db move --to <server> --yes
|
|
101
100
|
move the project database to another server
|
|
102
101
|
|
|
103
102
|
${(0, ui_1.bold)('Pages')}
|
|
@@ -160,10 +159,6 @@ the platform. A folder bound to an organization is not affected, its commands
|
|
|
160
159
|
stay in its own organization: two projects of two organizations in two terminals
|
|
161
160
|
work without switching anything.
|
|
162
161
|
|
|
163
|
-
Outside a project folder the switch also rewrites the key in the agent config
|
|
164
|
-
(see ${(0, ui_1.bold)('xflow mcp')}) when an xflow entry already exists there; a running agent
|
|
165
|
-
session picks the new key up only after a restart.
|
|
166
|
-
|
|
167
162
|
A project id is unique across the whole platform, so "project not found" under
|
|
168
163
|
the wrong organization can never touch somebody else's project. When that error
|
|
169
164
|
names a project you know exists, the key is simply from another organization:
|
|
@@ -301,11 +296,10 @@ build it and upload what the build writes. Dot entries are skipped. At most 5 MB
|
|
|
301
296
|
unpacked and 1000 files.
|
|
302
297
|
|
|
303
298
|
The page is public, so a file holding a secret key is refused whole: every visitor would read
|
|
304
|
-
it. A page
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
so do not promise SEO.
|
|
299
|
+
it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
|
|
300
|
+
key a page may hold is a function key, and it opens that function to every visitor: an HTML
|
|
301
|
+
form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
|
|
302
|
+
(no CORS). Search engines are asked not to index pages, so do not promise SEO.
|
|
309
303
|
|
|
310
304
|
${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
|
|
311
305
|
is no undo and no copy on the platform side. The name stays reserved for the organization, so
|
|
@@ -347,7 +341,7 @@ will work.`,
|
|
|
347
341
|
one, 300 at most; --json prints the values whole)
|
|
348
342
|
xflow db query <file>.sql the same, the query read from a file
|
|
349
343
|
xflow db migrate store the project root, then apply migrations/*.sql
|
|
350
|
-
xflow db move --to <server> --
|
|
344
|
+
xflow db move --to <server> --yes
|
|
351
345
|
move the project database to another server
|
|
352
346
|
|
|
353
347
|
Every project has exactly one database of its own, shared by all of its apps and functions,
|
|
@@ -362,7 +356,7 @@ ${(0, ui_1.bold)('move')} takes the database of this project to another server o
|
|
|
362
356
|
${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
|
|
363
357
|
the tables, switches the functions of the project and removes the old copy; nothing needs a
|
|
364
358
|
deploy. While it copies, the apps read data but cannot save anything new, usually for a
|
|
365
|
-
couple of minutes, which is why ${(0, ui_1.bold)('--
|
|
359
|
+
couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
|
|
366
360
|
right to move databases on the key: off by default, only its owner turns it on, in the
|
|
367
361
|
platform settings where the key permissions live. The command follows the move to the end;
|
|
368
362
|
stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
|
|
@@ -519,7 +513,7 @@ of the same service under one name; then the command prints their identifiers an
|
|
|
519
513
|
for one of those instead of guessing.
|
|
520
514
|
|
|
521
515
|
${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
|
|
522
|
-
those functions; ${(0, ui_1.bold)('--
|
|
516
|
+
those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
|
|
523
517
|
so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
|
|
524
518
|
that go with it are printed instead. Both commands take the flag, because an account
|
|
525
519
|
linked to a service project without a folder has to be removable the same way.
|
|
@@ -618,14 +612,14 @@ of the project runs the new code the moment it finishes; there is no publish ste
|
|
|
618
612
|
|
|
619
613
|
Who may call a function is its callers, in the root xflow.json:
|
|
620
614
|
|
|
621
|
-
{ "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"
|
|
615
|
+
{ "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
|
|
622
616
|
|
|
623
|
-
A caller is an app folder of this project (apps/zapis is "zapis")
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
617
|
+
A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
|
|
618
|
+
nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
|
|
619
|
+
holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
|
|
620
|
+
anyone with the link opens, so anyone on the internet can call it. The address in the note
|
|
621
|
+
under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
|
|
622
|
+
their own address.
|
|
629
623
|
|
|
630
624
|
${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
|
|
631
625
|
the function a one-minute ticket and calls it itself, so the function sees
|
|
@@ -643,24 +637,6 @@ Prints the body of the answer and nothing else, so it goes straight into a pipe
|
|
|
643
637
|
(${(0, ui_1.bold)('| jq')}, ${(0, ui_1.bold)('| python')}). A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step. A
|
|
644
638
|
refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
|
|
645
639
|
${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
|
|
646
|
-
mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
|
|
647
|
-
|
|
648
|
-
Writes the MCP server into the configuration of a client that has its own connect
|
|
649
|
-
command: today that is Claude Code, and for the rest the command prints what to enter
|
|
650
|
-
by hand. After that the agent runs the platform without a terminal: it inspects the
|
|
651
|
-
database, applies migrations, calls functions, sets schedules, publishes app builds, reads
|
|
652
|
-
logs. The commands stay: code, builds and functions deploys are still done by the CLI,
|
|
653
|
-
because pushing sources through MCP means filling the agent context and spending your money.
|
|
654
|
-
|
|
655
|
-
The command takes the access key from your own login and does not print it: usually
|
|
656
|
-
the agent runs this command itself, and everything printed lands in its context and in
|
|
657
|
-
the chat history.
|
|
658
|
-
|
|
659
|
-
--show-token print the key anyway (not while under an agent)
|
|
660
|
-
|
|
661
|
-
There is one key per person per organization and its scope is not narrowed: the agent
|
|
662
|
-
sees every project of the organization and names the one it needs. It takes the
|
|
663
|
-
project from the ${(0, ui_1.bold)('project_id')} argument, not from your folder, which it cannot see.`,
|
|
664
640
|
skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
|
|
665
641
|
|
|
666
642
|
An agent has no way to know about XFlow: the platform is not in its training. The
|
|
@@ -670,16 +646,12 @@ down on their own for the common agents, so this command exists to pick tools
|
|
|
670
646
|
precisely. Refreshing after a CLI update is not your job any more: the skill and the
|
|
671
647
|
CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
|
|
672
648
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
already installed come pre-selected and are updated in place. Without a terminal, in
|
|
676
|
-
CI or when an agent runs the command, there are no questions: the defaults plus every
|
|
677
|
-
already installed copy are refreshed.
|
|
649
|
+
Without flags it installs for Claude Code, Cursor and Codex in this project (globally:
|
|
650
|
+
Claude Code, Codex and the shared folder) and refreshes every copy already installed.
|
|
678
651
|
|
|
679
652
|
xflow skills list the agents, their folders and what is installed
|
|
680
|
-
--agent claude,cursor exact agents
|
|
653
|
+
--agent claude,cursor exact agents instead of the defaults
|
|
681
654
|
--global the home folder instead of the project
|
|
682
|
-
--yes no questions: defaults plus what is installed
|
|
683
655
|
--refresh rewrite the copies that exist, create none
|
|
684
656
|
|
|
685
657
|
The format is shared (agentskills.io) and inside a project most tools read the shared
|
|
@@ -731,7 +703,6 @@ apps never go in one run, because a change of functions reaches every app at onc
|
|
|
731
703
|
|
|
732
704
|
-m <text> the version comment, required: one short line in the language the
|
|
733
705
|
user speaks, saying what changed (--message is the long form)
|
|
734
|
-
--no-push do not send sources, build from the latest server revision
|
|
735
706
|
--revision <N> run a revision the platform already holds: that is how the functions
|
|
736
707
|
come back (xflow deployments functions lists them)
|
|
737
708
|
--project <id> with --revision only: the functions of that project, no folder needed
|
|
@@ -747,12 +718,12 @@ ignore field of xflow.json.
|
|
|
747
718
|
If the server holds a revision newer than the one you worked from, the sources are
|
|
748
719
|
rejected and the build does not start. That means somebody deployed before you: fetch
|
|
749
720
|
their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
|
|
750
|
-
your side, and retry
|
|
721
|
+
your side, and retry; or build what the server holds as it is with ${(0, ui_1.bold)('--revision <N>')}, the
|
|
722
|
+
refusal names the number. A folder with no record of the server revision (a fresh clone, a CI
|
|
751
723
|
checkout) is not rejected: its first deploy goes on top of the current revision and says
|
|
752
|
-
so, the check applies from the next one. ${(0, ui_1.bold)('--force')}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
version conflict has to fail the build rather than silently destroy somebody else's work.
|
|
724
|
+
so, the check applies from the next one. ${(0, ui_1.bold)('--force')} sends this folder over the server revision
|
|
725
|
+
anyway. The overwritten revision is not lost at once: it stays in the history while it is among
|
|
726
|
+
the last 10, and the deploy prints the ${(0, ui_1.bold)('xflow pull --revision <N>')} line that brings it back.
|
|
756
727
|
|
|
757
728
|
The platform builds, in a clean sandbox on one Node version for everybody. Before that the
|
|
758
729
|
sources are checked: what breaks silently (the platform files, the calls in the entry point,
|
|
@@ -770,9 +741,8 @@ internet now. A change that touches data and screens goes in this order: the fun
|
|
|
770
741
|
changes nobody deployed.
|
|
771
742
|
|
|
772
743
|
A function gone from the sources would be deleted from the cloud along with its keys and
|
|
773
|
-
schedules, and that is final. So the deploy does not start at all
|
|
774
|
-
|
|
775
|
-
to agree. If the sources hold no functions at all while the cloud holds several, nothing
|
|
744
|
+
schedules, and that is final. So the deploy does not start at all: it names the functions and stops, and ${(0, ui_1.bold)('--allow-removals')}
|
|
745
|
+
is the way to agree. If the sources hold no functions at all while the cloud holds several, nothing
|
|
776
746
|
is deleted and nothing is asked: that looks like a directory which never made it (a
|
|
777
747
|
${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
|
|
778
748
|
|
package/dist/limits.js
CHANGED
|
@@ -15,6 +15,7 @@ const DENIAL_LABELS = {
|
|
|
15
15
|
limit_builds_concurrent: 'builds running at once',
|
|
16
16
|
limit_seats: 'people in the organization',
|
|
17
17
|
limit_schedule_interval: 'schedule frequency',
|
|
18
|
+
limit_schedules: 'schedules in the organization',
|
|
18
19
|
db_write_locked: 'database volume, writes are off',
|
|
19
20
|
plan_no_schedules: 'schedules',
|
|
20
21
|
subscription_blocked: 'the subscription is unpaid',
|
|
@@ -38,6 +39,7 @@ const QUOTA_ORDER = [
|
|
|
38
39
|
'connections',
|
|
39
40
|
'pages',
|
|
40
41
|
'functions',
|
|
42
|
+
'schedules',
|
|
41
43
|
'people',
|
|
42
44
|
'database_mb',
|
|
43
45
|
'files_mb',
|
|
@@ -49,6 +51,7 @@ const QUOTA_LABELS = {
|
|
|
49
51
|
connections: 'Active connections',
|
|
50
52
|
pages: 'Public pages',
|
|
51
53
|
functions: 'Cloud functions',
|
|
54
|
+
schedules: 'Schedules',
|
|
52
55
|
people: 'People',
|
|
53
56
|
database_mb: 'Database',
|
|
54
57
|
files_mb: 'Files',
|
package/dist/template.js
CHANGED
|
@@ -13,7 +13,7 @@ const README = `# %NAME%
|
|
|
13
13
|
A project on XFlow: one database, cloud functions and the apps that use them.
|
|
14
14
|
|
|
15
15
|
\`\`\`
|
|
16
|
-
xflow.json projectId, and which apps
|
|
16
|
+
xflow.json projectId, and which apps may call each function
|
|
17
17
|
package.json dependencies of the functions
|
|
18
18
|
functions/<name>/ cloud functions, shared by every app
|
|
19
19
|
migrations/ the database of the project
|