@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.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/actions/communication.d.ts +201 -0
- package/dist/actions/communication.d.ts.map +1 -0
- package/dist/actions/communication.js +515 -0
- package/dist/actions/communication.js.map +1 -0
- package/dist/actions/decorator.d.ts +22 -0
- package/dist/actions/decorator.d.ts.map +1 -0
- package/dist/actions/decorator.js +110 -0
- package/dist/actions/decorator.js.map +1 -0
- package/dist/actions/executor.d.ts +85 -0
- package/dist/actions/executor.d.ts.map +1 -0
- package/dist/actions/executor.js +289 -0
- package/dist/actions/executor.js.map +1 -0
- package/dist/actions/index.d.ts +14 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +15 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/parameter-resolver.d.ts +54 -0
- package/dist/actions/parameter-resolver.d.ts.map +1 -0
- package/dist/actions/parameter-resolver.js +300 -0
- package/dist/actions/parameter-resolver.js.map +1 -0
- package/dist/actions/registry.d.ts +78 -0
- package/dist/actions/registry.d.ts.map +1 -0
- package/dist/actions/registry.js +221 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/types.d.ts +109 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/actions/types.js +31 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/actions/utils.d.ts +42 -0
- package/dist/actions/utils.d.ts.map +1 -0
- package/dist/actions/utils.js +144 -0
- package/dist/actions/utils.js.map +1 -0
- package/dist/ai/ai-collector.d.ts +52 -0
- package/dist/ai/ai-collector.d.ts.map +1 -0
- package/dist/ai/ai-collector.js +64 -0
- package/dist/ai/ai-collector.js.map +1 -0
- package/dist/ai/ai-config.d.ts +230 -0
- package/dist/ai/ai-config.d.ts.map +1 -0
- package/dist/ai/ai-config.js +8 -0
- package/dist/ai/ai-config.js.map +1 -0
- package/dist/ai/ai-service.d.ts +66 -0
- package/dist/ai/ai-service.d.ts.map +1 -0
- package/dist/ai/ai-service.js +198 -0
- package/dist/ai/ai-service.js.map +1 -0
- package/dist/ai/ai-variable-resolver.d.ts +59 -0
- package/dist/ai/ai-variable-resolver.d.ts.map +1 -0
- package/dist/ai/ai-variable-resolver.js +219 -0
- package/dist/ai/ai-variable-resolver.js.map +1 -0
- package/dist/ai/context-collector.d.ts +30 -0
- package/dist/ai/context-collector.d.ts.map +1 -0
- package/dist/ai/context-collector.js +158 -0
- package/dist/ai/context-collector.js.map +1 -0
- package/dist/ai/cost-tracker.d.ts +41 -0
- package/dist/ai/cost-tracker.d.ts.map +1 -0
- package/dist/ai/cost-tracker.js +131 -0
- package/dist/ai/cost-tracker.js.map +1 -0
- package/dist/ai/env.d.ts +36 -0
- package/dist/ai/env.d.ts.map +1 -0
- package/dist/ai/env.js +100 -0
- package/dist/ai/env.js.map +1 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +25 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/model-router.d.ts +32 -0
- package/dist/ai/model-router.d.ts.map +1 -0
- package/dist/ai/model-router.js +113 -0
- package/dist/ai/model-router.js.map +1 -0
- package/dist/ai/output-validator.d.ts +24 -0
- package/dist/ai/output-validator.d.ts.map +1 -0
- package/dist/ai/output-validator.js +279 -0
- package/dist/ai/output-validator.js.map +1 -0
- package/dist/ai/prompt-assembler.d.ts +30 -0
- package/dist/ai/prompt-assembler.d.ts.map +1 -0
- package/dist/ai/prompt-assembler.js +93 -0
- package/dist/ai/prompt-assembler.js.map +1 -0
- package/dist/ai/prompt-pipeline.d.ts +63 -0
- package/dist/ai/prompt-pipeline.d.ts.map +1 -0
- package/dist/ai/prompt-pipeline.js +119 -0
- package/dist/ai/prompt-pipeline.js.map +1 -0
- package/dist/ai/prompt-template.jig +88 -0
- package/dist/ai/transports/api-transport.d.ts +12 -0
- package/dist/ai/transports/api-transport.d.ts.map +1 -0
- package/dist/ai/transports/api-transport.js +86 -0
- package/dist/ai/transports/api-transport.js.map +1 -0
- package/dist/ai/transports/command-transport.d.ts +20 -0
- package/dist/ai/transports/command-transport.d.ts.map +1 -0
- package/dist/ai/transports/command-transport.js +203 -0
- package/dist/ai/transports/command-transport.js.map +1 -0
- package/dist/ai/transports/index.d.ts +11 -0
- package/dist/ai/transports/index.d.ts.map +1 -0
- package/dist/ai/transports/index.js +10 -0
- package/dist/ai/transports/index.js.map +1 -0
- package/dist/ai/transports/resolve-transport.d.ts +15 -0
- package/dist/ai/transports/resolve-transport.d.ts.map +1 -0
- package/dist/ai/transports/resolve-transport.js +96 -0
- package/dist/ai/transports/resolve-transport.js.map +1 -0
- package/dist/ai/transports/stdout-transport.d.ts +14 -0
- package/dist/ai/transports/stdout-transport.d.ts.map +1 -0
- package/dist/ai/transports/stdout-transport.js +27 -0
- package/dist/ai/transports/stdout-transport.js.map +1 -0
- package/dist/ai/transports/types.d.ts +77 -0
- package/dist/ai/transports/types.d.ts.map +1 -0
- package/dist/ai/transports/types.js +8 -0
- package/dist/ai/transports/types.js.map +1 -0
- package/dist/commands/cookbook/info.d.ts +22 -0
- package/dist/commands/cookbook/info.d.ts.map +1 -0
- package/dist/commands/cookbook/info.js +217 -0
- package/dist/commands/cookbook/info.js.map +1 -0
- package/dist/commands/cookbook/list.d.ts +20 -0
- package/dist/commands/cookbook/list.d.ts.map +1 -0
- package/dist/commands/cookbook/list.js +133 -0
- package/dist/commands/cookbook/list.js.map +1 -0
- package/dist/commands/gen.d.ts +65 -0
- package/dist/commands/gen.d.ts.map +1 -0
- package/dist/commands/gen.js +478 -0
- package/dist/commands/gen.js.map +1 -0
- package/dist/commands/recipe/info.d.ts +18 -0
- package/dist/commands/recipe/info.d.ts.map +1 -0
- package/dist/commands/recipe/info.js +89 -0
- package/dist/commands/recipe/info.js.map +1 -0
- package/dist/commands/recipe/list.d.ts +29 -0
- package/dist/commands/recipe/list.d.ts.map +1 -0
- package/dist/commands/recipe/list.js +215 -0
- package/dist/commands/recipe/list.js.map +1 -0
- package/dist/commands/recipe/run.d.ts +44 -0
- package/dist/commands/recipe/run.d.ts.map +1 -0
- package/dist/commands/recipe/run.js +239 -0
- package/dist/commands/recipe/run.js.map +1 -0
- package/dist/commands/recipe/validate.d.ts +19 -0
- package/dist/commands/recipe/validate.d.ts.map +1 -0
- package/dist/commands/recipe/validate.js +66 -0
- package/dist/commands/recipe/validate.js.map +1 -0
- package/dist/discovery/generator-discovery.d.ts +130 -0
- package/dist/discovery/generator-discovery.d.ts.map +1 -0
- package/dist/discovery/generator-discovery.js +674 -0
- package/dist/discovery/generator-discovery.js.map +1 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +7 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/hooks/command-not-found.d.ts +18 -0
- package/dist/hooks/command-not-found.d.ts.map +1 -0
- package/dist/hooks/command-not-found.js +182 -0
- package/dist/hooks/command-not-found.js.map +1 -0
- package/dist/hooks/suggest.d.ts +13 -0
- package/dist/hooks/suggest.d.ts.map +1 -0
- package/dist/hooks/suggest.js +28 -0
- package/dist/hooks/suggest.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/base-command.d.ts +26 -0
- package/dist/lib/base-command.d.ts.map +1 -0
- package/dist/lib/base-command.js +24 -0
- package/dist/lib/base-command.js.map +1 -0
- package/dist/lib/flags.d.ts +33 -0
- package/dist/lib/flags.d.ts.map +1 -0
- package/dist/lib/flags.js +64 -0
- package/dist/lib/flags.js.map +1 -0
- package/dist/ops/add.d.ts +4 -0
- package/dist/ops/add.d.ts.map +1 -0
- package/dist/ops/add.js +85 -0
- package/dist/ops/add.js.map +1 -0
- package/dist/ops/inject.d.ts +4 -0
- package/dist/ops/inject.d.ts.map +1 -0
- package/dist/ops/inject.js +28 -0
- package/dist/ops/inject.js.map +1 -0
- package/dist/ops/injector.d.ts +4 -0
- package/dist/ops/injector.d.ts.map +1 -0
- package/dist/ops/injector.js +68 -0
- package/dist/ops/injector.js.map +1 -0
- package/dist/ops/result.d.ts +3 -0
- package/dist/ops/result.d.ts.map +1 -0
- package/dist/ops/result.js +8 -0
- package/dist/ops/result.js.map +1 -0
- package/dist/prompts/interactive-prompts.d.ts +152 -0
- package/dist/prompts/interactive-prompts.d.ts.map +1 -0
- package/dist/prompts/interactive-prompts.js +574 -0
- package/dist/prompts/interactive-prompts.js.map +1 -0
- package/dist/recipe-engine/group-executor.d.ts +97 -0
- package/dist/recipe-engine/group-executor.d.ts.map +1 -0
- package/dist/recipe-engine/group-executor.js +293 -0
- package/dist/recipe-engine/group-executor.js.map +1 -0
- package/dist/recipe-engine/index.d.ts +112 -0
- package/dist/recipe-engine/index.d.ts.map +1 -0
- package/dist/recipe-engine/index.js +223 -0
- package/dist/recipe-engine/index.js.map +1 -0
- package/dist/recipe-engine/output-evaluator.d.ts +28 -0
- package/dist/recipe-engine/output-evaluator.d.ts.map +1 -0
- package/dist/recipe-engine/output-evaluator.js +78 -0
- package/dist/recipe-engine/output-evaluator.js.map +1 -0
- package/dist/recipe-engine/recipe-engine.d.ts +227 -0
- package/dist/recipe-engine/recipe-engine.d.ts.map +1 -0
- package/dist/recipe-engine/recipe-engine.js +1036 -0
- package/dist/recipe-engine/recipe-engine.js.map +1 -0
- package/dist/recipe-engine/step-executor.d.ts +172 -0
- package/dist/recipe-engine/step-executor.d.ts.map +1 -0
- package/dist/recipe-engine/step-executor.js +802 -0
- package/dist/recipe-engine/step-executor.js.map +1 -0
- package/dist/recipe-engine/tools/action-tool.d.ts +103 -0
- package/dist/recipe-engine/tools/action-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/action-tool.js +473 -0
- package/dist/recipe-engine/tools/action-tool.js.map +1 -0
- package/dist/recipe-engine/tools/ai-tool.d.ts +26 -0
- package/dist/recipe-engine/tools/ai-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/ai-tool.js +233 -0
- package/dist/recipe-engine/tools/ai-tool.js.map +1 -0
- package/dist/recipe-engine/tools/base.d.ts +214 -0
- package/dist/recipe-engine/tools/base.d.ts.map +1 -0
- package/dist/recipe-engine/tools/base.js +397 -0
- package/dist/recipe-engine/tools/base.js.map +1 -0
- package/dist/recipe-engine/tools/codemod-tool.d.ts +130 -0
- package/dist/recipe-engine/tools/codemod-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/codemod-tool.js +786 -0
- package/dist/recipe-engine/tools/codemod-tool.js.map +1 -0
- package/dist/recipe-engine/tools/ensure-dirs-tool.d.ts +21 -0
- package/dist/recipe-engine/tools/ensure-dirs-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/ensure-dirs-tool.js +130 -0
- package/dist/recipe-engine/tools/ensure-dirs-tool.js.map +1 -0
- package/dist/recipe-engine/tools/index.d.ts +126 -0
- package/dist/recipe-engine/tools/index.d.ts.map +1 -0
- package/dist/recipe-engine/tools/index.js +290 -0
- package/dist/recipe-engine/tools/index.js.map +1 -0
- package/dist/recipe-engine/tools/install-tool.d.ts +20 -0
- package/dist/recipe-engine/tools/install-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/install-tool.js +194 -0
- package/dist/recipe-engine/tools/install-tool.js.map +1 -0
- package/dist/recipe-engine/tools/parallel-tool.d.ts +21 -0
- package/dist/recipe-engine/tools/parallel-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/parallel-tool.js +134 -0
- package/dist/recipe-engine/tools/parallel-tool.js.map +1 -0
- package/dist/recipe-engine/tools/patch-tool.d.ts +21 -0
- package/dist/recipe-engine/tools/patch-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/patch-tool.js +248 -0
- package/dist/recipe-engine/tools/patch-tool.js.map +1 -0
- package/dist/recipe-engine/tools/prompt-tool.d.ts +25 -0
- package/dist/recipe-engine/tools/prompt-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/prompt-tool.js +162 -0
- package/dist/recipe-engine/tools/prompt-tool.js.map +1 -0
- package/dist/recipe-engine/tools/query-tool.d.ts +21 -0
- package/dist/recipe-engine/tools/query-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/query-tool.js +249 -0
- package/dist/recipe-engine/tools/query-tool.js.map +1 -0
- package/dist/recipe-engine/tools/recipe-tool.d.ts +103 -0
- package/dist/recipe-engine/tools/recipe-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/recipe-tool.js +617 -0
- package/dist/recipe-engine/tools/recipe-tool.js.map +1 -0
- package/dist/recipe-engine/tools/registry.d.ts +151 -0
- package/dist/recipe-engine/tools/registry.d.ts.map +1 -0
- package/dist/recipe-engine/tools/registry.js +244 -0
- package/dist/recipe-engine/tools/registry.js.map +1 -0
- package/dist/recipe-engine/tools/sequence-tool.d.ts +22 -0
- package/dist/recipe-engine/tools/sequence-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/sequence-tool.js +122 -0
- package/dist/recipe-engine/tools/sequence-tool.js.map +1 -0
- package/dist/recipe-engine/tools/shell-tool.d.ts +25 -0
- package/dist/recipe-engine/tools/shell-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/shell-tool.js +149 -0
- package/dist/recipe-engine/tools/shell-tool.js.map +1 -0
- package/dist/recipe-engine/tools/template-tool.d.ts +88 -0
- package/dist/recipe-engine/tools/template-tool.d.ts.map +1 -0
- package/dist/recipe-engine/tools/template-tool.js +613 -0
- package/dist/recipe-engine/tools/template-tool.js.map +1 -0
- package/dist/recipe-engine/types.d.ts +963 -0
- package/dist/recipe-engine/types.d.ts.map +1 -0
- package/dist/recipe-engine/types.js +94 -0
- package/dist/recipe-engine/types.js.map +1 -0
- package/dist/template-engines/ai-tags.d.ts +26 -0
- package/dist/template-engines/ai-tags.d.ts.map +1 -0
- package/dist/template-engines/ai-tags.js +233 -0
- package/dist/template-engines/ai-tags.js.map +1 -0
- package/dist/template-engines/index.d.ts +8 -0
- package/dist/template-engines/index.d.ts.map +1 -0
- package/dist/template-engines/index.js +8 -0
- package/dist/template-engines/index.js.map +1 -0
- package/dist/template-engines/jig-engine.d.ts +47 -0
- package/dist/template-engines/jig-engine.d.ts.map +1 -0
- package/dist/template-engines/jig-engine.js +173 -0
- package/dist/template-engines/jig-engine.js.map +1 -0
- package/dist/utils/coerce-value.d.ts +7 -0
- package/dist/utils/coerce-value.d.ts.map +1 -0
- package/dist/utils/coerce-value.js +18 -0
- package/dist/utils/coerce-value.js.map +1 -0
- package/dist/utils/diff.d.ts +8 -0
- package/dist/utils/diff.d.ts.map +1 -0
- package/dist/utils/diff.js +10 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/global-packages.d.ts +11 -0
- package/dist/utils/global-packages.d.ts.map +1 -0
- package/dist/utils/global-packages.js +88 -0
- package/dist/utils/global-packages.js.map +1 -0
- package/dist/utils/pager.d.ts +6 -0
- package/dist/utils/pager.d.ts.map +1 -0
- package/dist/utils/pager.js +41 -0
- package/dist/utils/pager.js.map +1 -0
- package/help/cookbook/info.md +35 -0
- package/help/cookbook/list.md +37 -0
- package/help/gen.md +26 -0
- package/help/recipe/run.md +52 -0
- package/help/recipe/validate.md +51 -0
- package/oclif.manifest.json +580 -0
- 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
|