@getxflow/cli 1.1.1 → 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 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
@@ -17,12 +17,10 @@ exports.BOOLEAN_FLAGS = new Set([
17
17
  'version',
18
18
  'global',
19
19
  'refresh',
20
- 'no-push',
21
20
  'all',
22
21
  'dry-run',
23
22
  'allow-destructive',
24
23
  'allow-removals',
25
- 'show-token',
26
24
  ]);
27
25
  function parseArgs(argv) {
28
26
  const words = [];
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, 'force')) {
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 --force if that is intended');
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' });
@@ -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: (0, args_1.flagString)(args, '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
- const found = await projectDatabase(client, projectId).catch(() => null);
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
  }
@@ -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, asking before anything is destroyed. Removing a cloud function
53
- * cannot be undone, so the platform refuses such a deploy until the caller says yes,
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
- const removing = e.removing ?? [];
67
- if (removing.length === 0)
64
+ if (!e.removing?.length)
68
65
  throw e;
69
- // No terminal (CI, an agent): the flag is the only way to say yes.
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
- if ((0, args_1.flagBool)(args, 'no-push')) {
191
- const server = await (0, sources_1.latestRevision)(client, base);
192
- if (!server)
193
- throw new errors_1.CliError('The server holds no sources', 'Repeat without --no-push, so that the sources go up first');
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
  }
@@ -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
  }
@@ -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
- let global = (0, args_1.flagBool)(args, 'global');
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
  }
@@ -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
- /** Show what --force would overwrite and ask for confirmation by the name of the project or app. */
72
- async function confirmForce(client, base, server, local) {
73
- const { name } = await (0, api_1.apiJson)(client, base);
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': ['force', 'project'],
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', 'confirm', 'project'],
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: ['no-push', 'force', 'allow-removals', 'message', 'revision', 'project', 'app'],
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> [--force]
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> --confirm <database>
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:
@@ -346,7 +341,7 @@ will work.`,
346
341
  one, 300 at most; --json prints the values whole)
347
342
  xflow db query <file>.sql the same, the query read from a file
348
343
  xflow db migrate store the project root, then apply migrations/*.sql
349
- xflow db move --to <server> --confirm <project name>
344
+ xflow db move --to <server> --yes
350
345
  move the project database to another server
351
346
 
352
347
  Every project has exactly one database of its own, shared by all of its apps and functions,
@@ -361,7 +356,7 @@ ${(0, ui_1.bold)('move')} takes the database of this project to another server o
361
356
  ${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
362
357
  the tables, switches the functions of the project and removes the old copy; nothing needs a
363
358
  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)('--confirm')} takes the project name. It also needs the
359
+ couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
365
360
  right to move databases on the key: off by default, only its owner turns it on, in the
366
361
  platform settings where the key permissions live. The command follows the move to the end;
367
362
  stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
@@ -518,7 +513,7 @@ of the same service under one name; then the command prints their identifiers an
518
513
  for one of those instead of guessing.
519
514
 
520
515
  ${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
521
- those functions; ${(0, ui_1.bold)('--force')} goes through anyway. Seeing them needs the sources,
516
+ those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
522
517
  so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
523
518
  that go with it are printed instead. Both commands take the flag, because an account
524
519
  linked to a service project without a folder has to be removable the same way.
@@ -642,24 +637,6 @@ Prints the body of the answer and nothing else, so it goes straight into a pipe
642
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
643
638
  refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
644
639
  ${(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
640
  skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
664
641
 
665
642
  An agent has no way to know about XFlow: the platform is not in its training. The
@@ -669,16 +646,12 @@ down on their own for the common agents, so this command exists to pick tools
669
646
  precisely. Refreshing after a CLI update is not your job any more: the skill and the
670
647
  CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
671
648
 
672
- Run in a terminal, it asks two questions: where (this project, or the home folder,
673
- which makes the skill visible in every project) and for which agents. Copies that are
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.
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.
677
651
 
678
652
  xflow skills list the agents, their folders and what is installed
679
- --agent claude,cursor exact agents, no questions asked
653
+ --agent claude,cursor exact agents instead of the defaults
680
654
  --global the home folder instead of the project
681
- --yes no questions: defaults plus what is installed
682
655
  --refresh rewrite the copies that exist, create none
683
656
 
684
657
  The format is shared (agentskills.io) and inside a project most tools read the shared
@@ -730,7 +703,6 @@ apps never go in one run, because a change of functions reaches every app at onc
730
703
 
731
704
  -m <text> the version comment, required: one short line in the language the
732
705
  user speaks, saying what changed (--message is the long form)
733
- --no-push do not send sources, build from the latest server revision
734
706
  --revision <N> run a revision the platform already holds: that is how the functions
735
707
  come back (xflow deployments functions lists them)
736
708
  --project <id> with --revision only: the functions of that project, no folder needed
@@ -746,12 +718,12 @@ ignore field of xflow.json.
746
718
  If the server holds a revision newer than the one you worked from, the sources are
747
719
  rejected and the build does not start. That means somebody deployed before you: fetch
748
720
  their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
749
- your side, and retry. A folder with no record of the server revision (a fresh clone, a CI
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
750
723
  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')} overwrites the server revision, and before that the
752
- CLI shows whose work you are about to destroy and asks for confirmation by typing the
753
- project name. In a non-interactive run (CI, an agent) there is no way to confirm: a
754
- 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.
755
727
 
756
728
  The platform builds, in a clean sandbox on one Node version for everybody. Before that the
757
729
  sources are checked: what breaks silently (the platform files, the calls in the entry point,
@@ -769,9 +741,8 @@ internet now. A change that touches data and screens goes in this order: the fun
769
741
  changes nobody deployed.
770
742
 
771
743
  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 until you say yes. In a terminal the CLI names the functions
773
- and asks; without one (CI, an agent) it stops and ${(0, ui_1.bold)('--allow-removals')} is the only way
774
- 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
775
746
  is deleted and nothing is asked: that looks like a directory which never made it (a
776
747
  ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
777
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/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.1.1';
5
+ exports.CLI_VERSION = '1.2.0';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
- {
2
- "name": "@getxflow/cli",
3
- "version": "1.1.1",
4
- "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
- "license": "UNLICENSED",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "bin": {
10
- "xflow": "dist/bin.js"
11
- },
12
- "files": [
13
- "dist",
14
- "skills"
15
- ],
16
- "scripts": {
17
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
18
- "build": "npm run clean && tsc -p tsconfig.json",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- }
24
- }
1
+ {
2
+ "name": "@getxflow/cli",
3
+ "version": "1.2.0",
4
+ "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
+ "license": "UNLICENSED",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "bin": {
10
+ "xflow": "dist/bin.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "skills"
15
+ ],
16
+ "scripts": {
17
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
18
+ "build": "npm run clean && tsc -p tsconfig.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }