@wyxos/zephyr 0.8.1 → 0.8.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.
package/README.md CHANGED
@@ -118,7 +118,7 @@ For Laravel app deployments, `--maintenance on|off` overrides both the saved pre
118
118
 
119
119
  `--skip-versioning` keeps Zephyr from mutating `package.json` or `composer.json`. On app deploys it skips the local npm version bump step. On package release workflows it releases the version already present in the manifest and creates the release tag from the current `HEAD`.
120
120
 
121
- `--then-deploy <path>` is available for `--type node` and `--type vue` package releases. After the package release succeeds and the published version is visible on npm, Zephyr updates the local consumer repository, commits the consumer dependency change, and deploys that consumer through its normal Zephyr app-deploy preset. Use `--consumer-preset <name>` to choose the app preset and `--consumer-package <name>` when the consumer dependency name differs from the released package name. Consumer lockfiles are refreshed only when the consumer repository already tracks an npm lockfile; Zephyr does not create or commit server-side lockfiles.
121
+ `--then-deploy <path>` is available for `--type node` and `--type vue` package releases. After the package release succeeds and the published version is visible on npm, Zephyr updates the local consumer repository, commits the consumer dependency change, and deploys that consumer through its normal Zephyr app-deploy preset. Use `--consumer-preset <name>` to choose the app preset and `--consumer-package <name>` when the consumer dependency name differs from the released package name. Pass `--consumer-auto-commit` when the consumer app may already have pending changes; Zephyr will stage, commit, and push all pending consumer changes before applying the dependency bump. Consumer lockfiles are refreshed only when the consumer repository already tracks an npm lockfile; Zephyr does not create or commit server-side lockfiles.
122
122
 
123
123
  ## AI Agents and Automation
124
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "A streamlined deployment tool for web applications with intelligent Laravel project detection",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -12,7 +12,7 @@ import {resolvePendingSnapshot} from '../deploy/resolve-pending-snapshot.mjs'
12
12
  import {runDeployment} from '../deploy/run-deployment.mjs'
13
13
  import {waitForNpmPackageVersion} from './npm-publish-wait.mjs'
14
14
  import {
15
- assertCleanConsumerRepo,
15
+ ensureConsumerRepoReady,
16
16
  updateConsumerDependency
17
17
  } from './update-consumer-dependency.mjs'
18
18
 
