fireflyy 4.0.0-dev.df58d2b → 4.0.0-dev.fc28987

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,80 +1,19 @@
1
- import { n as RuntimeEnv, t as logger } 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, 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, r as zip3Async, t as ensureNotAsync } from "./result.utilities-CdBL5noX.js";
4
- import { n as parseSchema, t as formatZodErrors } from "./schema.utilities-BOJqWEey.js";
1
+ import { t as RuntimeEnv } from "./main.js";
2
+ import { t as DebugFlags } from "./debug-flags-K3yK5B6O.js";
3
+ 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-DoAoYdfF.js";
4
+ import { n as wrapPromise, t as ensureNotAsync } from "./result.utilities-DXSJU70_.js";
5
+ import { t as logger } from "./logging-Bpk2RzGc.js";
6
+ import { t as Version } from "./version-DJuocyXy.js";
7
+ import { t as formatZodErrors } from "./schema.utilities-C1yimTtB.js";
8
+ import { Command, InvalidArgumentError } from "commander";
5
9
  import { LogLevels } from "consola";
6
10
  import { colors } from "consola/utils";
7
- import { Command } from "commander";
8
11
  import { loadConfig } from "c12";
9
12
  import { Result, ResultAsync, err, ok } from "neverthrow";
10
13
  import z$1 from "zod";
11
14
  import { parse } from "semver";
12
15
  import * as path from "path";
13
16
 
14
- //#region src/core/environment/debug-flags.ts
15
- /**
16
- * Debug flags are environment variables prefixed with `FIREFLY_DEBUG_` that
17
- * enable diagnostic features during development and troubleshooting.
18
- *
19
- * @example
20
- * ```typescript
21
- * if (DebugFlags.showRawError) {
22
- * logger.error(parseResult.error);
23
- * }
24
- * ```
25
- */
26
- var DebugFlags = class {
27
- constructor() {}
28
- /**
29
- * When enabled, displays raw Zod validation errors for configuration parsing.
30
- *
31
- * Useful for debugging configuration schema issues and understanding
32
- * why validation failed at a granular level.
33
- */
34
- static get showRawError() {
35
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_RAW_ERROR);
36
- }
37
- /**
38
- * When enabled, logs the loaded configuration file contents.
39
- *
40
- * Useful for debugging configuration loading and understanding
41
- * what values are being read from config files.
42
- */
43
- static get showFileConfig() {
44
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_FILE_CONFIG);
45
- }
46
- /**
47
- * When enabled, displays task graph statistics during release execution.
48
- *
49
- * Shows information about task dependencies, execution order,
50
- * and graph structure for debugging workflow issues.
51
- */
52
- static get showTaskGraphStats() {
53
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_TASK_GRAPH_STATS);
54
- }
55
- /**
56
- * When enabled, prevents truncation of release notes in GitHub CLI logs.
57
- *
58
- * By default, release notes are truncated in logs to avoid pollution.
59
- * Enable this flag to see full release notes content during debugging.
60
- */
61
- static get dontTruncateReleaseNotes() {
62
- return Boolean(process.env.FIREFLY_DEBUG_DONT_TRUNCATE_RELEASE_NOTES?.trim());
63
- }
64
- /**
65
- * When enabled, prevents redaction of sensitive GitHub CLI arguments in logs.
66
- *
67
- * By default, sensitive values (tokens, passwords, etc.) are redacted.
68
- * Enable this flag to see full argument values during debugging.
69
- *
70
- * WARNING: Use with caution as this may expose sensitive information.
71
- */
72
- static get dontRedactGithubCliArgs() {
73
- return Boolean(process.env.FIREFLY_DEBUG_DONT_REDACT_GITHUB_CLI_ARGS?.trim());
74
- }
75
- };
76
-
77
- //#endregion
78
17
  //#region src/cli/config/config.loader.ts
