@nocobase/cli 2.1.0-beta.20 → 2.1.0-beta.22

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 (74) hide show
  1. package/README.md +32 -50
  2. package/README.zh-CN.md +29 -46
  3. package/bin/run.js +15 -0
  4. package/dist/commands/app/down.js +260 -0
  5. package/dist/commands/app/info.js +140 -0
  6. package/dist/commands/app/logs.js +98 -0
  7. package/dist/commands/app/ps.js +60 -0
  8. package/dist/commands/app/restart.js +75 -0
  9. package/dist/commands/app/shared.js +95 -0
  10. package/dist/commands/app/start.js +252 -0
  11. package/dist/commands/app/stop.js +98 -0
  12. package/dist/commands/app/upgrade.js +595 -0
  13. package/dist/commands/build.js +3 -48
  14. package/dist/commands/db/shared.js +19 -5
  15. package/dist/commands/dev.js +3 -140
  16. package/dist/commands/down.js +3 -184
  17. package/dist/commands/download.js +4 -856
  18. package/dist/commands/env/add.js +33 -48
  19. package/dist/commands/env/auth.js +6 -13
  20. package/dist/commands/env/list.js +10 -15
  21. package/dist/commands/env/remove.js +4 -10
  22. package/dist/commands/env/update.js +7 -13
  23. package/dist/commands/env/use.js +5 -13
  24. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  25. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  26. package/dist/commands/init.js +262 -63
  27. package/dist/commands/install.js +352 -86
  28. package/dist/commands/logs.js +3 -81
  29. package/dist/commands/plugin/disable.js +64 -0
  30. package/dist/commands/plugin/enable.js +64 -0
  31. package/dist/commands/plugin/list.js +62 -0
  32. package/dist/commands/pm/disable.js +3 -54
  33. package/dist/commands/pm/enable.js +3 -54
  34. package/dist/commands/pm/list.js +3 -45
  35. package/dist/commands/ps.js +3 -107
  36. package/dist/commands/restart.js +3 -65
  37. package/dist/commands/scaffold/migration.js +1 -1
  38. package/dist/commands/scaffold/plugin.js +1 -1
  39. package/dist/commands/self/check.js +1 -1
  40. package/dist/commands/self/update.js +13 -3
  41. package/dist/commands/skills/check.js +11 -5
  42. package/dist/commands/skills/index.js +1 -1
  43. package/dist/commands/skills/install.js +20 -7
  44. package/dist/commands/skills/remove.js +71 -0
  45. package/dist/commands/skills/update.js +27 -7
  46. package/dist/commands/source/build.js +58 -0
  47. package/dist/commands/source/dev.js +157 -0
  48. package/dist/commands/source/download.js +866 -0
  49. package/dist/commands/source/test.js +467 -0
  50. package/dist/commands/start.js +3 -202
  51. package/dist/commands/stop.js +3 -81
  52. package/dist/commands/test.js +3 -457
  53. package/dist/commands/upgrade.js +3 -574
  54. package/dist/help/runtime-help.js +3 -0
  55. package/dist/lib/api-client.js +3 -2
  56. package/dist/lib/app-health.js +126 -0
  57. package/dist/lib/app-managed-resources.js +264 -0
  58. package/dist/lib/app-runtime.js +16 -5
  59. package/dist/lib/auth-store.js +162 -43
  60. package/dist/lib/bootstrap.js +13 -12
  61. package/dist/lib/cli-home.js +38 -6
  62. package/dist/lib/cli-locale.js +15 -1
  63. package/dist/lib/env-auth.js +3 -3
  64. package/dist/lib/env-config.js +80 -0
  65. package/dist/lib/generated-command.js +10 -2
  66. package/dist/lib/http-request.js +49 -0
  67. package/dist/lib/resource-command.js +10 -2
  68. package/dist/lib/runtime-generator.js +1 -1
  69. package/dist/lib/self-manager.js +1 -1
  70. package/dist/lib/skills-manager.js +173 -79
  71. package/dist/lib/startup-update.js +203 -0
  72. package/dist/locale/en-US.json +4 -1
  73. package/dist/locale/zh-CN.json +4 -1
  74. package/package.json +26 -3
