@getxflow/cli 0.34.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api.js +21 -6
- package/dist/args.js +1 -1
- package/dist/bin.js +10 -3
- package/dist/commands/connections.js +4 -4
- package/dist/commands/db.js +26 -63
- package/dist/commands/deploy.js +170 -216
- package/dist/commands/dev.js +15 -29
- package/dist/commands/env.js +6 -3
- package/dist/commands/functions.js +27 -119
- package/dist/commands/logs.js +19 -15
- package/dist/commands/pages.js +1 -0
- package/dist/commands/projects.js +134 -190
- package/dist/commands/sources.js +218 -120
- package/dist/commands/storage.js +2 -2
- package/dist/config.js +82 -26
- package/dist/flags.js +19 -12
- package/dist/help.js +210 -209
- package/dist/limits.js +6 -6
- package/dist/session.js +167 -52
- package/dist/template.js +29 -125
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/xflow/SKILL.md +682 -740
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
35
34
|
}
|
|
36
|
-
/** Give the
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
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,
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
152
|
-
|
|
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,
|
|
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)(
|
|
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
|
|
172
|
-
if (project.
|
|
173
|
-
|
|
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
|
-
(
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
185
|
-
*
|
|
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
|
-
//
|
|
215
|
-
|
|
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
|
-
(
|
|
234
|
-
|
|
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 (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
(0, ui_1.note)((0, ui_1.dim)(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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)(`
|
|
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)(
|
|
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) => [
|
|
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
|
}
|