@nocobase/cli 2.3.0-beta.3 → 3.0.0-alpha.11

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 (56) hide show
  1. package/dist/commands/api/swagger/get.js +96 -0
  2. package/dist/commands/api/swagger/index.js +20 -0
  3. package/dist/commands/api/swagger/list.js +92 -0
  4. package/dist/commands/config/set.js +1 -0
  5. package/dist/commands/env/add.js +6 -0
  6. package/dist/commands/env/update.js +13 -0
  7. package/dist/commands/init.js +51 -5
  8. package/dist/commands/install.js +60 -3
  9. package/dist/commands/portal/config.js +99 -0
  10. package/dist/commands/portal/create.js +96 -0
  11. package/dist/commands/portal/deploy.js +81 -0
  12. package/dist/commands/portal/destroy.js +126 -0
  13. package/dist/commands/portal/dev.js +73 -0
  14. package/dist/commands/portal/index.js +20 -0
  15. package/dist/commands/portal/info.js +82 -0
  16. package/dist/commands/portal/list.js +98 -0
  17. package/dist/commands/portal/pull.js +113 -0
  18. package/dist/commands/portal/push.js +79 -0
  19. package/dist/commands/source/dev.js +1 -1
  20. package/dist/lib/api-client.js +35 -9
  21. package/dist/lib/app-client-entry-mode.js +28 -0
  22. package/dist/lib/app-managed-resources.js +2 -0
  23. package/dist/lib/auth-store.js +55 -2
  24. package/dist/lib/bootstrap.js +3 -1
  25. package/dist/lib/cli-config.js +20 -1
  26. package/dist/lib/env-auth.js +2 -36
  27. package/dist/lib/env-command-config.js +1 -0
  28. package/dist/lib/env-config.js +11 -0
  29. package/dist/lib/env-portal-config.js +30 -0
  30. package/dist/lib/env-proxy.js +154 -7
  31. package/dist/lib/managed-env-file.js +119 -9
  32. package/dist/lib/managed-init-env.js +4 -1
  33. package/dist/lib/portal-build-html.js +27 -0
  34. package/dist/lib/portal-command-env.js +31 -0
  35. package/dist/lib/portal-config.js +119 -0
  36. package/dist/lib/portal-configure.js +110 -0
  37. package/dist/lib/portal-create.js +515 -0
  38. package/dist/lib/portal-deploy.js +266 -0
  39. package/dist/lib/portal-destroy.js +114 -0
  40. package/dist/lib/portal-dev.js +78 -0
  41. package/dist/lib/portal-env-files.js +54 -0
  42. package/dist/lib/portal-info.js +28 -0
  43. package/dist/lib/portal-list.js +205 -0
  44. package/dist/lib/portal-path-safety.js +76 -0
  45. package/dist/lib/portal-source.js +692 -0
  46. package/dist/lib/prompt-catalog-core.js +2 -2
  47. package/dist/lib/prompt-catalog-terminal.js +4 -5
  48. package/dist/lib/prompt-web-ui.js +12 -6
  49. package/dist/lib/proxy-caddy.js +2 -0
  50. package/dist/lib/proxy-nginx.js +1 -0
  51. package/dist/lib/run-npm.js +85 -20
  52. package/dist/lib/swagger-command.js +52 -0
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +245 -1
  55. package/dist/locale/zh-CN.json +245 -1
  56. package/package.json +5 -2
@@ -60,7 +60,7 @@ export function mergedBoolean(key, def, iv, useYesInitial) {
60
60
  }
61
61
  return def.initialValue ?? true;
62
62
  }
