wiggum-cli 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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/bin/ralph.js +8 -0
  4. package/dist/ai/enhancer.d.ts +100 -0
  5. package/dist/ai/enhancer.d.ts.map +1 -0
  6. package/dist/ai/enhancer.js +233 -0
  7. package/dist/ai/enhancer.js.map +1 -0
  8. package/dist/ai/index.d.ts +8 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/index.js +11 -0
  11. package/dist/ai/index.js.map +1 -0
  12. package/dist/ai/prompts.d.ts +26 -0
  13. package/dist/ai/prompts.d.ts.map +1 -0
  14. package/dist/ai/prompts.js +201 -0
  15. package/dist/ai/prompts.js.map +1 -0
  16. package/dist/ai/providers.d.ts +35 -0
  17. package/dist/ai/providers.d.ts.map +1 -0
  18. package/dist/ai/providers.js +104 -0
  19. package/dist/ai/providers.js.map +1 -0
  20. package/dist/cli.d.ts +6 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +196 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/commands/init.d.ts +16 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +124 -0
  27. package/dist/commands/init.js.map +1 -0
  28. package/dist/commands/monitor.d.ts +17 -0
  29. package/dist/commands/monitor.d.ts.map +1 -0
  30. package/dist/commands/monitor.js +342 -0
  31. package/dist/commands/monitor.js.map +1 -0
  32. package/dist/commands/new.d.ts +19 -0
  33. package/dist/commands/new.d.ts.map +1 -0
  34. package/dist/commands/new.js +272 -0
  35. package/dist/commands/new.js.map +1 -0
  36. package/dist/commands/run.d.ts +16 -0
  37. package/dist/commands/run.d.ts.map +1 -0
  38. package/dist/commands/run.js +175 -0
  39. package/dist/commands/run.js.map +1 -0
  40. package/dist/generator/config.d.ts +59 -0
  41. package/dist/generator/config.d.ts.map +1 -0
  42. package/dist/generator/config.js +68 -0
  43. package/dist/generator/config.js.map +1 -0
  44. package/dist/generator/index.d.ts +64 -0
  45. package/dist/generator/index.d.ts.map +1 -0
  46. package/dist/generator/index.js +147 -0
  47. package/dist/generator/index.js.map +1 -0
  48. package/dist/generator/templates.d.ts +70 -0
  49. package/dist/generator/templates.d.ts.map +1 -0
  50. package/dist/generator/templates.js +296 -0
  51. package/dist/generator/templates.js.map +1 -0
  52. package/dist/generator/writer.d.ts +93 -0
  53. package/dist/generator/writer.d.ts.map +1 -0
  54. package/dist/generator/writer.js +213 -0
  55. package/dist/generator/writer.js.map +1 -0
  56. package/dist/index.d.ts +12 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +17 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/scanner/detectors/core/framework.d.ts +11 -0
  61. package/dist/scanner/detectors/core/framework.d.ts.map +1 -0
  62. package/dist/scanner/detectors/core/framework.js +275 -0
  63. package/dist/scanner/detectors/core/framework.js.map +1 -0
  64. package/dist/scanner/detectors/core/packageManager.d.ts +11 -0
  65. package/dist/scanner/detectors/core/packageManager.d.ts.map +1 -0
  66. package/dist/scanner/detectors/core/packageManager.js +74 -0
  67. package/dist/scanner/detectors/core/packageManager.js.map +1 -0
  68. package/dist/scanner/detectors/core/styling.d.ts +12 -0
  69. package/dist/scanner/detectors/core/styling.d.ts.map +1 -0
  70. package/dist/scanner/detectors/core/styling.js +230 -0
  71. package/dist/scanner/detectors/core/styling.js.map +1 -0
  72. package/dist/scanner/detectors/core/testing.d.ts +12 -0
  73. package/dist/scanner/detectors/core/testing.d.ts.map +1 -0
  74. package/dist/scanner/detectors/core/testing.js +190 -0
  75. package/dist/scanner/detectors/core/testing.js.map +1 -0
  76. package/dist/scanner/detectors/data/api.d.ts +12 -0
  77. package/dist/scanner/detectors/data/api.d.ts.map +1 -0
  78. package/dist/scanner/detectors/data/api.js +261 -0
  79. package/dist/scanner/detectors/data/api.js.map +1 -0
  80. package/dist/scanner/detectors/data/database.d.ts +12 -0
  81. package/dist/scanner/detectors/data/database.d.ts.map +1 -0
  82. package/dist/scanner/detectors/data/database.js +213 -0
  83. package/dist/scanner/detectors/data/database.js.map +1 -0
  84. package/dist/scanner/detectors/data/orm.d.ts +12 -0
  85. package/dist/scanner/detectors/data/orm.d.ts.map +1 -0
  86. package/dist/scanner/detectors/data/orm.js +160 -0
  87. package/dist/scanner/detectors/data/orm.js.map +1 -0
  88. package/dist/scanner/detectors/frontend/formHandling.d.ts +12 -0
  89. package/dist/scanner/detectors/frontend/formHandling.d.ts.map +1 -0
  90. package/dist/scanner/detectors/frontend/formHandling.js +211 -0
  91. package/dist/scanner/detectors/frontend/formHandling.js.map +1 -0
  92. package/dist/scanner/detectors/frontend/stateManagement.d.ts +12 -0
  93. package/dist/scanner/detectors/frontend/stateManagement.d.ts.map +1 -0
  94. package/dist/scanner/detectors/frontend/stateManagement.js +221 -0
  95. package/dist/scanner/detectors/frontend/stateManagement.js.map +1 -0
  96. package/dist/scanner/detectors/frontend/uiComponents.d.ts +12 -0
  97. package/dist/scanner/detectors/frontend/uiComponents.d.ts.map +1 -0
  98. package/dist/scanner/detectors/frontend/uiComponents.js +285 -0
  99. package/dist/scanner/detectors/frontend/uiComponents.js.map +1 -0
  100. package/dist/scanner/detectors/infra/deployment.d.ts +12 -0
  101. package/dist/scanner/detectors/infra/deployment.d.ts.map +1 -0
  102. package/dist/scanner/detectors/infra/deployment.js +301 -0
  103. package/dist/scanner/detectors/infra/deployment.js.map +1 -0
  104. package/dist/scanner/detectors/infra/monorepo.d.ts +12 -0
  105. package/dist/scanner/detectors/infra/monorepo.d.ts.map +1 -0
  106. package/dist/scanner/detectors/infra/monorepo.js +219 -0
  107. package/dist/scanner/detectors/infra/monorepo.js.map +1 -0
  108. package/dist/scanner/detectors/mcp/mcpProject.d.ts +12 -0
  109. package/dist/scanner/detectors/mcp/mcpProject.d.ts.map +1 -0
  110. package/dist/scanner/detectors/mcp/mcpProject.js +154 -0
  111. package/dist/scanner/detectors/mcp/mcpProject.js.map +1 -0
  112. package/dist/scanner/detectors/mcp/mcpServers.d.ts +17 -0
  113. package/dist/scanner/detectors/mcp/mcpServers.d.ts.map +1 -0
  114. package/dist/scanner/detectors/mcp/mcpServers.js +193 -0
  115. package/dist/scanner/detectors/mcp/mcpServers.js.map +1 -0
  116. package/dist/scanner/detectors/services/analytics.d.ts +12 -0
  117. package/dist/scanner/detectors/services/analytics.d.ts.map +1 -0
  118. package/dist/scanner/detectors/services/analytics.js +236 -0
  119. package/dist/scanner/detectors/services/analytics.js.map +1 -0
  120. package/dist/scanner/detectors/services/auth.d.ts +12 -0
  121. package/dist/scanner/detectors/services/auth.d.ts.map +1 -0
  122. package/dist/scanner/detectors/services/auth.js +217 -0
  123. package/dist/scanner/detectors/services/auth.js.map +1 -0
  124. package/dist/scanner/detectors/services/email.d.ts +12 -0
  125. package/dist/scanner/detectors/services/email.d.ts.map +1 -0
  126. package/dist/scanner/detectors/services/email.js +211 -0
  127. package/dist/scanner/detectors/services/email.js.map +1 -0
  128. package/dist/scanner/detectors/services/payments.d.ts +12 -0
  129. package/dist/scanner/detectors/services/payments.d.ts.map +1 -0
  130. package/dist/scanner/detectors/services/payments.js +185 -0
  131. package/dist/scanner/detectors/services/payments.js.map +1 -0
  132. package/dist/scanner/detectors/utils.d.ts +160 -0
  133. package/dist/scanner/detectors/utils.d.ts.map +1 -0
  134. package/dist/scanner/detectors/utils.js +222 -0
  135. package/dist/scanner/detectors/utils.js.map +1 -0
  136. package/dist/scanner/index.d.ts +42 -0
  137. package/dist/scanner/index.d.ts.map +1 -0
  138. package/dist/scanner/index.js +282 -0
  139. package/dist/scanner/index.js.map +1 -0
  140. package/dist/scanner/registry.d.ts +43 -0
  141. package/dist/scanner/registry.d.ts.map +1 -0
  142. package/dist/scanner/registry.js +243 -0
  143. package/dist/scanner/registry.js.map +1 -0
  144. package/dist/scanner/types.d.ts +112 -0
  145. package/dist/scanner/types.d.ts.map +1 -0
  146. package/dist/scanner/types.js +6 -0
  147. package/dist/scanner/types.js.map +1 -0
  148. package/dist/templates/config/ralph.config.js.tmpl +38 -0
  149. package/dist/templates/guides/AGENTS.md.tmpl +100 -0
  150. package/dist/templates/guides/FRONTEND.md.tmpl +523 -0
  151. package/dist/templates/guides/PERFORMANCE.md.tmpl +264 -0
  152. package/dist/templates/guides/SECURITY.md.tmpl +100 -0
  153. package/dist/templates/prompts/PROMPT.md.tmpl +77 -0
  154. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
  155. package/dist/templates/prompts/PROMPT_feature.md.tmpl +83 -0
  156. package/dist/templates/prompts/PROMPT_review.md.tmpl +167 -0
  157. package/dist/templates/prompts/PROMPT_verify.md.tmpl +72 -0
  158. package/dist/templates/root/.gitignore.tmpl +5 -0
  159. package/dist/templates/root/LEARNINGS.md.tmpl +24 -0
  160. package/dist/templates/root/README.md.tmpl +61 -0
  161. package/dist/templates/scripts/feature-loop.sh.tmpl +267 -0
  162. package/dist/templates/scripts/loop.sh.tmpl +59 -0
  163. package/dist/templates/scripts/ralph-monitor.sh.tmpl +244 -0
  164. package/dist/templates/specs/README.md.tmpl +57 -0
  165. package/dist/templates/specs/_example.md.tmpl +71 -0
  166. package/dist/utils/config.d.ts +95 -0
  167. package/dist/utils/config.d.ts.map +1 -0
  168. package/dist/utils/config.js +148 -0
  169. package/dist/utils/config.js.map +1 -0
  170. package/dist/utils/header.d.ts +5 -0
  171. package/dist/utils/header.d.ts.map +1 -0
  172. package/dist/utils/header.js +15 -0
  173. package/dist/utils/header.js.map +1 -0
  174. package/dist/utils/logger.d.ts +11 -0
  175. package/dist/utils/logger.d.ts.map +1 -0
  176. package/dist/utils/logger.js +24 -0
  177. package/dist/utils/logger.js.map +1 -0
  178. package/package.json +44 -0
  179. package/src/ai/enhancer.ts +350 -0
  180. package/src/ai/index.ts +38 -0
  181. package/src/ai/prompts.ts +217 -0
  182. package/src/ai/providers.ts +136 -0
  183. package/src/cli.ts +255 -0
  184. package/src/commands/init.ts +149 -0
  185. package/src/commands/monitor.ts +412 -0
  186. package/src/commands/new.ts +312 -0
  187. package/src/commands/run.ts +214 -0
  188. package/src/generator/config.ts +116 -0
  189. package/src/generator/index.ts +227 -0
  190. package/src/generator/templates.ts +412 -0
  191. package/src/generator/writer.ts +293 -0
  192. package/src/index.ts +41 -0
  193. package/src/scanner/detectors/core/framework.ts +332 -0
  194. package/src/scanner/detectors/core/packageManager.ts +91 -0
  195. package/src/scanner/detectors/core/styling.ts +261 -0
  196. package/src/scanner/detectors/core/testing.ts +221 -0
  197. package/src/scanner/detectors/data/api.ts +303 -0
  198. package/src/scanner/detectors/data/database.ts +245 -0
  199. package/src/scanner/detectors/data/orm.ts +180 -0
  200. package/src/scanner/detectors/frontend/formHandling.ts +244 -0
  201. package/src/scanner/detectors/frontend/stateManagement.ts +261 -0
  202. package/src/scanner/detectors/frontend/uiComponents.ts +328 -0
  203. package/src/scanner/detectors/infra/deployment.ts +343 -0
  204. package/src/scanner/detectors/infra/monorepo.ts +251 -0
  205. package/src/scanner/detectors/mcp/mcpProject.ts +176 -0
  206. package/src/scanner/detectors/mcp/mcpServers.ts +237 -0
  207. package/src/scanner/detectors/services/analytics.ts +273 -0
  208. package/src/scanner/detectors/services/auth.ts +254 -0
  209. package/src/scanner/detectors/services/email.ts +244 -0
  210. package/src/scanner/detectors/services/payments.ts +213 -0
  211. package/src/scanner/detectors/utils.ts +251 -0
  212. package/src/scanner/index.ts +354 -0
  213. package/src/scanner/registry.ts +301 -0
  214. package/src/scanner/types.ts +152 -0
  215. package/src/templates/config/ralph.config.js.tmpl +38 -0
  216. package/src/templates/guides/AGENTS.md.tmpl +100 -0
  217. package/src/templates/guides/FRONTEND.md.tmpl +523 -0
  218. package/src/templates/guides/PERFORMANCE.md.tmpl +264 -0
  219. package/src/templates/guides/SECURITY.md.tmpl +100 -0
  220. package/src/templates/prompts/PROMPT.md.tmpl +77 -0
  221. package/src/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
  222. package/src/templates/prompts/PROMPT_feature.md.tmpl +83 -0
  223. package/src/templates/prompts/PROMPT_review.md.tmpl +167 -0
  224. package/src/templates/prompts/PROMPT_verify.md.tmpl +72 -0
  225. package/src/templates/root/.gitignore.tmpl +5 -0
  226. package/src/templates/root/LEARNINGS.md.tmpl +24 -0
  227. package/src/templates/root/README.md.tmpl +61 -0
  228. package/src/templates/scripts/feature-loop.sh.tmpl +267 -0
  229. package/src/templates/scripts/loop.sh.tmpl +59 -0
  230. package/src/templates/scripts/ralph-monitor.sh.tmpl +244 -0
  231. package/src/templates/specs/README.md.tmpl +57 -0
  232. package/src/templates/specs/_example.md.tmpl +71 -0
  233. package/src/utils/config.ts +221 -0
  234. package/src/utils/header.ts +15 -0
  235. package/src/utils/logger.ts +28 -0
  236. package/tsconfig.json +19 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Generator Orchestrator