@@ -113,7 +113,7 @@ export async function releasePackageThenDeployConsumer({
113
113
  createAppContextImpl = createAppContext,
114
114
  waitForNpmPackageVersionImpl = waitForNpmPackageVersion,
115
115
  updateConsumerDependencyImpl = updateConsumerDependency,
116
- assertCleanConsumerRepoImpl = assertCleanConsumerRepo,
116
+ ensureConsumerRepoReadyImpl = ensureConsumerRepoReady,
117
117
  validateLocalDependenciesImpl = validateLocalDependencies,
118
118
  selectDeploymentTargetImpl = selectDeploymentTarget,
119
119
  resolvePendingSnapshotImpl = resolvePendingSnapshot,
@@ -158,7 +158,16 @@ export async function releasePackageThenDeployConsumer({
158
158
  const configurationService = createConfigurationService(context)
159
159
 
160
160
  logProcessing?.(`Preparing consumer app at ${resolvedConsumerRootDir}...`)
161
- await assertCleanConsumerRepoImpl(resolvedConsumerRootDir, {runCommandCapture})
161
+ await ensureConsumerRepoReadyImpl(resolvedConsumerRootDir, {
162
+ runCommand,
163
+ runCommandCapture,
164
+ runPrompt,
165
+ logProcessing,
166
+ logSuccess,
167
+ logWarning,
168
+ autoCommit: executionMode.autoCommit,
169
+ skipGitHooks: executionMode.skipGitHooks
170
+ })
162
171
 
163
172
  await bootstrapImpl.ensureGitignoreEntry(resolvedConsumerRootDir, {
164
173
  runCommand,
@@ -226,6 +235,8 @@ export async function releasePackageThenDeployConsumer({
226
235
  logProcessing,
227
236
  logSuccess,
228
237
  logWarning,
238
+ runPrompt,
239
+ autoCommit: executionMode.autoCommit,
229
240
  skipGitHooks: executionMode.skipGitHooks
230
241
  })
231
242
 
@@ -1,7 +1,12 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
- import {gitCommitArgs} from '../../utils/git-hooks.mjs'
4
+ import {
5
+ formatWorkingTreePreview,
6
+ parseWorkingTreeEntries,
7
+ suggestReleaseCommitMessage as suggestCommitMessageImpl
8
+ } from '../../release/commit-message.mjs'
9
+ import {gitCommitArgs, gitPushArgs} from '../../utils/git-hooks.mjs'
5
10
 
6
11
  const DEPENDENCY_FIELDS = [
7
12
  'dependencies',
@@ -55,6 +60,113 @@ export async function assertCleanConsumerRepo(rootDir, {runCommandCapture} = {})
55
60
  }
56
61
  }
57
62
 
63
+ function normalizeCommandOutput(output) {
64
+ return typeof output === 'string' ? output : output?.stdout ?? ''
65
+ }
66
+
67
+ function createCaptureAwareRunCommand({runCommand, runCommandCapture}) {
68
+ return async (command, args, {capture = false, cwd} = {}) => {
69
+ if (capture) {
70
+ const captured = await runCommandCapture(command, args, {cwd})
71
+
72
+ if (typeof captured === 'string') {
73
+ return {stdout: captured.trim(), stderr: ''}
74
+ }
75
+
76
+ const stdout = captured?.stdout ?? ''
77
+ const stderr = captured?.stderr ?? ''
78
+ return {stdout: stdout.trim(), stderr: stderr.trim()}
79
+ }
80
+
81
+ await runCommand(command, args, {cwd})
82
+ return undefined
83
+ }
84
+ }
85
+
86
+ export async function ensureConsumerRepoReady(rootDir, {
87
+ runCommand,
88
+ runCommandCapture,
89
+ runPrompt,
90
+ logProcessing,
91
+ logSuccess,
92
+ logWarning,
93
+ autoCommit = false,
94
+ skipGitHooks = false,
95
+ suggestCommitMessage = suggestCommitMessageImpl
96
+ } = {}) {
97
+ if (typeof runCommandCapture !== 'function') {
98
+ throw new Error('Consumer repository checks require a command capture runner.')
99
+ }
100
+
101
+ const status = normalizeCommandOutput(await runCommandCapture('git', ['status', '--porcelain'], {cwd: rootDir}))
102
+ const statusEntries = parseWorkingTreeEntries(status)
103
+
104
+ if (statusEntries.length === 0) {
105
+ return {committed: false, pushed: false}
106
+ }
107
+
108
+ if (!autoCommit && typeof runPrompt !== 'function') {
109
+ throw new Error('Consumer repository has uncommitted changes. Commit, stash, or clean them before running a package-to-consumer release.')
110
+ }
111
+
112
+ if (typeof runCommand !== 'function') {
113
+ throw new Error('Consumer repository auto-commit requires a command runner.')
114
+ }
115
+
116
+ const captureAwareRunCommand = createCaptureAwareRunCommand({runCommand, runCommandCapture})
117
+ const suggestedCommitMessage = await suggestCommitMessage(rootDir, {
118
+ runCommand: captureAwareRunCommand,
119
+ logStep: logProcessing,
120
+ logWarning,
121
+ statusEntries
122
+ })
123
+ let message = suggestedCommitMessage?.trim() ?? ''
124
+
125
+ if (autoCommit) {
126
+ if (!message) {
127
+ throw new Error('Consumer auto-commit failed because Codex could not determine a usable commit message.')
128
+ }
129
+
130
+ logProcessing?.(`Consumer auto-commit enabled. Using Codex-generated commit message "${message}".`)
131
+ } else {
132
+ const {commitMessage} = await runPrompt([
133
+ {
134
+ type: 'input',
135
+ name: 'commitMessage',
136
+ message:
137
+ 'Pending changes detected in the consumer repository:\n\n' +
138
+ `${formatWorkingTreePreview(statusEntries)}\n\n` +
139
+ 'Enter a commit message to stage, commit, and push all current consumer changes before continuing.\n' +
140
+ 'Leave blank to cancel.',
141
+ default: message
142
+ }
143
+ ])
144
+
145
+ if (!commitMessage || commitMessage.trim().length === 0) {
146
+ throw new Error('Consumer release cancelled: pending changes were not committed.')
147
+ }
148
+
149
+ message = commitMessage.trim()
150
+ }
151
+
152
+ logProcessing?.('Staging all pending consumer changes before dependency update...')
153
+ await runCommand('git', ['add', '-A'], {cwd: rootDir})
154
+
155
+ logProcessing?.('Committing pending consumer changes before dependency update...')
156
+ await runCommand('git', gitCommitArgs(['-m', message], {skipGitHooks}), {cwd: rootDir})
157
+
158
+ logProcessing?.('Pushing pending consumer changes before dependency update...')
159
+ await runCommand('git', gitPushArgs([], {skipGitHooks}), {cwd: rootDir})
160
+
161
+ const finalStatus = normalizeCommandOutput(await runCommandCapture('git', ['status', '--porcelain'], {cwd: rootDir}))
162
+ if (parseWorkingTreeEntries(finalStatus).length > 0) {
163
+ throw new Error('Consumer repository still has uncommitted changes after auto-commit. Aborting package-to-consumer release.')
164
+ }
165
+
166
+ logSuccess?.(`Committed and pushed consumer changes with "${message}".`)
167
+ return {committed: true, pushed: true, message}
168
+ }
169
+
58
170
  async function isTracked(rootDir, filePath, {runCommandCapture} = {}) {
59
171
  try {
60
172
  await runCommandCapture('git', ['ls-files', '--error-unmatch', filePath], {cwd: rootDir})
@@ -91,10 +203,13 @@ export async function updateConsumerDependency({
91
203
  version,
92
204
  runCommand,
93
205
  runCommandCapture,
206
+ runPrompt,
94
207
  logProcessing,
95
208
  logSuccess,
96
209
  logWarning,
97
- skipGitHooks = false
210
+ autoCommit = false,
211
+ skipGitHooks = false,
212
+ suggestCommitMessage = suggestCommitMessageImpl
98
213
  } = {}) {
99
214
  if (!rootDir) {
100
215
  throw new Error('Consumer root directory is required.')
@@ -112,7 +227,17 @@ export async function updateConsumerDependency({
112
227
  throw new Error('Consumer dependency updates require command runners.')
113
228
  }
114
229
 
115
- await assertCleanConsumerRepo(rootDir, {runCommandCapture})
230
+ await ensureConsumerRepoReady(rootDir, {
231
+ runCommand,
232
+ runCommandCapture,
233
+ runPrompt,
234
+ logProcessing,
235
+ logSuccess,
236
+ logWarning,
237
+ autoCommit,
238
+ skipGitHooks,
239
+ suggestCommitMessage
240
+ })
116
241
 
117
242
  const pkg = await readPackageJson(rootDir)
118
243
  const dependencyField = findDependencyField(pkg, packageName)