@nocobase/cli 2.1.0-beta.29 → 2.1.0-beta.30

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 (50) hide show
  1. package/README.md +14 -0
  2. package/README.zh-CN.md +14 -0
  3. package/bin/run.js +3 -0
  4. package/bin/session-env.js +27 -0
  5. package/dist/commands/app/down.js +47 -9
  6. package/dist/commands/app/logs.js +17 -0
  7. package/dist/commands/app/restart.js +23 -1
  8. package/dist/commands/app/start.js +17 -0
  9. package/dist/commands/app/stop.js +17 -0
  10. package/dist/commands/app/upgrade.js +22 -2
  11. package/dist/commands/db/check.js +6 -4
  12. package/dist/commands/db/ps.js +1 -1
  13. package/dist/commands/env/add.js +3 -2
  14. package/dist/commands/env/auth.js +1 -1
  15. package/dist/commands/env/current.js +21 -0
  16. package/dist/commands/env/info.js +4 -3
  17. package/dist/commands/env/list.js +8 -14
  18. package/dist/commands/env/remove.js +2 -2
  19. package/dist/commands/env/status.js +90 -0
  20. package/dist/commands/env/update.js +1 -1
  21. package/dist/commands/env/use.js +11 -1
  22. package/dist/commands/install.js +10 -4
  23. package/dist/commands/license/activate.js +20 -24
  24. package/dist/commands/license/id.js +17 -2
  25. package/dist/commands/license/plugins/clean.js +17 -2
  26. package/dist/commands/license/plugins/list.js +17 -2
  27. package/dist/commands/license/plugins/sync.js +22 -5
  28. package/dist/commands/license/shared.js +15 -6
  29. package/dist/commands/license/status.js +17 -2
  30. package/dist/commands/plugin/disable.js +25 -4
  31. package/dist/commands/plugin/enable.js +25 -4
  32. package/dist/commands/plugin/list.js +25 -4
  33. package/dist/commands/session/id.js +24 -0
  34. package/dist/commands/session/remove.js +57 -0
  35. package/dist/commands/session/setup.js +62 -0
  36. package/dist/commands/source/dev.js +19 -1
  37. package/dist/commands/source/download.js +10 -8
  38. package/dist/lib/app-managed-resources.js +5 -3
  39. package/dist/lib/app-runtime.js +1 -1
  40. package/dist/lib/auth-store.js +28 -11
  41. package/dist/lib/docker-image.js +37 -0
  42. package/dist/lib/env-guard.js +61 -0
  43. package/dist/lib/generated-command.js +16 -0
  44. package/dist/lib/plugin-storage.js +1 -64
  45. package/dist/lib/resource-command.js +15 -0
  46. package/dist/lib/runtime-generator.js +1 -1
  47. package/dist/lib/session-id.js +17 -0
  48. package/dist/lib/session-integration.js +703 -0
  49. package/dist/lib/session-store.js +118 -0
  50. package/package.json +3 -3
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
12
  import { announceTargetEnv } from '../../lib/ui.js';
