@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,86 @@
1
+ // Generated by summon webapp
2
+ // Main App component for <%= name %>
3
+
4
+ import React from 'react';
5
+ <% if (hasState) { -%>
6
+ import { useAppStore } from './state';
7
+ <% } -%>
8
+ <% if (hasApi) { -%>
9
+ import { api } from './api';
10
+ <% } -%>
11
+ <% if (hasLogging) { -%>
12
+ import { logger } from './logging';
13
+ <% } -%>
14
+
15
+ export const App: React.FC = () => {
16
+ <% if (hasState) { -%>
17
+ const { count, increment, decrement } = useAppStore();
18
+ <% } -%>
19
+ <% if (hasLogging) { -%>
20
+
21
+ React.useEffect(() => {
22
+ logger.info('App mounted');
23
+ return () => logger.info('App unmounted');
24
+ }, []);
25
+ <% } -%>
26
+
27
+ return (
28
+ <div className="<%= styling === 'tailwind' ? 'min-h-screen bg-gray-100 p-8' : 'app-container' %>">
29
+ <header className="<%= styling === 'tailwind' ? 'text-center mb-8' : 'app-header' %>">
30
+ <h1 className="<%= styling === 'tailwind' ? 'text-4xl font-bold text-gray-800' : '' %>">
31
+ Welcome to <%= pascalCase(name) %>
32
+ </h1>
33
+ <p className="<%= styling === 'tailwind' ? 'text-gray-600 mt-2' : '' %>">
34
+ <%= description %>
35
+ </p>
36
+ </header>
37
+
38
+ <main className="<%= styling === 'tailwind' ? 'max-w-2xl mx-auto' : 'app-main' %>">
39
+ <% if (hasState) { -%>
40
+ <section className="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6 mb-6' : 'card' %>">
41
+ <h2 className="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Counter Demo</h2>
42
+ <div className="<%= styling === 'tailwind' ? 'flex items-center gap-4' : 'counter' %>">
43
+ <button
44
+ onClick={decrement}
45
+ className="<%= styling === 'tailwind' ? 'px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600' : 'btn btn-danger' %>"
46
+ >
47
+ -
48
+ </button>
49
+ <span className="<%= styling === 'tailwind' ? 'text-2xl font-mono' : 'count' %>">{count}</span>
50
+ <button
51
+ onClick={increment}
52
+ className="<%= styling === 'tailwind' ? 'px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600' : 'btn btn-success' %>"
53
+ >
54
+ +
55
+ </button>
56
+ </div>
57
+ </section>
58
+ <% } -%>
59
+
60
+ <section className="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6' : 'card' %>">
61
+ <h2 className="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Features</h2>
62
+ <ul className="<%= styling === 'tailwind' ? 'list-disc list-inside space-y-2' : 'feature-list' %>">
63
+ <li>Framework: <%= framework %></li>
64
+ <li>Styling: <%= styling %></li>
65
+ <% if (hasRouter) { -%>
66
+ <li>Routing enabled</li>
67
+ <% } -%>
68
+ <% if (hasState) { -%>
69
+ <li>State management with Zustand</li>
70
+ <% } -%>
71
+ <% if (hasApi) { -%>
72
+ <li>API client configured</li>
73
+ <% } -%>
74
+ <% if (hasLogging) { -%>
75
+ <li>Logging enabled</li>
76
+ <% } -%>
77
+ </ul>
78
+ </section>
79
+ </main>
80
+
81
+ <footer className="<%= styling === 'tailwind' ? 'text-center mt-8 text-gray-500' : 'app-footer' %>">
82
+ <p>Generated by Summon</p>
83
+ </footer>
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,154 @@
1
+ # <%= pascalCase(name) %>
2
+
3
+ <%= description %>
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ bun install
10
+
11
+ # Start development server
12
+ bun run dev
13
+ <% if (withTests) { -%>
14
+
15
+ # Run tests
16
+ bun run test
17
+ <% } -%>
18
+
19
+ # Build for production
20
+ bun run build
21
+ ```
22
+
23
+ ## Stack
24
+
25
+ - **Framework**: <%= framework === 'react' ? 'React 18' : 'Vanilla TypeScript' %>
26
+ - **Styling**: <%= styling === 'tailwind' ? 'Tailwind CSS' : styling === 'css' ? 'Plain CSS' : 'None' %>
27
+ <% if (features.length > 0) { -%>
28
+ - **Features**:
29
+ <% features.forEach(function(feature) { -%>
30
+ - <%= feature === 'router' ? 'Client-side routing' : feature === 'state' ? 'State management (Zustand)' : feature === 'api' ? 'API client (ky)' : feature === 'logging' ? 'Logging utilities' : feature %>
31
+ <% }); -%>
32
+ <% } -%>
33
+ <% if (withTests) { -%>
34
+ - **Testing**: Vitest
35
+ <% } -%>
36
+
37
+ ## Project Structure
38
+
39
+ ```
40
+ <%= name %>/
41
+ <%= '├── ' %>src/
42
+ <%= '│ ├── ' %>main.<%= framework === 'react' ? 'tsx' : 'ts' %> # Entry point
43
+ <%= '│ ├── ' %><%= framework === 'react' ? 'App.tsx' : 'app.ts' %> # Main component/module
44
+ <% if (styling !== 'none') { -%>
45
+ <%= '│ ├── ' %>styles.css # Global styles
46
+ <% } -%>
47
+ <% features.forEach(function(feature) { -%>
48
+ <%= '│ ├── ' %><%= feature %>.ts # <%= feature.charAt(0).toUpperCase() + feature.slice(1) %> module
49
+ <% }); -%>
50
+ <%= '│ └── ' %>components/ # Reusable components
51
+ <%= '├── ' %>public/ # Static assets
52
+ <% if (withTests) { -%>
53
+ <%= '├── ' %>tests/ # Test files
54
+ <% } -%>
55
+ <% if (withDocs) { -%>
56
+ <%= '├── ' %>docs/ # Documentation
57
+ <% } -%>
58
+ <%= '├── ' %>package.json
59
+ <%= '└── ' %>tsconfig.json
60
+ ```
61
+
62
+ ## Development
63
+
64
+ <% if (framework === 'react') { -%>
65
+ This project uses Vite for fast development with HMR (Hot Module Replacement).
66
+
67
+ ```bash
68
+ bun run dev
69
+ ```
70
+
71
+ Open http://localhost:5173 to view the app.
72
+ <% } else { -%>
73
+ This project uses a simple development setup with live-server.
74
+
75
+ ```bash
76
+ bun run dev
77
+ ```
78
+
79
+ Open http://localhost:8080 to view the app.
80
+ <% } -%>
81
+
82
+ <% if (hasState) { -%>
83
+ ## State Management
84
+
85
+ <% if (framework === 'react') { -%>
86
+ State is managed using [Zustand](https://github.com/pmndrs/zustand). Access the store in any component:
87
+
88
+ ```tsx
89
+ import { useAppStore } from './state';
90
+
91
+ function MyComponent() {
92
+ const { count, increment } = useAppStore();
93
+ return <button onClick={increment}>{count}</button>;
94
+ }
95
+ ```
96
+ <% } else { -%>
97
+ State is managed using a simple pub/sub store pattern:
98
+
99
+ ```ts
100
+ import { appStore } from './state';
101
+
102
+ // Get current state
103
+ const { count } = appStore.getState();
104
+
105
+ // Update state
106
+ appStore.setState({ count: count + 1 });
107
+
108
+ // Subscribe to changes
109
+ appStore.subscribe((state) => {
110
+ console.log('State changed:', state);
111
+ });
112
+ ```
113
+ <% } -%>
114
+
115
+ <% } -%>
116
+ <% if (hasApi) { -%>
117
+ ## API Client
118
+
119
+ The API client is configured in `src/api.ts` using [ky](https://github.com/sindresorhus/ky):
120
+
121
+ ```ts
122
+ import { get, post } from './api';
123
+
124
+ // GET request
125
+ const users = await get<User[]>('users');
126
+
127
+ // POST request
128
+ const newUser = await post<User>('users', { name: 'John' });
129
+ ```
130
+
131
+ Configure the API base URL via the `VITE_API_URL` environment variable.
132
+
133
+ <% } -%>
134
+ <% if (hasLogging) { -%>
135
+ ## Logging
136
+
137
+ Use the logger for debugging and monitoring:
138
+
139
+ ```ts
140
+ import { logger } from './logging';
141
+
142
+ logger.debug('Debug message');
143
+ logger.info('Info message');
144
+ logger.warn('Warning message');
145
+ logger.error('Error message');
146
+
147
+ // View log history
148
+ const history = logger.getHistory();
149
+ ```
150
+
151
+ <% } -%>
152
+ ---
153
+
154
+ Generated by [Summon](https://github.com/canonical/pragma) - A Monadic Task-Centric Code Generator Framework
@@ -0,0 +1,63 @@
1
+ // Generated by summon webapp
2
+ // Tests for <%= name %>
3
+
4
+ import { describe, it, expect } from 'vitest';
5
+
6
+ describe('<%= pascalCase(name) %>', () => {
7
+ it('should pass a basic test', () => {
8
+ expect(true).toBe(true);
9
+ });
10
+
11
+ <% if (hasState && framework === 'react') { -%>
12
+ describe('App Store', () => {
13
+ it('should initialize with count of 0', async () => {
14
+ const { useAppStore } = await import('../src/state');
15
+ const { count } = useAppStore.getState();
16
+ expect(count).toBe(0);
17
+ });
18
+
19
+ it('should increment count', async () => {
20
+ const { useAppStore } = await import('../src/state');
21
+ const { increment } = useAppStore.getState();
22
+ increment();
23
+ expect(useAppStore.getState().count).toBe(1);
24
+ });
25
+
26
+ it('should decrement count', async () => {
27
+ const { useAppStore } = await import('../src/state');
28
+ const store = useAppStore.getState();
29
+ store.reset();
30
+ store.decrement();
31
+ expect(useAppStore.getState().count).toBe(-1);
32
+ });
33
+ });
34
+ <% } -%>
35
+
36
+ <% if (hasApi) { -%>
37
+ describe('API Client', () => {
38
+ it('should be configured with base URL', async () => {
39
+ const { api } = await import('../src/api');
40
+ expect(api).toBeDefined();
41
+ });
42
+ });
43
+ <% } -%>
44
+
45
+ <% if (hasLogging) { -%>
46
+ describe('Logger', () => {
47
+ it('should log messages', async () => {
48
+ const { logger } = await import('../src/logging');
49
+ logger.info('Test message');
50
+ const history = logger.getHistory();
51
+ expect(history.length).toBeGreaterThan(0);
52
+ expect(history[history.length - 1].message).toBe('Test message');
53
+ });
54
+
55
+ it('should clear history', async () => {
56
+ const { logger } = await import('../src/logging');
57
+ logger.info('Another message');
58
+ logger.clearHistory();
59
+ expect(logger.getHistory().length).toBe(0);
60
+ });
61
+ });
62
+ <% } -%>
63
+ });
@@ -0,0 +1,132 @@
1
+ // Generated by summon webapp
2
+ // Main application module for <%= name %>
3
+
4
+ <% if (hasLogging) { -%>
5
+ import { logger } from './logging';
6
+ <% } -%>
7
+ <% if (hasApi) { -%>
8
+ import { api } from './api';
9
+ <% } -%>
10
+
11
+ interface AppState {
12
+ count: number;
13
+ }
14
+
15
+ const state: AppState = {
16
+ count: 0,
17
+ };
18
+
19
+ /**
20
+ * Initialize the application.
21
+ */
22
+ export function init(container: HTMLElement): void {
23
+ <% if (hasLogging) { -%>
24
+ logger.info('Initializing <%= name %>...');
25
+ <% } -%>
26
+
27
+ render(container);
28
+ setupEventListeners(container);
29
+
30
+ <% if (hasLogging) { -%>
31
+ logger.info('<%= name %> initialized successfully');
32
+ <% } -%>
33
+ }
34
+
35
+ /**
36
+ * Render the application UI.
37
+ */
38
+ function render(container: HTMLElement): void {
39
+ container.innerHTML = `
40
+ <div class="<%= styling === 'tailwind' ? 'min-h-screen bg-gray-100 p-8' : 'app-container' %>">
41
+ <header class="<%= styling === 'tailwind' ? 'text-center mb-8' : 'app-header' %>">
42
+ <h1 class="<%= styling === 'tailwind' ? 'text-4xl font-bold text-gray-800' : '' %>">
43
+ Welcome to <%= name %>
44
+ </h1>
45
+ <p class="<%= styling === 'tailwind' ? 'text-gray-600 mt-2' : '' %>">
46
+ <%= description %>
47
+ </p>
48
+ </header>
49
+
50
+ <main class="<%= styling === 'tailwind' ? 'max-w-2xl mx-auto' : 'app-main' %>">
51
+ <% if (hasState) { -%>
52
+ <section class="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6 mb-6' : 'card' %>">
53
+ <h2 class="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Counter Demo</h2>
54
+ <div class="<%= styling === 'tailwind' ? 'flex items-center gap-4' : 'counter' %>">
55
+ <button
56
+ id="decrement"
57
+ class="<%= styling === 'tailwind' ? 'px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600' : 'btn btn-danger' %>"
58
+ >
59
+ -
60
+ </button>
61
+ <span id="count" class="<%= styling === 'tailwind' ? 'text-2xl font-mono' : 'count' %>">${state.count}</span>
62
+ <button
63
+ id="increment"
64
+ class="<%= styling === 'tailwind' ? 'px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600' : 'btn btn-success' %>"
65
+ >
66
+ +
67
+ </button>
68
+ </div>
69
+ </section>
70
+ <% } -%>
71
+
72
+ <section class="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6' : 'card' %>">
73
+ <h2 class="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Features</h2>
74
+ <ul class="<%= styling === 'tailwind' ? 'list-disc list-inside space-y-2' : 'feature-list' %>">
75
+ <li>Framework: <%= framework %></li>
76
+ <li>Styling: <%= styling %></li>
77
+ <% if (hasRouter) { -%>
78
+ <li>Routing enabled</li>
79
+ <% } -%>
80
+ <% if (hasState) { -%>
81
+ <li>State management</li>
82
+ <% } -%>
83
+ <% if (hasApi) { -%>
84
+ <li>API client configured</li>
85
+ <% } -%>
86
+ <% if (hasLogging) { -%>
87
+ <li>Logging enabled</li>
88
+ <% } -%>
89
+ </ul>
90
+ </section>
91
+ </main>
92
+
93
+ <footer class="<%= styling === 'tailwind' ? 'text-center mt-8 text-gray-500' : 'app-footer' %>">
94
+ <p>Generated by Summon</p>
95
+ </footer>
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ <% if (hasState) { -%>
101
+ /**
102
+ * Setup event listeners for interactive elements.
103
+ */
104
+ function setupEventListeners(container: HTMLElement): void {
105
+ const incrementBtn = container.querySelector('#increment');
106
+ const decrementBtn = container.querySelector('#decrement');
107
+ const countDisplay = container.querySelector('#count');
108
+
109
+ incrementBtn?.addEventListener('click', () => {
110
+ state.count++;
111
+ if (countDisplay) countDisplay.textContent = String(state.count);
112
+ <% if (hasLogging) { -%>
113
+ logger.debug(`Count incremented to ${state.count}`);
114
+ <% } -%>
115
+ });
116
+
117
+ decrementBtn?.addEventListener('click', () => {
118
+ state.count--;
119
+ if (countDisplay) countDisplay.textContent = String(state.count);
120
+ <% if (hasLogging) { -%>
121
+ logger.debug(`Count decremented to ${state.count}`);
122
+ <% } -%>
123
+ });
124
+ }
125
+ <% } else { -%>
126
+ /**
127
+ * Setup event listeners for interactive elements.
128
+ */
129
+ function setupEventListeners(_container: HTMLElement): void {
130
+ // Add your event listeners here
131
+ }
132
+ <% } -%>
@@ -0,0 +1,264 @@
1
+ // Generated by summon webapp
2
+ // Feature module: <%= featureName %>
3
+
4
+ <% if (featureName === 'router') { -%>
5
+ /**
6
+ * Router configuration for <%= name %>
7
+ */
8
+ <% if (framework === 'react') { -%>
9
+ import { Routes, Route } from 'react-router-dom';
10
+
11
+ export const AppRoutes = () => (
12
+ <Routes>
13
+ <Route path="/" element={<div>Home</div>} />
14
+ <Route path="/about" element={<div>About</div>} />
15
+ <Route path="*" element={<div>Not Found</div>} />
16
+ </Routes>
17
+ );
18
+ <% } else { -%>
19
+ type RouteHandler = (params: Record<string, string>) => void;
20
+
21
+ interface Route {
22
+ path: string;
23
+ handler: RouteHandler;
24
+ }
25
+
26
+ const routes: Route[] = [];
27
+
28
+ export function addRoute(path: string, handler: RouteHandler): void {
29
+ routes.push({ path, handler });
30
+ }
31
+
32
+ export function navigate(path: string): void {
33
+ const route = routes.find((r) => matchPath(r.path, path));
34
+ if (route) {
35
+ const params = extractParams(route.path, path);
36
+ route.handler(params);
37
+ history.pushState(null, '', path);
38
+ }
39
+ }
40
+
41
+ function matchPath(pattern: string, path: string): boolean {
42
+ const regex = new RegExp('^' + pattern.replace(/:\w+/g, '([^/]+)') + '$');
43
+ return regex.test(path);
44
+ }
45
+
46
+ function extractParams(pattern: string, path: string): Record<string, string> {
47
+ const params: Record<string, string> = {};
48
+ const patternParts = pattern.split('/');
49
+ const pathParts = path.split('/');
50
+
51
+ patternParts.forEach((part, i) => {
52
+ if (part.startsWith(':')) {
53
+ params[part.slice(1)] = pathParts[i];
54
+ }
55
+ });
56
+
57
+ return params;
58
+ }
59
+
60
+ // Listen for popstate events
61
+ window.addEventListener('popstate', () => {
62
+ navigate(location.pathname);
63
+ });
64
+ <% } -%>
65
+ <% } else if (featureName === 'state') { -%>
66
+ /**
67
+ * State management for <%= name %>
68
+ */
69
+ <% if (framework === 'react') { -%>
70
+ import { create } from 'zustand';
71
+
72
+ interface AppState {
73
+ count: number;
74
+ increment: () => void;
75
+ decrement: () => void;
76
+ reset: () => void;
77
+ }
78
+
79
+ export const useAppStore = create<AppState>((set) => ({
80
+ count: 0,
81
+ increment: () => set((state) => ({ count: state.count + 1 })),
82
+ decrement: () => set((state) => ({ count: state.count - 1 })),
83
+ reset: () => set({ count: 0 }),
84
+ }));
85
+ <% } else { -%>
86
+ type Listener<T> = (state: T) => void;
87
+
88
+ export function createStore<T extends object>(initialState: T) {
89
+ let state = { ...initialState };
90
+ const listeners = new Set<Listener<T>>();
91
+
92
+ return {
93
+ getState: () => state,
94
+ setState: (partial: Partial<T>) => {
95
+ state = { ...state, ...partial };
96
+ listeners.forEach((listener) => listener(state));
97
+ },
98
+ subscribe: (listener: Listener<T>) => {
99
+ listeners.add(listener);
100
+ return () => listeners.delete(listener);
101
+ },
102
+ };
103
+ }
104
+
105
+ // Application store
106
+ export const appStore = createStore({
107
+ count: 0,
108
+ });
109
+ <% } -%>
110
+ <% } else if (featureName === 'api') { -%>
111
+ /**
112
+ * API client for <%= name %>
113
+ */
114
+ import ky from 'ky';
115
+
116
+ const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com';
117
+
118
+ export const api = ky.create({
119
+ prefixUrl: API_BASE_URL,
120
+ timeout: 30000,
121
+ retry: {
122
+ limit: 2,
123
+ methods: ['get'],
124
+ },
125
+ hooks: {
126
+ beforeRequest: [
127
+ (request) => {
128
+ // Add auth token if available
129
+ const token = localStorage.getItem('auth_token');
130
+ if (token) {
131
+ request.headers.set('Authorization', `Bearer ${token}`);
132
+ }
133
+ },
134
+ ],
135
+ afterResponse: [
136
+ async (request, options, response) => {
137
+ if (!response.ok) {
138
+ console.error(`API Error: ${response.status} ${response.statusText}`);
139
+ }
140
+ return response;
141
+ },
142
+ ],
143
+ },
144
+ });
145
+
146
+ // Typed API helpers
147
+ export async function get<T>(endpoint: string): Promise<T> {
148
+ return api.get(endpoint).json<T>();
149
+ }
150
+
151
+ export async function post<T, D = unknown>(endpoint: string, data: D): Promise<T> {
152
+ return api.post(endpoint, { json: data }).json<T>();
153
+ }
154
+
155
+ export async function put<T, D = unknown>(endpoint: string, data: D): Promise<T> {
156
+ return api.put(endpoint, { json: data }).json<T>();
157
+ }
158
+
159
+ export async function del<T>(endpoint: string): Promise<T> {
160
+ return api.delete(endpoint).json<T>();
161
+ }
162
+ <% } else if (featureName === 'logging') { -%>
163
+ /**
164
+ * Logging utilities for <%= name %>
165
+ */
166
+
167
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
168
+
169
+ interface LogEntry {
170
+ level: LogLevel;
171
+ message: string;
172
+ timestamp: Date;
173
+ context?: Record<string, unknown>;
174
+ }
175
+
176
+ const LOG_LEVELS: Record<LogLevel, number> = {
177
+ debug: 0,
178
+ info: 1,
179
+ warn: 2,
180
+ error: 3,
181
+ };
182
+
183
+ class Logger {
184
+ private minLevel: LogLevel = 'debug';
185
+ private history: LogEntry[] = [];
186
+ private maxHistory = 1000;
187
+
188
+ setLevel(level: LogLevel): void {
189
+ this.minLevel = level;
190
+ }
191
+
192
+ private log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
193
+ if (LOG_LEVELS[level] < LOG_LEVELS[this.minLevel]) {
194
+ return;
195
+ }
196
+
197
+ const entry: LogEntry = {
198
+ level,
199
+ message,
200
+ timestamp: new Date(),
201
+ context,
202
+ };
203
+
204
+ this.history.push(entry);
205
+ if (this.history.length > this.maxHistory) {
206
+ this.history.shift();
207
+ }
208
+
209
+ const styles: Record<LogLevel, string> = {
210
+ debug: 'color: gray',
211
+ info: 'color: blue',
212
+ warn: 'color: orange',
213
+ error: 'color: red',
214
+ };
215
+
216
+ console[level === 'debug' ? 'log' : level](
217
+ `%c[${level.toUpperCase()}] ${message}`,
218
+ styles[level],
219
+ context || ''
220
+ );
221
+ }
222
+
223
+ debug(message: string, context?: Record<string, unknown>): void {
224
+ this.log('debug', message, context);
225
+ }
226
+
227
+ info(message: string, context?: Record<string, unknown>): void {
228
+ this.log('info', message, context);
229
+ }
230
+
231
+ warn(message: string, context?: Record<string, unknown>): void {
232
+ this.log('warn', message, context);
233
+ }
234
+
235
+ error(message: string, context?: Record<string, unknown>): void {
236
+ this.log('error', message, context);
237
+ }
238
+
239
+ getHistory(): LogEntry[] {
240
+ return [...this.history];
241
+ }
242
+
243
+ clearHistory(): void {
244
+ this.history = [];
245
+ }
246
+ }
247
+
248
+ export const logger = new Logger();
249
+ <% } else { -%>
250
+ /**
251
+ * Feature module: <%= featureName %>
252
+ *
253
+ * This is a placeholder for the <%= featureName %> feature.
254
+ * Implement your feature logic here.
255
+ */
256
+
257
+ export function init<%= pascalCase(featureName) %>(): void {
258
+ console.log('<%= featureName %> feature initialized');
259
+ }
260
+
261
+ export const <%= camelCase(featureName) %> = {
262
+ init: init<%= pascalCase(featureName) %>,
263
+ };
264
+ <% } -%>