@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,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
|
+
};
|
package/src/cli-types.ts
ADDED
|
@@ -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
|
+
};
|