@nocobase/cli 2.2.0-alpha.1 → 2.2.0-alpha.2
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/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 +92 -0
- package/dist/commands/app/upgrade.js +11 -0
- package/dist/commands/examples/prompts-stages.js +2 -2
- package/dist/commands/examples/prompts-test.js +2 -2
- package/dist/commands/init.js +22 -10
- package/dist/commands/install.js +135 -4
- package/dist/commands/license/activate.js +4 -1
- package/dist/commands/license/shared.js +24 -15
- 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 +67 -2
- package/dist/lib/api-command-compat.js +51 -8
- package/dist/lib/app-managed-resources.js +101 -1
- package/dist/lib/auth-store.js +34 -12
- package/dist/lib/cli-config.js +19 -0
- package/dist/lib/env-config.js +6 -0
- package/dist/lib/hook-script.js +160 -0
- package/dist/lib/prompt-web-ui.js +7 -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 +10 -4
- package/dist/locale/zh-CN.json +10 -4
- package/package.json +7 -2
- package/assets/env-proxy/nginx/app.conf.tpl +0 -23
- package/assets/env-proxy/nginx/nocobase.conf.tpl +0 -5
- package/assets/env-proxy/nginx/snippets/dist-location.conf +0 -5
- package/assets/env-proxy/nginx/snippets/gzip.conf +0 -17
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +0 -13
- package/assets/env-proxy/nginx/snippets/maps-http.conf +0 -14
- package/assets/env-proxy/nginx/snippets/mime-types.conf +0 -98
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +0 -17
- package/assets/env-proxy/nginx/snippets/spa-location.conf +0 -6
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +0 -21
- 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,57 @@ 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 === 'app:restart' || text === 'app:upgrade') {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
return 'app:start';
|
|
44
|
+
}
|
|
45
|
+
function resolveAppStartHookPhase(options) {
|
|
46
|
+
if (options.isPreparedEnv) {
|
|
47
|
+
return 'init';
|
|
48
|
+
}
|
|
49
|
+
if (options.command === 'app:upgrade') {
|
|
50
|
+
return 'upgrade';
|
|
51
|
+
}
|
|
52
|
+
return 'app-start';
|
|
53
|
+
}
|
|
54
|
+
async function runRuntimeHookIfNeeded(params) {
|
|
55
|
+
const hookScript = params.runtime.env.config?.hookScript;
|
|
56
|
+
if (!hookScript) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
60
|
+
appPath: params.runtime.env.appPath,
|
|
61
|
+
hookScript,
|
|
62
|
+
});
|
|
63
|
+
if (!hookScriptPath) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const context = buildHookContext({
|
|
67
|
+
phase: params.phase,
|
|
68
|
+
command: params.command,
|
|
69
|
+
envName: params.runtime.envName,
|
|
70
|
+
source: params.runtime.source,
|
|
71
|
+
version: params.runtime.env.config?.downloadVersion,
|
|
72
|
+
appPath: params.runtime.env.appPath,
|
|
73
|
+
sourcePath: params.runtime.env.sourcePath,
|
|
74
|
+
storagePath: params.runtime.env.storagePath,
|
|
75
|
+
hookScript: String(hookScript).trim(),
|
|
76
|
+
envConfig: params.hookName === 'afterAppStart' && params.phase === 'init'
|
|
77
|
+
? { ...params.runtime.env.config, setupState: 'installed' }
|
|
78
|
+
: params.runtime.env.config,
|
|
79
|
+
});
|
|
80
|
+
if (!context) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await runHookScriptHook({
|
|
84
|
+
hookScriptPath,
|
|
85
|
+
hookName: params.hookName,
|
|
86
|
+
context,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
37
89
|
async function runWithSuppressedTargetEnvLog(task) {
|
|
38
90
|
const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
|
|
39
91
|
process.env.NB_SKIP_TARGET_ENV_LOG = '1';
|
|
@@ -171,6 +223,10 @@ export default class AppStart extends Command {
|
|
|
171
223
|
description: 'Show raw startup output from the underlying local or Docker command',
|
|
172
224
|
default: false,
|
|
173
225
|
}),
|
|
226
|
+
'hook-command': Flags.string({
|
|
227
|
+
hidden: true,
|
|
228
|
+
options: ['app:start', 'app:restart', 'app:upgrade'],
|
|
229
|
+
}),
|
|
174
230
|
};
|
|
175
231
|
async run() {
|
|
176
232
|
const { flags } = await this.parse(AppStart);
|
|
@@ -192,6 +248,12 @@ export default class AppStart extends Command {
|
|
|
192
248
|
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
193
249
|
const preparedInitEnvVars = buildInitAppEnvVarsFromConfig(runtime?.env.config);
|
|
194
250
|
const isPreparedEnv = isPreparedSetupState(runtime?.env.config?.setupState);
|
|
251
|
+
const hookCommand = resolveHookCommand(flags['hook-command']);
|
|
252
|
+
const hookPhase = resolveAppStartHookPhase({
|
|
253
|
+
isPreparedEnv,
|
|
254
|
+
command: hookCommand,
|
|
255
|
+
});
|
|
256
|
+
const shouldRunBeforeAppInstall = isPreparedEnv || hookCommand === 'app:upgrade';
|
|
195
257
|
const runCommand = this.config.runCommand.bind(this.config);
|
|
196
258
|
const commandStdio = flags.verbose ? 'inherit' : 'ignore';
|
|
197
259
|
if (!runtime) {
|
|
@@ -249,6 +311,14 @@ export default class AppStart extends Command {
|
|
|
249
311
|
errorName: 'docker rm',
|
|
250
312
|
stdio: commandStdio,
|
|
251
313
|
}).catch(() => undefined);
|
|
314
|
+
if (shouldRunBeforeAppInstall) {
|
|
315
|
+
await runRuntimeHookIfNeeded({
|
|
316
|
+
hookName: 'beforeAppInstall',
|
|
317
|
+
runtime,
|
|
318
|
+
phase: hookPhase,
|
|
319
|
+
command: hookCommand,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
252
322
|
await recreateSavedDockerApp(runtime, {
|
|
253
323
|
initEnvVars: isPreparedEnv ? preparedInitEnvVars : undefined,
|
|
254
324
|
verbose: flags.verbose,
|
|
@@ -270,6 +340,12 @@ export default class AppStart extends Command {
|
|
|
270
340
|
if (isPreparedEnv) {
|
|
271
341
|
await finalizePreparedEnv(runtime.envName);
|
|
272
342
|
}
|
|
343
|
+
await runRuntimeHookIfNeeded({
|
|
344
|
+
hookName: 'afterAppStart',
|
|
345
|
+
runtime,
|
|
346
|
+
phase: hookPhase,
|
|
347
|
+
command: hookCommand,
|
|
348
|
+
});
|
|
273
349
|
if (shouldPrintStartSuccess()) {
|
|
274
350
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
275
351
|
}
|
|
@@ -285,6 +361,8 @@ export default class AppStart extends Command {
|
|
|
285
361
|
const downloadableRuntime = runtime;
|
|
286
362
|
await ensureSavedLocalSource(downloadableRuntime, runCommand, {
|
|
287
363
|
verbose: flags.verbose,
|
|
364
|
+
hookPhase: isPreparedEnv ? 'init' : 'restore',
|
|
365
|
+
hookCommand,
|
|
288
366
|
onStartTask: startTask,
|
|
289
367
|
onSucceedTask: succeedTask,
|
|
290
368
|
onFailTask: failTask,
|
|
@@ -381,6 +459,14 @@ export default class AppStart extends Command {
|
|
|
381
459
|
...managedAppLifecycleEnvVars(),
|
|
382
460
|
...(isPreparedEnv ? preparedInitEnvVars : {}),
|
|
383
461
|
};
|
|
462
|
+
if (shouldRunBeforeAppInstall) {
|
|
463
|
+
await runRuntimeHookIfNeeded({
|
|
464
|
+
hookName: 'beforeAppInstall',
|
|
465
|
+
runtime,
|
|
466
|
+
phase: hookPhase,
|
|
467
|
+
command: hookCommand,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
384
470
|
await runLocalNocoBaseCommand(runtime, npmArgs, {
|
|
385
471
|
env: startEnv,
|
|
386
472
|
stdio: commandStdio,
|
|
@@ -394,6 +480,12 @@ export default class AppStart extends Command {
|
|
|
394
480
|
if (isPreparedEnv) {
|
|
395
481
|
await finalizePreparedEnv(runtime.envName);
|
|
396
482
|
}
|
|
483
|
+
await runRuntimeHookIfNeeded({
|
|
484
|
+
hookName: 'afterAppStart',
|
|
485
|
+
runtime,
|
|
486
|
+
phase: hookPhase,
|
|
487
|
+
command: hookCommand,
|
|
488
|
+
});
|
|
397
489
|
if (shouldPrintStartSuccess()) {
|
|
398
490
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
399
491
|
}
|
|
@@ -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
|
});
|
|
@@ -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)})`);
|