@@ -7,33 +7,129 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
- import path, { isAbsolute } from 'node:path';
11
- import { resolveCliHomeDir } from './cli-home.js';
12
- const DEFAULT_CONFIG = {
13
- currentEnv: 'default',
14
- envs: {},
15
- };
10
+ import path from 'node:path';
11
+ import { NB_CLI_ROOT_ENV, resolveCliHomeDir, resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath, } from './cli-home.js';
12
+ function normalizeStoredEnvKind(value) {
13
+ const kind = String(value ?? '').trim();
14
+ if (kind === 'remote') {
15
+ return 'http';
16
+ }
17
+ if (kind === 'local' || kind === 'http' || kind === 'docker' || kind === 'ssh') {
18
+ return kind;
19
+ }
20
+ return undefined;
21
+ }
22
+ function normalizeOptionalString(value) {
23
+ const normalized = String(value ?? '').trim();
24
+ return normalized || undefined;
25
+ }
26
+ export function readEnvApiBaseUrl(config) {
27
+ if (!config) {
28
+ return undefined;
29
+ }
30
+ return (normalizeOptionalString(config.apiBaseUrl)
31
+ ?? normalizeOptionalString(config.baseUrl)
32
+ ?? normalizeOptionalString(config.apibaseUrl));
33
+ }
34
+ export function resolveEnvKind(config) {
35
+ if (!config) {
36
+ return undefined;
37
+ }
38
+ const explicitKind = normalizeStoredEnvKind(config.kind);
39
+ if (explicitKind) {
40
+ return explicitKind;
41
+ }
42
+ const source = String(config.source ?? '').trim();
43
+ if (source === 'docker') {
44
+ return 'docker';
45
+ }
46
+ if (source === 'npm' || source === 'git' || source === 'local') {
47
+ return 'local';
48
+ }
49
+ if (String(config.appRootPath ?? '').trim()) {
50
+ return 'local';
51
+ }
52
+ if (readEnvApiBaseUrl(config) || config.auth) {
53
+ return 'http';
54
+ }
55
+ return undefined;
56
+ }
57
+ function normalizeEnvConfigEntry(entry) {
58
+ if (!entry) {
59
+ return entry;
60
+ }
61
+ const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, ...rest } = entry;
62
+ const normalizedKind = resolveEnvKind(entry);
63
+ const apiBaseUrl = readEnvApiBaseUrl(entry);
64
+ return {
65
+ ...rest,
66
+ ...(normalizedKind ? { kind: normalizedKind } : {}),
67
+ ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
68
+ };
69
+ }
70
+ function normalizeAuthConfig(config) {
71
+ return {
72
+ name: config.name || config.dockerResourcePrefix,
73
+ currentEnv: config.currentEnv || 'default',
74
+ envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
75
+ };
76
+ }
16
77
  function getConfigFile(options = {}) {
17
78
  return path.join(resolveCliHomeDir(options.scope), 'config.json');
18
79
  }
