@nocobase/cli 2.2.0-beta.9 → 2.2.0-test.15

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 (39) hide show
  1. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  2. package/dist/commands/app/start.js +21 -1
  3. package/dist/commands/config/set.js +1 -0
  4. package/dist/commands/env/info.js +11 -1
  5. package/dist/commands/init.js +131 -4
  6. package/dist/commands/install.js +129 -6
  7. package/dist/commands/portal/create.js +105 -0
  8. package/dist/commands/portal/deploy.js +81 -0
  9. package/dist/commands/portal/destroy.js +104 -0
  10. package/dist/commands/portal/dev.js +71 -0
  11. package/dist/commands/portal/index.js +20 -0
  12. package/dist/commands/portal/info.js +82 -0
  13. package/dist/commands/portal/list.js +98 -0
  14. package/dist/commands/portal/pull.js +77 -0
  15. package/dist/commands/portal/push.js +79 -0
  16. package/dist/commands/source/dev.js +1 -1
  17. package/dist/commands/source/download.js +2 -2
  18. package/dist/lib/auth-store.js +3 -1
  19. package/dist/lib/cli-config.js +23 -2
  20. package/dist/lib/env-config.js +3 -0
  21. package/dist/lib/env-proxy.js +102 -3
  22. package/dist/lib/managed-init-env.js +6 -1
  23. package/dist/lib/portal-command-env.js +31 -0
  24. package/dist/lib/portal-create.js +488 -0
  25. package/dist/lib/portal-deploy.js +275 -0
  26. package/dist/lib/portal-destroy.js +100 -0
  27. package/dist/lib/portal-dev.js +79 -0
  28. package/dist/lib/portal-env-files.js +53 -0
  29. package/dist/lib/portal-info.js +31 -0
  30. package/dist/lib/portal-list.js +197 -0
  31. package/dist/lib/portal-source.js +416 -0
  32. package/dist/lib/portal-template.js +190 -0
  33. package/dist/lib/prompt-catalog-terminal.js +32 -19
  34. package/dist/lib/prompt-web-ui.js +13 -2
  35. package/dist/lib/run-npm.js +17 -16
  36. package/dist/lib/ui.js +28 -1
  37. package/dist/locale/en-US.json +191 -37
  38. package/dist/locale/zh-CN.json +191 -37
  39. package/package.json +5 -3
@@ -1,12 +1,14 @@
1
1
  add_header Cache-Control "public" always;
2
+ add_header Content-Security-Policy "sandbox" always;
2
3
  add_header X-Content-Type-Options "nosniff" always;
3
4
 
4
5
  access_log off;
5
6
  autoindex off;
6
7
 
7
8
  # Force potentially renderable uploaded files to download.
8
- location ~* \.(?:htm|html|svg|svgz|xhtml)$ {
9
+ location ~* \.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$ {
9
10
  add_header Cache-Control "public" always;
11
+ add_header Content-Security-Policy "sandbox" always;
10
12
  add_header X-Content-Type-Options "nosniff" always;
11
13
  add_header Content-Disposition "attachment" always;
12
14
  }
@@ -16,6 +18,7 @@ location ~* \.md$ {
16
18
  default_type text/markdown;
17
19
 
18
20
  add_header Cache-Control "public" always;
21
+ add_header Content-Security-Policy "sandbox" always;
19
22
  add_header X-Content-Type-Options "nosniff" always;
20
23
  add_header Content-Disposition "inline" always;
21
24
  }
@@ -14,6 +14,7 @@ import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBase
14
14
  import { ensureBuiltinDbReady, ensureLocalPostinstall, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
15
15
  import { clearEnvRootSetup, getEnv, upsertEnv } from '../../lib/auth-store.js';
16
16
  import { buildInitAppEnvVarsFromConfig, isPreparedSetupState } from '../../lib/managed-init-env.js';
17
+ import { prepareInitialPortalTemplate } from '../../lib/portal-template.js';
17
18
  import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
18
19
  import { readManagedRuntimeEnvValues } from '../../lib/managed-env-file.js';
19
20
  import { run } from '../../lib/run-npm.js';
@@ -181,6 +182,19 @@ async function finalizePreparedEnv(envName) {
181
182
  setupState: 'installed',
182
183
  });
183
184
  }
