@wyxos/zephyr 0.3.4 → 0.4.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.
@@ -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
-
@@ -1,9 +1,48 @@
1
- export function createRunPrompt({ inquirer }) {
1
+ import {PromptRequiredError} from './errors.mjs'
2
+
3
+ function buildPromptMessage(questions = []) {
4
+ const messages = questions
5
+ .map((question) => question?.message)
6
+ .filter((message) => typeof message === 'string' && message.trim().length > 0)
7
+
8
+ return messages[0] ?? 'Zephyr requires interactive input to continue.'
9
+ }
10
+
11
+ export function createRunPrompt({
12
+ inquirer,
13
+ interactive = true,
14
+ emitEvent,
15
+ workflow = 'deploy'
16
+ }) {
2
17
  if (!inquirer) {
3
18
  throw new Error('createRunPrompt requires inquirer')
4
19
  }
5
20
 
6
21
  return async function runPrompt(questions) {
22
+ if (!interactive) {
23
+ const message = buildPromptMessage(questions)
24
+ const error = new PromptRequiredError(message, {
25
+ data: {
26
+ workflow,
27
+ questions: Array.isArray(questions)
28
+ ? questions.map((question) => ({
29
+ name: question?.name ?? null,
30
+ type: question?.type ?? null,
31
+ message: question?.message ?? null
32
+ }))
33
+ : []
34
+ }
35
+ })
36
+
37
+ emitEvent?.('prompt_required', {
38
+ message,
39
+ code: error.code,
40
+ data: error.data
41
+ })
42
+
43
+ throw error
44
+ }
45
+
7
46
  if (typeof globalThis !== 'undefined' && globalThis.__zephyrPrompt) {
8
47
  return globalThis.__zephyrPrompt(questions)
9
48
  }
@@ -11,4 +50,3 @@ export function createRunPrompt({ inquirer }) {
11
50
  return inquirer.prompt(questions)
12
51
  }
13
52
  }
14
-
@@ -25,6 +25,36 @@ export function writeStderr(message = '') {
25
25
  }
26
26
  }
27
27
 
28
+ export function createJsonEventEmitter({
29
+ workflow,
30
+ writeEvent = writeStdoutLine
31
+ } = {}) {
32
+ return function emitEvent(event, {
33
+ level,
34
+ message = '',
35
+ data = {},
36
+ code
37
+ } = {}) {
38
+ const payload = {
39
+ event,
40
+ timestamp: new Date().toISOString(),
41
+ workflow,
42
+ message
43
+ }
44
+
45
+ if (level) {
46
+ payload.level = level
47
+ }
48
+
49
+ if (code) {
50
+ payload.code = code
51
+ }
52
+
53
+ payload.data = data ?? {}
54
+ writeEvent(JSON.stringify(payload))
55
+ }
56
+ }
57
+
28
58
  export function formatLogMessage(message = '', prefix = '') {
29
59
  const text = message == null ? '' : String(message)
30
60
 
@@ -52,3 +82,18 @@ export function createChalkLogger(chalk, {
52
82
  logError: (message = '') => writeStderrLine(chalk.red(formatLogMessage(message, prefixes.error)))
53
83
  }
54
84
  }
85
+
86
+ export function createJsonLogger({
87
+ emitEvent
88
+ } = {}) {
89
+ if (typeof emitEvent !== 'function') {
90
+ throw new Error('createJsonLogger requires emitEvent')
91
+ }
92
+
93
+ return {
94
+ logProcessing: (message = '', data = {}) => emitEvent('log', {level: 'processing', message: String(message ?? ''), data}),
95
+ logSuccess: (message = '', data = {}) => emitEvent('log', {level: 'success', message: String(message ?? ''), data}),
96
+ logWarning: (message = '', data = {}) => emitEvent('log', {level: 'warning', message: String(message ?? ''), data}),
97
+ logError: (message = '', data = {}) => emitEvent('log', {level: 'error', message: String(message ?? ''), data})
98
+ }
99
+ }