@getxflow/cli 0.20.2 → 0.21.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/dist/bin.js CHANGED
@@ -27,7 +27,9 @@ const skills_1 = require("./commands/skills");
27
27
  const sources_1 = require("./commands/sources");
28
28
  const storage_1 = require("./commands/storage");
29
29
  const deploy_1 = require("./commands/deploy");
30
+ const dev_1 = require("./commands/dev");
30
31
  const update_1 = require("./commands/update");
32
+ /** A command that wraps another process answers with its exit code. */
31
33
  async function run(args) {
32
34
  const [first, second] = args.words;
33
35
  const rest = { ...args, words: args.words.slice(1) };
@@ -195,6 +197,8 @@ async function run(args) {
195
197
  case 'status':
196
198
  await (0, sources_1.status)();
197
199
  return;
200
+ case 'dev':
201
+ return (0, dev_1.dev)();
198
202
  case 'pull':
199
203
  await (0, sources_1.pull)(rest);
200
204
  return;
@@ -281,8 +285,7 @@ async function main() {
281
285
  let nudgeCovered = args.words[0] === 'update';
282
286
  try {
283
287
  (0, flags_1.checkFlags)(args);
284
- await run(args);
285
- return 0;
288
+ return (await run(args)) ?? 0;
286
289
  }
287
290
  catch (e) {
288
291
  if (e instanceof api_1.ApiError) {
@@ -56,7 +56,7 @@ async function dbStatus(args) {
56
56
  return;
57
57
  }
58
58
  const { root, config } = (0, config_1.requireProject)();
59
- const client = (0, session_1.connect)(config);
59
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
60
60
  const local = readMigrations(root);
61
61
  const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
62
62
  const applied = new Map(history.applied.map((row) => [row.name, row]));
@@ -224,7 +224,7 @@ async function dbQuery(args) {
224
224
  }
225
225
  async function dbMigrate(args) {
226
226
  const { root, config } = (0, config_1.requireProject)();
227
- const client = (0, session_1.connect)(config);
227
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
228
228
  const migrations = requireMigrations(root);
229
229
  const dryRun = (0, args_1.flagBool)(args, 'dry-run');
230
230
  (0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
@@ -185,7 +185,7 @@ async function deploy(args) {
185
185
  (0, ui_1.note)((0, ui_1.dim)(' Show the new pages to visitors: xflow publish'));
186
186
  }
187
187
  async function publish(args) {
188
- const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
188
+ const { projectId, client } = await (0, session_1.projectTarget)(args);
189
189
  (0, ui_1.step)('Publishing the current dev version');
190
190
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
191
191
  if (result.already_published) {
@@ -203,7 +203,7 @@ async function rollback(args) {
203
203
  if (!deployId) {
204
204
  throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
205
205
  }
206
- const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
206
+ const { projectId, client } = await (0, session_1.projectTarget)(args);
207
207
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
208
208
  (0, ui_1.ok)(`The project now serves the pages of build ${result.deploy_id}`);
209
209
  (0, ui_1.out)(result.project_url);
@@ -216,7 +216,7 @@ async function rollback(args) {
216
216
  (0, ui_1.note)((0, ui_1.dim)(' Visitors see these pages only after xflow publish'));
217
217
  }
218
218
  async function deployments(args) {
219
- const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
219
+ const { projectId, client } = await (0, session_1.projectTarget)(args);
220
220
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/deployments`);
221
221
  if (data.deployments.length === 0) {
222
222
  (0, ui_1.note)('No versions yet. To build and release: xflow deploy');
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dev = dev;
4
+ const node_os_1 = require("node:os");
5
+ const config_1 = require("../config");
6
+ const errors_1 = require("../errors");
7
+ const session_1 = require("../session");
8
+ const spawn_1 = require("../spawn");
9
+ const template_1 = require("../template");
10
+ /**
11
+ * The local dev server, handed the key this CLI picks for the project. The template
12
+ * proxy trades it for a pass on every function call, so which organization holds the
13
+ * project is decided here, by the rule every other command follows.
14
+ *
15
+ * Answers with the exit code of the server.
16
+ */
17
+ async function dev() {
18
+ const { root, config } = (0, config_1.requireProject)();
19
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
20
+ const env = {
21
+ ...process.env,
22
+ XFLOW_TOKEN: client.token,
23
+ // The proxy sends the key to this address unchecked, and the environment beats
24
+ // .env: pinned here, the key goes only to the platform that issued it.
25
+ VITE_XFLOW_API_URL: (0, template_1.envApiUrl)(client.apiUrl),
26
+ };
27
+ // Ctrl+C reaches the server on its own; staying alive until it is gone is what
28
+ // hands its exit code back instead of leaving it to print over the prompt.
29
+ process.on('SIGINT', () => { });
30
+ const result = (0, spawn_1.run)('npm', ['run', 'dev'], { stdio: 'inherit', cwd: root, env });
31
+ if (result.error) {
32
+ throw new errors_1.CliError(`Could not start npm run dev: ${result.error.message}`, 'npm ships with Node.js: check that npm works in this terminal');
33
+ }
34
+ // Killed by a signal: the code a shell would report for it.
35
+ const signal = result.signal ? node_os_1.constants.signals[result.signal] : 0;
36
+ return result.status ?? 128 + signal;
37
+ }
@@ -104,7 +104,8 @@ async function envList(args) {
104
104
  /** Check that every referenced variable is stored. */
105
105
  async function envCheck() {
106
106
  const { root, config } = (0, config_1.requireProject)();
107
- const { names } = await fetchVariables((0, session_1.connect)(config), config.projectId);
107
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
108
+ const { names } = await fetchVariables(client, config.projectId);
108
109
  const { needed, provided, opaque } = referencedByFunctions(root);
109
110
  if (needed.size === 0 && provided.size === 0 && opaque.size === 0) {
110
111
  (0, ui_1.note)('The functions of this project read no environment variables');
@@ -159,7 +160,7 @@ async function envCheck() {
159
160
  }
160
161
  async function envSet(args) {
161
162
  const { root, config } = (0, config_1.requireProject)();
162
- const client = (0, session_1.connect)(config);
163
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
163
164
  const pair = args.words[1];
164
165
  if (!pair || !pair.includes('=')) {
165
166
  throw new errors_1.CliError('A NAME=value pair is required', 'For example: xflow env set SMTP_PASSWORD=secret');
@@ -98,7 +98,7 @@ async function init(args) {
98
98
  (0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
99
99
  (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
100
100
  (0, ui_1.out)(' npm install');
101
- (0, ui_1.out)(' npm run dev # develop');
101
+ (0, ui_1.out)(' xflow dev # develop');
102
102
  (0, ui_1.out)(' xflow deploy # send the code, build and release');
103
103
  }
104
104
  /**
@@ -160,7 +160,7 @@ async function link(args) {
160
160
  if (client.organizationId)
161
161
  (0, config_1.writeState)(dir, { organizationId: client.organizationId });
162
162
  (0, template_1.writePlatformEnv)(dir, card.project_token, client.apiUrl, card.functions);
163
- (0, ui_1.note)((0, ui_1.dim)(' .env now points at this project'));
163
+ (0, ui_1.note)((0, ui_1.dim)(' .env now points at this project, run it locally with xflow dev'));
164
164
  const skillReady = (0, skills_1.installSkillQuietly)(dir);
165
165
  (0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
166
166
  (0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
@@ -53,7 +53,7 @@ This project is hosted on the XFlow platform and ships through the \`xflow\` CLI
53
53
  through a git push. Read \`${exports.SKILL_IN_PROJECT}\` before deploying, publishing,
54
54
  rolling back, touching the database or migrations, cloud functions, schedules,
55
55
  environment variables, file storage and uploads, connected accounts or production logs.
56
- Command list: \`xflow help\`.
56
+ Run the app locally with \`xflow dev\`. Command list: \`xflow help\`.
57
57
  `;
58
58
  /** The skill ships inside the package, next to dist. */
59
59
  function skillSource() {
package/dist/flags.js CHANGED
@@ -82,6 +82,7 @@ exports.KNOWN = {
82
82
  'storage rm': ['folder', 'yes', 'project'],
83
83
  'storage remove': ['folder', 'yes', 'project'],
84
84
  status: [],
85
+ dev: [],
85
86
  pull: ['into', 'force', 'revision'],
86
87
  deploy: ['no-push', 'force', 'allow-removals', 'message'],
87
88
  publish: ['project'],
@@ -105,6 +106,7 @@ exports.WORKING_COPY = {
105
106
  deploy: 'it sends the sources next to it',
106
107
  pull: 'it writes the sources next to it',
107
108
  status: 'it compares the folder with the revision on the platform',
109
+ dev: 'it runs the dev server of the code next to it',
108
110
  'db migrate': 'it applies the files of migrations/',
109
111
  'env check': 'it looks for the variables the code of the functions reads',
110
112
  'env set': 'it names the functions that need a deploy for the value to arrive',
package/dist/help.js CHANGED
@@ -40,6 +40,7 @@ ${(0, ui_1.bold)('Getting started')}
40
40
  xflow init [dir] new project from the platform template
41
41
  xflow templates which templates are available
42
42
  xflow link <id> link this folder to a project, an empty one also gets the sources
43
+ xflow dev run the app on localhost:3000, cloud functions included
43
44
  xflow skills pick the agents that get the platform instructions
44
45
  xflow mcp install give the agent platform access without a terminal
45
46
 
@@ -163,8 +164,8 @@ and ${(0, ui_1.bold)('connections unlink')} removes the link without naming the
163
164
  its variables, for the same reason.
164
165
 
165
166
  The rest cannot answer without the working copy and have nothing to do with an id
166
- instead: ${(0, ui_1.bold)('deploy')}, ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('db migrate')}, ${(0, ui_1.bold)('env check')},
167
- ${(0, ui_1.bold)('env set')}. They answer about the files next to them, so
167
+ instead: ${(0, ui_1.bold)('deploy')}, ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('dev')}, ${(0, ui_1.bold)('db migrate')},
168
+ ${(0, ui_1.bold)('env check')}, ${(0, ui_1.bold)('env set')}. They answer about the files next to them, so
168
169
  they keep asking for a folder. ${(0, ui_1.bold)('projects get')} and ${(0, ui_1.bold)('link')} take no flag either:
169
170
  they name the project by its id as an argument.
170
171
 
@@ -532,6 +533,20 @@ Plus a few pointer lines in ${(0, ui_1.bold)('AGENTS.md')}. A skill is picked up
532
533
  description matches the task, and "add a customers table" will not trigger it. AGENTS.md
533
534
  is always read by the agent, which is why the pointer is appended there, at the end of
534
535
  the file and once. ${(0, ui_1.bold)('CLAUDE.md')} is left alone: Claude Code reads .claude/skills anyway.`,
536
+ dev: `${(0, ui_1.bold)('xflow dev')}: run the application on this machine
537
+
538
+ Runs ${(0, ui_1.bold)('npm run dev')} in the project folder (localhost:3000) with the access key of the
539
+ organization that holds the project. The dev server trades that key for a visitor pass,
540
+ so the cloud functions answer here the way they do on the platform. The key is picked
541
+ as for every other command (${(0, ui_1.bold)('xflow help org')}): with several organizations signed
542
+ in, the right one is found and the folder is bound to it.
543
+
544
+ Use it instead of a bare ${(0, ui_1.bold)('npm run dev')}, which gets no key from here: its function
545
+ calls fail, or go out under the key of whichever organization is active.
546
+
547
+ The platform address comes from the CLI too, not from .env, so the key goes to the
548
+ platform that issued it and nowhere else. It never reaches the browser: the page gets
549
+ only the ${(0, ui_1.bold)('VITE_')} variables.`,
535
550
  pull: `${(0, ui_1.bold)('xflow pull')}: fetch the sources
536
551
 
537
552
  By default it fetches the latest revision into the project folder and refuses to write
package/dist/session.js CHANGED
@@ -125,16 +125,15 @@ function isProjectId(value) {
125
125
  * the default one or XFLOW_API_URL and nothing else: a config lying around must
126
126
  * not be able to redirect a call made by id.
127
127
  *
128
- * `probe` is the difference the folder path already has and keeps: publish,
129
- * rollback and deployments look for the organization of an unbound folder,
130
- * everything else takes the key it resolves to. Named by id there is no binding
131
- * to lean on, so the search always happens.
128
+ * Either way the key comes from connectProject: an unbound folder gets the
129
+ * organization of its project looked up once and written down, an id gets it
130
+ * looked up on every call.
132
131
  *
133
132
  * `root` is that folder, and null when the id was named: a command that reads the
134
133
  * sources for a check of its own then knows the check is not available rather than
135
134
  * running it against whatever folder the caller happened to stand in.
136
135
  */
137
- async function projectTarget(args, options) {
136
+ async function projectTarget(args) {
138
137
  const named = (0, args_1.flagString)(args, 'project');
139
138
  if (named !== undefined) {
140
139
  if (!isProjectId(named)) {
@@ -150,6 +149,6 @@ async function projectTarget(args, options) {
150
149
  return { projectId: named, client, root: null };
151
150
  }
152
151
  const { root, config } = (0, config_1.requireProject)();
153
- const client = options?.probe === true ? await connectProject(config.projectId, root, config) : connect(config);
152
+ const client = await connectProject(config.projectId, root, config);
154
153
  return { projectId: config.projectId, client, root };
155
154
  }
package/dist/spawn.js CHANGED
@@ -13,7 +13,13 @@ function quote(value) {
13
13
  return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
14
14
  }
15
15
  function run(binary, args, options = {}) {
16
- const common = { stdio: options.stdio, timeout: options.timeout, encoding: 'utf-8' };
16
+ const common = {
17
+ stdio: options.stdio,
18
+ timeout: options.timeout,
19
+ cwd: options.cwd,
20
+ env: options.env,
21
+ encoding: 'utf-8',
22
+ };
17
23
  return WINDOWS
18
24
  ? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...common, shell: true })
19
25
  : (0, node_child_process_1.spawnSync)(binary, args, common);
package/dist/template.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envApiUrl = envApiUrl;
3
4
  exports.writePlatformEnv = writePlatformEnv;
4
5
  exports.scaffoldFiles = scaffoldFiles;
5
6
  const node_fs_1 = require("node:fs");
@@ -16,7 +17,7 @@ An application on XFlow.
16
17
 
17
18
  \`\`\`bash
18
19
  npm install # dependencies
19
- npm run dev # develop on localhost:3000
20
+ xflow dev # develop on localhost:3000
20
21
  xflow deploy # send the code and build it on the platform
21
22
  xflow publish # show the dev version to visitors
22
23
  \`\`\`
@@ -38,13 +39,17 @@ function functionsEnvValue(functions) {
38
39
  }
39
40
  return JSON.stringify(map);
40
41
  }
42
+ /** The platform address the way .env holds it, and the way every template reads it. */
43
+ function envApiUrl(apiUrl) {
44
+ return apiUrl.replace(/^https?:\/\//, '');
45
+ }
41
46
  /**
42
47
  * Point .env at the current project: written on every link and deploy, so that `npm run dev`
43
48
  * never keeps talking to the previous one. Variables of your own are left alone.
44
49
  */
45
50
  function writePlatformEnv(root, projectToken, apiUrl, functions = []) {
46
51
  writeEnvValue(root, 'VITE_XFLOW_PROJECT_TOKEN', projectToken ?? '');
47
- writeEnvValue(root, 'VITE_XFLOW_API_URL', apiUrl.replace(/^https?:\/\//, ''));
52
+ writeEnvValue(root, 'VITE_XFLOW_API_URL', envApiUrl(apiUrl));
48
53
  writeEnvValue(root, FUNCTIONS_ENV_KEY, functionsEnvValue(functions));
49
54
  }
50
55
  /** Update one variable in .env without touching the rest of the file. */
package/dist/version.js CHANGED
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
4
4
  /** Keep in sync with cli/package.json. */
5
- exports.CLI_VERSION = '0.20.2';
5
+ exports.CLI_VERSION = '0.21.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -22,7 +22,7 @@ contains the fix.
22
22
 
23
23
  ## Keeping these instructions current
24
24
 
25
- These instructions ship with xflow CLI 0.20.2, so the copy you are reading can be older than
25
+ These instructions ship with xflow CLI 0.21.0, so the copy you are reading can be older than
26
26
  the CLI answering your commands.
27
27
 
28
28
  When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
@@ -594,12 +594,11 @@ rejected promises and 5xx responses. The same stream, functions included, is in
594
594
  platform UI: open the project, section «Облако» (Cloud), tab «Логи» (Logs). The last 200
595
595
  records per project are kept.
596
596
 
597
- Local `npm run dev` does not report anything: these logs exist for what you cannot open
598
- in your own devtools.
597
+ A local run does not report anything: these logs exist for what you cannot open in your
598
+ own devtools.
599
599
 
600
- Cloud functions do work under `npm run dev`, and nothing has to be configured for that:
601
- the dev server swaps the access key of the logged-in developer for the same narrow pass and
602
- forwards the call. If it answers that the key is missing, the fix is `xflow login`.
600
+ Run the app locally with `xflow dev`, not a bare `npm run dev`: it hands the dev server
601
+ the key of the project's organization, which cloud functions need to answer.
603
602
 
604
603
  ## Environment
605
604