@wyxos/zephyr 0.5.0 → 0.7.4

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,7 +2,6 @@ import { getCurrentBranch as getCurrentBranchImpl, getUpstreamRef as getUpstream
2
2
  import {hasPrePushHook} from './preflight.mjs'
3
3
  import {gitCommitArgs, gitPushArgs} from '../utils/git-hooks.mjs'
4
4
  import {
5
- buildFallbackCommitMessage,
6
5
  formatWorkingTreePreview,
7
6
  parseWorkingTreeEntries,
8
7
  suggestCommitMessage as suggestCommitMessageImpl
@@ -178,6 +177,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
178
177
  logProcessing,
179
178
  logSuccess,
180
179
  logWarning,
180
+ autoCommit = false,
181
181
  skipGitHooks = false,
182
182
  suggestCommitMessage = suggestCommitMessageImpl
183
183
  } = {}) {
@@ -187,7 +187,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
187
187
  return
188
188
  }
189
189
 
190
- if (typeof runPrompt !== 'function') {
190
+ if (!autoCommit && typeof runPrompt !== 'function') {
191
191
  throw new Error(DIRTY_DEPLOYMENT_MESSAGE)
192
192
  }
193
193
 
@@ -213,36 +213,35 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
213
213
  logStep: logProcessing,
214
214
  logWarning,
215
215
  statusEntries
216
- }) ?? buildFallbackCommitMessage(statusEntries)
217
-
218
- const changeLabel = statusEntries.length === 1 ? 'change' : 'changes'
219
- const {shouldCommitPendingChanges} = await runPrompt([
220
- {
221
- type: 'confirm',
222
- name: 'shouldCommitPendingChanges',
223
- message:
224
- `Pending ${changeLabel} detected before deployment:\n\n` +
225
- `${formatWorkingTreePreview(statusEntries)}\n\n` +
226
- 'Stage and commit all current changes before continuing?',
227
- default: true
216
+ })
217
+ let message = suggestedCommitMessage?.trim() ?? ''
218
+
219
+ if (autoCommit) {
220
+ if (!message) {
221
+ throw new Error('Deployment auto-commit failed because Codex could not determine a usable commit message.')
228
222
  }
229
- ])
230
223
 
231
- if (!shouldCommitPendingChanges) {
232
- throw new Error(DIRTY_DEPLOYMENT_CANCELLED_MESSAGE)
233
- }
224
+ logProcessing?.(`Auto-commit enabled. Using Codex-generated commit message "${message}".`)
225
+ } else {
226
+ const { commitMessage } = await runPrompt([
227
+ {
228
+ type: 'input',
229
+ name: 'commitMessage',
230
+ message:
231
+ 'Pending changes detected before deployment:\n\n' +
232
+ `${formatWorkingTreePreview(statusEntries)}\n\n` +
233
+ 'Enter a commit message to stage and commit all current changes before continuing.\n' +
234
+ 'Leave blank to cancel.',
235
+ default: message
236
+ }
237
+ ])
234
238
 
235
- const { commitMessage } = await runPrompt([
236
- {
237
- type: 'input',
238
- name: 'commitMessage',
239
- message: 'Commit message for pending deployment changes',
240
- default: suggestedCommitMessage,
241
- validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
239
+ if (!commitMessage || commitMessage.trim().length === 0) {
240
+ throw new Error(DIRTY_DEPLOYMENT_CANCELLED_MESSAGE)
242
241
  }
243
- ])
244
242
 
245
- const message = commitMessage.trim()
243
+ message = commitMessage.trim()
244
+ }
246
245
 
247
246
  logProcessing?.('Staging all pending changes before deployment...')
248
247
  await runCommand('git', ['add', '-A'], { cwd: rootDir })
@@ -369,6 +368,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
369
368
  logProcessing,
370
369
  logSuccess,
371
370
  logWarning,
371
+ autoCommit = false,
372
372
  skipGitHooks = false,
373
373
  suggestCommitMessage: suggestCommitMessageFn = suggestCommitMessageImpl,
374
374
  getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
@@ -448,6 +448,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
448
448
  logProcessing,
449
449
  logSuccess,
450
450
  logWarning,
451
+ autoCommit,
451
452
  skipGitHooks,
452
453
  suggestCommitMessage: suggestCommitMessageFn
453
454
  })
