@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
@@ -0,0 +1,165 @@
1
+ /**
2
+ * CLI Formatting Utilities
3
+ *
4
+ * Shared formatting functions for CLI output (both interactive and batch modes).
5
+ */
6
+
7
+ import chalk from "chalk";
8
+ import type { Effect } from "./types.js";
9
+
10
+ // Fixed width for action label column
11
+ const ACTION_LABEL_WIDTH = 14;
12
+
13
+ /**
14
+ * Filter effects to only show user-relevant ones (not internal effects).
15
+ * @param effect - The effect to check
16
+ * @param verbose - If true, include debug logs
17
+ */
18
+ export const isVisibleEffect = (effect: Effect, verbose = false): boolean => {
19
+ switch (effect._tag) {
20
+ case "WriteFile":
21
+ case "AppendFile":
22
+ case "MakeDir":
23
+ case "CopyFile":
24
+ case "CopyDirectory":
25
+ case "DeleteFile":
26
+ case "DeleteDirectory":
27
+ case "Exec":
28
+ return true;
29
+ case "Log":
30
+ // Filter out debug logs unless verbose is enabled
31
+ if (effect.level === "debug") {
32
+ return verbose;
33
+ }
34
+ return true;
35
+ // Internal effects are not shown
36
+ case "ReadFile":
37
+ case "Exists":
38
+ case "Glob":
39
+ case "ReadContext":
40
+ case "WriteContext":
41
+ case "Prompt":
42
+ case "Parallel":
43
+ case "Race":
44
+ return false;
45
+ default:
46
+ return false;
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Get human-readable action label for an effect.
52
+ */
53
+ export const getActionLabel = (effect: Effect): string => {
54
+ switch (effect._tag) {
55
+ case "WriteFile":
56
+ return "Create file";
57
+ case "AppendFile":
58
+ return "Append to";
59
+ case "MakeDir":
60
+ return "Create dir";
61
+ case "CopyFile":
62
+ return "Copy file";
63
+ case "CopyDirectory":
64
+ return "Copy dir";
65
+ case "DeleteFile":
66
+ return "Delete file";
67
+ case "DeleteDirectory":
68
+ return "Delete dir";
69
+ case "Exec":
70
+ return "Execute";
71
+ case "Log":
72
+ switch (effect.level) {
73
+ case "debug":
74
+ return "Debug";
75
+ case "info":
76
+ return "Info";
77
+ case "warn":
78
+ return "Warning";
79
+ case "error":
80
+ return "Error";
81
+ default:
82
+ return "Log";
83
+ }
84
+ default:
85
+ return effect._tag;
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Get color for action label based on effect type.
91
+ */
92
+ export const getActionColor = (
93
+ effect: Effect,
94
+ ): "green" | "red" | "yellow" | "cyan" | "blue" | "magenta" | undefined => {
95
+ switch (effect._tag) {
96
+ case "WriteFile":
97
+ case "MakeDir":
98
+ return "green";
99
+ case "AppendFile":
100
+ return "magenta";
101
+ case "DeleteFile":
102
+ case "DeleteDirectory":
103
+ return "red";
104
+ case "CopyFile":
105
+ case "CopyDirectory":
106
+ return "cyan";
107
+ case "Exec":
108
+ return "yellow";
109
+ case "Log":
110
+ switch (effect.level) {
111
+ case "error":
112
+ return "red";
113
+ case "warn":
114
+ return "yellow";
115
+ case "debug":
116
+ return undefined; // dim by default
117
+ default:
118
+ return "blue";
119
+ }
120
+ default:
121
+ return undefined;
122
+ }
123
+ };
124
+
125
+ /**
126
+ * Get the payload (description) for an effect.
127
+ */
128
+ export const getEffectPayload = (effect: Effect): string => {
129
+ switch (effect._tag) {
130
+ case "WriteFile":
131
+ return effect.path;
132
+ case "AppendFile":
133
+ return effect.path;
134
+ case "MakeDir":
135
+ return effect.path;
136
+ case "CopyFile":
137
+ return `${effect.source} → ${effect.dest}`;
138
+ case "CopyDirectory":
139
+ return `${effect.source}/ → ${effect.dest}/`;
140
+ case "DeleteFile":
141
+ case "DeleteDirectory":
142
+ return effect.path;
143
+ case "Exec":
144
+ return `${effect.command} ${effect.args.join(" ")}`;
145
+ case "Log":
146
+ return effect.message;
147
+ default:
148
+ return effect._tag;
149
+ }
150
+ };
151
+
152
+ /**
153
+ * Format a single effect as a CLI line (for non-interactive output).
154
+ */
155
+ export const formatEffectLine = (effect: Effect, isLast: boolean): string => {
156
+ const connector = isLast ? "└─" : "├─";
157
+ const actionLabel = getActionLabel(effect);
158
+ const color = getActionColor(effect);
159
+ const payload = getEffectPayload(effect);
160
+
161
+ const colorFn = color ? chalk[color] : (s: string) => s;
162
+ const paddedLabel = actionLabel.padEnd(ACTION_LABEL_WIDTH);
163
+
164
+ return `${chalk.dim(connector)} ${colorFn(paddedLabel)}${payload}`;
165
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * CLI Type Definitions
3
+ *
4
+ * Types for generator authors to validate their prompt names at compile-time.
5
+ */
6
+
7
+ /**
8
+ * Reserved option names for CLI global options.
9
+ * Generator prompts MUST NOT use these names.
10
+ *
11
+ * Use the `ForbidReserved` type to enforce at compile-time:
12
+ * @example
13
+ * ```typescript
14
+ * interface MyAnswers {
15
+ * name: string; // OK
16
+ * help: string; // Will cause type error
17
+ * }
18
+ *
19
+ * // This will produce a compile-time error if any key is reserved
20
+ * type ValidatedAnswers = ForbidReserved<MyAnswers>;
21
+ * ```
22
+ */
23
+ export type ReservedOption =
24
+ | "help"
25
+ | "version"
26
+ | "dryRun"
27
+ | "dry-run"
28
+ | "yes"
29
+ | "output"
30
+ | "preview"
31
+ | "generators"
32
+ | "run"
33
+ | "init";
34
+
35
+ /**
36
+ * Type helper that produces a compile-time error if T contains any reserved option names.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * interface BadAnswers {
41
+ * name: string;
42
+ * help: string; // Reserved!
43
+ * }
44
+ *
45
+ * // This will error: Type 'string' is not assignable to type '["Error: 'help' is a reserved option name"]'
46
+ * const generator: GeneratorDefinition<ForbidReserved<BadAnswers>> = { ... };
47
+ * ```
48
+ */
49
+ export type ForbidReserved<T> = {
50
+ [K in keyof T]: K extends ReservedOption
51
+ ? [`Error: '${K & string}' is a reserved option name`]
52
+ : T[K];
53
+ };