12
13
  export default class PluginEnable extends Command {
13
14
  static hidden = false;
@@ -27,18 +28,36 @@ export default class PluginEnable extends Command {
27
28
  static flags = {
28
29
  env: Flags.string({
29
30
  char: 'e',
30
- description: 'CLI env name (from `nb env` / `nb init`). Defaults to the current env when omitted',
31
+ description: 'CLI env name to enable plugins for. Defaults to the current env when omitted',
32
+ }),
33
+ yes: Flags.boolean({
34
+ char: 'y',
35
+ description: 'Confirm using --env when it targets a different env than the current env',
36
+ default: false,
31
37
  }),
32
38
  };
33
39
  async run() {
34
40
  const { args, flags } = await this.parse(PluginEnable);
41
+ const requestedEnv = flags.env?.trim() || undefined;
42
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
43
+ if (explicitEnvSelection) {
44
+ const confirmed = await ensureCrossEnvConfirmed({
45
+ command: this,
46
+ requestedEnv,
47
+ yes: flags.yes,
48
+ });
49
+ if (!confirmed) {
50
+ this.log('Canceled.');
51
+ return;
52
+ }
53
+ }
35
54
  const packages = args.packages;
36
55
  if (!Array.isArray(packages) || packages.length === 0) {
37
56
  this.error('Pass at least one plugin package name.');
38
57
  }
39
- const runtime = await resolveManagedAppRuntime(flags.env);
58
+ const runtime = await resolveManagedAppRuntime(requestedEnv);
40
59
  if (!runtime) {
41
- this.error(formatMissingManagedAppEnvMessage(flags.env));
60
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
42
61
  }
43
62
  announceTargetEnv(runtime.envName);
44
63
  if (runtime.kind === 'local') {
@@ -61,6 +80,8 @@ export default class PluginEnable extends Command {
61
80
  }
62
81
  return;
63
82
  }
64
- await this.config.runCommand('api:pm:enable', ['--await-response', '--filter-by-tk', packages.join(',')]);
83
+ await this.config.runCommand('api:pm:enable', explicitEnvSelection
84
+ ? ['--await-response', '--filter-by-tk', packages.join(','), '--env', runtime.envName, '--yes']
85
+ : ['--await-response', '--filter-by-tk', packages.join(',')]);
65
86
  }
66
87
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
12
  export default class PluginList extends Command {
12
13
  static hidden = false;
13
14
  static args = {};
@@ -21,14 +22,32 @@ export default class PluginList extends Command {
21
22
  static flags = {
22
23
  env: Flags.string({
23
24
  char: 'e',
24
- description: 'CLI env name (from `nb env` / `nb init`). Defaults to the current env when omitted',
25
+ description: 'CLI env name to inspect plugins for. Defaults to the current env when omitted',
26
+ }),
27
+ yes: Flags.boolean({
28
+ char: 'y',
29
+ description: 'Confirm using --env when it targets a different env than the current env',
30
+ default: false,
25
31
  }),
26
32
  };
27
33
  async run() {
28
34
  const { flags } = await this.parse(PluginList);
29
- const runtime = await resolveManagedAppRuntime(flags.env);
35
+ const requestedEnv = flags.env?.trim() || undefined;
36
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
37
+ if (explicitEnvSelection) {
38
+ const confirmed = await ensureCrossEnvConfirmed({
39
+ command: this,
40
+ requestedEnv,
41
+ yes: flags.yes,
42
+ });
43
+ if (!confirmed) {
44
+ this.log('Canceled.');
45
+ return;
46
+ }
47
+ }
48
+ const runtime = await resolveManagedAppRuntime(requestedEnv);
30
49
  if (!runtime) {
31
- this.error(formatMissingManagedAppEnvMessage(flags.env));
50
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
32
51
  }
33
52
  if (runtime.kind === 'local') {
34
53
  try {
@@ -57,6 +76,8 @@ export default class PluginList extends Command {
57
76
  'Use a local, Docker, or HTTP env for plugin inspection right now.',
58
77
  ].join('\n'));
59
78
  }
60
- await this.config.runCommand('api:pm:list', ['--mode=summary']);
79
+ await this.config.runCommand('api:pm:list', explicitEnvSelection
80
+ ? ['--mode=summary', '--env', runtime.envName, '--yes']
81
+ : ['--mode=summary']);
61
82
  }
62
83
  }
@@ -0,0 +1,24 @@
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 } from '@oclif/core';
10
+ import { resolveSessionIdentity } from '../../lib/session-id.js';
11
+ export default class SessionId extends Command {
12
+ static summary = 'Show the current effective session id';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %>',
15
+ ];
16
+ async run() {
17
+ await this.parse(SessionId);
18
+ const identity = resolveSessionIdentity();
19
+ if (!identity) {
20
+ this.error('No effective session id is available. Run `nb session setup`, then open a new shell session or runtime.');
21
+ }
22
+ this.log(identity.id);
23
+ }
24
+ }
@@ -0,0 +1,57 @@
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
+ /**
10
+ * This file is part of the NocoBase (R) project.
11
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
12
+ * Authors: NocoBase Team.
13
+ *
14
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
15
+ * For more information, please refer to: https://www.nocobase.com/agreement.
16
+ */
17
+ import { Command, Flags } from '@oclif/core';
18
+ import { detectSessionShell, removeSessionIntegration } from '../../lib/session-integration.js';
19
+ export default class SessionRemove extends Command {
20
+ static summary = 'Remove shell session integration for NB_SESSION_ID';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %>',
23
+ '<%= config.bin %> <%= command.id %> --shell zsh',
24
+ ];
25
+ static flags = {
26
+ shell: Flags.string({
27
+ description: 'Target shell to remove configuration from',
28
+ options: ['bash', 'zsh', 'fish', 'powershell', 'cmd'],
29
+ }),
30
+ };
31
+ async run() {
32
+ const { flags } = await this.parse(SessionRemove);
33
+ const shell = flags.shell ?? detectSessionShell();
34
+ if (!shell) {
35
+ this.error('Could not detect the current shell. Re-run with `--shell bash|zsh|fish|powershell|cmd`.');
36
+ }
37
+ const result = await removeSessionIntegration(shell);
38
+ this.log(`Session integration removed for ${result.shell}.`);
39
+ if (result.profileUpdated) {
40
+ for (const profileFile of result.profileFiles) {
41
+ this.log(`Profile updated: ${profileFile}`);
42
+ }
43
+ }
44
+ if (result.managedFileRemoved) {
45
+ this.log(`Managed file removed: ${result.managedFile}`);
46
+ }
47
+ if (result.cmdAutoRunRemoved) {
48
+ this.log(`cmd AutoRun updated: ${result.cmdAutoRunLocation}`);
49
+ }
50
+ if (result.agentConfigUpdated) {
51
+ this.log(`Opencode config updated: ${result.agentConfigFile}`);
52
+ }
53
+ if (result.agentPluginRemoved) {
54
+ this.log(`Opencode agent plugin removed: ${result.agentPluginFile}`);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,62 @@
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
+ /**
10
+ * This file is part of the NocoBase (R) project.
11
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
12
+ * Authors: NocoBase Team.
13
+ *
14
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
15
+ * For more information, please refer to: https://www.nocobase.com/agreement.
16
+ */
17
+ import { Command, Flags } from '@oclif/core';
18
+ import { detectSessionShell, setupSessionIntegration } from '../../lib/session-integration.js';
19
+ export default class SessionSetup extends Command {
20
+ static summary = 'Set up shell session integration for NB_SESSION_ID';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %>',
23
+ '<%= config.bin %> <%= command.id %> --shell zsh',
24
+ '<%= config.bin %> <%= command.id %> --shell powershell',
25
+ ];
26
+ static flags = {
27
+ shell: Flags.string({
28
+ description: 'Target shell to configure',
29
+ options: ['bash', 'zsh', 'fish', 'powershell', 'cmd'],
30
+ }),
31
+ };
32
+ async run() {
33
+ const { flags } = await this.parse(SessionSetup);
34
+ const shell = flags.shell ?? detectSessionShell();
35
+ if (!shell) {
36
+ this.error('Could not detect the current shell. Re-run with `--shell bash|zsh|fish|powershell|cmd`.');
37
+ }
38
+ const result = await setupSessionIntegration(shell);
39
+ this.log(`Session integration configured for ${result.shell}.`);
40
+ this.log(`Managed file: ${result.managedFile}`);
41
+ if (result.cmdAutoRunConfigured) {
42
+ this.log(`cmd AutoRun updated: ${result.cmdAutoRunLocation}`);
43
+ this.log('Open a new cmd session to initialize NB_SESSION_ID automatically.');
44
+ }
45
+ if (result.agentConfigured) {
46
+ this.log(`Opencode agent plugin installed: ${result.agentPluginFile}`);
47
+ this.log(`Opencode config updated: ${result.agentConfigFile}`);
48
+ }
49
+ else if (result.agentSkippedReason === 'opencode_dir_not_found') {
50
+ this.log('Opencode config directory not found. Skipped agent session integration.');
51
+ }
52
+ if (result.profileFiles.length > 0) {
53
+ for (const profileFile of result.profileFiles) {
54
+ this.log(`Profile updated: ${profileFile}`);
55
+ }
56
+ this.log('Open a new shell session or reload your profile to initialize NB_SESSION_ID automatically.');
57
+ }
58
+ if (result.manualStep) {
59
+ this.log(result.manualStep);
60
+ }
61
+ }
62
+ }
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
10
11
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
12
  import { announceTargetEnv, printInfo } from '../../lib/ui.js';
12
13
  function formatUnsupportedRuntimeMessage(kind, envName) {
@@ -72,7 +73,13 @@ export default class SourceDev extends Command {
72
73
  static flags = {
73
74
  env: Flags.string({
74
75
  char: 'e',
75
- description: 'CLI env name for dev mode. Defaults to the current env when omitted',
76
+ description: 'CLI env name to run dev mode for. Defaults to the current env when omitted',
77
+ required: false,
78
+ }),
79
+ yes: Flags.boolean({
80
+ char: 'y',
81
+ description: 'Confirm using --env when it targets a different env than the current env',
82
+ default: false,
76
83
  required: false,
77
84
  }),
78
85
  'db-sync': Flags.boolean({
@@ -103,6 +110,17 @@ export default class SourceDev extends Command {
103
110
  async run() {
104
111
  const { flags } = await this.parse(SourceDev);
105
112
  const requestedEnv = flags.env?.trim() || undefined;
113
+ if (requestedEnv && hasExplicitEnvSelection(this.argv)) {
114
+ const confirmed = await ensureCrossEnvConfirmed({
115
+ command: this,
116
+ requestedEnv,
117
+ yes: flags.yes,
118
+ });
119
+ if (!confirmed) {
120
+ this.log('Canceled.');
121
+ return;
122
+ }
123
+ }
106
124
  const runtime = await resolveManagedAppRuntime(requestedEnv);
107
125
  if (!runtime) {
108
126
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
@@ -13,10 +13,9 @@ import path from 'node:path';
13
13
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
14
14
  import { runPromptCatalog, } from "../../lib/prompt-catalog.js";
15
15
  import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, resolveCliLocale, translateCli, } from "../../lib/cli-locale.js";
16
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_REGISTRY_ZH_CN, resolveDockerImageRef, } from "../../lib/docker-image.js";
16
17
  import { run } from "../../lib/run-npm.js";
17
18
  import { printVerbose, setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
18
- const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
19
- const DEFAULT_DOCKER_REGISTRY_ZH_CN = 'registry.cn-shanghai.aliyuncs.com/nocobase/nocobase';
20
19
  const DEFAULT_DOCKER_PLATFORM = 'auto';
21
20
  const DEFAULT_DOWNLOAD_VERSION = 'beta';
22
21
  const downloadText = (key, values) => localeText(`commands.download.${key}`, values);
@@ -422,9 +421,11 @@ export default class SourceDownload extends Command {
422
421
  return outputAbs;
423
422
  }
424
423
  dockerTarPath(flags, outputAbs) {
425
- const image = String(flags['docker-registry'] ?? '').trim() || defaultDockerRegistryForLang(process.env.NB_LOCALE);
426
- const tag = flags.version ?? 'latest';
427
- const safeBase = `${image.replace(/[/:]/g, '-')}-${tag.replace(/[/\\]/g, '-')}`;
424
+ const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
425
+ defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
426
+ defaultVersion: 'latest',
427
+ });
428
+ const safeBase = imageRef.replace(/[\\/:]/g, '-');
428
429
  return path.join(outputAbs, `${safeBase}.tar`);
429
430
  }
430
431
  /**
@@ -709,9 +710,10 @@ export default class SourceDownload extends Command {
709
710
  return argv;
710
711
  }
711
712
  async downloadFromDocker(flags) {
712
- const image = String(flags['docker-registry'] ?? '').trim() || defaultDockerRegistryForLang(process.env.NB_LOCALE);
713
- const tag = flags.version ?? 'latest';
714
- const imageRef = `${image}:${tag}`;
713
+ const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
714
+ defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
715
+ defaultVersion: 'latest',
716
+ });
715
717
  const platform = dockerPlatformArg(flags['docker-platform']);
716
718
  const pullArgs = ['pull'];
717
719
  if (platform) {
@@ -10,10 +10,9 @@ import { mkdir, readdir } from 'node:fs/promises';
10
10
  import { dockerContainerExists, startDockerContainer } from './app-runtime.js';
11
11
  import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
12
12
  import { resolveConfiguredEnvPath } from './cli-home.js';
13
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "./docker-image.js";
13
14
  import { commandSucceeds, run } from './run-npm.js';
14
15
  import Install from '../commands/install.js';
15
- const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
16
- const DEFAULT_DOCKER_VERSION = 'alpha';
17
16
  const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
18
17
  function commandStdio(verbose) {
19
18
  return verbose ? 'inherit' : 'ignore';
@@ -102,7 +101,10 @@ export function buildSavedDockerRunArgs(runtime) {
102
101
  const dbPassword = trimValue(config.dbPassword);
103
102
  const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
104
103
  const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
105
- const imageRef = `${dockerRegistry}:${version}`;
104
+ const imageRef = resolveDockerImageRef(dockerRegistry, version, {
105
+ defaultRegistry: DEFAULT_DOCKER_REGISTRY,
106
+ defaultVersion: DEFAULT_DOCKER_VERSION,
107
+ });
106
108
  const missing = [];
107
109
  if (!storagePath) {
108
110
  missing.push('storagePath');
@@ -61,7 +61,7 @@ export async function resolveManagedAppRuntime(envName) {
61
61
  if (!env) {
62
62
  return undefined;
63
63
  }
64
- const resolvedName = env.name || envName?.trim() || config.currentEnv || 'default';
64
+ const resolvedName = env.name || envName?.trim() || config.lastEnv || 'default';
65
65
  const source = normalizeEnvSource(env);
66
66
  const dockerNetworkName = sanitizeDockerResourceName(getEffectiveCliConfigValue(config, 'docker.network') || defaultDockerNetworkName());
67
67
  const dockerContainerPrefix = sanitizeDockerResourceName(getEffectiveCliConfigValue(config, 'docker.container-prefix') || defaultDockerContainerPrefix());
@@ -9,6 +9,7 @@
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath, } from './cli-home.js';
12
+ import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
12
13
  function normalizeStoredEnvKind(value) {
13
14
  const kind = String(value ?? '').trim();
14
15
  if (kind === 'remote') {
@@ -84,7 +85,7 @@ function normalizeAuthConfig(config) {
84
85
  }
85
86
  : {}),
86
87
  },
87
- currentEnv: config.currentEnv || 'default',
88
+ lastEnv: config.lastEnv || config.currentEnv || 'default',
88
89
  envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
89
90
  };
90
91
  }
@@ -93,7 +94,7 @@ function getConfigFile(options = {}) {
93
94
  }
94
95
  function createDefaultConfig() {
95
96
  return {
96
- currentEnv: 'default',
97
+ lastEnv: 'default',
97
98
  envs: {},
98
99
  };
99
100
  }
@@ -121,20 +122,24 @@ export async function saveAuthConfig(config, options = {}) {
121
122
  export async function listEnvs(options = {}) {
122
123
  const config = await loadAuthConfig(options);
123
124
  return {
124
- currentEnv: config.currentEnv || 'default',
125
+ lastEnv: config.lastEnv || 'default',
125
126
  envs: config.envs,
126
127
  };
127
128
  }
128
129
  export async function getCurrentEnvName(options = {}) {
129
130
  const config = await loadAuthConfig(options);
130
- return config.currentEnv || 'default';
131
+ return await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
132
+ scope: options.scope,
133
+ lastEnv: config.lastEnv,
134
+ });
131
135
  }
132
136
  export async function setCurrentEnv(envName, options = {}) {
133
137
  const config = await loadExactAuthConfig(options);
134
138
  if (!config.envs[envName]) {
135
139
  throw new Error(`Env "${envName}" is not configured`);
136
140
  }
137
- config.currentEnv = envName;
141
+ config.lastEnv = envName;
142
+ await setSessionCurrentEnv(envName, options.scope);
138
143
  await saveAuthConfig(config, options);
139
144
  }
140
145
  export class Env {
@@ -211,7 +216,10 @@ export class Env {
211
216
  export async function getEnv(envName, options = {}) {
212
217
  const { config: snapshot, ...loadOptions } = options;
213
218
  const config = snapshot ?? (await loadAuthConfig(loadOptions));
214
- const resolved = envName?.trim() || config.currentEnv || 'default';
219
+ const resolved = envName?.trim() || (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
220
+ scope: loadOptions.scope,
221
+ lastEnv: config.lastEnv,
222
+ }));
215
223
  const envConfig = config.envs[resolved];
216
224
  if (!envConfig) {
217
225
  return undefined;
@@ -243,7 +251,6 @@ async function writeEnv(envName, updater, options = {}) {
243
251
  const config = await loadExactAuthConfig(options);
244
252
  const previous = config.envs[envName];
245
253
  config.envs[envName] = updater(previous);
246
- config.currentEnv = envName;
247
254
  await saveAuthConfig(config, options);
248
255
  }
249
256
  export async function upsertEnv(envName, config, options = {}) {
@@ -306,7 +313,6 @@ export async function setEnvRuntime(envName, runtime, options = {}) {
306
313
  ...current,
307
314
  runtime,
308
315
  };
309
- config.currentEnv = envName;
310
316
  await saveAuthConfig(config, options);
311
317
  }
312
318
  export async function removeEnv(envName, options = {}) {
@@ -315,14 +321,25 @@ export async function removeEnv(envName, options = {}) {
315
321
  throw new Error(`Env "${envName}" is not configured`);
316
322
  }
317
323
  delete config.envs[envName];
318
- if (config.currentEnv === envName) {
324
+ if (config.lastEnv === envName) {
319
325
  const nextEnv = Object.keys(config.envs).sort()[0];
320
- config.currentEnv = nextEnv ?? 'default';
326
+ config.lastEnv = nextEnv ?? 'default';
321
327
  }
322
328
  await saveAuthConfig(config, options);
329
+ const remainingEnvNames = Object.keys(config.envs).sort();
330
+ const fallbackEnv = remainingEnvNames.length
331
+ ? await resolveEffectiveCurrentEnv(remainingEnvNames, {
332
+ scope: options.scope,
333
+ lastEnv: config.lastEnv,
334
+ })
335
+ : undefined;
336
+ await cleanupCurrentSessionAfterEnvRemoval(envName, {
337
+ scope: options.scope,
338
+ fallbackEnv,
339
+ });
323
340
  return {
324
341
  removed: envName,
325
- currentEnv: config.currentEnv || 'default',
342
+ lastEnv: config.lastEnv || 'default',
326
343
  hasEnvs: Object.keys(config.envs).length > 0,
327
344
  };
328
345
  }
@@ -0,0 +1,37 @@
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 DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
10
+ export const DEFAULT_DOCKER_REGISTRY_ZH_CN = 'registry.cn-shanghai.aliyuncs.com/nocobase/nocobase';
11
+ export const DEFAULT_DOCKER_VERSION = 'alpha';
12
+ export const DOCKER_IMAGE_FULL_SUFFIX = '-full';
13
+ const OFFICIAL_FULL_IMAGE_REGISTRIES = new Set([
14
+ DEFAULT_DOCKER_REGISTRY,
15
+ DEFAULT_DOCKER_REGISTRY_ZH_CN,
16
+ ]);
17
+ function trimValue(value) {
18
+ return String(value ?? '').trim();
19
+ }
20
+ export function shouldUseFullDockerImageTag(registry) {
21
+ return OFFICIAL_FULL_IMAGE_REGISTRIES.has(trimValue(registry));
22
+ }
23
+ export function normalizeDockerImageTag(registry, version) {
24
+ const tag = trimValue(version) || DEFAULT_DOCKER_VERSION;
25
+ if (!shouldUseFullDockerImageTag(registry)) {
26
+ return tag;
27
+ }
28
+ return tag.endsWith(DOCKER_IMAGE_FULL_SUFFIX)
29
+ ? tag
30
+ : `${tag}${DOCKER_IMAGE_FULL_SUFFIX}`;
31
+ }
32
+ export function resolveDockerImageRef(registry, version, options) {
33
+ const resolvedRegistry = trimValue(registry) || options?.defaultRegistry || DEFAULT_DOCKER_REGISTRY;
34
+ const rawVersion = trimValue(version) || options?.defaultVersion || DEFAULT_DOCKER_VERSION;
35
+ const normalizedTag = normalizeDockerImageTag(resolvedRegistry, rawVersion);
36
+ return `${resolvedRegistry}:${normalizedTag}`;
37
+ }
@@ -0,0 +1,61 @@
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 * as p from '@clack/prompts';
10
+ import { stdin as input, stdout as output } from 'node:process';
11
+ import { getCurrentEnvName } from './auth-store.js';
12
+ function normalizeEnvName(value) {
13
+ const text = String(value ?? '').trim();
14
+ return text || undefined;
15
+ }
16
+ export function hasExplicitEnvSelection(argv) {
17
+ return argv.some((token, index) => (token === '--env'
18
+ || token === '-e'
19
+ || token.startsWith('--env=')
20
+ || (token.startsWith('-e') && token.length > 2 && index >= 0)));
21
+ }
22
+ function isInteractiveTerminal() {
23
+ return Boolean(input.isTTY && output.isTTY);
24
+ }
25
+ function formatCrossEnvPromptMessage(currentEnv, requestedEnv) {
26
+ return `Current env is "${currentEnv}", but this command targets "${requestedEnv}" via --env. Continue without switching the current env?`;
27
+ }
28
+ export function formatCrossEnvRefusalMessage(currentEnv, requestedEnv) {
29
+ return [
30
+ `Refusing to run against env "${requestedEnv}" because the current env is "${currentEnv}" and interactive confirmation is unavailable in the current agent session.`,
31
+ '',
32
+ 'For safety, the agent will not switch envs automatically and will not add --yes on your behalf.',
33
+ '',
34
+ 'To continue:',
35
+ `- run \`nb env use ${requestedEnv}\` yourself and then re-run the command, or`,
36
+ `- re-run the same command with \`--env ${requestedEnv} --yes\` to confirm this one-off cross-env operation.`,
37
+ ].join('\n');
38
+ }
39
+ export async function ensureCrossEnvConfirmed(options) {
40
+ const requestedEnv = normalizeEnvName(options.requestedEnv);
41
+ if (!requestedEnv) {
42
+ return true;
43
+ }
44
+ const currentEnv = normalizeEnvName(await getCurrentEnvName());
45
+ const interactiveTerminal = isInteractiveTerminal();
46
+ const bypassInteractivePrompt = interactiveTerminal && Boolean(options.yes);
47
+ if (!currentEnv || currentEnv === requestedEnv || bypassInteractivePrompt) {
48
+ return true;
49
+ }
50
+ if (!interactiveTerminal) {
51
+ options.command.error(formatCrossEnvRefusalMessage(currentEnv, requestedEnv));
52
+ }
53
+ const answer = await p.confirm({
54
+ message: formatCrossEnvPromptMessage(currentEnv, requestedEnv),
55
+ initialValue: false,
56
+ });
57
+ if (p.isCancel(answer)) {
58
+ return false;
59
+ }
60
+ return Boolean(answer);
61
+ }
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { Command, Flags } from '@oclif/core';
18
18
  import { executeApiRequest } from './api-client.js';
19
+ import { ensureCrossEnvConfirmed } from './env-guard.js';
19
20
  import { applyPostProcessor } from './post-processors.js';
20
21
  import { registerPostProcessors } from '../post-processors/index.js';
21
22
  function buildParameterFlag(parameter, options) {
@@ -110,6 +111,12 @@ export function createGeneratedFlags(operation) {
110
111
  default: false,
111
112
  helpGroup: 'Global',
112
113
  });
114
+ flags.yes = Flags.boolean({
115
+ char: 'y',
116
+ description: 'Confirm using --env when it targets a different env than the current env',
117
+ default: false,
118
+ helpGroup: 'Global',
119
+ });
113
120
  flags.env = Flags.string({
114
121
  char: 'e',
115
122
  description: 'Environment name',
@@ -139,6 +146,15 @@ export class GeneratedApiCommand extends Command {
139
146
  registerPostProcessors();
140
147
  const ctor = this.constructor;
141
148
  const { flags } = await this.parse(ctor);
149
+ const confirmed = await ensureCrossEnvConfirmed({
150
+ command: this,
151
+ requestedEnv: flags.env,
152
+ yes: flags.yes,
153
+ });
154
+ if (!confirmed) {
155
+ this.log('Canceled.');
156
+ return;
157
+ }
142
158
  const response = await executeApiRequest({
143
159
  envName: flags.env,
144
160
  baseUrl: flags['api-base-url'],