@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.
Files changed (45) hide show
  1. package/README.md +439 -0
  2. package/generators/example/hello/index.ts +132 -0
  3. package/generators/example/hello/templates/README.md.ejs +20 -0
  4. package/generators/example/hello/templates/index.ts.ejs +9 -0
  5. package/generators/example/webapp/index.ts +509 -0
  6. package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
  7. package/generators/example/webapp/templates/App.tsx.ejs +86 -0
  8. package/generators/example/webapp/templates/README.md.ejs +154 -0
  9. package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
  10. package/generators/example/webapp/templates/app.ts.ejs +132 -0
  11. package/generators/example/webapp/templates/feature.ts.ejs +264 -0
  12. package/generators/example/webapp/templates/index.html.ejs +20 -0
  13. package/generators/example/webapp/templates/main.tsx.ejs +43 -0
  14. package/generators/example/webapp/templates/styles.css.ejs +135 -0
  15. package/generators/init/index.ts +124 -0
  16. package/generators/init/templates/generator.ts.ejs +85 -0
  17. package/generators/init/templates/template-index.ts.ejs +9 -0
  18. package/generators/init/templates/template-test.ts.ejs +8 -0
  19. package/package.json +64 -0
  20. package/src/__tests__/combinators.test.ts +895 -0
  21. package/src/__tests__/dry-run.test.ts +927 -0
  22. package/src/__tests__/effect.test.ts +816 -0
  23. package/src/__tests__/interpreter.test.ts +673 -0
  24. package/src/__tests__/primitives.test.ts +970 -0
  25. package/src/__tests__/task.test.ts +929 -0
  26. package/src/__tests__/template.test.ts +666 -0
  27. package/src/cli-format.ts +165 -0
  28. package/src/cli-types.ts +53 -0
  29. package/src/cli.tsx +1322 -0
  30. package/src/combinators.ts +294 -0
  31. package/src/completion.ts +488 -0
  32. package/src/components/App.tsx +960 -0
  33. package/src/components/ExecutionProgress.tsx +205 -0
  34. package/src/components/FileTreePreview.tsx +97 -0
  35. package/src/components/PromptSequence.tsx +483 -0
  36. package/src/components/Spinner.tsx +36 -0
  37. package/src/components/index.ts +16 -0
  38. package/src/dry-run.ts +434 -0
  39. package/src/effect.ts +224 -0
  40. package/src/index.ts +266 -0
  41. package/src/interpreter.ts +463 -0
  42. package/src/primitives.ts +442 -0
  43. package/src/task.ts +245 -0
  44. package/src/template.ts +537 -0
  45. package/src/types.ts +453 -0
