@getxflow/cli 0.34.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/limits.js CHANGED
@@ -4,8 +4,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.limitLine = limitLine;
5
5
  exports.quotaRows = quotaRows;
6
6
  const DENIAL_LABELS = {
7
- limit_projects: 'projects',
8
- limit_databases: 'databases',
7
+ limit_projects: 'projects, the platform cap',
8
+ limit_apps: 'apps',
9
+ plan_app_access: 'access levels of apps',
10
+ functions_stopped: 'function minutes, the functions of the organization are stopped',
9
11
  limit_connections: 'active connector connections',
10
12
  limit_pages: 'public pages',
11
13
  limit_functions: 'cloud functions',
@@ -32,8 +34,7 @@ function limitLine(detail) {
32
34
  return `Plan limit: ${label}${numbers}. Repeating the same command will not help.`;
33
35
  }
34
36
  const QUOTA_ORDER = [
35
- 'projects',
36
- 'databases',
37
+ 'apps',
37
38
  'connections',
38
39
  'pages',
39
40
  'functions',
@@ -45,8 +46,7 @@ const QUOTA_ORDER = [
45
46
  'builds',
46
47
  ];
47
48
  const QUOTA_LABELS = {
48
- projects: 'Projects',
49
- databases: 'Databases',
49
+ apps: 'Apps',
50
50
  connections: 'Active connections',
51
51
  pages: 'Public pages',
52
52
  functions: 'Cloud functions',
package/dist/session.js CHANGED
@@ -6,6 +6,13 @@ exports.findProjectOrg = findProjectOrg;
6
6
  exports.connectProject = connectProject;
7
7
  exports.isProjectId = isProjectId;
8
8
  exports.projectTarget = projectTarget;
9
+ exports.parseTargetWord = parseTargetWord;
10
+ exports.onlyApp = onlyApp;
11
+ exports.folderTarget = folderTarget;
12
+ exports.appTarget = appTarget;
13
+ exports.streamTarget = streamTarget;
14
+ const node_fs_1 = require("node:fs");
15
+ const node_path_1 = require("node:path");
9
16
  const api_1 = require("./api");
10
17
  const args_1 = require("./args");
11
18
  const config_1 = require("./config");
@@ -15,9 +22,8 @@ const ui_1 = require("./ui");
15
22
  /**
16
23
  * Platform connection: address plus key.
17
24
  *
18
- * `ignoreFolder` is what --project needs: the folder around it belongs to another
19
- * project, so its organization must not decide the key. Taking it would hand the
20
- * flag the key of the wrong organization, and the answer would be a bare 404.
25
+ * `ignoreFolder` is what --project and --app need: the folder around it belongs to
26
+ * another project, so its organization must not decide the key.
21
27
  */
22
28
  function connect(config, options) {
23
29
  const apiUrl = (0, config_1.apiUrlFor)(config);
@@ -31,11 +37,7 @@ function connect(config, options) {
31
37
  ...(0, credentials_1.storedKeyInputs)(apiUrl),
32
38
  });
33
39
  if (!picked) {
34
- throw new errors_1.CliError(`No access key for ${apiUrl}`,
35
- // Who does it, not where the key comes from: the key is issued by the sign-in
36
- // itself and by nothing else, so an agent reading this has to hand the step over
37
- // instead of hunting for a value to paste.
38
- 'Sign in: xflow login (needs a person with a browser). In CI pass the key in XFLOW_TOKEN');
40
+ throw new errors_1.CliError(`No access key for ${apiUrl}`, 'Sign in: xflow login (needs a person with a browser). In CI pass the key in XFLOW_TOKEN');
39
41
  }
40
42
  if ('missing' in picked) {
41
43
  throw new errors_1.CliError(`This folder is bound to the organization ${picked.organizationId}, and there is no stored key for it`, 'Sign in to that organization: xflow login. The stored ones: xflow org');
@@ -47,16 +49,15 @@ function anonymous(config) {
47
49
  return { apiUrl: (0, config_1.apiUrlFor)(config), token: '' };
48
50
  }
49
51
  /**
50
- * Which signed-in organization serves the project. Project ids are unique
51
- * across the platform, so a 404 means "not this one" and exactly one
52
- * organization can answer. A dead key must not end the search: the answer may
53
- * sit behind the next one.
52
+ * Which signed-in organization answers this path. Ids are unique across the
53
+ * platform, so a 404 means "not this one" and exactly one organization can answer.
54
+ * A dead key must not end the search: the answer may sit behind the next one.
54
55
  */
55
- async function findProjectOrg(apiUrl, projectId) {
56
+ async function findOrgFor(apiUrl, path) {
56
57
  const dead = [];
57
58
  for (const candidate of (0, credentials_1.listOrgs)(apiUrl)) {
58
59
  try {
59
- await (0, api_1.apiJson)({ apiUrl, token: candidate.token }, `/api/v1/projects/${projectId}`);
60
+ await (0, api_1.apiJson)({ apiUrl, token: candidate.token }, path);
60
61
  return { hit: candidate, dead };
61
62
  }
62
63
  catch (e) {
@@ -71,28 +72,26 @@ async function findProjectOrg(apiUrl, projectId) {
71
72
  }
72
73
  return { hit: null, dead };
73
74
  }
75
+ function findProjectOrg(apiUrl, projectId) {
76
+ return findOrgFor(apiUrl, `/api/v1/projects/${projectId}`);
77
+ }
74
78
  /**
75
- * Connection for project commands. A folder not yet bound to an organization
76
- * gets probed once: the organization that serves the project is found and
77
- * written into .xflow/state.json, so every later command resolves the key
78
- * synchronously. With one stored key there is nothing to probe.
79
- *
80
- * Without a folder (--project) there is nowhere to write the answer, so the
81
- * probe repeats on every command. Kept that way on purpose: a second store of
82
- * "which organization holds what" would be a second truth to keep in step, and
83
- * the price is paid only by whoever did not switch the active organization.
79
+ * Connection for a project or an app. A folder not yet bound to an organization
80
+ * gets probed once and the answer written into .xflow/state.json of the project
81
+ * root. Named by id there is nowhere to write it, so the probe repeats: a second
82
+ * store of "which organization holds what" would be a second truth to keep true.
84
83
  */
85
- async function connectProject(projectId, root, config) {
84
+ async function connectFor(path, what, root, config) {
86
85
  const session = connect(config, { ignoreFolder: root === null });
87
86
  if (session.source !== 'active')
88
87
  return session;
89
88
  const orgs = (0, credentials_1.listOrgs)(session.apiUrl);
90
89
  if (orgs.length <= 1)
91
90
  return session;
92
- const { hit, dead } = await findProjectOrg(session.apiUrl, projectId);
91
+ const { hit, dead } = await findOrgFor(session.apiUrl, path);
93
92
  if (!hit) {
94
- throw new errors_1.CliError(`No signed-in organization holds the project ${projectId}`, dead.length > 0
95
- ? `The key of ${dead.map((d) => d.name ?? d.organizationId).join(', ')} is not working (sign in again: xflow login); the project may live there`
93
+ throw new errors_1.CliError(`No signed-in organization holds the ${what}`, dead.length > 0
94
+ ? `The key of ${dead.map((d) => d.name ?? d.organizationId).join(', ')} is not working (sign in again: xflow login); it may live there`
96
95
  : root === null
97
96
  ? 'Check the id, or sign in to the organization that owns it: xflow login'
98
97
  : 'Check projectId in xflow.json, or sign in to the organization that owns it: xflow login');
@@ -103,35 +102,31 @@ async function connectProject(projectId, root, config) {
103
102
  (0, ui_1.note)((0, ui_1.dim)(` The project belongs to "${hit.name ?? hit.organizationId}", not to the active organization: the folder is bound to it`));
104
103
  }
105
104
  }
106
- return {
107
- apiUrl: session.apiUrl,
108
- token: hit.token,
109
- source: root === null ? 'probe' : 'folder',
110
- organizationId: hit.organizationId,
111
- };
105
+ return { apiUrl: session.apiUrl, token: hit.token, source: root === null ? 'probe' : 'folder', organizationId: hit.organizationId };
106
+ }
107
+ function connectProject(projectId, root, config) {
108
+ return connectFor(`/api/v1/projects/${projectId}`, `project ${projectId}`, root, config);
112
109
  }
113
110
  /**
114
- * A project id, and nothing that could be mistaken for one. A name passed where
115
- * the id belongs would answer a bare 404, and that reads as "no such project"
111
+ * A project or app id, and nothing that could be mistaken for one. A name passed
112
+ * where the id belongs would answer a bare 404, which reads as "no such project"
116
113
  * rather than "wrong argument".
117
114
  */
118
115
  function isProjectId(value) {
119
116
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
120
117
  }
118
+ /** Echoed on every human run: an id typed by hand answers 404 on a read and works on a write. */
119
+ function echoNamed(client, line) {
120
+ const org = (0, credentials_1.listOrgs)(client.apiUrl).find((entry) => entry.organizationId === client.organizationId);
121
+ (0, ui_1.note)((0, ui_1.dim)(` ${line}${org ? ` of "${org.name ?? org.organizationId}"` : ''}`));
122
+ }
121
123
  /**
122
- * The project a command works on: the --project flag, or the folder it runs in.
124
+ * The project a command works on: the --project flag, or the project root of the
125
+ * folder it runs in (any folder of the project, apps included).
123
126
  *
124
127
  * The flag ignores the folder whole, its xflow.json included, so the address is
125
- * the default one or XFLOW_API_URL and nothing else: a config lying around must
126
- * not be able to redirect a call made by id.
127
- *
128
- * Either way the key comes from connectProject: an unbound folder gets the
129
- * organization of its project looked up once and written down, an id gets it
130
- * looked up on every call.
131
- *
132
- * `root` is that folder, and null when the id was named: a command that reads the
133
- * sources for a check of its own then knows the check is not available rather than
134
- * running it against whatever folder the caller happened to stand in.
128
+ * the default one or XFLOW_API_URL: a config lying around must not redirect a call
129
+ * made by id. `root` is null then.
135
130
  */
136
131
  async function projectTarget(args) {
137
132
  const named = (0, args_1.flagString)(args, 'project');
@@ -140,15 +135,135 @@ async function projectTarget(args) {
140
135
  throw new errors_1.CliError(`--project takes a project id, got "${named}"`, 'The ids: xflow projects list');
141
136
  }
142
137
  const client = await connectProject(named, null);
143
- // Echoed back on every human run: the flag is the only place the id is typed by
144
- // hand, and a wrong one answers 404 on a read and works on a write. The
145
- // organization is named too, since it may not be the active one. Under --json
146
- // the line is silenced with the rest, where the payload is the whole answer.
147
- const org = (0, credentials_1.listOrgs)(client.apiUrl).find((entry) => entry.organizationId === client.organizationId);
148
- (0, ui_1.note)((0, ui_1.dim)(` Project ${named}${org ? ` of "${org.name ?? org.organizationId}"` : ''} (--project)`));
138
+ echoNamed(client, `Project ${named} (--project)`);
149
139
  return { projectId: named, client, root: null };
150
140
  }
151
141
  const { root, config } = (0, config_1.requireProject)();
152
142
  const client = await connectProject(config.projectId, root, config);
153
143
  return { projectId: config.projectId, client, root };
154
144
  }
145
+ /** The word after a command: `functions` is the project stream, anything else a path to an app folder. */
146
+ function parseTargetWord(word) {
147
+ if (word === undefined)
148
+ return null;
149
+ if (word === 'functions')
150
+ return { kind: 'functions' };
151
+ return { kind: 'path', path: word.replace(/\\/g, '/').replace(/\/+$/, '') || '.' };
152
+ }
153
+ /** The app of a project that names none: the only one, or a refusal listing them. */
154
+ function onlyApp(apps, local) {
155
+ if (apps.length === 1)
156
+ return apps[0];
157
+ if (apps.length === 0)
158
+ throw new errors_1.CliError('The project has no apps', 'Create one: xflow apps add <folder>');
159
+ const names = apps.map((app) => {
160
+ const folder = local.find((item) => item.appId === app.id)?.folder;
161
+ return folder ? `apps/${folder}` : `--app ${app.id} (${app.name})`;
162
+ });
163
+ throw new errors_1.CliError('The project has several apps: name the one', `Stand in its folder or name it: ${names.join(', ')}`);
164
+ }
165
+ function appsHint(projectRoot) {
166
+ const apps = projectRoot ? (0, config_1.localApps)(projectRoot) : [];
167
+ return apps.length > 0
168
+ ? `The app folders here: ${apps.map((app) => `apps/${app.folder}`).join(', ')}`
169
+ : 'Apps are created with xflow apps add <folder>';
170
+ }
171
+ /** The app folder a typed path names: relative to where you stand, then to the project root. */
172
+ function appFolderFor(path, projectRoot) {
173
+ for (const base of [process.cwd(), projectRoot]) {
174
+ if (!base)
175
+ continue;
176
+ const dir = (0, node_path_1.resolve)(base, path);
177
+ const file = (0, node_path_1.join)(dir, config_1.CONFIG_FILE);
178
+ if ((0, node_fs_1.existsSync)(file) && (0, config_1.configKind)(safeRead(file)) === 'app')
179
+ return dir;
180
+ }
181
+ return null;
182
+ }
183
+ function safeRead(file) {
184
+ try {
185
+ return JSON.parse((0, node_fs_1.readFileSync)(file, 'utf-8'));
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ }
191
+ async function appInFolder(folder) {
192
+ const roots = (0, config_1.findRoots)(folder);
193
+ if (!roots)
194
+ throw new errors_1.CliError(`${folder} is not inside a project`);
195
+ const config = (0, config_1.readConfig)(roots.projectRoot);
196
+ const { appId } = (0, config_1.readAppConfig)(folder);
197
+ const client = await connectProject(config.projectId, roots.projectRoot, config);
198
+ return { kind: 'app', appId, projectId: config.projectId, client, root: folder, projectRoot: roots.projectRoot };
199
+ }
200
+ /** An app named by id: its project comes from the platform. */
201
+ async function appById(appId) {
202
+ if (!isProjectId(appId)) {
203
+ throw new errors_1.CliError(`--app takes an app id, got "${appId}"`, 'The apps of a project: xflow apps --project <id>');
204
+ }
205
+ const client = await connectFor(`/api/v1/apps/${appId}`, `app ${appId}`, null);
206
+ const card = await (0, api_1.apiJson)(client, `/api/v1/apps/${appId}`);
207
+ echoNamed(client, `App ${card.name} ${appId} (--app)`);
208
+ return { kind: 'app', appId, projectId: card.project_id, client, root: null, projectRoot: null };
209
+ }
210
+ /**
211
+ * The target of a command that stands in the folder: the word first, then the folder.
212
+ * `appCommand` commands (publish, deployments of an app, logs) have no project stream
213
+ * of their own, so in the project root they take the only app or refuse.
214
+ */
215
+ async function folderTarget(word, appCommand) {
216
+ const parsed = parseTargetWord(word);
217
+ if (parsed?.kind === 'path') {
218
+ const projectRoot = (0, config_1.findProjectRoot)();
219
+ const folder = appFolderFor(parsed.path, projectRoot);
220
+ if (!folder) {
221
+ throw new errors_1.CliError(`${word} is not an app folder: there is no ${config_1.CONFIG_FILE} with appId in it`, appsHint(projectRoot));
222
+ }
223
+ return appInFolder(folder);
224
+ }
225
+ const { root, config, appRoot } = (0, config_1.requireProject)();
226
+ if (appRoot && parsed === null)
227
+ return appInFolder(appRoot);
228
+ const client = await connectProject(config.projectId, root, config);
229
+ if (!appCommand || parsed?.kind === 'functions')
230
+ return { kind: 'project', projectId: config.projectId, client, root };
231
+ const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
232
+ const local = (0, config_1.localApps)(root);
233
+ const app = onlyApp(card.apps, local);
234
+ return {
235
+ kind: 'app',
236
+ appId: app.id,
237
+ projectId: config.projectId,
238
+ client,
239
+ root: local.find((item) => item.appId === app.id)?.root ?? null,
240
+ projectRoot: root,
241
+ };
242
+ }
243
+ /** An app by --app, or by the folder (in the project root: the only app). */
244
+ async function appTarget(args) {
245
+ const byApp = (0, args_1.flagString)(args, 'app');
246
+ const target = byApp !== undefined ? await appById(byApp) : await folderTarget(undefined, true);
247
+ if (target.kind !== 'app')
248
+ throw new errors_1.CliError('Only an app has versions to publish', 'Functions go live with xflow deploy functions');
249
+ return target;
250
+ }
251
+ /** The same, with --project (the project stream) and --app (an app) naming the target without a folder. */
252
+ async function streamTarget(args, word, appCommand) {
253
+ const byApp = (0, args_1.flagString)(args, 'app');
254
+ const byProject = (0, args_1.flagString)(args, 'project');
255
+ if (byApp !== undefined) {
256
+ if (byProject !== undefined)
257
+ throw new errors_1.CliError('--app names the app and its project at once', 'Keep --app alone');
258
+ if (word !== undefined)
259
+ throw new errors_1.CliError(`--app and "${word}" both name a target`, 'Keep one of them');
260
+ return appById(byApp);
261
+ }
262
+ if (byProject !== undefined) {
263
+ if (parseTargetWord(word)?.kind === 'path') {
264
+ throw new errors_1.CliError(`--project and "${word}" both name a target`, 'An app without its folder: --app <id>');
265
+ }
266
+ return { kind: 'project', ...(await projectTarget(args)) };
267
+ }
268
+ return folderTarget(word, appCommand);
269
+ }
package/dist/template.js CHANGED
@@ -1,12 +1,7 @@
1
1
  "use strict";
2
+ /** Files the CLI lays into a new project root: the app itself comes from the platform template. */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.envApiUrl = envApiUrl;
4
- exports.platformEnv = platformEnv;
5
- exports.writePlatformEnv = writePlatformEnv;
6
- exports.scaffoldFiles = scaffoldFiles;
7
- exports.functionScaffoldFiles = functionScaffoldFiles;
8
- const node_fs_1 = require("node:fs");
9
- const node_path_1 = require("node:path");
4
+ exports.projectScaffold = projectScaffold;
10
5
  const GITIGNORE = `node_modules/
11
6
  dist/
12
7
  .xflow/
@@ -15,127 +10,36 @@ dist/
15
10
  `;
16
11
  const README = `# %NAME%
17
12
 
18
- An application on XFlow.
13
+ A project on XFlow: one database, cloud functions and the apps that use them.
19
14
 
20
- \`\`\`bash
21
- npm install # dependencies
22
- xflow dev # develop on 127.0.0.1:3000
23
- xflow deploy # send the code and build it on the platform
24
- xflow publish # show the dev version to visitors
25
15
  \`\`\`
26
-
27
- The interface is built on the template components: \`src/components/ui\` (buttons, fields,
28
- dialogs) and \`src/components/blocks\` (a data table and charts). Layout built
29
- on top of them looks like the rest of the platform, while custom colors in place of the
30
- tokens from \`src/index.css\` break that consistency.
31
-
32
- Useful: \`xflow status\` for what is on the server, \`xflow deployments\` for version history.
33
- `;
34
- /** Function name → URL map, one JSON line. */
35
- const FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
36
- function functionsEnvValue(functions) {
37
- const map = {};
38
- for (const fn of functions) {
39
- if (fn.invoke_url)
40
- map[fn.name] = fn.invoke_url;
41
- }
42
- return JSON.stringify(map);
43
- }
44
- /** The platform address the way .env holds it, and the way every template reads it. */
45
- function envApiUrl(apiUrl) {
46
- return apiUrl.replace(/^https?:\/\//, '');
47
- }
48
- /** The variables that point a local run at the project: written to .env, handed to xflow dev. */
49
- function platformEnv(projectToken, apiUrl, functions = [],
50
- // The map the platform assembled: it also holds the standalone functions of the
51
- // organization. Own functions alone are the fallback for a platform without it.
52
- map) {
53
- return {
54
- VITE_XFLOW_PROJECT_TOKEN: projectToken ?? '',
55
- VITE_XFLOW_API_URL: envApiUrl(apiUrl),
56
- [FUNCTIONS_ENV_KEY]: map ? JSON.stringify(map) : functionsEnvValue(functions),
57
- };
58
- }
59
- /**
60
- * Point .env at the current project: written on every link and deploy, so that `npm run dev`
61
- * never keeps talking to the previous one. Variables of your own are left alone.
62
- */
63
- function writePlatformEnv(root, projectToken, apiUrl, functions = [], map) {
64
- for (const [key, value] of Object.entries(platformEnv(projectToken, apiUrl, functions, map))) {
65
- writeEnvValue(root, key, value);
66
- }
67
- }
68
- /** Update one variable in .env without touching the rest of the file. */
69
- function writeEnvValue(root, key, value) {
70
- const path = (0, node_path_1.join)(root, '.env');
71
- const line = `${key}=${value}`;
72
- if (!(0, node_fs_1.existsSync)(path)) {
73
- (0, node_fs_1.writeFileSync)(path, `${line}\n`, 'utf-8');
74
- return;
75
- }
76
- const text = (0, node_fs_1.readFileSync)(path, 'utf-8');
77
- const existing = new RegExp(`^${key}=.*$`, 'm');
78
- const next = existing.test(text) ? text.replace(existing, line) : `${text.replace(/\n*$/, '\n')}${line}\n`;
79
- if (next !== text)
80
- (0, node_fs_1.writeFileSync)(path, next, 'utf-8');
81
- }
82
- function scaffoldFiles(projectName) {
83
- return [
84
- { path: '.gitignore', content: GITIGNORE },
85
- { path: 'README.md', content: README.replace('%NAME%', projectName.replace(/"/g, '\\"')) },
86
- ];
87
- }
88
- /**
89
- * A standalone function has no site, so it gets no Vite tree: one handler, and
90
- * esbuild, which the sandbox takes from the project to bundle the function.
91
- */
92
- const FUNCTION_HANDLER = `/**
93
- * Cloud function of the %NAME% project.
94
- *
95
- * XFlow calls it over HTTP and, if a schedule is set, on a timer. The answer is
96
- * { statusCode, body }, where body is a string.
97
- */
98
- export async function handler(event: {
99
- httpMethod: string;
100
- body: string | null;
101
- queryStringParameters: Record<string, string> | null;
102
- }) {
103
- const input = event.body ? JSON.parse(event.body) : {};
104
-
105
- return {
106
- statusCode: 200,
107
- body: JSON.stringify({ ok: true, method: event.httpMethod, input }),
108
- };
109
- }
110
- `;
111
- const FUNCTION_README = `# %NAME%
112
-
113
- A standalone function on XFlow: a shared backend for the applications of the
114
- organization, without a site of its own.
16
+ xflow.json projectId, and which apps and pages may call each function
17
+ package.json dependencies of the functions
18
+ functions/<name>/ cloud functions, shared by every app
19
+ migrations/ the database of the project
20
+ apps/<folder>/ an app: its own code, versions and address
21
+ \`\`\`
115
22
 
116
23
  \`\`\`bash
117
- npm install # dependencies
118
- xflow deploy # send the code, build and release the function
119
- xflow functions # its address and status
24
+ xflow deploy functions -m "..." # ship the functions and their callers
25
+ xflow deploy apps/<folder> -m "..." # build one app
26
+ xflow publish # inside an app folder: show the newest build
27
+ xflow status # what is on the server
120
28
  \`\`\`
121
-
122
- The code lives in \`functions/%FUNCTION%/index.ts\`. A schedule, keys and secrets
123
- are set in the platform, in the "Functions" section.
124
29
  `;
125
- function functionScaffoldFiles(projectName, functionName) {
126
- const fill = (text) => text.replace(/%NAME%/g, projectName.replace(/"/g, '\\"')).replace(/%FUNCTION%/g, functionName);
30
+ function projectScaffold(projectName) {
127
31
  return [
128
32
  { path: '.gitignore', content: GITIGNORE },
129
- { path: 'README.md', content: fill(FUNCTION_README) },
33
+ { path: 'README.md', content: README.replace('%NAME%', projectName) },
130
34
  {
131
35
  path: 'package.json',
132
36
  content: `${JSON.stringify({
133
- name: functionName,
37
+ name: 'functions',
134
38
  private: true,
135
39
  type: 'module',
40
+ // The sandbox bundles every function with the esbuild of the project.
136
41
  devDependencies: { esbuild: '0.28.2' },
137
42
  }, null, 2)}\n`,
138
43
  },
139
- { path: `functions/${functionName}/index.ts`, content: fill(FUNCTION_HANDLER) },
140
44
  ];
141
45
  }
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.34.2';
5
+ exports.CLI_VERSION = '1.0.1';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.34.2",
4
- "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
3
+ "version": "1.0.1",
4
+ "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
7
7
  "node": ">=20"