@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.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +48 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/greet.d.ts +3 -0
- package/dist/commands/greet.d.ts.map +1 -0
- package/dist/commands/greet.js +10 -0
- package/dist/commands/greet.js.map +1 -0
- package/dist/generators/applyAiPlan.d.ts +3 -0
- package/dist/generators/applyAiPlan.d.ts.map +1 -0
- package/dist/generators/applyAiPlan.js +426 -0
- package/dist/generators/applyAiPlan.js.map +1 -0
- package/dist/generators/applyFeatureModules.d.ts +4 -0
- package/dist/generators/applyFeatureModules.d.ts.map +1 -0
- package/dist/generators/applyFeatureModules.js +242 -0
- package/dist/generators/applyFeatureModules.js.map +1 -0
- package/dist/generators/applyFullstackPlan.d.ts +3 -0
- package/dist/generators/applyFullstackPlan.d.ts.map +1 -0
- package/dist/generators/applyFullstackPlan.js +413 -0
- package/dist/generators/applyFullstackPlan.js.map +1 -0
- package/dist/generators/applyPlan.d.ts +3 -0
- package/dist/generators/applyPlan.d.ts.map +1 -0
- package/dist/generators/applyPlan.js +22 -0
- package/dist/generators/applyPlan.js.map +1 -0
- package/dist/generators/applyWebsitePlan.d.ts +3 -0
- package/dist/generators/applyWebsitePlan.d.ts.map +1 -0
- package/dist/generators/applyWebsitePlan.js +1053 -0
- package/dist/generators/applyWebsitePlan.js.map +1 -0
- package/dist/generators/createNextBase.d.ts +3 -0
- package/dist/generators/createNextBase.d.ts.map +1 -0
- package/dist/generators/createNextBase.js +20 -0
- package/dist/generators/createNextBase.js.map +1 -0
- package/dist/generators/generateBasicProject.d.ts +3 -0
- package/dist/generators/generateBasicProject.d.ts.map +1 -0
- package/dist/generators/generateBasicProject.js +65 -0
- package/dist/generators/generateBasicProject.js.map +1 -0
- package/dist/generators/starterBranding.d.ts +9 -0
- package/dist/generators/starterBranding.d.ts.map +1 -0
- package/dist/generators/starterBranding.js +48 -0
- package/dist/generators/starterBranding.js.map +1 -0
- package/dist/generators/writeGeneratedReadme.d.ts +3 -0
- package/dist/generators/writeGeneratedReadme.d.ts.map +1 -0
- package/dist/generators/writeGeneratedReadme.js +99 -0
- package/dist/generators/writeGeneratedReadme.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/askBasicScaffoldOptions.d.ts +3 -0
- package/dist/prompts/askBasicScaffoldOptions.d.ts.map +1 -0
- package/dist/prompts/askBasicScaffoldOptions.js +36 -0
- package/dist/prompts/askBasicScaffoldOptions.js.map +1 -0
- package/dist/prompts/askCreatePlan.d.ts +3 -0
- package/dist/prompts/askCreatePlan.d.ts.map +1 -0
- package/dist/prompts/askCreatePlan.js +232 -0
- package/dist/prompts/askCreatePlan.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/files.d.ts +14 -0
- package/dist/utils/files.d.ts.map +1 -0
- package/dist/utils/files.js +47 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/generateSecret.d.ts +2 -0
- package/dist/utils/generateSecret.d.ts.map +1 -0
- package/dist/utils/generateSecret.js +5 -0
- package/dist/utils/generateSecret.js.map +1 -0
- package/dist/utils/pathExists.d.ts +2 -0
- package/dist/utils/pathExists.d.ts.map +1 -0
- package/dist/utils/pathExists.js +11 -0
- package/dist/utils/pathExists.js.map +1 -0
- package/dist/utils/runCommand.d.ts +8 -0
- package/dist/utils/runCommand.d.ts.map +1 -0
- package/dist/utils/runCommand.js +59 -0
- package/dist/utils/runCommand.js.map +1 -0
- package/dist/utils/strings.d.ts +3 -0
- package/dist/utils/strings.d.ts.map +1 -0
- package/dist/utils/strings.js +15 -0
- package/dist/utils/strings.js.map +1 -0
- package/dist/utils/validateFreeText.d.ts +13 -0
- package/dist/utils/validateFreeText.d.ts.map +1 -0
- package/dist/utils/validateFreeText.js +19 -0
- package/dist/utils/validateFreeText.js.map +1 -0
- package/dist/utils/validateProjectName.d.ts +7 -0
- package/dist/utils/validateProjectName.d.ts.map +1 -0
- package/dist/utils/validateProjectName.js +42 -0
- package/dist/utils/validateProjectName.js.map +1 -0
- package/dist/utils/writeJson.d.ts +2 -0
- package/dist/utils/writeJson.d.ts.map +1 -0
- package/dist/utils/writeJson.js +2 -0
- package/dist/utils/writeJson.js.map +1 -0
- 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 @@
|
|
|
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
|