@@ -25,17 +25,30 @@ export async function hasPrePushHook(rootDir) {
25
25
  return false
26
26
  }
27
27
 
28
+ async function readPackageJson(rootDir) {
29
+ const packageJsonPath = path.join(rootDir, 'package.json')
30
+ const raw = await fs.readFile(packageJsonPath, 'utf8')
31
+ return JSON.parse(raw)
32
+ }
33
+
28
34
  export async function hasLintScript(rootDir) {
29
35
  try {
30
- const packageJsonPath = path.join(rootDir, 'package.json')
31
- const raw = await fs.readFile(packageJsonPath, 'utf8')
32
- const packageJson = JSON.parse(raw)
36
+ const packageJson = await readPackageJson(rootDir)
33
37
  return packageJson.scripts && typeof packageJson.scripts.lint === 'string'
34
38
  } catch {
35
39
  return false
36
40
  }
37
41
  }
38
42
 
43
+ export async function hasBuildScript(rootDir) {
44
+ try {
45
+ const packageJson = await readPackageJson(rootDir)
46
+ return packageJson.scripts && typeof packageJson.scripts.build === 'string'
47
+ } catch {
48
+ return false
49
+ }
50
+ }
51
+
39
52
  export async function hasLaravelPint(rootDir) {
40
53
  try {
41
54
  const pintPath = path.join(rootDir, 'vendor', 'bin', 'pint')
@@ -91,6 +104,28 @@ export async function resolveSupportedLintCommand(rootDir, {commandExists} = {})
91
104
  throw error
92
105
  }
93
106
 
107
+ export async function resolveSupportedBuildCommand(rootDir, {commandExists} = {}) {
108
+ const hasNpmBuild = await hasBuildScript(rootDir)
109
+
110
+ if (!hasNpmBuild) {
111
+ return null
112
+ }
113
+
114
+ if (commandExists && !commandExists('npm')) {
115
+ throw new Error(
116
+ 'Release cannot run because `npm run build` is configured but npm is not available in PATH.\n' +
117
+ 'Install npm or fix your PATH before deploying.'
118
+ )
119
+ }
120
+
121
+ return {
122
+ type: 'npm',
123
+ command: 'npm',
124
+ args: ['run', 'build'],
125
+ label: 'npm build'
126
+ }
127
+ }
128
+
94
129
  export async function runLinting(rootDir, {
95
130
  runCommand,
96
131
  logProcessing,
@@ -117,6 +152,25 @@ export async function runLinting(rootDir, {
117
152
  return false
118
153
  }
119
154
 
155
+ export async function runBuild(rootDir, {
156
+ runCommand,
157
+ logProcessing,
158
+ logSuccess,
159
+ commandExists,
160
+ buildCommand = null
161
+ } = {}) {
162
+ const selectedBuildCommand = buildCommand ?? await resolveSupportedBuildCommand(rootDir, {commandExists})
163
+
164
+ if (selectedBuildCommand === null) {
165
+ return false
166
+ }
167
+
168
+ logProcessing?.('Running local frontend build...')
169
+ await runCommand(selectedBuildCommand.command, selectedBuildCommand.args, {cwd: rootDir})
170
+ logSuccess?.('Local frontend build completed.')
171
+ return true
172
+ }
173
+
120
174
  export function hasStagedChanges(statusOutput) {
121
175
  if (!statusOutput || statusOutput.length === 0) {
122
176
  return false
package/src/main.mjs CHANGED
@@ -11,6 +11,7 @@ import * as bootstrap from './project/bootstrap.mjs'
11
11
  import {getErrorCode, ZephyrError} from './runtime/errors.mjs'
12
12
  import {PROJECT_CONFIG_DIR} from './utils/paths.mjs'
13
13
  import {writeStderrLine} from './utils/output.mjs'
14
+ import {mergeDeployOptions} from './config/preset-options.mjs'
14
15
  import {createAppContext} from './runtime/app-context.mjs'
15
16
  import {createConfigurationService} from './application/configuration/service.mjs'
16
17
  import {selectDeploymentTarget} from './application/configuration/select-deployment-target.mjs'
@@ -35,11 +36,21 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
35
36
  resumePending: firstArg.resumePending === true,
36
37
  discardPending: firstArg.discardPending === true,
37
38
  maintenanceMode: firstArg.maintenanceMode ?? null,
39
+ autoCommit: firstArg.autoCommit === true,
40
+ skipVersioning: firstArg.skipVersioning === true,
38
41
  skipGitHooks: firstArg.skipGitHooks === true,
39
- skipTests: firstArg.skipTests === true,
40
- skipLint: firstArg.skipLint === true,
42
+ skipChecks: firstArg.skipChecks === true,
43
+ skipTests: firstArg.skipTests === true || firstArg.skipChecks === true,
44
+ skipLint: firstArg.skipLint === true || firstArg.skipChecks === true,
41
45
  skipBuild: firstArg.skipBuild === true,
42
46
  skipDeploy: firstArg.skipDeploy === true,
47
+ explicitMaintenanceMode: firstArg.explicitMaintenanceMode === true || 'maintenanceMode' in firstArg,
48
+ explicitAutoCommit: firstArg.explicitAutoCommit === true || 'autoCommit' in firstArg,
49
+ explicitSkipVersioning: firstArg.explicitSkipVersioning === true || 'skipVersioning' in firstArg,
50
+ explicitSkipGitHooks: firstArg.explicitSkipGitHooks === true || 'skipGitHooks' in firstArg,
51
+ explicitSkipChecks: firstArg.explicitSkipChecks === true || 'skipChecks' in firstArg,
52
+ explicitSkipTests: firstArg.explicitSkipTests === true || 'skipTests' in firstArg || 'skipChecks' in firstArg,
53
+ explicitSkipLint: firstArg.explicitSkipLint === true || 'skipLint' in firstArg || 'skipChecks' in firstArg,
43
54
  context: firstArg.context ?? null
44
55
  }
45
56
  }
@@ -53,11 +64,21 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
53
64
  resumePending: false,
54
65
  discardPending: false,
55
66
  maintenanceMode: null,
67
+ autoCommit: false,
68
+ skipVersioning: false,
56
69
  skipGitHooks: false,
70
+ skipChecks: false,
57
71
  skipTests: false,
58
72
  skipLint: false,
59
73
  skipBuild: false,
60
74
  skipDeploy: false,
75
+ explicitMaintenanceMode: false,
76
+ explicitAutoCommit: false,
77
+ explicitSkipVersioning: false,
78
+ explicitSkipGitHooks: false,
79
+ explicitSkipChecks: false,
80
+ explicitSkipTests: false,
81
+ explicitSkipLint: false,
61
82
  context: null
62
83
  }
63
84
  }
@@ -108,9 +129,21 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
108
129
  workflow: resolveWorkflowName(options.workflowType),
109
130
  presetName: options.presetName,
110
131
  maintenanceMode: options.maintenanceMode,
132
+ autoCommit: options.autoCommit === true,
133
+ skipVersioning: options.skipVersioning === true,
111
134
  skipGitHooks: options.skipGitHooks === true,
135
+ skipChecks: options.skipChecks === true,
136
+ skipTests: options.skipTests === true,
137
+ skipLint: options.skipLint === true,
112
138
  resumePending: options.resumePending,
113
- discardPending: options.discardPending
139
+ discardPending: options.discardPending,
140
+ explicitMaintenanceMode: options.explicitMaintenanceMode === true,
141
+ explicitAutoCommit: options.explicitAutoCommit === true,
142
+ explicitSkipVersioning: options.explicitSkipVersioning === true,
143
+ explicitSkipGitHooks: options.explicitSkipGitHooks === true,
144
+ explicitSkipChecks: options.explicitSkipChecks === true,
145
+ explicitSkipTests: options.explicitSkipTests === true,
146
+ explicitSkipLint: options.explicitSkipLint === true
114
147
  }
115
148
  const appContext = options.context ?? createAppContext({executionMode})
116
149
  const {
@@ -122,7 +155,11 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
122
155
  runCommand,
123
156
  emitEvent
124
157
  } = appContext
125
- const currentExecutionMode = appContext.executionMode ?? executionMode
158
+ let currentExecutionMode = {
159
+ ...executionMode,
160
+ ...(appContext.executionMode ?? {})
161
+ }
162
+ appContext.executionMode = currentExecutionMode
126
163
  const configurationService = createConfigurationService(appContext)
127
164
 
128
165
  try {
@@ -137,7 +174,12 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
137
174
  nonInteractive: currentExecutionMode.interactive === false,
138
175
  presetName: currentExecutionMode.presetName,
139
176
  maintenanceMode: currentExecutionMode.maintenanceMode,
177
+ autoCommit: currentExecutionMode.autoCommit === true,
178
+ skipVersioning: currentExecutionMode.skipVersioning === true,
140
179
  skipGitHooks: currentExecutionMode.skipGitHooks === true,
180
+ skipChecks: currentExecutionMode.skipChecks === true,
181
+ skipTests: currentExecutionMode.skipTests === true,
182
+ skipLint: currentExecutionMode.skipLint === true,
141
183
  resumePending: currentExecutionMode.resumePending,
142
184
  discardPending: currentExecutionMode.discardPending
143
185
  }
@@ -162,6 +204,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
162
204
  skipGitHooks: options.skipGitHooks,
163
205
  skipTests: options.skipTests,
164
206
  skipLint: options.skipLint,
207
+ skipVersioning: options.skipVersioning,
165
208
  skipBuild: options.skipBuild,
166
209
  skipDeploy: options.skipDeploy,
167
210
  context: appContext
@@ -190,6 +233,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
190
233
  skipGitHooks: options.skipGitHooks,
191
234
  skipTests: options.skipTests,
192
235
  skipLint: options.skipLint,
236
+ skipVersioning: options.skipVersioning,
193
237
  context: appContext
194
238
  })
