@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.
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.templates = templates;
4
- exports.functionNameFrom = functionNameFrom;
5
3
  exports.init = init;
4
+ exports.appsList = appsList;
5
+ exports.appsAdd = appsAdd;
6
6
  exports.link = link;
7
7
  exports.list = list;
8
8
  exports.get = get;
@@ -17,7 +17,6 @@ const session_1 = require("../session");
17
17
  const skills_1 = require("./skills");
18
18
  const sources_1 = require("./sources");
19
19
  const template_1 = require("../template");
20
- const zip_1 = require("../zip");
21
20
  const ui_1 = require("../ui");
22
21
  /** .git alone does not count: linking a fresh clone is normal. */
23
22
  function isEmptyEnough(dir) {
@@ -33,157 +32,123 @@ function write(root, path, content) {
33
32
  function escapeHtml(value) {
34
33
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
34
  }
36
- /** Give the project its own tab title. */
37
- function titleFromProject(path, content, name) {
38
- if (path !== 'index.html')
39
- return content;
40
- const html = content.toString('utf-8').replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(name)}</title>`);
41
- return Buffer.from(html, 'utf-8');
42
- }
43
- async function templates() {
44
- const client = (0, session_1.connect)();
45
- const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
46
- if (list.length === 0) {
47
- (0, ui_1.note)('No templates');
35
+ /** Give the app its own tab title instead of the template one. */
36
+ function nameTheApp(appRoot, name) {
37
+ const path = (0, node_path_1.join)(appRoot, 'index.html');
38
+ if (!(0, node_fs_1.existsSync)(path))
48
39
  return;
49
- }
50
- (0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? 'default' : '', t.description ?? '']));
51
- }
52
- function slugify(name) {
53
- return name
54
- .toLowerCase()
55
- .replace(/[^a-z0-9-]+/g, '-')
56
- .replace(/^-+|-+$/g, '')
57
- .slice(0, 41);
40
+ const html = (0, node_fs_1.readFileSync)(path, 'utf-8');
41
+ (0, node_fs_1.writeFileSync)(path, html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(name)}</title>`));
58
42
  }
