@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.
@@ -13,6 +13,9 @@ 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';
@@ -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.',
@@ -573,6 +596,9 @@ export default class SourceDownload extends Command {
573
596
  const npmRegistryRaw = results.npmRegistry !== undefined ? String(results.npmRegistry) : flags['npm-registry'] ?? '';
574
597
  const npmRegistry = npmRegistryRaw.trim() || undefined;
575
598
  const npmDevDependencies = devDependencies ?? false;
599
+ const hookScript = flags['hook-script']?.trim()
600
+ ? path.resolve(process.cwd(), flags['hook-script'].trim())
601
+ : undefined;
576
602
  return {
577
603
  source,
578
604
  version,
@@ -586,6 +612,14 @@ export default class SourceDownload extends Command {
586
612
  ...(source === 'docker' ? { 'docker-platform': dockerPlatform } : {}),
587
613
  ...(source === 'docker' ? { 'docker-save': dockerSave } : {}),
588
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
+ : {}),
589
623
  };
590
624
  }
591
625
  async resolveDownloadFlags(flags) {
@@ -718,6 +752,36 @@ export default class SourceDownload extends Command {
718
752
  }
719
753
  return argv;
720
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
+ }
721
785
  async downloadFromDocker(flags) {
722
786
  const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
723
787
  defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
@@ -777,6 +841,7 @@ export default class SourceDownload extends Command {
777
841
  errorName: 'npx create-nocobase-app',
778
842
  loadingMessage: 'Creating the app scaffold',
779
843
  });
844
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
780
845
  const installArgs = ['install'];
781
846
  if (!flags['dev-dependencies']) {
782
847
  installArgs.push('--production');
@@ -820,6 +885,7 @@ export default class SourceDownload extends Command {
820
885
  });
821
886
  const projectRoot = path.resolve(process.cwd(), outputDir);
822
887
  const registryEnv = this.npmRegistryEnv(flags);
888
+ await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
823
889
  this.log(`Installing dependencies in ${projectRoot}`);
824
890
  await this.runExternalCommand('yarn', ['install'], {
825
891
  ...this.runOptionsWithCwd(projectRoot, registryEnv),
@@ -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
+ }
@@ -123,13 +123,19 @@ function normalizeAuthConfig(config) {
123
123
  },
124
124
  }
125
125
  : {}),
