@getxflow/cli 1.0.9 → 1.1.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/dist/commands/db.js +23 -6
- package/dist/commands/deploy.js +4 -1
- package/dist/commands/functions.js +1 -1
- package/dist/commands/pages.js +0 -1
- package/dist/flags.js +1 -1
- package/dist/help.js +20 -22
- package/dist/template.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/skills/xflow/SKILL.md +728 -728
package/dist/commands/db.js
CHANGED
|
@@ -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
|
|
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.
|
|
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:
|
|
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/${
|
|
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:
|
|
371
|
+
allow_destructive: allowDestructive,
|
|
355
372
|
},
|
|
356
373
|
timeoutMs: 300_000,
|
|
357
374
|
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
|
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);
|
|
@@ -24,7 +24,7 @@ async function functionsList(args) {
|
|
|
24
24
|
fn.last_called_at ? `called ${(0, ui_1.formatAge)(fn.last_called_at)}` : 'never called',
|
|
25
25
|
fn.last_error_at ? `failed ${(0, ui_1.formatAge)(fn.last_error_at)}` : '',
|
|
26
26
|
]));
|
|
27
|
-
(0, ui_1.note)((0, ui_1.dim)(' callers: the app folders
|
|
27
|
+
(0, ui_1.note)((0, ui_1.dim)(' callers: the app folders allowed to call it (functions in the root xflow.json)'));
|
|
28
28
|
(0, ui_1.note)((0, ui_1.dim)(' open: a caller is public, so anyone on the internet can call it; no callers: a workflow'));
|
|
29
29
|
(0, ui_1.note)((0, ui_1.dim)(` Webhooks and other functions call ${functions[0].url.replace(/\/[^/]+$/, '/<name>')} with a key`));
|
|
30
30
|
}
|
package/dist/commands/pages.js
CHANGED
|
@@ -99,7 +99,6 @@ async function pagesCreate(args) {
|
|
|
99
99
|
(0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
100
100
|
(0, ui_1.out)(result.page.url);
|
|
101
101
|
(0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The next version: xflow pages deploy ${result.page.slug} <path>`));
|
|
102
|
-
(0, ui_1.note)((0, ui_1.dim)(` A backend for it: name "${result.page.slug}" in functions.<name>.callers of a project, xflow deploy functions, then fetch /_fn/<name>`));
|
|
103
102
|
}
|
|
104
103
|
function contentTypeOf(upload) {
|
|
105
104
|
return upload.kind === 'zip' ? 'application/zip' : 'text/html';
|
package/dist/flags.js
CHANGED
|
@@ -80,7 +80,7 @@ exports.KNOWN = {
|
|
|
80
80
|
'db status': ['project'],
|
|
81
81
|
'db list': [],
|
|
82
82
|
'db schema': ['project'],
|
|
83
|
-
'db query': ['
|
|
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
|
@@ -301,11 +301,10 @@ build it and upload what the build writes. Dot entries are skipped. At most 5 MB
|
|
|
301
301
|
unpacked and 1000 files.
|
|
302
302
|
|
|
303
303
|
The page is public, so a file holding a secret key is refused whole: every visitor would read
|
|
304
|
-
it. A page
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
so do not promise SEO.
|
|
304
|
+
it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
|
|
305
|
+
key a page may hold is a function key, and it opens that function to every visitor: an HTML
|
|
306
|
+
form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
|
|
307
|
+
(no CORS). Search engines are asked not to index pages, so do not promise SEO.
|
|
309
308
|
|
|
310
309
|
${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
|
|
311
310
|
is no undo and no copy on the platform side. The name stays reserved for the organization, so
|
|
@@ -343,8 +342,8 @@ will work.`,
|
|
|
343
342
|
xflow db status what is applied, what is waiting, the server
|
|
344
343
|
xflow db list databases of the projects, the servers of the organization
|
|
345
344
|
xflow db schema [table] tables of the project, or the columns of one
|
|
346
|
-
xflow db query 'select ...' read data (
|
|
347
|
-
--json prints the values whole)
|
|
345
|
+
xflow db query 'select ...' read data (rows: the query's own limit, 50 without
|
|
346
|
+
one, 300 at most; --json prints the values whole)
|
|
348
347
|
xflow db query <file>.sql the same, the query read from a file
|
|
349
348
|
xflow db migrate store the project root, then apply migrations/*.sql
|
|
350
349
|
xflow db move --to <server> --confirm <project name>
|
|
@@ -618,14 +617,14 @@ of the project runs the new code the moment it finishes; there is no publish ste
|
|
|
618
617
|
|
|
619
618
|
Who may call a function is its callers, in the root xflow.json:
|
|
620
619
|
|
|
621
|
-
{ "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"
|
|
620
|
+
{ "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
|
|
622
621
|
|
|
623
|
-
A caller is an app folder of this project (apps/zapis is "zapis")
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
622
|
+
A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
|
|
623
|
+
nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
|
|
624
|
+
holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
|
|
625
|
+
anyone with the link opens, so anyone on the internet can call it. The address in the note
|
|
626
|
+
under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
|
|
627
|
+
their own address.
|
|
629
628
|
|
|
630
629
|
${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
|
|
631
630
|
the function a one-minute ticket and calls it itself, so the function sees
|
|
@@ -718,8 +717,8 @@ git's job.
|
|
|
718
717
|
--force overwrite the folder completely`,
|
|
719
718
|
deploy: `${(0, ui_1.bold)('xflow deploy')}: deploy the functions or build an app, then publish
|
|
720
719
|
|
|
721
|
-
xflow deploy functions -m "…" send the project root, check it,
|
|
722
|
-
and their callers
|
|
720
|
+
xflow deploy functions -m "…" send the project root, check it, apply the new
|
|
721
|
+
migrations, ship the functions and their callers
|
|
723
722
|
xflow deploy apps/<folder> -m "…" send one app and build it; inside its folder a plain
|
|
724
723
|
xflow deploy does the same
|
|
725
724
|
xflow publish [build] point the app address at the newest build, or a named one
|
|
@@ -760,11 +759,13 @@ keys in the code, alert instead of a dialog) is printed as a list and nothing st
|
|
|
760
759
|
merely unfinished (the template page title, no icon) goes through and prints a line
|
|
761
760
|
${(0, ui_1.bold)('Unfinished:')}. Fix it before telling the person the app is done.
|
|
762
761
|
|
|
763
|
-
${(0, ui_1.bold)('deploy functions')}
|
|
762
|
+
${(0, ui_1.bold)('deploy functions')} first applies the new migrations of ${(0, ui_1.bold)('migrations/')}: if one fails or
|
|
763
|
+
would destroy data, nothing ships, and ${(0, ui_1.bold)('xflow db migrate')} is where that gets settled. Then it
|
|
764
|
+
bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
|
|
764
765
|
code and variables did not change is left alone, and one that fails to ship fails the whole
|
|
765
766
|
deploy. It then records the callers and prints which functions are open to the whole
|
|
766
|
-
internet now. A change that touches data and screens goes in this order: the
|
|
767
|
-
the
|
|
767
|
+
internet now. A change that touches data and screens goes in this order: the functions
|
|
768
|
+
(with the migration), then every app that shows it; an app deploy warns when the root holds
|
|
768
769
|
changes nobody deployed.
|
|
769
770
|
|
|
770
771
|
A function gone from the sources would be deleted from the cloud along with its keys and
|
|
@@ -774,9 +775,6 @@ to agree. If the sources hold no functions at all while the cloud holds several,
|
|
|
774
775
|
is deleted and nothing is asked: that looks like a directory which never made it (a
|
|
775
776
|
${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
|
|
776
777
|
|
|
777
|
-
The database is not part of this: migrations change data in ways nothing can undo, so
|
|
778
|
-
they stay their own command (${(0, ui_1.bold)('xflow db migrate')}).
|
|
779
|
-
|
|
780
778
|
A build of an app is not shown to visitors until ${(0, ui_1.bold)('xflow publish')}: publishing points the
|
|
781
779
|
app address at a build and takes a second, and an older kept build is the rollback. Only
|
|
782
780
|
the app moves: the functions and the database are one per project and stay as they are.
|
package/dist/template.js
CHANGED
|
@@ -13,7 +13,7 @@ const README = `# %NAME%
|
|
|
13
13
|
A project on XFlow: one database, cloud functions and the apps that use them.
|
|
14
14
|
|
|
15
15
|
\`\`\`
|
|
16
|
-
xflow.json projectId, and which apps
|
|
16
|
+
xflow.json projectId, and which apps may call each function
|
|
17
17
|
package.json dependencies of the functions
|
|
18
18
|
functions/<name>/ cloud functions, shared by every app
|
|
19
19
|
migrations/ the database of the project
|
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.
|
|
5
|
+
exports.CLI_VERSION = '1.1.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,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getxflow/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=20"
|
|
8
|
-
},
|
|
9
|
-
"bin": {
|
|
10
|
-
"xflow": "dist/bin.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"skills"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@getxflow/cli",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"xflow": "dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|