@nocobase/cli 2.2.0-alpha.1 → 2.2.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 (78) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  3. package/bin/early-locale.js +89 -0
  4. package/bin/node-version.js +35 -0
  5. package/bin/run.js +9 -0
  6. package/bin/windows-admin.js +60 -0
  7. package/dist/commands/app/destroy.js +4 -3
  8. package/dist/commands/app/restart.js +38 -0
  9. package/dist/commands/app/shared.js +49 -3
  10. package/dist/commands/app/start.js +95 -0
  11. package/dist/commands/app/upgrade.js +11 -0
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/examples/prompts-stages.js +2 -2
  15. package/dist/commands/examples/prompts-test.js +2 -2
  16. package/dist/commands/init.js +152 -14
  17. package/dist/commands/install.js +256 -109
  18. package/dist/commands/license/activate.js +4 -1
  19. package/dist/commands/license/shared.js +24 -15
  20. package/dist/commands/portal/config.js +88 -0
  21. package/dist/commands/portal/create.js +104 -0
  22. package/dist/commands/portal/deploy.js +81 -0
  23. package/dist/commands/portal/destroy.js +104 -0
  24. package/dist/commands/portal/dev.js +71 -0
  25. package/dist/commands/portal/index.js +20 -0
  26. package/dist/commands/portal/info.js +82 -0
  27. package/dist/commands/portal/list.js +98 -0
  28. package/dist/commands/portal/pull.js +84 -0
  29. package/dist/commands/portal/push.js +79 -0
  30. package/dist/commands/proxy/caddy/generate.js +93 -7
  31. package/dist/commands/proxy/nginx/generate.js +98 -7
  32. package/dist/commands/revision/create.js +1 -1
  33. package/dist/commands/self/check.js +1 -1
  34. package/dist/commands/self/update.js +4 -4
  35. package/dist/commands/skills/check.js +4 -5
  36. package/dist/commands/skills/install.js +18 -1
  37. package/dist/commands/skills/update.js +19 -4
  38. package/dist/commands/source/dev.js +10 -6
  39. package/dist/commands/source/download.js +85 -16
  40. package/dist/lib/api-command-compat.js +51 -8
  41. package/dist/lib/app-managed-resources.js +104 -5
  42. package/dist/lib/auth-store.js +105 -13
  43. package/dist/lib/cli-config.js +93 -2
  44. package/dist/lib/docker-image.js +94 -6
  45. package/dist/lib/env-auth.js +291 -45
  46. package/dist/lib/env-config.js +14 -0
  47. package/dist/lib/env-proxy-config.js +48 -0
  48. package/dist/lib/env-proxy.js +276 -61
  49. package/dist/lib/hook-script.js +160 -0
  50. package/dist/lib/managed-init-env.js +6 -1
  51. package/dist/lib/portal-command-env.js +31 -0
  52. package/dist/lib/portal-config.js +133 -0
  53. package/dist/lib/portal-configure.js +117 -0
  54. package/dist/lib/portal-create.js +433 -0
  55. package/dist/lib/portal-deploy.js +283 -0
  56. package/dist/lib/portal-destroy.js +100 -0
  57. package/dist/lib/portal-dev.js +79 -0
  58. package/dist/lib/portal-env-files.js +53 -0
  59. package/dist/lib/portal-info.js +31 -0
  60. package/dist/lib/portal-list.js +211 -0
  61. package/dist/lib/portal-source.js +523 -0
  62. package/dist/lib/prompt-catalog-terminal.js +32 -19
  63. package/dist/lib/prompt-validators.js +1 -1
  64. package/dist/lib/prompt-web-ui.js +20 -13
  65. package/dist/lib/proxy-caddy.js +77 -9
  66. package/dist/lib/proxy-nginx.js +71 -11
  67. package/dist/lib/run-npm.js +21 -16
  68. package/dist/lib/self-manager.js +254 -46
  69. package/dist/lib/skills-manager.js +116 -23
  70. package/dist/lib/source-publish.js +2 -2
  71. package/dist/lib/startup-update.js +1 -1
  72. package/dist/lib/ui.js +28 -1
  73. package/dist/locale/en-US.json +227 -43
  74. package/dist/locale/zh-CN.json +227 -43
  75. package/package.json +11 -2
  76. package/scripts/build.mjs +0 -34
  77. package/scripts/clean.mjs +0 -9
  78. package/tsconfig.json +0 -19