195
239
  emitEvent?.('run_completed', {
@@ -241,7 +285,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
241
285
  })
242
286
  }
243
287
 
244
- const {deploymentConfig} = await selectDeploymentTarget(rootDir, {
288
+ const {deploymentConfig, presetState} = await selectDeploymentTarget(rootDir, {
245
289
  configurationService,
246
290
  runPrompt,
247
291
  logProcessing,
@@ -251,6 +295,19 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
251
295
  executionMode: currentExecutionMode
252
296
  })
253
297
 
298
+ if (presetState) {
299
+ const effectiveDeployOptions = mergeDeployOptions(currentExecutionMode, presetState.options)
300
+ currentExecutionMode = {
301
+ ...currentExecutionMode,
302
+ presetName: presetState.name,
303
+ ...effectiveDeployOptions,
304
+ skipChecks: currentExecutionMode.skipChecks === true ||
305
+ (effectiveDeployOptions.skipTests === true && effectiveDeployOptions.skipLint === true)
306
+ }
307
+ appContext.executionMode = currentExecutionMode
308
+ await presetState.applyExecutionMode(currentExecutionMode)
309
+ }
310
+
254
311
  const snapshotToUse = await resolvePendingSnapshot(rootDir, deploymentConfig, {
255
312
  runPrompt,
256
313
  logProcessing,
@@ -262,7 +319,8 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
262
319
  rootDir,
263
320
  snapshot: snapshotToUse,
264
321
  versionArg: options.versionArg,
265
- context: appContext
322
+ context: appContext,
323
+ presetState
266
324
  })
267
325
 
268
326
  emitEvent?.('run_completed', {
@@ -20,7 +20,8 @@ const GENERIC_SUBJECT_PATTERNS = [
20
20
  /^update work$/i,
21
21
  /^misc(ellaneous)?( updates?)?$/i,
22
22
  /^changes$/i,
23
- /^updates?$/i
23
+ /^updates?$/i,
24
+ /^(improve|update|adjust|refine|align|support|enable)\s+.+\s+(workflow|process|flow)$/i
24
25
  ]
25
26
  const MAX_WORKING_TREE_PREVIEW = 20
26
27
  const STATUS_LABELS = {
@@ -32,59 +33,6 @@ const STATUS_LABELS = {
32
33
  T: 'type-changed',
33
34
  U: 'conflicted'
34
35
  }
35
- const TOPIC_STOP_WORDS = new Set([
36
- 'src',
37
- 'test',
38
- 'tests',
39
- '__tests__',
40
- 'spec',
41
- 'specs',
42
- 'app',
43
- 'lib',
44
- 'dist',
45
- 'packages',
46
- 'package',
47
- 'application',
48
- 'shared',
49
- 'index',
50
- 'main',
51
- 'local',
52
- 'repo',
53
- 'prepare',
54
- 'commit',
55
- 'message',
56
- 'js',
57
- 'jsx',
58
- 'ts',
59
- 'tsx',
60
- 'mjs',
61
- 'cjs',
62
- 'php',
63
- 'json',
64
- 'yaml',
65
- 'yml',
66
- 'md',
67
- 'toml',
68
- 'lock'
69
- ])
70
-
71
- function buildTargetedFallbackCommitMessage(statusEntries = []) {
72
- const paths = statusEntries.map((entry) => entry.path.toLowerCase())
73
- const touchesDeployPrep = paths.some((pathValue) => pathValue.includes('prepare-local-deployment'))
74
- const touchesLocalRepo = paths.some((pathValue) => pathValue.includes('local-repo'))
75
- const touchesCommitMessage = paths.some((pathValue) => pathValue.includes('commit-message'))
76
- const touchesReleaseFlow = paths.some((pathValue) => pathValue.includes('/release/') || pathValue.includes('release-'))
77
-
78
- if (touchesDeployPrep && touchesLocalRepo) {
79
- return 'fix: prompt for dirty deploy changes before version bump'
80
- }
81
-
82
- if (touchesCommitMessage && touchesReleaseFlow) {
83
- return 'fix: tighten release commit suggestions'
84
- }
85
-
86
- return null
87
- }
88
36
 
89
37
  function resolveWorkingTreeEntryLabel(entry) {
90
38
  if (entry.indexStatus === '?' && entry.worktreeStatus === '?') {
@@ -105,32 +53,6 @@ function resolveWorkingTreeEntryLabel(entry) {
105
53
  return 'changed'
106
54
  }
107
55
 
108
- function tokenizePath(pathValue = '') {
109
- return pathValue
110
- .split(/[\\/]/)
111
- .flatMap((segment) => segment.split(/[^a-zA-Z0-9]+/))
112
- .map((token) => token.toLowerCase())
113
- .filter((token) => token.length >= 3 && !TOPIC_STOP_WORDS.has(token))
114
- }
115
-
116
- function inferCommitTypeFromEntries(statusEntries = []) {
117
- const paths = statusEntries.map((entry) => entry.path.toLowerCase())
118
-
119
- if (paths.every((pathValue) => pathValue.endsWith('.md') || pathValue.includes('/docs/') || pathValue.startsWith('docs/'))) {
120
- return 'docs'
121
- }
122
-
123
- if (paths.every((pathValue) => /\.test\.[^.]+$/.test(pathValue) || pathValue.includes('/tests/'))) {
124
- return 'test'
125
- }
126
-
127
- if (paths.some((pathValue) => pathValue.includes('.github/workflows/') || pathValue.includes('/ci/'))) {
128
- return 'ci'
129
- }
130
-
131
- return 'chore'
132
- }
133
-
134
56
  export function parseWorkingTreeStatus(stdout = '') {
135
57
  return stdout
136
58
  .split(/\r?\n/)
@@ -225,92 +147,11 @@ export function sanitizeSuggestedCommitMessage(message) {
225
147
  return normalized
226
148
  }
227
149
 
228
- export function buildFallbackCommitMessage(statusEntries = []) {
229
- const targetedFallback = buildTargetedFallbackCommitMessage(statusEntries)
230
- if (targetedFallback) {
231
- return targetedFallback
232
- }
233
-
234
- const tokenCounts = new Map()
235
-
236
- for (const entry of statusEntries) {
237
- for (const token of tokenizePath(entry.path)) {
238
- tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1)
239
- }
240
- }
241
-
242
- const orderedTokens = Array.from(tokenCounts.entries())
243
- .sort((left, right) => {
244
- if (right[1] !== left[1]) {
245
- return right[1] - left[1]
246
- }
247
-
248
- return left[0].localeCompare(right[0])
249
- })
250
- .map(([token]) => token)
251
-
252
- const primaryTopic = orderedTokens[0] ?? 'release'
253
- const commitType = inferCommitTypeFromEntries(statusEntries)
254
-
255
- if (commitType === 'docs') {
256
- return `docs: update ${primaryTopic} documentation`
257
- }
258
-
259
- if (commitType === 'test') {
260
- return `test: expand ${primaryTopic} coverage`
261
- }
262
-
263
- if (commitType === 'ci') {
264
- return `ci: update ${primaryTopic} workflow`
265
- }
266
-
267
- return `chore: improve ${primaryTopic} workflow`
268
- }
269
-
270
- async function collectDiffNumstat(rootDir, {runCommand} = {}) {
271
- try {
272
- const {stdout} = await runCommand('git', ['diff', '--numstat', 'HEAD', '--'], {
273
- capture: true,
274
- cwd: rootDir
275
- })
276
-
277
- return stdout
278
- .split(/\r?\n/)
279
- .map((line) => line.trim())
280
- .filter(Boolean)
281
- .reduce((map, line) => {
282
- const [addedRaw, deletedRaw, filePath] = line.split('\t')
283
- if (!filePath) {
284
- return map
285
- }
286
-
287
- const added = Number.parseInt(addedRaw, 10)
288
- const deleted = Number.parseInt(deletedRaw, 10)
289
- map.set(filePath, {
290
- added: Number.isFinite(added) ? added : 0,
291
- deleted: Number.isFinite(deleted) ? deleted : 0
292
- })
293
- return map
294
- }, new Map())
295
- } catch {
296
- return new Map()
297
- }
298
- }
299
-
300
- async function buildCommitMessageContext(rootDir, {
301
- runCommand,
302
- statusEntries = []
303
- } = {}) {
304
- const changeCountsByPath = await collectDiffNumstat(rootDir, {runCommand})
305
- return statusEntries.map((entry) => `- ${summarizeWorkingTreeEntry(entry, {changeCountsByPath})}`).join('\n')
306
- }
307
-
308
150
  export async function suggestCommitMessage(rootDir = process.cwd(), {
309
151
  runCommand,
310
152
  commandExistsImpl = commandExists,
311
153
  logStep,
312
- logWarning,
313
- statusEntries = []
154
+ logWarning
314
155
  } = {}) {
315
156
  if (!commandExistsImpl('codex')) {
316
157
  return null
@@ -321,10 +162,6 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
321
162
  try {
322
163
  tempDir = await mkdtemp(path.join(tmpdir(), 'zephyr-release-commit-'))
323
164
  const outputPath = path.join(tempDir, 'codex-last-message.txt')
324
- const commitContext = await buildCommitMessageContext(rootDir, {
325
- runCommand,
326
- statusEntries
327
- })
328
165
 
329
166
  logStep?.('Generating a suggested commit message with Codex...')
330
167
 
@@ -339,13 +176,11 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
339
176
  '--output-last-message',
340
177
  outputPath,
341
178
  [
342
- 'Write exactly one short conventional commit message for these pending changes.',
343
- 'Use the exact format "<type>: <subject>" with no scope, no exclamation mark, and no extra text.',
344
- 'Choose the most appropriate type from: fix, feat, chore, docs, refactor, test, style, perf, build, ci, revert.',
345
- 'Make the subject specific enough to describe the actual behavior or workflow change, not just that files changed.',
346
- 'Do not describe the commit itself, staging, or "pending changes"; describe the underlying behavior or workflow fix.',
347
- 'Pending change summary:',
348
- commitContext || '- changed files present'
179
+ 'Inspect the current repository and decide the best conventional commit message for the pending changes.',
180
+ 'Use whatever read-only inspection you need.',
181
+ 'Reply with exactly one line in the format "<type>: <subject>".',
182
+ 'Do not use scopes like "fix(scope): ...".',
183
+ 'Do not include extra text.'
349
184
  ].join('\n\n')
350
185
  ], {
351
186
  capture: true,
@@ -10,7 +10,6 @@ import {
10
10
  getUpstreamRef
11
11
  } from '../utils/git.mjs'
12
12
  import {
13
- buildFallbackCommitMessage,
14
13
  formatWorkingTreePreview,
15
14
  parseWorkingTreeEntries,
16
15
  parseWorkingTreeStatus,
@@ -117,35 +116,24 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
117
116
  logStep,
118
117
  logWarning,
119
118
  statusEntries
120
- }) ?? buildFallbackCommitMessage(statusEntries)
121
-
122
- const changeLabel = statusEntries.length === 1 ? 'change' : 'changes'
123
- const {shouldCommitPendingChanges} = await runPrompt([
119
+ })
120
+ const {commitMessage} = await runPrompt([
124
121
  {
125
- type: 'confirm',
126
- name: 'shouldCommitPendingChanges',
122
+ type: 'input',
123
+ name: 'commitMessage',
127
124
  message:
128
- `Pending ${changeLabel} detected before release:\n\n` +
125
+ 'Pending changes detected before release:\n\n' +
129
126
  `${formatWorkingTreePreview(statusEntries)}\n\n` +
130
- 'Stage and commit all current changes before continuing?',
131
- default: true
127
+ 'Enter a commit message to stage and commit all current changes before continuing.\n' +
128
+ 'Leave blank to cancel.',
129
+ default: suggestedCommitMessage ?? ''
132
130
  }
133
131
  ])
134
132
 
135
- if (!shouldCommitPendingChanges) {
133
+ if (!commitMessage || commitMessage.trim().length === 0) {
136
134
  throw new Error(DIRTY_WORKING_TREE_CANCELLED_MESSAGE)
137
135
  }
138
136
 
139
- const {commitMessage} = await runPrompt([
140
- {
141
- type: 'input',
142
- name: 'commitMessage',
143
- message: 'Commit message for pending release changes',
144
- default: suggestedCommitMessage,
145
- validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
146
- }
147
- ])
148
-
149
137
  const message = commitMessage.trim()
150
138
 
151
139
  logStep?.('Staging all pending changes before release...')
@@ -9,6 +9,7 @@ function hasExplicitReleaseOptions(options = {}) {
9
9
  'skipGitHooks',
10
10
  'skipTests',
11
11
  'skipLint',
12
+ 'skipVersioning',
12
13
  'skipBuild',
13
14
  'skipDeploy'
14
15
  ].some((key) => key in options)
@@ -21,12 +22,17 @@ export async function releaseNode(options = {}) {
21
22
  skipGitHooks: options.skipGitHooks === true,
22
23
  skipTests: options.skipTests === true,
23
24
  skipLint: options.skipLint === true,
25
+ skipVersioning: options.skipVersioning === true,
24
26
  skipBuild: options.skipBuild === true,
25
27
  skipDeploy: options.skipDeploy === true
26
28
  }
27
29
  : parseReleaseArgs({
28
- booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
30
+ booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-versioning', '--skip-build', '--skip-deploy']
29
31
  })
32
+
33
+ if (parsed.skipVersioning && parsed.releaseType) {
34
+ throw new Error('--skip-versioning cannot be used together with an explicit version or bump argument.')
35
+ }
30
36
  const rootDir = options.rootDir ?? process.cwd()
31
37
  const context = options.context ?? createAppContext({
32
38
  executionMode: {
@@ -42,6 +48,7 @@ export async function releaseNode(options = {}) {
42
48
  skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
43
49
  skipTests: parsed.skipTests === true,
44
50
  skipLint: parsed.skipLint === true,
51
+ skipVersioning: parsed.skipVersioning === true,
45
52
  skipBuild: parsed.skipBuild === true,
46
53
  skipDeploy: parsed.skipDeploy === true,
47
54
  rootDir,