@nocobase/cli 3.0.0-alpha.1 → 3.0.0-alpha.12

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 (52) hide show
  1. package/dist/commands/api/resource/create.js +11 -2
  2. package/dist/commands/api/swagger/get.js +96 -0
  3. package/dist/commands/api/swagger/index.js +20 -0
  4. package/dist/commands/api/swagger/list.js +92 -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 +38 -0
  8. package/dist/commands/install.js +18 -62
  9. package/dist/commands/portal/config.js +16 -5
  10. package/dist/commands/portal/create.js +17 -26
  11. package/dist/commands/portal/destroy.js +28 -6
  12. package/dist/commands/portal/dev.js +2 -0
  13. package/dist/commands/portal/list.js +5 -5
  14. package/dist/commands/portal/pull.js +32 -3
  15. package/dist/lib/api-client.js +35 -9
  16. package/dist/lib/app-client-entry-mode.js +28 -0
  17. package/dist/lib/app-managed-resources.js +2 -0
  18. package/dist/lib/auth-store.js +52 -1
  19. package/dist/lib/bootstrap.js +3 -1
  20. package/dist/lib/browser.js +29 -0
  21. package/dist/lib/env-auth.js +2 -36
  22. package/dist/lib/env-command-config.js +1 -0
  23. package/dist/lib/env-config.js +10 -2
  24. package/dist/lib/env-portal-config.js +30 -0
  25. package/dist/lib/env-proxy.js +25 -4
  26. package/dist/lib/generated-command.js +81 -0
  27. package/dist/lib/managed-env-file.js +67 -13
  28. package/dist/lib/managed-init-env.js +0 -2
  29. package/dist/lib/plugin-import.js +30 -8
  30. package/dist/lib/portal-build-html.js +27 -0
  31. package/dist/lib/portal-config.js +6 -20
  32. package/dist/lib/portal-configure.js +49 -56
  33. package/dist/lib/portal-create.js +96 -14
  34. package/dist/lib/portal-deploy.js +30 -47
  35. package/dist/lib/portal-destroy.js +34 -20
  36. package/dist/lib/portal-dev.js +6 -7
  37. package/dist/lib/portal-env-files.js +2 -1
  38. package/dist/lib/portal-info.js +2 -5
  39. package/dist/lib/portal-list.js +20 -26
  40. package/dist/lib/portal-path-safety.js +76 -0
  41. package/dist/lib/portal-source.js +227 -58
  42. package/dist/lib/prompt-catalog-core.js +2 -2
  43. package/dist/lib/prompt-catalog-terminal.js +4 -5
  44. package/dist/lib/prompt-web-ui.js +12 -6
  45. package/dist/lib/resource-command.js +18 -2
  46. package/dist/lib/resource-request.js +8 -0
  47. package/dist/lib/run-npm.js +68 -4
  48. package/dist/lib/runtime-generator.js +28 -1
  49. package/dist/lib/swagger-command.js +52 -0
  50. package/dist/locale/en-US.json +81 -15
  51. package/dist/locale/zh-CN.json +81 -15
  52. package/package.json +2 -2
@@ -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)};
@@ -41,6 +41,22 @@ function parseObjectFlag(value, flagName) {
41
41
  }
42
42
  return parsed;
43
43
  }
