@wyxos/zephyr 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.mjs CHANGED
@@ -3,10 +3,12 @@ import {createRequire} from 'node:module'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
5
5
 
6
+ import {validateCliOptions} from './cli/options.mjs'
6
7
  import {releaseNode} from './release-node.mjs'
7
8
  import {releasePackagist} from './release-packagist.mjs'
8
9
  import {validateLocalDependencies} from './dependency-scanner.mjs'
9
10
  import * as bootstrap from './project/bootstrap.mjs'
11
+ import {getErrorCode} from './runtime/errors.mjs'
10
12
  import {PROJECT_CONFIG_DIR} from './utils/paths.mjs'
11
13
  import {writeStderrLine} from './utils/output.mjs'
12
14
  import {createAppContext} from './runtime/app-context.mjs'
@@ -20,97 +22,223 @@ const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
20
22
  const require = createRequire(import.meta.url)
21
23
  const {version: ZEPHYR_VERSION} = require('../package.json')
22
24
 
23
- const appContext = createAppContext()
24
- const {
25
- logProcessing,
26
- logSuccess,
27
- logWarning,
28
- logError,
29
- runPrompt,
30
- runCommand
31
- } = appContext
32
- const configurationService = createConfigurationService(appContext)
25
+ function normalizeMainOptions(firstArg = null, secondArg = null) {
26
+ if (firstArg && typeof firstArg === 'object' && !Array.isArray(firstArg)) {
27
+ return {
28
+ workflowType: firstArg.workflowType ?? firstArg.type ?? null,
29
+ versionArg: firstArg.versionArg ?? null,
30
+ nonInteractive: firstArg.nonInteractive === true,
31
+ json: firstArg.json === true,
32
+ presetName: firstArg.presetName ?? null,
33
+ resumePending: firstArg.resumePending === true,
34
+ discardPending: firstArg.discardPending === true,
35
+ maintenanceMode: firstArg.maintenanceMode ?? null,
36
+ skipTests: firstArg.skipTests === true,
37
+ skipLint: firstArg.skipLint === true,
38
+ skipBuild: firstArg.skipBuild === true,
39
+ skipDeploy: firstArg.skipDeploy === true,
40
+ context: firstArg.context ?? null
41
+ }
42
+ }
43
+
44
+ return {
45
+ workflowType: firstArg ?? null,
46
+ versionArg: secondArg ?? null,
47
+ nonInteractive: false,
48
+ json: false,
49
+ presetName: null,
50
+ resumePending: false,
51
+ discardPending: false,
52
+ maintenanceMode: null,
53
+ skipTests: false,
54
+ skipLint: false,
55
+ skipBuild: false,
56
+ skipDeploy: false,
57
+ context: null
58
+ }
59
+ }
60
+
61
+ function resolveWorkflowName(workflowType = null) {
62
+ if (workflowType === 'node' || workflowType === 'vue') {
63
+ return `release-${workflowType}`
64
+ }
65
+
66
+ if (workflowType === 'packagist') {
67
+ return 'release-packagist'
68
+ }
69
+
70
+ return 'deploy'
71
+ }
33
72
 
34
73
  async function runRemoteTasks(config, options = {}) {
35
74
  return await runDeployment(config, {
36
75
  ...options,
37
- context: options.context ?? appContext
76
+ context: options.context
38
77
  })
39
78
  }
40
79
 
