fireflyy 4.0.0-dev.25c80f6 → 4.0.0-dev.3098d1f

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,10 +1,12 @@
1
- import { n as RuntimeEnv, t as logger } from "./main.js";
2
- import { _ as validationError, a as conflictErrAsync, c as notFoundErrAsync, d as validationErrAsync, f as conflictError, h as notFoundError, i as FireflyOkAsync, l as timeoutErrAsync, m as failedError, n as FireflyErrAsync, r as FireflyOk, s as invalidErr, t as FireflyErr, u as validationErr, v as wrapErrorMessage } from "./result.constructors-C9M1MP3_.js";
3
- import { n as wrapPromise, r as zip3Async, t as ensureNotAsync } from "./result.utilities-DC5shlhT.js";
4
- import { n as parseSchema, t as formatZodErrors } from "./schema.utilities-BGd9t1wm.js";
1
+ import { t as RuntimeEnv } from "./main.js";
2
+ import { _ as notFoundError, a as conflictErrAsync, b as wrapErrorMessage, c as invalidErrAsync, d as validationErr, f as validationErrAsync, h as failedError, i as FireflyOkAsync, l as notFoundErrAsync, n as FireflyErrAsync, o as failedErrAsync, p as conflictError, r as FireflyOk, s as invalidErr, t as FireflyErr, u as timeoutErrAsync, y as validationError } from "./result.constructors-D9jmQ0uj.js";
3
+ import { n as wrapPromise, t as ensureNotAsync } from "./result.utilities-DiG1ae54.js";
4
+ import { t as logger } from "./logging-BuIkRrn1.js";
5
+ import { t as Version } from "./version-D6rAEmbf.js";
6
+ import { t as formatZodErrors } from "./schema.utilities-Y2MdlRMu.js";
7
+ import { Command, InvalidArgumentError } from "commander";
5
8
  import { LogLevels } from "consola";
6
9
  import { colors } from "consola/utils";
7
- import { Command } from "commander";
8
10
  import { loadConfig } from "c12";
9
11
  import { Result, ResultAsync, err, ok } from "neverthrow";
10
12
  import z$1 from "zod";
@@ -184,6 +186,223 @@ function camelToKebab(str) {
184
186
  return result.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/_/g, "-");
185
187
  }
186
188
 
189
+ //#endregion
190
+ //#region src/cli/options/options.validation.ts
191
+ /**
192
+ * Extracts enum values from a Zod schema (handles wrapped types like optional, default).
193
+ *
194
+ * @param schema - The Zod schema to extract enum values from
195
+ * @returns Array of string values if it's an enum, or null if not
196
+ */
197
+ function extractEnumValues(schema) {
198
+ const unwrapped = unwrapZodSchema(schema);
199
+ if (unwrapped instanceof z$1.ZodEnum) return getEnumValuesFromZodEnum(unwrapped);
200
+ if (unwrapped instanceof z$1.ZodUnion) {
201
+ const unionOptions = getZodDef(unwrapped).options;
202
+ if (!unionOptions) return null;
203
+ const values = [];
204
+ for (const option of unionOptions) {
205
+ if (typeof option === "string") continue;
206
+ const innerUnwrapped = unwrapZodSchema(option);
207
+ if (innerUnwrapped instanceof z$1.ZodEnum) {
208
+ const enumValues = getEnumValuesFromZodEnum(innerUnwrapped);
209
+ if (enumValues) values.push(...enumValues);
210
+ } else if (innerUnwrapped instanceof z$1.ZodLiteral) {
211
+ const literalValue = getZodDef(innerUnwrapped).value;
212
+ if (typeof literalValue === "string" && literalValue !== "") values.push(literalValue);
213
+ }
214
+ }
215
+ return values.length > 0 ? values : null;
216
+ }
217
+ return null;
218
+ }
219
+ /**
220
+ * Extracts enum values from a ZodEnum type.
221
+ * Handles both Zod v4 (options array or entries object) and legacy (values array) structures.
222
+ *
223
+ * @param enumSchema - The ZodEnum schema
224
+ * @returns Array of string values, or null if not found
225
+ */
226
+ function getEnumValuesFromZodEnum(enumSchema) {
227
+ const schemaAsAny = enumSchema;
228
+ if (Array.isArray(schemaAsAny.options)) return schemaAsAny.options;
229
+ const def = getZodDef(enumSchema);
230
+ if (def.entries && typeof def.entries === "object") return Object.values(def.entries);
231
+ if (def.values && Array.isArray(def.values)) return def.values;
232
+ return null;
233
+ }
234
+ /**
235
+ * Extracts literal values from a Zod union schema (e.g., z.union([z.literal("0"), z.literal("1")])).
236
+ *
237
+ * @param schema - The Zod schema to extract literal values from
238
+ * @returns Array of literal values if it's a union of literals, or null if not
239
+ */
240
+ function extractLiteralValues(schema) {
241
+ const unwrapped = unwrapZodSchema(schema);
242
+ if (unwrapped instanceof z$1.ZodUnion) {
243
+ const options = getZodDef(unwrapped).options;
244
+ if (!options) return null;
245
+ const values = [];
246
+ for (const option of options) {
247
+ if (typeof option === "string" || typeof option === "number") continue;
248
+ const innerUnwrapped = unwrapZodSchema(option);
249
+ if (innerUnwrapped instanceof z$1.ZodLiteral) {
250
+ const value = getZodDef(innerUnwrapped).value;
251
+ if (typeof value === "string" || typeof value === "number") values.push(value);
252
+ }
253
+ }
254
+ return values.length > 0 ? values : null;
255
+ }
256
+ return null;
257
+ }
258
+ /**
259
+ * Creates a CLI option validator for enum fields.
260
+ *
261
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
262
+ * This is a boundary where exceptions are appropriate for the CLI framework.
263
+ *
264
+ * @param schema - The Zod schema for validation
265
+ * @param optionName - The CLI option name (for error messages)
266
+ * @param choices - The valid enum choices
267
+ * @returns A parser function that throws InvalidArgumentError on failure
268
+ */
269
+ function createEnumValidator(schema, optionName, choices) {
270
+ return (input) => {
271
+ const result = schema.safeParse(input);
272
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". Must be one of: ${choices.join(", ")}`);
273
+ return result.data;
274
+ };
275
+ }
276
+ /**
277
+ * Creates a CLI option validator for number fields.
278
+ *
279
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
280
+ * This is a boundary where exceptions are appropriate for the CLI framework.
281
+ *
282
+ * @param schema - The Zod schema for validation
283
+ * @param optionName - The CLI option name (for error messages)
284
+ * @returns A parser function that throws InvalidArgumentError on failure
285
+ */
286
+ function createNumberValidator(schema, optionName) {
287
+ return (input) => {
288
+ const num = Number(input);
289
+ if (Number.isNaN(num)) throw new InvalidArgumentError(`Invalid number for --${optionName}: "${input}". Expected a valid number.`);
290
+ const result = schema.safeParse(num);
291
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
292
+ return result.data;
293
+ };
294
+ }
295
+ /**
296
+ * Creates a CLI option validator for string fields.
297
+ *
298
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
299
+ * This is a boundary where exceptions are appropriate for the CLI framework.
300
+ *
301
+ * @param schema - The Zod schema for validation
302
+ * @param optionName - The CLI option name (for error messages)
303
+ * @returns A parser function that throws InvalidArgumentError on failure
304
+ */
305
+ function createStringValidator(schema, optionName) {
306
+ return (input) => {
307
+ const result = schema.safeParse(input);
308
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
309
+ return result.data;
310
+ };
311
+ }
312
+ /**
313
+ * Creates a CLI option validator for union types with literal values.
314
+ *
315
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
316
+ * This is a boundary where exceptions are appropriate for the CLI framework.
317
+ *
318
+ * @param schema - The Zod schema for validation
319
+ * @param optionName - The CLI option name (for error messages)
320
+ * @param values - The valid literal values
321
+ * @returns A parser function that throws InvalidArgumentError on failure
322
+ */
323
+ function createLiteralUnionValidator(schema, optionName, values) {
324
+ return (input) => {
325
+ const numericValues = values.filter((v) => typeof v === "number");
326
+ let parsedInput = input;
327
+ if (numericValues.length > 0 && !Number.isNaN(Number(input))) parsedInput = Number(input);
328
+ const result = schema.safeParse(parsedInput);
329
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". Must be one of: ${values.map((v) => typeof v === "string" ? `"${v}"` : String(v)).join(", ")}`);
330
+ return result.data;
331
+ };
332
+ }
333
+ /**
334
+ * Creates a generic CLI option validator.
335
+ *
336
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
337
+ * This is a boundary where exceptions are appropriate for the CLI framework.
338
+ *
339
+ * @param schema - The Zod schema for validation
340
+ * @param optionName - The CLI option name (for error messages)
341
+ * @returns A parser function that throws InvalidArgumentError on failure
342
+ */
343
+ function createGenericValidator(schema, optionName) {
344
+ return (input) => {
345
+ const result = schema.safeParse(input);
346
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
347
+ return result.data;
348
+ };
349
+ }
350
+ /**
351
+ * Formats a Zod error into a user-friendly message.
352
+ *
353
+ * @param error - The Zod error object
354
+ * @returns A formatted error message
355
+ */
356
+ function formatZodErrorMessage(error) {
357
+ const firstIssue = error.issues[0];
358
+ if (!firstIssue) return "Validation failed.";
359
+ switch (firstIssue.code) {
360
+ case "invalid_value": {
361
+ const issue = firstIssue;
362
+ if (issue.values) return `Expected one of: ${issue.values.join(", ")}`;
363
+ return firstIssue.message;
364
+ }
365
+ case "invalid_type": return `Expected ${firstIssue.expected}`;
366
+ case "too_small":
367
+ case "too_big": return firstIssue.message;
368
+ default: return firstIssue.message;
369
+ }
370
+ }
371
+ /**
372
+ * Gets the internal definition object from a Zod type.
373
+ * Handles both modern Zod v4 (_zod.def) and legacy (_def) structures.
374
+ *
375
+ * @param field - The Zod type to get the definition from
376
+ * @returns The internal definition object
377
+ */
378
+ function getZodDef(field) {
379
+ const zodContainer = field._zod;
380
+ if (zodContainer?.def) return zodContainer.def;
381
+ return field._def ?? {};
382
+ }
383
+ /**
384
+ * Unwraps Zod wrapper types (optional, default, etc.) to get the inner type.
385
+ *
386
+ * @param field - The Zod type to unwrap
387
+ * @returns The unwrapped inner type
388
+ */
389
+ function unwrapZodSchema(field) {
390
+ const def = getZodDef(field);
391
+ if (field instanceof z$1.ZodDefault) {
392
+ const inner = def.innerType;
393
+ return inner ? unwrapZodSchema(inner) : field;
394
+ }
395
+ if (field instanceof z$1.ZodOptional) {
396
+ const inner = def.innerType;
397
+ return inner ? unwrapZodSchema(inner) : field;
398
+ }
399
+ if (field instanceof z$1.ZodNullable) {
400
+ const inner = def.innerType;
401
+ return inner ? unwrapZodSchema(inner) : field;
402
+ }
403
+ return field;
404
+ }
405
+
187
406
  //#endregion
