@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,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= pascalCase(name) %></title>
|
|
7
|
+
<meta name="description" content="<%= description %>">
|
|
8
|
+
<% if (styling === 'tailwind') { -%>
|
|
9
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
<% } -%>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<% if (framework === 'react') { -%>
|
|
15
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
16
|
+
<% } else { -%>
|
|
17
|
+
<script type="module" src="/src/main.ts"></script>
|
|
18
|
+
<% } -%>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Generated by summon webapp
|
|
2
|
+
// <%= description %>
|
|
3
|
+
|
|
4
|
+
<% if (framework === 'react') { -%>
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom/client';
|
|
7
|
+
import { App } from './App';
|
|
8
|
+
<% if (styling !== 'none') { -%>
|
|
9
|
+
import './styles.css';
|
|
10
|
+
<% } -%>
|
|
11
|
+
<% if (hasRouter) { -%>
|
|
12
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
13
|
+
<% } -%>
|
|
14
|
+
|
|
15
|
+
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
16
|
+
|
|
17
|
+
root.render(
|
|
18
|
+
<React.StrictMode>
|
|
19
|
+
<% if (hasRouter) { -%>
|
|
20
|
+
<BrowserRouter>
|
|
21
|
+
<App />
|
|
22
|
+
</BrowserRouter>
|
|
23
|
+
<% } else { -%>
|
|
24
|
+
<App />
|
|
25
|
+
<% } -%>
|
|
26
|
+
</React.StrictMode>
|
|
27
|
+
);
|
|
28
|
+
<% } else { -%>
|
|
29
|
+
import { init } from './app';
|
|
30
|
+
<% if (styling !== 'none') { -%>
|
|
31
|
+
import './styles.css';
|
|
32
|
+
<% } -%>
|
|
33
|
+
|
|
34
|
+
// Initialize the application
|
|
35
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
36
|
+
const appElement = document.getElementById('root');
|
|
37
|
+
if (appElement) {
|
|
38
|
+
init(appElement);
|
|
39
|
+
} else {
|
|
40
|
+
console.error('Root element not found');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
<% } -%>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Generated by summon webapp */
|
|
2
|
+
/* Styles for <%= name %> */
|
|
3
|
+
|
|
4
|
+
<% if (styling === 'tailwind') { -%>
|
|
5
|
+
@tailwind base;
|
|
6
|
+
@tailwind components;
|
|
7
|
+
@tailwind utilities;
|
|
8
|
+
|
|
9
|
+
/* Custom styles can be added here */
|
|
10
|
+
<% } else { -%>
|
|
11
|
+
/* Reset and base styles */
|
|
12
|
+
*,
|
|
13
|
+
*::before,
|
|
14
|
+
*::after {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html {
|
|
21
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
22
|
+
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
color: #333;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
background-color: #f5f5f5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Container */
|
|
33
|
+
.app-container {
|
|
34
|
+
max-width: 800px;
|
|
35
|
+
margin: 0 auto;
|
|
36
|
+
padding: 2rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Header */
|
|
40
|
+
.app-header {
|
|
41
|
+
text-align: center;
|
|
42
|
+
margin-bottom: 2rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.app-header h1 {
|
|
46
|
+
font-size: 2.5rem;
|
|
47
|
+
font-weight: bold;
|
|
48
|
+
color: #1a1a1a;
|
|
49
|
+
margin-bottom: 0.5rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.app-header p {
|
|
53
|
+
color: #666;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Main content */
|
|
57
|
+
.app-main {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 1.5rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Cards */
|
|
64
|
+
.card {
|
|
65
|
+
background: white;
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
68
|
+
padding: 1.5rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.card h2 {
|
|
72
|
+
font-size: 1.25rem;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
margin-bottom: 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Buttons */
|
|
78
|
+
.btn {
|
|
79
|
+
padding: 0.5rem 1rem;
|
|
80
|
+
border: none;
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
font-size: 1rem;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: background-color 0.2s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn-success {
|
|
88
|
+
background-color: #22c55e;
|
|
89
|
+
color: white;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.btn-success:hover {
|
|
93
|
+
background-color: #16a34a;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.btn-danger {
|
|
97
|
+
background-color: #ef4444;
|
|
98
|
+
color: white;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.btn-danger:hover {
|
|
102
|
+
background-color: #dc2626;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Counter */
|
|
106
|
+
.counter {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 1rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.count {
|
|
113
|
+
font-size: 1.5rem;
|
|
114
|
+
font-family: monospace;
|
|
115
|
+
min-width: 3ch;
|
|
116
|
+
text-align: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Feature list */
|
|
120
|
+
.feature-list {
|
|
121
|
+
list-style: disc;
|
|
122
|
+
padding-left: 1.5rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.feature-list li {
|
|
126
|
+
margin-bottom: 0.5rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Footer */
|
|
130
|
+
.app-footer {
|
|
131
|
+
text-align: center;
|
|
132
|
+
margin-top: 2rem;
|
|
133
|
+
color: #999;
|
|
134
|
+
}
|
|
135
|
+
<% } -%>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Generator
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a new Summon generator with templates and test structure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { GeneratorDefinition } from "../../src/index.js";
|
|
10
|
+
import {
|
|
11
|
+
info,
|
|
12
|
+
mkdir,
|
|
13
|
+
sequence_,
|
|
14
|
+
template,
|
|
15
|
+
withHelpers,
|
|
16
|
+
} from "../../src/index.js";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
interface Answers {
|
|
21
|
+
/** Generator path (e.g., "my-gen" or "component/vue") */
|
|
22
|
+
generatorPath: string;
|
|
23
|
+
/** Description of the generator */
|
|
24
|
+
description: string;
|
|
25
|
+
/** Output directory for generators */
|
|
26
|
+
outputDir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const generator: GeneratorDefinition<Answers> = {
|
|
30
|
+
meta: {
|
|
31
|
+
name: "init",
|
|
32
|
+
description: "Create a new Summon generator",
|
|
33
|
+
version: "0.1.0",
|
|
34
|
+
help: `Scaffold a new generator with templates and tests.
|
|
35
|
+
|
|
36
|
+
The generator path supports nested structures:
|
|
37
|
+
summon init --generator-path=my-gen # Creates generators/my-gen/
|
|
38
|
+
summon init --generator-path=component/vue # Creates generators/component/vue/
|
|
39
|
+
|
|
40
|
+
GENERATED FILES:
|
|
41
|
+
generators/<path>/
|
|
42
|
+
├── index.ts # Generator definition
|
|
43
|
+
└── templates/
|
|
44
|
+
├── index.ts.ejs # Main file template
|
|
45
|
+
└── index.test.ts.ejs # Test file template`,
|
|
46
|
+
examples: [
|
|
47
|
+
"summon init",
|
|
48
|
+
"summon init --generator-path=my-gen",
|
|
49
|
+
"summon init --generator-path=component/vue",
|
|
50
|
+
"summon init --generator-path=api/rest --output-dir=./custom-generators",
|
|
51
|
+
"summon init --generator-path=util --dry-run",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
prompts: [
|
|
56
|
+
{
|
|
57
|
+
name: "generatorPath",
|
|
58
|
+
type: "text",
|
|
59
|
+
message: "Generator path (e.g., my-gen or component/vue):",
|
|
60
|
+
default: "my-generator",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "description",
|
|
64
|
+
type: "text",
|
|
65
|
+
message: "Description:",
|
|
66
|
+
default: "A custom generator",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "outputDir",
|
|
70
|
+
type: "text",
|
|
71
|
+
message: "Output directory:",
|
|
72
|
+
default: "./generators",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
generate: (answers) => {
|
|
77
|
+
// Parse the generator path into segments
|
|
78
|
+
const normalizedPath = answers.generatorPath.replace(/\s+/g, "/");
|
|
79
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
80
|
+
const name = segments[segments.length - 1];
|
|
81
|
+
const generatorDir = path.join(answers.outputDir, ...segments);
|
|
82
|
+
const templatesDir = path.join(generatorDir, "templates");
|
|
83
|
+
|
|
84
|
+
const vars = withHelpers({
|
|
85
|
+
name,
|
|
86
|
+
description: answers.description,
|
|
87
|
+
generatorPath: normalizedPath,
|
|
88
|
+
commandPath: segments.join(" "),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return sequence_([
|
|
92
|
+
info(`Creating generator: ${name}`),
|
|
93
|
+
|
|
94
|
+
// Create directories
|
|
95
|
+
mkdir(generatorDir),
|
|
96
|
+
mkdir(templatesDir),
|
|
97
|
+
|
|
98
|
+
// Create generator index.ts
|
|
99
|
+
template({
|
|
100
|
+
source: path.join(__dirname, "templates", "generator.ts.ejs"),
|
|
101
|
+
dest: path.join(generatorDir, "index.ts"),
|
|
102
|
+
vars,
|
|
103
|
+
}),
|
|
104
|
+
|
|
105
|
+
// Create template files
|
|
106
|
+
template({
|
|
107
|
+
source: path.join(__dirname, "templates", "template-index.ts.ejs"),
|
|
108
|
+
dest: path.join(templatesDir, "index.ts.ejs"),
|
|
109
|
+
vars,
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
template({
|
|
113
|
+
source: path.join(__dirname, "templates", "template-test.ts.ejs"),
|
|
114
|
+
dest: path.join(templatesDir, "index.test.ts.ejs"),
|
|
115
|
+
vars,
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
info(`Created generator at ${generatorDir}`),
|
|
119
|
+
info(`Run with: summon ${segments.join(" ")}`),
|
|
120
|
+
]);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default generator;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <%= name %> Generator
|
|
3
|
+
*
|
|
4
|
+
* <%= description %>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { GeneratorDefinition } from "@canonical/summon";
|
|
10
|
+
import {
|
|
11
|
+
info,
|
|
12
|
+
mkdir,
|
|
13
|
+
sequence_,
|
|
14
|
+
template,
|
|
15
|
+
when,
|
|
16
|
+
withHelpers,
|
|
17
|
+
} from "@canonical/summon";
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
interface Answers {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
withTests: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const generator: GeneratorDefinition<Answers> = {
|
|
28
|
+
meta: {
|
|
29
|
+
name: "<%= name %>",
|
|
30
|
+
description: "<%= description %>",
|
|
31
|
+
version: "0.1.0",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
prompts: [
|
|
35
|
+
{
|
|
36
|
+
name: "name",
|
|
37
|
+
type: "text",
|
|
38
|
+
message: "Name:",
|
|
39
|
+
default: "my-module",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "description",
|
|
43
|
+
type: "text",
|
|
44
|
+
message: "Description:",
|
|
45
|
+
default: "",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "withTests",
|
|
49
|
+
type: "confirm",
|
|
50
|
+
message: "Include tests?",
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
generate: (answers) => {
|
|
56
|
+
const vars = withHelpers({
|
|
57
|
+
...answers,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return sequence_([
|
|
61
|
+
info(`Generating ${answers.name}...`),
|
|
62
|
+
|
|
63
|
+
mkdir(answers.name),
|
|
64
|
+
|
|
65
|
+
template({
|
|
66
|
+
source: path.join(__dirname, "templates", "index.ts.ejs"),
|
|
67
|
+
dest: path.join(answers.name, "index.ts"),
|
|
68
|
+
vars,
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
when(
|
|
72
|
+
answers.withTests,
|
|
73
|
+
template({
|
|
74
|
+
source: path.join(__dirname, "templates", "index.test.ts.ejs"),
|
|
75
|
+
dest: path.join(answers.name, "index.test.ts"),
|
|
76
|
+
vars,
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
|
|
80
|
+
info(`Created ${answers.name}`),
|
|
81
|
+
]);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default generator;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canonical/summon",
|
|
3
|
+
"description": "A monadic task-centric code generator framework with React Ink CLI.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"summon": "src/cli.tsx"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"generators"
|
|
14
|
+
],
|
|
15
|
+
"author": {
|
|
16
|
+
"email": "webteam@canonical.com",
|
|
17
|
+
"name": "Canonical Webteam"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/canonical/pragma"
|
|
22
|
+
},
|
|
23
|
+
"license": "GPL-3.0",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/canonical/pragma/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/canonical/pragma#readme",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "echo 'No build needed - runs directly from TypeScript'",
|
|
30
|
+
"build:all": "bun run build",
|
|
31
|
+
"check": "bun run check:biome && bun run check:ts && bun run check:webarchitect",
|
|
32
|
+
"check:fix": "bun run check:biome:fix && bun run check:ts",
|
|
33
|
+
"check:biome": "biome check",
|
|
34
|
+
"check:biome:fix": "biome check --write",
|
|
35
|
+
"check:ts": "tsc --noEmit",
|
|
36
|
+
"check:webarchitect": "webarchitect tool-ts",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"test:coverage": "vitest run --coverage"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@biomejs/biome": "2.3.11",
|
|
43
|
+
"@canonical/biome-config": "^0.11.0",
|
|
44
|
+
"@canonical/typescript-config-base": "^0.11.0",
|
|
45
|
+
"@types/ejs": "^3.1.5",
|
|
46
|
+
"@types/omelette": "^0.4.5",
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"bun-types": "^1.0.0",
|
|
49
|
+
"ink-testing-library": "^4.0.0",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^3.2.1"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"chalk": "^5.6.2",
|
|
55
|
+
"commander": "^14.0.2",
|
|
56
|
+
"ejs": "^3.1.10",
|
|
57
|
+
"ink": "^6.0.0",
|
|
58
|
+
"ink-select-input": "^6.0.0",
|
|
59
|
+
"ink-spinner": "^5.0.0",
|
|
60
|
+
"ink-text-input": "^6.0.0",
|
|
61
|
+
"omelette": "^0.4.17",
|
|
62
|
+
"react": "^19.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|