@nocobase/cli 2.1.0-alpha.19 → 2.1.0-alpha.20
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 +15 -9
- package/bin/run.js +9 -3
- package/dist/commands/api/resource/index.js +20 -0
- package/dist/commands/build.js +2 -2
- package/dist/commands/db/start.js +22 -0
- package/dist/commands/dev.js +1 -1
- package/dist/commands/download.js +221 -49
- package/dist/commands/env/add.js +179 -34
- package/dist/commands/env/auth.js +31 -6
- package/dist/commands/env/list.js +12 -2
- package/dist/commands/env/remove.js +12 -1
- package/dist/commands/env/update.js +24 -9
- package/dist/commands/env/use.js +11 -1
- package/dist/commands/init.js +186 -0
- package/dist/commands/install.js +660 -12
- package/dist/commands/pm/disable.js +14 -15
- package/dist/commands/pm/enable.js +14 -15
- package/dist/commands/pm/list.js +5 -16
- package/dist/commands/scaffold/migration.js +1 -1
- package/dist/commands/scaffold/plugin.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/generated/command-registry.js +57 -11
- package/dist/help/runtime-help.js +20 -0
- package/dist/lib/auth-store.js +48 -3
- package/dist/lib/bootstrap.js +14 -9
- package/dist/lib/command-discovery.js +4 -4
- package/dist/lib/env-auth.js +95 -15
- package/dist/lib/init-browser-wizard.js +431 -0
- package/dist/lib/openapi.js +8 -200
- package/dist/lib/run-npm.js +27 -42
- package/nocobase-ctl.config.json +28 -68
- package/package.json +7 -6
- package/dist/commands/self-update.js +0 -46
package/dist/commands/install.js
CHANGED
|
@@ -7,27 +7,656 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { Command, Flags } from '@oclif/core';
|
|
10
|
-
import
|
|
10
|
+
import * as p from '@clack/prompts';
|
|
11
|
+
import pc from 'picocolors';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
|
|
14
|
+
import { loadAuthConfig } from "../lib/auth-store.js";
|
|
15
|
+
const DEFAULT_INSTALL_ENV_NAME = 'local';
|
|
16
|
+
const DEFAULT_INSTALL_SOURCE = 'docker';
|
|
17
|
+
const DEFAULT_INSTALL_LANG = 'en-US';
|
|
11
18
|
export default class Install extends Command {
|
|
12
19
|
static description = 'Run the legacy NocoBase install (forwards to `npm run install` in the repo root)';
|
|
13
20
|
static examples = [
|
|
14
21
|
'<%= config.bin %> <%= command.id %>',
|
|
15
22
|
'<%= config.bin %> <%= command.id %> -f',
|
|
16
23
|
'<%= config.bin %> <%= command.id %> -l zh-CN',
|
|
17
|
-
'<%= config.bin %> <%= command.id %> -m
|
|
18
|
-
'<%= config.bin %> <%= command.id %> -p admin123',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> -u nocobase -m admin@nocobase.com -p admin123',
|
|
19
25
|
'<%= config.bin %> <%= command.id %> -n "Super Admin"',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> --app-root-path=./nocobase --storage-path=./storage/myenv -e myenv',
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --source npm',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> -y --env dev --app-root-path=./nocobase',
|
|
29
|
+
'<%= config.bin %> <%= command.id %> -y --source npm --fetch-source --app-root-path=./nocobase',
|
|
20
30
|
];
|
|
21
31
|
static flags = {
|
|
22
|
-
|
|
32
|
+
source: Flags.string({
|
|
33
|
+
description: `Where to obtain the NocoBase package (default: ${DEFAULT_INSTALL_SOURCE})`,
|
|
34
|
+
options: ['git', 'npm', 'docker'],
|
|
35
|
+
}),
|
|
36
|
+
yes: Flags.boolean({
|
|
37
|
+
char: 'y',
|
|
38
|
+
description: 'Skip interactive prompts; use flags and defaults only',
|
|
39
|
+
default: false,
|
|
40
|
+
}),
|
|
41
|
+
env: Flags.string({
|
|
42
|
+
char: 'e',
|
|
43
|
+
description: 'Application name (CLI env key). Required. Storage defaults to ./storage/<name> unless --storage-path is set',
|
|
44
|
+
}),
|
|
23
45
|
force: Flags.boolean({ description: 'Reinstall the application by clearing the database', char: 'f', required: false }),
|
|
24
46
|
lang: Flags.string({ description: 'Language during installation', char: 'l', required: false }),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
'root-username': Flags.string({
|
|
48
|
+
char: 'u',
|
|
49
|
+
description: 'Root username (sets INIT_ROOT_USERNAME for install; forwarded as --root-username)',
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
52
|
+
'root-email': Flags.string({
|
|
53
|
+
char: 'm',
|
|
54
|
+
description: 'Root user email (sets INIT_ROOT_EMAIL for install; forwarded as --root-email)',
|
|
55
|
+
required: false,
|
|
56
|
+
}),
|
|
57
|
+
'root-password': Flags.string({
|
|
58
|
+
char: 'p',
|
|
59
|
+
description: 'Root user password (forwarded as --root-password)',
|
|
60
|
+
required: false,
|
|
61
|
+
}),
|
|
62
|
+
'root-nickname': Flags.string({
|
|
63
|
+
char: 'n',
|
|
64
|
+
description: 'Root user nickname (forwarded as --root-nickname)',
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
'app-root-path': Flags.string({
|
|
68
|
+
description: 'Application root directory for install (relative to cwd; default: ./nocobase)',
|
|
69
|
+
}),
|
|
70
|
+
'storage-path': Flags.string({
|
|
71
|
+
description: 'Storage directory (relative to cwd; default: ./storage/<env> when --env is set, else ./storage/default)',
|
|
72
|
+
}),
|
|
73
|
+
'app-port': Flags.string({
|
|
74
|
+
description: 'Application HTTP port (APP_PORT; default: 13000)',
|
|
75
|
+
}),
|
|
76
|
+
'db-dialect': Flags.string({
|
|
77
|
+
description: 'Database dialect (e.g. postgres, mysql)',
|
|
78
|
+
options: ['postgres', 'mysql', 'mariadb', 'kingbase'],
|
|
79
|
+
}),
|
|
80
|
+
'db-host': Flags.string({
|
|
81
|
+
description: 'Database host',
|
|
82
|
+
}),
|
|
83
|
+
'db-port': Flags.string({
|
|
84
|
+
description: 'Database port',
|
|
85
|
+
}),
|
|
86
|
+
'db-database': Flags.string({
|
|
87
|
+
description: 'Database name',
|
|
88
|
+
}),
|
|
89
|
+
'db-user': Flags.string({
|
|
90
|
+
description: 'Database user',
|
|
91
|
+
}),
|
|
92
|
+
'db-password': Flags.string({
|
|
93
|
+
description: 'Database password',
|
|
94
|
+
}),
|
|
95
|
+
'docker-registry': Flags.string({
|
|
96
|
+
description: 'Docker image without tag (e.g. nocobase/nocobase)',
|
|
97
|
+
}),
|
|
98
|
+
'docker-tag': Flags.string({
|
|
99
|
+
description: 'Docker image tag (e.g. latest)',
|
|
100
|
+
}),
|
|
101
|
+
'start-builtin-db': Flags.boolean({
|
|
102
|
+
description: 'Run `nb db start` before install (use with `-y` when you rely on the CLI-managed database)',
|
|
103
|
+
default: false,
|
|
104
|
+
}),
|
|
105
|
+
'fetch-source': Flags.boolean({
|
|
106
|
+
description: 'With --source npm|git, run `nb download` before install (non-interactive; in a TTY use the prompt unless you pass this to skip the question and force download)',
|
|
107
|
+
default: false,
|
|
108
|
+
}),
|
|
109
|
+
'download-version': Flags.string({
|
|
110
|
+
description: 'When using fetch-source or nb download: version / dist-tag (-v); default latest when non-interactive / -y',
|
|
111
|
+
}),
|
|
112
|
+
'download-git-url': Flags.string({
|
|
113
|
+
description: 'When using fetch-source with git: repository URL (nb download --git-url)',
|
|
114
|
+
}),
|
|
28
115
|
};
|
|
116
|
+
defaultDbPort(dialect) {
|
|
117
|
+
if (dialect === 'mysql' || dialect === 'mariadb') {
|
|
118
|
+
return '3306';
|
|
119
|
+
}
|
|
120
|
+
return '5432';
|
|
121
|
+
}
|
|
122
|
+
resolveAppRoot(flagPath, saved) {
|
|
123
|
+
if (flagPath) {
|
|
124
|
+
return path.resolve(process.cwd(), flagPath);
|
|
125
|
+
}
|
|
126
|
+
if (saved) {
|
|
127
|
+
return path.isAbsolute(saved) ? saved : path.resolve(process.cwd(), saved);
|
|
128
|
+
}
|
|
129
|
+
return path.resolve(process.cwd(), 'nocobase');
|
|
130
|
+
}
|
|
131
|
+
resolveStoragePath(flagPath, envName, saved) {
|
|
132
|
+
if (flagPath) {
|
|
133
|
+
return path.resolve(process.cwd(), flagPath);
|
|
134
|
+
}
|
|
135
|
+
if (saved) {
|
|
136
|
+
return path.isAbsolute(saved) ? saved : path.resolve(process.cwd(), saved);
|
|
137
|
+
}
|
|
138
|
+
return path.resolve(process.cwd(), 'storage', envName);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* For npm/git: optionally run `nb download`, otherwise (interactive, no saved env) prompt for app root.
|
|
142
|
+
*/
|
|
143
|
+
async resolveNpmGitAppRootWithDownload(options) {
|
|
144
|
+
const { source, interactive, hasSavedEnv, savedEnv, fetchSourceFlag, downloadGitUrl, } = options;
|
|
145
|
+
const downloadVersion = options.downloadVersion;
|
|
146
|
+
let appRootPathFlag = options.appRootPathFlag;
|
|
147
|
+
let runDownload;
|
|
148
|
+
if (!interactive) {
|
|
149
|
+
runDownload = fetchSourceFlag;
|
|
150
|
+
}
|
|
151
|
+
else if (hasSavedEnv) {
|
|
152
|
+
runDownload = fetchSourceFlag;
|
|
153
|
+
}
|
|
154
|
+
else if (fetchSourceFlag) {
|
|
155
|
+
runDownload = true;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const fetchAns = await p.confirm({
|
|
159
|
+
message: 'Download or clone NocoBase source with nb download before install?',
|
|
160
|
+
initialValue: true,
|
|
161
|
+
});
|
|
162
|
+
if (p.isCancel(fetchAns)) {
|
|
163
|
+
p.cancel('Install cancelled.');
|
|
164
|
+
this.exit(0);
|
|
165
|
+
}
|
|
166
|
+
runDownload = fetchAns;
|
|
167
|
+
}
|
|
168
|
+
if (!runDownload) {
|
|
169
|
+
if (interactive && !hasSavedEnv) {
|
|
170
|
+
const defaultRoot = appRootPathFlag ?? path.relative(process.cwd(), this.resolveAppRoot(undefined, savedEnv?.appRootPath));
|
|
171
|
+
const rootAns = await p.text({
|
|
172
|
+
message: 'Application root directory (where nocobase-v1 runs; relative to cwd)',
|
|
173
|
+
initialValue: defaultRoot || 'nocobase',
|
|
174
|
+
});
|
|
175
|
+
if (p.isCancel(rootAns)) {
|
|
176
|
+
p.cancel('Install cancelled.');
|
|
177
|
+
this.exit(0);
|
|
178
|
+
}
|
|
179
|
+
appRootPathFlag = rootAns.trim() || appRootPathFlag;
|
|
180
|
+
}
|
|
181
|
+
return appRootPathFlag;
|
|
182
|
+
}
|
|
183
|
+
const downloadArgv = [];
|
|
184
|
+
if (!interactive) {
|
|
185
|
+
downloadArgv.push('-y');
|
|
186
|
+
}
|
|
187
|
+
downloadArgv.push('--source', source);
|
|
188
|
+
if (!interactive) {
|
|
189
|
+
const version = downloadVersion?.trim() || 'latest';
|
|
190
|
+
downloadArgv.push('-v', version);
|
|
191
|
+
let outputDir = appRootPathFlag?.trim();
|
|
192
|
+
if (!outputDir) {
|
|
193
|
+
const safe = version.replace(/[/\\]/g, '-');
|
|
194
|
+
outputDir = `nocobase-${safe}`;
|
|
195
|
+
}
|
|
196
|
+
downloadArgv.push('-o', outputDir);
|
|
197
|
+
if (source === 'git' && downloadGitUrl?.trim()) {
|
|
198
|
+
downloadArgv.push('--git-url', downloadGitUrl.trim());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const v = downloadVersion?.trim();
|
|
203
|
+
if (v) {
|
|
204
|
+
downloadArgv.push('-v', v);
|
|
205
|
+
}
|
|
206
|
+
const out = appRootPathFlag?.trim();
|
|
207
|
+
if (out) {
|
|
208
|
+
downloadArgv.push('-o', out);
|
|
209
|
+
}
|
|
210
|
+
if (source === 'git' && downloadGitUrl?.trim()) {
|
|
211
|
+
downloadArgv.push('--git-url', downloadGitUrl.trim());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (interactive) {
|
|
215
|
+
p.log.step('Running nb download');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.log('Running nb download');
|
|
219
|
+
}
|
|
220
|
+
const dl = await this.config.runCommand('download', downloadArgv);
|
|
221
|
+
if (dl.projectRoot) {
|
|
222
|
+
return dl.projectRoot;
|
|
223
|
+
}
|
|
224
|
+
if (!interactive) {
|
|
225
|
+
const version = downloadVersion?.trim() || 'latest';
|
|
226
|
+
const safe = version.replace(/[/\\]/g, '-');
|
|
227
|
+
const outputDir = appRootPathFlag?.trim() || `nocobase-${safe}`;
|
|
228
|
+
return path.resolve(process.cwd(), outputDir);
|
|
229
|
+
}
|
|
230
|
+
this.error('Could not determine the project directory after nb download.');
|
|
231
|
+
}
|
|
29
232
|
async run() {
|
|
30
233
|
const { flags } = await this.parse(Install);
|
|
234
|
+
const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !flags.yes);
|
|
235
|
+
let installLang = flags.lang?.trim() || undefined;
|
|
236
|
+
let envName = flags.env?.trim();
|
|
237
|
+
if (envName === '') {
|
|
238
|
+
envName = undefined;
|
|
239
|
+
}
|
|
240
|
+
if (interactive) {
|
|
241
|
+
p.intro('nb install');
|
|
242
|
+
if (installLang === undefined) {
|
|
243
|
+
const langAns = await p.text({
|
|
244
|
+
message: 'Install language (--lang, e.g. en-US or zh-CN)',
|
|
245
|
+
placeholder: DEFAULT_INSTALL_LANG,
|
|
246
|
+
initialValue: DEFAULT_INSTALL_LANG,
|
|
247
|
+
});
|
|
248
|
+
if (p.isCancel(langAns)) {
|
|
249
|
+
p.cancel('Install cancelled.');
|
|
250
|
+
this.exit(0);
|
|
251
|
+
}
|
|
252
|
+
const t = langAns.trim();
|
|
253
|
+
installLang = t || DEFAULT_INSTALL_LANG;
|
|
254
|
+
}
|
|
255
|
+
if (!envName) {
|
|
256
|
+
const envAnswer = await p.text({
|
|
257
|
+
message: 'Application name',
|
|
258
|
+
placeholder: DEFAULT_INSTALL_ENV_NAME,
|
|
259
|
+
validate: (value) => (value.trim() ? undefined : 'Application name is required'),
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(envAnswer)) {
|
|
262
|
+
p.cancel('Install cancelled.');
|
|
263
|
+
this.exit(0);
|
|
264
|
+
}
|
|
265
|
+
envName = envAnswer.trim();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (!envName) {
|
|
269
|
+
this.error('Application name is required (pass -e or --env).');
|
|
270
|
+
}
|
|
271
|
+
const auth = await loadAuthConfig();
|
|
272
|
+
const savedEnv = auth.envs[envName];
|
|
273
|
+
const hasSavedEnv = savedEnv !== undefined;
|
|
274
|
+
if (interactive && hasSavedEnv) {
|
|
275
|
+
p.log.info(`Using saved CLI env ${pc.cyan(`"${envName}"`)} (from ${pc.dim('nb env add')}). Override any field with flags if needed.`);
|
|
276
|
+
}
|
|
277
|
+
// Default: built-in DB (non-interactive / -y skips the prompt and keeps this).
|
|
278
|
+
let hasExistingDb = false;
|
|
279
|
+
if (interactive && !hasSavedEnv) {
|
|
280
|
+
const dbMode = await p.select({
|
|
281
|
+
message: 'Use the built-in database, or connect to one you already have?',
|
|
282
|
+
options: [
|
|
283
|
+
{ value: 'builtin', label: 'Use built-in database' },
|
|
284
|
+
{ value: 'own', label: 'I already have a database' },
|
|
285
|
+
],
|
|
286
|
+
initialValue: 'builtin',
|
|
287
|
+
});
|
|
288
|
+
if (p.isCancel(dbMode)) {
|
|
289
|
+
p.cancel('Install cancelled.');
|
|
290
|
+
this.exit(0);
|
|
291
|
+
}
|
|
292
|
+
hasExistingDb = dbMode === 'own';
|
|
293
|
+
}
|
|
294
|
+
let dbDialect = flags['db-dialect'] ?? savedEnv?.dbDialect ?? 'postgres';
|
|
295
|
+
let dbHost = flags['db-host'] ?? savedEnv?.dbHost ?? 'localhost';
|
|
296
|
+
let dbPort = flags['db-port'] ?? (savedEnv?.dbPort !== undefined ? String(savedEnv.dbPort) : undefined);
|
|
297
|
+
let dbDatabase = flags['db-database'] ?? savedEnv?.dbDatabase ?? 'nocobase';
|
|
298
|
+
let dbUser = flags['db-user'] ?? savedEnv?.dbUser ?? 'nocobase';
|
|
299
|
+
let dbPassword = flags['db-password'] ?? savedEnv?.dbPassword ?? 'nocobase';
|
|
300
|
+
if (!hasExistingDb && !flags['db-port'] && savedEnv?.dbPort === undefined) {
|
|
301
|
+
dbPort = dbPort ?? '5432';
|
|
302
|
+
}
|
|
303
|
+
else if (!dbPort) {
|
|
304
|
+
dbPort = this.defaultDbPort(dbDialect);
|
|
305
|
+
}
|
|
306
|
+
if (interactive && !hasSavedEnv) {
|
|
307
|
+
const dialectAns = await p.select({
|
|
308
|
+
message: 'Database dialect',
|
|
309
|
+
options: [
|
|
310
|
+
{ value: 'postgres', label: 'PostgreSQL' },
|
|
311
|
+
{ value: 'mysql', label: 'MySQL' },
|
|
312
|
+
{ value: 'mariadb', label: 'MariaDB' },
|
|
313
|
+
{ value: 'kingbase', label: 'Kingbase' },
|
|
314
|
+
],
|
|
315
|
+
initialValue: dbDialect,
|
|
316
|
+
});
|
|
317
|
+
if (p.isCancel(dialectAns)) {
|
|
318
|
+
p.cancel('Install cancelled.');
|
|
319
|
+
this.exit(0);
|
|
320
|
+
}
|
|
321
|
+
dbDialect = dialectAns;
|
|
322
|
+
const hostAns = await p.text({
|
|
323
|
+
message: 'Database host',
|
|
324
|
+
initialValue: dbHost,
|
|
325
|
+
});
|
|
326
|
+
if (p.isCancel(hostAns)) {
|
|
327
|
+
p.cancel('Install cancelled.');
|
|
328
|
+
this.exit(0);
|
|
329
|
+
}
|
|
330
|
+
dbHost = hostAns.trim() || dbHost;
|
|
331
|
+
const portAns = await p.text({
|
|
332
|
+
message: 'Database port',
|
|
333
|
+
initialValue: dbPort,
|
|
334
|
+
});
|
|
335
|
+
if (p.isCancel(portAns)) {
|
|
336
|
+
p.cancel('Install cancelled.');
|
|
337
|
+
this.exit(0);
|
|
338
|
+
}
|
|
339
|
+
dbPort = portAns.trim() || dbPort;
|
|
340
|
+
const dbNameAns = await p.text({
|
|
341
|
+
message: 'Database name',
|
|
342
|
+
initialValue: dbDatabase,
|
|
343
|
+
});
|
|
344
|
+
if (p.isCancel(dbNameAns)) {
|
|
345
|
+
p.cancel('Install cancelled.');
|
|
346
|
+
this.exit(0);
|
|
347
|
+
}
|
|
348
|
+
dbDatabase = dbNameAns.trim() || dbDatabase;
|
|
349
|
+
const userAns = await p.text({
|
|
350
|
+
message: 'Database user',
|
|
351
|
+
initialValue: dbUser,
|
|
352
|
+
});
|
|
353
|
+
if (p.isCancel(userAns)) {
|
|
354
|
+
p.cancel('Install cancelled.');
|
|
355
|
+
this.exit(0);
|
|
356
|
+
}
|
|
357
|
+
dbUser = userAns.trim() || dbUser;
|
|
358
|
+
const passAns = await p.password({
|
|
359
|
+
message: 'Database password',
|
|
360
|
+
});
|
|
361
|
+
if (p.isCancel(passAns)) {
|
|
362
|
+
p.cancel('Install cancelled.');
|
|
363
|
+
this.exit(0);
|
|
364
|
+
}
|
|
365
|
+
dbPassword =
|
|
366
|
+
typeof passAns === 'string' && passAns.length > 0 ? passAns : dbPassword;
|
|
367
|
+
}
|
|
368
|
+
const runDbStart = (!hasSavedEnv && !hasExistingDb) || Boolean(flags['start-builtin-db']);
|
|
369
|
+
if (runDbStart) {
|
|
370
|
+
try {
|
|
371
|
+
if (interactive) {
|
|
372
|
+
p.log.step('Running nb db start');
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
this.log('Running nb db start');
|
|
376
|
+
}
|
|
377
|
+
// oclif explicit registry: `db:start` → user types `nb db start`
|
|
378
|
+
await this.config.runCommand('db:start', []);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
+
if (interactive) {
|
|
383
|
+
p.outro(pc.red(message));
|
|
384
|
+
}
|
|
385
|
+
this.error(message);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let resolvedStorage = this.resolveStoragePath(flags['storage-path'], envName, savedEnv?.storagePath);
|
|
389
|
+
let appPort = flags['app-port'] ?? '13000';
|
|
390
|
+
if (interactive && !hasSavedEnv) {
|
|
391
|
+
const relInitial = path.relative(process.cwd(), resolvedStorage) || path.join('storage', envName);
|
|
392
|
+
const sp = await p.text({
|
|
393
|
+
message: 'Storage directory (relative to cwd)',
|
|
394
|
+
initialValue: relInitial,
|
|
395
|
+
});
|
|
396
|
+
if (p.isCancel(sp)) {
|
|
397
|
+
p.cancel('Install cancelled.');
|
|
398
|
+
this.exit(0);
|
|
399
|
+
}
|
|
400
|
+
resolvedStorage = path.resolve(process.cwd(), sp.trim() || relInitial);
|
|
401
|
+
}
|
|
402
|
+
if (interactive && !hasSavedEnv) {
|
|
403
|
+
const portAns = await p.text({
|
|
404
|
+
message: 'Application port (APP_PORT)',
|
|
405
|
+
initialValue: appPort,
|
|
406
|
+
});
|
|
407
|
+
if (p.isCancel(portAns)) {
|
|
408
|
+
p.cancel('Install cancelled.');
|
|
409
|
+
this.exit(0);
|
|
410
|
+
}
|
|
411
|
+
appPort = portAns.trim() || appPort;
|
|
412
|
+
}
|
|
413
|
+
let source = flags.source ?? DEFAULT_INSTALL_SOURCE;
|
|
414
|
+
if (interactive && !hasSavedEnv) {
|
|
415
|
+
const src = await p.select({
|
|
416
|
+
message: 'Install source for this project',
|
|
417
|
+
options: [
|
|
418
|
+
{ value: 'docker', label: 'Docker image' },
|
|
419
|
+
{ value: 'npm', label: 'npm (create-nocobase-app style / published package)' },
|
|
420
|
+
{ value: 'git', label: 'Git clone / monorepo checkout' },
|
|
421
|
+
],
|
|
422
|
+
initialValue: source,
|
|
423
|
+
});
|
|
424
|
+
if (p.isCancel(src)) {
|
|
425
|
+
p.cancel('Install cancelled.');
|
|
426
|
+
this.exit(0);
|
|
427
|
+
}
|
|
428
|
+
source = src;
|
|
429
|
+
}
|
|
430
|
+
let appRootPathFlag = flags['app-root-path'];
|
|
431
|
+
let dockerRegistry = flags['docker-registry'];
|
|
432
|
+
let dockerTag = flags['docker-tag'];
|
|
433
|
+
if (interactive && !hasSavedEnv) {
|
|
434
|
+
if (source === 'docker') {
|
|
435
|
+
const reg = await p.text({
|
|
436
|
+
message: 'Docker image (without tag, e.g. nocobase/nocobase)',
|
|
437
|
+
initialValue: dockerRegistry ?? 'nocobase/nocobase',
|
|
438
|
+
});
|
|
439
|
+
if (p.isCancel(reg)) {
|
|
440
|
+
p.cancel('Install cancelled.');
|
|
441
|
+
this.exit(0);
|
|
442
|
+
}
|
|
443
|
+
dockerRegistry = reg.trim() || dockerRegistry;
|
|
444
|
+
const tag = await p.text({
|
|
445
|
+
message: 'Docker tag',
|
|
446
|
+
initialValue: dockerTag ?? 'latest',
|
|
447
|
+
});
|
|
448
|
+
if (p.isCancel(tag)) {
|
|
449
|
+
p.cancel('Install cancelled.');
|
|
450
|
+
this.exit(0);
|
|
451
|
+
}
|
|
452
|
+
dockerTag = tag.trim() || dockerTag;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (source === 'npm' || source === 'git') {
|
|
456
|
+
appRootPathFlag = await this.resolveNpmGitAppRootWithDownload({
|
|
457
|
+
source,
|
|
458
|
+
interactive,
|
|
459
|
+
hasSavedEnv,
|
|
460
|
+
appRootPathFlag,
|
|
461
|
+
savedEnv,
|
|
462
|
+
fetchSourceFlag: Boolean(flags['fetch-source']),
|
|
463
|
+
downloadVersion: flags['download-version']?.trim() || undefined,
|
|
464
|
+
downloadGitUrl: flags['download-git-url'],
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const appRoot = this.resolveAppRoot(appRootPathFlag, savedEnv?.appRootPath);
|
|
468
|
+
const procEnv = {
|
|
469
|
+
STORAGE_PATH: resolvedStorage,
|
|
470
|
+
APP_PORT: appPort,
|
|
471
|
+
DB_DIALECT: dbDialect,
|
|
472
|
+
DB_HOST: dbHost,
|
|
473
|
+
DB_PORT: dbPort,
|
|
474
|
+
DB_DATABASE: dbDatabase,
|
|
475
|
+
DB_USER: dbUser,
|
|
476
|
+
DB_PASSWORD: dbPassword,
|
|
477
|
+
NOCOBASE_INSTALL_SOURCE: source,
|
|
478
|
+
};
|
|
479
|
+
if (source === 'docker' && (dockerRegistry || dockerTag)) {
|
|
480
|
+
if (dockerRegistry) {
|
|
481
|
+
procEnv.NOCOBASE_DOCKER_REGISTRY = dockerRegistry;
|
|
482
|
+
}
|
|
483
|
+
if (dockerTag) {
|
|
484
|
+
procEnv.NOCOBASE_DOCKER_TAG = dockerTag;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const installOpts = await this.promptNocoBaseInstallOptions(interactive, {
|
|
488
|
+
force: flags.force,
|
|
489
|
+
lang: installLang,
|
|
490
|
+
rootUserName: flags['root-username'],
|
|
491
|
+
rootEmail: flags['root-email'],
|
|
492
|
+
rootPassword: flags['root-password'],
|
|
493
|
+
rootNickname: flags['root-nickname'],
|
|
494
|
+
});
|
|
495
|
+
const argvFlags = {
|
|
496
|
+
source,
|
|
497
|
+
...installOpts,
|
|
498
|
+
};
|
|
499
|
+
if (argvFlags.rootUserName) {
|
|
500
|
+
procEnv.INIT_ROOT_USERNAME = argvFlags.rootUserName;
|
|
501
|
+
}
|
|
502
|
+
if (argvFlags.rootEmail) {
|
|
503
|
+
procEnv.INIT_ROOT_EMAIL = argvFlags.rootEmail;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
await this.runNocoBaseInstall(appRoot, procEnv, argvFlags, interactive);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
510
|
+
if (interactive) {
|
|
511
|
+
p.outro(pc.red(message));
|
|
512
|
+
}
|
|
513
|
+
this.error(message);
|
|
514
|
+
}
|
|
515
|
+
const envPostInstall = await this.runPostInstallEnvCommand({
|
|
516
|
+
hasSavedEnv,
|
|
517
|
+
envName,
|
|
518
|
+
appPort,
|
|
519
|
+
interactive,
|
|
520
|
+
});
|
|
521
|
+
if (envPostInstall === 'failed') {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (interactive) {
|
|
525
|
+
p.outro('Install finished.');
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this.log('Install finished.');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* After a successful `nocobase-v1 install`, refresh or create the CLI env via oclif (`nb env update` / `nb env add`).
|
|
533
|
+
* Failures are logged but do not fail the overall install (artifacts are already on disk).
|
|
534
|
+
*/
|
|
535
|
+
async runPostInstallEnvCommand(options) {
|
|
536
|
+
const { hasSavedEnv, envName, appPort, interactive } = options;
|
|
537
|
+
try {
|
|
538
|
+
if (hasSavedEnv) {
|
|
539
|
+
if (interactive) {
|
|
540
|
+
p.log.step('Running nb env update');
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
this.log('Running nb env update');
|
|
544
|
+
}
|
|
545
|
+
await this.config.runCommand('env:update', [envName, '--scope', 'project']);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
if (interactive) {
|
|
549
|
+
p.log.step('Running nb env add');
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
this.log('Running nb env add');
|
|
553
|
+
}
|
|
554
|
+
const addArgv = interactive
|
|
555
|
+
? [envName, '--scope', 'project']
|
|
556
|
+
: [
|
|
557
|
+
envName,
|
|
558
|
+
'--scope',
|
|
559
|
+
'project',
|
|
560
|
+
'--api-base-url',
|
|
561
|
+
`http://127.0.0.1:${appPort}/api`,
|
|
562
|
+
'--auth-type',
|
|
563
|
+
'oauth',
|
|
564
|
+
];
|
|
565
|
+
await this.config.runCommand('env:add', addArgv);
|
|
566
|
+
}
|
|
567
|
+
return 'ok';
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
+
if (interactive) {
|
|
572
|
+
p.log.warn(`Post-install env command failed: ${message}`);
|
|
573
|
+
p.outro('Install finished.');
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
this.warn(`Post-install env command failed: ${message}`);
|
|
577
|
+
this.log('Install finished.');
|
|
578
|
+
}
|
|
579
|
+
return 'failed';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Interactive prompts for `nocobase-v1 install` options; CLI flags win when already set.
|
|
584
|
+
*/
|
|
585
|
+
async promptNocoBaseInstallOptions(interactive, flags) {
|
|
586
|
+
let force = flags.force;
|
|
587
|
+
let lang = flags.lang;
|
|
588
|
+
let rootUserName = flags.rootUserName;
|
|
589
|
+
let rootEmail = flags.rootEmail;
|
|
590
|
+
let rootPassword = flags.rootPassword;
|
|
591
|
+
let rootNickname = flags.rootNickname;
|
|
592
|
+
if (!interactive) {
|
|
593
|
+
return { force, lang, rootUserName, rootEmail, rootPassword, rootNickname };
|
|
594
|
+
}
|
|
595
|
+
if (!flags.force) {
|
|
596
|
+
const reinstall = await p.confirm({
|
|
597
|
+
message: 'Reinstall and clear the database? (--force)',
|
|
598
|
+
initialValue: false,
|
|
599
|
+
});
|
|
600
|
+
if (p.isCancel(reinstall)) {
|
|
601
|
+
p.cancel('Install cancelled.');
|
|
602
|
+
this.exit(0);
|
|
603
|
+
}
|
|
604
|
+
force = reinstall;
|
|
605
|
+
}
|
|
606
|
+
if (rootUserName === undefined) {
|
|
607
|
+
const userAns = await p.text({
|
|
608
|
+
message: 'Root username (--root-username)',
|
|
609
|
+
placeholder: 'nocobase',
|
|
610
|
+
initialValue: 'nocobase',
|
|
611
|
+
});
|
|
612
|
+
if (p.isCancel(userAns)) {
|
|
613
|
+
p.cancel('Install cancelled.');
|
|
614
|
+
this.exit(0);
|
|
615
|
+
}
|
|
616
|
+
const t = userAns.trim();
|
|
617
|
+
rootUserName = t || undefined;
|
|
618
|
+
}
|
|
619
|
+
if (rootEmail === undefined) {
|
|
620
|
+
const emailAns = await p.text({
|
|
621
|
+
message: 'Root user email (--root-email)',
|
|
622
|
+
placeholder: 'admin@nocobase.com',
|
|
623
|
+
initialValue: 'admin@nocobase.com',
|
|
624
|
+
});
|
|
625
|
+
if (p.isCancel(emailAns)) {
|
|
626
|
+
p.cancel('Install cancelled.');
|
|
627
|
+
this.exit(0);
|
|
628
|
+
}
|
|
629
|
+
const t = emailAns.trim();
|
|
630
|
+
rootEmail = t || undefined;
|
|
631
|
+
}
|
|
632
|
+
if (rootPassword === undefined) {
|
|
633
|
+
const passAns = await p.password({
|
|
634
|
+
message: 'Root user password (--root-password)',
|
|
635
|
+
});
|
|
636
|
+
if (p.isCancel(passAns)) {
|
|
637
|
+
p.cancel('Install cancelled.');
|
|
638
|
+
this.exit(0);
|
|
639
|
+
}
|
|
640
|
+
rootPassword = typeof passAns === 'string' && passAns.length > 0 ? passAns : undefined;
|
|
641
|
+
}
|
|
642
|
+
if (rootNickname === undefined) {
|
|
643
|
+
const nickAns = await p.text({
|
|
644
|
+
message: 'Root user nickname (--root-nickname)',
|
|
645
|
+
placeholder: 'Super Admin',
|
|
646
|
+
});
|
|
647
|
+
if (p.isCancel(nickAns)) {
|
|
648
|
+
p.cancel('Install cancelled.');
|
|
649
|
+
this.exit(0);
|
|
650
|
+
}
|
|
651
|
+
const t = nickAns.trim();
|
|
652
|
+
rootNickname = t || undefined;
|
|
653
|
+
}
|
|
654
|
+
return { force, lang, rootUserName, rootEmail, rootPassword, rootNickname };
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Build argv for `nocobase-v1 install` from CLI flags.
|
|
658
|
+
*/
|
|
659
|
+
buildNocoBaseInstallArgs(flags) {
|
|
31
660
|
const npmArgs = ['install'];
|
|
32
661
|
if (flags.force) {
|
|
33
662
|
npmArgs.push('--force');
|
|
@@ -35,6 +664,9 @@ export default class Install extends Command {
|
|
|
35
664
|
if (flags.lang !== undefined) {
|
|
36
665
|
npmArgs.push('--lang', flags.lang);
|
|
37
666
|
}
|
|
667
|
+
if (flags.rootUserName !== undefined) {
|
|
668
|
+
npmArgs.push('--root-username', flags.rootUserName);
|
|
669
|
+
}
|
|
38
670
|
if (flags.rootEmail !== undefined) {
|
|
39
671
|
npmArgs.push('--root-email', flags.rootEmail);
|
|
40
672
|
}
|
|
@@ -44,12 +676,28 @@ export default class Install extends Command {
|
|
|
44
676
|
if (flags.rootNickname !== undefined) {
|
|
45
677
|
npmArgs.push('--root-nickname', flags.rootNickname);
|
|
46
678
|
}
|
|
47
|
-
|
|
48
|
-
|
|
679
|
+
return npmArgs;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Execute `nocobase-v1` in the application root. Add pre/post steps or source-specific behavior here.
|
|
683
|
+
*/
|
|
684
|
+
async runNocoBaseInstall(appRoot, procEnv, flags, interactive) {
|
|
685
|
+
const rel = path.relative(process.cwd(), appRoot);
|
|
686
|
+
const where = rel === '' || rel === '.'
|
|
687
|
+
? 'this folder'
|
|
688
|
+
: rel.startsWith('..')
|
|
689
|
+
? appRoot
|
|
690
|
+
: `./${rel.split(path.sep).join('/')}`;
|
|
691
|
+
const label = `Installing NocoBase — this may take a few minutes.`;
|
|
692
|
+
if (interactive) {
|
|
693
|
+
p.log.step(label);
|
|
49
694
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.error(message);
|
|
695
|
+
else {
|
|
696
|
+
this.log(label);
|
|
53
697
|
}
|
|
698
|
+
const npmArgs = this.buildNocoBaseInstallArgs(flags);
|
|
699
|
+
// TODO: Re-enable when the app root has a runnable `nocobase-v1 install` (or swap to the supported entrypoint).
|
|
700
|
+
// await runNocoBaseCommand(npmArgs, { cwd: appRoot, env: procEnv });
|
|
701
|
+
void npmArgs;
|
|
54
702
|
}
|
|
55
703
|
}
|