@@ -143,11 +143,14 @@ export default class LicenseActivate extends Command {
143
143
  const validation = await validateLicenseKey(runtime, resolvedKey);
144
144
  const ok = !validation.keyStatus && validation.envMatch && validation.domainMatch && validation.licenseStatus === 'active';
145
145
  const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
146
+ const shouldResolveInstanceId = Boolean(interactiveKeyFlowInstanceId || ok || (flags.json && !validation.keyStatus));
146
147
  const payload = {
147
148
  ok,
148
149
  env: runtime.envName,
149
150
  kind: runtime.kind,
150
- instanceId: interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime)),
151
+ instanceId: shouldResolveInstanceId
152
+ ? interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime))
153
+ : undefined,
151
154
  mode: 'key',
152
155
  key: redactLicenseKey(resolvedKey),
153
156
  keyFile: keyFile || undefined,
@@ -10,8 +10,8 @@ import { Flags } from '@oclif/core';
10
10
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
11
11
  import path from 'node:path';
12
12
  import { getEnvAsync, getInstanceIdAsync, keyDecrypt } from '@nocobase/license-kit';
13
- import { checkExternalDbConnection, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
14
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../../lib/docker-image.js";
13
+ import { checkExternalDbConnection, readExternalDbConnectionConfig } from "../../lib/db-connection-check.js";
14
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../../lib/docker-image.js";
15
15
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
16
16
  import { buildRuntimeEnvVars } from '../../lib/runtime-env-vars.js';
17
17
  import { resolveLicensePkgUrlFromConfig } from '../../lib/cli-config.js';
@@ -102,12 +102,7 @@ function buildDockerLicenseDbFlagArgs(envVars) {
102
102
  ];
103
103
  }
104
104
  async function runDockerLicenseJsonCommand(runtime, commandArgs) {
105
- const args = [
106
- 'run',
107
- '--rm',
108
- '--network',
109
- runtime.dockerNetworkName || runtime.workspaceName,
110
- ];
105
+ const args = ['run', '--rm', '--network', runtime.dockerNetworkName || runtime.workspaceName];
111
106
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config?.dockerPlatform);
112
107
  if (dockerPlatform) {
113
108
  args.push('--platform', dockerPlatform);
@@ -193,11 +188,11 @@ export async function generateValidatedInstanceIdFromEnvVars(envVars) {
193
188
  }
194
189
  async function generateInstanceIdForDockerRuntime(runtime) {
195
190
  const envVars = await buildRuntimeEnvVars(runtime);
196
- const payload = await runDockerLicenseJsonCommand(runtime, [
191
+ const payload = (await runDockerLicenseJsonCommand(runtime, [
197
192
  'license',
198
193
  'generate-id',
199
194
  ...buildDockerLicenseDbFlagArgs(envVars),
200
- ]);
195
+ ]));
201
196
  const instanceId = trimValue(payload.instanceId);
202
197
  if (!instanceId) {
203
198
  throw new Error('Docker instance ID generation did not return an instance ID.');
@@ -337,7 +332,7 @@ export async function getLicenseStatus(keyData) {
337
332
  }),
338
333
  signal: controller.signal,
339
334
  });
340
- const payload = await response.json();
335
+ const payload = (await response.json());
341
336
  return payload?.data?.status === 'active' ? 'active' : 'invalid';
342
337
  }
