fireflyy 4.0.0-dev.a10ed44 → 4.0.0-dev.a8aacbe

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,9 +1,11 @@
1
1
  import { t as RuntimeEnv } from "./main.js";
2
- import { a as conflictErrAsync, c as notFoundErrAsync, d as validationErrAsync, f as conflictError, g as notFoundError, i as FireflyOkAsync, l as timeoutErrAsync, m as failedError, n as FireflyErrAsync, o as failedErrAsync, r as FireflyOk, s as invalidErr, t as FireflyErr, u as validationErr, v as validationError, y as wrapErrorMessage } from "./result.constructors-BMtOWD2-.js";
3
- import { n as wrapPromise, t as ensureNotAsync } from "./result.utilities-BTVU-GsT.js";
4
- import { t as logger } from "./logging-BuIkRrn1.js";
5
- import { n as parseSchema, t as formatZodErrors } from "./schema.utilities-BxiRR-GI.js";
6
- import { Command } from "commander";
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";
7
9
  import { LogLevels } from "consola";
8
10
  import { colors } from "consola/utils";
9
11
  import { loadConfig } from "c12";
@@ -12,70 +14,6 @@ import z$1 from "zod";
12
14
  import { parse } from "semver";
13
15
  import * as path from "path";
14
16
 
15
- //#region src/core/environment/debug-flags.ts
16
- /**
17
- * Debug flags are environment variables prefixed with `FIREFLY_DEBUG_` that
18
- * enable diagnostic features during development and troubleshooting.
19
- *
20
- * @example
21
- * ```typescript
22
- * if (DebugFlags.showRawError) {
23
- * logger.error(parseResult.error);
24
- * }
25
- * ```
26
- */
27
- var DebugFlags = class {
28
- constructor() {}
29
- /**
30
- * When enabled, displays raw Zod validation errors for configuration parsing.
31
- *
32
- * Useful for debugging configuration schema issues and understanding
33
- * why validation failed at a granular level.
34
- */
35
- static get showRawError() {
36
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_RAW_ERROR);
37
- }
38
- /**
39
- * When enabled, logs the loaded configuration file contents.
40
- *
41
- * Useful for debugging configuration loading and understanding
42
- * what values are being read from config files.
43
- */
44
- static get showFileConfig() {
45
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_FILE_CONFIG);
46
- }
47
- /**
48
- * When enabled, displays task graph statistics during release execution.
49
- *
50
- * Shows information about task dependencies, execution order,
51
- * and graph structure for debugging workflow issues.
52
- */
53
- static get showTaskGraphStats() {
54
- return Boolean(process.env.FIREFLY_DEBUG_SHOW_TASK_GRAPH_STATS);
55
- }
56
- /**
57
- * When enabled, prevents truncation of release notes in GitHub CLI logs.
58
- *
59
- * By default, release notes are truncated in logs to avoid pollution.
60
- * Enable this flag to see full release notes content during debugging.
61
- */
62
- static get dontTruncateReleaseNotes() {
63
- return Boolean(process.env.FIREFLY_DEBUG_DONT_TRUNCATE_RELEASE_NOTES?.trim());
64
- }
65
- /**
66
- * When enabled, prevents redaction of sensitive GitHub CLI arguments in logs.
67
- *
68
- * By default, sensitive values (tokens, passwords, etc.) are redacted.
69
- * Enable this flag to see full argument values during debugging.
70
- *
71
- * WARNING: Use with caution as this may expose sensitive information.
72
- */
73
- static get dontRedactGithubCliArgs() {
74
- return Boolean(process.env.FIREFLY_DEBUG_DONT_REDACT_GITHUB_CLI_ARGS?.trim());
75
- }
76
- };
77
-
78
- //#endregion
79
17
  //#region src/cli/config/config.loader.ts
80
18
  /**
81
19
  * Loads and resolves Firefly configuration from files.
@@ -185,6 +123,223 @@ function camelToKebab(str) {
185
123
  return result.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/_/g, "-");
186
124
  }
187
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
+
188
343
  //#endregion
189
344
  //#region src/cli/options/options.builder.ts
190
345
  /**
@@ -305,15 +460,14 @@ var OptionsBuilder = class {
305
460
  this.registerGenericOption(ctx);
306
461
  }
307
462
  /**
308
- * Registers a number option with numeric parsing.
463
+ * Registers a number option with numeric parsing and validation.
309
464
  *
310
465
  * @param ctx - The option context
311
466
  */
312
467
  registerNumberOption(ctx) {
313
468
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
314
- const parser = this.createNumberParser(rawField, optionName);
315
- const wrappedParser = this.wrapParser(parser);
316
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
469
+ const validator = createNumberValidator(rawField, optionName);
470
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
317
471
  }
