@nocobase/cli 2.1.9 → 2.1.11-test.1

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.
@@ -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
  });
@@ -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
  });
@@ -15,7 +15,7 @@ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
15
15
  import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
16
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
17
  import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
- import { resolveDefaultConfigScope } from '../lib/cli-home.js';
18
+ import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
19
19
  import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
20
20
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
21
21
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
@@ -25,6 +25,7 @@ import { installNocoBaseSkills, isNpmRegistryUnavailable } from '../lib/skills-m
25
25
  import { omitKeys, pickKeys } from "../lib/object-utils.js";
26
26
  import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
27
27
  import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
28
+ import { persistHookScript } from '../lib/hook-script.js';
28
29
  import Download from "./download.js";
29
30
  import EnvAdd from "./env/add.js";
30
31
  import Install, { defaultDbPortForDialect } from "./install.js";
@@ -1029,6 +1030,14 @@ Prompt modes:
1029
1030
  const skipDownload = results.skipDownload === true;
1030
1031
  const appKey = resolveManagedAppKey(results.appKey ?? existingEnv?.config.appKey);
1031
1032
  const timeZone = resolveManagedTimeZone(results.timeZone ?? existingEnv?.config.timezone);
1033
+ const hookScriptInput = String(flags['hook-script'] ?? '').trim();
1034
+ const hookAppPath = appPath || `./${envName}/`;
1035
+ const hookScript = hookScriptInput
1036
+ ? await persistHookScript({
1037
+ sourcePath: hookScriptInput,
1038
+ appPath: resolveConfiguredEnvPath(hookAppPath) ?? resolveEnvRelativePath(hookAppPath),
1039
+ })
1040
+ : String(results.hookScript ?? '').trim();
1032
1041
  const builtinDb = explicitDbHostFlag(flags)
1033
1042
  ? false
1034
1043
  : results.builtinDb === undefined
@@ -1055,6 +1064,7 @@ Prompt modes:
1055
1064
  ...(dockerPlatform ? { dockerPlatform } : {}),
1056
1065
  ...(gitUrl ? { gitUrl } : {}),
1057
1066
  ...(npmRegistry ? { npmRegistry } : {}),
1067
+ ...(hookScript ? { hookScript } : {}),
1058
1068
  ...(appPath ? { appPath } : {}),
1059
1069
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
1060
1070
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
@@ -1130,6 +1140,10 @@ Prompt modes:
1130
1140
  if (flags['prepare-only']) {
1131
1141
  argv.push('--prepare-only');
1132
1142
  }
1143
+ const hookScript = String(results.hookScript ?? flags['hook-script'] ?? '').trim();
1144
+ if (hookScript) {
1145
+ argv.push('--hook-script', hookScript);
1146
+ }
1133
1147
  if (flags.verbose) {
1134
1148
  argv.push('--verbose');
1135
1149
  }
@@ -15,7 +15,7 @@ import { appendAppPublicPath } from '../lib/app-public-path.js';
15
15
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
16
16
  import { applyCliLocale, localeText, resolveCliLocale, translateCli } from "../lib/cli-locale.js";
17
17
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
18
- import { defaultDockerContainerPrefix, defaultDockerNetworkName } from '../lib/app-runtime.js';
18
+ import { defaultDockerContainerPrefix, defaultDockerNetworkName, managedAppLifecycleEnvVars, } from '../lib/app-runtime.js';
19
19
  import { resolveDefaultApiHost, resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
20
20
  import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../lib/docker-image.js";
21
21
  import { findAvailableTcpPort, validateAppPublicPath, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
@@ -29,6 +29,7 @@ import { buildStoredEnvConfig } from '../lib/env-config.js';
29
29
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
30
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
31
  import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
32
+ import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
32
33
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
33
34
  import Download, { defaultDockerRegistryForLang } from './download.js';
34
35
  import EnvAdd from "./env/add.js";
@@ -472,7 +473,10 @@ export default class Install extends Command {
472
473
  description: 'Skip the download step and reuse an existing local app directory or Docker image',
473
474
  default: false,
474
475
  }),
475
- ...omitKeys(Download.flags, ['yes']),
476
+ 'hook-script': Flags.string({
477
+ description: 'Hook module copied to <appPath>/.nb/hooks.mjs and run before npm/git dependency installation.',
478
+ }),
479
+ ...omitKeys(Download.flags, ['yes', 'hook-script']),
476
480
  };