package/src/types.ts ADDED
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Core type definitions for the Summon code generator framework.
3
+ *
4
+ * This module defines the fundamental types that power the monadic task system:
5
+ * - Effects: Pure data descriptions of operations
6
+ * - Tasks: The Task monad for composable, testable generators
7
+ * - Errors: Structured error handling
8
+ * - Generator definitions: Schema for defining generators
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ // =============================================================================
14
+ // Log Levels
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Log levels for the logging primitives.
19
+ *
20
+ * - `debug` - Detailed information, only shown with `--verbose` flag
21
+ * - `info` - General information about generation progress
22
+ * - `warn` - Warning messages for non-fatal issues
23
+ * - `error` - Error messages for failures
24
+ */
25
+ export type LogLevel = "debug" | "info" | "warn" | "error";
26
+
27
+ // =============================================================================
28
+ // Prompt Types
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Base interface for all prompt types.
33
+ */
34
+ export interface PromptQuestionBase {
35
+ /** Unique identifier for this prompt, used as the answer key */
36
+ name: string;
37
+ /** Question text displayed to the user */
38
+ message: string;
39
+ /** Default value if user provides no input */
40
+ default?: unknown;
41
+ }
42
+
43
+ /**
44
+ * Free-form text input prompt.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * { name: "projectName", type: "text", message: "Project name:", default: "my-app" }
49
+ * ```
50
+ *
51
+ * CLI: `--project-name=value`
52
+ */
53
+ export interface TextPrompt extends PromptQuestionBase {
54
+ type: "text";
55
+ default?: string;
56
+ /** Validation function, returns true or error message */
57
+ validate?: (value: string) => boolean | string;
58
+ }
59
+
60
+ /**
61
+ * Boolean yes/no confirmation prompt.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * { name: "withTests", type: "confirm", message: "Include tests?", default: true }
66
+ * ```
67
+ *
68
+ * CLI: `--with-tests` (enable) or `--no-with-tests` (disable)
69
+ */
70
+ export interface ConfirmPrompt extends PromptQuestionBase {
71
+ type: "confirm";
72
+ default?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Single selection from a list of choices.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * {
81
+ * name: "framework",
82
+ * type: "select",
83
+ * message: "Framework:",
84
+ * choices: [
85
+ * { label: "React", value: "react" },
86
+ * { label: "Vue", value: "vue" },
87
+ * ],
88
+ * }
89
+ * ```
90
+ *
91
+ * CLI: `--framework=react`
92
+ */
93
+ export interface SelectPrompt extends PromptQuestionBase {
94
+ type: "select";
95
+ /** Available options to choose from */
96
+ choices: Array<{ label: string; value: string }>;
97
+ default?: string;
98
+ }
99
+
100
+ /**
101
+ * Multiple selection from a list of choices.
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * {
106
+ * name: "features",
107
+ * type: "multiselect",
108
+ * message: "Features:",
109
+ * choices: [
110
+ * { label: "TypeScript", value: "ts" },
111
+ * { label: "ESLint", value: "eslint" },
112
+ * ],
113
+ * }
114
+ * ```
115
+ *
116
+ * CLI: `--features=ts,eslint`
117
+ */
118
+ export interface MultiselectPrompt extends PromptQuestionBase {
119
+ type: "multiselect";
120
+ /** Available options to choose from */
121
+ choices: Array<{ label: string; value: string }>;
122
+ default?: string[];
123
+ }
124
+
125
+ /**
126
+ * Union of all prompt question types.
127
+ */
128
+ export type PromptQuestion =
129
+ | TextPrompt
130
+ | ConfirmPrompt
131
+ | SelectPrompt
132
+ | MultiselectPrompt;
133
+
134
+ // =============================================================================
135
+ // Effect Types - Pure data descriptions of operations
136
+ // =============================================================================
137
+
138
+ /**
139
+ * Effect represents a pure data description of a side-effecting operation.
140
+ *
141
+ * Effects are not executed directly - they are data structures that describe
142
+ * what should happen. The interpreter (production or dry-run) decides how
143
+ * to actually execute them.
144
+ *
145
+ * Each effect has a `_tag` discriminator for pattern matching.
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * // WriteFile effect
150
+ * { _tag: "WriteFile", path: "src/index.ts", content: "export {}" }
151
+ *
152
+ * // Log effect
153
+ * { _tag: "Log", level: "info", message: "Creating file..." }
154
+ * ```
155
+ */
156
+ export type Effect =
157
+ /** Read file contents as UTF-8 string */
158
+ | { _tag: "ReadFile"; path: string }
159
+ /** Write content to file, creating parent directories */
160
+ | { _tag: "WriteFile"; path: string; content: string }
161
+ /** Append content to file */
162
+ | {
163
+ _tag: "AppendFile";
164
+ path: string;
165
+ content: string;
166
+ createIfMissing: boolean;
167
+ }
168
+ /** Copy a single file */
169
+ | { _tag: "CopyFile"; source: string; dest: string }
170
+ /** Recursively copy a directory */
171
+ | { _tag: "CopyDirectory"; source: string; dest: string }
172
+ /** Delete a file */
173
+ | { _tag: "DeleteFile"; path: string }
174
+ /** Recursively delete a directory */
175
+ | { _tag: "DeleteDirectory"; path: string }
176
+ /** Create directory and parents */
177
+ | { _tag: "MakeDir"; path: string; recursive: boolean }
178
+ /** Check if path exists */
179
+ | { _tag: "Exists"; path: string }
180
+ /** Find files matching glob pattern */
181
+ | { _tag: "Glob"; pattern: string; cwd: string }
182
+ /** Execute shell command */
183
+ | { _tag: "Exec"; command: string; args: string[]; cwd?: string }
184
+ /** Interactive prompt */
185
+ | { _tag: "Prompt"; question: PromptQuestion }
186
+ /** Log message at specified level */
187
+ | { _tag: "Log"; level: LogLevel; message: string }
188
+ /** Read from task context */
189
+ | { _tag: "ReadContext"; key: string }
190
+ /** Write to task context */
191
+ | { _tag: "WriteContext"; key: string; value: unknown }
192
+ /** Run tasks in parallel */
193
+ | { _tag: "Parallel"; tasks: Task<unknown>[] }
194
+ /** Race tasks, return first to complete */
195
+ | { _tag: "Race"; tasks: Task<unknown>[] };
196
+
197
+ // =============================================================================
198
+ // Task Error
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Structured error type for task failures.
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * {
207
+ * code: "FILE_NOT_FOUND",
208
+ * message: "Cannot read file: src/missing.ts",
209
+ * context: { path: "src/missing.ts" }
210
+ * }
211
+ * ```
212
+ */
213
+ export interface TaskError {
214
+ /** Error code for programmatic handling */
215
+ code: string;
216
+ /** Human-readable error message */
217
+ message: string;
218
+ /** Original error that caused this failure */
219
+ cause?: unknown;
220
+ /** Additional context about the error */
221
+ context?: Record<string, unknown>;
222
+ /** Stack trace if available */
223
+ stack?: string;
224
+ }
225
+
226
+ // =============================================================================
227
+ // Task Monad - The core abstraction for composable generators
228
+ // =============================================================================
229
+
230
+ /**
231
+ * The Task monad - the core abstraction for composable, testable generators.
232
+ *
233
+ * A Task is a pure description of a computation that may:
234
+ * - Return a value immediately (Pure)
235
+ * - Perform an effect and continue (Effect)
236
+ * - Fail with an error (Fail)
237
+ *
238
+ * Tasks are lazy - they don't execute until interpreted by `runTask` or `dryRun`.
239
+ *
240
+ * @typeParam A - The type of the value this task produces
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * // Create a task that writes a file
245
+ * const task: Task<void> = writeFile("hello.txt", "Hello, world!");
246
+ *
247
+ * // Compose tasks
248
+ * const composed: Task<void> = sequence_([
249
+ * mkdir("src"),
250
+ * writeFile("src/index.ts", "export {}"),
251
+ * ]);
252
+ *
253
+ * // Run for real
254
+ * await runTask(task);
255
+ *
256
+ * // Or dry-run for testing
257
+ * const { effects } = dryRun(task);
258
+ * ```
259
+ */
260
+ export type Task<A> =
261
+ /** Pure value - computation complete */
262
+ | { _tag: "Pure"; value: A }
263
+ /** Effect with continuation - perform effect, then continue */
264
+ | { _tag: "Effect"; effect: Effect; cont: (result: unknown) => Task<A> }
265
+ /** Failure - computation failed with error */
266
+ | { _tag: "Fail"; error: TaskError };
267
+
268
+ // =============================================================================
269
+ // Execution Result
270
+ // =============================================================================
271
+
272
+ /**
273
+ * Result of executing a shell command.
274
+ */
275
+ export interface ExecResult {
276
+ /** Standard output from the command */
277
+ stdout: string;
278
+ /** Standard error from the command */
279
+ stderr: string;
280
+ /** Exit code (0 = success) */
281
+ exitCode: number;
282
+ }
283
+
284
+ // =============================================================================
285
+ // Generator Definition Types
286
+ // =============================================================================
287
+
288
+ /**
289
+ * Metadata for a generator, displayed in CLI help and discovery.
290
+ */
291
+ export interface GeneratorMeta {
292
+ /** Generator name, used in CLI path (e.g., "component/react") */
293
+ name: string;
294
+ /** One-line description shown in generator listings */
295
+ description: string;
296
+ /** Semantic version of the generator */
297
+ version: string;
298
+ /** Author name or email */
299
+ author?: string;
300
+ /**
301
+ * Extended help text shown when calling `summon <topic>` (without subgenerator)
302
+ * and in --help. Use this for detailed explanation and examples.
303
+ * Supports markdown-like formatting.
304
+ */
305
+ help?: string;
306
+ /**
307
+ * Usage examples shown in help. Each example should show a common invocation.
308
+ */
309
+ examples?: string[];
310
+ }
311
+
312
+ /**
313
+ * Definition of a prompt in a generator.
314
+ *
315
+ * Each prompt becomes a CLI flag. The prompt name is converted to kebab-case
316
+ * for the flag (e.g., `componentPath` → `--component-path`).
317
+ *
318
+ * Prompts can also be positional arguments by setting `positional: true`.
319
+ * Only text prompts can be positional, and only one prompt per generator
320
+ * should be marked as positional.
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * // Positional argument - can be used as:
325
+ * // summon component react src/components/Button
326
+ * // or: summon component react --component-path=src/components/Button
327
+ * {
328
+ * name: "componentPath",
329
+ * type: "text",
330
+ * message: "Component path:",
331
+ * positional: true,
332
+ * }
333
+ * ```
334
+ */
335
+ export interface PromptDefinition {
336
+ /** Unique identifier, used as answer key and CLI flag name */
337
+ name: string;
338
+ /** Question text displayed to the user */
339
+ message: string;
340
+ /** Type of input */
341
+ type: "text" | "confirm" | "select" | "multiselect";
342
+ /** Default value if user provides no input */
343
+ default?: unknown;
344
+ /** Choices for select/multiselect prompts */
345
+ choices?: Array<{ label: string; value: string }>;
346
+ /** Conditional function - prompt is skipped if this returns false */
347
+ when?: (answers: Record<string, unknown>) => boolean;
348
+ /** Validation function, returns true or error message */
349
+ validate?: (value: unknown) => boolean | string;
350
+ /**
351
+ * Group name for organizing options in --help output.
352
+ * Options without a group appear under "Options".
353
+ */
354
+ group?: string;
355
+ /**
356
+ * If true, this prompt can be provided as a positional argument.
357
+ * Only one prompt per generator should be positional.
358
+ * Only text prompts can be positional.
359
+ *
360
+ * @example
361
+ * ```bash
362
+ * # With positional: true on componentPath
363
+ * summon component react src/components/Button
364
+ * # Equivalent to:
365
+ * summon component react --component-path=src/components/Button
366
+ * ```
367
+ */
368
+ positional?: boolean;
369
+ }
370
+
371
+ /**
372
+ * The complete definition of a generator.
373
+ *
374
+ * A generator has three parts:
375
+ * 1. `meta` - Metadata for CLI display and help
376
+ * 2. `prompts` - Questions to ask the user (become CLI flags)
377
+ * 3. `generate` - Pure function that returns a Task describing what to do
378
+ *
379
+ * @typeParam TAnswers - Type of the answers object passed to generate
380
+ *
381
+ * @example
382
+ * ```typescript
383
+ * const generator = {
384
+ * meta: { name: "module", description: "Create a module", version: "1.0.0" },
385
+ * prompts: [{ name: "name", type: "text", message: "Module name:" }],
386
+ * generate: (answers) => writeFile(`src/${answers.name}.ts`, "export {}"),
387
+ * } as const satisfies GeneratorDefinition<{ name: string }>;
388
+ * ```
389
+ */
390
+ export interface GeneratorDefinition<TAnswers = Record<string, unknown>> {
391
+ /** Generator metadata for CLI display */
392
+ meta: GeneratorMeta;
393
+ /** Prompts to collect answers from user */
394
+ prompts: PromptDefinition[];
395
+ /** Pure function that returns a Task describing the generation */
396
+ generate: (answers: TAnswers) => Task<void>;
397
+ }
398
+
399
+ /**
400
+ * A generator definition without type parameters, used in barrels/collections.
401
+ * Generators with any answer type can be assigned to this.
402
+ *
403
+ * Uses `never` for contravariant position - since `generate` takes TAnswers as input,
404
+ * a GeneratorDefinition<SpecificAnswers> can accept Record<string, unknown> which
405
+ * is a supertype of SpecificAnswers.
406
+ */
407
+ // biome-ignore lint/suspicious/noExplicitAny: Required for contravariant generator collections
408
+ export type AnyGenerator = GeneratorDefinition<any>;
409
+
410
+ // =============================================================================
411
+ // Task Event Types (for RxJS integration / progress reporting)
412
+ // =============================================================================
413
+
414
+ export type TaskEvent<A> =
415
+ | { _tag: "Started"; taskId: string; timestamp: number }
416
+ | { _tag: "Progress"; message: string; percent?: number }
417
+ | { _tag: "EffectStarted"; effect: Effect; timestamp: number }
418
+ | { _tag: "EffectCompleted"; effect: Effect; duration: number }
419
+ | { _tag: "Log"; level: LogLevel; message: string }
420
+ | { _tag: "Completed"; value: A; totalDuration: number }
421
+ | { _tag: "Failed"; error: TaskError };
422
+
423
+ // =============================================================================
424
+ // Dry Run Result
425
+ // =============================================================================
426
+
427
+ export interface DryRunResult<A> {
428
+ value: A;
429
+ effects: Effect[];
430
+ }
431
+
432
+ // =============================================================================
433
+ // Tracing Types
434
+ // =============================================================================
435
+
436
+ export interface TraceSpan {
437
+ id: string;
438
+ parentId?: string;
439
+ name: string;
440
+ effect?: Effect;
441
+ startTime: number;
442
+ endTime?: number;
443
+ duration?: number;
444
+ status: "pending" | "running" | "completed" | "failed";
445
+ error?: TaskError;
446
+ children: TraceSpan[];
447
+ }
448
+
449
+ export interface TraceResult<A> {
450
+ value: A;
451
+ trace: TraceSpan;
452
+ totalDuration: number;
453
+ }