318
472
  /**
319
473
  * Registers an enum option with choice validation.
@@ -321,111 +475,46 @@ var OptionsBuilder = class {
321
475
  * @param ctx - The option context
322
476
  */
323
477
  registerEnumOption(ctx) {
324
- const { command, rawField, field, optionFlag, optionName, description, parsedDefault } = ctx;
325
- const choices = this.getEnumChoices(field);
326
- const parser = this.createEnumParser(rawField, optionName, choices);
327
- 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);
328
481
  const fullDescription = `${description}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}`;
329
- command.option(`${optionFlag} <${optionName}>`, fullDescription, wrappedParser, parsedDefault);
482
+ command.option(`${optionFlag} <${optionName}>`, fullDescription, validator, parsedDefault);
330
483
  }
331
484
  /**
332
- * Registers a string option with validation.
485
+ * Registers a string option with validation.
333
486
  *
334
487
  * @param ctx - The option context
335
488
  */
336
489
  registerStringOption(ctx) {
337
490
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
338
- const parser = this.createStringParser(rawField);
339
- const wrappedParser = this.wrapParser(parser);
340
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
491
+ const validator = createStringValidator(rawField, optionName);
492
+ command.option(`${optionFlag} <${optionName}>`, description, validator, parsedDefault);
341
493
  }
342
494
  /**
343
495
  * Registers a generic option for other Zod types.
496
+ * Handles union types with literal values specially for better error messages.
344
497
  *
345
498
  * @param ctx - The option context
346
499
  */
347
500
  registerGenericOption(ctx) {
348
501
  const { command, rawField, optionFlag, optionName, description, parsedDefault } = ctx;
349
- const parser = this.createGenericParser(rawField);
350
- const wrappedParser = this.wrapParser(parser);
351
- command.option(`${optionFlag} <${optionName}>`, description, wrappedParser, parsedDefault);
352
- }
353
- /**
354
- * Wraps a Result-returning parser into a Commander-compatible parser.
355
- *
356
- * Commander expects parsers to return values directly or throw on error.
357
- * This wrapper converts our Result-based parsers to that pattern.
358
- *
359
- * @param parser - The Result-based parser function
360
- * @returns A Commander-compatible parser function
361
- */
362
- wrapParser(parser) {
363
- return (input) => {
364
- const result = parser(input);
365
- if (result.isErr()) throw new Error(result.error);
366
- return result.value;
367
- };
368
- }
369
- /**
370
- * Creates a parser for number options.
371
- *
372
- * @template T - The expected return type
373
- * @param rawField - The raw Zod field
374
- * @param optionName - The option name for error messages
375
- * @returns A parser function that converts strings to numbers with validation
376
- */
377
- createNumberParser(rawField, optionName) {
378
- return (input) => {
379
- const num = Number(input);
380
- if (Number.isNaN(num)) return err(`Invalid number for --${optionName}: ${input}`);
381
- const result = parseSchema(rawField, num);
382
- if (result.isErr()) return err(result.error.message);
383
- return ok(result.value);
384
- };
385
- }
386
- /**
387
- * Creates a parser for enum options.
388
- *
389
- * @template T - The expected return type
390
- * @param rawField - The raw Zod field
391
- * @param optionName - The option name for error messages
392
- * @param choices - The valid enum choices
393
- * @returns A parser function that validates input against the enum choices
394
- */
395
- createEnumParser(rawField, optionName, choices) {
396
- return (input) => {
397
- const result = parseSchema(rawField, input);
398
- if (result.isErr()) return err(`Invalid value for --${optionName}: ${input}. Allowed: ${choices.join(", ")}`);
399
- return ok(result.value);
400
- };
401
- }
402
- /**
403
- * Creates a parser for string options.
404
- *
405
- * @template T - The expected return type
406
- * @param rawField - The raw Zod field
407
- * @returns A parser function that validates input against the schema
408
- */
409
- createStringParser(rawField) {
410
- return (input) => {
411
- const result = parseSchema(rawField, input);
412
- if (result.isErr()) return err(result.error.message);
413
- return ok(result.value);
414
- };
415
- }
416
- /**
417
- * Creates a generic parser using Zod validation.
418
- *
419
- * @template T - The expected return type
420
- * @param rawField - The raw Zod field
421
- * @returns A parser function that validates input against the schema
422
- */
423
- createGenericParser(rawField) {
424
- return (input) => {
425
- const result = parseSchema(rawField, input);
426
- if (result.isErr()) return err(result.error.message);
427
- return ok(result.value);
428
- };
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);
429
518
  }