188
407
  //#region src/cli/options/options.builder.ts
189
408
  /**
@@ -304,15 +523,14 @@ var OptionsBuilder = class {
304
523
  this.registerGenericOption(ctx);
305
524
  }
306
525
  /**
307
- * Registers a number option with numeric parsing.
526
+ * Registers a number option with numeric parsing and validation.
308
527
  *
309
528
  * @param ctx - The option context
310
529
  */
311
530
  registerNumberOption(ctx) {
312
531
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
313
- const parser = this.createNumberParser(rawField, optionName);
314
- const wrappedParser = this.wrapParser(parser);
315
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
532
+ const validator = createNumberValidator(rawField, optionName);
533
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
316
534
  }
317
535
  /**
318
536
  * Registers an enum option with choice validation.
@@ -320,111 +538,46 @@ var OptionsBuilder = class {
320
538
  * @param ctx - The option context
321
539
  */
322
540
  registerEnumOption(ctx) {
323
- const { command, rawField, field, optionFlag, optionName, description, parsedDefault } = ctx;
324
- const choices = this.getEnumChoices(field);
325
- const parser = this.createEnumParser(rawField, optionName, choices);
326
- const wrappedParser = this.wrapParser(parser);
541
+ const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
542
+ const choices = extractEnumValues(rawField) ?? [];
543
+ const validator = createEnumValidator(rawField, optionName, choices);
327
544
  const fullDescription = `${description}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}`;
328
- command.option(`${optionFlag} <${optionName}>`, fullDescription, wrappedParser, parsedDefault);
545
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator, parsedDefault);
329
546
  }
330
547
  /**
331
- * Registers a string option with validation.
548
+ * Registers a string option with validation.
332
549
  *
333
550
  * @param ctx - The option context
334
551
  */
335
552
  registerStringOption(ctx) {
336
553
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
337
- const parser = this.createStringParser(rawField);
338
- const wrappedParser = this.wrapParser(parser);
339
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
554
+ const validator = createStringValidator(rawField, optionName);
555
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
340
556
  }
341
557
  /**
342
558
  * Registers a generic option for other Zod types.
559
+ * Handles union types with literal values specially for better error messages.
343
560
  *
344
561
  * @param ctx - The option context
345
562
  */