59
- /**
60
- * A function name out of the project name: the platform takes lowercase latin
61
- * letters, digits and dashes, and the folder it lives in gives it its name.
62
- *
63
- * The folder name is the fallback, so a project called in Russian still gets a
64
- * function: the name a person reads and the name the cloud takes are different
65
- * things, and the folder already holds a usable one.
66
- */
67
- function functionNameFrom(name, folder) {
68
- const usable = /^[a-z0-9][a-z0-9-]{0,40}$/;
69
- const slug = slugify(name);
70
- if (usable.test(slug))
71
- return slug;
72
- const fromFolder = folder ? slugify(folder) : '';
73
- if (usable.test(fromFolder))
74
- return fromFolder;
75
- throw new errors_1.CliError(`Cannot make a function name out of "${name}"`, 'The name comes from the project name, and the folder is the fallback: name either of them in latin ' +
76
- 'letters, digits and dashes (xflow init my-fn --function)');
77
- }
78
- /**
79
- * Create a standalone function: a shared backend of the organization, a project
80
- * without a site. No template is fetched — there is no frontend to fetch.
81
- */
82
- async function initFunction(args, client, target, name) {
83
- const functionName = functionNameFrom(name, (0, node_path_1.basename)(target));
84
- (0, ui_1.step)('Creating the function on the platform');
85
- const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
86
- method: 'POST',
87
- body: {
88
- name,
89
- kind: 'function',
90
- database_id: (0, args_1.flagString)(args, 'database') ?? null,
91
- server: (0, args_1.flagString)(args, 'server') ?? null,
92
- },
93
- timeoutMs: 60_000,
94
- });
95
- try {
96
- (0, node_fs_1.mkdirSync)(target, { recursive: true });
97
- for (const file of (0, template_1.functionScaffoldFiles)(name, functionName))
98
- write(target, file.path, file.content);
99
- // No build block: the platform builds the functions of such a project and
100
- // never its site, so a build command would be a promise nobody keeps.
101
- (0, config_1.writeConfig)(target, { projectId: project.id });
102
- (0, config_1.ignoreStateInGit)(target);
103
- if (client.organizationId)
104
- (0, config_1.writeState)(target, { organizationId: client.organizationId });
105
- }
106
- catch (e) {
107
- (0, ui_1.note)((0, ui_1.dim)(` The function "${project.name}" is already created (${project.id}).`));
108
- (0, ui_1.note)((0, ui_1.dim)(' Sort the folder out and pick it up: xflow link ' + project.id));
109
- throw e;
110
- }
111
- (0, skills_1.installSkillQuietly)(target);
112
- (0, ui_1.ok)(`Function "${project.name}" created: functions/${functionName}`);
113
- if (project.database) {
114
- const { server } = project.database;
115
- (0, ui_1.note)((0, ui_1.dim)(` Its database "${project.database.name}" is on ${server ? `own "${server.name}"` : 'the platform server'}`));
43
+ /** A folder name typed for an app: one segment of latin letters, digits and dashes. */
44
+ function checkFolder(folder) {
45
+ if (!folder)
46
+ throw new errors_1.CliError('Name the folder of the new app', 'For example: xflow apps add shop');
47
+ if (!/^[a-z0-9][a-z0-9-]{0,39}$/.test(folder)) {
48
+ throw new errors_1.CliError(`"${folder}" does not fit as an app folder`, 'Lowercase latin letters, digits and dashes, for example: shop');
116
49
  }
117
- (0, ui_1.out)('');
118
- (0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
119
- (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
120
- (0, ui_1.out)(' npm install');
121
- (0, ui_1.out)(' xflow deploy # send the code, build and release the function');
50
+ return folder;
122
51
  }
123
- /** Create a project from the platform template. */
52
+ /** A new app: the platform seeds its first revision from the template, the CLI pulls it into apps/<folder>. */
53
+ async function layApp(client, app, projectRoot, folder) {
54
+ const appRoot = (0, node_path_1.join)(projectRoot, config_1.APPS_DIR, folder);
55
+ await (0, sources_1.pullAppInto)(client, app.id, appRoot);
56
+ nameTheApp(appRoot, app.name);
57
+ (0, ui_1.ok)(`App "${app.name}" in ${config_1.APPS_DIR}/${folder}: ${app.url}`);
58
+ }
59
+ /** A new project and, unless --no-app, its first app from the platform template. */
124
60
  async function init(args) {
125
61
  const target = (0, node_path_1.resolve)(args.words[0] ?? '.');
126
62
  const name = (0, args_1.flagString)(args, 'name') ?? (0, node_path_1.basename)(target);
63
+ const noApp = (0, args_1.flagBool)(args, 'no-app');
127
64
  const client = (0, session_1.connect)();
128
65
  if (!isEmptyEnough(target)) {
129
- throw new errors_1.CliError(`The directory ${target} is not empty`, 'Create the project in an empty folder: xflow init my-app. To link an existing folder to a project: xflow link <id>');
130
- }
131
- if ((0, args_1.flagBool)(args, 'function'))
132
- return initFunction(args, client, target, name);
133
- let templateId = (0, args_1.flagString)(args, 'template');
134
- if (!templateId) {
135
- const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
136
- templateId = (list.find((t) => t.is_default) ?? list[0])?.id;
137
- if (!templateId)
138
- throw new errors_1.CliError('The platform has no templates at all');
66
+ throw new errors_1.CliError(`The directory ${target} is not empty`, 'Create the project in an empty folder: xflow init my-project. To link an existing folder to a project: xflow link <id>');
139
67
  }
140
- (0, ui_1.step)('Fetching the template');
141
- const archive = await (0, api_1.apiBinary)(client, `/api/v1/templates/${templateId}/archive`);
142
- const files = (0, zip_1.dropDirectoryEntries)((0, zip_1.zipRead)(archive));
68
+ const folder = noApp ? null : checkFolder((0, args_1.flagString)(args, 'app') ?? (0, sources_1.appFolderName)(name, new Set(), 'app'));
143
69
  (0, ui_1.step)('Creating the project on the platform');
144
70
  const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
145
71
  method: 'POST',
146
- body: { name, database_id: (0, args_1.flagString)(args, 'database') ?? null, server: (0, args_1.flagString)(args, 'server') ?? null },
72
+ body: { name, app: folder ? { name } : null, server: (0, args_1.flagString)(args, 'server') ?? null },
147
73
  timeoutMs: 60_000,
148
74
  });
149
75
  try {
150
- (0, node_fs_1.mkdirSync)(target, { recursive: true });
151
- for (const file of files)
152
- write(target, file.path, titleFromProject(file.path, file.content, name));
153
- for (const file of (0, template_1.scaffoldFiles)(name))
76
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(target, 'functions'), { recursive: true });
77
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(target, 'migrations'), { recursive: true });
78
+ for (const file of (0, template_1.projectScaffold)(name))
154
79
  write(target, file.path, file.content);
155
- (0, template_1.writePlatformEnv)(target, project.project_token, client.apiUrl);
156
- const config = {
157
- projectId: project.id,
158
- build: { command: 'npm run build', dir: 'dist' },
159
- };
160
- (0, config_1.writeConfig)(target, config);
80
+ (0, config_1.writeConfig)(target, { projectId: project.id });
161
81
  (0, config_1.ignoreStateInGit)(target);
162
82
  if (client.organizationId)
163
83
  (0, config_1.writeState)(target, { organizationId: client.organizationId });
84
+ if (project.app && folder)
85
+ await layApp(client, project.app, target, folder);
164
86
  }
