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

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 (42) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +2 -2
  3. package/bin/run.js +15 -0
  4. package/dist/commands/db/shared.js +19 -5
  5. package/dist/commands/dev.js +8 -1
  6. package/dist/commands/down.js +10 -6
  7. package/dist/commands/env/add.js +14 -34
  8. package/dist/commands/env/auth.js +6 -13
  9. package/dist/commands/env/list.js +10 -15
  10. package/dist/commands/env/remove.js +4 -10
  11. package/dist/commands/env/update.js +7 -13
  12. package/dist/commands/env/use.js +5 -13
  13. package/dist/commands/init.js +190 -62
  14. package/dist/commands/install.js +65 -26
  15. package/dist/commands/logs.js +8 -1
  16. package/dist/commands/pm/list.js +8 -1
  17. package/dist/commands/ps.js +18 -15
  18. package/dist/commands/self/check.js +1 -1
  19. package/dist/commands/self/update.js +13 -3
  20. package/dist/commands/skills/check.js +11 -5
  21. package/dist/commands/skills/index.js +1 -1
  22. package/dist/commands/skills/install.js +20 -7
  23. package/dist/commands/skills/update.js +20 -7
  24. package/dist/commands/start.js +8 -1
  25. package/dist/commands/stop.js +8 -1
  26. package/dist/commands/upgrade.js +12 -1
  27. package/dist/lib/api-client.js +3 -2
  28. package/dist/lib/app-runtime.js +16 -5
  29. package/dist/lib/auth-store.js +159 -43
  30. package/dist/lib/bootstrap.js +13 -12
  31. package/dist/lib/cli-home.js +33 -2
  32. package/dist/lib/env-auth.js +3 -3
  33. package/dist/lib/generated-command.js +10 -2
  34. package/dist/lib/http-request.js +49 -0
  35. package/dist/lib/resource-command.js +10 -2
  36. package/dist/lib/runtime-generator.js +1 -1
  37. package/dist/lib/self-manager.js +1 -1
  38. package/dist/lib/skills-manager.js +140 -73
  39. package/dist/lib/startup-update.js +203 -0
  40. package/dist/locale/en-US.json +4 -1
  41. package/dist/locale/zh-CN.json +4 -1
  42. package/package.json +2 -2
