@getxflow/cli 0.1.10 → 0.2.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 CHANGED
@@ -21,7 +21,7 @@ else's code belongs next to it.
21
21
  | `status` / `push` / `pull` | state, sending and fetching sources |
22
22
  | `deploy` / `publish` / `rollback` / `deployments` | build, publish, roll back, version history |
23
23
  | `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
24
- | `functions list` / `functions deploy` | cloud functions of the project from `functions/<name>/index.ts` |
24
+ | `functions list` | cloud functions of the project from `functions/<name>/index.ts`, shipped by `deploy` |
25
25
  | `functions invoke` / `functions logs` | call a function, look at its crashes with the stack |
26
26
  | `schedules list` / `set` / `rm` | running functions on a timer (timer triggers) |
27
27
  | `env` / `env check` / `env set` | environment variables of the functions, values are never handed back |
package/dist/bin.js CHANGED
@@ -69,9 +69,9 @@ async function run(args) {
69
69
  }
70
70
  throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
71
71
  case 'functions':
72
+ // The command is gone, but silence would leave the habit unexplained.
72
73
  if (second === 'deploy') {
73
- await (0, functions_1.functionsDeploy)(rest);
74
- return;
74
+ throw new errors_1.CliError('Functions are deployed by the build now', 'Run xflow deploy: it ships the functions and then builds the application with their addresses');
75
75
  }
76
76
  if (second === 'invoke') {
77
77
  await (0, functions_1.functionsInvoke)(rest);
@@ -85,7 +85,7 @@ async function run(args) {
85
85
  await (0, functions_1.functionsList)();
86
86
  return;
87
87
  }
88
- throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, deploy, invoke and logs');
88
+ throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, invoke and logs');
89
89
  case 'logs':
90
90
  await (0, logs_1.logs)(rest);
91
91
  return;
@@ -4,11 +4,14 @@ exports.deploy = deploy;
4
4
  exports.publish = publish;
5
5
  exports.rollback = rollback;
6
6
  exports.deployments = deployments;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
7
9
  const api_1 = require("../api");
8
10
  const args_1 = require("../args");
9
11
  const config_1 = require("../config");
10
12
  const errors_1 = require("../errors");
11
13
  const session_1 = require("../session");
14
+ const template_1 = require("../template");
12
15
  const ui_1 = require("../ui");
13
16
  const sources_1 = require("./sources");
14
17
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -40,6 +43,24 @@ async function waitForBuild(client, projectId, started) {
40
43
  }
41
44
  throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
42
45
  }
46
+ /**
47
+ * Write the function addresses into the local .env.
48
+ *
49
+ * The build bakes its own copy of the map into the bundle, this one is for
50
+ * `npm run dev`: without it the local run calls a function that it has no
51
+ * address for.
52
+ */
53
+ async function refreshFunctionsEnv(root, client, projectId) {
54
+ const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
55
+ // No .env at all (fresh clone): recreate it fully, like link does.
56
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
57
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, '.env'), (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions), 'utf-8');
58
+ }
59
+ else {
60
+ (0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
61
+ }
62
+ return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
63
+ }
43
64
  async function deploy(args) {
44
65
  const { root, config } = (0, config_1.requireProject)();
45
66
  const client = (0, session_1.connect)(config);
@@ -68,6 +89,11 @@ async function deploy(args) {
68
89
  reportIssues(e);
69
90
  throw e;
70
91
  }
92
+ // Removing a function cannot be undone: a new one gets a new address. Say it
93
+ // out loud rather than leaving it to be discovered in functions list.
94
+ if (started.removed_functions?.length) {
95
+ (0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
96
+ }
71
97
  const build = await waitForBuild(client, config.projectId, started);
72
98
  if (build.phase === 'failed') {
73
99
  if (build.log_tail) {
@@ -78,6 +104,17 @@ async function deploy(args) {
78
104
  }
79
105
  (0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
80
106
  (0, ui_1.out)(build.project_url);
107
+ // The functions were shipped by the build itself, so their addresses are known
108
+ // only now. A failure here does not undo a finished build: warn and stop there.
109
+ try {
110
+ const functions = await refreshFunctionsEnv(root, client, config.projectId);
111
+ if (functions.length > 0) {
112
+ (0, ui_1.note)((0, ui_1.dim)(` Functions in .env: ${functions.join(', ')}`));
113
+ }
114
+ }
115
+ catch {
116
+ (0, ui_1.warn)('Could not refresh the function addresses in .env, the build itself is fine');
117
+ }
81
118
  (0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
82
119
  }
83
120
  async function publish() {
@@ -126,7 +126,7 @@ async function envSet(args) {
126
126
  // Values reach a function on its next deploy.
127
127
  const users = referencedByFunctions(root).needed.get(name);
128
128
  if (users && users.length > 0) {
129
- (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, redeploy: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
129
+ (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, run xflow deploy: it ships ${users.join(', ')} again`));
130
130
  }
