@getxflow/cli 0.15.0 → 0.17.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.
@@ -4,8 +4,6 @@ exports.deploy = deploy;
4
4
  exports.publish = publish;
5
5
  exports.rollback = rollback;
6
6
  exports.deployments = deployments;
7
- const node_fs_1 = require("node:fs");
8
- const node_path_1 = require("node:path");
9
7
  const api_1 = require("../api");
10
8
  const args_1 = require("../args");
11
9
  const config_1 = require("../config");
@@ -47,21 +45,15 @@ async function waitForBuild(client, projectId, started) {
47
45
  throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
48
46
  }
49
47
  /**
50
- * Write the function addresses into the local .env.
48
+ * Point the local .env at this project, function addresses included.
51
49
  *
52
50
  * The build bakes its own copy of the map into the bundle, this one is for
53
51
  * `npm run dev`: without it the local run calls a function that it has no
54
52
  * address for.
55
53
  */
56
- async function refreshFunctionsEnv(root, client, projectId) {
54
+ async function refreshPlatformEnv(root, client, projectId) {
57
55
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
58
- // No .env at all (fresh clone): recreate it fully, like link does.
59
- if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
60
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, '.env'), (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions), 'utf-8');
61
- }
62
- else {
63
- (0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
64
- }
56
+ (0, template_1.writePlatformEnv)(root, card.project_token, client.apiUrl, card.functions);
65
57
  return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
66
58
  }