44
+ function parseValuesFlag(value, flagName) {
45
+ if (value === undefined) {
46
+ return undefined;
47
+ }
48
+ const parsed = parseJson(value, flagName);
49
+ if (!parsed || typeof parsed !== 'object') {
50
+ throw new Error(`--${flagName} must be a JSON object, or a JSON array of objects to create multiple records`);
51
+ }
52
+ if (Array.isArray(parsed)) {
53
+ const invalidIndex = parsed.findIndex((item) => !item || Array.isArray(item) || typeof item !== 'object');
54
+ if (invalidIndex !== -1) {
55
+ throw new Error(`--${flagName} array items must all be JSON objects, but item ${invalidIndex} is not`);
56
+ }
57
+ }
58
+ return parsed;
59
+ }
44
60
  function parseJsonArrayFlag(value, flagName) {
45
61
  if (value === undefined) {
46
62
  return undefined;
@@ -187,7 +203,7 @@ export const createFlags = {
187
203
  ...resourceBaseFlags,
188
204
  ...resourceAssociationFlags,
189
205
  values: Flags.string({
190
- description: 'Record values used by create as a JSON object.',
206
+ description: 'Record values used by create as a JSON object, or a JSON array of objects to create multiple records in one request.',
191
207
  required: true,
192
208
  }),
193
209
  whitelist: Flags.string({
@@ -298,7 +314,7 @@ export function buildGetArgs(flags) {
298
314
  export function buildCreateArgs(flags) {
299
315
  return {
300
316
  ...pickSharedArgs(flags),
301
- values: parseObjectFlag(flags.values, 'values'),
317
+ values: parseValuesFlag(flags.values, 'values'),
302
318
  whitelist: parseStringArrayFlags(flags.whitelist, 'whitelist'),
303
319
  blacklist: parseStringArrayFlags(flags.blacklist, 'blacklist'),
304
320
  };
@@ -1,3 +1,11 @@
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
+ */
1
9
  import { executeRawApiRequest } from './api-client.js';
2
10
  function buildActionUrl(resource, action, sourceId) {
3
11
  if (typeof sourceId === 'undefined' || sourceId === null || !resource.includes('.')) {
@@ -75,18 +75,82 @@ function buildProcessEnv(options) {
75
75
  };
76
76
  }
77
77
  function createMissingCommandError(name, label, error) {
78
- const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
79
- if (code !== 'ENOENT') {
80
- return undefined;
81
- }
82
78
  if (!Object.prototype.hasOwnProperty.call(MISSING_COMMAND_SPECS, name)) {
83
79
  return undefined;
84
80
  }
85
81
  const spec = MISSING_COMMAND_SPECS[name];
82
+ if (!isMissingCommandError(name, spec.displayName, error)) {
83
+ return undefined;
84
+ }
86
85
  return new Error(translateCli('commands.shared.missingCommand', { action: label, displayName: spec.displayName, configKey: spec.configKey }, {
87
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.`,
88
87
  }));
89
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
+ }
90
154
  function isDockerDaemonUnavailableError(error) {
91
155
  const message = error instanceof Error ? error.message : String(error);
92
156
  return DOCKER_DAEMON_UNAVAILABLE_PATTERNS.some((pattern) => pattern.test(message));
@@ -18,7 +18,31 @@ import { createHash } from 'node:crypto';
18
18
  import { loadBuildConfig } from './build-config.js';
19
19
  import { toKebabCase, toLogicalActionName, toLogicalResourceName, toResourceSegments } from './naming.js';
20
20
  import { collectOperations } from './openapi.js';
21
- const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file', 'yes']);
21
+ const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file', 'ui', 'yes']);
22
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
23
+ const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string' && Boolean(item));
24
+ function getGeneratedUIOperation(operation, parameters) {
25
+ if (!operation.operationId) {
26
+ return undefined;
27
+ }
28
+ const extension = operation['x-nocobase-cli-ui'];
29
+ if (!isRecord(extension) || typeof extension.path !== 'string') {
30
+ return undefined;
31
+ }
32
+ const path = extension.path.trim();
33
+ if (!path || path.startsWith('/') || /[?#]/.test(path) || /^[a-z][a-z\d+.-]*:/i.test(path)) {
34
+ return undefined;
35
+ }
36
+ const mappedParameters = extension.parameters === undefined ? [] : extension.parameters;
37
+ if (!isStringArray(mappedParameters) || new Set(mappedParameters).size !== mappedParameters.length) {
38
+ return undefined;
39
+ }
40
+ const allowedParameters = new Set(parameters.map((parameter) => parameter.name));
41
+ if (!mappedParameters.every((parameter) => allowedParameters.has(parameter))) {
42
+ return undefined;
43
+ }
44
+ return { path, parameters: mappedParameters };
45
+ }
22
46
  function matchesPattern(value, pattern) {
23
47
  if (!value) {
24
48
  return false;
@@ -425,6 +449,7 @@ export async function generateRuntime(document, configFile, baseUrl) {
425
449
  const parameters = (operation.parameters ?? []).filter(isSupportedParameter).map((parameter) => toGeneratedParameter(parameter, usedFlagNames));
426
450
  const bodyParameters = extractBodyParameters(operation.requestBody, usedFlagNames);
427
451
  const allParameters = [...parameters, ...bodyParameters];
452
+ const ui = getGeneratedUIOperation(operation, allParameters);
428
453
  const hasBody = Boolean(operation.requestBody && !('$ref' in operation.requestBody));
429
454
  const requestContentType = getRequestContentType(operation.requestBody);
430
455
  const responseType = getResponseType(operation);
@@ -456,6 +481,8 @@ export async function generateRuntime(document, configFile, baseUrl) {
456
481
  resourceDisplayName,
457
482
  resourceDescription,
458
483
  commandId: segments.join(' '),
484
+ operationId: operation.operationId,
485
+ ui,
459
486
  method,
460
487
  pathTemplate,
461
488
  tags: operation.tags,
@@ -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
+ }
@@ -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": {
@@ -185,14 +188,16 @@
185
188
  "missingApiBaseUrl": "Cannot create a portal because the selected env has no apiBaseUrl.",
186
189
  "outsideParent": "Refusing to modify a portal outside {{parentDir}}: {{portalDir}}",
187
190
  "sshUnsupported": "Cannot create a portal for ssh envs in the first version.",
188
- "workspaceExists": "Portal already exists: {{portalDir}}\nPass --force to delete it and create a new portal."
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."
189
193
  },
190
194
  "messages": {
191
195
  "skipInstall": "Skipped pnpm install because package.json was not found in {{portalDir}}.",
192
- "created": "Portal \"{{portal}}\" created at {{portalDir}}.",
196
+ "created": "Portal \"{{portal}}\" created at {{portalDir}}",
193
197
  "app": "App: {{app}}",
194
198
  "base": "Base: {{base}}",
195
- "sourceStorage": "Source storage: {{sourceStorage}}"
199
+ "sourceStorage": "Source storage: {{sourceStorage}}",
200
+ "installFailed": "Dependency installation did not finish successfully. Run `pnpm install` manually in {{portalDir}}."
196
201
  }
197
202
  },
198
203
  "portalConfig": {
@@ -209,11 +214,12 @@
209
214
  "errors": {
210
215
  "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
211
216
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
212
- "noChanges": "No portal configuration changes were provided. Pass --source-storage or a --git-* flag.",
213
- "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` or `nb portal pull {{portal}}` 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."
214
219
  },
215
220
  "messages": {
216
- "updated": "Portal \"{{portal}}\" configuration updated at {{portalDir}}/portal.config.json.",
221
+ "updated": "Portal \"{{portal}}\" configuration updated.",
222
+ "pathUpdated": "Development path: {{portalDir}}",
217
223
  "remoteSynced": "Remote portal record: synced",
218
224
  "remoteSkipped": "Remote portal record: not found; local config only"
219
225
  }
@@ -254,9 +260,52 @@
254
260
  "name": "Name",
255
261
  "url": "URL",
256
262
  "portalType": "Portal type",
257
- "path": "Local path",
263
+ "path": "Development path",
258
264
  "enabled": "Enabled",
259
- "localSynced": "Local synced"
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"
260
309
  }
261
310
  },
262
311
  "portalInfo": {
@@ -269,9 +318,9 @@
269
318
  "name": "Name",
270
319
  "url": "URL",
271
320
  "portalType": "Portal type",
272
- "path": "Local path",
273
- "enabled": "Enabled",
274
- "localSynced": "Local synced"
321
+ "developmentPath": "Development path",
322
+ "deploymentPath": "Deployment path",
323
+ "enabled": "Enabled"
275
324
  }
276
325
  },
277
326
  "portalDestroy": {
@@ -280,12 +329,18 @@
280
329
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
281
330
  "confirmationRequired": "Refusing to destroy a portal in non-interactive mode without --yes.",
282
331
  "outsideParent": "Refusing to delete a portal outside {{parentDir}}: {{portalDir}}",
283
- "workspaceMissing": "Portal does not exist: {{portalDir}}\nPass --force to ignore missing local files.",
332
+ "workspaceMissing": "Portal deployment path does not exist: {{portalDir}}\nPass --force to ignore missing deployment files.",
284
333
  "unsupportedEnvKind": "Cannot destroy a portal for {{kind}} envs in the first version.",
334
+ "unsafeDevelopmentPath": "Refusing to delete an unsafe portal development path: {{portalDir}}",
285
335
  "recordDestroyFailed": "Portal record destroy failed with status {{status}}\n{{details}}"
286
336
  },
287
337
  "prompts": {
288
- "confirm": "Destroy portal \"{{portal}}\" and delete its storage directory?"
338
+ "confirm": "Destroy portal \"{{portal}}\" and delete its deployment directory?"
339
+ },
340
+ "statuses": {
341
+ "deleted": "deleted",
342
+ "missing": "missing",
343
+ "retained": "retained"
289
344
  },
290
345
  "messages": {
291
346
  "destroyed": "Portal \"{{portal}}\" destroyed.",
@@ -293,7 +348,9 @@
293
348
  "app": "App: {{app}}",
294
349
  "base": "Base: {{base}}",
295
350
  "record": "Record: {{status}}",
296
- "workspace": "Portal files: {{status}} ({{dir}})"
351
+ "deploymentPath": "Deployment path: {{status}} ({{dir}})",
352
+ "developmentPath": "Development path: {{status}} ({{dir}})",
353
+ "developmentPathMissing": "Development path: missing"
297
354
  }
298
355
  },
299
356
  "portalDev": {
@@ -411,7 +468,7 @@
411
468
  "betaLabel": "beta",
412
469
  "betaHint": "Preview release. Good for trying upcoming features before general release.",
413
470
  "alphaLabel": "alpha",
414
- "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.",
415
472
  "otherLabel": "Other",
416
473
  "otherHint": "Enter another package version, Docker tag, or Git ref manually, such as a branch name."
417
474
  },
@@ -600,6 +657,15 @@
600
657
  "skipDownload": {
601
658
  "message": "Skip downloading NocoBase and reuse existing local app files or Docker images"
602
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
+ },
603
669
  "portalType": {
604
670
  "message": "Portal type",
605
671
  "noCodeLabel": "No-code portal",
@@ -89,6 +89,9 @@
89
89
  "lowerCaseTableNamesRequiresUnderscored": "当 MySQL 的 lower_case_table_names=1 时,必须设置 DB_UNDERSCORED=true。"
90
90
  }
91
91
  },
92
+ "apiClient": {
93
+ "authRequiredHint": "认证失败或已保存的登录状态已过期。请运行 `{{command}}` 重新认证。"
94
+ },
92
95
  "commands": {
93
96
  "envAdd": {
94
97
  "prompts": {
@@ -185,14 +188,16 @@
185
188
  "missingApiBaseUrl": "无法创建 Portal,因为当前 env 没有 apiBaseUrl。",
186
189
  "outsideParent": "拒绝修改 {{parentDir}} 之外的 Portal:{{portalDir}}",
187
190
  "sshUnsupported": "第一版暂不支持为 ssh env 创建 Portal。",
188
- "workspaceExists": "Portal 已存在:{{portalDir}}\n如需删除并重新创建,请追加 --force。"
191
+ "workspaceExists": "Portal 已存在:{{portalDir}}\n如需删除并重新创建,请追加 --force。",
192
+ "workspaceNotReplaceable": "拒绝替换非 Portal 目录:{{portalDir}}\n目标目录必须为空,或包含带 nocobase 字段的 package.json。"
189
193
  },
190
194
  "messages": {
191
195
  "skipInstall": "未找到 {{portalDir}}/package.json,已跳过 pnpm install。",
192
- "created": "Portal \"{{portal}}\" 已创建:{{portalDir}}",
196
+ "created": "Portal \"{{portal}}\" 已创建:{{portalDir}}",
193
197
  "app": "App:{{app}}",
194
198
  "base": "Base:{{base}}",
195
- "sourceStorage": "源码存储:{{sourceStorage}}"
199
+ "sourceStorage": "源码存储:{{sourceStorage}}",
200
+ "installFailed": "依赖安装没有成功完成。请在 {{portalDir}} 中手动运行 `pnpm install`。"
196
201
  }
197
202
  },
198
203
  "portalConfig": {
@@ -209,11 +214,12 @@
209
214
  "errors": {
210
215
  "envNotConfigured": "env \"{{envName}}\" 尚未配置。请先运行 `nb env add {{envName}} --api-base-url <url>`。",
211
216
  "noEnvConfigured": "还没有配置 NocoBase env。请先运行 `nb init --ui` 创建一个。",
212
- "noChanges": "没有提供 Portal 配置变更。请传入 --source-storage 或 --git-* 参数。",
213
- "workspaceMissing": "Portal 不存在:{{portalDir}}\n请先运行 `nb portal create {{portal}}` `nb portal pull {{portal}}`。"
217
+ "noChanges": "没有提供 Portal 配置变更。请传入 --path、--source-storage 或 --git-* 参数。",
218
+ "notFound": "Portal \"{{portal}}\" 不存在。请运行 `nb portal list` 查看可用 Portal。"
214
219
  },
215
220
  "messages": {
216
- "updated": "Portal \"{{portal}}\" 配置已更新:{{portalDir}}/portal.config.json。",
221
+ "updated": "Portal \"{{portal}}\" 配置已更新。",
222
+ "pathUpdated": "开发路径:{{portalDir}}",
217
223
  "remoteSynced": "远端 Portal 记录:已同步",
218
224
  "remoteSkipped": "远端 Portal 记录:未找到,仅更新本地配置"
219
225
  }
@@ -254,9 +260,52 @@
254
260
  "name": "名称",
255
261
  "url": "访问 URL",
256
262
  "portalType": "Portal 类型",
257
- "path": "本地路径",
263
+ "path": "开发路径",
258
264
  "enabled": "启用",
259
- "localSynced": "本地已同步"
265
+ "default": "默认"
266
+ }
267
+ },
268
+ "portalPull": {
269
+ "errors": {
270
+ "envNotConfigured": "env \"{{envName}}\" 尚未配置。请先运行 `nb env add {{envName}} --api-base-url <url>`。",
271
+ "noEnvConfigured": "还没有配置 NocoBase env。请先运行 `nb init --ui` 创建一个。",
272
+ "gitRepoRequiredForTemporaryPull": "--git-branch 和 --git-path 作为临时 Git pull 参数时必须同时提供 --git-repo。如需更新 Portal 配置,请使用 `nb portal config`。"
273
+ },
274
+ "messages": {
275
+ "noop": "无需执行 pull。",
276
+ "pulled": "Portal 源码 \"{{portal}}\" 已拉取到 {{portalDir}}",
277
+ "installFailed": "依赖安装没有成功完成。请在 {{portalDir}} 中手动运行 `pnpm install`。"
278
+ }
279
+ },
280
+ "portalSource": {
281
+ "errors": {
282
+ "workspaceNotReplaceable": "拒绝替换非 Portal 目录:{{portalDir}}\n目标目录必须为空,或包含带 nocobase 字段的 package.json。"
283
+ }
284
+ },
285
+ "swagger": {
286
+ "errors": {
287
+ "pluginDisabled": "API 文档插件尚未启用。请先启用该插件,再获取 Swagger 文档。",
288
+ "requestFailed": "Swagger 请求失败,状态码 {{status}}\n{{details}}",
289
+ "invalidDestinations": "swagger:getUrls 返回了无效的文档列表。",
290
+ "invalidDocument": "swagger:get 返回了无效的 OpenAPI 文档。"
291
+ },
292
+ "messages": {
293
+ "empty": "没有可用的 Swagger 文档命名空间。",
294
+ "saved": "Swagger 文档已保存到 {{output}}。"
295
+ },
296
+ "table": {
297
+ "name": "名称",
298
+ "namespace": "命名空间",
299
+ "url": "URL",
300
+ "field": "字段",
301
+ "value": "值"
302
+ },
303
+ "fields": {
304
+ "namespace": "命名空间",
305
+ "title": "标题",
306
+ "version": "版本",
307
+ "openapi": "OpenAPI",
308
+ "paths": "路径数"
260
309
  }
261
310
  },
262
311
  "portalInfo": {
@@ -269,9 +318,9 @@
269
318
  "name": "名称",
270
319
  "url": "访问 URL",
271
320
  "portalType": "Portal 类型",
272
- "path": "本地路径",
273
- "enabled": "启用",
274
- "localSynced": "本地已同步"
321
+ "developmentPath": "开发路径",
322
+ "deploymentPath": "部署路径",
323
+ "enabled": "启用"
275
324
  }
276
325
  },
277
326
  "portalDestroy": {
@@ -280,12 +329,18 @@
280
329
  "noEnvConfigured": "还没有配置 NocoBase env。请先运行 `nb init --ui` 创建一个。",
281
330
  "confirmationRequired": "非交互模式下拒绝删除 Portal。请追加 --yes 后重试。",
282
331
  "outsideParent": "拒绝删除 {{parentDir}} 之外的 Portal:{{portalDir}}",
283
- "workspaceMissing": "Portal 不存在:{{portalDir}}\n如需忽略本地文件缺失,请追加 --force。",
332
+ "workspaceMissing": "Portal 部署路径不存在:{{portalDir}}\n如需忽略部署文件缺失,请追加 --force。",
284
333
  "unsupportedEnvKind": "第一版暂不支持为 {{kind}} env 删除 Portal。",
334
+ "unsafeDevelopmentPath": "拒绝删除不安全的 Portal 开发路径:{{portalDir}}",
285
335
  "recordDestroyFailed": "Portal 记录删除失败,状态码 {{status}}\n{{details}}"
286
336
  },
287
337
  "prompts": {
288
- "confirm": "删除 Portal \"{{portal}}\" 并移除它的 storage 目录?"
338
+ "confirm": "删除 Portal \"{{portal}}\" 并移除它的部署目录?"
339
+ },
340
+ "statuses": {
341
+ "deleted": "已删除",
342
+ "missing": "缺失",
343
+ "retained": "已保留"
289
344
  },
290
345
  "messages": {
291
346
  "destroyed": "Portal \"{{portal}}\" 已删除。",
@@ -293,7 +348,9 @@
293
348
  "app": "App:{{app}}",
294
349
  "base": "Base:{{base}}",
295
350
  "record": "记录:{{status}}",
296
- "workspace": "Portal:{{status}}({{dir}})"
351
+ "deploymentPath": "部署路径:{{status}}({{dir}})",
352
+ "developmentPath": "开发路径:{{status}}({{dir}})",
353
+ "developmentPathMissing": "开发路径:缺失"
297
354
  }
298
355
  },
299
356
  "portalDev": {
@@ -411,7 +468,7 @@
411
468
  "betaLabel": "beta",
412
469
  "betaHint": "测试版。包含即将发布的新功能,适合提前体验和反馈。",
413
470
  "alphaLabel": "alpha",
414
- "alphaHint": "开发版。功能更新最快,但可能不完整或不稳定。",
471
+ "alphaHint": "如果要体验 3.0 新功能,选择这个版本。",
415
472
  "otherLabel": "其他",
416
473
  "otherHint": "手动填写其他版本号、Docker tag 或 Git ref,例如分支名。"
417
474
  },
@@ -600,6 +657,15 @@
600
657
  "skipDownload": {
601
658
  "message": "跳过下载 NocoBase,直接复用已有的本地应用文件或 Docker 镜像"
602
659
  },
660
+ "appClientEntryMode": {
661
+ "message": "应用界面入口",
662
+ "modernOnlyLabel": "仅使用新版界面",
663
+ "modernOnlyHint": "只能进入新版界面,不可进入旧版界面。",
664
+ "modernDefaultLabel": "默认进入新版界面",
665
+ "modernDefaultHint": "默认进入新版界面,同时保留旧版界面入口。",
666
+ "legacyDefaultLabel": "默认进入旧版界面",
667
+ "legacyDefaultHint": "默认进入旧版界面,也可以进入新版界面。"
668
+ },
603
669
  "portalType": {
604
670
  "message": "Portal 类型",
605
671
  "noCodeLabel": "无代码 Portal",