@nocobase/cli 2.2.0-beta.1 → 2.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/bin/early-locale.js +89 -0
  3. package/bin/node-version.js +35 -0
  4. package/bin/run.js +9 -0
  5. package/bin/windows-admin.js +60 -0
  6. package/dist/commands/app/destroy.js +4 -3
  7. package/dist/commands/app/restart.js +38 -0
  8. package/dist/commands/app/shared.js +49 -3
  9. package/dist/commands/app/start.js +95 -0
  10. package/dist/commands/app/upgrade.js +11 -0
  11. package/dist/commands/env/info.js +11 -1
  12. package/dist/commands/examples/prompts-stages.js +2 -2
  13. package/dist/commands/examples/prompts-test.js +2 -2
  14. package/dist/commands/init.js +33 -11
  15. package/dist/commands/install.js +159 -107
  16. package/dist/commands/license/activate.js +4 -1
  17. package/dist/commands/license/shared.js +24 -15
  18. package/dist/commands/proxy/caddy/generate.js +93 -7
  19. package/dist/commands/proxy/nginx/generate.js +98 -7
  20. package/dist/commands/revision/create.js +1 -1
  21. package/dist/commands/self/check.js +1 -1
  22. package/dist/commands/self/update.js +4 -4
  23. package/dist/commands/skills/check.js +4 -5
  24. package/dist/commands/skills/install.js +18 -1
  25. package/dist/commands/skills/update.js +19 -4
  26. package/dist/commands/source/dev.js +9 -5
  27. package/dist/commands/source/download.js +85 -16
  28. package/dist/lib/api-command-compat.js +51 -8
  29. package/dist/lib/app-managed-resources.js +104 -5
  30. package/dist/lib/auth-store.js +102 -12
  31. package/dist/lib/cli-config.js +73 -1
  32. package/dist/lib/docker-image.js +94 -6
  33. package/dist/lib/env-auth.js +291 -45
  34. package/dist/lib/env-config.js +11 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +164 -58
  37. package/dist/lib/hook-script.js +160 -0
  38. package/dist/lib/prompt-catalog-terminal.js +32 -19
  39. package/dist/lib/prompt-validators.js +1 -1
  40. package/dist/lib/prompt-web-ui.js +20 -13
  41. package/dist/lib/proxy-caddy.js +77 -9
  42. package/dist/lib/proxy-nginx.js +71 -11
  43. package/dist/lib/run-npm.js +4 -0
  44. package/dist/lib/self-manager.js +254 -46
  45. package/dist/lib/skills-manager.js +116 -23
  46. package/dist/lib/source-publish.js +2 -2
  47. package/dist/lib/startup-update.js +1 -1
  48. package/dist/locale/en-US.json +49 -43
  49. package/dist/locale/zh-CN.json +49 -43
  50. package/package.json +8 -2
  51. package/scripts/build.mjs +0 -34
  52. package/scripts/clean.mjs +0 -9
  53. package/tsconfig.json +0 -19
@@ -12,11 +12,15 @@ import path from 'node:path';
12
12
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
13
13
  import { runPromptCatalog, } from "../../lib/prompt-catalog.js";
14
14
  import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, resolveCliLocale, translateCli, } from "../../lib/cli-locale.js";
15
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_REGISTRY_ZH_CN, resolveDockerImageRef, } from "../../lib/docker-image.js";
15
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_REGISTRY_ZH_CN, resolveOfficialDockerRegistry, resolveDockerImageRef, } from "../../lib/docker-image.js";
16
+ import { getEnv } from '../../lib/auth-store.js';
17
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
18
+ import { getCliConfigValue } from '../../lib/cli-config.js';
19
+ import { buildBeforeDependencyInstallHookContext, runBeforeDependencyInstallHook, } from '../../lib/hook-script.js';
16
20
  import { run } from "../../lib/run-npm.js";