430
519
  /**
431
520
  * Gets the internal definition object from a Zod type.
@@ -470,15 +559,6 @@ var OptionsBuilder = class {
470
559
  const def = this.getInternalDef(rawField);
471
560
  return (def.innerType ?? def.schema) instanceof z$1.ZodBoolean;
472
561
  }
473
- /**
474
- * Extracts enum choices from a ZodEnum field.
475
- *
476
- * @param field - The ZodEnum field
477
- * @returns The array of valid enum choices
478
- */
479
- getEnumChoices(field) {
480
- return this.getInternalDef(field).values ?? [];
481
- }
482
562
  };
483
563
 
484
564
  //#endregion
@@ -805,10 +885,63 @@ var TaskBuilder = class TaskBuilder {
805
885
 
806
886
  //#endregion
807
887
  //#region src/commands/release/tasks/bump-release-version.task.ts
888
+ const PACKAGE_JSON_FILE$1 = "package.json";
889
+ /**
890
+ * Updates the configured package.json with the next version.
891
+ *
892
+ * @param ctx - The current release context
893
+ * @returns A FireflyAsyncResult resolving to the release context after update
894
+ */
895
+ function updatePackageJsonVersion(ctx) {
896
+ return ctx.services.packageJson.updateVersion(PACKAGE_JSON_FILE$1, ctx.data.nextVersion).map(() => ctx);
897
+ }
898
+ /**
899
+ * Restores a previous version value in the configured package.json.
900
+ *
901
+ * @param ctx - The current release context
902
+ * @param previousVersion - The previous version string to restore
903
+ * @returns A FireflyAsyncResult resolving to the release context after restore
904
+ */
905
+ function restorePackageJsonVersion(ctx, previousVersion) {
906
+ return ctx.services.packageJson.updateVersion(PACKAGE_JSON_FILE$1, previousVersion).map(() => ctx);
907
+ }
908
+ /**
909
+ * Validates that the release context contains a next version value.
910
+ *
911
+ * @param ctx - The current release context
912
+ * @returns A FireflyResult with the next version or a validation error
913
+ */
914
+ function parseNextVersion(ctx) {
915
+ const nextVersion = ctx.data.nextVersion;
916
+ if (!nextVersion) return validationErr({ message: "Next version is undefined" });
917
+ return FireflyOk(nextVersion);
918
+ }
919
+ /**
920
+ * Creates the Bump Release Version task.
921
+ *
922
+ * This task updates the version of the project to the next version determined
923
+ * by earlier steps. Specifically, it:
924
+ * 1. Validates that a `nextVersion` exists in the release context
925
+ * 2. Writes the `nextVersion` into the configured package.json file
926
+ * 3. Provides an undo operation that restores the original version
927
+ */
808
928
  function createBumpReleaseVersion() {
809
- 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) => {
810
- logger.info("bump-release-version");
811
- return FireflyOkAsync(ctx);
929
+ /**
930
+ * Holds the previous version before bumping, for undo purposes if needed.
931
+ */
932
+ let previousVersion;
933
+ 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 || !ctx.data.nextVersion, "Skipped: skipBump is enabled or next version is not set").execute((ctx) => {
934
+ previousVersion = ctx.data.currentVersion;
935
+ const nextVersionRes = parseNextVersion(ctx);
936
+ if (nextVersionRes.isErr()) return FireflyErrAsync(nextVersionRes.error);
937
+ logger.info(`Next version to be released: ${colors.green(nextVersionRes.value)}`);
938
+ return updatePackageJsonVersion(ctx).andTee(() => logger.success("package.json version updated successfully."));
939
+ }).withUndo((ctx) => {
940
+ if (!previousVersion) {
941
+ logger.verbose("BumpReleaseVersionTask: Previous version is undefined, skipping undo operation.");
942
+ return FireflyOkAsync(void 0);
943
+ }
944
+ return restorePackageJsonVersion(ctx, previousVersion).map(() => void 0);
812
945
  }).build();
813
946
  }
814
947
 
@@ -1029,22 +1162,60 @@ function createDelegateBumpStrategyTask() {
1029
1162
  shouldSkip,
1030
1163
  reason: shouldSkip ? getSkipReason(ctx) : void 0
1031
1164
  });
1032
- }).execute((ctx) => {
1033
- logger.info("delegate-bump-strategy");
1034
- return FireflyOkAsync(ctx);
1035
- }).build();
1165
+ }).execute((ctx) => FireflyOkAsync(ctx)).build();
1036
1166
  }
1037
1167
 
1038
1168
  //#endregion
1039
1169
  //#region src/commands/release/tasks/determine-automatic-bump.task.ts
