@supunkalharajayasinghe/project-cli 1.3.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/dist/commands/create.d.ts +3 -0
  4. package/dist/commands/create.d.ts.map +1 -0
  5. package/dist/commands/create.js +48 -0
  6. package/dist/commands/create.js.map +1 -0
  7. package/dist/commands/greet.d.ts +3 -0
  8. package/dist/commands/greet.d.ts.map +1 -0
  9. package/dist/commands/greet.js +10 -0
  10. package/dist/commands/greet.js.map +1 -0
  11. package/dist/generators/applyAiPlan.d.ts +3 -0
  12. package/dist/generators/applyAiPlan.d.ts.map +1 -0
  13. package/dist/generators/applyAiPlan.js +426 -0
  14. package/dist/generators/applyAiPlan.js.map +1 -0
  15. package/dist/generators/applyFeatureModules.d.ts +4 -0
  16. package/dist/generators/applyFeatureModules.d.ts.map +1 -0
  17. package/dist/generators/applyFeatureModules.js +242 -0
  18. package/dist/generators/applyFeatureModules.js.map +1 -0
  19. package/dist/generators/applyFullstackPlan.d.ts +3 -0
  20. package/dist/generators/applyFullstackPlan.d.ts.map +1 -0
  21. package/dist/generators/applyFullstackPlan.js +413 -0
  22. package/dist/generators/applyFullstackPlan.js.map +1 -0
  23. package/dist/generators/applyPlan.d.ts +3 -0
  24. package/dist/generators/applyPlan.d.ts.map +1 -0
  25. package/dist/generators/applyPlan.js +22 -0
  26. package/dist/generators/applyPlan.js.map +1 -0
  27. package/dist/generators/applyWebsitePlan.d.ts +3 -0
  28. package/dist/generators/applyWebsitePlan.d.ts.map +1 -0
  29. package/dist/generators/applyWebsitePlan.js +1053 -0
  30. package/dist/generators/applyWebsitePlan.js.map +1 -0
  31. package/dist/generators/createNextBase.d.ts +3 -0
  32. package/dist/generators/createNextBase.d.ts.map +1 -0
  33. package/dist/generators/createNextBase.js +20 -0
  34. package/dist/generators/createNextBase.js.map +1 -0
  35. package/dist/generators/generateBasicProject.d.ts +3 -0
  36. package/dist/generators/generateBasicProject.d.ts.map +1 -0
  37. package/dist/generators/generateBasicProject.js +65 -0
  38. package/dist/generators/generateBasicProject.js.map +1 -0
  39. package/dist/generators/starterBranding.d.ts +9 -0
  40. package/dist/generators/starterBranding.d.ts.map +1 -0
  41. package/dist/generators/starterBranding.js +48 -0
  42. package/dist/generators/starterBranding.js.map +1 -0
  43. package/dist/generators/writeGeneratedReadme.d.ts +3 -0
  44. package/dist/generators/writeGeneratedReadme.d.ts.map +1 -0
  45. package/dist/generators/writeGeneratedReadme.js +99 -0
  46. package/dist/generators/writeGeneratedReadme.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +19 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/prompts/askBasicScaffoldOptions.d.ts +3 -0
  52. package/dist/prompts/askBasicScaffoldOptions.d.ts.map +1 -0
  53. package/dist/prompts/askBasicScaffoldOptions.js +36 -0
  54. package/dist/prompts/askBasicScaffoldOptions.js.map +1 -0
  55. package/dist/prompts/askCreatePlan.d.ts +3 -0
  56. package/dist/prompts/askCreatePlan.d.ts.map +1 -0
  57. package/dist/prompts/askCreatePlan.js +232 -0
  58. package/dist/prompts/askCreatePlan.js.map +1 -0
  59. package/dist/types.d.ts +64 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/utils/files.d.ts +14 -0
  64. package/dist/utils/files.d.ts.map +1 -0
  65. package/dist/utils/files.js +47 -0
  66. package/dist/utils/files.js.map +1 -0
  67. package/dist/utils/generateSecret.d.ts +2 -0
  68. package/dist/utils/generateSecret.d.ts.map +1 -0
  69. package/dist/utils/generateSecret.js +5 -0
  70. package/dist/utils/generateSecret.js.map +1 -0
  71. package/dist/utils/pathExists.d.ts +2 -0
  72. package/dist/utils/pathExists.d.ts.map +1 -0
  73. package/dist/utils/pathExists.js +11 -0
  74. package/dist/utils/pathExists.js.map +1 -0
  75. package/dist/utils/runCommand.d.ts +8 -0
  76. package/dist/utils/runCommand.d.ts.map +1 -0
  77. package/dist/utils/runCommand.js +59 -0
  78. package/dist/utils/runCommand.js.map +1 -0
  79. package/dist/utils/strings.d.ts +3 -0
  80. package/dist/utils/strings.d.ts.map +1 -0
  81. package/dist/utils/strings.js +15 -0
  82. package/dist/utils/strings.js.map +1 -0
  83. package/dist/utils/validateFreeText.d.ts +13 -0
  84. package/dist/utils/validateFreeText.d.ts.map +1 -0
  85. package/dist/utils/validateFreeText.js +19 -0
  86. package/dist/utils/validateFreeText.js.map +1 -0
  87. package/dist/utils/validateProjectName.d.ts +7 -0
  88. package/dist/utils/validateProjectName.d.ts.map +1 -0
  89. package/dist/utils/validateProjectName.js +42 -0
  90. package/dist/utils/validateProjectName.js.map +1 -0
  91. package/dist/utils/writeJson.d.ts +2 -0
  92. package/dist/utils/writeJson.d.ts.map +1 -0
  93. package/dist/utils/writeJson.js +2 -0
  94. package/dist/utils/writeJson.js.map +1 -0
  95. package/package.json +49 -0