185
+ async function prepareInitialPortalForRuntime(runtime, verbose) {
186
+ await prepareInitialPortalTemplate({
187
+ developmentMode: runtime.env.config?.developmentMode,
188
+ portalName: runtime.env.config?.portalName,
189
+ portalTemplate: runtime.env.config?.portalTemplate,
190
+ npmRegistry: runtime.env.config?.npmRegistry,
191
+ storagePath: runtime.env.storagePath || runtime.env.config?.storagePath,
192
+ verbose,
193
+ onStartTask: startTask,
194
+ onSucceedTask: succeedTask,
195
+ onFailTask: failTask,
196
+ });
197
+ }
184
198
  export default class AppStart extends Command {
185
199
  static hidden = false;
186
200
  static description = 'Start NocoBase for the selected env. When applicable, the CLI synchronizes licensed commercial plugins first, then prepares and starts the app or recreates the saved Docker container.';
@@ -249,7 +263,7 @@ export default class AppStart extends Command {
249
263
  }
250
264
  const daemonFlagWasProvided = argvHasToken(this.argv, ['--daemon', '--no-daemon']);
251
265
  const runtime = await resolveManagedAppRuntime(requestedEnv);
252
- const preparedInitEnvVars = buildInitAppEnvVarsFromConfig(runtime?.env.config);
266
+ const preparedInitEnvVars = buildInitAppEnvVarsFromConfig(runtime?.env.config, { includePortal: false });
253
267
  const isPreparedEnv = isPreparedSetupState(runtime?.env.config?.setupState);
254
268
  const hookCommand = resolveHookCommand(flags['hook-command']);
255
269
  const hookPhase = resolveAppStartHookPhase({
@@ -322,6 +336,9 @@ export default class AppStart extends Command {
322
336
  command: hookCommand,
323
337
  });
324
338
  }
339
+ if (isPreparedEnv) {
340
+ await prepareInitialPortalForRuntime(runtime, flags.verbose);
341
+ }
325
342
  await recreateSavedDockerApp(runtime, {
326
343
  initEnvVars: isPreparedEnv ? preparedInitEnvVars : undefined,
327
344
  verbose: flags.verbose,
@@ -470,6 +487,9 @@ export default class AppStart extends Command {
470
487
  command: hookCommand,
471
488
  });
472
489
  }
490
+ if (isPreparedEnv) {
491
+ await prepareInitialPortalForRuntime(runtime, flags.verbose);
492
+ }
473
493
  await runLocalNocoBaseCommand(runtime, npmArgs, {
474
494
  env: startEnv,
475
495
  stdio: commandStdio,
@@ -14,6 +14,7 @@ export default class ConfigSet extends Command {
14
14
  static description = 'Set a CLI configuration value for a supported configuration key.';
15
15
  static examples = [
16
16
  '<%= config.bin %> <%= command.id %> locale zh-CN',
17
+ '<%= config.bin %> <%= command.id %> default-portal-template git@github.com:nocobase/admin-starter.git',
17
18
  '<%= config.bin %> <%= command.id %> update.policy prompt',
18
19
  '<%= config.bin %> <%= command.id %> docker.network nocobase',
19
20
  '<%= config.bin %> <%= command.id %> docker.container-prefix nb',
@@ -170,8 +170,13 @@ export default class EnvInfo extends Command {
170
170
  'auth.accessToken': authGroup.accessToken,
171
171
  'auth.refreshToken': authGroup.refreshToken,
172
172
  };
173
+ const envGroup = {
174
+ name: runtime.envName,
175
+ kind: runtime.kind,
176
+ };
173
177
  const output = {
174
178
  ok: true,
179
+ name: runtime.envName,
175
180
  env: runtime.envName,
176
181
  kind: runtime.kind,
177
182
  app: serializeGroup(appGroup),
@@ -197,6 +202,11 @@ export default class EnvInfo extends Command {
197
202
  this.log(JSON.stringify(output, null, 2));
198
203
  return;
199
204
  }
200
- this.log([createGroupTable('App', appGroup), createGroupTable('DB', dbGroup), createGroupTable('API', apiGroup)].join('\n\n'));
205
+ this.log([
206
+ createGroupTable('Env', envGroup),
207
+ createGroupTable('App', appGroup),
208
+ createGroupTable('DB', dbGroup),
209
+ createGroupTable('API', apiGroup),
210
+ ].join('\n\n'));
201
211
  }
202
212
  }
@@ -16,7 +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 { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
19
- import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
19
+ import { getCliConfigValue, resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
20
+ import { resolveOfficialDockerRegistry } from '../lib/docker-image.js';
20
21
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
21
22
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
22
23
  import { runPromptCatalogWebUI } from "../lib/prompt-web-ui.js";
@@ -31,8 +32,12 @@ import EnvAdd from "./env/add.js";
31
32
  import Install, { defaultDbPortForDialect } from "./install.js";
32
33
  const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
33
34
  const DEFAULT_INIT_APP_NAME = 'local';
35
+ const DEFAULT_INIT_DEVELOPMENT_MODE = 'no-code';
36
+ const DEFAULT_INIT_PORTAL_NAME = 'admin';
37
+ const DEFAULT_INIT_PORTAL_TEMPLATE = 'git@github.com:nocobase/admin-starter.git';
34
38
  const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
35
39
  const INIT_SETUP_MODES = ['install-new', 'manage-local', 'connect-remote'];
40
+ const INIT_DEVELOPMENT_MODES = ['no-code', 'vibe-coding'];
36
41
  const INIT_ENV_ADD_FLAG_NAMES = [
37
42
  'locale',
38
43
  'default-api-base-url',
@@ -79,6 +84,9 @@ function isInstallNewSetupMode(values) {
79
84
  function isInstallLikeSetupMode(values) {
80
85
  return !isRemoteSetupMode(values);
81
86
  }
87
+ function isVibeCodingMode(values) {
88
+ return String(values.developmentMode ?? DEFAULT_INIT_DEVELOPMENT_MODE).trim() === 'vibe-coding';
89
+ }
82
90
  function remoteConnectionOnly(def) {
83
91
  return withExtraHidden(def, (values) => !isRemoteSetupMode(values));
84
92
  }
@@ -386,6 +394,43 @@ Prompt modes:
386
394
  devDependencies: installLikeDownloadExecutionOnly(Download.prompts.devDependencies),
387
395
  build: installLikeDownloadExecutionOnly(Download.prompts.build),
388
396
  buildDts: installLikeDownloadExecutionOnly(Download.prompts.buildDts),
397
+ developmentMode: installNewOnly({
398
+ type: 'select',
399
+ variant: 'radio',
400
+ message: initText('prompts.developmentMode.message'),
401
+ options: [
402
+ {
403
+ value: 'no-code',
404
+ label: initText('prompts.developmentMode.noCodeLabel'),
405
+ hint: initText('prompts.developmentMode.noCodeHint'),
406
+ },
407
+ {
408
+ value: 'vibe-coding',
409
+ label: initText('prompts.developmentMode.vibeCodingLabel'),
410
+ hint: initText('prompts.developmentMode.vibeCodingHint'),
411
+ },
412
+ ],
413
+ initialValue: DEFAULT_INIT_DEVELOPMENT_MODE,
414
+ yesInitialValue: DEFAULT_INIT_DEVELOPMENT_MODE,
415
+ required: true,
416
+ }),
417
+ portalName: installNewOnly({
418
+ type: 'text',
419
+ message: initText('prompts.portalName.message'),
420
+ placeholder: DEFAULT_INIT_PORTAL_NAME,
421
+ initialValue: DEFAULT_INIT_PORTAL_NAME,
422
+ yesInitialValue: DEFAULT_INIT_PORTAL_NAME,
423
+ hidden: (values) => !isVibeCodingMode(values),
424
+ required: true,
425
+ }),
426
+ portalTemplate: installNewOnly({
427
+ type: 'text',
428
+ message: initText('prompts.portalTemplate.message'),
429
+ placeholder: DEFAULT_INIT_PORTAL_TEMPLATE,
430
+ yesInitialValue: DEFAULT_INIT_PORTAL_TEMPLATE,
431
+ hidden: (values) => !isVibeCodingMode(values),
432
+ required: true,
433
+ }),
389
434
  dbDialect: installLikeOnly(Install.dbPrompts.dbDialect),
390
435
  builtinDb: installLikeOnly(Install.dbPrompts.builtinDb),
391
436
  builtinDbImage: installLikeOnly(Install.dbPrompts.builtinDbImage),
@@ -411,6 +456,13 @@ Prompt modes:
411
456
  ...Init.prompts,
412
457
  installApiBaseUrl: createInstallConnectionApiBaseUrlPrompt(options.defaultApiHost),
413
458
  };
459
+ const defaultPortalTemplate = String(options.defaultPortalTemplate ?? '').trim();
460
+ if (defaultPortalTemplate) {
461
+ prompts.portalTemplate = {
462
+ ...prompts.portalTemplate,
463
+ yesInitialValue: defaultPortalTemplate,
464
+ };
465
+ }
414
466
  if (flags['skip-auth']) {
415
467
  const accessTokenPrompt = {
416
468
  ...EnvAdd.prompts.accessToken,
@@ -460,6 +512,16 @@ Prompt modes:
460
512
  description: 'Setup mode: install a new app, manage a local app by reusing its database, or connect a remote app',
461
513
  options: [...INIT_SETUP_MODES],
462
514
  }),
515
+ 'development-mode': Flags.string({
516
+ description: 'Initial development mode: no-code or vibe-coding',
517
+ options: [...INIT_DEVELOPMENT_MODES],
518
+ }),
519
+ 'portal-name': Flags.string({
520
+ description: 'Initial portal name when --development-mode vibe-coding is used',
521
+ }),
522
+ 'portal-template': Flags.string({
523
+ description: 'Initial portal template Git URL or local path when --development-mode vibe-coding is used',
524
+ }),
463
525
  ui: Flags.boolean({
464
526
  description: 'Open the guided setup flow in a local browser form (not valid with --yes)',
465
527
  default: false,
@@ -577,7 +639,8 @@ Prompt modes:
577
639
  const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
578
640
  const defaultUiHost = await resolveDefaultUiHost();
579
641
  const defaultApiHost = await resolveDefaultApiHost();
580
- const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost });
642
+ const defaultPortalTemplate = String(dynamicInitialValues.portalTemplate ?? '').trim();
643
+ const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost, defaultPortalTemplate });
581
644
  if (useBrowserUi) {
582
645
  presetValues = await runPromptCatalogWebUI({
583
646
  stages: Init.buildWebUiStages(promptCatalog),
@@ -606,6 +669,7 @@ Prompt modes:
606
669
  ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
607
670
  : {}),
608
671
  },
672
+ yesInitialValues: pickKeys(dynamicInitialValues, ['portalTemplate']),
609
673
  values: presetValues,
610
674
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
611
675
  hooks: {
@@ -678,7 +742,9 @@ Prompt modes:
678
742
  }
679
743
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
680
744
  const out = {};
681
- if (!Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
745
+ const shouldResolveAppInitialValues = !Object.prototype.hasOwnProperty.call(presetValues, 'appPort') ||
746
+ !Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate');
747
+ if (shouldResolveAppInitialValues) {
682
748
  const appInitialValues = await Install.buildAppPromptInitialValues({
683
749
  envName: String(presetValues.appName ?? '').trim(),
684
750
  flags: {
@@ -686,17 +752,31 @@ Prompt modes:
686
752
  'app-path': flags['app-path'] ?? '',
687
753
  'app-root-path': flags['app-root-path'] ?? '',
688
754
  'storage-path': flags['storage-path'] ?? '',
755
+ 'portal-template': flags['portal-template'] ??
756
+ (Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate')
757
+ ? String(presetValues.portalTemplate ?? '')
758
+ : undefined),
689
759
  },
690
760
  warnOnPortFallback: false,
691
761
  });
692
- if (appInitialValues.appPort !== undefined) {
762
+ if (appInitialValues.appPort !== undefined && !Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
693
763
  out.appPort = appInitialValues.appPort;
694
764
  }
765
+ if (appInitialValues.portalTemplate !== undefined &&
766
+ !Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate')) {
767
+ out.portalTemplate = appInitialValues.portalTemplate;
768
+ }
695
769
  }
696
770
  const downloadSeed = { ...presetValues };
697
771
  if (flags.yes && !Object.prototype.hasOwnProperty.call(downloadSeed, 'source')) {
698
772
  downloadSeed.source = 'docker';
699
773
  }
774
+ const builtinDbImageRegistry = String(presetValues.dockerRegistry ?? '').trim() ||
775
+ resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
776
+ if (!Object.prototype.hasOwnProperty.call(presetValues, 'dockerRegistry')) {
777
+ out.dockerRegistry = builtinDbImageRegistry;
778
+ }
779
+ out.builtinDbImageRegistry = builtinDbImageRegistry;
700
780
  const dbInitial = await Install.buildDbPromptInitialValues({
701
781
  flags,
702
782
  downloadResults: downloadSeed,
@@ -704,6 +784,9 @@ Prompt modes:
704
784
  warnOnPortFallback: false,
705
785
  });
706
786
  for (const [key, value] of Object.entries(dbInitial)) {
787
+ if (key === 'builtinDbImage') {
788
+ continue;
789
+ }
707
790
  if (!Object.prototype.hasOwnProperty.call(presetValues, key)) {
708
791
  out[key] = value;
709
792
  }
@@ -761,6 +844,13 @@ Prompt modes:
761
844
  buildDts: c.buildDts,
762
845
  },
763
846
  },
847
+ {
848
+ sectionTitle: initText('webUi.developmentMode.title'),
849
+ sectionDescription: initText('webUi.developmentMode.description'),
850
+ catalog: {
851
+ developmentMode: c.developmentMode,
852
+ },
853
+ },
764
854
  {
765
855
  sectionTitle: initText('webUi.configureDatabase.title'),
766
856
  sectionDescription: initText('webUi.configureDatabase.description'),
@@ -861,6 +951,15 @@ Prompt modes:
861
951
  if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
862
952
  preset.appPublicPath = String(flags['app-public-path']).trim();
863
953
  }
954
+ if (flags['development-mode'] !== undefined && String(flags['development-mode']).trim() !== '') {
955
+ preset.developmentMode = String(flags['development-mode']).trim();
956
+ }
957
+ if (flags['portal-name'] !== undefined && String(flags['portal-name']).trim() !== '') {
958
+ preset.portalName = String(flags['portal-name']).trim();
959
+ }
960
+ if (flags['portal-template'] !== undefined && String(flags['portal-template']).trim() !== '') {
961
+ preset.portalTemplate = String(flags['portal-template']).trim();
962
+ }
864
963
  if (flags['root-username'] !== undefined) {
865
964
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
866
965
  }
@@ -998,6 +1097,9 @@ Prompt modes:
998
1097
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
999
1098
  const appPort = String(results.appPort ?? '').trim();
1000
1099
  const appPublicPath = String(results.appPublicPath ?? '').trim();
1100
+ const developmentMode = String(results.developmentMode ?? '').trim();
1101
+ const portalName = String(results.portalName ?? '').trim();
1102
+ const portalTemplate = String(results.portalTemplate ?? '').trim();
1001
1103
  const source = String(results.source ?? '').trim();
1002
1104
  const version = resolveInitDownloadVersion(results);
1003
1105
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
@@ -1070,6 +1172,9 @@ Prompt modes:
1070
1172
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
1071
1173
  ...(appPort ? { appPort } : {}),
1072
1174
  ...(appPublicPath ? { appPublicPath } : {}),
1175
+ ...(developmentMode && developmentMode !== DEFAULT_INIT_DEVELOPMENT_MODE ? { developmentMode } : {}),
1176
+ ...(portalName ? { portalName } : {}),
1177
+ ...(portalTemplate ? { portalTemplate } : {}),
1073
1178
  ...(appKey ? { appKey } : {}),
1074
1179
  ...(timeZone ? { timezone: timeZone } : {}),
1075
1180
  ...(!skipDownload && results.devDependencies !== undefined
@@ -1194,6 +1299,18 @@ Prompt modes:
1194
1299
  if (appPublicPath) {
1195
1300
  argv.push('--app-public-path', appPublicPath);
1196
1301
  }
1302
+ const developmentMode = String(results.developmentMode ?? '').trim();
1303
+ if (developmentMode && developmentMode !== DEFAULT_INIT_DEVELOPMENT_MODE) {
1304
+ argv.push('--development-mode', developmentMode);
1305
+ }
1306
+ const portalName = String(results.portalName ?? '').trim();
1307
+ if (portalName) {
1308
+ argv.push('--portal-name', portalName);
1309
+ }
1310
+ const portalTemplate = String(results.portalTemplate ?? '').trim();
1311
+ if (portalTemplate) {
1312
+ argv.push('--portal-template', portalTemplate);
1313
+ }
1197
1314
  if (flags.force) {
1198
1315
  argv.push('--force');
1199
1316
  }
@@ -1391,6 +1508,16 @@ Prompt modes:
1391
1508
  delete normalized.rootPassword;
1392
1509
  delete normalized.rootNickname;
1393
1510
  }
1511
+ const developmentMode = normalizeConnectionString(normalized.developmentMode) || DEFAULT_INIT_DEVELOPMENT_MODE;
1512
+ normalized.developmentMode = developmentMode;
1513
+ if (developmentMode === 'vibe-coding') {
1514
+ normalized.portalName = normalizeConnectionString(normalized.portalName) || DEFAULT_INIT_PORTAL_NAME;
1515
+ normalized.portalTemplate = normalizeConnectionString(normalized.portalTemplate);
1516
+ }
1517
+ else {
1518
+ delete normalized.portalName;
1519
+ delete normalized.portalTemplate;
1520
+ }
1394
1521
  delete normalized.installApiBaseUrl;
1395
1522
  delete normalized.installAuthType;
1396
1523
  delete normalized.installUsername;
@@ -29,6 +29,7 @@ import { buildStoredEnvConfig } from '../lib/env-config.js';
29
29
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
30
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
31
  import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
32
+ import { prepareInitialPortalTemplate } from '../lib/portal-template.js';
32
33
  import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
33
34
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
34
35
  import Download from './download.js';
@@ -53,6 +54,10 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
53
54
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
54
55
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
55
56
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
57
+ const DEFAULT_INSTALL_DEVELOPMENT_MODE = 'no-code';
58
+ const DEFAULT_INSTALL_PORTAL_NAME = 'admin';
59
+ const DEFAULT_INSTALL_PORTAL_TEMPLATE = 'git@github.com:nocobase/admin-starter.git';
60
+ const INSTALL_DEVELOPMENT_MODES = ['no-code', 'vibe-coding'];
56
61
  function toOptionalPromptString(value) {
57
62
  const text = String(value ?? '').trim();
58
63
  return text || undefined;
@@ -180,6 +185,13 @@ function defaultBuiltinDbImageForDialect(value, options) {
180
185
  function defaultDbDatabaseForDialect(value) {
181
186
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
182
187
  }
188
+ function isVibeCodingMode(values) {
189
+ return String(values.developmentMode ?? DEFAULT_INSTALL_DEVELOPMENT_MODE).trim() === 'vibe-coding';
190
+ }
191
+ function supportsDbSchemaPrompt(value) {
192
+ const dialect = String(value ?? '').trim();
193
+ return dialect === 'postgres' || dialect === 'kingbase';
194
+ }
183
195
  function defaultDbHostForBuiltinDb(values) {
184
196
  return values.builtinDb ? DEFAULT_INSTALL_BUILTIN_DB_HOST : DEFAULT_INSTALL_DB_HOST;
185
197
  }
@@ -400,6 +412,16 @@ export default class Install extends Command {
400
412
  'app-public-path': Flags.string({
401
413
  description: 'Public path for the local app, for example / or /console/',
402
414
  }),
415
+ 'development-mode': Flags.string({
416
+ description: 'Initial development mode for the installed app',
417
+ options: [...INSTALL_DEVELOPMENT_MODES],
418
+ }),
419
+ 'portal-name': Flags.string({
420
+ description: 'Initial portal name when --development-mode vibe-coding is used',
421
+ }),
422
+ 'portal-template': Flags.string({
423
+ description: 'Initial portal template Git URL or local path when --development-mode vibe-coding is used',
424
+ }),
403
425
  'root-username': Flags.string({
404
426
  description: 'Initial admin username for the installed app',
405
427
  required: false,
@@ -509,6 +531,42 @@ export default class Install extends Command {
509
531
  yesInitialValue: '/',
510
532
  validate: validateAppPublicPath,
511
533
  },
534
+ developmentMode: {
535
+ type: 'select',
536
+ message: installText('prompts.developmentMode.message'),
537
+ options: [
538
+ {
539
+ value: 'no-code',
540
+ label: installText('prompts.developmentMode.noCodeLabel'),
541
+ hint: installText('prompts.developmentMode.noCodeHint'),
542
+ },
543
+ {
544
+ value: 'vibe-coding',
545
+ label: installText('prompts.developmentMode.vibeCodingLabel'),
546
+ hint: installText('prompts.developmentMode.vibeCodingHint'),
547
+ },
548
+ ],
549
+ initialValue: DEFAULT_INSTALL_DEVELOPMENT_MODE,
550
+ yesInitialValue: DEFAULT_INSTALL_DEVELOPMENT_MODE,
551
+ required: true,
552
+ },
553
+ portalName: {
554
+ type: 'text',
555
+ message: installText('prompts.portalName.message'),
556
+ placeholder: DEFAULT_INSTALL_PORTAL_NAME,
557
+ initialValue: DEFAULT_INSTALL_PORTAL_NAME,
558
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_NAME,
559
+ hidden: (values) => !isVibeCodingMode(values),
560
+ required: true,
561
+ },
562
+ portalTemplate: {
563
+ type: 'text',
564
+ message: installText('prompts.portalTemplate.message'),
565
+ placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
566
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
567
+ hidden: (values) => !isVibeCodingMode(values),
568
+ required: true,
569
+ },
512
570
  };
513
571
  }
514
572
  static dbPrompts = {
@@ -536,7 +594,9 @@ export default class Install extends Command {
536
594
  type: 'text',
537
595
  message: installText('prompts.builtinDbImage.message'),
538
596
  placeholder: installText('prompts.builtinDbImage.placeholder'),
539
- initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
597
+ initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect, {
598
+ registry: String(values.builtinDbImageRegistry ?? '').trim() || undefined,
599
+ }),
540
600
  hidden: (values) => !values.builtinDb || !supportsBuiltinDbDialect(values.dbDialect),
541
601
  required: true,
542
602
  },
@@ -586,7 +646,7 @@ export default class Install extends Command {
586
646
  type: 'text',
587
647
  message: installText('prompts.dbSchema.message'),
588
648
  placeholder: installText('prompts.dbSchema.placeholder'),
589
- hidden: (values) => String(values.dbDialect ?? '').trim() !== 'postgres',
649
+ hidden: (values) => !supportsDbSchemaPrompt(values.dbDialect),
590
650
  },
591
651
  dbTablePrefix: {
592
652
  type: 'text',
@@ -766,6 +826,24 @@ export default class Install extends Command {
766
826
  preset.appPublicPath = v;
767
827
  }
768
828
  }
829
+ if (flags['development-mode'] !== undefined) {
830
+ const v = String(flags['development-mode'] ?? '').trim();
831
+ if (v) {
832
+ preset.developmentMode = v;
833
+ }
834
+ }
835
+ if (flags['portal-name'] !== undefined) {
836
+ const v = String(flags['portal-name'] ?? '').trim();
837
+ if (v) {
838
+ preset.portalName = v;
839
+ }
840
+ }
841
+ if (flags['portal-template'] !== undefined) {
842
+ const v = String(flags['portal-template'] ?? '').trim();
843
+ if (v) {
844
+ preset.portalTemplate = v;
845
+ }
846
+ }
769
847
  if (flags['root-username'] !== undefined) {
770
848
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
771
849
  }
@@ -857,6 +935,9 @@ export default class Install extends Command {
857
935
  'appPort',
858
936
  'storagePath',
859
937
  'appPublicPath',
938
+ 'developmentMode',
939
+ 'portalName',
940
+ 'portalTemplate',
860
941
  ]);
861
942
  }
862
943
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -1106,6 +1187,9 @@ export default class Install extends Command {
1106
1187
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1107
1188
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1108
1189
  const lang = Install.toOptionalPromptString(config.lang);
1190
+ const developmentMode = Install.toOptionalPromptString(config.developmentMode);
1191
+ const portalName = Install.toOptionalPromptString(config.portalName);
1192
+ const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1109
1193
  const auth = config.auth;
1110
1194
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1111
1195
  const appPreset = {
@@ -1115,6 +1199,9 @@ export default class Install extends Command {
1115
1199
  ...(appPort ? { appPort } : {}),
1116
1200
  ...(storagePath ? { storagePath } : {}),
1117
1201
  ...(appPublicPath ? { appPublicPath } : {}),
1202
+ ...(developmentMode ? { developmentMode } : {}),
1203
+ ...(portalName ? { portalName } : {}),
1204
+ ...(portalTemplate ? { portalTemplate } : {}),
1118
1205
  ...(hookScript ? { hookScript } : {}),
1119
1206
  };
1120
1207
  const downloadPreset = {
@@ -1257,6 +1344,12 @@ export default class Install extends Command {
1257
1344
  warn: params.warnOnPortFallback ?? true,
1258
1345
  });
1259
1346
  }
1347
+ if (params.flags['portal-template'] === undefined) {
1348
+ const defaultPortalTemplate = Install.toOptionalPromptString(await getCliConfigValue('default-portal-template'));
1349
+ if (defaultPortalTemplate) {
1350
+ initialValues.portalTemplate = defaultPortalTemplate;
1351
+ }
1352
+ }
1260
1353
  return initialValues;
1261
1354
  }
1262
1355
  static shouldPublishBuiltinDbPortForValues(values) {
@@ -1424,14 +1517,17 @@ export default class Install extends Command {
1424
1517
  static buildDockerAppContainerName(envName, containerPrefix) {
1425
1518
  return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1426
1519
  }
1427
- static buildInitAppEnvVars(params) {
1520
+ static buildInitAppEnvVars(params, options = {}) {
1428
1521
  return buildInitAppEnvVarsFromConfig({
1429
1522
  lang: String(params.appResults.lang ?? ''),
1430
1523
  rootUsername: String(params.rootResults.rootUsername ?? ''),
1431
1524
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1432
1525
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1433
1526
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1434
- });
1527
+ developmentMode: String(params.appResults.developmentMode ?? ''),
1528
+ portalName: String(params.appResults.portalName ?? ''),
1529
+ portalTemplate: String(params.appResults.portalTemplate ?? ''),
1530
+ }, options);
1435
1531
  }
1436
1532
  static shouldPublishBuiltinDbPort(source) {
1437
1533
  return String(source ?? '').trim() !== 'docker';
@@ -1786,7 +1882,7 @@ export default class Install extends Command {
1786
1882
  const initEnvVars = Install.buildInitAppEnvVars({
1787
1883
  appResults: params.appResults,
1788
1884
  rootResults: params.rootResults,
1789
- });
1885
+ }, { includePortal: false });
1790
1886
  const containerPort = resolveDockerImageContainerPort(imageRef);
1791
1887
  const args = [
1792
1888
  'run',
@@ -2063,7 +2159,7 @@ export default class Install extends Command {
2063
2159
  ...Install.buildInitAppEnvVars({
2064
2160
  appResults: params.appResults,
2065
2161
  rootResults: params.rootResults,
2066
- }),
2162
+ }, { includePortal: false }),
2067
2163
  };
2068
2164
  setOptionalEnvVar(env, 'APP_PUBLIC_PATH', Install.toOptionalPromptString(params.appResults.appPublicPath));
2069
2165
  setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
@@ -2260,6 +2356,19 @@ export default class Install extends Command {
2260
2356
  }
2261
2357
  return argv;
2262
2358
  }
2359
+ async prepareInitialPortalIfNeeded(params) {
2360
+ await prepareInitialPortalTemplate({
2361
+ developmentMode: Install.toOptionalPromptString(params.appResults.developmentMode),
2362
+ portalName: Install.toOptionalPromptString(params.appResults.portalName),
2363
+ portalTemplate: Install.toOptionalPromptString(params.appResults.portalTemplate),
2364
+ npmRegistry: Install.toOptionalPromptString(params.appResults.npmRegistry),
2365
+ storagePath: Install.resolveAbsoluteStoragePath(params.envName, params.appResults),
2366
+ verbose: params.verbose,
2367
+ onStartTask: (message) => this.logStage(message),
2368
+ onSucceedTask: (message) => printInfo(message),
2369
+ onFailTask: (message) => printWarning(message),
2370
+ });
2371
+ }
2263
2372
  async runInstallHookIfNeeded(params) {
2264
2373
  const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2265
2374
  const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
@@ -2303,6 +2412,9 @@ export default class Install extends Command {
2303
2412
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2304
2413
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2305
2414
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2415
+ const developmentMode = Install.toOptionalPromptString(params.appResults.developmentMode);
2416
+ const portalName = Install.toOptionalPromptString(params.appResults.portalName);
2417
+ const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2306
2418
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2307
2419
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2308
2420
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
@@ -2337,6 +2449,9 @@ export default class Install extends Command {
2337
2449
  ...(appPublicPath ? { appPublicPath } : {}),
2338
2450
  ...(envFile ? { envFile } : {}),
2339
2451
  lang: params.appResults.lang,
2452
+ developmentMode,
2453
+ portalName,
2454
+ portalTemplate,
2340
2455
  appKey: params.appResults.appKey,
2341
2456
  timezone: params.appResults.timeZone,
2342
2457
  builtinDb: params.dbResults.builtinDb,
@@ -2388,6 +2503,7 @@ export default class Install extends Command {
2388
2503
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2389
2504
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2390
2505
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2506
+ 'portal-template': parsed['portal-template'] ?? Install.toOptionalPromptString(appPreset.portalTemplate),
2391
2507
  },
2392
2508
  }),
2393
2509
  values: appPreset,
@@ -2626,6 +2742,13 @@ export default class Install extends Command {
2626
2742
  envAddResults,
2627
2743
  });
2628
2744
  }
2745
+ if (shouldStartApp) {
2746
+ await this.prepareInitialPortalIfNeeded({
2747
+ envName,
2748
+ appResults,
2749
+ verbose: parsed.verbose,
2750
+ });
2751
+ }
2629
2752
  if (shouldStartApp) {
2630
2753
  this.logStage('Starting NocoBase');
2631
2754
  await this.config.runCommand('app:start', this.buildAppStartArgv({ envName, verbose: parsed.verbose }));