@getxflow/cli 0.10.5 → 0.11.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/README.md +1 -1
- package/dist/args.js +2 -1
- package/dist/commands/auth.js +6 -0
- package/dist/commands/deploy.js +28 -4
- package/dist/commands/functions.js +6 -2
- package/dist/commands/projects.js +20 -2
- package/dist/commands/skills.js +22 -2
- package/dist/commands/sources.js +3 -1
- package/dist/flags.js +1 -1
- package/dist/help.js +6 -2
- package/dist/limits.js +7 -0
- package/dist/tree.js +16 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +31 -11
package/README.md
CHANGED
package/dist/args.js
CHANGED
|
@@ -27,7 +27,8 @@ function parseArgs(argv) {
|
|
|
27
27
|
const words = [];
|
|
28
28
|
const flags = {};
|
|
29
29
|
for (let i = 0; i < argv.length; i++) {
|
|
30
|
-
|
|
30
|
+
// -m is what git taught everyone to type for a message; honour it as an alias.
|
|
31
|
+
const token = argv[i] === '-m' ? '--message' : argv[i];
|
|
31
32
|
if (token === '--') {
|
|
32
33
|
words.push(...argv.slice(i + 1));
|
|
33
34
|
break;
|
package/dist/commands/auth.js
CHANGED
|
@@ -158,5 +158,11 @@ async function whoami() {
|
|
|
158
158
|
? `yes, no more often than one run every ${interval} min`
|
|
159
159
|
: 'yes'
|
|
160
160
|
: 'no'}`);
|
|
161
|
+
if (typeof me.features.function_timeout_s === 'number') {
|
|
162
|
+
(0, ui_1.out)(`Function time cap: ${me.features.function_timeout_s} s per call, applied on the next build`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof me.features.builds_concurrent === 'number') {
|
|
165
|
+
(0, ui_1.out)(`Concurrent builds: ${me.features.builds_concurrent}`);
|
|
166
|
+
}
|
|
161
167
|
}
|
|
162
168
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -72,12 +72,12 @@ async function refreshFunctionsEnv(root, client, projectId) {
|
|
|
72
72
|
* refuses such a build until the caller says yes, and the question is asked
|
|
73
73
|
* here, before the build starts, rather than reported once it already has.
|
|
74
74
|
*/
|
|
75
|
-
async function startBuild(client, projectId, revision, allowRemovals) {
|
|
75
|
+
async function startBuild(client, projectId, revision, allowRemovals, comment) {
|
|
76
76
|
const path = `/api/v1/projects/${projectId}/builds`;
|
|
77
77
|
try {
|
|
78
78
|
return await (0, api_1.apiJson)(client, path, {
|
|
79
79
|
method: 'POST',
|
|
80
|
-
body: { revision, allow_removals: allowRemovals },
|
|
80
|
+
body: { revision, allow_removals: allowRemovals, comment },
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
catch (e) {
|
|
@@ -104,7 +104,7 @@ async function startBuild(client, projectId, revision, allowRemovals) {
|
|
|
104
104
|
throw new errors_1.CliError('The build did not start', 'Nothing was removed');
|
|
105
105
|
return (0, api_1.apiJson)(client, path, {
|
|
106
106
|
method: 'POST',
|
|
107
|
-
body: { revision, allow_removals: true },
|
|
107
|
+
body: { revision, allow_removals: true, comment },
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -120,7 +120,29 @@ async function startBuild(client, projectId, revision, allowRemovals) {
|
|
|
120
120
|
async function assertBuildAllowed(client, projectId) {
|
|
121
121
|
await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/builds?check=plan`);
|
|
122
122
|
}
|
|
123
|
+
/** Keep in sync with the builds route on the platform. */
|
|
124
|
+
const MAX_COMMENT_LENGTH = 200;
|
|
125
|
+
/** Control and bidi-override characters: they spoof terminals, not comments. */
|
|
126
|
+
const COMMENT_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
127
|
+
/**
|
|
128
|
+
* The version comment, checked before anything goes up: a refusal after the push
|
|
129
|
+
* would leave the project with a revision newer than its build for no good reason.
|
|
130
|
+
*/
|
|
131
|
+
function requireComment(args) {
|
|
132
|
+
const comment = ((0, args_1.flagString)(args, 'message') ?? '')
|
|
133
|
+
.replace(/\s+/g, ' ')
|
|
134
|
+
.replace(COMMENT_CONTROL_CHARS, '')
|
|
135
|
+
.trim();
|
|
136
|
+
if (!comment) {
|
|
137
|
+
throw new errors_1.CliError('Every version needs a comment saying what changed in it', 'Add -m: xflow deploy -m "Cart and card payments". One short line, written in the language the user speaks: it names this version in the history');
|
|
138
|
+
}
|
|
139
|
+
if (comment.length > MAX_COMMENT_LENGTH) {
|
|
140
|
+
throw new errors_1.CliError(`The version comment is too long: ${MAX_COMMENT_LENGTH} characters at most`, 'One short line about what changed in this version is enough');
|
|
141
|
+
}
|
|
142
|
+
return comment;
|
|
143
|
+
}
|
|
123
144
|
async function deploy(args) {
|
|
145
|
+
const comment = requireComment(args);
|
|
124
146
|
const { root, config } = (0, config_1.requireProject)();
|
|
125
147
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
126
148
|
let revision;
|
|
@@ -137,7 +159,7 @@ async function deploy(args) {
|
|
|
137
159
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
138
160
|
}
|
|
139
161
|
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
140
|
-
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'));
|
|
162
|
+
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
|
|
141
163
|
if (started.removed_functions?.length) {
|
|
142
164
|
(0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
|
|
143
165
|
}
|
|
@@ -213,6 +235,8 @@ async function deployments(args) {
|
|
|
213
235
|
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
214
236
|
d.status === 'deployed' ? 'ready' : d.status,
|
|
215
237
|
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
238
|
+
// Plain text only: a padded cell with ANSI codes would skew every column after it.
|
|
239
|
+
d.comment && d.comment.length > 60 ? `${d.comment.slice(0, 59)}…` : (d.comment ?? ''),
|
|
216
240
|
[d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
|
|
217
241
|
]));
|
|
218
242
|
(0, ui_1.note)((0, ui_1.dim)(' Serve the pages of an earlier build: xflow rollback <number>'));
|
|
@@ -67,6 +67,10 @@ async function functionsInvoke(args) {
|
|
|
67
67
|
catch {
|
|
68
68
|
(0, ui_1.note)((0, ui_1.dim)(' Could not get a visitor pass: calling with the project token only'));
|
|
69
69
|
}
|
|
70
|
+
// The cap the deployed version actually carries, not the plan's current one:
|
|
71
|
+
// they diverge until the next build. Null means deployed before the platform
|
|
72
|
+
// recorded caps, that is with 90.
|
|
73
|
+
const capS = fn.execution_timeout_s ?? 90;
|
|
70
74
|
const started = Date.now();
|
|
71
75
|
let response;
|
|
72
76
|
try {
|
|
@@ -78,8 +82,8 @@ async function functionsInvoke(args) {
|
|
|
78
82
|
...(pass ? { 'X-Project-Pass': pass } : {}),
|
|
79
83
|
},
|
|
80
84
|
body: sendsBody ? (data ?? '{}') : undefined,
|
|
81
|
-
// Wait past the function's own
|
|
82
|
-
signal: AbortSignal.timeout(
|
|
85
|
+
// Wait past the function's own cap to see its timeout, not ours.
|
|
86
|
+
signal: AbortSignal.timeout((capS + 10) * 1000),
|
|
83
87
|
});
|
|
84
88
|
}
|
|
85
89
|
catch (e) {
|
|
@@ -100,6 +100,18 @@ async function init(args) {
|
|
|
100
100
|
(0, ui_1.out)(' npm run dev # develop');
|
|
101
101
|
(0, ui_1.out)(' xflow deploy # send the code, build and release');
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Linking is a step, not a task. Without this the agent reports the command as done and
|
|
105
|
+
* goes quiet, leaving the person in front of a folder: it has to read the skill and come
|
|
106
|
+
* back with a question instead.
|
|
107
|
+
*/
|
|
108
|
+
function nextAfterLink(skillReady) {
|
|
109
|
+
(0, ui_1.out)('');
|
|
110
|
+
(0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
|
|
111
|
+
if (skillReady)
|
|
112
|
+
(0, ui_1.out)(` read ${skills_1.SKILL_IN_PROJECT}: how this platform is built and shipped`);
|
|
113
|
+
(0, ui_1.out)(' then ask the user what to build or change, and start there');
|
|
114
|
+
}
|
|
103
115
|
async function link(args) {
|
|
104
116
|
const root = (0, config_1.findProjectRoot)() ?? process.cwd();
|
|
105
117
|
const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
|
|
@@ -148,11 +160,13 @@ async function link(args) {
|
|
|
148
160
|
write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
|
|
149
161
|
(0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));
|
|
150
162
|
}
|
|
151
|
-
(0, skills_1.installSkillQuietly)(dir);
|
|
163
|
+
const skillReady = (0, skills_1.installSkillQuietly)(dir);
|
|
152
164
|
(0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
|
|
153
165
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
|
|
154
|
-
if (!fresh)
|
|
166
|
+
if (!fresh) {
|
|
167
|
+
nextAfterLink(skillReady);
|
|
155
168
|
return;
|
|
169
|
+
}
|
|
156
170
|
// A clone without the code is half a clone, and the folder has to remember
|
|
157
171
|
// which revision it works from: without that the first push has nothing to
|
|
158
172
|
// compare against and stops.
|
|
@@ -164,11 +178,15 @@ async function link(args) {
|
|
|
164
178
|
catch (e) {
|
|
165
179
|
if (e instanceof api_1.ApiError && e.status === 404) {
|
|
166
180
|
(0, ui_1.note)((0, ui_1.dim)(' The project has no sources yet: write the code and run xflow deploy'));
|
|
181
|
+
nextAfterLink(skillReady);
|
|
167
182
|
return;
|
|
168
183
|
}
|
|
184
|
+
// No next step while the code is missing: the pull has to succeed first.
|
|
169
185
|
(0, ui_1.warn)(`The sources did not arrive: ${e instanceof Error ? e.message : e}`);
|
|
170
186
|
(0, ui_1.note)((0, ui_1.dim)(' The folder is linked, fetch them separately: xflow pull'));
|
|
187
|
+
return;
|
|
171
188
|
}
|
|
189
|
+
nextAfterLink(skillReady);
|
|
172
190
|
}
|
|
173
191
|
async function list() {
|
|
174
192
|
const root = (0, config_1.findProjectRoot)();
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SKILL_IN_PROJECT = void 0;
|
|
4
|
+
exports.skillCopyPaths = skillCopyPaths;
|
|
3
5
|
exports.replacePointer = replacePointer;
|
|
4
6
|
exports.copyIsStale = copyIsStale;
|
|
5
7
|
exports.staleSkillCopy = staleSkillCopy;
|
|
@@ -28,13 +30,25 @@ const AGENTS = [
|
|
|
28
30
|
const DEFAULT_PROJECT = ['claude', 'cursor', 'codex'];
|
|
29
31
|
const DEFAULT_GLOBAL = ['claude', 'codex', 'universal'];
|
|
30
32
|
const CURSOR_RULE = ['.cursor', 'rules', 'xflow.mdc'];
|
|
33
|
+
/** Written by every default install, so the pointer and the link nudge can both name it. */
|
|
34
|
+
exports.SKILL_IN_PROJECT = '.agents/skills/xflow/SKILL.md';
|
|
35
|
+
/**
|
|
36
|
+
* Every place inside a project a copy of the skill can live. The archive of sources is
|
|
37
|
+
* built around this list, so a new agent in the table above is kept out of it by adding
|
|
38
|
+
* the agent and nothing else.
|
|
39
|
+
*/
|
|
40
|
+
function skillCopyPaths() {
|
|
41
|
+
const paths = new Set(AGENTS.map((agent) => `${[...agent.project, 'xflow'].join('/')}/`));
|
|
42
|
+
paths.add(CURSOR_RULE.join('/'));
|
|
43
|
+
return [...paths];
|
|
44
|
+
}
|
|
31
45
|
/** AGENTS.md is always read, unlike the lazily loaded skill: a short pointer lives there. */
|
|
32
46
|
const POINTER_MARKER = '<!-- xflow-skill -->';
|
|
33
47
|
const POINTER = `${POINTER_MARKER}
|
|
34
48
|
## XFlow
|
|
35
49
|
|
|
36
50
|
This project is hosted on the XFlow platform and ships through the \`xflow\` CLI, not
|
|
37
|
-
through a git push. Read
|
|
51
|
+
through a git push. Read \`${exports.SKILL_IN_PROJECT}\` before deploying, publishing,
|
|
38
52
|
rolling back, touching the database or migrations, cloud functions, schedules,
|
|
39
53
|
environment variables, file storage and uploads, connected accounts or production logs.
|
|
40
54
|
Command list: \`xflow help\`.
|
|
@@ -242,15 +256,21 @@ function defaultIds(base, global) {
|
|
|
242
256
|
}
|
|
243
257
|
return [...ids];
|
|
244
258
|
}
|
|
245
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Best-effort install from init and link: a failure must not break the linking.
|
|
261
|
+
* Reports whether the skill is in place, so the caller does not send an agent to a
|
|
262
|
+
* file that was never written.
|
|
263
|
+
*/
|
|
246
264
|
function installSkillQuietly(base) {
|
|
247
265
|
try {
|
|
248
266
|
if (install(base, defaultIds(base, false), false).length > 0) {
|
|
249
267
|
(0, ui_1.note)((0, ui_1.dim)(' The platform instructions are laid out for the AI agent'));
|
|
250
268
|
}
|
|
269
|
+
return true;
|
|
251
270
|
}
|
|
252
271
|
catch {
|
|
253
272
|
(0, ui_1.note)((0, ui_1.dim)(' Could not lay out the AI agent instructions: xflow skills'));
|
|
273
|
+
return false;
|
|
254
274
|
}
|
|
255
275
|
}
|
|
256
276
|
function pathLabel(agent, global) {
|
package/dist/commands/sources.js
CHANGED
|
@@ -142,7 +142,9 @@ async function downloadRevision(client, projectId, target, revision) {
|
|
|
142
142
|
(0, ui_1.warn)('The hash of the downloaded tree did not match the server one: the contents may have changed in transit');
|
|
143
143
|
}
|
|
144
144
|
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
145
|
-
|
|
145
|
+
// The hash above covers the archive as the server holds it; what lands on disk is the
|
|
146
|
+
// code, without the skill copies older revisions still carry.
|
|
147
|
+
const files = (0, zip_1.dropDirectoryEntries)(entries).filter((entry) => !(0, tree_1.isSkillCopy)(entry.path));
|
|
146
148
|
for (const entry of files) {
|
|
147
149
|
const path = (0, node_path_1.join)(target, entry.path);
|
|
148
150
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
package/dist/flags.js
CHANGED
|
@@ -73,7 +73,7 @@ const KNOWN = {
|
|
|
73
73
|
'storage remove': ['folder', 'yes', 'project'],
|
|
74
74
|
status: [],
|
|
75
75
|
pull: ['into', 'force', 'revision'],
|
|
76
|
-
deploy: ['no-push', 'force', 'allow-removals'],
|
|
76
|
+
deploy: ['no-push', 'force', 'allow-removals', 'message'],
|
|
77
77
|
publish: ['project'],
|
|
78
78
|
rollback: ['project'],
|
|
79
79
|
deployments: ['project'],
|
package/dist/help.js
CHANGED
|
@@ -57,7 +57,7 @@ ${(0, ui_1.bold)('Files')}
|
|
|
57
57
|
delete a folder with everything in it
|
|
58
58
|
|
|
59
59
|
${(0, ui_1.bold)('Releasing')}
|
|
60
|
-
xflow deploy
|
|
60
|
+
xflow deploy -m "what changed" send the code, ship the functions, build on the platform
|
|
61
61
|
xflow publish show the dev version to visitors
|
|
62
62
|
xflow rollback <version number> serve the pages of an earlier build
|
|
63
63
|
xflow deployments version history
|
|
@@ -253,7 +253,8 @@ ${(0, ui_1.bold)('console.log')} lines of that call) and sends it to the platfor
|
|
|
253
253
|
write nothing, otherwise every request would pay for it in latency.
|
|
254
254
|
|
|
255
255
|
What never reaches this list: a crash while the module is starting (the function
|
|
256
|
-
never gets as far as the wrapper), going over
|
|
256
|
+
never gets as far as the wrapper), going over the execution time cap of the plan
|
|
257
|
+
(${(0, ui_1.bold)('xflow whoami')} names it), and running out of memory.
|
|
257
258
|
Those show up in the answer to ${(0, ui_1.bold)('xflow functions invoke')}.
|
|
258
259
|
|
|
259
260
|
Browser errors are collected by ${(0, ui_1.bold)('src/utils/error-logger.ts')} of the template and
|
|
@@ -467,6 +468,9 @@ Three steps: sending the sources, shipping the cloud functions, building the app
|
|
|
467
468
|
The build command and the output directory come from xflow.json (npm run build and dist
|
|
468
469
|
by default).
|
|
469
470
|
|
|
471
|
+
-m <text> the version comment, required: one short line in the language the
|
|
472
|
+
user speaks, saying what changed. It names the version in the
|
|
473
|
+
history (--message is the long form)
|
|
470
474
|
--no-push do not send sources, build from the latest server revision
|
|
471
475
|
--force allow overwriting the server revision while sending
|
|
472
476
|
--allow-removals agree in advance to remove the functions gone from the sources
|
package/dist/limits.js
CHANGED
|
@@ -5,8 +5,11 @@ exports.limitLine = limitLine;
|
|
|
5
5
|
exports.quotaRows = quotaRows;
|
|
6
6
|
const DENIAL_LABELS = {
|
|
7
7
|
limit_projects: 'projects',
|
|
8
|
+
limit_databases: 'databases',
|
|
9
|
+
limit_connections: 'active connector connections',
|
|
8
10
|
limit_functions: 'cloud functions',
|
|
9
11
|
limit_builds: 'builds this month',
|
|
12
|
+
limit_builds_concurrent: 'builds running at once',
|
|
10
13
|
limit_seats: 'seats in the organization',
|
|
11
14
|
limit_schedule_interval: 'schedule frequency',
|
|
12
15
|
db_write_locked: 'database volume, writes are off',
|
|
@@ -29,6 +32,8 @@ function limitLine(detail) {
|
|
|
29
32
|
}
|
|
30
33
|
const QUOTA_ORDER = [
|
|
31
34
|
'projects',
|
|
35
|
+
'databases',
|
|
36
|
+
'connections',
|
|
32
37
|
'functions',
|
|
33
38
|
'developers',
|
|
34
39
|
'members',
|
|
@@ -39,6 +44,8 @@ const QUOTA_ORDER = [
|
|
|
39
44
|
];
|
|
40
45
|
const QUOTA_LABELS = {
|
|
41
46
|
projects: 'Projects',
|
|
47
|
+
databases: 'Databases',
|
|
48
|
+
connections: 'Active connections',
|
|
42
49
|
functions: 'Cloud functions',
|
|
43
50
|
developers: 'Developers',
|
|
44
51
|
members: 'Staff',
|
package/dist/tree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSkillCopy = isSkillCopy;
|
|
3
4
|
exports.loadIgnoreRules = loadIgnoreRules;
|
|
4
5
|
exports.collectFiles = collectFiles;
|
|
5
6
|
exports.treeHash = treeHash;
|
|
@@ -7,6 +8,7 @@ exports.heaviest = heaviest;
|
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_path_1 = require("node:path");
|
|
11
|
+
const skills_1 = require("./commands/skills");
|
|
10
12
|
/** Never sent. */
|
|
11
13
|
const DEFAULT_IGNORE = [
|
|
12
14
|
'node_modules/',
|
|
@@ -22,6 +24,10 @@ const DEFAULT_IGNORE = [
|
|
|
22
24
|
// Compiler cache: rewritten by every local build, so it would make a new
|
|
23
25
|
// revision out of nothing.
|
|
24
26
|
'tsconfig.tsbuildinfo',
|
|
27
|
+
// The skill ships inside the CLI and is laid out by init and link, so a copy of it
|
|
28
|
+
// is not project code. Sent once, it comes back with every clone and overwrites the
|
|
29
|
+
// fresh copy the CLI has just written with the one deployed back then.
|
|
30
|
+
...(0, skills_1.skillCopyPaths)(),
|
|
25
31
|
];
|
|
26
32
|
const IGNORE_FILE = '.xflowignore';
|
|
27
33
|
function globToRegExpSource(pattern) {
|
|
@@ -58,6 +64,16 @@ function compileRule(raw) {
|
|
|
58
64
|
const suffix = dirOnly ? '/' : '($|/)';
|
|
59
65
|
return { re: new RegExp(`${prefix}${source}${suffix}`) };
|
|
60
66
|
}
|
|
67
|
+
const skillRules = (0, skills_1.skillCopyPaths)()
|
|
68
|
+
.map(compileRule)
|
|
69
|
+
.filter((rule) => rule !== null);
|
|
70
|
+
/**
|
|
71
|
+
* The same paths on the way back: revisions deployed before the rule above still carry a
|
|
72
|
+
* copy, and unpacking it would put a stale skill in front of the agent.
|
|
73
|
+
*/
|
|
74
|
+
function isSkillCopy(path) {
|
|
75
|
+
return skillRules.some((rule) => rule.re.test(path));
|
|
76
|
+
}
|
|
61
77
|
/** Defaults, the .xflowignore file and the ignore field of xflow.json. */
|
|
62
78
|
function loadIgnoreRules(root, extra = []) {
|
|
63
79
|
const lines = [...DEFAULT_IGNORE, ...extra];
|
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.
|
|
5
|
+
exports.CLI_VERSION = '0.11.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
package/skills/xflow/SKILL.md
CHANGED
|
@@ -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.
|
|
27
|
+
These instructions ship with xflow CLI 0.11.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
|
|
|
@@ -65,11 +65,23 @@ the error points away from the cause. The sections below carry the details.
|
|
|
65
65
|
|
|
66
66
|
## Plan limits
|
|
67
67
|
|
|
68
|
-
The organization runs on a plan with finite limits: projects, cloud functions,
|
|
69
|
-
and staff seats, database and file storage,
|
|
70
|
-
|
|
68
|
+
The organization runs on a plan with finite limits: projects, databases, cloud functions,
|
|
69
|
+
active connector connections, developer and staff seats, database and file storage,
|
|
70
|
+
function minutes and builds per month, builds running at once, plus schedules and how
|
|
71
|
+
often they may run. `xflow whoami` prints every one of them next to what is already
|
|
71
72
|
used, and reading it before a long task is cheaper than hitting a wall mid-way.
|
|
72
73
|
|
|
74
|
+
Two of them deserve a word. A database outlives its project (deleting a project only
|
|
75
|
+
detaches it), so a refused database on a small plan usually means an orphan: the fix is
|
|
76
|
+
deleting the unused database in the "Data" section of the web interface, not renaming
|
|
77
|
+
anything. Connector connections are counted while active: switching an unused connection
|
|
78
|
+
off in the web interface frees the seat without deleting it.
|
|
79
|
+
|
|
80
|
+
The plan also caps how long one call of a cloud function may run. The cap is written
|
|
81
|
+
into the function when it is deployed, so a plan change reaches the cloud only with the
|
|
82
|
+
next `xflow deploy` of each project, in both directions. `xflow whoami` names the plan's
|
|
83
|
+
cap; what a deployed function actually carries is what it was last deployed with.
|
|
84
|
+
|
|
73
85
|
The same output names the rights of your own key, which is the other half of the answer:
|
|
74
86
|
destroying data in a migration, deleting files from storage and linking a connected
|
|
75
87
|
account each need a right that is off by default. Reading that line first turns a refusal
|
|
@@ -96,8 +108,10 @@ back within an hour of the data going under the limit.
|
|
|
96
108
|
script, then `npx tsc --noEmit`).
|
|
97
109
|
3. `npm run build` if the change is substantial. The platform builds again in its own
|
|
98
110
|
sandbox, on one Node version for everyone, so this is only a fast way to see errors early.
|
|
99
|
-
4. `xflow deploy` sends the sources, ships the cloud functions and
|
|
100
|
-
on the platform, printing each phase and the six-digit number
|
|
111
|
+
4. `xflow deploy -m "what changed"` sends the sources, ships the cloud functions and
|
|
112
|
+
builds the application on the platform, printing each phase and the six-digit number
|
|
113
|
+
of the version it built. The comment is required: one short line in the language the
|
|
114
|
+
user speaks, it names this version in the history.
|
|
101
115
|
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
102
116
|
browser for them.
|
|
103
117
|
6. `xflow publish` makes that same version visible to visitors.
|
|
@@ -361,7 +375,7 @@ The pieces line up in one pass. From a new function to a verified schedule:
|
|
|
361
375
|
```
|
|
362
376
|
xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
|
|
363
377
|
# write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
|
|
364
|
-
xflow deploy
|
|
378
|
+
xflow deploy -m "Nightly report function" # ships the function, then builds the app
|
|
365
379
|
xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}' # needs a deployed function
|
|
366
380
|
xflow functions invoke report # run it once, the way the app would
|
|
367
381
|
xflow functions logs report # empty output means it never crashed
|
|
@@ -467,7 +481,7 @@ for a visitor who is signed in and has access to this project, the same rule tha
|
|
|
467
481
|
application itself, so it works on your pages and does nothing in an email or on a page
|
|
468
482
|
anyone can open.
|
|
469
483
|
|
|
470
|
-
|
|
484
|
+
Four things bite an upload that otherwise looks right:
|
|
471
485
|
|
|
472
486
|
- **A name already taken in that folder is refused before the link is issued.** Pass
|
|
473
487
|
`overwrite: true` to replace the file: the bytes change and the address stays, which is
|
|
@@ -477,11 +491,17 @@ Three things bite an upload that otherwise looks right:
|
|
|
477
491
|
bytes are already up; the platform then removes the object and your table stays clean.
|
|
478
492
|
- **Confirm only after the PUT has finished.** The platform looks the object up in storage
|
|
479
493
|
and takes its real size and content type from there, not from what you declared, so an
|
|
480
|
-
early `confirm` answers that the file is not there. Send on the PUT the
|
|
481
|
-
named when asking for the link:
|
|
494
|
+
early `confirm` answers that the file is not there. Send on the PUT exactly the
|
|
495
|
+
`Content-Type` you named when asking for the link: the link is signed for that header, a
|
|
496
|
+
different or missing type gets 403, and storage serves the file under it.
|
|
497
|
+
- **Storage takes application assets, not code.** Images, documents, data files, audio,
|
|
498
|
+
video, fonts and archives pass; pages, scripts and executables (`html`, `js`, `exe`, …)
|
|
499
|
+
are refused with `forbidden_type` when the link is requested. Both the extension and the
|
|
500
|
+
declared `content_type` are checked. Application code travels through `xflow deploy`, not
|
|
501
|
+
through storage.
|
|
482
502
|
|
|
483
503
|
A refusal comes back as `{ error, code }`. Branch on `code` (`invalid_name`, `file_too_large`,
|
|
484
|
-
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `not_found`, …) and never on the text:
|
|
504
|
+
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …) and never on the text:
|
|
485
505
|
the wording is free to change, the code is not.
|
|
486
506
|
|
|
487
507
|
What the app may do with files is decided inside that function, because the page in the
|