165
87
  catch (e) {
166
88
  (0, ui_1.note)((0, ui_1.dim)(` The project "${project.name}" is already created (${project.id}).`));
167
- (0, ui_1.note)((0, ui_1.dim)(' Sort the folder out and pick it up: xflow link ' + project.id));
89
+ (0, ui_1.note)((0, ui_1.dim)(` Sort the folder out and pick it up in an empty one: xflow link ${project.id}`));
168
90
  throw e;
169
91
  }
170
92
  (0, skills_1.installSkillQuietly)(target);
171
- (0, ui_1.ok)(`Project "${project.name}" created: ${files.length} template files`);
172
- if (project.database) {
173
- const { server } = project.database;
174
- (0, ui_1.note)((0, ui_1.dim)(` Its database "${project.database.name}" is on ${server ? `own "${server.name}"` : 'the platform server'}`));
175
- }
93
+ (0, ui_1.ok)(`Project "${project.name}" created`);
94
+ if (project.server)
95
+ (0, ui_1.note)((0, ui_1.dim)(` Its database goes to the own server "${project.server.name}"`));
176
96
  (0, ui_1.out)('');
177
97
  (0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
178
- (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
179
- (0, ui_1.out)(' npm install');
180
- (0, ui_1.out)(' xflow dev # develop');
181
- (0, ui_1.out)(' xflow deploy # send the code, build and release');
98
+ (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)} && npm install # functions`);
99
+ if (folder) {
100
+ (0, ui_1.out)(` cd ${config_1.APPS_DIR}/${folder} && npm install && xflow dev # the app on 127.0.0.1:3000`);
101
+ (0, ui_1.out)(` xflow deploy ${config_1.APPS_DIR}/${folder} -m "First version"`);
102
+ }
103
+ else {
104
+ (0, ui_1.out)(' xflow deploy functions -m "First version" # the project has no apps: its functions are workflows');
105
+ }
106
+ }
107
+ /** `xflow apps`: the apps of the project, with their folders here. */
108
+ async function appsList(args) {
109
+ const { projectId, client, root } = await (0, session_1.projectTarget)(args);
110
+ const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
111
+ const local = root ? (0, config_1.localApps)(root) : [];
112
+ const folderOf = (id) => local.find((item) => item.appId === id)?.folder;
113
+ if ((0, args_1.flagBool)(args, 'json')) {
114
+ (0, ui_1.out)(JSON.stringify(card.apps.map((app) => ({ ...app, folder: folderOf(app.id) ?? null })), null, 2));
115
+ return;
116
+ }
117
+ if (card.apps.length === 0) {
118
+ (0, ui_1.note)('No apps: the functions of this project are workflows. Add one: xflow apps add <folder>');
119
+ return;
120
+ }
121
+ (0, ui_1.table)(card.apps.map((app) => [
122
+ folderOf(app.id) ? `${config_1.APPS_DIR}/${folderOf(app.id)}` : '(no folder here)',
123
+ app.name,
124
+ app.url,
125
+ app.access,
126
+ app.live_deploy_id ? `live ${app.live_deploy_id}` : 'never published',
127
+ ]));
128
+ (0, ui_1.note)((0, ui_1.dim)(' Who opens an app (the access level) is set by a person in the web interface'));
129
+ }
130
+ /** `xflow apps add <folder>`: a new app of this project, laid out from the template. */
131
+ async function appsAdd(args) {
132
+ const folder = checkFolder(args.words[0] ?? '');
133
+ const { root, config } = (0, config_1.requireProject)();
134
+ const appRoot = (0, node_path_1.join)(root, config_1.APPS_DIR, folder);
135
+ if (!isEmptyEnough(appRoot))
136
+ throw new errors_1.CliError(`${config_1.APPS_DIR}/${folder} is not empty`, 'Pick another folder name');
137
+ const client = await (0, session_1.connectProject)(config.projectId, root, config);
138
+ (0, ui_1.step)('Creating the app on the platform');
139
+ const { app } = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/apps`, {
140
+ method: 'POST',
141
+ // The tile color is optional; the platform names the colors it knows if this one is wrong.
142
+ body: { name: (0, args_1.flagString)(args, 'name') ?? folder, tone: (0, args_1.flagString)(args, 'color') },
143
+ timeoutMs: 60_000,
144
+ });
145
+ await layApp(client, app, root, folder);
146
+ (0, ui_1.note)((0, ui_1.dim)(` Let it call a function: add "${folder}" to functions.<name>.callers in xflow.json, then xflow deploy functions`));
147
+ (0, ui_1.note)((0, ui_1.dim)(` Build it: xflow deploy ${config_1.APPS_DIR}/${folder} -m "First version" (the first build publishes itself)`));
182
148
  }
183
149
  /**
184
- * Linking is a step, not a task. Without this the agent reports the command as done and
185
- * goes quiet, leaving the person in front of a folder: it has to read the skill and come
186
- * back with a question instead.
150
+ * Linking is a step, not a task: the agent has to read the skill and come back with
151
+ * a question instead of reporting the command as done.
187
152
  */
188
153
  function nextAfterLink(skillReady) {
189
154
  (0, ui_1.out)('');
@@ -195,30 +160,22 @@ function nextAfterLink(skillReady) {
195
160
  async function link(args) {
196
161
  const root = (0, config_1.findProjectRoot)() ?? process.cwd();
197
162
  const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
198
- // Asked before anything is written: an empty folder means a clone, and the
199
- // sources have to come with it.
163
+ // Asked before anything is written: an empty folder means a clone, and the whole project comes with it.
200
164
  const fresh = !existing && isEmptyEnough(process.cwd());
201
165
  let client = (0, session_1.connect)(existing);
202
166
  const projectId = args.words[0];
203
- if (!projectId) {
167
+ if (!projectId)
204
168
  throw new errors_1.CliError('A project identifier is required', 'The list: xflow projects list');
205
- }
206
- if (!(0, session_1.isProjectId)(projectId)) {
169
+ if (!(0, session_1.isProjectId)(projectId))
207
170
  throw new errors_1.CliError(`link takes a project id, got "${projectId}"`, 'The ids: xflow projects list');
208
- }
209
171
  let card;
210
172
  try {
211
173
  card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
212
174
  }
213
175
  catch (e) {
214
- // Project ids are unique across the platform, so a 404 under this key does
215
- // not mean the project does not exist: another signed-in organization may
216
- // hold it. A dead key (401/403) says even less. Ask every stored
217
- // organization before giving up.
218
- if (!(e instanceof api_1.ApiError && (e.status === 404 || e.status === 401 || e.status === 403)) ||
219
- client.source === 'env') {
176
+ // Ids are unique across the platform: another signed-in organization may hold it.
177
+ if (!(e instanceof api_1.ApiError && (e.status === 404 || e.status === 401 || e.status === 403)) || client.source === 'env')
220
178
  throw e;
221
- }
222
179
  const { hit, dead } = await (0, session_1.findProjectOrg)(client.apiUrl, projectId);
223
180
  if (!hit) {
224
181
  throw new errors_1.CliError(`No signed-in organization holds the project ${projectId}`, dead.length > 0
@@ -230,41 +187,27 @@ async function link(args) {
230
187
  (0, ui_1.note)((0, ui_1.dim)(` The project belongs to "${hit.name ?? hit.organizationId}", not to the active organization`));
231
188
  }
232
189
  const dir = existing ? root : process.cwd();
233
- (0, config_1.writeConfig)(dir, {
234
- ...(existing ?? {}),
235
- projectId: card.id,
236
- build: existing?.build ?? { command: 'npm run build', dir: 'dist' },
237
- });
190
+ const config = { ...(existing ?? {}), projectId: card.id };
191
+ (0, config_1.writeConfig)(dir, config);
238
192
  (0, config_1.ignoreStateInGit)(dir);
239
193
  if (client.organizationId)
240
194
  (0, config_1.writeState)(dir, { organizationId: client.organizationId });
241
- (0, template_1.writePlatformEnv)(dir, card.project_token, client.apiUrl, card.functions, card.function_map);
242
- (0, ui_1.note)((0, ui_1.dim)(' .env now points at this project, run it locally with xflow dev'));
243
195
  const skillReady = (0, skills_1.installSkillQuietly)(dir);
244
196
  (0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
245
197
  (0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
246
- if (!fresh) {
247
- nextAfterLink(skillReady);
248
- return;
249
- }
250
- // A clone without the code is half a clone, and the folder has to remember
251
- // which revision it works from: without that the first push has nothing to
252
- // compare against and stops.
253
- try {
254
- const { info, files } = await (0, sources_1.downloadRevision)(client, card.id, dir);
255
- (0, config_1.writeState)(dir, { treeHash: info.tree_hash });
256
- (0, ui_1.ok)(`Revision ${info.revision}: ${files} files`);
257
- }
258
- catch (e) {
259
- if (e instanceof api_1.ApiError && e.status === 404) {
260
- (0, ui_1.note)((0, ui_1.dim)(' The project has no sources yet: write the code and run xflow deploy'));
261
- nextAfterLink(skillReady);
198
+ if (fresh) {
199
+ // A clone without the code is half a clone: the root and every app come down.
200
+ try {
201
+ await (0, sources_1.cloneProject)(client, card, dir);
202
+ }
203
+ catch (e) {
204
+ (0, ui_1.warn)(`The sources did not arrive: ${e instanceof Error ? e.message : e}`);
205
+ (0, ui_1.note)((0, ui_1.dim)(' The folder is linked, fetch them separately: xflow pull --into <empty dir>'));
262
206
  return;
263
207
  }
264
- // No next step while the code is missing: the pull has to succeed first.
265
- (0, ui_1.warn)(`The sources did not arrive: ${e instanceof Error ? e.message : e}`);
266
- (0, ui_1.note)((0, ui_1.dim)(' The folder is linked, fetch them separately: xflow pull'));
267
- return;
208
+ }
209
+ else if (card.apps.length > 0 && (0, config_1.localApps)(dir).length === 0) {
210
+ (0, ui_1.note)((0, ui_1.dim)(` The project has ${card.apps.length} app(s) and no folder here holds one: clone it into an empty folder with xflow link`));
268
211
  }
269
212
  nextAfterLink(skillReady);
270
213
  }
@@ -276,24 +219,14 @@ async function list() {
276
219
  (0, ui_1.table)(projects.map((p) => [
277
220
  p.id,
278
221
  p.name,
279
- // A standalone function has no pages at all, so the states of a site
280
- // ("published", "dev only") would say nothing about it.
281
- p.kind === 'function'
282
- ? 'function'
283
- : p.live_deploy_id
284
- ? 'published'
285
- : p.dev_deploy_id
286
- ? 'dev only'
287
- : 'no build',
222
+ p.apps.length === 0 ? 'no apps' : p.apps.length === 1 ? '1 app' : `${p.apps.length} apps`,
288
223
  (0, ui_1.formatAge)(p.updated_at),
289
224
  ]));
290
225
  }
291
226
  else {
292
227
  (0, ui_1.note)('No projects. To create one: xflow init');
293
228
  }
294
- // One organization answers, and with several signed in that is worth saying:
295
- // a project that "is not here" may simply live in another one. Not under an
296
- // env key or a folder binding, where "active" would be the wrong word.
229
+ // With several organizations signed in, a project that "is not here" may live in another one.
297
230
  const orgs = (0, credentials_1.listOrgs)(client.apiUrl);
298
231
  if (client.source === 'active' && orgs.length > 1) {
299
232
  const name = orgs.find((org) => org.active)?.name;
@@ -308,34 +241,45 @@ async function get(args) {
308
241
  throw new errors_1.CliError(`projects get takes a project id, got "${explicit}"`, 'The ids: xflow projects list');
309
242
  }
310
243
  const projectId = explicit ?? config?.projectId;
311
- if (!projectId) {
244
+ if (!projectId)
312
245
  throw new errors_1.CliError('A project identifier is required', 'Or run the command inside the project folder');
313
- }
314
- // An id typed by hand is looked for across every signed-in organization and
315
- // ignores the folder whole, xflow.json included, the way --project does it.
316
- // Named by the folder, the project of an unbound one gets probed and the
317
- // binding written, the way publish and pull do it.
318
- const client = explicit !== undefined
319
- ? await (0, session_1.connectProject)(explicit, null)
320
- : await (0, session_1.connectProject)(projectId, root, config);
246
+ // An id typed by hand ignores the folder whole, the way --project does it.
247
+ const client = explicit !== undefined ? await (0, session_1.connectProject)(explicit, null) : await (0, session_1.connectProject)(projectId, root, config);
321
248
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
249
+ const local = explicit === undefined && root ? (0, config_1.localApps)(root) : [];
322
250
  (0, ui_1.out)(`${(0, ui_1.bold)(card.name)} ${(0, ui_1.dim)(card.id)}`);
323
251
  if (card.description)
324
252
  (0, ui_1.out)(card.description);
325
253
  (0, ui_1.out)('');
326
- (0, ui_1.out)(`Built version: ${card.dev_deploy_id ?? '(no build)'}`);
327
- (0, ui_1.out)(`Visitors see: ${card.live_deploy_id ?? '(never published)'}`);
328
- (0, ui_1.out)(`Database: ${card.database ? `${card.database.name} (${card.database.schema})` : '(none)'}`);
254
+ (0, ui_1.out)(`Database: ${card.database.schema}${card.database.server_id ? ' (own server)' : ''}`);
329
255
  (0, ui_1.out)('');
330
- (0, ui_1.out)(card.project_url);
256
+ (0, ui_1.out)('Apps:');
257
+ if (card.apps.length === 0)
258
+ (0, ui_1.note)((0, ui_1.dim)(' none: the functions are workflows'));
259
+ else {
260
+ (0, ui_1.table)(card.apps.map((app) => [
261
+ ` ${local.find((item) => item.appId === app.id)?.folder ?? app.name}`,
262
+ app.url,
263
+ app.access,
264
+ app.live_deploy_id ? `live ${app.live_deploy_id}` : 'never published',
265
+ ]));
266
+ }
331
267
  if (card.functions.length > 0) {
332
268
  (0, ui_1.out)('');
333
269
  (0, ui_1.out)('Functions:');
334
- (0, ui_1.table)(card.functions.map((f) => [` ${f.name}`, f.status, (0, ui_1.formatAge)(f.last_deployed_at)]));
270
+ (0, ui_1.table)(card.functions.map((f) => [
271
+ ` ${f.name}`,
272
+ f.status,
273
+ f.callers.length > 0 ? f.callers.join(', ') : 'no callers',
274
+ f.open ? 'open' : '',
275
+ (0, ui_1.formatAge)(f.last_deployed_at),
276
+ ]));
335
277
  }
336
278
  if (card.schedules.length > 0) {
337
279
  (0, ui_1.out)('');
338
280
  (0, ui_1.out)('Schedules:');
339
281
  (0, ui_1.table)(card.schedules.map((s) => [` ${s.function_name}`, s.cron_expression, s.status]));
340
282
  }
283
+ (0, ui_1.out)('');
284
+ (0, ui_1.out)(card.project_url);
341
285
  }