@hypercli/gen 0.1.1

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.
Files changed (306) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/dist/actions/communication.d.ts +201 -0
  4. package/dist/actions/communication.d.ts.map +1 -0
  5. package/dist/actions/communication.js +515 -0
  6. package/dist/actions/communication.js.map +1 -0
  7. package/dist/actions/decorator.d.ts +22 -0
  8. package/dist/actions/decorator.d.ts.map +1 -0
  9. package/dist/actions/decorator.js +110 -0
  10. package/dist/actions/decorator.js.map +1 -0
  11. package/dist/actions/executor.d.ts +85 -0
  12. package/dist/actions/executor.d.ts.map +1 -0
  13. package/dist/actions/executor.js +289 -0
  14. package/dist/actions/executor.js.map +1 -0
  15. package/dist/actions/index.d.ts +14 -0
  16. package/dist/actions/index.d.ts.map +1 -0
  17. package/dist/actions/index.js +15 -0
  18. package/dist/actions/index.js.map +1 -0
  19. package/dist/actions/parameter-resolver.d.ts +54 -0
  20. package/dist/actions/parameter-resolver.d.ts.map +1 -0
  21. package/dist/actions/parameter-resolver.js +300 -0
  22. package/dist/actions/parameter-resolver.js.map +1 -0
  23. package/dist/actions/registry.d.ts +78 -0
  24. package/dist/actions/registry.d.ts.map +1 -0
  25. package/dist/actions/registry.js +221 -0
  26. package/dist/actions/registry.js.map +1 -0
  27. package/dist/actions/types.d.ts +109 -0
  28. package/dist/actions/types.d.ts.map +1 -0
  29. package/dist/actions/types.js +31 -0
  30. package/dist/actions/types.js.map +1 -0
  31. package/dist/actions/utils.d.ts +42 -0
  32. package/dist/actions/utils.d.ts.map +1 -0
  33. package/dist/actions/utils.js +144 -0
  34. package/dist/actions/utils.js.map +1 -0
  35. package/dist/ai/ai-collector.d.ts +52 -0
  36. package/dist/ai/ai-collector.d.ts.map +1 -0
  37. package/dist/ai/ai-collector.js +64 -0
  38. package/dist/ai/ai-collector.js.map +1 -0
  39. package/dist/ai/ai-config.d.ts +230 -0
  40. package/dist/ai/ai-config.d.ts.map +1 -0
  41. package/dist/ai/ai-config.js +8 -0
  42. package/dist/ai/ai-config.js.map +1 -0
  43. package/dist/ai/ai-service.d.ts +66 -0
  44. package/dist/ai/ai-service.d.ts.map +1 -0
  45. package/dist/ai/ai-service.js +198 -0
  46. package/dist/ai/ai-service.js.map +1 -0
  47. package/dist/ai/ai-variable-resolver.d.ts +59 -0
  48. package/dist/ai/ai-variable-resolver.d.ts.map +1 -0
  49. package/dist/ai/ai-variable-resolver.js +219 -0
  50. package/dist/ai/ai-variable-resolver.js.map +1 -0
  51. package/dist/ai/context-collector.d.ts +30 -0
  52. package/dist/ai/context-collector.d.ts.map +1 -0
  53. package/dist/ai/context-collector.js +158 -0
  54. package/dist/ai/context-collector.js.map +1 -0
  55. package/dist/ai/cost-tracker.d.ts +41 -0
  56. package/dist/ai/cost-tracker.d.ts.map +1 -0
  57. package/dist/ai/cost-tracker.js +131 -0
  58. package/dist/ai/cost-tracker.js.map +1 -0
  59. package/dist/ai/env.d.ts +36 -0
  60. package/dist/ai/env.d.ts.map +1 -0
  61. package/dist/ai/env.js +100 -0
  62. package/dist/ai/env.js.map +1 -0
  63. package/dist/ai/index.d.ts +17 -0
  64. package/dist/ai/index.d.ts.map +1 -0
  65. package/dist/ai/index.js +25 -0
  66. package/dist/ai/index.js.map +1 -0
  67. package/dist/ai/model-router.d.ts +32 -0
  68. package/dist/ai/model-router.d.ts.map +1 -0
  69. package/dist/ai/model-router.js +113 -0
  70. package/dist/ai/model-router.js.map +1 -0
  71. package/dist/ai/output-validator.d.ts +24 -0
  72. package/dist/ai/output-validator.d.ts.map +1 -0
  73. package/dist/ai/output-validator.js +279 -0
  74. package/dist/ai/output-validator.js.map +1 -0
  75. package/dist/ai/prompt-assembler.d.ts +30 -0
  76. package/dist/ai/prompt-assembler.d.ts.map +1 -0
  77. package/dist/ai/prompt-assembler.js +93 -0
  78. package/dist/ai/prompt-assembler.js.map +1 -0
  79. package/dist/ai/prompt-pipeline.d.ts +63 -0
  80. package/dist/ai/prompt-pipeline.d.ts.map +1 -0
  81. package/dist/ai/prompt-pipeline.js +119 -0
  82. package/dist/ai/prompt-pipeline.js.map +1 -0
  83. package/dist/ai/prompt-template.jig +88 -0
  84. package/dist/ai/transports/api-transport.d.ts +12 -0
  85. package/dist/ai/transports/api-transport.d.ts.map +1 -0
  86. package/dist/ai/transports/api-transport.js +86 -0
  87. package/dist/ai/transports/api-transport.js.map +1 -0
  88. package/dist/ai/transports/command-transport.d.ts +20 -0
  89. package/dist/ai/transports/command-transport.d.ts.map +1 -0
  90. package/dist/ai/transports/command-transport.js +203 -0
  91. package/dist/ai/transports/command-transport.js.map +1 -0
  92. package/dist/ai/transports/index.d.ts +11 -0
  93. package/dist/ai/transports/index.d.ts.map +1 -0
  94. package/dist/ai/transports/index.js +10 -0
  95. package/dist/ai/transports/index.js.map +1 -0
  96. package/dist/ai/transports/resolve-transport.d.ts +15 -0
  97. package/dist/ai/transports/resolve-transport.d.ts.map +1 -0
  98. package/dist/ai/transports/resolve-transport.js +96 -0
  99. package/dist/ai/transports/resolve-transport.js.map +1 -0
  100. package/dist/ai/transports/stdout-transport.d.ts +14 -0
  101. package/dist/ai/transports/stdout-transport.d.ts.map +1 -0
  102. package/dist/ai/transports/stdout-transport.js +27 -0
  103. package/dist/ai/transports/stdout-transport.js.map +1 -0
  104. package/dist/ai/transports/types.d.ts +77 -0
  105. package/dist/ai/transports/types.d.ts.map +1 -0
  106. package/dist/ai/transports/types.js +8 -0
  107. package/dist/ai/transports/types.js.map +1 -0
  108. package/dist/commands/cookbook/info.d.ts +22 -0
  109. package/dist/commands/cookbook/info.d.ts.map +1 -0
  110. package/dist/commands/cookbook/info.js +217 -0
  111. package/dist/commands/cookbook/info.js.map +1 -0
  112. package/dist/commands/cookbook/list.d.ts +20 -0
  113. package/dist/commands/cookbook/list.d.ts.map +1 -0
  114. package/dist/commands/cookbook/list.js +133 -0
  115. package/dist/commands/cookbook/list.js.map +1 -0
  116. package/dist/commands/gen.d.ts +65 -0
  117. package/dist/commands/gen.d.ts.map +1 -0
  118. package/dist/commands/gen.js +478 -0
  119. package/dist/commands/gen.js.map +1 -0
  120. package/dist/commands/recipe/info.d.ts +18 -0
  121. package/dist/commands/recipe/info.d.ts.map +1 -0
  122. package/dist/commands/recipe/info.js +89 -0
  123. package/dist/commands/recipe/info.js.map +1 -0
  124. package/dist/commands/recipe/list.d.ts +29 -0
  125. package/dist/commands/recipe/list.d.ts.map +1 -0
  126. package/dist/commands/recipe/list.js +215 -0
  127. package/dist/commands/recipe/list.js.map +1 -0
  128. package/dist/commands/recipe/run.d.ts +44 -0
  129. package/dist/commands/recipe/run.d.ts.map +1 -0
  130. package/dist/commands/recipe/run.js +239 -0
  131. package/dist/commands/recipe/run.js.map +1 -0
  132. package/dist/commands/recipe/validate.d.ts +19 -0
  133. package/dist/commands/recipe/validate.d.ts.map +1 -0
  134. package/dist/commands/recipe/validate.js +66 -0
  135. package/dist/commands/recipe/validate.js.map +1 -0
  136. package/dist/discovery/generator-discovery.d.ts +130 -0
  137. package/dist/discovery/generator-discovery.d.ts.map +1 -0
  138. package/dist/discovery/generator-discovery.js +674 -0
  139. package/dist/discovery/generator-discovery.js.map +1 -0
  140. package/dist/discovery/index.d.ts +8 -0
  141. package/dist/discovery/index.d.ts.map +1 -0
  142. package/dist/discovery/index.js +7 -0
  143. package/dist/discovery/index.js.map +1 -0
  144. package/dist/hooks/command-not-found.d.ts +18 -0
  145. package/dist/hooks/command-not-found.d.ts.map +1 -0
  146. package/dist/hooks/command-not-found.js +182 -0
  147. package/dist/hooks/command-not-found.js.map +1 -0
  148. package/dist/hooks/suggest.d.ts +13 -0
  149. package/dist/hooks/suggest.d.ts.map +1 -0
  150. package/dist/hooks/suggest.js +28 -0
  151. package/dist/hooks/suggest.js.map +1 -0
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.d.ts.map +1 -0
  154. package/dist/index.js +3 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/base-command.d.ts +26 -0
  157. package/dist/lib/base-command.d.ts.map +1 -0
  158. package/dist/lib/base-command.js +24 -0
  159. package/dist/lib/base-command.js.map +1 -0
  160. package/dist/lib/flags.d.ts +33 -0
  161. package/dist/lib/flags.d.ts.map +1 -0
  162. package/dist/lib/flags.js +64 -0
  163. package/dist/lib/flags.js.map +1 -0
  164. package/dist/ops/add.d.ts +4 -0
  165. package/dist/ops/add.d.ts.map +1 -0
  166. package/dist/ops/add.js +85 -0
  167. package/dist/ops/add.js.map +1 -0
  168. package/dist/ops/inject.d.ts +4 -0
  169. package/dist/ops/inject.d.ts.map +1 -0
  170. package/dist/ops/inject.js +28 -0
  171. package/dist/ops/inject.js.map +1 -0
  172. package/dist/ops/injector.d.ts +4 -0
  173. package/dist/ops/injector.d.ts.map +1 -0
  174. package/dist/ops/injector.js +68 -0
  175. package/dist/ops/injector.js.map +1 -0
  176. package/dist/ops/result.d.ts +3 -0
  177. package/dist/ops/result.d.ts.map +1 -0
  178. package/dist/ops/result.js +8 -0
  179. package/dist/ops/result.js.map +1 -0
  180. package/dist/prompts/interactive-prompts.d.ts +152 -0
  181. package/dist/prompts/interactive-prompts.d.ts.map +1 -0
  182. package/dist/prompts/interactive-prompts.js +574 -0
  183. package/dist/prompts/interactive-prompts.js.map +1 -0
  184. package/dist/recipe-engine/group-executor.d.ts +97 -0
  185. package/dist/recipe-engine/group-executor.d.ts.map +1 -0
  186. package/dist/recipe-engine/group-executor.js +293 -0
  187. package/dist/recipe-engine/group-executor.js.map +1 -0
  188. package/dist/recipe-engine/index.d.ts +112 -0
  189. package/dist/recipe-engine/index.d.ts.map +1 -0
  190. package/dist/recipe-engine/index.js +223 -0
  191. package/dist/recipe-engine/index.js.map +1 -0
  192. package/dist/recipe-engine/output-evaluator.d.ts +28 -0
  193. package/dist/recipe-engine/output-evaluator.d.ts.map +1 -0
  194. package/dist/recipe-engine/output-evaluator.js +78 -0
  195. package/dist/recipe-engine/output-evaluator.js.map +1 -0
  196. package/dist/recipe-engine/recipe-engine.d.ts +227 -0
  197. package/dist/recipe-engine/recipe-engine.d.ts.map +1 -0
  198. package/dist/recipe-engine/recipe-engine.js +1036 -0
  199. package/dist/recipe-engine/recipe-engine.js.map +1 -0
  200. package/dist/recipe-engine/step-executor.d.ts +172 -0
  201. package/dist/recipe-engine/step-executor.d.ts.map +1 -0
  202. package/dist/recipe-engine/step-executor.js +802 -0
  203. package/dist/recipe-engine/step-executor.js.map +1 -0
  204. package/dist/recipe-engine/tools/action-tool.d.ts +103 -0
  205. package/dist/recipe-engine/tools/action-tool.d.ts.map +1 -0
  206. package/dist/recipe-engine/tools/action-tool.js +473 -0
  207. package/dist/recipe-engine/tools/action-tool.js.map +1 -0
  208. package/dist/recipe-engine/tools/ai-tool.d.ts +26 -0
  209. package/dist/recipe-engine/tools/ai-tool.d.ts.map +1 -0
  210. package/dist/recipe-engine/tools/ai-tool.js +233 -0
  211. package/dist/recipe-engine/tools/ai-tool.js.map +1 -0
  212. package/dist/recipe-engine/tools/base.d.ts +214 -0
  213. package/dist/recipe-engine/tools/base.d.ts.map +1 -0
  214. package/dist/recipe-engine/tools/base.js +397 -0
  215. package/dist/recipe-engine/tools/base.js.map +1 -0
  216. package/dist/recipe-engine/tools/codemod-tool.d.ts +130 -0
  217. package/dist/recipe-engine/tools/codemod-tool.d.ts.map +1 -0
  218. package/dist/recipe-engine/tools/codemod-tool.js +786 -0
  219. package/dist/recipe-engine/tools/codemod-tool.js.map +1 -0
  220. package/dist/recipe-engine/tools/ensure-dirs-tool.d.ts +21 -0
  221. package/dist/recipe-engine/tools/ensure-dirs-tool.d.ts.map +1 -0
  222. package/dist/recipe-engine/tools/ensure-dirs-tool.js +130 -0
  223. package/dist/recipe-engine/tools/ensure-dirs-tool.js.map +1 -0
  224. package/dist/recipe-engine/tools/index.d.ts +126 -0
  225. package/dist/recipe-engine/tools/index.d.ts.map +1 -0
  226. package/dist/recipe-engine/tools/index.js +290 -0
  227. package/dist/recipe-engine/tools/index.js.map +1 -0
  228. package/dist/recipe-engine/tools/install-tool.d.ts +20 -0
  229. package/dist/recipe-engine/tools/install-tool.d.ts.map +1 -0
  230. package/dist/recipe-engine/tools/install-tool.js +194 -0
  231. package/dist/recipe-engine/tools/install-tool.js.map +1 -0
  232. package/dist/recipe-engine/tools/parallel-tool.d.ts +21 -0
  233. package/dist/recipe-engine/tools/parallel-tool.d.ts.map +1 -0
  234. package/dist/recipe-engine/tools/parallel-tool.js +134 -0
  235. package/dist/recipe-engine/tools/parallel-tool.js.map +1 -0
  236. package/dist/recipe-engine/tools/patch-tool.d.ts +21 -0
  237. package/dist/recipe-engine/tools/patch-tool.d.ts.map +1 -0
  238. package/dist/recipe-engine/tools/patch-tool.js +248 -0
  239. package/dist/recipe-engine/tools/patch-tool.js.map +1 -0
  240. package/dist/recipe-engine/tools/prompt-tool.d.ts +25 -0
  241. package/dist/recipe-engine/tools/prompt-tool.d.ts.map +1 -0
  242. package/dist/recipe-engine/tools/prompt-tool.js +162 -0
  243. package/dist/recipe-engine/tools/prompt-tool.js.map +1 -0
  244. package/dist/recipe-engine/tools/query-tool.d.ts +21 -0
  245. package/dist/recipe-engine/tools/query-tool.d.ts.map +1 -0
  246. package/dist/recipe-engine/tools/query-tool.js +249 -0
  247. package/dist/recipe-engine/tools/query-tool.js.map +1 -0
  248. package/dist/recipe-engine/tools/recipe-tool.d.ts +103 -0
  249. package/dist/recipe-engine/tools/recipe-tool.d.ts.map +1 -0
  250. package/dist/recipe-engine/tools/recipe-tool.js +617 -0
  251. package/dist/recipe-engine/tools/recipe-tool.js.map +1 -0
  252. package/dist/recipe-engine/tools/registry.d.ts +151 -0
  253. package/dist/recipe-engine/tools/registry.d.ts.map +1 -0
  254. package/dist/recipe-engine/tools/registry.js +244 -0
  255. package/dist/recipe-engine/tools/registry.js.map +1 -0
  256. package/dist/recipe-engine/tools/sequence-tool.d.ts +22 -0
  257. package/dist/recipe-engine/tools/sequence-tool.d.ts.map +1 -0
  258. package/dist/recipe-engine/tools/sequence-tool.js +122 -0
  259. package/dist/recipe-engine/tools/sequence-tool.js.map +1 -0
  260. package/dist/recipe-engine/tools/shell-tool.d.ts +25 -0
  261. package/dist/recipe-engine/tools/shell-tool.d.ts.map +1 -0
  262. package/dist/recipe-engine/tools/shell-tool.js +149 -0
  263. package/dist/recipe-engine/tools/shell-tool.js.map +1 -0
  264. package/dist/recipe-engine/tools/template-tool.d.ts +88 -0
  265. package/dist/recipe-engine/tools/template-tool.d.ts.map +1 -0
  266. package/dist/recipe-engine/tools/template-tool.js +613 -0
  267. package/dist/recipe-engine/tools/template-tool.js.map +1 -0
  268. package/dist/recipe-engine/types.d.ts +963 -0
  269. package/dist/recipe-engine/types.d.ts.map +1 -0
  270. package/dist/recipe-engine/types.js +94 -0
  271. package/dist/recipe-engine/types.js.map +1 -0
  272. package/dist/template-engines/ai-tags.d.ts +26 -0
  273. package/dist/template-engines/ai-tags.d.ts.map +1 -0
  274. package/dist/template-engines/ai-tags.js +233 -0
  275. package/dist/template-engines/ai-tags.js.map +1 -0
  276. package/dist/template-engines/index.d.ts +8 -0
  277. package/dist/template-engines/index.d.ts.map +1 -0
  278. package/dist/template-engines/index.js +8 -0
  279. package/dist/template-engines/index.js.map +1 -0
  280. package/dist/template-engines/jig-engine.d.ts +47 -0
  281. package/dist/template-engines/jig-engine.d.ts.map +1 -0
  282. package/dist/template-engines/jig-engine.js +173 -0
  283. package/dist/template-engines/jig-engine.js.map +1 -0
  284. package/dist/utils/coerce-value.d.ts +7 -0
  285. package/dist/utils/coerce-value.d.ts.map +1 -0
  286. package/dist/utils/coerce-value.js +18 -0
  287. package/dist/utils/coerce-value.js.map +1 -0
  288. package/dist/utils/diff.d.ts +8 -0
  289. package/dist/utils/diff.d.ts.map +1 -0
  290. package/dist/utils/diff.js +10 -0
  291. package/dist/utils/diff.js.map +1 -0
  292. package/dist/utils/global-packages.d.ts +11 -0
  293. package/dist/utils/global-packages.d.ts.map +1 -0
  294. package/dist/utils/global-packages.js +88 -0
  295. package/dist/utils/global-packages.js.map +1 -0
  296. package/dist/utils/pager.d.ts +6 -0
  297. package/dist/utils/pager.d.ts.map +1 -0
  298. package/dist/utils/pager.js +41 -0
  299. package/dist/utils/pager.js.map +1 -0
  300. package/help/cookbook/info.md +35 -0
  301. package/help/cookbook/list.md +37 -0
  302. package/help/gen.md +26 -0
  303. package/help/recipe/run.md +52 -0
  304. package/help/recipe/validate.md +51 -0
  305. package/oclif.manifest.json +580 -0
  306. package/package.json +120 -0