63
- export function mergedSelect(key, def, iv, useYesInitial) {
63
+ export function mergedSelect(key, def, iv, useYesInitial, valuesSoFar = {}) {
64
64
  const enabledValueList = enabledSelectOptionValues(def.options);
65
65
  if (hasIvKey(iv, key)) {
66
66
  const s = String(iv[key]);
@@ -72,7 +72,7 @@ export function mergedSelect(key, def, iv, useYesInitial) {
72
72
  if (useYesInitial && def.yesInitialValue !== undefined && enabledValueList.includes(def.yesInitialValue)) {
73
73
  return def.yesInitialValue;
74
74
  }
75
- const d = def.initialValue;
75
+ const d = typeof def.initialValue === 'function' ? def.initialValue(valuesSoFar) : def.initialValue;
76
76
  if (d !== undefined && enabledValueList.includes(d)) {
77
77
  return d;
78
78
  }
@@ -221,11 +221,12 @@ export async function runPromptCatalog(catalog, options = {}) {
221
221
  if (def.type === 'select') {
222
222
  const message = resolvePromptText(def.message, locale, key);
223
223
  const valueList = selectOptionValues(def.options);
224
+ const valuesSoFar = { ...computationSeed, ...out };
224
225
  if (def.required && def.options.length === 0) {
225
226
  hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.selectRequiredNoOptions', { key }));
226
227
  }
227
228
  if (!interactive) {
228
- const merged = mergedSelect(key, def, resolveIv, useYesInitial);
229
+ const merged = mergedSelect(key, def, resolveIv, useYesInitial, valuesSoFar);
229
230
  if (merged === undefined || !valueList.includes(merged)) {
230
231
  const bad = hasIvKey(resolveIv, key) && !valueList.includes(String(resolveIv[key]))
231
232
  ? String(resolveIv[key])
@@ -243,10 +244,8 @@ export async function runPromptCatalog(catalog, options = {}) {
243
244
  }
244
245
  continue;
245
246
  }
246
- const merged = mergedSelect(key, def, promptIv, false);
247
- const uiInitial = merged ??
248
- (def.initialValue && valueList.includes(def.initialValue) ? def.initialValue : undefined) ??
249
- valueList[0];
247
+ const merged = mergedSelect(key, def, promptIv, false, valuesSoFar);
248
+ const uiInitial = merged ?? valueList[0];
250
249
  if (uiInitial === undefined || !valueList.includes(uiInitial)) {
251
250
  const hint = def.required
252
251
  ? t('promptCatalog.nonInteractive.selectRequiredInteractive', { key })
@@ -34,6 +34,13 @@ function resolveTextDefault(def, out) {
34
34
  }
35
35
  return String(iv ?? '');
36
36
  }
37
+ function resolveSelectDefault(def, out) {
38
+ const iv = def.initialValue;
39
+ if (typeof iv === 'function') {
40
+ return iv(out);
41
+ }
42
+ return iv;
43
+ }
37
44
  function resolvePasswordDefault(def, out) {
38
45
  const iv = def.initialValue;
39
46
  if (typeof iv === 'function') {
@@ -98,7 +105,7 @@ function defaultValueForInput(key, def, out) {
98
105
  const first = def.options
99
106
  .find((o) => typeof o === 'string' || o.disabled !== true);
100
107
  const firstValue = typeof first === 'string' ? first : first?.value;
101
- const i = def.initialValue;
108
+ const i = resolveSelectDefault(def, out);
102
109
  const enabledValues = def.options
103
110
  .filter((o) => typeof o === 'string' || o.disabled !== true)
104
111
  .map((o) => (typeof o === 'string' ? o : o.value));
@@ -765,11 +772,10 @@ function runPromptCatalogWebUIImpl(options) {
765
772
  }
766
773
  };
767
774
  const servePage = (port) => {
768
- const base = `http://${publicHost}:${port}`;
769
775
  const formInner = buildPwcFormHtml(catalog, formDefaults, initialShow, pwcStepDefs, 0, pwcNSteps, locale, uiText);
770
776
  const wizardClientJson = JSON.stringify({ n: pwcNSteps, stepDefs: pwcStepDefs });
771
- const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(base + resolveValidateStepPath) : 'null';
772
- const pwcValFieldUrl = JSON.stringify(base + resolveValidateFieldPath);
777
+ const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(resolveValidateStepPath) : 'null';
778
+ const pwcValFieldUrl = JSON.stringify(resolveValidateFieldPath);
773
779
  const uiTextJson = JSON.stringify(uiText);
774
780
  const pwcShellClass = options.stages && options.stages.length > 0
775
781
  ? 'pwc-shell pwc-shell--stages'
@@ -1493,8 +1499,8 @@ function runPromptCatalogWebUIImpl(options) {
1493
1499
  </div>
1494
1500
  <script>
1495
1501
  (function () {
1496
- var sub = ${JSON.stringify(base + submitPath)};
1497
- var ref = ${JSON.stringify(base + reflowPath)};
1502
+ var sub = ${JSON.stringify(submitPath)};
1503
+ var ref = ${JSON.stringify(reflowPath)};
1498
1504
  var pwcValStep = ${pwcValStepUrl};
1499
1505
  var pwcValField = ${pwcValFieldUrl};
1500
1506
  var pwcStepMeta = ${JSON.stringify(PWC_FORM_META_STEP)};
@@ -79,6 +79,7 @@ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeCon
79
79
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
80
80
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
81
81
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
82
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
82
83
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
83
84
  ]);
84
85
  return {
@@ -100,6 +101,7 @@ export async function writeManualCaddyProxyBundle(input, appEntryOptions, runtim
100
101
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
101
102
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
102
103
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
104
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
103
105
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
104
106
  ]);
105
107
  return {
@@ -107,6 +107,7 @@ async function writeResolvedNginxProxyBundle(bundle, appEntryOptions, options) {
107
107
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
108
108
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
109
109
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
110
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
110
111
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
111
112
  syncEnvProxyNginxSnippets(),
112
113
  ]);
@@ -48,6 +48,10 @@ const MISSING_COMMAND_SPECS = {
48
48
  displayName: 'pnpm',
49
49
  configKey: 'bin.pnpm',
50
50
  },
51
+ npm: {
52
+ displayName: 'npm',
53
+ configKey: 'bin.npm',
54
+ },
51
55
  };
52
56
  const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
53
57
  /cannot connect to the docker daemon/i,
@@ -61,19 +65,92 @@ async function resolveCommandName(name) {
61
65
  function shouldTeeInheritedOutput(options) {
62
66
  return options?.stdio === 'inherit' && Boolean(String(process.env.NB_CLI_ACTIVE_LOG_FILE ?? '').trim());
63
67
  }
64
- function createMissingCommandError(name, label, error) {
65
- const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
66
- if (code !== 'ENOENT') {
67
- return undefined;
68
+ function buildProcessEnv(options) {
69
+ if (options?.envMode === 'replace') {
70
+ return options.env ?? {};
68
71
  }
72
+ return {
73
+ ...process.env,
74
+ ...options?.env,
75
+ };
76
+ }
77
+ function createMissingCommandError(name, label, error) {
69
78
  if (!Object.prototype.hasOwnProperty.call(MISSING_COMMAND_SPECS, name)) {
70
79
  return undefined;
71
80
  }
72
81
  const spec = MISSING_COMMAND_SPECS[name];
82
+ if (!isMissingCommandError(name, spec.displayName, error)) {
83
+ return undefined;
84
+ }
73
85
  return new Error(translateCli('commands.shared.missingCommand', { action: label, displayName: spec.displayName, configKey: spec.configKey }, {
74
86
  fallback: `Couldn't run \`${label}\` because the ${spec.displayName} executable could not be found. Install ${spec.displayName} or update \`nb config set ${spec.configKey} <path>\` and try again.`,
75
87
  }));
76
88
  }
89
+ function isMissingCommandError(name, displayName, error) {
90
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
91
+ if (code === 'ENOENT') {
92
+ return true;
93
+ }
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ const lowerMessage = message.toLowerCase();
96
+ return (lowerMessage.includes(`spawn ${name.toLowerCase()} enoent`) ||
97
+ lowerMessage.includes(`${name.toLowerCase()} executable could not be found`) ||
98
+ lowerMessage.includes(`${displayName.toLowerCase()} executable could not be found`));
99
+ }
100
+ export async function runPnpmCommand(runCommand, args, options) {
101
+ try {
102
+ await runCommand('pnpm', args, options);
103
+ }
104
+ catch (error) {
105
+ throw createMissingCommandError('pnpm', options.errorName ?? `pnpm ${args.join(' ')}`.trim(), error) ?? error;
106
+ }
107
+ }
108
+ function createInstallArgsWithoutTrustLockfile(args) {
109
+ if (!args.includes('install') || !args.includes('--trust-lockfile')) {
110
+ return undefined;
111
+ }
112
+ return args.filter((arg) => arg !== '--trust-lockfile');
113
+ }
114
+ function isFriendlyMissingPnpmError(error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ return message.includes('because the pnpm executable could not be found');
117
+ }
118
+ function formatPnpmLabel(args) {
119
+ return `pnpm ${args.join(' ')}`.trim();
120
+ }
121
+ export async function resolvePnpmInstallCommand(cwd) {
122
+ const hasLockfile = await fsp
123
+ .stat(path.join(cwd, 'pnpm-lock.yaml'))
124
+ .then((stats) => stats.isFile())
125
+ .catch(() => false);
126
+ if (hasLockfile) {
127
+ const args = ['install', '--frozen-lockfile', '--trust-lockfile'];
128
+ return {
129
+ args,
130
+ errorName: formatPnpmLabel(args),
131
+ };
132
+ }
133
+ const args = ['install'];
134
+ return {
135
+ args,
136
+ errorName: formatPnpmLabel(args),
137
+ };
138
+ }
139
+ export async function runPnpmInstallCommand(runCommand, args, options) {
140
+ try {
141
+ await runPnpmCommand(runCommand, args, options);
142
+ }
143
+ catch (error) {
144
+ const fallbackArgs = createInstallArgsWithoutTrustLockfile(args);
145
+ if (!fallbackArgs || isFriendlyMissingPnpmError(error)) {
146
+ throw error;
147
+ }
148
+ await runPnpmCommand(runCommand, fallbackArgs, {
149
+ ...options,
150
+ errorName: formatPnpmLabel(fallbackArgs),
151
+ });
152
+ }
153
+ }
77
154
  function isDockerDaemonUnavailableError(error) {
78
155
  const message = error instanceof Error ? error.message : String(error);
79
156
  return DOCKER_DAEMON_UNAVAILABLE_PATTERNS.some((pattern) => pattern.test(message));
@@ -146,10 +223,7 @@ export async function run(name, args, options) {
146
223
  const child = spawn(command, [...args], {
147
224
  stdio,
148
225
  cwd,
149
- env: {
150
- ...process.env,
151
- ...options?.env,
152
- },
226
+ env: buildProcessEnv(options),
153
227
  windowsHide: process.platform === 'win32',
154
228
  });
155
229
  if (options?.stdio === 'pipe' || shouldTeeInheritedOutput(options)) {
@@ -268,10 +342,7 @@ export async function commandSucceeds(name, args, options) {
268
342
  return await new Promise((resolve, reject) => {
269
343
  const child = spawn(command, [...args], {
270
344
  cwd,
271
- env: {
272
- ...process.env,
273
- ...options?.env,
274
- },
345
+ env: buildProcessEnv(options),
275
346
  stdio: 'ignore',
276
347
  windowsHide: process.platform === 'win32',
277
348
  });
@@ -302,10 +373,7 @@ export async function commandOutput(name, args, options) {
302
373
  return await new Promise((resolve, reject) => {
303
374
  const child = spawn(command, [...args], {
304
375
  cwd,
305
- env: {
306
- ...process.env,
307
- ...options?.env,
308
- },
376
+ env: buildProcessEnv(options),
309
377
  stdio: ['ignore', 'pipe', 'pipe'],
310
378
  windowsHide: process.platform === 'win32',
311
379
  });
@@ -364,10 +432,7 @@ export async function commandOutputViaFile(name, args, options) {
364
432
  const result = await new Promise((resolve, reject) => {
365
433
  const child = spawn(command, [...args], {
366
434
  cwd,
367
- env: {
368
- ...process.env,
369
- ...options?.env,
370
- },
435
+ env: buildProcessEnv(options),
371
436
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
372
437
  windowsHide: process.platform === 'win32',
373
438
  });
@@ -0,0 +1,52 @@
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 { Flags } from '@oclif/core';
10
+ import { executeRawApiRequest } from './api-client.js';
11
+ import { ensureCrossEnvConfirmed } from './env-guard.js';
12
+ export const swaggerRequestFlags = {
13
+ env: Flags.string({
14
+ char: 'e',
15
+ description: 'CLI env name; omitted uses the current env',
16
+ }),
17
+ yes: Flags.boolean({
18
+ char: 'y',
19
+ description: 'Confirm using --env when it targets a different env than the current env',
20
+ default: false,
21
+ }),
22
+ 'api-base-url': Flags.string({
23
+ description: 'NocoBase API base URL, for example http://localhost:13000/api',
24
+ }),
25
+ role: Flags.string({
26
+ description: 'Role override, sent as X-Role',
27
+ }),
28
+ token: Flags.string({
29
+ char: 't',
30
+ description: 'API key or access token override',
31
+ }),
32
+ };
33
+ export async function executeSwaggerRequest(command, flags, path, query) {
34
+ const requestedEnv = flags.env?.trim() || undefined;
35
+ const confirmed = await ensureCrossEnvConfirmed({
36
+ command,
37
+ requestedEnv,
38
+ yes: flags.yes,
39
+ });
40
+ if (!confirmed) {
41
+ return undefined;
42
+ }
43
+ return executeRawApiRequest({
44
+ envName: requestedEnv,
45
+ baseUrl: flags['api-base-url'],
46
+ token: flags.token,
47
+ role: flags.role,
48
+ method: 'GET',
49
+ path,
50
+ query,
51
+ });
52
+ }
package/dist/lib/ui.js CHANGED
@@ -15,8 +15,35 @@ let verboseMode = false;
15
15
  let lastStaticTaskMessage;
16
16
  let lastStaticTaskAt = 0;
17
17
  const STATIC_TASK_UPDATE_THROTTLE_MS = 3_000;
18
+ function isCombiningCodePoint(codePoint) {
19
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
20
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
21
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
22
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
23
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
24
+ }
25
+ function isFullWidthCodePoint(codePoint) {
26
+ return (codePoint >= 0x1100 &&
27
+ (codePoint <= 0x115f ||
28
+ codePoint === 0x2329 ||
29
+ codePoint === 0x232a ||
30
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
31
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
32
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
33
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
34
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
35
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
36
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
37
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
38
+ }
18
39
  function stringWidth(value) {
19
- return Array.from(value).length;
40
+ return Array.from(value).reduce((width, character) => {
41
+ const codePoint = character.codePointAt(0);
42
+ if (!codePoint || codePoint === 0 || codePoint < 32 || isCombiningCodePoint(codePoint)) {
43
+ return width;
44
+ }
45
+ return width + (isFullWidthCodePoint(codePoint) ? 2 : 1);
46
+ }, 0);
20
47
  }
21
48
  function pad(value, width) {
22
49
  const padding = Math.max(0, width - stringWidth(value));
@@ -89,6 +89,9 @@
89
89
  "lowerCaseTableNamesRequiresUnderscored": "MySQL lower_case_table_names=1 requires DB_UNDERSCORED=true."
90
90
  }
91
91
  },
92
+ "apiClient": {
93
+ "authRequiredHint": "Authentication failed or the saved session has expired. Run `{{command}}` to sign in again."
94
+ },
92
95
  "commands": {
93
96
  "envAdd": {
94
97
  "prompts": {
@@ -168,6 +171,204 @@
168
171
  "refusal": "Refusing to run against env \"{{requestedEnv}}\" because the current env is \"{{currentEnv}}\" and interactive confirmation is unavailable in the current agent session.\n\nFor safety, the agent will not switch envs automatically and will not add --yes on your behalf.\n\nTo continue:\n- run `nb env use {{requestedEnv}}` yourself and then re-run the command, or\n- re-run the same command with `--env {{requestedEnv}} --yes` to confirm this one-off cross-env operation."
169
172
  }
170
173
  },
174
+ "portalCreate": {
175
+ "errors": {
176
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
177
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
178
+ "templateInvalidDirectory": "Portal template \"{{source}}\" is invalid: expected a directory.",
179
+ "localTemplateMissing": "Portal template directory does not exist: {{source}}",
180
+ "templateUnresolved": "Portal template \"{{source}}\" could not be resolved. {{details}}",
181
+ "templateDownloadFailed": "Failed to download portal template \"{{source}}\" with npm pack. {{details}}",
182
+ "templateExtractFailed": "Failed to extract portal template \"{{source}}\": {{details}}",
183
+ "templateMissingPackageJson": "Portal template \"{{source}}\" is invalid: package.json is missing.",
184
+ "npmPackNoTarball": "npm pack did not produce a local tarball for {{source}}.",
185
+ "npmPackMultipleTarballs": "npm pack produced multiple tarballs for {{source}}.",
186
+ "invalidPortalName": "Invalid portal name \"{{value}}\". Use lowercase letters, numbers, underscores, or hyphens, and start with a lowercase letter or number.",
187
+ "invalidPortalAppName": "Invalid portal app name \"{{value}}\" from apiBaseUrl. Use letters, numbers, underscores, or hyphens, and start with a letter or number.",
188
+ "missingApiBaseUrl": "Cannot create a portal because the selected env has no apiBaseUrl.",
189
+ "outsideParent": "Refusing to modify a portal outside {{parentDir}}: {{portalDir}}",
190
+ "sshUnsupported": "Cannot create a portal for ssh envs in the first version.",
191
+ "workspaceExists": "Portal already exists: {{portalDir}}\nPass --force to delete it and create a new portal.",
192
+ "workspaceNotReplaceable": "Refusing to replace a non-portal directory: {{portalDir}}\nThe target directory must be empty or contain package.json with a nocobase field."
193
+ },
194
+ "messages": {
195
+ "skipInstall": "Skipped pnpm install because package.json was not found in {{portalDir}}.",
196
+ "created": "Portal \"{{portal}}\" created at {{portalDir}}",
197
+ "app": "App: {{app}}",
198
+ "base": "Base: {{base}}",
199
+ "sourceStorage": "Source storage: {{sourceStorage}}",
200
+ "installFailed": "Dependency installation did not finish successfully. Run `pnpm install` manually in {{portalDir}}."
201
+ }
202
+ },
203
+ "portalConfig": {
204
+ "errors": {
205
+ "invalidSourceStorage": "Invalid source storage \"{{value}}\". Use \"nocobase\" or \"git\".",
206
+ "invalidGitPath": "--git-path must be a relative path inside the Git repository.",
207
+ "gitOptionsForNocobaseStorage": "--git-repo, --git-branch, and --git-path can only be used with --source-storage git.",
208
+ "gitRepoRequired": "--git-repo is required when --source-storage is git.",
209
+ "gitRepoInvalid": "--git-repo must be a full Git remote URL.",
210
+ "updateFailed": "Portal config update failed with status {{status}}\n{{details}}"
211
+ }
212
+ },
213
+ "portalConfigure": {
214
+ "errors": {
215
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
216
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
217
+ "noChanges": "No portal configuration changes were provided. Pass --path, --source-storage, or a --git-* flag.",
218
+ "notFound": "Portal \"{{portal}}\" was not found. Run `nb portal list` to see available portals."
219
+ },
220
+ "messages": {
221
+ "updated": "Portal \"{{portal}}\" configuration updated.",
222
+ "pathUpdated": "Development path: {{portalDir}}",
223
+ "remoteSynced": "Remote portal record: synced",
224
+ "remoteSkipped": "Remote portal record: not found; local config only"
225
+ }
226
+ },
227
+ "portalDeploy": {
228
+ "errors": {
229
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
230
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
231
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
232
+ "packageJsonMissing": "Portal is invalid: package.json is missing in {{portalDir}}.",
233
+ "distMissing": "Portal build did not produce {{distDir}}/index.html.",
234
+ "unsupportedEnvKind": "Cannot deploy a portal for {{kind}} envs in the first version.",
235
+ "uploadFailed": "Portal dist upload failed with status {{status}}\n{{details}}",
236
+ "recordSyncFailed": "Portal record sync failed with status {{status}}\n{{details}}"
237
+ },
238
+ "messages": {
239
+ "deployed": "Portal \"{{portal}}\" deployed.",
240
+ "mode": "Mode: {{mode}}",
241
+ "app": "App: {{app}}",
242
+ "base": "Base: {{base}}",
243
+ "record": "Record: synced",
244
+ "dist": "Dist: {{dist}}",
245
+ "localDist": "Local dist: {{dist}}",
246
+ "serverDist": "Server dist: {{dist}}"
247
+ }
248
+ },
249
+ "portalList": {
250
+ "errors": {
251
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
252
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
253
+ "unsupportedEnvKind": "Cannot list portals for {{kind}} envs in the first version.",
254
+ "listFailed": "Portal list failed with status {{status}}\n{{details}}"
255
+ },
256
+ "messages": {
257
+ "empty": "No portal records found."
258
+ },
259
+ "table": {
260
+ "name": "Name",
261
+ "url": "URL",
262
+ "portalType": "Portal type",
263
+ "path": "Development path",
264
+ "enabled": "Enabled",
265
+ "default": "Default"
266
+ }
267
+ },
268
+ "portalPull": {
269
+ "errors": {
270
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
271
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
272
+ "gitRepoRequiredForTemporaryPull": "--git-branch and --git-path require --git-repo for a temporary Git pull. To update the portal configuration, use `nb portal config`."
273
+ },
274
+ "messages": {
275
+ "noop": "No pull is needed.",
276
+ "pulled": "Pulled portal source \"{{portal}}\" into {{portalDir}}",
277
+ "installFailed": "Dependency installation did not finish successfully. Run `pnpm install` manually in {{portalDir}}."
278
+ }
279
+ },
280
+ "portalSource": {
281
+ "errors": {
282
+ "workspaceNotReplaceable": "Refusing to replace a non-portal directory: {{portalDir}}\nThe target directory must be empty or contain package.json with a nocobase field."
283
+ }
284
+ },
285
+ "swagger": {
286
+ "errors": {
287
+ "pluginDisabled": "The API documentation plugin is not enabled. Enable it before requesting Swagger documents.",
288
+ "requestFailed": "Swagger request failed with status {{status}}\n{{details}}",
289
+ "invalidDestinations": "swagger:getUrls returned an invalid destination list.",
290
+ "invalidDocument": "swagger:get returned an invalid OpenAPI document."
291
+ },
292
+ "messages": {
293
+ "empty": "No Swagger document namespaces are available.",
294
+ "saved": "Saved Swagger document to {{output}}."
295
+ },
296
+ "table": {
297
+ "name": "Name",
298
+ "namespace": "Namespace",
299
+ "url": "URL",
300
+ "field": "Field",
301
+ "value": "Value"
302
+ },
303
+ "fields": {
304
+ "namespace": "Namespace",
305
+ "title": "Title",
306
+ "version": "Version",
307
+ "openapi": "OpenAPI",
308
+ "paths": "Paths"
309
+ }
310
+ },
311
+ "portalInfo": {
312
+ "errors": {
313
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
314
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
315
+ "notFound": "Portal \"{{portal}}\" was not found."
316
+ },
317
+ "fields": {
318
+ "name": "Name",
319
+ "url": "URL",
320
+ "portalType": "Portal type",
321
+ "developmentPath": "Development path",
322
+ "deploymentPath": "Deployment path",
323
+ "enabled": "Enabled"
324
+ }
325
+ },
326
+ "portalDestroy": {
327
+ "errors": {
328
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
329
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
330
+ "confirmationRequired": "Refusing to destroy a portal in non-interactive mode without --yes.",
331
+ "outsideParent": "Refusing to delete a portal outside {{parentDir}}: {{portalDir}}",
332
+ "workspaceMissing": "Portal deployment path does not exist: {{portalDir}}\nPass --force to ignore missing deployment files.",
333
+ "unsupportedEnvKind": "Cannot destroy a portal for {{kind}} envs in the first version.",
334
+ "unsafeDevelopmentPath": "Refusing to delete an unsafe portal development path: {{portalDir}}",
335
+ "recordDestroyFailed": "Portal record destroy failed with status {{status}}\n{{details}}"
336
+ },
337
+ "prompts": {
338
+ "confirm": "Destroy portal \"{{portal}}\" and delete its deployment directory?"
339
+ },
340
+ "statuses": {
341
+ "deleted": "deleted",
342
+ "missing": "missing",
343
+ "retained": "retained"
344
+ },
345
+ "messages": {
346
+ "destroyed": "Portal \"{{portal}}\" destroyed.",
347
+ "mode": "Mode: {{mode}}",
348
+ "app": "App: {{app}}",
349
+ "base": "Base: {{base}}",
350
+ "record": "Record: {{status}}",
351
+ "deploymentPath": "Deployment path: {{status}} ({{dir}})",
352
+ "developmentPath": "Development path: {{status}} ({{dir}})",
353
+ "developmentPathMissing": "Development path: missing"
354
+ }
355
+ },
356
+ "portalDev": {
357
+ "errors": {
358
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
359
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
360
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
361
+ "packageJsonMissing": "Portal is invalid: package.json is missing in {{portalDir}}.",
362
+ "sshUnsupported": "Cannot start a portal in dev mode for ssh envs in the first version."
363
+ },
364
+ "messages": {
365
+ "starting": "Starting portal \"{{portal}}\"...",
366
+ "mode": "Mode: {{mode}}",
367
+ "app": "App: {{app}}",
368
+ "base": "Base: {{base}}",
369
+ "dir": "Dir: {{dir}}"
370
+ }
371
+ },
171
372
  "license": {
172
373
  "activate": {
173
374
  "interactive": {
@@ -267,7 +468,7 @@
267
468
  "betaLabel": "beta",
268
469
  "betaHint": "Preview release. Good for trying upcoming features before general release.",
269
470
  "alphaLabel": "alpha",
270
- "alphaHint": "Development release. Includes the newest changes, but may be incomplete or unstable.",
471
+ "alphaHint": "Choose this version to try the new features in 3.0.",
271
472
  "otherLabel": "Other",
272
473
  "otherHint": "Enter another package version, Docker tag, or Git ref manually, such as a branch name."
273
474
  },
@@ -341,6 +542,21 @@
341
542
  "message": "App subpath (for example, /nocobase/)",
342
543
  "placeholder": "/ or /nocobase/"
343
544
  },
545
+ "portalType": {
546
+ "message": "Portal type",
547
+ "noCodeLabel": "No-code portal",
548
+ "noCodeHint": "Create with visual configuration. AI can help adjust the configuration. Path: /v/<name>",
549
+ "aiLabel": "AI portal",
550
+ "aiHint": "Create with AI Agent and code. Users can request changes in natural language. Path: /x/<name>"
551
+ },
552
+ "portalName": {
553
+ "message": "Portal name",
554
+ "placeholder": "admin"
555
+ },
556
+ "portalTemplate": {
557
+ "message": "Starter template (copied to ./storage/portals/)",
558
+ "placeholder": "git@github.com:nocobase/admin-starter.git"
559
+ },
344
560
  "storagePath": {
345
561
  "message": "Uploads and local files directory",
346
562
  "placeholder": "./<env>/storage/"
@@ -440,6 +656,30 @@
440
656
  },
441
657
  "skipDownload": {
442
658
  "message": "Skip downloading NocoBase and reuse existing local app files or Docker images"
659
+ },
660
+ "appClientEntryMode": {
661
+ "message": "App client entry mode",
662
+ "modernOnlyLabel": "Modern UI only",
663
+ "modernOnlyHint": "Only the modern UI is available; the legacy UI cannot be accessed.",
664
+ "modernDefaultLabel": "Modern UI by default",
665
+ "modernDefaultHint": "Open the modern UI by default while keeping the legacy UI available.",
666
+ "legacyDefaultLabel": "Legacy UI by default",
667
+ "legacyDefaultHint": "Open the legacy UI by default while keeping the modern UI available."
668
+ },
669
+ "portalType": {
670
+ "message": "Portal type",
671
+ "noCodeLabel": "No-code portal",
672
+ "noCodeHint": "Create with visual configuration. AI can help adjust the configuration. Path: /v/<name>",
673
+ "aiLabel": "AI portal",
674
+ "aiHint": "Create with AI Agent and code. Users can request changes in natural language. Path: /x/<name>"
675
+ },
676
+ "portalName": {
677
+ "message": "Portal name",
678
+ "placeholder": "admin"
679
+ },
680
+ "portalTemplate": {
681
+ "message": "Starter template (copied to ./storage/portals/)",
682
+ "placeholder": "git@github.com:nocobase/admin-starter.git"
443
683
  }
444
684
  },
445
685
  "webUi": {
@@ -462,6 +702,10 @@
462
702
  "title": "App source and version",
463
703
  "description": "Choose how to get the app and which source and version to use."
464
704
  },
705
+ "portalType": {
706
+ "title": "Configure portal",
707
+ "description": "Set the default portal name and type."
708
+ },
465
709
  "configureDatabase": {
466
710
  "title": "Configure the database",
467
711
  "description": "Use built-in or custom."