@getxflow/cli 1.0.8 → 1.1.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
@@ -309,7 +309,8 @@ async function main() {
309
309
  const args = (0, args_1.parseArgs)(process.argv.slice(2));
310
310
  // Decided here rather than in each command: the header of --project, the nudges and
311
311
  // every progress line are written on the way to a payload that has to stand alone.
312
- (0, ui_1.setQuiet)((0, args_1.flagBool)(args, 'json'));
312
+ // An invoke is always such a payload: its answer goes straight into a pipe.
313
+ (0, ui_1.setQuiet)((0, args_1.flagBool)(args, 'json') || (args.words[0] === 'functions' && args.words[1] === 'invoke'));
313
314
  if ((0, args_1.flagBool)(args, 'version')) {
314
315
  (0, ui_1.out)(version_1.CLI_VERSION);
315
316
  return 0;
@@ -6,6 +6,7 @@ exports.dbSchema = dbSchema;
6
6
  exports.dbQuery = dbQuery;
7
7
  exports.dbMove = dbMove;
8
8
  exports.dbMigrate = dbMigrate;
9
+ exports.migrateBeforeDeploy = migrateBeforeDeploy;
9
10
  const node_fs_1 = require("node:fs");
10
11
  const node_path_1 = require("node:path");
11
12
  const api_1 = require("../api");
@@ -244,7 +245,7 @@ async function dbQuery(args) {
244
245
  const { projectId, client } = await (0, session_1.projectTarget)(args);
245
246
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/query`, {
246
247
  method: 'POST',
247
- body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
248
+ body: { sql },
248
249
  timeoutMs: 60_000,
249
250
  });
250
251
  // The whole values, machine-readable, and the only thing printed: --json silences
@@ -272,11 +273,10 @@ async function dbQuery(args) {
272
273
  }
273
274
  if (data.truncated) {
274
275
  // No exact total here: the platform stops reading at limit + 1 rows, so all
275
- // it knows is that more exist. The "raise --limit" advice shows only while
276
- // the flag still has room below the ceiling.
276
+ // it knows is that more exist.
277
277
  const room = data.limit !== undefined && data.limit_max !== undefined && data.limit < data.limit_max;
278
278
  (0, ui_1.note)((0, ui_1.dim)(room
279
- ? ` Shown the first ${data.rows.length} rows, more exist: raise --limit (up to ${data.limit_max}), or narrow the query`
279
+ ? ` Shown the first ${data.rows.length} rows, more exist: end the query with a larger limit (up to ${data.limit_max}), or narrow it`
280
280
  : ` Shown the first ${data.rows.length} rows, more exist: narrow the query, with a where clause or an offset`));
281
281
  }
282
282
  }
@@ -345,13 +345,30 @@ async function dbMigrate(args) {
345
345
  // without functions would lose them from its history, and a clone would miss them.
346
346
  if (!dryRun)
347
347
  await (0, sources_1.pushSources)({ kind: 'project', root, projectId: config.projectId }, client, { force: false });
348
+ await migrate(client, config.projectId, migrations, dryRun, (0, args_1.flagBool)(args, 'allow-destructive'));
349
+ }
350
+ /** The functions deploy runs this first: new code must not meet an old schema. */
351
+ async function migrateBeforeDeploy(client, root, projectId) {
352
+ const migrations = readMigrations(root);
353
+ if (migrations.length === 0)
354
+ return;
355
+ try {
356
+ await migrate(client, projectId, migrations, false, false);
357
+ }
358
+ catch (e) {
359
+ (0, ui_1.warn)('The functions were not deployed: the migrations stopped first');
360
+ (0, ui_1.note)((0, ui_1.dim)(' Once xflow db migrate goes through, deploy again'));
361
+ throw e;
362
+ }
363
+ }
364
+ async function migrate(client, projectId, migrations, dryRun, allowDestructive) {
348
365
  (0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
349
- const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
366
+ const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`, {
350
367
  method: 'POST',
351
368
  body: {
352
369
  migrations,
353
370
  dry_run: dryRun,
354
- allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive'),
371
+ allow_destructive: allowDestructive,
355
372
  },
356
373
  timeoutMs: 300_000,
357
374
  });
@@ -14,6 +14,7 @@ const prompt_1 = require("../prompt");
14
14
  const session_1 = require("../session");
15
15
  const tree_1 = require("../tree");
16
16
  const ui_1 = require("../ui");
17
+ const db_1 = require("./db");
17
18
  const sources_1 = require("./sources");
18
19
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
19
20
  /** Slightly longer than the platform's own build timeout. */
@@ -166,7 +167,7 @@ async function warnUndeployedRoot(client, projectRoot, projectId) {
166
167
  const relation = server ? (0, tree_1.syncState)(tree.hash, server.tree_hash, (0, config_1.readState)(projectRoot).treeHash) : 'local-changes';
167
168
  if (relation !== 'matches') {
168
169
  (0, ui_1.warn)('The project root holds changes nobody deployed (functions, migrations or callers)');
169
- (0, ui_1.note)((0, ui_1.dim)(' If this app needs them: xflow db migrate and xflow deploy functions first'));
170
+ (0, ui_1.note)((0, ui_1.dim)(' If this app needs them: xflow deploy functions first, it applies the migrations too'));
170
171
  }
171
172
  }
172
173
  catch {
@@ -199,6 +200,8 @@ async function deployTarget(target, args, comment, revision) {
199
200
  : { kind: 'project', root: target.root, projectId: target.projectId };
200
201
  revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
201
202
  }
203
+ if (target.kind === 'project')
204
+ await (0, db_1.migrateBeforeDeploy)(client, target.root, target.projectId);
202
205
  }
203
206
  (0, ui_1.step)(target.kind === 'app' ? `Building the app from revision ${revision}` : `Deploying the functions from revision ${revision}`);
204
207
  const started = await startBuild(client, base, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
@@ -50,9 +50,8 @@ async function functionsInvoke(args) {
50
50
  body: { method, ...(data !== undefined && method !== 'GET' && method !== 'HEAD' ? { body: data } : {}) },
51
51
  timeoutMs: 15 * 60_000,
52
52
  });
53
- (0, ui_1.note)((0, ui_1.dim)(`${answer.status} in ${answer.elapsedMs} ms`));
54
53
  if (answer.body)
55
- (0, ui_1.out)((0, args_1.flagBool)(args, 'json') ? answer.body : prettyBody(answer.body));
54
+ (0, ui_1.out)(prettyBody(answer.body));
56
55
  if (answer.status >= 400)
57
56
  throw new errors_1.CliError(`The function answered ${answer.status}`, answer.hint);
58
57
  }
package/dist/flags.js CHANGED
@@ -57,7 +57,7 @@ exports.KNOWN = {
57
57
  'mcp install': ['show-token'],
58
58
  functions: ['project'],
59
59
  'functions list': ['project'],
60
- 'functions invoke': ['data', 'data-file', 'json', 'method', 'project'],
60
+ 'functions invoke': ['data', 'data-file', 'method', 'project'],
61
61
  'functions logs': ['limit', 'project'],
62
62
  logs: ['limit', 'project', 'app'],
63
63
  env: ['project'],
@@ -80,7 +80,7 @@ exports.KNOWN = {
80
80
  'db status': ['project'],
81
81
  'db list': [],
82
82
  'db schema': ['project'],
83
- 'db query': ['limit', 'json', 'project'],
83
+ 'db query': ['json', 'project'],
84
84
  'db migrate': ['dry-run', 'allow-destructive'],
85
85
  'db move': ['to', 'confirm', 'project'],
86
86
  storage: ['json', 'project'],
package/dist/help.js CHANGED
@@ -343,8 +343,8 @@ will work.`,
343
343
  xflow db status what is applied, what is waiting, the server
344
344
  xflow db list databases of the projects, the servers of the organization
345
345
  xflow db schema [table] tables of the project, or the columns of one
346
- xflow db query 'select ...' read data (--limit N: 50 by default, 300 at most;
347
- --json prints the values whole)
346
+ xflow db query 'select ...' read data (rows: the query's own limit, 50 without
347
+ one, 300 at most; --json prints the values whole)
348
348
  xflow db query <file>.sql the same, the query read from a file
349
349
  xflow db migrate store the project root, then apply migrations/*.sql
350
350
  xflow db move --to <server> --confirm <project name>
@@ -634,14 +634,13 @@ ${(0, ui_1.bold)("event.xflow.caller === 'platform'")} and the person in ${(0, u
634
634
  --data '{"a":1}' request body (the method becomes POST by default)
635
635
  --data-file body.json the same body from a file
636
636
  --method GET a different method
637
- --json the answer of the function alone, exactly as it came
638
637
 
639
638
  The body has to be JSON, and it is checked before the call goes out: PowerShell
640
639
  strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
641
640
  (${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
642
641
 
643
- Prints the status, the response time and the body; under ${(0, ui_1.bold)('--json')} the body
644
- alone. A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step. A
642
+ Prints the body of the answer and nothing else, so it goes straight into a pipe
643
+ (${(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
645
644
  refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
646
645
  ${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
647
646
  mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
@@ -719,8 +718,8 @@ git's job.
719
718
  --force overwrite the folder completely`,
720
719
  deploy: `${(0, ui_1.bold)('xflow deploy')}: deploy the functions or build an app, then publish
721
720
 
722
- xflow deploy functions -m "…" send the project root, check it, ship the functions
723
- and their callers
721
+ xflow deploy functions -m "…" send the project root, check it, apply the new
722
+ migrations, ship the functions and their callers
724
723
  xflow deploy apps/<folder> -m "…" send one app and build it; inside its folder a plain
725
724
  xflow deploy does the same
726
725
  xflow publish [build] point the app address at the newest build, or a named one
@@ -761,11 +760,13 @@ keys in the code, alert instead of a dialog) is printed as a list and nothing st
761
760
  merely unfinished (the template page title, no icon) goes through and prints a line
762
761
  ${(0, ui_1.bold)('Unfinished:')}. Fix it before telling the person the app is done.
763
762
 
764
- ${(0, ui_1.bold)('deploy functions')} bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
763
+ ${(0, ui_1.bold)('deploy functions')} first applies the new migrations of ${(0, ui_1.bold)('migrations/')}: if one fails or
764
+ would destroy data, nothing ships, and ${(0, ui_1.bold)('xflow db migrate')} is where that gets settled. Then it
765
+ bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
765
766
  code and variables did not change is left alone, and one that fails to ship fails the whole
766
767
  deploy. It then records the callers and prints which functions are open to the whole
767
- internet now. A change that touches data and screens goes in this order: the migration,
768
- the functions, then every app that shows it; an app deploy warns when the root holds
768
+ internet now. A change that touches data and screens goes in this order: the functions
769
+ (with the migration), then every app that shows it; an app deploy warns when the root holds
769
770
  changes nobody deployed.
770
771
 
771
772
  A function gone from the sources would be deleted from the cloud along with its keys and
@@ -775,9 +776,6 @@ to agree. If the sources hold no functions at all while the cloud holds several,
775
776
  is deleted and nothing is asked: that looks like a directory which never made it (a
776
777
  ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
777
778
 
778
- The database is not part of this: migrations change data in ways nothing can undo, so
779
- they stay their own command (${(0, ui_1.bold)('xflow db migrate')}).
780
-
781
779
  A build of an app is not shown to visitors until ${(0, ui_1.bold)('xflow publish')}: publishing points the
782
780
  app address at a build and takes a second, and an older kept build is the rollback. Only
783
781
  the app moves: the functions and the database are one per project and stay as they are.
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.0.8';
5
+ exports.CLI_VERSION = '1.1.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.0.8",
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.1.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
+ }