79
18
  /**
80
19
  * Loads and resolves Firefly configuration from files.
@@ -184,6 +123,223 @@ function camelToKebab(str) {
184
123
  return result.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/_/g, "-");
185
124
  }
186
125
 
126
+ //#endregion
127
+ //#region src/cli/options/options.validation.ts
128
+ /**
129
+ * Extracts enum values from a Zod schema (handles wrapped types like optional, default).
130
+ *
131
+ * @param schema - The Zod schema to extract enum values from
132
+ * @returns Array of string values if it's an enum, or null if not
133
+ */
134
+ function extractEnumValues(schema) {
135
+ const unwrapped = unwrapZodSchema(schema);
136
+ if (unwrapped instanceof z$1.ZodEnum) return getEnumValuesFromZodEnum(unwrapped);
137
+ if (unwrapped instanceof z$1.ZodUnion) {
138
+ const unionOptions = getZodDef(unwrapped).options;
139
+ if (!unionOptions) return null;
140
+ const values = [];
141
+ for (const option of unionOptions) {
142
+ if (typeof option === "string") continue;
143
+ const innerUnwrapped = unwrapZodSchema(option);
144
+ if (innerUnwrapped instanceof z$1.ZodEnum) {
145
+ const enumValues = getEnumValuesFromZodEnum(innerUnwrapped);
146
+ if (enumValues) values.push(...enumValues);
147
+ } else if (innerUnwrapped instanceof z$1.ZodLiteral) {
148
+ const literalValue = getZodDef(innerUnwrapped).value;
149
+ if (typeof literalValue === "string" && literalValue !== "") values.push(literalValue);
150
+ }
151
+ }
152
+ return values.length > 0 ? values : null;
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Extracts enum values from a ZodEnum type.
158
+ * Handles both Zod v4 (options array or entries object) and legacy (values array) structures.
159
+ *
160
+ * @param enumSchema - The ZodEnum schema
161
+ * @returns Array of string values, or null if not found
162
+ */
163
+ function getEnumValuesFromZodEnum(enumSchema) {
164
+ const schemaAsAny = enumSchema;
165
+ if (Array.isArray(schemaAsAny.options)) return schemaAsAny.options;
166
+ const def = getZodDef(enumSchema);
167
+ if (def.entries && typeof def.entries === "object") return Object.values(def.entries);
168
+ if (def.values && Array.isArray(def.values)) return def.values;
169
+ return null;
170
+ }
171
+ /**
172
+ * Extracts literal values from a Zod union schema (e.g., z.union([z.literal("0"), z.literal("1")])).
173
+ *
174
+ * @param schema - The Zod schema to extract literal values from
175
+ * @returns Array of literal values if it's a union of literals, or null if not
176
+ */
177
+ function extractLiteralValues(schema) {
178
+ const unwrapped = unwrapZodSchema(schema);
179
+ if (unwrapped instanceof z$1.ZodUnion) {
180
+ const options = getZodDef(unwrapped).options;
181
+ if (!options) return null;
182
+ const values = [];
183
+ for (const option of options) {
184
+ if (typeof option === "string" || typeof option === "number") continue;
185
+ const innerUnwrapped = unwrapZodSchema(option);
186
+ if (innerUnwrapped instanceof z$1.ZodLiteral) {
187
+ const value = getZodDef(innerUnwrapped).value;
188
+ if (typeof value === "string" || typeof value === "number") values.push(value);
189
+ }
190
+ }
191
+ return values.length > 0 ? values : null;
192
+ }
193
+ return null;
194
+ }
195
+ /**
196
+ * Creates a CLI option validator for enum fields.
197
+ *
198
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
199
+ * This is a boundary where exceptions are appropriate for the CLI framework.
200
+ *
201
+ * @param schema - The Zod schema for validation
202
+ * @param optionName - The CLI option name (for error messages)
203
+ * @param choices - The valid enum choices
204
+ * @returns A parser function that throws InvalidArgumentError on failure
205
+ */
206
+ function createEnumValidator(schema, optionName, choices) {
207
+ return (input) => {
208
+ const result = schema.safeParse(input);
209
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". Must be one of: ${choices.join(", ")}`);
210
+ return result.data;
211
+ };
212
+ }
213
+ /**
214
+ * Creates a CLI option validator for number fields.
215
+ *
216
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
217
+ * This is a boundary where exceptions are appropriate for the CLI framework.
218
+ *
219
+ * @param schema - The Zod schema for validation
220
+ * @param optionName - The CLI option name (for error messages)
221
+ * @returns A parser function that throws InvalidArgumentError on failure
222
+ */
223
+ function createNumberValidator(schema, optionName) {
224
+ return (input) => {
225
+ const num = Number(input);
226
+ if (Number.isNaN(num)) throw new InvalidArgumentError(`Invalid number for --${optionName}: "${input}". Expected a valid number.`);
227
+ const result = schema.safeParse(num);
228
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
229
+ return result.data;
230
+ };
231
+ }
232
+ /**
233
+ * Creates a CLI option validator for string fields.
234
+ *
235
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
236
+ * This is a boundary where exceptions are appropriate for the CLI framework.
237
+ *
238
+ * @param schema - The Zod schema for validation
239
+ * @param optionName - The CLI option name (for error messages)
240
+ * @returns A parser function that throws InvalidArgumentError on failure
241
+ */
242
+ function createStringValidator(schema, optionName) {
243
+ return (input) => {
244
+ const result = schema.safeParse(input);
245
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
246
+ return result.data;
247
+ };
248
+ }
249
+ /**
250
+ * Creates a CLI option validator for union types with literal values.
251
+ *
252
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
253
+ * This is a boundary where exceptions are appropriate for the CLI framework.
254
+ *
255
+ * @param schema - The Zod schema for validation
256
+ * @param optionName - The CLI option name (for error messages)
257
+ * @param values - The valid literal values
258
+ * @returns A parser function that throws InvalidArgumentError on failure
259
+ */
260
+ function createLiteralUnionValidator(schema, optionName, values) {
261
+ return (input) => {
262
+ const numericValues = values.filter((v) => typeof v === "number");
263
+ let parsedInput = input;
264
+ if (numericValues.length > 0 && !Number.isNaN(Number(input))) parsedInput = Number(input);
265
+ const result = schema.safeParse(parsedInput);
266
+ 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(", ")}`);
267
+ return result.data;
268
+ };
269
+ }
270
+ /**
271
+ * Creates a generic CLI option validator.
272
+ *
273
+ * Commander.js requires throwing InvalidArgumentError for validation errors.
274
+ * This is a boundary where exceptions are appropriate for the CLI framework.
275
+ *
276
+ * @param schema - The Zod schema for validation
277
+ * @param optionName - The CLI option name (for error messages)
278
+ * @returns A parser function that throws InvalidArgumentError on failure
279
+ */
280
+ function createGenericValidator(schema, optionName) {
281
+ return (input) => {
282
+ const result = schema.safeParse(input);
283
+ if (!result.success) throw new InvalidArgumentError(`Invalid value for --${optionName}: "${input}". ${formatZodErrorMessage(result.error)}`);
284
+ return result.data;
285
+ };
286
+ }
287
+ /**
288
+ * Formats a Zod error into a user-friendly message.
289
+ *
290
+ * @param error - The Zod error object
291
+ * @returns A formatted error message
292
+ */
293
+ function formatZodErrorMessage(error) {
294
+ const firstIssue = error.issues[0];
295
+ if (!firstIssue) return "Validation failed.";
296
+ switch (firstIssue.code) {
297
+ case "invalid_value": {
298
+ const issue = firstIssue;
299
+ if (issue.values) return `Expected one of: ${issue.values.join(", ")}`;
300
+ return firstIssue.message;
301
+ }
302
+ case "invalid_type": return `Expected ${firstIssue.expected}`;
303
+ case "too_small":
304
+ case "too_big": return firstIssue.message;
305
+ default: return firstIssue.message;
306
+ }
307
+ }
308
+ /**
309
+ * Gets the internal definition object from a Zod type.
310
+ * Handles both modern Zod v4 (_zod.def) and legacy (_def) structures.
311
+ *
312
+ * @param field - The Zod type to get the definition from
313
+ * @returns The internal definition object
314
+ */
315
+ function getZodDef(field) {
316
+ const zodContainer = field._zod;
317
+ if (zodContainer?.def) return zodContainer.def;
318
+ return field._def ?? {};
319
+ }
320
+ /**
321
+ * Unwraps Zod wrapper types (optional, default, etc.) to get the inner type.
322
+ *
323
+ * @param field - The Zod type to unwrap
324
+ * @returns The unwrapped inner type
325
+ */
326
+ function unwrapZodSchema(field) {
327
+ const def = getZodDef(field);
328
+ if (field instanceof z$1.ZodDefault) {
329
+ const inner = def.innerType;
330
+ return inner ? unwrapZodSchema(inner) : field;
331
+ }
332
+ if (field instanceof z$1.ZodOptional) {
333
+ const inner = def.innerType;
334
+ return inner ? unwrapZodSchema(inner) : field;
335
+ }
336
+ if (field instanceof z$1.ZodNullable) {
337
+ const inner = def.innerType;
338
+ return inner ? unwrapZodSchema(inner) : field;
339
+ }
340
+ return field;
341
+ }
342
+
187
343
  //#endregion
188
344
  //#region src/cli/options/options.builder.ts
189
345
  /**
@@ -304,15 +460,14 @@ var OptionsBuilder = class {
304
460
  this.registerGenericOption(ctx);
305
461
  }
306
462
  /**
307
- * Registers a number option with numeric parsing.
463
+ * Registers a number option with numeric parsing and validation.
308
464
  *
309
465
  * @param ctx - The option context
310
466
  */
311
467
  registerNumberOption(ctx) {
312
468
  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);
469
+ const validator = createNumberValidator(rawField, optionName);
470
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
316
471
  }
317
472
  /**
318
473
  * Registers an enum option with choice validation.
@@ -320,111 +475,46 @@ var OptionsBuilder = class {
320
475
  * @param ctx - The option context
321
476
  */
322
477
  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);
478
+ const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
479
+ const choices = extractEnumValues(rawField) ?? [];
480
+ const validator = createEnumValidator(rawField, optionName, choices);
327
481
  const fullDescription = `${description}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}`;
328
- command.option(`${optionFlag} <${optionName}>`, fullDescription, wrappedParser, parsedDefault);
482
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator, parsedDefault);
329
483
  }
330
484
  /**
331
- * Registers a string option with validation.
485
+ * Registers a string option with validation.
332
486
  *
333
487
  * @param ctx - The option context
334
488
  */
335
489
  registerStringOption(ctx) {
336
490
  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);
491
+ const validator = createStringValidator(rawField, optionName);
492
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
340
493
  }
341
494
  /**
342
495
  * Registers a generic option for other Zod types.
496
+ * Handles union types with literal values specially for better error messages.
343
497
  *
344
498
  * @param ctx - The option context
345
499
  */
346
500
  registerGenericOption(ctx) {
347
501
  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
- };
502
+ const literalValues = extractLiteralValues(rawField);
503
+ if (literalValues) {
504
+ const validator$1 = createLiteralUnionValidator(rawField, optionName, literalValues);
505
+ const fullDescription = `${description} (choices: ${literalValues.map((v) => typeof v === "string" ? v : String(v)).join(", ")})`;
506
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator$1, parsedDefault);
507
+ return;
508
+ }
509
+ const enumValues = extractEnumValues(rawField);
510
+ if (enumValues) {
511
+ const validator$1 = createEnumValidator(rawField, optionName, enumValues);
512
+ const fullDescription = `${description} (choices: ${enumValues.join(", ")})`;
513
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator$1, parsedDefault);
514
+ return;
515
+ }
516
+ const validator = createGenericValidator(rawField, optionName);
517
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
428
518
  }
429
519
  /**
430
520
  * Gets the internal definition object from a Zod type.
@@ -469,15 +559,6 @@ var OptionsBuilder = class {
469
559
  const def = this.getInternalDef(rawField);
470
560
  return (def.innerType ?? def.schema) instanceof z$1.ZodBoolean;
471
561
  }
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
562
  };
482
563
 
483
564
  //#endregion
@@ -806,7 +887,7 @@ var TaskBuilder = class TaskBuilder {
806
887
  //#region src/commands/release/tasks/bump-release-version.task.ts
807
888
  function createBumpReleaseVersion() {
808
889
  return TaskBuilder.create("bump-release-version").description("Applies the new version bump to relevant files").dependsOnAll("straight-version-bump", "determine-automatic-bump", "prompt-manual-version", "prompt-bump-strategy").skipWhenWithReason((ctx) => ctx.config.skipBump, "Skipped: skipBump is enabled").execute((ctx) => {
809
- logger.info("bump-release-version");
890
+ logger.info(`Next version to be released: ${colors.green(ctx.data.nextVersion)}`);
810
891
  return FireflyOkAsync(ctx);
811
892
  }).build();
812
893
  }
@@ -1028,22 +1109,60 @@ function createDelegateBumpStrategyTask() {
1028
1109
  shouldSkip,
1029
1110
  reason: shouldSkip ? getSkipReason(ctx) : void 0
1030
1111
  });
1031
- }).execute((ctx) => {
1032
- logger.info("delegate-bump-strategy");
1033
- return FireflyOkAsync(ctx);
1034
- }).build();
1112
+ }).execute((ctx) => FireflyOkAsync(ctx)).build();
1035
1113
  }
1036
1114
 
1037
1115
  //#endregion
1038
1116
  //#region src/commands/release/tasks/determine-automatic-bump.task.ts
1117
+ /**
1118
+ * Parses the current version from a raw string.
1119
+ *
1120
+ * @param currentVersionRaw - The raw string representing the current version
1121
+ * @returns A FireflyResult containing the parsed Version or a validation error
1122
+ */
1123
+ function parseCurrentVersion$2(currentVersionRaw) {
1124
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1125
+ return Version.from(currentVersionRaw);
1126
+ }
1127
+ /**
1128
+ * Logs the analysis recommendation with formatting.
1129
+ *
1130
+ * @param reason - The reason string from the recommendation
1131
+ */
1132
+ function logRecommendation(reason) {
1133
+ if (!reason) return;
1134
+ if (reason.startsWith("Analysis found:")) {
1135
+ const prefix = "Analysis found:";
1136
+ const details = reason.slice(15).trim();
1137
+ logger.info(`${prefix} ${colors.green(details)}`);
1138
+ } else logger.info(reason);
1139
+ }
1140
+ /**
1141
+ * Performs the automatic bump by analyzing commits and resolving version strategy.
1142
+ */
1143
+ function executeAutomaticBump(ctx) {
1144
+ const currentVersionResult = parseCurrentVersion$2(ctx.data.currentVersion);
1145
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1146
+ const currentVersion = currentVersionResult.value;
1147
+ return ctx.services.commitAnalysis.analyzeForVersion().andThen((recommendation) => {
1148
+ logRecommendation(recommendation.reason);
1149
+ const options = {
1150
+ currentVersion,
1151
+ preReleaseId: ctx.config.preReleaseId,
1152
+ preReleaseBase: ctx.config.preReleaseBase
1153
+ };
1154
+ return ctx.services.versionStrategy.resolveVersion(options, recommendation);
1155
+ }).andThen((newVersion) => {
1156
+ const from = ctx.data.currentVersion || "unknown";
1157
+ logger.info(`Determined version: ${colors.green(from)} -> ${colors.green(newVersion.raw)}`);
1158
+ return FireflyOkAsync(ctx.fork("nextVersion", newVersion.toString()));
1159
+ });
1160
+ }
1039
1161
  function createDetermineAutomaticBump() {
1040
1162
  return TaskBuilder.create("determine-automatic-bump").description("Automatically determines the version bump from commit messages").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1041
1163
  const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1042
1164
  return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_AUTO;
1043
- }, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => {
1044
- logger.info("determine-automatic-bump");
1045
- return FireflyOkAsync(ctx);
1046
- }).build();
1165
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => executeAutomaticBump(ctx)).build();
1047
1166
  }
1048
1167
 
1049
1168
  //#endregion
@@ -1075,46 +1194,123 @@ function promptBumpStrategy() {
1075
1194
  const defaultStrategy = BUMP_STRATEGIES[0];
1076
1195
  if (!defaultStrategy) return notFoundErrAsync({ message: "No default version bump strategy found" });
1077
1196
  logger.verbose("PromptBumpStrategyTask: Prompting user for version bump strategy.");
1078
- const prompt = logger.prompt("Select version bump strategy", {
1197
+ return wrapPromise(logger.prompt("Select version bump strategy", {
1079
1198
  type: "select",
1080
1199
  options: BUMP_STRATEGIES,
1081
1200
  initial: defaultStrategy.value,
1082
- cancel: "reject"
1083
- });
1084
- if (logger.level !== LogLevels.verbose) logger.log("");
1085
- return wrapPromise(prompt).andTee(() => {
1201
+ cancel: "undefined"
1202
+ })).andThen((selected) => {
1203
+ if (!selected || selected === "") return failedErrAsync({ message: "Operation cancelled by user" });
1086
1204
  if (logger.level === LogLevels.verbose) logger.log("");
1087
- }).andThen((selected) => {
1088
- if (!selected || selected === "") return invalidErrAsync({ message: "No version bump strategy selected" });
1205
+ if (logger.level !== LogLevels.verbose) logger.log("");
1089
1206
  const validationResult = validateStrategy(selected);
1090
1207
  if (validationResult.isErr()) return FireflyErrAsync(validationResult.error);
1091
1208
  logger.verbose(`PromptBumpStrategyTask: Selected version bump strategy: '${selected}'`);
1092
1209
  return FireflyOkAsync(selected);
1093
1210
  });
1094
1211
  }
1212
+ /**
1213
+ * Creates the Prompt Bump Strategy Task.
1214
+ *
1215
+ * This task prompts the user to select a version bump strategy (automatic or manual) and
1216
+ * stores it in the release context. It depends on `initialize-release-version` and will be
1217
+ * skipped when `skipBump` is enabled or a `bumpStrategy`/`releaseType` is already provided.
1218
+ *
1219
+ * This task:
1220
+ * 1. Prompts the user to select a bump strategy
1221
+ */
1095
1222
  function createPromptBumpStrategyTask() {
1096
1223
  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();
1097
1224
  }
1098
1225
 
1099
1226
  //#endregion
1100
1227
  //#region src/commands/release/tasks/prompt-manual-bump.task.ts
1228
+ /**
1229
+ * Parses the current version from a raw string.
1230
+ *
1231
+ * @param currentVersionRaw - The raw string representing the current version
1232
+ * @returns A FireflyResult containing the parsed Version or a validation error
1233
+ */
1234
+ function parseCurrentVersion$1(currentVersionRaw) {
1235
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1236
+ return Version.from(currentVersionRaw);
1237
+ }
1238
+ /**
1239
+ * Generates version choices and prompts the user to select one.
1240
+ */
1241
+ function promptForManualVersion(ctx) {
1242
+ const currentVersionResult = parseCurrentVersion$1(ctx.data.currentVersion);
1243
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1244
+ const currentVersion = currentVersionResult.value;
1245
+ logger.verbose("PromptManualVersionTask: Generating version choices...");
1246
+ return ctx.services.versionStrategy.generateChoices({
1247
+ currentVersion,
1248
+ preReleaseId: ctx.config.preReleaseId,
1249
+ preReleaseBase: ctx.config.preReleaseBase
1250
+ }).andThen((choices) => {
1251
+ if (!choices || choices.length === 0) return validationErrAsync({ message: "No version choices available" });
1252
+ logger.verbose(`PromptManualVersionTask: Generated ${choices.length} version choices.`);
1253
+ const defaultChoice = choices[0];
1254
+ return wrapPromise(logger.prompt("Select version to release", {
1255
+ type: "select",
1256
+ options: choices,
1257
+ initial: defaultChoice?.value,
1258
+ cancel: "undefined"
1259
+ })).andThen((selected) => {
1260
+ if (!selected || selected === "") return failedErrAsync({ message: "Operation cancelled by user" });
1261
+ if (logger.level === LogLevels.verbose) logger.log("");
1262
+ if (logger.level !== LogLevels.verbose) logger.log("");
1263
+ logger.verbose(`PromptManualVersionTask: Selected version: '${selected}'`);
1264
+ return FireflyOkAsync(selected);
1265
+ });
1266
+ });
1267
+ }
1101
1268
  function createPromptManualVersionTask() {
1102
- return TaskBuilder.create("prompt-manual-version").description("Prompts the user for a manual version bump selections").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1269
+ return TaskBuilder.create("prompt-manual-version").description("Prompts the user to manually select the next version").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1103
1270
  const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1104
1271
  return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_MANUAL;
1105
- }, "Skipped: skipBump enabled or bumpStrategy is not 'manual'").execute((ctx) => {
1106
- logger.info("prompt-manual-version");
1107
- return FireflyOkAsync(ctx);
1108
- }).build();
1272
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'manual'").execute((ctx) => promptForManualVersion(ctx).andThen((selectedVersion) => FireflyOkAsync(ctx.fork("nextVersion", selectedVersion)))).build();
1109
1273
  }
1110
1274
 
1111
1275
  //#endregion
1112
1276
  //#region src/commands/release/tasks/straight-version-bump.task.ts
1277
+ /**
1278
+ * Parses the current version from a raw string.
1279
+ *
1280
+ * @param currentVersionRaw - The raw string representing the current version
1281
+ * @returns A FireflyResult containing the parsed Version or a validation error
1282
+ */
1283
+ function parseCurrentVersion(currentVersionRaw) {
1284
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1285
+ return Version.from(currentVersionRaw);
1286
+ }
1287
+ /**
1288
+ * Builds the bump options from the release context.
1289
+ */
1290
+ function buildBumpOptionsFromContext(ctx) {
1291
+ const currentVersionResult = parseCurrentVersion(ctx.data.currentVersion);
1292
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1293
+ const releaseType = ctx.config.releaseType;
1294
+ if (releaseType === void 0) return invalidErrAsync({ message: "Release type is required for straight bump" });
1295
+ return FireflyOkAsync({
1296
+ currentVersion: currentVersionResult.value,
1297
+ releaseType,
1298
+ preReleaseId: ctx.config.preReleaseId,
1299
+ preReleaseBase: ctx.config.preReleaseBase
1300
+ });
1301
+ }
1302
+ /**
1303
+ * Performs the straight bump by delegating to the version bumper service.
1304
+ */
1305
+ function executeStraightVersionBump(ctx) {
1306
+ return buildBumpOptionsFromContext(ctx).andThen((options) => ctx.services.versionBumper.bump(options)).andThen((newVersion) => {
1307
+ const from = ctx.data.currentVersion || "unknown";
1308
+ logger.info(`Bumped version: ${colors.green(from)} -> ${colors.green(newVersion.raw)}`);
1309
+ return FireflyOkAsync(ctx.fork("nextVersion", newVersion.toString()));
1310
+ });
1311
+ }
1113
1312
  function createStraightVersionBump() {
1114
- 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) => {
1115
- logger.info("straight-version-bump");
1116
- return FireflyOkAsync(ctx);
1117
- }).build();
1313
+ 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();
1118
1314
  }
1119
1315
 
1120
1316
  //#endregion
@@ -1168,7 +1364,7 @@ function getVersionFromPackageJson(ctx) {
1168
1364
  */
1169
1365
  function createInitializeReleaseVersion() {
1170
1366
  return TaskBuilder.create("initialize-release-version").description("Initialize current release version from package.json").dependsOn("prepare-release-config").execute((ctx) => getVersionFromPackageJson(ctx).andThen((currentVersion) => {
1171
- logger.info(`Current version is ${currentVersion}`);
1367
+ logger.info(`Current version is ${colors.green(currentVersion)}`);
1172
1368
  return FireflyOkAsync(ctx.fork("currentVersion", currentVersion));
1173
1369
  })).build();
1174
1370
  }
@@ -1180,6 +1376,12 @@ const SSH_REMOTE_REGEX = /git@[^:]+:([^/]+)\/([^/.]+)(?:\.git)?/;
1180
1376
  const SCOPED_PACKAGE_REGEX = /^@([^/]+)\/(.+)$/;
1181
1377
  const PRERELEASE_REGEX = /^\d+\.\d+\.\d+-([a-zA-Z]+)/;
1182
1378
  /**
1379
+ * Terminologies:
1380
+ *
1381
+ * Prepared: The value has been determined and set in the context.
1382
+ * Using: The value was explicitly provided in the config and is used as-is.
1383
+ */
1384
+ /**
1183
1385
  * Parses a git remote URL to extract owner and repository name.
1184
1386
  * Supports both HTTPS and SSH formats.
1185
1387
  *
@@ -1226,46 +1428,6 @@ function extractPreReleaseId(version) {
1226
1428
  return version.match(PRERELEASE_REGEX)?.[1];
1227
1429
  }
1228
1430
  /**
1229
- * Hydrates the repository field from git remote URL.
1230
- *
1231
- * Behavior:
1232
- * - If not inside a git repository, resolves to undefined.
1233
- * - If inside a repository, detect the repository URL
1234
- * using a fall-through strategy (upstream remote → origin → first remote).
1235
- * - Parses the URL and returns "owner/repo" when possible.
1236
- */
1237
- function hydrateRepository(ctx) {
1238
- return ctx.services.git.inferRepositoryUrl().map((url) => {
1239
- if (!url) return null;
1240
- const parsed = parseGitRemoteUrl(url);
1241
- if (parsed) return `${parsed.owner}/${parsed.repo}`;
1242
- return null;
1243
- }).map((val) => val ?? void 0).andTee((repository) => logger.verbose(`PrepareReleaseConfigTask: Prepared repository: ${repository}`));
1244
- }
1245
- /**
1246
- * Hydrates name, scope, and preReleaseId from package.json.
1247
- *
1248
- * Behavior:
1249
- * - If package.json does not exist, returns all values as undefined.
1250
- * - If it exists, reads package.json and returns parsed results for name, scope and preReleaseId.
1251
- */
1252
- function hydrateFromPackageJson(ctx) {
1253
- return ctx.services.fs.exists("package.json").andThen((exists) => {
1254
- if (!exists) return FireflyOkAsync({
1255
- name: void 0,
1256
- scope: void 0,
1257
- preReleaseId: void 0
1258
- });
1259
- return ctx.services.packageJson.read("package.json").andThen((pkg) => zip3Async(hydrateNameFromPackageJson(ctx, pkg), hydrateScopeFromPackageJson(ctx, pkg), hydratePreReleaseIdFromPackageJson(ctx, pkg)).map(([name, scope, preReleaseId]) => {
1260
- const result = {};
1261
- if (name) result.name = name;
1262
- if (scope) result.scope = scope;
1263
- if (preReleaseId) result.preReleaseId = preReleaseId;
1264
- return result;
1265
- }));
1266
- });
1267
- }
1268
- /**
1269
1431
  * Hydrates the `name` field from package.json when not provided in config.
1270
1432
  *
1271
1433
  * Cases:
@@ -1315,7 +1477,7 @@ function hydrateScopeFromPackageJson(ctx, packageJson) {
1315
1477
  * 3. Otherwise the function defaults to "alpha".
1316
1478
  */
1317
1479
  function hydratePreReleaseIdFromPackageJson(ctx, packageJson) {
1318
- if (ctx.config.preReleaseId !== void 0 && ctx.config.preReleaseId.trim() !== "") {
1480
+ if (Object.hasOwn(ctx.config, "preReleaseId") && ctx.config.preReleaseId !== void 0) {
1319
1481
  logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseId: "${ctx.config.preReleaseId}" as it is explicitly set`);
1320
1482
  return FireflyOkAsync(ctx.config.preReleaseId);
1321
1483
  }
