@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,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;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * <%%= name %>
3
+ *
4
+ * <%%= description %>
5
+ */
6
+
7
+ export const <%%= camelCase(name) %> = () => {
8
+ // TODO: Implement
9
+ };
@@ -0,0 +1,8 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { <%%= camelCase(name) %> } from "./index";
3
+
4
+ describe("<%%= name %>", () => {
5
+ it("should be defined", () => {
6
+ expect(<%%= camelCase(name) %>).toBeDefined();
7
+ });
8
+ });
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
+ }