@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.
@@ -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 template_1 = require("../template");
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: a function created again later gets a different address, schedules included';
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 no field,
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, projectId, started) {
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, `/api/v1/projects/${projectId}/builds/${started.deploy_id}`);
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. Version ${started.deploy_id}`);
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
- * Point the local .env at this project, function addresses included.
53
- *
54
- * The build bakes its own copy of the map into the bundle, this one is for
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 refreshPlatformEnv(root, client, projectId) {
59
- const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
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. The platform
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 build would remove them: ${removing.join(', ')}`);
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 build', hint: removing.join(', ') },
76
+ { label: 'Yes, remove and deploy', hint: removing.join(', ') },
98
77
  ]);
99
78
  if (answer !== 1)
100
- throw new errors_1.CliError('The build did not start', 'Nothing was removed');
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
- * Everything after the build has started: the same for a build of the sources
147
- * just sent and for a rebuild of a revision the platform already holds.
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
- async function finishBuild(client, projectId, started, root) {
153
- if (started.removed_functions?.length) {
154
- (0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
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 (started.revoked_connections?.length) {
157
- (0, ui_1.warn)(`Access revoked by the provider: ${started.revoked_connections.join(', ')}`);
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
- if (started.shadowed_functions?.length) {
161
- (0, ui_1.warn)(`Named like a standalone function of the organization: ${started.shadowed_functions.join(', ')}`);
162
- (0, ui_1.note)((0, ui_1.dim)(' The app calls its own function under that name. Rename one of them to call the shared one'));
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
- const build = await waitForBuild(client, projectId, started);
165
- if (build.phase === 'failed') {
166
- if (build.log_tail) {
167
- (0, ui_1.out)();
168
- (0, ui_1.out)(build.log_tail);
169
- }
170
- throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: xflow deploy. Version ${build.deploy_id}`);
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
- (0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
173
- (0, ui_1.out)(build.project_url);
174
- // The functions were shipped by the build itself, so their addresses are known
175
- // only now. A failure here does not undo a finished build: warn and stop there.
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
- // A project without a site has no pages to publish, and its functions went live
186
- // with this very build. Naming publish there sends the reader to a refusal.
187
- const hasSite = started.has_site !== false;
188
- if (functions === null) {
189
- (0, ui_1.note)((0, ui_1.dim)(' The cloud functions of this build are already live'));
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
- else if (functions.length > 0) {
194
- (0, ui_1.note)((0, ui_1.dim)(` Cloud functions live now: ${functions.map((fn) => fn.name).join(', ')}`));
195
- if (hasSite)
196
- (0, ui_1.note)((0, ui_1.dim)(' There is one copy per project, publishing does not hold them back'));
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
- // An open function is called by its address and by nothing else: a page holds it
199
- // in its own files. Printing it here saves the xflow functions list that every
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
- * Build a revision the platform already holds: this is how a version comes back.
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
- * The revision named by --revision, with the two rules that guard such a build.
230
- *
231
- * Kept pure and read before anything else: a wrong number or a flag that cannot
232
- * apply is worth saying before the version comment is asked for, and a rule like
233
- * this deserves a test that needs no platform to talk to.
234
- */
235
- function statedRevision(revision, force) {
236
- if (revision === undefined)
237
- return undefined;
238
- // flagNumber already refuses anything but a whole number, so zero is what is
239
- // left to catch here: revisions are counted from one.
240
- if (revision < 1) {
241
- throw new errors_1.CliError(`--revision takes a revision number, got "${revision}"`, 'The revision of each version: xflow deployments');
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
- if (force) {
244
- throw new errors_1.CliError('--force does not apply to --revision', 'Nothing is sent: the build runs the sources the platform already holds');
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
- return revision;
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
- if (stated !== undefined)
252
- return deployRevision(args, stated, comment);
253
- if ((0, args_1.flagString)(args, 'project') !== undefined) {
254
- throw new errors_1.CliError('xflow deploy sends the sources next to it, so it runs in the project folder', 'Name a project by id only to build a revision it already has: xflow deploy --project <id> --revision <n> -m "what came back"');
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
- const { root, config } = (0, config_1.requireProject)();
257
- const client = await (0, session_1.connectProject)(config.projectId, root, config);
258
- let revision;
259
- if ((0, args_1.flagBool)(args, 'no-push')) {
260
- const server = await (0, sources_1.latestRevision)(client, config.projectId);
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, ui_1.step)(`Building on the platform from revision ${revision}`);
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 { projectId, client } = await (0, session_1.projectTarget)(args);
277
- (0, ui_1.step)('Publishing the current dev version');
278
- const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
279
- if (result.already_published) {
280
- (0, ui_1.ok)('This version is already published');
281
- }
282
- else {
283
- (0, ui_1.ok)(`Version ${result.deploy_id} published: visitors see its pages now`);
284
- }
285
- (0, ui_1.out)(result.project_url);
286
- (0, ui_1.note)((0, ui_1.dim)(' Publishing moves the pages only. Cloud functions are one per project: visitors'));
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
- const deployId = args.words[0];
291
- if (!deployId) {
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
- const { projectId, client } = await (0, session_1.projectTarget)(args);
295
- const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
296
- (0, ui_1.ok)(`The project now serves the pages of build ${result.deploy_id}`);
297
- (0, ui_1.out)(result.project_url);
298
- (0, ui_1.note)((0, ui_1.dim)(' Only the pages came back. Cloud functions and the database are one per project:'));
299
- (0, ui_1.note)((0, ui_1.dim)(' they are not versioned and stay as they are now'));
300
- if (result.revision !== null) {
301
- (0, ui_1.note)((0, ui_1.dim)(` The code of this build: xflow pull --revision ${result.revision} --into ../v${result.deploy_id}`));
302
- (0, ui_1.note)((0, ui_1.dim)(' Building that code over the current one is a separate step: it overwrites the server revision'));
303
- }
304
- (0, ui_1.note)((0, ui_1.dim)(' Visitors see these pages only after xflow publish'));
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 { projectId, client } = await (0, session_1.projectTarget)(args);
308
- const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/deployments`);
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 versions yet. To build and release: xflow deploy');
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
- // Plain text only: a padded cell with ANSI codes would skew every column after it.
319
- d.comment && d.comment.length > 60 ? `${d.comment.slice(0, 59)}…` : (d.comment ?? ''),
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)(' Serve the pages of an earlier build: xflow rollback <number>'));
332
- (0, ui_1.note)((0, ui_1.dim)(' Pages live for the three latest builds; "no pages" marks the rest'));
333
- (0, ui_1.note)((0, ui_1.dim)(' Fetch the code of a build: xflow pull --revision <its revision> --into ../copy'));
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
  }
@@ -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, handed the key this CLI picks for the project. The template
14
- * proxy trades it for a pass on every function call, so which organization holds the
15
- * project is decided here, by the rule every other command follows.
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, and the environment beats
26
- // .env: pinned here, the key goes only to the platform that issued it.
27
- VITE_XFLOW_API_URL: (0, template_1.envApiUrl)(client.apiUrl),
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
- // .env only moves on link and deploy: a function someone else shipped, or a new
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
- // of the project and answers this machine alone. By number, not by name: proxies of
43
- // older templates cut the Host at ':' and so never match [::1].
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
  }
@@ -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 their next deploy'));
198
+ (0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the value until xflow deploy functions'));
196
199
  }