1170
+ /**
1171
+ * Parses the current version from a raw string.
1172
+ *
1173
+ * @param currentVersionRaw - The raw string representing the current version
1174
+ * @returns A FireflyResult containing the parsed Version or a validation error
1175
+ */
1176
+ function parseCurrentVersion$2(currentVersionRaw) {
1177
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1178
+ return Version.from(currentVersionRaw);
1179
+ }
1180
+ /**
1181
+ * Logs the analysis recommendation with formatting.
1182
+ *
1183
+ * @param reason - The reason string from the recommendation
1184
+ */
1185
+ function logRecommendation(reason) {
1186
+ if (!reason) return;
1187
+ if (reason.startsWith("Analysis found:")) {
1188
+ const prefix = "Analysis found:";
1189
+ const details = reason.slice(15).trim();
1190
+ logger.info(`${prefix} ${colors.green(details)}`);
1191
+ } else logger.info(reason);
1192
+ }
1193
+ /**
1194
+ * Performs the automatic bump by analyzing commits and resolving version strategy.
1195
+ */
1196
+ function executeAutomaticBump(ctx) {
1197
+ const currentVersionResult = parseCurrentVersion$2(ctx.data.currentVersion);
1198
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1199
+ const currentVersion = currentVersionResult.value;
1200
+ return ctx.services.commitAnalysis.analyzeForVersion().andThen((recommendation) => {
1201
+ logRecommendation(recommendation.reason);
1202
+ const options = {
1203
+ currentVersion,
1204
+ preReleaseId: ctx.config.preReleaseId,
1205
+ preReleaseBase: ctx.config.preReleaseBase
1206
+ };
1207
+ return ctx.services.versionStrategy.resolveVersion(options, recommendation);
1208
+ }).andThen((newVersion) => {
1209
+ const from = ctx.data.currentVersion || "unknown";
1210
+ logger.info(`Determined version: ${colors.green(from)} -> ${colors.green(newVersion.raw)}`);
1211
+ return FireflyOkAsync(ctx.fork("nextVersion", newVersion.toString()));
1212
+ });
1213
+ }
1040
1214
  function createDetermineAutomaticBump() {
1041
1215
  return TaskBuilder.create("determine-automatic-bump").description("Automatically determines the version bump from commit messages").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1042
1216
  const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1043
1217
  return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_AUTO;
1044
- }, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => {
1045
- logger.info("determine-automatic-bump");
1046
- return FireflyOkAsync(ctx);
1047
- }).build();
1218
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'auto'").execute((ctx) => executeAutomaticBump(ctx)).build();
1048
1219
  }
1049
1220
 
1050
1221
  //#endregion
@@ -1091,29 +1262,108 @@ function promptBumpStrategy() {
1091
1262
  return FireflyOkAsync(selected);
1092
1263
  });
1093
1264
  }
1265
+ /**
1266
+ * Creates the Prompt Bump Strategy Task.
1267
+ *
1268
+ * This task prompts the user to select a version bump strategy (automatic or manual) and
1269
+ * stores it in the release context. It depends on `initialize-release-version` and will be
1270
+ * skipped when `skipBump` is enabled or a `bumpStrategy`/`releaseType` is already provided.
1271
+ *
1272
+ * This task:
1273
+ * 1. Prompts the user to select a bump strategy
1274
+ */
1094
1275
  function createPromptBumpStrategyTask() {
1095
1276
  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();
1096
1277
  }
1097
1278
 
1098
1279
  //#endregion
1099
1280
  //#region src/commands/release/tasks/prompt-manual-bump.task.ts