@@ -50,13 +50,20 @@ export default class Stop extends Command {
50
50
  if (!runtime) {
51
51
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
52
52
  }
53
- if (runtime.kind === 'remote') {
53
+ if (runtime.kind === 'http') {
54
54
  this.error([
55
55
  `Can't stop "${runtime.envName}" from this machine.`,
56
56
  'This env only has an API connection, so there is no saved local app or Docker runtime to stop here.',
57
57
  'If the app is running on a server, stop it there or reconnect this env to a local runtime first.',
58
58
  ].join('\n'));
59
59
  }
60
+ if (runtime.kind === 'ssh') {
61
+ this.error([
62
+ `Can't stop "${runtime.envName}" yet.`,
63
+ 'SSH env support is reserved but not implemented yet.',
64
+ 'Use a local or Docker env if you need CLI-managed stop right now.',
65
+ ].join('\n'));
66
+ }
60
67
  if (runtime.kind === 'docker') {
61
68
  startTask(`Stopping NocoBase for "${runtime.envName}"...`);
62
69
  try {
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../lib/app-runtime.js';
11
+ import { resolveConfiguredEnvPath } from '../lib/cli-home.js';
11
12
  import { commandOutput, commandSucceeds, run } from '../lib/run-npm.js';
12
13
  import { failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../lib/ui.js';
13
14
  const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
@@ -123,6 +124,9 @@ function normalizeDockerPlatform(value) {
123
124
  return undefined;
124
125
  }
125
126
  function readEnvValue(env, key) {
127
+ if (key === 'appRootPath' || key === 'storagePath') {
128
+ return trimValue(resolveConfiguredEnvPath(env.config[key]));
129
+ }
126
130
  return trimValue(env.config[key]);
127
131
  }
128
132
  async function sleep(ms) {
@@ -560,13 +564,20 @@ export default class Upgrade extends Command {
560
564
  if (!runtime) {
561
565
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
562
566
  }
563
- if (runtime.kind === 'remote') {
567
+ if (runtime.kind === 'http') {
564
568
  this.error([
565
569
  `Can't upgrade "${runtime.envName}" from this machine.`,
566
570
  'This env only has an API connection, so there is no saved local app or Docker runtime to upgrade here.',
567
571
  'If you want a local NocoBase AI environment that the CLI can upgrade, run `nb init` first.',
568
572
  ].join('\n'));
569
573
  }
574
+ if (runtime.kind === 'ssh') {
575
+ this.error([
576
+ `Can't upgrade "${runtime.envName}" yet.`,
577
+ 'SSH env support is reserved but not implemented yet.',
578
+ 'Use a local or Docker env if you need CLI-managed upgrades right now.',
579
+ ].join('\n'));
580
+ }
570
581
  try {
571
582
  const runCommand = this.config.runCommand.bind(this.config);
572
583
  if (runtime.kind === 'docker') {
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import { resolveServerRequestTarget } from './env-auth.js';
11
+ import { fetchWithPreservedAuthRedirect } from './http-request.js';
11
12
  const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
12
13
  const CLI_REQUEST_SOURCE_VALUE = 'cli';
13
14
  function stripUtf8Bom(text) {
@@ -195,7 +196,7 @@ export async function executeApiRequest(options) {
195
196
  }
196
197
  const url = new URL(`${normalizeBaseUrl(baseUrl)}${requestPath}`);
197
198
  query.forEach((value, key) => url.searchParams.append(key, value));
198
- const response = await fetch(url, {
199
+ const response = await fetchWithPreservedAuthRedirect(url.toString(), {
199
200
  method: options.operation.method.toUpperCase(),
200
201
  headers,
201
202
  body: body === undefined ? undefined : JSON.stringify(body),
@@ -234,7 +235,7 @@ export async function executeRawApiRequest(options) {
234
235
  }
235
236
  url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
236
237
  }
237
- const response = await fetch(url, {
238
+ const response = await fetchWithPreservedAuthRedirect(url.toString(), {
238
239
  method: options.method.toUpperCase(),
239
240
  headers,
240
241
  body: options.body === undefined ? undefined : JSON.stringify(options.body),
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import path from 'node:path';
10
+ import { resolveEnvKind } from './auth-store.js';
10
11
  import { getEnv, loadAuthConfig } from './auth-store.js';
11
12
  import { commandOutput, commandSucceeds, run, runNocoBaseCommand } from './run-npm.js';
12
13
  const DOCKER_APP_WORKDIR = '/app/nocobase';
@@ -36,7 +37,8 @@ function normalizeEnvSource(env) {
36
37
  if (source === 'docker' || source === 'npm' || source === 'git') {
37
38
  return source;
38
39
  }
39
- if (env.config.appRootPath) {
40
+ const kind = resolveEnvKind(env.config);
41
+ if (kind === 'local') {
40
42
  return 'local';
41
43
  }
42
44
  return undefined;
@@ -50,17 +52,18 @@ export async function resolveManagedAppRuntime(envName) {
50
52
  const resolvedName = env.name || envName?.trim() || config.currentEnv || 'default';
51
53
  const source = normalizeEnvSource(env);
52
54
  const workspaceName = config.name?.trim() || defaultWorkspaceName();
53
- if (source === 'docker') {
55
+ const kind = env.kind ?? resolveEnvKind(env.config);
56
+ if (kind === 'docker') {
54
57
  return {
55
58
  kind: 'docker',
56
59
  env,
57
60
  envName: resolvedName,
58
- source,
61
+ source: 'docker',
59
62
  workspaceName,
60
63
  containerName: buildDockerAppContainerName(resolvedName, workspaceName),
61
64
  };
62
65
  }
63
- if (env.config.appRootPath) {
66
+ if (kind === 'local') {
64
67
  return {
65
68
  kind: 'local',
66
69
  env,
@@ -70,8 +73,16 @@ export async function resolveManagedAppRuntime(envName) {
70
73
  workspaceName,
71
74
  };
72
75
  }
76
+ if (kind === 'ssh') {
77
+ return {
78
+ kind: 'ssh',
79
+ env,
80
+ envName: resolvedName,
81
+ source,
82
+ };
83
+ }
73
84
  return {
74
- kind: 'remote',
85
+ kind: 'http',
75
86
  env,
76
87
  envName: resolvedName,
77
88
  source,
@@ -7,33 +7,126 @@
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 { 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
+ return requestedScope === 'global';
92
+ }
93
+ async function loadExactAuthConfig(options = {}) {
20
94
  try {
21
95
  const content = await fs.readFile(getConfigFile(options), 'utf8');
22
96
  const parsed = JSON.parse(content);
23
- return {
24
- name: parsed.name || parsed.dockerResourcePrefix,
25
- currentEnv: parsed.currentEnv || 'default',
26
- envs: parsed.envs || {},
27
- };
97
+ return normalizeAuthConfig(parsed);
28
98
  }
29
99
  catch (_error) {
30
- return DEFAULT_CONFIG;
100
+ return createDefaultConfig();
101
+ }
102
+ }
103
+ async function resolveEnvStorageScope(envName, options = {}) {
104
+ const requestedScope = options.scope ?? resolveDefaultConfigScope();
105
+ if (requestedScope !== 'global') {
106
+ return { ...options, scope: requestedScope };
107
+ }
108
+ const globalConfig = await loadExactAuthConfig({ scope: 'global' });
109
+ if (globalConfig.envs[envName]) {
110
+ return { ...options, scope: 'global' };
111
+ }
112
+ const projectConfig = await loadExactAuthConfig({ scope: 'project' });
113
+ if (projectConfig.envs[envName]) {
114
+ return { ...options, scope: 'project' };
31
115
  }
116
+ return { ...options, scope: 'global' };
117
+ }
118
+ export async function loadAuthConfig(options = {}) {
119
+ const config = await loadExactAuthConfig(options);
120
+ if (!shouldFallbackToLegacyProjectScope(options) || hasConfiguredEnvs(config)) {
121
+ return config;
122
+ }
123
+ const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
124
+ return hasConfiguredEnvs(legacyProjectConfig) ? legacyProjectConfig : config;
32
125
  }
33
126
  export async function saveAuthConfig(config, options = {}) {
34
127
  const filePath = getConfigFile(options);
35
128
  await fs.mkdir(path.dirname(filePath), { recursive: true });
36
- await fs.writeFile(filePath, JSON.stringify(config, null, 2));
129
+ await fs.writeFile(filePath, JSON.stringify(normalizeAuthConfig(config), null, 2));
37
130
  }
38
131
  export async function listEnvs(options = {}) {
39
132
  const config = await loadAuthConfig(options);
@@ -47,15 +140,16 @@ export async function getCurrentEnvName(options = {}) {
47
140
  return config.currentEnv || 'default';
48
141
  }
49
142
  export async function setCurrentEnv(envName, options = {}) {
50
- const config = await loadAuthConfig(options);
143
+ const writeOptions = await resolveEnvStorageScope(envName, options);
144
+ const config = await loadExactAuthConfig(writeOptions);
51
145
  if (!config.envs[envName]) {
52
146
  throw new Error(`Env "${envName}" is not configured`);
53
147
  }
54
148
  config.currentEnv = envName;
55
- await saveAuthConfig(config, options);
149
+ await saveAuthConfig(config, writeOptions);
56
150
  }
57
151
  export async function ensureWorkspaceName(defaultName, options = {}) {
58
- const config = await loadAuthConfig(options);
152
+ const config = await loadExactAuthConfig(options);
59
153
  const existing = config.name?.trim();
60
154
  if (existing) {
61
155
  return existing;
@@ -74,7 +168,10 @@ export class Env {
74
168
  return this.config.name;
75
169
  }
76
170
  get baseUrl() {
77
- return this.config.baseUrl;
171
+ return readEnvApiBaseUrl(this.config);
172
+ }
173
+ get apiBaseUrl() {
174
+ return readEnvApiBaseUrl(this.config);
78
175
  }
79
176
  get auth() {
80
177
  return this.config.auth;
@@ -82,22 +179,26 @@ export class Env {
82
179
  get runtime() {
83
180
  return this.config.runtime;
84
181
  }
182
+ get kind() {
183
+ return resolveEnvKind(this.config);
184
+ }
85
185
  get appRootPath() {
86
- const appRootPath = this.config.appRootPath;
87
- if (!appRootPath) {
88
- return process.cwd();
89
- }
90
- if (isAbsolute(appRootPath)) {
91
- return appRootPath;
186
+ if (this.kind === 'ssh') {
187
+ const configuredPath = String(this.config.appRootPath ?? '').trim();
188
+ if (configuredPath) {
189
+ return configuredPath;
190
+ }
92
191
  }
93
- return path.resolve(process.cwd(), appRootPath);
192
+ return resolveConfiguredEnvPath(this.config.appRootPath) ?? resolveEnvRelativePath('.');
94
193
  }
95
194
  get storagePath() {
96
- const storagePath = this.config.storagePath;
97
- if (isAbsolute(storagePath)) {
98
- return storagePath;
195
+ if (this.kind === 'ssh') {
196
+ const configuredPath = String(this.config.storagePath ?? '').trim();
197
+ if (configuredPath) {
198
+ return configuredPath;
199
+ }
99
200
  }
100
- return path.resolve(process.cwd(), storagePath);
201
+ return resolveConfiguredEnvPath(this.config.storagePath) ?? resolveEnvRelativePath('.');
101
202
  }
102
203
  get appPort() {
103
204
  return this.config.appPort;
@@ -130,9 +231,18 @@ export async function getEnv(envName, options = {}) {
130
231
  const resolved = envName?.trim() || config.currentEnv || 'default';
131
232
  const envConfig = config.envs[resolved];
132
233
  if (!envConfig) {
133
- return undefined;
234
+ if (!shouldFallbackToLegacyProjectScope(loadOptions)) {
235
+ return undefined;
236
+ }
237
+ const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
238
+ const legacyResolved = envName?.trim() || legacyProjectConfig.currentEnv || 'default';
239
+ const legacyEnvConfig = legacyProjectConfig.envs[legacyResolved];
240
+ if (!legacyEnvConfig) {
241
+ return undefined;
242
+ }
243
+ return new Env({ ...(normalizeEnvConfigEntry(legacyEnvConfig) ?? {}), name: legacyResolved });
134
244
  }
135
- return new Env({ ...envConfig, name: resolved });
245
+ return new Env({ ...(normalizeEnvConfigEntry(envConfig) ?? {}), name: resolved });
136
246
  }
137
247
  function areAuthConfigsEquivalent(left, right) {
138
248
  if (!left && !right) {
@@ -156,16 +266,19 @@ function areAuthConfigsEquivalent(left, right) {
156
266
  return false;
157
267
  }
158
268
  async function writeEnv(envName, updater, options = {}) {
159
- const config = await loadAuthConfig(options);
269
+ const writeOptions = await resolveEnvStorageScope(envName, options);
270
+ const config = await loadExactAuthConfig(writeOptions);
160
271
  const previous = config.envs[envName];
161
272
  config.envs[envName] = updater(previous);
162
273
  config.currentEnv = envName;
163
- await saveAuthConfig(config, options);
274
+ await saveAuthConfig(config, writeOptions);
164
275
  }
165
276
  export async function upsertEnv(envName, config, options = {}) {
166
277
  await writeEnv(envName, (previous) => {
167
- const { baseUrl, accessToken, ...rest } = config;
168
- const baseUrlChanged = previous?.baseUrl !== baseUrl;
278
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, ...rest } = config;
279
+ const nextApiBaseUrl = readEnvApiBaseUrl(config);
280
+ const previousApiBaseUrl = readEnvApiBaseUrl(previous);
281
+ const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
169
282
  const nextAuth = accessToken
170
283
  ? {
171
284
  type: 'token',
@@ -177,7 +290,7 @@ export async function upsertEnv(envName, config, options = {}) {
177
290
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
178
291
  return {
179
292
  ...previous,
180
- baseUrl,
293
+ apiBaseUrl: nextApiBaseUrl,
181
294
  auth: nextAuth,
182
295
  ...rest,
183
296
  runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
@@ -186,8 +299,9 @@ export async function upsertEnv(envName, config, options = {}) {
186
299
  }
187
300
  export async function updateEnvConnection(envName, updates, options = {}) {
188
301
  await writeEnv(envName, (previous) => {
189
- const nextBaseUrl = updates.baseUrl ?? previous?.baseUrl;
190
- const baseUrlChanged = previous?.baseUrl !== nextBaseUrl;
302
+ const nextApiBaseUrl = readEnvApiBaseUrl(updates) ?? readEnvApiBaseUrl(previous);
303
+ const previousApiBaseUrl = readEnvApiBaseUrl(previous);
304
+ const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
191
305
  const nextAuth = updates.accessToken
192
306
  ? {
193
307
  type: 'token',
@@ -199,7 +313,7 @@ export async function updateEnvConnection(envName, updates, options = {}) {
199
313
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
200
314
  return {
201
315
  ...previous,
202
- ...(nextBaseUrl !== undefined ? { baseUrl: nextBaseUrl } : {}),
316
+ ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
203
317
  auth: nextAuth,
204
318
  runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
205
319
  };
@@ -213,17 +327,19 @@ export async function setEnvOauthSession(envName, auth, options = {}) {
213
327
  }), options);
214
328
  }
215
329
  export async function setEnvRuntime(envName, runtime, options = {}) {
216
- const config = await loadAuthConfig(options);
330
+ const writeOptions = await resolveEnvStorageScope(envName, options);
331
+ const config = await loadExactAuthConfig(writeOptions);
217
332
  const current = config.envs[envName] ?? {};
218
333
  config.envs[envName] = {
219
334
  ...current,
220
335
  runtime,
221
336
  };
222
337
  config.currentEnv = envName;
223
- await saveAuthConfig(config, options);
338
+ await saveAuthConfig(config, writeOptions);
224
339
  }
225
340
  export async function removeEnv(envName, options = {}) {
226
- const config = await loadAuthConfig(options);
341
+ const writeOptions = await resolveEnvStorageScope(envName, options);
342
+ const config = await loadExactAuthConfig(writeOptions);
227
343
  if (!config.envs[envName]) {
228
344
  throw new Error(`Env "${envName}" is not configured`);
229
345
  }
@@ -232,7 +348,7 @@ export async function removeEnv(envName, options = {}) {
232
348
  const nextEnv = Object.keys(config.envs).sort()[0];
233
349
  config.currentEnv = nextEnv ?? 'default';
234
350
  }
235
- await saveAuthConfig(config, options);
351
+ await saveAuthConfig(config, writeOptions);
236
352
  return {
237
353
  removed: envName,
238
354
  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,28 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
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_ENV_ROOT_ENV = 'NB_ENV_ROOT';
15
+ export function resolveDefaultConfigScope() {
16
+ const raw = String(process.env[NB_CONFIG_SCOPE_ENV] ?? '').trim().toLowerCase();
17
+ return raw === 'project' ? 'project' : 'global';
18
+ }
5
19
  function resolveGlobalCliHomeRoot() {
6
20
  if (process.env.NOCOBASE_CTL_HOME) {
7
21
  return process.env.NOCOBASE_CTL_HOME;
8
22
  }
9
23
  return os.homedir();
10
24
  }
11
- export function resolveCliHomeRoot(scope = 'auto') {
25
+ export function resolveCliHomeRoot(scope = resolveDefaultConfigScope()) {
12
26
  const cwdRoot = process.cwd();
13
27
  if (scope === 'project') {
14
28
  return cwdRoot;
@@ -22,9 +36,26 @@ export function resolveCliHomeRoot(scope = 'auto') {
22
36
  }
23
37
  return resolveGlobalCliHomeRoot();
24
38
  }
25
- export function resolveCliHomeDir(scope = 'auto') {
39
+ export function resolveCliHomeDir(scope = resolveDefaultConfigScope()) {
26
40
  return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
27
41
  }
42
+ export function resolveEnvRoot(scope = resolveDefaultConfigScope()) {
43
+ const envRoot = String(process.env[NB_ENV_ROOT_ENV] ?? '').trim();
44
+ if (envRoot) {
45
+ return path.resolve(envRoot);
46
+ }
47
+ return resolveCliHomeRoot(scope);
48
+ }
49
+ export function resolveEnvRelativePath(relativePath, scope = resolveDefaultConfigScope()) {
50
+ return path.resolve(resolveEnvRoot(scope), relativePath);
51
+ }
52
+ export function resolveConfiguredEnvPath(value, scope = resolveDefaultConfigScope()) {
53
+ const text = String(value ?? '').trim();
54
+ if (!text) {
55
+ return undefined;
56
+ }
57
+ return path.isAbsolute(text) ? text : resolveEnvRelativePath(text, scope);
58
+ }
28
59
  export function formatCliHomeScope(scope) {
29
60
  return scope === 'project' ? 'project' : 'global';
30
61
  }
@@ -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)