@nocobase/cli 2.1.0-alpha.45 → 2.1.0-alpha.47

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 (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -32
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +92 -20
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +143 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +11 -7
  27. package/dist/commands/config/delete.js +10 -2
  28. package/dist/commands/config/get.js +7 -1
  29. package/dist/commands/config/set.js +12 -2
  30. package/dist/commands/db/ps.js +5 -18
  31. package/dist/commands/env/add.js +24 -44
  32. package/dist/commands/env/auth.js +1 -1
  33. package/dist/commands/env/info.js +3 -2
  34. package/dist/commands/env/list.js +4 -5
  35. package/dist/commands/env/proxy/caddy.js +28 -0
  36. package/dist/commands/env/proxy/index.js +353 -0
  37. package/dist/commands/env/proxy/nginx.js +28 -0
  38. package/dist/commands/env/remove.js +112 -22
  39. package/dist/commands/env/shared.js +17 -9
  40. package/dist/commands/env/status.js +10 -7
  41. package/dist/commands/env/update.js +385 -21
  42. package/dist/commands/init.js +368 -98
  43. package/dist/commands/install.js +271 -104
  44. package/dist/commands/license/activate.js +63 -244
  45. package/dist/commands/license/plugins/shared.js +64 -13
  46. package/dist/commands/plugin/import.js +108 -0
  47. package/dist/commands/revision/create.js +89 -0
  48. package/dist/commands/self/update.js +58 -1
  49. package/dist/commands/source/dev.js +5 -5
  50. package/dist/commands/source/test.js +13 -14
  51. package/dist/lib/app-health.js +39 -32
  52. package/dist/lib/app-managed-resources.js +25 -9
  53. package/dist/lib/app-public-path.js +80 -0
  54. package/dist/lib/app-runtime.js +12 -3
  55. package/dist/lib/auth-store.js +128 -4
  56. package/dist/lib/backup.js +3 -3
  57. package/dist/lib/bootstrap.js +14 -8
  58. package/dist/lib/cli-config.js +229 -3
  59. package/dist/lib/cli-entry-error.js +52 -0
  60. package/dist/lib/command-log.js +284 -0
  61. package/dist/lib/docker-env-file.js +10 -2
  62. package/dist/lib/docker-log-stream.js +45 -0
  63. package/dist/lib/env-auth.js +25 -14
  64. package/dist/lib/env-command-config.js +45 -0
  65. package/dist/lib/env-config.js +7 -2
  66. package/dist/lib/env-guard.js +9 -10
  67. package/dist/lib/env-paths.js +101 -0
  68. package/dist/lib/env-proxy.js +1295 -0
  69. package/dist/lib/inquirer.js +4 -5
  70. package/dist/lib/managed-env-file.js +98 -0
  71. package/dist/lib/plugin-import.js +279 -0
  72. package/dist/lib/prompt-catalog-core.js +3 -2
  73. package/dist/lib/prompt-validators.js +14 -0
  74. package/dist/lib/prompt-web-ui.js +8 -1
  75. package/dist/lib/run-npm.js +56 -12
  76. package/dist/lib/skills-manager.js +3 -1
  77. package/dist/lib/source-publish.js +22 -2
  78. package/dist/lib/startup-update.js +74 -50
  79. package/dist/lib/ui.js +2 -1
  80. package/dist/locale/en-US.json +122 -22
  81. package/dist/locale/zh-CN.json +119 -19
  82. package/package.json +5 -7
  83. package/scripts/build.mjs +34 -0
  84. package/scripts/clean.mjs +9 -0
  85. package/tsconfig.json +19 -0
  86. package/LICENSE.txt +0 -107
  87. package/README.zh-CN.md +0 -355
@@ -0,0 +1,284 @@
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 fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { resolveCliHomeDir } from './cli-home.js';
13
+ import { DEFAULT_LOG_ENABLED, DEFAULT_LOG_RETENTION_DAYS, getCliConfigValue } from './cli-config.js';
14
+ const ACTIVE_LOG_FILE_ENV = 'NB_CLI_ACTIVE_LOG_FILE';
15
+ const ACTIVE_META_FILE_ENV = 'NB_CLI_ACTIVE_META_FILE';
16
+ const LOG_DIR_ENV = 'NB_CLI_LOG_DIR';
17
+ const LOG_DISABLED_ENV = 'NB_CLI_LOG_DISABLED';
18
+ const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, 'g');
19
+ const CLEANUP_STATE_FILE = '.cleanup-state.json';
20
+ const CLEANUP_INTERVAL_MS = 12 * 60 * 60 * 1000;
21
+ function isTruthyEnvFlag(value) {
22
+ const normalized = String(value ?? '').trim().toLowerCase();
23
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
24
+ }
25
+ function pad(value) {
26
+ return String(value).padStart(2, '0');
27
+ }
28
+ function formatDateParts(date) {
29
+ const year = date.getFullYear();
30
+ const month = pad(date.getMonth() + 1);
31
+ const day = pad(date.getDate());
32
+ const hour = pad(date.getHours());
33
+ const minute = pad(date.getMinutes());
34
+ const second = pad(date.getSeconds());
35
+ return {
36
+ dayKey: `${year}-${month}-${day}`,
37
+ timestampKey: `${year}${month}${day}-${hour}${minute}${second}`,
38
+ };
39
+ }
40
+ function sanitizeFileNameSegment(value) {
41
+ const normalized = value
42
+ .trim()
43
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
44
+ .replace(/-+/g, '-')
45
+ .replace(/^-|-$/g, '');
46
+ return normalized || 'command';
47
+ }
48
+ function resolveSessionDirName(sessionId) {
49
+ const trimmed = String(sessionId ?? '').trim();
50
+ if (!trimmed) {
51
+ return 'no-session';
52
+ }
53
+ return sanitizeFileNameSegment(trimmed);
54
+ }
55
+ export function stripAnsi(value) {
56
+ return value.replace(ANSI_ESCAPE_PATTERN, '');
57
+ }
58
+ export function shouldEnableCommandLog(env = process.env) {
59
+ return !isTruthyEnvFlag(env[LOG_DISABLED_ENV]);
60
+ }
61
+ export function resolveCommandLogDir(env = process.env) {
62
+ if (!shouldEnableCommandLog(env)) {
63
+ return undefined;
64
+ }
65
+ const configured = String(env[LOG_DIR_ENV] ?? '').trim();
66
+ if (configured) {
67
+ return path.resolve(configured);
68
+ }
69
+ return path.join(resolveCliHomeDir(), 'logs');
70
+ }
71
+ export function getActiveCommandLogFile(env = process.env) {
72
+ const value = String(env[ACTIVE_LOG_FILE_ENV] ?? '').trim();
73
+ return value || undefined;
74
+ }
75
+ function readActiveCommandMetaFile(env = process.env) {
76
+ const value = String(env[ACTIVE_META_FILE_ENV] ?? '').trim();
77
+ return value || undefined;
78
+ }
79
+ export function hasActiveCommandLog(env = process.env) {
80
+ return Boolean(getActiveCommandLogFile(env));
81
+ }
82
+ export function sanitizeArgv(argv) {
83
+ const sensitiveFlags = new Set(['--token', '--access-token', '--password', '--secret']);
84
+ const sanitized = [];
85
+ for (let index = 0; index < argv.length; index += 1) {
86
+ const token = argv[index];
87
+ if (sensitiveFlags.has(token)) {
88
+ sanitized.push(token);
89
+ if (index + 1 < argv.length) {
90
+ sanitized.push('***');
91
+ index += 1;
92
+ }
93
+ continue;
94
+ }
95
+ const matched = token.match(/^(--(?:token|access-token|password|secret))=(.*)$/);
96
+ if (matched) {
97
+ sanitized.push(`${matched[1]}=***`);
98
+ continue;
99
+ }
100
+ sanitized.push(token);
101
+ }
102
+ return sanitized;
103
+ }
104
+ export function getCommandLogCommandId(argv) {
105
+ const positional = argv.filter((token) => token && !token.startsWith('-'));
106
+ if (positional.length === 0) {
107
+ return 'command';
108
+ }
109
+ const commandPath = positional[0] === 'api' ? positional.slice(0, 3) : positional.slice(0, 2);
110
+ return sanitizeFileNameSegment(commandPath.join('-'));
111
+ }
112
+ function parseDateDirName(name) {
113
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) {
114
+ return undefined;
115
+ }
116
+ const parsed = new Date(`${name}T00:00:00.000Z`);
117
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
118
+ }
119
+ async function readCleanupState(filePath) {
120
+ try {
121
+ return JSON.parse(await fsp.readFile(filePath, 'utf8'));
122
+ }
123
+ catch {
124
+ return {};
125
+ }
126
+ }
127
+ async function writeCleanupState(filePath, state) {
128
+ await fsp.writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
129
+ }
130
+ async function resolveCommandLogRetentionDays() {
131
+ const configured = await getCliConfigValue('log.retention-days').catch(() => String(DEFAULT_LOG_RETENTION_DAYS));
132
+ const parsed = Number.parseInt(String(configured).trim(), 10);
133
+ if (!Number.isInteger(parsed) || parsed < 0) {
134
+ return DEFAULT_LOG_RETENTION_DAYS;
135
+ }
136
+ return parsed;
137
+ }
138
+ async function resolveCommandLogEnabled() {
139
+ const configured = await getCliConfigValue('log.enabled').catch(() => String(DEFAULT_LOG_ENABLED));
140
+ const normalized = String(configured).trim().toLowerCase();
141
+ if (normalized === 'false') {
142
+ return false;
143
+ }
144
+ if (normalized === 'true') {
145
+ return true;
146
+ }
147
+ return DEFAULT_LOG_ENABLED;
148
+ }
149
+ async function cleanupCommandLogs(rootDir, retentionDays, now = new Date()) {
150
+ if (retentionDays === 0) {
151
+ return;
152
+ }
153
+ const entries = await fsp.readdir(rootDir, { withFileTypes: true }).catch(() => []);
154
+ const cutoff = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
155
+ cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays + 1);
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory()) {
158
+ continue;
159
+ }
160
+ const dirDate = parseDateDirName(entry.name);
161
+ if (!dirDate || dirDate >= cutoff) {
162
+ continue;
163
+ }
164
+ await fsp.rm(path.join(rootDir, entry.name), { recursive: true, force: true });
165
+ }
166
+ }
167
+ async function maybeCleanupCommandLogs(rootDir, env = process.env) {
168
+ await fsp.mkdir(rootDir, { recursive: true });
169
+ const stateFile = path.join(rootDir, CLEANUP_STATE_FILE);
170
+ const state = await readCleanupState(stateFile);
171
+ const lastCleanupAt = Date.parse(String(state.lastCleanupAt ?? ''));
172
+ if (!Number.isNaN(lastCleanupAt) && Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) {
173
+ return;
174
+ }
175
+ const retentionDays = await resolveCommandLogRetentionDays();
176
+ await cleanupCommandLogs(rootDir, retentionDays);
177
+ await writeCleanupState(stateFile, { lastCleanupAt: new Date().toISOString() });
178
+ }
179
+ async function writeMetaFile(filePath, meta) {
180
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
181
+ await fsp.writeFile(filePath, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
182
+ }
183
+ function writeMetaFileSync(filePath, meta) {
184
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
185
+ fs.writeFileSync(filePath, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
186
+ }
187
+ export async function initCommandLogSession(options) {
188
+ const env = options.env ?? process.env;
189
+ if (!shouldEnableCommandLog(env)) {
190
+ return undefined;
191
+ }
192
+ const enabled = await resolveCommandLogEnabled().catch(() => DEFAULT_LOG_ENABLED);
193
+ if (!enabled) {
194
+ return undefined;
195
+ }
196
+ const rootDir = resolveCommandLogDir(env);
197
+ if (!rootDir) {
198
+ return undefined;
199
+ }
200
+ await maybeCleanupCommandLogs(rootDir, env).catch(() => undefined);
201
+ const startedAt = options.startedAt ?? new Date();
202
+ const { dayKey, timestampKey } = formatDateParts(startedAt);
203
+ const commandId = getCommandLogCommandId(options.argv);
204
+ const fileStem = `${timestampKey}-${commandId}-${options.pid ?? process.pid}`;
205
+ const sessionDirName = resolveSessionDirName(options.sessionId);
206
+ const logDir = path.join(rootDir, dayKey, sessionDirName);
207
+ const logFile = path.join(logDir, `${fileStem}.log`);
208
+ const metaFile = path.join(logDir, `${fileStem}.meta.json`);
209
+ const meta = {
210
+ command: commandId,
211
+ argv: sanitizeArgv(options.argv),
212
+ cwd: options.cwd,
213
+ pid: options.pid ?? process.pid,
214
+ ppid: options.ppid ?? process.ppid,
215
+ sessionId: options.sessionId,
216
+ startedAt: startedAt.toISOString(),
217
+ logFile,
218
+ cliVersion: options.cliVersion,
219
+ nodeVersion: options.nodeVersion,
220
+ platform: options.platform,
221
+ interactive: options.interactive,
222
+ verbose: options.verbose,
223
+ };
224
+ await fsp.mkdir(logDir, { recursive: true });
225
+ await fsp.writeFile(logFile, '', 'utf8');
226
+ await writeMetaFile(metaFile, meta);
227
+ env[ACTIVE_LOG_FILE_ENV] = logFile;
228
+ env[ACTIVE_META_FILE_ENV] = metaFile;
229
+ return {
230
+ logFile,
231
+ metaFile,
232
+ meta,
233
+ };
234
+ }
235
+ export function finalizeCommandLogSessionSync(session, options = {}) {
236
+ if (!session) {
237
+ return;
238
+ }
239
+ const endedAt = options.endedAt ?? new Date();
240
+ const startedAtMs = Date.parse(session.meta.startedAt);
241
+ const nextMeta = {
242
+ ...session.meta,
243
+ endedAt: endedAt.toISOString(),
244
+ durationMs: Number.isNaN(startedAtMs) ? undefined : Math.max(0, endedAt.getTime() - startedAtMs),
245
+ exitCode: options.exitCode ?? session.meta.exitCode,
246
+ signal: options.signal ?? session.meta.signal,
247
+ errorMessage: options.errorMessage ?? session.meta.errorMessage,
248
+ };
249
+ writeMetaFileSync(session.metaFile, nextMeta);
250
+ session.meta = nextMeta;
251
+ }
252
+ export function appendCommandLogChunk(chunk, env = process.env) {
253
+ const logFile = getActiveCommandLogFile(env);
254
+ if (!logFile) {
255
+ return;
256
+ }
257
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
258
+ if (!text) {
259
+ return;
260
+ }
261
+ fs.appendFileSync(logFile, stripAnsi(text), 'utf8');
262
+ }
263
+ export function installCommandLogWriteHooks(env = process.env) {
264
+ if (!hasActiveCommandLog(env)) {
265
+ return undefined;
266
+ }
267
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
268
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
269
+ process.stdout.write = ((chunk, encoding, callback) => {
270
+ appendCommandLogChunk(chunk, env);
271
+ return originalStdoutWrite(chunk, encoding, callback);
272
+ });
273
+ process.stderr.write = ((chunk, encoding, callback) => {
274
+ appendCommandLogChunk(chunk, env);
275
+ return originalStderrWrite(chunk, encoding, callback);
276
+ });
277
+ return () => {
278
+ process.stdout.write = originalStdoutWrite;
279
+ process.stderr.write = originalStderrWrite;
280
+ };
281
+ }
282
+ export function getActiveCommandMetaFile(env = process.env) {
283
+ return readActiveCommandMetaFile(env);
284
+ }
@@ -8,15 +8,23 @@
8
8
  */
9
9
  import { access } from 'node:fs/promises';
10
10
  import { resolveConfiguredEnvPath } from './cli-home.js';
11
+ import { inferConfiguredAppPathFromLegacyConfig } from './env-paths.js';
11
12
  function trimValue(value) {
12
13
  const text = String(value ?? '').trim();
13
14
  return text || undefined;
14
15
  }
15
- export function defaultDockerEnvFilePath(envName) {
16
+ function appendDotEnvPath(basePath) {
17
+ return `${basePath.replace(/[\\/]+$/, '')}/.env`;
18
+ }
19
+ export function defaultDockerEnvFilePath(envName, config) {
20
+ const appPath = inferConfiguredAppPathFromLegacyConfig(config ?? {});
21
+ if (appPath) {
22
+ return appendDotEnvPath(appPath);
23
+ }
16
24
  return `${envName}/.env`;
17
25
  }
18
26
  export function resolveConfiguredDockerEnvFilePath(envName, config) {
19
- return trimValue(config?.envFile) || defaultDockerEnvFilePath(envName);
27
+ return trimValue(config?.envFile) || defaultDockerEnvFilePath(envName, config);
20
28
  }
21
29
  export function hasExplicitDockerEnvFile(config) {
22
30
  return Boolean(trimValue(config?.envFile));
@@ -0,0 +1,45 @@
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 { spawn } from 'node:child_process';
10
+ const DEFAULT_DOCKER_LOG_TAIL = 50;
11
+ export function startDockerLogFollower(containerName, options) {
12
+ const tail = Math.max(0, options?.tail ?? DEFAULT_DOCKER_LOG_TAIL);
13
+ const child = spawn('docker', ['logs', '--tail', String(tail), '--follow', containerName], {
14
+ stdio: 'inherit',
15
+ });
16
+ let settled = false;
17
+ let resolveClosed;
18
+ const closed = new Promise((resolve) => {
19
+ resolveClosed = resolve;
20
+ });
21
+ const settle = () => {
22
+ if (!settled) {
23
+ settled = true;
24
+ resolveClosed();
25
+ }
26
+ };
27
+ child.once('error', settle);
28
+ child.once('close', settle);
29
+ return {
30
+ stop: async () => {
31
+ if (settled) {
32
+ return;
33
+ }
34
+ try {
35
+ if (!child.kill()) {
36
+ settle();
37
+ }
38
+ }
39
+ catch {
40
+ settle();
41
+ }
42
+ await closed;
43
+ },
44
+ };
45
+ }
@@ -34,6 +34,9 @@ export function getOauthResource(issuerOrBaseUrl) {
34
34
  export function getDefaultOauthScope() {
35
35
  return DEFAULT_OAUTH_SCOPE;
36
36
  }
37
+ export function getOauthLoopbackRedirectUri(port) {
38
+ return `http://${LOOPBACK_HOST}:${port}/callback`;
39
+ }
37
40
  function formatApiAuthError(prefix, data, fallbackStatus) {
38
41
  if (typeof data === 'string' && data.trim()) {
39
42
  return `${prefix}: ${data}`;
@@ -629,19 +632,19 @@ async function createLoopbackServer(state) {
629
632
  });
630
633
  let resolveWaiter;
631
634
  let rejectWaiter;
632
- const waitForCode = () => new Promise((resolveCode, rejectCode) => {
635
+ const waitForCode = () => new Promise((resolve, reject) => {
633
636
  resolveWaiter = (code) => {
634
637
  void close();
635
- resolveCode(code);
638
+ resolve(code);
636
639
  };
637
640
  rejectWaiter = (error) => {
638
641
  void close();
639
- rejectCode(error);
642
+ reject(error);
640
643
  };
641
644
  });
642
645
  const close = async () => {
643
- await new Promise((resolveClose) => {
644
- server.close(() => resolveClose());
646
+ await new Promise((resolve) => {
647
+ server.close(() => resolve());
645
648
  });
646
649
  };
647
650
  server.on('error', (error) => {
@@ -655,7 +658,7 @@ async function createLoopbackServer(state) {
655
658
  return;
656
659
  }
657
660
  resolve({
658
- redirectUri: `http://${LOOPBACK_HOST}:${address.port}/callback`,
661
+ redirectUri: getOauthLoopbackRedirectUri(address.port),
659
662
  waitForCode,
660
663
  close,
661
664
  });
@@ -772,7 +775,7 @@ export async function resolveAccessToken(options) {
772
775
  }
773
776
  const baseUrl = options.baseUrl ?? env.baseUrl;
774
777
  if (!baseUrl) {
775
- throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add ${envName} --api-base-url <url>\`.`);
778
+ throw new Error(`Env "${envName}" is missing a base URL. Update it with \`nb env update ${envName} --api-base-url <url>\` first.`);
776
779
  }
777
780
  printVerbose(`Refreshing OAuth session for env "${envName}"`);
778
781
  return refreshOauthAccessToken({
@@ -793,7 +796,12 @@ export async function resolveServerRequestTarget(options) {
793
796
  scope: options.scope,
794
797
  });
795
798
  if (!baseUrl) {
796
- throw new Error('Missing base URL. Use --api-base-url or configure one with `nb env add`.');
799
+ throw new Error([
800
+ env ? `Env "${envName}" is missing a base URL.` : `Env "${envName}" is not configured.`,
801
+ env
802
+ ? `Use --api-base-url or update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\`.`
803
+ : `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
804
+ ].join('\n'));
797
805
  }
798
806
  return { baseUrl, token };
799
807
  }
@@ -807,8 +815,8 @@ export async function authenticateEnvWithBasic(options) {
807
815
  ? `Environment "${envName}" does not have an API base URL yet.`
808
816
  : `Environment "${envName}" has not been set up yet.`,
809
817
  env
810
- ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
811
- : `Run \`nb env add ${envName}\` first.`,
818
+ ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
819
+ : `Run \`nb init --ui --env ${envName}\` first.`,
812
820
  ]
813
821
  .filter(Boolean)
814
822
  .join('\n'));
@@ -864,8 +872,8 @@ export async function authenticateEnvWithOauth(options) {
864
872
  ? `Environment "${envName}" does not have an API base URL yet.`
865
873
  : `Environment "${envName}" has not been set up yet.`,
866
874
  env
867
- ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
868
- : `Run \`nb env add ${envName}\` first.`,
875
+ ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
876
+ : `Run \`nb init --ui --env ${envName}\` first.`,
869
877
  ]
870
878
  .filter(Boolean)
871
879
  .join('\n'));
@@ -912,10 +920,13 @@ export async function authenticateEnvWithOauth(options) {
912
920
  const code = await new Promise((resolve, reject) => {
913
921
  const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
914
922
  timeout.unref?.();
915
- callback.waitForCode().then((value) => {
923
+ callback
924
+ .waitForCode()
925
+ .then((value) => {
916
926
  clearTimeout(timeout);
917
927
  resolve(value);
918
- }, (error) => {
928
+ })
929
+ .catch((error) => {
919
930
  clearTimeout(timeout);
920
931
  reject(error);
921
932
  });
@@ -0,0 +1,45 @@
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 ENV_STRING_CONFIG_FLAG_MAP = {
10
+ source: 'source',
11
+ 'download-version': 'downloadVersion',
12
+ 'docker-registry': 'dockerRegistry',
13
+ 'docker-platform': 'dockerPlatform',
14
+ 'git-url': 'gitUrl',
15
+ 'npm-registry': 'npmRegistry',
16
+ 'app-path': 'appPath',
17
+ 'app-root-path': 'appRootPath',
18
+ 'storage-path': 'storagePath',
19
+ 'app-public-path': 'appPublicPath',
20
+ 'cdn-base-url': 'cdnBaseUrl',
21
+ 'env-file': 'envFile',
22
+ 'app-port': 'appPort',
23
+ 'app-key': 'appKey',
24
+ timezone: 'timezone',
25
+ 'db-dialect': 'dbDialect',
26
+ 'builtin-db-image': 'builtinDbImage',
27
+ 'db-host': 'dbHost',
28
+ 'db-port': 'dbPort',
29
+ 'db-database': 'dbDatabase',
30
+ 'db-user': 'dbUser',
31
+ 'db-password': 'dbPassword',
32
+ 'db-schema': 'dbSchema',
33
+ 'db-table-prefix': 'dbTablePrefix',
34
+ 'root-username': 'rootUsername',
35
+ 'root-email': 'rootEmail',
36
+ 'root-password': 'rootPassword',
37
+ 'root-nickname': 'rootNickname',
38
+ };
39
+ export const ENV_BOOLEAN_CONFIG_FLAG_MAP = {
40
+ 'builtin-db': 'builtinDb',
41
+ 'dev-dependencies': 'devDependencies',
42
+ build: 'build',
43
+ 'build-dts': 'buildDts',
44
+ 'db-underscored': 'dbUnderscored',
45
+ };
@@ -6,6 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { resolveAppPublicPath } from './app-public-path.js';
9
10
  const STRING_ENV_CONFIG_KEYS = [
10
11
  'source',
11
12
  'downloadVersion',
@@ -13,8 +14,11 @@ const STRING_ENV_CONFIG_KEYS = [
13
14
  'dockerPlatform',
14
15
  'gitUrl',
15
16
  'npmRegistry',
17
+ 'appPath',
16
18
  'appRootPath',
17
19
  'storagePath',
20
+ 'appPublicPath',
21
+ 'cdnBaseUrl',
18
22
  'envFile',
19
23
  'appPort',
20
24
  'appKey',
@@ -47,11 +51,12 @@ function trimConfigValue(value) {
47
51
  }
48
52
  function resolveEnvKind(input) {
49
53
  const source = trimConfigValue(input.source);
54
+ const appPath = trimConfigValue(input.appPath);
50
55
  const appRootPath = trimConfigValue(input.appRootPath);
51
56
  if (source === 'docker') {
52
57
  return 'docker';
53
58
  }
54
- if (source === 'npm' || source === 'git' || source === 'local' || appRootPath) {
59
+ if (source === 'npm' || source === 'git' || source === 'local' || appPath || appRootPath) {
55
60
  return 'local';
56
61
  }
57
62
  return 'http';
@@ -64,7 +69,7 @@ export function buildStoredEnvConfig(input) {
64
69
  for (const key of STRING_ENV_CONFIG_KEYS) {
65
70
  const value = trimConfigValue(input[key]);
66
71
  if (value) {
67
- envConfig[key] = value;
72
+ envConfig[key] = key === 'appPublicPath' ? resolveAppPublicPath(value) : value;
68
73
  }
69
74
  }
70
75
  for (const key of BOOLEAN_ENV_CONFIG_KEYS) {
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { stdin as input, stdout as output } from 'node:process';
10
10
  import { getCurrentEnvName } from './auth-store.js';
11
+ import { translateCli } from './cli-locale.js';
11
12
  import { confirm } from "./inquirer.js";
12
13
  function normalizeEnvName(value) {
13
14
  const text = String(value ?? '').trim();
@@ -23,18 +24,16 @@ function isInteractiveTerminal() {
23
24
  return Boolean(input.isTTY && output.isTTY);
24
25
  }
25
26
  function formatCrossEnvPromptMessage(currentEnv, requestedEnv) {
26
- return `Current env is "${currentEnv}", but this command targets "${requestedEnv}" via --env. Continue without switching the current env?`;
27
+ return translateCli('commands.shared.crossEnv.prompt', {
28
+ currentEnv,
29
+ requestedEnv,
30
+ });
27
31
  }
28
32
  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');
33
+ return translateCli('commands.shared.crossEnv.refusal', {
34
+ currentEnv,
35
+ requestedEnv,
36
+ });
38
37
  }
39
38
  export async function ensureCrossEnvConfirmed(options) {
40
39
  const requestedEnv = normalizeEnvName(options.requestedEnv);
@@ -0,0 +1,101 @@
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 path from 'node:path';
10
+ import { resolveConfiguredEnvPath } from './cli-home.js';
11
+ function trimPathValue(value) {
12
+ const text = String(value ?? '').trim();
13
+ return text || undefined;
14
+ }
15
+ function normalizePathForComparison(value) {
16
+ return value.replace(/[\\/]+/g, '/').replace(/\/+$/, '');
17
+ }
18
+ export function areConfiguredPathsEquivalent(left, right) {
19
+ const leftValue = trimPathValue(left);
20
+ const rightValue = trimPathValue(right);
21
+ if (!leftValue || !rightValue) {
22
+ return leftValue === rightValue;
23
+ }
24
+ return normalizePathForComparison(leftValue) === normalizePathForComparison(rightValue);
25
+ }
26
+ function trimTrailingSeparators(value) {
27
+ return value.replace(/[\\/]+$/, '');
28
+ }
29
+ function hasTerminalPathSegment(value, segment) {
30
+ const normalized = normalizePathForComparison(value);
31
+ return normalized.endsWith(`/${segment}`) || normalized === segment;
32
+ }
33
+ function dirnameWithTrailingSeparator(value) {
34
+ const trimmed = trimTrailingSeparators(value);
35
+ if (!trimmed) {
36
+ return undefined;
37
+ }
38
+ const separatorIndex = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
39
+ if (separatorIndex < 0) {
40
+ return undefined;
41
+ }
42
+ return trimmed.slice(0, separatorIndex + 1);
43
+ }
44
+ function inferAppPathFromLegacyConfigValue(value, leaf) {
45
+ if (!value || !hasTerminalPathSegment(value, leaf)) {
46
+ return undefined;
47
+ }
48
+ return dirnameWithTrailingSeparator(value);
49
+ }
50
+ export function deriveConfiguredSourcePath(appPath) {
51
+ const base = trimPathValue(appPath);
52
+ if (!base) {
53
+ return 'source/';
54
+ }
55
+ return `${base.replace(/[\\/]+$/, '')}/source/`;
56
+ }
57
+ export function deriveConfiguredStoragePath(appPath) {
58
+ const base = trimPathValue(appPath);
59
+ if (!base) {
60
+ return 'storage/';
61
+ }
62
+ return `${base.replace(/[\\/]+$/, '')}/storage/`;
63
+ }
64
+ export function inferConfiguredAppPathFromLegacyConfig(config) {
65
+ const explicitAppPath = trimPathValue(config.appPath);
66
+ if (explicitAppPath) {
67
+ return explicitAppPath;
68
+ }
69
+ const candidates = [
70
+ inferAppPathFromLegacyConfigValue(trimPathValue(config.appRootPath), 'source'),
71
+ inferAppPathFromLegacyConfigValue(trimPathValue(config.storagePath), 'storage'),
72
+ ].filter((value) => Boolean(value));
73
+ if (candidates.length === 0) {
74
+ return undefined;
75
+ }
76
+ const [first] = candidates;
77
+ if (!first) {
78
+ return undefined;
79
+ }
80
+ return candidates.every((candidate) => areConfiguredPathsEquivalent(candidate, first)) ? first : undefined;
81
+ }
82
+ export function resolveConfiguredAppPath(config, scope) {
83
+ const configuredAppPath = inferConfiguredAppPathFromLegacyConfig(config);
84
+ return configuredAppPath ? resolveConfiguredEnvPath(configuredAppPath, scope) : undefined;
85
+ }
86
+ export function resolveConfiguredSourcePath(config, scope) {
87
+ const legacySourcePath = trimPathValue(config.appRootPath);
88
+ if (legacySourcePath) {
89
+ return resolveConfiguredEnvPath(legacySourcePath, scope);
90
+ }
91
+ const appPath = resolveConfiguredAppPath(config, scope);
92
+ return appPath ? path.join(appPath, 'source') : undefined;
93
+ }
94
+ export function resolveConfiguredStoragePath(config, scope) {
95
+ const legacyStoragePath = trimPathValue(config.storagePath);
96
+ if (legacyStoragePath) {
97
+ return resolveConfiguredEnvPath(legacyStoragePath, scope);
98
+ }
99
+ const appPath = resolveConfiguredAppPath(config, scope);
100
+ return appPath ? path.join(appPath, 'storage') : undefined;
101
+ }