1281
+ /**
1282
+ * Parses the current version from a raw string.
1283
+ *
1284
+ * @param currentVersionRaw - The raw string representing the current version
1285
+ * @returns A FireflyResult containing the parsed Version or a validation error
1286
+ */
1287
+ function parseCurrentVersion$1(currentVersionRaw) {
1288
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1289
+ return Version.from(currentVersionRaw);
1290
+ }
1291
+ /**
1292
+ * Generates version choices and prompts the user to select one.
1293
+ */
1294
+ function promptForManualVersion(ctx) {
1295
+ const currentVersionResult = parseCurrentVersion$1(ctx.data.currentVersion);
1296
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1297
+ const currentVersion = currentVersionResult.value;
1298
+ logger.verbose("PromptManualVersionTask: Generating version choices...");
1299
+ return ctx.services.versionStrategy.generateChoices({
1300
+ currentVersion,
1301
+ preReleaseId: ctx.config.preReleaseId,
1302
+ preReleaseBase: ctx.config.preReleaseBase
1303
+ }).andThen((choices) => {
1304
+ if (!choices || choices.length === 0) return validationErrAsync({ message: "No version choices available" });
1305
+ logger.verbose(`PromptManualVersionTask: Generated ${choices.length} version choices.`);
1306
+ const defaultChoice = choices[0];
1307
+ return wrapPromise(logger.prompt("Select version to release", {
1308
+ type: "select",
1309
+ options: choices,
1310
+ initial: defaultChoice?.value,
1311
+ cancel: "undefined"
1312
+ })).andThen((selected) => {
1313
+ if (!selected || selected === "") return failedErrAsync({ message: "Operation cancelled by user" });
1314
+ if (logger.level === LogLevels.verbose) logger.log("");
1315
+ if (logger.level !== LogLevels.verbose) logger.log("");
1316
+ logger.verbose(`PromptManualVersionTask: Selected version: '${selected}'`);
1317
+ return FireflyOkAsync(selected);
1318
+ });
1319
+ });
1320
+ }
1100
1321
  function createPromptManualVersionTask() {
1101
- return TaskBuilder.create("prompt-manual-version").description("Prompts the user for a manual version bump selections").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1322
+ return TaskBuilder.create("prompt-manual-version").description("Prompts the user to manually select the next version").dependsOn("delegate-bump-strategy").skipWhenWithReason((ctx) => {
1102
1323
  const bumpStrategy = ctx.data.selectedBumpStrategy ?? ctx.config.bumpStrategy;
1103
1324
  return ctx.config.skipBump || bumpStrategy !== BUMP_STRATEGY_MANUAL;
1104
- }, "Skipped: skipBump enabled or bumpStrategy is not 'manual'").execute((ctx) => {
1105
- logger.info("prompt-manual-version");
1106
- return FireflyOkAsync(ctx);
1107
- }).build();
1325
+ }, "Skipped: skipBump enabled or bumpStrategy is not 'manual'").execute((ctx) => promptForManualVersion(ctx).andThen((selectedVersion) => FireflyOkAsync(ctx.fork("nextVersion", selectedVersion)))).build();
1108
1326
  }
1109
1327
 
1110
1328
  //#endregion
1111
1329
  //#region src/commands/release/tasks/straight-version-bump.task.ts
1330
+ /**
1331
+ * Parses the current version from a raw string.
1332
+ *
1333
+ * @param currentVersionRaw - The raw string representing the current version
1334
+ * @returns A FireflyResult containing the parsed Version or a validation error
1335
+ */
1336
+ function parseCurrentVersion(currentVersionRaw) {
1337
+ if (!currentVersionRaw) return validationErr({ message: "Current version is undefined" });
1338
+ return Version.from(currentVersionRaw);
1339
+ }
1340
+ /**
1341
+ * Builds the bump options from the release context.
1342
+ */
1343
+ function buildBumpOptionsFromContext(ctx) {
1344
+ const currentVersionResult = parseCurrentVersion(ctx.data.currentVersion);
1345
+ if (currentVersionResult.isErr()) return FireflyErrAsync(currentVersionResult.error);
1346
+ const releaseType = ctx.config.releaseType;
1347
+ if (releaseType === void 0) return invalidErrAsync({ message: "Release type is required for straight bump" });
1348
+ return FireflyOkAsync({
1349
+ currentVersion: currentVersionResult.value,
1350
+ releaseType,
1351
+ preReleaseId: ctx.config.preReleaseId,
1352
+ preReleaseBase: ctx.config.preReleaseBase
1353
+ });
1354
+ }
1355
+ /**
1356
+ * Performs the straight bump by delegating to the version bumper service.
1357
+ */
1358
+ function executeStraightVersionBump(ctx) {
1359
+ return buildBumpOptionsFromContext(ctx).andThen((options) => ctx.services.versionBumper.bump(options)).andThen((newVersion) => {
1360
+ const from = ctx.data.currentVersion || "unknown";
1361
+ logger.info(`Bumped version: ${colors.green(from)} -> ${colors.green(newVersion.raw)}`);
1362
+ return FireflyOkAsync(ctx.fork("nextVersion", newVersion.toString()));
1363
+ });
1364
+ }
1112
1365
  function createStraightVersionBump() {
1113
- 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) => {
1114
- logger.info("straight-version-bump");
1115
- return FireflyOkAsync(ctx);
1116
- }).build();
1366
+ 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();
1117
1367
  }
1118
1368
 
1119
1369
  //#endregion
