@canonical/summon 0.1.0
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/README.md +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production Interpreter
|
|
3
|
+
*
|
|
4
|
+
* This module implements the production interpreter that actually executes effects.
|
|
5
|
+
* It transforms the pure Task descriptions into real operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import type { Effect, ExecResult, Task, TaskError } from "./types.js";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Stamp Options
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export interface StampConfig {
|
|
17
|
+
/** Generator name (e.g., "@canonical/summon-package") */
|
|
18
|
+
generator: string;
|
|
19
|
+
/** Generator version (e.g., "0.1.0") */
|
|
20
|
+
version: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Comment styles for stamp generation
|
|
24
|
+
const COMMENT_STYLES: Record<
|
|
25
|
+
string,
|
|
26
|
+
{
|
|
27
|
+
single?: string;
|
|
28
|
+
blockStart?: string;
|
|
29
|
+
blockEnd?: string;
|
|
30
|
+
preferBlock?: boolean;
|
|
31
|
+
}
|
|
32
|
+
> = {
|
|
33
|
+
// JavaScript/TypeScript
|
|
34
|
+
".ts": { single: "//" },
|
|
35
|
+
".tsx": { single: "//" },
|
|
36
|
+
".js": { single: "//" },
|
|
37
|
+
".jsx": { single: "//" },
|
|
38
|
+
".mjs": { single: "//" },
|
|
39
|
+
".cjs": { single: "//" },
|
|
40
|
+
// CSS
|
|
41
|
+
".css": { blockStart: "/*", blockEnd: "*/", preferBlock: true },
|
|
42
|
+
".scss": { single: "//" },
|
|
43
|
+
".sass": { single: "//" },
|
|
44
|
+
".less": { single: "//" },
|
|
45
|
+
// HTML/XML
|
|
46
|
+
".html": { blockStart: "<!--", blockEnd: "-->" },
|
|
47
|
+
".htm": { blockStart: "<!--", blockEnd: "-->" },
|
|
48
|
+
".xml": { blockStart: "<!--", blockEnd: "-->" },
|
|
49
|
+
".svg": { blockStart: "<!--", blockEnd: "-->" },
|
|
50
|
+
".vue": { blockStart: "<!--", blockEnd: "-->" },
|
|
51
|
+
".svelte": { blockStart: "<!--", blockEnd: "-->" },
|
|
52
|
+
// Config (YAML/TOML support comments, JSON does not)
|
|
53
|
+
".yaml": { single: "#" },
|
|
54
|
+
".yml": { single: "#" },
|
|
55
|
+
".toml": { single: "#" },
|
|
56
|
+
// Shell/scripting
|
|
57
|
+
".sh": { single: "#" },
|
|
58
|
+
".bash": { single: "#" },
|
|
59
|
+
".zsh": { single: "#" },
|
|
60
|
+
".py": { single: "#" },
|
|
61
|
+
".rb": { single: "#" },
|
|
62
|
+
// Other languages
|
|
63
|
+
".go": { single: "//" },
|
|
64
|
+
".rs": { single: "//" },
|
|
65
|
+
".java": { single: "//" },
|
|
66
|
+
".kt": { single: "//" },
|
|
67
|
+
".swift": { single: "//" },
|
|
68
|
+
".c": { single: "//" },
|
|
69
|
+
".cpp": { single: "//" },
|
|
70
|
+
".h": { single: "//" },
|
|
71
|
+
".php": { single: "//" },
|
|
72
|
+
// Documentation
|
|
73
|
+
".md": { blockStart: "<!--", blockEnd: "-->" },
|
|
74
|
+
".mdx": { blockStart: "{/*", blockEnd: "*/}" },
|
|
75
|
+
// SQL
|
|
76
|
+
".sql": { single: "--" },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Apply a stamp to file content based on file extension.
|
|
81
|
+
* Returns the original content if the file type doesn't support comments.
|
|
82
|
+
*/
|
|
83
|
+
const applyStamp = (
|
|
84
|
+
filePath: string,
|
|
85
|
+
content: string,
|
|
86
|
+
stamp: StampConfig,
|
|
87
|
+
): string => {
|
|
88
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
89
|
+
const style = COMMENT_STYLES[ext];
|
|
90
|
+
|
|
91
|
+
if (!style) {
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stampText = `Generated by ${stamp.generator} v${stamp.version}`;
|
|
96
|
+
let stampLine: string;
|
|
97
|
+
|
|
98
|
+
if (style.preferBlock || (!style.single && style.blockStart)) {
|
|
99
|
+
if (!style.blockStart || !style.blockEnd) return content;
|
|
100
|
+
stampLine = `${style.blockStart} ${stampText} ${style.blockEnd}`;
|
|
101
|
+
} else if (style.single) {
|
|
102
|
+
stampLine = `${style.single} ${stampText}`;
|
|
103
|
+
} else {
|
|
104
|
+
return content;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle shebang - place stamp after it
|
|
108
|
+
if (content.startsWith("#!")) {
|
|
109
|
+
const firstNewline = content.indexOf("\n");
|
|
110
|
+
if (firstNewline !== -1) {
|
|
111
|
+
const shebang = content.slice(0, firstNewline + 1);
|
|
112
|
+
const rest = content.slice(firstNewline + 1);
|
|
113
|
+
return `${shebang}${stampLine}\n${rest}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `${stampLine}\n${content}`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// Task Execution Error
|
|
122
|
+
// =============================================================================
|
|
123
|
+
|
|
124
|
+
export class TaskExecutionError extends Error {
|
|
125
|
+
public readonly code: string;
|
|
126
|
+
public readonly taskError: TaskError;
|
|
127
|
+
|
|
128
|
+
constructor(error: TaskError) {
|
|
129
|
+
super(error.message);
|
|
130
|
+
this.name = "TaskExecutionError";
|
|
131
|
+
this.code = error.code;
|
|
132
|
+
this.taskError = error;
|
|
133
|
+
|
|
134
|
+
if (error.stack) {
|
|
135
|
+
this.stack = error.stack;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Effect Executor
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute a single effect and return the result.
|
|
146
|
+
* This is where the actual I/O happens.
|
|
147
|
+
*/
|
|
148
|
+
export const executeEffect = async (
|
|
149
|
+
effect: Effect,
|
|
150
|
+
context: Map<string, unknown>,
|
|
151
|
+
promptHandler?: (question: Effect & { _tag: "Prompt" }) => Promise<unknown>,
|
|
152
|
+
onLog?: (level: "debug" | "info" | "warn" | "error", message: string) => void,
|
|
153
|
+
stamp?: StampConfig,
|
|
154
|
+
): Promise<unknown> => {
|
|
155
|
+
switch (effect._tag) {
|
|
156
|
+
case "ReadFile": {
|
|
157
|
+
return fs.readFile(effect.path, "utf-8");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case "WriteFile": {
|
|
161
|
+
const dir = path.dirname(effect.path);
|
|
162
|
+
await fs.mkdir(dir, { recursive: true });
|
|
163
|
+
// Apply stamp if configured
|
|
164
|
+
const content = stamp
|
|
165
|
+
? applyStamp(effect.path, effect.content, stamp)
|
|
166
|
+
: effect.content;
|
|
167
|
+
await fs.writeFile(effect.path, content, "utf-8");
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "AppendFile": {
|
|
172
|
+
const dir = path.dirname(effect.path);
|
|
173
|
+
await fs.mkdir(dir, { recursive: true });
|
|
174
|
+
if (effect.createIfMissing) {
|
|
175
|
+
// Create file if it doesn't exist, then append
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(effect.path);
|
|
178
|
+
} catch {
|
|
179
|
+
// File doesn't exist, create it
|
|
180
|
+
await fs.writeFile(effect.path, "", "utf-8");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await fs.appendFile(effect.path, effect.content, "utf-8");
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case "CopyFile": {
|
|
188
|
+
const destDir = path.dirname(effect.dest);
|
|
189
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
190
|
+
await fs.copyFile(effect.source, effect.dest);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "CopyDirectory": {
|
|
195
|
+
await fs.cp(effect.source, effect.dest, { recursive: true });
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "DeleteFile": {
|
|
200
|
+
await fs.unlink(effect.path);
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "DeleteDirectory": {
|
|
205
|
+
await fs.rm(effect.path, { recursive: true, force: true });
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case "MakeDir": {
|
|
210
|
+
await fs.mkdir(effect.path, { recursive: effect.recursive });
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case "Exists": {
|
|
215
|
+
try {
|
|
216
|
+
await fs.access(effect.path);
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "Glob": {
|
|
224
|
+
// Use Bun's glob if available, otherwise fall back to fs.readdir
|
|
225
|
+
if (typeof Bun !== "undefined" && Bun.Glob) {
|
|
226
|
+
const globber = new Bun.Glob(effect.pattern);
|
|
227
|
+
const matches: string[] = [];
|
|
228
|
+
for await (const file of globber.scan({ cwd: effect.cwd })) {
|
|
229
|
+
matches.push(file);
|
|
230
|
+
}
|
|
231
|
+
return matches;
|
|
232
|
+
}
|
|
233
|
+
// Fallback: simple recursive readdir (limited glob support)
|
|
234
|
+
return simpleGlob(effect.pattern, effect.cwd);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case "Exec": {
|
|
238
|
+
if (typeof Bun !== "undefined") {
|
|
239
|
+
const proc = Bun.spawn([effect.command, ...effect.args], {
|
|
240
|
+
cwd: effect.cwd,
|
|
241
|
+
stdout: "pipe",
|
|
242
|
+
stderr: "pipe",
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const stdout = await new Response(proc.stdout).text();
|
|
246
|
+
const stderr = await new Response(proc.stderr).text();
|
|
247
|
+
const exitCode = await proc.exited;
|
|
248
|
+
|
|
249
|
+
return { stdout, stderr, exitCode } satisfies ExecResult;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Node.js fallback
|
|
253
|
+
const { spawn } = await import("node:child_process");
|
|
254
|
+
return new Promise<ExecResult>((resolve, reject) => {
|
|
255
|
+
const child = spawn(effect.command, effect.args, {
|
|
256
|
+
cwd: effect.cwd,
|
|
257
|
+
shell: false,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let stdout = "";
|
|
261
|
+
let stderr = "";
|
|
262
|
+
|
|
263
|
+
child.stdout?.on("data", (data) => {
|
|
264
|
+
stdout += data.toString();
|
|
265
|
+
});
|
|
266
|
+
child.stderr?.on("data", (data) => {
|
|
267
|
+
stderr += data.toString();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
child.on("close", (code) => {
|
|
271
|
+
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
272
|
+
});
|
|
273
|
+
child.on("error", reject);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "Prompt": {
|
|
278
|
+
if (promptHandler) {
|
|
279
|
+
return promptHandler(effect);
|
|
280
|
+
}
|
|
281
|
+
throw new TaskExecutionError({
|
|
282
|
+
code: "NO_PROMPT_HANDLER",
|
|
283
|
+
message: "No prompt handler provided for interactive prompts",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "Log": {
|
|
288
|
+
if (onLog) {
|
|
289
|
+
onLog(effect.level, effect.message);
|
|
290
|
+
} else {
|
|
291
|
+
const prefix = {
|
|
292
|
+
debug: "[DEBUG]",
|
|
293
|
+
info: "[INFO]",
|
|
294
|
+
warn: "[WARN]",
|
|
295
|
+
error: "[ERROR]",
|
|
296
|
+
}[effect.level];
|
|
297
|
+
console.log(`${prefix} ${effect.message}`);
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case "ReadContext": {
|
|
303
|
+
return context.get(effect.key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case "WriteContext": {
|
|
307
|
+
context.set(effect.key, effect.value);
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case "Parallel":
|
|
312
|
+
case "Race":
|
|
313
|
+
// These are handled specially in runTask to preserve options
|
|
314
|
+
throw new Error(
|
|
315
|
+
`${effect._tag} effect must be handled by runTask, not executeEffect directly`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// =============================================================================
|
|
321
|
+
// Simple Glob Implementation (fallback)
|
|
322
|
+
// =============================================================================
|
|
323
|
+
|
|
324
|
+
const simpleGlob = async (pattern: string, cwd: string): Promise<string[]> => {
|
|
325
|
+
const results: string[] = [];
|
|
326
|
+
|
|
327
|
+
const walk = async (dir: string, prefix: string): Promise<void> => {
|
|
328
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
const fullPath = path.join(dir, entry.name);
|
|
331
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
332
|
+
|
|
333
|
+
if (entry.isDirectory()) {
|
|
334
|
+
await walk(fullPath, relativePath);
|
|
335
|
+
} else if (matchesPattern(relativePath, pattern)) {
|
|
336
|
+
results.push(relativePath);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
await walk(cwd, "");
|
|
342
|
+
return results;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const matchesPattern = (filepath: string, pattern: string): boolean => {
|
|
346
|
+
// Very simple glob matching - just handles * and **
|
|
347
|
+
const regex = pattern
|
|
348
|
+
.replace(/\*\*/g, "<<GLOBSTAR>>")
|
|
349
|
+
.replace(/\*/g, "[^/]*")
|
|
350
|
+
.replace(/<<GLOBSTAR>>/g, ".*")
|
|
351
|
+
.replace(/\./g, "\\.");
|
|
352
|
+
return new RegExp(`^${regex}$`).test(filepath);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// =============================================================================
|
|
356
|
+
// Task Runner Options
|
|
357
|
+
// =============================================================================
|
|
358
|
+
|
|
359
|
+
export interface RunTaskOptions {
|
|
360
|
+
/** Context for storing values between effects */
|
|
361
|
+
context?: Map<string, unknown>;
|
|
362
|
+
/** Handler for interactive prompts */
|
|
363
|
+
promptHandler?: (question: Effect & { _tag: "Prompt" }) => Promise<unknown>;
|
|
364
|
+
/** Called before each effect is executed */
|
|
365
|
+
onEffectStart?: (effect: Effect) => void;
|
|
366
|
+
/** Called after each effect completes */
|
|
367
|
+
onEffectComplete?: (effect: Effect, duration: number) => void;
|
|
368
|
+
/** Handler for log effects. If provided, log output goes here instead of console */
|
|
369
|
+
onLog?: (level: "debug" | "info" | "warn" | "error", message: string) => void;
|
|
370
|
+
/** Stamp configuration for generated files. If provided, all written files get a stamp comment. */
|
|
371
|
+
stamp?: StampConfig;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// Task Runner
|
|
376
|
+
// =============================================================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Run a task to completion, executing all effects.
|
|
380
|
+
*/
|
|
381
|
+
export const runTask = async <A>(
|
|
382
|
+
task: Task<A>,
|
|
383
|
+
options: RunTaskOptions = {},
|
|
384
|
+
): Promise<A> => {
|
|
385
|
+
const {
|
|
386
|
+
context = new Map(),
|
|
387
|
+
promptHandler,
|
|
388
|
+
onEffectStart,
|
|
389
|
+
onEffectComplete,
|
|
390
|
+
onLog,
|
|
391
|
+
stamp,
|
|
392
|
+
} = options;
|
|
393
|
+
|
|
394
|
+
const runInternal = async <B>(t: Task<B>): Promise<B> => {
|
|
395
|
+
switch (t._tag) {
|
|
396
|
+
case "Pure":
|
|
397
|
+
return t.value;
|
|
398
|
+
|
|
399
|
+
case "Fail":
|
|
400
|
+
throw new TaskExecutionError(t.error);
|
|
401
|
+
|
|
402
|
+
case "Effect": {
|
|
403
|
+
const effect = t.effect;
|
|
404
|
+
|
|
405
|
+
// Handle Parallel and Race effects specially to preserve options
|
|
406
|
+
if (effect._tag === "Parallel") {
|
|
407
|
+
onEffectStart?.(effect);
|
|
408
|
+
const startTime = performance.now();
|
|
409
|
+
|
|
410
|
+
const results = await Promise.all(
|
|
411
|
+
effect.tasks.map((task) => runInternal(task)),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const duration = performance.now() - startTime;
|
|
415
|
+
onEffectComplete?.(effect, duration);
|
|
416
|
+
|
|
417
|
+
return runInternal(t.cont(results));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (effect._tag === "Race") {
|
|
421
|
+
onEffectStart?.(effect);
|
|
422
|
+
const startTime = performance.now();
|
|
423
|
+
|
|
424
|
+
const result = await Promise.race(
|
|
425
|
+
effect.tasks.map((task) => runInternal(task)),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const duration = performance.now() - startTime;
|
|
429
|
+
onEffectComplete?.(effect, duration);
|
|
430
|
+
|
|
431
|
+
return runInternal(t.cont(result));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Handle all other effects
|
|
435
|
+
onEffectStart?.(effect);
|
|
436
|
+
const startTime = performance.now();
|
|
437
|
+
|
|
438
|
+
const result = await executeEffect(
|
|
439
|
+
effect,
|
|
440
|
+
context,
|
|
441
|
+
promptHandler,
|
|
442
|
+
onLog,
|
|
443
|
+
stamp,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const duration = performance.now() - startTime;
|
|
447
|
+
onEffectComplete?.(effect, duration);
|
|
448
|
+
|
|
449
|
+
return runInternal(t.cont(result));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return runInternal(task);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Run a task with a fresh context (simple API for backward compatibility).
|
|
459
|
+
*/
|
|
460
|
+
export const run = <A>(
|
|
461
|
+
task: Task<A>,
|
|
462
|
+
promptHandler?: (question: Effect & { _tag: "Prompt" }) => Promise<unknown>,
|
|
463
|
+
): Promise<A> => runTask(task, { promptHandler });
|