41
- async function main(releaseType = null, versionArg = null) {
42
- logProcessing(`Zephyr v${ZEPHYR_VERSION}`)
80
+ async function main(optionsOrWorkflowType = null, versionArg = null) {
81
+ const options = normalizeMainOptions(optionsOrWorkflowType, versionArg)
82
+
83
+ const executionMode = {
84
+ interactive: !options.nonInteractive,
85
+ json: options.json === true && options.nonInteractive === true,
86
+ workflow: resolveWorkflowName(options.workflowType),
87
+ presetName: options.presetName,
88
+ maintenanceMode: options.maintenanceMode,
89
+ resumePending: options.resumePending,
90
+ discardPending: options.discardPending
91
+ }
92
+ const appContext = options.context ?? createAppContext({executionMode})
93
+ const {
94
+ logProcessing,
95
+ logSuccess,
96
+ logWarning,
97
+ logError,
98
+ runPrompt,
99
+ runCommand,
100
+ emitEvent
101
+ } = appContext
102
+ const currentExecutionMode = appContext.executionMode ?? executionMode
103
+ const configurationService = createConfigurationService(appContext)
104
+
105
+ try {
106
+ validateCliOptions(options)
107
+
108
+ if (currentExecutionMode.json) {
109
+ emitEvent?.('run_started', {
110
+ message: `Zephyr v${ZEPHYR_VERSION} starting`,
111
+ data: {
112
+ version: ZEPHYR_VERSION,
113
+ workflow: currentExecutionMode.workflow,
114
+ nonInteractive: currentExecutionMode.interactive === false,
115
+ presetName: currentExecutionMode.presetName,
116
+ maintenanceMode: currentExecutionMode.maintenanceMode,
117
+ resumePending: currentExecutionMode.resumePending,
118
+ discardPending: currentExecutionMode.discardPending
119
+ }
120
+ })
121
+ } else {
122
+ logProcessing(`Zephyr v${ZEPHYR_VERSION}`)
123
+ }
43
124
 
44
- if (releaseType === 'node' || releaseType === 'vue') {
45
- try {
46
- await releaseNode()
125
+ if (options.workflowType === 'node' || options.workflowType === 'vue') {
126
+ await releaseNode({
127
+ releaseType: options.versionArg,
128
+ skipTests: options.skipTests,
129
+ skipLint: options.skipLint,
130
+ skipBuild: options.skipBuild,
131
+ skipDeploy: options.skipDeploy,
132
+ context: appContext
133
+ })
134
+ emitEvent?.('run_completed', {
135
+ message: 'Zephyr workflow completed successfully.',
136
+ data: {
137
+ version: ZEPHYR_VERSION,
138
+ workflow: currentExecutionMode.workflow
139
+ }
140
+ })
47
141
  return
48
- } catch (error) {
49
- logError('\nRelease failed:')
50
- logError(error.message)
51
- if (error.stack) {
52
- writeStderrLine(error.stack)
53
- }
54
- process.exit(1)
55
142
  }
56
- }
57
143
 
58
- if (releaseType === 'packagist') {
59
- try {
60
- await releasePackagist()
144
+ if (options.workflowType === 'packagist') {
145
+ await releasePackagist({
146
+ releaseType: options.versionArg,
147
+ skipTests: options.skipTests,
148
+ skipLint: options.skipLint,
149
+ context: appContext
150
+ })
151
+ emitEvent?.('run_completed', {
152
+ message: 'Zephyr workflow completed successfully.',
153
+ data: {
154
+ version: ZEPHYR_VERSION,
155
+ workflow: currentExecutionMode.workflow
156
+ }
157
+ })
61
158
  return
62
- } catch (error) {
63
- logError('\nRelease failed:')
64
- logError(error.message)
65
- if (error.stack) {
66
- writeStderrLine(error.stack)
67
- }
68
- process.exit(1)
69
159
  }
70
- }
71
160
 
72
- const rootDir = process.cwd()
161
+ const rootDir = process.cwd()
73
162
 
74
- await bootstrap.ensureGitignoreEntry(rootDir, {
75
- projectConfigDir: PROJECT_CONFIG_DIR,
76
- runCommand,
77
- logSuccess,
78
- logWarning
79
- })
80
- await bootstrap.ensureProjectReleaseScript(rootDir, {
81
- runPrompt,
82
- runCommand,
83
- logSuccess,
84
- logWarning,
85
- releaseScriptName: RELEASE_SCRIPT_NAME,
86
- releaseScriptCommand: RELEASE_SCRIPT_COMMAND
87
- })
163
+ await bootstrap.ensureGitignoreEntry(rootDir, {
164
+ projectConfigDir: PROJECT_CONFIG_DIR,
165
+ runCommand,
166
+ logSuccess,
167
+ logWarning
168
+ })
169
+ await bootstrap.ensureProjectReleaseScript(rootDir, {
170
+ runPrompt,
171
+ runCommand,
172
+ logSuccess,
173
+ logWarning,
174
+ interactive: currentExecutionMode.interactive,
175
+ releaseScriptName: RELEASE_SCRIPT_NAME,
176
+ releaseScriptCommand: RELEASE_SCRIPT_COMMAND
177
+ })
88
178
 
89
- const packageJsonPath = path.join(rootDir, 'package.json')
90
- const composerJsonPath = path.join(rootDir, 'composer.json')
91
- const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false)
92
- const hasComposerJson = await fs.access(composerJsonPath).then(() => true).catch(() => false)
179
+ const packageJsonPath = path.join(rootDir, 'package.json')
180
+ const composerJsonPath = path.join(rootDir, 'composer.json')
181
+ const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false)
182
+ const hasComposerJson = await fs.access(composerJsonPath).then(() => true).catch(() => false)
93
183
 
94
- if (hasPackageJson || hasComposerJson) {
95
- logProcessing('Validating dependencies...')
96
- await validateLocalDependencies(rootDir, runPrompt, logSuccess)
97
- }
184
+ if (hasPackageJson || hasComposerJson) {
185
+ logProcessing('Validating dependencies...')
186
+ await validateLocalDependencies(rootDir, runPrompt, logSuccess, {
187
+ interactive: currentExecutionMode.interactive
188
+ })
189
+ }
98
190
 
99
- const {deploymentConfig} = await selectDeploymentTarget(rootDir, {
100
- configurationService,
101
- runPrompt,
102
- logProcessing,
103
- logSuccess,
104
- logWarning
105
- })
191
+ const {deploymentConfig} = await selectDeploymentTarget(rootDir, {
192
+ configurationService,
193
+ runPrompt,
194
+ logProcessing,
195
+ logSuccess,
196
+ logWarning,
197
+ emitEvent,
198
+ executionMode: currentExecutionMode
199
+ })
106
200
 
107
- const snapshotToUse = await resolvePendingSnapshot(rootDir, deploymentConfig, {
108
- runPrompt,
109
- logProcessing,
110
- logWarning
111
- })
201
+ const snapshotToUse = await resolvePendingSnapshot(rootDir, deploymentConfig, {
202
+ runPrompt,
203
+ logProcessing,
204
+ logWarning,
205
+ executionMode: currentExecutionMode
206
+ })
207
+
208
+ await runRemoteTasks(deploymentConfig, {
209
+ rootDir,
210
+ snapshot: snapshotToUse,
211
+ versionArg: options.versionArg,
212
+ context: appContext
213
+ })
214
+
215
+ emitEvent?.('run_completed', {
216
+ message: 'Zephyr workflow completed successfully.',
217
+ data: {
218
+ version: ZEPHYR_VERSION,
219
+ workflow: currentExecutionMode.workflow
220
+ }
221
+ })
222
+ } catch (error) {
223
+ const errorCode = getErrorCode(error)
224
+ emitEvent?.('run_failed', {
225
+ message: error.message,
226
+ code: errorCode,
227
+ data: {
228
+ version: ZEPHYR_VERSION,
229
+ workflow: currentExecutionMode.workflow
230
+ }
231
+ })
112
232
 
113
- await runRemoteTasks(deploymentConfig, {rootDir, snapshot: snapshotToUse, versionArg})
233
+ if (!currentExecutionMode.json) {
234
+ logError(error.message)
235
+ if (errorCode === 'ZEPHYR_FAILURE' && error.stack) {
236
+ writeStderrLine(error.stack)
237
+ }
238
+ }
239
+
240
+ throw error
241
+ }
114
242
  }
115
243
 
116
244
  export {main, runRemoteTasks}
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
+ import {ZephyrError} from '../runtime/errors.mjs'
5
+
4
6
  export async function ensureGitignoreEntry(rootDir, {
5
7
  projectConfigDir = '.zephyr',
6
8
  runCommand,
@@ -66,6 +68,7 @@ export async function ensureProjectReleaseScript(rootDir, {
66
68
  runCommand,
67
69
  logSuccess,
68
70
  logWarning,
71
+ interactive = true,
69
72
  releaseScriptName = 'release',
70
73
  releaseScriptCommand = 'npx @wyxos/zephyr@latest'
71
74
  } = {}) {
@@ -96,6 +99,13 @@ export async function ensureProjectReleaseScript(rootDir, {
96
99
  return false
97
100
  }
98
101
 
102
+ if (!interactive) {
103
+ throw new ZephyrError(
104
+ 'Zephyr cannot run non-interactively because package.json is missing the Zephyr release script. Add `"release": "npx @wyxos/zephyr@latest"` and rerun.',
105
+ {code: 'ZEPHYR_RELEASE_SCRIPT_REQUIRED'}
106
+ )
107
+ }
108
+
99
109
  const { installReleaseScript } = await runPrompt([
100
110
  {
101
111
  type: 'confirm',
@@ -144,4 +154,3 @@ export async function ensureProjectReleaseScript(rootDir, {
144
154
 
145
155
  return true
146
156
  }
147
-
@@ -65,14 +65,23 @@ export function parseReleaseArgs({
65
65
 
66
66
  export async function runReleaseCommand(command, args, {
67
67
  cwd = process.cwd(),
68
- capture = false
68
+ capture = false,
69
+ runCommandImpl = runCommandBase,
70
+ runCommandCaptureImpl = runCommandCaptureBase
69
71
  } = {}) {
70
72
  if (capture) {
71
- const { stdout, stderr } = await runCommandCaptureBase(command, args, { cwd })
73
+ const captured = await runCommandCaptureImpl(command, args, { cwd })
74
+
75
+ if (typeof captured === 'string') {
76
+ return { stdout: captured.trim(), stderr: '' }
77
+ }
78
+
79
+ const stdout = captured?.stdout ?? ''
80
+ const stderr = captured?.stderr ?? ''
72
81
  return { stdout: stdout.trim(), stderr: stderr.trim() }
73
82
  }
74
83
 
75
- await runCommandBase(command, args, { cwd })
84
+ await runCommandImpl(command, args, { cwd })
76
85
  return undefined
77
86
  }
78
87
 
@@ -91,9 +100,10 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
91
100
 
92
101
  export async function validateReleaseDependencies(rootDir = process.cwd(), {
93
102
  prompt = (questions) => inquirer.prompt(questions),
94
- logSuccess
103
+ logSuccess,
104
+ interactive = true
95
105
  } = {}) {
96
- await validateLocalDependencies(rootDir, prompt, logSuccess)
106
+ await validateLocalDependencies(rootDir, prompt, logSuccess, { interactive })
97
107
  }
98
108
 
99
109
  export async function ensureReleaseBranchReady({
@@ -1,27 +1,37 @@
1
1
  import process from 'node:process'
2
- import chalk from 'chalk'
3
- import {createChalkLogger} from './utils/output.mjs'
4
- import {
5
- parseReleaseArgs,
6
- } from './release/shared.mjs'
2
+ import {createAppContext} from './runtime/app-context.mjs'
3
+ import {parseReleaseArgs} from './release/shared.mjs'
7
4
  import {releaseNodePackage} from './application/release/release-node-package.mjs'
8
5
 
9
- const {logProcessing: logStep, logSuccess, logWarning} = createChalkLogger(chalk)
10
-
11
- export async function releaseNode() {
12
- const {releaseType, skipTests, skipLint, skipBuild, skipDeploy} = parseReleaseArgs({
13
- booleanFlags: ['--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
6
+ export async function releaseNode(options = {}) {
7
+ const parsed = options.releaseType
8
+ ? options
9
+ : parseReleaseArgs({
10
+ booleanFlags: ['--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
11
+ })
12
+ const rootDir = options.rootDir ?? process.cwd()
13
+ const context = options.context ?? createAppContext({
14
+ executionMode: {
15
+ interactive: true,
16
+ json: false,
17
+ workflow: 'release-node'
18
+ }
14
19
  })
15
- const rootDir = process.cwd()
20
+ const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
21
+
16
22
  await releaseNodePackage({
17
- releaseType,
18
- skipTests,
19
- skipLint,
20
- skipBuild,
21
- skipDeploy,
23
+ releaseType: parsed.releaseType,
24
+ skipTests: parsed.skipTests === true,
25
+ skipLint: parsed.skipLint === true,
26
+ skipBuild: parsed.skipBuild === true,
27
+ skipDeploy: parsed.skipDeploy === true,
22
28
  rootDir,
23
29
  logStep,
24
30
  logSuccess,
25
- logWarning
31
+ logWarning,
32
+ runPrompt,
33
+ runCommandImpl: runCommand,
34
+ runCommandCaptureImpl: runCommandCapture,
35
+ interactive: executionMode?.interactive !== false
26
36
  })
27
37
  }
@@ -1,25 +1,36 @@
1
1
  import process from 'node:process'
2
- import chalk from 'chalk'
3
- import {createChalkLogger} from './utils/output.mjs'
4
- import {
5
- parseReleaseArgs,
6
- } from './release/shared.mjs'
2
+ import {createAppContext} from './runtime/app-context.mjs'
3
+ import {parseReleaseArgs} from './release/shared.mjs'
7
4
  import {releasePackagistPackage} from './application/release/release-packagist-package.mjs'
8
5
 
9
- const {logProcessing: logStep, logSuccess, logWarning} = createChalkLogger(chalk)
10
-
11
- export async function releasePackagist() {
12
- const {releaseType, skipTests, skipLint} = parseReleaseArgs({
13
- booleanFlags: ['--skip-tests', '--skip-lint']
6
+ export async function releasePackagist(options = {}) {
7
+ const parsed = options.releaseType
8
+ ? options
9
+ : parseReleaseArgs({
10
+ booleanFlags: ['--skip-tests', '--skip-lint']
11
+ })
12
+ const rootDir = options.rootDir ?? process.cwd()
13
+ const context = options.context ?? createAppContext({
14
+ executionMode: {
15
+ interactive: true,
16
+ json: false,
17
+ workflow: 'release-packagist'
18
+ }
14
19
  })
15
- const rootDir = process.cwd()
20
+ const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
21
+
16
22
  await releasePackagistPackage({
17
- releaseType,
18
- skipTests,
19
- skipLint,
23
+ releaseType: parsed.releaseType,
24
+ skipTests: parsed.skipTests === true,
25
+ skipLint: parsed.skipLint === true,
20
26
  rootDir,
21
27
  logStep,
22
28
  logSuccess,
23
- logWarning
29
+ logWarning,
30
+ runPrompt,
31
+ runCommandImpl: runCommand,
32
+ runCommandCaptureImpl: runCommandCapture,
33
+ interactive: executionMode?.interactive !== false,
34
+ progressWriter: executionMode?.json ? process.stderr : process.stdout
24
35
  })
25
36
  }
@@ -2,7 +2,7 @@ import chalk from 'chalk'
2
2
  import inquirer from 'inquirer'
3
3
  import {NodeSSH} from 'node-ssh'
4
4
 
5
- import {createChalkLogger} from '../utils/output.mjs'
5
+ import {createChalkLogger, createJsonEventEmitter, createJsonLogger} from '../utils/output.mjs'
6
6
  import {runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase} from '../utils/command.mjs'
7
7
  import {createLocalCommandRunners} from './local-command.mjs'
8
8
  import {createRunPrompt} from './prompt.mjs'
@@ -13,16 +13,41 @@ export function createAppContext({
13
13
  inquirerInstance = inquirer,
14
14
  NodeSSHClass = NodeSSH,
15
15
  runCommandImpl = runCommandBase,
16
- runCommandCaptureImpl = runCommandCaptureBase
16
+ runCommandCaptureImpl = runCommandCaptureBase,
17
+ executionMode = {}
17
18
  } = {}) {
18
- const {logProcessing, logSuccess, logWarning, logError} = createChalkLogger(chalkInstance)
19
- const runPrompt = createRunPrompt({inquirer: inquirerInstance})
19
+ const normalizedExecutionMode = {
20
+ interactive: executionMode.interactive !== false,
21
+ json: executionMode.json === true,
22
+ workflow: executionMode.workflow ?? 'deploy',
23
+ presetName: executionMode.presetName ?? null,
24
+ maintenanceMode: executionMode.maintenanceMode ?? null,
25
+ resumePending: executionMode.resumePending === true,
26
+ discardPending: executionMode.discardPending === true
27
+ }
28
+ const emitEvent = normalizedExecutionMode.json
29
+ ? createJsonEventEmitter({workflow: normalizedExecutionMode.workflow})
30
+ : null
31
+ const {logProcessing, logSuccess, logWarning, logError} = normalizedExecutionMode.json
32
+ ? createJsonLogger({emitEvent})
33
+ : createChalkLogger(chalkInstance)
34
+ const runPrompt = createRunPrompt({
35
+ inquirer: inquirerInstance,
36
+ interactive: normalizedExecutionMode.interactive,
37
+ emitEvent,
38
+ workflow: normalizedExecutionMode.workflow
39
+ })
20
40
  const createSshClient = createSshClientFactory({NodeSSH: NodeSSHClass})
21
41
  const {runCommand, runCommandCapture} = createLocalCommandRunners({
22
42
  runCommandBase: runCommandImpl,
23
43
  runCommandCaptureBase: runCommandCaptureImpl
24
44
  })
25
45
 
46
+ const runCommandWithMode = (command, args, options = {}) => runCommand(command, args, {
47
+ ...options,
48
+ forwardStdoutToStderr: normalizedExecutionMode.json
49
+ })
50
+
26
51
  return {
27
52
  logProcessing,
28
53
  logSuccess,
@@ -30,7 +55,9 @@ export function createAppContext({
30
55
  logError,
31
56
  runPrompt,
32
57
  createSshClient,
33
- runCommand,
34
- runCommandCapture
58
+ runCommand: runCommandWithMode,
59
+ runCommandCapture,
60
+ emitEvent,
61
+ executionMode: normalizedExecutionMode
35
62
  }
36
63
  }
@@ -0,0 +1,46 @@
1
+ export class ZephyrError extends Error {
2
+ constructor(message, {
3
+ code = 'ZEPHYR_FAILURE',
4
+ data = {},
5
+ cause
6
+ } = {}) {
7
+ super(message, cause ? {cause} : undefined)
8
+ this.name = this.constructor.name
9
+ this.code = code
10
+ this.data = data
11
+ }
12
+ }
13
+
14
+ export class PromptRequiredError extends ZephyrError {
15
+ constructor(message, {
16
+ data = {},
17
+ cause
18
+ } = {}) {
19
+ super(message, {
20
+ code: 'ZEPHYR_PROMPT_REQUIRED',
21
+ data,
22
+ cause
23
+ })
24
+ }
25
+ }
26
+
27
+ export class InvalidCliOptionsError extends ZephyrError {
28
+ constructor(message, {
29
+ data = {},
30
+ cause
31
+ } = {}) {
32
+ super(message, {
33
+ code: 'ZEPHYR_INVALID_OPTIONS',
34
+ data,
35
+ cause
36
+ })
37
+ }
38
+ }
39
+
40
+ export function getErrorCode(error) {
41
+ if (error && typeof error === 'object' && typeof error.code === 'string' && error.code.length > 0) {
42
+ return error.code
43
+ }
44
+
45
+ return 'ZEPHYR_FAILURE'
46
+ }
@@ -1,10 +1,20 @@
1
+ import process from 'node:process'
2
+
1
3
  export function createLocalCommandRunners({ runCommandBase, runCommandCaptureBase }) {
2
4
  if (!runCommandBase || !runCommandCaptureBase) {
3
5
  throw new Error('createLocalCommandRunners requires runCommandBase and runCommandCaptureBase')
4
6
  }
5
7
 
6
- const runCommand = async (command, args, { silent = false, cwd } = {}) => {
7
- const stdio = silent ? 'ignore' : 'inherit'
8
+ const runCommand = async (command, args, {
9
+ silent = false,
10
+ cwd,
11
+ forwardStdoutToStderr = false
12
+ } = {}) => {
13
+ const stdio = silent
14
+ ? 'ignore'
15
+ : forwardStdoutToStderr
16
+ ? ['ignore', process.stderr, process.stderr]
17
+ : 'inherit'
8
18
  return runCommandBase(command, args, { cwd, stdio })
9
19
  }
10
20
 
@@ -15,4 +25,3 @@ export function createLocalCommandRunners({ runCommandBase, runCommandCaptureBas
15
25
 
16
26
  return { runCommand, runCommandCapture }
17
27
  }
18
-