@wyxos/zephyr 0.3.3 → 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.
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
2
2
  import os from 'node:os'
3
3
  import process from 'node:process'
4
4
 
5
+ import {ZephyrError} from '../runtime/errors.mjs'
5
6
  import { PROJECT_LOCK_FILE, ensureDirectory, getLockFilePath, getProjectConfigDir } from '../utils/paths.mjs'
6
7
 
7
8
  function createLockPayload() {
@@ -67,7 +68,26 @@ export async function readRemoteLock(ssh, remoteCwd) {
67
68
  return null
68
69
  }
69
70
 
70
- export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning } = {}) {
71
+ function parseLockDetails(rawContent = '') {
72
+ try {
73
+ return JSON.parse(rawContent.trim())
74
+ } catch (_error) {
75
+ return { raw: rawContent.trim() }
76
+ }
77
+ }
78
+
79
+ function formatLockHolder(details = {}) {
80
+ const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
81
+ const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
82
+
83
+ return { startedBy, startedAt }
84
+ }
85
+
86
+ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
87
+ runPrompt,
88
+ logWarning,
89
+ interactive = true
90
+ } = {}) {
71
91
  const localLock = await readLocalLock(rootDir)
72
92
  const remoteLock = await readRemoteLock(ssh, remoteCwd)
73
93
 
@@ -79,8 +99,15 @@ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt
79
99
  const remoteKey = `${remoteLock.user}@${remoteLock.hostname}:${remoteLock.pid}:${remoteLock.startedAt}`
80
100
 
81
101
  if (localKey === remoteKey) {
82
- const startedBy = remoteLock.user ? `${remoteLock.user}@${remoteLock.hostname ?? 'unknown'}` : 'unknown user'
83
- const startedAt = remoteLock.startedAt ? ` at ${remoteLock.startedAt}` : ''
102
+ const { startedBy, startedAt } = formatLockHolder(remoteLock)
103
+
104
+ if (!interactive) {
105
+ throw new ZephyrError(
106
+ `Stale deployment lock detected on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/.zephyr/${PROJECT_LOCK_FILE} manually before rerunning with --non-interactive.`,
107
+ {code: 'ZEPHYR_STALE_REMOTE_LOCK'}
108
+ )
109
+ }
110
+
84
111
  const { shouldRemove } = await runPrompt([
85
112
  {
86
113
  type: 'confirm',
@@ -103,7 +130,11 @@ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt
103
130
  return false
104
131
  }
105
132
 
106
- export async function acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, logWarning } = {}) {
133
+ export async function acquireRemoteLock(ssh, remoteCwd, rootDir, {
134
+ runPrompt,
135
+ logWarning,
136
+ interactive = true
137
+ } = {}) {
107
138
  const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
108
139
  const escapedLockPath = lockPath.replace(/'/g, "'\\''")
109
140
  const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
@@ -113,31 +144,17 @@ export async function acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, lo
113
144
  if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
114
145
  const localLock = await readLocalLock(rootDir)
115
146
  if (localLock) {
116
- const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning })
147
+ const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning, interactive })
117
148
  if (!removed) {
118
- let details = {}
119
- try {
120
- details = JSON.parse(checkResult.stdout.trim())
121
- } catch (_error) {
122
- details = { raw: checkResult.stdout.trim() }
123
- }
124
-
125
- const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
126
- const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
149
+ const details = parseLockDetails(checkResult.stdout.trim())
150
+ const { startedBy, startedAt } = formatLockHolder(details)
127
151
  throw new Error(
128
152
  `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
129
153
  )
130
154
  }
131
155
  } else {
132
- let details = {}
133
- try {
134
- details = JSON.parse(checkResult.stdout.trim())
135
- } catch (_error) {
136
- details = { raw: checkResult.stdout.trim() }
137
- }
138
-
139
- const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
140
- const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
156
+ const details = parseLockDetails(checkResult.stdout.trim())
157
+ const { startedBy, startedAt } = formatLockHolder(details)
141
158
  throw new Error(
142
159
  `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
143
160
  )
@@ -168,4 +185,3 @@ export async function releaseRemoteLock(ssh, remoteCwd, { logWarning } = {}) {
168
185
  logWarning?.(`Failed to remove lock file: ${result.stderr}`)
169
186
  }
170
187
  }
171
-
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
  }