@@ -0,0 +1,1036 @@
1
+ /**
2
+ * Recipe Engine - Main Orchestrator for Recipe Step System
3
+ *
4
+ * The RecipeEngine is the primary entry point for executing recipes in HyperDev.
5
+ * It provides recipe discovery, loading, validation, variable resolution, and execution
6
+ * coordination through the complete Recipe Step System.
7
+ */
8
+ import { EventEmitter } from "node:events";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { TemplateParser } from "@hypercli/core";
12
+ import { ErrorCode, ErrorHandler, HypergenError } from "@hypercli/core";
13
+ import { Logger } from "@hypercli/core";
14
+ import createDebug from "debug";
15
+ import yaml from "js-yaml";
16
+ import { AiCollector } from "#ai/ai-collector";
17
+ import { AiVariableResolver } from "#ai/ai-variable-resolver";
18
+ import { resolveTransport } from "#ai/transports/resolve-transport";
19
+ import { performInteractivePrompting } from "#prompts/interactive-prompts";
20
+ import { renderTemplate as jigRenderTemplate } from "#template-engines/jig-engine";
21
+ import { StepExecutor } from "./step-executor.js";
22
+ import { registerDefaultTools } from "./tools/index.js";
23
+ import { getToolRegistry } from "./tools/registry.js";
24
+ import { RecipeDependencyError, RecipeValidationError } from "./types.js";
25
+ /**
26
+ * Default recipe engine configuration
27
+ */
28
+ const DEFAULT_CONFIG = {
29
+ stepExecutor: {
30
+ maxConcurrency: 10,
31
+ defaultTimeout: 30000,
32
+ defaultRetries: 3,
33
+ continueOnError: false,
34
+ enableParallelExecution: true,
35
+ collectMetrics: true,
36
+ enableProgressTracking: true,
37
+ memoryWarningThreshold: 1024,
38
+ timeoutSafetyFactor: 1.2,
39
+ },
40
+ workingDir: process.cwd(),
41
+ defaultTimeout: 60000,
42
+ enableDebugLogging: false,
43
+ };
44
+ /**
45
+ * Main Recipe Engine
46
+ *
47
+ * The RecipeEngine provides the primary API for executing recipes. It handles:
48
+ * - Recipe discovery and loading from various sources
49
+ * - Recipe validation and preprocessing
50
+ * - Variable resolution with user prompts
51
+ * - Step orchestration through StepExecutor
52
+ * - Result aggregation and reporting
53
+ * - Error handling and recovery
54
+ */
55
+ export class RecipeEngine extends EventEmitter {
56
+ config;
57
+ logger;
58
+ debug;
59
+ stepExecutor;
60
+ toolRegistry;
61
+ // Execution state
62
+ activeExecutions = new Map();
63
+ executionCounter = 0;
64
+ // Caching (simple in-memory, no TTL)
65
+ recipeCache = new Map();
66
+ constructor(config = {}) {
67
+ super();
68
+ this.config = { ...DEFAULT_CONFIG, ...config };
69
+ this.logger = new Logger(console.log);
70
+ this.debug = createDebug("hyper:recipe:engine");
71
+ // Initialize tool registry and register built-in tools
72
+ this.toolRegistry = getToolRegistry();
73
+ registerDefaultTools();
74
+ // Initialize step executor
75
+ this.stepExecutor = new StepExecutor(this.toolRegistry, this.config.stepExecutor);
76
+ this.debug("Recipe engine initialized with config: %o", {
77
+ workingDir: this.config.workingDir,
78
+ });
79
+ // Set up debug logging if enabled
80
+ if (this.config.enableDebugLogging) {
81
+ const existing = process.env.DEBUG || "";
82
+ const recipeDebug = "hyper:recipe:*";
83
+ process.env.DEBUG = existing ? `${existing},${recipeDebug}` : recipeDebug;
84
+ }
85
+ // Forward step executor events
86
+ this.stepExecutor.on("execution:started", (data) => this.emit("execution:started", data));
87
+ this.stepExecutor.on("execution:completed", (data) => this.emit("execution:completed", data));
88
+ this.stepExecutor.on("execution:failed", (data) => this.emit("execution:failed", data));
89
+ this.stepExecutor.on("step:started", (data) => this.emit("step:started", data));
90
+ this.stepExecutor.on("step:completed", (data) => this.emit("step:completed", data));
91
+ this.stepExecutor.on("step:failed", (data) => this.emit("step:failed", data));
92
+ this.stepExecutor.on("phase:started", (data) => this.emit("phase:started", data));
93
+ this.stepExecutor.on("phase:completed", (data) => this.emit("phase:completed", data));
94
+ }
95
+ /**
96
+ * Execute a recipe from various sources
97
+ *
98
+ * @param source Recipe source (file path, URL, package name, or content)
99
+ * @param options Execution options including variables and behavior settings
100
+ * @returns Promise resolving to execution result
101
+ */
102
+ async executeRecipe(source, options = {}) {
103
+ const executionId = this.generateExecutionId();
104
+ const startTime = Date.now();
105
+ this.debug("Starting recipe execution [%s] from source: %o", executionId, source);
106
+ this.emit("recipe:started", { executionId, source });
107
+ try {
108
+ // Normalize source
109
+ const normalizedSource = this.normalizeSource(source);
110
+ // Load and validate recipe
111
+ const loadResult = await this.loadRecipe(normalizedSource);
112
+ const { recipe, validation } = loadResult;
113
+ if (!validation.isValid) {
114
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, `Recipe validation failed: ${validation.errors.map((e) => e.message).join(", ")}`, { executionId, errors: validation.errors.map((e) => e.message) });
115
+ }
116
+ this.debug("Recipe loaded and validated: %s", recipe.name);
117
+ // Determine effective ask mode (skipPrompts is legacy compat for --ask=nobody)
118
+ const effectiveAskMode = options.askMode ?? (options.skipPrompts ? "nobody" : undefined);
119
+ // Resolve variables with user input if needed
120
+ const resolvedVariables = await this.resolveVariables(recipe, options.variables || {}, {
121
+ askMode: effectiveAskMode,
122
+ noDefaults: options.noDefaults || false,
123
+ aiConfig: options.aiConfig,
124
+ logger: options.logger,
125
+ });
126
+ this.debug("Variables resolved: %o", Object.keys(resolvedVariables));
127
+ // Create execution context
128
+ const context = await this.createExecutionContext(recipe, resolvedVariables, options, executionId, normalizedSource);
129
+ // Create step execution options
130
+ const stepOptions = {
131
+ timeout: options.stepOptions?.timeout || this.config.defaultTimeout,
132
+ continueOnError: options.continueOnError || this.config.stepExecutor.continueOnError,
133
+ dryRun: options.dryRun || false,
134
+ ...options.stepOptions,
135
+ };
136
+ // Execute steps through StepExecutor
137
+ this.debug("Starting step execution with %d steps", recipe.steps.length);
138
+ const stepResults = await this.stepExecutor.executeSteps(recipe.steps, context, stepOptions);
139
+ // Aggregate results
140
+ const result = this.aggregateResults(executionId, recipe, stepResults, resolvedVariables, startTime, context);
141
+ // Render and print onSuccess/onError messages
142
+ await this.renderLifecycleMessage(recipe, result, resolvedVariables, options);
143
+ this.debug("Recipe execution completed [%s] in %dms", executionId, result.duration);
144
+ this.emit("recipe:completed", { executionId, result });
145
+ return result;
146
+ }
147
+ catch (error) {
148
+ const duration = Date.now() - startTime;
149
+ const normalizedSource = this.normalizeSource(source);
150
+ this.debug("Recipe execution failed [%s]: %s", executionId, error instanceof Error ? error.message : String(error));
151
+ this.emit("recipe:failed", {
152
+ executionId,
153
+ error,
154
+ duration,
155
+ source: normalizedSource,
156
+ });
157
+ if (error instanceof HypergenError) {
158
+ throw error;
159
+ }
160
+ throw ErrorHandler.createError(ErrorCode.INTERNAL_ERROR, `Recipe execution failed: ${error instanceof Error ? error.message : String(error)}`);
161
+ }
162
+ finally {
163
+ this.activeExecutions.delete(executionId);
164
+ }
165
+ }
166
+ /**
167
+ * Load a recipe from a source without executing it
168
+ */
169
+ async loadRecipe(source) {
170
+ const normalizedSource = this.normalizeSource(source);
171
+ const cacheKey = this.getCacheKey(normalizedSource);
172
+ // Check cache first
173
+ const cached = this.recipeCache.get(cacheKey);
174
+ if (cached) {
175
+ this.debug("Recipe loaded from cache: %s", cacheKey);
176
+ // Still need to validate for dependencies
177
+ const validation = await this.validateRecipe(cached);
178
+ return {
179
+ recipe: cached,
180
+ source: normalizedSource,
181
+ validation,
182
+ dependencies: [],
183
+ };
184
+ }
185
+ this.debug("Loading recipe from source: %o", normalizedSource);
186
+ // Load recipe content
187
+ const content = await this.loadRecipeContent(normalizedSource);
188
+ // Parse recipe
189
+ const recipe = await this.parseRecipeContent(content, normalizedSource);
190
+ // Validate recipe
191
+ const validation = await this.validateRecipe(recipe);
192
+ // Load dependencies
193
+ const dependencies = await this.loadDependencies(recipe);
194
+ // Cache result
195
+ if (validation.isValid) {
196
+ this.recipeCache.set(cacheKey, recipe);
197
+ }
198
+ this.debug("Recipe loaded successfully: %s", recipe.name);
199
+ return {
200
+ recipe,
201
+ source: normalizedSource,
202
+ validation,
203
+ dependencies,
204
+ };
205
+ }
206
+ /**
207
+ * Validate a recipe configuration
208
+ */
209
+ async validateRecipe(recipe) {
210
+ const errors = [];
211
+ const warnings = [];
212
+ this.debug("Validating recipe: %s", recipe.name);
213
+ // Basic validation
214
+ if (!recipe.name || typeof recipe.name !== "string") {
215
+ errors.push(new RecipeValidationError("Recipe name is required and must be a string", "MISSING_NAME"));
216
+ }
217
+ if (!recipe.variables || typeof recipe.variables !== "object") {
218
+ errors.push(new RecipeValidationError("Recipe variables section is required", "MISSING_VARIABLES"));
219
+ }
220
+ else if (typeof recipe.variables === "object" && Object.keys(recipe.variables).length === 0) {
221
+ // Allow empty variables object, don't treat as error
222
+ }
223
+ if (!Array.isArray(recipe.steps) || recipe.steps.length === 0) {
224
+ errors.push(new RecipeValidationError("Recipe must have at least one step", "MISSING_STEPS"));
225
+ }
226
+ // Validate variables
227
+ if (recipe.variables) {
228
+ for (const [varName, varConfig] of Object.entries(recipe.variables)) {
229
+ const validation = this.validateVariable(varName, varConfig);
230
+ if (validation.error) {
231
+ errors.push(new RecipeValidationError(validation.error, "INVALID_VARIABLE", {
232
+ field: `variables.${varName}`,
233
+ }));
234
+ }
235
+ }
236
+ }
237
+ // Validate steps
238
+ if (recipe.steps) {
239
+ const stepNames = new Set();
240
+ for (const [index, step] of recipe.steps.entries()) {
241
+ const stepErrors = this.validateStep(step, index, stepNames);
242
+ errors.push(...stepErrors);
243
+ }
244
+ // Validate dependencies
245
+ this.validateStepDependencies(recipe.steps, errors);
246
+ }
247
+ // Validate dependencies
248
+ if (recipe.dependencies) {
249
+ for (const dep of recipe.dependencies) {
250
+ const depValidation = await this.validateDependency(dep);
251
+ if (!depValidation.isValid) {
252
+ errors.push(new RecipeValidationError(`Dependency validation failed: ${dep.name}`, "INVALID_DEPENDENCY"));
253
+ }
254
+ }
255
+ }
256
+ const result = {
257
+ isValid: errors.length === 0,
258
+ errors,
259
+ warnings: warnings.map((w) => ({
260
+ code: "WARNING",
261
+ message: w,
262
+ severity: "warning",
263
+ suggestion: undefined,
264
+ })),
265
+ recipe,
266
+ context: {
267
+ timestamp: new Date(),
268
+ validatorVersion: "8.0.0",
269
+ scope: "full",
270
+ },
271
+ };
272
+ this.debug("Recipe validation completed: %s (errors: %d, warnings: %d)", recipe.name, errors.length, warnings.length);
273
+ return result;
274
+ }
275
+ /**
276
+ * Get current execution status
277
+ */
278
+ getExecutions() {
279
+ return Array.from(this.activeExecutions.values());
280
+ }
281
+ /**
282
+ * Cancel a recipe execution
283
+ */
284
+ async cancelExecution(executionId) {
285
+ this.debug("Cancelling execution: %s", executionId);
286
+ const execution = this.activeExecutions.get(executionId);
287
+ if (!execution) {
288
+ throw ErrorHandler.createError(ErrorCode.ACTION_NOT_FOUND, `Execution not found: ${executionId}`);
289
+ }
290
+ // Cancel through step executor
291
+ await this.stepExecutor.cancelExecution(executionId);
292
+ // Update execution status
293
+ execution.status = "cancelled";
294
+ execution.endTime = new Date();
295
+ this.emit("recipe:cancelled", { executionId });
296
+ }
297
+ /**
298
+ * Cancel all active executions
299
+ */
300
+ async cancelAllExecutions() {
301
+ this.debug("Cancelling all executions");
302
+ const promises = Array.from(this.activeExecutions.keys()).map((id) => this.cancelExecution(id));
303
+ await Promise.allSettled(promises);
304
+ }
305
+ /**
306
+ * Clean up resources
307
+ */
308
+ async cleanup() {
309
+ this.debug("Cleaning up recipe engine");
310
+ // Cancel all executions
311
+ await this.cancelAllExecutions();
312
+ // Clear caches
313
+ this.recipeCache.clear();
314
+ // Clean up step executor
315
+ await this.stepExecutor.cancelAllExecutions();
316
+ this.emit("cleanup:completed");
317
+ }
318
+ // Private implementation methods
319
+ normalizeSource(source) {
320
+ if (typeof source === "string") {
321
+ // Auto-detect source type - only file paths supported
322
+ return { type: "file", path: source };
323
+ }
324
+ return source;
325
+ }
326
+ getCacheKey(source) {
327
+ switch (source.type) {
328
+ case "file":
329
+ return `file:${source.path}`;
330
+ case "content":
331
+ return `content:${source.name}`;
332
+ default:
333
+ return "unknown";
334
+ }
335
+ }
336
+ async loadRecipeContent(source) {
337
+ switch (source.type) {
338
+ case "file":
339
+ return this.loadFileContent(source.path);
340
+ case "content":
341
+ return source.content;
342
+ default:
343
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, `Unsupported source type: ${source.type}`);
344
+ }
345
+ }
346
+ async loadFileContent(filePath) {
347
+ try {
348
+ const fullPath = path.resolve(this.config.workingDir, filePath);
349
+ if (!fs.existsSync(fullPath)) {
350
+ throw ErrorHandler.createError(ErrorCode.ACTION_NOT_FOUND, `Recipe file not found: ${fullPath}`);
351
+ }
352
+ return fs.readFileSync(fullPath, "utf-8");
353
+ }
354
+ catch (error) {
355
+ if (error instanceof HypergenError) {
356
+ throw error;
357
+ }
358
+ throw ErrorHandler.createError(ErrorCode.INTERNAL_ERROR, `Failed to load recipe file: ${error instanceof Error ? error.message : String(error)}`, { filePath });
359
+ }
360
+ }
361
+ async parseRecipeContent(content, source) {
362
+ try {
363
+ const parsed = yaml.load(content);
364
+ if (!parsed || typeof parsed !== "object") {
365
+ throw new Error("Invalid YAML format or empty content");
366
+ }
367
+ // Convert from template.yml format if needed
368
+ const recipe = {
369
+ name: parsed.name || "", // Don't provide default to trigger validation
370
+ description: parsed.description,
371
+ version: parsed.version || "1.0.0",
372
+ author: parsed.author,
373
+ category: parsed.category || "general",
374
+ tags: parsed.tags || [],
375
+ variables: parsed.variables || {},
376
+ steps: this.normalizeSteps(parsed.steps || []),
377
+ provides: this.parseProvides(parsed.provides),
378
+ examples: parsed.examples || [],
379
+ dependencies: parsed.dependencies || [],
380
+ onSuccess: parsed.onSuccess,
381
+ onError: parsed.onError,
382
+ outputs: parsed.outputs || [],
383
+ engines: parsed.engines,
384
+ hooks: parsed.hooks,
385
+ settings: parsed.settings,
386
+ composition: parsed.composition,
387
+ };
388
+ return recipe;
389
+ }
390
+ catch (error) {
391
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, `Failed to parse recipe content: ${error instanceof Error ? error.message : String(error)}`, { source });
392
+ }
393
+ }
394
+ /**
395
+ * Parse the `provides` field from recipe YAML
396
+ */
397
+ parseProvides(raw) {
398
+ if (!raw || !Array.isArray(raw))
399
+ return undefined;
400
+ const provides = [];
401
+ for (const item of raw) {
402
+ if (typeof item === "string") {
403
+ provides.push({ name: item });
404
+ }
405
+ else if (typeof item === "object" && item !== null && typeof item.name === "string") {
406
+ const entry = { name: item.name };
407
+ if (item.type && typeof item.type === "string")
408
+ entry.type = item.type;
409
+ if (item.description && typeof item.description === "string")
410
+ entry.description = item.description;
411
+ provides.push(entry);
412
+ }
413
+ }
414
+ return provides.length > 0 ? provides : undefined;
415
+ }
416
+ /**
417
+ * Normalize steps to infer tool types from shorthands
418
+ */
419
+ normalizeSteps(steps) {
420
+ return steps.map((step) => {
421
+ if (!step.tool) {
422
+ if (step.command) {
423
+ step.tool = "shell";
424
+ }
425
+ else if (step.recipe) {
426
+ step.tool = "recipe";
427
+ }
428
+ else if (step.promptType) {
429
+ // Inference for prompt
430
+ step.tool = "prompt";
431
+ }
432
+ else if (step.sequence) {
433
+ // Inference for sequence shorthand
434
+ step.tool = "sequence";
435
+ step.steps = step.sequence;
436
+ step.sequence = undefined;
437
+ }
438
+ else if (step.parallel) {
439
+ // Inference for parallel shorthand
440
+ step.tool = "parallel";
441
+ step.steps = step.parallel;
442
+ step.parallel = undefined;
443
+ }
444
+ else if (step.steps) {
445
+ // Default to sequence for generic steps property
446
+ step.tool = "sequence";
447
+ }
448
+ else if (step.template) {
449
+ step.tool = "template";
450
+ }
451
+ else if (step.action) {
452
+ step.tool = "action";
453
+ }
454
+ else if (step.codemod) {
455
+ step.tool = "codemod";
456
+ }
457
+ else if (step.packages) {
458
+ step.tool = "install";
459
+ }
460
+ }
461
+ // Map args shorthand to variableOverrides for recipe steps
462
+ if (step.args && !step.variableOverrides) {
463
+ step.variableOverrides = step.args;
464
+ step.args = undefined;
465
+ }
466
+ // Recursively normalize nested steps in sequence/parallel
467
+ if (step.steps && Array.isArray(step.steps)) {
468
+ step.steps = this.normalizeSteps(step.steps);
469
+ }
470
+ return step;
471
+ });
472
+ }
473
+ async resolveVariables(recipe, providedVariables, opts) {
474
+ const { noDefaults = false, aiConfig, logger } = opts;
475
+ // Default: interactive in TTY, error in non-TTY (matches plan)
476
+ const askMode = opts.askMode ?? (process.stdout.isTTY ? "me" : "nobody");
477
+ const resolved = {};
478
+ const missingRequired = [];
479
+ const toAsk = [];
480
+ this.debug("Resolving variables for recipe: %s (askMode=%s, noDefaults=%s)", recipe.name, askMode, noDefaults);
481
+ // Phase 1: Apply provided values and defaults
482
+ for (const [varName, varConfig] of Object.entries(recipe.variables)) {
483
+ // Step 1: Check provided value
484
+ let value = providedVariables[varName];
485
+ // Step 2: Apply default (unless --no-defaults)
486
+ if (value === undefined && !noDefaults) {
487
+ value = varConfig.default;
488
+ }
489
+ // Step 3: If still unresolved, determine if we need to ask
490
+ if (value === undefined || value === null || value === "") {
491
+ const hint = varConfig.suggestion ?? (noDefaults ? varConfig.default : undefined);
492
+ const shouldAsk = varConfig.required || noDefaults;
493
+ if (shouldAsk) {
494
+ toAsk.push({ varName, varConfig, hint });
495
+ continue;
496
+ }
497
+ // Optional, not asked about — value stays undefined
498
+ }
499
+ // Step 4: If we have a value, validate it
500
+ if (value !== undefined) {
501
+ const validation = TemplateParser.validateVariableValue(varName, value, varConfig);
502
+ if (!validation.isValid) {
503
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, validation.error || `Invalid value for variable: ${varName}`, { variable: varName, value, config: varConfig });
504
+ }
505
+ }
506
+ resolved[varName] =
507
+ value !== undefined ? value : TemplateParser.getResolvedValue(value, varConfig);
508
+ }
509
+ // Phase 2: Resolve unresolved variables based on ask mode
510
+ if (toAsk.length > 0) {
511
+ switch (askMode) {
512
+ case "me": {
513
+ // Build all prompts and run them in a single session (one intro/outro)
514
+ const promptConfigs = toAsk.map(({ varName, varConfig, hint }) => {
515
+ const configWithHint = hint !== undefined ? { ...varConfig, default: hint } : varConfig;
516
+ return {
517
+ type: this.getPromptType(configWithHint),
518
+ name: varName,
519
+ message: configWithHint.description || `Enter value for ${varName}:`,
520
+ default: configWithHint.default,
521
+ choices: configWithHint.type === "enum" ? configWithHint.values : undefined,
522
+ validate: (input) => {
523
+ const validation = TemplateParser.validateVariableValue(varName, input, varConfig);
524
+ return validation.isValid ? true : validation.error || false;
525
+ },
526
+ };
527
+ });
528
+ const loggerAdapter = logger ? { log: (msg) => logger.info(msg) } : undefined;
529
+ const answers = await performInteractivePrompting(promptConfigs, loggerAdapter);
530
+ for (const { varName, varConfig } of toAsk) {
531
+ const value = answers[varName];
532
+ const validation = TemplateParser.validateVariableValue(varName, value, varConfig);
533
+ if (!validation.isValid) {
534
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, validation.error || `Invalid value for variable: ${varName}`, { variable: varName, value });
535
+ }
536
+ resolved[varName] = value;
537
+ }
538
+ break;
539
+ }
540
+ case "ai": {
541
+ // Check transport compatibility
542
+ const transport = aiConfig ? resolveTransport(aiConfig) : null;
543
+ const transportName = transport?.name;
544
+ if (!transport || transportName === "stdout") {
545
+ this.debug("AI mode requires api or command transport, falling back to interactive");
546
+ console.warn("Warning: --ask=ai requires an API key or command transport configured. " +
547
+ "Falling back to interactive prompts.");
548
+ // Fall through to interactive (batch all prompts into one session)
549
+ const fallbackConfigs = toAsk.map(({ varName, varConfig, hint }) => {
550
+ const configWithHint = hint !== undefined ? { ...varConfig, default: hint } : varConfig;
551
+ return {
552
+ type: this.getPromptType(configWithHint),
553
+ name: varName,
554
+ message: configWithHint.description || `Enter value for ${varName}:`,
555
+ default: configWithHint.default,
556
+ choices: configWithHint.type === "enum" ? configWithHint.values : undefined,
557
+ };
558
+ });
559
+ const fbLoggerAdapter = logger ? { log: (msg) => logger.info(msg) } : undefined;
560
+ const fbAnswers = await performInteractivePrompting(fallbackConfigs, fbLoggerAdapter);
561
+ for (const { varName } of toAsk) {
562
+ resolved[varName] = fbAnswers[varName];
563
+ }
564
+ break;
565
+ }
566
+ // Batch-resolve via AI
567
+ const unresolvedVars = toAsk.map(({ varName, varConfig, hint }) => ({
568
+ name: varName,
569
+ config: varConfig,
570
+ defaultValue: noDefaults ? varConfig.default : undefined,
571
+ }));
572
+ const resolver = new AiVariableResolver(aiConfig);
573
+ const aiAnswers = await resolver.resolveBatch(unresolvedVars, resolved, {
574
+ name: recipe.name,
575
+ description: recipe.description,
576
+ });
577
+ // Apply AI answers and validate
578
+ for (const { varName, varConfig } of toAsk) {
579
+ const value = aiAnswers[varName];
580
+ if (value !== undefined) {
581
+ const validation = TemplateParser.validateVariableValue(varName, value, varConfig);
582
+ if (!validation.isValid) {
583
+ this.debug("AI value for %s failed validation: %s", varName, validation.error);
584
+ if (varConfig.required) {
585
+ missingRequired.push(varName);
586
+ }
587
+ }
588
+ else {
589
+ resolved[varName] = value;
590
+ }
591
+ }
592
+ else if (varConfig.required) {
593
+ missingRequired.push(varName);
594
+ }
595
+ }
596
+ break;
597
+ }
598
+ case "nobody": {
599
+ for (const { varName, varConfig } of toAsk) {
600
+ if (varConfig.required) {
601
+ missingRequired.push(varName);
602
+ }
603
+ }
604
+ break;
605
+ }
606
+ }
607
+ }
608
+ if (missingRequired.length > 0) {
609
+ throw ErrorHandler.createError(ErrorCode.VALIDATION_ERROR, `Missing required variables: ${missingRequired.join(", ")}`, { missingVariables: missingRequired });
610
+ }
611
+ // Add any additional provided variables not defined in recipe
612
+ for (const [varName, value] of Object.entries(providedVariables)) {
613
+ if (!recipe.variables[varName]) {
614
+ resolved[varName] = value;
615
+ }
616
+ }
617
+ this.debug("Variables resolved successfully: %o", Object.keys(resolved));
618
+ return resolved;
619
+ }
620
+ getPromptType(varConfig) {
621
+ switch (varConfig.type) {
622
+ case "boolean":
623
+ return "confirm";
624
+ case "enum":
625
+ return "list";
626
+ case "number":
627
+ return "number";
628
+ case "file":
629
+ return "input"; // Could be enhanced with file picker
630
+ case "directory":
631
+ return "input"; // Could be enhanced with directory picker
632
+ default:
633
+ return "input";
634
+ }
635
+ }
636
+ async createExecutionContext(recipe, variables, options, executionId, source) {
637
+ // Determine collect mode: if no answers provided and AiCollector is in collect mode
638
+ const collectMode = !options.answers && AiCollector.getInstance().collectMode;
639
+ return {
640
+ step: {}, // Will be set by step executor
641
+ variables: { ...variables },
642
+ projectRoot: options.workingDir || this.config.workingDir,
643
+ recipeVariables: variables,
644
+ stepResults: new Map(),
645
+ recipe: {
646
+ id: executionId,
647
+ name: recipe.name,
648
+ version: recipe.version,
649
+ startTime: new Date(),
650
+ },
651
+ stepData: {},
652
+ evaluateCondition: this.createConditionEvaluator(variables),
653
+ answers: options.answers,
654
+ collectMode,
655
+ dryRun: options.dryRun,
656
+ force: options.force,
657
+ logger: options.logger || this.logger,
658
+ templatePath: source && typeof source === "object" && source.type === "file"
659
+ ? path.dirname(source.path)
660
+ : undefined,
661
+ };
662
+ }
663
+ createConditionEvaluator(variables) {
664
+ return (expression, ctx) => {
665
+ try {
666
+ // Built-in helper functions available in condition expressions
667
+ const projectRoot = ctx.projectRoot || this.config.workingDir || process.cwd();
668
+ const builtinFunctions = {
669
+ fileExists: (filePath) => {
670
+ const resolved = path.isAbsolute(filePath)
671
+ ? filePath
672
+ : path.resolve(projectRoot, filePath);
673
+ return fs.existsSync(resolved);
674
+ },
675
+ dirExists: (dirPath) => {
676
+ const resolved = path.isAbsolute(dirPath)
677
+ ? dirPath
678
+ : path.resolve(projectRoot, dirPath);
679
+ try {
680
+ return fs.statSync(resolved).isDirectory();
681
+ }
682
+ catch {
683
+ return false;
684
+ }
685
+ },
686
+ };
687
+ // Flatten variables into scope for easier access in condition expressions
688
+ // ctx is the current context.variables dict (flat key-value), which includes
689
+ // both initial variables AND any exports from earlier steps.
690
+ const variableScope = {
691
+ ...variables,
692
+ ...ctx,
693
+ variables: { ...variables, ...ctx },
694
+ };
695
+ const mergedContext = { ...builtinFunctions, ...variableScope };
696
+ // Use a set to ensure unique argument names for Function constructor
697
+ // Filter out reserved keywords to prevent SyntaxError
698
+ const reservedKeywords = new Set([
699
+ "break",
700
+ "case",
701
+ "catch",
702
+ "class",
703
+ "const",
704
+ "continue",
705
+ "debugger",
706
+ "default",
707
+ "delete",
708
+ "do",
709
+ "else",
710
+ "export",
711
+ "extends",
712
+ "finally",
713
+ "for",
714
+ "function",
715
+ "if",
716
+ "import",
717
+ "in",
718
+ "instanceof",
719
+ "new",
720
+ "return",
721
+ "super",
722
+ "switch",
723
+ "this",
724
+ "throw",
725
+ "try",
726
+ "typeof",
727
+ "var",
728
+ "void",
729
+ "while",
730
+ "with",
731
+ "yield",
732
+ "let",
733
+ "static",
734
+ "enum",
735
+ "await",
736
+ "implements",
737
+ "interface",
738
+ "package",
739
+ "private",
740
+ "protected",
741
+ "public",
742
+ ]);
743
+ const argNames = Array.from(new Set(Object.keys(mergedContext))).filter((name) => !reservedKeywords.has(name) && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name));
744
+ const argValues = argNames.map((name) => mergedContext[name]);
745
+ const func = new Function(...argNames, `return ${expression}`);
746
+ return Boolean(func(...argValues));
747
+ }
748
+ catch (error) {
749
+ this.debug("Condition evaluation failed: %s - %s", expression, error instanceof Error ? error.message : String(error));
750
+ return false;
751
+ }
752
+ };
753
+ }
754
+ /**
755
+ * Recursively count steps including those nested in sequence/parallel tools
756
+ */
757
+ countNestedSteps(results, status, depth = 0) {
758
+ let count = 0;
759
+ for (const result of results) {
760
+ // For sequence/parallel tools, count only their nested steps (not the container itself)
761
+ // For all other tools, count the step if status matches or no filter
762
+ const isContainer = result.toolType === "sequence" || result.toolType === "parallel";
763
+ if (!isContainer && (!status || result.status === status)) {
764
+ count++;
765
+ }
766
+ // Recursively count nested steps in sequence/parallel tools
767
+ // Note: The toolResult is wrapped - it's result.toolResult.toolResult.steps, not result.toolResult.steps
768
+ // This is because the tool returns a StepResult with toolResult field, creating double nesting
769
+ if (isContainer && result.toolResult) {
770
+ const wrapped = result.toolResult;
771
+ const executionResult = wrapped.toolResult;
772
+ if (executionResult?.steps &&
773
+ Array.isArray(executionResult.steps) &&
774
+ executionResult.steps.length > 0) {
775
+ const nestedCount = this.countNestedSteps(executionResult.steps, status, depth + 1);
776
+ count += nestedCount;
777
+ }
778
+ }
779
+ }
780
+ return count;
781
+ }
782
+ aggregateResults(executionId, recipe, stepResults, variables, startTime, context) {
783
+ const duration = Date.now() - startTime;
784
+ // Count steps recursively including nested ones in sequences/parallel
785
+ const totalSteps = this.countNestedSteps(stepResults);
786
+ const completedSteps = this.countNestedSteps(stepResults, "completed");
787
+ const failedSteps = this.countNestedSteps(stepResults, "failed");
788
+ const skippedSteps = this.countNestedSteps(stepResults, "skipped");
789
+ // Aggregate file changes
790
+ const filesCreated = new Set();
791
+ const filesModified = new Set();
792
+ const filesDeleted = new Set();
793
+ const errors = [];
794
+ const warnings = [];
795
+ for (const result of stepResults) {
796
+ if (result.filesCreated) {
797
+ result.filesCreated.forEach((file) => filesCreated.add(file));
798
+ }
799
+ if (result.filesModified) {
800
+ result.filesModified.forEach((file) => filesModified.add(file));
801
+ }
802
+ if (result.filesDeleted) {
803
+ result.filesDeleted.forEach((file) => filesDeleted.add(file));
804
+ }
805
+ if (result.error) {
806
+ errors.push(`${result.stepName}: ${result.error.message}`);
807
+ }
808
+ }
809
+ // Collect providedValues from recipe.provides declarations
810
+ let providedValues;
811
+ if (recipe.provides && recipe.provides.length > 0) {
812
+ providedValues = {};
813
+ for (const p of recipe.provides) {
814
+ if (p.name in context.variables) {
815
+ providedValues[p.name] = context.variables[p.name];
816
+ }
817
+ }
818
+ if (Object.keys(providedValues).length === 0) {
819
+ providedValues = undefined;
820
+ }
821
+ }
822
+ return {
823
+ executionId,
824
+ recipe,
825
+ success: failedSteps === 0,
826
+ stepResults,
827
+ duration,
828
+ filesCreated: Array.from(filesCreated),
829
+ filesModified: Array.from(filesModified),
830
+ filesDeleted: Array.from(filesDeleted),
831
+ errors,
832
+ warnings,
833
+ variables,
834
+ metadata: {
835
+ startTime: new Date(startTime),
836
+ endTime: new Date(),
837
+ workingDir: context.projectRoot,
838
+ totalSteps,
839
+ completedSteps,
840
+ failedSteps,
841
+ skippedSteps,
842
+ providedValues,
843
+ },
844
+ };
845
+ }
846
+ /**
847
+ * Render and print onSuccess or onError message after recipe execution
848
+ */
849
+ async renderLifecycleMessage(recipe, result, variables, options) {
850
+ // Don't print lifecycle messages during collect mode (Pass 1 of 2-pass AI generation)
851
+ // — only the AI prompt document should be printed to stdout
852
+ if (AiCollector.getInstance().collectMode)
853
+ return;
854
+ const template = result.success ? recipe.onSuccess : recipe.onError;
855
+ if (!template)
856
+ return;
857
+ try {
858
+ const renderContext = {
859
+ ...variables,
860
+ recipe: {
861
+ name: recipe.name,
862
+ description: recipe.description,
863
+ version: recipe.version,
864
+ },
865
+ result: {
866
+ success: result.success,
867
+ filesCreated: result.filesCreated,
868
+ filesModified: result.filesModified,
869
+ errors: result.errors,
870
+ duration: result.duration,
871
+ },
872
+ };
873
+ const rendered = await jigRenderTemplate(template, renderContext);
874
+ const trimmed = rendered.trim();
875
+ if (trimmed) {
876
+ const logger = options.logger || this.logger;
877
+ console.log(); // blank line before message
878
+ if (result.success) {
879
+ logger.ok(trimmed);
880
+ }
881
+ else {
882
+ logger.err(trimmed);
883
+ }
884
+ }
885
+ }
886
+ catch (error) {
887
+ this.debug("Failed to render %s message: %s", result.success ? "onSuccess" : "onError", error instanceof Error ? error.message : String(error));
888
+ }
889
+ }
890
+ async loadDependencies(recipe) {
891
+ const dependencies = [];
892
+ if (!recipe.dependencies) {
893
+ return dependencies;
894
+ }
895
+ for (const dep of recipe.dependencies) {
896
+ try {
897
+ const depSource = this.dependencyToSource(dep);
898
+ const depResult = await this.loadRecipe(depSource);
899
+ if (!depResult.validation.isValid && !dep.optional) {
900
+ throw new RecipeDependencyError(`Required dependency validation failed: ${dep.name}`, dep.name, dep.version);
901
+ }
902
+ if (depResult.validation.isValid) {
903
+ dependencies.push(depResult.recipe);
904
+ }
905
+ }
906
+ catch (error) {
907
+ if (dep.optional) {
908
+ this.debug("Optional dependency failed to load: %s - %s", dep.name, error instanceof Error ? error.message : String(error));
909
+ }
910
+ else {
911
+ throw error;
912
+ }
913
+ }
914
+ }
915
+ return dependencies;
916
+ }
917
+ dependencyToSource(dependency) {
918
+ const name = typeof dependency === "string" ? dependency : dependency.name;
919
+ // Only local file dependencies supported
920
+ return { type: "file", path: name };
921
+ }
922
+ validateVariable(varName, varConfig) {
923
+ if (!varConfig || typeof varConfig !== "object") {
924
+ return { error: `Variable '${varName}' must be an object` };
925
+ }
926
+ if (!varConfig.type) {
927
+ return { error: `Variable '${varName}' must have a type` };
928
+ }
929
+ const validTypes = [
930
+ "string",
931
+ "number",
932
+ "boolean",
933
+ "enum",
934
+ "array",
935
+ "object",
936
+ "file",
937
+ "directory",
938
+ ];
939
+ if (!validTypes.includes(varConfig.type)) {
940
+ return {
941
+ error: `Variable '${varName}' has invalid type: ${varConfig.type}`,
942
+ };
943
+ }
944
+ return {};
945
+ }
946
+ validateStep(step, index, stepNames) {
947
+ const errors = [];
948
+ const stepAny = step; // Type assertion for validation context
949
+ if (!stepAny.name) {
950
+ errors.push(new RecipeValidationError(`Step ${index + 1} must have a name`, "MISSING_STEP_NAME", {
951
+ field: `steps[${index}].name`,
952
+ }));
953
+ }
954
+ else {
955
+ if (stepNames.has(stepAny.name)) {
956
+ errors.push(new RecipeValidationError(`Duplicate step name: ${stepAny.name}`, "DUPLICATE_STEP_NAME", {
957
+ field: `steps[${index}].name`,
958
+ }));
959
+ }
960
+ stepNames.add(stepAny.name);
961
+ }
962
+ if (!stepAny.tool) {
963
+ errors.push(new RecipeValidationError(`Step ${stepAny.name || index + 1} must specify a tool`, "MISSING_TOOL", { field: `steps[${index}].tool` }));
964
+ }
965
+ const validTools = [
966
+ "template",
967
+ "action",
968
+ "codemod",
969
+ "recipe",
970
+ "shell",
971
+ "prompt",
972
+ "sequence",
973
+ "parallel",
974
+ "ai",
975
+ "install",
976
+ "query",
977
+ "patch",
978
+ "ensure-dirs",
979
+ ];
980
+ if (step.tool && !validTools.includes(step.tool)) {
981
+ errors.push(new RecipeValidationError(`Step ${step.name || index + 1} has invalid tool: ${step.tool}`, "INVALID_TOOL", { field: `steps[${index}].tool` }));
982
+ }
983
+ return errors;
984
+ }
985
+ validateStepDependencies(steps, errors) {
986
+ const stepNames = new Set(steps.map((s) => s.name));
987
+ for (const step of steps) {
988
+ if (step.dependsOn) {
989
+ for (const depName of step.dependsOn) {
990
+ if (!stepNames.has(depName)) {
991
+ errors.push(new RecipeValidationError(`Step ${step.name} depends on unknown step: ${depName}`, "UNKNOWN_DEPENDENCY", { step: step.name, field: "dependsOn" }));
992
+ }
993
+ }
994
+ }
995
+ }
996
+ }
997
+ async validateDependency(dependency) {
998
+ const name = typeof dependency === "string" ? dependency : dependency.name;
999
+ if (!name) {
1000
+ return { isValid: false, error: "Dependency must have a name" };
1001
+ }
1002
+ // Basic validation - in production you'd check if package/URL exists
1003
+ return { isValid: true };
1004
+ }
1005
+ generateExecutionId() {
1006
+ return `recipe_${Date.now()}_${++this.executionCounter}`;
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Create a new recipe engine instance
1011
+ */
1012
+ export function createRecipeEngine(config) {
1013
+ return new RecipeEngine(config);
1014
+ }
1015
+ /**
1016
+ * Execute a recipe with default configuration
1017
+ */
1018
+ export async function executeRecipe(source, options) {
1019
+ const engine = createRecipeEngine();
1020
+ return await engine.executeRecipe(source, options);
1021
+ }
1022
+ /**
1023
+ * Load and validate a recipe without executing
1024
+ */
1025
+ export async function loadRecipe(source, config) {
1026
+ const engine = createRecipeEngine(config);
1027
+ return await engine.loadRecipe(source);
1028
+ }
1029
+ /**
1030
+ * Validate a recipe configuration
1031
+ */
1032
+ export async function validateRecipe(recipe, config) {
1033
+ const engine = createRecipeEngine(config);
1034
+ return await engine.validateRecipe(recipe);
1035
+ }
1036
+ //# sourceMappingURL=recipe-engine.js.map