@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 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
@@ -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);
@@ -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: platform settings, Connectors'));
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, '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
  }
@@ -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)} && npm install # functions`);
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"`);
@@ -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')}
@@ -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> --confirm <project name>
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)('--confirm')} takes the project name. It also needs the
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)('--force')} goes through anyway. Seeing them needs the sources,
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
- 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.
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, no questions asked
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. A folder with no record of the server revision (a fresh clone, a CI
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')} 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.
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 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
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.1.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -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.1.1, so the copy you are reading can be older than
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`, and every MCP tool, answers `subscription_blocked`,
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`), MCP tools
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` destroys
176
- that work. A folder with no record of the server revision (a fresh clone, a CI checkout) is
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 `--force`.
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> --confirm <project name>`. Writes close while it
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.
@@ -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) => `${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
- }