67
59
  /**
@@ -180,7 +172,7 @@ async function deploy(args) {
180
172
  // The functions were shipped by the build itself, so their addresses are known
181
173
  // only now. A failure here does not undo a finished build: warn and stop there.
182
174
  try {
183
- const functions = await refreshFunctionsEnv(root, client, config.projectId);
175
+ const functions = await refreshPlatformEnv(root, client, config.projectId);
184
176
  if (functions.length > 0) {
185
177
  (0, ui_1.note)((0, ui_1.dim)(` Functions in .env: ${functions.join(', ')}`));
186
178
  }
@@ -93,7 +93,7 @@ async function functionsInvoke(args) {
93
93
  const text = await response.text();
94
94
  (0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} in ${elapsed} ms`));
95
95
  if (text)
96
- (0, ui_1.out)(prettyBody(text));
96
+ (0, ui_1.out)((0, args_1.flagBool)(args, 'json') ? text : prettyBody(text));
97
97
  if (!response.ok) {
98
98
  throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
99
99
  }
@@ -77,7 +77,7 @@ async function init(args) {
77
77
  write(target, file.path, titleFromProject(file.path, file.content, name));
78
78
  for (const file of (0, template_1.scaffoldFiles)(name))
79
79
  write(target, file.path, file.content);
80
- write(target, '.env', (0, template_1.envFile)(project.project_token ?? '', client.apiUrl));
80
+ (0, template_1.writePlatformEnv)(target, project.project_token, client.apiUrl);
81
81
  const config = {
82
82
  projectId: project.id,
83
83
  build: { command: 'npm run build', dir: 'dist' },
@@ -159,11 +159,8 @@ async function link(args) {
159
159
  (0, config_1.ignoreStateInGit)(dir);
160
160
  if (client.organizationId)
161
161
  (0, config_1.writeState)(dir, { organizationId: client.organizationId });
162
- // A fresh clone has no .env: recreate it, never overwrite an existing one.
163
- if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.env')) && card.project_token) {
164
- write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
165
- (0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));
166
- }
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'));
167
164
  const skillReady = (0, skills_1.installSkillQuietly)(dir);
168
165
  (0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
169
166
  (0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
@@ -176,7 +173,7 @@ async function link(args) {
176
173
  // compare against and stops.
177
174
  try {
178
175
  const { info, files } = await (0, sources_1.downloadRevision)(client, card.id, dir);
179
- (0, config_1.writeState)(dir, { revision: info.revision, treeHash: info.tree_hash });
176
+ (0, config_1.writeState)(dir, { treeHash: info.tree_hash });
180
177
  (0, ui_1.ok)(`Revision ${info.revision}: ${files} files`);
181
178
  }
182
179
  catch (e) {
@@ -74,33 +74,32 @@ async function confirmForce(client, projectId, server, local) {
74
74
  async function pushSources(root, config, client, options) {
75
75
  const tree = prepareTree(root, config);
76
76
  const state = (0, config_1.readState)(root);
77
- (0, ui_1.step)(`Sending ${tree.files.length} files (${(0, ui_1.formatBytes)(tree.archive.length)})`);
78
77
  const server = await latestRevision(client, config.projectId);
79
- // Unknown base revision (fresh clone): a matching hash just syncs,
80
- // a mismatch must not overwrite silently.
81
- if (server && state.revision === undefined && !options.force) {
82
- if (server.tree_hash === tree.hash) {
83
- (0, config_1.writeState)(root, {
84
- revision: server.revision,
85
- treeHash: server.tree_hash,
86
- organizationId: client.organizationId ?? undefined,
87
- });
88
- (0, ui_1.ok)(`Already in sync, revision ${server.revision}`);
89
- return { revision: server.revision, status: 'unchanged' };
90
- }
91
- throw new errors_1.CliError(`The server holds revision ${server.revision} while this folder does not remember which version it worked from`, 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy');
78
+ if (server && server.tree_hash === tree.hash) {
79
+ (0, config_1.writeState)(root, {
80
+ treeHash: server.tree_hash,
81
+ organizationId: client.organizationId ?? undefined,
82
+ });
83
+ (0, ui_1.ok)(`No changes, revision ${server.revision}`);
84
+ return { revision: server.revision, status: 'unchanged' };
85
+ }
86
+ // Asked before the upload, so a doomed archive never leaves the machine.
87
+ if (server && server.tree_hash !== state.treeHash && !options.force) {
88
+ 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. ' +
89
+ 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy');
92
90
  }
93
91
  if (options.force && server && server.tree_hash !== tree.hash) {
94
92
  await confirmForce(client, config.projectId, server, tree.files);
95
93
  }
94
+ (0, ui_1.step)(`Sending ${tree.files.length} files (${(0, ui_1.formatBytes)(tree.archive.length)})`);
96
95
  const headers = {};
97
- if (state.revision !== undefined)
98
- headers['X-Base-Revision'] = String(state.revision);
96
+ // Just read from this project: the header only guards the gap until the upload lands.
97
+ if (server)
98
+ headers['X-Base-Revision'] = String(server.revision);
99
99
  if (options.force)
100
100
  headers['X-Force'] = 'true';
101
101
  const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/push`, tree.archive, headers);
102
102
  (0, config_1.writeState)(root, {
103
- revision: result.revision,
104
103
  treeHash: result.tree_hash,
105
104
  organizationId: client.organizationId ?? undefined,
106
105
  });
@@ -165,7 +164,6 @@ async function pull(args) {
165
164
  // Only the working folder tracks revisions; a side copy (--into) must not reset them.
166
165
  if (!into) {
167
166
  (0, config_1.writeState)(root, {
168
- revision: info.revision,
169
167
  treeHash: info.tree_hash,
170
168
  organizationId: client.organizationId ?? undefined,
171
169
  });
@@ -196,8 +194,8 @@ async function status() {
196
194
  if (server.tree_hash === tree.hash) {
197
195
  (0, ui_1.out)('State: matches the server');
198
196
  }
199
- else if (state.revision !== undefined && state.revision < server.revision) {
200
- (0, ui_1.out)(`State: ${(0, ui_1.bold)('diverged')}, the server is newer (you worked from ${state.revision})`);
197
+ else if (server.tree_hash !== state.treeHash) {
198
+ (0, ui_1.out)(`State: ${(0, ui_1.bold)('diverged')}, the server holds a version this folder has not seen`);
201
199
  (0, ui_1.note)((0, ui_1.dim)(' Fetch the server copy alongside: xflow pull --into ./server-copy'));
202
200
  }
203
201
  else {
package/dist/config.js CHANGED
@@ -82,7 +82,9 @@ function readState(root) {
82
82
  if (!(0, node_fs_1.existsSync)(path))
83
83
  return {};
84
84
  try {
85
- return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
85
+ // Read by field, so that keys older versions wrote leave the file on the next write.
86
+ const { treeHash, organizationId } = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
87
+ return { treeHash, organizationId };
86
88
  }
87
89
  catch {
88
90
  return {};
package/dist/flags.js CHANGED
@@ -50,7 +50,7 @@ exports.KNOWN = {
50
50
  'mcp install': ['show-token'],
51
51
  functions: ['project'],
52
52
  'functions list': ['project'],
53
- 'functions invoke': ['data', 'data-file', 'method', 'project'],
53
+ 'functions invoke': ['data', 'data-file', 'json', 'method', 'project'],
54
54
  'functions logs': ['limit', 'project'],
55
55
  logs: ['limit', 'project'],
56
56
  env: ['project'],
package/dist/help.js CHANGED
@@ -472,14 +472,15 @@ Both are taken from the platform, no local .env is needed.
472
472
  --data '{"a":1}' request body (the method becomes POST by default)
473
473
  --data-file body.json the same body from a file
474
474
  --method GET a different method
475
+ --json the answer of the function alone, exactly as it came
475
476
 
476
477
  The body has to be JSON, and it is checked before the call goes out: PowerShell
477
478
  strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
478
479
  (${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
479
480
 
480
- Prints the status, the response time and the body. A non-zero exit code on 4xx and
481
- 5xx: in CI such a call has to fail the step. The cause of a crash is shown by
482
- ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
481
+ Prints the status, the response time and the body; under ${(0, ui_1.bold)('--json')} the body
482
+ alone. A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step.
483
+ The cause of a crash is shown by ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
483
484
  mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
484
485
 
485
486
  Writes the MCP server into the configuration of a client that has its own connect
package/dist/template.js CHANGED
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FUNCTIONS_ENV_KEY = void 0;
4
- exports.functionsEnvValue = functionsEnvValue;
5
- exports.envFile = envFile;
6
- exports.writeEnvValue = writeEnvValue;
3
+ exports.writePlatformEnv = writePlatformEnv;
7
4
  exports.scaffoldFiles = scaffoldFiles;
8
5
  const node_fs_1 = require("node:fs");
9
6
  const node_path_1 = require("node:path");
@@ -32,7 +29,7 @@ tokens from \`src/index.css\` break that consistency.
32
29
  Useful: \`xflow status\` for what is on the server, \`xflow deployments\` for version history.
33
30
  `;
34
31
  /** Function name → URL map, one JSON line. */
