@nocobase/cli 2.1.0-alpha.20 → 2.1.0-alpha.21
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 +256 -89
- package/README.zh-CN.md +332 -0
- package/bin/run.js +21 -2
- package/dist/commands/build.js +7 -1
- package/dist/commands/db/logs.js +85 -0
- package/dist/commands/db/ps.js +60 -0
- package/dist/commands/db/shared.js +81 -0
- package/dist/commands/db/start.js +55 -7
- package/dist/commands/db/stop.js +70 -0
- package/dist/commands/dev.js +112 -21
- package/dist/commands/down.js +193 -0
- package/dist/commands/download.js +622 -183
- package/dist/commands/env/add.js +233 -131
- package/dist/commands/env/auth.js +9 -8
- package/dist/commands/init.js +696 -103
- package/dist/commands/install.js +1588 -566
- package/dist/commands/logs.js +90 -0
- package/dist/commands/pm/disable.js +35 -3
- package/dist/commands/pm/enable.js +35 -3
- package/dist/commands/pm/list.js +37 -4
- package/dist/commands/prompts-stages.js +144 -0
- package/dist/commands/prompts-test.js +175 -0
- package/dist/commands/ps.js +116 -0
- package/dist/commands/start.js +171 -15
- package/dist/commands/stop.js +90 -0
- package/dist/commands/upgrade.js +559 -11
- package/dist/lib/app-runtime.js +142 -0
- package/dist/lib/auth-store.js +44 -3
- package/dist/lib/bootstrap.js +7 -3
- package/dist/lib/env-auth.js +427 -82
- package/dist/lib/prompt-catalog.js +552 -0
- package/dist/lib/prompt-validators.js +184 -0
- package/dist/lib/prompt-web-ui.js +2027 -0
- package/dist/lib/run-npm.js +71 -7
- package/package.json +3 -3
- package/dist/commands/restart.js +0 -32
- package/dist/lib/init-browser-wizard.js +0 -431
package/dist/commands/install.js
CHANGED
|
@@ -8,696 +8,1718 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Command, Flags } from '@oclif/core';
|
|
10
10
|
import * as p from '@clack/prompts';
|
|
11
|
-
import
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import { mkdir } from 'node:fs/promises';
|
|
12
15
|
import path from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { exit } from 'node:process';
|
|
17
|
+
import { runPromptCatalog, } from "../lib/prompt-catalog.js";
|
|
18
|
+
import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
|
|
19
|
+
import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
|
|
20
|
+
import { run, runNocoBaseCommand } from '../lib/run-npm.js';
|
|
21
|
+
import { startTask, stopTask, updateTask } from '../lib/ui.js';
|
|
22
|
+
import { ensureWorkspaceName, getEnv } from '../lib/auth-store.js';
|
|
23
|
+
import Download, { defaultDockerRegistryForLang, } from './download.js';
|
|
24
|
+
import EnvAdd from "./env/add.js";
|
|
15
25
|
const DEFAULT_INSTALL_ENV_NAME = 'local';
|
|
16
|
-
const DEFAULT_INSTALL_SOURCE = 'docker';
|
|
17
26
|
const DEFAULT_INSTALL_LANG = 'en-US';
|
|
27
|
+
const DEFAULT_INSTALL_APP_PORT = '13000';
|
|
28
|
+
const DEFAULT_INSTALL_DB_HOST = '127.0.0.1';
|
|
29
|
+
const DEFAULT_INSTALL_BUILTIN_DB_HOST = 'postgres';
|
|
30
|
+
const DEFAULT_INSTALL_DB_PORTS = {
|
|
31
|
+
postgres: '5432',
|
|
32
|
+
mysql: '3306',
|
|
33
|
+
mariadb: '3306',
|
|
34
|
+
kingbase: '54321',
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_INSTALL_DB_DATABASE = 'nocobase';
|
|
37
|
+
const DEFAULT_INSTALL_DB_USER = 'nocobase';
|
|
38
|
+
const DEFAULT_INSTALL_DB_PASSWORD = 'nocobase';
|
|
39
|
+
const DEFAULT_INSTALL_ROOT_USERNAME = 'nocobase';
|
|
40
|
+
const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@example.com';
|
|
41
|
+
const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
|
|
42
|
+
const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
|
|
43
|
+
const CONFIG_SCOPE = 'project';
|
|
44
|
+
const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
|
|
45
|
+
const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
|
|
46
|
+
const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
|
|
47
|
+
const INSTALL_DB_DIALECTS = ['postgres', 'mysql', 'mariadb', 'kingbase'];
|
|
48
|
+
const INSTALL_LANGUAGE_CODES = {
|
|
49
|
+
'ar-EG': { label: 'العربية' },
|
|
50
|
+
'az-AZ': { label: 'Azərbaycan dili' },
|
|
51
|
+
'bg-BG': { label: 'Български' },
|
|
52
|
+
'bn-BD': { label: 'Bengali' },
|
|
53
|
+
'by-BY': { label: 'Беларускі' },
|
|
54
|
+
'ca-ES': { label: 'Сatalà/Espanya' },
|
|
55
|
+
'cs-CZ': { label: 'Česky' },
|
|
56
|
+
'da-DK': { label: 'Dansk' },
|
|
57
|
+
'de-DE': { label: 'Deutsch' },
|
|
58
|
+
'el-GR': { label: 'Ελληνικά' },
|
|
59
|
+
'en-GB': { label: 'English(GB)' },
|
|
60
|
+
'en-US': { label: 'English' },
|
|
61
|
+
'es-ES': { label: 'Español' },
|
|
62
|
+
'et-EE': { label: 'Estonian (Eesti)' },
|
|
63
|
+
'fa-IR': { label: 'فارسی' },
|
|
64
|
+
'fi-FI': { label: 'Suomi' },
|
|
65
|
+
'fr-BE': { label: 'Français(BE)' },
|
|
66
|
+
'fr-CA': { label: 'Français(CA)' },
|
|
67
|
+
'fr-FR': { label: 'Français' },
|
|
68
|
+
'ga-IE': { label: 'Gaeilge' },
|
|
69
|
+
'gl-ES': { label: 'Galego' },
|
|
70
|
+
'he-IL': { label: 'עברית' },
|
|
71
|
+
'hi-IN': { label: 'हिन्दी' },
|
|
72
|
+
'hr-HR': { label: 'Hrvatski jezik' },
|
|
73
|
+
'hu-HU': { label: 'Magyar' },
|
|
74
|
+
'hy-AM': { label: 'Հայերեն' },
|
|
75
|
+
'id-ID': { label: 'Bahasa Indonesia' },
|
|
76
|
+
'is-IS': { label: 'Íslenska' },
|
|
77
|
+
'it-IT': { label: 'Italiano' },
|
|
78
|
+
'ja-JP': { label: '日本語' },
|
|
79
|
+
'ka-GE': { label: 'ქართული' },
|
|
80
|
+
'kk-KZ': { label: 'Қазақ тілі' },
|
|
81
|
+
'km-KH': { label: 'ភាសាខ្មែរ' },
|
|
82
|
+
'kn-IN': { label: 'ಕನ್ನಡ' },
|
|
83
|
+
'ko-KR': { label: '한국어' },
|
|
84
|
+
'ku-IQ': { label: 'کوردی' },
|
|
85
|
+
'lt-LT': { label: 'lietuvių' },
|
|
86
|
+
'lv-LV': { label: 'Latviešu valoda' },
|
|
87
|
+
'mk-MK': { label: 'македонски јазик' },
|
|
88
|
+
'ml-IN': { label: 'മലയാളം' },
|
|
89
|
+
'mn-MN': { label: 'Монгол хэл' },
|
|
90
|
+
'ms-MY': { label: 'بهاس ملايو' },
|
|
91
|
+
'nb-NO': { label: 'Norsk bokmål' },
|
|
92
|
+
'ne-NP': { label: 'नेपाली' },
|
|
93
|
+
'nl-BE': { label: 'Vlaams' },
|
|
94
|
+
'nl-NL': { label: 'Nederlands' },
|
|
95
|
+
'pl-PL': { label: 'Polski' },
|
|
96
|
+
'pt-BR': { label: 'Português brasileiro' },
|
|
97
|
+
'pt-PT': { label: 'Português' },
|
|
98
|
+
'ro-RO': { label: 'România' },
|
|
99
|
+
'ru-RU': { label: 'Русский' },
|
|
100
|
+
'si-LK': { label: 'සිංහල' },
|
|
101
|
+
'sk-SK': { label: 'Slovenčina' },
|
|
102
|
+
'sl-SI': { label: 'Slovenščina' },
|
|
103
|
+
'sr-RS': { label: 'српски језик' },
|
|
104
|
+
'sv-SE': { label: 'Svenska' },
|
|
105
|
+
'ta-IN': { label: 'Tamil' },
|
|
106
|
+
'th-TH': { label: 'ภาษาไทย' },
|
|
107
|
+
'tk-TK': { label: 'Turkmen' },
|
|
108
|
+
'tr-TR': { label: 'Türkçe' },
|
|
109
|
+
'uk-UA': { label: 'Українська' },
|
|
110
|
+
'ur-PK': { label: 'Oʻzbekcha' },
|
|
111
|
+
'vi-VN': { label: 'Tiếng Việt' },
|
|
112
|
+
'zh-CN': { label: '简体中文' },
|
|
113
|
+
'zh-HK': { label: '繁體中文(香港)' },
|
|
114
|
+
'zh-TW': { label: '繁體中文(台湾)' },
|
|
115
|
+
};
|
|
116
|
+
const INSTALL_LANGUAGE_OPTIONS = Object.entries(INSTALL_LANGUAGE_CODES).map(([value, { label }]) => ({
|
|
117
|
+
value,
|
|
118
|
+
label: `${label} (${value})`,
|
|
119
|
+
}));
|
|
120
|
+
function argvHasToken(argv, tokens) {
|
|
121
|
+
return tokens.some((t) => argv.includes(t));
|
|
122
|
+
}
|
|
123
|
+
function isInstallDbDialect(value) {
|
|
124
|
+
return INSTALL_DB_DIALECTS.includes(value);
|
|
125
|
+
}
|
|
126
|
+
export function defaultDbPortForDialect(value) {
|
|
127
|
+
const dialect = String(value ?? 'postgres').trim();
|
|
128
|
+
return DEFAULT_INSTALL_DB_PORTS[isInstallDbDialect(dialect) ? dialect : 'postgres'];
|
|
129
|
+
}
|
|
130
|
+
function defaultDbHostForBuiltinDb(values) {
|
|
131
|
+
return Boolean(values.builtinDb)
|
|
132
|
+
? DEFAULT_INSTALL_BUILTIN_DB_HOST
|
|
133
|
+
: DEFAULT_INSTALL_DB_HOST;
|
|
134
|
+
}
|
|
135
|
+
function defaultInstallAppRootPath(envName) {
|
|
136
|
+
const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
|
|
137
|
+
return `./${name}/source/`;
|
|
138
|
+
}
|
|
139
|
+
function defaultInstallStoragePath(envName) {
|
|
140
|
+
const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
|
|
141
|
+
return `./${name}/storage/`;
|
|
142
|
+
}
|
|
143
|
+
function pickPresetKeys(source, keys) {
|
|
144
|
+
const out = {};
|
|
145
|
+
for (const k of keys) {
|
|
146
|
+
if (Object.prototype.hasOwnProperty.call(source, k)) {
|
|
147
|
+
out[k] = source[k];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
async function commandSucceeds(command, args, options) {
|
|
153
|
+
return await new Promise((resolve) => {
|
|
154
|
+
const child = spawn(command, args, {
|
|
155
|
+
cwd: options?.cwd,
|
|
156
|
+
env: {
|
|
157
|
+
...process.env,
|
|
158
|
+
...options?.env,
|
|
159
|
+
},
|
|
160
|
+
stdio: 'ignore',
|
|
161
|
+
});
|
|
162
|
+
child.once('error', () => resolve(false));
|
|
163
|
+
child.once('close', (code) => resolve(code === 0));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function commandOutput(command, args, options) {
|
|
167
|
+
return await new Promise((resolve, reject) => {
|
|
168
|
+
const child = spawn(command, args, {
|
|
169
|
+
cwd: options?.cwd,
|
|
170
|
+
env: {
|
|
171
|
+
...process.env,
|
|
172
|
+
...options?.env,
|
|
173
|
+
},
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
});
|
|
176
|
+
let stdout = '';
|
|
177
|
+
let stderr = '';
|
|
178
|
+
child.stdout.setEncoding('utf8');
|
|
179
|
+
child.stderr.setEncoding('utf8');
|
|
180
|
+
child.stdout.on('data', (chunk) => {
|
|
181
|
+
stdout += chunk;
|
|
182
|
+
});
|
|
183
|
+
child.stderr.on('data', (chunk) => {
|
|
184
|
+
stderr += chunk;
|
|
185
|
+
});
|
|
186
|
+
child.once('error', reject);
|
|
187
|
+
child.once('close', (code, signal) => {
|
|
188
|
+
if (code === 0) {
|
|
189
|
+
resolve(stdout);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (signal) {
|
|
193
|
+
reject(new Error(`${command} exited due to signal ${signal}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
18
200
|
export default class Install extends Command {
|
|
19
|
-
static description = '
|
|
201
|
+
static description = 'Install NocoBase: database, storage, admin user, and `nocobase-v1 install`. Optionally run `nb download` first; distribution and image details are configured on `nb download`, not here. Use `--resume` to continue an interrupted setup from the saved workspace env config.';
|
|
20
202
|
static examples = [
|
|
21
203
|
'<%= config.bin %> <%= command.id %>',
|
|
22
|
-
'<%= config.bin %> <%= command.id %>
|
|
23
|
-
'<%= config.bin %> <%= command.id %>
|
|
24
|
-
'<%= config.bin %> <%= command.id %>
|
|
25
|
-
'<%= config.bin %> <%= command.id %> -
|
|
26
|
-
'<%= config.bin %> <%= command.id %> --
|
|
27
|
-
'<%= config.bin %> <%= command.id %> --
|
|
28
|
-
'<%= config.bin %> <%= command.id %>
|
|
29
|
-
'<%= config.bin %> <%= command.id %>
|
|
204
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
205
|
+
'<%= config.bin %> <%= command.id %> --env app1 --resume',
|
|
206
|
+
'<%= config.bin %> <%= command.id %> --env app1 -f',
|
|
207
|
+
'<%= config.bin %> <%= command.id %> --env app1 -l zh-CN',
|
|
208
|
+
'<%= config.bin %> <%= command.id %> --env app1 --root-username nocobase --root-email admin@nocobase.com --root-password admin123',
|
|
209
|
+
'<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
|
|
210
|
+
'<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
|
|
211
|
+
'<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
|
|
212
|
+
'<%= config.bin %> <%= command.id %> --env dev -y --fetch-source --app-root-path=./dev/source/',
|
|
30
213
|
];
|
|
31
214
|
static flags = {
|
|
32
|
-
source: Flags.string({
|
|
33
|
-
description: `Where to obtain the NocoBase package (default: ${DEFAULT_INSTALL_SOURCE})`,
|
|
34
|
-
options: ['git', 'npm', 'docker'],
|
|
35
|
-
}),
|
|
36
215
|
yes: Flags.boolean({
|
|
37
216
|
char: 'y',
|
|
38
217
|
description: 'Skip interactive prompts; use flags and defaults only',
|
|
39
218
|
default: false,
|
|
40
219
|
}),
|
|
220
|
+
resume: Flags.boolean({
|
|
221
|
+
description: 'Resume a previous unfinished setup for this env using the saved workspace env config',
|
|
222
|
+
default: false,
|
|
223
|
+
}),
|
|
41
224
|
env: Flags.string({
|
|
42
225
|
char: 'e',
|
|
43
|
-
description: '
|
|
226
|
+
description: 'App/env name to create or update. Defaults app paths to ./<envName>/source/ and ./<envName>/storage/.',
|
|
227
|
+
}),
|
|
228
|
+
lang: Flags.string({ description: 'Language for the installed NocoBase app', char: 'l', required: false }),
|
|
229
|
+
force: Flags.boolean({
|
|
230
|
+
description: 'Reconfigure an existing env and replace conflicting runtime resources when needed',
|
|
231
|
+
char: 'f',
|
|
232
|
+
required: false,
|
|
233
|
+
}),
|
|
234
|
+
'app-root-path': Flags.string({
|
|
235
|
+
description: 'Source directory for a local npm/git app (default: ./<envName>/source/)',
|
|
236
|
+
}),
|
|
237
|
+
'app-port': Flags.string({
|
|
238
|
+
description: 'HTTP port for the local app (default: 13000, or an available port with --yes)',
|
|
239
|
+
}),
|
|
240
|
+
'storage-path': Flags.string({
|
|
241
|
+
description: 'Storage directory for uploads and managed database data (default: ./<envName>/storage/)',
|
|
44
242
|
}),
|
|
45
|
-
force: Flags.boolean({ description: 'Reinstall the application by clearing the database', char: 'f', required: false }),
|
|
46
|
-
lang: Flags.string({ description: 'Language during installation', char: 'l', required: false }),
|
|
47
243
|
'root-username': Flags.string({
|
|
48
|
-
|
|
49
|
-
description: 'Root username (sets INIT_ROOT_USERNAME for install; forwarded as --root-username)',
|
|
244
|
+
description: 'Initial admin username for the installed app',
|
|
50
245
|
required: false,
|
|
51
246
|
}),
|
|
52
247
|
'root-email': Flags.string({
|
|
53
|
-
|
|
54
|
-
description: 'Root user email (sets INIT_ROOT_EMAIL for install; forwarded as --root-email)',
|
|
248
|
+
description: 'Initial admin email for the installed app',
|
|
55
249
|
required: false,
|
|
56
250
|
}),
|
|
57
251
|
'root-password': Flags.string({
|
|
58
|
-
|
|
59
|
-
description: 'Root user password (forwarded as --root-password)',
|
|
252
|
+
description: 'Initial admin password for the installed app',
|
|
60
253
|
required: false,
|
|
61
254
|
}),
|
|
62
255
|
'root-nickname': Flags.string({
|
|
63
|
-
|
|
64
|
-
description: 'Root user nickname (forwarded as --root-nickname)',
|
|
256
|
+
description: 'Initial admin display name for the installed app',
|
|
65
257
|
required: false,
|
|
66
258
|
}),
|
|
67
|
-
'
|
|
68
|
-
description: '
|
|
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)',
|
|
259
|
+
'builtin-db': Flags.boolean({
|
|
260
|
+
description: 'Create and connect a CLI-managed built-in database for the app',
|
|
261
|
+
default: false,
|
|
75
262
|
}),
|
|
76
263
|
'db-dialect': Flags.string({
|
|
77
|
-
description: 'Database dialect
|
|
264
|
+
description: 'Database dialect for the app',
|
|
78
265
|
options: ['postgres', 'mysql', 'mariadb', 'kingbase'],
|
|
79
266
|
}),
|
|
80
267
|
'db-host': Flags.string({
|
|
81
|
-
description: 'Database host',
|
|
268
|
+
description: 'Database host for the app',
|
|
82
269
|
}),
|
|
83
270
|
'db-port': Flags.string({
|
|
84
|
-
description: 'Database port',
|
|
271
|
+
description: 'Database port for the app',
|
|
85
272
|
}),
|
|
86
273
|
'db-database': Flags.string({
|
|
87
|
-
description: 'Database name',
|
|
274
|
+
description: 'Database name for the app',
|
|
88
275
|
}),
|
|
89
276
|
'db-user': Flags.string({
|
|
90
|
-
description: 'Database
|
|
277
|
+
description: 'Database username for the app',
|
|
91
278
|
}),
|
|
92
279
|
'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,
|
|
280
|
+
description: 'Database password for the app',
|
|
104
281
|
}),
|
|
105
282
|
'fetch-source': Flags.boolean({
|
|
106
|
-
description: '
|
|
283
|
+
description: 'Download NocoBase app files or pull a Docker image before installing',
|
|
107
284
|
default: false,
|
|
108
285
|
}),
|
|
109
|
-
'
|
|
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
|
-
}),
|
|
286
|
+
..._.omit(Download.flags, ['yes']),
|
|
115
287
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
288
|
+
/** Environment name only: run before {@link Install.prompts} (see `run`). */
|
|
289
|
+
static envPrompts = {
|
|
290
|
+
env: {
|
|
291
|
+
type: 'text',
|
|
292
|
+
message: 'What would you like to call this app?',
|
|
293
|
+
placeholder: DEFAULT_INSTALL_ENV_NAME,
|
|
294
|
+
required: true,
|
|
295
|
+
validate: validateEnvKey,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
static appPrompts = {
|
|
299
|
+
lang: {
|
|
300
|
+
type: 'select',
|
|
301
|
+
message: 'Which language would you like to use?',
|
|
302
|
+
options: INSTALL_LANGUAGE_OPTIONS,
|
|
303
|
+
initialValue: DEFAULT_INSTALL_LANG,
|
|
304
|
+
yesInitialValue: DEFAULT_INSTALL_LANG,
|
|
305
|
+
},
|
|
306
|
+
// force: {
|
|
307
|
+
// type: 'boolean',
|
|
308
|
+
// message: 'Reinstall the application by clearing the database? (-f / --force)',
|
|
309
|
+
// initialValue: false,
|
|
310
|
+
// yesInitialValue: false,
|
|
311
|
+
// },
|
|
312
|
+
appRootPath: {
|
|
313
|
+
type: 'text',
|
|
314
|
+
message: 'Where should this app be stored?',
|
|
315
|
+
placeholder: './<env>/source/',
|
|
316
|
+
initialValue: (values) => defaultInstallAppRootPath(values.env ?? values.appName),
|
|
317
|
+
},
|
|
318
|
+
appPort: {
|
|
319
|
+
type: 'text',
|
|
320
|
+
message: 'Which port should this app use?',
|
|
321
|
+
placeholder: DEFAULT_INSTALL_APP_PORT,
|
|
322
|
+
validate: validateAvailableTcpPort,
|
|
323
|
+
},
|
|
324
|
+
storagePath: {
|
|
325
|
+
type: 'text',
|
|
326
|
+
message: 'Where should uploads and local files be stored?',
|
|
327
|
+
placeholder: './<env>/storage/',
|
|
328
|
+
initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
|
|
329
|
+
},
|
|
330
|
+
fetchSource: {
|
|
331
|
+
type: 'boolean',
|
|
332
|
+
message: 'Download NocoBase automatically if the app directory is empty?',
|
|
333
|
+
initialValue: true,
|
|
334
|
+
yesInitialValue: true,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
static dbPrompts = {
|
|
338
|
+
builtinDb: {
|
|
339
|
+
type: 'boolean',
|
|
340
|
+
message: "Would you like to use the built-in database?",
|
|
341
|
+
initialValue: true,
|
|
342
|
+
yesInitialValue: true,
|
|
343
|
+
},
|
|
344
|
+
dbDialect: {
|
|
345
|
+
type: 'select',
|
|
346
|
+
message: 'Which database would you like to use?',
|
|
347
|
+
options: [
|
|
348
|
+
{ value: 'postgres', label: 'PostgreSQL' },
|
|
349
|
+
{ value: 'mysql', label: 'MySQL' },
|
|
350
|
+
{ value: 'mariadb', label: 'MariaDB' },
|
|
351
|
+
{ value: 'kingbase', label: 'KingbaseES' },
|
|
352
|
+
],
|
|
353
|
+
initialValue: 'postgres',
|
|
354
|
+
yesInitialValue: 'postgres',
|
|
355
|
+
required: true,
|
|
356
|
+
},
|
|
357
|
+
dbHost: {
|
|
358
|
+
type: 'text',
|
|
359
|
+
message: 'What is the database host?',
|
|
360
|
+
placeholder: DEFAULT_INSTALL_DB_HOST,
|
|
361
|
+
initialValue: (values) => defaultDbHostForBuiltinDb(values),
|
|
362
|
+
yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
|
|
363
|
+
required: true,
|
|
364
|
+
hidden: (values) => Boolean(values.builtinDb),
|
|
365
|
+
},
|
|
366
|
+
dbPort: {
|
|
367
|
+
type: 'text',
|
|
368
|
+
message: 'What is the database port?',
|
|
369
|
+
placeholder: DEFAULT_INSTALL_DB_PORTS.postgres,
|
|
370
|
+
initialValue: (values) => defaultDbPortForDialect(values.dbDialect),
|
|
371
|
+
required: true,
|
|
372
|
+
validate: validateTcpPort,
|
|
373
|
+
hidden: (values) => Boolean(values.builtinDb)
|
|
374
|
+
&& String(values.source ?? '').trim() === 'docker',
|
|
375
|
+
},
|
|
376
|
+
dbDatabase: {
|
|
377
|
+
type: 'text',
|
|
378
|
+
message: 'What is the database name?',
|
|
379
|
+
initialValue: DEFAULT_INSTALL_DB_DATABASE,
|
|
380
|
+
yesInitialValue: DEFAULT_INSTALL_DB_DATABASE,
|
|
381
|
+
required: true,
|
|
382
|
+
},
|
|
383
|
+
dbUser: {
|
|
384
|
+
type: 'text',
|
|
385
|
+
message: 'What is the database username?',
|
|
386
|
+
initialValue: DEFAULT_INSTALL_DB_USER,
|
|
387
|
+
yesInitialValue: DEFAULT_INSTALL_DB_USER,
|
|
388
|
+
required: true,
|
|
389
|
+
},
|
|
390
|
+
dbPassword: {
|
|
391
|
+
type: 'password',
|
|
392
|
+
message: 'What is the database password?',
|
|
393
|
+
initialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
394
|
+
yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
395
|
+
required: true,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
static rootUserPrompts = {
|
|
399
|
+
rootUsername: {
|
|
400
|
+
type: 'text',
|
|
401
|
+
message: 'Choose the initial admin username',
|
|
402
|
+
placeholder: DEFAULT_INSTALL_ROOT_USERNAME,
|
|
403
|
+
initialValue: DEFAULT_INSTALL_ROOT_USERNAME,
|
|
404
|
+
yesInitialValue: DEFAULT_INSTALL_ROOT_USERNAME,
|
|
405
|
+
},
|
|
406
|
+
rootEmail: {
|
|
407
|
+
type: 'text',
|
|
408
|
+
message: 'What is the initial admin email?',
|
|
409
|
+
placeholder: DEFAULT_INSTALL_ROOT_EMAIL,
|
|
410
|
+
initialValue: DEFAULT_INSTALL_ROOT_EMAIL,
|
|
411
|
+
yesInitialValue: DEFAULT_INSTALL_ROOT_EMAIL,
|
|
412
|
+
},
|
|
413
|
+
rootPassword: {
|
|
414
|
+
type: 'password',
|
|
415
|
+
message: 'Choose the initial admin password',
|
|
416
|
+
initialValue: DEFAULT_INSTALL_ROOT_PASSWORD,
|
|
417
|
+
yesInitialValue: DEFAULT_INSTALL_ROOT_PASSWORD,
|
|
418
|
+
},
|
|
419
|
+
rootNickname: {
|
|
420
|
+
type: 'text',
|
|
421
|
+
message: 'What display name should the initial admin use?',
|
|
422
|
+
placeholder: DEFAULT_INSTALL_ROOT_NICKNAME,
|
|
423
|
+
initialValue: DEFAULT_INSTALL_ROOT_NICKNAME,
|
|
424
|
+
yesInitialValue: DEFAULT_INSTALL_ROOT_NICKNAME,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* App catalog with `env` seeded into `out` first so `storagePath`’s `initialValue(values)`
|
|
429
|
+
* sees `values.env` (same iteration order as {@link runPromptCatalog}).
|
|
430
|
+
*/
|
|
431
|
+
static buildAppPromptsCatalog(seedEnv) {
|
|
432
|
+
return {
|
|
433
|
+
seedEnv: {
|
|
434
|
+
type: 'run',
|
|
435
|
+
run: (values) => {
|
|
436
|
+
values.env = seedEnv;
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
...Install.appPrompts,
|
|
440
|
+
};
|
|
121
441
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
442
|
+
static buildDbPromptsCatalog(downloadResults) {
|
|
443
|
+
const source = String(downloadResults.source ?? '').trim();
|
|
444
|
+
return {
|
|
445
|
+
seedDownloadSource: {
|
|
446
|
+
type: 'run',
|
|
447
|
+
run: (values) => {
|
|
448
|
+
if (source) {
|
|
449
|
+
values.source = source;
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
...Install.dbPrompts,
|
|
454
|
+
};
|
|
130
455
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
456
|
+
/** Preset for {@link Install.envPrompts} only (`env` flag). */
|
|
457
|
+
static buildEnvPresetValuesFromFlags(flags) {
|
|
458
|
+
const preset = {};
|
|
459
|
+
if (flags.env !== undefined && String(flags.env).trim() !== '') {
|
|
460
|
+
preset.env = String(flags.env).trim();
|
|
134
461
|
}
|
|
135
|
-
|
|
136
|
-
return path.isAbsolute(saved) ? saved : path.resolve(process.cwd(), saved);
|
|
137
|
-
}
|
|
138
|
-
return path.resolve(process.cwd(), 'storage', envName);
|
|
462
|
+
return preset;
|
|
139
463
|
}
|
|
140
464
|
/**
|
|
141
|
-
*
|
|
465
|
+
* Preset `values` for `runPromptCatalog`: keys here skip that prompt and fix the result.
|
|
466
|
+
* Booleans with defaults are only preset when the user passed the flag on argv (see `download`).
|
|
467
|
+
* Does not include `env` — use {@link buildEnvPresetValuesFromFlags} for {@link Install.envPrompts}.
|
|
142
468
|
*/
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
469
|
+
static buildPresetValuesFromFlags(flags) {
|
|
470
|
+
const preset = {};
|
|
471
|
+
const argv = process.argv.slice(2);
|
|
472
|
+
if (flags.lang !== undefined) {
|
|
473
|
+
const v = String(flags.lang).trim();
|
|
474
|
+
if (v) {
|
|
475
|
+
preset.lang = v;
|
|
476
|
+
}
|
|
150
477
|
}
|
|
151
|
-
|
|
152
|
-
|
|
478
|
+
if (argvHasToken(argv, ['--force', '-f'])) {
|
|
479
|
+
preset.force = flags.force;
|
|
153
480
|
}
|
|
154
|
-
|
|
155
|
-
|
|
481
|
+
if (flags['app-root-path'] !== undefined) {
|
|
482
|
+
const v = flags['app-root-path']?.trim();
|
|
483
|
+
if (v) {
|
|
484
|
+
preset.appRootPath = v;
|
|
485
|
+
}
|
|
156
486
|
}
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
if (p.isCancel(fetchAns)) {
|
|
163
|
-
p.cancel('Install cancelled.');
|
|
164
|
-
this.exit(0);
|
|
487
|
+
if (flags['app-port'] !== undefined) {
|
|
488
|
+
const v = String(flags['app-port'] ?? '').trim();
|
|
489
|
+
if (v) {
|
|
490
|
+
preset.appPort = v;
|
|
165
491
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
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;
|
|
492
|
+
}
|
|
493
|
+
if (flags['storage-path'] !== undefined) {
|
|
494
|
+
const v = flags['storage-path']?.trim();
|
|
495
|
+
if (v) {
|
|
496
|
+
preset.storagePath = v;
|
|
180
497
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
498
|
+
}
|
|
499
|
+
if (flags['root-username'] !== undefined) {
|
|
500
|
+
preset.rootUsername = String(flags['root-username'] ?? '').trim();
|
|
501
|
+
}
|
|
502
|
+
if (flags['root-email'] !== undefined) {
|
|
503
|
+
preset.rootEmail = String(flags['root-email'] ?? '').trim();
|
|
504
|
+
}
|
|
505
|
+
if (flags['root-password'] !== undefined) {
|
|
506
|
+
preset.rootPassword = String(flags['root-password'] ?? '');
|
|
507
|
+
}
|
|
508
|
+
if (flags['root-nickname'] !== undefined) {
|
|
509
|
+
preset.rootNickname = String(flags['root-nickname'] ?? '').trim();
|
|
510
|
+
}
|
|
511
|
+
if (argvHasToken(argv, ['--fetch-source'])) {
|
|
512
|
+
preset.fetchSource = flags['fetch-source'];
|
|
513
|
+
}
|
|
514
|
+
if (argvHasToken(argv, ['--builtin-db'])) {
|
|
515
|
+
preset.builtinDb = flags['builtin-db'];
|
|
516
|
+
}
|
|
517
|
+
if (flags['db-dialect'] !== undefined) {
|
|
518
|
+
const t = String(flags['db-dialect']).trim();
|
|
519
|
+
if (t && isInstallDbDialect(t)) {
|
|
520
|
+
preset.dbDialect = t;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (flags['db-host'] !== undefined) {
|
|
524
|
+
const v = String(flags['db-host'] ?? '').trim();
|
|
525
|
+
if (v) {
|
|
526
|
+
preset.dbHost = v;
|
|
195
527
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
528
|
+
}
|
|
529
|
+
if (flags['db-port'] !== undefined) {
|
|
530
|
+
const v = String(flags['db-port'] ?? '').trim();
|
|
531
|
+
if (v) {
|
|
532
|
+
preset.dbPort = v;
|
|
199
533
|
}
|
|
200
534
|
}
|
|
201
|
-
|
|
202
|
-
const v =
|
|
535
|
+
if (flags['db-database'] !== undefined) {
|
|
536
|
+
const v = String(flags['db-database'] ?? '').trim();
|
|
203
537
|
if (v) {
|
|
204
|
-
|
|
538
|
+
preset.dbDatabase = v;
|
|
205
539
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
540
|
+
}
|
|
541
|
+
if (flags['db-user'] !== undefined) {
|
|
542
|
+
const v = String(flags['db-user'] ?? '').trim();
|
|
543
|
+
if (v) {
|
|
544
|
+
preset.dbUser = v;
|
|
209
545
|
}
|
|
210
|
-
|
|
211
|
-
|
|
546
|
+
}
|
|
547
|
+
if (flags['db-password'] !== undefined) {
|
|
548
|
+
preset.dbPassword = String(flags['db-password'] ?? '');
|
|
549
|
+
}
|
|
550
|
+
return preset;
|
|
551
|
+
}
|
|
552
|
+
static buildAppPresetValuesFromFlags(flags) {
|
|
553
|
+
return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
|
|
554
|
+
'lang',
|
|
555
|
+
'force',
|
|
556
|
+
'appRootPath',
|
|
557
|
+
'appPort',
|
|
558
|
+
'storagePath',
|
|
559
|
+
'fetchSource',
|
|
560
|
+
]);
|
|
561
|
+
}
|
|
562
|
+
static buildDbPresetValuesFromFlags(flags) {
|
|
563
|
+
return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
|
|
564
|
+
'builtinDb',
|
|
565
|
+
'dbDialect',
|
|
566
|
+
'dbHost',
|
|
567
|
+
'dbPort',
|
|
568
|
+
'dbDatabase',
|
|
569
|
+
'dbUser',
|
|
570
|
+
'dbPassword',
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
static buildRootPresetValuesFromFlags(flags) {
|
|
574
|
+
return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
|
|
575
|
+
'rootUsername',
|
|
576
|
+
'rootEmail',
|
|
577
|
+
'rootPassword',
|
|
578
|
+
'rootNickname',
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
static toOptionalPromptString(value) {
|
|
582
|
+
if (value === undefined || value === null) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
const text = String(value).trim();
|
|
586
|
+
return text || undefined;
|
|
587
|
+
}
|
|
588
|
+
static buildResumePresetValues(env) {
|
|
589
|
+
const envName = String(env.name ?? '').trim();
|
|
590
|
+
const config = env.config ?? {};
|
|
591
|
+
const source = Install.toOptionalPromptString(config.source);
|
|
592
|
+
const appRootPath = Install.toOptionalPromptString(config.appRootPath);
|
|
593
|
+
const appPort = Install.toOptionalPromptString(config.appPort);
|
|
594
|
+
const storagePath = Install.toOptionalPromptString(config.storagePath);
|
|
595
|
+
const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
|
|
596
|
+
const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
|
|
597
|
+
const dockerPlatform = Install.toOptionalPromptString(config.dockerPlatform);
|
|
598
|
+
const gitUrl = Install.toOptionalPromptString(config.gitUrl);
|
|
599
|
+
const npmRegistry = Install.toOptionalPromptString(config.npmRegistry);
|
|
600
|
+
const dbDialect = Install.toOptionalPromptString(config.dbDialect);
|
|
601
|
+
const dbHost = Install.toOptionalPromptString(config.dbHost);
|
|
602
|
+
const dbPort = Install.toOptionalPromptString(config.dbPort);
|
|
603
|
+
const dbDatabase = Install.toOptionalPromptString(config.dbDatabase);
|
|
604
|
+
const dbUser = Install.toOptionalPromptString(config.dbUser);
|
|
605
|
+
const dbPassword = Install.toOptionalPromptString(config.dbPassword);
|
|
606
|
+
const auth = config.auth;
|
|
607
|
+
const appPreset = {
|
|
608
|
+
...(appRootPath ? { appRootPath } : {}),
|
|
609
|
+
...(appPort ? { appPort } : {}),
|
|
610
|
+
...(storagePath ? { storagePath } : {}),
|
|
611
|
+
...(source
|
|
612
|
+
? { fetchSource: true }
|
|
613
|
+
: appRootPath
|
|
614
|
+
? { fetchSource: false }
|
|
615
|
+
: {}),
|
|
616
|
+
};
|
|
617
|
+
const downloadPreset = {
|
|
618
|
+
...(source ? { source } : {}),
|
|
619
|
+
...(downloadVersion ? { version: downloadVersion } : {}),
|
|
620
|
+
...(dockerRegistry ? { dockerRegistry } : {}),
|
|
621
|
+
...(dockerPlatform ? { dockerPlatform } : {}),
|
|
622
|
+
...(gitUrl ? { gitUrl } : {}),
|
|
623
|
+
...(npmRegistry ? { npmRegistry } : {}),
|
|
624
|
+
...(typeof config.devDependencies === 'boolean'
|
|
625
|
+
? { devDependencies: config.devDependencies }
|
|
626
|
+
: {}),
|
|
627
|
+
...(typeof config.build === 'boolean' ? { build: config.build } : {}),
|
|
628
|
+
...(typeof config.buildDts === 'boolean' ? { buildDts: config.buildDts } : {}),
|
|
629
|
+
};
|
|
630
|
+
const dbPreset = {
|
|
631
|
+
...(typeof config.builtinDb === 'boolean' ? { builtinDb: config.builtinDb } : {}),
|
|
632
|
+
...(dbDialect ? { dbDialect } : {}),
|
|
633
|
+
...(dbHost ? { dbHost } : {}),
|
|
634
|
+
...(dbPort ? { dbPort } : {}),
|
|
635
|
+
...(dbDatabase ? { dbDatabase } : {}),
|
|
636
|
+
...(dbUser ? { dbUser } : {}),
|
|
637
|
+
...(dbPassword ? { dbPassword } : {}),
|
|
638
|
+
};
|
|
639
|
+
const envAddPreset = {};
|
|
640
|
+
if (auth?.type === 'token') {
|
|
641
|
+
envAddPreset.authType = 'token';
|
|
642
|
+
if (Install.toOptionalPromptString(auth.accessToken)) {
|
|
643
|
+
envAddPreset.accessToken = String(auth.accessToken);
|
|
212
644
|
}
|
|
213
645
|
}
|
|
214
|
-
if (
|
|
215
|
-
|
|
646
|
+
else if (auth?.type === 'oauth') {
|
|
647
|
+
envAddPreset.authType = 'oauth';
|
|
216
648
|
}
|
|
217
|
-
|
|
218
|
-
|
|
649
|
+
return {
|
|
650
|
+
envPreset: {
|
|
651
|
+
...(envName ? { env: envName } : {}),
|
|
652
|
+
},
|
|
653
|
+
appPreset,
|
|
654
|
+
downloadPreset,
|
|
655
|
+
dbPreset,
|
|
656
|
+
envAddPreset,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
static buildResumeMissingYesFlags(flags) {
|
|
660
|
+
const missing = [];
|
|
661
|
+
if (!Install.toOptionalPromptString(flags.lang)) {
|
|
662
|
+
missing.push('--lang');
|
|
663
|
+
}
|
|
664
|
+
if (!Install.toOptionalPromptString(flags['root-username'])) {
|
|
665
|
+
missing.push('--root-username');
|
|
666
|
+
}
|
|
667
|
+
if (!Install.toOptionalPromptString(flags['root-email'])) {
|
|
668
|
+
missing.push('--root-email');
|
|
219
669
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return dl.projectRoot;
|
|
670
|
+
if (!Install.toOptionalPromptString(flags['root-password'])) {
|
|
671
|
+
missing.push('--root-password');
|
|
223
672
|
}
|
|
224
|
-
if (!
|
|
225
|
-
|
|
226
|
-
const safe = version.replace(/[/\\]/g, '-');
|
|
227
|
-
const outputDir = appRootPathFlag?.trim() || `nocobase-${safe}`;
|
|
228
|
-
return path.resolve(process.cwd(), outputDir);
|
|
673
|
+
if (!Install.toOptionalPromptString(flags['root-nickname'])) {
|
|
674
|
+
missing.push('--root-nickname');
|
|
229
675
|
}
|
|
230
|
-
|
|
676
|
+
return missing;
|
|
231
677
|
}
|
|
232
|
-
async
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
}
|
|
678
|
+
async resolveResumePresetValues(parsed, yes) {
|
|
679
|
+
if (!parsed.resume) {
|
|
680
|
+
return undefined;
|
|
267
681
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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);
|
|
682
|
+
const env = await getEnv(parsed.env, { scope: CONFIG_SCOPE });
|
|
683
|
+
if (!env) {
|
|
684
|
+
throw new Error(formatMissingManagedAppEnvMessage(parsed.env));
|
|
685
|
+
}
|
|
686
|
+
if (yes) {
|
|
687
|
+
const missingFlags = Install.buildResumeMissingYesFlags(parsed);
|
|
688
|
+
if (missingFlags.length > 0) {
|
|
689
|
+
throw new Error([
|
|
690
|
+
`Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
|
|
691
|
+
`These setup-only flags are not saved in the env config: ${missingFlags.join(', ')}`,
|
|
692
|
+
`Run \`nb install --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
|
|
693
|
+
].join('\n'));
|
|
329
694
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
695
|
+
}
|
|
696
|
+
return Install.buildResumePresetValues(env);
|
|
697
|
+
}
|
|
698
|
+
static async resolveAvailableDefaultPort(defaultPort, options) {
|
|
699
|
+
const normalized = String(defaultPort).trim();
|
|
700
|
+
const portError = await validateAvailableTcpPort(normalized);
|
|
701
|
+
if (!portError) {
|
|
702
|
+
return normalized;
|
|
703
|
+
}
|
|
704
|
+
const nextPort = await findAvailableTcpPort();
|
|
705
|
+
if (options?.warn) {
|
|
706
|
+
p.log.warn(`${options.label ?? 'Default port'} ${normalized} is already in use. Using available port ${nextPort} for this setup.`);
|
|
707
|
+
}
|
|
708
|
+
return nextPort;
|
|
709
|
+
}
|
|
710
|
+
static async buildAppPromptInitialValues(params) {
|
|
711
|
+
const initialValues = {};
|
|
712
|
+
const envName = params.envName ?? DEFAULT_INSTALL_ENV_NAME;
|
|
713
|
+
if (params.flags['app-root-path'] === undefined) {
|
|
714
|
+
initialValues.appRootPath = defaultInstallAppRootPath(envName);
|
|
715
|
+
}
|
|
716
|
+
if (params.flags['storage-path'] === undefined) {
|
|
717
|
+
initialValues.storagePath = defaultInstallStoragePath(envName);
|
|
718
|
+
}
|
|
719
|
+
if (params.flags['app-port'] === undefined) {
|
|
720
|
+
initialValues.appPort = await Install.resolveAvailableDefaultPort(DEFAULT_INSTALL_APP_PORT, {
|
|
721
|
+
label: 'Default app port',
|
|
722
|
+
warn: true,
|
|
334
723
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
724
|
+
}
|
|
725
|
+
return initialValues;
|
|
726
|
+
}
|
|
727
|
+
static shouldPublishBuiltinDbPortForValues(values) {
|
|
728
|
+
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
729
|
+
return builtinDb
|
|
730
|
+
&& Install.shouldPublishBuiltinDbPort(values.source);
|
|
731
|
+
}
|
|
732
|
+
static async buildDbPromptInitialValues(params) {
|
|
733
|
+
if (params.flags['db-port'] !== undefined) {
|
|
734
|
+
return {};
|
|
735
|
+
}
|
|
736
|
+
const values = {
|
|
737
|
+
...params.downloadResults,
|
|
738
|
+
...params.dbPreset,
|
|
739
|
+
};
|
|
740
|
+
if (!Install.shouldPublishBuiltinDbPortForValues(values)) {
|
|
741
|
+
return {};
|
|
742
|
+
}
|
|
743
|
+
const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
744
|
+
const defaultPort = defaultDbPortForDialect(dialect);
|
|
745
|
+
return {
|
|
746
|
+
dbPort: await Install.resolveAvailableDefaultPort(defaultPort, {
|
|
747
|
+
label: `Default ${dialect} port`,
|
|
748
|
+
warn: true,
|
|
749
|
+
}),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* When install runs {@link Download.prompts} after app prompts, align language and
|
|
754
|
+
* output directory defaults with the app settings collected earlier in the flow.
|
|
755
|
+
*/
|
|
756
|
+
static buildDownloadPromptOptionsForInstall(appResults, envName) {
|
|
757
|
+
const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
|
|
758
|
+
const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
|
|
759
|
+
const initialValues = {
|
|
760
|
+
lang,
|
|
761
|
+
dockerRegistry: defaultDockerRegistryForLang(lang),
|
|
762
|
+
outputDir: appRoot,
|
|
763
|
+
};
|
|
764
|
+
const values = {
|
|
765
|
+
lang,
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
initialValues,
|
|
769
|
+
values,
|
|
770
|
+
yes: false,
|
|
771
|
+
hooks: {
|
|
772
|
+
onCancel: () => {
|
|
773
|
+
p.cancel('Download cancelled.');
|
|
774
|
+
exit(0);
|
|
775
|
+
},
|
|
776
|
+
onMissingNonInteractive: (message) => {
|
|
777
|
+
console.error(message);
|
|
778
|
+
exit(1);
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Resolve the effective preset `values` for the embedded download step.
|
|
785
|
+
* Explicit download flags win; otherwise `-y` falls back to the docker + alpha quickstart path.
|
|
786
|
+
*/
|
|
787
|
+
static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes) {
|
|
788
|
+
const preset = {};
|
|
789
|
+
const argv = process.argv.slice(2);
|
|
790
|
+
const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
|
|
791
|
+
const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
|
|
792
|
+
preset.lang = lang;
|
|
793
|
+
if (flags.source !== undefined && String(flags.source).trim() !== '') {
|
|
794
|
+
preset.source = String(flags.source).trim();
|
|
795
|
+
}
|
|
796
|
+
if (flags.version !== undefined) {
|
|
797
|
+
preset.version = String(flags.version).trim() || 'latest';
|
|
798
|
+
}
|
|
799
|
+
if (flags['docker-registry'] !== undefined) {
|
|
800
|
+
const value = String(flags['docker-registry'] ?? '').trim();
|
|
801
|
+
if (value) {
|
|
802
|
+
preset.dockerRegistry = value;
|
|
338
803
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (p.isCancel(dbNameAns)) {
|
|
345
|
-
p.cancel('Install cancelled.');
|
|
346
|
-
this.exit(0);
|
|
804
|
+
}
|
|
805
|
+
if (flags['docker-platform'] !== undefined) {
|
|
806
|
+
const value = String(flags['docker-platform'] ?? '').trim();
|
|
807
|
+
if (value) {
|
|
808
|
+
preset.dockerPlatform = value;
|
|
347
809
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (p.isCancel(userAns)) {
|
|
354
|
-
p.cancel('Install cancelled.');
|
|
355
|
-
this.exit(0);
|
|
810
|
+
}
|
|
811
|
+
if (flags['output-dir'] !== undefined) {
|
|
812
|
+
const value = String(flags['output-dir'] ?? '').trim();
|
|
813
|
+
if (value) {
|
|
814
|
+
preset.outputDir = value;
|
|
356
815
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
p.cancel('Install cancelled.');
|
|
363
|
-
this.exit(0);
|
|
816
|
+
}
|
|
817
|
+
if (flags['git-url'] !== undefined) {
|
|
818
|
+
const value = String(flags['git-url'] ?? '').trim();
|
|
819
|
+
if (value) {
|
|
820
|
+
preset.gitUrl = value;
|
|
364
821
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
822
|
+
}
|
|
823
|
+
if (flags['npm-registry'] !== undefined) {
|
|
824
|
+
preset.npmRegistry =
|
|
825
|
+
typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
|
|
826
|
+
}
|
|
827
|
+
if (argvHasToken(argv, ['--replace', '-r'])) {
|
|
828
|
+
preset.replace = flags.replace;
|
|
829
|
+
}
|
|
830
|
+
if (argvHasToken(argv, ['--dev-dependencies', '--no-dev-dependencies', '-D'])) {
|
|
831
|
+
preset.devDependencies = flags['dev-dependencies'];
|
|
832
|
+
}
|
|
833
|
+
if (argvHasToken(argv, ['--docker-save', '--no-docker-save'])) {
|
|
834
|
+
preset.dockerSave = flags['docker-save'];
|
|
835
|
+
}
|
|
836
|
+
if (argvHasToken(argv, ['--build', '--no-build'])) {
|
|
837
|
+
preset.build = flags.build;
|
|
838
|
+
}
|
|
839
|
+
if (argvHasToken(argv, ['--build-dts', '--no-build-dts'])) {
|
|
840
|
+
preset.buildDts = flags['build-dts'];
|
|
841
|
+
}
|
|
842
|
+
if (yes) {
|
|
843
|
+
preset.source ??= 'docker';
|
|
844
|
+
preset.version ??= 'alpha';
|
|
845
|
+
preset.outputDir ??= appRoot;
|
|
846
|
+
}
|
|
847
|
+
return preset;
|
|
848
|
+
}
|
|
849
|
+
static sanitizeDockerResourceName(value) {
|
|
850
|
+
const normalized = value
|
|
851
|
+
.trim()
|
|
852
|
+
.toLowerCase()
|
|
853
|
+
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
854
|
+
.replace(/-+/g, '-')
|
|
855
|
+
.replace(/^-+|-+$/g, '');
|
|
856
|
+
return normalized || 'nocobase';
|
|
857
|
+
}
|
|
858
|
+
static defaultWorkspaceName() {
|
|
859
|
+
return Install.sanitizeDockerResourceName(`nb-${path.basename(process.cwd())}`);
|
|
860
|
+
}
|
|
861
|
+
static buildBuiltinDbResourcePrefix(envName, workspaceName) {
|
|
862
|
+
void envName;
|
|
863
|
+
const storedName = String(workspaceName ?? '').trim();
|
|
864
|
+
return storedName
|
|
865
|
+
? Install.sanitizeDockerResourceName(storedName)
|
|
866
|
+
: Install.defaultWorkspaceName();
|
|
867
|
+
}
|
|
868
|
+
static async ensureWorkspaceName() {
|
|
869
|
+
return await ensureWorkspaceName(Install.defaultWorkspaceName(), { scope: CONFIG_SCOPE });
|
|
870
|
+
}
|
|
871
|
+
static buildBuiltinDbNetworkName(envName, workspaceName) {
|
|
872
|
+
return Install.buildBuiltinDbResourcePrefix(envName, workspaceName);
|
|
873
|
+
}
|
|
874
|
+
static buildBuiltinDbContainerName(envName, dbDialect, workspaceName) {
|
|
875
|
+
return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-${dbDialect}`);
|
|
876
|
+
}
|
|
877
|
+
static buildDockerAppContainerName(envName, workspaceName) {
|
|
878
|
+
return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-app`);
|
|
879
|
+
}
|
|
880
|
+
static buildInitAppEnvVars(params) {
|
|
881
|
+
const out = {};
|
|
882
|
+
const put = (key, value) => {
|
|
883
|
+
const text = String(value ?? '').trim();
|
|
884
|
+
if (!text) {
|
|
885
|
+
return;
|
|
379
886
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
887
|
+
out[key] = text;
|
|
888
|
+
};
|
|
889
|
+
put('INIT_APP_LANG', params.appResults.lang);
|
|
890
|
+
put('INIT_ROOT_USERNAME', params.rootResults.rootUsername);
|
|
891
|
+
put('INIT_ROOT_EMAIL', params.rootResults.rootEmail);
|
|
892
|
+
put('INIT_ROOT_PASSWORD', params.rootResults.rootPassword);
|
|
893
|
+
put('INIT_ROOT_NICKNAME', params.rootResults.rootNickname);
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
static shouldPublishBuiltinDbPort(source) {
|
|
897
|
+
return String(source ?? '').trim() !== 'docker';
|
|
898
|
+
}
|
|
899
|
+
static buildBuiltinDbPlan(params) {
|
|
900
|
+
const dbDialect = String(params.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
901
|
+
const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
|
|
902
|
+
|| defaultDbPortForDialect(dbDialect);
|
|
903
|
+
const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
|
|
904
|
+
const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.workspaceName);
|
|
905
|
+
const dbHostInput = String(params.dbHost ?? '').trim();
|
|
906
|
+
const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
|
|
907
|
+
? (dbHostInput
|
|
908
|
+
&& dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST
|
|
909
|
+
&& dbHostInput !== containerName
|
|
910
|
+
? dbHostInput
|
|
911
|
+
: DEFAULT_INSTALL_DB_HOST)
|
|
912
|
+
: (dbHostInput
|
|
913
|
+
&& dbHostInput !== DEFAULT_INSTALL_DB_HOST
|
|
914
|
+
&& dbHostInput !== 'localhost'
|
|
915
|
+
? dbHostInput
|
|
916
|
+
: containerName);
|
|
917
|
+
if (dbDialect === 'postgres') {
|
|
918
|
+
const dataDir = path.resolve(params.storagePath, 'db', 'postgres');
|
|
919
|
+
const args = [
|
|
920
|
+
'run',
|
|
921
|
+
'-d',
|
|
922
|
+
'--name',
|
|
923
|
+
containerName,
|
|
924
|
+
'--restart',
|
|
925
|
+
'always',
|
|
926
|
+
'--network',
|
|
927
|
+
networkName,
|
|
928
|
+
'-e',
|
|
929
|
+
`POSTGRES_USER=${String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER}`,
|
|
930
|
+
'-e',
|
|
931
|
+
`POSTGRES_DB=${String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE}`,
|
|
932
|
+
'-e',
|
|
933
|
+
`POSTGRES_PASSWORD=${String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD}`,
|
|
934
|
+
'-v',
|
|
935
|
+
`${dataDir}:/var/lib/postgresql/data`,
|
|
936
|
+
];
|
|
937
|
+
if (Install.shouldPublishBuiltinDbPort(params.source)) {
|
|
938
|
+
args.push('-p', `${dbPort}:5432`);
|
|
386
939
|
}
|
|
940
|
+
args.push('postgres:16', 'postgres', '-c', 'wal_level=logical');
|
|
941
|
+
return {
|
|
942
|
+
source: String(params.source ?? '').trim() || undefined,
|
|
943
|
+
dbDialect,
|
|
944
|
+
dbHost,
|
|
945
|
+
dbPort,
|
|
946
|
+
dbDatabase: String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
|
|
947
|
+
|| DEFAULT_INSTALL_DB_DATABASE,
|
|
948
|
+
dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
|
|
949
|
+
|| DEFAULT_INSTALL_DB_USER,
|
|
950
|
+
dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
|
|
951
|
+
|| DEFAULT_INSTALL_DB_PASSWORD,
|
|
952
|
+
networkName,
|
|
953
|
+
containerName,
|
|
954
|
+
dataDir,
|
|
955
|
+
image: 'postgres:16',
|
|
956
|
+
args,
|
|
957
|
+
};
|
|
387
958
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
959
|
+
if (dbDialect === 'mysql') {
|
|
960
|
+
const dataDir = path.resolve(params.storagePath, 'db', 'mysql');
|
|
961
|
+
const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
|
|
962
|
+
const dbDatabase = String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
|
|
963
|
+
const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
|
|
964
|
+
const args = [
|
|
965
|
+
'run',
|
|
966
|
+
'-d',
|
|
967
|
+
'--name',
|
|
968
|
+
containerName,
|
|
969
|
+
'--restart',
|
|
970
|
+
'always',
|
|
971
|
+
'--network',
|
|
972
|
+
networkName,
|
|
973
|
+
'-e',
|
|
974
|
+
`MYSQL_USER=${dbUser}`,
|
|
975
|
+
'-e',
|
|
976
|
+
`MYSQL_DATABASE=${dbDatabase}`,
|
|
977
|
+
'-e',
|
|
978
|
+
`MYSQL_PASSWORD=${dbPassword}`,
|
|
979
|
+
'-e',
|
|
980
|
+
`MYSQL_ROOT_PASSWORD=${dbPassword}`,
|
|
981
|
+
'-v',
|
|
982
|
+
`${dataDir}:/var/lib/mysql`,
|
|
983
|
+
];
|
|
984
|
+
if (Install.shouldPublishBuiltinDbPort(params.source)) {
|
|
985
|
+
args.push('-p', `${dbPort}:3306`);
|
|
399
986
|
}
|
|
400
|
-
|
|
987
|
+
args.push('mysql:8');
|
|
988
|
+
return {
|
|
989
|
+
source: String(params.source ?? '').trim() || undefined,
|
|
990
|
+
dbDialect,
|
|
991
|
+
dbHost,
|
|
992
|
+
dbPort,
|
|
993
|
+
dbDatabase,
|
|
994
|
+
dbUser,
|
|
995
|
+
dbPassword,
|
|
996
|
+
networkName,
|
|
997
|
+
containerName,
|
|
998
|
+
dataDir,
|
|
999
|
+
image: 'mysql:8',
|
|
1000
|
+
args,
|
|
1001
|
+
};
|
|
401
1002
|
}
|
|
402
|
-
if (
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1003
|
+
if (dbDialect === 'mariadb') {
|
|
1004
|
+
const dataDir = path.resolve(params.storagePath, 'db', 'mariadb');
|
|
1005
|
+
const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
|
|
1006
|
+
const dbDatabase = String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
|
|
1007
|
+
const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
|
|
1008
|
+
const args = [
|
|
1009
|
+
'run',
|
|
1010
|
+
'-d',
|
|
1011
|
+
'--name',
|
|
1012
|
+
containerName,
|
|
1013
|
+
'--restart',
|
|
1014
|
+
'always',
|
|
1015
|
+
'--network',
|
|
1016
|
+
networkName,
|
|
1017
|
+
'-e',
|
|
1018
|
+
`MARIADB_USER=${dbUser}`,
|
|
1019
|
+
'-e',
|
|
1020
|
+
`MARIADB_DATABASE=${dbDatabase}`,
|
|
1021
|
+
'-e',
|
|
1022
|
+
`MARIADB_PASSWORD=${dbPassword}`,
|
|
1023
|
+
'-e',
|
|
1024
|
+
`MARIADB_ROOT_PASSWORD=${dbPassword}`,
|
|
1025
|
+
'-v',
|
|
1026
|
+
`${dataDir}:/var/lib/mysql`,
|
|
1027
|
+
];
|
|
1028
|
+
if (Install.shouldPublishBuiltinDbPort(params.source)) {
|
|
1029
|
+
args.push('-p', `${dbPort}:3306`);
|
|
427
1030
|
}
|
|
428
|
-
|
|
1031
|
+
args.push('mariadb:11');
|
|
1032
|
+
return {
|
|
1033
|
+
source: String(params.source ?? '').trim() || undefined,
|
|
1034
|
+
dbDialect,
|
|
1035
|
+
dbHost,
|
|
1036
|
+
dbPort,
|
|
1037
|
+
dbDatabase,
|
|
1038
|
+
dbUser,
|
|
1039
|
+
dbPassword,
|
|
1040
|
+
networkName,
|
|
1041
|
+
containerName,
|
|
1042
|
+
dataDir,
|
|
1043
|
+
image: 'mariadb:11',
|
|
1044
|
+
args,
|
|
1045
|
+
};
|
|
429
1046
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
}
|
|
1047
|
+
throw new Error(`Built-in database does not support "${dbDialect}" yet. Please choose PostgreSQL, MySQL, or MariaDB.`);
|
|
1048
|
+
}
|
|
1049
|
+
async ensureDockerNetwork(name) {
|
|
1050
|
+
p.log.step(`Checking Docker network: ${name}`);
|
|
1051
|
+
const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
|
|
1052
|
+
if (exists) {
|
|
1053
|
+
p.log.info(`Docker network already exists: ${name}`);
|
|
1054
|
+
return;
|
|
454
1055
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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'],
|
|
1056
|
+
p.log.step(`Creating Docker network: ${name}`);
|
|
1057
|
+
try {
|
|
1058
|
+
await run('docker', ['network', 'create', name], {
|
|
1059
|
+
errorName: 'docker network create',
|
|
465
1060
|
});
|
|
1061
|
+
p.log.info(`Docker network is ready: ${name}`);
|
|
466
1062
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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;
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
if (/address pools have been fully subnetted/i.test(message)) {
|
|
1066
|
+
throw new Error([
|
|
1067
|
+
`Docker could not create network "${name}" because its address pools are exhausted.`,
|
|
1068
|
+
'Remove unused Docker networks and try again, for example: docker network prune',
|
|
1069
|
+
`Original error: ${message}`,
|
|
1070
|
+
].join('\n'));
|
|
485
1071
|
}
|
|
1072
|
+
throw error;
|
|
486
1073
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1074
|
+
}
|
|
1075
|
+
async dockerContainerExists(name) {
|
|
1076
|
+
return await commandSucceeds('docker', [
|
|
1077
|
+
'container',
|
|
1078
|
+
'inspect',
|
|
1079
|
+
name,
|
|
1080
|
+
]);
|
|
1081
|
+
}
|
|
1082
|
+
async removeDockerContainer(name) {
|
|
1083
|
+
await run('docker', ['rm', '-f', name], {
|
|
1084
|
+
errorName: 'docker rm',
|
|
494
1085
|
});
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
procEnv.INIT_ROOT_USERNAME = argvFlags.rootUserName;
|
|
1086
|
+
}
|
|
1087
|
+
async removeDockerContainerIfForced(params) {
|
|
1088
|
+
const exists = await this.dockerContainerExists(params.containerName);
|
|
1089
|
+
if (!exists) {
|
|
1090
|
+
return false;
|
|
501
1091
|
}
|
|
502
|
-
if (
|
|
503
|
-
|
|
1092
|
+
if (!params.force) {
|
|
1093
|
+
return true;
|
|
504
1094
|
}
|
|
505
|
-
|
|
506
|
-
|
|
1095
|
+
p.log.info(`Removing existing ${params.displayName}: ${params.containerName}`);
|
|
1096
|
+
await this.removeDockerContainer(params.containerName);
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
async inspectDockerContainerEnv(name) {
|
|
1100
|
+
const output = await commandOutput('docker', [
|
|
1101
|
+
'inspect',
|
|
1102
|
+
'--format',
|
|
1103
|
+
'{{range .Config.Env}}{{println .}}{{end}}',
|
|
1104
|
+
name,
|
|
1105
|
+
]);
|
|
1106
|
+
const env = {};
|
|
1107
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1108
|
+
const index = line.indexOf('=');
|
|
1109
|
+
if (index <= 0) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
env[line.slice(0, index)] = line.slice(index + 1);
|
|
507
1113
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1114
|
+
return env;
|
|
1115
|
+
}
|
|
1116
|
+
async ensureBuiltinDbContainer(plan) {
|
|
1117
|
+
const exists = await this.dockerContainerExists(plan.containerName);
|
|
1118
|
+
if (exists) {
|
|
1119
|
+
p.log.info(`Built-in ${plan.dbDialect} container already exists: ${plan.containerName}`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
await mkdir(plan.dataDir, { recursive: true });
|
|
1123
|
+
await run('docker', plan.args, {
|
|
1124
|
+
errorName: 'docker run',
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
async startBuiltinDb(params) {
|
|
1128
|
+
const storagePath = String(params.appResults.storagePath ?? '').trim()
|
|
1129
|
+
|| defaultInstallStoragePath(params.envName);
|
|
1130
|
+
const plan = Install.buildBuiltinDbPlan({
|
|
1131
|
+
envName: params.envName,
|
|
1132
|
+
workspaceName: params.workspaceName,
|
|
1133
|
+
storagePath,
|
|
1134
|
+
source: params.downloadResults.source,
|
|
1135
|
+
dbDialect: params.dbResults.dbDialect,
|
|
1136
|
+
dbHost: params.dbResults.dbHost,
|
|
1137
|
+
dbPort: params.dbResults.dbPort,
|
|
1138
|
+
dbDatabase: params.dbResults.dbDatabase,
|
|
1139
|
+
dbUser: params.dbResults.dbUser,
|
|
1140
|
+
dbPassword: params.dbResults.dbPassword,
|
|
1141
|
+
});
|
|
1142
|
+
p.log.step(`Preparing built-in ${plan.dbDialect} database`);
|
|
1143
|
+
await this.ensureDockerNetwork(plan.networkName);
|
|
1144
|
+
await this.removeDockerContainerIfForced({
|
|
1145
|
+
containerName: plan.containerName,
|
|
1146
|
+
displayName: `built-in ${plan.dbDialect} container`,
|
|
1147
|
+
force: params.force,
|
|
1148
|
+
});
|
|
1149
|
+
if (Install.shouldPublishBuiltinDbPort(params.downloadResults.source)) {
|
|
1150
|
+
const portError = await validateAvailableTcpPort(plan.dbPort);
|
|
1151
|
+
if (portError) {
|
|
1152
|
+
throw new Error(`Built-in ${plan.dbDialect} needs host port ${plan.dbPort}, but ${portError}`);
|
|
512
1153
|
}
|
|
513
|
-
this.error(message);
|
|
514
1154
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
1155
|
+
await this.ensureBuiltinDbContainer(plan);
|
|
1156
|
+
p.log.info(`Built-in ${plan.dbDialect} database is ready at ${plan.dbHost}:${plan.dbPort}`);
|
|
1157
|
+
return plan;
|
|
1158
|
+
}
|
|
1159
|
+
static buildDockerAppPlan(params) {
|
|
1160
|
+
const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
|
|
1161
|
+
|| defaultDockerRegistryForLang(params.appResults.lang);
|
|
1162
|
+
const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || 'latest';
|
|
1163
|
+
const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
|
|
1164
|
+
const storagePath = path.resolve(String(params.appResults.storagePath ?? '').trim()
|
|
1165
|
+
|| defaultInstallStoragePath(params.envName));
|
|
1166
|
+
const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
1167
|
+
const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
|
|
1168
|
+
const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
|
|
1169
|
+
|| defaultDbPortForDialect(dbDialect);
|
|
1170
|
+
const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
|
|
1171
|
+
|| DEFAULT_INSTALL_DB_DATABASE;
|
|
1172
|
+
const dbUser = String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
|
|
1173
|
+
const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
|
|
1174
|
+
const appKey = crypto.randomBytes(32).toString('hex');
|
|
1175
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1176
|
+
const containerName = Install.buildDockerAppContainerName(params.envName, params.workspaceName);
|
|
1177
|
+
const initEnvVars = Install.buildInitAppEnvVars({
|
|
1178
|
+
appResults: params.appResults,
|
|
1179
|
+
rootResults: params.rootResults,
|
|
1180
|
+
});
|
|
1181
|
+
const args = [
|
|
1182
|
+
'run',
|
|
1183
|
+
'-d',
|
|
1184
|
+
'--name',
|
|
1185
|
+
containerName,
|
|
1186
|
+
'--restart',
|
|
1187
|
+
'always',
|
|
1188
|
+
'--network',
|
|
1189
|
+
params.networkName,
|
|
1190
|
+
'-p',
|
|
1191
|
+
`${appPort}:80`,
|
|
1192
|
+
];
|
|
1193
|
+
for (const [key, value] of Object.entries(initEnvVars)) {
|
|
1194
|
+
args.push('-e', `${key}=${value}`);
|
|
1195
|
+
}
|
|
1196
|
+
args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:/app/nocobase/storage`, `${dockerRegistry}:${version}`);
|
|
1197
|
+
return {
|
|
1198
|
+
source: 'docker',
|
|
1199
|
+
networkName: params.networkName,
|
|
1200
|
+
containerName,
|
|
1201
|
+
imageRef: `${dockerRegistry}:${version}`,
|
|
518
1202
|
appPort,
|
|
519
|
-
|
|
1203
|
+
storagePath,
|
|
1204
|
+
appKey,
|
|
1205
|
+
timeZone,
|
|
1206
|
+
args,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
async ensureDockerAppContainer(plan) {
|
|
1210
|
+
const exists = await this.dockerContainerExists(plan.containerName);
|
|
1211
|
+
if (exists) {
|
|
1212
|
+
p.log.info(`App container already exists: ${plan.containerName}`);
|
|
1213
|
+
return 'existing';
|
|
1214
|
+
}
|
|
1215
|
+
await mkdir(plan.storagePath, { recursive: true });
|
|
1216
|
+
await run('docker', plan.args, {
|
|
1217
|
+
errorName: 'docker run',
|
|
520
1218
|
});
|
|
521
|
-
|
|
522
|
-
|
|
1219
|
+
return 'created';
|
|
1220
|
+
}
|
|
1221
|
+
async installDockerApp(params) {
|
|
1222
|
+
const networkName = params.builtinDbPlan?.networkName
|
|
1223
|
+
?? Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
|
|
1224
|
+
await this.ensureDockerNetwork(networkName);
|
|
1225
|
+
const plan = Install.buildDockerAppPlan({
|
|
1226
|
+
envName: params.envName,
|
|
1227
|
+
workspaceName: params.workspaceName,
|
|
1228
|
+
appResults: params.appResults,
|
|
1229
|
+
downloadResults: params.downloadResults,
|
|
1230
|
+
dbResults: params.dbResults,
|
|
1231
|
+
rootResults: params.rootResults,
|
|
1232
|
+
networkName,
|
|
1233
|
+
});
|
|
1234
|
+
p.log.step(`Starting Docker app ${plan.imageRef}`);
|
|
1235
|
+
await this.removeDockerContainerIfForced({
|
|
1236
|
+
containerName: plan.containerName,
|
|
1237
|
+
displayName: 'app container',
|
|
1238
|
+
force: params.force,
|
|
1239
|
+
});
|
|
1240
|
+
const containerState = await this.ensureDockerAppContainer(plan);
|
|
1241
|
+
if (containerState === 'existing') {
|
|
1242
|
+
const env = await this.inspectDockerContainerEnv(plan.containerName);
|
|
1243
|
+
plan.appKey = env.APP_KEY || plan.appKey;
|
|
1244
|
+
plan.timeZone = env.TZ || plan.timeZone;
|
|
523
1245
|
}
|
|
524
|
-
|
|
525
|
-
|
|
1246
|
+
p.log.info(`App container is ready at http://127.0.0.1:${plan.appPort}`);
|
|
1247
|
+
return plan;
|
|
1248
|
+
}
|
|
1249
|
+
static pushDownloadArgIfValue(argv, flag, value) {
|
|
1250
|
+
const text = String(value ?? '').trim();
|
|
1251
|
+
if (text) {
|
|
1252
|
+
argv.push(flag, text);
|
|
526
1253
|
}
|
|
527
|
-
|
|
528
|
-
|
|
1254
|
+
}
|
|
1255
|
+
static buildDownloadArgvFromResults(results) {
|
|
1256
|
+
const argv = ['-y', '--no-intro'];
|
|
1257
|
+
Install.pushDownloadArgIfValue(argv, '--source', results.source);
|
|
1258
|
+
Install.pushDownloadArgIfValue(argv, '--version', results.version);
|
|
1259
|
+
Install.pushDownloadArgIfValue(argv, '--output-dir', results.outputDir);
|
|
1260
|
+
Install.pushDownloadArgIfValue(argv, '--git-url', results.gitUrl);
|
|
1261
|
+
Install.pushDownloadArgIfValue(argv, '--docker-registry', results.dockerRegistry);
|
|
1262
|
+
Install.pushDownloadArgIfValue(argv, '--docker-platform', results.dockerPlatform);
|
|
1263
|
+
Install.pushDownloadArgIfValue(argv, '--npm-registry', results.npmRegistry);
|
|
1264
|
+
if (Boolean(results.replace)) {
|
|
1265
|
+
argv.push('--replace');
|
|
1266
|
+
}
|
|
1267
|
+
if (Boolean(results.devDependencies)) {
|
|
1268
|
+
argv.push('--dev-dependencies');
|
|
1269
|
+
}
|
|
1270
|
+
if (Boolean(results.dockerSave)) {
|
|
1271
|
+
argv.push('--docker-save');
|
|
1272
|
+
}
|
|
1273
|
+
if (results.build !== undefined && !Boolean(results.build)) {
|
|
1274
|
+
argv.push('--no-build');
|
|
529
1275
|
}
|
|
1276
|
+
if (Boolean(results.buildDts)) {
|
|
1277
|
+
argv.push('--build-dts');
|
|
1278
|
+
}
|
|
1279
|
+
return argv;
|
|
530
1280
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
1281
|
+
static resolveLocalProjectRoot(params) {
|
|
1282
|
+
const projectRoot = params.downloadCommandResult?.projectRoot;
|
|
1283
|
+
if (projectRoot) {
|
|
1284
|
+
return projectRoot;
|
|
1285
|
+
}
|
|
1286
|
+
const outputDir = String(params.downloadResults.outputDir ?? '').trim()
|
|
1287
|
+
|| String(params.appResults.appRootPath ?? '').trim()
|
|
1288
|
+
|| defaultInstallAppRootPath(params.envName);
|
|
1289
|
+
return path.resolve(process.cwd(), outputDir);
|
|
1290
|
+
}
|
|
1291
|
+
async downloadLocalApp(params) {
|
|
1292
|
+
const argv = Install.buildDownloadArgvFromResults(params.downloadResults);
|
|
1293
|
+
p.log.step('Downloading local NocoBase app files');
|
|
1294
|
+
const result = await this.config.runCommand('download', argv);
|
|
1295
|
+
const projectRoot = Install.resolveLocalProjectRoot({
|
|
1296
|
+
envName: params.envName,
|
|
1297
|
+
appResults: params.appResults,
|
|
1298
|
+
downloadResults: params.downloadResults,
|
|
1299
|
+
downloadCommandResult: result,
|
|
1300
|
+
});
|
|
1301
|
+
params.appResults.appRootPath = projectRoot;
|
|
1302
|
+
return projectRoot;
|
|
1303
|
+
}
|
|
1304
|
+
static buildLocalAppEnvVars(params) {
|
|
1305
|
+
const storagePath = path.resolve(String(params.appResults.storagePath ?? '').trim()
|
|
1306
|
+
|| defaultInstallStoragePath(params.envName));
|
|
1307
|
+
const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim()
|
|
1308
|
+
|| 'postgres';
|
|
1309
|
+
const appKey = crypto.randomBytes(32).toString('hex');
|
|
1310
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1311
|
+
const env = {
|
|
1312
|
+
STORAGE_PATH: storagePath,
|
|
1313
|
+
APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
|
|
1314
|
+
|| DEFAULT_INSTALL_APP_PORT,
|
|
1315
|
+
APP_KEY: appKey,
|
|
1316
|
+
TZ: timeZone,
|
|
1317
|
+
DB_DIALECT: dbDialect,
|
|
1318
|
+
DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim()
|
|
1319
|
+
|| DEFAULT_INSTALL_DB_HOST,
|
|
1320
|
+
DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
|
|
1321
|
+
|| defaultDbPortForDialect(dbDialect),
|
|
1322
|
+
DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
|
|
1323
|
+
|| DEFAULT_INSTALL_DB_DATABASE,
|
|
1324
|
+
DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
|
|
1325
|
+
|| DEFAULT_INSTALL_DB_USER,
|
|
1326
|
+
DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
|
|
1327
|
+
|| DEFAULT_INSTALL_DB_PASSWORD,
|
|
1328
|
+
...Install.buildInitAppEnvVars({
|
|
1329
|
+
appResults: params.appResults,
|
|
1330
|
+
rootResults: params.rootResults,
|
|
1331
|
+
}),
|
|
1332
|
+
};
|
|
1333
|
+
return env;
|
|
1334
|
+
}
|
|
1335
|
+
async startLocalApp(params) {
|
|
1336
|
+
const env = Install.buildLocalAppEnvVars({
|
|
1337
|
+
envName: params.envName,
|
|
1338
|
+
appResults: params.appResults,
|
|
1339
|
+
dbResults: params.dbResults,
|
|
1340
|
+
rootResults: params.rootResults,
|
|
1341
|
+
});
|
|
1342
|
+
const args = ['start', '--quickstart', '--daemon'];
|
|
1343
|
+
p.log.step(`Stopping any existing local NocoBase process in ${params.projectRoot}`);
|
|
537
1344
|
try {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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';
|
|
1345
|
+
await runNocoBaseCommand(['pm2', 'kill'], {
|
|
1346
|
+
cwd: params.projectRoot,
|
|
1347
|
+
env,
|
|
1348
|
+
});
|
|
568
1349
|
}
|
|
569
1350
|
catch (error) {
|
|
570
1351
|
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
-
|
|
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';
|
|
1352
|
+
p.log.info(`Skipped local process cleanup before start: ${message}`);
|
|
580
1353
|
}
|
|
1354
|
+
p.log.step(`Starting local NocoBase app from ${params.projectRoot}`);
|
|
1355
|
+
await runNocoBaseCommand(args, {
|
|
1356
|
+
cwd: params.projectRoot,
|
|
1357
|
+
env,
|
|
1358
|
+
});
|
|
1359
|
+
p.log.info(`Local app is starting at http://127.0.0.1:${env.APP_PORT}`);
|
|
1360
|
+
return {
|
|
1361
|
+
source: params.source,
|
|
1362
|
+
projectRoot: params.projectRoot,
|
|
1363
|
+
appPort: env.APP_PORT,
|
|
1364
|
+
storagePath: env.STORAGE_PATH,
|
|
1365
|
+
appKey: env.APP_KEY,
|
|
1366
|
+
timeZone: env.TZ,
|
|
1367
|
+
env,
|
|
1368
|
+
args,
|
|
1369
|
+
};
|
|
581
1370
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
initialValue: false,
|
|
599
|
-
});
|
|
600
|
-
if (p.isCancel(reinstall)) {
|
|
601
|
-
p.cancel('Install cancelled.');
|
|
602
|
-
this.exit(0);
|
|
603
|
-
}
|
|
604
|
-
force = reinstall;
|
|
1371
|
+
static resolveApiBaseUrl(params) {
|
|
1372
|
+
const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
|
|
1373
|
+
|| DEFAULT_INSTALL_APP_PORT;
|
|
1374
|
+
return (String(params.envAddResults.apiBaseUrl ?? '').trim()
|
|
1375
|
+
|| `http://127.0.0.1:${appPort}/api`);
|
|
1376
|
+
}
|
|
1377
|
+
static buildHealthCheckUrl(apiBaseUrl) {
|
|
1378
|
+
return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
1379
|
+
}
|
|
1380
|
+
static async sleep(ms) {
|
|
1381
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1382
|
+
}
|
|
1383
|
+
static formatHealthCheckMessage(message, maxLength = 120) {
|
|
1384
|
+
const text = message.replace(/\s+/g, ' ').trim();
|
|
1385
|
+
if (!text) {
|
|
1386
|
+
return 'No response yet';
|
|
605
1387
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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',
|
|
1388
|
+
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
|
1389
|
+
}
|
|
1390
|
+
static async requestAppHealthCheck(params) {
|
|
1391
|
+
const controller = new AbortController();
|
|
1392
|
+
const timeout = setTimeout(() => {
|
|
1393
|
+
controller.abort();
|
|
1394
|
+
}, params.requestTimeoutMs);
|
|
1395
|
+
try {
|
|
1396
|
+
const response = await params.fetchImpl(params.healthCheckUrl, {
|
|
1397
|
+
method: 'GET',
|
|
1398
|
+
signal: controller.signal,
|
|
624
1399
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1400
|
+
const text = await response.text().catch(() => '');
|
|
1401
|
+
const body = Install.formatHealthCheckMessage(text);
|
|
1402
|
+
return {
|
|
1403
|
+
ok: response.ok && text.trim().toLowerCase() === 'ok',
|
|
1404
|
+
message: response.ok
|
|
1405
|
+
? `HTTP ${response.status}: ${body}`
|
|
1406
|
+
: `HTTP ${response.status}: ${body}`,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
1411
|
+
return {
|
|
1412
|
+
ok: false,
|
|
1413
|
+
message: `No response within ${Math.ceil(params.requestTimeoutMs / 1000)}s`,
|
|
1414
|
+
};
|
|
628
1415
|
}
|
|
629
|
-
|
|
630
|
-
|
|
1416
|
+
return {
|
|
1417
|
+
ok: false,
|
|
1418
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1419
|
+
};
|
|
631
1420
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1421
|
+
finally {
|
|
1422
|
+
clearTimeout(timeout);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async waitForAppHealthCheck(apiBaseUrl, options) {
|
|
1426
|
+
const healthCheckUrl = Install.buildHealthCheckUrl(apiBaseUrl);
|
|
1427
|
+
const timeoutMs = options?.timeoutMs ?? APP_HEALTH_CHECK_TIMEOUT_MS;
|
|
1428
|
+
const intervalMs = options?.intervalMs ?? APP_HEALTH_CHECK_INTERVAL_MS;
|
|
1429
|
+
const requestTimeoutMs = options?.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS;
|
|
1430
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
1431
|
+
const startedAt = Date.now();
|
|
1432
|
+
let lastMessage = 'No response yet';
|
|
1433
|
+
let taskActive = true;
|
|
1434
|
+
startTask(`Waiting for application health check: ${healthCheckUrl}. NocoBase has started and is still booting...`);
|
|
1435
|
+
try {
|
|
1436
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1437
|
+
const result = await Install.requestAppHealthCheck({
|
|
1438
|
+
healthCheckUrl,
|
|
1439
|
+
fetchImpl,
|
|
1440
|
+
requestTimeoutMs,
|
|
1441
|
+
});
|
|
1442
|
+
if (result.ok) {
|
|
1443
|
+
stopTask();
|
|
1444
|
+
taskActive = false;
|
|
1445
|
+
p.log.info(`Application health check passed: ${healthCheckUrl}`);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
lastMessage = result.message;
|
|
1449
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
1450
|
+
updateTask(`Waiting for application health check: ${healthCheckUrl}. Still starting... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`);
|
|
1451
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
1452
|
+
if (remainingMs <= 0) {
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
await Install.sleep(Math.min(intervalMs, remainingMs));
|
|
639
1456
|
}
|
|
640
|
-
rootPassword = typeof passAns === 'string' && passAns.length > 0 ? passAns : undefined;
|
|
641
1457
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
placeholder: 'Super Admin',
|
|
646
|
-
});
|
|
647
|
-
if (p.isCancel(nickAns)) {
|
|
648
|
-
p.cancel('Install cancelled.');
|
|
649
|
-
this.exit(0);
|
|
1458
|
+
finally {
|
|
1459
|
+
if (taskActive) {
|
|
1460
|
+
stopTask();
|
|
650
1461
|
}
|
|
651
|
-
const t = nickAns.trim();
|
|
652
|
-
rootNickname = t || undefined;
|
|
653
1462
|
}
|
|
654
|
-
|
|
1463
|
+
const logHint = options?.containerName
|
|
1464
|
+
? ` You can inspect startup logs with: docker logs ${options.containerName}`
|
|
1465
|
+
: '';
|
|
1466
|
+
throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
|
|
655
1467
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
662
|
-
|
|
1468
|
+
async saveInstalledEnv(params) {
|
|
1469
|
+
await this.config.runCommand('env:add', Install.buildEnvAddArgv(params));
|
|
1470
|
+
}
|
|
1471
|
+
static pushArgIfValue(argv, flag, value) {
|
|
1472
|
+
const text = String(value ?? '').trim();
|
|
1473
|
+
if (text) {
|
|
1474
|
+
argv.push(flag, text);
|
|
663
1475
|
}
|
|
664
|
-
|
|
665
|
-
|
|
1476
|
+
}
|
|
1477
|
+
static pushBooleanArgIfSet(argv, flag, value) {
|
|
1478
|
+
if (value === undefined) {
|
|
1479
|
+
return;
|
|
666
1480
|
}
|
|
667
|
-
|
|
668
|
-
|
|
1481
|
+
argv.push(Boolean(value) ? flag : `--no-${flag.replace(/^--/, '')}`);
|
|
1482
|
+
}
|
|
1483
|
+
static buildEnvAddArgv(params) {
|
|
1484
|
+
const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
|
|
1485
|
+
|| DEFAULT_INSTALL_APP_PORT;
|
|
1486
|
+
const storagePath = String(params.appResults.storagePath ?? '').trim()
|
|
1487
|
+
|| defaultInstallStoragePath(params.envName);
|
|
1488
|
+
const apiBaseUrl = Install.resolveApiBaseUrl({
|
|
1489
|
+
appResults: params.appResults,
|
|
1490
|
+
envAddResults: params.envAddResults,
|
|
1491
|
+
});
|
|
1492
|
+
const authType = String(params.envAddResults.authType ?? 'oauth').trim()
|
|
1493
|
+
|| 'oauth';
|
|
1494
|
+
const argv = [
|
|
1495
|
+
params.envName,
|
|
1496
|
+
'--no-intro',
|
|
1497
|
+
'--scope',
|
|
1498
|
+
CONFIG_SCOPE,
|
|
1499
|
+
'--api-base-url',
|
|
1500
|
+
apiBaseUrl,
|
|
1501
|
+
'--auth-type',
|
|
1502
|
+
authType,
|
|
1503
|
+
'--app-port',
|
|
1504
|
+
appPort,
|
|
1505
|
+
'--storage-path',
|
|
1506
|
+
storagePath,
|
|
1507
|
+
];
|
|
1508
|
+
Install.pushArgIfValue(argv, '--source', downloadResultsValue(params.downloadResults, 'source'));
|
|
1509
|
+
Install.pushArgIfValue(argv, '--download-version', downloadResultsValue(params.downloadResults, 'version'));
|
|
1510
|
+
Install.pushArgIfValue(argv, '--docker-registry', downloadResultsValue(params.downloadResults, 'dockerRegistry'));
|
|
1511
|
+
Install.pushArgIfValue(argv, '--docker-platform', downloadResultsValue(params.downloadResults, 'dockerPlatform'));
|
|
1512
|
+
Install.pushArgIfValue(argv, '--git-url', downloadResultsValue(params.downloadResults, 'gitUrl'));
|
|
1513
|
+
Install.pushArgIfValue(argv, '--npm-registry', downloadResultsValue(params.downloadResults, 'npmRegistry'));
|
|
1514
|
+
Install.pushBooleanArgIfSet(argv, '--dev-dependencies', downloadResultsValue(params.downloadResults, 'devDependencies'));
|
|
1515
|
+
Install.pushBooleanArgIfSet(argv, '--build', downloadResultsValue(params.downloadResults, 'build'));
|
|
1516
|
+
Install.pushBooleanArgIfSet(argv, '--build-dts', downloadResultsValue(params.downloadResults, 'buildDts'));
|
|
1517
|
+
Install.pushArgIfValue(argv, '--app-root-path', params.appResults.appRootPath);
|
|
1518
|
+
Install.pushArgIfValue(argv, '--app-key', params.appResults.appKey);
|
|
1519
|
+
Install.pushArgIfValue(argv, '--timezone', params.appResults.timeZone);
|
|
1520
|
+
Install.pushBooleanArgIfSet(argv, '--builtin-db', params.dbResults.builtinDb);
|
|
1521
|
+
Install.pushArgIfValue(argv, '--db-dialect', params.dbResults.dbDialect);
|
|
1522
|
+
Install.pushArgIfValue(argv, '--db-host', params.dbResults.dbHost);
|
|
1523
|
+
Install.pushArgIfValue(argv, '--db-port', params.dbResults.dbPort);
|
|
1524
|
+
Install.pushArgIfValue(argv, '--db-database', params.dbResults.dbDatabase);
|
|
1525
|
+
Install.pushArgIfValue(argv, '--db-user', params.dbResults.dbUser);
|
|
1526
|
+
Install.pushArgIfValue(argv, '--db-password', params.dbResults.dbPassword);
|
|
1527
|
+
if (authType === 'token') {
|
|
1528
|
+
argv.push('--access-token', String(params.envAddResults.accessToken ?? ''));
|
|
1529
|
+
}
|
|
1530
|
+
return argv;
|
|
1531
|
+
}
|
|
1532
|
+
async collectPromptResults(parsed, yes) {
|
|
1533
|
+
const resumePreset = await this.resolveResumePresetValues(parsed, yes);
|
|
1534
|
+
const envPreset = {
|
|
1535
|
+
...(resumePreset?.envPreset ?? {}),
|
|
1536
|
+
...Install.buildEnvPresetValuesFromFlags(parsed),
|
|
1537
|
+
};
|
|
1538
|
+
const envResults = await runPromptCatalog(Install.envPrompts, {
|
|
1539
|
+
initialValues: {
|
|
1540
|
+
env: DEFAULT_INSTALL_ENV_NAME,
|
|
1541
|
+
},
|
|
1542
|
+
values: envPreset,
|
|
1543
|
+
yes,
|
|
1544
|
+
});
|
|
1545
|
+
const envName = String(envResults.env ?? '').trim() || DEFAULT_INSTALL_ENV_NAME;
|
|
1546
|
+
const appPreset = {
|
|
1547
|
+
...(resumePreset?.appPreset ?? {}),
|
|
1548
|
+
...Install.buildAppPresetValuesFromFlags(parsed),
|
|
1549
|
+
};
|
|
1550
|
+
const appCatalog = Install.buildAppPromptsCatalog(envName);
|
|
1551
|
+
const appResults = await runPromptCatalog(appCatalog, {
|
|
1552
|
+
initialValues: await Install.buildAppPromptInitialValues({
|
|
1553
|
+
envName,
|
|
1554
|
+
flags: {
|
|
1555
|
+
...parsed,
|
|
1556
|
+
'app-root-path': parsed['app-root-path']
|
|
1557
|
+
?? Install.toOptionalPromptString(appPreset.appRootPath),
|
|
1558
|
+
'app-port': parsed['app-port']
|
|
1559
|
+
?? Install.toOptionalPromptString(appPreset.appPort),
|
|
1560
|
+
'storage-path': parsed['storage-path']
|
|
1561
|
+
?? Install.toOptionalPromptString(appPreset.storagePath),
|
|
1562
|
+
},
|
|
1563
|
+
}),
|
|
1564
|
+
values: appPreset,
|
|
1565
|
+
yes,
|
|
1566
|
+
});
|
|
1567
|
+
let downloadResults = {};
|
|
1568
|
+
if (Boolean(appResults.fetchSource)) {
|
|
1569
|
+
const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
|
|
1570
|
+
downloadOpts.values = {
|
|
1571
|
+
...(resumePreset?.downloadPreset ?? {}),
|
|
1572
|
+
...downloadOpts.values,
|
|
1573
|
+
...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes),
|
|
1574
|
+
};
|
|
1575
|
+
downloadOpts.yes = yes;
|
|
1576
|
+
downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
|
|
669
1577
|
}
|
|
670
|
-
|
|
671
|
-
|
|
1578
|
+
const dbPreset = {
|
|
1579
|
+
...(resumePreset?.dbPreset ?? {}),
|
|
1580
|
+
...Install.buildDbPresetValuesFromFlags(parsed),
|
|
1581
|
+
};
|
|
1582
|
+
const dbResults = await runPromptCatalog(Install.buildDbPromptsCatalog(downloadResults), {
|
|
1583
|
+
initialValues: {
|
|
1584
|
+
...downloadResults,
|
|
1585
|
+
...await Install.buildDbPromptInitialValues({
|
|
1586
|
+
flags: {
|
|
1587
|
+
...parsed,
|
|
1588
|
+
'db-port': parsed['db-port']
|
|
1589
|
+
?? Install.toOptionalPromptString(dbPreset.dbPort),
|
|
1590
|
+
},
|
|
1591
|
+
downloadResults,
|
|
1592
|
+
dbPreset,
|
|
1593
|
+
}),
|
|
1594
|
+
},
|
|
1595
|
+
values: dbPreset,
|
|
1596
|
+
yes,
|
|
1597
|
+
});
|
|
1598
|
+
const rootPreset = Install.buildRootPresetValuesFromFlags(parsed);
|
|
1599
|
+
const rootResults = await runPromptCatalog(Install.rootUserPrompts, {
|
|
1600
|
+
initialValues: {},
|
|
1601
|
+
values: rootPreset,
|
|
1602
|
+
yes,
|
|
1603
|
+
});
|
|
1604
|
+
const envAddResults = await runPromptCatalog(EnvAdd.prompts, {
|
|
1605
|
+
initialValues: {
|
|
1606
|
+
apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
|
|
1607
|
+
},
|
|
1608
|
+
values: {
|
|
1609
|
+
name: envName,
|
|
1610
|
+
scope: 'project',
|
|
1611
|
+
...(resumePreset?.envAddPreset ?? {}),
|
|
1612
|
+
},
|
|
1613
|
+
yes,
|
|
1614
|
+
});
|
|
1615
|
+
return {
|
|
1616
|
+
envName,
|
|
1617
|
+
envResults,
|
|
1618
|
+
appResults,
|
|
1619
|
+
downloadResults,
|
|
1620
|
+
dbResults,
|
|
1621
|
+
rootResults,
|
|
1622
|
+
envAddResults,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
async run() {
|
|
1626
|
+
const parsedResult = await this.parse(Install);
|
|
1627
|
+
const flags = parsedResult.flags;
|
|
1628
|
+
const parsed = {
|
|
1629
|
+
...flags,
|
|
1630
|
+
};
|
|
1631
|
+
if (!parsed['no-intro']) {
|
|
1632
|
+
p.intro('Set Up NocoBase');
|
|
672
1633
|
}
|
|
673
|
-
if (
|
|
674
|
-
|
|
1634
|
+
if (parsed.resume) {
|
|
1635
|
+
const envLabel = Install.toOptionalPromptString(parsed.env);
|
|
1636
|
+
p.log.step(envLabel
|
|
1637
|
+
? `Resuming setup for env "${envLabel}" from the saved workspace config`
|
|
1638
|
+
: 'Resuming setup from the saved workspace config');
|
|
675
1639
|
}
|
|
676
|
-
|
|
677
|
-
|
|
1640
|
+
const promptResults = await this.collectPromptResults(parsed, flags.yes);
|
|
1641
|
+
const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults, } = promptResults;
|
|
1642
|
+
const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
|
|
1643
|
+
const usesDockerResources = Boolean(dbResults.builtinDb)
|
|
1644
|
+
|| (Boolean(appResults.fetchSource) && source === 'docker');
|
|
1645
|
+
const workspaceName = usesDockerResources
|
|
1646
|
+
? await Install.ensureWorkspaceName()
|
|
1647
|
+
: undefined;
|
|
1648
|
+
let builtinDbPlan;
|
|
1649
|
+
if (Boolean(dbResults.builtinDb)) {
|
|
1650
|
+
builtinDbPlan = await this.startBuiltinDb({
|
|
1651
|
+
envName,
|
|
1652
|
+
workspaceName,
|
|
1653
|
+
appResults,
|
|
1654
|
+
downloadResults,
|
|
1655
|
+
dbResults,
|
|
1656
|
+
force: parsed.force,
|
|
1657
|
+
});
|
|
1658
|
+
dbResults.dbHost = builtinDbPlan.dbHost;
|
|
1659
|
+
dbResults.dbPort = builtinDbPlan.dbPort;
|
|
1660
|
+
dbResults.dbDialect = builtinDbPlan.dbDialect;
|
|
1661
|
+
dbResults.dbDatabase = builtinDbPlan.dbDatabase;
|
|
1662
|
+
dbResults.dbUser = builtinDbPlan.dbUser;
|
|
1663
|
+
dbResults.dbPassword = builtinDbPlan.dbPassword;
|
|
678
1664
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1665
|
+
let dockerAppPlan;
|
|
1666
|
+
let localAppPlan;
|
|
1667
|
+
if (Boolean(appResults.fetchSource)) {
|
|
1668
|
+
if (source === 'docker') {
|
|
1669
|
+
dockerAppPlan = await this.installDockerApp({
|
|
1670
|
+
envName,
|
|
1671
|
+
workspaceName,
|
|
1672
|
+
appResults,
|
|
1673
|
+
downloadResults,
|
|
1674
|
+
dbResults,
|
|
1675
|
+
rootResults,
|
|
1676
|
+
builtinDbPlan,
|
|
1677
|
+
force: parsed.force,
|
|
1678
|
+
});
|
|
1679
|
+
appResults.appKey = dockerAppPlan.appKey;
|
|
1680
|
+
appResults.timeZone = dockerAppPlan.timeZone;
|
|
1681
|
+
}
|
|
1682
|
+
else if (source === 'npm' || source === 'git') {
|
|
1683
|
+
const projectRoot = await this.downloadLocalApp({
|
|
1684
|
+
envName,
|
|
1685
|
+
appResults,
|
|
1686
|
+
downloadResults,
|
|
1687
|
+
});
|
|
1688
|
+
localAppPlan = await this.startLocalApp({
|
|
1689
|
+
envName,
|
|
1690
|
+
source,
|
|
1691
|
+
projectRoot,
|
|
1692
|
+
appResults,
|
|
1693
|
+
dbResults,
|
|
1694
|
+
rootResults,
|
|
1695
|
+
});
|
|
1696
|
+
appResults.appKey = localAppPlan.appKey;
|
|
1697
|
+
appResults.timeZone = localAppPlan.timeZone;
|
|
1698
|
+
}
|
|
694
1699
|
}
|
|
695
1700
|
else {
|
|
696
|
-
|
|
1701
|
+
p.log.info('Skipped app download and install.');
|
|
697
1702
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1703
|
+
if (dockerAppPlan || localAppPlan) {
|
|
1704
|
+
await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
|
|
1705
|
+
appResults,
|
|
1706
|
+
envAddResults,
|
|
1707
|
+
}), {
|
|
1708
|
+
containerName: dockerAppPlan?.containerName,
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
await this.saveInstalledEnv({
|
|
1712
|
+
envName,
|
|
1713
|
+
appResults,
|
|
1714
|
+
downloadResults,
|
|
1715
|
+
dbResults,
|
|
1716
|
+
envAddResults,
|
|
1717
|
+
});
|
|
1718
|
+
p.outro(dockerAppPlan || localAppPlan
|
|
1719
|
+
? `NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`
|
|
1720
|
+
: `Install config for "${envName}" has been saved.`);
|
|
702
1721
|
}
|
|
703
1722
|
}
|
|
1723
|
+
function downloadResultsValue(downloadResults, key) {
|
|
1724
|
+
return downloadResults[key];
|
|
1725
|
+
}
|