346
563
  registerGenericOption(ctx) {
347
564
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
348
- const parser = this.createGenericParser(rawField);
349
- const wrappedParser = this.wrapParser(parser);
350
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
351
- }
352
- /**
353
- * Wraps a Result-returning parser into a Commander-compatible parser.
354
- *
355
- * Commander expects parsers to return values directly or throw on error.
356
- * This wrapper converts our Result-based parsers to that pattern.
357
- *
358
- * @param parser - The Result-based parser function
359
- * @returns A Commander-compatible parser function
360
- */
361
- wrapParser(parser) {
362
- return (input) => {
363
- const result = parser(input);
364
- if (result.isErr()) throw new Error(result.error);
365
- return result.value;
366
- };
367
- }
368
- /**
369
- * Creates a parser for number options.
370
- *
371
- * @template T - The expected return type
372
- * @param rawField - The raw Zod field
373
- * @param optionName - The option name for error messages
374
- * @returns A parser function that converts strings to numbers with validation
375
- */
376
- createNumberParser(rawField, optionName) {
377
- return (input) => {
378
- const num = Number(input);
379
- if (Number.isNaN(num)) return err(`Invalid number for --${optionName}: ${input}`);
380
- const result = parseSchema(rawField, num);
381
- if (result.isErr()) return err(result.error.message);
382
- return ok(result.value);
383
- };
384
- }
385
- /**
386
- * Creates a parser for enum options.
387
- *
388
- * @template T - The expected return type
389
- * @param rawField - The raw Zod field
390
- * @param optionName - The option name for error messages
391
- * @param choices - The valid enum choices
392
- * @returns A parser function that validates input against the enum choices
393
- */
394
- createEnumParser(rawField, optionName, choices) {
395
- return (input) => {
396
- const result = parseSchema(rawField, input);
397
- if (result.isErr()) return err(`Invalid value for --${optionName}: ${input}. Allowed: ${choices.join(", ")}`);
398
- return ok(result.value);
399
- };
400
- }
401
- /**
402
- * Creates a parser for string options.
403
- *
404
- * @template T - The expected return type
405
- * @param rawField - The raw Zod field
406
- * @returns A parser function that validates input against the schema
407
- */
408
- createStringParser(rawField) {
409
- return (input) => {
410
- const result = parseSchema(rawField, input);
411
- if (result.isErr()) return err(result.error.message);
412
- return ok(result.value);
413
- };
414
- }
415
- /**
416
- * Creates a generic parser using Zod validation.
417
- *
418
- * @template T - The expected return type
419
- * @param rawField - The raw Zod field
420
- * @returns A parser function that validates input against the schema
421
- */
422
- createGenericParser(rawField) {
423
- return (input) => {
424
- const result = parseSchema(rawField, input);
425
- if (result.isErr()) return err(result.error.message);
426
- return ok(result.value);
427
- };
565
+ const literalValues = extractLiteralValues(rawField);
566
+ if (literalValues) {
567
+ const validator$1 = createLiteralUnionValidator(rawField, optionName, literalValues);
568
+ const fullDescription = `${description} (choices: ${literalValues.map((v) => typeof v === "string" ? v : String(v)).join(", ")})`;
569
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator$1, parsedDefault);
570
+ return;
571
+ }
572
+ const enumValues = extractEnumValues(rawField);
573
+ if (enumValues) {
574
+ const validator$1 = createEnumValidator(rawField, optionName, enumValues);
575
+ const fullDescription = `${description} (choices: ${enumValues.join(", ")})`;
576
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator$1, parsedDefault);
577
+ return;
578
+ }
579
+ const validator = createGenericValidator(rawField, optionName);
580
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
428
581
  }
429
582
  /**
430
583
  * Gets the internal definition object from a Zod type.
@@ -469,15 +622,6 @@ var OptionsBuilder = class {
469
622
  const def = this.getInternalDef(rawField);
470
623
  return (def.innerType ?? def.schema) instanceof z$1.ZodBoolean;
471
624
  }
472
- /**
473
- * Extracts enum choices from a ZodEnum field.
474
- *
475
- * @param field - The ZodEnum field
476
- * @returns The array of valid enum choices
477
- */
478
- getEnumChoices(field) {
479
- return this.getInternalDef(field).values ?? [];
480
- }
481
625
  };
482
626
 
483
627
  //#endregion
