@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.
Files changed (50) hide show
  1. package/bin/early-locale.js +89 -0
  2. package/bin/node-version.js +35 -0
  3. package/bin/run.js +9 -0
  4. package/bin/windows-admin.js +60 -0
  5. package/dist/commands/app/destroy.js +4 -3
  6. package/dist/commands/app/restart.js +38 -0
  7. package/dist/commands/app/shared.js +49 -3
  8. package/dist/commands/app/start.js +92 -0
  9. package/dist/commands/app/upgrade.js +11 -0
  10. package/dist/commands/examples/prompts-stages.js +2 -2
  11. package/dist/commands/examples/prompts-test.js +2 -2
  12. package/dist/commands/init.js +22 -10
  13. package/dist/commands/install.js +135 -4
  14. package/dist/commands/license/activate.js +4 -1
  15. package/dist/commands/license/shared.js +24 -15
  16. package/dist/commands/self/check.js +1 -1
  17. package/dist/commands/self/update.js +4 -4
  18. package/dist/commands/skills/check.js +4 -5
  19. package/dist/commands/skills/install.js +18 -1
  20. package/dist/commands/skills/update.js +19 -4
  21. package/dist/commands/source/dev.js +9 -5
  22. package/dist/commands/source/download.js +67 -2
  23. package/dist/lib/api-command-compat.js +51 -8
  24. package/dist/lib/app-managed-resources.js +101 -1
  25. package/dist/lib/auth-store.js +34 -12
  26. package/dist/lib/cli-config.js +19 -0
  27. package/dist/lib/env-config.js +6 -0
  28. package/dist/lib/hook-script.js +160 -0
  29. package/dist/lib/prompt-web-ui.js +7 -11
  30. package/dist/lib/run-npm.js +4 -0
  31. package/dist/lib/self-manager.js +254 -46
  32. package/dist/lib/skills-manager.js +116 -23
  33. package/dist/lib/source-publish.js +2 -2
  34. package/dist/lib/startup-update.js +1 -1
  35. package/dist/locale/en-US.json +10 -4
  36. package/dist/locale/zh-CN.json +10 -4
  37. package/package.json +7 -2
  38. package/assets/env-proxy/nginx/app.conf.tpl +0 -23
  39. package/assets/env-proxy/nginx/nocobase.conf.tpl +0 -5
  40. package/assets/env-proxy/nginx/snippets/dist-location.conf +0 -5
  41. package/assets/env-proxy/nginx/snippets/gzip.conf +0 -17
  42. package/assets/env-proxy/nginx/snippets/log-format-http.conf +0 -13
  43. package/assets/env-proxy/nginx/snippets/maps-http.conf +0 -14
  44. package/assets/env-proxy/nginx/snippets/mime-types.conf +0 -98
  45. package/assets/env-proxy/nginx/snippets/proxy-location.conf +0 -17
  46. package/assets/env-proxy/nginx/snippets/spa-location.conf +0 -6
  47. package/assets/env-proxy/nginx/snippets/uploads-location.conf +0 -21
  48. package/scripts/build.mjs +0 -34
  49. package/scripts/clean.mjs +0 -9
  50. package/tsconfig.json +0 -19
@@ -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,
@@ -13,10 +13,13 @@ 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
15
  import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_REGISTRY_ZH_CN, resolveDockerImageRef, } from "../../lib/docker-image.js";
16
+ import { getEnv } from '../../lib/auth-store.js';
17
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
18
+ import { buildBeforeDependencyInstallHookContext, runBeforeDependencyInstallHook, } from '../../lib/hook-script.js';
16
19
  import { run } from "../../lib/run-npm.js";