35
- exports.FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
32
+ const FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
36
33
  function functionsEnvValue(functions) {
37
34
  const map = {};
38
35
  for (const fn of functions) {
@@ -41,14 +38,14 @@ function functionsEnvValue(functions) {
41
38
  }
42
39
  return JSON.stringify(map);
43
40
  }
44
- /** .env is never pushed; xflow link recreates it in a fresh clone. */
45
- function envFile(projectToken, apiUrl, functions = []) {
46
- return [
47
- `VITE_XFLOW_PROJECT_TOKEN=${projectToken}`,
48
- `VITE_XFLOW_API_URL=${apiUrl.replace(/^https?:\/\//, '')}`,
49
- `${exports.FUNCTIONS_ENV_KEY}=${functionsEnvValue(functions)}`,
50
- '',
51
- ].join('\n');
41
+ /**
42
+ * Point .env at the current project: written on every link and deploy, so that `npm run dev`
43
+ * never keeps talking to the previous one. Variables of your own are left alone.
44
+ */
45
+ function writePlatformEnv(root, projectToken, apiUrl, functions = []) {
46
+ writeEnvValue(root, 'VITE_XFLOW_PROJECT_TOKEN', projectToken ?? '');
47
+ writeEnvValue(root, 'VITE_XFLOW_API_URL', apiUrl.replace(/^https?:\/\//, ''));
48
+ writeEnvValue(root, FUNCTIONS_ENV_KEY, functionsEnvValue(functions));
52
49
  }
53
50
  /** Update one variable in .env without touching the rest of the file. */
54
51
  function writeEnvValue(root, key, value) {
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.15.0';
5
+ exports.CLI_VERSION = '0.17.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.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -24,7 +24,7 @@ contains the fix.
24
24
 
25
25
  ## Keeping these instructions current
26
26
 
27
- These instructions ship with xflow CLI 0.15.0. They travel inside the package, so the copy
27
+ These instructions ship with xflow CLI 0.17.0. They travel inside the package, so the copy
28
28
  you are reading can be older than the CLI answering your commands, and nothing about that
29
29
  is visible in the text itself.
30
30
 
@@ -122,9 +122,10 @@ the very first version of a project: it publishes automatically, since there is
122
122
  live version to protect yet.
123
123
 
124
124
  `xflow status` says how the local copy differs from the server revision. When a deploy is
125
- rejected before the build starts, the server revision is newer: somebody deployed first.
126
- Fetch their work next to yours (`xflow pull --into ./server-copy`), merge it in git, deploy
127
- again. `--force` destroys their work, a last resort rather than a way around the error.
125
+ rejected before the build starts, the server holds a version this folder has never seen:
126
+ somebody deployed first, or the folder was linked to a different project along the way.
127
+ Fetch the server work next to yours (`xflow pull --into ./server-copy`), merge it in git,
128
+ deploy again. `--force` destroys that work, a last resort rather than a way around the error.
128
129
 
129
130
  **Only pages are versioned.** Cloud functions and the database are one per project: they
130
131
  are not versioned, and dev and live share them. So `xflow deploy` changes the running
@@ -256,8 +257,9 @@ which functions are about to go and let the user answer.
256
257
 
257
258
  Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
258
259
  the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body,
259
- `--data-file body.json` sends the same from a file), and `xflow functions logs <name>` shows
260
- the failures, each with its stack and the console output of that call. Only failed calls are
260
+ `--data-file body.json` sends the same from a file, `--json` prints the body alone), and
261
+ `xflow functions logs <name>` shows the failures, each with its stack and the console
262
+ output of that call. Only failed calls are
261
263
  logged, so an empty output means the function never crashed, not that logging is broken.
262
264
 
263
265
  The body has to be JSON and the CLI checks it before calling. In PowerShell the double