@@ -1003,9 +1147,10 @@ function createBumpExecutionGroup() {
1003
1147
  */
1004
1148
  function shouldSkipBumpStrategy(ctx) {
1005
1149
  const { skipBump, releaseType, bumpStrategy } = ctx.config;
1150
+ const { selectedReleaseType, selectedBumpStrategy } = ctx.data;
1006
1151
  if (skipBump) return true;
1007
- if (releaseType) return true;
1008
- if (!bumpStrategy) return true;
1152
+ if (releaseType || selectedReleaseType) return true;
1153
+ if (!Boolean(bumpStrategy || selectedBumpStrategy)) return true;
1009
1154
  return false;
1010
1155
  }
1011
1156
  /**
@@ -1013,9 +1158,11 @@ function shouldSkipBumpStrategy(ctx) {
1013
1158
  */
1014
1159
  function getSkipReason(ctx) {
1015
1160
  const { skipBump, releaseType, bumpStrategy } = ctx.config;
1161
+ const { selectedReleaseType, selectedBumpStrategy } = ctx.data;
1016
1162
  if (skipBump) return "skipBump is enabled";
1017
- if (releaseType) return `releaseType already set to '${releaseType}'`;
1018
- if (!bumpStrategy) return "no bumpStrategy configured";
1163
+ if (releaseType) return `releaseType already set to '${releaseType}' (config)`;
1164
+ if (selectedReleaseType) return `releaseType already set to '${selectedReleaseType}' (data)`;
1165
+ if (!Boolean(bumpStrategy || selectedBumpStrategy)) return "no bumpStrategy configured";
1019
1166
  return "unknown reason";
1020
1167
  }
1021
1168
  function createDelegateBumpStrategyTask() {
@@ -1034,7 +1181,10 @@ function createDelegateBumpStrategyTask() {
1034
1181
  //#endregion
1035
1182
  //#region src/commands/release/tasks/determine-automatic-bump.task.ts
1036
1183
  function createDetermineAutomaticBump() {
1037
- return TaskBuilder.create("determine-automatic-bump").description("Automatically determines the version bump from commit messages").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => ctx.config.skipBump || ctx.config.bumpStrategy !== BUMP_STRATEGY_AUTO, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => {
1184
+ return TaskBuilder.create("determine-automatic-bump").description("Automatically determines the version bump from commit messages").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1185
+ const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1186
+ return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_AUTO;
1187
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => {
1038
1188
  logger.info("determine-automatic-bump");
1039
1189
  return FireflyOkAsync(ctx);
1040
1190
  }).build();
@@ -1042,17 +1192,69 @@ function createDetermineAutomaticBump() {
1042
1192
 
1043
1193
  //#endregion
1044
1194
  //#region src/commands/release/tasks/prompt-bump-strategy.task.ts
1195
+ const BUMP_STRATEGIES = [{
1196
+ label: "Automatic Bump",
1197
+ value: BUMP_STRATEGY_AUTO,
1198
+ hint: "Determines the next version based on conventional commits history"
1199
+ }, {
1200
+ label: "Manual Bump",
1201
+ value: BUMP_STRATEGY_MANUAL,
1202
+ hint: "Manually specify the next version"
1203
+ }];
1204
+ const VALID_STRATEGY_VALUES = BUMP_STRATEGIES.map((s) => s.value);
1205
+ /**
1206
+ * Validates that the selected strategy is one of the allowed values.
1207
+ *
1208
+ * @param strategy The selected version bump strategy.
1209
+ * @returns A FireflyResult indicating success or failure of validation.
1210
+ */
1211
+ function validateStrategy(strategy) {
1212
+ if (!VALID_STRATEGY_VALUES.includes(strategy)) return invalidErr({ message: `Invalid version bump strategy: ${strategy}` });
1213
+ return FireflyOk(strategy);
1214
+ }
1215
+ /**
1216
+ * Prompts the user to select a bump strategy using the logger's prompt API.
1217
+ */
1218
+ function promptBumpStrategy() {
1219
+ const defaultStrategy = BUMP_STRATEGIES[0];
1220
+ if (!defaultStrategy) return notFoundErrAsync({ message: "No default version bump strategy found" });
1221
+ logger.verbose("PromptBumpStrategyTask: Prompting user for version bump strategy.");
1222
+ return wrapPromise(logger.prompt("Select version bump strategy", {
1223
+ type: "select",
1224
+ options: BUMP_STRATEGIES,
1225
+ initial: defaultStrategy.value,
1226
+ cancel: "undefined"
1227
+ })).andThen((selected) => {
1228
+ if (!selected || selected === "") return failedErrAsync({ message: "Operation cancelled by user" });
1229
+ if (logger.level === LogLevels.verbose) logger.log("");
1230
+ if (logger.level !== LogLevels.verbose) logger.log("");
1231
+ const validationResult = validateStrategy(selected);
1232
+ if (validationResult.isErr()) return FireflyErrAsync(validationResult.error);
1233
+ logger.verbose(`PromptBumpStrategyTask: Selected version bump strategy: '${selected}'`);
1234
+ return FireflyOkAsync(selected);
1235
+ });
1236
+ }
1237
+ /**
1238
+ * Creates the Prompt Bump Strategy Task.
1239
+ *
1240
+ * This task prompts the user to select a version bump strategy (automatic or manual) and
1241
+ * stores it in the release context. It depends on `initialize-release-version` and will be
1242
+ * skipped when `skipBump` is enabled or a `bumpStrategy`/`releaseType` is already provided.
1243
+ *
1244
+ * This task:
1245
+ * 1. Prompts the user to select a bump strategy
1246
+ */
1045
1247
  function createPromptBumpStrategyTask() {
1046
- return TaskBuilder.create("prompt-bump-strategy").description("Prompts the user for a version bump strategy").dependsOn("initialize-release-version").skipWhenWithReason((ctx) => ctx.config.skipBump || Boolean(ctx.config.bumpStrategy) || Boolean(ctx.config.releaseType), "Skipped: skipBump enabled, or bumpStrategy/releaseType already specified").execute((ctx) => {
1047
- logger.info("prompt-bump-strategy");
1048
- return FireflyOkAsync(ctx);
1049
- }).build();
1248
+ return TaskBuilder.create("prompt-bump-strategy").description("Prompts the user for a version bump strategy").dependsOn("initialize-release-version").skipWhenWithReason((ctx) => ctx.config.skipBump || Boolean(ctx.config.bumpStrategy) || Boolean(ctx.config.releaseType), "Skipped: skipBump enabled, or bumpStrategy/releaseType already specified").execute((ctx) => promptBumpStrategy().andThen((strategy) => FireflyOkAsync(ctx.fork("selectedBumpStrategy", strategy)))).build();
1050
1249
  }
1051
1250
 
1052
1251
  //#endregion
1053
1252
  //#region src/commands/release/tasks/prompt-manual-bump.task.ts
1054
1253
  function createPromptManualVersionTask() {
1055
- return TaskBuilder.create("prompt-manual-version").description("Prompts the user for a manual version bump selections").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => ctx.config.skipBump || Boolean(ctx.config.bumpStrategy) || Boolean(ctx.config.releaseType), "Skipped: skipBump enabled, or bumpStrategy/releaseType already specified").execute((ctx) => {
1254
+ return TaskBuilder.create("prompt-manual-version").description("Prompts the user for a manual version bump selections").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1255
+ const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1256
+ return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_MANUAL;
1257
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'manual'").execute((ctx) => {
1056
1258
  logger.info("prompt-manual-version");
1057
1259
  return FireflyOkAsync(ctx);
1058
1260
  }).build();
@@ -1060,11 +1262,43 @@ function createPromptManualVersionTask() {
1060
1262
 
1061
1263
  //#endregion
1062
1264
  //#region src/commands/release/tasks/straight-version-bump.task.ts
1265
+ /**
1266
+ * Parses the current version from a raw string.
1267
+ *
1268
+ * @param currentVersionRaw - The raw string representing the current version
1269
+ * @returns A FireflyResult containing the parsed Version or a validation error
1270
+ */
1271
+ function parseCurrentVersion(currentVersionRaw) {
1272
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1273
+ return Version.from(currentVersionRaw);
1274
+ }
1275
+ /**
1276
+ * Builds the bump options from the release context.
1277
+ */
1278
+ function buildBumpOptionsFromContext(ctx) {
1279
+ const currentVersionResult = parseCurrentVersion(ctx.data.currentVersion);
1280
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1281
+ const releaseType = ctx.config.releaseType;
1282
+ if (releaseType === void 0) return invalidErrAsync({ message: "Release type is required for straight bump" });
1283
+ return FireflyOkAsync({
1284
+ currentVersion: currentVersionResult.value,
1285
+ releaseType,
1286
+ preReleaseID: ctx.config.preReleaseID,
1287
+ preReleaseBase: ctx.config.preReleaseBase
1288
+ });
1289
+ }
1290
+ /**
1291
+ * Performs the straight bump by delegating to the version bumper service.
1292
+ */
1293
+ function executeStraightVersionBump(ctx) {
1294
+ return buildBumpOptionsFromContext(ctx).andThen((options) => ctx.services.versionBumper.bump(options)).andThen((newVersion) => {
1295
+ const from = ctx.data.currentVersion || "unknown";
1296
+ logger.info(`Bumped version: ${colors.green(from)} -> ${colors.green(newVersion.raw)}`);
1297
+ return FireflyOkAsync(ctx.fork("nextVersion", newVersion.toString()));
1298
+ });
1299
+ }
1063
1300
  function createStraightVersionBump() {
1064
- return TaskBuilder.create("straight-version-bump").description("Performs a direct version bump based on the configured release type").dependsOn("initialize-release-version").skipWhenWithReason((ctx) => ctx.config.skipBump || ctx.config.releaseType === void 0, "Skipped: skipBump is enabled or no release type specified").execute((ctx) => {
1065
- logger.info("straight-version-bump");
1066
- return FireflyOkAsync(ctx);
1067
- }).build();
1301
+ return TaskBuilder.create("straight-version-bump").description("Performs a direct version bump based on the configured release type").dependsOn("initialize-release-version").skipWhenWithReason((ctx) => ctx.config.skipBump || ctx.config.releaseType === void 0, "Skipped: skipBump is enabled or no release type specified").execute((ctx) => executeStraightVersionBump(ctx)).build();
1068
1302
  }
1069
1303
 
1070
1304
  //#endregion
@@ -1118,7 +1352,7 @@ function getVersionFromPackageJson(ctx) {
1118
1352
  */
1119
1353
  function createInitializeReleaseVersion() {
1120
1354
  return TaskBuilder.create("initialize-release-version").description("Initialize current release version from package.json").dependsOn("prepare-release-config").execute((ctx) => getVersionFromPackageJson(ctx).andThen((currentVersion) => {
1121
- logger.info(`Current version is ${currentVersion}`);
1355
+ logger.info(`Current version is ${colors.green(currentVersion)}`);
1122
1356
  return FireflyOkAsync(ctx.fork("currentVersion", currentVersion));
1123
1357
  })).build();
1124
1358
  }
@@ -1130,6 +1364,12 @@ const SSH_REMOTE_REGEX = /git@[^:]+:([^/]+)\/([^/.]+)(?:\.git)?/;
1130
1364
  const SCOPED_PACKAGE_REGEX = /^@([^/]+)\/(.+)$/;
1131
1365
  const PRERELEASE_REGEX = /^\d+\.\d+\.\d+-([a-zA-Z]+)/;
1132
1366
  /**
1367
+ * Terminologies:
1368
+ *
1369
+ * Prepared: The value has been determined and set in the context.
1370
+ * Using: The value was explicitly provided in the config and is used as-is.
1371
+ */
1372
+ /**
1133
1373
  * Parses a git remote URL to extract owner and repository name.
1134
1374
  * Supports both HTTPS and SSH formats.
1135
1375
  *
@@ -1172,50 +1412,10 @@ function parsePackageName(packageName) {
1172
1412
  * extractPreReleaseId("1.0.0-beta.1") // "beta"
1173
1413
  * extractPreReleaseId("1.0.0") // undefined
1174
1414
  */
1175
- function extractPreReleaseId(version) {
1415
+ function extractPreReleaseID(version) {
1176
1416
  return version.match(PRERELEASE_REGEX)?.[1];
1177
1417
  }
1178
1418
  /**
1179
- * Hydrates the repository field from git remote URL.
1180
- *
1181
- * Behavior:
1182
- * - If not inside a git repository, resolves to undefined.
1183
- * - If inside a repository, detect the repository URL
1184
- * using a fall-through strategy (upstream remote → origin → first remote).
1185
- * - Parses the URL and returns "owner/repo" when possible.
1186
- */
1187
- function hydrateRepository(ctx) {
1188
- return ctx.services.git.inferRepositoryUrl().map((url) => {
1189
- if (!url) return null;
1190
- const parsed = parseGitRemoteUrl(url);
1191
- if (parsed) return `${parsed.owner}/${parsed.repo}`;
1192
- return null;
1193
- }).map((val) => val ?? void 0).andTee((repository) => logger.verbose(`PrepareReleaseConfigTask: Prepared repository: ${repository}`));
1194
- }
1195
- /**
1196
- * Hydrates name, scope, and preReleaseId from package.json.
1197
- *
1198
- * Behavior:
1199
- * - If package.json does not exist, returns all values as undefined.
1200
- * - If it exists, reads package.json and returns parsed results for name, scope and preReleaseId.
1201
- */
1202
- function hydrateFromPackageJson(ctx) {
1203
- return ctx.services.fs.exists("package.json").andThen((exists) => {
1204
- if (!exists) return FireflyOkAsync({
1205
- name: void 0,
1206
- scope: void 0,
1207
- preReleaseId: void 0
1208
- });
1209
- return ctx.services.packageJson.read("package.json").andThen((pkg) => zip3Async(hydrateNameFromPackageJson(ctx, pkg), hydrateScopeFromPackageJson(ctx, pkg), hydratePreReleaseIdFromPackageJson(ctx, pkg)).map(([name, scope, preReleaseId]) => {
1210
- const result = {};
1211
- if (name) result.name = name;
1212
- if (scope) result.scope = scope;
1213
- if (preReleaseId) result.preReleaseId = preReleaseId;
1214
- return result;
1215
- }));
1216
- });
1217
- }
1218
- /**
1219
1419
  * Hydrates the `name` field from package.json when not provided in config.
1220
1420
  *
1221
1421
  * Cases:
@@ -1257,31 +1457,92 @@ function hydrateScopeFromPackageJson(ctx, packageJson) {
1257
1457
  return FireflyOkAsync(void 0);
1258
1458
  }
1259
1459
  /**
1260
- * Hydrates the `preReleaseId` field from `package.json.version` when not provided.
1460
+ * Hydrates the `preReleaseID` field from `package.json.version` when not provided.
1261
1461
  *
1262
1462
  * Cases:
1263
- * 1. If preReleaseId is explicitly provided and not an empty string, it is used.
1463
+ * 1. If preReleaseID is explicitly provided and not an empty string, it is used.
1264
1464
  * 2. If not provided, and `package.json.version` contains a prerelease segment, the prerelease identifier will be extracted and returned.
1265
1465
  * 3. Otherwise the function defaults to "alpha".
1266
1466
  */
1267
- function hydratePreReleaseIdFromPackageJson(ctx, packageJson) {
1268
- if (ctx.config.preReleaseId !== void 0 && ctx.config.preReleaseId.trim() !== "") {
1269
- logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseId: "${ctx.config.preReleaseId}" as it is explicitly set`);
1270
- return FireflyOkAsync(ctx.config.preReleaseId);
1467
+ function hydratePreReleaseIDFromPackageJson(ctx, packageJson) {
1468
+ if (Object.hasOwn(ctx.config, "preReleaseID") && ctx.config.preReleaseID !== void 0) {
1469
+ logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseID: "${ctx.config.preReleaseID}" as it is explicitly set`);
1470
+ return FireflyOkAsync(ctx.config.preReleaseID);
1271
1471
  }
1272
1472
  if (packageJson.version) {
1273
1473
  const parsed = parse(packageJson.version);
1274
1474
  if (!parsed) return validationErrAsync({ message: `Invalid version format in package.json: ${packageJson.version}` });
1275
1475
  if (parsed.prerelease.length > 0 && typeof parsed.prerelease[0] === "string") {
1276
- const preReleaseId = extractPreReleaseId(packageJson.version);
1277
- logger.verbose(`PrepareReleaseConfigTask: Prepared preReleaseId from package.json: ${preReleaseId}`);
1278
- return FireflyOkAsync(preReleaseId);
1476
+ const preReleaseID = extractPreReleaseID(packageJson.version);
1477
+ logger.verbose(`PrepareReleaseConfigTask: Prepared preReleaseID from package.json: ${preReleaseID}`);
1478
+ return FireflyOkAsync(preReleaseID);
1279
1479
  }
1280
1480
  }
1281
- logger.verbose("PrepareReleaseConfigTask: No preReleaseId to prepare from package.json, defaulting to 'alpha'");
1481
+ logger.verbose("PrepareReleaseConfigTask: No preReleaseID to prepare from package.json, defaulting to 'alpha'");
1282
1482
  return FireflyOkAsync("alpha");
1283
1483
  }
1284
1484
  /**
1485
+ * Hydrates the `preReleaseBase` field.
1486
+ *
1487
+ * Behavior:
1488
+ * - If explicitly provided in config (key exists and value is not undefined) use as-is.
1489
+ * - Otherwise default to 0.
1490
+ *
1491
+ * Note: we do NOT infer `preReleaseBase` from package.json anymore.
1492
+ */
1493
+ function hydratePreReleaseBase(ctx) {
1494
+ const baseMaybe = ctx.config.preReleaseBase;
1495
+ if (Object.hasOwn(ctx.config, "preReleaseBase") && baseMaybe !== void 0) {
1496
+ logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseBase: "${ctx.config.preReleaseBase}" as it is explicitly set`);
1497
+ return FireflyOkAsync(baseMaybe);
1498
+ }
1499
+ logger.verbose("PrepareReleaseConfigTask: No preReleaseBase explicitly provided, defaulting to 0");
1500
+ return FireflyOkAsync(0);
1501
+ }
1502
+ /**
1503
+ * Hydrates name, scope, and preReleaseId from package.json.
1504
+ *
1505
+ * Behavior:
1506
+ * - If package.json does not exist, returns all values as undefined.
1507
+ * - If it exists, reads package.json and returns parsed results for name, scope and preReleaseId.
1508
+ * - If preReleaseBase is explicitly provided in config, it is used as-is, if not, it defaults to 0.
1509
+ */
1510
+ function hydrateFromPackageJson(ctx) {
1511
+ return ctx.services.fs.exists("package.json").andThen((exists) => {
1512
+ if (!exists) return FireflyOkAsync({
1513
+ name: void 0,
1514
+ scope: void 0,
1515
+ preReleaseID: void 0,
1516
+ preReleaseBase: void 0
1517
+ });
1518
+ return ctx.services.packageJson.read("package.json").andThen((pkg) => hydrateNameFromPackageJson(ctx, pkg).andThen((name) => hydrateScopeFromPackageJson(ctx, pkg).andThen((scope) => hydratePreReleaseIDFromPackageJson(ctx, pkg).andThen((preReleaseId) => hydratePreReleaseBase(ctx).map((preReleaseBase) => {
1519
+ const result = {};
1520
+ if (name) result.name = name;
1521
+ if (scope) result.scope = scope;
1522
+ if (preReleaseId) result.preReleaseId = preReleaseId;
1523
+ if (preReleaseBase !== void 0) result.preReleaseBase = preReleaseBase;
1524
+ return result;
1525
+ })))));
1526
+ });
1527
+ }
1528
+ /**
1529
+ * Hydrates the repository field from git remote URL.
1530
+ *
1531
+ * Behavior:
1532
+ * - If not inside a git repository, resolves to undefined.
1533
+ * - If inside a repository, detect the repository URL
1534
+ * using a fall-through strategy (upstream remote → origin → first remote).
1535
+ * - Parses the URL and returns "owner/repo" when possible.
1536
+ */
1537
+ function hydrateRepository(ctx) {
1538
+ return ctx.services.git.inferRepositoryUrl().andThen((url) => {
1539
+ if (!url) return validationErrAsync({ message: "Could not determine git remote URL to infer repository information" });
1540
+ const parsed = parseGitRemoteUrl(url);
1541
+ if (parsed) return FireflyOkAsync(`${parsed.owner}/${parsed.repo}`);
1542
+ return validationErrAsync({ message: `Could not parse repository information from git remote URL: ${url}` });
1543
+ }).andTee((repository) => logger.verbose(`PrepareReleaseConfigTask: Prepared repository: ${repository}`));
1544
+ }
1545
+ /**
1285
1546
  * Hydrates branch setting from git.
1286
1547
  *
1287
1548
  * Behavior:
@@ -1305,6 +1566,71 @@ function hydrateBranch(ctx) {
1305
1566
  });
1306
1567
  }
1307
1568
  /**
1569
+ * Hydrates repository and branch information from git.
1570
+ *
1571
+ * Behavior:
1572
+ * - If not inside a git repository, resolves both values to undefined.
1573
+ * - Otherwise it composes `hydrateRepository` and `hydrateBranch` and returns both values.
1574
+ */
1575
+ function hydrateFromGit(ctx) {
1576
+ return ctx.services.git.isInsideRepository().andThen((isRepo) => {
1577
+ if (!isRepo) return FireflyOkAsync({
1578
+ repository: void 0,
1579
+ branch: void 0
1580
+ });
1581
+ return hydrateRepository(ctx).andThen((repository) => hydrateBranch(ctx).map((branch) => {
1582
+ const result = {};
1583
+ if (repository) result.repository = repository;
1584
+ if (branch) result.branch = branch;
1585
+ return result;
1586
+ }));
1587
+ });
1588
+ }
1589
+ /**
1590
+ * Hydrates release flags (releaseLatest, releasePreRelease, releaseDraft).
1591
+ *
1592
+ * Behavior:
1593
+ * - If exactly one flag is explicitly set to true, use that and set others to false.
1594
+ * - If no flags are explicitly set, default to releaseLatest = true, others = false.
1595
+ * - Validation of exclusivity is handled by the schema, so we only need to determine defaults.
1596
+ */
1597
+ function hydrateReleaseFlags(ctx) {
1598
+ const { releaseLatest, releasePreRelease, releaseDraft } = ctx.config;
1599
+ const latestExplicit = releaseLatest === true;
1600
+ const preReleaseExplicit = releasePreRelease === true;
1601
+ const draftExplicit = releaseDraft === true;
1602
+ if (preReleaseExplicit) {
1603
+ logger.verbose(`PrepareReleaseConfigTask: Using "releasePreRelease" as it is explicitly set`);
1604
+ return FireflyOkAsync({
1605
+ releaseLatest: false,
1606
+ releasePreRelease: true,
1607
+ releaseDraft: false
1608
+ });
1609
+ }
1610
+ if (draftExplicit) {
1611
+ logger.verbose(`PrepareReleaseConfigTask: Using "releaseDraft" as it is explicitly set`);
1612
+ return FireflyOkAsync({
1613
+ releaseLatest: false,
1614
+ releasePreRelease: false,
1615
+ releaseDraft: true
1616
+ });
1617
+ }
1618
+ if (latestExplicit) {
1619
+ logger.verbose(`PrepareReleaseConfigTask: Using "releaseLatest" as it is explicitly set`);
1620
+ return FireflyOkAsync({
1621
+ releaseLatest: true,
1622
+ releasePreRelease: false,
1623
+ releaseDraft: false
1624
+ });
1625
+ }
1626
+ logger.verbose("PrepareReleaseConfigTask: Prepared releaseLatest as default since no flag was explicitly set");
1627
+ return FireflyOkAsync({
1628
+ releaseLatest: true,
1629
+ releasePreRelease: false,
1630
+ releaseDraft: false
1631
+ });
1632
+ }
1633
+ /**
1308
1634
  * Creates the Prepare Release Config Task.
1309
1635
  *
1310
1636
  * This task determines and hydrates configuration settings, by inferring values from the environment.
@@ -1314,18 +1640,26 @@ function hydrateBranch(ctx) {
1314
1640
  * 2. Extracts name and scope from package.json
1315
1641
  * 3. Extracts preReleaseId from package.json version
1316
1642
  * 4. Detects current git branch if not provided
1643
+ * 5. Determines release flags (latest, preRelease, draft) with proper defaults
1317
1644
  */
1318
1645
  function createPrepareReleaseConfigTask() {
1319
1646
  return TaskBuilder.create("prepare-release-config").description("Hydrate and prepare the release configuration").execute((ctx) => {
1320
1647
  const hydrated = {};
1321
- return zip3Async(hydrateRepository(ctx), hydrateFromPackageJson(ctx), hydrateBranch(ctx)).map(([repository, pkgData, branch]) => {
1322
- if (repository) hydrated.repository = repository;
1648
+ return hydrateFromGit(ctx).andThen((gitData) => {
1649
+ if (gitData.repository) hydrated.repository = gitData.repository;
1650
+ if (gitData.branch) hydrated.branch = gitData.branch;
1651
+ return hydrateFromPackageJson(ctx);
1652
+ }).andThen((pkgData) => {
1323
1653
  if (pkgData.name) hydrated.name = pkgData.name;
1324
1654
  if (pkgData.scope) hydrated.scope = pkgData.scope;
1325
- if (pkgData.preReleaseId) hydrated.preReleaseId = pkgData.preReleaseId;
1326
- if (branch) hydrated.branch = branch;
1327
- logger.verbose(`PrepareReleaseConfigTask: Hydrated config: ${JSON.stringify(hydrated)}`);
1328
- return ctx.fork("hydratedConfig", hydrated);
1655
+ if (pkgData.preReleaseID) hydrated.preReleaseID = pkgData.preReleaseID;
1656
+ if (pkgData.preReleaseBase !== void 0) hydrated.preReleaseBase = pkgData.preReleaseBase;
1657
+ return hydrateReleaseFlags(ctx);
1658
+ }).map((releaseFlags) => {
1659
+ hydrated.releaseLatest = releaseFlags.releaseLatest;
1660
+ hydrated.releasePreRelease = releaseFlags.releasePreRelease;
1661
+ hydrated.releaseDraft = releaseFlags.releaseDraft;
1662
+ return ctx.forkConfig(hydrated);
1329
1663
  });
1330
1664
  }).build();
1331
1665
  }
@@ -1492,7 +1826,7 @@ function validateReleaseFlagExclusivity(ctx) {
1492
1826
  "releasePreRelease",
1493
1827
  "releaseDraft"
1494
1828
  ];
1495
- if (flagNames.filter((k) => ctx.value[k]).length > 1) ctx.issues.push({
1829
+ if (flagNames.filter((k) => ctx.value[k] === true).length > 1) ctx.issues.push({
1496
1830
  code: "custom",
1497
1831
  message: `Only one of ${flagNames.join(", ")} can be set to true.`,
1498
1832
  input: ctx.value,
@@ -1538,14 +1872,15 @@ function validateSkipFlagCombinations(ctx) {
1538
1872
  const ReleaseConfigSchema = z$1.object({
1539
1873
  name: z$1.string().optional().describe("Unscoped project name. Auto-detected from package.json."),
1540
1874
  scope: z$1.string().optional().describe("Org/user scope without '@'. Auto-detected from package.json."),
1541
- base: z$1.string().default("").describe("Relative path from repository root to project root."),
1875
+ repository: z$1.string().optional().describe("GitHub repository in 'owner/repo' format."),
1876
+ base: z$1.string().optional().describe("Relative path from repository root to project root."),
1542
1877
  branch: z$1.string().optional().describe("Git branch to release from."),
1543
1878
  changelogPath: z$1.string().default("CHANGELOG.md").describe("Changelog file path, relative to project root."),
1544
1879
  bumpStrategy: BumpStrategySchema.describe("\"auto\" (from commits) or \"manual\" (user-specified)."),
1545
1880
  releaseType: ReleaseTypeSchema.optional().describe("The release type to bump."),
1546
- preReleaseId: z$1.string().optional().describe("Pre-release ID (e.g., \"alpha\", \"beta\")."),
1881
+ preReleaseID: z$1.string().optional().describe("Pre-release ID (e.g., \"alpha\", \"beta\")."),
1547
1882
  preReleaseBase: PreReleaseBaseSchema.describe("Starting version for pre-releases."),
1548
- releaseNotes: z$1.string().default("").describe("Custom release notes for changelog."),
1883
+ releaseNotes: z$1.string().optional().describe("Custom release notes for changelog."),
1549
1884
  commitMessage: z$1.string().default(COMMIT_MSG_TEMPLATE).describe("Commit message template with placeholders."),
1550
1885
  tagName: z$1.string().default(TAG_NAME_TEMPLATE).describe("Tag name template with placeholders."),
1551
1886
  skipBump: z$1.coerce.boolean().default(false).describe("Skip version bump step."),
@@ -1555,9 +1890,9 @@ const ReleaseConfigSchema = z$1.object({
1555
1890
  skipGit: z$1.coerce.boolean().default(false).describe("Skip all git-related steps."),
1556
1891
  skipPreflightCheck: z$1.coerce.boolean().default(false).describe("Skip preflight checks."),
1557
1892
  releaseTitle: z$1.string().default(RELEASE_TITLE_TEMPLATE).describe("GitHub release title with placeholders."),
1558
- releaseLatest: z$1.coerce.boolean().default(true).describe("Mark as latest release."),
1559
- releasePreRelease: z$1.coerce.boolean().default(false).describe("Mark as pre-release."),
1560
- releaseDraft: z$1.coerce.boolean().default(false).describe("Release as draft version.")
1893
+ releaseLatest: z$1.coerce.boolean().optional().describe("Mark as latest release."),
1894
+ releasePreRelease: z$1.coerce.boolean().optional().describe("Mark as pre-release."),
1895
+ releaseDraft: z$1.coerce.boolean().optional().describe("Release as draft version.")
1561
1896
  }).check((ctx) => {
1562
1897
  validateReleaseFlagExclusivity(ctx);
1563
1898
  validateSkipGitRedundancy(ctx);
@@ -1611,21 +1946,41 @@ function defineService(definition) {
1611
1946
  */
1612
1947
  const SERVICE_DEFINITIONS = {
1613
1948
  fs: defineService({ factory: async ({ basePath }) => {
1614
- const { createFileSystemService } = await import("./filesystem.service-9VHML130.js");
1949
+ const { createFileSystemService } = await import("./filesystem.service-Bsm3j1Bv.js");
1615
1950
  return createFileSystemService(basePath);
1616
1951
  } }),
1617
1952
  packageJson: defineService({
1618
1953
  dependencies: ["fs"],
1619
1954
  factory: async ({ getService }) => {
1620
1955
  const fs = await getService("fs");
1621
- const { createPackageJsonService } = await import("./package-json.service-DACeZzRg.js");
1956
+ const { createPackageJsonService } = await import("./package-json.service-d4pVcOFd.js");
1622
1957
  return createPackageJsonService(fs);
1623
1958
  }
1624
1959
  }),
1625
1960
  git: defineService({ factory: async ({ basePath }) => {
1626
- const { createGitService } = await import("./git.service-CACrfCW8.js");
1961
+ const { createGitService } = await import("./git.service-B3PC7qIe.js");
1627
1962
  return createGitService(basePath);
1628
- } })
1963
+ } }),
1964
+ versionBumper: defineService({ factory: async () => {
1965
+ const { createVersionBumperService } = await import("./version-bumper.service-ZBJ2jcLS.js");
1966
+ return createVersionBumperService();
1967
+ } }),
1968
+ versionStrategy: defineService({
1969
+ dependencies: ["versionBumper"],
1970
+ factory: async ({ getService }) => {
1971
+ const versionBumper = await getService("versionBumper");
1972
+ const { createVersionStrategyService } = await import("./version-strategy.service-BdpKRWLc.js");
1973
+ return createVersionStrategyService(versionBumper);
1974
+ }
1975
+ }),
1976
+ commitAnalysis: defineService({
1977
+ dependencies: ["git"],
1978
+ factory: async ({ getService }) => {
1979
+ const git = await getService("git");
1980
+ const { createCommitAnalysisService } = await import("./commit-analysis.service-B8W5aORO.js");
1981
+ return createCommitAnalysisService(git);
1982
+ }
1983
+ })
1629
1984
  };
1630
1985
  /**
1631
1986
  * Array of all service keys for iteration
@@ -1886,7 +2241,7 @@ function logGraphStatistics(stats) {
1886
2241
 
1887
2242
  //#endregion
1888
2243
  //#region src/commands/release/release.command.ts
1889
- const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git");
2244
+ const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git", "commitAnalysis", "versionBumper", "versionStrategy");
1890
2245
  const releaseCommand = createCommand({
1891
2246
  meta: {
1892
2247
  name: "release",
@@ -2206,6 +2561,30 @@ var ImmutableWorkflowContext = class ImmutableWorkflowContext {
2206
2561
  services: this.services
2207
2562
  });
2208
2563
  }
2564
+ /**
2565
+ * @example Merging hydrated config values
2566
+ * ```typescript
2567
+ * const updatedCtx = ctx.forkConfig({
2568
+ * repository: "owner/repo",
2569
+ * branch: "main"
2570
+ * });
2571
+ * ```
2572
+ */
2573
+ forkConfig(updates) {
2574
+ if (Object.keys(updates).length === 0) return this;
2575
+ const mergedConfig = {
2576
+ ...this.config,
2577
+ ...updates
2578
+ };
2579
+ const frozenConfig = Object.freeze(mergedConfig);
2580
+ return new ImmutableWorkflowContext({
2581
+ startTime: this.startTime,
2582
+ workspace: this.workspace,
2583
+ config: frozenConfig,
2584
+ data: this.#data,
2585
+ services: this.services
2586
+ });
2587
+ }
2209
2588
  has(key) {
2210
2589
  return key in this.#data;
2211
2590
  }
@@ -2296,6 +2675,8 @@ var WorkflowExecutor = class {
2296
2675
  handleExecutionFailure(args) {
2297
2676
  const { error, startTime, executedTaskIds, skippedTaskIds, initialContext } = args;
2298
2677
  const endTime = /* @__PURE__ */ new Date();
2678
+ if (DebugFlags.showRawError) logger.error(error);
2679
+ else logger.error(error.message);
2299
2680
  if (this.options.enableRollback && this.executedTasks.length > 0) {
2300
2681
  logger.verbose(`WorkflowExecutor: Attempting rollback of ${this.executedTasks.length} tasks`);
2301
2682
  return this.rollback(initialContext).andThen((rollbackSuccess) => {
@@ -2371,9 +2752,6 @@ var WorkflowExecutor = class {
2371
2752
  executionLists.executedTaskIds.push(currentTask.meta.id);
2372
2753
  this.executedTasks.push(currentTask);
2373
2754
  return this.executeTasksSequentially(remainingTasks, updatedContext, executionLists.executedTaskIds, executionLists.skippedTaskIds);
2374
- }).mapErr((error) => {
2375
- logger.error(error.message);
2376
- return error;
2377
2755
  });
2378
2756
  }
2379
2757
  rollback(context) {
@@ -3045,6 +3423,18 @@ async function resolveServiceWithContext(key, context) {
3045
3423
  /**
3046
3424
  * Creates a lazy proxy that defers async service instantiation until first access.
3047
3425
  *
3426
+ * IMPORTANT:
3427
+ * This proxy always defers instantiation and wraps calls in a ResultAsync chain
3428
+ * in order to safely await the real service instance before invoking the method.
3429
+ *
3430
+ * At runtime the proxy therefore returns ResultAsync even if the concrete service
3431
+ * method returns a synchronous Result.
3432
+ *
3433
+ * Because of this behavior, service interfaces should return FireflyAsyncResult<T>
3434
+ * from their public methods to avoid type mismatches and confusion. Implementations
3435
+ * that have purely synchronous behavior should return FireflyOkAsync / FireflyErrAsync
3436
+ * so they still conform to the async contract expected by the proxy.
3437
+ *
3048
3438
  * @template T - The service interface type
3049
3439
  * @param factory - Async function that creates the actual service instance
3050
3440
  * @returns A proxy that behaves like the service but instantiates lazily