477
481
  /** Environment name only: run before {@link Install.prompts} (see `run`). */
478
482
  static envPrompts = {
@@ -1095,6 +1099,7 @@ export default class Install extends Command {
1095
1099
  const appPort = Install.toOptionalPromptString(config.appPort);
1096
1100
  const storagePath = Install.toOptionalPromptString(config.storagePath);
1097
1101
  const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
1102
+ const hookScript = Install.toOptionalPromptString(config.hookScript);
1098
1103
  const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
1099
1104
  const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
1100
1105
  const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
@@ -1125,6 +1130,7 @@ export default class Install extends Command {
1125
1130
  ...(appPort ? { appPort } : {}),
1126
1131
  ...(storagePath ? { storagePath } : {}),
1127
1132
  ...(appPublicPath ? { appPublicPath } : {}),
1133
+ ...(hookScript ? { hookScript } : {}),
1128
1134
  };
1129
1135
  const downloadPreset = {
1130
1136
  ...(source ? { source } : {}),
@@ -1916,6 +1922,12 @@ export default class Install extends Command {
1916
1922
  if (results.buildDts) {
1917
1923
  argv.push('--build-dts');
1918
1924
  }
1925
+ Install.pushDownloadArgIfValue(argv, '--hook-script', results.hookScript);
1926
+ Install.pushDownloadArgIfValue(argv, '--hook-phase', results.hookPhase);
1927
+ Install.pushDownloadArgIfValue(argv, '--hook-command', results.hookCommand);
1928
+ Install.pushDownloadArgIfValue(argv, '--hook-env-name', results.hookEnvName);
1929
+ Install.pushDownloadArgIfValue(argv, '--hook-app-path', results.hookAppPath);
1930
+ Install.pushDownloadArgIfValue(argv, '--hook-storage-path', results.hookStoragePath);
1919
1931
  return argv;
1920
1932
  }
1921
1933
  static resolveLocalProjectRoot(params) {
@@ -1942,6 +1954,40 @@ export default class Install extends Command {
1942
1954
  buildDts: false,
1943
1955
  };
1944
1956
  }
1957
+ static resolveAbsoluteAppPath(envName, appResults) {
1958
+ const configuredAppPath = resolveConfiguredAppPathValue(appResults) ?? defaultInstallAppPath(envName);
1959
+ return resolveConfiguredEnvPath(configuredAppPath) ?? resolveEnvRelativePath(defaultInstallAppPath(envName));
1960
+ }
1961
+ static resolveAbsoluteStoragePath(envName, appResults) {
1962
+ const configuredStoragePath = resolveConfiguredStoragePathValue(appResults, envName);
1963
+ return (resolveConfiguredEnvPath(configuredStoragePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(envName)));
1964
+ }
1965
+ async prepareHookScriptForInstall(params) {
1966
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
1967
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
1968
+ const inputHookScript = Install.toOptionalPromptString(params.hookScript);
1969
+ if (inputHookScript) {
1970
+ params.appResults.hookScript = await persistHookScript({
1971
+ sourcePath: inputHookScript,
1972
+ appPath,
1973
+ });
1974
+ printInfo(`Saved hook script to ${path.join(appPath, String(params.appResults.hookScript))}.`);
1975
+ }
1976
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
1977
+ const hookScriptPath = resolveHookScriptPath({
1978
+ appPath,
1979
+ hookScript: savedHookScript,
1980
+ });
1981
+ if (!hookScriptPath) {
1982
+ return;
1983
+ }
1984
+ params.downloadResults.hookScript = hookScriptPath;
1985
+ params.downloadResults.hookPhase = 'init';
1986
+ params.downloadResults.hookCommand = 'init';
1987
+ params.downloadResults.hookEnvName = params.envName;
1988
+ params.downloadResults.hookAppPath = appPath;
1989
+ params.downloadResults.hookStoragePath = storagePath;
1990
+ }
1945
1991
  commandStdio(verbose) {
1946
1992
  return verbose ? 'inherit' : 'ignore';
1947
1993
  }
@@ -2011,7 +2057,9 @@ export default class Install extends Command {
2011
2057
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
2012
2058
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
2013
2059
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
2060
+ const lifecycleEnvVars = managedAppLifecycleEnvVars();
2014
2061
  const env = {
2062
+ ...lifecycleEnvVars,
2015
2063
  STORAGE_PATH: storagePath,
2016
2064
  APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
2017
2065
  APP_KEY: appKey,
@@ -2216,6 +2264,44 @@ export default class Install extends Command {
2216
2264
  }
2217
2265
  await this.config.runCommand('env:update', [params.envName]);
2218
2266
  }
2267
+ async runInstallHookIfNeeded(params) {
2268
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2269
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
2270
+ const hookScriptPath = resolveHookScriptPath({
2271
+ appPath,
2272
+ hookScript: savedHookScript,
2273
+ });
2274
+ if (!hookScriptPath) {
2275
+ return;
2276
+ }
2277
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
2278
+ const sourcePath = params.projectRoot ??
2279
+ resolveConfiguredEnvPath(resolveConfiguredSourcePathValue(params.appResults, params.envName)) ??
2280
+ path.join(appPath, 'source');
2281
+ const context = buildHookContext({
2282
+ phase: 'init',
2283
+ command: 'init',
2284
+ envName: params.envName,
2285
+ source: params.source,
2286
+ version: downloadResultsValue(params.downloadResults, 'version'),
2287
+ appPath,
2288
+ sourcePath,
2289
+ storagePath,
2290
+ hookScript: savedHookScript,
2291
+ envConfig: Install.buildSavedEnvConfig(params, {
2292
+ defaultApiHost: params.defaultApiHost,
2293
+ }),
2294
+ });
2295
+ if (!context) {
2296
+ return;
2297
+ }
2298
+ this.log(`Running hook ${params.hookName}: ${hookScriptPath}`);
2299
+ await runHookScriptHook({
2300
+ hookScriptPath,
2301
+ hookName: params.hookName,
2302
+ context,
2303
+ });
2304
+ }
2219
2305
  static buildSavedEnvConfig(params, options = {}) {
2220
2306
  const appPath = resolveConfiguredAppPathValue(params.appResults);
2221
2307
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
@@ -2247,6 +2333,7 @@ export default class Install extends Command {
2247
2333
  devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2248
2334
  build: downloadResultsValue(params.downloadResults, 'build'),
2249
2335
  buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2336
+ hookScript: params.appResults.hookScript,
2250
2337
  ...(appPath ? { appPath } : {}),
2251
2338
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
2252
2339
  appPort,
@@ -2311,6 +2398,9 @@ export default class Install extends Command {
2311
2398
  yesInitialValues: { resume: parsed.resume },
2312
2399
  yes,
2313
2400
  });
2401
+ if (resumePreset?.appPreset?.hookScript !== undefined && appResults.hookScript === undefined) {
2402
+ appResults.hookScript = resumePreset.appPreset.hookScript;
2403
+ }
2314
2404
  const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2315
2405
  downloadOpts.values = {
2316
2406
  ...(resumePreset?.downloadPreset ?? {}),
@@ -2440,6 +2530,12 @@ export default class Install extends Command {
2440
2530
  appResults,
2441
2531
  });
2442
2532
  appResults.setupState = 'prepared';
2533
+ await this.prepareHookScriptForInstall({
2534
+ envName,
2535
+ appResults,
2536
+ downloadResults,
2537
+ hookScript: parsed['hook-script'],
2538
+ });
2443
2539
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2444
2540
  const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2445
2541
  const dockerNetworkName = usesDockerResources
@@ -2505,6 +2601,17 @@ export default class Install extends Command {
2505
2601
  printInfo('Application image ready.');
2506
2602
  }
2507
2603
  if (!parsed['prepare-only']) {
2604
+ await this.runInstallHookIfNeeded({
2605
+ hookName: 'beforeAppInstall',
2606
+ envName,
2607
+ source,
2608
+ appResults,
2609
+ downloadResults,
2610
+ dbResults,
2611
+ rootResults,
2612
+ envAddResults,
2613
+ defaultApiHost,
2614
+ });
2508
2615
  dockerAppPlan = await this.installDockerApp({
2509
2616
  envName,
2510
2617
  dockerNetworkName,
@@ -2523,7 +2630,7 @@ export default class Install extends Command {
2523
2630
  }
2524
2631
  else if (source === 'npm' || source === 'git') {
2525
2632
  const localSource = source === 'npm' ? 'npm' : 'git';
2526
- const projectRoot = parsed['skip-download']
2633
+ const projectRoot = parsed['skip-download'] || parsed['prepare-only']
2527
2634
  ? Install.resolveLocalProjectRoot({
2528
2635
  envName,
2529
2636
  appResults,
@@ -2535,10 +2642,22 @@ export default class Install extends Command {
2535
2642
  downloadResults,
2536
2643
  verbose: parsed.verbose,
2537
2644
  });
2538
- if (!parsed['skip-download']) {
2645
+ if (!parsed['skip-download'] && !parsed['prepare-only']) {
2539
2646
  printInfo('Application files ready.');
2540
2647
  }
2541
2648
  if (!parsed['prepare-only']) {
2649
+ await this.runInstallHookIfNeeded({
2650
+ hookName: 'beforeAppInstall',
2651
+ envName,
2652
+ source,
2653
+ appResults,
2654
+ downloadResults,
2655
+ dbResults,
2656
+ rootResults,
2657
+ envAddResults,
2658
+ projectRoot,
2659
+ defaultApiHost,
2660
+ });
2542
2661
  localAppPlan = await this.startLocalApp({
2543
2662
  envName,
2544
2663
  source: localSource,
@@ -2573,6 +2692,18 @@ export default class Install extends Command {
2573
2692
  });
2574
2693
  printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
2575
2694
  appResults.setupState = 'installed';
2695
+ await this.runInstallHookIfNeeded({
2696
+ hookName: 'afterAppStart',
2697
+ envName,
2698
+ source,
2699
+ appResults,
2700
+ downloadResults,
2701
+ dbResults,
2702
+ rootResults,
2703
+ envAddResults,
2704
+ projectRoot: localAppPlan?.projectRoot,
2705
+ defaultApiHost,
2706
+ });
2576
2707
  }
2577
2708
  if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2578
2709
  await this.saveInstalledEnv({
@@ -26,7 +26,7 @@ function formatSkillsUpdateMessage(result, verbose) {
26
26
  }
27
27
  export default class SelfUpdate extends Command {
28
28
  static summary = 'Update the globally installed NocoBase CLI';
29
- static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm install.';
29
+ static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm, pnpm, or yarn install.';
30
30
  static examples = [
31
31
  '<%= config.bin %> <%= command.id %>',
32
32
  '<%= config.bin %> <%= command.id %> --yes',
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
- import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
11
+ import { ensureLocalPostinstall, ensureNpmSourceDevDependencies } from '../../lib/app-managed-resources.js';
12
12
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
13
+ import { findAvailableTcpPort } from '../../lib/prompt-validators.js';
13
14
  import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
14
15
  function formatUnsupportedRuntimeMessage(kind, envName) {
15
16
  if (kind === 'docker') {
@@ -129,10 +130,7 @@ export default class SourceDev extends Command {
129
130
  this.error(formatUnsupportedRuntimeMessage(runtime.kind, runtime.envName));
130
131
  }
131
132
  announceTargetEnv(runtime.envName);
132
- const devPort = flags.port ||
133
- (runtime.env.appPort !== undefined && runtime.env.appPort !== null
134
- ? String(runtime.env.appPort).trim()
135
- : undefined);
133
+ const devPort = flags.port?.trim() || (await findAvailableTcpPort());
136
134
  const appUrl = appUrlForPort(devPort);
137
135
  if (await isAppAlreadyRunning(appUrl)) {
138
136
  this.error([
@@ -160,6 +158,12 @@ export default class SourceDev extends Command {
160
158
  }
161
159
  printInfo(`Starting NocoBase dev mode for "${runtime.envName}" from ${runtime.projectRoot}. Press Ctrl+C to stop.`);
162
160
  try {
161
+ await ensureNpmSourceDevDependencies(runtime, {
162
+ onStartTask: startTask,
163
+ onSucceedTask: succeedTask,
164
+ onFailTask: failTask,
165
+ verbose: true,
166
+ });
163
167
  await ensureLocalPostinstall(runtime, {
164
168
  onStartTask: startTask,
165
169
  onSucceedTask: succeedTask,