@@ -1167,7 +1417,7 @@ function getVersionFromPackageJson(ctx) {
1167
1417
  */
1168
1418
  function createInitializeReleaseVersion() {
1169
1419
  return TaskBuilder.create("initialize-release-version").description("Initialize current release version from package.json").dependsOn("prepare-release-config").execute((ctx) => getVersionFromPackageJson(ctx).andThen((currentVersion) => {
1170
- logger.info(`Current version is ${currentVersion}`);
1420
+ logger.info(`Current version is ${colors.green(currentVersion)}`);
1171
1421
  return FireflyOkAsync(ctx.fork("currentVersion", currentVersion));
1172
1422
  })).build();
1173
1423
  }
@@ -1280,7 +1530,7 @@ function hydrateScopeFromPackageJson(ctx, packageJson) {
1280
1530
  * 3. Otherwise the function defaults to "alpha".
1281
1531
  */
1282
1532
  function hydratePreReleaseIdFromPackageJson(ctx, packageJson) {
1283
- if (ctx.config.preReleaseId !== void 0 && ctx.config.preReleaseId.trim() !== "") {
1533
+ if (Object.hasOwn(ctx.config, "preReleaseId") && ctx.config.preReleaseId !== void 0) {
1284
1534
  logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseId: "${ctx.config.preReleaseId}" as it is explicitly set`);
1285
1535
  return FireflyOkAsync(ctx.config.preReleaseId);
1286
1536
  }
@@ -1297,26 +1547,47 @@ function hydratePreReleaseIdFromPackageJson(ctx, packageJson) {
1297
1547
  return FireflyOkAsync("alpha");
1298
1548
  }
1299
1549
  /**
1550
+ * Hydrates the `preReleaseBase` field.
1551
+ *
1552
+ * Behavior:
1553
+ * - If explicitly provided in config (key exists and value is not undefined) use as-is.
1554
+ * - Otherwise default to 0.
1555
+ *
1556
+ * Note: we do NOT infer `preReleaseBase` from package.json anymore.
1557
+ */
1558
+ function hydratePreReleaseBase(ctx) {
1559
+ const baseMaybe = ctx.config.preReleaseBase;
1560
+ if (Object.hasOwn(ctx.config, "preReleaseBase") && baseMaybe !== void 0) {
1561
+ logger.verbose(`PrepareReleaseConfigTask: Using provided preReleaseBase: "${ctx.config.preReleaseBase}" as it is explicitly set`);
1562
+ return FireflyOkAsync(baseMaybe);
1563
+ }
1564
+ logger.verbose("PrepareReleaseConfigTask: No preReleaseBase explicitly provided, defaulting to 0");
1565
+ return FireflyOkAsync(0);
1566
+ }
1567
+ /**
1300
1568
  * Hydrates name, scope, and preReleaseId from package.json.
1301
1569
  *
1302
1570
  * Behavior:
1303
1571
  * - If package.json does not exist, returns all values as undefined.
1304
1572
  * - If it exists, reads package.json and returns parsed results for name, scope and preReleaseId.
1573
+ * - If preReleaseBase is explicitly provided in config, it is used as-is, if not, it defaults to 0.
1305
1574
  */
1306
1575
  function hydrateFromPackageJson(ctx) {
1307
1576
  return ctx.services.fs.exists("package.json").andThen((exists) => {
1308
1577
  if (!exists) return FireflyOkAsync({
1309
1578
  name: void 0,
1310
1579
  scope: void 0,
1311
- preReleaseId: void 0
1580
+ preReleaseId: void 0,
1581
+ preReleaseBase: void 0
1312
1582
  });
1313
- return ctx.services.packageJson.read("package.json").andThen((pkg) => hydrateNameFromPackageJson(ctx, pkg).andThen((name) => hydrateScopeFromPackageJson(ctx, pkg).andThen((scope) => hydratePreReleaseIdFromPackageJson(ctx, pkg).map((preReleaseId) => {
1583
+ 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) => {
1314
1584
  const result = {};
1315
1585
  if (name) result.name = name;
1316
1586
  if (scope) result.scope = scope;
1317
1587
  if (preReleaseId) result.preReleaseId = preReleaseId;
1588
+ if (preReleaseBase !== void 0) result.preReleaseBase = preReleaseBase;
1318
1589
  return result;
1319
- }))));
1590
+ })))));
1320
1591
  });
1321
1592
  }
1322
1593
  /**
@@ -1447,12 +1718,13 @@ function createPrepareReleaseConfigTask() {
1447
1718
  if (pkgData.name) hydrated.name = pkgData.name;
1448
1719
  if (pkgData.scope) hydrated.scope = pkgData.scope;
1449
1720
  if (pkgData.preReleaseId) hydrated.preReleaseId = pkgData.preReleaseId;
1721
+ if (pkgData.preReleaseBase !== void 0) hydrated.preReleaseBase = pkgData.preReleaseBase;
1450
1722
  return hydrateReleaseFlags(ctx);
1451
1723
  }).map((releaseFlags) => {
1452
1724
  hydrated.releaseLatest = releaseFlags.releaseLatest;
1453
1725
  hydrated.releasePreRelease = releaseFlags.releasePreRelease;
1454
1726
  hydrated.releaseDraft = releaseFlags.releaseDraft;
1455
- return ctx.fork("hydratedConfig", hydrated);
1727
+ return ctx.forkConfig(hydrated);
1456
1728
  });
1457
1729
  }).build();
1458
1730
  }
@@ -1662,17 +1934,23 @@ function validateSkipFlagCombinations(ctx) {
1662
1934
  input: ctx.value
1663
1935
  });
1664
1936
  }
1665
- const ReleaseConfigSchema = z$1.object({
1937
+ /**
1938
+ * Base release configuration schema without refinements.
1939
+ * Use this schema for JSON schema generation and `.partial()` operations.
1940
+ * Zod v4 does not allow `.partial()` on schemas with refinements.
1941
+ */
1942
+ const ReleaseConfigBaseSchema = z$1.object({
1666
1943
  name: z$1.string().optional().describe("Unscoped project name. Auto-detected from package.json."),
1667
1944
  scope: z$1.string().optional().describe("Org/user scope without '@'. Auto-detected from package.json."),
1668
- base: z$1.string().default("").describe("Relative path from repository root to project root."),
1945
+ repository: z$1.string().optional().describe("GitHub repository in 'owner/repo' format."),
1946
+ base: z$1.string().optional().describe("Relative path from repository root to project root."),
1669
1947
  branch: z$1.string().optional().describe("Git branch to release from."),
1670
1948
  changelogPath: z$1.string().default("CHANGELOG.md").describe("Changelog file path, relative to project root."),
1671
1949
  bumpStrategy: BumpStrategySchema.describe("\"auto\" (from commits) or \"manual\" (user-specified)."),
1672
1950
  releaseType: ReleaseTypeSchema.optional().describe("The release type to bump."),
1673
1951
  preReleaseId: z$1.string().optional().describe("Pre-release ID (e.g., \"alpha\", \"beta\")."),
1674
1952
  preReleaseBase: PreReleaseBaseSchema.describe("Starting version for pre-releases."),
1675
- releaseNotes: z$1.string().default("").describe("Custom release notes for changelog."),
1953
+ releaseNotes: z$1.string().optional().describe("Custom release notes for changelog."),
1676
1954
  commitMessage: z$1.string().default(COMMIT_MSG_TEMPLATE).describe("Commit message template with placeholders."),
1677
1955
  tagName: z$1.string().default(TAG_NAME_TEMPLATE).describe("Tag name template with placeholders."),
1678
1956
  skipBump: z$1.coerce.boolean().default(false).describe("Skip version bump step."),
@@ -1685,7 +1963,12 @@ const ReleaseConfigSchema = z$1.object({
1685
1963
  releaseLatest: z$1.coerce.boolean().optional().describe("Mark as latest release."),
1686
1964
  releasePreRelease: z$1.coerce.boolean().optional().describe("Mark as pre-release."),
1687
1965
  releaseDraft: z$1.coerce.boolean().optional().describe("Release as draft version.")
1688
- }).check((ctx) => {
1966
+ });
1967
+ /**
1968
+ * Release configuration schema with runtime validation refinements.
1969
+ * Use this schema for parsing and validating user input.
1970
+ */
1971
+ const ReleaseConfigSchema = ReleaseConfigBaseSchema.check((ctx) => {
1689
1972
  validateReleaseFlagExclusivity(ctx);
1690
1973
  validateSkipGitRedundancy(ctx);
1691
1974
  validateBumpStrategyCompatibility(ctx);
@@ -1738,30 +2021,30 @@ function defineService(definition) {
1738
2021
  */
1739
2022
  const SERVICE_DEFINITIONS = {
1740
2023
  fs: defineService({ factory: async ({ basePath }) => {
1741
- const { createFileSystemService } = await import("./filesystem.service-B8qg87n-.js");
2024
+ const { createFileSystemService } = await import("./filesystem.service-DS2AQGNq.js");
1742
2025
  return createFileSystemService(basePath);
1743
2026
  } }),
1744
2027
  packageJson: defineService({
1745
2028
  dependencies: ["fs"],
1746
2029
  factory: async ({ getService }) => {
1747
2030
  const fs = await getService("fs");
1748
- const { createPackageJsonService } = await import("./package-json.service-CHmtIoQQ.js");
2031
+ const { createPackageJsonService } = await import("./package-json.service-BlMNgPGQ.js");
1749
2032
  return createPackageJsonService(fs);
1750
2033
  }
1751
2034
  }),
1752
2035
  git: defineService({ factory: async ({ basePath }) => {
1753
- const { createGitService } = await import("./git.service-BBqDH7qx.js");
2036
+ const { createGitService } = await import("./git.service-B6RdTilO.js");
1754
2037
  return createGitService(basePath);
1755
2038
  } }),
1756
2039
  versionBumper: defineService({ factory: async () => {
1757
- const { createVersionBumperService } = await import("./version-bumper.service-DMYR0npB.js");
2040
+ const { createVersionBumperService } = await import("./version-bumper.service-glxzf9Qm.js");
1758
2041
  return createVersionBumperService();
1759
2042
  } }),
1760
2043
  versionStrategy: defineService({
1761
2044
  dependencies: ["versionBumper"],
1762
2045
  factory: async ({ getService }) => {
1763
2046
  const versionBumper = await getService("versionBumper");
1764
- const { createVersionStrategyService } = await import("./version-strategy.service-CmfeZLYC.js");
2047
+ const { createVersionStrategyService } = await import("./version-strategy.service-Dln42gxC.js");
1765
2048
  return createVersionStrategyService(versionBumper);
1766
2049
  }
1767
2050
  }),
@@ -1769,7 +2052,7 @@ const SERVICE_DEFINITIONS = {
1769
2052
  dependencies: ["git"],
1770
2053
  factory: async ({ getService }) => {
1771
2054
  const git = await getService("git");
1772
- const { createCommitAnalysisService } = await import("./commit-analysis.service-Ba6hLgsr.js");
2055
+ const { createCommitAnalysisService } = await import("./commit-analysis.service-B2Z128t8.js");
1773
2056
  return createCommitAnalysisService(git);
1774
2057
  }
1775
2058
  })
@@ -2033,7 +2316,7 @@ function logGraphStatistics(stats) {
2033
2316
 
2034
2317
  //#endregion
2035
2318
  //#region src/commands/release/release.command.ts
2036
- const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git");
2319
+ const RELEASE_SERVICES = defineServiceKeys("fs", "packageJson", "git", "commitAnalysis", "versionBumper", "versionStrategy");
2037
2320
  const releaseCommand = createCommand({
2038
2321
  meta: {
2039
2322
  name: "release",
@@ -2353,6 +2636,30 @@ var ImmutableWorkflowContext = class ImmutableWorkflowContext {
2353
2636
  services: this.services
2354
2637
  });
2355
2638
  }
2639
+ /**
2640
+ * @example Merging hydrated config values
2641
+ * ```typescript
2642
+ * const updatedCtx = ctx.forkConfig({
2643
+ * repository: "owner/repo",
2644
+ * branch: "main"
2645
+ * });
2646
+ * ```
2647
+ */
2648
+ forkConfig(updates) {
2649
+ if (Object.keys(updates).length === 0) return this;
2650
+ const mergedConfig = {
2651
+ ...this.config,
2652
+ ...updates
2653
+ };
2654
+ const frozenConfig = Object.freeze(mergedConfig);
2655
+ return new ImmutableWorkflowContext({
2656
+ startTime: this.startTime,
2657
+ workspace: this.workspace,
2658
+ config: frozenConfig,
2659
+ data: this.#data,
2660
+ services: this.services
2661
+ });
2662
+ }
2356
2663
  has(key) {
2357
2664
  return key in this.#data;
2358
2665
  }
@@ -3191,6 +3498,18 @@ async function resolveServiceWithContext(key, context) {
3191
3498
  /**
3192
3499
  * Creates a lazy proxy that defers async service instantiation until first access.
3193
3500
  *
3501
+ * IMPORTANT:
3502
+ * This proxy always defers instantiation and wraps calls in a ResultAsync chain
3503
+ * in order to safely await the real service instance before invoking the method.
3504
+ *
3505
+ * At runtime the proxy therefore returns ResultAsync even if the concrete service
3506
+ * method returns a synchronous Result.
3507
+ *
3508
+ * Because of this behavior, service interfaces should return FireflyAsyncResult<T>
3509
+ * from their public methods to avoid type mismatches and confusion. Implementations
3510
+ * that have purely synchronous behavior should return FireflyOkAsync / FireflyErrAsync
3511
+ * so they still conform to the async contract expected by the proxy.
3512
+ *
3194
3513
  * @template T - The service interface type
3195
3514
  * @param factory - Async function that creates the actual service instance
3196
3515
  * @returns A proxy that behaves like the service but instantiates lazily