17
20
  import { printVerbose, setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
18
21
  const DEFAULT_DOCKER_PLATFORM = 'auto';
19
- const DEFAULT_DOWNLOAD_VERSION = 'beta';
22
+ const DEFAULT_DOWNLOAD_VERSION = 'latest';
20
23
  const downloadText = (key, values) => localeText(`commands.download.${key}`, values);
21
24
  const downloadTranslatedText = (key, values, fallback) => translateCli(`commands.download.${key}`, values, { fallback });
22
25
  function defaultOutputDirForVersion(versionTag) {
@@ -231,6 +234,26 @@ export default class SourceDownload extends Command {
231
234
  'npm-registry': Flags.string({
232
235
  description: 'npm registry for npm/git downloads and dependency installation.',
233
236
  }),
237
+ 'hook-script': Flags.string({
238
+ description: 'Hook module to run after npm scaffold or git clone and before dependency installation.',
239
+ }),
240
+ 'hook-phase': Flags.string({
241
+ hidden: true,
242
+ options: ['init', 'upgrade', 'restore', 'source-download', 'app-start'],
243
+ }),
244
+ 'hook-command': Flags.string({
245
+ hidden: true,
246
+ options: ['init', 'source:download', 'app:start', 'app:restart', 'app:upgrade'],
247
+ }),
248
+ 'hook-env-name': Flags.string({
249
+ hidden: true,
250
+ }),
251
+ 'hook-app-path': Flags.string({
252
+ hidden: true,
253
+ }),
254
+ 'hook-storage-path': Flags.string({
255
+ hidden: true,
256
+ }),
234
257
  build: Flags.boolean({
235
258
  allowNo: true,
236
259
  description: 'Build npm/git source after dependencies are installed.',
@@ -276,7 +299,6 @@ export default class SourceDownload extends Command {
276
299
  value: 'latest',
277
300
  label: downloadText('prompts.version.latestLabel'),
278
301
  hint: downloadText('prompts.version.latestHint'),
279
- disabled: true,
280
302
  },
281
303
  {
282
304
  value: 'beta',
@@ -574,6 +596,9 @@ export default class SourceDownload extends Command {
574
596
  const npmRegistryRaw = results.npmRegistry !== undefined ? String(results.npmRegistry) : flags['npm-registry'] ?? '';
575
597
  const npmRegistry = npmRegistryRaw.trim() || undefined;
576
598
  const npmDevDependencies = devDependencies ?? false;
599
+ const hookScript = flags['hook-script']?.trim()
600
+ ? path.resolve(process.cwd(), flags['hook-script'].trim())
601
+ : undefined;
577
602
  return {
578
603
  source,
579
604
  version,
@@ -587,6 +612,14 @@ export default class SourceDownload extends Command {
587
612
  ...(source === 'docker' ? { 'docker-platform': dockerPlatform } : {}),
588
613
  ...(source === 'docker' ? { 'docker-save': dockerSave } : {}),
589
614
  ...(npmRegistry ? { 'npm-registry': npmRegistry } : {}),
615
+ ...(hookScript ? { 'hook-script': hookScript } : {}),
616
+ ...(flags['hook-phase'] ? { 'hook-phase': flags['hook-phase'] } : {}),
617
+ ...(flags['hook-command'] ? { 'hook-command': flags['hook-command'] } : {}),
618
+ ...(flags['hook-env-name'] ? { 'hook-env-name': flags['hook-env-name'].trim() } : {}),
619
+ ...(flags['hook-app-path'] ? { 'hook-app-path': path.resolve(process.cwd(), flags['hook-app-path']) } : {}),
620
+ ...(flags['hook-storage-path']
621
+ ? { 'hook-storage-path': path.resolve(process.cwd(), flags['hook-storage-path']) }
622
+ : {}),
590
623
  };
591
624
  }
592
625
  async resolveDownloadFlags(flags) {
@@ -719,6 +752,36 @@ export default class SourceDownload extends Command {
719
752
  }
720
753
  return argv;
721
754
  }
755
+ async runBeforeDependencyInstallHookIfNeeded(flags, projectRoot) {
756
+ const hookScript = flags['hook-script']?.trim();
757
+ if (!hookScript || (flags.source !== 'npm' && flags.source !== 'git')) {
758
+ return;
759
+ }
760
+ const envName = flags['hook-env-name']?.trim() || '';
761
+ const env = envName ? await getEnv(envName, { scope: resolveDefaultConfigScope() }) : undefined;
762
+ const appPath = flags['hook-app-path']?.trim() || projectRoot;
763
+ const storagePath = flags['hook-storage-path']?.trim() || path.join(appPath, 'storage');
764
+ const context = buildBeforeDependencyInstallHookContext({
765
+ phase: flags['hook-phase'] ?? 'source-download',
766
+ command: flags['hook-command'] ?? 'source:download',
767
+ envName,
768
+ source: flags.source,
769
+ version: flags.version,
770
+ appPath,
771
+ sourcePath: projectRoot,
772
+ storagePath,
773
+ hookScript,
774
+ envConfig: env?.config ?? {},
775
+ });
776
+ if (!context) {
777
+ return;
778
+ }
779
+ this.log(`Running hook before dependency install: ${hookScript}`);
780
+ await runBeforeDependencyInstallHook({
781
+ hookScriptPath: hookScript,
782
+ context,
783
+ });
784
+ }
722
785
  async downloadFromDocker(flags) {
723
786
  const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
724
787
  defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
@@ -778,6 +841,7 @@ export default class SourceDownload extends Command {
778
841
  errorName: 'npx create-nocobase-app',
779
842
  loadingMessage: 'Creating the app scaffold',
780
843
  });
844
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
781
845
  const installArgs = ['install'];
782
846
  if (!flags['dev-dependencies']) {
783
847
  installArgs.push('--production');
@@ -821,6 +885,7 @@ export default class SourceDownload extends Command {
821
885
  });
822
886
  const projectRoot = path.resolve(process.cwd(), outputDir);
823
887
  const registryEnv = this.npmRegistryEnv(flags);
888
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
824
889
  this.log(`Installing dependencies in ${projectRoot}`);
825
890
  await this.runExternalCommand('yarn', ['install'], {
826
891
  ...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
17
  import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, 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));
@@ -302,6 +353,13 @@ export function buildSavedLocalDownloadArgv(runtime, options) {
302
353
  if (config.buildDts === true) {
303
354
  argv.push('--build-dts');
304
355
  }
356
+ const hookScriptPath = resolveHookScriptPath({
357
+ appPath: runtime.env.appPath,
358
+ hookScript: config.hookScript,
359
+ });
360
+ if (hookScriptPath) {
361
+ 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);
362
+ }
305
363
  return argv;
306
364
  }
307
365
  export async function ensureSavedLocalSource(runtime, runCommand, options) {
@@ -313,6 +371,8 @@ export async function ensureSavedLocalSource(runtime, runCommand, options) {
313
371
  try {
314
372
  await runCommand('source:download', buildSavedLocalDownloadArgv(runtime, {
315
373
  verbose: options?.verbose,
374
+ hookPhase: options?.hookPhase,
375
+ hookCommand: options?.hookCommand,
316
376
  }));
317
377
  options?.onSucceedTask?.(`NocoBase files are ready for "${runtime.envName}".`);
318
378
  }
@@ -335,3 +395,43 @@ export async function ensureLocalPostinstall(runtime, options) {
335
395
  throw new Error(formatLocalPostinstallFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
336
396
  }
337
397
  }
398
+ export async function ensureNpmSourceDevDependencies(runtime, options) {
399
+ if (runtime.source !== 'npm') {
400
+ return;
401
+ }
402
+ let taskStarted = false;
403
+ try {
404
+ const packageJson = await readPackageJson(runtime.projectRoot);
405
+ const appVersion = getStringDependency(packageJson, 'dependencies', '@nocobase/app');
406
+ const devtoolsVersion = getStringDependency(packageJson, 'devDependencies', '@nocobase/devtools');
407
+ let updatedPackageJson = false;
408
+ if (!devtoolsVersion) {
409
+ if (!appVersion) {
410
+ throw new Error('Cannot determine @nocobase/devtools version because dependencies["@nocobase/app"] is missing.');
411
+ }
412
+ const devDependencies = ensureDevDependencies(packageJson);
413
+ devDependencies['@nocobase/devtools'] = appVersion;
414
+ updatedPackageJson = true;
415
+ await writeFile(path.join(runtime.projectRoot, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8');
416
+ }
417
+ const needsInstall = updatedPackageJson || !hasNpmSourceDevtools(runtime.projectRoot);
418
+ if (!needsInstall) {
419
+ return;
420
+ }
421
+ options?.onStartTask?.(`Preparing source dev dependencies for "${runtime.envName}"...`);
422
+ taskStarted = true;
423
+ await run('yarn', ['install'], {
424
+ cwd: runtime.projectRoot,
425
+ env: npmRegistryEnv(runtime),
426
+ errorName: 'yarn install',
427
+ stdio: commandStdio(options?.verbose),
428
+ });
429
+ options?.onSucceedTask?.(`Source dev dependencies are ready for "${runtime.envName}".`);
430
+ }
431
+ catch (error) {
432
+ if (taskStarted) {
433
+ options?.onFailTask?.(`Failed to prepare source dev dependencies for "${runtime.envName}".`);
434
+ }
435
+ throw new Error(formatNpmSourceDevDependenciesFailure(runtime.envName, runtime.projectRoot, error instanceof Error ? error.message : String(error)));
436
+ }
437
+ }
@@ -12,6 +12,7 @@ import { resolveAppPublicPath } from './app-public-path.js';
12
12
  import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
13
13
  import { normalizeCliLocale } from './cli-locale.js';
14
14
  import { inferConfiguredAppPathFromLegacyConfig, resolveConfiguredAppPath, resolveConfiguredSourcePath, resolveConfiguredStoragePath, } from './env-paths.js';
15
+ import { ENV_CONFIG_SCHEMA_VERSION, normalizeEnvConfigSchemaVersion } from './env-config.js';
15
16
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
16
17
  function normalizeStoredEnvKind(value) {
17
18
  const kind = String(value ?? '').trim();
@@ -76,11 +77,13 @@ function normalizeEnvConfigEntry(entry) {
76
77
  if (!entry) {
77
78
  return entry;
78
79
  }
79
- const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, ...rest } = entry;
80
+ const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, schemaVersion: _schemaVersion, ...rest } = entry;
80
81
  const normalizedKind = resolveEnvKind(entry);
81
82
  const apiBaseUrl = readEnvApiBaseUrl(entry);
83
+ const schemaVersion = normalizeEnvConfigSchemaVersion(entry.schemaVersion);
82
84
  return {
83
85
  ...rest,
86
+ ...(schemaVersion ? { schemaVersion } : {}),
84
87
  ...(normalizedKind ? { kind: normalizedKind } : {}),
85
88
  ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
86
89
  ...(normalizeOptionalString(entry.appPublicPath) ? { appPublicPath: resolveAppPublicPath(entry.appPublicPath) } : {}),
@@ -96,6 +99,17 @@ function normalizeAuthConfig(config) {
96
99
  ? settings.log.retentionDays
97
100
  : undefined;
98
101
  const logEnabled = typeof settings.log?.enabled === 'boolean' ? settings.log.enabled : undefined;
102
+ const hasBinSettings = settings.bin?.docker ||
103
+ settings.bin?.caddy ||
104
+ settings.bin?.git ||
105
+ settings.bin?.nginx ||
106
+ settings.bin?.pnpm ||
107
+ settings.bin?.yarn;
108
+ const hasProxySettings = settings.proxy?.nbCliRoot ||
109
+ settings.proxy?.caddyDriver ||
110
+ settings.proxy?.nginxDriver ||
111
+ settings.proxy?.upstreamHost ||
112
+ settings.proxy?.host;
99
113
  return {
100
114
  name: config.name || config.dockerResourcePrefix,
101
115
  settings: {
@@ -120,22 +134,19 @@ function normalizeAuthConfig(config) {
120
134
  },
121
135
  }
122
136
  : {}),
123
- ...(settings.bin?.docker || settings.bin?.caddy || settings.bin?.git || settings.bin?.nginx || settings.bin?.yarn
137
+ ...(hasBinSettings
124
138
  ? {
125
139
  bin: {
126
140
  ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
127
141
  ...(settings.bin?.caddy ? { caddy: normalizeOptionalString(settings.bin.caddy) } : {}),
128
142
  ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
129
143
  ...(settings.bin?.nginx ? { nginx: normalizeOptionalString(settings.bin.nginx) } : {}),
144
+ ...(settings.bin?.pnpm ? { pnpm: normalizeOptionalString(settings.bin.pnpm) } : {}),
130
145
  ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
131
146
  },
132
147
  }
133
148
  : {}),
134
- ...(settings.proxy?.nbCliRoot ||
135
- settings.proxy?.caddyDriver ||
136
- settings.proxy?.nginxDriver ||
137
- settings.proxy?.upstreamHost ||
138
- settings.proxy?.host
149
+ ...(hasProxySettings
139
150
  ? {
140
151
  proxy: {
141
152
  ...(settings.proxy?.nbCliRoot ? { nbCliRoot: normalizeOptionalString(settings.proxy.nbCliRoot) } : {}),
@@ -366,7 +377,13 @@ function areAuthConfigsEquivalent(left, right) {
366
377
  async function writeEnv(envName, updater, options = {}) {
367
378
  const config = await loadExactAuthConfig(options);
368
379
  const previous = config.envs[envName];
369
- config.envs[envName] = updater(previous);
380
+ const next = updater(previous);
381
+ config.envs[envName] = {
382
+ ...next,
383
+ schemaVersion: normalizeEnvConfigSchemaVersion(next.schemaVersion) ??
384
+ normalizeEnvConfigSchemaVersion(previous?.schemaVersion) ??
385
+ ENV_CONFIG_SCHEMA_VERSION,
386
+ };
370
387
  await saveAuthConfig(config, options);
371
388
  }
372
389
  function normalizeConfiguredAuthType(value) {
@@ -377,18 +394,19 @@ export function resolveConfiguredAuthType(config) {
377
394
  }
378
395
  export async function upsertEnv(envName, config, options = {}) {
379
396
  await writeEnv(envName, (previous) => {
380
- const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, ...rest } = config;
397
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, schemaVersion, ...rest } = config;
381
398
  const nextApiBaseUrl = readEnvApiBaseUrl(config);
382
399
  const previousApiBaseUrl = readEnvApiBaseUrl(previous);
383
400
  const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
384
401
  const previousAuthType = resolveConfiguredAuthType(previous);
385
402
  const requestedAuthType = normalizeConfiguredAuthType(authType);
386
- const nextAuthType = requestedAuthType ?? (accessToken ? 'token' : previousAuthType);
403
+ const nextAccessToken = normalizeOptionalString(accessToken);
404
+ const nextAuthType = requestedAuthType ?? (nextAccessToken ? 'token' : previousAuthType);
387
405
  const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(authUsername) ?? previous?.authUsername : undefined;
388
- const nextAuth = accessToken
406
+ const nextAuth = nextAccessToken
389
407
  ? {
390
408
  type: 'token',
391
- accessToken,
409
+ accessToken: nextAccessToken,
392
410
  }
393
411
  : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
394
412
  ? previous.auth
@@ -396,6 +414,9 @@ export async function upsertEnv(envName, config, options = {}) {
396
414
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
397
415
  const authTypeChanged = previousAuthType !== nextAuthType;
398
416
  const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
417
+ const nextSchemaVersion = normalizeEnvConfigSchemaVersion(schemaVersion) ??
418
+ normalizeEnvConfigSchemaVersion(previous?.schemaVersion) ??
419
+ ENV_CONFIG_SCHEMA_VERSION;
399
420
  return {
400
421
  ...previous,
401
422
  apiBaseUrl: nextApiBaseUrl,
@@ -403,6 +424,7 @@ export async function upsertEnv(envName, config, options = {}) {
403
424
  authUsername: nextAuthUsername,
404
425
  auth: nextAuth,
405
426
  ...rest,
427
+ schemaVersion: nextSchemaVersion,
406
428
  runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
407
429
  };
408
430
  }, options);
@@ -16,6 +16,7 @@ export const DEFAULT_DOCKER_BIN = 'docker';
16
16
  export const DEFAULT_CADDY_BIN = 'caddy';
17
17
  export const DEFAULT_GIT_BIN = 'git';
18
18
  export const DEFAULT_NGINX_BIN = 'nginx';
19
+ export const DEFAULT_PNPM_BIN = 'pnpm';
19
20
  export const PROXY_PROVIDER_OPTIONS = ['nginx', 'caddy'];
20
21
  export const DEFAULT_PROXY_PROVIDER = 'nginx';
21
22
  export const NGINX_PROXY_DRIVER_OPTIONS = ['local', 'docker'];
@@ -40,6 +41,7 @@ export const SUPPORTED_CLI_CONFIG_KEYS = [
40
41
  'bin.caddy',
41
42
  'bin.git',
42
43
  'bin.nginx',
44
+ 'bin.pnpm',
43
45
  'proxy.nb-cli-root',
44
46
  'proxy.caddy-driver',
45
47
  'proxy.nginx-driver',
@@ -136,6 +138,7 @@ function pruneSettings(config) {
136
138
  !trimValue(bin.caddy) &&
137
139
  !trimValue(bin.git) &&
138
140
  !trimValue(bin.nginx) &&
141
+ !trimValue(bin.pnpm) &&
139
142
  !trimValue(bin.yarn)) {
140
143
  delete config.settings?.bin;
141
144
  }
@@ -187,6 +190,8 @@ export function getExplicitCliConfigValue(config, key) {
187
190
  return trimValue(config.settings?.bin?.git);
188
191
  case 'bin.nginx':
189
192
  return trimValue(config.settings?.bin?.nginx);
193
+ case 'bin.pnpm':
194
+ return trimValue(config.settings?.bin?.pnpm);
190
195
  case 'proxy.nb-cli-root':
191
196
  return trimValue(config.settings?.proxy?.nbCliRoot);
192
197
  case 'proxy.caddy-driver':
@@ -233,6 +238,8 @@ export function getEffectiveCliConfigValue(config, key) {
233
238
  return DEFAULT_GIT_BIN;
234
239
  case 'bin.nginx':
235
240
  return DEFAULT_NGINX_BIN;
241
+ case 'bin.pnpm':
242
+ return DEFAULT_PNPM_BIN;
236
243
  case 'proxy.nb-cli-root':
237
244
  return explicit ?? resolveCliHomeRoot();
238
245
  case 'proxy.caddy-driver':
@@ -387,6 +394,12 @@ export async function setCliConfigValue(key, value, options = {}) {
387
394
  nginx: normalized,
388
395
  };
389
396
  break;
397
+ case 'bin.pnpm':
398
+ config.settings.bin = {
399
+ ...(config.settings.bin ?? {}),
400
+ pnpm: normalized,
401
+ };
402
+ break;
390
403
  case 'proxy.nb-cli-root':
391
404
  config.settings.proxy = {
392
405
  ...(config.settings.proxy ?? {}),
@@ -496,6 +509,11 @@ export async function deleteCliConfigValue(key, options = {}) {
496
509
  delete config.settings.bin.nginx;
497
510
  }
498
511
  break;
512
+ case 'bin.pnpm':
513
+ if (config.settings.bin) {
514
+ delete config.settings.bin.pnpm;
515
+ }
516
+ break;
499
517
  case 'proxy.nb-cli-root':
500
518
  if (config.settings.proxy) {
501
519
  delete config.settings.proxy.nbCliRoot;
@@ -556,6 +574,7 @@ const CONFIGURABLE_COMMAND_KEYS = {
556
574
  caddy: 'bin.caddy',
557
575
  git: 'bin.git',
558
576
  nginx: 'bin.nginx',
577
+ pnpm: 'bin.pnpm',
559
578
  yarn: 'bin.yarn',
560
579
  };
561
580
  export function isConfigurableCommandName(value) {
@@ -14,6 +14,7 @@ const STRING_ENV_CONFIG_KEYS = [
14
14
  'dockerPlatform',
15
15
  'gitUrl',
16
16
  'npmRegistry',
17
+ 'hookScript',
17
18
  'appPath',
18
19
  'appRootPath',
19
20
  'storagePath',
@@ -46,6 +47,7 @@ const BOOLEAN_ENV_CONFIG_KEYS = [
46
47
  'buildDts',
47
48
  'dbUnderscored',
48
49
  ];
50
+ export const ENV_CONFIG_SCHEMA_VERSION = 1;
49
51
  function trimConfigValue(value) {
50
52
  const text = String(value ?? '').trim();
51
53
  return text || undefined;
@@ -53,6 +55,9 @@ function trimConfigValue(value) {
53
55
  function resolveSetupState(value) {
54
56
  return value === 'prepared' || value === 'installed' ? value : undefined;
55
57
  }
58
+ export function normalizeEnvConfigSchemaVersion(value) {
59
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
60
+ }
56
61
  function resolveEnvKind(input) {
57
62
  const source = trimConfigValue(input.source);
58
63
  const appPath = trimConfigValue(input.appPath);
@@ -67,6 +72,7 @@ function resolveEnvKind(input) {
67
72
  }
68
73
  export function buildStoredEnvConfig(input) {
69
74
  const envConfig = {
75
+ schemaVersion: normalizeEnvConfigSchemaVersion(input.schemaVersion) ?? ENV_CONFIG_SCHEMA_VERSION,
70
76
  kind: resolveEnvKind(input),
71
77
  apiBaseUrl: trimConfigValue(input.apiBaseUrl) ?? '',
72
78
  };