17
21
  import { printVerbose, setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
18
22
  const DEFAULT_DOCKER_PLATFORM = 'auto';
19
- const DEFAULT_DOWNLOAD_VERSION = 'beta';
23
+ const DEFAULT_DOWNLOAD_VERSION = 'latest';
20
24
  const downloadText = (key, values) => localeText(`commands.download.${key}`, values);
21
25
  const downloadTranslatedText = (key, values, fallback) => translateCli(`commands.download.${key}`, values, { fallback });
22
26
  function defaultOutputDirForVersion(versionTag) {
@@ -231,6 +235,26 @@ export default class SourceDownload extends Command {
231
235
  'npm-registry': Flags.string({
232
236
  description: 'npm registry for npm/git downloads and dependency installation.',
233
237
  }),
238
+ 'hook-script': Flags.string({
239
+ description: 'Hook module to run after npm scaffold or git clone and before dependency installation.',
240
+ }),
241
+ 'hook-phase': Flags.string({
242
+ hidden: true,
243
+ options: ['init', 'upgrade', 'restore', 'source-download', 'app-start'],
244
+ }),
245
+ 'hook-command': Flags.string({
246
+ hidden: true,
247
+ options: ['init', 'source:download', 'app:start', 'app:restart', 'app:upgrade'],
248
+ }),
249
+ 'hook-env-name': Flags.string({
250
+ hidden: true,
251
+ }),
252
+ 'hook-app-path': Flags.string({
253
+ hidden: true,
254
+ }),
255
+ 'hook-storage-path': Flags.string({
256
+ hidden: true,
257
+ }),
234
258
  build: Flags.boolean({
235
259
  allowNo: true,
236
260
  description: 'Build npm/git source after dependencies are installed.',
@@ -276,7 +300,6 @@ export default class SourceDownload extends Command {
276
300
  value: 'latest',
277
301
  label: downloadText('prompts.version.latestLabel'),
278
302
  hint: downloadText('prompts.version.latestHint'),
279
- disabled: true,
280
303
  },
281
304
  {
282
305
  value: 'beta',
@@ -322,8 +345,8 @@ export default class SourceDownload extends Command {
322
345
  label: downloadText('prompts.dockerPlatform.autoLabel'),
323
346
  hint: downloadText('prompts.dockerPlatform.autoHint'),
324
347
  },
325
- { value: 'linux/amd64', label: 'linux/amd64' },
326
- { value: 'linux/arm64', label: 'linux/arm64' },
348
+ { value: 'linux/amd64', label: 'amd64' },
349
+ { value: 'linux/arm64', label: 'arm64' },
327
350
  ],
328
351
  initialValue: DEFAULT_DOCKER_PLATFORM,
329
352
  yesInitialValue: DEFAULT_DOCKER_PLATFORM,
@@ -428,9 +451,9 @@ export default class SourceDownload extends Command {
428
451
  }
429
452
  return outputAbs;
430
453
  }
431
- dockerTarPath(flags, outputAbs) {
454
+ async dockerTarPath(flags, outputAbs) {
432
455
  const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
433
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
456
+ defaultRegistry: await this.resolveConfiguredDockerRegistryDefault(),
434
457
  defaultVersion: 'latest',
435
458
  });
436
459
  const safeBase = imageRef.replace(/[\\/:]/g, '-');
@@ -440,9 +463,11 @@ export default class SourceDownload extends Command {
440
463
  * Defaults for prompts only. Keys present in **`preset`** are omitted so `runPromptCatalog` uses
441
464
  * **`values`** (preset) alone for those steps — no duplicate prefill for skipped prompts.
442
465
  */
443
- buildInitialValuesFromParsed(flags, preset) {
466
+ async resolveConfiguredDockerRegistryDefault() {
467
+ return resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
468
+ }
469
+ buildInitialValuesFromParsed(flags, preset, defaultDockerRegistry) {
444
470
  const initialValues = {};
445
- const localeDefaultDockerRegistry = defaultDockerRegistryForLang(flags.locale ?? process.env.NB_LOCALE);
446
471
  const source = flags.source?.trim();
447
472
  if (source) {
448
473
  initialValues.source = source;
@@ -466,7 +491,7 @@ export default class SourceDownload extends Command {
466
491
  initialValues.dockerRegistry = String(flags['docker-registry'] ?? '').trim();
467
492
  }
468
493
  else {
469
- initialValues.dockerRegistry = localeDefaultDockerRegistry;
494
+ initialValues.dockerRegistry = defaultDockerRegistry;
470
495
  }
471
496
  initialValues.dockerPlatform = normalizeDockerPlatform(flags['docker-platform']);
472
497
  initialValues.dockerSave = flags['docker-save'];
@@ -547,7 +572,7 @@ export default class SourceDownload extends Command {
547
572
  }
548
573
  return flags.build;
549
574
  }
550
- mapCatalogResultsToResolved(results, flags) {
575
+ mapCatalogResultsToResolved(results, flags, defaultDockerRegistry) {
551
576
  const source = String(results.source);
552
577
  const version = resolveVersionFromResults(results, flags.version) || 'latest';
553
578
  const devDependencies = source === 'npm' ? Boolean(results.devDependencies) : undefined;
@@ -561,7 +586,7 @@ export default class SourceDownload extends Command {
561
586
  const dockerRegistry = source === 'docker'
562
587
  ? results.dockerRegistry !== undefined
563
588
  ? String(results.dockerRegistry).trim() || undefined
564
- : flags['docker-registry']?.trim() || defaultDockerRegistryForLang(flags.locale ?? process.env.NB_LOCALE)
589
+ : flags['docker-registry']?.trim() || defaultDockerRegistry
565
590
  : undefined;
566
591
  const dockerPlatform = source === 'docker'
567
592
  ? normalizeDockerPlatform(results.dockerPlatform !== undefined ? results.dockerPlatform : flags['docker-platform'])
@@ -574,6 +599,9 @@ export default class SourceDownload extends Command {
574
599
  const npmRegistryRaw = results.npmRegistry !== undefined ? String(results.npmRegistry) : flags['npm-registry'] ?? '';
575
600
  const npmRegistry = npmRegistryRaw.trim() || undefined;
576
601
  const npmDevDependencies = devDependencies ?? false;
602
+ const hookScript = flags['hook-script']?.trim()
603
+ ? path.resolve(process.cwd(), flags['hook-script'].trim())
604
+ : undefined;
577
605
  return {
578
606
  source,
579
607
  version,
@@ -587,6 +615,14 @@ export default class SourceDownload extends Command {
587
615
  ...(source === 'docker' ? { 'docker-platform': dockerPlatform } : {}),
588
616
  ...(source === 'docker' ? { 'docker-save': dockerSave } : {}),
589
617
  ...(npmRegistry ? { 'npm-registry': npmRegistry } : {}),
618
+ ...(hookScript ? { 'hook-script': hookScript } : {}),
619
+ ...(flags['hook-phase'] ? { 'hook-phase': flags['hook-phase'] } : {}),
620
+ ...(flags['hook-command'] ? { 'hook-command': flags['hook-command'] } : {}),
621
+ ...(flags['hook-env-name'] ? { 'hook-env-name': flags['hook-env-name'].trim() } : {}),
622
+ ...(flags['hook-app-path'] ? { 'hook-app-path': path.resolve(process.cwd(), flags['hook-app-path']) } : {}),
623
+ ...(flags['hook-storage-path']
624
+ ? { 'hook-storage-path': path.resolve(process.cwd(), flags['hook-storage-path']) }
625
+ : {}),
590
626
  };
591
627
  }
592
628
  async resolveDownloadFlags(flags) {
@@ -595,7 +631,8 @@ export default class SourceDownload extends Command {
595
631
  this.error('Download source is required in non-interactive mode. Use --source npm, --source git, or --source docker.');
596
632
  }
597
633
  const presetValues = this.buildPresetValuesFromFlags(flags);
598
- const initialValues = this.buildInitialValuesFromParsed(flags, presetValues);
634
+ const defaultDockerRegistry = await this.resolveConfiguredDockerRegistryDefault();
635
+ const initialValues = this.buildInitialValuesFromParsed(flags, presetValues, defaultDockerRegistry);
599
636
  const results = await runPromptCatalog(SourceDownload.prompts, {
600
637
  initialValues,
601
638
  values: presetValues,
@@ -621,7 +658,7 @@ export default class SourceDownload extends Command {
621
658
  if (flags['docker-save'] && source !== 'docker') {
622
659
  this.error('--docker-save is only available when --source docker is selected.');
623
660
  }
624
- return this.mapCatalogResultsToResolved(results, flags);
661
+ return this.mapCatalogResultsToResolved(results, flags, defaultDockerRegistry);
625
662
  }
626
663
  npmRegistryUrl(flags) {
627
664
  const url = flags['npm-registry']?.trim();
@@ -719,9 +756,39 @@ export default class SourceDownload extends Command {
719
756
  }
720
757
  return argv;
721
758
  }
759
+ async runBeforeDependencyInstallHookIfNeeded(flags, projectRoot) {
760
+ const hookScript = flags['hook-script']?.trim();
761
+ if (!hookScript || (flags.source !== 'npm' && flags.source !== 'git')) {
762
+ return;
763
+ }
764
+ const envName = flags['hook-env-name']?.trim() || '';
765
+ const env = envName ? await getEnv(envName, { scope: resolveDefaultConfigScope() }) : undefined;
766
+ const appPath = flags['hook-app-path']?.trim() || projectRoot;
767
+ const storagePath = flags['hook-storage-path']?.trim() || path.join(appPath, 'storage');
768
+ const context = buildBeforeDependencyInstallHookContext({
769
+ phase: flags['hook-phase'] ?? 'source-download',
770
+ command: flags['hook-command'] ?? 'source:download',
771
+ envName,
772
+ source: flags.source,
773
+ version: flags.version,
774
+ appPath,
775
+ sourcePath: projectRoot,
776
+ storagePath,
777
+ hookScript,
778
+ envConfig: env?.config ?? {},
779
+ });
780
+ if (!context) {
781
+ return;
782
+ }
783
+ this.log(`Running hook before dependency install: ${hookScript}`);
784
+ await runBeforeDependencyInstallHook({
785
+ hookScriptPath: hookScript,
786
+ context,
787
+ });
788
+ }
722
789
  async downloadFromDocker(flags) {
723
790
  const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
724
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
791
+ defaultRegistry: await this.resolveConfiguredDockerRegistryDefault(),
725
792
  defaultVersion: 'latest',
726
793
  });
727
794
  const platform = dockerPlatformArg(flags['docker-platform']);
@@ -748,7 +815,7 @@ export default class SourceDownload extends Command {
748
815
  await fsp.rm(outAbs, { recursive: true, force: true });
749
816
  }
750
817
  await fsp.mkdir(outAbs, { recursive: true });
751
- const tarPath = this.dockerTarPath(flags, outAbs);
818
+ const tarPath = await this.dockerTarPath(flags, outAbs);
752
819
  this.log(`Saving Docker image tarball to ${tarPath}`);
753
820
  await this.runExternalCommand('docker', ['save', '-o', tarPath, imageRef], {
754
821
  errorName: 'docker save',
@@ -778,6 +845,7 @@ export default class SourceDownload extends Command {
778
845
  errorName: 'npx create-nocobase-app',
779
846
  loadingMessage: 'Creating the app scaffold',
780
847
  });
848
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
781
849
  const installArgs = ['install'];
782
850
  if (!flags['dev-dependencies']) {
783
851
  installArgs.push('--production');
@@ -821,6 +889,7 @@ export default class SourceDownload extends Command {
821
889
  });
822
890
  const projectRoot = path.resolve(process.cwd(), outputDir);
823
891
  const registryEnv = this.npmRegistryEnv(flags);
892
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
824
893
  this.log(`Installing dependencies in ${projectRoot}`);
825
894
  await this.runExternalCommand('yarn', ['install'], {
826
895
  ...this.runOptionsWithCwd(projectRoot, registryEnv),
@@ -114,12 +114,22 @@ function compareWithOperator(version, operator, expected) {
114
114
  }
115
115
  function normalizeAppByChannelComparableVersion(version) {
116
116
  const normalized = String(version ?? '').trim();
117
- const match = normalized.match(/^(\d+\.\d+\.\d+)-(alpha|beta|rc)(?:\.(\d+))?(?:\..+)?$/);
117
+ const match = normalized.match(/^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-.]+))?$/);
118
118
  if (!match) {
119
119
  return normalized;
120
120
  }
121
- const [, base, channel, sequence] = match;
122
- return sequence ? `${base}-${channel}.${sequence}` : `${base}-${channel}`;
121
+ const [, base, prerelease] = match;
122
+ if (!prerelease) {
123
+ return base;
124
+ }
125
+ const [channel, sequence] = prerelease.split('.');
126
+ if ((channel === 'alpha' || channel === 'beta') && sequence && /^\d+$/.test(sequence)) {
127
+ return `${base}-${channel}.${sequence}`;
128
+ }
129
+ if (channel === 'alpha' || channel === 'beta') {
130
+ return `${base}-${channel}`;
131
+ }
132
+ return base;
123
133
  }
124
134
  function normalizeAppByChannelCondition(condition) {
125
135
  if (!condition) {
@@ -134,6 +144,42 @@ function normalizeAppByChannelCondition(condition) {
134
144
  }
135
145
  return normalized;
136
146
  }
147
+ function compareAppByChannelVersions(version, expected) {
148
+ const comparableVersion = normalizeAppByChannelComparableVersion(version);
149
+ const comparableExpected = normalizeAppByChannelComparableVersion(expected);
150
+ if (comparableVersion === comparableExpected ||
151
+ comparableVersion.startsWith(`${comparableExpected}.`) ||
152
+ comparableVersion.startsWith(`${comparableExpected}-`)) {
153
+ return 0;
154
+ }
155
+ return compareVersions(comparableVersion, comparableExpected);
156
+ }
157
+ function compareAppByChannelWithOperator(version, operator, expected) {
158
+ const compared = compareAppByChannelVersions(version, expected);
159
+ switch (operator) {
160
+ case 'eq':
161
+ return compared === 0;
162
+ case 'gt':
163
+ return compared > 0;
164
+ case 'gte':
165
+ return compared >= 0;
166
+ case 'lt':
167
+ return compared < 0;
168
+ case 'lte':
169
+ return compared <= 0;
170
+ default:
171
+ return false;
172
+ }
173
+ }
174
+ function matchesAppByChannelVersionCondition(version, condition) {
175
+ if (!condition) {
176
+ return true;
177
+ }
178
+ return ['eq', 'gt', 'gte', 'lt', 'lte'].every((operator) => {
179
+ const expected = condition[operator];
180
+ return expected ? compareAppByChannelWithOperator(version, operator, expected) : true;
181
+ });
182
+ }
137
183
  function matchesVersionCondition(version, condition) {
138
184
  if (!condition) {
139
185
  return true;
@@ -168,10 +214,7 @@ function resolveAppChannel(version) {
168
214
  if (channel === 'beta') {
169
215
  return 'beta';
170
216
  }
171
- if (channel === 'rc') {
172
- return 'rc';
173
- }
174
- return 'unknownPrerelease';
217
+ return 'stable';
175
218
  }
176
219
  function evaluateAppByChannelCondition(version, condition) {
177
220
  if (!condition) {
@@ -208,7 +251,7 @@ function evaluateAppByChannelCondition(version, condition) {
208
251
  const comparableVersion = normalizeAppByChannelComparableVersion(version);
209
252
  const comparableCondition = normalizeAppByChannelCondition(channelCondition);
210
253
  return {
211
- result: matchesVersionCondition(comparableVersion, comparableCondition) ? 'match' : 'mismatch',
254
+ result: matchesAppByChannelVersionCondition(comparableVersion, comparableCondition) ? 'match' : 'mismatch',
212
255
  channel,
213
256
  condition: channelCondition,
214
257
  };
@@ -6,13 +6,16 @@
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 { mkdir, readdir } from 'node:fs/promises';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
10
12
  import { resolveAppPublicPath } from './app-public-path.js';
11
13
  import { dockerContainerExists, managedAppLifecycleEnvVars, runLocalNocoBaseCommand, startDockerContainer, } from './app-runtime.js';
12
14
  import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
13
15
  import { resolveConfiguredStoragePath } from './env-paths.js';
14
16
  import { resolveDockerEnvFileArg } from "./docker-env-file.js";
15
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "./docker-image.js";
17
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageContainerPort, resolveDockerImageRef, } from "./docker-image.js";
18
+ import { resolveHookScriptPath } from './hook-script.js';
16
19
  import { commandSucceeds, ensureDockerDaemonRunning, run } from './run-npm.js';
17
20
  import Install from '../commands/install.js';
18
21
  const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
@@ -90,6 +93,15 @@ function formatLocalPostinstallFailure(envName, message) {
90
93
  `Details: ${message}`,
91
94
  ].join('\n');
92
95
  }
96
+ function formatNpmSourceDevDependenciesFailure(envName, projectRoot, message) {
97
+ return [
98
+ `Couldn't prepare source dev dependencies for "${envName}".`,
99
+ '`nb source dev` requires @nocobase/devtools in npm source envs.',
100
+ `Source directory: ${projectRoot}`,
101
+ `Run \`cd ${projectRoot} && yarn install\` after fixing package.json, then try again.`,
102
+ `Details: ${message}`,
103
+ ].join('\n');
104
+ }
93
105
  function formatSavedDockerSettingsIncomplete(envName, missing) {
94
106
  return [
95
107
  `Can't start NocoBase for "${envName}" yet.`,
@@ -114,6 +126,45 @@ async function localProjectHasFiles(projectRoot) {
114
126
  return false;
115
127
  }
116
128
  }
129
+ function isRecord(value) {
130
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
131
+ }
132
+ async function readPackageJson(projectRoot) {
133
+ const packageJsonPath = path.join(projectRoot, 'package.json');
134
+ const content = await readFile(packageJsonPath, 'utf-8');
135
+ const parsed = JSON.parse(content);
136
+ if (!isRecord(parsed)) {
137
+ throw new Error(`${packageJsonPath} must contain a JSON object.`);
138
+ }
139
+ return parsed;
140
+ }
141
+ function getStringDependency(packageJson, section, packageName) {
142
+ const dependencies = packageJson[section];
143
+ if (!isRecord(dependencies)) {
144
+ return undefined;
145
+ }
146
+ const version = dependencies[packageName];
147
+ return typeof version === 'string' && version.trim() ? version.trim() : undefined;
148
+ }
149
+ function ensureDevDependencies(packageJson) {
150
+ const devDependencies = packageJson.devDependencies;
151
+ if (devDependencies === undefined) {
152
+ const next = {};
153
+ packageJson.devDependencies = next;
154
+ return next;
155
+ }
156
+ if (!isRecord(devDependencies)) {
157
+ throw new Error('package.json devDependencies must be an object.');
158
+ }
159
+ return devDependencies;
160
+ }
161
+ function hasNpmSourceDevtools(projectRoot) {
162
+ return existsSync(path.join(projectRoot, 'node_modules', '@nocobase', 'devtools', 'package.json'));
163
+ }
164
+ function npmRegistryEnv(runtime) {
165
+ const npmRegistry = String(runtime.env.config?.npmRegistry ?? '').trim();
166
+ return npmRegistry ? { npm_config_registry: npmRegistry } : undefined;
167
+ }
117
168
  export async function buildSavedDockerRunArgs(runtime, options) {
118
169
  const config = runtime.env.config ?? {};
119
170
  const storagePath = trimValue(resolveConfiguredStoragePath(config));
@@ -141,6 +192,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
141
192
  defaultRegistry: DEFAULT_DOCKER_REGISTRY,
142
193
  defaultVersion: DEFAULT_DOCKER_VERSION,
143
194
  });
195
+ const containerPort = resolveDockerImageContainerPort(imageRef);
144
196
  const missing = [];
145
197
  if (!storagePath) {
146
198
  missing.push('storagePath');
@@ -174,8 +226,6 @@ export async function buildSavedDockerRunArgs(runtime, options) {
174
226
  '-d',
175
227
  '--name',
176
228
  runtime.containerName,
177
- '--restart',
178
- 'always',
179
229
  '--network',
180
230
  runtime.workspaceName,
181
231
  ];
@@ -184,7 +234,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
184
234
  args.push('--platform', dockerPlatform);
185
235
  }
186
236
  if (appPort) {
187
- args.push('-p', `${appPort}:80`);
237
+ args.push('-p', `${appPort}:${containerPort}`);
188
238
  }
189
239
  if (envFile) {
190
240
  args.push('--env-file', envFile);
@@ -302,6 +352,13 @@ export function buildSavedLocalDownloadArgv(runtime, options) {
302
352
  if (config.buildDts === true) {
303
353
  argv.push('--build-dts');
304
354
  }
355
+ const hookScriptPath = resolveHookScriptPath({
356
+ appPath: runtime.env.appPath,
357
+ hookScript: config.hookScript,
358
+ });
359
+ if (hookScriptPath) {
360
+ argv.push('--hook-script', hookScriptPath, '--hook-phase', options?.hookPhase ?? 'restore', '--hook-command', options?.hookCommand ?? 'app:start', '--hook-env-name', runtime.envName, '--hook-app-path', runtime.env.appPath, '--hook-storage-path', runtime.env.storagePath);
361
+ }
305
362
  return argv;
306
363
  }
307
364
  export async function ensureSavedLocalSource(runtime, runCommand, options) {
@@ -313,6 +370,8 @@ export async function ensureSavedLocalSource(runtime, runCommand, options) {
313
370
  try {
314
371
  await runCommand('source:download', buildSavedLocalDownloadArgv(runtime, {
315
372
  verbose: options?.verbose,
373
+ hookPhase: options?.hookPhase,
374
+ hookCommand: options?.hookCommand,
316
375
  }));
317
376
  options?.onSucceedTask?.(`NocoBase files are ready for "${runtime.envName}".`);
318
377
  }
@@ -335,3 +394,43 @@ export async function ensureLocalPostinstall(runtime, options) {
335
394
  throw new Error(formatLocalPostinstallFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
336
395
  }
337
396
  }
397
+ export async function ensureNpmSourceDevDependencies(runtime, options) {
398
+ if (runtime.source !== 'npm') {
399
+ return;
400
+ }
401
+ let taskStarted = false;
402
+ try {
403
+ const packageJson = await readPackageJson(runtime.projectRoot);
404
+ const appVersion = getStringDependency(packageJson, 'dependencies', '@nocobase/app');
405
+ const devtoolsVersion = getStringDependency(packageJson, 'devDependencies', '@nocobase/devtools');
406
+ let updatedPackageJson = false;
407
+ if (!devtoolsVersion) {
408
+ if (!appVersion) {
409
+ throw new Error('Cannot determine @nocobase/devtools version because dependencies["@nocobase/app"] is missing.');
410
+ }
411
+ const devDependencies = ensureDevDependencies(packageJson);
412
+ devDependencies['@nocobase/devtools'] = appVersion;
413
+ updatedPackageJson = true;
414
+ await writeFile(path.join(runtime.projectRoot, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8');
415
+ }
416
+ const needsInstall = updatedPackageJson || !hasNpmSourceDevtools(runtime.projectRoot);
417
+ if (!needsInstall) {
418
+ return;
419
+ }
420
+ options?.onStartTask?.(`Preparing source dev dependencies for "${runtime.envName}"...`);
421
+ taskStarted = true;
422
+ await run('yarn', ['install'], {
423
+ cwd: runtime.projectRoot,
424
+ env: npmRegistryEnv(runtime),
425
+ errorName: 'yarn install',
426
+ stdio: commandStdio(options?.verbose),
427
+ });
428
+ options?.onSucceedTask?.(`Source dev dependencies are ready for "${runtime.envName}".`);
429
+ }
430
+ catch (error) {
431
+ if (taskStarted) {
432
+ options?.onFailTask?.(`Failed to prepare source dev dependencies for "${runtime.envName}".`);
433
+ }
434
+ throw new Error(formatNpmSourceDevDependenciesFailure(runtime.envName, runtime.projectRoot, error instanceof Error ? error.message : String(error)));
435
+ }
436
+ }