@nocobase/cli 2.1.0-alpha.4 → 2.1.0-alpha.40

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.
Files changed (211) hide show
  1. package/LICENSE.txt +107 -0
  2. package/README.md +393 -19
  3. package/README.zh-CN.md +343 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +135 -0
  6. package/bin/session-env.js +39 -0
  7. package/dist/commands/api/resource/create.js +15 -0
  8. package/dist/commands/api/resource/destroy.js +15 -0
  9. package/dist/commands/api/resource/get.js +15 -0
  10. package/dist/commands/api/resource/index.js +20 -0
  11. package/dist/commands/api/resource/list.js +16 -0
  12. package/dist/commands/api/resource/query.js +15 -0
  13. package/dist/commands/api/resource/update.js +15 -0
  14. package/dist/commands/app/down.js +301 -0
  15. package/dist/commands/app/logs.js +114 -0
  16. package/dist/commands/app/restart.js +158 -0
  17. package/dist/commands/app/start.js +305 -0
  18. package/dist/commands/app/stop.js +115 -0
  19. package/dist/commands/app/upgrade.js +636 -0
  20. package/dist/commands/backup/create.js +147 -0
  21. package/dist/commands/backup/index.js +20 -0
  22. package/dist/commands/backup/restore.js +105 -0
  23. package/{src/cli.js → dist/commands/build.js} +4 -11
  24. package/dist/commands/config/delete.js +30 -0
  25. package/dist/commands/config/get.js +29 -0
  26. package/dist/commands/config/index.js +20 -0
  27. package/dist/commands/config/list.js +29 -0
  28. package/dist/commands/config/set.js +35 -0
  29. package/dist/commands/db/check.js +240 -0
  30. package/dist/commands/db/logs.js +85 -0
  31. package/dist/commands/db/ps.js +60 -0
  32. package/dist/commands/db/shared.js +96 -0
  33. package/dist/commands/db/start.js +71 -0
  34. package/dist/commands/db/stop.js +71 -0
  35. package/{templates/plugin/src/client/models/index.ts → dist/commands/dev.js} +4 -4
  36. package/{src/commands/locale/react-js-cron/index.js → dist/commands/down.js} +3 -8
  37. package/dist/commands/download.js +13 -0
  38. package/dist/commands/env/add.js +366 -0
  39. package/dist/commands/env/auth.js +130 -0
  40. package/dist/commands/env/current.js +21 -0
  41. package/dist/commands/env/info.js +157 -0
  42. package/dist/commands/env/list.js +44 -0
  43. package/dist/commands/env/remove.js +84 -0
  44. package/dist/commands/env/shared.js +158 -0
  45. package/dist/commands/env/status.js +90 -0
  46. package/dist/commands/env/update.js +74 -0
  47. package/dist/commands/env/use.js +38 -0
  48. package/dist/commands/examples/prompts-stages.js +150 -0
  49. package/dist/commands/examples/prompts-test.js +181 -0
  50. package/dist/commands/init.js +1092 -0
  51. package/dist/commands/install.js +2378 -0
  52. package/dist/commands/license/activate.js +360 -0
  53. package/dist/commands/license/env.js +94 -0
  54. package/dist/commands/license/generate-id.js +108 -0
  55. package/dist/commands/license/id.js +70 -0
  56. package/dist/commands/license/index.js +20 -0
  57. package/dist/commands/license/plugins/clean.js +115 -0
  58. package/dist/commands/license/plugins/index.js +20 -0
  59. package/dist/commands/license/plugins/list.js +64 -0
  60. package/dist/commands/license/plugins/shared.js +325 -0
  61. package/dist/commands/license/plugins/sync.js +285 -0
  62. package/dist/commands/license/shared.js +423 -0
  63. package/dist/commands/license/status.js +64 -0
  64. package/dist/commands/logs.js +12 -0
  65. package/dist/commands/plugin/disable.js +86 -0
  66. package/dist/commands/plugin/enable.js +86 -0
  67. package/dist/commands/plugin/list.js +82 -0
  68. package/dist/commands/pm/disable.js +12 -0
  69. package/dist/commands/pm/enable.js +12 -0
  70. package/dist/commands/pm/list.js +12 -0
  71. package/dist/commands/restart.js +12 -0
  72. package/dist/commands/scaffold/migration.js +38 -0
  73. package/dist/commands/scaffold/plugin.js +37 -0
  74. package/dist/commands/self/check.js +71 -0
  75. package/dist/commands/self/index.js +20 -0
  76. package/dist/commands/self/update.js +95 -0
  77. package/dist/commands/session/id.js +24 -0
  78. package/dist/commands/session/remove.js +57 -0
  79. package/dist/commands/session/setup.js +62 -0
  80. package/dist/commands/skills/check.js +69 -0
  81. package/dist/commands/skills/index.js +20 -0
  82. package/dist/commands/skills/install.js +80 -0
  83. package/dist/commands/skills/remove.js +80 -0
  84. package/dist/commands/skills/update.js +87 -0
  85. package/dist/commands/source/build.js +58 -0
  86. package/dist/commands/source/dev.js +182 -0
  87. package/dist/commands/source/download.js +880 -0
  88. package/dist/commands/source/publish.js +109 -0
  89. package/dist/commands/source/registry/logs.js +70 -0
  90. package/dist/commands/source/registry/start.js +57 -0
  91. package/dist/commands/source/registry/status.js +33 -0
  92. package/dist/commands/source/registry/stop.js +48 -0
  93. package/dist/commands/source/test.js +477 -0
  94. package/dist/commands/start.js +12 -0
  95. package/dist/commands/stop.js +12 -0
  96. package/dist/commands/test.js +12 -0
  97. package/dist/commands/upgrade.js +12 -0
  98. package/dist/commands/v1.js +210 -0
  99. package/dist/generated/command-registry.js +133 -0
  100. package/dist/help/runtime-help.js +23 -0
  101. package/dist/lib/api-client.js +329 -0
  102. package/dist/lib/app-health.js +126 -0
  103. package/dist/lib/app-managed-resources.js +316 -0
  104. package/dist/lib/app-runtime.js +180 -0
  105. package/dist/lib/auth-store.js +368 -0
  106. package/dist/lib/backup.js +171 -0
  107. package/dist/lib/bootstrap.js +403 -0
  108. package/dist/lib/build-config.js +18 -0
  109. package/dist/lib/builtin-db.js +86 -0
  110. package/dist/lib/cli-config.js +176 -0
  111. package/dist/lib/cli-home.js +47 -0
  112. package/dist/lib/cli-locale.js +129 -0
  113. package/dist/lib/command-discovery.js +39 -0
  114. package/dist/lib/db-connection-check.js +158 -0
  115. package/dist/lib/docker-env-file.js +52 -0
  116. package/dist/lib/docker-image.js +37 -0
  117. package/dist/lib/env-auth.js +873 -0
  118. package/dist/lib/env-config.js +94 -0
  119. package/dist/lib/env-guard.js +62 -0
  120. package/dist/lib/generated-command.js +186 -0
  121. package/dist/lib/http-request.js +49 -0
  122. package/dist/lib/inquirer-theme.js +17 -0
  123. package/dist/lib/inquirer.js +244 -0
  124. package/dist/lib/naming.js +70 -0
  125. package/dist/lib/object-utils.js +76 -0
  126. package/dist/lib/openapi.js +62 -0
  127. package/dist/lib/plugin-storage.js +64 -0
  128. package/dist/lib/post-processors.js +23 -0
  129. package/dist/lib/prompt-catalog-core.js +185 -0
  130. package/dist/lib/prompt-catalog-terminal.js +375 -0
  131. package/{src/index.js → dist/lib/prompt-catalog.js} +2 -6
  132. package/dist/lib/prompt-validators.js +240 -0
  133. package/dist/lib/prompt-web-ui.js +2103 -0
  134. package/dist/lib/resource-command.js +357 -0
  135. package/dist/lib/resource-request.js +104 -0
  136. package/dist/lib/run-npm.js +275 -0
  137. package/dist/lib/runtime-env-vars.js +32 -0
  138. package/dist/lib/runtime-generator.js +498 -0
  139. package/dist/lib/runtime-store.js +56 -0
  140. package/dist/lib/self-manager.js +301 -0
  141. package/dist/lib/session-id.js +17 -0
  142. package/dist/lib/session-integration.js +703 -0
  143. package/dist/lib/session-store.js +118 -0
  144. package/dist/lib/skills-manager.js +360 -0
  145. package/dist/lib/source-publish.js +306 -0
  146. package/dist/lib/source-registry.js +188 -0
  147. package/dist/lib/startup-update.js +285 -0
  148. package/dist/lib/ui.js +155 -0
  149. package/dist/locale/en-US.json +344 -0
  150. package/dist/locale/zh-CN.json +344 -0
  151. package/dist/post-processors/data-modeling.js +84 -0
  152. package/dist/post-processors/data-source-manager.js +138 -0
  153. package/dist/post-processors/index.js +19 -0
  154. package/nocobase-ctl.config.json +388 -0
  155. package/package.json +100 -26
  156. package/LICENSE +0 -661
  157. package/bin/index.js +0 -39
  158. package/nocobase.conf.tpl +0 -95
  159. package/src/commands/benchmark.js +0 -73
  160. package/src/commands/build.js +0 -49
  161. package/src/commands/clean.js +0 -30
  162. package/src/commands/client.js +0 -166
  163. package/src/commands/create-nginx-conf.js +0 -37
  164. package/src/commands/create-plugin.js +0 -33
  165. package/src/commands/dev.js +0 -200
  166. package/src/commands/doc.js +0 -76
  167. package/src/commands/e2e.js +0 -265
  168. package/src/commands/global.js +0 -43
  169. package/src/commands/index.js +0 -45
  170. package/src/commands/instance-id.js +0 -47
  171. package/src/commands/locale/cronstrue.js +0 -122
  172. package/src/commands/locale/react-js-cron/en-US.json +0 -75
  173. package/src/commands/locale/react-js-cron/zh-CN.json +0 -33
  174. package/src/commands/locale/react-js-cron/zh-TW.json +0 -33
  175. package/src/commands/locale.js +0 -81
  176. package/src/commands/p-test.js +0 -88
  177. package/src/commands/perf.js +0 -63
  178. package/src/commands/pkg.js +0 -321
  179. package/src/commands/pm2.js +0 -37
  180. package/src/commands/postinstall.js +0 -88
  181. package/src/commands/start.js +0 -148
  182. package/src/commands/tar.js +0 -36
  183. package/src/commands/test-coverage.js +0 -55
  184. package/src/commands/test.js +0 -107
  185. package/src/commands/umi.js +0 -33
  186. package/src/commands/update-deps.js +0 -72
  187. package/src/commands/upgrade.js +0 -47
  188. package/src/commands/view-license-key.js +0 -44
  189. package/src/license.js +0 -76
  190. package/src/logger.js +0 -75
  191. package/src/plugin-generator.js +0 -80
  192. package/src/util.js +0 -517
  193. package/templates/bundle-status.html +0 -338
  194. package/templates/create-app-package.json +0 -39
  195. package/templates/plugin/.npmignore.tpl +0 -2
  196. package/templates/plugin/README.md.tpl +0 -1
  197. package/templates/plugin/client.d.ts +0 -2
  198. package/templates/plugin/client.js +0 -1
  199. package/templates/plugin/package.json.tpl +0 -11
  200. package/templates/plugin/server.d.ts +0 -2
  201. package/templates/plugin/server.js +0 -1
  202. package/templates/plugin/src/client/client.d.ts +0 -249
  203. package/templates/plugin/src/client/index.tsx.tpl +0 -1
  204. package/templates/plugin/src/client/locale.ts +0 -21
  205. package/templates/plugin/src/client/plugin.tsx.tpl +0 -10
  206. package/templates/plugin/src/index.ts +0 -2
  207. package/templates/plugin/src/locale/en-US.json +0 -1
  208. package/templates/plugin/src/locale/zh-CN.json +0 -1
  209. package/templates/plugin/src/server/collections/.gitkeep +0 -0
  210. package/templates/plugin/src/server/index.ts.tpl +0 -1
  211. package/templates/plugin/src/server/plugin.ts.tpl +0 -19