3
+ * Main entry point for generating ralph configuration files
4
+ */
5
+
6
+ import { join } from 'node:path';
7
+ import type { ScanResult } from '../scanner/types.js';
8
+ import {
9
+ extractVariables,
10
+ processAllTemplates,
11
+ getTemplatesDir,
12
+ type TemplateVariables,
13
+ } from './templates.js';
14
+ import { generateConfig, generateConfigFile, type RalphConfig } from './config.js';
15
+ import {
16
+ writeFiles,
17
+ createDirectoryStructure,
18
+ mapTemplateOutputPaths,
19
+ formatWriteSummary,
20
+ type WriteOptions,
21
+ type WriteSummary,
22
+ DEFAULT_WRITE_OPTIONS,
23
+ } from './writer.js';
24
+
25
+ // Re-export types and utilities
26
+ export type { TemplateVariables } from './templates.js';
27
+ export type { RalphConfig } from './config.js';
28
+ export type { WriteOptions, WriteSummary, WriteResult } from './writer.js';
29
+ export {
30
+ extractVariables,
31
+ processTemplate,
32
+ processTemplateFile,
33
+ getTemplatesDir,
34
+ } from './templates.js';
35
+ export { generateConfig, generateConfigFile } from './config.js';
36
+ export { writeFileWithOptions, formatWriteSummary, DEFAULT_WRITE_OPTIONS } from './writer.js';
37
+
38
+ /**
39
+ * Options for the generator
40
+ */
41
+ export interface GeneratorOptions {
42
+ /** Custom variables to add/override */
43
+ customVariables?: Record<string, string>;
44
+ /** How to handle existing files */
45
+ existingFiles?: 'backup' | 'skip' | 'overwrite';
46
+ /** Whether to generate config file */
47
+ generateConfig?: boolean;
48
+ /** Verbose output */
49
+ verbose?: boolean;
50
+ }
51
+
52
+ /**
53
+ * Default generator options
54
+ */
55
+ const DEFAULT_GENERATOR_OPTIONS: GeneratorOptions = {
56
+ customVariables: {},
57
+ existingFiles: 'backup',
58
+ generateConfig: true,
59
+ verbose: false,
60
+ };
61
+
62
+ /**
63
+ * Result of generation
64
+ */
65
+ export interface GenerationResult {
66
+ /** Whether generation was successful */
67
+ success: boolean;
68
+ /** Template variables used */
69
+ variables: TemplateVariables;
70
+ /** Generated config */
71
+ config?: RalphConfig;
72
+ /** Write summary */
73
+ writeSummary: WriteSummary;
74
+ /** Errors encountered */
75
+ errors: string[];
76
+ /** Time taken in milliseconds */
77
+ generationTime: number;
78
+ }
79
+
80
+ /**
81
+ * Main Generator class
82
+ */
83
+ export class Generator {
84
+ private options: GeneratorOptions;
85
+
86
+ constructor(options: GeneratorOptions = {}) {
87
+ this.options = { ...DEFAULT_GENERATOR_OPTIONS, ...options };
88
+ }
89
+
90
+ /**
91
+ * Generate all ralph files from scan result
92
+ */
93
+ async generate(scanResult: ScanResult): Promise<GenerationResult> {
94
+ const startTime = Date.now();
95
+ const errors: string[] = [];
96
+
97
+ // Extract variables from scan result
98
+ const variables = extractVariables(scanResult, this.options.customVariables || {});
99
+
100
+ // Get templates directory
101
+ const templatesDir = getTemplatesDir();
102
+
103
+ // Process all templates
104
+ let processedTemplates: Map<string, string>;
105
+ try {
106
+ processedTemplates = await processAllTemplates(templatesDir, variables);
107
+ } catch (error) {
108
+ errors.push(`Failed to process templates: ${error instanceof Error ? error.message : String(error)}`);
109
+ return {
110
+ success: false,
111
+ variables,
112
+ writeSummary: {
113
+ total: 0,
114
+ created: 0,
115
+ backedUp: 0,
116
+ skipped: 0,
117
+ overwritten: 0,
118
+ errors: 1,
119
+ results: [],
120
+ },
121
+ errors,
122
+ generationTime: Date.now() - startTime,
123
+ };
124
+ }
125
+
126
+ // Generate config if requested
127
+ let config: RalphConfig | undefined;
128
+ if (this.options.generateConfig) {
129
+ config = generateConfig(scanResult, this.options.customVariables || {});
130
+ const configContent = generateConfigFile(config);
131
+ processedTemplates.set('config/ralph.config.js', configContent);
132
+ }
133
+
134
+ // Map template outputs to final paths
135
+ const mappedOutputs = mapTemplateOutputPaths(processedTemplates);
136
+
137
+ // Create directory structure
138
+ try {
139
+ await createDirectoryStructure(scanResult.projectRoot);
140
+ } catch (error) {
141
+ errors.push(`Failed to create directory structure: ${error instanceof Error ? error.message : String(error)}`);
142
+ }
143
+
144
+ // Write all files
145
+ const writeOptions: WriteOptions = {
146
+ existingFiles: this.options.existingFiles || 'backup',
147
+ createBackups: this.options.existingFiles === 'backup',
148
+ verbose: this.options.verbose || false,
149
+ };
150
+
151
+ const writeSummary = await writeFiles(mappedOutputs, scanResult.projectRoot, writeOptions);
152
+
153
+ // Add any write errors to errors list
154
+ for (const result of writeSummary.results) {
155
+ if (result.action === 'error' && result.error) {
156
+ errors.push(`Failed to write ${result.path}: ${result.error}`);
157
+ }
158
+ }
159
+
160
+ const generationTime = Date.now() - startTime;
161
+
162
+ return {
163
+ success: errors.length === 0,
164
+ variables,
165
+ config,
166
+ writeSummary,
167
+ errors,
168
+ generationTime,
169
+ };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Convenience function to generate ralph files
175
+ */
176
+ export async function generateRalph(
177
+ scanResult: ScanResult,
178
+ options: GeneratorOptions = {}
179
+ ): Promise<GenerationResult> {
180
+ const generator = new Generator(options);
181
+ return generator.generate(scanResult);
182
+ }
183
+
184
+ /**
185
+ * Format generation result for display
186
+ */
187
+ export function formatGenerationResult(result: GenerationResult): string {
188
+ const lines: string[] = [];
189
+
190
+ if (result.success) {
191
+ lines.push('Generation completed successfully!');
192
+ } else {
193
+ lines.push('Generation completed with errors.');
194
+ }
195
+
196
+ lines.push(`Time: ${result.generationTime}ms`);
197
+ lines.push('');
198
+
199
+ // Show stack info
200
+ lines.push('Detected Stack:');
201
+ lines.push(` Framework: ${result.variables.framework}${result.variables.frameworkVersion ? ' ' + result.variables.frameworkVersion : ''}`);
202
+ lines.push(` Package Manager: ${result.variables.packageManager}`);
203
+ if (result.variables.unitTest !== 'none') {
204
+ lines.push(` Unit Testing: ${result.variables.unitTest}`);
205
+ }
206
+ if (result.variables.e2eTest !== 'none') {
207
+ lines.push(` E2E Testing: ${result.variables.e2eTest}`);
208
+ }
209
+ if (result.variables.styling !== 'css') {
210
+ lines.push(` Styling: ${result.variables.styling}`);
211
+ }
212
+ lines.push('');
213
+
214
+ // Show write summary
215
+ lines.push(formatWriteSummary(result.writeSummary));
216
+
217
+ // Show errors
218
+ if (result.errors.length > 0) {
219
+ lines.push('');
220
+ lines.push('Errors:');
221
+ for (const error of result.errors) {
222
+ lines.push(` - ${error}`);
223
+ }
224
+ }
225
+
226
+ return lines.join('\n');
227
+ }
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Template Processing System
3
+ * Reads template files with {{variable}} placeholders and substitutes values
4
+ */
5
+
6
+ import { readFile, readdir, stat } from 'node:fs/promises';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import type { ScanResult, DetectedStack } from '../scanner/types.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ /**
15
+ * Template variables available for substitution
16
+ */
17
+ export interface TemplateVariables {
18
+ // Project info
19
+ projectName: string;
20
+ projectRoot: string;
21
+
22
+ // Framework info
23
+ framework: string;
24
+ frameworkVersion: string;
25
+ frameworkVariant: string;
26
+
27
+ // Package manager
28
+ packageManager: string;
29
+ packageManagerVersion: string;
30
+
31
+ // Testing
32
+ unitTest: string;
33
+ unitTestVersion: string;
34
+ e2eTest: string;
35
+ e2eTestVersion: string;
36
+
37
+ // Styling
38
+ styling: string;
39
+ stylingVersion: string;
40
+ stylingVariant: string;
41
+
42
+ // Commands (derived from package manager)
43
+ devCommand: string;
44
+ buildCommand: string;
45
+ testCommand: string;
46
+ lintCommand: string;
47
+ typecheckCommand: string;
48
+ formatCommand: string;
49
+
50
+ // Paths
51
+ appDir: string;
52
+
53
+ // Custom variables
54
+ [key: string]: string;
55
+ }
56
+
57
+ /**
58
+ * Default template variables
59
+ */
60
+ const DEFAULT_VARIABLES: Partial<TemplateVariables> = {
61
+ framework: 'unknown',
62
+ frameworkVersion: '',
63
+ frameworkVariant: '',
64
+ packageManager: 'npm',
65
+ packageManagerVersion: '',
66
+ unitTest: 'none',
67
+ unitTestVersion: '',
68
+ e2eTest: 'none',
69
+ e2eTestVersion: '',
70
+ styling: 'css',
71
+ stylingVersion: '',
72
+ stylingVariant: '',
73
+ appDir: 'app',
74
+ };
75
+
76
+ /**
77
+ * Derive commands from package manager
78
+ */
79
+ function deriveCommands(packageManager: string): Pick<
80
+ TemplateVariables,
81
+ 'devCommand' | 'buildCommand' | 'testCommand' | 'lintCommand' | 'typecheckCommand' | 'formatCommand'
82
+ > {
83
+ const pm = packageManager.toLowerCase();
84
+ const run = pm === 'npm' ? 'npm run' : pm;
85
+
86
+ return {
87
+ devCommand: `${run} dev`,
88
+ buildCommand: `${run} build`,
89
+ testCommand: pm === 'npm' ? 'npm test' : `${pm} test`,
90
+ lintCommand: `${run} lint`,
91
+ typecheckCommand: pm === 'npm' ? 'npx tsc --noEmit' : `${pm} tsc --noEmit`,
92
+ formatCommand: pm === 'npm' ? 'npx prettier --write .' : `${pm} prettier --write .`,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Extract template variables from scan result
98
+ */
99
+ export function extractVariables(scanResult: ScanResult, customVars: Record<string, string> = {}): TemplateVariables {
100
+ const { stack, projectRoot } = scanResult;
101
+
102
+ // Extract project name from path
103
+ const projectName = projectRoot.split('/').pop() || 'project';
104
+
105
+ // Extract framework info
106
+ const framework = stack.framework?.name || DEFAULT_VARIABLES.framework!;
107
+ const frameworkVersion = stack.framework?.version || DEFAULT_VARIABLES.frameworkVersion!;
108
+ const frameworkVariant = stack.framework?.variant || DEFAULT_VARIABLES.frameworkVariant!;
109
+
110
+ // Extract package manager info
111
+ const packageManager = stack.packageManager?.name || DEFAULT_VARIABLES.packageManager!;
112
+ const packageManagerVersion = stack.packageManager?.version || DEFAULT_VARIABLES.packageManagerVersion!;
113
+
114
+ // Extract testing info
115
+ const unitTest = stack.testing?.unit?.name || DEFAULT_VARIABLES.unitTest!;
116
+ const unitTestVersion = stack.testing?.unit?.version || DEFAULT_VARIABLES.unitTestVersion!;
117
+ const e2eTest = stack.testing?.e2e?.name || DEFAULT_VARIABLES.e2eTest!;
118
+ const e2eTestVersion = stack.testing?.e2e?.version || DEFAULT_VARIABLES.e2eTestVersion!;
119
+
120
+ // Extract styling info
121
+ const styling = stack.styling?.name || DEFAULT_VARIABLES.styling!;
122
+ const stylingVersion = stack.styling?.version || DEFAULT_VARIABLES.stylingVersion!;
123
+ const stylingVariant = stack.styling?.variant || DEFAULT_VARIABLES.stylingVariant!;
124
+
125
+ // Derive commands
126
+ const commands = deriveCommands(packageManager);
127
+
128
+ // Determine app directory
129
+ const appDir = frameworkVariant === 'app-router' || framework.toLowerCase().includes('next') ? 'app' : 'src';
130
+
131
+ return {
132
+ projectName,
133
+ projectRoot,
134
+ framework,
135
+ frameworkVersion,
136
+ frameworkVariant,
137
+ packageManager,
138
+ packageManagerVersion,
139
+ unitTest,
140
+ unitTestVersion,
141
+ e2eTest,
142
+ e2eTestVersion,
143
+ styling,
144
+ stylingVersion,
145
+ stylingVariant,
146
+ appDir,
147
+ ...commands,
148
+ ...customVars,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Check if a value is considered "truthy" for template conditionals
154
+ */
155
+ function isTruthyValue(value: string | undefined): boolean {
156
+ return Boolean(value && value !== 'none' && value !== 'unknown' && value !== '');
157
+ }
158
+
159
+ /**
160
+ * Find matching closing tag for nested blocks
161
+ * Returns the index of the matching {{/tag}} or -1 if not found
162
+ */
163
+ function findMatchingClose(content: string, openTag: string, closeTag: string, startIndex: number = 0): number {
164
+ let depth = 1;
165
+ let i = startIndex;
166
+
167
+ while (i < content.length && depth > 0) {
168
+ const openMatch = content.indexOf(openTag, i);
169
+ const closeMatch = content.indexOf(closeTag, i);
170
+
171
+ if (closeMatch === -1) {
172
+ return -1; // No more closing tags
173
+ }
174
+
175
+ if (openMatch !== -1 && openMatch < closeMatch) {
176
+ // Found another opening tag before the closing tag
177
+ depth++;
178
+ i = openMatch + openTag.length;
179
+ } else {
180
+ // Found a closing tag
181
+ depth--;
182
+ if (depth === 0) {
183
+ return closeMatch;
184
+ }
185
+ i = closeMatch + closeTag.length;
186
+ }
187
+ }
188
+
189
+ return -1;
190
+ }
191
+
192
+ /**
193
+ * Process a single if block (handles nesting via recursion)
194
+ */
195
+ function processIfBlock(
196
+ template: string,
197
+ variables: TemplateVariables,
198
+ startIndex: number,
199
+ varName: string
200
+ ): { result: string; endIndex: number } {
201
+ const openTagEnd = template.indexOf('}}', startIndex) + 2;
202
+ const closeTag = '{{/if}}';
203
+
204
+ // Find the matching close tag (handling nesting)
205
+ let searchStart = openTagEnd;
206
+ let closeIndex = findMatchingClose(template, '{{#if', closeTag, searchStart);
207
+
208
+ if (closeIndex === -1) {
209
+ // No matching close, return as-is
210
+ return { result: template.slice(startIndex, openTagEnd), endIndex: openTagEnd };
211
+ }
212
+
213
+ const innerContent = template.slice(openTagEnd, closeIndex);
214
+
215
+ // Check for else clause (not inside nested if)
216
+ let elseIndex = -1;
217
+ let depth = 0;
218
+ for (let i = 0; i < innerContent.length; i++) {
219
+ if (innerContent.slice(i, i + 5) === '{{#if') {
220
+ depth++;
221
+ } else if (innerContent.slice(i, i + 7) === '{{/if}}') {
222
+ depth--;
223
+ } else if (depth === 0 && innerContent.slice(i, i + 8) === '{{else}}') {
224
+ elseIndex = i;
225
+ break;
226
+ }
227
+ }
228
+
229
+ const value = variables[varName as keyof TemplateVariables];
230
+ const isTruthy = isTruthyValue(value);
231
+
232
+ let selectedContent: string;
233
+ if (elseIndex !== -1) {
234
+ const ifPart = innerContent.slice(0, elseIndex);
235
+ const elsePart = innerContent.slice(elseIndex + 8);
236
+ selectedContent = isTruthy ? ifPart : elsePart;
237
+ } else {
238
+ selectedContent = isTruthy ? innerContent : '';
239
+ }
240
+
241
+ return { result: selectedContent, endIndex: closeIndex + closeTag.length };
242
+ }
243
+
244
+ /**
245
+ * Process conditional blocks in template
246
+ * Supports: {{#if variable}}...{{/if}} and {{#if variable}}...{{else}}...{{/if}}
247
+ * Handles nested conditionals correctly
248
+ */
249
+ function processConditionals(template: string, variables: TemplateVariables): string {
250
+ let result = '';
251
+ let i = 0;
252
+
253
+ while (i < template.length) {
254
+ // Look for {{#if
255
+ const ifMatch = template.slice(i).match(/^\{\{#if\s+(\w+)\}\}/);
256
+ if (ifMatch) {
257
+ const varName = ifMatch[1];
258
+ const { result: blockResult, endIndex } = processIfBlock(template, variables, i, varName);
259
+ // Recursively process the content for nested conditionals
260
+ result += processConditionals(blockResult, variables);
261
+ i += endIndex - i;
262
+ continue;
263
+ }
264
+
265
+ // Look for {{#unless
266
+ const unlessMatch = template.slice(i).match(/^\{\{#unless\s+(\w+)\}\}/);
267
+ if (unlessMatch) {
268
+ const varName = unlessMatch[1];
269
+ const openTagEnd = i + unlessMatch[0].length;
270
+ const closeTag = '{{/unless}}';
271
+ const closeIndex = template.indexOf(closeTag, openTagEnd);
272
+
273
+ if (closeIndex !== -1) {
274
+ const innerContent = template.slice(openTagEnd, closeIndex);
275
+ const value = variables[varName as keyof TemplateVariables];
276
+ const isTruthy = isTruthyValue(value);
277
+
278
+ if (!isTruthy) {
279
+ result += processConditionals(innerContent, variables);
280
+ }
281
+ i = closeIndex + closeTag.length;
282
+ continue;
283
+ }
284
+ }
285
+
286
+ // Regular character, copy it
287
+ result += template[i];
288
+ i++;
289
+ }
290
+
291
+ return result;
292
+ }
293
+
294
+ /**
295
+ * Process variable substitution in template
296
+ * Supports: {{variable}} and {{variable || default}}
297
+ */
298
+ function processVariables(template: string, variables: TemplateVariables): string {
299
+ // Process variables with defaults
300
+ const defaultRegex = /\{\{(\w+)\s*\|\|\s*([^}]+)\}\}/g;
301
+ let result = template.replace(defaultRegex, (_, varName, defaultValue) => {
302
+ const value = variables[varName as keyof TemplateVariables];
303
+ return value && value !== '' ? value : defaultValue.trim();
304
+ });
305
+
306
+ // Process simple variables
307
+ const varRegex = /\{\{(\w+)\}\}/g;
308
+ result = result.replace(varRegex, (_, varName) => {
309
+ const value = variables[varName as keyof TemplateVariables];
310
+ return value !== undefined ? value : '';
311
+ });
312
+
313
+ return result;
314
+ }
315
+
316
+ /**
317
+ * Process a template string with variable substitution
318
+ */
319
+ export function processTemplate(template: string, variables: TemplateVariables): string {
320
+ // First process conditionals
321
+ let result = processConditionals(template, variables);
322
+
323
+ // Then substitute variables
324
+ result = processVariables(result, variables);
325
+
326
+ // Clean up multiple blank lines
327
+ result = result.replace(/\n{3,}/g, '\n\n');
328
+
329
+ return result;
330
+ }
331
+
332
+ /**
333
+ * Read and process a template file
334
+ */
335
+ export async function processTemplateFile(templatePath: string, variables: TemplateVariables): Promise<string> {
336
+ const template = await readFile(templatePath, 'utf-8');
337
+ return processTemplate(template, variables);
338
+ }
339
+
340
+ /**
341
+ * Get the templates directory path
342
+ */
343
+ export function getTemplatesDir(): string {
344
+ // In development (src), templates are at src/templates
345
+ // In production (dist), templates are at dist/templates
346
+ const srcTemplates = join(__dirname, '..', 'templates');
347
+ return srcTemplates;
348
+ }
349
+
350
+ /**
351
+ * Template file info
352
+ */
353
+ export interface TemplateFile {
354
+ /** Source template path */
355
+ sourcePath: string;
356
+ /** Relative path from templates directory */
357
+ relativePath: string;
358
+ /** Output path (without .tmpl extension) */
359
+ outputPath: string;
360
+ /** Category (prompts, guides, specs, config) */
361
+ category: string;
362
+ }
363
+
364
+ /**
365
+ * Discover all template files
366
+ */
367
+ export async function discoverTemplates(templatesDir: string): Promise<TemplateFile[]> {
368
+ const templates: TemplateFile[] = [];
369
+
370
+ async function scanDir(dir: string, category: string = ''): Promise<void> {
371
+ const entries = await readdir(dir, { withFileTypes: true });
372
+
373
+ for (const entry of entries) {
374
+ const fullPath = join(dir, entry.name);
375
+
376
+ if (entry.isDirectory()) {
377
+ await scanDir(fullPath, entry.name);
378
+ } else if (entry.name.endsWith('.tmpl')) {
379
+ const relativePath = fullPath.slice(templatesDir.length + 1);
380
+ const outputPath = relativePath.replace(/\.tmpl$/, '');
381
+
382
+ templates.push({
383
+ sourcePath: fullPath,
384
+ relativePath,
385
+ outputPath,
386
+ category,
387
+ });
388
+ }
389
+ }
390
+ }
391
+
392
+ await scanDir(templatesDir);
393
+ return templates;
394
+ }
395
+
396
+ /**
397
+ * Process all templates and return their contents
398
+ */
399
+ export async function processAllTemplates(
400
+ templatesDir: string,
401
+ variables: TemplateVariables
402
+ ): Promise<Map<string, string>> {
403
+ const templates = await discoverTemplates(templatesDir);
404
+ const processed = new Map<string, string>();
405
+
406
+ for (const template of templates) {
407
+ const content = await processTemplateFile(template.sourcePath, variables);
408
+ processed.set(template.outputPath, content);
409
+ }
410
+
411
+ return processed;
412
+ }