@getxflow/cli 0.34.1 → 1.0.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/api.js +21 -6
- package/dist/args.js +1 -1
- package/dist/bin.js +15 -3
- package/dist/commands/connections.js +4 -4
- package/dist/commands/db.js +27 -64
- package/dist/commands/deploy.js +170 -216
- package/dist/commands/dev.js +15 -29
- package/dist/commands/env.js +6 -3
- package/dist/commands/functions.js +27 -119
- package/dist/commands/logs.js +19 -15
- package/dist/commands/pages.js +1 -0
- package/dist/commands/projects.js +134 -190
- package/dist/commands/sources.js +218 -120
- package/dist/commands/storage.js +2 -2
- package/dist/config.js +82 -26
- package/dist/flags.js +19 -12
- package/dist/help.js +219 -216
- package/dist/limits.js +6 -6
- package/dist/session.js +167 -52
- package/dist/template.js +29 -125
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/xflow/SKILL.md +682 -739
package/dist/commands/deploy.js
CHANGED
|
@@ -12,14 +12,14 @@ const config_1 = require("../config");
|
|
|
12
12
|
const errors_1 = require("../errors");
|
|
13
13
|
const prompt_1 = require("../prompt");
|
|
14
14
|
const session_1 = require("../session");
|
|
15
|
-
const
|
|
15
|
+
const tree_1 = require("../tree");
|
|
16
16
|
const ui_1 = require("../ui");
|
|
17
17
|
const sources_1 = require("./sources");
|
|
18
18
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
19
|
/** Slightly longer than the platform's own build timeout. */
|
|
20
20
|
const WAIT_LIMIT_MS = 16 * 60_000;
|
|
21
21
|
/** Said the same way with a terminal and without one. */
|
|
22
|
-
const REMOVAL_IS_FINAL = 'Removal is final:
|
|
22
|
+
const REMOVAL_IS_FINAL = 'Removal is final: the keys and schedules of those functions go with them';
|
|
23
23
|
/** Print every check violation at once. */
|
|
24
24
|
function reportIssues(e) {
|
|
25
25
|
const issues = e.issues ?? [];
|
|
@@ -28,17 +28,16 @@ function reportIssues(e) {
|
|
|
28
28
|
(0, ui_1.out)(` ${(0, ui_1.bold)(issue.code)} ${where}`);
|
|
29
29
|
(0, ui_1.note)((0, ui_1.dim)(` ${issue.message}`));
|
|
30
30
|
}
|
|
31
|
-
// Not the platform hint: it names its own JSON field, and a terminal has
|
|
32
|
-
// it has the list printed right above.
|
|
31
|
+
// Not the platform hint: it names its own JSON field, and a terminal has the list printed above.
|
|
33
32
|
throw new errors_1.CliError(e.message, 'Fix what is listed above and retry: the build did not start');
|
|
34
33
|
}
|
|
35
34
|
/** Poll the build until it finishes, printing phases. */
|
|
36
|
-
async function waitForBuild(client,
|
|
35
|
+
async function waitForBuild(client, base, started) {
|
|
37
36
|
const deadline = Date.now() + WAIT_LIMIT_MS;
|
|
38
37
|
let shown = '';
|
|
39
38
|
while (Date.now() < deadline) {
|
|
40
39
|
await sleep(started.poll_after_ms || 3_000);
|
|
41
|
-
const build = await (0, api_1.apiJson)(client,
|
|
40
|
+
const build = await (0, api_1.apiJson)(client, `${base}/builds/${started.deploy_id}`);
|
|
42
41
|
if (build.phase !== shown) {
|
|
43
42
|
shown = build.phase;
|
|
44
43
|
(0, ui_1.step)(build.phase_title);
|
|
@@ -46,35 +45,17 @@ async function waitForBuild(client, projectId, started) {
|
|
|
46
45
|
if (build.phase === 'done' || build.phase === 'failed')
|
|
47
46
|
return build;
|
|
48
47
|
}
|
|
49
|
-
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments.
|
|
48
|
+
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Build ${started.deploy_id}`);
|
|
50
49
|
}
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `npm run dev`: without it the local run calls a function that it has no
|
|
56
|
-
* address for.
|
|
51
|
+
* Start the build, asking before anything is destroyed. Removing a cloud function
|
|
52
|
+
* cannot be undone, so the platform refuses such a deploy until the caller says yes,
|
|
53
|
+
* and the question is asked here, before it starts.
|
|
57
54
|
*/
|
|
58
|
-
async function
|
|
59
|
-
const
|
|
60
|
-
(0, template_1.writePlatformEnv)(root, card.project_token, client.apiUrl, card.functions, card.function_map);
|
|
61
|
-
return card.functions.filter((fn) => fn.invoke_url);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Start the build, asking before anything is destroyed.
|
|
65
|
-
*
|
|
66
|
-
* Removing a cloud function cannot be undone: one created again later gets a
|
|
67
|
-
* different address, and its schedules go with it. The platform therefore
|
|
68
|
-
* refuses such a build until the caller says yes, and the question is asked
|
|
69
|
-
* here, before the build starts, rather than reported once it already has.
|
|
70
|
-
*/
|
|
71
|
-
async function startBuild(client, projectId, revision, allowRemovals, comment) {
|
|
72
|
-
const path = `/api/v1/projects/${projectId}/builds`;
|
|
55
|
+
async function startBuild(client, base, revision, allowRemovals, comment) {
|
|
56
|
+
const path = `${base}/builds`;
|
|
73
57
|
try {
|
|
74
|
-
return await (0, api_1.apiJson)(client, path, {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
body: { revision, allow_removals: allowRemovals, comment },
|
|
77
|
-
});
|
|
58
|
+
return await (0, api_1.apiJson)(client, path, { method: 'POST', body: { revision, allow_removals: allowRemovals, comment } });
|
|
78
59
|
}
|
|
79
60
|
catch (e) {
|
|
80
61
|
if (e instanceof api_1.ApiError && e.issues?.length)
|
|
@@ -84,251 +65,224 @@ async function startBuild(client, projectId, revision, allowRemovals, comment) {
|
|
|
84
65
|
const removing = e.removing ?? [];
|
|
85
66
|
if (removing.length === 0)
|
|
86
67
|
throw e;
|
|
87
|
-
// No terminal (CI, an agent): the flag is the only way to say yes.
|
|
88
|
-
// hint names the API field, so translate it into the flag this CLI accepts.
|
|
68
|
+
// No terminal (CI, an agent): the flag is the only way to say yes.
|
|
89
69
|
if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) {
|
|
90
|
-
throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. `
|
|
91
|
-
'If it is not, bring the functions/<name>/index.ts directories back');
|
|
70
|
+
throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. If it is not, bring the functions/<name>/index.ts directories back`);
|
|
92
71
|
}
|
|
93
|
-
(0, ui_1.warn)(`Gone from the sources, the
|
|
72
|
+
(0, ui_1.warn)(`Gone from the sources, the deploy would remove them: ${removing.join(', ')}`);
|
|
94
73
|
(0, ui_1.note)((0, ui_1.dim)(` ${REMOVAL_IS_FINAL}`));
|
|
95
74
|
const answer = await (0, prompt_1.select)('Remove them from the cloud?', [
|
|
96
75
|
{ label: 'No, stop here', hint: 'bring the functions/<name>/index.ts directories back' },
|
|
97
|
-
{ label: 'Yes, remove and
|
|
76
|
+
{ label: 'Yes, remove and deploy', hint: removing.join(', ') },
|
|
98
77
|
]);
|
|
99
78
|
if (answer !== 1)
|
|
100
|
-
throw new errors_1.CliError('The
|
|
101
|
-
return (0, api_1.apiJson)(client, path, {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
body: { revision, allow_removals: true, comment },
|
|
104
|
-
});
|
|
79
|
+
throw new errors_1.CliError('The deploy did not start', 'Nothing was removed');
|
|
80
|
+
return (0, api_1.apiJson)(client, path, { method: 'POST', body: { revision, allow_removals: true, comment } });
|
|
105
81
|
}
|
|
106
82
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Ask whether the plan allows a build before the sources go up.
|
|
109
|
-
*
|
|
110
|
-
* The gate sits on the build itself, so a refusal used to arrive after the archive
|
|
111
|
-
* had already become a revision, leaving the project looking like a forgotten
|
|
112
|
-
* deploy. Same gate, same wording, one round trip earlier. A platform older than
|
|
113
|
-
* this CLI answers the plain build state instead, and the deploy carries on as
|
|
114
|
-
* before: the check is an improvement, not a requirement.
|
|
115
|
-
*/
|
|
116
|
-
async function assertBuildAllowed(client, projectId) {
|
|
117
|
-
await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/builds?check=plan`);
|
|
118
|
-
}
|
|
119
|
-
/** Keep in sync with the builds route on the platform. */
|
|
83
|
+
/** Keep in sync with lib/version-comment.ts on the platform. */
|
|
120
84
|
const MAX_COMMENT_LENGTH = 200;
|
|
121
85
|
/** Control and bidi-override characters: they spoof terminals, not comments. */
|
|
122
86
|
const COMMENT_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
123
87
|
/** The version comment as typed after -m, cleaned; empty when there is none. Pages take the same. */
|
|
124
88
|
function versionComment(raw) {
|
|
125
|
-
const comment = (raw ?? '')
|
|
126
|
-
.replace(/\s+/g, ' ')
|
|
127
|
-
.replace(COMMENT_CONTROL_CHARS, '')
|
|
128
|
-
.trim();
|
|
89
|
+
const comment = (raw ?? '').replace(/\s+/g, ' ').replace(COMMENT_CONTROL_CHARS, '').trim();
|
|
129
90
|
if (comment.length > MAX_COMMENT_LENGTH) {
|
|
130
91
|
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');
|
|
131
92
|
}
|
|
132
93
|
return comment;
|
|
133
94
|
}
|
|
134
|
-
/**
|
|
135
|
-
* The version comment, checked before anything goes up: a refusal after the push
|
|
136
|
-
* would leave the project with a revision newer than its build for no good reason.
|
|
137
|
-
*/
|
|
95
|
+
/** Checked before anything goes up: a refusal after the push would leave a revision newer than its build. */
|
|
138
96
|
function requireComment(args) {
|
|
139
97
|
const comment = versionComment((0, args_1.flagString)(args, 'message'));
|
|
140
98
|
if (!comment) {
|
|
141
|
-
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');
|
|
99
|
+
throw new errors_1.CliError('Every version needs a comment saying what changed in it', 'Add -m: xflow deploy apps/shop -m "Cart and card payments". One short line, written in the language the user speaks: it names this version in the history');
|
|
142
100
|
}
|
|
143
101
|
return comment;
|
|
144
102
|
}
|
|
145
103
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* `root` is the folder to point at this project afterwards, and null when there
|
|
150
|
-
* is none: a build asked for by id has no .env to refresh.
|
|
104
|
+
* The revision named by --revision. Pure and read first: a wrong number or a flag
|
|
105
|
+
* that cannot apply is worth saying before the version comment is asked for.
|
|
151
106
|
*/
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
107
|
+
function statedRevision(revision, force) {
|
|
108
|
+
if (revision === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (revision < 1) {
|
|
111
|
+
throw new errors_1.CliError(`--revision takes a revision number, got "${revision}"`, 'The revision of each build: xflow deployments');
|
|
155
112
|
}
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
(0, ui_1.note)((0, ui_1.dim)(' Reconnect the account in the platform settings: another build will not fix this'));
|
|
113
|
+
if (force) {
|
|
114
|
+
throw new errors_1.CliError('--force does not apply to --revision', 'Nothing is sent: the build runs the sources the platform already holds');
|
|
159
115
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
116
|
+
return revision;
|
|
117
|
+
}
|
|
118
|
+
/** The lines the functions deploy writes about what the internet can reach now. */
|
|
119
|
+
function openLines(log) {
|
|
120
|
+
return (log ?? '').split('\n').filter((line) => /^(Open to the whole internet|Newly open)/.test(line));
|
|
121
|
+
}
|
|
122
|
+
function printNotes(started) {
|
|
123
|
+
for (const item of started.notes ?? []) {
|
|
124
|
+
(0, ui_1.note)(` Unfinished: ${item.message}${item.file ? (0, ui_1.dim)(` (${item.file})`) : ''}`);
|
|
163
125
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
126
|
+
}
|
|
127
|
+
async function finishFunctions(client, base, started) {
|
|
128
|
+
printNotes(started);
|
|
129
|
+
const build = await waitForBuild(client, base, started);
|
|
130
|
+
if (build.phase === 'failed')
|
|
131
|
+
failed(build, 'xflow deploy functions');
|
|
132
|
+
(0, ui_1.ok)(`Functions deployed from revision ${build.revision} in ${build.elapsed_s} s`);
|
|
133
|
+
for (const line of openLines(build.log_tail))
|
|
134
|
+
(0, ui_1.warn)(line);
|
|
135
|
+
(0, ui_1.note)((0, ui_1.dim)(' Every app of the project calls the new code already: there is no publish step for functions'));
|
|
136
|
+
}
|
|
137
|
+
async function finishApp(client, base, started) {
|
|
138
|
+
printNotes(started);
|
|
139
|
+
const build = await waitForBuild(client, base, started);
|
|
140
|
+
if (build.phase === 'failed')
|
|
141
|
+
failed(build, 'xflow deploy');
|
|
142
|
+
(0, ui_1.ok)(`Build ${build.deploy_id} from revision ${build.revision} in ${build.elapsed_s} s`);
|
|
143
|
+
const card = await (0, api_1.apiJson)(client, base).catch(() => null);
|
|
144
|
+
if (card?.live?.deploy_id === build.deploy_id) {
|
|
145
|
+
(0, ui_1.ok)('Published: visitors see it now');
|
|
146
|
+
(0, ui_1.out)(card.url);
|
|
171
147
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
let functions = null;
|
|
177
|
-
if (root !== null) {
|
|
178
|
-
try {
|
|
179
|
-
functions = await refreshPlatformEnv(root, client, projectId);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
(0, ui_1.warn)('Could not refresh the function addresses in .env, the build itself is fine');
|
|
183
|
-
}
|
|
148
|
+
else {
|
|
149
|
+
if (card)
|
|
150
|
+
(0, ui_1.out)(card.url);
|
|
151
|
+
(0, ui_1.note)((0, ui_1.dim)(' Visitors still see the previous build. Show this one: xflow publish'));
|
|
184
152
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
(0, ui_1.
|
|
190
|
-
if (hasSite)
|
|
191
|
-
(0, ui_1.note)((0, ui_1.dim)(' Publishing does not hold them back'));
|
|
153
|
+
}
|
|
154
|
+
function failed(build, retry) {
|
|
155
|
+
if (build.log_tail) {
|
|
156
|
+
(0, ui_1.out)();
|
|
157
|
+
(0, ui_1.out)(build.log_tail);
|
|
192
158
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
159
|
+
throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: ${retry}. Build ${build.deploy_id}`);
|
|
160
|
+
}
|
|
161
|
+
/** An app deploy with root changes nobody deployed: the app may call code that is not in the cloud yet. */
|
|
162
|
+
async function warnUndeployedRoot(client, projectRoot, projectId) {
|
|
163
|
+
try {
|
|
164
|
+
const tree = (0, sources_1.prepareTree)({ kind: 'project', root: projectRoot, projectId });
|
|
165
|
+
const server = await (0, sources_1.latestRevision)(client, `/api/v1/projects/${projectId}`);
|
|
166
|
+
const relation = server ? (0, tree_1.syncState)(tree.hash, server.tree_hash, (0, config_1.readState)(projectRoot).treeHash) : 'local-changes';
|
|
167
|
+
if (relation !== 'matches') {
|
|
168
|
+
(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
|
+
}
|
|
197
171
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// page needed anyway, and says out loud which functions this build left open.
|
|
201
|
-
const open = functions?.filter((fn) => fn.is_public) ?? [];
|
|
202
|
-
if (open.length > 0) {
|
|
203
|
-
(0, ui_1.note)((0, ui_1.dim)(' Open to anyone, no pass and no key, called at this address:'));
|
|
204
|
-
for (const fn of open)
|
|
205
|
-
(0, ui_1.note)((0, ui_1.dim)(` ${fn.name} ${fn.invoke_url}`));
|
|
172
|
+
catch {
|
|
173
|
+
// Only a hint: a root that cannot be read must not stop the app deploy.
|
|
206
174
|
}
|
|
207
|
-
if (hasSite)
|
|
208
|
-
(0, ui_1.note)((0, ui_1.dim)(' Show the new pages to visitors: xflow publish'));
|
|
209
175
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
* The sources of revision N are on the server, so nothing goes up and no copy of
|
|
214
|
-
* them is needed next to the caller. That also means `--force` has nothing to
|
|
215
|
-
* force: the revision on the server stays the newest one, and this build simply
|
|
216
|
-
* runs an older one. The project can therefore be named by id, which is the only
|
|
217
|
-
* way to bring back the version of a standalone function: its folder may live on
|
|
218
|
-
* another machine entirely, and there is nothing else to roll back with, because
|
|
219
|
-
* a project without a site has no pages to return.
|
|
220
|
-
*/
|
|
221
|
-
async function deployRevision(args, revision, comment) {
|
|
222
|
-
const { projectId, client, root } = await (0, session_1.projectTarget)(args);
|
|
223
|
-
await assertBuildAllowed(client, projectId);
|
|
224
|
-
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
225
|
-
const started = await startBuild(client, projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
|
|
226
|
-
await finishBuild(client, projectId, started, root);
|
|
176
|
+
function targetsHint(projectRoot) {
|
|
177
|
+
const apps = (0, config_1.localApps)(projectRoot).map((app) => `xflow deploy ${config_1.APPS_DIR}/${app.folder}`);
|
|
178
|
+
return ['xflow deploy functions', ...apps].join(', ');
|
|
227
179
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
180
|
+
async function deployTarget(target, args, comment, revision) {
|
|
181
|
+
const base = (0, sources_1.streamPath)(target);
|
|
182
|
+
const { client } = target;
|
|
183
|
+
if (revision === undefined) {
|
|
184
|
+
if (!target.root)
|
|
185
|
+
throw new errors_1.CliError('Nothing to send without a folder', 'Build a revision the platform holds: --revision <n>');
|
|
186
|
+
await (0, api_1.apiJson)(client, `${base}/builds?check=plan`);
|
|
187
|
+
if (target.kind === 'app' && target.projectRoot)
|
|
188
|
+
await warnUndeployedRoot(client, target.projectRoot, target.projectId);
|
|
189
|
+
if ((0, args_1.flagBool)(args, 'no-push')) {
|
|
190
|
+
const server = await (0, sources_1.latestRevision)(client, base);
|
|
191
|
+
if (!server)
|
|
192
|
+
throw new errors_1.CliError('The server holds no sources', 'Repeat without --no-push, so that the sources go up first');
|
|
193
|
+
revision = server.revision;
|
|
194
|
+
(0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const stream = target.kind === 'app'
|
|
198
|
+
? { kind: 'app', root: target.root, appId: target.appId }
|
|
199
|
+
: { kind: 'project', root: target.root, projectId: target.projectId };
|
|
200
|
+
revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
201
|
+
}
|
|
242
202
|
}
|
|
243
|
-
|
|
244
|
-
|
|
203
|
+
(0, ui_1.step)(target.kind === 'app' ? `Building the app from revision ${revision}` : `Deploying the functions from revision ${revision}`);
|
|
204
|
+
const started = await startBuild(client, base, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
|
|
205
|
+
if (started.removed_functions?.length)
|
|
206
|
+
(0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
|
|
207
|
+
if (started.revoked_connections?.length) {
|
|
208
|
+
(0, ui_1.warn)(`Access revoked by the provider: ${started.revoked_connections.join(', ')}`);
|
|
209
|
+
(0, ui_1.note)((0, ui_1.dim)(' Reconnect the account in the platform settings: another deploy will not fix this'));
|
|
245
210
|
}
|
|
246
|
-
|
|
211
|
+
if (target.kind === 'app')
|
|
212
|
+
await finishApp(client, base, started);
|
|
213
|
+
else
|
|
214
|
+
await finishFunctions(client, base, started);
|
|
247
215
|
}
|
|
248
216
|
async function deploy(args) {
|
|
249
217
|
const stated = statedRevision((0, args_1.flagNumber)(args, 'revision'), (0, args_1.flagBool)(args, 'force'));
|
|
250
218
|
const comment = requireComment(args);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
throw new errors_1.CliError('xflow deploy sends the sources next to it, so it runs in the project folder', 'Name
|
|
219
|
+
const word = args.words[0];
|
|
220
|
+
const byId = (0, args_1.flagString)(args, 'project') !== undefined || (0, args_1.flagString)(args, 'app') !== undefined;
|
|
221
|
+
if (byId && stated === undefined) {
|
|
222
|
+
throw new errors_1.CliError('xflow deploy sends the sources next to it, so it runs in the project folder', 'Name one by id only to build a revision it already has: --project <id> --revision <n> (functions) or --app <id> --revision <n>');
|
|
255
223
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (!server) {
|
|
262
|
-
throw new errors_1.CliError('The server holds no sources', 'Repeat without --no-push, so that the sources go up first');
|
|
224
|
+
// In the project root a bare deploy would hide what changes for every app: name the target.
|
|
225
|
+
if (!byId && word === undefined) {
|
|
226
|
+
const { root, appRoot } = (0, config_1.requireProject)();
|
|
227
|
+
if (!appRoot) {
|
|
228
|
+
throw new errors_1.CliError('Name what to deploy: the functions or one app', `From here: ${targetsHint(root)}`);
|
|
263
229
|
}
|
|
264
|
-
revision = server.revision;
|
|
265
|
-
(0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
await assertBuildAllowed(client, config.projectId);
|
|
269
|
-
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
270
230
|
}
|
|
271
|
-
(0,
|
|
272
|
-
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
|
|
273
|
-
await finishBuild(client, config.projectId, started, root);
|
|
231
|
+
await deployTarget(await (0, session_1.streamTarget)(args, word, false), args, comment, stated);
|
|
274
232
|
}
|
|
275
233
|
async function publish(args) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
else
|
|
283
|
-
(0, ui_1.ok)(`
|
|
284
|
-
|
|
285
|
-
(0, ui_1.
|
|
286
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
287
|
-
(0, ui_1.note)((0, ui_1.dim)(' have been running the code of the last xflow deploy since it finished'));
|
|
234
|
+
const target = await (0, session_1.appTarget)(args);
|
|
235
|
+
const deployId = args.words[0];
|
|
236
|
+
(0, ui_1.step)(deployId ? `Publishing build ${deployId}` : 'Publishing the newest build');
|
|
237
|
+
const result = await (0, api_1.apiJson)(target.client, `/api/v1/apps/${target.appId}/publish`, { method: 'POST', body: deployId ? { deploy_id: deployId } : {} });
|
|
238
|
+
if (result.already_live)
|
|
239
|
+
(0, ui_1.ok)(`Build ${result.deploy_id} is already live`);
|
|
240
|
+
else
|
|
241
|
+
(0, ui_1.ok)(`Build ${result.deploy_id} published: visitors see it now`);
|
|
242
|
+
(0, ui_1.out)(result.url);
|
|
243
|
+
(0, ui_1.note)((0, ui_1.dim)(' Only the app moved. Functions and the database are one per project: to bring back the'));
|
|
244
|
+
(0, ui_1.note)((0, ui_1.dim)(' functions of an earlier revision, xflow deploy functions --revision <n> (xflow deployments functions)'));
|
|
288
245
|
}
|
|
246
|
+
/** The same pointer move as publish, with the number required. */
|
|
289
247
|
async function rollback(args) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
|
|
248
|
+
if (!args.words[0]) {
|
|
249
|
+
throw new errors_1.CliError('A build number is required', 'The builds: xflow deployments. For example: xflow rollback 481203');
|
|
293
250
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
(0, ui_1.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
(0, ui_1.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
251
|
+
await publish(args);
|
|
252
|
+
}
|
|
253
|
+
function functionsTable(rows) {
|
|
254
|
+
(0, ui_1.table)(rows.map((d) => [
|
|
255
|
+
d.deploy_id ?? '-',
|
|
256
|
+
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
257
|
+
d.status === 'deployed' ? 'done' : d.status,
|
|
258
|
+
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
259
|
+
d.kind === 'wrapper' ? 'platform: wrapper update' : shortComment(d.comment),
|
|
260
|
+
]));
|
|
261
|
+
(0, ui_1.note)((0, ui_1.dim)(' Bring the functions of a revision back: xflow deploy functions --revision <n> -m "why"'));
|
|
262
|
+
}
|
|
263
|
+
function shortComment(comment) {
|
|
264
|
+
// Plain text only: a padded cell with ANSI codes would skew every column after it.
|
|
265
|
+
return comment && comment.length > 60 ? `${comment.slice(0, 59)}…` : (comment ?? '');
|
|
305
266
|
}
|
|
306
267
|
async function deployments(args) {
|
|
307
|
-
const
|
|
308
|
-
const
|
|
268
|
+
const word = args.words[0];
|
|
269
|
+
const target = await (0, session_1.streamTarget)(args, word, word !== 'functions');
|
|
270
|
+
const data = await (0, api_1.apiJson)(target.client, `${(0, sources_1.streamPath)(target)}/deployments`);
|
|
309
271
|
if (data.deployments.length === 0) {
|
|
310
|
-
(0, ui_1.note)('No
|
|
272
|
+
(0, ui_1.note)(target.kind === 'app' ? 'No builds yet: xflow deploy' : 'No functions deploys yet: xflow deploy functions');
|
|
311
273
|
return;
|
|
312
274
|
}
|
|
275
|
+
if (target.kind === 'project')
|
|
276
|
+
return functionsTable(data.deployments);
|
|
313
277
|
(0, ui_1.table)(data.deployments.map((d) => [
|
|
314
278
|
d.deploy_id ?? '-',
|
|
315
279
|
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
316
280
|
d.status === 'deployed' ? 'ready' : d.status,
|
|
317
281
|
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
318
|
-
|
|
319
|
-
d.
|
|
320
|
-
// pages_kept === false marks a build the storage cleanup has stripped:
|
|
321
|
-
// rollback refuses it, so saying so here saves the agent a call. Older
|
|
322
|
-
// platforms omit the field, and then the column stays as it was.
|
|
323
|
-
[
|
|
324
|
-
d.is_dev ? (0, ui_1.bold)('dev') : '',
|
|
325
|
-
d.is_live ? (0, ui_1.bold)('live') : '',
|
|
326
|
-
d.pages_kept === false ? 'no pages' : '',
|
|
327
|
-
]
|
|
328
|
-
.filter(Boolean)
|
|
329
|
-
.join(' '),
|
|
282
|
+
shortComment(d.comment),
|
|
283
|
+
[d.is_live ? (0, ui_1.bold)('live') : '', d.status === 'deployed' && !d.pages_kept ? 'no pages' : ''].filter(Boolean).join(' '),
|
|
330
284
|
]));
|
|
331
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
332
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
333
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
285
|
+
(0, ui_1.note)((0, ui_1.dim)(' Point the address at another build (that is the rollback): xflow publish <number>'));
|
|
286
|
+
(0, ui_1.note)((0, ui_1.dim)(' The files live for the three latest builds; "no pages" marks the rest'));
|
|
287
|
+
(0, ui_1.note)((0, ui_1.dim)(' The code of a build: xflow pull --revision <its revision> --into ../copy'));
|
|
334
288
|
}
|
package/dist/commands/dev.js
CHANGED
|
@@ -2,54 +2,40 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dev = dev;
|
|
4
4
|
const node_os_1 = require("node:os");
|
|
5
|
-
const api_1 = require("../api");
|
|
6
5
|
const config_1 = require("../config");
|
|
7
6
|
const errors_1 = require("../errors");
|
|
8
7
|
const session_1 = require("../session");
|
|
9
8
|
const spawn_1 = require("../spawn");
|
|
10
|
-
const template_1 = require("../template");
|
|
11
|
-
const ui_1 = require("../ui");
|
|
12
9
|
/**
|
|
13
|
-
* The local dev server
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* The local dev server of one app. The template proxy sends every /_fn/<name> call
|
|
11
|
+
* to the platform as this app with the key picked here, so the callers and access
|
|
12
|
+
* are the same as in production and nothing about functions lives on this machine.
|
|
16
13
|
*
|
|
17
14
|
* Answers with the exit code of the server.
|
|
18
15
|
*/
|
|
19
16
|
async function dev() {
|
|
20
|
-
const { root, config } = (0, config_1.requireProject)();
|
|
17
|
+
const { root, config, appRoot } = (0, config_1.requireProject)();
|
|
18
|
+
if (!appRoot) {
|
|
19
|
+
throw new errors_1.CliError('xflow dev runs one app: stand in its folder', 'For example: cd apps/<folder> && xflow dev');
|
|
20
|
+
}
|
|
21
|
+
const { appId } = (0, config_1.readAppConfig)(appRoot);
|
|
21
22
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
22
23
|
const env = {
|
|
23
24
|
...process.env,
|
|
24
25
|
XFLOW_TOKEN: client.token,
|
|
25
|
-
// The proxy sends the key to this address unchecked,
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// The proxy sends the key to this address unchecked: pinned here, it goes only
|
|
27
|
+
// to the platform that issued it.
|
|
28
|
+
XFLOW_API_URL: client.apiUrl,
|
|
29
|
+
XFLOW_APP_ID: appId,
|
|
28
30
|
};
|
|
29
|
-
//
|
|
30
|
-
// project token, would reach this run only after one of them.
|
|
31
|
-
try {
|
|
32
|
-
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
|
|
33
|
-
Object.assign(env, (0, template_1.platformEnv)(card.project_token, client.apiUrl, card.functions, card.function_map));
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
(0, ui_1.warn)(`The function addresses were not fetched (${e instanceof Error ? e.message : String(e)}): the ones in .env are used`);
|
|
37
|
-
}
|
|
38
|
-
// Ctrl+C reaches the server on its own; staying alive until it is gone is what
|
|
39
|
-
// hands its exit code back instead of leaving it to print over the prompt.
|
|
31
|
+
// Ctrl+C reaches the server on its own; staying alive until it is gone hands its exit code back.
|
|
40
32
|
process.on('SIGINT', () => { });
|
|
41
33
|
// Appended last, so it beats the --host of the dev script: the server holds the key
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
const result = (0, spawn_1.run)('npm', ['run', 'dev', '--', '--host', '127.0.0.1'], {
|
|
45
|
-
stdio: 'inherit',
|
|
46
|
-
cwd: root,
|
|
47
|
-
env,
|
|
48
|
-
});
|
|
34
|
+
// and answers this machine alone.
|
|
35
|
+
const result = (0, spawn_1.run)('npm', ['run', 'dev', '--', '--host', '127.0.0.1'], { stdio: 'inherit', cwd: appRoot, env });
|
|
49
36
|
if (result.error) {
|
|
50
37
|
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');
|
|
51
38
|
}
|
|
52
|
-
// Killed by a signal: the code a shell would report for it.
|
|
53
39
|
const signal = result.signal ? node_os_1.constants.signals[result.signal] : 0;
|
|
54
40
|
return result.status ?? 128 + signal;
|
|
55
41
|
}
|
package/dist/commands/env.js
CHANGED
|
@@ -32,11 +32,14 @@ const OPAQUE_REFERENCE = /process\.env\b(?!\.[A-Z0-9_]+|\[['"][A-Z0-9_]+['"]\])/
|
|
|
32
32
|
/** Set by the platform itself. */
|
|
33
33
|
const PROVIDED = new Set([
|
|
34
34
|
'XFLOW_PROJECT_ID',
|
|
35
|
-
'XFLOW_PROJECT_TOKEN',
|
|
36
35
|
'XFLOW_PROJECT_SCHEMA',
|
|
36
|
+
'XFLOW_FUNCTION_ID',
|
|
37
37
|
'XFLOW_FUNCTION_NAME',
|
|
38
|
+
'XFLOW_FN_URL',
|
|
38
39
|
'XFLOW_API_URL',
|
|
39
40
|
'XFLOW_SERVER_KEY',
|
|
41
|
+
'XFLOW_SCHEDULE_SECRET',
|
|
42
|
+
'XFLOW_AUTH_SECRET',
|
|
40
43
|
'DATABASE_URL',
|
|
41
44
|
'NODE_ENV',
|
|
42
45
|
]);
|
|
@@ -178,7 +181,7 @@ async function envSet(args) {
|
|
|
178
181
|
// Values reach a function on its next deploy.
|
|
179
182
|
const users = referencedByFunctions(root).needed.get(name);
|
|
180
183
|
if (users && users.length > 0) {
|
|
181
|
-
(0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, run xflow deploy: it ships ${users.join(', ')} again`));
|
|
184
|
+
(0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, run xflow deploy functions: it ships ${users.join(', ')} again`));
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
async function envRemove(args) {
|
|
@@ -192,5 +195,5 @@ async function envRemove(args) {
|
|
|
192
195
|
return;
|
|
193
196
|
}
|
|
194
197
|
(0, ui_1.ok)(`${name} deleted`);
|
|
195
|
-
(0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the value until
|
|
198
|
+
(0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the value until xflow deploy functions'));
|
|
196
199
|
}
|