@@ -1332,6 +1494,67 @@ function hydratePreReleaseIdFromPackageJson(ctx, packageJson) {
1332
1494
  return FireflyOkAsync("alpha");
1333
1495
  }
1334
1496
  /**
1497
+ * Hydrates the `preReleaseBase` field.
1498
+ *
1499
+ * Behavior:
1500
+ * - If explicitly provided in config (key exists and value is not undefined) use as-is.
1501
+ * - Otherwise default to 0.
1502
+ *
1503
+ * Note: we do NOT infer `preReleaseBase` from package.json anymore.
1504
+ */
1505
+ function hydratePreReleaseBase(ctx) {
1506
+ const baseMaybe = ctx.config.preReleaseBase;
1507
+ if (Object.hasOwn(ctx.config, "preReleaseBase") && baseMaybe !== void 0) {
1508
+ logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseBase: "${ctx.config.preReleaseBase}" as it is explicitly set`);
1509
+ return FireflyOkAsync(baseMaybe);
1510
+ }
1511
+ logger.verbose("PrepareReleaseConfigTask: No preReleaseBase explicitly provided, defaulting to 0");
1512
+ return FireflyOkAsync(0);
1513
+ }
1514
+ /**
1515
+ * Hydrates name, scope, and preReleaseId from package.json.
1516
+ *
1517
+ * Behavior:
1518
+ * - If package.json does not exist, returns all values as undefined.
1519
+ * - If it exists, reads package.json and returns parsed results for name, scope and preReleaseId.
1520
+ * - If preReleaseBase is explicitly provided in config, it is used as-is, if not, it defaults to 0.
1521
+ */
1522
+ function hydrateFromPackageJson(ctx) {
1523
+ return ctx.services.fs.exists("package.json").andThen((exists) => {
1524
+ if (!exists) return FireflyOkAsync({
1525
+ name: void 0,
1526
+ scope: void 0,
1527
+ preReleaseId: void 0,
1528
+ preReleaseBase: void 0
1529
+ });
1530
+ 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) => {
1531
+ const result = {};
1532
+ if (name) result.name = name;
1533
+ if (scope) result.scope = scope;
1534
+ if (preReleaseId) result.preReleaseId = preReleaseId;
1535
+ if (preReleaseBase !== void 0) result.preReleaseBase = preReleaseBase;
1536
+ return result;
1537
+ })))));
1538
+ });
1539
+ }
1540
+ /**
1541
+ * Hydrates the repository field from git remote URL.
1542
+ *
1543
+ * Behavior:
1544
+ * - If not inside a git repository, resolves to undefined.
1545
+ * - If inside a repository, detect the repository URL
1546
+ * using a fall-through strategy (upstream remote → origin → first remote).
1547
+ * - Parses the URL and returns "owner/repo" when possible.
1548
+ */
1549
+ function hydrateRepository(ctx) {
1550
+ return ctx.services.git.inferRepositoryUrl().andThen((url) => {
1551
+ if (!url) return validationErrAsync({ message: "Could not determine git remote URL to infer repository information" });
1552
+ const parsed = parseGitRemoteUrl(url);
1553
+ if (parsed) return FireflyOkAsync(`${parsed.owner}/${parsed.repo}`);
1554
+ return validationErrAsync({ message: `Could not parse repository information from git remote URL: ${url}` });
1555
+ }).andTee((repository) => logger.verbose(`PrepareReleaseConfigTask: Prepared repository: ${repository}`));
1556
+ }
1557
+ /**
1335
1558
  * Hydrates branch setting from git.
1336
1559
  *
1337
1560
  * Behavior:
@@ -1355,6 +1578,71 @@ function hydrateBranch(ctx) {
1355
1578
  });
1356
1579
  }
1357
1580
  /**
1581
+ * Hydrates repository and branch information from git.
1582
+ *
1583
+ * Behavior:
1584
+ * - If not inside a git repository, resolves both values to undefined.
1585
+ * - Otherwise it composes `hydrateRepository` and `hydrateBranch` and returns both values.
1586
+ */
1587
+ function hydrateFromGit(ctx) {
1588
+ return ctx.services.git.isInsideRepository().andThen((isRepo) => {
1589
+ if (!isRepo) return FireflyOkAsync({
1590
+ repository: void 0,
1591
+ branch: void 0
1592
+ });
1593
+ return hydrateRepository(ctx).andThen((repository) => hydrateBranch(ctx).map((branch) => {
1594
+ const result = {};
1595
+ if (repository) result.repository = repository;
1596
+ if (branch) result.branch = branch;
1597
+ return result;
1598
+ }));
1599
+ });
1600
+ }
1601
+ /**
1602
+ * Hydrates release flags (releaseLatest, releasePreRelease, releaseDraft).
1603
+ *
1604
+ * Behavior:
1605
+ * - If exactly one flag is explicitly set to true, use that and set others to false.
1606
+ * - If no flags are explicitly set, default to releaseLatest = true, others = false.
1607
+ * - Validation of exclusivity is handled by the schema, so we only need to determine defaults.
1608
+ */
1609
+ function hydrateReleaseFlags(ctx) {
1610
+ const { releaseLatest, releasePreRelease, releaseDraft } = ctx.config;
1611
+ const latestExplicit = releaseLatest === true;
1612
+ const preReleaseExplicit = releasePreRelease === true;
1613
+ const draftExplicit = releaseDraft === true;
1614
+ if (preReleaseExplicit) {
1615
+ logger.verbose(`PrepareReleaseConfigTask: Using "releasePreRelease" as it is explicitly set`);
1616
+ return FireflyOkAsync({
1617
+ releaseLatest: false,
1618
+ releasePreRelease: true,
1619
+ releaseDraft: false
1620
+ });
1621
+ }
1622
+ if (draftExplicit) {
1623
+ logger.verbose(`PrepareReleaseConfigTask: Using "releaseDraft" as it is explicitly set`);
1624
+ return FireflyOkAsync({
1625
+ releaseLatest: false,
1626
+ releasePreRelease: false,
1627
+ releaseDraft: true
1628
+ });
1629
+ }
1630
+ if (latestExplicit) {
1631
+ logger.verbose(`PrepareReleaseConfigTask: Using "releaseLatest" as it is explicitly set`);
1632
+ return FireflyOkAsync({
1633
+ releaseLatest: true,
1634
+ releasePreRelease: false,
1635
+ releaseDraft: false
1636
+ });
1637
+ }
1638
+ logger.verbose("PrepareReleaseConfigTask: Prepared releaseLatest as default since no flag was explicitly set");
1639
+ return FireflyOkAsync({
1640
+ releaseLatest: true,
1641
+ releasePreRelease: false,
1642
+ releaseDraft: false
1643
+ });
1644
+ }
1645
+ /**
1358
1646
  * Creates the Prepare Release Config Task.
1359
1647
  *
1360
1648
  * This task determines and hydrates configuration settings, by inferring values from the environment.
@@ -1364,18 +1652,26 @@ function hydrateBranch(ctx) {
1364
1652
  * 2. Extracts name and scope from package.json
1365
1653
  * 3. Extracts preReleaseId from package.json version
1366
1654
  * 4. Detects current git branch if not provided
1655
+ * 5. Determines release flags (latest, preRelease, draft) with proper defaults
1367
1656
  */
1368
1657
  function createPrepareReleaseConfigTask() {
1369
1658
  return TaskBuilder.create("prepare-release-config").description("Hydrate and prepare the release configuration").execute((ctx) => {
1370
1659
  const hydrated = {};
1371
- return zip3Async(hydrateRepository(ctx), hydrateFromPackageJson(ctx), hydrateBranch(ctx)).map(([repository, pkgData, branch]) => {
1372
- if (repository) hydrated.repository = repository;
1660
+ return hydrateFromGit(ctx).andThen((gitData) => {
1661
+ if (gitData.repository) hydrated.repository = gitData.repository;
1662
+ if (gitData.branch) hydrated.branch = gitData.branch;
1663
+ return hydrateFromPackageJson(ctx);
1664
+ }).andThen((pkgData) => {
1373
1665
  if (pkgData.name) hydrated.name = pkgData.name;
1374
1666
  if (pkgData.scope) hydrated.scope = pkgData.scope;
1375
1667
  if (pkgData.preReleaseId) hydrated.preReleaseId = pkgData.preReleaseId;
1376
- if (branch) hydrated.branch = branch;
1377
- logger.verbose(`PrepareReleaseConfigTask: Hydrated config: ${JSON.stringify(hydrated)}`);
1378
- return ctx.fork("hydratedConfig", hydrated);
1668
+ if (pkgData.preReleaseBase !== void 0) hydrated.preReleaseBase = pkgData.preReleaseBase;
1669
+ return hydrateReleaseFlags(ctx);
1670
+ }).map((releaseFlags) => {
1671
+ hydrated.releaseLatest = releaseFlags.releaseLatest;
1672
+ hydrated.releasePreRelease = releaseFlags.releasePreRelease;
1673
+ hydrated.releaseDraft = releaseFlags.releaseDraft;
1674
+ return ctx.forkConfig(hydrated);
1379
1675
  });
1380
1676
  }).build();
1381
1677
  }
@@ -1542,7 +1838,7 @@ function validateReleaseFlagExclusivity(ctx) {
1542
1838
  "releasePreRelease",
1543
1839
  "releaseDraft"
1544
1840
  ];
1545
- if (flagNames.filter((k) => ctx.value[k]).length > 1) ctx.issues.push({
1841
+ if (flagNames.filter((k) => ctx.value[k] === true).length > 1) ctx.issues.push({
1546
1842
  code: "custom",
1547
1843
  message: `Only one of ${flagNames.join(", ")} can be set to true.`,
1548
1844
  input: ctx.value,
@@ -1588,14 +1884,15 @@ function validateSkipFlagCombinations(ctx) {
1588
1884
  const ReleaseConfigSchema = z$1.object({
1589
1885
  name: z$1.string().optional().describe("Unscoped project name. Auto-detected from package.json."),
1590
1886
  scope: z$1.string().optional().describe("Org/user scope without '@'. Auto-detected from package.json."),
1591
- base: z$1.string().default("").describe("Relative path from repository root to project root."),
1887
+ repository: z$1.string().optional().describe("GitHub repository in 'owner/repo' format."),
1888
+ base: z$1.string().optional().describe("Relative path from repository root to project root."),
1592
1889
  branch: z$1.string().optional().describe("Git branch to release from."),
1593
1890
  changelogPath: z$1.string().default("CHANGELOG.md").describe("Changelog file path, relative to project root."),
1594
1891
  bumpStrategy: BumpStrategySchema.describe("\"auto\" (from commits) or \"manual\" (user-specified)."),
1595
1892
  releaseType: ReleaseTypeSchema.optional().describe("The release type to bump."),
1596
1893
  preReleaseId: z$1.string().optional().describe("Pre-release ID (e.g., \"alpha\", \"beta\")."),
1597
1894
  preReleaseBase: PreReleaseBaseSchema.describe("Starting version for pre-releases."),
1598
- releaseNotes: z$1.string().default("").describe("Custom release notes for changelog."),
1895
+ releaseNotes: z$1.string().optional().describe("Custom release notes for changelog."),
1599
1896
  commitMessage: z$1.string().default(COMMIT_MSG_TEMPLATE).describe("Commit message template with placeholders."),
1600
1897
  tagName: z$1.string().default(TAG_NAME_TEMPLATE).describe("Tag name template with placeholders."),
1601
1898
  skipBump: z$1.coerce.boolean().default(false).describe("Skip version bump step."),
@@ -1605,9 +1902,9 @@ const ReleaseConfigSchema = z$1.object({
1605
1902
  skipGit: z$1.coerce.boolean().default(false).describe("Skip all git-related steps."),
1606
1903
  skipPreflightCheck: z$1.coerce.boolean().default(false).describe("Skip preflight checks."),
1607
1904
  releaseTitle: z$1.string().default(RELEASE_TITLE_TEMPLATE).describe("GitHub release title with placeholders."),
1608
- releaseLatest: z$1.coerce.boolean().default(true).describe("Mark as latest release."),
1609
- releasePreRelease: z$1.coerce.boolean().default(false).describe("Mark as pre-release."),
1610
- releaseDraft: z$1.coerce.boolean().default(false).describe("Release as draft version.")
1905
+ releaseLatest: z$1.coerce.boolean().optional().describe("Mark as latest release."),
1906
+ releasePreRelease: z$1.coerce.boolean().optional().describe("Mark as pre-release."),
1907
+ releaseDraft: z$1.coerce.boolean().optional().describe("Release as draft version.")
1611
1908
  }).check((ctx) => {
1612
1909
  validateReleaseFlagExclusivity(ctx);
1613
1910
  validateSkipGitRedundancy(ctx);
@@ -1661,30 +1958,30 @@ function defineService(definition) {
1661
1958
  */
1662
1959
  const SERVICE_DEFINITIONS = {
1663
1960
  fs: defineService({ factory: async ({ basePath }) => {
1664
- const { createFileSystemService } = await import("./filesystem.service-BcQtLzfl.js");
1961
+ const { createFileSystemService } = await import("./filesystem.service-DS2AQGNq.js");
1665
1962
  return createFileSystemService(basePath);
1666
1963
  } }),
1667
1964
  packageJson: defineService({
1668
1965
  dependencies: ["fs"],
1669
1966
  factory: async ({ getService }) => {
1670
1967
  const fs = await getService("fs");
1671
- const { createPackageJsonService } = await import("./package-json.service-BKbXaHXQ.js");
1968
+ const { createPackageJsonService } = await import("./package-json.service-BlMNgPGQ.js");
1672
1969
  return createPackageJsonService(fs);
1673
1970
  }
1674
1971
  }),
1675
1972
  git: defineService({ factory: async ({ basePath }) => {
1676
- const { createGitService } = await import("./git.service-D1Mf6LZ0.js");
1973
+ const { createGitService } = await import("./git.service-B6RdTilO.js");
1677
1974
  return createGitService(basePath);
1678
1975
  } }),
1679
1976
  versionBumper: defineService({ factory: async () => {
1680
- const { createVersionBumperService } = await import("./version-bumper.service-CvPx7EZx.js");
1977
+ const { createVersionBumperService } = await import("./version-bumper.service-glxzf9Qm.js");
1681
1978
  return createVersionBumperService();
1682
1979
  } }),
1683
1980
  versionStrategy: defineService({
1684
1981
  dependencies: ["versionBumper"],
1685
1982
  factory: async ({ getService }) => {
1686
1983
  const versionBumper = await getService("versionBumper");
1687
- const { createVersionStrategyService } = await import("./version-strategy.service-BO_7MY5O.js");
1984
+ const { createVersionStrategyService } = await import("./version-strategy.service-Dln42gxC.js");
1688
1985
  return createVersionStrategyService(versionBumper);
1689
1986
  }
1690
1987
  }),
@@ -1692,7 +1989,7 @@ const SERVICE_DEFINITIONS = {
1692
1989
  dependencies: ["git"],
1693
1990
  factory: async ({ getService }) => {
1694
1991
  const git = await getService("git");
1695
- const { createCommitAnalysisService } = await import("./commit-analysis.service-CtIh7eez.js");
1992
+ const { createCommitAnalysisService } = await import("./commit-analysis.service-B2Z128t8.js");
1696
1993
  return createCommitAnalysisService(git);
1697
1994
  }
1698
1995
  })
@@ -1956,7 +2253,7 @@ function logGraphStatistics(stats) {
1956
2253
 
1957
2254
  //#endregion
1958
2255
  //#region src/commands/release/release.command.ts
1959
- const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git");
2256
+ const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git", "commitAnalysis", "versionBumper", "versionStrategy");
1960
2257
  const releaseCommand = createCommand({
1961
2258
  meta: {
1962
2259
  name: "release",
@@ -2276,6 +2573,30 @@ var ImmutableWorkflowContext = class ImmutableWorkflowContext {
2276
2573
  services: this.services
2277
2574
  });
2278
2575
  }
2576
+ /**
2577
+ * @example Merging hydrated config values
2578
+ * ```typescript
2579
+ * const updatedCtx = ctx.forkConfig({
2580
+ * repository: "owner/repo",
2581
+ * branch: "main"
2582
+ * });
2583
+ * ```
2584
+ */
2585
+ forkConfig(updates) {
2586
+ if (Object.keys(updates).length === 0) return this;
2587
+ const mergedConfig = {
2588
+ ...this.config,
2589
+ ...updates
2590
+ };
2591
+ const frozenConfig = Object.freeze(mergedConfig);
2592
+ return new ImmutableWorkflowContext({
2593
+ startTime: this.startTime,
2594
+ workspace: this.workspace,
2595
+ config: frozenConfig,
2596
+ data: this.#data,
2597
+ services: this.services
2598
+ });
2599
+ }
2279
2600
  has(key) {
2280
2601
  return key in this.#data;
2281
2602
  }
@@ -2366,6 +2687,8 @@ var WorkflowExecutor = class {
2366
2687
  handleExecutionFailure(args) {
2367
2688
  const { error, startTime, executedTaskIds, skippedTaskIds, initialContext } = args;
2368
2689
  const endTime = /* @__PURE__ */ new Date();
2690
+ if (DebugFlags.showRawError) logger.error(error);
2691
+ else logger.error(error.message);
2369
2692
  if (this.options.enableRollback && this.executedTasks.length > 0) {
2370
2693
  logger.verbose(`WorkflowExecutor: Attempting rollback of ${this.executedTasks.length} tasks`);
2371
2694
  return this.rollback(initialContext).andThen((rollbackSuccess) => {
@@ -2441,9 +2764,6 @@ var WorkflowExecutor = class {
2441
2764
  executionLists.executedTaskIds.push(currentTask.meta.id);
2442
2765
  this.executedTasks.push(currentTask);
2443
2766
  return this.executeTasksSequentially(remainingTasks, updatedContext, executionLists.executedTaskIds, executionLists.skippedTaskIds);
2444
- }).mapErr((error) => {
2445
- logger.error(error.message);
2446
- return error;
2447
2767
  });
2448
2768
  }
2449
2769
  rollback(context) {
@@ -3115,6 +3435,18 @@ async function resolveServiceWithContext(key, context) {
3115
3435
  /**
3116
3436
  * Creates a lazy proxy that defers async service instantiation until first access.
3117
3437
  *
3438
+ * IMPORTANT:
3439
+ * This proxy always defers instantiation and wraps calls in a ResultAsync chain
3440
+ * in order to safely await the real service instance before invoking the method.
3441
+ *
3442
+ * At runtime the proxy therefore returns ResultAsync even if the concrete service
3443
+ * method returns a synchronous Result.
3444
+ *
3445
+ * Because of this behavior, service interfaces should return FireflyAsyncResult<T>
3446
+ * from their public methods to avoid type mismatches and confusion. Implementations
3447
+ * that have purely synchronous behavior should return FireflyOkAsync / FireflyErrAsync
3448
+ * so they still conform to the async contract expected by the proxy.
3449
+ *
3118
3450
  * @template T - The service interface type
3119
3451
  * @param factory - Async function that creates the actual service instance
3120
3452
  * @returns A proxy that behaves like the service but instantiates lazily