126
- ...(settings.bin?.docker || settings.bin?.caddy || settings.bin?.git || settings.bin?.nginx || settings.bin?.yarn
126
+ ...(settings.bin?.docker ||
127
+ settings.bin?.caddy ||
128
+ settings.bin?.git ||
129
+ settings.bin?.nginx ||
130
+ settings.bin?.pnpm ||
131
+ settings.bin?.yarn
127
132
  ? {
128
133
  bin: {
129
134
  ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
130
135
  ...(settings.bin?.caddy ? { caddy: normalizeOptionalString(settings.bin.caddy) } : {}),
131
136
  ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
132
137
  ...(settings.bin?.nginx ? { nginx: normalizeOptionalString(settings.bin.nginx) } : {}),
138
+ ...(settings.bin?.pnpm ? { pnpm: normalizeOptionalString(settings.bin.pnpm) } : {}),
133
139
  ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
134
140
  },
135
141
  }
@@ -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',
@@ -0,0 +1,160 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { copyFile, mkdir } from 'node:fs/promises';
10
+ import { createRequire } from 'node:module';
11
+ import path from 'node:path';
12
+ export const ENV_HOOK_SCRIPT_CONFIG_PATH = '.nb/hooks.mjs';
13
+ const require = createRequire(import.meta.url);
14
+ const { spawn } = require('node:child_process');
15
+ function trimValue(value) {
16
+ return String(value ?? '').trim();
17
+ }
18
+ function normalizeHookPhase(value) {
19
+ const text = trimValue(value);
20
+ if (text === 'init' || text === 'upgrade' || text === 'restore' || text === 'source-download' || text === 'app-start') {
21
+ return text;
22
+ }
23
+ return 'init';
24
+ }
25
+ function normalizeHookCommand(value) {
26
+ const text = trimValue(value);
27
+ if (text === 'source:download' || text === 'app:start' || text === 'app:restart' || text === 'app:upgrade') {
28
+ return text;
29
+ }
30
+ return 'init';
31
+ }
32
+ function normalizeHookSource(value) {
33
+ const text = trimValue(value);
34
+ if (text === 'npm' || text === 'git' || text === 'docker') {
35
+ return text;
36
+ }
37
+ return undefined;
38
+ }
39
+ function isDependencyHookSource(source) {
40
+ return source === 'npm' || source === 'git';
41
+ }
42
+ function isRecord(value) {
43
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
44
+ }
45
+ export function resolveHookScriptPath(params) {
46
+ const hookScript = trimValue(params.hookScript);
47
+ if (!hookScript) {
48
+ return undefined;
49
+ }
50
+ if (path.isAbsolute(hookScript)) {
51
+ return hookScript;
52
+ }
53
+ const appPath = trimValue(params.appPath);
54
+ if (!appPath) {
55
+ return hookScript;
56
+ }
57
+ const usesWindowsSeparators = appPath.includes('\\') || /^[a-zA-Z]:([\\/]|$)/.test(appPath) || appPath.startsWith('\\\\');
58
+ return usesWindowsSeparators ? path.win32.join(appPath, hookScript) : path.posix.join(appPath, hookScript);
59
+ }
60
+ export async function persistHookScript(params) {
61
+ const sourcePath = path.resolve(params.sourcePath);
62
+ const targetPath = path.join(params.appPath, ENV_HOOK_SCRIPT_CONFIG_PATH);
63
+ await mkdir(path.dirname(targetPath), { recursive: true });
64
+ if (path.resolve(sourcePath) !== path.resolve(targetPath)) {
65
+ await copyFile(sourcePath, targetPath);
66
+ }
67
+ return ENV_HOOK_SCRIPT_CONFIG_PATH;
68
+ }
69
+ const hookRunnerScript = `
70
+ import { pathToFileURL } from 'node:url';
71
+
72
+ const knownHookNames = ['beforeDependencyInstall', 'beforeAppInstall', 'afterAppStart'];
73
+ const [, hookScriptPath, hookName, contextJson] = process.argv;
74
+ const url = pathToFileURL(hookScriptPath);
75
+ url.searchParams.set('t', String(Date.now()));
76
+
77
+ const imported = await import(url.href);
78
+ const hooks = imported.default ?? imported;
79
+
80
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
81
+ throw new Error('Hook script must export an object.');
82
+ }
83
+
84
+ for (const knownHookName of knownHookNames) {
85
+ if (Object.prototype.hasOwnProperty.call(hooks, knownHookName) && typeof hooks[knownHookName] !== 'function') {
86
+ throw new Error(\`Hook "\${knownHookName}" must be a function.\`);
87
+ }
88
+ }
89
+
90
+ const hook = hooks[hookName];
91
+ if (typeof hook === 'function') {
92
+ await hook(JSON.parse(contextJson));
93
+ }
94
+ `;
95
+ async function runHookInSubprocess(params) {
96
+ await new Promise((resolve, reject) => {
97
+ const child = spawn(process.execPath, ['--input-type=module', '--eval', hookRunnerScript, params.hookScriptPath, params.hookName, JSON.stringify(params.context)], {
98
+ stdio: ['ignore', 'pipe', 'pipe'],
99
+ });
100
+ let stdout = '';
101
+ let stderr = '';
102
+ child.stdout?.on?.('data', (chunk) => {
103
+ stdout += String(chunk);
104
+ });
105
+ child.stderr?.on?.('data', (chunk) => {
106
+ stderr += String(chunk);
107
+ });
108
+ child.once('error', reject);
109
+ child.once('close', (code) => {
110
+ if (code === 0) {
111
+ resolve();
112
+ return;
113
+ }
114
+ const output = stderr.trim() || stdout.trim();
115
+ reject(new Error(output || `Hook process exited with code ${code ?? 'unknown'}.`));
116
+ });
117
+ });
118
+ }
119
+ export function buildHookContext(params) {
120
+ const source = normalizeHookSource(params.source);
121
+ if (!source) {
122
+ return undefined;
123
+ }
124
+ const version = trimValue(params.version);
125
+ return {
126
+ phase: normalizeHookPhase(params.phase),
127
+ command: normalizeHookCommand(params.command),
128
+ envName: trimValue(params.envName),
129
+ source,
130
+ ...(version ? { version } : {}),
131
+ appPath: params.appPath,
132
+ sourcePath: params.sourcePath,
133
+ storagePath: params.storagePath,
134
+ hookScript: params.hookScript,
135
+ envConfig: { ...(params.envConfig ?? {}) },
136
+ };
137
+ }
138
+ export function buildBeforeDependencyInstallHookContext(params) {
139
+ const context = buildHookContext(params);
140
+ if (!context || !isDependencyHookSource(context.source)) {
141
+ return undefined;
142
+ }
143
+ return context;
144
+ }
145
+ export async function runHookScriptHook(params) {
146
+ try {
147
+ await runHookInSubprocess(params);
148
+ }
149
+ catch (error) {
150
+ const message = error instanceof Error ? error.message : String(error);
151
+ throw new Error([`Hook script failed: ${params.hookScriptPath}`, `Hook stage: ${params.hookName}`, `Details: ${message}`].join('\n'));
152
+ }
153
+ }
154
+ export async function runBeforeDependencyInstallHook(params) {
155
+ await runHookScriptHook({
156
+ hookScriptPath: params.hookScriptPath,
157
+ hookName: 'beforeDependencyInstall',
158
+ context: params.context,
159
+ });
160
+ }
@@ -44,6 +44,10 @@ const MISSING_COMMAND_SPECS = {
44
44
  displayName: 'Yarn',
45
45
  configKey: 'bin.yarn',
46
46
  },
47
+ pnpm: {
48
+ displayName: 'pnpm',
49
+ configKey: 'bin.pnpm',
50
+ },
47
51
  };
48
52
  const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
49
53
  /cannot connect to the docker daemon/i,