131
131
  }
132
132
  async function envRemove(args) {
@@ -2,68 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.functionsList = functionsList;
4
4
  exports.functionsInvoke = functionsInvoke;
5
- exports.functionsDeploy = functionsDeploy;
6
- const node_fs_1 = require("node:fs");
7
- const node_path_1 = require("node:path");
8
5
  const api_1 = require("../api");
9
6
  const args_1 = require("../args");
10
7
  const config_1 = require("../config");
11
8
  const errors_1 = require("../errors");
12
9
  const session_1 = require("../session");
13
- const template_1 = require("../template");
14
10
  const ui_1 = require("../ui");
15
- const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
16
11
  const FUNCTIONS_DIR = 'functions';
17
- function entryFor(root, name) {
18
- for (const entry of ENTRY_NAMES) {
19
- const candidate = (0, node_path_1.join)(root, FUNCTIONS_DIR, name, entry);
20
- if ((0, node_fs_1.existsSync)(candidate))
21
- return candidate;
22
- }
23
- return null;
24
- }
25
- function discover(root) {
26
- const dir = (0, node_path_1.join)(root, FUNCTIONS_DIR);
27
- if (!(0, node_fs_1.existsSync)(dir))
28
- return [];
29
- return (0, node_fs_1.readdirSync)(dir)
30
- .filter((name) => (0, node_fs_1.statSync)((0, node_path_1.join)(dir, name)).isDirectory())
31
- .filter((name) => entryFor(root, name) !== null)
32
- .sort();
33
- }
34
- /**
35
- * Bundle a function into one file. esbuild comes from the project's node_modules;
36
- * `pg` stays external so the server wrapper can inject the project schema.
37
- */
38
- function bundle(root, entry) {
39
- let esbuild;
40
- try {
41
- esbuild = require(require.resolve('esbuild', { paths: [root] }));
42
- }
43
- catch {
44
- throw new errors_1.CliError('Could not find esbuild in the project', 'Install it: npm i -D esbuild. It usually comes with Vite already');
45
- }
46
- const result = esbuild.buildSync({
47
- entryPoints: [entry],
48
- bundle: true,
49
- platform: 'node',
50
- target: 'node20',
51
- format: 'cjs',
52
- external: ['pg'],
53
- write: false,
54
- logLevel: 'silent',
55
- });
56
- const text = result.outputFiles[0]?.text;
57
- if (!text)
58
- throw new errors_1.CliError(`Building ${entry} produced nothing`);
59
- return text;
60
- }
61
12
  async function functionsList() {
62
13
  const { config } = (0, config_1.requireProject)();
63
14
  const client = (0, session_1.connect)(config);
64
15
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
65
16
  if (data.functions.length === 0) {
66
- (0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow functions deploy`);
17
+ (0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
67
18
  return;
68
19
  }
69
20
  (0, ui_1.table)(data.functions.map((fn) => [
@@ -73,18 +24,6 @@ async function functionsList() {
73
24
  fn.error_message ?? '',
74
25
  ]));
75
26
  }
76
- /** Write function URLs into .env, where the build picks them up. */
77
- async function refreshFunctionsEnv(root, client, projectId) {
78
- const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
79
- // No .env at all (fresh clone): recreate it fully, like link does.
80
- if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
81
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, '.env'), (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions), 'utf-8');
82
- }
83
- else {
84
- (0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
85
- }
86
- return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
87
- }
88
27
  function prettyBody(text) {
89
28
  try {
90
29
  return JSON.stringify(JSON.parse(text), null, 2);
@@ -105,7 +44,7 @@ async function functionsInvoke(args) {
105
44
  if (!fn || !fn.invoke_url) {
106
45
  throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
107
46
  ? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
108
- : `To deploy it: xflow functions deploy ${name}`);
47
+ : 'Functions ship with the build: xflow deploy');
109
48
  }
110
49
  const data = (0, args_1.flagString)(args, 'data');
111
50
  const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
@@ -136,29 +75,3 @@ async function functionsInvoke(args) {
136
75
  throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
137
76
  }
138
77
  }
139
- async function functionsDeploy(args) {
140
- const { root, config } = (0, config_1.requireProject)();
141
- const client = (0, session_1.connect)(config);
142
- const wanted = args.words[1];
143
- const names = wanted ? [wanted] : discover(root);
144
- if (names.length === 0) {
145
- throw new errors_1.CliError(`The project has no functions`, `Create ${FUNCTIONS_DIR}/<name>/index.ts exporting handler and try again`);
146
- }
147
- for (const name of names) {
148
- const entry = entryFor(root, name);
149
- if (!entry) {
150
- throw new errors_1.CliError(`Could not find ${FUNCTIONS_DIR}/${name}/index.ts`, `Available functions: ${discover(root).join(', ') || 'none at all'}`);
151
- }
152
- (0, ui_1.step)(`Building ${name}`);
153
- const code = bundle(root, entry);
154
- (0, ui_1.step)(`Deploying ${name}`);
155
- const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/functions?name=${encodeURIComponent(name)}`, Buffer.from(code, 'utf-8'), { 'Content-Type': 'application/javascript' });
156
- (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} deployed`);
157
- (0, ui_1.out)(result.url);
158
- if (result.secrets.length > 0) {
159
- (0, ui_1.note)((0, ui_1.dim)(` Organization secrets in the environment: ${result.secrets.join(', ')}`));
160
- }
161
- }
162
- const available = await refreshFunctionsEnv(root, client, config.projectId);
163
- (0, ui_1.note)((0, ui_1.dim)(` Addresses in .env updated (${available.join(', ')}). In the frontend: xflow.functions.invoke('${names[0]}')`));
164
- }
package/dist/help.js CHANGED
@@ -26,14 +26,13 @@ ${(0, ui_1.bold)('Code')}
26
26
  fetch the sources (the latest revision by default)
27
27
 
28
28
  ${(0, ui_1.bold)('Releasing')}
29
- xflow deploy [--no-push] send the code, build on the platform, release to dev
29
+ xflow deploy [--no-push] send the code, ship the functions, build on the platform
30
30
  xflow publish show the dev version to visitors
31
31
  xflow rollback <version number> return the project to an earlier version
32
32
  xflow deployments version history
33
33
 
34
34
  ${(0, ui_1.bold)('Functions')}
35
- xflow functions list what is deployed
36
- xflow functions deploy [name] build and ship a function
35
+ xflow functions list what is deployed (they ship with xflow deploy)
37
36
  xflow functions invoke <name> [--data '{"a":1}']
38
37
  call a function and print the answer
39
38
  xflow functions logs [name] function crashes: stack and console output
@@ -125,8 +124,9 @@ mentions by name. A name assembled from an expression (${(0, ui_1.bold)("process
125
124
  never reaches the environment, so read variables literally.
126
125
 
127
126
  The value reaches the function on deploy, not at the moment it is stored: after
128
- ${(0, ui_1.bold)('env set')} you need ${(0, ui_1.bold)('xflow functions deploy')} for the functions involved. The same
129
- after a delete: a function already deployed keeps the old value until its next deploy.`,
127
+ ${(0, ui_1.bold)('env set')} run ${(0, ui_1.bold)('xflow deploy')}. The build ships a function whose code did not
128
+ change but whose variables did, so nothing is left holding an old value. The same after a
129
+ delete: a function already deployed keeps the old value until its next deploy.`,
130
130
  schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
131
131
 
132
132
  A schedule is a Yandex timer trigger: it calls the function itself, with no
@@ -224,8 +224,9 @@ into a non-empty one: the platform cannot merge changes, that is git's job.
224
224
  --force overwrite the folder completely`,
225
225
  deploy: `${(0, ui_1.bold)('xflow deploy')}: build and release
226
226
 
227
- Two steps: sending the sources and building on the platform. The build command and the
228
- output directory come from xflow.json (npm run build and dist by default).
227
+ Three steps: sending the sources, shipping the cloud functions, building the application.
228
+ The build command and the output directory come from xflow.json (npm run build and dist
229
+ by default).
229
230
 
230
231
  --no-push do not send sources, build from the latest server revision
231
232
  --force allow overwriting the server revision while sending
@@ -235,6 +236,21 @@ worked on my machine" no longer depends on your machine. Before the build the pr
235
236
  is checked against the template: mismatches are printed as a list and the build does
236
237
  not start at all.
237
238
 
239
+ Everything in ${(0, ui_1.bold)('functions/<name>/index.ts')} is bundled and shipped by the same run,
240
+ before the application is built. That order is not a convention but the only one that
241
+ works: the addresses of the functions are baked into the bundle, so they have to exist
242
+ first. A function whose code and variables did not change is left alone, and a function
243
+ that fails to ship fails the whole build.
244
+
245
+ A function gone from the sources is deleted from the cloud along with its schedules, and
246
+ the CLI names it before the build starts. That one is final: a function created again
247
+ later gets a different address. If the sources hold no functions at all while the cloud
248
+ holds several, nothing is deleted: that looks like a directory which never made it (a
249
+ ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
250
+
251
+ The database is not part of this: migrations change data in ways nothing can undo, so
252
+ they stay their own command (${(0, ui_1.bold)('xflow db migrate')}).
253
+
238
254
  The built version is visible on the project page, and the CLI prints the link. Visitors
239
255
  see it after ${(0, ui_1.bold)('xflow publish')}. The address of the build itself is not printed:
240
256
  it carries the version number, and after the next publish such a link quietly serves an
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.1.10';
5
+ exports.CLI_VERSION = '0.2.0';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -40,8 +40,8 @@ restored within an hour of the data going back under the limit.
40
40
  2. `npm run typecheck` for a two-second type check (older projects may not have the
41
41
  script, then `npx tsc --noEmit`).
42
42
  3. `npm run build` if the change is substantial, before deploying.
43
- 4. `xflow deploy` sends the sources and builds them on the platform, printing each
44
- phase and the six-digit number of the version it built.
43
+ 4. `xflow deploy` sends the sources, ships the cloud functions and builds the application
44
+ on the platform, printing each phase and the six-digit number of the version it built.
45
45
  5. Give the user the project link the CLI printed and let them look. Do not open a
46
46
  browser for them.
47
47
  6. `xflow publish` makes that same version visible to visitors.
@@ -105,11 +105,16 @@ Interface rules, checked outside `src/components/ui` and `src/components/blocks`
105
105
 
106
106
  ## Cloud functions
107
107
 
108
- Server-side code lives in `functions/<name>/index.ts` and exports `handler`. Deploy it
109
- with `xflow functions deploy` (one name to deploy a single function, no name for all),
110
- list what is live with `xflow functions list`. The handler returns
108
+ Server-side code lives in `functions/<name>/index.ts` and exports `handler`. There is no
109
+ separate deploy command: `xflow deploy` ships the functions and then builds the application,
110
+ in that order. List what is live with `xflow functions list`. The handler returns
111
111
  `{ statusCode, body }` where `body` is a JSON string.
112
112
 
113
+ The sources are the whole truth about which functions exist. Delete the directory and the
114
+ next deploy deletes the function from the cloud, schedules included, and that cannot be
115
+ undone: a function created again later gets a different address. So never remove a function
116
+ directory to "clean up" unless the user asked for the function to go.
117
+
113
118
  Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
114
119
  the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
115
120
  and `xflow functions logs <name>` shows the failures, each with its stack and the console
@@ -118,9 +123,9 @@ never crashed, not that logging is broken.
118
123
 
119
124
  From the app, call a function through `src/lib/xflow.ts`:
120
125
  `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the project
121
- token for you. Addresses are baked into the build: the CLI writes them to `.env` when
122
- you deploy a function, so a frontend built before the function existed cannot see it,
123
- and needs `xflow deploy` again.
126
+ token for you. Addresses are baked into the build, which is why the functions go out first:
127
+ by the time the bundle is built they already exist, and a new function is never missing
128
+ from the application that calls it.
124
129
 
125
130
  Treat a function as a public API: the token ships inside the frontend bundle, so anyone
126
131
  who opens the app can call it.
@@ -131,8 +136,8 @@ functions read but the platform does not have. Values never come back out — th
131
136
  they exist is inside the running function.
132
137
 
133
138
  A function receives only the variables it mentions by name via `process.env.NAME`, so never
134
- assemble a variable name from an expression. New values arrive on the next
135
- `xflow functions deploy`, not at the moment they are written.
139
+ assemble a variable name from an expression. New values arrive on the next `xflow deploy`,
140
+ not at the moment they are written.
136
141
 
137
142
  To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
138
143
  Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
@@ -184,12 +189,12 @@ read-only queries, migrations, function logs and invocations, schedules, environ
184
189
  variables, versions, publish and rollback. They answer with aggregates and say explicitly
185
190
  when a result is truncated, which parsing terminal output does not.
186
191
 
187
- Code never travels through those tools. Sending sources and deploying functions stay in
188
- the CLI (`xflow push`, `xflow functions deploy`): pulling a repository through tool calls
189
- burns the user's tokens for nothing. Building is available through the tools, because it
190
- runs from the revision already stored on the server: `deployments action=build` starts it
191
- and answers immediately, `action=status` reports the phase. A build takes minutes, so
192
- never expect the starting call to return a finished version.
192
+ Code never travels through those tools. Sending sources stays in the CLI (`xflow push`):
193
+ pulling a repository through tool calls burns the user's tokens for nothing. Building is
194
+ available through the tools, because it runs from the revision already stored on the
195
+ server: `deployments action=build` starts it and answers immediately, `action=status`
196
+ reports the phase. It ships the functions too, from that same revision. A build takes
197
+ minutes, so never expect the starting call to return a finished version.
193
198
 
194
199
  ## Do not
195
200