@getxflow/cli 1.1.1 → 1.2.1
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 -7
- package/dist/args.js +0 -2
- package/dist/bin.js +0 -7
- package/dist/commands/auth.js +1 -0
- package/dist/commands/connections.js +3 -3
- package/dist/commands/db.js +6 -4
- package/dist/commands/deploy.js +8 -32
- package/dist/commands/org.js +0 -7
- package/dist/commands/projects.js +1 -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 +17 -45
- package/dist/limits.js +3 -0
- package/dist/ui.js +0 -19
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +16 -22
- package/dist/commands/mcp.js +0 -100
- package/dist/prompt.js +0 -113
package/README.md
CHANGED
|
@@ -6,9 +6,8 @@ CLI for the [XFlow](https://getxflow.com) platform: source sync, builds and rele
|
|
|
6
6
|
npm i -g @getxflow/cli
|
|
7
7
|
xflow login
|
|
8
8
|
xflow init my-app
|
|
9
|
-
cd my-app && npm install
|
|
9
|
+
cd my-app/apps/my-app && npm install && xflow dev
|
|
10
10
|
xflow deploy -m "First version"
|
|
11
|
-
xflow publish
|
|
12
11
|
```
|
|
13
12
|
|
|
14
13
|
Requires Node 20+. The package has no dependencies: it holds an access key, and nobody
|
|
@@ -31,7 +30,6 @@ else's code belongs next to it.
|
|
|
31
30
|
| `logs` | browser errors from the released application |
|
|
32
31
|
| `projects list` / `projects get` | projects of the organization and the application addresses |
|
|
33
32
|
| `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
33
|
| `update` | update the CLI itself, and the skill that ships inside it |
|
|
36
34
|
|
|
37
35
|
Help for one command: `xflow help <command>`.
|
|
@@ -42,9 +40,5 @@ OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `C
|
|
|
42
40
|
left alone. The description ships inside the package and shares its version, so `xflow update`
|
|
43
41
|
brings both.
|
|
44
42
|
|
|
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
43
|
The key is stored in `~/.xflow/credentials.json`, the project configuration in `xflow.json`.
|
|
50
44
|
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);
|
package/dist/commands/auth.js
CHANGED
|
@@ -79,6 +79,7 @@ async function awaitConfirmation(client, start) {
|
|
|
79
79
|
const others = (0, credentials_1.listOrgs)(client.apiUrl).length - 1;
|
|
80
80
|
if (others > 0)
|
|
81
81
|
(0, ui_1.note)((0, ui_1.dim)(` ${others} more stored: xflow org`));
|
|
82
|
+
(0, ui_1.note)((0, ui_1.dim)(' Documentation: https://docs.getxflow.com'));
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -70,7 +70,7 @@ async function connectionsList(args) {
|
|
|
70
70
|
const data = await (0, api_1.apiJson)(client, endpoint(projectId));
|
|
71
71
|
if (data.connections.length === 0) {
|
|
72
72
|
(0, ui_1.note)('No connections you can use in this organization');
|
|
73
|
-
(0, ui_1.note)((0, ui_1.dim)(' Somebody connects an account first:
|
|
73
|
+
(0, ui_1.note)((0, ui_1.dim)(' Somebody connects an account first: Integrations in the xflow sidebar'));
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
(0, ui_1.table)(data.connections.map((row) => [
|
|
@@ -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
|
}
|
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
|
}
|
|
@@ -95,7 +95,7 @@ async function init(args) {
|
|
|
95
95
|
(0, ui_1.note)((0, ui_1.dim)(` Its database goes to the own server "${project.server.name}"`));
|
|
96
96
|
(0, ui_1.out)('');
|
|
97
97
|
(0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
|
|
98
|
-
(0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}
|
|
98
|
+
(0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
|
|
99
99
|
if (folder) {
|
|
100
100
|
(0, ui_1.out)(` cd ${config_1.APPS_DIR}/${folder} && npm install && xflow dev # the app on 127.0.0.1:3000`);
|
|
101
101
|
(0, ui_1.out)(` xflow deploy ${config_1.APPS_DIR}/${folder} -m "First version"`);
|
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')}
|
|
@@ -132,7 +131,8 @@ ${(0, ui_1.bold)('Environment')}
|
|
|
132
131
|
XFLOW_TOKEN access key (for CI, instead of xflow login)
|
|
133
132
|
XFLOW_API_URL platform address when it is not app.getxflow.com
|
|
134
133
|
|
|
135
|
-
More about one command: xflow help <command
|
|
134
|
+
More about one command: xflow help <command>
|
|
135
|
+
Documentation: https://docs.getxflow.com (for agents: https://docs.getxflow.com/llms.txt)`);
|
|
136
136
|
}
|
|
137
137
|
const TOPICS = {
|
|
138
138
|
org: `${(0, ui_1.bold)('xflow org')}: several organizations, one terminal
|
|
@@ -160,10 +160,6 @@ the platform. A folder bound to an organization is not affected, its commands
|
|
|
160
160
|
stay in its own organization: two projects of two organizations in two terminals
|
|
161
161
|
work without switching anything.
|
|
162
162
|
|
|
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
163
|
A project id is unique across the whole platform, so "project not found" under
|
|
168
164
|
the wrong organization can never touch somebody else's project. When that error
|
|
169
165
|
names a project you know exists, the key is simply from another organization:
|
|
@@ -346,7 +342,7 @@ will work.`,
|
|
|
346
342
|
one, 300 at most; --json prints the values whole)
|
|
347
343
|
xflow db query <file>.sql the same, the query read from a file
|
|
348
344
|
xflow db migrate store the project root, then apply migrations/*.sql
|
|
349
|
-
xflow db move --to <server> --
|
|
345
|
+
xflow db move --to <server> --yes
|
|
350
346
|
move the project database to another server
|
|
351
347
|
|
|
352
348
|
Every project has exactly one database of its own, shared by all of its apps and functions,
|
|
@@ -361,7 +357,7 @@ ${(0, ui_1.bold)('move')} takes the database of this project to another server o
|
|
|
361
357
|
${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
|
|
362
358
|
the tables, switches the functions of the project and removes the old copy; nothing needs a
|
|
363
359
|
deploy. While it copies, the apps read data but cannot save anything new, usually for a
|
|
364
|
-
couple of minutes, which is why ${(0, ui_1.bold)('--
|
|
360
|
+
couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
|
|
365
361
|
right to move databases on the key: off by default, only its owner turns it on, in the
|
|
366
362
|
platform settings where the key permissions live. The command follows the move to the end;
|
|
367
363
|
stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
|
|
@@ -518,7 +514,7 @@ of the same service under one name; then the command prints their identifiers an
|
|
|
518
514
|
for one of those instead of guessing.
|
|
519
515
|
|
|
520
516
|
${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
|
|
521
|
-
those functions; ${(0, ui_1.bold)('--
|
|
517
|
+
those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
|
|
522
518
|
so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
|
|
523
519
|
that go with it are printed instead. Both commands take the flag, because an account
|
|
524
520
|
linked to a service project without a folder has to be removable the same way.
|
|
@@ -642,24 +638,6 @@ Prints the body of the answer and nothing else, so it goes straight into a pipe
|
|
|
642
638
|
(${(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
|
|
643
639
|
refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
|
|
644
640
|
${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
|
|
645
|
-
mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
|
|
646
|
-
|
|
647
|
-
Writes the MCP server into the configuration of a client that has its own connect
|
|
648
|
-
command: today that is Claude Code, and for the rest the command prints what to enter
|
|
649
|
-
by hand. After that the agent runs the platform without a terminal: it inspects the
|
|
650
|
-
database, applies migrations, calls functions, sets schedules, publishes app builds, reads
|
|
651
|
-
logs. The commands stay: code, builds and functions deploys are still done by the CLI,
|
|
652
|
-
because pushing sources through MCP means filling the agent context and spending your money.
|
|
653
|
-
|
|
654
|
-
The command takes the access key from your own login and does not print it: usually
|
|
655
|
-
the agent runs this command itself, and everything printed lands in its context and in
|
|
656
|
-
the chat history.
|
|
657
|
-
|
|
658
|
-
--show-token print the key anyway (not while under an agent)
|
|
659
|
-
|
|
660
|
-
There is one key per person per organization and its scope is not narrowed: the agent
|
|
661
|
-
sees every project of the organization and names the one it needs. It takes the
|
|
662
|
-
project from the ${(0, ui_1.bold)('project_id')} argument, not from your folder, which it cannot see.`,
|
|
663
641
|
skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
|
|
664
642
|
|
|
665
643
|
An agent has no way to know about XFlow: the platform is not in its training. The
|
|
@@ -669,16 +647,12 @@ down on their own for the common agents, so this command exists to pick tools
|
|
|
669
647
|
precisely. Refreshing after a CLI update is not your job any more: the skill and the
|
|
670
648
|
CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
|
|
671
649
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
already installed come pre-selected and are updated in place. Without a terminal, in
|
|
675
|
-
CI or when an agent runs the command, there are no questions: the defaults plus every
|
|
676
|
-
already installed copy are refreshed.
|
|
650
|
+
Without flags it installs for Claude Code, Cursor and Codex in this project (globally:
|
|
651
|
+
Claude Code, Codex and the shared folder) and refreshes every copy already installed.
|
|
677
652
|
|
|
678
653
|
xflow skills list the agents, their folders and what is installed
|
|
679
|
-
--agent claude,cursor exact agents
|
|
654
|
+
--agent claude,cursor exact agents instead of the defaults
|
|
680
655
|
--global the home folder instead of the project
|
|
681
|
-
--yes no questions: defaults plus what is installed
|
|
682
656
|
--refresh rewrite the copies that exist, create none
|
|
683
657
|
|
|
684
658
|
The format is shared (agentskills.io) and inside a project most tools read the shared
|
|
@@ -730,7 +704,6 @@ apps never go in one run, because a change of functions reaches every app at onc
|
|
|
730
704
|
|
|
731
705
|
-m <text> the version comment, required: one short line in the language the
|
|
732
706
|
user speaks, saying what changed (--message is the long form)
|
|
733
|
-
--no-push do not send sources, build from the latest server revision
|
|
734
707
|
--revision <N> run a revision the platform already holds: that is how the functions
|
|
735
708
|
come back (xflow deployments functions lists them)
|
|
736
709
|
--project <id> with --revision only: the functions of that project, no folder needed
|
|
@@ -746,12 +719,12 @@ ignore field of xflow.json.
|
|
|
746
719
|
If the server holds a revision newer than the one you worked from, the sources are
|
|
747
720
|
rejected and the build does not start. That means somebody deployed before you: fetch
|
|
748
721
|
their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
|
|
749
|
-
your side, and retry
|
|
722
|
+
your side, and retry; or build what the server holds as it is with ${(0, ui_1.bold)('--revision <N>')}, the
|
|
723
|
+
refusal names the number. A folder with no record of the server revision (a fresh clone, a CI
|
|
750
724
|
checkout) is not rejected: its first deploy goes on top of the current revision and says
|
|
751
|
-
so, the check applies from the next one. ${(0, ui_1.bold)('--force')}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
version conflict has to fail the build rather than silently destroy somebody else's work.
|
|
725
|
+
so, the check applies from the next one. ${(0, ui_1.bold)('--force')} sends this folder over the server revision
|
|
726
|
+
anyway. The overwritten revision is not lost at once: it stays in the history while it is among
|
|
727
|
+
the last 10, and the deploy prints the ${(0, ui_1.bold)('xflow pull --revision <N>')} line that brings it back.
|
|
755
728
|
|
|
756
729
|
The platform builds, in a clean sandbox on one Node version for everybody. Before that the
|
|
757
730
|
sources are checked: what breaks silently (the platform files, the calls in the entry point,
|
|
@@ -769,9 +742,8 @@ internet now. A change that touches data and screens goes in this order: the fun
|
|
|
769
742
|
changes nobody deployed.
|
|
770
743
|
|
|
771
744
|
A function gone from the sources would be deleted from the cloud along with its keys and
|
|
772
|
-
schedules, and that is final. So the deploy does not start at all
|
|
773
|
-
|
|
774
|
-
to agree. If the sources hold no functions at all while the cloud holds several, nothing
|
|
745
|
+
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')}
|
|
746
|
+
is the way to agree. If the sources hold no functions at all while the cloud holds several, nothing
|
|
775
747
|
is deleted and nothing is asked: that looks like a directory which never made it (a
|
|
776
748
|
${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
|
|
777
749
|
|
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/ui.js
CHANGED
|
@@ -14,7 +14,6 @@ exports.table = table;
|
|
|
14
14
|
exports.formatBytes = formatBytes;
|
|
15
15
|
exports.formatAge = formatAge;
|
|
16
16
|
exports.offerBrowser = offerBrowser;
|
|
17
|
-
exports.confirmWord = confirmWord;
|
|
18
17
|
const node_child_process_1 = require("node:child_process");
|
|
19
18
|
const node_readline_1 = require("node:readline");
|
|
20
19
|
/**
|
|
@@ -147,21 +146,3 @@ function offerBrowser(url) {
|
|
|
147
146
|
});
|
|
148
147
|
return () => rl.close();
|
|
149
148
|
}
|
|
150
|
-
/** Dangerous-action confirmation: type the exact word. Unavailable without a TTY. */
|
|
151
|
-
async function confirmWord(question, expected) {
|
|
152
|
-
if (!process.stdin.isTTY) {
|
|
153
|
-
fatal('Confirmation is only possible in an interactive terminal');
|
|
154
|
-
fatalNote((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite what the platform holds'));
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
|
158
|
-
try {
|
|
159
|
-
const answer = await new Promise((resolve) => {
|
|
160
|
-
rl.question(`${question}\n Type "${expected}" to confirm: `, resolve);
|
|
161
|
-
});
|
|
162
|
-
return answer.trim() === expected;
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
rl.close();
|
|
166
|
-
}
|
|
167
|
-
}
|
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 = '1.
|
|
5
|
+
exports.CLI_VERSION = '1.2.1';
|
|
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/package.json
CHANGED
package/skills/xflow/SKILL.md
CHANGED
|
@@ -21,9 +21,13 @@ memory. If a command is not in the help output, it does not exist: guessing flag
|
|
|
21
21
|
pointless. The CLI prints a hint with almost every error, read it in full, it usually
|
|
22
22
|
contains the fix.
|
|
23
23
|
|
|
24
|
+
The public documentation, in Russian, goes deeper than this file: its table of contents is
|
|
25
|
+
https://docs.getxflow.com/llms.txt, every page has a Markdown copy. Read the page you need
|
|
26
|
+
instead of guessing; give the person https://docs.getxflow.com when they ask how something works.
|
|
27
|
+
|
|
24
28
|
## Keeping these instructions current
|
|
25
29
|
|
|
26
|
-
These instructions ship with xflow CLI 1.
|
|
30
|
+
These instructions ship with xflow CLI 1.2.1, so the copy you are reading can be older than
|
|
27
31
|
the CLI answering your commands.
|
|
28
32
|
|
|
29
33
|
When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
|
|
@@ -110,7 +114,7 @@ One folder per project, apps in subfolders:
|
|
|
110
114
|
|
|
111
115
|
The organization runs on a plan with limits: apps, functions, pages, active connections,
|
|
112
116
|
people in the organization, database and file storage, function minutes, builds per month and at once, schedules
|
|
113
|
-
and how often they may run, and which access levels an app may have. Most are finite; a plan
|
|
117
|
+
(one count for all projects of the organization) and how often they may run, and which access levels an app may have. Most are finite; a plan
|
|
114
118
|
may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
|
|
115
119
|
`xflow whoami` prints every one of them next to what is used, and the rights of your own key
|
|
116
120
|
with them: destroying data in a migration, deleting files from storage, moving a database and
|
|
@@ -122,7 +126,7 @@ halfway through the work into a sentence said before it starts.
|
|
|
122
126
|
- Function minutes are shared by the whole organization: every app, page and schedule spends
|
|
123
127
|
them. When they run out, every function of the organization stops (`functions_stopped`).
|
|
124
128
|
- An unpaid plan stops the functions at once, and five days later freezes the organization:
|
|
125
|
-
every command except `xflow whoami
|
|
129
|
+
every command except `xflow whoami` answers `subscription_blocked`,
|
|
126
130
|
apps and pages show a "suspended" page. Nothing is deleted and no code fixes it: tell the
|
|
127
131
|
person that the owner pays the invoice in the organization settings, section «Тариф» (Plan).
|
|
128
132
|
- The cap on how long one function call may run is written into the function when it is
|
|
@@ -133,8 +137,7 @@ halfway through the work into a sentence said before it starts.
|
|
|
133
137
|
once: fan out in batches of that size, never all items of a list in parallel.
|
|
134
138
|
|
|
135
139
|
A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
|
|
136
|
-
API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`),
|
|
137
|
-
carry the same field. Retrying, renaming or rewriting the code changes nothing: tell the user
|
|
140
|
+
API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`). Retrying, renaming or rewriting the code changes nothing: tell the user
|
|
138
141
|
what ran out and stop, only the owner lifts it. A schedule refused for running too often is one
|
|
139
142
|
of these too, not a bad cron string: the minimum interval comes from the plan, never guess it.
|
|
140
143
|
|
|
@@ -172,8 +175,9 @@ deployed.
|
|
|
172
175
|
by their number.
|
|
173
176
|
- `xflow status` says how the local copies differ from the server. A deploy rejected before
|
|
174
177
|
the build starts means the server holds a revision this folder has never seen: fetch it next
|
|
175
|
-
to yours (`xflow pull --into ./server-copy`), merge in git, deploy again. `--force`
|
|
176
|
-
|
|
178
|
+
to yours (`xflow pull --into ./server-copy`), merge in git, deploy again. `--force` sends this
|
|
179
|
+
folder over it anyway; the overwritten revision is kept while it is among the last 10, and the
|
|
180
|
+
deploy prints the `xflow pull --revision <N>` line that brings it back. A folder with no record of the server revision (a fresh clone, a CI checkout) is
|
|
177
181
|
not rejected: its first deploy goes on top and says so.
|
|
178
182
|
- A deploy that would delete functions gone from the sources refuses until somebody agrees;
|
|
179
183
|
`--allow-removals` is the only way past it without a terminal. Reaching for it to get a
|
|
@@ -475,7 +479,7 @@ marked `available, not linked`. Check it before telling anyone a service is unav
|
|
|
475
479
|
`xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
|
|
476
480
|
`xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the naming
|
|
477
481
|
rules. In the project folder unlink refuses while a function still reads one of the variables,
|
|
478
|
-
naming those functions, so read that list before reaching for `--
|
|
482
|
+
naming those functions, so read that list before reaching for `--yes`.
|
|
479
483
|
|
|
480
484
|
Linking needs the `connections:link` right on the key, and only accounts granted to the owner
|
|
481
485
|
of the key personally can be linked at all. A key cannot grant it to itself: ask the person.
|
|
@@ -508,6 +512,9 @@ nothing undoes that: ask the person first when the endpoint is not clearly a rea
|
|
|
508
512
|
and exactly one of day-of-month / day-of-week must be `?`. A run reaches the handler as a POST
|
|
509
513
|
with `event.xflow.caller === 'schedule'`, and `--payload '{"mode":"full"}'` (or
|
|
510
514
|
`--payload-file body.json`) is its body. The function has to be deployed first.
|
|
515
|
+
A schedule is the function plus its cron: `set` with the same pair only changes the payload, a
|
|
516
|
+
new cron is one more schedule next to the old one. The plan caps the count for the whole
|
|
517
|
+
organization, so take the old one off with `xflow schedules rm report "<old cron>"`.
|
|
511
518
|
|
|
512
519
|
```
|
|
513
520
|
xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
|
|
@@ -556,7 +563,7 @@ the person, no command does it. Check with `--dry-run`.
|
|
|
556
563
|
An organization can have own database servers. `xflow db list` names the server of each project
|
|
557
564
|
database (`platform` is the common one) and the own servers with the room used.
|
|
558
565
|
- A new project with its database on a chosen server: `xflow init --server <name>`.
|
|
559
|
-
- An existing one: `xflow db move --to <name> --
|
|
566
|
+
- An existing one: `xflow db move --to <name> --yes`. Writes close while it
|
|
560
567
|
copies, usually a couple of minutes; the platform switches the functions itself. It needs the
|
|
561
568
|
right to move databases, off on the key by default: refused for the right, say what should move
|
|
562
569
|
where and why, and the person turns it on or moves it in the «Базы данных» (Databases) section.
|
|
@@ -655,7 +662,6 @@ the address opens it, without signing in.
|
|
|
655
662
|
anything else simply leaks.
|
|
656
663
|
- Search engines are asked not to index pages or apps: do not promise SEO.
|
|
657
664
|
- The tab icon is the platform's on every page: do not spend a request on a favicon of your own.
|
|
658
|
-
- The MCP `pages` tool uploads one self-contained HTML document up to 150 KB.
|
|
659
665
|
- A page is deleted by whoever made it, or by the owner of the organization. Switching a page
|
|
660
666
|
off for a while is for a person, in the platform interface.
|
|
661
667
|
|
|
@@ -674,18 +680,6 @@ A key has its owner's level in each project. With view-only access reading works
|
|
|
674
680
|
get`, logs, `db query`, `pull`), and every change fails with `Your access to this project is
|
|
675
681
|
view-only`. That is not a bug to work around: tell the person, edit access is theirs to ask for.
|
|
676
682
|
|
|
677
|
-
## Direct access without the terminal
|
|
678
|
-
|
|
679
|
-
The platform also exposes an MCP server, connected with `xflow mcp install`. When its tools are
|
|
680
|
-
available, prefer them for control-plane work: project state, database schema and read-only
|
|
681
|
-
queries, migrations, function logs and invocations, schedules, variables, app builds and
|
|
682
|
-
publish. They say explicitly when a result is truncated.
|
|
683
|
-
|
|
684
|
-
Anything that depends on the working copy stays in the CLI: sending sources, building apps and
|
|
685
|
-
deploying functions (`xflow deploy`), creating projects and apps (`xflow init`,
|
|
686
|
-
`xflow apps add`). The tools cannot see your folder. Access levels, function keys, money and
|
|
687
|
-
people are set by a person in the web interface.
|
|
688
|
-
|
|
689
683
|
## Do not
|
|
690
684
|
|
|
691
685
|
- Deploy with `--force` without checking `xflow status` first.
|
package/dist/commands/mcp.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.refreshAgentKey = refreshAgentKey;
|
|
4
|
-
exports.mcpInstall = mcpInstall;
|
|
5
|
-
const node_os_1 = require("node:os");
|
|
6
|
-
const node_path_1 = require("node:path");
|
|
7
|
-
const api_1 = require("../api");
|
|
8
|
-
const args_1 = require("../args");
|
|
9
|
-
const config_1 = require("../config");
|
|
10
|
-
const session_1 = require("../session");
|
|
11
|
-
const spawn_1 = require("../spawn");
|
|
12
|
-
const ui_1 = require("../ui");
|
|
13
|
-
/** Clients with their own server-add command. */
|
|
14
|
-
const CLIENTS = [
|
|
15
|
-
{
|
|
16
|
-
binary: 'claude',
|
|
17
|
-
label: 'Claude Code',
|
|
18
|
-
args: (url, token) => [
|
|
19
|
-
'mcp',
|
|
20
|
-
'add',
|
|
21
|
-
'--transport',
|
|
22
|
-
'http',
|
|
23
|
-
'xflow',
|
|
24
|
-
url,
|
|
25
|
-
'--header',
|
|
26
|
-
`Authorization: Bearer ${token}`,
|
|
27
|
-
],
|
|
28
|
-
// Re-running must update the entry, not fail on it.
|
|
29
|
-
reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
|
|
30
|
-
// Succeeds only when an xflow entry is visible from this folder.
|
|
31
|
-
probe: ['mcp', 'get', 'xflow'],
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
// Output is captured, not inherited: clients may echo the added header together
|
|
35
|
-
// with the key.
|
|
36
|
-
function hasBinary(binary) {
|
|
37
|
-
return (0, spawn_1.run)(binary, ['--version']).status === 0;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Rewrite the key in clients that already hold an xflow entry visible from this
|
|
41
|
-
* folder. Entries are never created here: that stays with xflow mcp install.
|
|
42
|
-
*/
|
|
43
|
-
function refreshAgentKey(client) {
|
|
44
|
-
const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
|
|
45
|
-
let updated = false;
|
|
46
|
-
for (const target of CLIENTS) {
|
|
47
|
-
if (!hasBinary(target.binary))
|
|
48
|
-
continue;
|
|
49
|
-
if ((0, spawn_1.run)(target.binary, [...target.probe]).status !== 0)
|
|
50
|
-
continue;
|
|
51
|
-
(0, spawn_1.run)(target.binary, [...target.reset]);
|
|
52
|
-
if ((0, spawn_1.run)(target.binary, target.args(url, client.token)).status === 0)
|
|
53
|
-
updated = true;
|
|
54
|
-
}
|
|
55
|
-
return updated;
|
|
56
|
-
}
|
|
57
|
-
async function mcpInstall(args) {
|
|
58
|
-
// The server covers the organization: the command works outside a project folder too.
|
|
59
|
-
const root = (0, config_1.findProjectRoot)();
|
|
60
|
-
const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
|
|
61
|
-
const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
|
|
62
|
-
const identity = await (0, api_1.apiJson)(client, '/api/v1/me');
|
|
63
|
-
(0, ui_1.out)(`${(0, ui_1.bold)('MCP server')} ${url}`);
|
|
64
|
-
(0, ui_1.note)((0, ui_1.dim)(` Organization: ${identity.organization.name ?? 'unnamed'}`));
|
|
65
|
-
(0, ui_1.note)((0, ui_1.dim)(' The key covers the whole organization: the agent names the project itself'));
|
|
66
|
-
let found = 0;
|
|
67
|
-
let installed = 0;
|
|
68
|
-
for (const target of CLIENTS) {
|
|
69
|
-
if (!hasBinary(target.binary))
|
|
70
|
-
continue;
|
|
71
|
-
found++;
|
|
72
|
-
(0, ui_1.step)(`Writing into ${target.label}`);
|
|
73
|
-
(0, spawn_1.run)(target.binary, [...target.reset]);
|
|
74
|
-
const result = (0, spawn_1.run)(target.binary, target.args(url, client.token));
|
|
75
|
-
if (result.status === 0) {
|
|
76
|
-
installed++;
|
|
77
|
-
(0, ui_1.ok)(`${target.label}: the xflow server is connected`);
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const reason = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
|
|
81
|
-
(0, ui_1.note)(`${target.label}: the command exited with an error`);
|
|
82
|
-
if (reason)
|
|
83
|
-
(0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('<key>').split('\n').slice(0, 5).join('\n ')}`));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (installed === 0) {
|
|
87
|
-
(0, ui_1.note)(found === 0 ? 'Found no client with its own connect command.' : 'Add the server by hand:');
|
|
88
|
-
(0, ui_1.out)(` type: HTTP (streamable)`);
|
|
89
|
-
(0, ui_1.out)(` address: ${url}`);
|
|
90
|
-
(0, ui_1.out)(` header: Authorization: Bearer <key>`);
|
|
91
|
-
}
|
|
92
|
-
if ((0, args_1.flagBool)(args, 'show-token')) {
|
|
93
|
-
(0, ui_1.out)('');
|
|
94
|
-
(0, ui_1.out)(`${(0, ui_1.bold)('Key')} ${client.token}`);
|
|
95
|
-
(0, ui_1.note)((0, ui_1.dim)(' Do not leave it in the chat with the agent'));
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
(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`));
|
|
99
|
-
}
|
|
100
|
-
}
|
package/dist/prompt.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.multiSelect = multiSelect;
|
|
4
|
-
exports.select = select;
|
|
5
|
-
const node_readline_1 = require("node:readline");
|
|
6
|
-
const errors_1 = require("./errors");
|
|
7
|
-
const ui_1 = require("./ui");
|
|
8
|
-
function render(lines, first) {
|
|
9
|
-
const up = first || lines.length === 1 ? '' : `[${lines.length - 1}A`;
|
|
10
|
-
process.stderr.write(`${up}\r${lines.map((line) => `[2K${line}`).join('\n')}`);
|
|
11
|
-
}
|
|
12
|
-
function listen(onKey) {
|
|
13
|
-
if (process.stdin.isTTY !== true) {
|
|
14
|
-
throw new errors_1.CliError('An interactive prompt needs a terminal', 'Pass the choice through flags instead');
|
|
15
|
-
}
|
|
16
|
-
(0, node_readline_1.emitKeypressEvents)(process.stdin);
|
|
17
|
-
const wasRaw = process.stdin.isRaw === true;
|
|
18
|
-
process.stdin.setRawMode(true);
|
|
19
|
-
process.stdin.resume();
|
|
20
|
-
process.stderr.write('[?25l');
|
|
21
|
-
const handler = (_, key) => {
|
|
22
|
-
if (key)
|
|
23
|
-
onKey(key);
|
|
24
|
-
};
|
|
25
|
-
process.stdin.on('keypress', handler);
|
|
26
|
-
return () => {
|
|
27
|
-
process.stdin.off('keypress', handler);
|
|
28
|
-
process.stdin.setRawMode(wasRaw);
|
|
29
|
-
process.stdin.pause();
|
|
30
|
-
process.stderr.write('[?25h\n');
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
const isConfirm = (key) => key.name === 'return' || key.name === 'enter';
|
|
34
|
-
const isCancel = (key) => key.name === 'escape' || (key.ctrl === true && key.name === 'c');
|
|
35
|
-
/** Checkbox list. Resolves with the picked indexes, or null when cancelled. */
|
|
36
|
-
function multiSelect(title, choices) {
|
|
37
|
-
return new Promise((resolve) => {
|
|
38
|
-
let cursor = 0;
|
|
39
|
-
let first = true;
|
|
40
|
-
const checked = choices.map((choice) => choice.checked === true);
|
|
41
|
-
const draw = () => {
|
|
42
|
-
render([
|
|
43
|
-
(0, ui_1.bold)(title),
|
|
44
|
-
...choices.map((choice, i) => {
|
|
45
|
-
const hint = choice.hint ? ` ${(0, ui_1.dim)(choice.hint)}` : '';
|
|
46
|
-
return `${i === cursor ? '>' : ' '} ${checked[i] ? '[x]' : '[ ]'} ${choice.label}${hint}`;
|
|
47
|
-
}),
|
|
48
|
-
(0, ui_1.dim)(' up/down move, space toggle, enter confirm, esc cancel'),
|
|
49
|
-
], first);
|
|
50
|
-
first = false;
|
|
51
|
-
};
|
|
52
|
-
const stop = listen((key) => {
|
|
53
|
-
if (key.name === 'up')
|
|
54
|
-
cursor = (cursor + choices.length - 1) % choices.length;
|
|
55
|
-
else if (key.name === 'down')
|
|
56
|
-
cursor = (cursor + 1) % choices.length;
|
|
57
|
-
else if (key.name === 'space')
|
|
58
|
-
checked[cursor] = !checked[cursor];
|
|
59
|
-
else if (isConfirm(key)) {
|
|
60
|
-
stop();
|
|
61
|
-
resolve(checked.flatMap((on, i) => (on ? [i] : [])));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
else if (isCancel(key)) {
|
|
65
|
-
stop();
|
|
66
|
-
resolve(null);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
else
|
|
70
|
-
return;
|
|
71
|
-
draw();
|
|
72
|
-
});
|
|
73
|
-
draw();
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
/** Radio list. Resolves with the picked index, or null when cancelled. */
|
|
77
|
-
function select(title, choices) {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
let cursor = 0;
|
|
80
|
-
let first = true;
|
|
81
|
-
const draw = () => {
|
|
82
|
-
render([
|
|
83
|
-
(0, ui_1.bold)(title),
|
|
84
|
-
...choices.map((choice, i) => {
|
|
85
|
-
const hint = choice.hint ? ` ${(0, ui_1.dim)(choice.hint)}` : '';
|
|
86
|
-
return `${i === cursor ? '>' : ' '} ${choice.label}${hint}`;
|
|
87
|
-
}),
|
|
88
|
-
(0, ui_1.dim)(' up/down move, enter confirm, esc cancel'),
|
|
89
|
-
], first);
|
|
90
|
-
first = false;
|
|
91
|
-
};
|
|
92
|
-
const stop = listen((key) => {
|
|
93
|
-
if (key.name === 'up')
|
|
94
|
-
cursor = (cursor + choices.length - 1) % choices.length;
|
|
95
|
-
else if (key.name === 'down')
|
|
96
|
-
cursor = (cursor + 1) % choices.length;
|
|
97
|
-
else if (isConfirm(key)) {
|
|
98
|
-
stop();
|
|
99
|
-
resolve(cursor);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
else if (isCancel(key)) {
|
|
103
|
-
stop();
|
|
104
|
-
resolve(null);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
else
|
|
108
|
-
return;
|
|
109
|
-
draw();
|
|
110
|
-
});
|
|
111
|
-
draw();
|
|
112
|
-
});
|
|
113
|
-
}
|