@nocobase/cli 2.1.0-alpha.30 → 2.1.0-alpha.32

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 (38) hide show
  1. package/README.md +14 -0
  2. package/README.zh-CN.md +14 -0
  3. package/dist/commands/app/down.js +47 -9
  4. package/dist/commands/app/logs.js +17 -0
  5. package/dist/commands/app/restart.js +23 -1
  6. package/dist/commands/app/start.js +17 -0
  7. package/dist/commands/app/stop.js +17 -0
  8. package/dist/commands/app/upgrade.js +17 -0
  9. package/dist/commands/env/add.js +2 -1
  10. package/dist/commands/env/current.js +21 -0
  11. package/dist/commands/env/list.js +8 -14
  12. package/dist/commands/env/remove.js +1 -1
  13. package/dist/commands/env/status.js +90 -0
  14. package/dist/commands/env/use.js +10 -0
  15. package/dist/commands/install.js +2 -1
  16. package/dist/commands/license/activate.js +18 -19
  17. package/dist/commands/license/id.js +18 -0
  18. package/dist/commands/license/plugins/clean.js +18 -0
  19. package/dist/commands/license/plugins/list.js +19 -1
  20. package/dist/commands/license/plugins/sync.js +18 -0
  21. package/dist/commands/license/status.js +18 -0
  22. package/dist/commands/plugin/disable.js +24 -3
  23. package/dist/commands/plugin/enable.js +24 -3
  24. package/dist/commands/plugin/list.js +24 -3
  25. package/dist/commands/session/id.js +24 -0
  26. package/dist/commands/session/remove.js +57 -0
  27. package/dist/commands/session/setup.js +62 -0
  28. package/dist/commands/source/test.js +11 -1
  29. package/dist/lib/app-runtime.js +1 -1
  30. package/dist/lib/auth-store.js +28 -11
  31. package/dist/lib/env-guard.js +61 -0
  32. package/dist/lib/generated-command.js +16 -0
  33. package/dist/lib/resource-command.js +15 -0
  34. package/dist/lib/runtime-generator.js +1 -1
  35. package/dist/lib/session-id.js +17 -0
  36. package/dist/lib/session-integration.js +719 -0
  37. package/dist/lib/session-store.js +118 -0
  38. package/package.json +3 -3
@@ -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,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'],
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Flags } from '@oclif/core';
10
+ import { ensureCrossEnvConfirmed } from './env-guard.js';
10
11
  import { executeResourceRequest } from './resource-request.js';
11
12
  import { setVerboseMode } from './ui.js';
12
13
  function parseJson(value, flagName) {
@@ -95,6 +96,11 @@ export const resourceBaseFlags = {
95
96
  description: 'Show detailed progress output',
96
97
  default: false,
97
98
  }),
99
+ yes: Flags.boolean({
100
+ char: 'y',
101
+ description: 'Confirm using --env when it targets a different env than the current env',
102
+ default: false,
103
+ }),
98
104
  env: Flags.string({
99
105
  char: 'e',
100
106
  description: 'Environment name',
@@ -331,6 +337,15 @@ export function buildQueryArgs(flags) {
331
337
  }
332
338
  export async function runResourceCommand(command, action, flags, args) {
333
339
  setVerboseMode(Boolean(flags.verbose));
340
+ const confirmed = await ensureCrossEnvConfirmed({
341
+ command,
342
+ requestedEnv: flags.env,
343
+ yes: flags.yes,
344
+ });
345
+ if (!confirmed) {
346
+ command.log('Canceled.');
347
+ return;
348
+ }
334
349
  const response = await executeResourceRequest({
335
350
  envName: flags.env,
336
351
  baseUrl: flags['api-base-url'],
@@ -18,7 +18,7 @@ import { createHash } from 'node:crypto';
18
18
  import { loadBuildConfig } from './build-config.js';
19
19
  import { toKebabCase, toLogicalActionName, toLogicalResourceName, toResourceSegments } from './naming.js';
20
20
  import { collectOperations } from './openapi.js';
21
- const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file']);
21
+ const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file', 'yes']);
22
22
  function matchesPattern(value, pattern) {
23
23
  if (!value) {
24
24
  return false;
@@ -0,0 +1,17 @@
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 function resolveSessionIdentity() {
10
+ const sessionId = String(process.env.NB_SESSION_ID ?? '').trim();
11
+ if (sessionId) {
12
+ return {
13
+ id: sessionId,
14
+ };
15
+ }
16
+ return undefined;
17
+ }