343
338
  catch {
@@ -356,6 +351,22 @@ export async function validateLicenseKey(runtime, key) {
356
351
  catch {
357
352
  keyStatus = 'invalid';
358
353
  }
354
+ if (keyStatus) {
355
+ const currentDomain = appUrl(runtime);
356
+ return {
357
+ current: {
358
+ env: undefined,
359
+ domain: currentDomain ? new URL(currentDomain).host : '',
360
+ },
361
+ keyData,
362
+ keyStatus,
363
+ dbMatch: false,
364
+ sysMatch: false,
365
+ envMatch: false,
366
+ domainMatch: false,
367
+ licenseStatus: 'invalid',
368
+ };
369
+ }
359
370
  const currentEnv = await getCurrentLicenseEnv(runtime);
360
371
  const currentDomain = appUrl(runtime);
361
372
  const dbMatch = isDbMatch(currentEnv, keyData);
@@ -391,7 +402,7 @@ export async function resolveLicenseServiceUrl(value) {
391
402
  return (await resolveLicensePkgUrl(value)).replace(/\/+$/, '');
392
403
  }
393
404
  export async function resolveLicensePkgUrl(value) {
394
- const normalized = String(value ?? '').trim() || await resolveLicensePkgUrlFromConfig();
405
+ const normalized = String(value ?? '').trim() || (await resolveLicensePkgUrlFromConfig());
395
406
  return normalized.replace(/\/+$/, '') + '/';
396
407
  }
397
408
  function shouldRedactOutputKey(key) {
@@ -414,9 +425,7 @@ export function sanitizeLicenseOutput(value) {
414
425
  if (value && typeof value === 'object') {
415
426
  return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
416
427
  key,
417
- shouldRedactOutputKey(key)
418
- ? redactOutputValue(String(nestedValue ?? ''))
419
- : sanitizeLicenseOutput(nestedValue),
428
+ shouldRedactOutputKey(key) ? redactOutputValue(String(nestedValue ?? '')) : sanitizeLicenseOutput(nestedValue),
420
429
  ]));
421
430
  }
422
431
  return value;
@@ -0,0 +1,88 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { configurePortalWorkspace } from '../../lib/portal-configure.js';
15
+ import { printInfo, printSuccess } from '../../lib/ui.js';
16
+ const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
17
+ export default class PortalConfig extends Command {
18
+ static summary = 'Update portal source configuration';
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %> customer --source-storage nocobase',
21
+ '<%= config.bin %> <%= command.id %> customer --source-storage git --git-repo git@github.com:nocobase/customer-portal.git',
22
+ '<%= config.bin %> <%= command.id %> customer --git-branch main --git-path portals/customer',
23
+ ];
24
+ static args = {
25
+ portal: Args.string({
26
+ required: true,
27
+ description: 'Portal name',
28
+ }),
29
+ };
30
+ static flags = {
31
+ env: Flags.string({
32
+ char: 'e',
33
+ description: 'CLI env name; omitted uses the current env',
34
+ }),
35
+ yes: Flags.boolean({
36
+ char: 'y',
37
+ description: 'Confirm using --env when it targets a different env than the current env',
38
+ default: false,
39
+ }),
40
+ 'source-storage': Flags.string({
41
+ description: 'Where portal source code is managed',
42
+ options: ['nocobase', 'git'],
43
+ }),
44
+ 'git-repo': Flags.string({
45
+ description: 'Git repository URL used when --source-storage=git',
46
+ }),
47
+ 'git-branch': Flags.string({
48
+ description: 'Git branch used when --source-storage=git',
49
+ }),
50
+ 'git-path': Flags.string({
51
+ description: 'Directory inside the Git repository for this portal; defaults to the repository root',
52
+ }),
53
+ };
54
+ async run() {
55
+ const { args, flags } = await this.parse(PortalConfig);
56
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
57
+ const confirmed = await ensureCrossEnvConfirmed({
58
+ command: this,
59
+ requestedEnv,
60
+ yes: flags.yes,
61
+ });
62
+ if (!confirmed) {
63
+ return;
64
+ }
65
+ const scope = resolveDefaultConfigScope();
66
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
67
+ const env = await getEnv(envName, { scope });
68
+ if (!env) {
69
+ this.error(portalConfigureText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
70
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
71
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
72
+ }
73
+ const result = await configurePortalWorkspace({
74
+ portal: args.portal,
75
+ env,
76
+ envName,
77
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
78
+ sourceStorage: flags['source-storage'],
79
+ gitRepo: flags['git-repo'],
80
+ gitBranch: flags['git-branch'],
81
+ gitPath: flags['git-path'],
82
+ });
83
+ printSuccess(portalConfigureText('messages.updated', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" configuration updated at ${result.portalDir}/portal.config.json.`));
84
+ printInfo(result.remoteSynced
85
+ ? portalConfigureText('messages.remoteSynced', undefined, 'Remote portal record: synced')
86
+ : portalConfigureText('messages.remoteSkipped', undefined, 'Remote portal record: not found; local config only'));
87
+ }
88
+ }
@@ -0,0 +1,104 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { createPortalWorkspace } from '../../lib/portal-create.js';
15
+ import { printInfo, printSuccess } from '../../lib/ui.js';
16
+ const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
17
+ const portalCreateText = (key, values, fallback) => translateCli(`commands.portalCreate.${key}`, values, { fallback });
18
+ export default class PortalCreate extends Command {
19
+ static summary = 'Create a local portal from a template';
20
+ static examples = [
21
+ '<%= config.bin %> <%= command.id %> customer',
22
+ '<%= config.bin %> <%= command.id %> customer --template @nocobase/portal-template-default',
23
+ '<%= 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
+ ];
26
+ static args = {
27
+ portal: Args.string({
28
+ required: true,
29
+ description: 'Portal name',
30
+ }),
31
+ };
32
+ static flags = {
33
+ template: Flags.string({
34
+ description: 'Template package, local path, or file:// URL',
35
+ default: DEFAULT_PORTAL_TEMPLATE,
36
+ }),
37
+ env: Flags.string({
38
+ char: 'e',
39
+ description: 'CLI env name; omitted uses the current env',
40
+ }),
41
+ yes: Flags.boolean({
42
+ char: 'y',
43
+ description: 'Confirm using --env when it targets a different env than the current env',
44
+ default: false,
45
+ }),
46
+ title: Flags.string({
47
+ description: 'Portal display title; defaults to a title generated from the portal slug',
48
+ }),
49
+ force: Flags.boolean({
50
+ description: 'Delete the existing portal and recreate it',
51
+ default: false,
52
+ }),
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
+ };
68
+ async run() {
69
+ const { args, flags } = await this.parse(PortalCreate);
70
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
71
+ const confirmed = await ensureCrossEnvConfirmed({
72
+ command: this,
73
+ requestedEnv,
74
+ yes: flags.yes,
75
+ });
76
+ if (!confirmed) {
77
+ return;
78
+ }
79
+ const env = await getEnv(flags.env, { scope: resolveDefaultConfigScope() });
80
+ if (!env) {
81
+ this.error(flags.env
82
+ ? portalCreateText('errors.envNotConfigured', { envName: flags.env }, `Env "${flags.env}" is not configured. Run \`nb env add ${flags.env} --api-base-url <url>\` first.`)
83
+ : portalCreateText('errors.noEnvConfigured', undefined, 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
84
+ }
85
+ const result = await createPortalWorkspace({
86
+ portal: args.portal,
87
+ title: flags.title,
88
+ template: flags.template,
89
+ env,
90
+ envName: flags.env,
91
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
92
+ force: flags.force,
93
+ sourceStorage: flags['source-storage'],
94
+ gitRepo: flags['git-repo'],
95
+ gitBranch: flags['git-branch'],
96
+ gitPath: flags['git-path'],
97
+ onSkipInstall: (message) => printInfo(message),
98
+ });
99
+ printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}.`));
100
+ printInfo(portalCreateText('messages.app', { app: result.app }, `App: ${result.app}`));
101
+ printInfo(portalCreateText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
102
+ printInfo(portalCreateText('messages.sourceStorage', { sourceStorage: result.sourceStorage }, `Source storage: ${result.sourceStorage}`));
103
+ }
104
+ }
@@ -0,0 +1,81 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { findPortalListItem, formatPortalInfo } from '../../lib/portal-info.js';
15
+ import { deployPortalWorkspace } from '../../lib/portal-deploy.js';
16
+ import { listPortalWorkspaces } from '../../lib/portal-list.js';
17
+ import { printSuccess } from '../../lib/ui.js';
18
+ const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
19
+ export default class PortalDeploy extends Command {
20
+ static summary = 'Build and deploy a portal';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %> customer',
23
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
24
+ ];
25
+ static args = {
26
+ portal: Args.string({
27
+ required: true,
28
+ description: 'Portal name',
29
+ }),
30
+ };
31
+ static flags = {
32
+ env: Flags.string({
33
+ char: 'e',
34
+ description: 'CLI env name; omitted uses the current env',
35
+ }),
36
+ yes: Flags.boolean({
37
+ char: 'y',
38
+ description: 'Confirm using --env when it targets a different env than the current env',
39
+ default: false,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(PortalDeploy);
44
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
45
+ const confirmed = await ensureCrossEnvConfirmed({
46
+ command: this,
47
+ requestedEnv,
48
+ yes: flags.yes,
49
+ });
50
+ if (!confirmed) {
51
+ return;
52
+ }
53
+ const scope = resolveDefaultConfigScope();
54
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
55
+ const env = await getEnv(envName, { scope });
56
+ if (!env) {
57
+ this.error(portalDeployText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
58
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
59
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
60
+ }
61
+ const result = await deployPortalWorkspace({
62
+ portal: args.portal,
63
+ env,
64
+ envName,
65
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
66
+ });
67
+ printSuccess(portalDeployText('messages.deployed', { portal: result.portal }, `Portal "${result.portal}" deployed.`));
68
+ const info = await listPortalWorkspaces({
69
+ env,
70
+ envName,
71
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
72
+ });
73
+ const portal = findPortalListItem(info.items, result.portal);
74
+ if (!portal) {
75
+ this.error(translateCli(`commands.portalInfo.errors.notFound`, { portal: result.portal }, {
76
+ fallback: `Portal "${result.portal}" was not found.`,
77
+ }));
78
+ }
79
+ this.log(formatPortalInfo(portal));
80
+ }
81
+ }
@@ -0,0 +1,104 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { confirm } from "../../lib/inquirer.js";
15
+ import { destroyPortalWorkspace } from '../../lib/portal-destroy.js';
16
+ import { isInteractiveTerminal, printInfo, printSuccess } from '../../lib/ui.js';
17
+ const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
18
+ async function ensureDestroyConfirmed(options) {
19
+ if (options.yes) {
20
+ return true;
21
+ }
22
+ if (!isInteractiveTerminal()) {
23
+ options.command.error(portalDestroyText('errors.confirmationRequired', undefined, 'Refusing to destroy a portal in non-interactive mode without --yes.'));
24
+ }
25
+ try {
26
+ return Boolean(await confirm({
27
+ message: portalDestroyText('prompts.confirm', { portal: options.portal }, `Destroy portal "${options.portal}" and delete its storage directory?`),
28
+ default: false,
29
+ }));
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ export default class PortalDestroy extends Command {
36
+ static summary = 'Destroy a portal record and local files';
37
+ static examples = [
38
+ '<%= config.bin %> <%= command.id %> customer --yes',
39
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
40
+ '<%= config.bin %> <%= command.id %> customer --force --yes',
41
+ ];
42
+ static args = {
43
+ portal: Args.string({
44
+ required: true,
45
+ description: 'Portal name',
46
+ }),
47
+ };
48
+ static flags = {
49
+ env: Flags.string({
50
+ char: 'e',
51
+ description: 'CLI env name; omitted uses the current env',
52
+ }),
53
+ yes: Flags.boolean({
54
+ char: 'y',
55
+ description: 'Skip confirmation prompts',
56
+ default: false,
57
+ }),
58
+ force: Flags.boolean({
59
+ description: 'Ignore missing portal records or local files',
60
+ default: false,
61
+ }),
62
+ };
63
+ async run() {
64
+ const { args, flags } = await this.parse(PortalDestroy);
65
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
66
+ const crossEnvConfirmed = await ensureCrossEnvConfirmed({
67
+ command: this,
68
+ requestedEnv,
69
+ yes: flags.yes,
70
+ });
71
+ if (!crossEnvConfirmed) {
72
+ return;
73
+ }
74
+ const destroyConfirmed = await ensureDestroyConfirmed({
75
+ command: this,
76
+ portal: args.portal,
77
+ yes: flags.yes,
78
+ });
79
+ if (!destroyConfirmed) {
80
+ return;
81
+ }
82
+ const scope = resolveDefaultConfigScope();
83
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
84
+ const env = await getEnv(envName, { scope });
85
+ if (!env) {
86
+ this.error(portalDestroyText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
87
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
88
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
89
+ }
90
+ const result = await destroyPortalWorkspace({
91
+ portal: args.portal,
92
+ env,
93
+ envName,
94
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
95
+ force: flags.force,
96
+ });
97
+ printSuccess(portalDestroyText('messages.destroyed', { portal: result.portal }, `Portal "${result.portal}" destroyed.`));
98
+ printInfo(portalDestroyText('messages.mode', { mode: result.mode }, `Mode: ${result.mode}`));
99
+ printInfo(portalDestroyText('messages.app', { app: result.app }, `App: ${result.app}`));
100
+ 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})`));
103
+ }
104
+ }
@@ -0,0 +1,71 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { devPortalWorkspace } from '../../lib/portal-dev.js';
15
+ import { printInfo } from '../../lib/ui.js';
16
+ const portalDevText = (key, values, fallback) => translateCli(`commands.portalDev.${key}`, values, { fallback });
17
+ export default class PortalDev extends Command {
18
+ static summary = 'Start a portal in development mode';
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %> customer',
21
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
22
+ ];
23
+ static args = {
24
+ portal: Args.string({
25
+ required: true,
26
+ description: 'Portal name',
27
+ }),
28
+ };
29
+ static flags = {
30
+ env: Flags.string({
31
+ char: 'e',
32
+ description: 'CLI env name; omitted uses the current env',
33
+ }),
34
+ yes: Flags.boolean({
35
+ char: 'y',
36
+ description: 'Confirm using --env when it targets a different env than the current env',
37
+ default: false,
38
+ }),
39
+ };
40
+ async run() {
41
+ const { args, flags } = await this.parse(PortalDev);
42
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
43
+ const confirmed = await ensureCrossEnvConfirmed({
44
+ command: this,
45
+ requestedEnv,
46
+ yes: flags.yes,
47
+ });
48
+ if (!confirmed) {
49
+ return;
50
+ }
51
+ const scope = resolveDefaultConfigScope();
52
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
53
+ const env = await getEnv(envName, { scope });
54
+ if (!env) {
55
+ this.error(portalDevText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
56
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
57
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
58
+ }
59
+ await devPortalWorkspace({
60
+ portal: args.portal,
61
+ env,
62
+ onStart: (result) => {
63
+ printInfo(portalDevText('messages.starting', { portal: result.portal }, `Starting portal "${result.portal}"...`));
64
+ printInfo(portalDevText('messages.mode', { mode: result.mode }, `Mode: ${result.mode}`));
65
+ printInfo(portalDevText('messages.app', { app: result.app }, `App: ${result.app}`));
66
+ printInfo(portalDevText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
67
+ printInfo(portalDevText('messages.dir', { dir: result.portalDir }, `Dir: ${result.portalDir}`));
68
+ },
69
+ });
70
+ }
71
+ }
@@ -0,0 +1,20 @@
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 { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Portal extends Command {
11
+ static summary = 'Manage portals';
12
+ async run() {
13
+ await this.parse(Portal);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'portal',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,82 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { findPortalListItem, formatPortalInfo } from '../../lib/portal-info.js';
15
+ import { listPortalWorkspaces, toPortalOutputItem } from '../../lib/portal-list.js';
16
+ const portalInfoText = (key, values, fallback) => translateCli(`commands.portalInfo.${key}`, values, { fallback });
17
+ export default class PortalInfo extends Command {
18
+ static summary = 'Show portal record and local file details';
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %> customer',
21
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
22
+ '<%= config.bin %> <%= command.id %> customer --json',
23
+ ];
24
+ static args = {
25
+ portal: Args.string({
26
+ required: true,
27
+ description: 'Portal name',
28
+ }),
29
+ };
30
+ static flags = {
31
+ env: Flags.string({
32
+ char: 'e',
33
+ description: 'CLI env name; omitted uses the current env',
34
+ }),
35
+ yes: Flags.boolean({
36
+ char: 'y',
37
+ description: 'Confirm using --env when it targets a different env than the current env',
38
+ default: false,
39
+ }),
40
+ 'json-output': Flags.boolean({
41
+ char: 'j',
42
+ aliases: ['json'],
43
+ description: 'Print portal details as JSON',
44
+ default: false,
45
+ }),
46
+ };
47
+ async run() {
48
+ const { args, flags } = await this.parse(PortalInfo);
49
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
50
+ const confirmed = await ensureCrossEnvConfirmed({
51
+ command: this,
52
+ requestedEnv,
53
+ yes: flags.yes,
54
+ });
55
+ if (!confirmed) {
56
+ return;
57
+ }
58
+ const scope = resolveDefaultConfigScope();
59
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
60
+ const env = await getEnv(envName, { scope });
61
+ if (!env) {
62
+ this.error(portalInfoText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
63
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
64
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
65
+ }
66
+ const result = await listPortalWorkspaces({
67
+ env,
68
+ envName,
69
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
70
+ });
71
+ const portal = findPortalListItem(result.items, args.portal);
72
+ if (!portal) {
73
+ this.error(portalInfoText('errors.notFound', { portal: args.portal }, `Portal "${args.portal}" was not found.`));
74
+ }
75
+ const outputItem = toPortalOutputItem(portal);
76
+ if (flags['json-output']) {
77
+ this.log(JSON.stringify(outputItem, null, 2));
78
+ return;
79
+ }
80
+ this.log(formatPortalInfo(portal));
81
+ }
82
+ }