@nocobase/cli 2.1.0-alpha.46 → 2.1.0-alpha.47

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 (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -3
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +89 -24
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +142 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +9 -5
  27. package/dist/commands/config/delete.js +6 -1
  28. package/dist/commands/config/get.js +6 -1
  29. package/dist/commands/config/set.js +7 -2
  30. package/dist/commands/env/add.js +24 -44
  31. package/dist/commands/env/info.js +3 -2
  32. package/dist/commands/env/proxy/caddy.js +28 -0
  33. package/dist/commands/env/proxy/index.js +353 -0
  34. package/dist/commands/env/proxy/nginx.js +28 -0
  35. package/dist/commands/env/remove.js +112 -22
  36. package/dist/commands/env/shared.js +17 -9
  37. package/dist/commands/env/update.js +385 -21
  38. package/dist/commands/init.js +350 -98
  39. package/dist/commands/install.js +218 -75
  40. package/dist/commands/license/activate.js +63 -244
  41. package/dist/commands/license/plugins/shared.js +64 -13
  42. package/dist/commands/plugin/import.js +108 -0
  43. package/dist/commands/revision/create.js +89 -0
  44. package/dist/commands/source/test.js +13 -14
  45. package/dist/lib/app-health.js +5 -5
  46. package/dist/lib/app-managed-resources.js +24 -8
  47. package/dist/lib/app-public-path.js +80 -0
  48. package/dist/lib/app-runtime.js +10 -1
  49. package/dist/lib/auth-store.js +119 -4
  50. package/dist/lib/cli-config.js +191 -3
  51. package/dist/lib/cli-entry-error.js +8 -0
  52. package/dist/lib/command-log.js +284 -0
  53. package/dist/lib/docker-env-file.js +10 -2
  54. package/dist/lib/env-auth.js +4 -1
  55. package/dist/lib/env-command-config.js +45 -0
  56. package/dist/lib/env-config.js +7 -2
  57. package/dist/lib/env-guard.js +9 -10
  58. package/dist/lib/env-paths.js +101 -0
  59. package/dist/lib/env-proxy.js +1295 -0
  60. package/dist/lib/inquirer.js +4 -5
  61. package/dist/lib/managed-env-file.js +98 -0
  62. package/dist/lib/plugin-import.js +279 -0
  63. package/dist/lib/prompt-catalog-core.js +3 -2
  64. package/dist/lib/prompt-validators.js +14 -0
  65. package/dist/lib/prompt-web-ui.js +8 -1
  66. package/dist/lib/run-npm.js +56 -12
  67. package/dist/lib/skills-manager.js +3 -1
  68. package/dist/lib/source-publish.js +18 -1
  69. package/dist/lib/ui.js +2 -1
  70. package/dist/locale/en-US.json +116 -22
  71. package/dist/locale/zh-CN.json +113 -19
  72. package/package.json +5 -7
  73. package/scripts/build.mjs +34 -0
  74. package/scripts/clean.mjs +9 -0
  75. package/tsconfig.json +19 -0
  76. package/LICENSE.txt +0 -107
  77. package/README.zh-CN.md +0 -355
@@ -16,6 +16,8 @@ import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
16
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
17
  import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
18
  import { resolveDefaultConfigScope } from '../lib/cli-home.js';
19
+ import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
20
+ import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
19
21
  import { runPromptCatalogWebUI } from "../lib/prompt-web-ui.js";
20
22
  import { validateApiBaseUrl, validateEnvKey } from "../lib/prompt-validators.js";
21
23
  import { installNocoBaseSkills, isNpmRegistryUnavailable } from '../lib/skills-manager.js';
@@ -27,6 +29,7 @@ import Install, { defaultDbPortForDialect } from "./install.js";
27
29
  const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
28
30
  const DEFAULT_INIT_APP_NAME = 'local';
29
31
  const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
32
+ const INIT_SETUP_MODES = ['install-new', 'manage-local', 'connect-remote'];
30
33
  const INIT_ENV_ADD_FLAG_NAMES = [
31
34
  'locale',
32
35
  'default-api-base-url',
@@ -48,14 +51,45 @@ function withExtraHidden(def, extraHidden) {
48
51
  hidden: (values) => extraHidden(values) || (def.hidden?.(values) ?? false),
49
52
  };
50
53
  }
51
- function existingAppOnly(def) {
52
- return withExtraHidden(def, (values) => values.hasNocobase !== 'yes');
54
+ function normalizeInitSetupMode(value) {
55
+ const mode = String(value ?? '').trim();
56
+ if (mode === 'manage-local' || mode === 'connect-remote' || mode === 'install-new') {
57
+ return mode;
58
+ }
59
+ if (mode === 'yes') {
60
+ return 'connect-remote';
61
+ }
62
+ if (mode === 'no') {
63
+ return 'install-new';
64
+ }
65
+ return 'install-new';
66
+ }
67
+ function resolveInitSetupMode(values) {
68
+ return normalizeInitSetupMode(values.setupMode ?? values.hasNocobase);
69
+ }
70
+ function isRemoteSetupMode(values) {
71
+ return resolveInitSetupMode(values) === 'connect-remote';
72
+ }
73
+ function isInstallNewSetupMode(values) {
74
+ return resolveInitSetupMode(values) === 'install-new';
53
75
  }
54
- function newInstallOnly(def) {
55
- return withExtraHidden(def, (values) => values.hasNocobase !== 'no');
76
+ function isInstallLikeSetupMode(values) {
77
+ return !isRemoteSetupMode(values);
56
78
  }
57
- function newInstallDownloadExecutionOnly(def) {
58
- return withExtraHidden(def, (values) => values.hasNocobase !== 'no' || values.skipDownload === true);
79
+ function remoteConnectionOnly(def) {
80
+ return withExtraHidden(def, (values) => !isRemoteSetupMode(values));
81
+ }
82
+ function installLikeOnly(def) {
83
+ return withExtraHidden(def, (values) => !isInstallLikeSetupMode(values));
84
+ }
85
+ function installNewOnly(def) {
86
+ return withExtraHidden(def, (values) => !isInstallNewSetupMode(values));
87
+ }
88
+ function installConnectionOnly(def) {
89
+ return withExtraHidden(def, (values) => !isInstallNewSetupMode(values));
90
+ }
91
+ function installLikeDownloadExecutionOnly(def) {
92
+ return withExtraHidden(def, (values) => !isInstallLikeSetupMode(values) || values.skipDownload === true);
59
93
  }
60
94
  function argvHasToken(argv, tokens) {
61
95
  return tokens.some((token) => argv.includes(token));
@@ -85,6 +119,26 @@ function explicitApiBaseUrlFlag(flags) {
85
119
  function explicitDbHostFlag(flags) {
86
120
  return String(flags['db-host'] ?? '').trim();
87
121
  }
122
+ function explicitSetupModeFlag(flags) {
123
+ const mode = String(flags['setup-mode'] ?? '').trim();
124
+ return mode ? normalizeInitSetupMode(mode) : undefined;
125
+ }
126
+ function applyLegacyHasNocobaseAlias(values) {
127
+ if (!Object.prototype.hasOwnProperty.call(values, 'setupMode') &&
128
+ !Object.prototype.hasOwnProperty.call(values, 'hasNocobase')) {
129
+ return;
130
+ }
131
+ const setupMode = resolveInitSetupMode(values);
132
+ if (setupMode === 'connect-remote') {
133
+ values.hasNocobase = 'yes';
134
+ return;
135
+ }
136
+ if (setupMode === 'install-new') {
137
+ values.hasNocobase = 'no';
138
+ return;
139
+ }
140
+ delete values.hasNocobase;
141
+ }
88
142
  function optionalInitString(value) {
89
143
  const text = String(value ?? '').trim();
90
144
  return text || undefined;
@@ -95,6 +149,39 @@ function resolveManagedAppKey(value) {
95
149
  function resolveManagedTimeZone(value) {
96
150
  return optionalInitString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
97
151
  }
152
+ function normalizeConnectionString(value) {
153
+ return String(value ?? '').trim();
154
+ }
155
+ function deriveInstallConnectionApiBaseUrl(values, defaultApiHost = '127.0.0.1') {
156
+ const appPort = normalizeConnectionString(values.appPort);
157
+ return appPort ? `http://${defaultApiHost}:${appPort}/api` : DEFAULT_INIT_API_BASE_URL;
158
+ }
159
+ function createInstallConnectionApiBaseUrlPrompt(defaultApiHost) {
160
+ return installConnectionOnly({
161
+ type: 'text',
162
+ message: initText('prompts.apiBaseUrl.message'),
163
+ placeholder: initText('prompts.apiBaseUrl.placeholder'),
164
+ required: true,
165
+ initialValue: (values) => deriveInstallConnectionApiBaseUrl(values, defaultApiHost),
166
+ });
167
+ }
168
+ const installConnectionAuthTypePrompt = installConnectionOnly({
169
+ ...EnvAdd.prompts.authType,
170
+ });
171
+ const installConnectionUsernamePrompt = installConnectionOnly({
172
+ ...Install.rootUserPrompts.rootUsername,
173
+ hidden: () => true,
174
+ initialValue: (values) => normalizeConnectionString(values.rootUsername),
175
+ });
176
+ const installConnectionPasswordPrompt = installConnectionOnly({
177
+ ...Install.rootUserPrompts.rootPassword,
178
+ hidden: () => true,
179
+ initialValue: (values) => String(values.rootPassword ?? ''),
180
+ });
181
+ const installConnectionAccessTokenPrompt = installConnectionOnly({
182
+ ...EnvAdd.prompts.accessToken,
183
+ hidden: (values) => values.installAuthType !== 'token' || values.skipAuth === true,
184
+ });
98
185
  function shouldAllowExistingInitEnv() {
99
186
  return argvHasToken(process.argv.slice(2), ['--force', '-f']);
100
187
  }
@@ -161,10 +248,11 @@ export default class Init extends Command {
161
248
  static summary = 'Set up NocoBase so coding agents can connect and work with it';
162
249
  static description = `Set up NocoBase for coding agents in the current workspace.
163
250
 
164
- \`nb init\` prepares a NocoBase environment that coding agents can use. It supports two setup paths:
251
+ \`nb init\` prepares a NocoBase environment that coding agents can use. It supports three setup paths:
165
252
 
166
- - Connect an existing NocoBase app and save it as a CLI env.
167
253
  - Install a new NocoBase app, then save it as a CLI env.
254
+ - Take over managing an app that already exists on this machine by reusing its database.
255
+ - Connect a remote NocoBase app and save it as a CLI env.
168
256
 
169
257
  It can also install NocoBase AI coding skills (\`nocobase/skills\`) so agents get the project-specific workflow guidance.
170
258
 
@@ -173,7 +261,7 @@ If setup was interrupted earlier, use \`--resume\` with an existing env name to
173
261
  Prompt modes:
174
262
  - Default: guided prompts in the terminal.
175
263
  - \`--ui\`: open the same setup flow in a local browser form.
176
- - \`-y\`, \`--yes\`: skip prompts. In this mode \`--env <envName>\` is required, and init creates a new local NocoBase app with safe defaults.
264
+ - \`-y\`, \`--yes\`: skip prompts. In this mode \`--env <envName>\` is required, and init uses flags plus safe defaults for the selected setup mode.
177
265
 
178
266
  \`--ui\` cannot be combined with \`--yes\`.`;
179
267
  static examples = [
@@ -184,8 +272,10 @@ Prompt modes:
184
272
  '<%= config.bin %> <%= command.id %> --env app1 --yes',
185
273
  '<%= config.bin %> <%= command.id %> --env app1 --resume',
186
274
  '<%= config.bin %> <%= command.id %> --env app1 --yes --source docker --version alpha',
275
+ '<%= config.bin %> <%= command.id %> --env app1 --yes --setup-mode manage-local --source npm --version beta',
187
276
  '<%= config.bin %> <%= command.id %> --env app1 --yes --source npm --version alpha --app-port 13080',
188
277
  '<%= config.bin %> <%= command.id %> --env app1 --yes --source git --version fix/cli-v2',
278
+ '<%= config.bin %> <%= command.id %> --env staging --yes --setup-mode connect-remote --api-base-url https://demo.example.com/api',
189
279
  '<%= config.bin %> <%= command.id %> --ui --ui-port 3000',
190
280
  ];
191
281
  static prompts = {
@@ -216,53 +306,61 @@ Prompt modes:
216
306
  required: true,
217
307
  validate: validateInitAppName,
218
308
  },
219
- hasNocobase: {
309
+ setupMode: {
220
310
  type: 'select',
221
311
  variant: 'radio',
222
- message: initText('prompts.hasNocobase.message'),
312
+ message: initText('prompts.setupMode.message'),
223
313
  options: [
224
314
  {
225
- value: 'no',
226
- label: initText('prompts.hasNocobase.noLabel'),
315
+ value: 'install-new',
316
+ label: initText('prompts.setupMode.installNewLabel'),
317
+ hint: initText('prompts.setupMode.installNewHint'),
227
318
  },
228
319
  {
229
- value: 'yes',
230
- label: initText('prompts.hasNocobase.yesLabel'),
320
+ value: 'connect-remote',
321
+ label: initText('prompts.setupMode.connectRemoteLabel'),
322
+ hint: initText('prompts.setupMode.connectRemoteHint'),
323
+ },
324
+ {
325
+ value: 'manage-local',
326
+ label: initText('prompts.setupMode.manageLocalLabel'),
327
+ hint: initText('prompts.setupMode.manageLocalHint'),
328
+ disabled: true,
231
329
  },
232
330
  ],
233
- initialValue: 'no',
234
- yesInitialValue: 'no',
331
+ initialValue: 'install-new',
332
+ yesInitialValue: 'install-new',
235
333
  required: true,
236
334
  },
237
- apiBaseUrl: existingAppOnly({
335
+ apiBaseUrl: remoteConnectionOnly({
238
336
  type: 'text',
239
337
  message: initText('prompts.apiBaseUrl.message'),
240
338
  placeholder: initText('prompts.apiBaseUrl.placeholder'),
241
339
  required: true,
242
340
  validate: validateApiBaseUrl,
243
341
  }),
244
- authType: existingAppOnly(EnvAdd.prompts.authType),
245
- username: existingAppOnly(EnvAdd.prompts.username),
246
- password: existingAppOnly(EnvAdd.prompts.password),
247
- accessToken: existingAppOnly(EnvAdd.prompts.accessToken),
248
- lang: newInstallOnly(Install.appPrompts.lang),
249
- appRootPath: newInstallOnly(Install.appPrompts.appRootPath),
250
- appPort: newInstallOnly(Install.appPrompts.appPort),
251
- storagePath: newInstallOnly(Install.appPrompts.storagePath),
252
- skipDownload: newInstallOnly({
342
+ authType: remoteConnectionOnly(EnvAdd.prompts.authType),
343
+ username: remoteConnectionOnly(EnvAdd.prompts.username),
344
+ password: remoteConnectionOnly(EnvAdd.prompts.password),
345
+ accessToken: remoteConnectionOnly(EnvAdd.prompts.accessToken),
346
+ lang: installLikeOnly(Install.appPrompts.lang),
347
+ appPath: installLikeOnly(Install.appPrompts.appPath),
348
+ appPort: installLikeOnly(Install.appPrompts.appPort),
349
+ appPublicPath: installLikeOnly(Install.appPrompts.appPublicPath),
350
+ skipDownload: installNewOnly({
253
351
  type: 'boolean',
254
352
  message: initText('prompts.skipDownload.message'),
255
353
  initialValue: false,
256
354
  yesInitialValue: false,
257
355
  }),
258
- source: newInstallOnly(Download.prompts.source),
259
- version: newInstallOnly(Download.prompts.version),
260
- otherVersion: newInstallOnly(Download.prompts.otherVersion),
261
- dockerRegistry: newInstallOnly(Download.prompts.dockerRegistry),
262
- dockerPlatform: newInstallOnly(Download.prompts.dockerPlatform),
263
- dockerSave: newInstallDownloadExecutionOnly(Download.prompts.dockerSave),
264
- gitUrl: newInstallOnly(Download.prompts.gitUrl),
265
- outputDir: newInstallDownloadExecutionOnly({
356
+ source: installLikeOnly(Download.prompts.source),
357
+ version: installLikeOnly(Download.prompts.version),
358
+ otherVersion: installLikeOnly(Download.prompts.otherVersion),
359
+ dockerRegistry: installLikeOnly(Download.prompts.dockerRegistry),
360
+ dockerPlatform: installLikeOnly(Download.prompts.dockerPlatform),
361
+ dockerSave: installLikeDownloadExecutionOnly(Download.prompts.dockerSave),
362
+ gitUrl: installLikeOnly(Download.prompts.gitUrl),
363
+ outputDir: installLikeDownloadExecutionOnly({
266
364
  ...DOWNLOAD_OUTPUT_DIR_PROMPT,
267
365
  hidden: (values) => {
268
366
  const source = String(values.source ?? '').trim();
@@ -274,7 +372,8 @@ Prompt modes:
274
372
  initialValue: (values) => {
275
373
  const source = String(values.source ?? '').trim();
276
374
  if (source === 'npm' || source === 'git') {
277
- const appRootPath = String(values.appRootPath ?? '').trim();
375
+ const appPath = String(values.appPath ?? inferConfiguredAppPathFromLegacyConfig(values) ?? '').trim();
376
+ const appRootPath = String(values.appRootPath ?? '').trim() || (appPath ? deriveConfiguredSourcePath(appPath) : '');
278
377
  if (appRootPath) {
279
378
  return appRootPath;
280
379
  }
@@ -283,30 +382,35 @@ Prompt modes:
283
382
  return typeof initialValue === 'function' ? initialValue(values) : String(initialValue ?? '');
284
383
  },
285
384
  }),
286
- npmRegistry: newInstallOnly(Download.prompts.npmRegistry),
287
- replace: newInstallDownloadExecutionOnly(Download.prompts.replace),
288
- devDependencies: newInstallDownloadExecutionOnly(Download.prompts.devDependencies),
289
- build: newInstallDownloadExecutionOnly(Download.prompts.build),
290
- buildDts: newInstallDownloadExecutionOnly(Download.prompts.buildDts),
291
- dbDialect: newInstallOnly(Install.dbPrompts.dbDialect),
292
- builtinDb: newInstallOnly(Install.dbPrompts.builtinDb),
293
- builtinDbImage: newInstallOnly(Install.dbPrompts.builtinDbImage),
294
- dbHost: newInstallOnly(Install.dbPrompts.dbHost),
295
- dbPort: newInstallOnly(Install.dbPrompts.dbPort),
296
- dbDatabase: newInstallOnly(Install.dbPrompts.dbDatabase),
297
- dbUser: newInstallOnly(Install.dbPrompts.dbUser),
298
- dbPassword: newInstallOnly(Install.dbPrompts.dbPassword),
299
- dbSchema: newInstallOnly(Install.dbPrompts.dbSchema),
300
- dbTablePrefix: newInstallOnly(Install.dbPrompts.dbTablePrefix),
301
- dbUnderscored: newInstallOnly(Install.dbPrompts.dbUnderscored),
302
- rootUsername: newInstallOnly(Install.rootUserPrompts.rootUsername),
303
- rootEmail: newInstallOnly(Install.rootUserPrompts.rootEmail),
304
- rootPassword: newInstallOnly(Install.rootUserPrompts.rootPassword),
305
- rootNickname: newInstallOnly(Install.rootUserPrompts.rootNickname),
385
+ npmRegistry: installLikeOnly(Download.prompts.npmRegistry),
386
+ replace: installLikeDownloadExecutionOnly(Download.prompts.replace),
387
+ devDependencies: installLikeDownloadExecutionOnly(Download.prompts.devDependencies),
388
+ build: installLikeDownloadExecutionOnly(Download.prompts.build),
389
+ buildDts: installLikeDownloadExecutionOnly(Download.prompts.buildDts),
390
+ dbDialect: installLikeOnly(Install.dbPrompts.dbDialect),
391
+ builtinDb: installLikeOnly(Install.dbPrompts.builtinDb),
392
+ builtinDbImage: installLikeOnly(Install.dbPrompts.builtinDbImage),
393
+ dbHost: installLikeOnly(Install.dbPrompts.dbHost),
394
+ dbPort: installLikeOnly(Install.dbPrompts.dbPort),
395
+ dbDatabase: installLikeOnly(Install.dbPrompts.dbDatabase),
396
+ dbUser: installLikeOnly(Install.dbPrompts.dbUser),
397
+ dbPassword: installLikeOnly(Install.dbPrompts.dbPassword),
398
+ dbSchema: installLikeOnly(Install.dbPrompts.dbSchema),
399
+ dbTablePrefix: installLikeOnly(Install.dbPrompts.dbTablePrefix),
400
+ dbUnderscored: installLikeOnly(Install.dbPrompts.dbUnderscored),
401
+ rootUsername: installNewOnly(Install.rootUserPrompts.rootUsername),
402
+ rootEmail: installNewOnly(Install.rootUserPrompts.rootEmail),
403
+ rootPassword: installNewOnly(Install.rootUserPrompts.rootPassword),
404
+ rootNickname: installNewOnly(Install.rootUserPrompts.rootNickname),
405
+ installAuthType: installConnectionAuthTypePrompt,
406
+ installUsername: installConnectionUsernamePrompt,
407
+ installPassword: installConnectionPasswordPrompt,
408
+ installAccessToken: installConnectionAccessTokenPrompt,
306
409
  };
307
- buildPromptCatalog(flags) {
410
+ buildPromptCatalog(flags, options) {
308
411
  const prompts = {
309
412
  ...Init.prompts,
413
+ installApiBaseUrl: createInstallConnectionApiBaseUrlPrompt(options.defaultApiHost),
310
414
  };
311
415
  if (flags['skip-auth']) {
312
416
  const accessTokenPrompt = {
@@ -321,9 +425,24 @@ Prompt modes:
321
425
  ...EnvAdd.prompts.password,
322
426
  hidden: () => true,
323
427
  };
324
- prompts.username = existingAppOnly(usernamePrompt);
325
- prompts.password = existingAppOnly(passwordPrompt);
326
- prompts.accessToken = existingAppOnly(accessTokenPrompt);
428
+ const installAccessTokenPrompt = {
429
+ ...installConnectionAccessTokenPrompt,
430
+ hidden: () => true,
431
+ };
432
+ const installUsernamePrompt = {
433
+ ...installConnectionUsernamePrompt,
434
+ hidden: () => true,
435
+ };
436
+ const installPasswordPrompt = {
437
+ ...installConnectionPasswordPrompt,
438
+ hidden: () => true,
439
+ };
440
+ prompts.username = remoteConnectionOnly(usernamePrompt);
441
+ prompts.password = remoteConnectionOnly(passwordPrompt);
442
+ prompts.accessToken = remoteConnectionOnly(accessTokenPrompt);
443
+ prompts.installUsername = installUsernamePrompt;
444
+ prompts.installPassword = installPasswordPrompt;
445
+ prompts.installAccessToken = installAccessTokenPrompt;
327
446
  }
328
447
  return prompts;
329
448
  }
@@ -331,13 +450,17 @@ Prompt modes:
331
450
  static flags = {
332
451
  yes: Flags.boolean({
333
452
  char: 'y',
334
- description: 'Skip prompts and create a new local NocoBase app. Requires an env name.',
453
+ description: 'Skip prompts and use flags plus defaults for the selected setup mode. Requires an env name.',
335
454
  default: false,
336
455
  }),
337
456
  env: Flags.string({
338
457
  char: 'e',
339
458
  description: 'Env name for this setup. Required with --yes and --resume',
340
459
  }),
460
+ 'setup-mode': Flags.string({
461
+ description: 'Setup mode: install a new app, manage a local app by reusing its database, or connect a remote app',
462
+ options: [...INIT_SETUP_MODES],
463
+ }),
341
464
  ui: Flags.boolean({
342
465
  description: 'Open the guided setup flow in a local browser form (not valid with --yes)',
343
466
  default: false,
@@ -442,7 +565,9 @@ Prompt modes:
442
565
  }
443
566
  }
444
567
  const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
445
- const promptCatalog = this.buildPromptCatalog(normalizedFlags);
568
+ const defaultUiHost = await resolveDefaultUiHost();
569
+ const defaultApiHost = await resolveDefaultApiHost();
570
+ const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost });
446
571
  if (useBrowserUi) {
447
572
  presetValues = await runPromptCatalogWebUI({
448
573
  stages: Init.buildWebUiStages(promptCatalog),
@@ -450,7 +575,7 @@ Prompt modes:
450
575
  ...dynamicInitialValues,
451
576
  ...presetValues,
452
577
  },
453
- host: normalizedFlags['ui-host']?.trim() || '127.0.0.1',
578
+ host: normalizedFlags['ui-host']?.trim() || defaultUiHost,
454
579
  port: normalizedFlags['ui-port'] ?? 0,
455
580
  pageTitle: initText('webUi.pageTitle'),
456
581
  documentHeading: initText('webUi.documentHeading'),
@@ -465,7 +590,12 @@ Prompt modes:
465
590
  });
466
591
  }
467
592
  const results = await runPromptCatalog(promptCatalog, {
468
- initialValues: dynamicInitialValues,
593
+ initialValues: {
594
+ ...dynamicInitialValues,
595
+ ...(presetValues.setupMode === undefined && presetValues.hasNocobase !== undefined
596
+ ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
597
+ : {}),
598
+ },
469
599
  values: presetValues,
470
600
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
471
601
  hooks: {
@@ -480,21 +610,28 @@ Prompt modes:
480
610
  },
481
611
  command: this,
482
612
  });
483
- const normalizedResults = {
613
+ const normalizedResults = this.normalizeInitResults({
484
614
  ...pickKeys(presetValues, [
615
+ 'defaultApiHost',
485
616
  'authType',
486
617
  'accessToken',
487
618
  'dbSchema',
488
619
  'dbTablePrefix',
489
620
  'dbUnderscored',
621
+ 'installApiBaseUrl',
622
+ 'installAuthType',
623
+ 'installAccessToken',
624
+ 'installUsername',
625
+ 'installPassword',
490
626
  'skipAuth',
491
627
  'username',
492
628
  'password',
493
629
  ]),
630
+ defaultApiHost,
494
631
  ...results,
495
- };
496
- const hasNocobase = normalizedResults.hasNocobase === 'yes';
497
- const existingEnv = !hasNocobase
632
+ });
633
+ const setupMode = resolveInitSetupMode(normalizedResults);
634
+ const existingEnv = isInstallLikeSetupMode(normalizedResults)
498
635
  ? await getEnv(String(normalizedResults.appName ?? '').trim(), { scope: resolveDefaultConfigScope() })
499
636
  : undefined;
500
637
  if (existingEnv && Boolean(normalizedFlags.force)) {
@@ -506,7 +643,7 @@ Prompt modes:
506
643
  let managedInstallResults;
507
644
  try {
508
645
  // oclif explicit registry keys use `:` (e.g. `env:add`); users still type `nb env add`.
509
- if (hasNocobase) {
646
+ if (setupMode === 'connect-remote') {
510
647
  logInitStage('Connecting to the env');
511
648
  printVerbose('Running nb env add');
512
649
  await this.config.runCommand('env:add', this.buildEnvAddArgv(normalizedResults));
@@ -536,6 +673,7 @@ Prompt modes:
536
673
  envName: String(presetValues.appName ?? '').trim(),
537
674
  flags: {
538
675
  ...flags,
676
+ 'app-path': flags['app-path'] ?? '',
539
677
  'app-root-path': flags['app-root-path'] ?? '',
540
678
  'storage-path': flags['storage-path'] ?? '',
541
679
  },
@@ -569,7 +707,7 @@ Prompt modes:
569
707
  sectionDescription: initText('webUi.gettingStarted.description'),
570
708
  catalog: {
571
709
  appName: c.appName,
572
- hasNocobase: c.hasNocobase,
710
+ setupMode: c.setupMode,
573
711
  },
574
712
  },
575
713
  {
@@ -588,9 +726,9 @@ Prompt modes:
588
726
  sectionDescription: initText('webUi.createNewApp.description'),
589
727
  catalog: {
590
728
  lang: c.lang,
591
- appRootPath: c.appRootPath,
729
+ appPath: c.appPath,
592
730
  appPort: c.appPort,
593
- storagePath: c.storagePath,
731
+ appPublicPath: c.appPublicPath,
594
732
  skipDownload: c.skipDownload,
595
733
  },
596
734
  },
@@ -640,24 +778,42 @@ Prompt modes:
640
778
  rootNickname: c.rootNickname,
641
779
  },
642
780
  },
781
+ {
782
+ sectionTitle: initText('webUi.connectExistingApp.title'),
783
+ sectionDescription: initText('webUi.connectExistingApp.description'),
784
+ catalog: {
785
+ installApiBaseUrl: c.installApiBaseUrl,
786
+ installAuthType: c.installAuthType,
787
+ installAccessToken: c.installAccessToken,
788
+ },
789
+ },
643
790
  ];
644
791
  }
645
792
  buildPresetValuesFromFlags(flags) {
646
793
  const preset = {};
647
794
  const argv = process.argv.slice(2);
795
+ const setupMode = explicitSetupModeFlag(flags);
648
796
  if (flags.env !== undefined && String(flags.env).trim() !== '') {
649
797
  preset.appName = String(flags.env).trim();
650
798
  }
799
+ if (setupMode) {
800
+ preset.setupMode = setupMode;
801
+ }
651
802
  const apiBaseUrl = explicitApiBaseUrlFlag(flags);
652
803
  if (apiBaseUrl) {
653
- preset.hasNocobase = 'yes';
804
+ preset.setupMode ??= 'connect-remote';
654
805
  preset.apiBaseUrl = apiBaseUrl;
806
+ preset.installApiBaseUrl = apiBaseUrl;
655
807
  }
656
808
  else if (flags['default-api-base-url'] !== undefined && String(flags['default-api-base-url']).trim() !== '') {
657
- preset.apiBaseUrl = String(flags['default-api-base-url']).trim();
809
+ const defaultApiBaseUrl = String(flags['default-api-base-url']).trim();
810
+ preset.apiBaseUrl = defaultApiBaseUrl;
811
+ preset.installApiBaseUrl = defaultApiBaseUrl;
658
812
  }
659
813
  if (flags['auth-type'] !== undefined && String(flags['auth-type']).trim() !== '') {
660
- preset.authType = String(flags['auth-type']).trim();
814
+ const authType = String(flags['auth-type']).trim();
815
+ preset.authType = authType;
816
+ preset.installAuthType = authType;
661
817
  }
662
818
  if (flags['skip-auth']) {
663
819
  preset.skipAuth = true;
@@ -665,16 +821,24 @@ Prompt modes:
665
821
  const accessToken = String(flags['access-token'] ?? flags.token ?? '');
666
822
  if (flags['access-token'] !== undefined || flags.token !== undefined) {
667
823
  preset.accessToken = accessToken;
824
+ preset.installAccessToken = accessToken;
668
825
  }
669
826
  if (flags.username !== undefined) {
670
- preset.username = String(flags.username ?? '').trim();
827
+ const username = String(flags.username ?? '').trim();
828
+ preset.username = username;
829
+ preset.installUsername = username;
671
830
  }
672
831
  if (flags.password !== undefined) {
673
- preset.password = String(flags.password ?? '');
832
+ const password = String(flags.password ?? '');
833
+ preset.password = password;
834
+ preset.installPassword = password;
674
835
  }
675
836
  if (flags.lang !== undefined && String(flags.lang).trim() !== '') {
676
837
  preset.lang = String(flags.lang).trim();
677
838
  }
839
+ if (flags['app-path'] !== undefined && String(flags['app-path']).trim() !== '') {
840
+ preset.appPath = String(flags['app-path']).trim();
841
+ }
678
842
  if (flags['app-root-path'] !== undefined && String(flags['app-root-path']).trim() !== '') {
679
843
  preset.appRootPath = String(flags['app-root-path']).trim();
680
844
  }
@@ -684,6 +848,9 @@ Prompt modes:
684
848
  if (flags['storage-path'] !== undefined && String(flags['storage-path']).trim() !== '') {
685
849
  preset.storagePath = String(flags['storage-path']).trim();
686
850
  }
851
+ if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
852
+ preset.appPublicPath = String(flags['app-public-path']).trim();
853
+ }
687
854
  if (flags['root-username'] !== undefined) {
688
855
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
689
856
  }
@@ -783,9 +950,13 @@ Prompt modes:
783
950
  if (explicitDbHostFlag(flags)) {
784
951
  preset.builtinDb = false;
785
952
  }
786
- if (!preset.hasNocobase && hasDownloadOverride(flags)) {
787
- preset.hasNocobase = 'no';
953
+ if (!preset.setupMode && hasDownloadOverride(flags)) {
954
+ preset.setupMode = 'install-new';
788
955
  }
956
+ if (preset.setupMode === 'manage-local') {
957
+ preset.skipDownload = false;
958
+ }
959
+ applyLegacyHasNocobaseAlias(preset);
789
960
  return preset;
790
961
  }
791
962
  hasAgentsDirInCwd() {
@@ -816,14 +987,23 @@ Prompt modes:
816
987
  const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
817
988
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
818
989
  const appPort = String(results.appPort ?? '').trim();
990
+ const appPublicPath = String(results.appPublicPath ?? '').trim();
819
991
  const source = String(results.source ?? '').trim();
820
992
  const version = resolveInitDownloadVersion(results);
821
993
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
822
994
  const dockerPlatform = String(results.dockerPlatform ?? '').trim();
823
995
  const gitUrl = String(results.gitUrl ?? '').trim();
824
996
  const npmRegistry = String(results.npmRegistry ?? '').trim();
997
+ const appPath = String(results.appPath ?? '').trim() ||
998
+ inferConfiguredAppPathFromLegacyConfig({
999
+ appRootPath: results.appRootPath,
1000
+ storagePath: results.storagePath,
1001
+ }) ||
1002
+ '';
825
1003
  const appRootPath = String(results.appRootPath ?? '').trim();
826
1004
  const storagePath = String(results.storagePath ?? '').trim();
1005
+ const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : '';
1006
+ const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : '';
827
1007
  const dbDialect = String(results.dbDialect ?? '').trim();
828
1008
  const builtinDbImage = String(results.builtinDbImage ?? '').trim();
829
1009
  const dbHost = String(results.dbHost ?? '').trim();
@@ -850,7 +1030,7 @@ Prompt modes:
850
1030
  await upsertEnv(envName, {
851
1031
  ...(source === 'docker'
852
1032
  ? { kind: 'docker' }
853
- : source || appRootPath
1033
+ : source || appPath || appRootPath
854
1034
  ? { kind: 'local' }
855
1035
  : appPort
856
1036
  ? { kind: 'http' }
@@ -865,9 +1045,11 @@ Prompt modes:
865
1045
  ...(dockerPlatform ? { dockerPlatform } : {}),
866
1046
  ...(gitUrl ? { gitUrl } : {}),
867
1047
  ...(npmRegistry ? { npmRegistry } : {}),
868
- ...(appRootPath ? { appRootPath } : {}),
869
- ...(storagePath ? { storagePath } : {}),
1048
+ ...(appPath ? { appPath } : {}),
1049
+ ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
1050
+ ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
870
1051
  ...(appPort ? { appPort } : {}),
1052
+ ...(appPublicPath ? { appPublicPath } : {}),
871
1053
  ...(appKey ? { appKey } : {}),
872
1054
  ...(timeZone ? { timezone: timeZone } : {}),
873
1055
  ...(!skipDownload && results.devDependencies !== undefined
@@ -919,8 +1101,11 @@ Prompt modes:
919
1101
  const processArgv = process.argv.slice(2);
920
1102
  const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
921
1103
  const source = String(results.source ?? '').trim();
922
- const skipDownload = Boolean(flags['skip-download']) || results.skipDownload === true;
923
- const hasNocobase = String(results.hasNocobase ?? '').trim() === 'yes';
1104
+ const setupMode = resolveInitSetupMode({
1105
+ setupMode: results.setupMode ?? flags['setup-mode'],
1106
+ hasNocobase: results.hasNocobase,
1107
+ });
1108
+ const skipDownload = setupMode === 'manage-local' ? false : Boolean(flags['skip-download']) || results.skipDownload === true;
924
1109
  const apiBaseUrl = String(results.apiBaseUrl ?? '').trim();
925
1110
  const authType = String(results.authType ?? '').trim();
926
1111
  const accessToken = String(results.accessToken ?? '');
@@ -933,7 +1118,7 @@ Prompt modes:
933
1118
  if (flags.verbose) {
934
1119
  argv.push('--verbose');
935
1120
  }
936
- if (hasNocobase && apiBaseUrl) {
1121
+ if (setupMode === 'connect-remote' && apiBaseUrl) {
937
1122
  argv.push('--api-base-url', apiBaseUrl);
938
1123
  }
939
1124
  if (authType) {
@@ -955,8 +1140,17 @@ Prompt modes:
955
1140
  if (lang) {
956
1141
  argv.push('--lang', lang);
957
1142
  }
1143
+ const appPath = String(results.appPath ?? '').trim() ||
1144
+ inferConfiguredAppPathFromLegacyConfig({
1145
+ appRootPath: results.appRootPath,
1146
+ storagePath: results.storagePath,
1147
+ }) ||
1148
+ '';
1149
+ if (appPath) {
1150
+ argv.push('--app-path', appPath);
1151
+ }
958
1152
  const appRootPath = String(results.appRootPath ?? '').trim();
959
- if (appRootPath) {
1153
+ if (appRootPath && (!appPath || !areConfiguredPathsEquivalent(appRootPath, deriveConfiguredSourcePath(appPath)))) {
960
1154
  argv.push('--app-root-path', appRootPath);
961
1155
  }
962
1156
  const appPort = String(results.appPort ?? '').trim();
@@ -964,9 +1158,13 @@ Prompt modes:
964
1158
  argv.push('--app-port', appPort);
965
1159
  }
966
1160
  const storagePath = String(results.storagePath ?? '').trim();
967
- if (storagePath) {
1161
+ if (storagePath && (!appPath || !areConfiguredPathsEquivalent(storagePath, deriveConfiguredStoragePath(appPath)))) {
968
1162
  argv.push('--storage-path', storagePath);
969
1163
  }
1164
+ const appPublicPath = String(results.appPublicPath ?? '').trim();
1165
+ if (appPublicPath) {
1166
+ argv.push('--app-public-path', appPublicPath);
1167
+ }
970
1168
  if (flags.force) {
971
1169
  argv.push('--force');
972
1170
  }
@@ -1072,20 +1270,21 @@ Prompt modes:
1072
1270
  if (results.dbUnderscored !== undefined) {
1073
1271
  argv.push(results.dbUnderscored ? '--db-underscored' : '--no-db-underscored');
1074
1272
  }
1273
+ const includeRootUser = setupMode === 'install-new';
1075
1274
  const rootUsername = String(results.rootUsername ?? '').trim();
1076
- if (rootUsername) {
1275
+ if (includeRootUser && rootUsername) {
1077
1276
  argv.push('--root-username', rootUsername);
1078
1277
  }
1079
1278
  const rootEmail = String(results.rootEmail ?? '').trim();
1080
- if (rootEmail) {
1279
+ if (includeRootUser && rootEmail) {
1081
1280
  argv.push('--root-email', rootEmail);
1082
1281
  }
1083
1282
  const rootPassword = String(results.rootPassword ?? '');
1084
- if (rootPassword) {
1283
+ if (includeRootUser && rootPassword) {
1085
1284
  argv.push('--root-password', rootPassword);
1086
1285
  }
1087
1286
  const rootNickname = String(results.rootNickname ?? '').trim();
1088
- if (rootNickname) {
1287
+ if (includeRootUser && rootNickname) {
1089
1288
  argv.push('--root-nickname', rootNickname);
1090
1289
  }
1091
1290
  return argv;
@@ -1097,8 +1296,12 @@ Prompt modes:
1097
1296
  }
1098
1297
  const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
1099
1298
  argv.push('--env', envName);
1299
+ const setupMode = resolveInitSetupMode(results);
1300
+ if (setupMode === 'manage-local') {
1301
+ argv.push('--setup-mode', 'manage-local');
1302
+ }
1100
1303
  const source = String(results.source ?? '').trim();
1101
- const skipDownload = Boolean(flags['skip-download']) || results.skipDownload === true;
1304
+ const skipDownload = setupMode === 'manage-local' ? false : Boolean(flags['skip-download']) || results.skipDownload === true;
1102
1305
  if (source) {
1103
1306
  argv.push('--source', source);
1104
1307
  }
@@ -1127,7 +1330,14 @@ Prompt modes:
1127
1330
  }
1128
1331
  else {
1129
1332
  const outputDir = String(results.outputDir ?? '').trim();
1130
- if (outputDir && outputDir !== String(results.appRootPath ?? '').trim()) {
1333
+ const appPath = String(results.appPath ?? '').trim() ||
1334
+ inferConfiguredAppPathFromLegacyConfig({
1335
+ appRootPath: results.appRootPath,
1336
+ storagePath: results.storagePath,
1337
+ }) ||
1338
+ '';
1339
+ const expectedOutputDir = String(results.appRootPath ?? '').trim() || (appPath ? deriveConfiguredSourcePath(appPath) : '');
1340
+ if (outputDir && outputDir !== expectedOutputDir) {
1131
1341
  argv.push('--output-dir', outputDir);
1132
1342
  }
1133
1343
  if (results.devDependencies) {
@@ -1152,14 +1362,20 @@ Prompt modes:
1152
1362
  }
1153
1363
  buildResumeInstallArgv(flags) {
1154
1364
  const preset = this.buildPresetValuesFromFlags(flags);
1365
+ const setupMode = resolveInitSetupMode(preset);
1155
1366
  if (flags.yes) {
1156
1367
  preset.lang ??= yesInitialValue(Install.appPrompts.lang, 'en-US');
1157
- preset.rootUsername ??= yesInitialValue(Install.rootUserPrompts.rootUsername, 'nocobase');
1158
- preset.rootEmail ??= yesInitialValue(Install.rootUserPrompts.rootEmail, 'admin@nocobase.com');
1159
- preset.rootPassword ??= yesInitialValue(Install.rootUserPrompts.rootPassword, 'admin123');
1160
- preset.rootNickname ??= yesInitialValue(Install.rootUserPrompts.rootNickname, 'Super Admin');
1368
+ if (setupMode === 'install-new') {
1369
+ preset.rootUsername ??= yesInitialValue(Install.rootUserPrompts.rootUsername, 'nocobase');
1370
+ preset.rootEmail ??= yesInitialValue(Install.rootUserPrompts.rootEmail, 'admin@nocobase.com');
1371
+ preset.rootPassword ??= yesInitialValue(Install.rootUserPrompts.rootPassword, 'admin123');
1372
+ preset.rootNickname ??= yesInitialValue(Install.rootUserPrompts.rootNickname, 'Super Admin');
1373
+ }
1161
1374
  }
1162
- if (!flags['skip-download']) {
1375
+ if (setupMode === 'manage-local') {
1376
+ preset.skipDownload = false;
1377
+ }
1378
+ else if (!flags['skip-download']) {
1163
1379
  preset.replace ??= true;
1164
1380
  }
1165
1381
  return this.buildInstallArgv(preset, flags, {
@@ -1167,4 +1383,40 @@ Prompt modes:
1167
1383
  resume: true,
1168
1384
  });
1169
1385
  }
1386
+ normalizeInitResults(results) {
1387
+ const normalized = { ...results };
1388
+ const setupMode = resolveInitSetupMode(normalized);
1389
+ const defaultApiHost = normalizeConnectionString(normalized.defaultApiHost) || '127.0.0.1';
1390
+ if (setupMode === 'install-new') {
1391
+ normalized.apiBaseUrl =
1392
+ normalizeConnectionString(normalized.installApiBaseUrl) ||
1393
+ deriveInstallConnectionApiBaseUrl(normalized, defaultApiHost);
1394
+ normalized.authType = normalizeConnectionString(normalized.installAuthType) || 'oauth';
1395
+ normalized.username =
1396
+ normalized.authType === 'basic'
1397
+ ? normalizeConnectionString(normalized.installUsername) || normalizeConnectionString(normalized.rootUsername)
1398
+ : normalizeConnectionString(normalized.installUsername);
1399
+ normalized.password =
1400
+ normalized.authType === 'basic'
1401
+ ? String(normalized.installPassword ?? '') || String(normalized.rootPassword ?? '')
1402
+ : String(normalized.installPassword ?? '');
1403
+ normalized.accessToken = String(normalized.installAccessToken ?? '');
1404
+ }
1405
+ normalized.setupMode = setupMode;
1406
+ applyLegacyHasNocobaseAlias(normalized);
1407
+ if (setupMode === 'manage-local') {
1408
+ normalized.skipDownload = false;
1409
+ delete normalized.rootUsername;
1410
+ delete normalized.rootEmail;
1411
+ delete normalized.rootPassword;
1412
+ delete normalized.rootNickname;
1413
+ }
1414
+ delete normalized.installApiBaseUrl;
1415
+ delete normalized.installAuthType;
1416
+ delete normalized.installUsername;
1417
+ delete normalized.installPassword;
1418
+ delete normalized.installAccessToken;
1419
+ delete normalized.defaultApiHost;
1420
+ return normalized;
1421
+ }
1170
1422
  }