@nocobase/cli 3.0.0-alpha.1 → 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 (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
@@ -7,21 +7,21 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, setEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
14
  import { createPortalWorkspace } from '../../lib/portal-create.js';
15
- import { printInfo, printSuccess } from '../../lib/ui.js';
15
+ import { printInfo, printSuccess, printWarning } from '../../lib/ui.js';
16
16
  const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
17
17
  const portalCreateText = (key, values, fallback) => translateCli(`commands.portalCreate.${key}`, values, { fallback });
18
18
  export default class PortalCreate extends Command {
19
19
  static summary = 'Create a local AI portal from a template';
20
20
  static examples = [
21
21
  '<%= config.bin %> <%= command.id %> customer',
22
+ '<%= config.bin %> <%= command.id %> customer --path ./portals/customer',
22
23
  '<%= config.bin %> <%= command.id %> customer --template @nocobase/portal-template-default',
23
24
  '<%= config.bin %> <%= command.id %> customer --env dev --yes',
24
- '<%= config.bin %> <%= command.id %> customer --source-storage git --git-repo git@github.com:nocobase/customer-portal.git',
25
25
  ];
26
26
  static args = {
27
27
  portal: Args.string({
@@ -46,24 +46,13 @@ export default class PortalCreate extends Command {
46
46
  title: Flags.string({
47
47
  description: 'Portal display title; defaults to a title generated from the portal slug',
48
48
  }),
49
+ path: Flags.string({
50
+ description: 'Portal workspace directory; defaults to ./<portal>',
51
+ }),
49
52
  force: Flags.boolean({
50
53
  description: 'Delete the existing portal and recreate it',
51
54
  default: false,
52
55
  }),
53
- 'source-storage': Flags.string({
54
- description: 'Where portal source code is managed',
55
- options: ['nocobase', 'git'],
56
- default: 'nocobase',
57
- }),
58
- 'git-repo': Flags.string({
59
- description: 'Git repository URL used when --source-storage=git',
60
- }),
61
- 'git-branch': Flags.string({
62
- description: 'Git branch used when --source-storage=git',
63
- }),
64
- 'git-path': Flags.string({
65
- description: 'Directory inside the Git repository for this portal; defaults to the repository root',
66
- }),
67
56
  };
68
57
  async run() {
69
58
  const { args, flags } = await this.parse(PortalCreate);
@@ -77,10 +66,11 @@ export default class PortalCreate extends Command {
77
66
  return;
78
67
  }
79
68
  const scope = resolveDefaultConfigScope();
80
- const env = await getEnv(flags.env, { scope });
69
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
70
+ const env = await getEnv(envName, { scope });
81
71
  if (!env) {
82
- this.error(flags.env
83
- ? portalCreateText('errors.envNotConfigured', { envName: flags.env }, `Env "${flags.env}" is not configured. Run \`nb env add ${flags.env} --api-base-url <url>\` first.`)
72
+ this.error(requestedEnv
73
+ ? portalCreateText('errors.envNotConfigured', { envName }, `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`)
84
74
  : portalCreateText('errors.noEnvConfigured', undefined, 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
85
75
  }
86
76
  const result = await createPortalWorkspace({
@@ -88,18 +78,19 @@ export default class PortalCreate extends Command {
88
78
  title: flags.title,
89
79
  template: flags.template,
90
80
  env,
91
- envName: flags.env,
81
+ envName,
92
82
  cliVersion: String(this.config.pjson.version ?? '').trim(),
93
83
  force: flags.force,
94
- sourceStorage: flags['source-storage'],
95
- gitRepo: flags['git-repo'],
96
- gitBranch: flags['git-branch'],
97
- gitPath: flags['git-path'],
84
+ sourcePath: flags.path,
98
85
  onSkipInstall: (message) => printInfo(message),
99
86
  });
100
- printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}.`));
87
+ await setEnvPortalPath(envName, result.portal, result.portalDir, { scope });
88
+ printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}`));
101
89
  printInfo(portalCreateText('messages.app', { app: result.app }, `App: ${result.app}`));
102
90
  printInfo(portalCreateText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
103
91
  printInfo(portalCreateText('messages.sourceStorage', { sourceStorage: result.sourceStorage }, `Source storage: ${result.sourceStorage}`));
92
+ if (result.installFailed) {
93
+ printWarning(portalCreateText('messages.installFailed', { portalDir: result.portalDir }, `Dependency installation did not finish successfully. Run \`pnpm install\` manually in ${result.portalDir}.`));
94
+ }
104
95
  }
105
96
  }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, unsetEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
@@ -15,6 +15,9 @@ import { confirm } from "../../lib/inquirer.js";
15
15
  import { destroyPortalWorkspace } from '../../lib/portal-destroy.js';
16
16
  import { isInteractiveTerminal, printInfo, printSuccess } from '../../lib/ui.js';
17
17
  const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
18
+ function portalDestroyStatus(status) {
19
+ return portalDestroyText(`statuses.${status}`, undefined, status);
20
+ }
18
21
  async function ensureDestroyConfirmed(options) {
19
22
  if (options.yes) {
20
23
  return true;
@@ -24,7 +27,7 @@ async function ensureDestroyConfirmed(options) {
24
27
  }
25
28
  try {
26
29
  return Boolean(await confirm({
27
- message: portalDestroyText('prompts.confirm', { portal: options.portal }, `Destroy portal "${options.portal}" and delete its storage directory?`),
30
+ message: portalDestroyText('prompts.confirm', { portal: options.portal }, `Destroy portal "${options.portal}" and delete its deployment directory?`),
28
31
  default: false,
29
32
  }));
30
33
  }
@@ -33,9 +36,10 @@ async function ensureDestroyConfirmed(options) {
33
36
  }
34
37
  }
35
38
  export default class PortalDestroy extends Command {
36
- static summary = 'Destroy a portal record and local files';
39
+ static summary = 'Destroy a portal record and deployed files';
37
40
  static examples = [
38
41
  '<%= config.bin %> <%= command.id %> customer --yes',
42
+ '<%= config.bin %> <%= command.id %> customer --delete-dev-path --yes',
39
43
  '<%= config.bin %> <%= command.id %> customer --env dev --yes',
40
44
  '<%= config.bin %> <%= command.id %> customer --force --yes',
41
45
  ];
@@ -56,7 +60,12 @@ export default class PortalDestroy extends Command {
56
60
  default: false,
57
61
  }),
58
62
  force: Flags.boolean({
59
- description: 'Ignore missing portal records or local files',
63
+ description: 'Ignore missing portal records or deployment files',
64
+ default: false,
65
+ }),
66
+ 'delete-dev-path': Flags.boolean({
67
+ char: 'D',
68
+ description: 'Delete the portal development directory in addition to the deployed portal',
60
69
  default: false,
61
70
  }),
62
71
  };
@@ -93,12 +102,25 @@ export default class PortalDestroy extends Command {
93
102
  envName,
94
103
  cliVersion: String(this.config.pjson.version ?? '').trim(),
95
104
  force: flags.force,
105
+ deleteDevPath: flags['delete-dev-path'],
96
106
  });
107
+ await unsetEnvPortalPath(envName, result.portal, { scope });
97
108
  printSuccess(portalDestroyText('messages.destroyed', { portal: result.portal }, `Portal "${result.portal}" destroyed.`));
98
109
  printInfo(portalDestroyText('messages.mode', { mode: result.mode }, `Mode: ${result.mode}`));
99
110
  printInfo(portalDestroyText('messages.app', { app: result.app }, `App: ${result.app}`));
100
111
  printInfo(portalDestroyText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
101
- printInfo(portalDestroyText('messages.record', { status: result.recordDeleted ? 'deleted' : 'missing' }, `Record: ${result.recordDeleted ? 'deleted' : 'missing'}`));
102
- printInfo(portalDestroyText('messages.workspace', { dir: result.portalDir, status: result.workspaceDeleted ? 'deleted' : 'missing' }, `Portal files: ${result.workspaceDeleted ? 'deleted' : 'missing'} (${result.portalDir})`));
112
+ printInfo(portalDestroyText('messages.record', { status: portalDestroyStatus(result.recordDeleted ? 'deleted' : 'missing') }, `Record: ${result.recordDeleted ? 'deleted' : 'missing'}`));
113
+ const deploymentPathStatus = result.deploymentPathDeleted ? 'deleted' : 'missing';
114
+ printInfo(portalDestroyText('messages.deploymentPath', { dir: result.deploymentPath, status: portalDestroyStatus(deploymentPathStatus) }, `Deployment path: ${deploymentPathStatus} (${result.deploymentPath})`));
115
+ let developmentPathStatus = 'missing';
116
+ if (result.developmentPath) {
117
+ developmentPathStatus = result.developmentPathDeleted ? 'deleted' : 'retained';
118
+ }
119
+ printInfo(portalDestroyText(result.developmentPath ? 'messages.developmentPath' : 'messages.developmentPathMissing', {
120
+ dir: result.developmentPath,
121
+ status: portalDestroyStatus(developmentPathStatus),
122
+ }, result.developmentPath
123
+ ? `Development path: ${developmentPathStatus} (${result.developmentPath})`
124
+ : 'Development path: missing'));
103
125
  }
104
126
  }
@@ -59,6 +59,8 @@ export default class PortalDev extends Command {
59
59
  await devPortalWorkspace({
60
60
  portal: args.portal,
61
61
  env,
62
+ envName,
63
+ cliVersion: String(this.config?.pjson?.version ?? '').trim(),
62
64
  onStart: (result) => {
63
65
  printInfo(portalDevText('messages.starting', { portal: result.portal }, `Starting portal "${result.portal}"...`));
64
66
  printInfo(portalDevText('messages.mode', { mode: result.mode }, `Mode: ${result.mode}`));
@@ -21,7 +21,7 @@ function formatBoolean(value) {
21
21
  return value ? 'yes' : 'no';
22
22
  }
23
23
  export default class PortalList extends Command {
24
- static summary = 'List portal records and local sync status';
24
+ static summary = 'List portal records and development paths';
25
25
  static examples = [
26
26
  '<%= config.bin %> <%= command.id %>',
27
27
  '<%= config.bin %> <%= command.id %> --env dev --yes',
@@ -82,17 +82,17 @@ export default class PortalList extends Command {
82
82
  portalListText('table.url', undefined, 'URL'),
83
83
  portalListText('table.portalType', undefined, 'Portal type'),
84
84
  portalListText('table.sourceStorage', undefined, 'Source storage'),
85
- portalListText('table.path', undefined, 'Local path'),
85
+ portalListText('table.path', undefined, 'Development path'),
86
86
  portalListText('table.enabled', undefined, 'Enabled'),
87
- portalListText('table.localSynced', undefined, 'Local synced'),
87
+ portalListText('table.default', undefined, 'Default'),
88
88
  ], outputItems.map((item) => [
89
89
  item.name,
90
90
  item.url,
91
91
  item.portalType,
92
92
  item.sourceStorage,
93
- item.localPath,
93
+ item.developmentPath,
94
94
  formatBoolean(item.enabled),
95
- formatBoolean(item.localSynced),
95
+ formatBoolean(item.isDefault),
96
96
  ])));
97
97
  }
98
98
  }
@@ -7,18 +7,20 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, setEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
14
  import { pullPortalSource } from '../../lib/portal-source.js';
15
- import { printInfo, printSuccess } from '../../lib/ui.js';
15
+ import { printInfo, printSuccess, printWarning } from '../../lib/ui.js';
16
16
  const portalPullText = (key, values, fallback) => translateCli(`commands.portalPull.${key}`, values, { fallback });
17
17
  export default class PortalPull extends Command {
18
18
  static summary = 'Pull portal source into local files';
19
19
  static examples = [
20
20
  '<%= config.bin %> <%= command.id %> customer',
21
21
  '<%= config.bin %> <%= command.id %> customer --env prod --yes',
22
+ '<%= config.bin %> <%= command.id %> customer --path ./portals/customer',
23
+ '<%= config.bin %> <%= command.id %> customer --git-repo git@github.com:nocobase/customer.git',
22
24
  '<%= config.bin %> <%= command.id %> customer --force',
23
25
  '<%= config.bin %> <%= command.id %> customer --no-install',
24
26
  ];
@@ -42,6 +44,18 @@ export default class PortalPull extends Command {
42
44
  description: 'Delete the existing local files and pull them again',
43
45
  default: false,
44
46
  }),
47
+ path: Flags.string({
48
+ description: 'Portal workspace directory; defaults to the saved path, then ./<portal>',
49
+ }),
50
+ 'git-repo': Flags.string({
51
+ description: 'Temporarily pull source from this Git repository without updating the portal source configuration',
52
+ }),
53
+ 'git-branch': Flags.string({
54
+ description: 'Git branch for the temporary --git-repo pull; defaults to main',
55
+ }),
56
+ 'git-path': Flags.string({
57
+ description: 'Directory inside the temporary Git repository; defaults to the repository root',
58
+ }),
45
59
  install: Flags.boolean({
46
60
  description: 'Run pnpm install after pulling the portal source',
47
61
  default: true,
@@ -50,6 +64,12 @@ export default class PortalPull extends Command {
50
64
  };
51
65
  async run() {
52
66
  const { args, flags } = await this.parse(PortalPull);
67
+ if ((flags['git-branch'] || flags['git-path']) && !flags['git-repo']) {
68
+ this.error(portalPullText('errors.gitRepoRequiredForTemporaryPull', undefined, [
69
+ '--git-branch and --git-path require --git-repo for a temporary Git pull.',
70
+ 'To update the portal configuration, use `nb portal config`.',
71
+ ].join(' ')));
72
+ }
53
73
  const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
54
74
  const confirmed = await ensureCrossEnvConfirmed({
55
75
  command: this,
@@ -74,11 +94,20 @@ export default class PortalPull extends Command {
74
94
  cliVersion: String(this.config.pjson.version ?? '').trim(),
75
95
  force: flags.force,
76
96
  installDependencies: flags.install,
97
+ sourcePath: flags.path,
98
+ defaultSourcePath: true,
99
+ gitRepo: flags['git-repo'],
100
+ gitBranch: flags['git-branch'],
101
+ gitPath: flags['git-path'],
77
102
  });
78
103
  if (!result.changed) {
79
104
  printInfo(result.noopReason ?? portalPullText('messages.noop', undefined, 'No pull is needed.'));
80
105
  return;
81
106
  }
82
- printSuccess(portalPullText('messages.pulled', { portal: result.portal, portalDir: result.portalDir }, `Pulled portal source "${result.portal}" into ${result.portalDir}.`));
107
+ await setEnvPortalPath(envName, result.portal, result.portalDir, { scope });
108
+ printSuccess(portalPullText('messages.pulled', { portal: result.portal, portalDir: result.portalDir }, `Pulled portal source "${result.portal}" into ${result.portalDir}`));
109
+ if (result.installFailed) {
110
+ printWarning(portalPullText('messages.installFailed', { portalDir: result.portalDir }, `Dependency installation did not finish successfully. Run \`pnpm install\` manually in ${result.portalDir}.`));
111
+ }
83
112
  }
84
113
  }
@@ -19,6 +19,7 @@ import { promises as fs } from 'node:fs';
19
19
  import { basename, dirname } from 'node:path';
20
20
  import { Readable } from 'node:stream';
21
21
  import { pipeline } from 'node:stream/promises';
22
+ import { translateCli } from './cli-locale.js';
22
23
  import { resolveServerRequestTarget } from './env-auth.js';
23
24
  import { fetchWithPreservedAuthRedirect } from './http-request.js';
24
25
  const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
@@ -40,7 +41,32 @@ function parseJsonInput(raw, flagName) {
40
41
  function normalizeBaseUrl(baseUrl) {
41
42
  return baseUrl.replace(/\/+$/, '');
42
43
  }
43
- async function parseResponse(response) {
44
+ function buildAuthFailureCommand(envName) {
45
+ const normalizedEnvName = String(envName ?? '').trim();
46
+ return normalizedEnvName ? `nb env auth ${normalizedEnvName}` : 'nb env auth';
47
+ }
48
+ function decorateAuthFailureResponse(data, status, envName) {
49
+ if (status !== 401) {
50
+ return data;
51
+ }
52
+ const cliCommand = buildAuthFailureCommand(envName);
53
+ const cliHint = translateCli('apiClient.authRequiredHint', { command: cliCommand }, {
54
+ fallback: 'Authentication failed or the saved session has expired. Run `{{command}}` to sign in again.',
55
+ });
56
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
57
+ return {
58
+ ...data,
59
+ cliHint,
60
+ cliCommand,
61
+ };
62
+ }
63
+ return {
64
+ error: data,
65
+ cliHint,
66
+ cliCommand,
67
+ };
68
+ }
69
+ async function parseResponse(response, envName) {
44
70
  const text = await response.text();
45
71
  let data = text;
46
72
  if (text) {
@@ -54,10 +80,10 @@ async function parseResponse(response) {
54
80
  return {
55
81
  ok: response.ok,
56
82
  status: response.status,
57
- data,
83
+ data: decorateAuthFailureResponse(data, response.status, envName),
58
84
  };
59
85
  }
60
- async function parseBinaryResponse(response, outputPath) {
86
+ async function parseBinaryResponse(response, outputPath, envName) {
61
87
  if (response.ok && response.body) {
62
88
  await fs.mkdir(dirname(outputPath), { recursive: true }).catch(() => undefined);
63
89
  await pipeline(Readable.fromWeb(response.body), createWriteStream(outputPath));
@@ -69,7 +95,7 @@ async function parseBinaryResponse(response, outputPath) {
69
95
  },
70
96
  };
71
97
  }
72
- return parseResponse(response);
98
+ return parseResponse(response, envName);
73
99
  }
74
100
  function parseScalarValue(value, type) {
75
101
  if (value === undefined) {
@@ -215,7 +241,7 @@ async function createMultipartBody(flags, operation) {
215
241
  return hasValues ? formData : undefined;
216
242
  }
217
243
  export async function executeApiRequest(options) {
218
- const { baseUrl, token } = await resolveServerRequestTarget(options);
244
+ const { baseUrl, token, envName } = await resolveServerRequestTarget(options);
219
245
  const headers = new Headers();
220
246
  headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
221
247
  headers.set(CLI_VERSION_HEADER, options.cliVersion);
@@ -283,12 +309,12 @@ export async function executeApiRequest(options) {
283
309
  if (!outputPath) {
284
310
  throw new Error('Missing required output path --output');
285
311
  }
286
- return parseBinaryResponse(response, outputPath);
312
+ return parseBinaryResponse(response, outputPath, envName);
287
313
  }
288
- return parseResponse(response);
314
+ return parseResponse(response, envName);
289
315
  }
290
316
  export async function executeRawApiRequest(options) {
291
- const { baseUrl, token } = await resolveServerRequestTarget(options);
317
+ const { baseUrl, token, envName } = await resolveServerRequestTarget(options);
292
318
  const headers = new Headers();
293
319
  headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
294
320
  if (token) {
@@ -332,7 +358,7 @@ export async function executeRawApiRequest(options) {
332
358
  body: options.body === undefined ? undefined : JSON.stringify(options.body),
333
359
  signal: controller?.signal,
334
360
  });
335
- return parseResponse(response);
361
+ return parseResponse(response, envName);
336
362
  }
337
363
  finally {
338
364
  if (timeout) {
@@ -0,0 +1,28 @@
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
+ export const APP_CLIENT_ENTRY_MODES = [
10
+ 'legacy-default',
11
+ 'modern-default',
12
+ 'modern-only',
13
+ 'settings-default',
14
+ ];
15
+ export const PUBLIC_APP_CLIENT_ENTRY_MODES = ['legacy-default', 'modern-default', 'modern-only'];
16
+ export function normalizeAppClientEntryMode(value) {
17
+ const text = String(value ?? '').trim();
18
+ return APP_CLIENT_ENTRY_MODES.includes(text) ? text : undefined;
19
+ }
20
+ export function normalizePublicAppClientEntryMode(value) {
21
+ const text = String(value ?? '').trim();
22
+ return PUBLIC_APP_CLIENT_ENTRY_MODES.includes(text)
23
+ ? text
24
+ : undefined;
25
+ }
26
+ export function defaultAppClientEntryModeForDownloadVersion(version) {
27
+ return String(version ?? '').trim() === 'latest' ? 'legacy-default' : 'modern-only';
28
+ }
@@ -186,6 +186,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
186
186
  const dbTablePrefix = trimValue(config.dbTablePrefix);
187
187
  const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
188
188
  const extractClientAssets = resolveDockerClientAssetsExtractEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
189
+ const appClientEntryMode = trimValue(config.appClientEntryMode);
189
190
  const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
190
191
  const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
191
192
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
@@ -245,6 +246,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
245
246
  const lifecycleEnvVars = managedAppLifecycleEnvVars();
246
247
  args.push('-e', `APP_ENV=${lifecycleEnvVars.APP_ENV}`, '-e', `NODE_ENV=${lifecycleEnvVars.NODE_ENV}`, '-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
247
248
  pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath ? resolveAppPublicPath(appPublicPath) : undefined);
249
+ pushOptionalEnvArg(args, 'APP_CLIENT_ENTRY_MODE', appClientEntryMode);
248
250
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
249
251
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
250
252
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
@@ -14,6 +14,7 @@ import { normalizeCliLocale } from './cli-locale.js';
14
14
  import { normalizeEnvProxyConfig, normalizeEnvProxyProviderConfig, } from './env-proxy-config.js';
15
15
  import { inferConfiguredAppPathFromLegacyConfig, resolveConfiguredAppPath, resolveConfiguredSourcePath, resolveConfiguredStoragePath, } from './env-paths.js';
16
16
  import { ENV_CONFIG_SCHEMA_VERSION, normalizeEnvConfigSchemaVersion } from './env-config.js';
17
+ import { normalizeEnvPortalsConfig, } from './env-portal-config.js';
17
18
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
18
19
  function normalizeStoredEnvKind(value) {
19
20
  const kind = String(value ?? '').trim();
@@ -78,11 +79,12 @@ function normalizeEnvConfigEntry(entry) {
78
79
  if (!entry) {
79
80
  return entry;
80
81
  }
81
- const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, schemaVersion: _schemaVersion, ...rest } = entry;
82
+ const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, schemaVersion: _schemaVersion, portals: _portals, ...rest } = entry;
82
83
  const normalizedKind = resolveEnvKind(entry);
83
84
  const apiBaseUrl = readEnvApiBaseUrl(entry);
84
85
  const schemaVersion = normalizeEnvConfigSchemaVersion(entry.schemaVersion);
85
86
  const proxy = normalizeEnvProxyConfig(entry.proxy);
87
+ const portals = normalizeEnvPortalsConfig(entry.portals);
86
88
  return {
87
89
  ...rest,
88
90
  ...(schemaVersion ? { schemaVersion } : {}),
@@ -90,6 +92,7 @@ function normalizeEnvConfigEntry(entry) {
90
92
  ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
91
93
  ...(normalizeOptionalString(entry.appPublicPath) ? { appPublicPath: resolveAppPublicPath(entry.appPublicPath) } : {}),
92
94
  ...(proxy ? { proxy } : {}),
95
+ ...(portals ? { portals } : {}),
93
96
  };
94
97
  }
95
98
  function normalizeAuthConfig(config) {
@@ -332,6 +335,7 @@ export class Env {
332
335
  put('APP_PORT', this.appPort);
333
336
  put('APP_PUBLIC_PATH', this.config.appPublicPath ? resolveAppPublicPath(this.config.appPublicPath) : undefined);
334
337
  put('CDN_BASE_URL', this.config.cdnBaseUrl);
338
+ put('APP_CLIENT_ENTRY_MODE', this.config.appClientEntryMode);
335
339
  put('APP_KEY', this.config.appKey);
336
340
  put('TZ', this.config.timezone);
337
341
  put('DB_DIALECT', this.config.dbDialect);
@@ -521,6 +525,53 @@ export async function setEnvRuntime(envName, runtime, options = {}) {
521
525
  };
522
526
  await saveAuthConfig(config, options);
523
527
  }
528
+ export function resolveEnvPortalPath(config, portal) {
529
+ const portalName = normalizeOptionalString(portal);
530
+ if (!portalName) {
531
+ return undefined;
532
+ }
533
+ return normalizeOptionalString(config?.portals?.[portalName]?.path);
534
+ }
535
+ export async function setEnvPortalPath(envName, portal, portalPath, options = {}) {
536
+ const portalName = normalizeOptionalString(portal);
537
+ const normalizedPath = normalizeOptionalString(portalPath);
538
+ if (!portalName || !normalizedPath) {
539
+ return;
540
+ }
541
+ await writeEnv(envName, (previous) => {
542
+ if (!previous) {
543
+ throw new Error(`Env "${envName}" is not configured`);
544
+ }
545
+ const portals = normalizeEnvPortalsConfig(previous.portals) ?? {};
546
+ return {
547
+ ...previous,
548
+ portals: {
549
+ ...portals,
550
+ [portalName]: {
551
+ ...(portals[portalName] ?? {}),
552
+ path: normalizedPath,
553
+ },
554
+ },
555
+ };
556
+ }, options);
557
+ }
558
+ export async function unsetEnvPortalPath(envName, portal, options = {}) {
559
+ const portalName = normalizeOptionalString(portal);
560
+ if (!portalName) {
561
+ return;
562
+ }
563
+ await writeEnv(envName, (previous) => {
564
+ if (!previous) {
565
+ throw new Error(`Env "${envName}" is not configured`);
566
+ }
567
+ const portals = normalizeEnvPortalsConfig(previous.portals) ?? {};
568
+ delete portals[portalName];
569
+ return {
570
+ ...previous,
571
+ ...(Object.keys(portals).length > 0 ? { portals } : { portals: undefined }),
572
+ };
573
+ }, options);
574
+ }
524
575
  export function resolveEnvProxyEntry(config, provider) {
525
576
  const proxy = normalizeEnvProxyConfig(config?.proxy);
526
577
  const resolved = {
@@ -72,7 +72,9 @@ function hasVersionFlag(argv) {
72
72
  function isBuiltinCommand(argv) {
73
73
  const commandTokens = argv.filter((token) => token && !token.startsWith('-'));
74
74
  const [topic, subtopic] = commandTokens;
75
- return topic === 'env' || topic === 'resource' || (topic === 'api' && subtopic === 'resource');
75
+ return (topic === 'env' ||
76
+ topic === 'resource' ||
77
+ (topic === 'api' && (subtopic === 'resource' || subtopic === 'swagger')));
76
78
  }
77
79
  export function shouldSkipRuntimeBootstrap(argv) {
78
80
  return hasVersionFlag(argv) || isBuiltinCommand(argv);
@@ -0,0 +1,29 @@
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 { spawn } from 'node:child_process';
10
+ export async function openUrlInDefaultBrowser(url) {
11
+ const [command, args, options] = process.platform === 'darwin'
12
+ ? ['open', [url], { detached: true, stdio: 'ignore' }]
13
+ : process.platform === 'win32'
14
+ ? ['cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }]
15
+ : ['xdg-open', [url], { detached: true, stdio: 'ignore' }];
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const child = spawn(command, args, options);
19
+ child.once('error', () => resolve(false));
20
+ child.once('spawn', () => {
21
+ child.unref();
22
+ resolve(true);
23
+ });
24
+ }
25
+ catch {
26
+ resolve(false);
27
+ }
28
+ });
29
+ }
@@ -27,40 +27,6 @@ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
27
27
  function normalizeBaseUrl(baseUrl) {
28
28
  return baseUrl.replace(/\/+$/, '');
29
29
  }
30
- function buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl) {
31
- const url = new URL(apiBaseUrl);
32
- const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
33
- if (subappMatch) {
34
- const publicPath = (subappMatch[1] || '').replace(/\/+$/, '');
35
- return `${publicPath}/settings/apps/${subappMatch[2]}/idpOAuth/device`;
36
- }
37
- const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
38
- if (appMatch) {
39
- const publicPath = (appMatch[1] || '').replace(/\/+$/, '');
40
- return `${publicPath}/settings/idpOAuth/device`;
41
- }
42
- return undefined;
43
- }
44
- export function resolveDeviceVerificationUrlForApiBaseUrl(verificationUrl, apiBaseUrl) {
45
- try {
46
- const devicePath = buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl);
47
- if (!devicePath) {
48
- return verificationUrl;
49
- }
50
- const originalUrl = new URL(verificationUrl);
51
- if (!originalUrl.pathname.endsWith('/idpOAuth/device')) {
52
- return verificationUrl;
53
- }
54
- const publicUrl = new URL(apiBaseUrl);
55
- publicUrl.pathname = devicePath;
56
- publicUrl.search = originalUrl.search;
57
- publicUrl.hash = originalUrl.hash;
58
- return publicUrl.toString();
59
- }
60
- catch {
61
- return verificationUrl;
62
- }
63
- }
64
30
  export function getOauthMetadataUrl(baseUrl) {
65
31
  return `${normalizeBaseUrl(baseUrl)}/.well-known/oauth-authorization-server`;
66
32
  }
@@ -973,7 +939,7 @@ export async function resolveServerRequestTarget(options) {
973
939
  : `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
974
940
  ].join('\n'));
975
941
  }
976
- return { baseUrl, token };
942
+ return { baseUrl, token, envName };
977
943
  }
978
944
  export async function authenticateEnvWithBasic(options) {
979
945
  const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
@@ -1051,7 +1017,7 @@ async function authenticateEnvWithOauthDevice(options) {
1051
1017
  baseUrl: options.baseUrl,
1052
1018
  });
1053
1019
  stopTask();
1054
- const verificationUrl = resolveDeviceVerificationUrlForApiBaseUrl(deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri, options.baseUrl);
1020
+ const verificationUrl = deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri;
1055
1021
  const browser = await browserOpener(verificationUrl);
1056
1022
  cleanupBrowserOpenTarget = browser.cleanup;
1057
1023
  if (!browser.opened) {
@@ -19,6 +19,7 @@ export const ENV_STRING_CONFIG_FLAG_MAP = {
19
19
  'app-public-path': 'appPublicPath',
20
20
  'cdn-base-url': 'cdnBaseUrl',
21
21
  'env-file': 'envFile',
22
+ 'app-client-entry-mode': 'appClientEntryMode',
22
23
  'app-port': 'appPort',
23
24
  'app-key': 'appKey',
24
25
  timezone: 'timezone',