@@ -0,0 +1,2378 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { spawn } from 'node:child_process';
11
+ import crypto from 'node:crypto';
12
+ import { mkdir } from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ import { exit } from 'node:process';
15
+ import { runPromptCatalog, } from "../lib/prompt-catalog.js";
16
+ import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
17
+ import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
18
+ import { defaultDockerContainerPrefix, defaultDockerNetworkName, } from '../lib/app-runtime.js';
19
+ import { resolveDockerContainerPrefix, resolveDockerNetworkName, } from '../lib/cli-config.js';
20
+ import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../lib/docker-image.js";
21
+ import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
22
+ import { validateExternalDbConfig } from "../lib/db-connection-check.js";
23
+ import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
24
+ import { run, runNocoBaseCommand } from '../lib/run-npm.js';
25
+ import { printInfo, printStage, printVerbose, printWarning, setVerboseMode, } from '../lib/ui.js';
26
+ import { omitKeys, upperFirst } from "../lib/object-utils.js";
27
+ import { getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
28
+ import { buildStoredEnvConfig } from '../lib/env-config.js';
29
+ import { resolveDockerEnvFileArg, } from "../lib/docker-env-file.js";
30
+ import Download, { defaultDockerRegistryForLang, } from './download.js';
31
+ import EnvAdd from "./env/add.js";
32
+ const DEFAULT_INSTALL_ENV_NAME = 'local';
33
+ const DEFAULT_INSTALL_LANG = 'en-US';
34
+ const DEFAULT_INSTALL_APP_PORT = '13000';
35
+ const DEFAULT_INSTALL_DB_HOST = '127.0.0.1';
36
+ const DEFAULT_INSTALL_BUILTIN_DB_HOST = 'postgres';
37
+ const DEFAULT_INSTALL_DB_PORTS = {
38
+ postgres: '5432',
39
+ mysql: '3306',
40
+ mariadb: '3306',
41
+ kingbase: '54321',
42
+ };
43
+ const DEFAULT_INSTALL_BUILTIN_DB_IMAGES = {
44
+ postgres: 'postgres:16',
45
+ mysql: 'mysql:8',
46
+ mariadb: 'mariadb:11',
47
+ kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
48
+ };
49
+ const DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN = {
50
+ postgres: 'registry.cn-shanghai.aliyuncs.com/nocobase/postgres:16',
51
+ mysql: 'registry.cn-shanghai.aliyuncs.com/nocobase/mysql:8',
52
+ mariadb: 'registry.cn-shanghai.aliyuncs.com/nocobase/mariadb:11',
53
+ kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
54
+ };
55
+ const DEFAULT_INSTALL_DB_DATABASE = 'nocobase';
56
+ const DEFAULT_INSTALL_DB_USER = 'nocobase';
57
+ const DEFAULT_INSTALL_DB_PASSWORD = 'nocobase';
58
+ const DEFAULT_INSTALL_ROOT_USERNAME = 'nocobase';
59
+ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
60
+ const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
61
+ const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
62
+ const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
63
+ const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
64
+ const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
65
+ const INSTALL_DB_DIALECTS = ['postgres', 'mysql', 'mariadb', 'kingbase'];
66
+ const INSTALL_LANGUAGE_CODES = {
67
+ 'ar-EG': { label: 'العربية' },
68
+ 'az-AZ': { label: 'Azərbaycan dili' },
69
+ 'bg-BG': { label: 'Български' },
70
+ 'bn-BD': { label: 'Bengali' },
71
+ 'by-BY': { label: 'Беларускі' },
72
+ 'ca-ES': { label: 'Сatalà/Espanya' },
73
+ 'cs-CZ': { label: 'Česky' },
74
+ 'da-DK': { label: 'Dansk' },
75
+ 'de-DE': { label: 'Deutsch' },
76
+ 'el-GR': { label: 'Ελληνικά' },
77
+ 'en-GB': { label: 'English(GB)' },
78
+ 'en-US': { label: 'English' },
79
+ 'es-ES': { label: 'Español' },
80
+ 'et-EE': { label: 'Estonian (Eesti)' },
81
+ 'fa-IR': { label: 'فارسی' },
82
+ 'fi-FI': { label: 'Suomi' },
83
+ 'fr-BE': { label: 'Français(BE)' },
84
+ 'fr-CA': { label: 'Français(CA)' },
85
+ 'fr-FR': { label: 'Français' },
86
+ 'ga-IE': { label: 'Gaeilge' },
87
+ 'gl-ES': { label: 'Galego' },
88
+ 'he-IL': { label: 'עברית' },
89
+ 'hi-IN': { label: 'हिन्दी' },
90
+ 'hr-HR': { label: 'Hrvatski jezik' },
91
+ 'hu-HU': { label: 'Magyar' },
92
+ 'hy-AM': { label: 'Հայերեն' },
93
+ 'id-ID': { label: 'Bahasa Indonesia' },
94
+ 'is-IS': { label: 'Íslenska' },
95
+ 'it-IT': { label: 'Italiano' },
96
+ 'ja-JP': { label: '日本語' },
97
+ 'ka-GE': { label: 'ქართული' },
98
+ 'kk-KZ': { label: 'Қазақ тілі' },
99
+ 'km-KH': { label: 'ភាសាខ្មែរ' },
100
+ 'kn-IN': { label: 'ಕನ್ನಡ' },
101
+ 'ko-KR': { label: '한국어' },
102
+ 'ku-IQ': { label: 'کوردی' },
103
+ 'lt-LT': { label: 'lietuvių' },
104
+ 'lv-LV': { label: 'Latviešu valoda' },
105
+ 'mk-MK': { label: 'македонски јазик' },
106
+ 'ml-IN': { label: 'മലയാളം' },
107
+ 'mn-MN': { label: 'Монгол хэл' },
108
+ 'ms-MY': { label: 'بهاس ملايو' },
109
+ 'nb-NO': { label: 'Norsk bokmål' },
110
+ 'ne-NP': { label: 'नेपाली' },
111
+ 'nl-BE': { label: 'Vlaams' },
112
+ 'nl-NL': { label: 'Nederlands' },
113
+ 'pl-PL': { label: 'Polski' },
114
+ 'pt-BR': { label: 'Português brasileiro' },
115
+ 'pt-PT': { label: 'Português' },
116
+ 'ro-RO': { label: 'România' },
117
+ 'ru-RU': { label: 'Русский' },
118
+ 'si-LK': { label: 'සිංහල' },
119
+ 'sk-SK': { label: 'Slovenčina' },
120
+ 'sl-SI': { label: 'Slovenščina' },
121
+ 'sr-RS': { label: 'српски језик' },
122
+ 'sv-SE': { label: 'Svenska' },
123
+ 'ta-IN': { label: 'Tamil' },
124
+ 'th-TH': { label: 'ภาษาไทย' },
125
+ 'tk-TK': { label: 'Turkmen' },
126
+ 'tr-TR': { label: 'Türkçe' },
127
+ 'uk-UA': { label: 'Українська' },
128
+ 'ur-PK': { label: 'Oʻzbekcha' },
129
+ 'vi-VN': { label: 'Tiếng Việt' },
130
+ 'zh-CN': { label: '简体中文' },
131
+ 'zh-HK': { label: '繁體中文(香港)' },
132
+ 'zh-TW': { label: '繁體中文(台湾)' },
133
+ };
134
+ const INSTALL_LANGUAGE_OPTIONS = Object.entries(INSTALL_LANGUAGE_CODES).map(([value, { label }]) => ({
135
+ value,
136
+ label: `${label} (${value})`,
137
+ }));
138
+ const installText = (key, values) => localeText(`commands.install.${key}`, values);
139
+ function formatDeferredAuthMessage(envName, authType) {
140
+ const normalizedAuthType = String(authType ?? '').trim();
141
+ const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
142
+ if (normalizedAuthType === 'token') {
143
+ return `${nextStep} You will be prompted for an access token.`;
144
+ }
145
+ if (normalizedAuthType === 'oauth') {
146
+ return `${nextStep} A browser sign-in flow will be started.`;
147
+ }
148
+ return nextStep;
149
+ }
150
+ function argvHasToken(argv, tokens) {
151
+ return tokens.some((t) => argv.includes(t));
152
+ }
153
+ function isInstallDbDialect(value) {
154
+ return INSTALL_DB_DIALECTS.includes(value);
155
+ }
156
+ function downloadVersionPromptValue(version) {
157
+ return version === 'latest' || version === 'beta' || version === 'alpha'
158
+ ? version
159
+ : 'other';
160
+ }
161
+ function supportsBuiltinDbDialect(value) {
162
+ const dialect = String(value ?? '').trim();
163
+ return Object.prototype.hasOwnProperty.call(DEFAULT_INSTALL_BUILTIN_DB_IMAGES, dialect);
164
+ }
165
+ export function defaultDbPortForDialect(value) {
166
+ const dialect = String(value ?? 'postgres').trim();
167
+ return DEFAULT_INSTALL_DB_PORTS[isInstallDbDialect(dialect) ? dialect : 'postgres'];
168
+ }
169
+ function defaultBuiltinDbImageForDialect(value) {
170
+ const dialect = String(value ?? 'postgres').trim();
171
+ const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
172
+ ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN
173
+ : DEFAULT_INSTALL_BUILTIN_DB_IMAGES;
174
+ return supportsBuiltinDbDialect(dialect)
175
+ ? defaults[dialect]
176
+ : defaults.postgres;
177
+ }
178
+ function defaultDbDatabaseForDialect(value) {
179
+ return String(value ?? '').trim() === 'kingbase'
180
+ ? 'kingbase'
181
+ : DEFAULT_INSTALL_DB_DATABASE;
182
+ }
183
+ function defaultDbHostForBuiltinDb(values) {
184
+ return Boolean(values.builtinDb)
185
+ ? DEFAULT_INSTALL_BUILTIN_DB_HOST
186
+ : DEFAULT_INSTALL_DB_HOST;
187
+ }
188
+ function validateBuiltinDbEnabled(value, values) {
189
+ if (!Boolean(value)) {
190
+ return undefined;
191
+ }
192
+ const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
193
+ if (supportsBuiltinDbDialect(dialect)) {
194
+ return undefined;
195
+ }
196
+ return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
197
+ }
198
+ async function validateExternalDbPromptField(value, values) {
199
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
200
+ if (builtinDb) {
201
+ return undefined;
202
+ }
203
+ if (typeof value === 'string' && value.trim() === '') {
204
+ return undefined;
205
+ }
206
+ return await validateExternalDbConfig(values);
207
+ }
208
+ function defaultInstallAppRootPath(envName) {
209
+ const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
210
+ return `./${name}/source/`;
211
+ }
212
+ function defaultInstallStoragePath(envName) {
213
+ const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
214
+ return `./${name}/storage/`;
215
+ }
216
+ function pickPresetKeys(source, keys) {
217
+ const out = {};
218
+ for (const k of keys) {
219
+ if (Object.prototype.hasOwnProperty.call(source, k)) {
220
+ out[k] = source[k];
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ async function commandSucceeds(command, args, options) {
226
+ return await new Promise((resolve) => {
227
+ const child = spawn(command, args, {
228
+ cwd: options?.cwd,
229
+ env: {
230
+ ...process.env,
231
+ ...options?.env,
232
+ },
233
+ stdio: 'ignore',
234
+ });
235
+ child.once('error', () => resolve(false));
236
+ child.once('close', (code) => resolve(code === 0));
237
+ });
238
+ }
239
+ async function commandOutput(command, args, options) {
240
+ return await new Promise((resolve, reject) => {
241
+ const child = spawn(command, args, {
242
+ cwd: options?.cwd,
243
+ env: {
244
+ ...process.env,
245
+ ...options?.env,
246
+ },
247
+ stdio: ['ignore', 'pipe', 'pipe'],
248
+ });
249
+ let stdout = '';
250
+ let stderr = '';
251
+ child.stdout.setEncoding('utf8');
252
+ child.stderr.setEncoding('utf8');
253
+ child.stdout.on('data', (chunk) => {
254
+ stdout += chunk;
255
+ });
256
+ child.stderr.on('data', (chunk) => {
257
+ stderr += chunk;
258
+ });
259
+ child.once('error', reject);
260
+ child.once('close', (code, signal) => {
261
+ if (code === 0) {
262
+ resolve(stdout);
263
+ return;
264
+ }
265
+ if (signal) {
266
+ reject(new Error(`${command} exited due to signal ${signal}`));
267
+ return;
268
+ }
269
+ reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
270
+ });
271
+ });
272
+ }
273
+ function optionalEnvString(value) {
274
+ const text = String(value ?? '').trim();
275
+ return text || undefined;
276
+ }
277
+ function optionalEnvBoolean(value) {
278
+ if (value === undefined || value === null) {
279
+ return undefined;
280
+ }
281
+ return Boolean(value);
282
+ }
283
+ function pushOptionalEnvArg(args, key, value) {
284
+ if (typeof value === 'string') {
285
+ if (!value) {
286
+ return;
287
+ }
288
+ args.push('-e', `${key}=${value}`);
289
+ return;
290
+ }
291
+ if (typeof value === 'boolean') {
292
+ args.push('-e', `${key}=${String(value)}`);
293
+ }
294
+ }
295
+ function setOptionalEnvVar(out, key, value) {
296
+ if (typeof value === 'string') {
297
+ if (!value) {
298
+ return;
299
+ }
300
+ out[key] = value;
301
+ return;
302
+ }
303
+ if (typeof value === 'boolean') {
304
+ out[key] = String(value);
305
+ }
306
+ }
307
+ export default class Install extends Command {
308
+ ensuredDockerNetworks = new Set();
309
+ logStage(title) {
310
+ printStage(title);
311
+ }
312
+ logDetail(message) {
313
+ printVerbose(message);
314
+ }
315
+ static hidden = true;
316
+ static description = 'Install NocoBase: database, storage, admin user, and `nocobase-v1 install`. Optionally run `nb source download` first; distribution and image details are configured on `nb source download`, not here. Use `--resume` to continue an interrupted setup from the saved workspace env config.';
317
+ static examples = [
318
+ '<%= config.bin %> <%= command.id %>',
319
+ '<%= config.bin %> <%= command.id %> --env app1',
320
+ '<%= config.bin %> <%= command.id %> --env app1 --resume',
321
+ '<%= config.bin %> <%= command.id %> --env app1 -f',
322
+ '<%= config.bin %> <%= command.id %> --env app1 -l zh-CN',
323
+ '<%= config.bin %> <%= command.id %> --env app1 --root-username nocobase --root-email admin@nocobase.com --root-password admin123',
324
+ '<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
325
+ '<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
326
+ '<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
327
+ '<%= config.bin %> <%= command.id %> --env dev -y --fetch-source --app-root-path=./dev/source/',
328
+ ];
329
+ static flags = {
330
+ yes: Flags.boolean({
331
+ char: 'y',
332
+ description: 'Skip interactive prompts; use flags and defaults only',
333
+ default: false,
334
+ }),
335
+ resume: Flags.boolean({
336
+ description: 'Resume a previous unfinished setup for this env using the saved workspace env config',
337
+ default: false,
338
+ }),
339
+ verbose: Flags.boolean({
340
+ description: 'Show detailed command output',
341
+ default: false,
342
+ }),
343
+ 'skip-save-env-log': Flags.boolean({
344
+ hidden: true,
345
+ default: false,
346
+ }),
347
+ env: Flags.string({
348
+ char: 'e',
349
+ description: 'App/env name to create or update. Defaults app paths to ./<envName>/source/ and ./<envName>/storage/.',
350
+ }),
351
+ 'default-api-base-url': Flags.string({
352
+ char: 'd',
353
+ hidden: true,
354
+ description: 'Default API base URL for HTTP API calls, including the /api prefix (e.g. http://localhost:13000/api)',
355
+ }),
356
+ 'api-base-url': Flags.string({
357
+ char: 'u',
358
+ description: 'Root URL for HTTP API calls, including the /api prefix (e.g. http://localhost:13000/api)',
359
+ }),
360
+ 'auth-type': Flags.string({
361
+ char: 'a',
362
+ description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`)',
363
+ options: ['token', 'oauth'],
364
+ }),
365
+ 'access-token': Flags.string({
366
+ char: 't',
367
+ aliases: ['token'],
368
+ description: 'API key or access token when using --auth-type token',
369
+ }),
370
+ 'skip-auth': Flags.boolean({
371
+ description: 'Save the env auth mode now and finish authentication later with `nb env auth`',
372
+ default: false,
373
+ }),
374
+ lang: Flags.string({ description: 'Language for the installed NocoBase app', char: 'l', required: false }),
375
+ force: Flags.boolean({
376
+ description: 'Reconfigure an existing env and replace conflicting runtime resources when needed',
377
+ char: 'f',
378
+ required: false,
379
+ }),
380
+ 'app-root-path': Flags.string({
381
+ description: 'Source directory for a local npm/git app (default: ./<envName>/source/)',
382
+ }),
383
+ 'app-port': Flags.string({
384
+ description: 'HTTP port for the local app (default: 13000, or an available port with --yes)',
385
+ }),
386
+ 'storage-path': Flags.string({
387
+ description: 'Storage directory for uploads and managed database data (default: ./<envName>/storage/)',
388
+ }),
389
+ 'root-username': Flags.string({
390
+ description: 'Initial admin username for the installed app',
391
+ required: false,
392
+ }),
393
+ 'root-email': Flags.string({
394
+ description: 'Initial admin email for the installed app',
395
+ required: false,
396
+ }),
397
+ 'root-password': Flags.string({
398
+ description: 'Initial admin password for the installed app',
399
+ required: false,
400
+ }),
401
+ 'root-nickname': Flags.string({
402
+ description: 'Initial admin display name for the installed app',
403
+ required: false,
404
+ }),
405
+ 'builtin-db': Flags.boolean({
406
+ allowNo: true,
407
+ description: 'Create and connect a CLI-managed built-in database for the app',
408
+ default: false,
409
+ }),
410
+ 'db-dialect': Flags.string({
411
+ description: 'Database dialect for the app',
412
+ options: ['postgres', 'mysql', 'mariadb', 'kingbase'],
413
+ }),
414
+ 'builtin-db-image': Flags.string({
415
+ description: 'Docker image for the built-in database container (default follows the selected database)',
416
+ }),
417
+ 'db-host': Flags.string({
418
+ description: 'Database host for the app',
419
+ }),
420
+ 'db-port': Flags.string({
421
+ description: 'Database port for the app',
422
+ }),
423
+ 'db-database': Flags.string({
424
+ description: 'Database name for the app',
425
+ }),
426
+ 'db-user': Flags.string({
427
+ description: 'Database username for the app',
428
+ }),
429
+ 'db-password': Flags.string({
430
+ description: 'Database password for the app',
431
+ }),
432
+ 'db-schema': Flags.string({
433
+ description: 'Database schema for the app',
434
+ }),
435
+ 'db-table-prefix': Flags.string({
436
+ description: 'Database table prefix for the app',
437
+ }),
438
+ 'db-underscored': Flags.boolean({
439
+ allowNo: true,
440
+ description: 'Use underscored database naming for the app',
441
+ default: false,
442
+ }),
443
+ 'fetch-source': Flags.boolean({
444
+ description: 'Download NocoBase app files or pull a Docker image before installing',
445
+ default: false,
446
+ }),
447
+ ...omitKeys(Download.flags, ['yes']),
448
+ };
449
+ /** Environment name only: run before {@link Install.prompts} (see `run`). */
450
+ static envPrompts = {
451
+ env: {
452
+ type: 'text',
453
+ message: installText('prompts.env.message'),
454
+ placeholder: installText('prompts.env.placeholder'),
455
+ required: true,
456
+ validate: validateEnvKey,
457
+ },
458
+ };
459
+ static appPrompts = {
460
+ lang: {
461
+ type: 'select',
462
+ message: installText('prompts.lang.message'),
463
+ options: INSTALL_LANGUAGE_OPTIONS,
464
+ initialValue: DEFAULT_INSTALL_LANG,
465
+ yesInitialValue: DEFAULT_INSTALL_LANG,
466
+ },
467
+ // force: {
468
+ // type: 'boolean',
469
+ // message: 'Reinstall the application by clearing the database? (-f / --force)',
470
+ // initialValue: false,
471
+ // yesInitialValue: false,
472
+ // },
473
+ appRootPath: {
474
+ type: 'text',
475
+ message: installText('prompts.appRootPath.message'),
476
+ placeholder: installText('prompts.appRootPath.placeholder'),
477
+ initialValue: (values) => defaultInstallAppRootPath(values.env ?? values.appName),
478
+ },
479
+ appPort: {
480
+ type: 'text',
481
+ message: installText('prompts.appPort.message'),
482
+ placeholder: installText('prompts.appPort.placeholder'),
483
+ validate: Install.validateAppPort,
484
+ },
485
+ storagePath: {
486
+ type: 'text',
487
+ message: installText('prompts.storagePath.message'),
488
+ placeholder: installText('prompts.storagePath.placeholder'),
489
+ initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
490
+ },
491
+ fetchSource: {
492
+ type: 'boolean',
493
+ message: installText('prompts.fetchSource.message'),
494
+ initialValue: true,
495
+ yesInitialValue: true,
496
+ },
497
+ };
498
+ static dbPrompts = {
499
+ dbDialect: {
500
+ type: 'select',
501
+ message: installText('prompts.dbDialect.message'),
502
+ options: [
503
+ { value: 'postgres', label: 'PostgreSQL' },
504
+ { value: 'mysql', label: 'MySQL' },
505
+ { value: 'mariadb', label: 'MariaDB' },
506
+ { value: 'kingbase', label: 'KingbaseES' },
507
+ ],
508
+ initialValue: 'postgres',
509
+ yesInitialValue: 'postgres',
510
+ required: true,
511
+ },
512
+ builtinDb: {
513
+ type: 'boolean',
514
+ message: installText('prompts.builtinDb.message'),
515
+ initialValue: true,
516
+ yesInitialValue: true,
517
+ validate: validateBuiltinDbEnabled,
518
+ },
519
+ builtinDbImage: {
520
+ type: 'text',
521
+ message: installText('prompts.builtinDbImage.message'),
522
+ placeholder: installText('prompts.builtinDbImage.placeholder'),
523
+ initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
524
+ hidden: (values) => !Boolean(values.builtinDb)
525
+ || !supportsBuiltinDbDialect(values.dbDialect),
526
+ required: true,
527
+ },
528
+ dbHost: {
529
+ type: 'text',
530
+ message: installText('prompts.dbHost.message'),
531
+ placeholder: installText('prompts.dbHost.placeholder'),
532
+ initialValue: (values) => defaultDbHostForBuiltinDb(values),
533
+ yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
534
+ required: true,
535
+ validate: validateExternalDbPromptField,
536
+ hidden: (values) => Boolean(values.builtinDb),
537
+ },
538
+ dbPort: {
539
+ type: 'text',
540
+ message: installText('prompts.dbPort.message'),
541
+ placeholder: installText('prompts.dbPort.placeholder'),
542
+ initialValue: (values) => defaultDbPortForDialect(values.dbDialect),
543
+ required: true,
544
+ validate: Install.validateDbPort,
545
+ hidden: (values) => Boolean(values.builtinDb)
546
+ && String(values.source ?? '').trim() === 'docker',
547
+ },
548
+ dbDatabase: {
549
+ type: 'text',
550
+ message: installText('prompts.dbDatabase.message'),
551
+ initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
552
+ required: true,
553
+ validate: validateExternalDbPromptField,
554
+ },
555
+ dbUser: {
556
+ type: 'text',
557
+ message: installText('prompts.dbUser.message'),
558
+ initialValue: DEFAULT_INSTALL_DB_USER,
559
+ yesInitialValue: DEFAULT_INSTALL_DB_USER,
560
+ required: true,
561
+ validate: validateExternalDbPromptField,
562
+ },
563
+ dbPassword: {
564
+ type: 'password',
565
+ message: installText('prompts.dbPassword.message'),
566
+ initialValue: DEFAULT_INSTALL_DB_PASSWORD,
567
+ yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
568
+ required: true,
569
+ validate: validateExternalDbPromptField,
570
+ },
571
+ };
572
+ static rootUserPrompts = {
573
+ rootUsername: {
574
+ type: 'text',
575
+ message: installText('prompts.rootUsername.message'),
576
+ placeholder: installText('prompts.rootUsername.placeholder'),
577
+ yesInitialValue: DEFAULT_INSTALL_ROOT_USERNAME,
578
+ required: true,
579
+ },
580
+ rootEmail: {
581
+ type: 'text',
582
+ message: installText('prompts.rootEmail.message'),
583
+ placeholder: installText('prompts.rootEmail.placeholder'),
584
+ yesInitialValue: DEFAULT_INSTALL_ROOT_EMAIL,
585
+ required: true,
586
+ },
587
+ rootPassword: {
588
+ type: 'password',
589
+ message: installText('prompts.rootPassword.message'),
590
+ yesInitialValue: DEFAULT_INSTALL_ROOT_PASSWORD,
591
+ required: true,
592
+ },
593
+ rootNickname: {
594
+ type: 'text',
595
+ message: installText('prompts.rootNickname.message'),
596
+ placeholder: installText('prompts.rootNickname.placeholder'),
597
+ yesInitialValue: DEFAULT_INSTALL_ROOT_NICKNAME,
598
+ required: true,
599
+ },
600
+ };
601
+ /**
602
+ * App catalog with `env` seeded into `out` first so `storagePath`’s `initialValue(values)`
603
+ * sees `values.env` (same iteration order as {@link runPromptCatalog}).
604
+ */
605
+ static buildAppPromptsCatalog(seedEnv, options) {
606
+ return {
607
+ seedEnv: {
608
+ type: 'run',
609
+ run: (values) => {
610
+ values.env = seedEnv;
611
+ },
612
+ },
613
+ seedResume: {
614
+ type: 'run',
615
+ run: (values) => {
616
+ const record = values;
617
+ record.resume = Boolean(options?.resume);
618
+ },
619
+ },
620
+ ...Install.appPrompts,
621
+ };
622
+ }
623
+ static buildDbPromptsCatalog(envName, downloadResults, options) {
624
+ const source = String(downloadResults.source ?? '').trim();
625
+ return {
626
+ seedEnv: {
627
+ type: 'run',
628
+ run: (values) => {
629
+ if (envName) {
630
+ values.env = envName;
631
+ }
632
+ },
633
+ },
634
+ seedDownloadSource: {
635
+ type: 'run',
636
+ run: (values) => {
637
+ if (source) {
638
+ values.source = source;
639
+ }
640
+ },
641
+ },
642
+ seedResume: {
643
+ type: 'run',
644
+ run: (values) => {
645
+ const record = values;
646
+ record.resume = Boolean(options?.resume);
647
+ },
648
+ },
649
+ ...Install.dbPrompts,
650
+ };
651
+ }
652
+ /** Preset for {@link Install.envPrompts} only (`env` flag). */
653
+ static buildEnvPresetValuesFromFlags(flags) {
654
+ const preset = {};
655
+ if (flags.env !== undefined && String(flags.env).trim() !== '') {
656
+ preset.env = String(flags.env).trim();
657
+ }
658
+ return preset;
659
+ }
660
+ /**
661
+ * Preset `values` for `runPromptCatalog`: keys here skip that prompt and fix the result.
662
+ * Booleans with defaults are only preset when the user passed the flag on argv (see `download`).
663
+ * Does not include `env` — use {@link buildEnvPresetValuesFromFlags} for {@link Install.envPrompts}.
664
+ */
665
+ static buildPresetValuesFromFlags(flags) {
666
+ const preset = {};
667
+ const argv = process.argv.slice(2);
668
+ const apiBaseUrl = Install.toOptionalPromptString(flags['api-base-url']);
669
+ if (apiBaseUrl) {
670
+ preset.apiBaseUrl = apiBaseUrl;
671
+ }
672
+ else if (flags['default-api-base-url'] !== undefined) {
673
+ const defaultApiBaseUrl = Install.toOptionalPromptString(flags['default-api-base-url']);
674
+ if (defaultApiBaseUrl) {
675
+ preset.apiBaseUrl = defaultApiBaseUrl;
676
+ }
677
+ }
678
+ if (flags['auth-type'] !== undefined) {
679
+ const authType = Install.toOptionalPromptString(flags['auth-type']);
680
+ if (authType) {
681
+ preset.authType = authType;
682
+ }
683
+ }
684
+ if (flags['skip-auth']) {
685
+ preset.skipAuth = true;
686
+ }
687
+ if (flags['access-token'] !== undefined || flags.token !== undefined) {
688
+ preset.accessToken = String(flags['access-token'] ?? flags.token ?? '');
689
+ }
690
+ if (flags.lang !== undefined) {
691
+ const v = String(flags.lang).trim();
692
+ if (v) {
693
+ preset.lang = v;
694
+ }
695
+ }
696
+ if (argvHasToken(argv, ['--force', '-f'])) {
697
+ preset.force = flags.force;
698
+ }
699
+ if (flags['app-root-path'] !== undefined) {
700
+ const v = flags['app-root-path']?.trim();
701
+ if (v) {
702
+ preset.appRootPath = v;
703
+ }
704
+ }
705
+ if (flags['app-port'] !== undefined) {
706
+ const v = String(flags['app-port'] ?? '').trim();
707
+ if (v) {
708
+ preset.appPort = v;
709
+ }
710
+ }
711
+ if (flags['storage-path'] !== undefined) {
712
+ const v = flags['storage-path']?.trim();
713
+ if (v) {
714
+ preset.storagePath = v;
715
+ }
716
+ }
717
+ if (flags['root-username'] !== undefined) {
718
+ preset.rootUsername = String(flags['root-username'] ?? '').trim();
719
+ }
720
+ if (flags['root-email'] !== undefined) {
721
+ preset.rootEmail = String(flags['root-email'] ?? '').trim();
722
+ }
723
+ if (flags['root-password'] !== undefined) {
724
+ preset.rootPassword = String(flags['root-password'] ?? '');
725
+ }
726
+ if (flags['root-nickname'] !== undefined) {
727
+ preset.rootNickname = String(flags['root-nickname'] ?? '').trim();
728
+ }
729
+ if (argvHasToken(argv, ['--fetch-source'])) {
730
+ preset.fetchSource = flags['fetch-source'];
731
+ }
732
+ if (argvHasToken(argv, ['--builtin-db', '--no-builtin-db'])) {
733
+ preset.builtinDb = flags['builtin-db'];
734
+ }
735
+ if (flags['db-dialect'] !== undefined) {
736
+ const t = String(flags['db-dialect']).trim();
737
+ if (t && isInstallDbDialect(t)) {
738
+ preset.dbDialect = t;
739
+ }
740
+ }
741
+ if (flags['builtin-db-image'] !== undefined) {
742
+ const v = String(flags['builtin-db-image'] ?? '').trim();
743
+ if (v) {
744
+ preset.builtinDbImage = v;
745
+ }
746
+ }
747
+ if (flags['db-host'] !== undefined) {
748
+ const v = String(flags['db-host'] ?? '').trim();
749
+ if (v) {
750
+ preset.dbHost = v;
751
+ preset.builtinDb = false;
752
+ }
753
+ }
754
+ if (flags['db-port'] !== undefined) {
755
+ const v = String(flags['db-port'] ?? '').trim();
756
+ if (v) {
757
+ preset.dbPort = v;
758
+ }
759
+ }
760
+ if (flags['db-database'] !== undefined) {
761
+ const v = String(flags['db-database'] ?? '').trim();
762
+ if (v) {
763
+ preset.dbDatabase = v;
764
+ }
765
+ }
766
+ if (flags['db-user'] !== undefined) {
767
+ const v = String(flags['db-user'] ?? '').trim();
768
+ if (v) {
769
+ preset.dbUser = v;
770
+ }
771
+ }
772
+ if (flags['db-password'] !== undefined) {
773
+ preset.dbPassword = String(flags['db-password'] ?? '');
774
+ }
775
+ if (flags['db-schema'] !== undefined) {
776
+ const v = String(flags['db-schema'] ?? '').trim();
777
+ if (v) {
778
+ preset.dbSchema = v;
779
+ }
780
+ }
781
+ if (flags['db-table-prefix'] !== undefined) {
782
+ const v = String(flags['db-table-prefix'] ?? '').trim();
783
+ if (v) {
784
+ preset.dbTablePrefix = v;
785
+ }
786
+ }
787
+ if (argvHasToken(argv, ['--db-underscored', '--no-db-underscored'])) {
788
+ preset.dbUnderscored = flags['db-underscored'];
789
+ }
790
+ return preset;
791
+ }
792
+ static buildAppPresetValuesFromFlags(flags) {
793
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
794
+ 'lang',
795
+ 'force',
796
+ 'appRootPath',
797
+ 'appPort',
798
+ 'storagePath',
799
+ 'fetchSource',
800
+ ]);
801
+ }
802
+ static buildDbPresetValuesFromFlags(flags) {
803
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
804
+ 'builtinDb',
805
+ 'dbDialect',
806
+ 'builtinDbImage',
807
+ 'dbHost',
808
+ 'dbPort',
809
+ 'dbDatabase',
810
+ 'dbUser',
811
+ 'dbPassword',
812
+ 'dbSchema',
813
+ 'dbTablePrefix',
814
+ 'dbUnderscored',
815
+ ]);
816
+ }
817
+ static buildRootPresetValuesFromFlags(flags) {
818
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
819
+ 'rootUsername',
820
+ 'rootEmail',
821
+ 'rootPassword',
822
+ 'rootNickname',
823
+ ]);
824
+ }
825
+ static buildEnvAddPresetValuesFromFlags(flags) {
826
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
827
+ 'apiBaseUrl',
828
+ 'authType',
829
+ 'accessToken',
830
+ ]);
831
+ }
832
+ buildEnvAddPromptsForInstall(parsed) {
833
+ const apiBaseUrlPrompt = {
834
+ ...EnvAdd.prompts.apiBaseUrl,
835
+ validate: undefined,
836
+ };
837
+ const prompts = {
838
+ ...EnvAdd.prompts,
839
+ apiBaseUrl: apiBaseUrlPrompt,
840
+ };
841
+ if (!parsed['skip-auth']) {
842
+ return prompts;
843
+ }
844
+ const accessTokenPrompt = {
845
+ ...EnvAdd.prompts.accessToken,
846
+ hidden: () => true,
847
+ };
848
+ return {
849
+ ...prompts,
850
+ accessToken: accessTokenPrompt,
851
+ };
852
+ }
853
+ static toOptionalPromptString(value) {
854
+ if (value === undefined || value === null) {
855
+ return undefined;
856
+ }
857
+ const text = String(value).trim();
858
+ return text || undefined;
859
+ }
860
+ static async validateAppPort(value, values) {
861
+ const formatError = validateTcpPort(value);
862
+ if (formatError) {
863
+ return formatError;
864
+ }
865
+ return await Install.validateResumeAwareTcpPort(value, values, 'app');
866
+ }
867
+ static async validateDbPort(value, values) {
868
+ const formatError = validateTcpPort(value);
869
+ if (formatError) {
870
+ return formatError;
871
+ }
872
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
873
+ const source = String(values.source ?? '').trim();
874
+ if (!builtinDb || source === 'docker') {
875
+ if (!builtinDb) {
876
+ return await validateExternalDbConfig({ ...values, dbPort: value });
877
+ }
878
+ return undefined;
879
+ }
880
+ return await Install.validateResumeAwareTcpPort(value, values, 'db');
881
+ }
882
+ static async validateResumeAwareTcpPort(value, values, target) {
883
+ const portError = await validateAvailableTcpPort(value);
884
+ if (!portError) {
885
+ return undefined;
886
+ }
887
+ const context = await Install.readResumePortValidationContext(values);
888
+ if (!context) {
889
+ return portError;
890
+ }
891
+ const port = Install.toOptionalPromptString(value);
892
+ if (!port) {
893
+ return portError;
894
+ }
895
+ const reusesManagedPort = await Install.isResumeManagedPortReuse({
896
+ target,
897
+ port,
898
+ context,
899
+ });
900
+ return reusesManagedPort ? undefined : portError;
901
+ }
902
+ static async ensureExternalDbReadyForInstall(dbResults) {
903
+ const builtinDb = dbResults.builtinDb === undefined ? true : Boolean(dbResults.builtinDb);
904
+ if (builtinDb) {
905
+ return;
906
+ }
907
+ const dialect = String(dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
908
+ const host = String(dbResults.dbHost ?? '').trim();
909
+ const port = String(dbResults.dbPort ?? '').trim();
910
+ const database = String(dbResults.dbDatabase ?? '').trim();
911
+ const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
912
+ const target = database ? `${address}/${database}` : address;
913
+ printVerbose(`Checking external ${dialect} database: ${target}`);
914
+ const validationError = await validateExternalDbConfig(dbResults);
915
+ if (validationError) {
916
+ throw new Error(validationError);
917
+ }
918
+ }
919
+ static async readResumePortValidationContext(values) {
920
+ if (!Boolean(values.resume)) {
921
+ return undefined;
922
+ }
923
+ const envName = Install.toOptionalPromptString(values.env);
924
+ if (!envName) {
925
+ return undefined;
926
+ }
927
+ const source = Install.toOptionalPromptString(values.source);
928
+ const builtinDb = values.builtinDb === undefined ? undefined : Boolean(values.builtinDb);
929
+ const dbDialect = Install.toOptionalPromptString(values.dbDialect);
930
+ const appRootPath = Install.toOptionalPromptString(values.appRootPath);
931
+ const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
932
+ const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
933
+ return {
934
+ envName,
935
+ ...(dockerNetworkName ? { dockerNetworkName } : {}),
936
+ ...(dockerContainerPrefix ? { dockerContainerPrefix } : {}),
937
+ ...(source ? { source } : {}),
938
+ ...(builtinDb !== undefined ? { builtinDb } : {}),
939
+ ...(dbDialect ? { dbDialect } : {}),
940
+ ...(appRootPath ? { appRootPath } : {}),
941
+ };
942
+ }
943
+ static async resolveResumeDockerNetworkName() {
944
+ return await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() });
945
+ }
946
+ static async resolveResumeDockerContainerPrefix() {
947
+ return await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() });
948
+ }
949
+ static async isResumeManagedPortReuse(params) {
950
+ if (params.target === 'app') {
951
+ if ((params.context.source === 'npm' || params.context.source === 'git')
952
+ && params.context.appRootPath) {
953
+ return await Install.isLocalPm2ProcessUsingPort(params.context.appRootPath, params.port);
954
+ }
955
+ const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.dockerContainerPrefix);
956
+ return await Install.isDockerContainerPublishingPort(containerName, params.port);
957
+ }
958
+ if (!params.context.builtinDb || params.context.source === 'docker') {
959
+ return false;
960
+ }
961
+ const containerName = Install.buildBuiltinDbContainerName(params.context.envName, params.context.dbDialect ?? 'postgres', params.context.dockerContainerPrefix);
962
+ return await Install.isDockerContainerPublishingPort(containerName, params.port);
963
+ }
964
+ static async isDockerContainerPublishingPort(containerName, port) {
965
+ if (!containerName || !port) {
966
+ return false;
967
+ }
968
+ const exists = await commandSucceeds('docker', [
969
+ 'container',
970
+ 'inspect',
971
+ containerName,
972
+ ]);
973
+ if (!exists) {
974
+ return false;
975
+ }
976
+ try {
977
+ const output = await commandOutput('docker', ['port', containerName]);
978
+ return output
979
+ .split(/\r?\n/)
980
+ .some((line) => line.includes(`:${port}`));
981
+ }
982
+ catch {
983
+ return false;
984
+ }
985
+ }
986
+ static async isLocalPm2ProcessUsingPort(appRootPath, port) {
987
+ const cwd = resolveConfiguredEnvPath(appRootPath);
988
+ if (!cwd) {
989
+ return false;
990
+ }
991
+ try {
992
+ const output = await commandOutput('pm2', ['jlist'], { cwd });
993
+ const rows = JSON.parse(output);
994
+ return rows.some((row) => {
995
+ const pmCwd = Install.toOptionalPromptString(row.pm2_env?.pm_cwd);
996
+ const appPort = Install.toOptionalPromptString(row.pm2_env?.env?.APP_PORT);
997
+ return Boolean(pmCwd && appPort && pmCwd === cwd && appPort === port);
998
+ });
999
+ }
1000
+ catch {
1001
+ return false;
1002
+ }
1003
+ }
1004
+ static buildResumePresetValues(env) {
1005
+ const envName = String(env.name ?? '').trim();
1006
+ const config = env.config ?? {};
1007
+ const source = Install.toOptionalPromptString(config.source);
1008
+ const appRootPath = Install.toOptionalPromptString(config.appRootPath);
1009
+ const appPort = Install.toOptionalPromptString(config.appPort);
1010
+ const storagePath = Install.toOptionalPromptString(config.storagePath);
1011
+ const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
1012
+ const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
1013
+ const dockerPlatform = Install.toOptionalPromptString(config.dockerPlatform);
1014
+ const gitUrl = Install.toOptionalPromptString(config.gitUrl);
1015
+ const npmRegistry = Install.toOptionalPromptString(config.npmRegistry);
1016
+ const dbDialect = Install.toOptionalPromptString(config.dbDialect);
1017
+ const dbHost = Install.toOptionalPromptString(config.dbHost);
1018
+ const dbPort = Install.toOptionalPromptString(config.dbPort);
1019
+ const dbDatabase = Install.toOptionalPromptString(config.dbDatabase);
1020
+ const dbUser = Install.toOptionalPromptString(config.dbUser);
1021
+ const dbPassword = Install.toOptionalPromptString(config.dbPassword);
1022
+ const dbSchema = Install.toOptionalPromptString(config.dbSchema);
1023
+ const dbTablePrefix = Install.toOptionalPromptString(config.dbTablePrefix);
1024
+ const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
1025
+ const builtinDbImage = Install.toOptionalPromptString(config.builtinDbImage);
1026
+ const rootUsername = Install.toOptionalPromptString(config.rootUsername);
1027
+ const rootEmail = Install.toOptionalPromptString(config.rootEmail);
1028
+ const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1029
+ const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1030
+ const auth = config.auth;
1031
+ const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1032
+ const appPreset = {
1033
+ ...(appRootPath ? { appRootPath } : {}),
1034
+ ...(appPort ? { appPort } : {}),
1035
+ ...(storagePath ? { storagePath } : {}),
1036
+ ...(source
1037
+ ? { fetchSource: true }
1038
+ : appRootPath
1039
+ ? { fetchSource: false }
1040
+ : {}),
1041
+ };
1042
+ const downloadPreset = {
1043
+ ...(source ? { source } : {}),
1044
+ ...(downloadVersion
1045
+ ? {
1046
+ version: downloadVersionPromptValue(downloadVersion),
1047
+ ...(downloadVersionPromptValue(downloadVersion) === 'other'
1048
+ ? { otherVersion: downloadVersion }
1049
+ : {}),
1050
+ }
1051
+ : {}),
1052
+ ...(dockerRegistry ? { dockerRegistry } : {}),
1053
+ ...(dockerPlatform ? { dockerPlatform } : {}),
1054
+ ...(gitUrl ? { gitUrl } : {}),
1055
+ ...(npmRegistry ? { npmRegistry } : {}),
1056
+ ...(typeof config.devDependencies === 'boolean'
1057
+ ? { devDependencies: config.devDependencies }
1058
+ : {}),
1059
+ ...(typeof config.build === 'boolean' ? { build: config.build } : {}),
1060
+ ...(typeof config.buildDts === 'boolean' ? { buildDts: config.buildDts } : {}),
1061
+ };
1062
+ const dbPreset = {
1063
+ ...(typeof config.builtinDb === 'boolean' ? { builtinDb: config.builtinDb } : {}),
1064
+ ...(dbDialect ? { dbDialect } : {}),
1065
+ ...(builtinDbImage ? { builtinDbImage } : {}),
1066
+ ...(dbHost ? { dbHost } : {}),
1067
+ ...(dbPort ? { dbPort } : {}),
1068
+ ...(dbDatabase ? { dbDatabase } : {}),
1069
+ ...(dbUser ? { dbUser } : {}),
1070
+ ...(dbPassword ? { dbPassword } : {}),
1071
+ ...(dbSchema ? { dbSchema } : {}),
1072
+ ...(dbTablePrefix ? { dbTablePrefix } : {}),
1073
+ ...(dbUnderscored !== undefined ? { dbUnderscored } : {}),
1074
+ };
1075
+ const rootPreset = {
1076
+ ...(rootUsername ? { rootUsername } : {}),
1077
+ ...(rootEmail ? { rootEmail } : {}),
1078
+ ...(rootPassword ? { rootPassword } : {}),
1079
+ ...(rootNickname ? { rootNickname } : {}),
1080
+ };
1081
+ const envAddPreset = {};
1082
+ if (savedAuthType === 'token') {
1083
+ envAddPreset.authType = 'token';
1084
+ if (Install.toOptionalPromptString(auth.accessToken)) {
1085
+ envAddPreset.accessToken = String(auth.accessToken);
1086
+ }
1087
+ }
1088
+ else if (savedAuthType === 'oauth') {
1089
+ envAddPreset.authType = 'oauth';
1090
+ }
1091
+ return {
1092
+ envPreset: {
1093
+ ...(envName ? { env: envName } : {}),
1094
+ },
1095
+ appPreset,
1096
+ downloadPreset,
1097
+ dbPreset,
1098
+ rootPreset,
1099
+ envAddPreset,
1100
+ };
1101
+ }
1102
+ static buildResumeMissingYesFlags(flags) {
1103
+ const missing = [];
1104
+ if (!Install.toOptionalPromptString(flags.lang)) {
1105
+ missing.push('--lang');
1106
+ }
1107
+ if (!Install.toOptionalPromptString(flags['root-username'])) {
1108
+ missing.push('--root-username');
1109
+ }
1110
+ if (!Install.toOptionalPromptString(flags['root-email'])) {
1111
+ missing.push('--root-email');
1112
+ }
1113
+ if (!Install.toOptionalPromptString(flags['root-password'])) {
1114
+ missing.push('--root-password');
1115
+ }
1116
+ if (!Install.toOptionalPromptString(flags['root-nickname'])) {
1117
+ missing.push('--root-nickname');
1118
+ }
1119
+ return missing;
1120
+ }
1121
+ async resolveResumePresetValues(parsed, yes) {
1122
+ if (!parsed.resume) {
1123
+ return undefined;
1124
+ }
1125
+ const env = await getEnv(parsed.env, { scope: resolveDefaultConfigScope() });
1126
+ if (!env) {
1127
+ throw new Error(formatMissingManagedAppEnvMessage(parsed.env));
1128
+ }
1129
+ if (yes) {
1130
+ const missingFlags = Install.buildResumeMissingYesFlags(parsed);
1131
+ if (missingFlags.length > 0) {
1132
+ throw new Error([
1133
+ `Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
1134
+ `These setup-only flags are not saved in the env config: ${missingFlags.join(', ')}`,
1135
+ `Run \`nb init --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1136
+ ].join('\n'));
1137
+ }
1138
+ }
1139
+ return Install.buildResumePresetValues(env);
1140
+ }
1141
+ static async resolveAvailableDefaultPort(defaultPort, options) {
1142
+ const normalized = String(defaultPort).trim();
1143
+ const portError = await validateAvailableTcpPort(normalized);
1144
+ if (!portError) {
1145
+ return normalized;
1146
+ }
1147
+ const nextPort = await findAvailableTcpPort();
1148
+ if (options?.warn) {
1149
+ printWarning(`${options.label ?? 'Default port'} ${normalized} is already in use. Using available port ${nextPort} for this setup.`);
1150
+ }
1151
+ return nextPort;
1152
+ }
1153
+ static async buildAppPromptInitialValues(params) {
1154
+ const initialValues = {};
1155
+ const envName = params.envName ?? DEFAULT_INSTALL_ENV_NAME;
1156
+ if (params.flags['app-root-path'] === undefined) {
1157
+ initialValues.appRootPath = defaultInstallAppRootPath(envName);
1158
+ }
1159
+ if (params.flags['storage-path'] === undefined) {
1160
+ initialValues.storagePath = defaultInstallStoragePath(envName);
1161
+ }
1162
+ if (params.flags['app-port'] === undefined) {
1163
+ initialValues.appPort = await Install.resolveAvailableDefaultPort(DEFAULT_INSTALL_APP_PORT, {
1164
+ label: 'Default app port',
1165
+ warn: params.warnOnPortFallback ?? true,
1166
+ });
1167
+ }
1168
+ return initialValues;
1169
+ }
1170
+ static shouldPublishBuiltinDbPortForValues(values) {
1171
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
1172
+ return builtinDb
1173
+ && Install.shouldPublishBuiltinDbPort(values.source);
1174
+ }
1175
+ static async buildDbPromptInitialValues(params) {
1176
+ if (params.flags['db-port'] !== undefined) {
1177
+ return {};
1178
+ }
1179
+ const values = {
1180
+ ...params.downloadResults,
1181
+ ...params.dbPreset,
1182
+ };
1183
+ if (!Install.shouldPublishBuiltinDbPortForValues(values)) {
1184
+ return {};
1185
+ }
1186
+ const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
1187
+ const defaultPort = defaultDbPortForDialect(dialect);
1188
+ return {
1189
+ dbPort: await Install.resolveAvailableDefaultPort(defaultPort, {
1190
+ label: `Default ${dialect} port`,
1191
+ warn: params.warnOnPortFallback ?? true,
1192
+ }),
1193
+ };
1194
+ }
1195
+ /**
1196
+ * When install runs {@link Download.prompts} after app prompts, align the download
1197
+ * output directory with app settings, while Docker registry defaults follow the CLI locale.
1198
+ */
1199
+ static buildDownloadPromptOptionsForInstall(appResults, envName) {
1200
+ const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1201
+ const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1202
+ const initialValues = {
1203
+ lang,
1204
+ dockerRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1205
+ outputDir: appRoot,
1206
+ };
1207
+ const values = {
1208
+ lang,
1209
+ };
1210
+ return {
1211
+ initialValues,
1212
+ values,
1213
+ yes: false,
1214
+ hooks: {
1215
+ onCancel: () => {
1216
+ exit(0);
1217
+ },
1218
+ onMissingNonInteractive: (message) => {
1219
+ console.error(message);
1220
+ exit(1);
1221
+ },
1222
+ },
1223
+ };
1224
+ }
1225
+ /**
1226
+ * Resolve the effective preset `values` for the embedded download step.
1227
+ * Explicit download flags win; otherwise `-y` falls back to the docker + alpha quickstart path.
1228
+ */
1229
+ static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes) {
1230
+ const preset = {};
1231
+ const argv = process.argv.slice(2);
1232
+ const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1233
+ const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1234
+ preset.lang = lang;
1235
+ if (flags.source !== undefined && String(flags.source).trim() !== '') {
1236
+ preset.source = String(flags.source).trim();
1237
+ }
1238
+ if (flags.version !== undefined) {
1239
+ const version = String(flags.version).trim() || 'latest';
1240
+ preset.version = downloadVersionPromptValue(version);
1241
+ if (preset.version === 'other') {
1242
+ preset.otherVersion = version;
1243
+ }
1244
+ }
1245
+ if (flags['docker-registry'] !== undefined) {
1246
+ const value = String(flags['docker-registry'] ?? '').trim();
1247
+ if (value) {
1248
+ preset.dockerRegistry = value;
1249
+ }
1250
+ }
1251
+ if (flags['docker-platform'] !== undefined) {
1252
+ const value = String(flags['docker-platform'] ?? '').trim();
1253
+ if (value) {
1254
+ preset.dockerPlatform = value;
1255
+ }
1256
+ }
1257
+ if (flags['output-dir'] !== undefined) {
1258
+ const value = String(flags['output-dir'] ?? '').trim();
1259
+ if (value) {
1260
+ preset.outputDir = value;
1261
+ }
1262
+ }
1263
+ if (flags['git-url'] !== undefined) {
1264
+ const value = String(flags['git-url'] ?? '').trim();
1265
+ if (value) {
1266
+ preset.gitUrl = value;
1267
+ }
1268
+ }
1269
+ if (flags['npm-registry'] !== undefined) {
1270
+ preset.npmRegistry =
1271
+ typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
1272
+ }
1273
+ if (flags.resume && !argvHasToken(argv, ['--replace', '-r'])) {
1274
+ preset.replace = true;
1275
+ }
1276
+ else if (argvHasToken(argv, ['--replace', '-r'])) {
1277
+ preset.replace = flags.replace;
1278
+ }
1279
+ if (argvHasToken(argv, ['--dev-dependencies', '--no-dev-dependencies', '-D'])) {
1280
+ preset.devDependencies = flags['dev-dependencies'];
1281
+ }
1282
+ if (argvHasToken(argv, ['--docker-save', '--no-docker-save'])) {
1283
+ preset.dockerSave = flags['docker-save'];
1284
+ }
1285
+ if (argvHasToken(argv, ['--build', '--no-build'])) {
1286
+ preset.build = flags.build;
1287
+ }
1288
+ if (argvHasToken(argv, ['--build-dts', '--no-build-dts'])) {
1289
+ preset.buildDts = flags['build-dts'];
1290
+ }
1291
+ if (yes) {
1292
+ preset.source ??= 'docker';
1293
+ preset.version ??= 'alpha';
1294
+ preset.outputDir ??= appRoot;
1295
+ }
1296
+ return preset;
1297
+ }
1298
+ static sanitizeDockerResourceName(value) {
1299
+ const normalized = value
1300
+ .trim()
1301
+ .toLowerCase()
1302
+ .replace(/[^a-z0-9_.-]+/g, '-')
1303
+ .replace(/-+/g, '-')
1304
+ .replace(/^-+|-+$/g, '');
1305
+ return normalized || 'nocobase';
1306
+ }
1307
+ static defaultDockerNetworkName() {
1308
+ return Install.sanitizeDockerResourceName(defaultDockerNetworkName());
1309
+ }
1310
+ static defaultDockerContainerPrefix() {
1311
+ return Install.sanitizeDockerResourceName(defaultDockerContainerPrefix(resolveEnvRoot(resolveDefaultConfigScope())));
1312
+ }
1313
+ static buildBuiltinDbContainerPrefix(containerPrefix) {
1314
+ const storedName = String(containerPrefix ?? '').trim();
1315
+ return storedName
1316
+ ? Install.sanitizeDockerResourceName(storedName)
1317
+ : Install.defaultDockerContainerPrefix();
1318
+ }
1319
+ static buildManagedDockerNetworkName(networkName) {
1320
+ const storedName = String(networkName ?? '').trim();
1321
+ return storedName
1322
+ ? Install.sanitizeDockerResourceName(storedName)
1323
+ : Install.defaultDockerNetworkName();
1324
+ }
1325
+ static buildBuiltinDbNetworkName(envName, networkName) {
1326
+ void envName;
1327
+ return Install.buildManagedDockerNetworkName(networkName);
1328
+ }
1329
+ static buildBuiltinDbContainerName(envName, dbDialect, containerPrefix) {
1330
+ return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-${dbDialect}`);
1331
+ }
1332
+ static buildDockerAppContainerName(envName, containerPrefix) {
1333
+ return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1334
+ }
1335
+ static buildInitAppEnvVars(params) {
1336
+ const out = {};
1337
+ const put = (key, value) => {
1338
+ const text = String(value ?? '').trim();
1339
+ if (!text) {
1340
+ return;
1341
+ }
1342
+ out[key] = text;
1343
+ };
1344
+ put('INIT_APP_LANG', params.appResults.lang);
1345
+ put('INIT_ROOT_USERNAME', params.rootResults.rootUsername);
1346
+ put('INIT_ROOT_EMAIL', params.rootResults.rootEmail);
1347
+ put('INIT_ROOT_PASSWORD', params.rootResults.rootPassword);
1348
+ put('INIT_ROOT_NICKNAME', params.rootResults.rootNickname);
1349
+ return out;
1350
+ }
1351
+ static shouldPublishBuiltinDbPort(source) {
1352
+ return String(source ?? '').trim() !== 'docker';
1353
+ }
1354
+ static buildBuiltinDbPlan(params) {
1355
+ const dbDialect = String(params.dbDialect ?? 'postgres').trim() || 'postgres';
1356
+ const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1357
+ || defaultDbPortForDialect(dbDialect);
1358
+ const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
1359
+ const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1360
+ const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.dockerContainerPrefix ?? params.workspaceName);
1361
+ const dbHostInput = String(params.dbHost ?? '').trim();
1362
+ const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
1363
+ ? (dbHostInput
1364
+ && dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST
1365
+ && dbHostInput !== containerName
1366
+ ? dbHostInput
1367
+ : DEFAULT_INSTALL_DB_HOST)
1368
+ : (dbHostInput
1369
+ && dbHostInput !== DEFAULT_INSTALL_DB_HOST
1370
+ && dbHostInput !== 'localhost'
1371
+ ? dbHostInput
1372
+ : containerName);
1373
+ const storagePath = resolveConfiguredEnvPath(params.storagePath)
1374
+ ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1375
+ if (dbDialect === 'postgres') {
1376
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1377
+ const dataDir = path.resolve(storagePath, 'db', 'postgres');
1378
+ const args = [
1379
+ 'run',
1380
+ '-d',
1381
+ '--name',
1382
+ containerName,
1383
+ '--restart',
1384
+ 'always',
1385
+ '--network',
1386
+ networkName,
1387
+ '-e',
1388
+ `POSTGRES_USER=${String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER}`,
1389
+ '-e',
1390
+ `POSTGRES_DB=${String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE}`,
1391
+ '-e',
1392
+ `POSTGRES_PASSWORD=${String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD}`,
1393
+ '-v',
1394
+ `${dataDir}:/var/lib/postgresql/data`,
1395
+ ];
1396
+ if (Install.shouldPublishBuiltinDbPort(params.source)) {
1397
+ args.push('-p', `${dbPort}:5432`);
1398
+ }
1399
+ args.push(image, 'postgres', '-c', 'wal_level=logical');
1400
+ return {
1401
+ source: String(params.source ?? '').trim() || undefined,
1402
+ dbDialect,
1403
+ dbHost,
1404
+ dbPort,
1405
+ dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim()
1406
+ || defaultDbDatabase,
1407
+ dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1408
+ || DEFAULT_INSTALL_DB_USER,
1409
+ dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1410
+ || DEFAULT_INSTALL_DB_PASSWORD,
1411
+ networkName,
1412
+ containerName,
1413
+ dataDir,
1414
+ builtinDbImage: image,
1415
+ image,
1416
+ args,
1417
+ };
1418
+ }
1419
+ if (dbDialect === 'mysql') {
1420
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1421
+ const dataDir = path.resolve(storagePath, 'db', 'mysql');
1422
+ const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1423
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
1424
+ const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1425
+ const args = [
1426
+ 'run',
1427
+ '-d',
1428
+ '--name',
1429
+ containerName,
1430
+ '--restart',
1431
+ 'always',
1432
+ '--network',
1433
+ networkName,
1434
+ '-e',
1435
+ `MYSQL_USER=${dbUser}`,
1436
+ '-e',
1437
+ `MYSQL_DATABASE=${dbDatabase}`,
1438
+ '-e',
1439
+ `MYSQL_PASSWORD=${dbPassword}`,
1440
+ '-e',
1441
+ `MYSQL_ROOT_PASSWORD=${dbPassword}`,
1442
+ '-v',
1443
+ `${dataDir}:/var/lib/mysql`,
1444
+ ];
1445
+ if (Install.shouldPublishBuiltinDbPort(params.source)) {
1446
+ args.push('-p', `${dbPort}:3306`);
1447
+ }
1448
+ args.push(image);
1449
+ return {
1450
+ source: String(params.source ?? '').trim() || undefined,
1451
+ dbDialect,
1452
+ dbHost,
1453
+ dbPort,
1454
+ dbDatabase,
1455
+ dbUser,
1456
+ dbPassword,
1457
+ networkName,
1458
+ containerName,
1459
+ dataDir,
1460
+ builtinDbImage: image,
1461
+ image,
1462
+ args,
1463
+ };
1464
+ }
1465
+ if (dbDialect === 'mariadb') {
1466
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1467
+ const dataDir = path.resolve(storagePath, 'db', 'mariadb');
1468
+ const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1469
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
1470
+ const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1471
+ const args = [
1472
+ 'run',
1473
+ '-d',
1474
+ '--name',
1475
+ containerName,
1476
+ '--restart',
1477
+ 'always',
1478
+ '--network',
1479
+ networkName,
1480
+ '-e',
1481
+ `MARIADB_USER=${dbUser}`,
1482
+ '-e',
1483
+ `MARIADB_DATABASE=${dbDatabase}`,
1484
+ '-e',
1485
+ `MARIADB_PASSWORD=${dbPassword}`,
1486
+ '-e',
1487
+ `MARIADB_ROOT_PASSWORD=${dbPassword}`,
1488
+ '-v',
1489
+ `${dataDir}:/var/lib/mysql`,
1490
+ ];
1491
+ if (Install.shouldPublishBuiltinDbPort(params.source)) {
1492
+ args.push('-p', `${dbPort}:3306`);
1493
+ }
1494
+ args.push(image);
1495
+ return {
1496
+ source: String(params.source ?? '').trim() || undefined,
1497
+ dbDialect,
1498
+ dbHost,
1499
+ dbPort,
1500
+ dbDatabase,
1501
+ dbUser,
1502
+ dbPassword,
1503
+ networkName,
1504
+ containerName,
1505
+ dataDir,
1506
+ builtinDbImage: image,
1507
+ image,
1508
+ args,
1509
+ };
1510
+ }
1511
+ if (dbDialect === 'kingbase') {
1512
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1513
+ const dataDir = path.resolve(storagePath, 'db', 'kingbase');
1514
+ const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1515
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
1516
+ const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1517
+ const args = [
1518
+ 'run',
1519
+ '-d',
1520
+ '--name',
1521
+ containerName,
1522
+ '--restart',
1523
+ 'always',
1524
+ '--network',
1525
+ networkName,
1526
+ '--platform',
1527
+ 'linux/amd64',
1528
+ '--privileged',
1529
+ '-e',
1530
+ 'ENABLE_CI=no',
1531
+ '-e',
1532
+ `DB_USER=${dbUser}`,
1533
+ '-e',
1534
+ `DB_PASSWORD=${dbPassword}`,
1535
+ '-e',
1536
+ 'DB_MODE=pg',
1537
+ '-e',
1538
+ 'NEED_START=yes',
1539
+ '-v',
1540
+ `${dataDir}:/home/kingbase/userdata`,
1541
+ ];
1542
+ if (Install.shouldPublishBuiltinDbPort(params.source)) {
1543
+ args.push('-p', `${dbPort}:54321`);
1544
+ }
1545
+ args.push(image, '/usr/sbin/init');
1546
+ return {
1547
+ source: String(params.source ?? '').trim() || undefined,
1548
+ dbDialect,
1549
+ dbHost,
1550
+ dbPort,
1551
+ dbDatabase,
1552
+ dbUser,
1553
+ dbPassword,
1554
+ networkName,
1555
+ containerName,
1556
+ dataDir,
1557
+ builtinDbImage: image,
1558
+ image,
1559
+ args,
1560
+ };
1561
+ }
1562
+ throw new Error(`Built-in database does not support "${dbDialect}" yet. Please choose PostgreSQL, MySQL, MariaDB, or KingbaseES.`);
1563
+ }
1564
+ async ensureDockerNetwork(name) {
1565
+ if (this.ensuredDockerNetworks.has(name)) {
1566
+ return;
1567
+ }
1568
+ printVerbose(`Checking Docker network: ${name}`);
1569
+ const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
1570
+ if (exists) {
1571
+ printVerbose(`Docker network already exists: ${name}`);
1572
+ this.ensuredDockerNetworks.add(name);
1573
+ return;
1574
+ }
1575
+ printVerbose(`Creating Docker network: ${name}`);
1576
+ try {
1577
+ await run('docker', ['network', 'create', name], {
1578
+ errorName: 'docker network create',
1579
+ });
1580
+ printVerbose(`Docker network is ready: ${name}`);
1581
+ this.ensuredDockerNetworks.add(name);
1582
+ }
1583
+ catch (error) {
1584
+ const message = error instanceof Error ? error.message : String(error);
1585
+ if (/address pools have been fully subnetted/i.test(message)) {
1586
+ throw new Error([
1587
+ `Docker could not create network "${name}" because its address pools are exhausted.`,
1588
+ 'Remove unused Docker networks and try again, for example: docker network prune',
1589
+ `Original error: ${message}`,
1590
+ ].join('\n'));
1591
+ }
1592
+ throw error;
1593
+ }
1594
+ }
1595
+ async dockerContainerExists(name) {
1596
+ return await commandSucceeds('docker', [
1597
+ 'container',
1598
+ 'inspect',
1599
+ name,
1600
+ ]);
1601
+ }
1602
+ async removeDockerContainer(name) {
1603
+ await run('docker', ['rm', '-f', name], {
1604
+ errorName: 'docker rm',
1605
+ stdio: 'ignore',
1606
+ });
1607
+ }
1608
+ async removeDockerContainerIfForced(params) {
1609
+ const exists = await this.dockerContainerExists(params.containerName);
1610
+ if (!exists) {
1611
+ return false;
1612
+ }
1613
+ if (!params.force) {
1614
+ return true;
1615
+ }
1616
+ printVerbose(`Removing existing ${params.displayName}: ${params.containerName}`);
1617
+ await this.removeDockerContainer(params.containerName);
1618
+ return false;
1619
+ }
1620
+ async inspectDockerContainerEnv(name) {
1621
+ const output = await commandOutput('docker', [
1622
+ 'inspect',
1623
+ '--format',
1624
+ '{{range .Config.Env}}{{println .}}{{end}}',
1625
+ name,
1626
+ ]);
1627
+ const env = {};
1628
+ for (const line of output.split(/\r?\n/)) {
1629
+ const index = line.indexOf('=');
1630
+ if (index <= 0) {
1631
+ continue;
1632
+ }
1633
+ env[line.slice(0, index)] = line.slice(index + 1);
1634
+ }
1635
+ return env;
1636
+ }
1637
+ async ensureBuiltinDbContainer(plan, options) {
1638
+ const exists = await this.dockerContainerExists(plan.containerName);
1639
+ if (exists) {
1640
+ printVerbose(`Built-in ${plan.dbDialect} container already exists: ${plan.containerName}`);
1641
+ return;
1642
+ }
1643
+ await mkdir(plan.dataDir, { recursive: true });
1644
+ await run('docker', plan.args, {
1645
+ errorName: 'docker run',
1646
+ stdio: options?.stdio ?? 'ignore',
1647
+ });
1648
+ }
1649
+ async startBuiltinDb(params) {
1650
+ const storagePath = String(params.appResults.storagePath ?? '').trim()
1651
+ || defaultInstallStoragePath(params.envName);
1652
+ const plan = Install.buildBuiltinDbPlan({
1653
+ envName: params.envName,
1654
+ workspaceName: params.workspaceName,
1655
+ dockerNetworkName: params.dockerNetworkName,
1656
+ dockerContainerPrefix: params.dockerContainerPrefix,
1657
+ storagePath,
1658
+ source: params.downloadResults.source,
1659
+ dbDialect: params.dbResults.dbDialect,
1660
+ dbHost: params.dbResults.dbHost,
1661
+ dbPort: params.dbResults.dbPort,
1662
+ dbDatabase: params.dbResults.dbDatabase,
1663
+ dbUser: params.dbResults.dbUser,
1664
+ dbPassword: params.dbResults.dbPassword,
1665
+ builtinDbImage: params.dbResults.builtinDbImage,
1666
+ });
1667
+ this.logStage('Preparing database');
1668
+ printInfo(`Using built-in ${plan.dbDialect} database.`);
1669
+ await this.ensureDockerNetwork(plan.networkName);
1670
+ const existingContainerKept = await this.removeDockerContainerIfForced({
1671
+ containerName: plan.containerName,
1672
+ displayName: `built-in ${plan.dbDialect} container`,
1673
+ force: params.force,
1674
+ });
1675
+ if (!existingContainerKept && Install.shouldPublishBuiltinDbPort(params.downloadResults.source)) {
1676
+ const portError = await validateAvailableTcpPort(plan.dbPort);
1677
+ if (portError) {
1678
+ throw new Error(`Built-in ${plan.dbDialect} needs host port ${plan.dbPort}, but ${portError}`);
1679
+ }
1680
+ }
1681
+ await this.ensureBuiltinDbContainer(plan, {
1682
+ stdio: params.commandStdio ?? 'ignore',
1683
+ });
1684
+ printInfo(`${upperFirst(plan.dbDialect)} database ready.`);
1685
+ printVerbose(`Built-in ${plan.dbDialect} database ready at ${plan.dbHost}:${plan.dbPort}`);
1686
+ return plan;
1687
+ }
1688
+ static async buildDockerAppPlan(params) {
1689
+ const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
1690
+ || defaultDockerRegistryForLang(process.env.NB_LOCALE);
1691
+ const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1692
+ const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1693
+ defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1694
+ defaultVersion: DEFAULT_DOCKER_VERSION,
1695
+ });
1696
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1697
+ const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim()
1698
+ || defaultInstallStoragePath(params.envName))
1699
+ ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1700
+ const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1701
+ const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
1702
+ const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1703
+ || defaultDbPortForDialect(dbDialect);
1704
+ const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1705
+ || DEFAULT_INSTALL_DB_DATABASE;
1706
+ const dbUser = String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1707
+ const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1708
+ const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1709
+ const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1710
+ const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1711
+ const appKey = crypto.randomBytes(32).toString('hex');
1712
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1713
+ const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1714
+ const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
1715
+ const envFile = await resolveDockerEnvFileArg(params.envName, configuredEnvFile ? { envFile: configuredEnvFile } : undefined);
1716
+ const initEnvVars = Install.buildInitAppEnvVars({
1717
+ appResults: params.appResults,
1718
+ rootResults: params.rootResults,
1719
+ });
1720
+ const args = [
1721
+ 'run',
1722
+ '-d',
1723
+ '--name',
1724
+ containerName,
1725
+ '--restart',
1726
+ 'always',
1727
+ '--network',
1728
+ params.networkName,
1729
+ '-p',
1730
+ `${appPort}:80`,
1731
+ ];
1732
+ if (envFile) {
1733
+ args.push('--env-file', envFile);
1734
+ }
1735
+ for (const [key, value] of Object.entries(initEnvVars)) {
1736
+ args.push('-e', `${key}=${value}`);
1737
+ }
1738
+ 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`);
1739
+ pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1740
+ pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1741
+ pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
1742
+ args.push(imageRef);
1743
+ return {
1744
+ source: 'docker',
1745
+ networkName: params.networkName,
1746
+ containerName,
1747
+ imageRef,
1748
+ appPort,
1749
+ storagePath,
1750
+ envFile,
1751
+ appKey,
1752
+ timeZone,
1753
+ args,
1754
+ };
1755
+ }
1756
+ async ensureDockerAppContainer(plan, options) {
1757
+ const exists = await this.dockerContainerExists(plan.containerName);
1758
+ if (exists) {
1759
+ printVerbose(`App container already exists: ${plan.containerName}`);
1760
+ return 'existing';
1761
+ }
1762
+ await mkdir(plan.storagePath, { recursive: true });
1763
+ await run('docker', plan.args, {
1764
+ errorName: 'docker run',
1765
+ stdio: options?.stdio ?? 'ignore',
1766
+ });
1767
+ return 'created';
1768
+ }
1769
+ async installDockerApp(params) {
1770
+ const networkName = params.builtinDbPlan?.networkName
1771
+ ?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1772
+ await this.ensureDockerNetwork(networkName);
1773
+ const plan = await Install.buildDockerAppPlan({
1774
+ envName: params.envName,
1775
+ workspaceName: params.workspaceName,
1776
+ dockerContainerPrefix: params.dockerContainerPrefix,
1777
+ appResults: params.appResults,
1778
+ downloadResults: params.downloadResults,
1779
+ dbResults: params.dbResults,
1780
+ rootResults: params.rootResults,
1781
+ networkName,
1782
+ });
1783
+ printVerbose('Starting NocoBase app (Docker)');
1784
+ await this.removeDockerContainerIfForced({
1785
+ containerName: plan.containerName,
1786
+ displayName: 'app container',
1787
+ force: params.force,
1788
+ });
1789
+ const containerState = await this.ensureDockerAppContainer(plan, {
1790
+ stdio: params.commandStdio ?? 'ignore',
1791
+ });
1792
+ if (containerState === 'existing') {
1793
+ const env = await this.inspectDockerContainerEnv(plan.containerName);
1794
+ plan.appKey = env.APP_KEY || plan.appKey;
1795
+ plan.timeZone = env.TZ || plan.timeZone;
1796
+ }
1797
+ printVerbose(`NocoBase app is starting at http://127.0.0.1:${plan.appPort}`);
1798
+ return plan;
1799
+ }
1800
+ static pushDownloadArgIfValue(argv, flag, value) {
1801
+ const text = String(value ?? '').trim();
1802
+ if (text) {
1803
+ argv.push(flag, text);
1804
+ }
1805
+ }
1806
+ static buildDownloadArgvFromResults(results, options) {
1807
+ const argv = ['-y', '--no-intro'];
1808
+ if (options?.compactLog) {
1809
+ argv.push('--compact-log');
1810
+ }
1811
+ const source = String(results.source ?? '').trim();
1812
+ if (options?.verbose) {
1813
+ argv.push('--verbose');
1814
+ }
1815
+ Install.pushDownloadArgIfValue(argv, '--source', results.source);
1816
+ Install.pushDownloadArgIfValue(argv, '--version', downloadResultsValue(results, 'version'));
1817
+ Install.pushDownloadArgIfValue(argv, '--output-dir', source === 'npm' || source === 'git'
1818
+ ? (resolveConfiguredEnvPath(results.outputDir)
1819
+ ?? resolveConfiguredEnvPath(String(results.outputDir ?? '').trim() || defaultInstallAppRootPath(results.env)))
1820
+ : results.outputDir);
1821
+ Install.pushDownloadArgIfValue(argv, '--git-url', results.gitUrl);
1822
+ Install.pushDownloadArgIfValue(argv, '--docker-registry', results.dockerRegistry);
1823
+ Install.pushDownloadArgIfValue(argv, '--docker-platform', results.dockerPlatform);
1824
+ Install.pushDownloadArgIfValue(argv, '--npm-registry', results.npmRegistry);
1825
+ if (Boolean(results.replace)) {
1826
+ argv.push('--replace');
1827
+ }
1828
+ if (Boolean(results.devDependencies)) {
1829
+ argv.push('--dev-dependencies');
1830
+ }
1831
+ if (Boolean(results.dockerSave)) {
1832
+ argv.push('--docker-save');
1833
+ }
1834
+ if (results.build !== undefined && !Boolean(results.build)) {
1835
+ argv.push('--no-build');
1836
+ }
1837
+ if (Boolean(results.buildDts)) {
1838
+ argv.push('--build-dts');
1839
+ }
1840
+ return argv;
1841
+ }
1842
+ static resolveLocalProjectRoot(params) {
1843
+ const projectRoot = params.downloadCommandResult?.projectRoot;
1844
+ if (projectRoot) {
1845
+ return projectRoot;
1846
+ }
1847
+ const outputDir = String(params.downloadResults.outputDir ?? '').trim()
1848
+ || String(params.appResults.appRootPath ?? '').trim()
1849
+ || defaultInstallAppRootPath(params.envName);
1850
+ return resolveConfiguredEnvPath(outputDir) ?? resolveEnvRelativePath(defaultInstallAppRootPath(params.envName));
1851
+ }
1852
+ static resolveLocalProjectConfigPath(params) {
1853
+ return (String(params.downloadResults.outputDir ?? '').trim()
1854
+ || String(params.appResults.appRootPath ?? '').trim()
1855
+ || defaultInstallAppRootPath(params.envName));
1856
+ }
1857
+ commandStdio(verbose) {
1858
+ return verbose ? 'inherit' : 'ignore';
1859
+ }
1860
+ async downloadManagedSource(params) {
1861
+ const argv = Install.buildDownloadArgvFromResults(params.downloadResults, {
1862
+ verbose: params.verbose,
1863
+ compactLog: true,
1864
+ });
1865
+ return await this.config.runCommand('source:download', argv);
1866
+ }
1867
+ async downloadLocalApp(params) {
1868
+ const result = await this.downloadManagedSource({
1869
+ downloadResults: params.downloadResults,
1870
+ verbose: params.verbose,
1871
+ });
1872
+ const downloadedProjectRoot = Install.resolveLocalProjectRoot({
1873
+ envName: params.envName,
1874
+ appResults: params.appResults,
1875
+ downloadResults: params.downloadResults,
1876
+ downloadCommandResult: result,
1877
+ });
1878
+ params.appResults.appRootPath = Install.resolveLocalProjectConfigPath({
1879
+ envName: params.envName,
1880
+ appResults: params.appResults,
1881
+ downloadResults: params.downloadResults,
1882
+ });
1883
+ return downloadedProjectRoot;
1884
+ }
1885
+ static buildLocalAppEnvVars(params) {
1886
+ const configuredStoragePath = String(params.appResults.storagePath ?? '').trim()
1887
+ || defaultInstallStoragePath(params.envName);
1888
+ const storagePath = resolveConfiguredEnvPath(configuredStoragePath)
1889
+ ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1890
+ const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim()
1891
+ || 'postgres';
1892
+ const appKey = crypto.randomBytes(32).toString('hex');
1893
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1894
+ const env = {
1895
+ STORAGE_PATH: storagePath,
1896
+ APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1897
+ || DEFAULT_INSTALL_APP_PORT,
1898
+ APP_KEY: appKey,
1899
+ TZ: timeZone,
1900
+ DB_DIALECT: dbDialect,
1901
+ DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim()
1902
+ || DEFAULT_INSTALL_DB_HOST,
1903
+ DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1904
+ || defaultDbPortForDialect(dbDialect),
1905
+ DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1906
+ || DEFAULT_INSTALL_DB_DATABASE,
1907
+ DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1908
+ || DEFAULT_INSTALL_DB_USER,
1909
+ DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1910
+ || DEFAULT_INSTALL_DB_PASSWORD,
1911
+ ...Install.buildInitAppEnvVars({
1912
+ appResults: params.appResults,
1913
+ rootResults: params.rootResults,
1914
+ }),
1915
+ };
1916
+ setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
1917
+ setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
1918
+ setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
1919
+ return env;
1920
+ }
1921
+ async startLocalApp(params) {
1922
+ const env = Install.buildLocalAppEnvVars({
1923
+ envName: params.envName,
1924
+ appResults: params.appResults,
1925
+ dbResults: params.dbResults,
1926
+ rootResults: params.rootResults,
1927
+ });
1928
+ const args = ['start', '--quickstart', '--daemon'];
1929
+ this.logDetail(`Stopping any existing local NocoBase process in ${params.projectRoot}`);
1930
+ try {
1931
+ await runNocoBaseCommand(['pm2', 'kill'], {
1932
+ cwd: params.projectRoot,
1933
+ env,
1934
+ stdio: params.commandStdio ?? 'ignore',
1935
+ });
1936
+ }
1937
+ catch (error) {
1938
+ const message = error instanceof Error ? error.message : String(error);
1939
+ this.logDetail(`Skipped local process cleanup before start: ${message}`);
1940
+ }
1941
+ this.logDetail(`Starting local NocoBase app from ${params.projectRoot}`);
1942
+ await runNocoBaseCommand(args, {
1943
+ cwd: params.projectRoot,
1944
+ env,
1945
+ stdio: params.commandStdio ?? 'ignore',
1946
+ });
1947
+ this.logDetail(`Local app is starting at http://127.0.0.1:${env.APP_PORT}`);
1948
+ return {
1949
+ source: params.source,
1950
+ projectRoot: params.projectRoot,
1951
+ appPort: env.APP_PORT,
1952
+ storagePath: env.STORAGE_PATH,
1953
+ appKey: env.APP_KEY,
1954
+ timeZone: env.TZ,
1955
+ env,
1956
+ args,
1957
+ };
1958
+ }
1959
+ static resolveApiBaseUrl(params) {
1960
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1961
+ || DEFAULT_INSTALL_APP_PORT;
1962
+ return (String(params.envAddResults.apiBaseUrl ?? '').trim()
1963
+ || `http://127.0.0.1:${appPort}/api`);
1964
+ }
1965
+ static buildHealthCheckUrl(apiBaseUrl) {
1966
+ return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
1967
+ }
1968
+ static async sleep(ms) {
1969
+ await new Promise((resolve) => setTimeout(resolve, ms));
1970
+ }
1971
+ static formatHealthCheckMessage(message, maxLength = 120) {
1972
+ const text = message.replace(/\s+/g, ' ').trim();
1973
+ if (!text) {
1974
+ return 'No response yet';
1975
+ }
1976
+ return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
1977
+ }
1978
+ static async requestAppHealthCheck(params) {
1979
+ const controller = new AbortController();
1980
+ const timeout = setTimeout(() => {
1981
+ controller.abort();
1982
+ }, params.requestTimeoutMs);
1983
+ try {
1984
+ const response = await params.fetchImpl(params.healthCheckUrl, {
1985
+ method: 'GET',
1986
+ signal: controller.signal,
1987
+ });
1988
+ const text = await response.text().catch(() => '');
1989
+ const body = Install.formatHealthCheckMessage(text);
1990
+ return {
1991
+ ok: response.ok && text.trim().toLowerCase() === 'ok',
1992
+ message: response.ok
1993
+ ? `HTTP ${response.status}: ${body}`
1994
+ : `HTTP ${response.status}: ${body}`,
1995
+ };
1996
+ }
1997
+ catch (error) {
1998
+ if (error instanceof Error && error.name === 'AbortError') {
1999
+ return {
2000
+ ok: false,
2001
+ message: `No response within ${Math.ceil(params.requestTimeoutMs / 1000)}s`,
2002
+ };
2003
+ }
2004
+ return {
2005
+ ok: false,
2006
+ message: error instanceof Error ? error.message : String(error),
2007
+ };
2008
+ }
2009
+ finally {
2010
+ clearTimeout(timeout);
2011
+ }
2012
+ }
2013
+ async waitForAppHealthCheck(apiBaseUrl, options) {
2014
+ const healthCheckUrl = Install.buildHealthCheckUrl(apiBaseUrl);
2015
+ const timeoutMs = options?.timeoutMs ?? APP_HEALTH_CHECK_TIMEOUT_MS;
2016
+ const intervalMs = options?.intervalMs ?? APP_HEALTH_CHECK_INTERVAL_MS;
2017
+ const requestTimeoutMs = options?.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS;
2018
+ const fetchImpl = options?.fetchImpl ?? fetch;
2019
+ const startedAt = Date.now();
2020
+ let lastMessage = 'No response yet';
2021
+ let lastLoggedStatus = '';
2022
+ printInfo('Waiting for NocoBase to become ready...');
2023
+ while (Date.now() - startedAt < timeoutMs) {
2024
+ const result = await Install.requestAppHealthCheck({
2025
+ healthCheckUrl,
2026
+ fetchImpl,
2027
+ requestTimeoutMs,
2028
+ });
2029
+ if (result.ok) {
2030
+ return;
2031
+ }
2032
+ lastMessage = result.message;
2033
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2034
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2035
+ if (statusLine !== lastLoggedStatus) {
2036
+ printInfo(statusLine);
2037
+ lastLoggedStatus = statusLine;
2038
+ }
2039
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
2040
+ if (remainingMs <= 0) {
2041
+ break;
2042
+ }
2043
+ await Install.sleep(Math.min(intervalMs, remainingMs));
2044
+ }
2045
+ const logHint = options?.containerName
2046
+ ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2047
+ : '';
2048
+ 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}`);
2049
+ }
2050
+ async saveInstalledEnv(params) {
2051
+ await upsertEnv(params.envName, Install.buildSavedEnvConfig(params), { scope: resolveDefaultConfigScope() });
2052
+ await setCurrentEnv(params.envName, { scope: resolveDefaultConfigScope() });
2053
+ }
2054
+ async syncInstalledEnvConnection(params) {
2055
+ if (!params.appReady) {
2056
+ return;
2057
+ }
2058
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim()
2059
+ || 'oauth';
2060
+ if (params.skipAuth) {
2061
+ printInfo(formatDeferredAuthMessage(params.envName, authType));
2062
+ return;
2063
+ }
2064
+ if (authType === 'oauth') {
2065
+ await this.config.runCommand('env:auth', [params.envName]);
2066
+ }
2067
+ await this.config.runCommand('env:update', [params.envName]);
2068
+ }
2069
+ static buildSavedEnvConfig(params) {
2070
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
2071
+ || DEFAULT_INSTALL_APP_PORT;
2072
+ const storagePath = String(params.appResults.storagePath ?? '').trim()
2073
+ || defaultInstallStoragePath(params.envName);
2074
+ const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
2075
+ const apiBaseUrl = Install.resolveApiBaseUrl({
2076
+ appResults: params.appResults,
2077
+ envAddResults: params.envAddResults,
2078
+ });
2079
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim()
2080
+ || 'oauth';
2081
+ return buildStoredEnvConfig({
2082
+ apiBaseUrl,
2083
+ authType,
2084
+ accessToken: params.envAddResults.accessToken,
2085
+ source: downloadResultsValue(params.downloadResults, 'source'),
2086
+ downloadVersion: downloadResultsValue(params.downloadResults, 'version'),
2087
+ dockerRegistry: downloadResultsValue(params.downloadResults, 'dockerRegistry'),
2088
+ dockerPlatform: downloadResultsValue(params.downloadResults, 'dockerPlatform'),
2089
+ gitUrl: downloadResultsValue(params.downloadResults, 'gitUrl'),
2090
+ npmRegistry: downloadResultsValue(params.downloadResults, 'npmRegistry'),
2091
+ devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2092
+ build: downloadResultsValue(params.downloadResults, 'build'),
2093
+ buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2094
+ appRootPath: params.appResults.appRootPath,
2095
+ appPort,
2096
+ storagePath,
2097
+ ...(envFile ? { envFile } : {}),
2098
+ appKey: params.appResults.appKey,
2099
+ timezone: params.appResults.timeZone,
2100
+ builtinDb: params.dbResults.builtinDb,
2101
+ dbDialect: params.dbResults.dbDialect,
2102
+ builtinDbImage: params.dbResults.builtinDbImage,
2103
+ dbHost: params.dbResults.dbHost,
2104
+ dbPort: params.dbResults.dbPort,
2105
+ dbDatabase: params.dbResults.dbDatabase,
2106
+ dbUser: params.dbResults.dbUser,
2107
+ dbPassword: params.dbResults.dbPassword,
2108
+ dbSchema: params.dbResults.dbSchema,
2109
+ dbTablePrefix: params.dbResults.dbTablePrefix,
2110
+ dbUnderscored: params.dbResults.dbUnderscored,
2111
+ rootUsername: params.rootResults.rootUsername,
2112
+ rootEmail: params.rootResults.rootEmail,
2113
+ rootPassword: params.rootResults.rootPassword,
2114
+ rootNickname: params.rootResults.rootNickname,
2115
+ });
2116
+ }
2117
+ async collectPromptResults(parsed, yes) {
2118
+ const resumePreset = await this.resolveResumePresetValues(parsed, yes);
2119
+ const envPreset = {
2120
+ ...(resumePreset?.envPreset ?? {}),
2121
+ ...Install.buildEnvPresetValuesFromFlags(parsed),
2122
+ };
2123
+ const envResults = await runPromptCatalog(Install.envPrompts, {
2124
+ initialValues: {
2125
+ env: DEFAULT_INSTALL_ENV_NAME,
2126
+ },
2127
+ values: envPreset,
2128
+ yes,
2129
+ });
2130
+ const envName = String(envResults.env ?? '').trim() || DEFAULT_INSTALL_ENV_NAME;
2131
+ const appPreset = {
2132
+ ...(resumePreset?.appPreset ?? {}),
2133
+ ...Install.buildAppPresetValuesFromFlags(parsed),
2134
+ };
2135
+ const appCatalog = Install.buildAppPromptsCatalog(envName, {
2136
+ resume: parsed.resume,
2137
+ });
2138
+ const appResults = await runPromptCatalog(appCatalog, {
2139
+ initialValues: await Install.buildAppPromptInitialValues({
2140
+ envName,
2141
+ flags: {
2142
+ ...parsed,
2143
+ 'app-root-path': parsed['app-root-path']
2144
+ ?? Install.toOptionalPromptString(appPreset.appRootPath),
2145
+ 'app-port': parsed['app-port']
2146
+ ?? Install.toOptionalPromptString(appPreset.appPort),
2147
+ 'storage-path': parsed['storage-path']
2148
+ ?? Install.toOptionalPromptString(appPreset.storagePath),
2149
+ },
2150
+ }),
2151
+ values: appPreset,
2152
+ yesInitialValues: { resume: parsed.resume },
2153
+ yes,
2154
+ });
2155
+ let downloadResults = {};
2156
+ if (Boolean(appResults.fetchSource)) {
2157
+ const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2158
+ downloadOpts.values = {
2159
+ ...(resumePreset?.downloadPreset ?? {}),
2160
+ ...downloadOpts.values,
2161
+ ...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes),
2162
+ };
2163
+ downloadOpts.yes = yes;
2164
+ downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2165
+ }
2166
+ const dbPreset = {
2167
+ ...(resumePreset?.dbPreset ?? {}),
2168
+ ...Install.buildDbPresetValuesFromFlags(parsed),
2169
+ };
2170
+ const promptedDbResults = await runPromptCatalog(Install.buildDbPromptsCatalog(envName, downloadResults, {
2171
+ resume: parsed.resume,
2172
+ }), {
2173
+ initialValues: {
2174
+ ...downloadResults,
2175
+ ...await Install.buildDbPromptInitialValues({
2176
+ flags: {
2177
+ ...parsed,
2178
+ 'db-port': parsed['db-port']
2179
+ ?? Install.toOptionalPromptString(dbPreset.dbPort),
2180
+ },
2181
+ downloadResults,
2182
+ dbPreset,
2183
+ }),
2184
+ },
2185
+ values: dbPreset,
2186
+ yes,
2187
+ });
2188
+ const dbResults = {
2189
+ ...promptedDbResults,
2190
+ ...pickPresetKeys(dbPreset, ['dbSchema', 'dbTablePrefix', 'dbUnderscored']),
2191
+ };
2192
+ const rootPreset = Install.buildRootPresetValuesFromFlags(parsed);
2193
+ const rootResults = await runPromptCatalog(Install.rootUserPrompts, {
2194
+ initialValues: {},
2195
+ values: {
2196
+ ...(resumePreset?.rootPreset ?? {}),
2197
+ ...rootPreset,
2198
+ },
2199
+ yes,
2200
+ });
2201
+ const envAddPromptsForInstall = this.buildEnvAddPromptsForInstall(parsed);
2202
+ const envAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2203
+ initialValues: {
2204
+ apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2205
+ },
2206
+ values: {
2207
+ name: envName,
2208
+ ...(parsed['skip-auth'] ? { skipAuth: true } : {}),
2209
+ ...(resumePreset?.envAddPreset ?? {}),
2210
+ ...Install.buildEnvAddPresetValuesFromFlags(parsed),
2211
+ },
2212
+ yes,
2213
+ });
2214
+ return {
2215
+ envName,
2216
+ envResults,
2217
+ appResults,
2218
+ downloadResults,
2219
+ dbResults,
2220
+ rootResults,
2221
+ envAddResults,
2222
+ };
2223
+ }
2224
+ async run() {
2225
+ const parsedResult = await this.parse(Install);
2226
+ applyCliLocale(parsedResult.flags.locale);
2227
+ const flags = parsedResult.flags;
2228
+ const parsed = {
2229
+ ...flags,
2230
+ };
2231
+ if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2232
+ this.error('--skip-auth cannot be used with --access-token or --token.');
2233
+ }
2234
+ setVerboseMode(Boolean(parsed.verbose));
2235
+ const commandStdio = this.commandStdio(parsed.verbose);
2236
+ if (!parsed['no-intro']) {
2237
+ this.logStage('Set up NocoBase');
2238
+ }
2239
+ if (parsed.resume) {
2240
+ const envLabel = Install.toOptionalPromptString(parsed.env);
2241
+ printInfo(envLabel
2242
+ ? `Resuming setup for env "${envLabel}" from the saved workspace config`
2243
+ : 'Resuming setup from the saved workspace config');
2244
+ }
2245
+ const promptResults = await this.collectPromptResults(parsed, flags.yes);
2246
+ const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults, } = promptResults;
2247
+ const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2248
+ const usesDockerResources = Boolean(dbResults.builtinDb)
2249
+ || (Boolean(appResults.fetchSource) && source === 'docker');
2250
+ const dockerNetworkName = usesDockerResources
2251
+ ? await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() })
2252
+ : undefined;
2253
+ const dockerContainerPrefix = usesDockerResources
2254
+ ? await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() })
2255
+ : undefined;
2256
+ await Install.ensureExternalDbReadyForInstall(dbResults);
2257
+ if (!parsed.resume) {
2258
+ if (!parsed['skip-save-env-log']) {
2259
+ this.logStage('Saving env config');
2260
+ }
2261
+ await this.saveInstalledEnv({
2262
+ envName,
2263
+ appResults,
2264
+ downloadResults,
2265
+ dbResults,
2266
+ rootResults,
2267
+ envAddResults,
2268
+ });
2269
+ if (!parsed['skip-save-env-log']) {
2270
+ printInfo(`Saved env config for "${envName}".`);
2271
+ }
2272
+ }
2273
+ let builtinDbPlan;
2274
+ if (Boolean(dbResults.builtinDb)) {
2275
+ builtinDbPlan = await this.startBuiltinDb({
2276
+ envName,
2277
+ dockerNetworkName,
2278
+ dockerContainerPrefix,
2279
+ appResults,
2280
+ downloadResults,
2281
+ dbResults,
2282
+ force: parsed.force,
2283
+ commandStdio,
2284
+ });
2285
+ dbResults.dbHost = builtinDbPlan.dbHost;
2286
+ dbResults.dbPort = builtinDbPlan.dbPort;
2287
+ dbResults.dbDialect = builtinDbPlan.dbDialect;
2288
+ dbResults.dbDatabase = builtinDbPlan.dbDatabase;
2289
+ dbResults.dbUser = builtinDbPlan.dbUser;
2290
+ dbResults.dbPassword = builtinDbPlan.dbPassword;
2291
+ }
2292
+ let dockerAppPlan;
2293
+ let localAppPlan;
2294
+ if (Boolean(appResults.fetchSource)) {
2295
+ this.logStage('Preparing application');
2296
+ if (source === 'docker') {
2297
+ await this.downloadManagedSource({
2298
+ downloadResults,
2299
+ verbose: parsed.verbose,
2300
+ });
2301
+ printInfo('Application image ready.');
2302
+ dockerAppPlan = await this.installDockerApp({
2303
+ envName,
2304
+ dockerNetworkName,
2305
+ dockerContainerPrefix,
2306
+ appResults,
2307
+ downloadResults,
2308
+ dbResults,
2309
+ rootResults,
2310
+ builtinDbPlan,
2311
+ force: parsed.force,
2312
+ commandStdio,
2313
+ });
2314
+ appResults.appKey = dockerAppPlan.appKey;
2315
+ appResults.timeZone = dockerAppPlan.timeZone;
2316
+ }
2317
+ else if (source === 'npm' || source === 'git') {
2318
+ const localSource = source === 'npm' ? 'npm' : 'git';
2319
+ const projectRoot = await this.downloadLocalApp({
2320
+ envName,
2321
+ appResults,
2322
+ downloadResults,
2323
+ verbose: parsed.verbose,
2324
+ });
2325
+ printInfo('Application files ready.');
2326
+ localAppPlan = await this.startLocalApp({
2327
+ envName,
2328
+ source: localSource,
2329
+ projectRoot,
2330
+ appResults,
2331
+ dbResults,
2332
+ rootResults,
2333
+ commandStdio,
2334
+ });
2335
+ appResults.appKey = localAppPlan.appKey;
2336
+ appResults.timeZone = localAppPlan.timeZone;
2337
+ }
2338
+ }
2339
+ else {
2340
+ this.logDetail('Skipped app download and install.');
2341
+ }
2342
+ if (dockerAppPlan || localAppPlan) {
2343
+ this.logStage('Starting NocoBase');
2344
+ await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2345
+ appResults,
2346
+ envAddResults,
2347
+ }), {
2348
+ containerName: dockerAppPlan?.containerName,
2349
+ });
2350
+ printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2351
+ }
2352
+ if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2353
+ await this.saveInstalledEnv({
2354
+ envName,
2355
+ appResults,
2356
+ downloadResults,
2357
+ dbResults,
2358
+ rootResults,
2359
+ envAddResults,
2360
+ });
2361
+ }
2362
+ await this.syncInstalledEnvConnection({
2363
+ envName,
2364
+ envAddResults,
2365
+ appReady: Boolean(dockerAppPlan || localAppPlan),
2366
+ skipAuth: Boolean(parsed['skip-auth']),
2367
+ });
2368
+ if (!dockerAppPlan && !localAppPlan) {
2369
+ printInfo(`Install config for "${envName}" has been saved.`);
2370
+ }
2371
+ }
2372
+ }
2373
+ function downloadResultsValue(downloadResults, key) {
2374
+ if (key === 'version' && String(downloadResults.version ?? '').trim() === 'other') {
2375
+ return downloadResults.otherVersion;
2376
+ }
2377
+ return downloadResults[key];
2378
+ }