@nocobase/cli 2.2.0-beta.1 → 2.2.0-beta.11
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/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
- package/bin/early-locale.js +89 -0
- package/bin/node-version.js +35 -0
- package/bin/run.js +9 -0
- package/bin/windows-admin.js +60 -0
- package/dist/commands/app/destroy.js +4 -3
- package/dist/commands/app/restart.js +38 -0
- package/dist/commands/app/shared.js +49 -3
- package/dist/commands/app/start.js +95 -0
- package/dist/commands/app/upgrade.js +11 -0
- package/dist/commands/env/info.js +11 -1
- package/dist/commands/examples/prompts-stages.js +2 -2
- package/dist/commands/examples/prompts-test.js +2 -2
- package/dist/commands/init.js +33 -11
- package/dist/commands/install.js +159 -107
- package/dist/commands/license/activate.js +4 -1
- package/dist/commands/license/shared.js +24 -15
- package/dist/commands/proxy/caddy/generate.js +93 -7
- package/dist/commands/proxy/nginx/generate.js +98 -7
- package/dist/commands/revision/create.js +1 -1
- package/dist/commands/self/check.js +1 -1
- package/dist/commands/self/update.js +4 -4
- package/dist/commands/skills/check.js +4 -5
- package/dist/commands/skills/install.js +18 -1
- package/dist/commands/skills/update.js +19 -4
- package/dist/commands/source/dev.js +9 -5
- package/dist/commands/source/download.js +85 -16
- package/dist/lib/api-command-compat.js +51 -8
- package/dist/lib/app-managed-resources.js +104 -5
- package/dist/lib/auth-store.js +102 -12
- package/dist/lib/cli-config.js +73 -1
- package/dist/lib/docker-image.js +94 -6
- package/dist/lib/env-auth.js +291 -45
- package/dist/lib/env-config.js +11 -0
- package/dist/lib/env-proxy-config.js +48 -0
- package/dist/lib/env-proxy.js +164 -58
- package/dist/lib/hook-script.js +160 -0
- package/dist/lib/prompt-catalog-terminal.js +32 -19
- package/dist/lib/prompt-validators.js +1 -1
- package/dist/lib/prompt-web-ui.js +20 -13
- package/dist/lib/proxy-caddy.js +77 -9
- package/dist/lib/proxy-nginx.js +71 -11
- package/dist/lib/run-npm.js +4 -0
- package/dist/lib/self-manager.js +254 -46
- package/dist/lib/skills-manager.js +116 -23
- package/dist/lib/source-publish.js +2 -2
- package/dist/lib/startup-update.js +1 -1
- package/dist/locale/en-US.json +49 -43
- package/dist/locale/zh-CN.json +49 -43
- package/package.json +8 -2
- package/scripts/build.mjs +0 -34
- package/scripts/clean.mjs +0 -9
- package/tsconfig.json +0 -19
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export function normalizeEarlyCliLocale(value) {
|
|
7
|
+
const normalized = String(value ?? '')
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/\..*$/, '')
|
|
10
|
+
.replace(/_/g, '-')
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
|
|
13
|
+
if (normalized === 'zh' || normalized.startsWith('zh-')) {
|
|
14
|
+
return 'zh-CN';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (normalized === 'en' || normalized.startsWith('en-')) {
|
|
18
|
+
return 'en-US';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readConfiguredEarlyCliLocale() {
|
|
25
|
+
try {
|
|
26
|
+
const cliHomeRoot = String(process.env.NB_CLI_ROOT ?? '').trim() || os.homedir();
|
|
27
|
+
const configPath = path.join(cliHomeRoot, '.nocobase', 'config.json');
|
|
28
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
29
|
+
const parsed = JSON.parse(content);
|
|
30
|
+
return normalizeEarlyCliLocale(parsed?.settings?.locale);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function detectEarlyCliLocale() {
|
|
37
|
+
const candidates = [
|
|
38
|
+
process.env.NB_LOCALE,
|
|
39
|
+
readConfiguredEarlyCliLocale(),
|
|
40
|
+
process.env.LC_ALL,
|
|
41
|
+
process.env.LC_MESSAGES,
|
|
42
|
+
process.env.LANG,
|
|
43
|
+
Intl.DateTimeFormat().resolvedOptions().locale,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
const locale = normalizeEarlyCliLocale(candidate);
|
|
48
|
+
if (locale) {
|
|
49
|
+
return locale;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 'en-US';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getEarlyLocalePathValue(input, key) {
|
|
57
|
+
let current = input;
|
|
58
|
+
for (const part of key.split('.')) {
|
|
59
|
+
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
return typeof current === 'string' ? current : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readEarlyLocaleMessages(locale) {
|
|
68
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const packageRoot = path.resolve(moduleDir, '..');
|
|
70
|
+
const localePaths = [
|
|
71
|
+
path.join(packageRoot, 'src', 'locale', `${locale}.json`),
|
|
72
|
+
path.join(packageRoot, 'dist', 'locale', `${locale}.json`),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const localePath of localePaths) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(fs.readFileSync(localePath, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
// Try the next runtime layout.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function translateEarlyCli(key, fallback, locale = detectEarlyCliLocale()) {
|
|
87
|
+
const messages = readEarlyLocaleMessages(locale);
|
|
88
|
+
return getEarlyLocalePathValue(messages, key) ?? fallback;
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const MINIMUM_NODE_MAJOR_VERSION = 22;
|
|
2
|
+
|
|
3
|
+
export function getNodeMajorVersion(version = process.versions.node) {
|
|
4
|
+
const match = String(version ?? '')
|
|
5
|
+
.trim()
|
|
6
|
+
.match(/^v?(\d+)/);
|
|
7
|
+
|
|
8
|
+
if (!match) {
|
|
9
|
+
return Number.NaN;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return Number.parseInt(match[1], 10);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSupportedNodeVersion(
|
|
16
|
+
version = process.versions.node,
|
|
17
|
+
minimumMajorVersion = MINIMUM_NODE_MAJOR_VERSION,
|
|
18
|
+
) {
|
|
19
|
+
const majorVersion = getNodeMajorVersion(version);
|
|
20
|
+
return Number.isInteger(majorVersion) && majorVersion >= minimumMajorVersion;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatUnsupportedNodeVersionMessage(
|
|
24
|
+
version = process.version,
|
|
25
|
+
minimumMajorVersion = MINIMUM_NODE_MAJOR_VERSION,
|
|
26
|
+
) {
|
|
27
|
+
const currentVersion = String(version ?? '').trim() || 'unknown';
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
`[nocobase cli]: Node.js ${minimumMajorVersion} or later is required to run nb.`,
|
|
31
|
+
`[nocobase cli]: Current version: ${currentVersion}. Please install Node.js ${minimumMajorVersion} or later and try again.`,
|
|
32
|
+
].join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { MINIMUM_NODE_MAJOR_VERSION };
|
package/bin/run.js
CHANGED
|
@@ -6,7 +6,9 @@ import { createRequire } from 'node:module';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { formatUnsupportedNodeVersionMessage, isSupportedNodeVersion } from './node-version.js';
|
|
9
10
|
import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
|
|
11
|
+
import { ensureWindowsAdministrator } from './windows-admin.js';
|
|
10
12
|
|
|
11
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
const requireFromCli = createRequire(import.meta.url);
|
|
@@ -18,9 +20,16 @@ if (process.env.NB_CLI_USE_DIST === '1') {
|
|
|
18
20
|
isDev = false;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
if (!isSupportedNodeVersion()) {
|
|
24
|
+
console.error(pc.red(formatUnsupportedNodeVersionMessage(process.version)));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
normalizeSessionEnv();
|
|
22
29
|
normalizeNodeOptions();
|
|
23
30
|
|
|
31
|
+
ensureWindowsAdministrator();
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
34
|
* In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
|
|
26
35
|
* (same effect as a dedicated dev entry with `#!/usr/bin/env -S node --import tsx`).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { detectEarlyCliLocale, translateEarlyCli } from './early-locale.js';
|
|
4
|
+
|
|
5
|
+
const windowsAdministratorCheckScript = [
|
|
6
|
+
'$identity = [Security.Principal.WindowsIdentity]::GetCurrent();',
|
|
7
|
+
'$principal = New-Object Security.Principal.WindowsPrincipal($identity);',
|
|
8
|
+
'if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }',
|
|
9
|
+
'exit 1',
|
|
10
|
+
].join(' ');
|
|
11
|
+
|
|
12
|
+
export function formatWindowsAdministratorRequiredMessage() {
|
|
13
|
+
const locale = detectEarlyCliLocale();
|
|
14
|
+
const message = translateEarlyCli(
|
|
15
|
+
'entry.windowsAdministratorRequired.message',
|
|
16
|
+
'NocoBase CLI must be run as Administrator on Windows.',
|
|
17
|
+
locale,
|
|
18
|
+
);
|
|
19
|
+
const hint = translateEarlyCli(
|
|
20
|
+
'entry.windowsAdministratorRequired.hint',
|
|
21
|
+
'Open your terminal as Administrator, then run the command again.',
|
|
22
|
+
locale,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return [message, hint].join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isWindowsAdministrator() {
|
|
29
|
+
for (const command of ['pwsh.exe', 'powershell.exe']) {
|
|
30
|
+
const result = spawnSync(
|
|
31
|
+
command,
|
|
32
|
+
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsAdministratorCheckScript],
|
|
33
|
+
{
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (result.error?.code === 'ENOENT') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result.status === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ensureWindowsAdministrator() {
|
|
50
|
+
if (process.platform !== 'win32' || process.env.NB_CLI_WINDOWS_ADMIN_CHECKED === '1') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!isWindowsAdministrator()) {
|
|
55
|
+
console.error(pc.red(formatWindowsAdministratorRequiredMessage()));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.env.NB_CLI_WINDOWS_ADMIN_CHECKED = '1';
|
|
60
|
+
}
|
|
@@ -143,6 +143,7 @@ export default class AppDestroy extends Command {
|
|
|
143
143
|
}
|
|
144
144
|
announceTargetEnv(runtime.envName);
|
|
145
145
|
try {
|
|
146
|
+
const retryCommand = `nb env remove ${runtime.envName} --purge --force`;
|
|
146
147
|
if (runtime.kind === 'docker') {
|
|
147
148
|
startTask(`Removing Docker app container for "${runtime.envName}"...`);
|
|
148
149
|
const state = await removeDockerContainerIfExists(runtime.containerName, {
|
|
@@ -192,7 +193,7 @@ export default class AppDestroy extends Command {
|
|
|
192
193
|
const localAppPath = resolveManagedLocalAppPath(runtime);
|
|
193
194
|
if (localAppPath && removesManagedLocalAppFiles) {
|
|
194
195
|
startTask(`Removing managed local app files for "${runtime.envName}"...`);
|
|
195
|
-
await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"
|
|
196
|
+
await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`, { retryCommand });
|
|
196
197
|
succeedTask(`Managed local app files removed for "${runtime.envName}".`);
|
|
197
198
|
}
|
|
198
199
|
else {
|
|
@@ -206,13 +207,13 @@ export default class AppDestroy extends Command {
|
|
|
206
207
|
];
|
|
207
208
|
startTask(`Removing proxy entry files for "${runtime.envName}"...`);
|
|
208
209
|
for (const proxyEntryDir of proxyEntryDirs) {
|
|
209
|
-
await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"
|
|
210
|
+
await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`, { retryCommand });
|
|
210
211
|
}
|
|
211
212
|
succeedTask(`Proxy entry files removed for "${runtime.envName}".`);
|
|
212
213
|
const configuredStoragePath = resolveConfiguredStoragePath(runtime.env.config);
|
|
213
214
|
if (configuredStoragePath) {
|
|
214
215
|
startTask(`Removing storage data for "${runtime.envName}"...`);
|
|
215
|
-
await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"
|
|
216
|
+
await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`, { retryCommand });
|
|
216
217
|
succeedTask(`Storage data removed for "${runtime.envName}".`);
|
|
217
218
|
}
|
|
218
219
|
else {
|
|
@@ -14,6 +14,7 @@ import { recreateSavedDockerApp } from '../../lib/app-managed-resources.js';
|
|
|
14
14
|
import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
15
15
|
import { run } from '../../lib/run-npm.js';
|
|
16
16
|
import { announceTargetEnv, failTask, startTask, succeedTask } from '../../lib/ui.js';
|
|
17
|
+
import { buildHookContext, resolveHookScriptPath, runHookScriptHook } from '../../lib/hook-script.js';
|
|
17
18
|
function argvHasToken(argv, tokens) {
|
|
18
19
|
return tokens.some((token) => argv.includes(token));
|
|
19
20
|
}
|
|
@@ -62,6 +63,39 @@ function resolveDisplayAppUrl(apiBaseUrl, port, appPublicPath) {
|
|
|
62
63
|
}
|
|
63
64
|
return formatAppUrl(port, appPublicPath);
|
|
64
65
|
}
|
|
66
|
+
async function runDockerAfterAppStartHookIfNeeded(runtime) {
|
|
67
|
+
const hookScript = runtime.env.config?.hookScript;
|
|
68
|
+
if (!hookScript) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
72
|
+
appPath: runtime.env.appPath,
|
|
73
|
+
hookScript,
|
|
74
|
+
});
|
|
75
|
+
if (!hookScriptPath) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const context = buildHookContext({
|
|
79
|
+
phase: 'app-start',
|
|
80
|
+
command: 'app:restart',
|
|
81
|
+
envName: runtime.envName,
|
|
82
|
+
source: runtime.source,
|
|
83
|
+
version: runtime.env.config?.downloadVersion,
|
|
84
|
+
appPath: runtime.env.appPath,
|
|
85
|
+
sourcePath: runtime.env.sourcePath,
|
|
86
|
+
storagePath: runtime.env.storagePath,
|
|
87
|
+
hookScript: String(hookScript).trim(),
|
|
88
|
+
envConfig: runtime.env.config,
|
|
89
|
+
});
|
|
90
|
+
if (!context) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await runHookScriptHook({
|
|
94
|
+
hookScriptPath,
|
|
95
|
+
hookName: 'afterAppStart',
|
|
96
|
+
context,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
65
99
|
export default class AppRestart extends Command {
|
|
66
100
|
static hidden = false;
|
|
67
101
|
static description = 'Restart NocoBase for the selected env. When applicable, the CLI synchronizes licensed commercial plugins first, then restarts the local app or recreates the saved Docker container.';
|
|
@@ -199,6 +233,7 @@ export default class AppRestart extends Command {
|
|
|
199
233
|
logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
|
|
200
234
|
...(flags.verbose ? { verbose: true } : {}),
|
|
201
235
|
});
|
|
236
|
+
await runDockerAfterAppStartHookIfNeeded(runtime);
|
|
202
237
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
203
238
|
return;
|
|
204
239
|
}
|
|
@@ -222,6 +257,9 @@ export default class AppRestart extends Command {
|
|
|
222
257
|
if (daemonFlagWasProvided) {
|
|
223
258
|
startArgv.push(flags.daemon === false ? '--no-daemon' : '--daemon');
|
|
224
259
|
}
|
|
260
|
+
if (runtime.env.config?.hookScript) {
|
|
261
|
+
startArgv.push('--hook-command', 'app:restart');
|
|
262
|
+
}
|
|
225
263
|
await runWithSuppressedTargetEnvLog(async () => {
|
|
226
264
|
await runCommand('app:start', startArgv);
|
|
227
265
|
});
|
|
@@ -25,10 +25,54 @@ function assertSafeRemovalPath(target, label) {
|
|
|
25
25
|
throw new Error(`Refusing to remove ${label} at "${resolved}" because it is too broad.`);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
function getErrorCode(error) {
|
|
29
|
+
if (!(error instanceof Error)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const { code } = error;
|
|
33
|
+
return typeof code === 'string' ? code : undefined;
|
|
34
|
+
}
|
|
35
|
+
function isPermissionDeniedError(error) {
|
|
36
|
+
const code = getErrorCode(error);
|
|
37
|
+
return code === 'EACCES' || code === 'EPERM';
|
|
38
|
+
}
|
|
39
|
+
function formatOriginalError(error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
const code = getErrorCode(error);
|
|
42
|
+
return code && !message.includes(code) ? `${code}: ${message}` : message;
|
|
43
|
+
}
|
|
44
|
+
function quoteShellValue(value) {
|
|
45
|
+
return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
|
|
46
|
+
}
|
|
47
|
+
function formatPermissionDeniedRemovalError(target, label, error, options) {
|
|
48
|
+
const retryLines = os.platform() === 'win32' ? [] : [` sudo chown -R "$(id -u):$(id -g)" ${quoteShellValue(target)}`];
|
|
49
|
+
if (options.retryCommand) {
|
|
50
|
+
retryLines.push(` ${options.retryCommand}`);
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
`Failed to remove ${label} at "${target}".`,
|
|
54
|
+
'The current user cannot delete one or more files under this path. Files may have been created by a Docker container running as root.',
|
|
55
|
+
'',
|
|
56
|
+
retryLines.length > 0
|
|
57
|
+
? 'Fix ownership or permissions, then retry:'
|
|
58
|
+
: 'Fix ownership or permissions, then retry the command.',
|
|
59
|
+
...retryLines,
|
|
60
|
+
'',
|
|
61
|
+
`Original error: ${formatOriginalError(error)}`,
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
export async function removePathIfExists(target, label, options = {}) {
|
|
29
65
|
const resolved = path.resolve(target);
|
|
30
66
|
assertSafeRemovalPath(resolved, label);
|
|
31
|
-
|
|
67
|
+
try {
|
|
68
|
+
await fsp.rm(resolved, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isPermissionDeniedError(error)) {
|
|
72
|
+
throw new Error(formatPermissionDeniedRemovalError(resolved, label, error, options));
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
32
76
|
}
|
|
33
77
|
function isMissingDockerContainerError(error) {
|
|
34
78
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -116,7 +160,9 @@ export function managedDockerNetworkName(runtime) {
|
|
|
116
160
|
return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
|
|
117
161
|
}
|
|
118
162
|
export function resolveManagedLocalAppPath(runtime) {
|
|
119
|
-
return resolveConfiguredAppPath(runtime.env.config) ||
|
|
163
|
+
return (resolveConfiguredAppPath(runtime.env.config) ||
|
|
164
|
+
runtime.projectRoot ||
|
|
165
|
+
resolveConfiguredPath(runtime.env.config.appRootPath));
|
|
120
166
|
}
|
|
121
167
|
export function shouldRemoveManagedLocalAppFiles(runtime) {
|
|
122
168
|
return runtime.source === 'npm' || runtime.source === 'git' || runtime.source === 'local';
|
|
@@ -18,6 +18,7 @@ import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
|
18
18
|
import { readManagedRuntimeEnvValues } from '../../lib/managed-env-file.js';
|
|
19
19
|
import { run } from '../../lib/run-npm.js';
|
|
20
20
|
import { announceTargetEnv, failTask, printInfo, printWarning, startTask, succeedTask } from '../../lib/ui.js';
|
|
21
|
+
import { buildHookContext, resolveHookScriptPath, runHookScriptHook, } from '../../lib/hook-script.js';
|
|
21
22
|
function shouldPrintStartSuccess() {
|
|
22
23
|
return process.env.NB_SKIP_APP_START_SUCCESS_LOG !== '1';
|
|
23
24
|
}
|
|
@@ -34,6 +35,60 @@ function buildLicenseSyncArgv(envName, options) {
|
|
|
34
35
|
}
|
|
35
36
|
return argv;
|
|
36
37
|
}
|
|
38
|
+
function resolveHookCommand(value) {
|
|
39
|
+
const text = String(value ?? '').trim();
|
|
40
|
+
if (text === 'init') {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
if (text === 'app:restart' || text === 'app:upgrade') {
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
return 'app:start';
|
|
47
|
+
}
|
|
48
|
+
function resolveAppStartHookPhase(options) {
|
|
49
|
+
if (options.isPreparedEnv) {
|
|
50
|
+
return 'init';
|
|
51
|
+
}
|
|
52
|
+
if (options.command === 'app:upgrade') {
|
|
53
|
+
return 'upgrade';
|
|
54
|
+
}
|
|
55
|
+
return 'app-start';
|
|
56
|
+
}
|
|
57
|
+
async function runRuntimeHookIfNeeded(params) {
|
|
58
|
+
const hookScript = params.runtime.env.config?.hookScript;
|
|
59
|
+
if (!hookScript) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
63
|
+
appPath: params.runtime.env.appPath,
|
|
64
|
+
hookScript,
|
|
65
|
+
});
|
|
66
|
+
if (!hookScriptPath) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const context = buildHookContext({
|
|
70
|
+
phase: params.phase,
|
|
71
|
+
command: params.command,
|
|
72
|
+
envName: params.runtime.envName,
|
|
73
|
+
source: params.runtime.source,
|
|
74
|
+
version: params.runtime.env.config?.downloadVersion,
|
|
75
|
+
appPath: params.runtime.env.appPath,
|
|
76
|
+
sourcePath: params.runtime.env.sourcePath,
|
|
77
|
+
storagePath: params.runtime.env.storagePath,
|
|
78
|
+
hookScript: String(hookScript).trim(),
|
|
79
|
+
envConfig: params.hookName === 'afterAppStart' && params.phase === 'init'
|
|
80
|
+
? { ...params.runtime.env.config, setupState: 'installed' }
|
|
81
|
+
: params.runtime.env.config,
|
|
82
|
+
});
|
|
83
|
+
if (!context) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await runHookScriptHook({
|
|
87
|
+
hookScriptPath,
|
|
88
|
+
hookName: params.hookName,
|
|
89
|
+
context,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
37
92
|
async function runWithSuppressedTargetEnvLog(task) {
|
|
38
93
|
const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
|
|
39
94
|
process.env.NB_SKIP_TARGET_ENV_LOG = '1';
|
|
@@ -171,6 +226,10 @@ export default class AppStart extends Command {
|
|
|
171
226
|
description: 'Show raw startup output from the underlying local or Docker command',
|
|
172
227
|
default: false,
|
|
173
228
|
}),
|
|
229
|
+
'hook-command': Flags.string({
|
|
230
|
+
hidden: true,
|
|
231
|
+
options: ['init', 'app:start', 'app:restart', 'app:upgrade'],
|
|
232
|
+
}),
|
|
174
233
|
};
|
|
175
234
|
async run() {
|
|
176
235
|
const { flags } = await this.parse(AppStart);
|
|
@@ -192,6 +251,12 @@ export default class AppStart extends Command {
|
|
|
192
251
|
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
193
252
|
const preparedInitEnvVars = buildInitAppEnvVarsFromConfig(runtime?.env.config);
|
|
194
253
|
const isPreparedEnv = isPreparedSetupState(runtime?.env.config?.setupState);
|
|
254
|
+
const hookCommand = resolveHookCommand(flags['hook-command']);
|
|
255
|
+
const hookPhase = resolveAppStartHookPhase({
|
|
256
|
+
isPreparedEnv,
|
|
257
|
+
command: hookCommand,
|
|
258
|
+
});
|
|
259
|
+
const shouldRunBeforeAppInstall = isPreparedEnv || hookCommand === 'app:upgrade';
|
|
195
260
|
const runCommand = this.config.runCommand.bind(this.config);
|
|
196
261
|
const commandStdio = flags.verbose ? 'inherit' : 'ignore';
|
|
197
262
|
if (!runtime) {
|
|
@@ -249,6 +314,14 @@ export default class AppStart extends Command {
|
|
|
249
314
|
errorName: 'docker rm',
|
|
250
315
|
stdio: commandStdio,
|
|
251
316
|
}).catch(() => undefined);
|
|
317
|
+
if (shouldRunBeforeAppInstall) {
|
|
318
|
+
await runRuntimeHookIfNeeded({
|
|
319
|
+
hookName: 'beforeAppInstall',
|
|
320
|
+
runtime,
|
|
321
|
+
phase: hookPhase,
|
|
322
|
+
command: hookCommand,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
252
325
|
await recreateSavedDockerApp(runtime, {
|
|
253
326
|
initEnvVars: isPreparedEnv ? preparedInitEnvVars : undefined,
|
|
254
327
|
verbose: flags.verbose,
|
|
@@ -270,6 +343,12 @@ export default class AppStart extends Command {
|
|
|
270
343
|
if (isPreparedEnv) {
|
|
271
344
|
await finalizePreparedEnv(runtime.envName);
|
|
272
345
|
}
|
|
346
|
+
await runRuntimeHookIfNeeded({
|
|
347
|
+
hookName: 'afterAppStart',
|
|
348
|
+
runtime,
|
|
349
|
+
phase: hookPhase,
|
|
350
|
+
command: hookCommand,
|
|
351
|
+
});
|
|
273
352
|
if (shouldPrintStartSuccess()) {
|
|
274
353
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
275
354
|
}
|
|
@@ -285,6 +364,8 @@ export default class AppStart extends Command {
|
|
|
285
364
|
const downloadableRuntime = runtime;
|
|
286
365
|
await ensureSavedLocalSource(downloadableRuntime, runCommand, {
|
|
287
366
|
verbose: flags.verbose,
|
|
367
|
+
hookPhase: isPreparedEnv ? 'init' : 'restore',
|
|
368
|
+
hookCommand,
|
|
288
369
|
onStartTask: startTask,
|
|
289
370
|
onSucceedTask: succeedTask,
|
|
290
371
|
onFailTask: failTask,
|
|
@@ -381,6 +462,14 @@ export default class AppStart extends Command {
|
|
|
381
462
|
...managedAppLifecycleEnvVars(),
|
|
382
463
|
...(isPreparedEnv ? preparedInitEnvVars : {}),
|
|
383
464
|
};
|
|
465
|
+
if (shouldRunBeforeAppInstall) {
|
|
466
|
+
await runRuntimeHookIfNeeded({
|
|
467
|
+
hookName: 'beforeAppInstall',
|
|
468
|
+
runtime,
|
|
469
|
+
phase: hookPhase,
|
|
470
|
+
command: hookCommand,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
384
473
|
await runLocalNocoBaseCommand(runtime, npmArgs, {
|
|
385
474
|
env: startEnv,
|
|
386
475
|
stdio: commandStdio,
|
|
@@ -394,6 +483,12 @@ export default class AppStart extends Command {
|
|
|
394
483
|
if (isPreparedEnv) {
|
|
395
484
|
await finalizePreparedEnv(runtime.envName);
|
|
396
485
|
}
|
|
486
|
+
await runRuntimeHookIfNeeded({
|
|
487
|
+
hookName: 'afterAppStart',
|
|
488
|
+
runtime,
|
|
489
|
+
phase: hookPhase,
|
|
490
|
+
command: hookCommand,
|
|
491
|
+
});
|
|
397
492
|
if (shouldPrintStartSuccess()) {
|
|
398
493
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
399
494
|
}
|
|
@@ -12,6 +12,7 @@ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '..
|
|
|
12
12
|
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
13
13
|
import { DEFAULT_DOCKER_REGISTRY } from "../../lib/docker-image.js";
|
|
14
14
|
import { confirm } from "../../lib/inquirer.js";
|
|
15
|
+
import { resolveHookScriptPath } from '../../lib/hook-script.js';
|
|
15
16
|
import { announceTargetEnv, isInteractiveTerminal, printInfo, printWarning, succeedTask } from '../../lib/ui.js';
|
|
16
17
|
import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
17
18
|
function trimValue(value) {
|
|
@@ -345,6 +346,13 @@ export default class AppUpgrade extends Command {
|
|
|
345
346
|
if (runtime.env.config.buildDts === true) {
|
|
346
347
|
argv.push('--build-dts');
|
|
347
348
|
}
|
|
349
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
350
|
+
appPath: runtime.env.appPath,
|
|
351
|
+
hookScript: runtime.env.config.hookScript,
|
|
352
|
+
});
|
|
353
|
+
if (hookScriptPath) {
|
|
354
|
+
argv.push('--hook-script', hookScriptPath, '--hook-phase', 'upgrade', '--hook-command', 'app:upgrade', '--hook-env-name', runtime.envName, '--hook-app-path', runtime.env.appPath, '--hook-storage-path', runtime.env.storagePath);
|
|
355
|
+
}
|
|
348
356
|
return argv;
|
|
349
357
|
}
|
|
350
358
|
static buildDockerDownloadArgv(runtime, downloadVersion, options) {
|
|
@@ -497,6 +505,9 @@ export default class AppUpgrade extends Command {
|
|
|
497
505
|
await runWithSuppressedTargetEnvLog(async () => {
|
|
498
506
|
const startArgv = buildManagedActionArgv(runtime.envName, parsed, { quickstart: true });
|
|
499
507
|
startArgv.push('--no-sync-licensed-plugins');
|
|
508
|
+
if (runtime.env.config.hookScript) {
|
|
509
|
+
startArgv.push('--hook-command', 'app:upgrade');
|
|
510
|
+
}
|
|
500
511
|
await runWithSuppressedStartSuccessLog(async () => {
|
|
501
512
|
await runCommand('app:start', startArgv);
|
|
502
513
|
});
|
|
@@ -170,8 +170,13 @@ export default class EnvInfo extends Command {
|
|
|
170
170
|
'auth.accessToken': authGroup.accessToken,
|
|
171
171
|
'auth.refreshToken': authGroup.refreshToken,
|
|
172
172
|
};
|
|
173
|
+
const envGroup = {
|
|
174
|
+
name: runtime.envName,
|
|
175
|
+
kind: runtime.kind,
|
|
176
|
+
};
|
|
173
177
|
const output = {
|
|
174
178
|
ok: true,
|
|
179
|
+
name: runtime.envName,
|
|
175
180
|
env: runtime.envName,
|
|
176
181
|
kind: runtime.kind,
|
|
177
182
|
app: serializeGroup(appGroup),
|
|
@@ -197,6 +202,11 @@ export default class EnvInfo extends Command {
|
|
|
197
202
|
this.log(JSON.stringify(output, null, 2));
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
200
|
-
this.log([
|
|
205
|
+
this.log([
|
|
206
|
+
createGroupTable('Env', envGroup),
|
|
207
|
+
createGroupTable('App', appGroup),
|
|
208
|
+
createGroupTable('DB', dbGroup),
|
|
209
|
+
createGroupTable('API', apiGroup),
|
|
210
|
+
].join('\n\n'));
|
|
201
211
|
}
|
|
202
212
|
}
|
|
@@ -109,8 +109,8 @@ export default class PromptsStages extends Command {
|
|
|
109
109
|
values: presetValues,
|
|
110
110
|
pageTitle: 'nb prompts-stages — Web UI',
|
|
111
111
|
documentHeading: 'nb prompts-stages — `stages` demo',
|
|
112
|
-
onServerStart: ({
|
|
113
|
-
this.log(`Local Web UI (multi-stage) ready — ${url} (listening on ${
|
|
112
|
+
onServerStart: ({ listenHost, port, url }) => {
|
|
113
|
+
this.log(`Local Web UI (multi-stage) ready — ${url} (listening on ${listenHost}:${port}). Submit the form in the browser to continue.`);
|
|
114
114
|
},
|
|
115
115
|
onOpenBrowserError: (url, err) => {
|
|
116
116
|
this.log(`Open this URL in a browser: ${url} (${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -139,8 +139,8 @@ export default class PromptsTest extends Command {
|
|
|
139
139
|
values: presetValues,
|
|
140
140
|
pageTitle: 'nb prompts-test — UI',
|
|
141
141
|
documentHeading: 'nb prompts-test',
|
|
142
|
-
onServerStart: ({
|
|
143
|
-
this.log(`Local Web UI ready — ${url} (listening on ${
|
|
142
|
+
onServerStart: ({ listenHost, port, url }) => {
|
|
143
|
+
this.log(`Local Web UI ready — ${url} (listening on ${listenHost}:${port}). Submit the form in the browser to continue.`);
|
|
144
144
|
},
|
|
145
145
|
onOpenBrowserError: (url, err) => {
|
|
146
146
|
this.log(`Open this URL in a browser: ${url} (${err instanceof Error ? err.message : String(err)})`);
|