@@ -0,0 +1,426 @@
1
+ import path from 'node:path';
2
+ import { writeFileSafe } from '../utils/files.js';
3
+ import { toTitle } from '../utils/strings.js';
4
+ import { starterBranding } from './starterBranding.js';
5
+ export async function applyAiPlan(plan) {
6
+ if (plan.blueprint !== 'ai-app') {
7
+ throw new Error('applyAiPlan can only be used with ai-app plans.');
8
+ }
9
+ await writeLayout(plan);
10
+ await writeHomePage(plan);
11
+ await writeChatRoute(plan);
12
+ await writeChatComponents(plan);
13
+ await writeAiLib(plan);
14
+ await writeAiTypes(plan);
15
+ }
16
+ async function writeLayout(plan) {
17
+ await writeFileSafe(path.join(plan.targetPath, 'src/app/layout.tsx'), `import type { Metadata } from 'next';
18
+ import type { ReactNode } from 'react';
19
+ import './globals.css';
20
+
21
+ export const metadata: Metadata = {
22
+ title: ${JSON.stringify(toTitle(plan.projectName))},
23
+ description: 'Generated AI application starter.',
24
+ };
25
+
26
+ export default function RootLayout({
27
+ children,
28
+ }: Readonly<{
29
+ children: ReactNode;
30
+ }>) {
31
+ return (
32
+ <html lang="en">
33
+ <body className="antialiased">{children}</body>
34
+ </html>
35
+ );
36
+ }
37
+ `);
38
+ }
39
+ async function writeHomePage(plan) {
40
+ const intro = plan.shape === 'chat'
41
+ ? 'A premium starter chat experience for building conversational AI products.'
42
+ : plan.shape === 'assistant'
43
+ ? 'A premium starter assistant experience for building AI-powered workflows.'
44
+ : 'A premium starter content generation experience for building AI tools.';
45
+ await writeFileSafe(path.join(plan.targetPath, 'src/app/page.tsx'), `import { Chat } from '@/components/chat/Chat';
46
+ import { Sparkles, Terminal, Settings, Cpu, Database } from 'lucide-react';
47
+
48
+ export default function HomePage() {
49
+ const intro = '${intro}';
50
+
51
+ return (
52
+ <div className="min-h-screen flex flex-col bg-slate-50/50">
53
+ <header className="border-b border-slate-200/80 bg-white/80 backdrop-blur-md sticky top-0 z-50">
54
+ <div className="mx-auto max-w-6xl px-6 py-4 flex items-center justify-between">
55
+ <div className="flex items-center gap-2">
56
+ <span className="h-6 w-6 rounded-lg bg-blue-600 flex items-center justify-center text-xs font-bold text-white shadow-sm shadow-blue-500/30">
57
+ AI
58
+ </span>
59
+ <span className="text-base font-bold tracking-tight text-slate-900">${toTitle(plan.projectName)}</span>
60
+ </div>
61
+ <div className="flex items-center gap-4">
62
+ <a
63
+ href="${starterBranding.githubUrl}"
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ className="text-sm font-medium text-slate-500 hover:text-slate-900 transition"
67
+ >
68
+ GitHub Docs
69
+ </a>
70
+ </div>
71
+ </div>
72
+ </header>
73
+
74
+ <main className="flex-1 mx-auto w-full max-w-6xl px-6 py-12 grid gap-8 md:grid-cols-4 relative overflow-hidden">
75
+ <div className="absolute top-1/4 left-1/4 -translate-x-1/2 -translate-y-1/2 -z-10 h-72 w-72 rounded-full bg-blue-400/5 blur-3xl pointer-events-none" />
76
+
77
+ {/* Left Column: AI Config Info Card */}
78
+ <section className="md:col-span-1 space-y-6">
79
+ <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
80
+ <div className="flex items-center gap-2 mb-4">
81
+ <Terminal className="h-5 w-5 text-blue-600" />
82
+ <h2 className="font-bold text-slate-950 text-base">Specifications</h2>
83
+ </div>
84
+ <p className="text-xs text-slate-500 leading-relaxed font-medium">
85
+ Details about your configured AI model integration.
86
+ </p>
87
+
88
+ <div className="mt-4 pt-4 border-t border-slate-100 space-y-3 font-semibold">
89
+ <div>
90
+ <span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">Blueprint</span>
91
+ <span className="text-xs text-slate-700">${plan.shape}</span>
92
+ </div>
93
+ <div>
94
+ <span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">Target Model</span>
95
+ <code className="text-[10px] bg-slate-100 px-1.5 py-0.5 rounded font-mono text-blue-600">gpt-4o / gemini-1.5</code>
96
+ </div>
97
+ <div>
98
+ <span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">API Route</span>
99
+ <code className="text-[10px] bg-slate-100 px-1.5 py-0.5 rounded font-mono text-slate-650">/api/chat</code>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Model Capabilities Card */}
105
+ <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
106
+ <div className="flex items-center gap-2 mb-4">
107
+ <Settings className="h-5 w-5 text-blue-600" />
108
+ <h3 className="font-bold text-slate-950 text-base">Model Tuning</h3>
109
+ </div>
110
+ <ul className="space-y-3 text-xs text-slate-600 font-medium">
111
+ <li className="flex items-center gap-2">
112
+ <Cpu className="h-4 w-4 text-slate-400" />
113
+ <span>Temperature: 0.7</span>
114
+ </li>
115
+ <li className="flex items-center gap-2">
116
+ <Database className="h-4 w-4 text-slate-400" />
117
+ <span>Max Tokens: 4096</span>
118
+ </li>
119
+ </ul>
120
+ </div>
121
+
122
+ <div className="rounded-2xl border border-blue-100 bg-blue-50/50 p-6 shadow-sm flex items-start gap-4">
123
+ <Sparkles className="h-5 w-5 text-blue-600 flex-shrink-0" />
124
+ <div>
125
+ <h3 className="text-xs font-bold text-blue-900 uppercase tracking-wide">Developer Tip</h3>
126
+ <p className="mt-2 text-xs text-blue-800 leading-relaxed font-medium">
127
+ Connect this app to OpenAI, Anthropic, or Google Gemini by editing the backend route in <code className="font-mono text-[10px] bg-blue-100/50 px-1.5 py-0.5 rounded text-blue-900">src/app/api/chat/route.ts</code>.
128
+ </p>
129
+ </div>
130
+ </div>
131
+ </section>
132
+
133
+ {/* Right Column: Chat Box */}
134
+ <section className="md:col-span-3 flex flex-col">
135
+ <div className="mb-6">
136
+ <span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 ring-1 ring-inset ring-blue-700/10 mb-3">
137
+ <Sparkles className="h-3 w-3 animate-pulse" />
138
+ AI App Scaffolding
139
+ </span>
140
+ <h1 className="text-3xl font-extrabold tracking-tight text-slate-900">Interactive Chat Console</h1>
141
+ <p className="text-sm text-slate-500 font-medium mt-1">{intro}</p>
142
+ </div>
143
+ <Chat />
144
+ </section>
145
+ </main>
146
+
147
+ <footer className="border-t border-slate-200 bg-white py-6 text-center text-xs text-slate-400">
148
+ <div className="mx-auto max-w-6xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
149
+ <div>
150
+ © {new Date().getFullYear()} ${toTitle(plan.projectName)}. All rights reserved.
151
+ </div>
152
+ <div className="flex items-center gap-1.5">
153
+ <span>Generated with ${starterBranding.cliName} ${starterBranding.version}</span>
154
+ <span className="text-slate-300">•</span>
155
+ <span className="font-semibold text-slate-550">${starterBranding.releaseName}</span>
156
+ </div>
157
+ </div>
158
+ </footer>
159
+ </div>
160
+ );
161
+ }
162
+ `);
163
+ }
164
+ async function writeChatRoute(plan) {
165
+ await writeFileSafe(path.join(plan.targetPath, 'src/app/api/chat/route.ts'), `import { getSystemPrompt } from '@/lib/ai';
166
+ import { z } from 'zod';
167
+
168
+ const ChatRequestSchema = z.object({
169
+ messages: z
170
+ .array(
171
+ z.object({
172
+ role: z.enum(['user', 'assistant', 'system']),
173
+ content: z.string().min(1).max(8000),
174
+ })
175
+ )
176
+ .min(1)
177
+ .max(50),
178
+ });
179
+
180
+ export async function POST(req: Request) {
181
+ let body: unknown;
182
+ try {
183
+ body = await req.json();
184
+ } catch {
185
+ return Response.json({ error: 'Invalid JSON body.' }, { status: 400 });
186
+ }
187
+
188
+ const parsed = ChatRequestSchema.safeParse(body);
189
+ if (!parsed.success) {
190
+ return Response.json(
191
+ { error: 'Request did not match the expected shape.', details: parsed.error.flatten() },
192
+ { status: 400 }
193
+ );
194
+ }
195
+
196
+ const { messages } = parsed.data;
197
+
198
+ return Response.json({
199
+ id: crypto.randomUUID(),
200
+ role: 'assistant',
201
+ content:
202
+ 'This is a placeholder AI response. Connect your preferred AI provider inside src/app/api/chat/route.ts.',
203
+ systemPrompt: getSystemPrompt(),
204
+ receivedMessages: messages.length,
205
+ blueprint: '${plan.shape}',
206
+ });
207
+ }
208
+ `);
209
+ }
210
+ async function writeChatComponents(plan) {
211
+ await writeFileSafe(path.join(plan.targetPath, 'src/components/chat/Chat.tsx'), `'use client';
212
+
213
+ import { useState } from 'react';
214
+ import type { ChatMessage } from '@/types/ai';
215
+ import { ChatInput } from './ChatInput';
216
+ import { MessageList } from './MessageList';
217
+
218
+ const initialMessages: ChatMessage[] = [
219
+ {
220
+ id: 'welcome',
221
+ role: 'assistant',
222
+ content:
223
+ 'Hello! This is your generated AI app starter. Replace the placeholder API route with your real AI provider logic.',
224
+ },
225
+ ];
226
+
227
+ export function Chat() {
228
+ const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
229
+ const [isLoading, setIsLoading] = useState(false);
230
+
231
+ async function handleSendMessage(content: string) {
232
+ const userMessage: ChatMessage = {
233
+ id: crypto.randomUUID(),
234
+ role: 'user',
235
+ content,
236
+ };
237
+
238
+ const nextMessages = [...messages, userMessage];
239
+
240
+ setMessages(nextMessages);
241
+ setIsLoading(true);
242
+
243
+ try {
244
+ const response = await fetch('/api/chat', {
245
+ method: 'POST',
246
+ headers: {
247
+ 'Content-Type': 'application/json',
248
+ },
249
+ body: JSON.stringify({ messages: nextMessages }),
250
+ });
251
+
252
+ if (!response.ok) {
253
+ throw new Error('Failed to get AI response.');
254
+ }
255
+
256
+ const data = (await response.json()) as ChatMessage;
257
+
258
+ setMessages((currentMessages) => [
259
+ ...currentMessages,
260
+ {
261
+ id: data.id ?? crypto.randomUUID(),
262
+ role: 'assistant',
263
+ content: data.content,
264
+ },
265
+ ]);
266
+ } catch {
267
+ setMessages((currentMessages) => [
268
+ ...currentMessages,
269
+ {
270
+ id: crypto.randomUUID(),
271
+ role: 'assistant',
272
+ content:
273
+ 'Something went wrong while calling the AI route. Check src/app/api/chat/route.ts.',
274
+ },
275
+ ]);
276
+ } finally {
277
+ setIsLoading(false);
278
+ }
279
+ }
280
+
281
+ return (
282
+ <div className="flex flex-col h-[520px] rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
283
+ <MessageList messages={messages} isLoading={isLoading} />
284
+ <ChatInput disabled={isLoading} onSendMessage={handleSendMessage} />
285
+ </div>
286
+ );
287
+ }
288
+ `);
289
+ await writeFileSafe(path.join(plan.targetPath, 'src/components/chat/MessageList.tsx'), `import type { ChatMessage } from '@/types/ai';
290
+ import { Bot, User } from 'lucide-react';
291
+
292
+ interface MessageListProps {
293
+ messages: ChatMessage[];
294
+ isLoading: boolean;
295
+ }
296
+
297
+ export function MessageList({ messages, isLoading }: MessageListProps) {
298
+ return (
299
+ <div className="flex-1 space-y-4 overflow-y-auto p-6 bg-slate-50/30">
300
+ {messages.map((message) => {
301
+ const isUser = message.role === 'user';
302
+ return (
303
+ <div
304
+ className={\`flex \${isUser ? 'justify-end' : 'justify-start'}\`}
305
+ key={message.id}
306
+ >
307
+ <div
308
+ className={\`max-w-[80%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm \${
309
+ isUser
310
+ ? 'bg-blue-600 text-white rounded-br-none'
311
+ : 'bg-white text-slate-800 border border-slate-200 rounded-bl-none'
312
+ }\`}
313
+ >
314
+ <span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider mb-1 opacity-70">
315
+ {isUser ? <User className="h-3 w-3" /> : <Bot className="h-3 w-3" />}
316
+ {isUser ? 'You' : 'AI Assistant'}
317
+ </span>
318
+ <p className="whitespace-pre-wrap">{message.content}</p>
319
+ </div>
320
+ </div>
321
+ );
322
+ })}
323
+
324
+ {isLoading && (
325
+ <div className="flex justify-start">
326
+ <div className="max-w-[80%] rounded-2xl rounded-bl-none border border-slate-200 bg-white px-4 py-3 text-sm text-slate-500 shadow-sm">
327
+ <span className="flex items-center gap-1 py-1">
328
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style={{ animationDelay: '0ms' }} />
329
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style={{ animationDelay: '150ms' }} />
330
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style={{ animationDelay: '300ms' }} />
331
+ </span>
332
+ </div>
333
+ </div>
334
+ )}
335
+ </div>
336
+ );
337
+ }
338
+ `);
339
+ await writeFileSafe(path.join(plan.targetPath, 'src/components/chat/ChatInput.tsx'), `'use client';
340
+
341
+ import { useState } from 'react';
342
+ import { Send } from 'lucide-react';
343
+
344
+ interface ChatInputProps {
345
+ disabled: boolean;
346
+ onSendMessage: (content: string) => Promise<void>;
347
+ }
348
+
349
+ export function ChatInput({ disabled, onSendMessage }: ChatInputProps) {
350
+ const [input, setInput] = useState('');
351
+
352
+ async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
353
+ event.preventDefault();
354
+
355
+ const content = input.trim();
356
+
357
+ if (!content) {
358
+ return;
359
+ }
360
+
361
+ setInput('');
362
+ await onSendMessage(content);
363
+ }
364
+
365
+ return (
366
+ <form
367
+ className="flex gap-3 border-t border-slate-200 p-4 bg-white"
368
+ onSubmit={handleSubmit}
369
+ >
370
+ <input
371
+ className="flex-1 rounded-xl border border-slate-200 bg-slate-50/50 px-4 py-3 text-sm text-slate-800 placeholder-slate-400 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-1 focus:ring-blue-500"
372
+ disabled={disabled}
373
+ onChange={(event) => setInput(event.currentTarget.value)}
374
+ placeholder="Type a message..."
375
+ type="text"
376
+ value={input}
377
+ />
378
+
379
+ <button
380
+ className="rounded-xl bg-blue-600 px-5 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition disabled:cursor-not-allowed disabled:opacity-50 inline-flex items-center gap-1.5"
381
+ disabled={disabled}
382
+ type="submit"
383
+ >
384
+ <span>Send</span>
385
+ <Send className="h-4 w-4" />
386
+ </button>
387
+ </form>
388
+ );
389
+ }
390
+ `);
391
+ }
392
+ async function writeAiLib(plan) {
393
+ const systemPrompt = plan.shape === 'chat'
394
+ ? 'You are a helpful AI chat assistant.'
395
+ : plan.shape === 'assistant'
396
+ ? 'You are a practical AI assistant that helps users complete workflows.'
397
+ : 'You are an AI content generation assistant.';
398
+ await writeFileSafe(path.join(plan.targetPath, 'src/lib/ai.ts'), `export function getSystemPrompt(): string {
399
+ return '${systemPrompt}';
400
+ }
401
+
402
+ export const aiConfig = {
403
+ blueprint: '${plan.shape}',
404
+ model: process.env.AI_MODEL ?? 'your-model-name',
405
+ apiKey: process.env.AI_API_KEY,
406
+ } as const;
407
+ `);
408
+ }
409
+ async function writeAiTypes(plan) {
410
+ await writeFileSafe(path.join(plan.targetPath, 'src/types/ai.ts'), `export type AiAppShape = 'chat' | 'assistant' | 'content-generator';
411
+
412
+ export type ChatRole = 'user' | 'assistant' | 'system';
413
+
414
+ export interface ChatMessage {
415
+ id: string;
416
+ role: ChatRole;
417
+ content: string;
418
+ }
419
+
420
+ export interface AiAppConfig {
421
+ name: string;
422
+ shape: AiAppShape;
423
+ }
424
+ `);
425
+ }
426
+ //# sourceMappingURL=applyAiPlan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyAiPlan.js","sourceRoot":"","sources":["../../src/generators/applyAiPlan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { CreatePlan } from '../types.js';
2
+ export declare function applyFeatureModules(plan: CreatePlan): Promise<void>;
3
+ export declare function appendToGitignore(projectRoot: string): Promise<void>;
4
+ //# sourceMappingURL=applyFeatureModules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyFeatureModules.d.ts","sourceRoot":"","sources":["../../src/generators/applyFeatureModules.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAU9C,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAkQD,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB1E"}
@@ -0,0 +1,242 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { readJsonFile, writeFileSafe, writeJsonFile, assertPathSafe } from '../utils/files.js';
4
+ export async function applyFeatureModules(plan) {
5
+ await updateGeneratedPackageJson(plan);
6
+ if (plan.modules.includes('prisma')) {
7
+ await writePrismaModule(plan);
8
+ }
9
+ if (plan.modules.includes('redis')) {
10
+ await writeRedisModule(plan);
11
+ }
12
+ if (shouldWriteEnvExample(plan)) {
13
+ await writeEnvExample(plan);
14
+ }
15
+ if (plan.modules.includes('docker')) {
16
+ await writeDockerFiles(plan);
17
+ }
18
+ if (plan.modules.includes('github-actions')) {
19
+ await writeGithubActionsWorkflow(plan);
20
+ }
21
+ // Ensure environment files are always ignored by git
22
+ await appendToGitignore(plan.targetPath);
23
+ }
24
+ async function updateGeneratedPackageJson(plan) {
25
+ const packageJsonPath = path.join(plan.targetPath, 'package.json');
26
+ const packageJson = await readJsonFile(packageJsonPath);
27
+ const scripts = {
28
+ ...(packageJson.scripts ?? {}),
29
+ };
30
+ const dependencies = {
31
+ ...(packageJson.dependencies ?? {}),
32
+ };
33
+ const devDependencies = {
34
+ ...(packageJson.devDependencies ?? {}),
35
+ };
36
+ scripts.typecheck = 'tsc --noEmit';
37
+ // Add lucide-react and zod for premium icons and schema validation across all starter projects
38
+ dependencies['lucide-react'] = '^0.400.0';
39
+ dependencies['zod'] = '^3.23.8';
40
+ const overrides = {
41
+ ...(packageJson.overrides ?? {}),
42
+ };
43
+ // Pin postcss to resolve GHSA-qx2v-qp2m-jg93 XSS advisory in Next.js subdependencies
44
+ overrides.postcss = '^8.5.10';
45
+ if (plan.modules.includes('prisma')) {
46
+ dependencies['@prisma/client'] = '^6.0.0';
47
+ devDependencies.prisma = '^6.0.0';
48
+ scripts.db = 'prisma studio';
49
+ scripts['db:push'] = 'prisma db push';
50
+ scripts['db:generate'] = 'prisma generate';
51
+ }
52
+ if (plan.modules.includes('redis')) {
53
+ dependencies.ioredis = '^5.0.0';
54
+ }
55
+ packageJson.scripts = scripts;
56
+ packageJson.dependencies = dependencies;
57
+ packageJson.devDependencies = devDependencies;
58
+ packageJson.overrides = overrides;
59
+ await writeJsonFile(packageJsonPath, packageJson);
60
+ }
61
+ async function writePrismaModule(plan) {
62
+ await writeFileSafe(path.join(plan.targetPath, 'prisma/schema.prisma'), `generator client {
63
+ provider = "prisma-client-js"
64
+ }
65
+
66
+ datasource db {
67
+ provider = "postgresql"
68
+ url = env("DATABASE_URL")
69
+ }
70
+
71
+ model User {
72
+ id String @id @default(cuid())
73
+ email String @unique
74
+ name String?
75
+ createdAt DateTime @default(now())
76
+ updatedAt DateTime @updatedAt
77
+ }
78
+ `);
79
+ await writeFileSafe(path.join(plan.targetPath, 'src/lib/db.ts'), `import { PrismaClient } from '@prisma/client';
80
+
81
+ const globalForPrisma = globalThis as unknown as {
82
+ prisma?: PrismaClient;
83
+ };
84
+
85
+ export const db = globalForPrisma.prisma ?? new PrismaClient();
86
+
87
+ if (process.env.NODE_ENV !== 'production') {
88
+ globalForPrisma.prisma = db;
89
+ }
90
+ `);
91
+ }
92
+ async function writeRedisModule(plan) {
93
+ await writeFileSafe(path.join(plan.targetPath, 'src/lib/redis.ts'), `import Redis from 'ioredis';
94
+
95
+ const globalForRedis = globalThis as unknown as {
96
+ redis?: Redis;
97
+ };
98
+
99
+ export const redis =
100
+ globalForRedis.redis ??
101
+ new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379');
102
+
103
+ if (process.env.NODE_ENV !== 'production') {
104
+ globalForRedis.redis = redis;
105
+ }
106
+ `);
107
+ }
108
+ function shouldWriteEnvExample(plan) {
109
+ return (plan.modules.includes('env-example') ||
110
+ plan.modules.includes('postgresql') ||
111
+ plan.modules.includes('redis') ||
112
+ plan.modules.includes('ai-sdk'));
113
+ }
114
+ async function writeEnvExample(plan) {
115
+ const lines = ['# Generated by ts-cli-tool'];
116
+ if (plan.modules.includes('postgresql')) {
117
+ lines.push('DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app_db?schema=public"');
118
+ }
119
+ if (plan.modules.includes('redis')) {
120
+ lines.push('REDIS_URL="redis://localhost:6379"');
121
+ }
122
+ if (plan.modules.includes('ai-sdk')) {
123
+ lines.push('AI_API_KEY="your-ai-provider-api-key"');
124
+ lines.push('AI_MODEL="your-model-name"');
125
+ }
126
+ await writeFileSafe(path.join(plan.targetPath, '.env.example'), `${lines.join('\n')}\n`);
127
+ }
128
+ async function writeDockerFiles(plan) {
129
+ await writeFileSafe(path.join(plan.targetPath, 'Dockerfile'), `FROM node:20-alpine AS deps
130
+ WORKDIR /app
131
+ COPY package*.json ./
132
+ RUN npm install
133
+
134
+ FROM node:20-alpine AS builder
135
+ WORKDIR /app
136
+ COPY --from=deps /app/node_modules ./node_modules
137
+ COPY . .
138
+ RUN npm run build
139
+
140
+ FROM node:20-alpine AS runner
141
+ WORKDIR /app
142
+ ENV NODE_ENV=production
143
+ COPY --from=builder /app ./
144
+ EXPOSE 3000
145
+ CMD ["npm", "start"]
146
+ `);
147
+ if (!plan.modules.includes('postgresql') && !plan.modules.includes('redis')) {
148
+ return;
149
+ }
150
+ const services = [];
151
+ if (plan.modules.includes('postgresql')) {
152
+ services.push(` postgres:
153
+ image: postgres:16-alpine
154
+ restart: unless-stopped
155
+ environment:
156
+ POSTGRES_USER: postgres
157
+ POSTGRES_PASSWORD: postgres
158
+ POSTGRES_DB: app_db
159
+ ports:
160
+ - "5432:5432"
161
+ volumes:
162
+ - postgres_data:/var/lib/postgresql/data`);
163
+ }
164
+ if (plan.modules.includes('redis')) {
165
+ services.push(` redis:
166
+ image: redis:7-alpine
167
+ restart: unless-stopped
168
+ ports:
169
+ - "6379:6379"`);
170
+ }
171
+ const volumes = plan.modules.includes('postgresql')
172
+ ? `
173
+ volumes:
174
+ postgres_data:
175
+ `
176
+ : '';
177
+ await writeFileSafe(path.join(plan.targetPath, 'docker-compose.yml'), `services:
178
+ ${services.join('\n\n')}
179
+ ${volumes}`);
180
+ }
181
+ async function writeGithubActionsWorkflow(plan) {
182
+ await writeFileSafe(path.join(plan.targetPath, '.github/workflows/ci.yml'), `name: CI
183
+
184
+ on:
185
+ push:
186
+ branches:
187
+ - main
188
+ pull_request:
189
+
190
+ jobs:
191
+ build:
192
+ runs-on: ubuntu-latest
193
+
194
+ steps:
195
+ - name: Checkout repository
196
+ uses: actions/checkout@v4
197
+
198
+ - name: Setup Node.js
199
+ uses: actions/setup-node@v4
200
+ with:
201
+ node-version: 20
202
+ cache: npm
203
+
204
+ - name: Install dependencies
205
+ run: npm ci
206
+
207
+ - name: Lint
208
+ run: npm run lint --if-present
209
+
210
+ - name: Typecheck
211
+ run: npm run typecheck --if-present
212
+
213
+ - name: Build
214
+ run: npm run build --if-present
215
+ `);
216
+ }
217
+ const REQUIRED_ENV_IGNORES = [
218
+ '.env',
219
+ '.env.local',
220
+ '.env.development.local',
221
+ '.env.test.local',
222
+ '.env.production.local',
223
+ ];
224
+ export async function appendToGitignore(projectRoot) {
225
+ const gitignorePath = path.join(projectRoot, '.gitignore');
226
+ assertPathSafe(gitignorePath);
227
+ let existing = '';
228
+ try {
229
+ existing = await fs.readFile(gitignorePath, 'utf-8');
230
+ }
231
+ catch {
232
+ existing = ''; // no .gitignore yet — fine, create one
233
+ }
234
+ const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
235
+ const missing = REQUIRED_ENV_IGNORES.filter((entry) => !existingLines.has(entry));
236
+ if (missing.length === 0)
237
+ return;
238
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith('\n');
239
+ const addition = `${needsLeadingNewline ? '\n' : ''}\n# Environment variables\n${missing.join('\n')}\n`;
240
+ await fs.writeFile(gitignorePath, existing + addition, 'utf-8');
241
+ }
242
+ //# sourceMappingURL=applyFeatureModules.js.map