@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.
- package/LICENSE +201 -0
- package/README.md +63 -380
- package/assets/env-proxy/nginx/app.conf.tpl +23 -0
- package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
- package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
- package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
- package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
- package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
- package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
- package/bin/run.js +40 -32
- package/dist/commands/app/autostart/disable.js +55 -0
- package/dist/commands/app/autostart/enable.js +55 -0
- package/dist/commands/app/autostart/list.js +37 -0
- package/dist/commands/app/autostart/run.js +84 -0
- package/dist/commands/app/autostart/shared.js +49 -0
- package/dist/commands/app/destroy.js +21 -12
- package/dist/commands/app/down.js +2 -2
- package/dist/commands/app/logs.js +2 -1
- package/dist/commands/app/restart.js +92 -20
- package/dist/commands/app/shared.js +3 -2
- package/dist/commands/app/start.js +143 -38
- package/dist/commands/app/stop.js +31 -2
- package/dist/commands/app/upgrade.js +11 -7
- package/dist/commands/config/delete.js +10 -2
- package/dist/commands/config/get.js +7 -1
- package/dist/commands/config/set.js +12 -2
- package/dist/commands/db/ps.js +5 -18
- package/dist/commands/env/add.js +24 -44
- package/dist/commands/env/auth.js +1 -1
- package/dist/commands/env/info.js +3 -2
- package/dist/commands/env/list.js +4 -5
- package/dist/commands/env/proxy/caddy.js +28 -0
- package/dist/commands/env/proxy/index.js +353 -0
- package/dist/commands/env/proxy/nginx.js +28 -0
- package/dist/commands/env/remove.js +112 -22
- package/dist/commands/env/shared.js +17 -9
- package/dist/commands/env/status.js +10 -7
- package/dist/commands/env/update.js +385 -21
- package/dist/commands/init.js +368 -98
- package/dist/commands/install.js +271 -104
- package/dist/commands/license/activate.js +63 -244
- package/dist/commands/license/plugins/shared.js +64 -13
- package/dist/commands/plugin/import.js +108 -0
- package/dist/commands/revision/create.js +89 -0
- package/dist/commands/self/update.js +58 -1
- package/dist/commands/source/dev.js +5 -5
- package/dist/commands/source/test.js +13 -14
- package/dist/lib/app-health.js +39 -32
- package/dist/lib/app-managed-resources.js +25 -9
- package/dist/lib/app-public-path.js +80 -0
- package/dist/lib/app-runtime.js +12 -3
- package/dist/lib/auth-store.js +128 -4
- package/dist/lib/backup.js +3 -3
- package/dist/lib/bootstrap.js +14 -8
- package/dist/lib/cli-config.js +229 -3
- package/dist/lib/cli-entry-error.js +52 -0
- package/dist/lib/command-log.js +284 -0
- package/dist/lib/docker-env-file.js +10 -2
- package/dist/lib/docker-log-stream.js +45 -0
- package/dist/lib/env-auth.js +25 -14
- package/dist/lib/env-command-config.js +45 -0
- package/dist/lib/env-config.js +7 -2
- package/dist/lib/env-guard.js +9 -10
- package/dist/lib/env-paths.js +101 -0
- package/dist/lib/env-proxy.js +1295 -0
- package/dist/lib/inquirer.js +4 -5
- package/dist/lib/managed-env-file.js +98 -0
- package/dist/lib/plugin-import.js +279 -0
- package/dist/lib/prompt-catalog-core.js +3 -2
- package/dist/lib/prompt-validators.js +14 -0
- package/dist/lib/prompt-web-ui.js +8 -1
- package/dist/lib/run-npm.js +56 -12
- package/dist/lib/skills-manager.js +3 -1
- package/dist/lib/source-publish.js +22 -2
- package/dist/lib/startup-update.js +74 -50
- package/dist/lib/ui.js +2 -1
- package/dist/locale/en-US.json +122 -22
- package/dist/locale/zh-CN.json +119 -19
- package/package.json +5 -7
- package/scripts/build.mjs +34 -0
- package/scripts/clean.mjs +9 -0
- package/tsconfig.json +19 -0
- package/LICENSE.txt +0 -107
- 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
|
-
|
|
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
|
+
}
|
package/dist/lib/env-auth.js
CHANGED
|
@@ -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((
|
|
635
|
+
const waitForCode = () => new Promise((resolve, reject) => {
|
|
633
636
|
resolveWaiter = (code) => {
|
|
634
637
|
void close();
|
|
635
|
-
|
|
638
|
+
resolve(code);
|
|
636
639
|
};
|
|
637
640
|
rejectWaiter = (error) => {
|
|
638
641
|
void close();
|
|
639
|
-
|
|
642
|
+
reject(error);
|
|
640
643
|
};
|
|
641
644
|
});
|
|
642
645
|
const close = async () => {
|
|
643
|
-
await new Promise((
|
|
644
|
-
server.close(() =>
|
|
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:
|
|
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.
|
|
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(
|
|
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
|
|
811
|
-
: `Run \`nb env
|
|
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
|
|
868
|
-
: `Run \`nb env
|
|
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
|
|
923
|
+
callback
|
|
924
|
+
.waitForCode()
|
|
925
|
+
.then((value) => {
|
|
916
926
|
clearTimeout(timeout);
|
|
917
927
|
resolve(value);
|
|
918
|
-
}
|
|
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
|
+
};
|
package/dist/lib/env-config.js
CHANGED
|
@@ -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) {
|
package/dist/lib/env-guard.js
CHANGED
|
@@ -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
|
|
27
|
+
return translateCli('commands.shared.crossEnv.prompt', {
|
|
28
|
+
currentEnv,
|
|
29
|
+
requestedEnv,
|
|
30
|
+
});
|
|
27
31
|
}
|
|
28
32
|
export function formatCrossEnvRefusalMessage(currentEnv, requestedEnv) {
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|