@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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Combinators
|
|
3
|
+
*
|
|
4
|
+
* This module provides combinators for composing tasks in various ways:
|
|
5
|
+
* - Sequencing: Run tasks one after another
|
|
6
|
+
* - Parallelism: Run tasks concurrently
|
|
7
|
+
* - Conditionals: Run tasks based on conditions
|
|
8
|
+
* - Error handling: Retry, fallback, and recovery patterns
|
|
9
|
+
* - Resource management: Bracket pattern for cleanup
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parallelEffect, raceEffect } from "./effect.js";
|
|
13
|
+
import { effect, fail, flatMap, map, pure, recover } from "./task.js";
|
|
14
|
+
import type { Task, TaskError } from "./types.js";
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Sequencing Combinators
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run tasks in sequence, collecting results.
|
|
22
|
+
*/
|
|
23
|
+
export const sequence = <A>(tasks: Task<A>[]): Task<A[]> => {
|
|
24
|
+
if (tasks.length === 0) {
|
|
25
|
+
return pure([]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return tasks.reduce<Task<A[]>>(
|
|
29
|
+
(acc, task) =>
|
|
30
|
+
flatMap(acc, (results) => map(task, (result) => [...results, result])),
|
|
31
|
+
pure([]),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run tasks in sequence, discarding results.
|
|
37
|
+
*/
|
|
38
|
+
export const sequence_ = (tasks: Task<unknown>[]): Task<void> =>
|
|
39
|
+
map(sequence(tasks), () => undefined);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply a task-returning function to each element of an array.
|
|
43
|
+
*/
|
|
44
|
+
export const traverse = <A, B>(
|
|
45
|
+
items: A[],
|
|
46
|
+
f: (item: A, index: number) => Task<B>,
|
|
47
|
+
): Task<B[]> => sequence(items.map((item, index) => f(item, index)));
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Apply a task-returning function to each element, discarding results.
|
|
51
|
+
*/
|
|
52
|
+
export const traverse_ = <A>(
|
|
53
|
+
items: A[],
|
|
54
|
+
f: (item: A, index: number) => Task<unknown>,
|
|
55
|
+
): Task<void> => sequence_(items.map((item, index) => f(item, index)));
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Parallel Combinators
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run tasks in parallel, collecting results.
|
|
63
|
+
* All tasks must succeed for the result to succeed.
|
|
64
|
+
*/
|
|
65
|
+
export const parallel = <A>(tasks: Task<A>[]): Task<A[]> => {
|
|
66
|
+
if (tasks.length === 0) {
|
|
67
|
+
return pure([]);
|
|
68
|
+
}
|
|
69
|
+
return effect(parallelEffect(tasks as Task<unknown>[]));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run tasks in parallel with a concurrency limit.
|
|
74
|
+
*/
|
|
75
|
+
export const parallelN = <A>(n: number, tasks: Task<A>[]): Task<A[]> => {
|
|
76
|
+
if (tasks.length === 0) {
|
|
77
|
+
return pure([]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Split into batches of size n and run batches sequentially
|
|
81
|
+
const batches: Task<A>[][] = [];
|
|
82
|
+
for (let i = 0; i < tasks.length; i += n) {
|
|
83
|
+
batches.push(tasks.slice(i, i + n));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return flatMap(sequence(batches.map((batch) => parallel(batch))), (results) =>
|
|
87
|
+
pure(results.flat()),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run tasks in parallel, returning the first to complete.
|
|
93
|
+
*/
|
|
94
|
+
export const race = <A>(tasks: Task<A>[]): Task<A> => {
|
|
95
|
+
if (tasks.length === 0) {
|
|
96
|
+
return fail({
|
|
97
|
+
code: "RACE_EMPTY",
|
|
98
|
+
message: "Cannot race empty array of tasks",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return effect(raceEffect(tasks as Task<unknown>[]));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Conditional Combinators
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run a task only if a condition is true.
|
|
110
|
+
*/
|
|
111
|
+
export const when = (condition: boolean, task: Task<void>): Task<void> =>
|
|
112
|
+
condition ? task : pure(undefined);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run a task only if a condition is false.
|
|
116
|
+
*/
|
|
117
|
+
export const unless = (condition: boolean, task: Task<void>): Task<void> =>
|
|
118
|
+
condition ? pure(undefined) : task;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Choose between two tasks based on a condition.
|
|
122
|
+
*/
|
|
123
|
+
export const ifElse = <A>(
|
|
124
|
+
condition: boolean,
|
|
125
|
+
onTrue: Task<A>,
|
|
126
|
+
onFalse: Task<A>,
|
|
127
|
+
): Task<A> => (condition ? onTrue : onFalse);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Run a task only if a condition task returns true.
|
|
131
|
+
*/
|
|
132
|
+
export const whenM = (
|
|
133
|
+
conditionTask: Task<boolean>,
|
|
134
|
+
task: Task<void>,
|
|
135
|
+
): Task<void> => flatMap(conditionTask, (condition) => when(condition, task));
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Choose between two tasks based on a condition task.
|
|
139
|
+
*/
|
|
140
|
+
export const ifElseM = <A>(
|
|
141
|
+
conditionTask: Task<boolean>,
|
|
142
|
+
onTrue: Task<A>,
|
|
143
|
+
onFalse: Task<A>,
|
|
144
|
+
): Task<A> =>
|
|
145
|
+
flatMap(conditionTask, (condition) => ifElse(condition, onTrue, onFalse));
|
|
146
|
+
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// Error Handling Combinators
|
|
149
|
+
// =============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Retry a task up to n times on failure.
|
|
153
|
+
*/
|
|
154
|
+
export const retry = <A>(task: Task<A>, maxAttempts: number): Task<A> => {
|
|
155
|
+
if (maxAttempts <= 1) {
|
|
156
|
+
return task;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return recover(task, (_error) => retry(task, maxAttempts - 1));
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Retry a task with exponential backoff.
|
|
164
|
+
* Note: Delay is handled by the interpreter, this just structures the retries.
|
|
165
|
+
*/
|
|
166
|
+
export const retryWithBackoff = <A>(
|
|
167
|
+
task: Task<A>,
|
|
168
|
+
maxAttempts: number,
|
|
169
|
+
_baseDelayMs: number,
|
|
170
|
+
): Task<A> => {
|
|
171
|
+
// The actual delay handling would be in the interpreter
|
|
172
|
+
// Here we just structure the retry logic
|
|
173
|
+
return retry(task, maxAttempts);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Try the first task, falling back to the second on failure.
|
|
178
|
+
*/
|
|
179
|
+
export const orElse = <A>(primary: Task<A>, fallback: Task<A>): Task<A> =>
|
|
180
|
+
recover(primary, () => fallback);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Try a task, returning None on failure.
|
|
184
|
+
*/
|
|
185
|
+
export const optional = <A>(task: Task<A>): Task<A | undefined> =>
|
|
186
|
+
recover(
|
|
187
|
+
map(task, (a): A | undefined => a),
|
|
188
|
+
() => pure(undefined),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
type AttemptResult<A> =
|
|
192
|
+
| { ok: true; value: A }
|
|
193
|
+
| { ok: false; error: TaskError };
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Attempt a task, capturing the result or error.
|
|
197
|
+
*/
|
|
198
|
+
export const attempt = <A>(task: Task<A>): Task<AttemptResult<A>> =>
|
|
199
|
+
recover(
|
|
200
|
+
map(task, (value): AttemptResult<A> => ({ ok: true, value })),
|
|
201
|
+
(error): Task<AttemptResult<A>> => pure({ ok: false, error }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// Resource Management Combinators
|
|
206
|
+
// =============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Bracket pattern: acquire -> use -> release.
|
|
210
|
+
* Release is always called, even if use fails.
|
|
211
|
+
*/
|
|
212
|
+
export const bracket = <A, B>(
|
|
213
|
+
acquire: Task<A>,
|
|
214
|
+
use: (resource: A) => Task<B>,
|
|
215
|
+
release: (resource: A) => Task<void>,
|
|
216
|
+
): Task<B> =>
|
|
217
|
+
flatMap(acquire, (resource) =>
|
|
218
|
+
recover(
|
|
219
|
+
flatMap(use(resource), (result) => map(release(resource), () => result)),
|
|
220
|
+
(error) => flatMap(release(resource), () => fail(error)),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Ensure a cleanup task runs after the main task, regardless of success/failure.
|
|
226
|
+
*/
|
|
227
|
+
export const ensure = <A>(task: Task<A>, cleanup: Task<void>): Task<A> =>
|
|
228
|
+
recover(
|
|
229
|
+
flatMap(task, (result) => map(cleanup, () => result)),
|
|
230
|
+
(error) => flatMap(cleanup, () => fail(error)),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Utility Combinators
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Execute a side effect without changing the task's value.
|
|
239
|
+
*/
|
|
240
|
+
export const tap = <A>(task: Task<A>, f: (a: A) => Task<unknown>): Task<A> =>
|
|
241
|
+
flatMap(task, (a) => map(f(a), () => a));
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Execute a side effect on failure without changing the error.
|
|
245
|
+
*/
|
|
246
|
+
export const tapError = <A>(
|
|
247
|
+
task: Task<A>,
|
|
248
|
+
f: (error: TaskError) => Task<unknown>,
|
|
249
|
+
): Task<A> => recover(task, (error) => flatMap(f(error), () => fail(error)));
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Delay the execution of a task.
|
|
253
|
+
* Note: The actual delay is handled by the interpreter.
|
|
254
|
+
*/
|
|
255
|
+
export const delay = <A>(task: Task<A>, _ms: number): Task<A> => {
|
|
256
|
+
// The interpreter would handle the actual delay
|
|
257
|
+
return task;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Add a timeout to a task.
|
|
262
|
+
* Note: The actual timeout is handled by the interpreter.
|
|
263
|
+
*/
|
|
264
|
+
export const timeout = <A>(task: Task<A>, _ms: number): Task<A> => {
|
|
265
|
+
// The interpreter would handle the actual timeout
|
|
266
|
+
return task;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Fold over a task, handling both success and failure.
|
|
271
|
+
*/
|
|
272
|
+
export const fold = <A, B>(
|
|
273
|
+
task: Task<A>,
|
|
274
|
+
onSuccess: (a: A) => B,
|
|
275
|
+
onFailure: (error: TaskError) => B,
|
|
276
|
+
): Task<B> => recover(map(task, onSuccess), (error) => pure(onFailure(error)));
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Combine two tasks into a tuple.
|
|
280
|
+
*/
|
|
281
|
+
export const zip = <A, B>(taskA: Task<A>, taskB: Task<B>): Task<[A, B]> =>
|
|
282
|
+
flatMap(taskA, (a) => map(taskB, (b): [A, B] => [a, b]));
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Combine three tasks into a tuple.
|
|
286
|
+
*/
|
|
287
|
+
export const zip3 = <A, B, C>(
|
|
288
|
+
taskA: Task<A>,
|
|
289
|
+
taskB: Task<B>,
|
|
290
|
+
taskC: Task<C>,
|
|
291
|
+
): Task<[A, B, C]> =>
|
|
292
|
+
flatMap(taskA, (a) =>
|
|
293
|
+
flatMap(taskB, (b) => map(taskC, (c): [A, B, C] => [a, b, c])),
|
|
294
|
+
);
|