19
- export async function loadAuthConfig(options = {}) {
80
+ function createDefaultConfig() {
81
+ return {
82
+ currentEnv: 'default',
83
+ envs: {},
84
+ };
85
+ }
86
+ function hasConfiguredEnvs(config) {
87
+ return Object.keys(config.envs).length > 0;
88
+ }
89
+ function shouldFallbackToLegacyProjectScope(options = {}) {
90
+ const requestedScope = options.scope ?? resolveDefaultConfigScope();
91
+ if (requestedScope !== 'global') {
92
+ return false;
93
+ }
94
+ return !process.env[NB_CLI_ROOT_ENV];
95
+ }
96
+ async function loadExactAuthConfig(options = {}) {
20
97
  try {
21
98
  const content = await fs.readFile(getConfigFile(options), 'utf8');
22
99
  const parsed = JSON.parse(content);
23
- return {
24
- name: parsed.name || parsed.dockerResourcePrefix,
25
- currentEnv: parsed.currentEnv || 'default',
26
- envs: parsed.envs || {},
27
- };
100
+ return normalizeAuthConfig(parsed);
28
101
  }
29
102
  catch (_error) {
30
- return DEFAULT_CONFIG;
103
+ return createDefaultConfig();
104
+ }
105
+ }
106
+ async function resolveEnvStorageScope(envName, options = {}) {
107
+ const requestedScope = options.scope ?? resolveDefaultConfigScope();
108
+ if (requestedScope !== 'global') {
109
+ return { ...options, scope: requestedScope };
110
+ }
111
+ const globalConfig = await loadExactAuthConfig({ scope: 'global' });
112
+ if (globalConfig.envs[envName]) {
113
+ return { ...options, scope: 'global' };
31
114
  }
115
+ const projectConfig = await loadExactAuthConfig({ scope: 'project' });
116
+ if (projectConfig.envs[envName]) {
117
+ return { ...options, scope: 'project' };
118
+ }
119
+ return { ...options, scope: 'global' };
120
+ }
121
+ export async function loadAuthConfig(options = {}) {
122
+ const config = await loadExactAuthConfig(options);
123
+ if (!shouldFallbackToLegacyProjectScope(options) || hasConfiguredEnvs(config)) {
124
+ return config;
125
+ }
126
+ const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
127
+ return hasConfiguredEnvs(legacyProjectConfig) ? legacyProjectConfig : config;
32
128
  }
33
129
  export async function saveAuthConfig(config, options = {}) {
34
130
  const filePath = getConfigFile(options);
35
131
  await fs.mkdir(path.dirname(filePath), { recursive: true });
36
- await fs.writeFile(filePath, JSON.stringify(config, null, 2));
132
+ await fs.writeFile(filePath, JSON.stringify(normalizeAuthConfig(config), null, 2));
37
133
  }
38
134
  export async function listEnvs(options = {}) {
39
135
  const config = await loadAuthConfig(options);
@@ -47,15 +143,16 @@ export async function getCurrentEnvName(options = {}) {
47
143
  return config.currentEnv || 'default';
48
144
  }
49
145
  export async function setCurrentEnv(envName, options = {}) {
50
- const config = await loadAuthConfig(options);
146
+ const writeOptions = await resolveEnvStorageScope(envName, options);
147
+ const config = await loadExactAuthConfig(writeOptions);
51
148
  if (!config.envs[envName]) {
52
149
  throw new Error(`Env "${envName}" is not configured`);
53
150
  }
54
151
  config.currentEnv = envName;
55
- await saveAuthConfig(config, options);
152
+ await saveAuthConfig(config, writeOptions);
56
153
  }
57
154
  export async function ensureWorkspaceName(defaultName, options = {}) {
58
- const config = await loadAuthConfig(options);
155
+ const config = await loadExactAuthConfig(options);
59
156
  const existing = config.name?.trim();
60
157
  if (existing) {
61
158
  return existing;
@@ -74,7 +171,10 @@ export class Env {
74
171
  return this.config.name;
75
172
  }
76
173
  get baseUrl() {
77
- return this.config.baseUrl;
174
+ return readEnvApiBaseUrl(this.config);
175
+ }
176
+ get apiBaseUrl() {
177
+ return readEnvApiBaseUrl(this.config);
78
178
  }
79
179
  get auth() {
80
180
  return this.config.auth;
@@ -82,22 +182,26 @@ export class Env {
82
182
  get runtime() {
83
183
  return this.config.runtime;
84
184
  }
185
+ get kind() {
186
+ return resolveEnvKind(this.config);
187
+ }
85
188
  get appRootPath() {
86
- const appRootPath = this.config.appRootPath;
87
- if (!appRootPath) {
88
- return process.cwd();
89
- }
90
- if (isAbsolute(appRootPath)) {
91
- return appRootPath;
189
+ if (this.kind === 'ssh') {
190
+ const configuredPath = String(this.config.appRootPath ?? '').trim();
191
+ if (configuredPath) {
192
+ return configuredPath;
193
+ }
92
194
  }
93
- return path.resolve(process.cwd(), appRootPath);
195
+ return resolveConfiguredEnvPath(this.config.appRootPath) ?? resolveEnvRelativePath('.');
94
196
  }
95
197
  get storagePath() {
96
- const storagePath = this.config.storagePath;
97
- if (isAbsolute(storagePath)) {
98
- return storagePath;
198
+ if (this.kind === 'ssh') {
199
+ const configuredPath = String(this.config.storagePath ?? '').trim();
200
+ if (configuredPath) {
201
+ return configuredPath;
202
+ }
99
203
  }
100
- return path.resolve(process.cwd(), storagePath);
204
+ return resolveConfiguredEnvPath(this.config.storagePath) ?? resolveEnvRelativePath('.');
101
205
  }
102
206
  get appPort() {
103
207
  return this.config.appPort;
@@ -130,9 +234,18 @@ export async function getEnv(envName, options = {}) {
130
234
  const resolved = envName?.trim() || config.currentEnv || 'default';
131
235
  const envConfig = config.envs[resolved];
132
236
  if (!envConfig) {
133
- return undefined;
237
+ if (!shouldFallbackToLegacyProjectScope(loadOptions)) {
238
+ return undefined;
239
+ }
240
+ const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
241
+ const legacyResolved = envName?.trim() || legacyProjectConfig.currentEnv || 'default';
242
+ const legacyEnvConfig = legacyProjectConfig.envs[legacyResolved];
243
+ if (!legacyEnvConfig) {
244
+ return undefined;
245
+ }
246
+ return new Env({ ...(normalizeEnvConfigEntry(legacyEnvConfig) ?? {}), name: legacyResolved });
134
247
  }
135
- return new Env({ ...envConfig, name: resolved });
248
+ return new Env({ ...(normalizeEnvConfigEntry(envConfig) ?? {}), name: resolved });
136
249
  }
137
250
  function areAuthConfigsEquivalent(left, right) {
138
251
  if (!left && !right) {
@@ -156,16 +269,19 @@ function areAuthConfigsEquivalent(left, right) {
156
269
  return false;
157
270
  }
158
271
  async function writeEnv(envName, updater, options = {}) {
159
- const config = await loadAuthConfig(options);
272
+ const writeOptions = await resolveEnvStorageScope(envName, options);
273
+ const config = await loadExactAuthConfig(writeOptions);
160
274
  const previous = config.envs[envName];
161
275
  config.envs[envName] = updater(previous);
162
276
  config.currentEnv = envName;
163
- await saveAuthConfig(config, options);
277
+ await saveAuthConfig(config, writeOptions);
164
278
  }
165
279
  export async function upsertEnv(envName, config, options = {}) {
166
280
  await writeEnv(envName, (previous) => {
167
- const { baseUrl, accessToken, ...rest } = config;
168
- const baseUrlChanged = previous?.baseUrl !== baseUrl;
281
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, ...rest } = config;
282
+ const nextApiBaseUrl = readEnvApiBaseUrl(config);
283
+ const previousApiBaseUrl = readEnvApiBaseUrl(previous);
284
+ const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
169
285
  const nextAuth = accessToken
170
286
  ? {
171
287
  type: 'token',
@@ -177,7 +293,7 @@ export async function upsertEnv(envName, config, options = {}) {
177
293
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
178
294
  return {
179
295
  ...previous,
180
- baseUrl,
296
+ apiBaseUrl: nextApiBaseUrl,
181
297
  auth: nextAuth,
182
298
  ...rest,
183
299
  runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
@@ -186,8 +302,9 @@ export async function upsertEnv(envName, config, options = {}) {
186
302
  }
187
303
  export async function updateEnvConnection(envName, updates, options = {}) {
188
304
  await writeEnv(envName, (previous) => {
189
- const nextBaseUrl = updates.baseUrl ?? previous?.baseUrl;
190
- const baseUrlChanged = previous?.baseUrl !== nextBaseUrl;
305
+ const nextApiBaseUrl = readEnvApiBaseUrl(updates) ?? readEnvApiBaseUrl(previous);
306
+ const previousApiBaseUrl = readEnvApiBaseUrl(previous);
307
+ const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
191
308
  const nextAuth = updates.accessToken
192
309
  ? {
193
310
  type: 'token',
@@ -199,7 +316,7 @@ export async function updateEnvConnection(envName, updates, options = {}) {
199
316
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
200
317
  return {
201
318
  ...previous,
202
- ...(nextBaseUrl !== undefined ? { baseUrl: nextBaseUrl } : {}),
319
+ ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
203
320
  auth: nextAuth,
204
321
  runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
205
322
  };
@@ -213,17 +330,19 @@ export async function setEnvOauthSession(envName, auth, options = {}) {
213
330
  }), options);
214
331
  }
215
332
  export async function setEnvRuntime(envName, runtime, options = {}) {
216
- const config = await loadAuthConfig(options);
333
+ const writeOptions = await resolveEnvStorageScope(envName, options);
334
+ const config = await loadExactAuthConfig(writeOptions);
217
335
  const current = config.envs[envName] ?? {};
218
336
  config.envs[envName] = {
219
337
  ...current,
220
338
  runtime,
221
339
  };
222
340
  config.currentEnv = envName;
223
- await saveAuthConfig(config, options);
341
+ await saveAuthConfig(config, writeOptions);
224
342
  }
225
343
  export async function removeEnv(envName, options = {}) {
226
- const config = await loadAuthConfig(options);
344
+ const writeOptions = await resolveEnvStorageScope(envName, options);
345
+ const config = await loadExactAuthConfig(writeOptions);
227
346
  if (!config.envs[envName]) {
228
347
  throw new Error(`Env "${envName}" is not configured`);
229
348
  }
@@ -232,7 +351,7 @@ export async function removeEnv(envName, options = {}) {
232
351
  const nextEnv = Object.keys(config.envs).sort()[0];
233
352
  config.currentEnv = nextEnv ?? 'default';
234
353
  }
235
- await saveAuthConfig(config, options);
354
+ await saveAuthConfig(config, writeOptions);
236
355
  return {
237
356
  removed: envName,
238
357
  currentEnv: config.currentEnv || 'default',
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { getCurrentEnvName, getEnv, setEnvRuntime, updateEnvConnection } from './auth-store.js';
10
10
  import { resolveAccessToken } from './env-auth.js';
11
+ import { fetchWithPreservedAuthRedirect } from './http-request.js';
11
12
  import { generateRuntime } from './runtime-generator.js';
12
13
  import { hasRuntimeSync, saveRuntime } from './runtime-store.js';
13
14
  import { confirmAction, printInfo, printVerbose, printWarningBlock, setVerboseMode, stopTask, updateTask } from './ui.js';
@@ -92,7 +93,7 @@ async function requestJson(url, options) {
92
93
  }
93
94
  let response;
94
95
  try {
95
- response = await fetch(url, {
96
+ response = await fetchWithPreservedAuthRedirect(url, {
96
97
  method: options.method ?? 'GET',
97
98
  headers,
98
99
  });
@@ -144,7 +145,7 @@ async function waitForServiceReady(baseUrl, token, role) {
144
145
  const startedAt = Date.now();
145
146
  let notified = false;
146
147
  while (Date.now() - startedAt < APP_RETRY_TIMEOUT) {
147
- const response = await fetch(healthCheckUrl, {
148
+ const response = await fetchWithPreservedAuthRedirect(healthCheckUrl, {
148
149
  method: 'GET',
149
150
  headers: token || role
150
151
  ? {
@@ -228,14 +229,14 @@ function collectErrorEntries(data) {
228
229
  }
229
230
  return [];
230
231
  }
231
- function hasInvalidTokenError(data) {
232
- return collectErrorEntries(data).some((entry) => entry?.code === 'INVALID_TOKEN');
232
+ function hasAuthenticationError(data) {
233
+ return collectErrorEntries(data).some((entry) => entry?.code === 'INVALID_TOKEN' || entry?.code === 'EMPTY_TOKEN');
233
234
  }
234
235
  function isNetworkFetchFailure(response) {
235
236
  return response.status === 0;
236
237
  }
237
238
  export function formatSwaggerSchemaError(response, context) {
238
- if (hasInvalidTokenError(response.data)) {
239
+ if (hasAuthenticationError(response.data)) {
239
240
  const entries = collectErrorEntries(response.data);
240
241
  const details = entries
241
242
  .map((entry) => {
@@ -251,7 +252,7 @@ export function formatSwaggerSchemaError(response, context) {
251
252
  `Authentication failed while loading the command runtime from \`swagger:get\`${envLabel}.`,
252
253
  `Base URL: ${context.baseUrl}`,
253
254
  details,
254
- 'Update the API key with `nb env add <name> --base-url <url> --auth-type token --token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--token <api-key>`.',
255
+ 'Update the API key with `nb env add <name> --api-base-url <url> --auth-type token --token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--token <api-key>`.',
255
256
  commandHint,
256
257
  ].join('\n');
257
258
  }
@@ -262,7 +263,7 @@ export function formatSwaggerSchemaError(response, context) {
262
263
  `Base URL: ${context.baseUrl}`,
263
264
  `Network error: ${rawMessage}`,
264
265
  'Check that the NocoBase app is running, the base URL is correct, and the server is reachable from this machine.',
265
- 'If you recently changed the server address, update it with `nb env add <name> --base-url <url>` and retry `nb env update`.',
266
+ 'If you recently changed the server address, update it with `nb env add <name> --api-base-url <url>` and retry `nb env update`.',
266
267
  'Use `nb env list` to inspect the current env configuration.',
267
268
  ].join('\n');
268
269
  }
@@ -272,7 +273,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
272
273
  if (!commandToken) {
273
274
  return [
274
275
  'No env is configured for runtime commands.',
275
- 'Run `nb env add <name> --base-url <url>` first.',
276
+ 'Run `nb env add <name> --api-base-url <url>` first.',
276
277
  'If you configure multiple environments later, switch with `nb env use <name>`.',
277
278
  ].join('\n');
278
279
  }
@@ -280,7 +281,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
280
281
  `Unable to resolve runtime command \`${commandToken}\`.`,
281
282
  'No env is configured, so the CLI cannot load runtime commands from `swagger:get`.',
282
283
  'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
283
- 'If this should be an application runtime command, run `nb env add <name> --base-url <url>` and then `nb env update`.',
284
+ 'If this should be an application runtime command, run `nb env add <name> --api-base-url <url>` and then `nb env update`.',
284
285
  ].join('\n');
285
286
  }
286
287
  export async function ensureRuntimeFromArgv(argv, options) {
@@ -294,7 +295,7 @@ export async function ensureRuntimeFromArgv(argv, options) {
294
295
  try {
295
296
  const envName = readFlag(argv, 'env') ?? (await getCurrentEnvName());
296
297
  const env = await getEnv(envName);
297
- const baseUrl = readFlag(argv, 'base-url') ?? env?.baseUrl;
298
+ const baseUrl = readFlag(argv, 'api-base-url') ?? env?.baseUrl;
298
299
  const role = readFlag(argv, 'role');
299
300
  const token = await resolveAccessToken({
300
301
  envName,
@@ -357,7 +358,7 @@ export async function updateEnvRuntime(options) {
357
358
  env
358
359
  ? `Env "${envName}" is missing a base URL.`
359
360
  : `Env "${envName}" is not configured. Run \`nb env add ${envName}\` first.`,
360
- env ? 'Update it with `nb env add <name> --base-url <url>` first.' : '',
361
+ env ? 'Update it with `nb env add <name> --api-base-url <url>` first.' : '',
361
362
  ]
362
363
  .filter(Boolean)
363
364
  .join('\n'));
@@ -370,7 +371,7 @@ export async function updateEnvRuntime(options) {
370
371
  await saveRuntime(runtime, { scope: options.scope });
371
372
  if (options.baseUrl !== undefined || options.token !== undefined) {
372
373
  await updateEnvConnection(envName, {
373
- baseUrl: options.baseUrl,
374
+ apiBaseUrl: options.baseUrl,
374
375
  accessToken: options.token,
375
376
  }, { scope: options.scope });
376
377
  }
@@ -1,14 +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
+ */
1
9
  import fs from 'node:fs';
2
10
  import os from 'node:os';
3
11
  import path from 'node:path';
4
12
  export const CLI_HOME_DIRNAME = '.nocobase';
13
+ export const NB_CONFIG_SCOPE_ENV = 'NB_CONFIG_SCOPE';
14
+ export const NB_CLI_ROOT_ENV = 'NB_CLI_ROOT';
15
+ export function resolveDefaultConfigScope() {
16
+ const raw = String(process.env[NB_CONFIG_SCOPE_ENV] ?? '').trim().toLowerCase();
17
+ return raw === 'project' ? 'project' : 'global';
18
+ }
19
+ function readConfiguredPath(name) {
20
+ const value = String(process.env[name] ?? '').trim();
21
+ return value || undefined;
22
+ }
5
23
  function resolveGlobalCliHomeRoot() {
6
- if (process.env.NOCOBASE_CTL_HOME) {
7
- return process.env.NOCOBASE_CTL_HOME;
8
- }
9
- return os.homedir();
24
+ return readConfiguredPath(NB_CLI_ROOT_ENV) ?? os.homedir();
10
25
  }
11
- export function resolveCliHomeRoot(scope = 'auto') {
26
+ export function resolveCliHomeRoot(scope = resolveDefaultConfigScope()) {
12
27
  const cwdRoot = process.cwd();
13
28
  if (scope === 'project') {
14
29
  return cwdRoot;
@@ -22,9 +37,26 @@ export function resolveCliHomeRoot(scope = 'auto') {
22
37
  }
23
38
  return resolveGlobalCliHomeRoot();
24
39
  }
25
- export function resolveCliHomeDir(scope = 'auto') {
40
+ export function resolveCliHomeDir(scope = resolveDefaultConfigScope()) {
26
41
  return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
27
42
  }
43
+ export function resolveEnvRoot(scope = resolveDefaultConfigScope()) {
44
+ const envRoot = readConfiguredPath(NB_CLI_ROOT_ENV);
45
+ if (envRoot) {
46
+ return path.resolve(envRoot);
47
+ }
48
+ return resolveCliHomeRoot(scope);
49
+ }
50
+ export function resolveEnvRelativePath(relativePath, scope = resolveDefaultConfigScope()) {
51
+ return path.resolve(resolveEnvRoot(scope), relativePath);
52
+ }
53
+ export function resolveConfiguredEnvPath(value, scope = resolveDefaultConfigScope()) {
54
+ const text = String(value ?? '').trim();
55
+ if (!text) {
56
+ return undefined;
57
+ }
58
+ return path.isAbsolute(text) ? text : resolveEnvRelativePath(text, scope);
59
+ }
28
60
  export function formatCliHomeScope(scope) {
29
61
  return scope === 'project' ? 'project' : 'global';
30
62
  }
@@ -7,6 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { readFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
10
12
  export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
11
13
  export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
12
14
  export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
@@ -30,8 +32,20 @@ function loadLocaleMessages(locale) {
30
32
  if (localeCache[locale]) {
31
33
  return localeCache[locale];
32
34
  }
35
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
36
+ const fallbackPath = path.resolve(moduleDir, '..', 'locale', `${locale}.json`);
33
37
  const fileUrl = new URL(`../locale/${locale}.json`, import.meta.url);
34
- const parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
41
+ }
42
+ catch (error) {
43
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
44
+ if (code !== 'ENOENT') {
45
+ throw error;
46
+ }
47
+ parsed = JSON.parse(readFileSync(fallbackPath, 'utf8'));
48
+ }
35
49
  localeCache[locale] = parsed;
36
50
  return parsed;
37
51
  }
@@ -750,7 +750,7 @@ export async function resolveAccessToken(options) {
750
750
  }
751
751
  const baseUrl = options.baseUrl ?? env.baseUrl;
752
752
  if (!baseUrl) {
753
- throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add ${envName} --base-url <url>\`.`);
753
+ throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add ${envName} --api-base-url <url>\`.`);
754
754
  }
755
755
  printVerbose(`Refreshing OAuth session for env "${envName}"`);
756
756
  return refreshOauthAccessToken({
@@ -771,7 +771,7 @@ export async function resolveServerRequestTarget(options) {
771
771
  scope: options.scope,
772
772
  });
773
773
  if (!baseUrl) {
774
- throw new Error('Missing base URL. Use --base-url or configure one with `nb env add`.');
774
+ throw new Error('Missing base URL. Use --api-base-url or configure one with `nb env add`.');
775
775
  }
776
776
  return { baseUrl, token };
777
777
  }
@@ -785,7 +785,7 @@ export async function authenticateEnvWithOauth(options) {
785
785
  ? `Environment "${envName}" does not have an API base URL yet.`
786
786
  : `Environment "${envName}" has not been set up yet.`,
787
787
  env
788
- ? `Run \`nb env add ${envName} --base-url <url>\` to finish setting it up.`
788
+ ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
789
789
  : `Run \`nb env add ${envName}\` first.`,
790
790
  ]
791
791
  .filter(Boolean)
@@ -0,0 +1,80 @@
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
+ const STRING_ENV_CONFIG_KEYS = [
10
+ 'source',
11
+ 'downloadVersion',
12
+ 'dockerRegistry',
13
+ 'dockerPlatform',
14
+ 'gitUrl',
15
+ 'npmRegistry',
16
+ 'appRootPath',
17
+ 'storagePath',
18
+ 'appPort',
19
+ 'appKey',
20
+ 'timezone',
21
+ 'dbDialect',
22
+ 'builtinDbImage',
23
+ 'dbHost',
24
+ 'dbPort',
25
+ 'dbDatabase',
26
+ 'dbUser',
27
+ 'dbPassword',
28
+ 'rootUsername',
29
+ 'rootEmail',
30
+ 'rootPassword',
31
+ 'rootNickname',
32
+ ];
33
+ const BOOLEAN_ENV_CONFIG_KEYS = [
34
+ 'builtinDb',
35
+ 'devDependencies',
36
+ 'build',
37
+ 'buildDts',
38
+ ];
39
+ function trimConfigValue(value) {
40
+ const text = String(value ?? '').trim();
41
+ return text || undefined;
42
+ }
43
+ function resolveEnvKind(input) {
44
+ const source = trimConfigValue(input.source);
45
+ const appRootPath = trimConfigValue(input.appRootPath);
46
+ if (source === 'docker') {
47
+ return 'docker';
48
+ }
49
+ if (source === 'npm' || source === 'git' || source === 'local' || appRootPath) {
50
+ return 'local';
51
+ }
52
+ return 'http';
53
+ }
54
+ export function buildStoredEnvConfig(input) {
55
+ const envConfig = {
56
+ kind: resolveEnvKind(input),
57
+ apiBaseUrl: trimConfigValue(input.apiBaseUrl) ?? '',
58
+ };
59
+ for (const key of STRING_ENV_CONFIG_KEYS) {
60
+ const value = trimConfigValue(input[key]);
61
+ if (value) {
62
+ envConfig[key] = value;
63
+ }
64
+ }
65
+ for (const key of BOOLEAN_ENV_CONFIG_KEYS) {
66
+ const value = input[key];
67
+ if (typeof value === 'boolean') {
68
+ envConfig[key] = value;
69
+ }
70
+ }
71
+ if (input.builtinDb === false) {
72
+ envConfig.builtinDbImage = undefined;
73
+ }
74
+ const authType = trimConfigValue(input.authType);
75
+ const accessToken = trimConfigValue(input.accessToken);
76
+ if (authType === 'token' && accessToken) {
77
+ envConfig.accessToken = accessToken;
78
+ }
79
+ return envConfig;
80
+ }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { Command, Flags } from '@oclif/core';
2
10
  import { executeApiRequest } from './api-client.js';
3
11
  import { applyPostProcessor } from './post-processors.js';
@@ -74,7 +82,7 @@ export function createGeneratedFlags(operation) {
74
82
  exclusive: ['body'],
75
83
  });
76
84
  }
77
- flags['base-url'] = Flags.string({
85
+ flags['api-base-url'] = Flags.string({
78
86
  description: 'NocoBase API base URL, for example http://localhost:13000/api',
79
87
  helpGroup: 'Global',
80
88
  });
@@ -114,7 +122,7 @@ export class GeneratedApiCommand extends Command {
114
122
  const { flags } = await this.parse(ctor);
115
123
  const response = await executeApiRequest({
116
124
  envName: flags.env,
117
- baseUrl: flags['base-url'],
125
+ baseUrl: flags['api-base-url'],
118
126
  role: flags.role,
119
127
  token: flags.token,
120
128
  flags,