create-stackforge 0.0.1
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/README.md +125 -0
- package/bin/cli.js +2 -0
- package/dist/cli.js +3148 -0
- package/dist/cli.js.map +1 -0
- package/dist/npm-registry-F7EVX3RR.js +18 -0
- package/dist/npm-registry-F7EVX3RR.js.map +1 -0
- package/package.json +74 -0
- package/templates/api/graphql/client-usage.jsx +14 -0
- package/templates/api/graphql/client-usage.tsx +14 -0
- package/templates/api/graphql/client.js +12 -0
- package/templates/api/graphql/client.ts +12 -0
- package/templates/api/graphql/route.js +32 -0
- package/templates/api/graphql/route.ts +23 -0
- package/templates/api/graphql/schema.graphql +13 -0
- package/templates/api/graphql/vite-server.js +27 -0
- package/templates/api/graphql/vite-server.ts +27 -0
- package/templates/api/rest/client-usage.jsx +14 -0
- package/templates/api/rest/client-usage.tsx +14 -0
- package/templates/api/rest/client.js +4 -0
- package/templates/api/rest/client.ts +4 -0
- package/templates/api/rest/route.js +3 -0
- package/templates/api/rest/route.ts +3 -0
- package/templates/api/rest/users-route.js +11 -0
- package/templates/api/rest/users-route.ts +11 -0
- package/templates/api/rest/vite-server.js +15 -0
- package/templates/api/rest/vite-server.ts +15 -0
- package/templates/api/trpc/client-usage.jsx +26 -0
- package/templates/api/trpc/client-usage.tsx +26 -0
- package/templates/api/trpc/client-vite.js +15 -0
- package/templates/api/trpc/client-vite.ts +16 -0
- package/templates/api/trpc/client.js +13 -0
- package/templates/api/trpc/client.ts +14 -0
- package/templates/api/trpc/root.ts +11 -0
- package/templates/api/trpc/route.js +12 -0
- package/templates/api/trpc/route.ts +12 -0
- package/templates/api/trpc/trpc.ts +6 -0
- package/templates/api/trpc/vite-server.js +9 -0
- package/templates/api/trpc/vite-server.ts +9 -0
- package/templates/auth/clerk-protected-page.jsx +11 -0
- package/templates/auth/clerk-protected-page.tsx +11 -0
- package/templates/auth/clerk-protected.jsx +11 -0
- package/templates/auth/clerk-protected.tsx +11 -0
- package/templates/auth/clerk-signin.jsx +11 -0
- package/templates/auth/clerk-signin.tsx +11 -0
- package/templates/auth/clerk.README.md +8 -0
- package/templates/auth/nextauth-options.js +3 -0
- package/templates/auth/nextauth-options.ts +5 -0
- package/templates/auth/nextauth-protected-page.jsx +12 -0
- package/templates/auth/nextauth-protected-page.tsx +12 -0
- package/templates/auth/nextauth-protected.jsx +12 -0
- package/templates/auth/nextauth-protected.tsx +12 -0
- package/templates/auth/nextauth-route.ts +6 -0
- package/templates/auth/nextauth-signin.jsx +15 -0
- package/templates/auth/nextauth-signin.tsx +15 -0
- package/templates/auth/nextauth.README.md +10 -0
- package/templates/auth/supabase-protected-page.jsx +13 -0
- package/templates/auth/supabase-protected-page.tsx +13 -0
- package/templates/auth/supabase-protected.jsx +13 -0
- package/templates/auth/supabase-protected.tsx +13 -0
- package/templates/auth/supabase-signin.jsx +25 -0
- package/templates/auth/supabase-signin.tsx +25 -0
- package/templates/auth/supabase-vite-signin.jsx +20 -0
- package/templates/auth/supabase-vite-signin.tsx +20 -0
- package/templates/auth/supabase.README.md +9 -0
- package/templates/database/drizzle/client.js +8 -0
- package/templates/database/drizzle/client.ts +8 -0
- package/templates/database/drizzle/drizzle.config.ts +6 -0
- package/templates/database/drizzle/example.js +6 -0
- package/templates/database/drizzle/example.ts +6 -0
- package/templates/database/drizzle/schema.ts +6 -0
- package/templates/database/mongoose/connection.js +13 -0
- package/templates/database/mongoose/connection.ts +13 -0
- package/templates/database/mongoose/model.js +7 -0
- package/templates/database/mongoose/model.ts +7 -0
- package/templates/database/prisma/client.js +3 -0
- package/templates/database/prisma/client.ts +3 -0
- package/templates/database/prisma/example.js +5 -0
- package/templates/database/prisma/example.ts +7 -0
- package/templates/database/prisma/schema.prisma +13 -0
- package/templates/database/providers/neon.README.md +9 -0
- package/templates/database/providers/supabase.README.md +9 -0
- package/templates/database/typeorm/data-source.js +15 -0
- package/templates/database/typeorm/data-source.ts +15 -0
- package/templates/database/typeorm/entity.js +10 -0
- package/templates/database/typeorm/entity.ts +10 -0
- package/templates/database/typeorm/migrations/README.md +5 -0
- package/templates/database/usage/drizzle-users.js +6 -0
- package/templates/database/usage/drizzle-users.ts +6 -0
- package/templates/database/usage/mongoose-users.js +5 -0
- package/templates/database/usage/mongoose-users.ts +5 -0
- package/templates/database/usage/prisma-users.js +5 -0
- package/templates/database/usage/prisma-users.ts +5 -0
- package/templates/database/usage/typeorm-users.js +9 -0
- package/templates/database/usage/typeorm-users.ts +9 -0
- package/templates/features/analytics/README.md +9 -0
- package/templates/features/analytics/posthog.js +7 -0
- package/templates/features/analytics/posthog.ts +9 -0
- package/templates/features/email/README.md +17 -0
- package/templates/features/email/resend.js +3 -0
- package/templates/features/email/resend.ts +3 -0
- package/templates/features/error-tracking/README.md +9 -0
- package/templates/features/error-tracking/sentry.js +7 -0
- package/templates/features/error-tracking/sentry.ts +7 -0
- package/templates/features/payments/README.md +17 -0
- package/templates/features/payments/stripe.js +5 -0
- package/templates/features/payments/stripe.ts +5 -0
- package/templates/features/storage/README.md +12 -0
- package/templates/features/storage/storage.js +5 -0
- package/templates/features/storage/storage.ts +5 -0
- package/templates/nextjs/app/actions.js +6 -0
- package/templates/nextjs/app/actions.ts +6 -0
- package/templates/nextjs/app/examples-page.jsx +16 -0
- package/templates/nextjs/app/examples-page.tsx +16 -0
- package/templates/nextjs/app/layout.jsx +7 -0
- package/templates/nextjs/app/layout.tsx +7 -0
- package/templates/nextjs/app/page.jsx +12 -0
- package/templates/nextjs/app/page.tsx +12 -0
- package/templates/nextjs/next.config.js +4 -0
- package/templates/nextjs/next.config.ts +7 -0
- package/templates/shared/.editorconfig +8 -0
- package/templates/ui/antd.README.md +7 -0
- package/templates/ui/antd.theme.js +5 -0
- package/templates/ui/antd.theme.ts +7 -0
- package/templates/ui/button.jsx +13 -0
- package/templates/ui/button.tsx +13 -0
- package/templates/ui/chakra.README.md +7 -0
- package/templates/ui/chakra.theme.js +8 -0
- package/templates/ui/chakra.theme.ts +8 -0
- package/templates/ui/components.json +14 -0
- package/templates/ui/demo-antd.jsx +5 -0
- package/templates/ui/demo-antd.tsx +5 -0
- package/templates/ui/demo-chakra.jsx +5 -0
- package/templates/ui/demo-chakra.tsx +5 -0
- package/templates/ui/demo-mantine.jsx +5 -0
- package/templates/ui/demo-mantine.tsx +5 -0
- package/templates/ui/demo-mui.jsx +5 -0
- package/templates/ui/demo-mui.tsx +5 -0
- package/templates/ui/demo-nextui.jsx +5 -0
- package/templates/ui/demo-nextui.tsx +5 -0
- package/templates/ui/demo-shadcn.jsx +5 -0
- package/templates/ui/demo-shadcn.tsx +5 -0
- package/templates/ui/demo-tailwind.jsx +3 -0
- package/templates/ui/demo-tailwind.tsx +3 -0
- package/templates/ui/mantine.README.md +7 -0
- package/templates/ui/mantine.theme.js +3 -0
- package/templates/ui/mantine.theme.ts +3 -0
- package/templates/ui/mui.README.md +7 -0
- package/templates/ui/mui.theme.js +7 -0
- package/templates/ui/mui.theme.ts +7 -0
- package/templates/ui/nextui.README.md +7 -0
- package/templates/ui/nextui.theme.js +1 -0
- package/templates/ui/nextui.theme.ts +1 -0
- package/templates/ui/postcss.config.js +1 -0
- package/templates/ui/shadcn.README.md +1 -0
- package/templates/ui/styles.css +3 -0
- package/templates/ui/tailwind.config.js +6 -0
- package/templates/ui/utils.js +3 -0
- package/templates/ui/utils.ts +3 -0
- package/templates/vite/App.jsx +12 -0
- package/templates/vite/App.tsx +12 -0
- package/templates/vite/index.html +12 -0
- package/templates/vite/main.jsx +12 -0
- package/templates/vite/main.tsx +12 -0
- package/templates/vite/vite-env.d.ts +1 -0
- package/templates/vite/vite.config.ts +12 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3148 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { Command as Command17 } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/cli/commands/create.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/cli/prompts/index.ts
|
|
8
|
+
import inquirer from "inquirer";
|
|
9
|
+
|
|
10
|
+
// src/utils/package-manager.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { unlink } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var LOCKFILES = [
|
|
15
|
+
{ pm: "pnpm", file: "pnpm-lock.yaml" },
|
|
16
|
+
{ pm: "yarn", file: "yarn.lock" },
|
|
17
|
+
{ pm: "bun", file: "bun.lockb" },
|
|
18
|
+
{ pm: "npm", file: "package-lock.json" }
|
|
19
|
+
];
|
|
20
|
+
function detectPackageManager(cwd) {
|
|
21
|
+
for (const entry of LOCKFILES) {
|
|
22
|
+
if (existsSync(join(cwd, entry.file))) {
|
|
23
|
+
return entry.pm;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
async function removeLockfiles(cwd) {
|
|
29
|
+
for (const entry of LOCKFILES) {
|
|
30
|
+
const target = join(cwd, entry.file);
|
|
31
|
+
if (existsSync(target)) {
|
|
32
|
+
await unlink(target);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/presets/index.ts
|
|
38
|
+
var presets = {
|
|
39
|
+
starter: {
|
|
40
|
+
frontend: { type: "nextjs", language: "ts" },
|
|
41
|
+
ui: { library: "tailwind" },
|
|
42
|
+
database: { provider: "postgres", orm: "drizzle" },
|
|
43
|
+
auth: { provider: "none" },
|
|
44
|
+
api: { type: "trpc" },
|
|
45
|
+
features: []
|
|
46
|
+
},
|
|
47
|
+
saas: {
|
|
48
|
+
frontend: { type: "nextjs", language: "ts" },
|
|
49
|
+
ui: { library: "tailwind" },
|
|
50
|
+
database: { provider: "postgres", orm: "prisma" },
|
|
51
|
+
auth: { provider: "nextauth" },
|
|
52
|
+
api: { type: "trpc" },
|
|
53
|
+
features: ["email", "payments"]
|
|
54
|
+
},
|
|
55
|
+
ecommerce: {
|
|
56
|
+
frontend: { type: "nextjs", language: "ts" },
|
|
57
|
+
ui: { library: "tailwind" },
|
|
58
|
+
database: { provider: "postgres", orm: "prisma" },
|
|
59
|
+
auth: { provider: "none" },
|
|
60
|
+
api: { type: "rest" },
|
|
61
|
+
features: ["payments", "storage"]
|
|
62
|
+
},
|
|
63
|
+
blog: {
|
|
64
|
+
frontend: { type: "nextjs", language: "ts" },
|
|
65
|
+
ui: { library: "tailwind" },
|
|
66
|
+
database: { provider: "sqlite", orm: "prisma" },
|
|
67
|
+
auth: { provider: "none" },
|
|
68
|
+
api: { type: "rest" },
|
|
69
|
+
features: ["storage"]
|
|
70
|
+
},
|
|
71
|
+
api: {
|
|
72
|
+
frontend: { type: "vite", language: "ts" },
|
|
73
|
+
ui: { library: "none" },
|
|
74
|
+
database: { provider: "postgres", orm: "drizzle" },
|
|
75
|
+
auth: { provider: "none" },
|
|
76
|
+
api: { type: "rest" },
|
|
77
|
+
features: []
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function getPreset(name) {
|
|
81
|
+
if (!name) return null;
|
|
82
|
+
return presets[name] ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/cli/config-builder.ts
|
|
86
|
+
function buildConfig(base) {
|
|
87
|
+
const preset = getPreset(base.preset);
|
|
88
|
+
const merged = preset ? {
|
|
89
|
+
...base,
|
|
90
|
+
...preset,
|
|
91
|
+
frontend: preset.frontend ?? base.frontend,
|
|
92
|
+
ui: preset.ui ?? base.ui,
|
|
93
|
+
database: preset.database ?? base.database,
|
|
94
|
+
auth: preset.auth ?? base.auth,
|
|
95
|
+
api: preset.api ?? base.api,
|
|
96
|
+
features: preset.features ?? base.features,
|
|
97
|
+
aiAgents: preset.aiAgents ?? base.aiAgents
|
|
98
|
+
} : base;
|
|
99
|
+
if (merged.ui.library === "shadcn") {
|
|
100
|
+
merged.ui = { library: "shadcn" };
|
|
101
|
+
}
|
|
102
|
+
return merged;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/cli/defaults.ts
|
|
106
|
+
function defaultConfig() {
|
|
107
|
+
return {
|
|
108
|
+
projectName: "my-app",
|
|
109
|
+
packageManager: "npm",
|
|
110
|
+
frontend: { type: "nextjs", language: "ts" },
|
|
111
|
+
ui: { library: "none" },
|
|
112
|
+
database: { provider: "none" },
|
|
113
|
+
auth: { provider: "none" },
|
|
114
|
+
api: { type: "none" },
|
|
115
|
+
features: [],
|
|
116
|
+
aiAgents: []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/utils/supported.ts
|
|
121
|
+
var supported = {
|
|
122
|
+
frontend: ["nextjs", "vite"],
|
|
123
|
+
ui: ["none", "tailwind", "shadcn", "mui", "chakra", "mantine", "antd", "nextui"],
|
|
124
|
+
database: ["none", "postgres", "mysql", "sqlite", "neon", "supabase"],
|
|
125
|
+
orm: ["drizzle", "prisma", "mongoose", "typeorm"],
|
|
126
|
+
auth: ["none", "nextauth", "clerk", "supabase"],
|
|
127
|
+
api: ["none", "rest", "trpc", "graphql"],
|
|
128
|
+
agents: ["claude", "copilot", "codex", "gemini", "cursor", "codeium", "windsurf", "tabnine"],
|
|
129
|
+
features: ["email", "storage", "payments", "analytics", "error-tracking"]
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/cli/prompts/index.ts
|
|
133
|
+
async function promptForConfig(input) {
|
|
134
|
+
if (input.skipPrompts) {
|
|
135
|
+
const base2 = defaultConfig();
|
|
136
|
+
const merged = {
|
|
137
|
+
...base2,
|
|
138
|
+
projectName: input.projectName ?? base2.projectName,
|
|
139
|
+
preset: input.preset
|
|
140
|
+
};
|
|
141
|
+
return buildConfig(merged);
|
|
142
|
+
}
|
|
143
|
+
const detected = detectPackageManager(process.cwd());
|
|
144
|
+
const answers = await inquirer.prompt([
|
|
145
|
+
{
|
|
146
|
+
type: "input",
|
|
147
|
+
name: "projectName",
|
|
148
|
+
message: "Project name",
|
|
149
|
+
default: input.projectName || "my-app"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "list",
|
|
153
|
+
name: "packageManager",
|
|
154
|
+
message: "Package manager",
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: "npm", value: "npm" },
|
|
157
|
+
{ name: "pnpm", value: "pnpm" },
|
|
158
|
+
{ name: "yarn", value: "yarn" },
|
|
159
|
+
{ name: "bun", value: "bun" }
|
|
160
|
+
],
|
|
161
|
+
default: detected || "npm"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: "list",
|
|
165
|
+
name: "frontend",
|
|
166
|
+
message: "Frontend framework",
|
|
167
|
+
choices: supported.frontend.map((v) => ({ name: v, value: v }))
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
type: "list",
|
|
171
|
+
name: "language",
|
|
172
|
+
message: "Language",
|
|
173
|
+
choices: [
|
|
174
|
+
{ name: "TypeScript", value: "ts" },
|
|
175
|
+
{ name: "JavaScript", value: "js" }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: "list",
|
|
180
|
+
name: "uiLibrary",
|
|
181
|
+
message: "UI library",
|
|
182
|
+
choices: supported.ui.map((v) => ({ name: v, value: v }))
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: "list",
|
|
186
|
+
name: "databaseProvider",
|
|
187
|
+
message: "Database provider",
|
|
188
|
+
choices: supported.database.map((v) => ({ name: v, value: v }))
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "list",
|
|
192
|
+
name: "orm",
|
|
193
|
+
message: "ORM",
|
|
194
|
+
when: (ans) => ans.databaseProvider !== "none",
|
|
195
|
+
choices: supported.orm.map((v) => ({ name: v, value: v }))
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: "list",
|
|
199
|
+
name: "authProvider",
|
|
200
|
+
message: "Authentication",
|
|
201
|
+
choices: supported.auth.map((v) => ({ name: v, value: v }))
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: "list",
|
|
205
|
+
name: "apiType",
|
|
206
|
+
message: "API type",
|
|
207
|
+
choices: supported.api.map((v) => ({ name: v, value: v }))
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: "checkbox",
|
|
211
|
+
name: "features",
|
|
212
|
+
message: "Additional features",
|
|
213
|
+
choices: supported.features.map((v) => ({ name: v, value: v }))
|
|
214
|
+
}
|
|
215
|
+
]);
|
|
216
|
+
const base = {
|
|
217
|
+
projectName: answers.projectName,
|
|
218
|
+
packageManager: answers.packageManager,
|
|
219
|
+
frontend: { type: answers.frontend, language: answers.language },
|
|
220
|
+
ui: { library: answers.uiLibrary },
|
|
221
|
+
database: { provider: answers.databaseProvider, orm: answers.orm },
|
|
222
|
+
auth: { provider: answers.authProvider },
|
|
223
|
+
api: { type: answers.apiType },
|
|
224
|
+
features: answers.features ?? [],
|
|
225
|
+
aiAgents: [],
|
|
226
|
+
preset: input.preset
|
|
227
|
+
};
|
|
228
|
+
return buildConfig(base);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/generators/core/project-creator.ts
|
|
232
|
+
import { join as join3 } from "path";
|
|
233
|
+
import { fileURLToPath } from "url";
|
|
234
|
+
|
|
235
|
+
// src/utils/file-system.ts
|
|
236
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
237
|
+
import { dirname } from "path";
|
|
238
|
+
async function ensureDir(path, ctx) {
|
|
239
|
+
if (ctx?.dryRun) {
|
|
240
|
+
ctx.log(`dry-run: mkdir ${path}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
await mkdir(path, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
async function writeTextFile(path, content, ctx) {
|
|
246
|
+
if (ctx?.dryRun) {
|
|
247
|
+
ctx.log(`dry-run: write ${path}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await ensureDir(dirname(path), ctx);
|
|
251
|
+
await writeFile(path, content, "utf8");
|
|
252
|
+
}
|
|
253
|
+
async function readTextFile(path) {
|
|
254
|
+
return readFile(path, "utf8");
|
|
255
|
+
}
|
|
256
|
+
async function removePath(path, ctx) {
|
|
257
|
+
if (ctx?.dryRun) {
|
|
258
|
+
ctx.log(`dry-run: remove ${path}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await rm(path, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/generators/scripts/scripts-registry.ts
|
|
265
|
+
function collectScripts(config) {
|
|
266
|
+
const scripts = {};
|
|
267
|
+
if (config.frontend.type === "nextjs") {
|
|
268
|
+
scripts["dev"] = "next dev";
|
|
269
|
+
scripts["build"] = "next build";
|
|
270
|
+
scripts["start"] = "next start";
|
|
271
|
+
scripts["lint"] = "next lint";
|
|
272
|
+
}
|
|
273
|
+
if (config.frontend.type === "vite") {
|
|
274
|
+
scripts["dev"] = "vite";
|
|
275
|
+
scripts["build"] = "vite build";
|
|
276
|
+
scripts["preview"] = "vite preview";
|
|
277
|
+
if (config.api.type === "rest" || config.api.type === "graphql" || config.api.type === "trpc") {
|
|
278
|
+
scripts["api:dev"] = config.frontend.language === "ts" ? "tsx src/server/index.ts" : "node src/server/index.js";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (config.ui.library === "tailwind") {
|
|
282
|
+
scripts["css:build"] = "npx tailwindcss -i ./src/styles.css -o ./dist/styles.css";
|
|
283
|
+
scripts["css:watch"] = "npx tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch";
|
|
284
|
+
}
|
|
285
|
+
if (config.database.orm === "drizzle") {
|
|
286
|
+
scripts["db:generate"] = "npx drizzle-kit generate";
|
|
287
|
+
scripts["db:migrate"] = "npx drizzle-kit migrate";
|
|
288
|
+
}
|
|
289
|
+
if (config.database.orm === "prisma") {
|
|
290
|
+
scripts["db:generate"] = "npx prisma generate";
|
|
291
|
+
scripts["db:migrate"] = "npx prisma migrate dev";
|
|
292
|
+
scripts["db:studio"] = "npx prisma studio";
|
|
293
|
+
}
|
|
294
|
+
if (config.ui.library === "shadcn") {
|
|
295
|
+
scripts["ui:add"] = "npx shadcn-ui@latest add";
|
|
296
|
+
}
|
|
297
|
+
if (config.database.orm === "typeorm") {
|
|
298
|
+
scripts["db:generate"] = "npx typeorm migration:generate";
|
|
299
|
+
scripts["db:migrate"] = "npx typeorm migration:run";
|
|
300
|
+
}
|
|
301
|
+
return scripts;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/utils/versions.ts
|
|
305
|
+
var versions = {
|
|
306
|
+
next: "^16.0.10",
|
|
307
|
+
react: "^19.0.0",
|
|
308
|
+
reactDom: "^19.0.0",
|
|
309
|
+
vite: "^6.0.0",
|
|
310
|
+
viteReactSwc: "^3.5.0",
|
|
311
|
+
typescript: "^5.5.4",
|
|
312
|
+
typesReact: "^19.0.0",
|
|
313
|
+
typesReactDom: "^19.0.0",
|
|
314
|
+
typesNode: "^20.14.10",
|
|
315
|
+
tsx: "^4.16.5",
|
|
316
|
+
tailwindcss: "^3.4.0",
|
|
317
|
+
postcss: "^8.4.0",
|
|
318
|
+
autoprefixer: "^10.4.0",
|
|
319
|
+
cva: "^0.7.0",
|
|
320
|
+
clsx: "^2.1.1",
|
|
321
|
+
tailwindMerge: "^2.4.0",
|
|
322
|
+
muiMaterial: "^5.16.0",
|
|
323
|
+
muiEmotionReact: "^11.13.0",
|
|
324
|
+
muiEmotionStyled: "^11.13.0",
|
|
325
|
+
chakraUi: "^2.8.2",
|
|
326
|
+
chakraEmotionReact: "^11.11.0",
|
|
327
|
+
chakraEmotionStyled: "^11.11.0",
|
|
328
|
+
chakraFramerMotion: "^11.11.0",
|
|
329
|
+
mantineCore: "^7.12.0",
|
|
330
|
+
mantineHooks: "^7.12.0",
|
|
331
|
+
mantineDates: "^7.12.0",
|
|
332
|
+
mantineNotifications: "^7.12.0",
|
|
333
|
+
antd: "^5.20.0",
|
|
334
|
+
nextui: "^2.4.6",
|
|
335
|
+
drizzleOrm: "^0.36.0",
|
|
336
|
+
drizzleKit: "^0.24.0",
|
|
337
|
+
prisma: "^5.17.0",
|
|
338
|
+
prismaClient: "^5.17.0",
|
|
339
|
+
mongoose: "^8.5.0",
|
|
340
|
+
typeorm: "^0.3.20",
|
|
341
|
+
reflectMetadata: "^0.2.2",
|
|
342
|
+
pg: "^8.12.0",
|
|
343
|
+
typesPg: "^8.11.10",
|
|
344
|
+
mysql2: "^3.10.0",
|
|
345
|
+
betterSqlite3: "^9.4.0",
|
|
346
|
+
neonServerless: "^0.9.5",
|
|
347
|
+
nextAuth: "^4.24.0",
|
|
348
|
+
clerkNext: "^5.0.0",
|
|
349
|
+
supabaseJs: "^2.45.0",
|
|
350
|
+
supabaseSsr: "^0.5.2",
|
|
351
|
+
trpcServer: "^10.45.0",
|
|
352
|
+
trpcClient: "^10.45.0",
|
|
353
|
+
trpcReactQuery: "^10.45.0",
|
|
354
|
+
tanstackQuery: "^4.18.0",
|
|
355
|
+
zod: "^3.23.0",
|
|
356
|
+
graphql: "^16.9.0",
|
|
357
|
+
graphqlRequest: "^6.1.0",
|
|
358
|
+
graphqlYoga: "^5.7.0",
|
|
359
|
+
resend: "^3.5.0",
|
|
360
|
+
cloudinary: "^2.0.0",
|
|
361
|
+
stripe: "^14.0.0",
|
|
362
|
+
posthog: "^1.165.0",
|
|
363
|
+
sentryNext: "^8.35.0"
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/generators/deps/deps-registry.ts
|
|
367
|
+
function collectDependencies(config) {
|
|
368
|
+
const dependencies = {};
|
|
369
|
+
const devDependencies = {};
|
|
370
|
+
if (config.frontend.type === "nextjs") {
|
|
371
|
+
dependencies["next"] = versions.next;
|
|
372
|
+
dependencies["react"] = versions.react;
|
|
373
|
+
dependencies["react-dom"] = versions.reactDom;
|
|
374
|
+
}
|
|
375
|
+
if (config.frontend.type === "vite") {
|
|
376
|
+
dependencies["react"] = versions.react;
|
|
377
|
+
dependencies["react-dom"] = versions.reactDom;
|
|
378
|
+
devDependencies["vite"] = versions.vite;
|
|
379
|
+
devDependencies["@vitejs/plugin-react-swc"] = versions.viteReactSwc;
|
|
380
|
+
if ((config.api.type === "rest" || config.api.type === "graphql") && config.frontend.language === "ts") {
|
|
381
|
+
devDependencies["tsx"] = versions.tsx;
|
|
382
|
+
devDependencies["@types/node"] = versions.typesNode;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (config.frontend.language === "ts") {
|
|
386
|
+
devDependencies["typescript"] = versions.typescript;
|
|
387
|
+
devDependencies["@types/react"] = versions.typesReact;
|
|
388
|
+
devDependencies["@types/react-dom"] = versions.typesReactDom;
|
|
389
|
+
}
|
|
390
|
+
if (config.ui.library === "tailwind" || config.ui.library === "shadcn") {
|
|
391
|
+
devDependencies["tailwindcss"] = versions.tailwindcss;
|
|
392
|
+
devDependencies["postcss"] = versions.postcss;
|
|
393
|
+
devDependencies["autoprefixer"] = versions.autoprefixer;
|
|
394
|
+
}
|
|
395
|
+
if (config.ui.library === "shadcn") {
|
|
396
|
+
dependencies["class-variance-authority"] = versions.cva;
|
|
397
|
+
dependencies["clsx"] = versions.clsx;
|
|
398
|
+
dependencies["tailwind-merge"] = versions.tailwindMerge;
|
|
399
|
+
}
|
|
400
|
+
if (config.ui.library === "mui") {
|
|
401
|
+
dependencies["@mui/material"] = versions.muiMaterial;
|
|
402
|
+
dependencies["@emotion/react"] = versions.muiEmotionReact;
|
|
403
|
+
dependencies["@emotion/styled"] = versions.muiEmotionStyled;
|
|
404
|
+
}
|
|
405
|
+
if (config.ui.library === "chakra") {
|
|
406
|
+
dependencies["@chakra-ui/react"] = versions.chakraUi;
|
|
407
|
+
dependencies["@emotion/react"] = versions.chakraEmotionReact;
|
|
408
|
+
dependencies["@emotion/styled"] = versions.chakraEmotionStyled;
|
|
409
|
+
dependencies["framer-motion"] = versions.chakraFramerMotion;
|
|
410
|
+
}
|
|
411
|
+
if (config.ui.library === "mantine") {
|
|
412
|
+
dependencies["@mantine/core"] = versions.mantineCore;
|
|
413
|
+
dependencies["@mantine/hooks"] = versions.mantineHooks;
|
|
414
|
+
dependencies["@mantine/dates"] = versions.mantineDates;
|
|
415
|
+
dependencies["@mantine/notifications"] = versions.mantineNotifications;
|
|
416
|
+
}
|
|
417
|
+
if (config.ui.library === "antd") {
|
|
418
|
+
dependencies["antd"] = versions.antd;
|
|
419
|
+
}
|
|
420
|
+
if (config.ui.library === "nextui") {
|
|
421
|
+
dependencies["@nextui-org/react"] = versions.nextui;
|
|
422
|
+
}
|
|
423
|
+
if (config.database.provider === "postgres" || config.database.provider === "neon" || config.database.provider === "supabase") {
|
|
424
|
+
dependencies["pg"] = versions.pg;
|
|
425
|
+
if (config.frontend.language === "ts") {
|
|
426
|
+
devDependencies["@types/pg"] = versions.typesPg;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (config.database.provider === "mysql") {
|
|
430
|
+
dependencies["mysql2"] = versions.mysql2;
|
|
431
|
+
}
|
|
432
|
+
if (config.database.provider === "sqlite") {
|
|
433
|
+
dependencies["better-sqlite3"] = versions.betterSqlite3;
|
|
434
|
+
}
|
|
435
|
+
if (config.database.provider === "neon") {
|
|
436
|
+
dependencies["@neondatabase/serverless"] = versions.neonServerless;
|
|
437
|
+
}
|
|
438
|
+
if (config.database.provider === "supabase") {
|
|
439
|
+
dependencies["@supabase/supabase-js"] = versions.supabaseJs;
|
|
440
|
+
}
|
|
441
|
+
if (config.database.orm === "drizzle") {
|
|
442
|
+
dependencies["drizzle-orm"] = versions.drizzleOrm;
|
|
443
|
+
devDependencies["drizzle-kit"] = versions.drizzleKit;
|
|
444
|
+
}
|
|
445
|
+
if (config.database.orm === "prisma") {
|
|
446
|
+
devDependencies["prisma"] = versions.prisma;
|
|
447
|
+
dependencies["@prisma/client"] = versions.prismaClient;
|
|
448
|
+
}
|
|
449
|
+
if (config.database.orm === "mongoose") {
|
|
450
|
+
dependencies["mongoose"] = versions.mongoose;
|
|
451
|
+
}
|
|
452
|
+
if (config.database.orm === "typeorm") {
|
|
453
|
+
dependencies["typeorm"] = versions.typeorm;
|
|
454
|
+
dependencies["reflect-metadata"] = versions.reflectMetadata;
|
|
455
|
+
}
|
|
456
|
+
if (config.auth.provider === "nextauth") {
|
|
457
|
+
dependencies["next-auth"] = versions.nextAuth;
|
|
458
|
+
}
|
|
459
|
+
if (config.auth.provider === "clerk") {
|
|
460
|
+
dependencies["@clerk/nextjs"] = versions.clerkNext;
|
|
461
|
+
}
|
|
462
|
+
if (config.auth.provider === "supabase") {
|
|
463
|
+
dependencies["@supabase/supabase-js"] = versions.supabaseJs;
|
|
464
|
+
if (config.frontend.type === "nextjs") {
|
|
465
|
+
dependencies["@supabase/ssr"] = versions.supabaseSsr;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (config.api.type === "trpc") {
|
|
469
|
+
dependencies["@trpc/server"] = versions.trpcServer;
|
|
470
|
+
dependencies["@trpc/client"] = versions.trpcClient;
|
|
471
|
+
dependencies["@trpc/react-query"] = versions.trpcReactQuery;
|
|
472
|
+
dependencies["@tanstack/react-query"] = versions.tanstackQuery;
|
|
473
|
+
dependencies["zod"] = versions.zod;
|
|
474
|
+
if (config.frontend.type === "vite") {
|
|
475
|
+
devDependencies["tsx"] = versions.tsx;
|
|
476
|
+
devDependencies["@types/node"] = versions.typesNode;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (config.api.type === "graphql") {
|
|
480
|
+
dependencies["graphql"] = versions.graphql;
|
|
481
|
+
dependencies["graphql-request"] = versions.graphqlRequest;
|
|
482
|
+
dependencies["graphql-yoga"] = versions.graphqlYoga;
|
|
483
|
+
}
|
|
484
|
+
if (config.features.includes("email")) {
|
|
485
|
+
dependencies["resend"] = versions.resend;
|
|
486
|
+
}
|
|
487
|
+
if (config.features.includes("storage")) {
|
|
488
|
+
dependencies["cloudinary"] = versions.cloudinary;
|
|
489
|
+
}
|
|
490
|
+
if (config.features.includes("payments")) {
|
|
491
|
+
dependencies["stripe"] = versions.stripe;
|
|
492
|
+
}
|
|
493
|
+
if (config.features.includes("analytics")) {
|
|
494
|
+
dependencies["posthog-js"] = versions.posthog;
|
|
495
|
+
}
|
|
496
|
+
if (config.features.includes("error-tracking") && config.frontend.type === "nextjs") {
|
|
497
|
+
dependencies["@sentry/nextjs"] = versions.sentryNext;
|
|
498
|
+
}
|
|
499
|
+
return { dependencies, devDependencies };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/generators/core/readme.ts
|
|
503
|
+
function formatDb(config) {
|
|
504
|
+
if (config.database.provider === "none") return "none";
|
|
505
|
+
return `${config.database.provider}${config.database.orm ? ` (${config.database.orm})` : ""}`;
|
|
506
|
+
}
|
|
507
|
+
function getDevCommand(config) {
|
|
508
|
+
if (config.frontend.type === "vite" && (config.api.type === "rest" || config.api.type === "graphql")) {
|
|
509
|
+
return "npm run dev (frontend) + npm run api:dev (API)";
|
|
510
|
+
}
|
|
511
|
+
return "npm run dev";
|
|
512
|
+
}
|
|
513
|
+
function formatFeatures(config) {
|
|
514
|
+
if (!config.features || config.features.length === 0) return "none";
|
|
515
|
+
return config.features.join(", ");
|
|
516
|
+
}
|
|
517
|
+
function buildProjectReadme(config) {
|
|
518
|
+
const featureLinks = config.features.map((f) => `- features/${f}/README.md`).join("\n");
|
|
519
|
+
return `# ${config.projectName}
|
|
520
|
+
|
|
521
|
+
Generated by StackForge.
|
|
522
|
+
|
|
523
|
+
## Stack
|
|
524
|
+
- Frontend: ${config.frontend.type} (${config.frontend.language})
|
|
525
|
+
- UI: ${config.ui.library}
|
|
526
|
+
- Database: ${formatDb(config)}
|
|
527
|
+
- Auth: ${config.auth.provider}
|
|
528
|
+
- API: ${config.api.type}
|
|
529
|
+
- Features: ${formatFeatures(config)}
|
|
530
|
+
|
|
531
|
+
## Commands
|
|
532
|
+
- Install: ${config.packageManager} install
|
|
533
|
+
- Dev: ${getDevCommand(config)}
|
|
534
|
+
|
|
535
|
+
${config.features.length ? `## Feature Docs
|
|
536
|
+
${featureLinks}
|
|
537
|
+
` : ""}
|
|
538
|
+
## Notes
|
|
539
|
+
- Update .env values before running in production.
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/utils/project-config.ts
|
|
544
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
545
|
+
import { join as join2 } from "path";
|
|
546
|
+
|
|
547
|
+
// src/utils/schema.ts
|
|
548
|
+
var STACKFORGE_SCHEMA_VERSION = 1;
|
|
549
|
+
|
|
550
|
+
// src/utils/migrations.ts
|
|
551
|
+
function migrateConfig(input) {
|
|
552
|
+
let current = input._schemaVersion ?? 0;
|
|
553
|
+
let config = { ...input };
|
|
554
|
+
if (current < 1) {
|
|
555
|
+
config = { ...config, _schemaVersion: 1 };
|
|
556
|
+
current = 1;
|
|
557
|
+
}
|
|
558
|
+
if (current !== STACKFORGE_SCHEMA_VERSION) {
|
|
559
|
+
config._schemaVersion = STACKFORGE_SCHEMA_VERSION;
|
|
560
|
+
}
|
|
561
|
+
return config;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/utils/project-config.ts
|
|
565
|
+
async function writeProjectConfig(root, config) {
|
|
566
|
+
const path = join2(root, "stackforge.json");
|
|
567
|
+
const payload = { ...config, _schemaVersion: STACKFORGE_SCHEMA_VERSION };
|
|
568
|
+
await writeFile2(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
569
|
+
}
|
|
570
|
+
async function readProjectConfig(root) {
|
|
571
|
+
const path = join2(root, "stackforge.json");
|
|
572
|
+
const raw = await readFile2(path, "utf8");
|
|
573
|
+
const parsed = JSON.parse(raw);
|
|
574
|
+
const migrated = migrateConfig(parsed);
|
|
575
|
+
if (migrated._schemaVersion !== parsed._schemaVersion) {
|
|
576
|
+
await writeFile2(path, JSON.stringify(migrated, null, 2) + "\n", "utf8");
|
|
577
|
+
}
|
|
578
|
+
return migrated;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/generators/core/project-creator.ts
|
|
582
|
+
async function createProjectSkeleton(root, config, ctx) {
|
|
583
|
+
const projectRoot = join3(root, config.projectName);
|
|
584
|
+
const templatesRoot = fileURLToPath(new URL("../../../templates", import.meta.url));
|
|
585
|
+
await ensureDir(projectRoot, ctx);
|
|
586
|
+
const readme = buildProjectReadme(config);
|
|
587
|
+
await writeTextFile(join3(projectRoot, "README.md"), readme + "\n", ctx);
|
|
588
|
+
const envExample = `# Environment Variables
|
|
589
|
+
`;
|
|
590
|
+
await writeTextFile(join3(projectRoot, ".env.example"), envExample, ctx);
|
|
591
|
+
const gitignore = await readTextFile(join3(templatesRoot, "shared", ".gitignore"));
|
|
592
|
+
await writeTextFile(join3(projectRoot, ".gitignore"), gitignore, ctx);
|
|
593
|
+
const editorconfig = await readTextFile(join3(templatesRoot, "shared", ".editorconfig"));
|
|
594
|
+
await writeTextFile(join3(projectRoot, ".editorconfig"), editorconfig, ctx);
|
|
595
|
+
const pkg = {
|
|
596
|
+
name: config.projectName,
|
|
597
|
+
version: "0.0.0",
|
|
598
|
+
private: true
|
|
599
|
+
};
|
|
600
|
+
const featureScripts = collectScripts(config);
|
|
601
|
+
const featureDeps = collectDependencies(config);
|
|
602
|
+
if (Object.keys(featureScripts).length > 0) {
|
|
603
|
+
pkg.scripts = featureScripts;
|
|
604
|
+
}
|
|
605
|
+
pkg.dependencies = featureDeps.dependencies;
|
|
606
|
+
pkg.devDependencies = featureDeps.devDependencies;
|
|
607
|
+
await writeTextFile(join3(projectRoot, "package.json"), JSON.stringify(pkg, null, 2) + "\n", ctx);
|
|
608
|
+
if (!ctx?.dryRun) {
|
|
609
|
+
await writeProjectConfig(projectRoot, config);
|
|
610
|
+
}
|
|
611
|
+
if (config.frontend.language === "ts") {
|
|
612
|
+
const tsconfig = {
|
|
613
|
+
compilerOptions: {
|
|
614
|
+
target: "ES2022",
|
|
615
|
+
module: "ESNext",
|
|
616
|
+
moduleResolution: "Bundler",
|
|
617
|
+
strict: true,
|
|
618
|
+
jsx: "preserve",
|
|
619
|
+
baseUrl: ".",
|
|
620
|
+
paths: {
|
|
621
|
+
"@/*": ["src/*"]
|
|
622
|
+
},
|
|
623
|
+
esModuleInterop: true,
|
|
624
|
+
resolveJsonModule: true,
|
|
625
|
+
incremental: true,
|
|
626
|
+
noEmit: true,
|
|
627
|
+
forceConsistentCasingInFileNames: true,
|
|
628
|
+
skipLibCheck: true
|
|
629
|
+
},
|
|
630
|
+
include: ["src", "app"]
|
|
631
|
+
};
|
|
632
|
+
await writeTextFile(join3(projectRoot, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n", ctx);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/generators/database/database-files.ts
|
|
637
|
+
import { join as join4 } from "path";
|
|
638
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
639
|
+
|
|
640
|
+
// src/utils/env-file.ts
|
|
641
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
642
|
+
import { dirname as dirname2 } from "path";
|
|
643
|
+
async function appendEnvLine(path, line, ctx) {
|
|
644
|
+
if (ctx?.dryRun) {
|
|
645
|
+
ctx.log(`dry-run: append ${path} -> ${line}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
await ensureDir(dirname2(path), ctx);
|
|
649
|
+
let existing = "";
|
|
650
|
+
try {
|
|
651
|
+
existing = await readFile3(path, "utf8");
|
|
652
|
+
} catch {
|
|
653
|
+
existing = "";
|
|
654
|
+
}
|
|
655
|
+
const normalized = existing.endsWith("\n") || existing.length === 0 ? existing : existing + "\n";
|
|
656
|
+
const content = normalized + line + (line.endsWith("\n") ? "" : "\n");
|
|
657
|
+
await writeFile3(path, content, "utf8");
|
|
658
|
+
}
|
|
659
|
+
async function removeEnvKey(path, key, ctx) {
|
|
660
|
+
if (ctx?.dryRun) {
|
|
661
|
+
ctx.log(`dry-run: remove env ${key} from ${path}`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
let existing = "";
|
|
665
|
+
try {
|
|
666
|
+
existing = await readFile3(path, "utf8");
|
|
667
|
+
} catch {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const lines = existing.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
671
|
+
const filtered = lines.filter((line) => !line.startsWith(`${key}=`) && !line.startsWith(`${key}="`));
|
|
672
|
+
await writeFile3(path, filtered.join("\n") + (filtered.length ? "\n" : ""), "utf8");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/generators/database/database-files.ts
|
|
676
|
+
async function generateDatabaseFiles(root, config, ctx) {
|
|
677
|
+
const projectRoot = join4(root, config.projectName);
|
|
678
|
+
const templatesRoot = fileURLToPath2(new URL("../../../templates", import.meta.url));
|
|
679
|
+
if (config.database.provider !== "none") {
|
|
680
|
+
const envPath = join4(projectRoot, ".env.example");
|
|
681
|
+
await appendEnvLine(envPath, 'DATABASE_URL=""', ctx);
|
|
682
|
+
if (config.database.provider === "neon") {
|
|
683
|
+
await appendEnvLine(envPath, 'NEON_API_KEY=""', ctx);
|
|
684
|
+
await appendEnvLine(envPath, 'NEON_PROJECT_ID=""', ctx);
|
|
685
|
+
const providerDir = join4(projectRoot, "database");
|
|
686
|
+
await ensureDir(providerDir, ctx);
|
|
687
|
+
const providerReadme = await readTextFile(join4(templatesRoot, "database", "providers", "neon.README.md"));
|
|
688
|
+
await writeTextFile(join4(providerDir, "README.md"), providerReadme, ctx);
|
|
689
|
+
}
|
|
690
|
+
if (config.database.provider === "supabase") {
|
|
691
|
+
await appendEnvLine(envPath, 'SUPABASE_URL=""', ctx);
|
|
692
|
+
await appendEnvLine(envPath, 'SUPABASE_ANON_KEY=""', ctx);
|
|
693
|
+
const providerDir = join4(projectRoot, "database");
|
|
694
|
+
await ensureDir(providerDir, ctx);
|
|
695
|
+
const providerReadme = await readTextFile(join4(templatesRoot, "database", "providers", "supabase.README.md"));
|
|
696
|
+
await writeTextFile(join4(providerDir, "README.md"), providerReadme, ctx);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (config.database.orm === "drizzle") {
|
|
700
|
+
const drizzleDir = join4(projectRoot, "drizzle");
|
|
701
|
+
await ensureDir(drizzleDir, ctx);
|
|
702
|
+
const configContent = await readTextFile(join4(templatesRoot, "database", "drizzle", "drizzle.config.ts"));
|
|
703
|
+
const schemaContent = await readTextFile(join4(templatesRoot, "database", "drizzle", "schema.ts"));
|
|
704
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
705
|
+
const clientContent = await readTextFile(join4(templatesRoot, "database", "drizzle", `client.${ext}`));
|
|
706
|
+
const exampleContent = await readTextFile(join4(templatesRoot, "database", "drizzle", `example.${ext}`));
|
|
707
|
+
await writeTextFile(join4(projectRoot, "drizzle.config.ts"), configContent, ctx);
|
|
708
|
+
await writeTextFile(join4(drizzleDir, "schema.ts"), schemaContent, ctx);
|
|
709
|
+
await writeTextFile(join4(drizzleDir, `client.${ext}`), clientContent, ctx);
|
|
710
|
+
await writeTextFile(join4(drizzleDir, `example.${ext}`), exampleContent, ctx);
|
|
711
|
+
}
|
|
712
|
+
if (config.database.orm === "prisma") {
|
|
713
|
+
const prismaDir = join4(projectRoot, "prisma");
|
|
714
|
+
await ensureDir(prismaDir, ctx);
|
|
715
|
+
const schema = await readTextFile(join4(templatesRoot, "database", "prisma", "schema.prisma"));
|
|
716
|
+
await writeTextFile(join4(prismaDir, "schema.prisma"), schema, ctx);
|
|
717
|
+
const dbDir = join4(projectRoot, "src", "db");
|
|
718
|
+
await ensureDir(dbDir, ctx);
|
|
719
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
720
|
+
const client = await readTextFile(join4(templatesRoot, "database", "prisma", `client.${ext}`));
|
|
721
|
+
const example = await readTextFile(join4(templatesRoot, "database", "prisma", `example.${ext}`));
|
|
722
|
+
await writeTextFile(join4(dbDir, `prisma.${ext}`), client, ctx);
|
|
723
|
+
await writeTextFile(join4(dbDir, `prisma-example.${ext}`), example, ctx);
|
|
724
|
+
const usage = await readTextFile(join4(templatesRoot, "database", "usage", `prisma-users.${ext}`));
|
|
725
|
+
await writeTextFile(join4(dbDir, `users.${ext}`), usage, ctx);
|
|
726
|
+
}
|
|
727
|
+
if (config.database.orm === "mongoose") {
|
|
728
|
+
const dbDir = join4(projectRoot, "src", "db");
|
|
729
|
+
await ensureDir(dbDir, ctx);
|
|
730
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
731
|
+
const connection = await readTextFile(join4(templatesRoot, "database", "mongoose", `connection.${ext}`));
|
|
732
|
+
const model = await readTextFile(join4(templatesRoot, "database", "mongoose", `model.${ext}`));
|
|
733
|
+
await writeTextFile(join4(dbDir, `mongoose.${ext}`), connection, ctx);
|
|
734
|
+
await writeTextFile(join4(dbDir, `mongoose-model.${ext}`), model, ctx);
|
|
735
|
+
const usage = await readTextFile(join4(templatesRoot, "database", "usage", `mongoose-users.${ext}`));
|
|
736
|
+
await writeTextFile(join4(dbDir, `users.${ext}`), usage, ctx);
|
|
737
|
+
}
|
|
738
|
+
if (config.database.orm === "typeorm") {
|
|
739
|
+
const dbDir = join4(projectRoot, "src", "db");
|
|
740
|
+
await ensureDir(dbDir, ctx);
|
|
741
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
742
|
+
const template = await readTextFile(join4(templatesRoot, "database", "typeorm", `data-source.${ext}`));
|
|
743
|
+
const typeormType = config.database.provider === "mysql" ? "mysql" : config.database.provider === "sqlite" ? "sqlite" : "postgres";
|
|
744
|
+
const migrationsPath = config.frontend.language === "ts" ? "migrations/*.ts" : "migrations/*.js";
|
|
745
|
+
const content = template.replace("{{typeormType}}", typeormType).replace("{{migrationsPath}}", migrationsPath);
|
|
746
|
+
await writeTextFile(join4(dbDir, `data-source.${ext}`), content, ctx);
|
|
747
|
+
const entitiesDir = join4(dbDir, "entities");
|
|
748
|
+
await ensureDir(entitiesDir, ctx);
|
|
749
|
+
const entity = await readTextFile(join4(templatesRoot, "database", "typeorm", `entity.${ext}`));
|
|
750
|
+
await writeTextFile(join4(entitiesDir, `User.${ext}`), entity, ctx);
|
|
751
|
+
const migrationsDir = join4(dbDir, "migrations");
|
|
752
|
+
await ensureDir(migrationsDir, ctx);
|
|
753
|
+
const migrationReadme = await readTextFile(join4(templatesRoot, "database", "typeorm", "migrations", "README.md"));
|
|
754
|
+
await writeTextFile(join4(migrationsDir, "README.md"), migrationReadme, ctx);
|
|
755
|
+
const usage = await readTextFile(join4(templatesRoot, "database", "usage", `typeorm-users.${ext}`));
|
|
756
|
+
await writeTextFile(join4(dbDir, `users.${ext}`), usage, ctx);
|
|
757
|
+
}
|
|
758
|
+
if (config.database.orm === "drizzle") {
|
|
759
|
+
const dbDir = join4(projectRoot, "src", "db");
|
|
760
|
+
await ensureDir(dbDir, ctx);
|
|
761
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
762
|
+
const usage = await readTextFile(join4(templatesRoot, "database", "usage", `drizzle-users.${ext}`));
|
|
763
|
+
await writeTextFile(join4(dbDir, `users.${ext}`), usage, ctx);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/generators/frontend/frontend-files.ts
|
|
768
|
+
import { join as join5 } from "path";
|
|
769
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
770
|
+
|
|
771
|
+
// src/generators/templates/template-engine.ts
|
|
772
|
+
function applyTemplate(content, vars) {
|
|
773
|
+
let out = content;
|
|
774
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
775
|
+
out = out.replaceAll(`{{${key}}}`, value);
|
|
776
|
+
}
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/generators/frontend/frontend-files.ts
|
|
781
|
+
async function generateFrontendFiles(root, config, ctx) {
|
|
782
|
+
const projectRoot = join5(root, config.projectName);
|
|
783
|
+
const templatesRoot = fileURLToPath3(new URL("../../../templates", import.meta.url));
|
|
784
|
+
if (config.frontend.type === "nextjs") {
|
|
785
|
+
const appDir = join5(projectRoot, "app");
|
|
786
|
+
await ensureDir(appDir, ctx);
|
|
787
|
+
const uiCssImports = buildUiCssImports(config, "nextjs");
|
|
788
|
+
const importCss = uiCssImports.length ? uiCssImports.join("") + "\n" : "";
|
|
789
|
+
const hasTrpc = config.api.type === "trpc";
|
|
790
|
+
const hasUiProvider = requiresUiProvider(config);
|
|
791
|
+
const hasProviders = hasTrpc || hasUiProvider;
|
|
792
|
+
const providersImport = hasProviders ? "import { Providers } from './providers';\n" : "";
|
|
793
|
+
const wrapChildren = hasProviders ? "<Providers>{children}</Providers>" : "{children}";
|
|
794
|
+
const layoutTemplatePath = join5(
|
|
795
|
+
templatesRoot,
|
|
796
|
+
"nextjs",
|
|
797
|
+
"app",
|
|
798
|
+
config.frontend.language === "ts" ? "layout.tsx" : "layout.jsx"
|
|
799
|
+
);
|
|
800
|
+
const pageTemplatePath = join5(
|
|
801
|
+
templatesRoot,
|
|
802
|
+
"nextjs",
|
|
803
|
+
"app",
|
|
804
|
+
config.frontend.language === "ts" ? "page.tsx" : "page.jsx"
|
|
805
|
+
);
|
|
806
|
+
const actionsTemplatePath = join5(
|
|
807
|
+
templatesRoot,
|
|
808
|
+
"nextjs",
|
|
809
|
+
"app",
|
|
810
|
+
config.frontend.language === "ts" ? "actions.ts" : "actions.js"
|
|
811
|
+
);
|
|
812
|
+
const nextConfigTemplatePath = join5(
|
|
813
|
+
templatesRoot,
|
|
814
|
+
"nextjs",
|
|
815
|
+
config.frontend.language === "ts" ? "next.config.ts" : "next.config.js"
|
|
816
|
+
);
|
|
817
|
+
const layoutTemplate = await readTextFile(layoutTemplatePath);
|
|
818
|
+
const pageTemplate = await readTextFile(pageTemplatePath);
|
|
819
|
+
const examplesTemplate = await readTextFile(
|
|
820
|
+
join5(
|
|
821
|
+
templatesRoot,
|
|
822
|
+
"nextjs",
|
|
823
|
+
"app",
|
|
824
|
+
config.frontend.language === "ts" ? "examples-page.tsx" : "examples-page.jsx"
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
const nextConfigTemplate = await readTextFile(nextConfigTemplatePath);
|
|
828
|
+
const actionsTemplate = await readTextFile(actionsTemplatePath);
|
|
829
|
+
const layout = applyTemplate(layoutTemplate, {
|
|
830
|
+
importCss,
|
|
831
|
+
providersImport,
|
|
832
|
+
wrapChildren
|
|
833
|
+
});
|
|
834
|
+
const links = buildPageLinks(config);
|
|
835
|
+
const page = applyTemplate(pageTemplate, { projectName: config.projectName, links });
|
|
836
|
+
await writeTextFile(
|
|
837
|
+
join5(projectRoot, config.frontend.language === "ts" ? "next.config.ts" : "next.config.js"),
|
|
838
|
+
nextConfigTemplate,
|
|
839
|
+
ctx
|
|
840
|
+
);
|
|
841
|
+
await writeTextFile(join5(appDir, config.frontend.language === "ts" ? "layout.tsx" : "layout.jsx"), layout, ctx);
|
|
842
|
+
await writeTextFile(join5(appDir, config.frontend.language === "ts" ? "page.tsx" : "page.jsx"), page, ctx);
|
|
843
|
+
await writeTextFile(join5(appDir, config.frontend.language === "ts" ? "actions.ts" : "actions.js"), actionsTemplate, ctx);
|
|
844
|
+
if (config.api.type !== "none") {
|
|
845
|
+
const examplesDir = join5(appDir, "examples");
|
|
846
|
+
await ensureDir(examplesDir, ctx);
|
|
847
|
+
const { imports, components } = buildApiExamples(config, "nextjs");
|
|
848
|
+
const featureNotes = buildFeatureNotes(config);
|
|
849
|
+
const { uiDemoImport, uiDemoComponent } = buildUiDemoTokens(config, "nextjs");
|
|
850
|
+
const examplesPage = applyTemplate(examplesTemplate, {
|
|
851
|
+
imports,
|
|
852
|
+
components,
|
|
853
|
+
featureNotes,
|
|
854
|
+
uiDemoImport,
|
|
855
|
+
uiDemoComponent
|
|
856
|
+
});
|
|
857
|
+
await writeTextFile(
|
|
858
|
+
join5(examplesDir, config.frontend.language === "ts" ? "page.tsx" : "page.jsx"),
|
|
859
|
+
examplesPage,
|
|
860
|
+
ctx
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (hasProviders) {
|
|
864
|
+
const providers = buildProvidersComponent(config, "nextjs");
|
|
865
|
+
await writeTextFile(
|
|
866
|
+
join5(appDir, config.frontend.language === "ts" ? "providers.tsx" : "providers.jsx"),
|
|
867
|
+
providers,
|
|
868
|
+
ctx
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (config.frontend.type === "vite") {
|
|
873
|
+
const srcDir = join5(projectRoot, "src");
|
|
874
|
+
await ensureDir(srcDir, ctx);
|
|
875
|
+
const uiCssImports = buildUiCssImports(config, "vite");
|
|
876
|
+
const cssImport = uiCssImports.length ? uiCssImports.join("") : "";
|
|
877
|
+
const ext = config.frontend.language === "ts" ? "tsx" : "jsx";
|
|
878
|
+
const mainTemplatePath = join5(templatesRoot, "vite", config.frontend.language === "ts" ? "main.tsx" : "main.jsx");
|
|
879
|
+
const appTemplatePath = join5(templatesRoot, "vite", config.frontend.language === "ts" ? "App.tsx" : "App.jsx");
|
|
880
|
+
const indexTemplatePath = join5(templatesRoot, "vite", "index.html");
|
|
881
|
+
const viteConfigTemplatePath = join5(templatesRoot, "vite", "vite.config.ts");
|
|
882
|
+
const viteEnvTemplatePath = join5(templatesRoot, "vite", "vite-env.d.ts");
|
|
883
|
+
const mainTemplate = await readTextFile(mainTemplatePath);
|
|
884
|
+
const appTemplate = await readTextFile(appTemplatePath);
|
|
885
|
+
const indexTemplate = await readTextFile(indexTemplatePath);
|
|
886
|
+
const viteConfigTemplate = await readTextFile(viteConfigTemplatePath);
|
|
887
|
+
const viteEnvTemplate = await readTextFile(viteEnvTemplatePath);
|
|
888
|
+
const hasTrpc = config.api.type === "trpc";
|
|
889
|
+
const hasUiProvider = requiresUiProvider(config);
|
|
890
|
+
const hasProviders = hasTrpc || hasUiProvider;
|
|
891
|
+
const providersImport = hasProviders ? "import { Providers } from './providers';\n" : "";
|
|
892
|
+
const wrapApp = hasProviders ? "<Providers><App /></Providers>" : "<App />";
|
|
893
|
+
const { initImports, initCalls } = buildFeatureInit(config, "vite");
|
|
894
|
+
const main = applyTemplate(mainTemplate, {
|
|
895
|
+
importCss: cssImport,
|
|
896
|
+
providersImport,
|
|
897
|
+
wrapApp,
|
|
898
|
+
initImports,
|
|
899
|
+
initCalls
|
|
900
|
+
});
|
|
901
|
+
const { imports, components } = buildApiExamples(config, "vite");
|
|
902
|
+
const featureNotes = buildFeatureNotes(config);
|
|
903
|
+
const { uiDemoImport, uiDemoComponent } = buildUiDemoTokens(config, "vite");
|
|
904
|
+
const apiImports = imports ? imports + "\n" : "";
|
|
905
|
+
const apiExamples = components || "";
|
|
906
|
+
const app = applyTemplate(appTemplate, {
|
|
907
|
+
projectName: config.projectName,
|
|
908
|
+
apiImports,
|
|
909
|
+
apiExamples,
|
|
910
|
+
featureNotes,
|
|
911
|
+
uiDemoImport,
|
|
912
|
+
uiDemoComponent
|
|
913
|
+
});
|
|
914
|
+
const indexHtml = applyTemplate(indexTemplate, { projectName: config.projectName, ext });
|
|
915
|
+
await writeTextFile(join5(projectRoot, "index.html"), indexHtml, ctx);
|
|
916
|
+
await writeTextFile(join5(projectRoot, "vite.config.ts"), viteConfigTemplate, ctx);
|
|
917
|
+
await writeTextFile(join5(srcDir, config.frontend.language === "ts" ? "main.tsx" : "main.jsx"), main, ctx);
|
|
918
|
+
await writeTextFile(join5(srcDir, config.frontend.language === "ts" ? "App.tsx" : "App.jsx"), app, ctx);
|
|
919
|
+
if (config.frontend.language === "ts") {
|
|
920
|
+
await writeTextFile(join5(srcDir, "vite-env.d.ts"), viteEnvTemplate, ctx);
|
|
921
|
+
}
|
|
922
|
+
await appendEnvLine(join5(projectRoot, ".env.example"), 'VITE_API_URL="http://localhost:3001"', ctx);
|
|
923
|
+
if (hasProviders) {
|
|
924
|
+
const providers = buildProvidersComponent(config, "vite");
|
|
925
|
+
await writeTextFile(join5(srcDir, config.frontend.language === "ts" ? "providers.tsx" : "providers.jsx"), providers, ctx);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function buildUiCssImports(config, target) {
|
|
930
|
+
const imports = [];
|
|
931
|
+
const tailwindPath = target === "nextjs" ? "../src/styles.css" : "./styles.css";
|
|
932
|
+
if (config.ui.library === "tailwind") {
|
|
933
|
+
imports.push(`import '${tailwindPath}';
|
|
934
|
+
`);
|
|
935
|
+
}
|
|
936
|
+
if (config.ui.library === "mantine") {
|
|
937
|
+
imports.push("import '@mantine/core/styles.css';\n");
|
|
938
|
+
imports.push("import '@mantine/dates/styles.css';\n");
|
|
939
|
+
imports.push("import '@mantine/notifications/styles.css';\n");
|
|
940
|
+
}
|
|
941
|
+
if (config.ui.library === "antd") {
|
|
942
|
+
imports.push("import 'antd/dist/reset.css';\n");
|
|
943
|
+
}
|
|
944
|
+
return imports;
|
|
945
|
+
}
|
|
946
|
+
function requiresUiProvider(config) {
|
|
947
|
+
return ["mui", "chakra", "mantine", "antd", "nextui"].includes(config.ui.library);
|
|
948
|
+
}
|
|
949
|
+
function buildProvidersComponent(config, target) {
|
|
950
|
+
const hasTrpc = config.api.type === "trpc";
|
|
951
|
+
const ui = config.ui.library;
|
|
952
|
+
const needsTheme = ["mui", "chakra", "mantine", "antd", "nextui"].includes(ui);
|
|
953
|
+
const isTypescript = config.frontend.language === "ts";
|
|
954
|
+
const hasFeatureInit = config.features.includes("analytics") || config.features.includes("error-tracking");
|
|
955
|
+
const themeImportPath = target === "nextjs" ? "../src/theme" : "./theme";
|
|
956
|
+
const imports = [];
|
|
957
|
+
const lines = [];
|
|
958
|
+
if (target === "nextjs") {
|
|
959
|
+
lines.push('"use client";\n');
|
|
960
|
+
}
|
|
961
|
+
if (hasTrpc && isTypescript) {
|
|
962
|
+
imports.push("import type React from 'react';");
|
|
963
|
+
imports.push("import { useState, useEffect } from 'react';");
|
|
964
|
+
} else if (hasTrpc) {
|
|
965
|
+
imports.push("import { useState, useEffect } from 'react';");
|
|
966
|
+
} else if (isTypescript) {
|
|
967
|
+
imports.push("import type React from 'react';");
|
|
968
|
+
if (hasFeatureInit) {
|
|
969
|
+
imports.push("import { useEffect } from 'react';");
|
|
970
|
+
}
|
|
971
|
+
} else if (hasFeatureInit) {
|
|
972
|
+
imports.push("import { useEffect } from 'react';");
|
|
973
|
+
}
|
|
974
|
+
if (hasTrpc) {
|
|
975
|
+
imports.push("import { QueryClient, QueryClientProvider } from '@tanstack/react-query';");
|
|
976
|
+
}
|
|
977
|
+
if (ui === "mui") {
|
|
978
|
+
imports.push("import { ThemeProvider, CssBaseline } from '@mui/material';");
|
|
979
|
+
}
|
|
980
|
+
if (ui === "chakra") {
|
|
981
|
+
imports.push("import { ChakraProvider } from '@chakra-ui/react';");
|
|
982
|
+
}
|
|
983
|
+
if (ui === "mantine") {
|
|
984
|
+
imports.push("import { MantineProvider } from '@mantine/core';");
|
|
985
|
+
}
|
|
986
|
+
if (ui === "antd") {
|
|
987
|
+
imports.push("import { ConfigProvider } from 'antd';");
|
|
988
|
+
}
|
|
989
|
+
if (ui === "nextui") {
|
|
990
|
+
imports.push("import { NextUIProvider } from '@nextui-org/react';");
|
|
991
|
+
}
|
|
992
|
+
if (needsTheme) {
|
|
993
|
+
imports.push(`import { theme } from '${themeImportPath}';`);
|
|
994
|
+
}
|
|
995
|
+
if (config.features.includes("analytics")) {
|
|
996
|
+
const analyticsPath = target === "nextjs" ? "../src/lib/posthog" : "./lib/posthog";
|
|
997
|
+
imports.push(`import { initPosthog } from '${analyticsPath}';`);
|
|
998
|
+
}
|
|
999
|
+
if (config.features.includes("error-tracking")) {
|
|
1000
|
+
const sentryPath = target === "nextjs" ? "../src/lib/sentry" : "./lib/sentry";
|
|
1001
|
+
imports.push(`import { initSentry } from '${sentryPath}';`);
|
|
1002
|
+
}
|
|
1003
|
+
lines.push(imports.join("\n") + "\n");
|
|
1004
|
+
const propsType = isTypescript ? ": { children: React.ReactNode }" : "";
|
|
1005
|
+
lines.push(`export function Providers({ children }${propsType}) {
|
|
1006
|
+
`);
|
|
1007
|
+
if (hasTrpc) {
|
|
1008
|
+
lines.push(" const [client] = useState(() => new QueryClient());\n\n");
|
|
1009
|
+
}
|
|
1010
|
+
if (hasFeatureInit) {
|
|
1011
|
+
lines.push(" useEffect(() => {\n");
|
|
1012
|
+
if (config.features.includes("analytics")) {
|
|
1013
|
+
lines.push(" initPosthog();\n");
|
|
1014
|
+
}
|
|
1015
|
+
if (config.features.includes("error-tracking")) {
|
|
1016
|
+
lines.push(" initSentry();\n");
|
|
1017
|
+
}
|
|
1018
|
+
lines.push(" }, []);\n\n");
|
|
1019
|
+
}
|
|
1020
|
+
let body = "{children}";
|
|
1021
|
+
if (ui === "mui") {
|
|
1022
|
+
body = "<ThemeProvider theme={theme}><CssBaseline />{children}</ThemeProvider>";
|
|
1023
|
+
} else if (ui === "chakra") {
|
|
1024
|
+
body = "<ChakraProvider theme={theme}>{children}</ChakraProvider>";
|
|
1025
|
+
} else if (ui === "mantine") {
|
|
1026
|
+
body = "<MantineProvider theme={theme}>{children}</MantineProvider>";
|
|
1027
|
+
} else if (ui === "antd") {
|
|
1028
|
+
body = "<ConfigProvider theme={theme}>{children}</ConfigProvider>";
|
|
1029
|
+
} else if (ui === "nextui") {
|
|
1030
|
+
body = "<NextUIProvider theme={theme}>{children}</NextUIProvider>";
|
|
1031
|
+
}
|
|
1032
|
+
if (hasTrpc) {
|
|
1033
|
+
body = `<QueryClientProvider client={client}>${body}</QueryClientProvider>`;
|
|
1034
|
+
}
|
|
1035
|
+
lines.push(` return ${body};
|
|
1036
|
+
`);
|
|
1037
|
+
lines.push("}\n");
|
|
1038
|
+
return lines.join("");
|
|
1039
|
+
}
|
|
1040
|
+
function buildFeatureInit(config, target) {
|
|
1041
|
+
if (target !== "vite") return { initImports: "", initCalls: "" };
|
|
1042
|
+
const imports = [];
|
|
1043
|
+
const calls = [];
|
|
1044
|
+
if (config.features.includes("analytics")) {
|
|
1045
|
+
imports.push("import { initPosthog } from './lib/posthog';");
|
|
1046
|
+
calls.push("initPosthog();");
|
|
1047
|
+
}
|
|
1048
|
+
if (config.features.includes("error-tracking")) {
|
|
1049
|
+
imports.push("import { initSentry } from './lib/sentry';");
|
|
1050
|
+
calls.push("initSentry();");
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
initImports: imports.length ? imports.join("\n") + "\n" : "",
|
|
1054
|
+
initCalls: calls.length ? calls.join("\n") + "\n" : ""
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function buildApiExamples(config, target) {
|
|
1058
|
+
if (config.api.type === "none") {
|
|
1059
|
+
return { imports: "", components: "" };
|
|
1060
|
+
}
|
|
1061
|
+
const imports = [];
|
|
1062
|
+
const components = [];
|
|
1063
|
+
if (config.api.type === "rest") {
|
|
1064
|
+
const path = target === "nextjs" ? "../../src/api/client-usage" : "./api/client-usage";
|
|
1065
|
+
imports.push(`import { RestExample } from '${path}';`);
|
|
1066
|
+
components.push("<RestExample />");
|
|
1067
|
+
}
|
|
1068
|
+
if (config.api.type === "graphql") {
|
|
1069
|
+
const path = target === "nextjs" ? "../../src/graphql/client-usage" : "./graphql/client-usage";
|
|
1070
|
+
imports.push(`import { GraphqlExample } from '${path}';`);
|
|
1071
|
+
components.push("<GraphqlExample />");
|
|
1072
|
+
}
|
|
1073
|
+
if (config.api.type === "trpc") {
|
|
1074
|
+
const path = target === "nextjs" ? "../../src/trpc/client-usage" : "./trpc/client-usage";
|
|
1075
|
+
imports.push(`import { TrpcExample } from '${path}';`);
|
|
1076
|
+
components.push("<TrpcExample />");
|
|
1077
|
+
}
|
|
1078
|
+
return { imports: imports.join("\n") + "\n", components: components.join("\n ") };
|
|
1079
|
+
}
|
|
1080
|
+
function buildFeatureNotes(config) {
|
|
1081
|
+
const notes = [];
|
|
1082
|
+
if (config.features.includes("analytics")) notes.push("<p>Analytics enabled (PostHog).</p>");
|
|
1083
|
+
if (config.features.includes("error-tracking")) notes.push("<p>Error tracking enabled (Sentry).</p>");
|
|
1084
|
+
if (config.features.includes("email")) notes.push("<p>Email feature enabled (Resend).</p>");
|
|
1085
|
+
if (config.features.includes("payments")) notes.push("<p>Payments feature enabled (Stripe).</p>");
|
|
1086
|
+
if (config.features.includes("storage")) notes.push("<p>Storage feature enabled (Cloudinary).</p>");
|
|
1087
|
+
if (!notes.length) return "";
|
|
1088
|
+
return notes.join("\n ");
|
|
1089
|
+
}
|
|
1090
|
+
function buildUiDemoTokens(config, target) {
|
|
1091
|
+
if (config.ui.library === "none") {
|
|
1092
|
+
return { uiDemoImport: "", uiDemoComponent: "" };
|
|
1093
|
+
}
|
|
1094
|
+
const importPath = target === "nextjs" ? "../../src/components/ui-demo" : "./components/ui-demo";
|
|
1095
|
+
return {
|
|
1096
|
+
uiDemoImport: `import { UiDemo } from '${importPath}';
|
|
1097
|
+
`,
|
|
1098
|
+
uiDemoComponent: "<UiDemo />"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function buildPageLinks(config) {
|
|
1102
|
+
const items = [];
|
|
1103
|
+
if (config.api.type === "rest") {
|
|
1104
|
+
items.push('<li><Link href="/api/hello">REST hello</Link></li>');
|
|
1105
|
+
}
|
|
1106
|
+
if (config.api.type === "graphql") {
|
|
1107
|
+
items.push('<li><Link href="/api/graphql">GraphQL endpoint</Link></li>');
|
|
1108
|
+
}
|
|
1109
|
+
if (config.api.type !== "none") {
|
|
1110
|
+
items.push('<li><Link href="/examples">API examples</Link></li>');
|
|
1111
|
+
}
|
|
1112
|
+
if (config.auth.provider !== "none") {
|
|
1113
|
+
items.push('<li><Link href="/auth/protected">Auth protected page</Link></li>');
|
|
1114
|
+
}
|
|
1115
|
+
return items.join("\n ");
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/generators/ui/ui-files.ts
|
|
1119
|
+
import { join as join6 } from "path";
|
|
1120
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1121
|
+
async function generateUiFiles(root, config, ctx) {
|
|
1122
|
+
const projectRoot = join6(root, config.projectName);
|
|
1123
|
+
const templatesRoot = fileURLToPath4(new URL("../../../templates", import.meta.url));
|
|
1124
|
+
if (config.ui.library === "tailwind") {
|
|
1125
|
+
const tailwindTemplate = await readTextFile(join6(templatesRoot, "ui", "tailwind.config.js"));
|
|
1126
|
+
const postcssTemplate = await readTextFile(join6(templatesRoot, "ui", "postcss.config.js"));
|
|
1127
|
+
const stylesTemplate = await readTextFile(join6(templatesRoot, "ui", "styles.css"));
|
|
1128
|
+
await writeTextFile(join6(projectRoot, "tailwind.config.js"), tailwindTemplate, ctx);
|
|
1129
|
+
await writeTextFile(join6(projectRoot, "postcss.config.js"), postcssTemplate, ctx);
|
|
1130
|
+
const stylesDir = join6(projectRoot, "src");
|
|
1131
|
+
await ensureDir(stylesDir, ctx);
|
|
1132
|
+
await writeTextFile(join6(stylesDir, "styles.css"), stylesTemplate, ctx);
|
|
1133
|
+
const demo = await readTextFile(
|
|
1134
|
+
join6(templatesRoot, "ui", config.frontend.language === "ts" ? "demo-tailwind.tsx" : "demo-tailwind.jsx")
|
|
1135
|
+
);
|
|
1136
|
+
await ensureDir(join6(projectRoot, "src", "components"), ctx);
|
|
1137
|
+
await writeTextFile(
|
|
1138
|
+
join6(projectRoot, "src", "components", config.frontend.language === "ts" ? "ui-demo.tsx" : "ui-demo.jsx"),
|
|
1139
|
+
demo,
|
|
1140
|
+
ctx
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (config.ui.library === "shadcn") {
|
|
1144
|
+
await ensureDir(join6(projectRoot, "components"), ctx);
|
|
1145
|
+
const shadcnReadme = await readTextFile(join6(templatesRoot, "ui", "shadcn.README.md"));
|
|
1146
|
+
await writeTextFile(join6(projectRoot, "components", "README.md"), shadcnReadme, ctx);
|
|
1147
|
+
const componentsJson = await readTextFile(join6(templatesRoot, "ui", "components.json"));
|
|
1148
|
+
await writeTextFile(join6(projectRoot, "components.json"), componentsJson, ctx);
|
|
1149
|
+
const srcDir = join6(projectRoot, "src");
|
|
1150
|
+
await ensureDir(join6(srcDir, "lib"), ctx);
|
|
1151
|
+
await ensureDir(join6(srcDir, "components", "ui"), ctx);
|
|
1152
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1153
|
+
const utilsTemplate = await readTextFile(join6(templatesRoot, "ui", `utils.${ext}`));
|
|
1154
|
+
await writeTextFile(join6(srcDir, "lib", `utils.${ext}`), utilsTemplate, ctx);
|
|
1155
|
+
const buttonTemplate = await readTextFile(
|
|
1156
|
+
join6(templatesRoot, "ui", config.frontend.language === "ts" ? "button.tsx" : "button.jsx")
|
|
1157
|
+
);
|
|
1158
|
+
await writeTextFile(join6(srcDir, "components", "ui", config.frontend.language === "ts" ? "button.tsx" : "button.jsx"), buttonTemplate, ctx);
|
|
1159
|
+
const demo = await readTextFile(
|
|
1160
|
+
join6(templatesRoot, "ui", config.frontend.language === "ts" ? "demo-shadcn.tsx" : "demo-shadcn.jsx")
|
|
1161
|
+
);
|
|
1162
|
+
await writeTextFile(
|
|
1163
|
+
join6(srcDir, "components", config.frontend.language === "ts" ? "ui-demo.tsx" : "ui-demo.jsx"),
|
|
1164
|
+
demo,
|
|
1165
|
+
ctx
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (["mui", "chakra", "mantine", "antd", "nextui"].includes(config.ui.library)) {
|
|
1169
|
+
await ensureDir(join6(projectRoot, "components"), ctx);
|
|
1170
|
+
const readme = await readTextFile(join6(templatesRoot, "ui", `${config.ui.library}.README.md`));
|
|
1171
|
+
await writeTextFile(join6(projectRoot, "components", "README.md"), readme, ctx);
|
|
1172
|
+
const srcDir = join6(projectRoot, "src");
|
|
1173
|
+
await ensureDir(srcDir, ctx);
|
|
1174
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1175
|
+
const themeTemplate = await readTextFile(join6(templatesRoot, "ui", `${config.ui.library}.theme.${ext}`));
|
|
1176
|
+
await writeTextFile(join6(srcDir, `theme.${ext}`), themeTemplate, ctx);
|
|
1177
|
+
const demo = await readTextFile(
|
|
1178
|
+
join6(templatesRoot, "ui", `demo-${config.ui.library}.${config.frontend.language === "ts" ? "tsx" : "jsx"}`)
|
|
1179
|
+
);
|
|
1180
|
+
await ensureDir(join6(srcDir, "components"), ctx);
|
|
1181
|
+
await writeTextFile(
|
|
1182
|
+
join6(srcDir, "components", config.frontend.language === "ts" ? "ui-demo.tsx" : "ui-demo.jsx"),
|
|
1183
|
+
demo,
|
|
1184
|
+
ctx
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/generators/auth/auth-files.ts
|
|
1190
|
+
import { join as join7 } from "path";
|
|
1191
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1192
|
+
async function generateAuthFiles(root, config, ctx) {
|
|
1193
|
+
const projectRoot = join7(root, config.projectName);
|
|
1194
|
+
const templatesRoot = fileURLToPath5(new URL("../../../templates", import.meta.url));
|
|
1195
|
+
if (config.auth.provider === "nextauth") {
|
|
1196
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'NEXTAUTH_SECRET=""', ctx);
|
|
1197
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'NEXTAUTH_URL=""', ctx);
|
|
1198
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1199
|
+
if (config.frontend.type === "nextjs") {
|
|
1200
|
+
const routeDir = join7(projectRoot, "app", "api", "auth", "[...nextauth]");
|
|
1201
|
+
await ensureDir(routeDir, ctx);
|
|
1202
|
+
const route = await readTextFile(join7(templatesRoot, "auth", "nextauth-route.ts"));
|
|
1203
|
+
await writeTextFile(join7(routeDir, config.frontend.language === "ts" ? "route.ts" : "route.js"), route, ctx);
|
|
1204
|
+
}
|
|
1205
|
+
const authDir = join7(projectRoot, "auth");
|
|
1206
|
+
await ensureDir(authDir, ctx);
|
|
1207
|
+
const readme = await readTextFile(join7(templatesRoot, "auth", "nextauth.README.md"));
|
|
1208
|
+
await writeTextFile(join7(authDir, "README.md"), readme, ctx);
|
|
1209
|
+
const options = await readTextFile(join7(templatesRoot, "auth", `nextauth-options.${ext}`));
|
|
1210
|
+
await writeTextFile(join7(authDir, `auth-options.${ext}`), options, ctx);
|
|
1211
|
+
if (config.frontend.type === "nextjs") {
|
|
1212
|
+
const protectedDir = join7(projectRoot, "app", "auth", "protected");
|
|
1213
|
+
await ensureDir(protectedDir, ctx);
|
|
1214
|
+
const protectedPage = await readTextFile(
|
|
1215
|
+
join7(templatesRoot, "auth", `nextauth-protected-page.${ext}x`)
|
|
1216
|
+
);
|
|
1217
|
+
await writeTextFile(join7(protectedDir, `page.${ext}x`), protectedPage, ctx);
|
|
1218
|
+
const signInDir = join7(projectRoot, "app", "auth", "signin");
|
|
1219
|
+
await ensureDir(signInDir, ctx);
|
|
1220
|
+
const signInPage = await readTextFile(join7(templatesRoot, "auth", `nextauth-signin.${ext}x`));
|
|
1221
|
+
await writeTextFile(join7(signInDir, `page.${ext}x`), signInPage, ctx);
|
|
1222
|
+
} else {
|
|
1223
|
+
const protectedPage = await readTextFile(join7(templatesRoot, "auth", `nextauth-protected.${ext}x`));
|
|
1224
|
+
await writeTextFile(join7(authDir, `protected.${ext}x`), protectedPage, ctx);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (config.auth.provider === "clerk") {
|
|
1228
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'CLERK_SECRET_KEY=""', ctx);
|
|
1229
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""', ctx);
|
|
1230
|
+
const libDir = join7(projectRoot, "src", "lib");
|
|
1231
|
+
await ensureDir(libDir, ctx);
|
|
1232
|
+
const clerkClient = `import { clerkClient } from '@clerk/nextjs/server';
|
|
1233
|
+
|
|
1234
|
+
export { clerkClient };
|
|
1235
|
+
`;
|
|
1236
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1237
|
+
await writeTextFile(join7(libDir, `clerk.${ext}`), clerkClient, ctx);
|
|
1238
|
+
if (config.frontend.type === "nextjs") {
|
|
1239
|
+
const middleware = `import { authMiddleware } from '@clerk/nextjs';
|
|
1240
|
+
|
|
1241
|
+
export default authMiddleware();
|
|
1242
|
+
|
|
1243
|
+
export const config = {
|
|
1244
|
+
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
|
|
1245
|
+
};
|
|
1246
|
+
`;
|
|
1247
|
+
await writeTextFile(join7(projectRoot, `middleware.${ext}`), middleware, ctx);
|
|
1248
|
+
}
|
|
1249
|
+
const authDir = join7(projectRoot, "auth");
|
|
1250
|
+
await ensureDir(authDir, ctx);
|
|
1251
|
+
const readme = await readTextFile(join7(templatesRoot, "auth", "clerk.README.md"));
|
|
1252
|
+
await writeTextFile(join7(authDir, "README.md"), readme, ctx);
|
|
1253
|
+
if (config.frontend.type === "nextjs") {
|
|
1254
|
+
const protectedDir = join7(projectRoot, "app", "auth", "protected");
|
|
1255
|
+
await ensureDir(protectedDir, ctx);
|
|
1256
|
+
const protectedPage = await readTextFile(
|
|
1257
|
+
join7(templatesRoot, "auth", `clerk-protected-page.${ext}x`)
|
|
1258
|
+
);
|
|
1259
|
+
await writeTextFile(join7(protectedDir, `page.${ext}x`), protectedPage, ctx);
|
|
1260
|
+
const signInDir = join7(projectRoot, "app", "auth", "signin");
|
|
1261
|
+
await ensureDir(signInDir, ctx);
|
|
1262
|
+
const signInPage = await readTextFile(join7(templatesRoot, "auth", `clerk-signin.${ext}x`));
|
|
1263
|
+
await writeTextFile(join7(signInDir, `page.${ext}x`), signInPage, ctx);
|
|
1264
|
+
} else {
|
|
1265
|
+
const protectedPage = await readTextFile(join7(templatesRoot, "auth", `clerk-protected.${ext}x`));
|
|
1266
|
+
await writeTextFile(join7(authDir, `protected.${ext}x`), protectedPage, ctx);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (config.auth.provider === "supabase") {
|
|
1270
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'NEXT_PUBLIC_SUPABASE_URL=""', ctx);
|
|
1271
|
+
await appendEnvLine(join7(projectRoot, ".env.example"), 'NEXT_PUBLIC_SUPABASE_ANON_KEY=""', ctx);
|
|
1272
|
+
const libDir = join7(projectRoot, "src", "lib");
|
|
1273
|
+
await ensureDir(libDir, ctx);
|
|
1274
|
+
const supabaseClient = `import { createClient } from '@supabase/supabase-js';
|
|
1275
|
+
|
|
1276
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
|
|
1277
|
+
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
|
|
1278
|
+
|
|
1279
|
+
export const supabase = createClient(url, key);
|
|
1280
|
+
`;
|
|
1281
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1282
|
+
await writeTextFile(join7(libDir, `supabase.${ext}`), supabaseClient, ctx);
|
|
1283
|
+
if (config.frontend.type === "nextjs") {
|
|
1284
|
+
const supabaseServer = `import { createServerClient } from '@supabase/ssr';
|
|
1285
|
+
import { cookies } from 'next/headers';
|
|
1286
|
+
|
|
1287
|
+
export function createSupabaseServerClient() {
|
|
1288
|
+
const cookieStore = cookies();
|
|
1289
|
+
return createServerClient(
|
|
1290
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
|
|
1291
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
|
|
1292
|
+
{
|
|
1293
|
+
cookies: {
|
|
1294
|
+
get(name) {
|
|
1295
|
+
return cookieStore.get(name)?.value;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
`;
|
|
1302
|
+
await writeTextFile(join7(libDir, `supabase-server.${ext}`), supabaseServer, ctx);
|
|
1303
|
+
}
|
|
1304
|
+
const authDir = join7(projectRoot, "auth");
|
|
1305
|
+
await ensureDir(authDir, ctx);
|
|
1306
|
+
const readme = await readTextFile(join7(templatesRoot, "auth", "supabase.README.md"));
|
|
1307
|
+
await writeTextFile(join7(authDir, "README.md"), readme, ctx);
|
|
1308
|
+
if (config.frontend.type === "nextjs") {
|
|
1309
|
+
const protectedDir = join7(projectRoot, "app", "auth", "protected");
|
|
1310
|
+
await ensureDir(protectedDir, ctx);
|
|
1311
|
+
const protectedPage = await readTextFile(
|
|
1312
|
+
join7(templatesRoot, "auth", `supabase-protected-page.${ext}x`)
|
|
1313
|
+
);
|
|
1314
|
+
await writeTextFile(join7(protectedDir, `page.${ext}x`), protectedPage, ctx);
|
|
1315
|
+
const signInDir = join7(projectRoot, "app", "auth", "signin");
|
|
1316
|
+
await ensureDir(signInDir, ctx);
|
|
1317
|
+
const signInPage = await readTextFile(join7(templatesRoot, "auth", `supabase-signin.${ext}x`));
|
|
1318
|
+
await writeTextFile(join7(signInDir, `page.${ext}x`), signInPage, ctx);
|
|
1319
|
+
} else {
|
|
1320
|
+
const protectedPage = await readTextFile(join7(templatesRoot, "auth", `supabase-protected.${ext}x`));
|
|
1321
|
+
await writeTextFile(join7(authDir, `protected.${ext}x`), protectedPage, ctx);
|
|
1322
|
+
const signin = await readTextFile(
|
|
1323
|
+
join7(templatesRoot, "auth", `supabase-vite-signin.${ext}x`)
|
|
1324
|
+
);
|
|
1325
|
+
const authUiDir = join7(projectRoot, "src", "auth");
|
|
1326
|
+
await ensureDir(authUiDir, ctx);
|
|
1327
|
+
await writeTextFile(join7(authUiDir, `signin.${ext}x`), signin, ctx);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/generators/api/api-files.ts
|
|
1333
|
+
import { join as join8 } from "path";
|
|
1334
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
1335
|
+
async function generateApiFiles(root, config, ctx) {
|
|
1336
|
+
const projectRoot = join8(root, config.projectName);
|
|
1337
|
+
const templatesRoot = fileURLToPath6(new URL("../../../templates", import.meta.url));
|
|
1338
|
+
if (config.api.type === "rest") {
|
|
1339
|
+
const apiDir = join8(projectRoot, "api");
|
|
1340
|
+
await ensureDir(apiDir, ctx);
|
|
1341
|
+
await writeTextFile(
|
|
1342
|
+
join8(apiDir, "README.md"),
|
|
1343
|
+
`# REST API
|
|
1344
|
+
|
|
1345
|
+
## Overview
|
|
1346
|
+
- Next.js: app/api/hello/route.(ts|js) provides a sample GET route.
|
|
1347
|
+
- If a database is selected, app/api/users/route.(ts|js) includes CRUD examples.
|
|
1348
|
+
- Vite: src/server/index.(ts|js) hosts the API server (run npm run api:dev).
|
|
1349
|
+
|
|
1350
|
+
## Client usage
|
|
1351
|
+
- src/api/client.(ts|js) wraps fetch helpers.
|
|
1352
|
+
- src/api/client-usage.(tsx|jsx) shows usage in the UI.
|
|
1353
|
+
- For Vite, set VITE_API_URL in .env to point at the API server.
|
|
1354
|
+
`,
|
|
1355
|
+
ctx
|
|
1356
|
+
);
|
|
1357
|
+
if (config.frontend.type === "nextjs") {
|
|
1358
|
+
const routeDir = join8(projectRoot, "app", "api", "hello");
|
|
1359
|
+
await ensureDir(routeDir, ctx);
|
|
1360
|
+
const handler = await readTextFile(join8(templatesRoot, "api", "rest", "route.ts"));
|
|
1361
|
+
await writeTextFile(join8(routeDir, config.frontend.language === "ts" ? "route.ts" : "route.js"), handler, ctx);
|
|
1362
|
+
if (config.database.orm) {
|
|
1363
|
+
const usersDir = join8(projectRoot, "app", "api", "users");
|
|
1364
|
+
await ensureDir(usersDir, ctx);
|
|
1365
|
+
const usersRoute = await readTextFile(
|
|
1366
|
+
join8(templatesRoot, "api", "rest", config.frontend.language === "ts" ? "users-route.ts" : "users-route.js")
|
|
1367
|
+
);
|
|
1368
|
+
await writeTextFile(
|
|
1369
|
+
join8(usersDir, config.frontend.language === "ts" ? "route.ts" : "route.js"),
|
|
1370
|
+
usersRoute,
|
|
1371
|
+
ctx
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (config.frontend.type === "vite") {
|
|
1376
|
+
const serverDir = join8(projectRoot, "src", "server");
|
|
1377
|
+
await ensureDir(serverDir, ctx);
|
|
1378
|
+
const serverTemplate = await readTextFile(
|
|
1379
|
+
join8(templatesRoot, "api", "rest", config.frontend.language === "ts" ? "vite-server.ts" : "vite-server.js")
|
|
1380
|
+
);
|
|
1381
|
+
await writeTextFile(
|
|
1382
|
+
join8(serverDir, config.frontend.language === "ts" ? "index.ts" : "index.js"),
|
|
1383
|
+
serverTemplate,
|
|
1384
|
+
ctx
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
const clientDir = join8(projectRoot, "src", "api");
|
|
1388
|
+
await ensureDir(clientDir, ctx);
|
|
1389
|
+
const client = await readTextFile(
|
|
1390
|
+
join8(templatesRoot, "api", "rest", config.frontend.language === "ts" ? "client.ts" : "client.js")
|
|
1391
|
+
);
|
|
1392
|
+
await writeTextFile(join8(clientDir, config.frontend.language === "ts" ? "client.ts" : "client.js"), client, ctx);
|
|
1393
|
+
const usage = await readTextFile(
|
|
1394
|
+
join8(templatesRoot, "api", "rest", config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx")
|
|
1395
|
+
);
|
|
1396
|
+
await writeTextFile(
|
|
1397
|
+
join8(clientDir, config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx"),
|
|
1398
|
+
usage,
|
|
1399
|
+
ctx
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
if (config.api.type === "trpc") {
|
|
1403
|
+
const apiDir = join8(projectRoot, "api");
|
|
1404
|
+
await ensureDir(apiDir, ctx);
|
|
1405
|
+
await writeTextFile(
|
|
1406
|
+
join8(apiDir, "README.md"),
|
|
1407
|
+
`# tRPC Setup
|
|
1408
|
+
|
|
1409
|
+
## Overview
|
|
1410
|
+
- Router lives in src/server/api/trpc.ts and src/server/api/root.ts.
|
|
1411
|
+
- Next.js: app/api/trpc/[trpc]/route.(ts|js) wires the handler.
|
|
1412
|
+
- Vite: src/server/index.(ts|js) runs the API server (npm run api:dev).
|
|
1413
|
+
|
|
1414
|
+
## Client usage
|
|
1415
|
+
- src/trpc/client.(ts|js) configures the tRPC client.
|
|
1416
|
+
- src/trpc/client-usage.(tsx|jsx) shows usage in the UI.
|
|
1417
|
+
- For Vite, set VITE_API_URL in .env to point at the API server.
|
|
1418
|
+
`,
|
|
1419
|
+
ctx
|
|
1420
|
+
);
|
|
1421
|
+
if (config.frontend.type === "nextjs") {
|
|
1422
|
+
const trpcDir = join8(projectRoot, "src", "server", "api");
|
|
1423
|
+
await ensureDir(trpcDir, ctx);
|
|
1424
|
+
const router = await readTextFile(join8(templatesRoot, "api", "trpc", "trpc.ts"));
|
|
1425
|
+
const appRouter = await readTextFile(join8(templatesRoot, "api", "trpc", "root.ts"));
|
|
1426
|
+
await writeTextFile(join8(trpcDir, "trpc.ts"), router, ctx);
|
|
1427
|
+
await writeTextFile(join8(trpcDir, "root.ts"), appRouter, ctx);
|
|
1428
|
+
const routeDir = join8(projectRoot, "app", "api", "trpc", "[trpc]");
|
|
1429
|
+
await ensureDir(routeDir, ctx);
|
|
1430
|
+
const handler = await readTextFile(join8(templatesRoot, "api", "trpc", "route.ts"));
|
|
1431
|
+
await writeTextFile(join8(routeDir, config.frontend.language === "ts" ? "route.ts" : "route.js"), handler, ctx);
|
|
1432
|
+
const clientDir = join8(projectRoot, "src", "trpc");
|
|
1433
|
+
await ensureDir(clientDir, ctx);
|
|
1434
|
+
const client = await readTextFile(
|
|
1435
|
+
join8(templatesRoot, "api", "trpc", config.frontend.language === "ts" ? "client.ts" : "client.js")
|
|
1436
|
+
);
|
|
1437
|
+
await writeTextFile(join8(clientDir, config.frontend.language === "ts" ? "client.ts" : "client.js"), client, ctx);
|
|
1438
|
+
const usage = await readTextFile(
|
|
1439
|
+
join8(templatesRoot, "api", "trpc", config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx")
|
|
1440
|
+
);
|
|
1441
|
+
await writeTextFile(
|
|
1442
|
+
join8(clientDir, config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx"),
|
|
1443
|
+
usage,
|
|
1444
|
+
ctx
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
if (config.frontend.type === "vite") {
|
|
1448
|
+
const trpcDir = join8(projectRoot, "src", "server", "api");
|
|
1449
|
+
await ensureDir(trpcDir, ctx);
|
|
1450
|
+
const router = await readTextFile(join8(templatesRoot, "api", "trpc", "trpc.ts"));
|
|
1451
|
+
const appRouter = await readTextFile(join8(templatesRoot, "api", "trpc", "root.ts"));
|
|
1452
|
+
await writeTextFile(join8(trpcDir, "trpc.ts"), router, ctx);
|
|
1453
|
+
await writeTextFile(join8(trpcDir, "root.ts"), appRouter, ctx);
|
|
1454
|
+
const clientDir = join8(projectRoot, "src", "trpc");
|
|
1455
|
+
await ensureDir(clientDir, ctx);
|
|
1456
|
+
const client = await readTextFile(
|
|
1457
|
+
join8(templatesRoot, "api", "trpc", config.frontend.language === "ts" ? "client-vite.ts" : "client-vite.js")
|
|
1458
|
+
);
|
|
1459
|
+
await writeTextFile(join8(clientDir, config.frontend.language === "ts" ? "client.ts" : "client.js"), client, ctx);
|
|
1460
|
+
const usage = await readTextFile(
|
|
1461
|
+
join8(templatesRoot, "api", "trpc", config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx")
|
|
1462
|
+
);
|
|
1463
|
+
await writeTextFile(
|
|
1464
|
+
join8(clientDir, config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx"),
|
|
1465
|
+
usage,
|
|
1466
|
+
ctx
|
|
1467
|
+
);
|
|
1468
|
+
const serverDir = join8(projectRoot, "src", "server");
|
|
1469
|
+
await ensureDir(serverDir, ctx);
|
|
1470
|
+
const serverTemplate = await readTextFile(
|
|
1471
|
+
join8(templatesRoot, "api", "trpc", config.frontend.language === "ts" ? "vite-server.ts" : "vite-server.js")
|
|
1472
|
+
);
|
|
1473
|
+
await writeTextFile(
|
|
1474
|
+
join8(serverDir, config.frontend.language === "ts" ? "index.ts" : "index.js"),
|
|
1475
|
+
serverTemplate,
|
|
1476
|
+
ctx
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (config.api.type === "graphql") {
|
|
1481
|
+
const apiDir = join8(projectRoot, "api");
|
|
1482
|
+
await ensureDir(apiDir, ctx);
|
|
1483
|
+
await writeTextFile(
|
|
1484
|
+
join8(apiDir, "README.md"),
|
|
1485
|
+
`# GraphQL Setup
|
|
1486
|
+
|
|
1487
|
+
## Overview
|
|
1488
|
+
- Schema in src/graphql/schema.graphql.
|
|
1489
|
+
- Next.js: app/api/graphql/route.(ts|js) runs the Yoga handler.
|
|
1490
|
+
- Vite: src/server/index.(ts|js) runs the API server (npm run api:dev).
|
|
1491
|
+
|
|
1492
|
+
## Client usage
|
|
1493
|
+
- src/graphql/client.(ts|js) configures the GraphQL client.
|
|
1494
|
+
- src/graphql/client-usage.(tsx|jsx) shows usage in the UI.
|
|
1495
|
+
- For Vite, set VITE_API_URL in .env to point at the API server.
|
|
1496
|
+
`,
|
|
1497
|
+
ctx
|
|
1498
|
+
);
|
|
1499
|
+
if (config.frontend.type === "nextjs") {
|
|
1500
|
+
const gqlDir = join8(projectRoot, "src", "graphql");
|
|
1501
|
+
await ensureDir(gqlDir, ctx);
|
|
1502
|
+
const schema = await readTextFile(join8(templatesRoot, "api", "graphql", "schema.graphql"));
|
|
1503
|
+
await writeTextFile(join8(gqlDir, "schema.graphql"), schema, ctx);
|
|
1504
|
+
const routeDir = join8(projectRoot, "app", "api", "graphql");
|
|
1505
|
+
await ensureDir(routeDir, ctx);
|
|
1506
|
+
const handler = await readTextFile(join8(templatesRoot, "api", "graphql", "route.ts"));
|
|
1507
|
+
await writeTextFile(join8(routeDir, config.frontend.language === "ts" ? "route.ts" : "route.js"), handler, ctx);
|
|
1508
|
+
const clientDir = join8(projectRoot, "src", "graphql");
|
|
1509
|
+
const client = await readTextFile(
|
|
1510
|
+
join8(templatesRoot, "api", "graphql", config.frontend.language === "ts" ? "client.ts" : "client.js")
|
|
1511
|
+
);
|
|
1512
|
+
await writeTextFile(join8(clientDir, config.frontend.language === "ts" ? "client.ts" : "client.js"), client, ctx);
|
|
1513
|
+
const usage = await readTextFile(
|
|
1514
|
+
join8(templatesRoot, "api", "graphql", config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx")
|
|
1515
|
+
);
|
|
1516
|
+
await writeTextFile(
|
|
1517
|
+
join8(clientDir, config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx"),
|
|
1518
|
+
usage,
|
|
1519
|
+
ctx
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (config.frontend.type === "vite") {
|
|
1523
|
+
const gqlDir = join8(projectRoot, "src", "graphql");
|
|
1524
|
+
await ensureDir(gqlDir, ctx);
|
|
1525
|
+
const schema = await readTextFile(join8(templatesRoot, "api", "graphql", "schema.graphql"));
|
|
1526
|
+
await writeTextFile(join8(gqlDir, "schema.graphql"), schema, ctx);
|
|
1527
|
+
const serverDir = join8(projectRoot, "src", "server");
|
|
1528
|
+
await ensureDir(serverDir, ctx);
|
|
1529
|
+
const serverTemplate = await readTextFile(
|
|
1530
|
+
join8(templatesRoot, "api", "graphql", config.frontend.language === "ts" ? "vite-server.ts" : "vite-server.js")
|
|
1531
|
+
);
|
|
1532
|
+
await writeTextFile(
|
|
1533
|
+
join8(serverDir, config.frontend.language === "ts" ? "index.ts" : "index.js"),
|
|
1534
|
+
serverTemplate,
|
|
1535
|
+
ctx
|
|
1536
|
+
);
|
|
1537
|
+
const clientDir = join8(projectRoot, "src", "graphql");
|
|
1538
|
+
const client = await readTextFile(
|
|
1539
|
+
join8(templatesRoot, "api", "graphql", config.frontend.language === "ts" ? "client.ts" : "client.js")
|
|
1540
|
+
);
|
|
1541
|
+
await writeTextFile(join8(clientDir, config.frontend.language === "ts" ? "client.ts" : "client.js"), client, ctx);
|
|
1542
|
+
const usage = await readTextFile(
|
|
1543
|
+
join8(templatesRoot, "api", "graphql", config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx")
|
|
1544
|
+
);
|
|
1545
|
+
await writeTextFile(
|
|
1546
|
+
join8(clientDir, config.frontend.language === "ts" ? "client-usage.tsx" : "client-usage.jsx"),
|
|
1547
|
+
usage,
|
|
1548
|
+
ctx
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/ai-agents/config-generator.ts
|
|
1555
|
+
import { join as join9 } from "path";
|
|
1556
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1557
|
+
function buildAgentContext(config) {
|
|
1558
|
+
return {
|
|
1559
|
+
stack: {
|
|
1560
|
+
frontend: config.frontend,
|
|
1561
|
+
ui: config.ui,
|
|
1562
|
+
database: config.database,
|
|
1563
|
+
auth: config.auth,
|
|
1564
|
+
api: config.api
|
|
1565
|
+
},
|
|
1566
|
+
features: config.features,
|
|
1567
|
+
aiAgents: config.aiAgents,
|
|
1568
|
+
hints: buildHints(config)
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
function buildTools(config) {
|
|
1572
|
+
const tools = [];
|
|
1573
|
+
if (config.database.provider !== "none") tools.push("database");
|
|
1574
|
+
if (config.database.orm) tools.push("orm");
|
|
1575
|
+
if (config.api.type !== "none") tools.push("api");
|
|
1576
|
+
if (config.auth.provider !== "none") tools.push("auth");
|
|
1577
|
+
if (config.features.includes("email")) tools.push("email");
|
|
1578
|
+
if (config.features.includes("storage")) tools.push("storage");
|
|
1579
|
+
if (config.features.includes("payments")) tools.push("payments");
|
|
1580
|
+
if (config.features.includes("analytics")) tools.push("analytics");
|
|
1581
|
+
if (config.features.includes("error-tracking")) tools.push("error-tracking");
|
|
1582
|
+
return tools;
|
|
1583
|
+
}
|
|
1584
|
+
function buildFunctionDefinitions(tools) {
|
|
1585
|
+
return tools.map((tool) => {
|
|
1586
|
+
switch (tool) {
|
|
1587
|
+
case "database":
|
|
1588
|
+
return {
|
|
1589
|
+
name: "stackforge_database",
|
|
1590
|
+
description: "Inspect database connection and configuration.",
|
|
1591
|
+
parameters: {
|
|
1592
|
+
type: "object",
|
|
1593
|
+
properties: {
|
|
1594
|
+
action: { type: "string", enum: ["status", "env", "client-paths"] }
|
|
1595
|
+
},
|
|
1596
|
+
required: ["action"]
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
case "orm":
|
|
1600
|
+
return {
|
|
1601
|
+
name: "stackforge_orm",
|
|
1602
|
+
description: "Inspect ORM configuration and schema locations.",
|
|
1603
|
+
parameters: {
|
|
1604
|
+
type: "object",
|
|
1605
|
+
properties: {
|
|
1606
|
+
action: { type: "string", enum: ["schema", "migrations", "examples"] }
|
|
1607
|
+
},
|
|
1608
|
+
required: ["action"]
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
case "api":
|
|
1612
|
+
return {
|
|
1613
|
+
name: "stackforge_api",
|
|
1614
|
+
description: "Inspect API routes and clients.",
|
|
1615
|
+
parameters: {
|
|
1616
|
+
type: "object",
|
|
1617
|
+
properties: {
|
|
1618
|
+
action: { type: "string", enum: ["routes", "clients", "examples"] }
|
|
1619
|
+
},
|
|
1620
|
+
required: ["action"]
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
case "auth":
|
|
1624
|
+
return {
|
|
1625
|
+
name: "stackforge_auth",
|
|
1626
|
+
description: "Inspect auth routes and helpers.",
|
|
1627
|
+
parameters: {
|
|
1628
|
+
type: "object",
|
|
1629
|
+
properties: {
|
|
1630
|
+
action: { type: "string", enum: ["routes", "helpers", "env"] }
|
|
1631
|
+
},
|
|
1632
|
+
required: ["action"]
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
case "email":
|
|
1636
|
+
return {
|
|
1637
|
+
name: "stackforge_email",
|
|
1638
|
+
description: "Inspect email client and docs.",
|
|
1639
|
+
parameters: {
|
|
1640
|
+
type: "object",
|
|
1641
|
+
properties: {
|
|
1642
|
+
action: { type: "string", enum: ["client", "docs", "env"] }
|
|
1643
|
+
},
|
|
1644
|
+
required: ["action"]
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
case "storage":
|
|
1648
|
+
return {
|
|
1649
|
+
name: "stackforge_storage",
|
|
1650
|
+
description: "Inspect storage setup.",
|
|
1651
|
+
parameters: {
|
|
1652
|
+
type: "object",
|
|
1653
|
+
properties: {
|
|
1654
|
+
action: { type: "string", enum: ["docs", "env"] }
|
|
1655
|
+
},
|
|
1656
|
+
required: ["action"]
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
case "payments":
|
|
1660
|
+
return {
|
|
1661
|
+
name: "stackforge_payments",
|
|
1662
|
+
description: "Inspect payments setup.",
|
|
1663
|
+
parameters: {
|
|
1664
|
+
type: "object",
|
|
1665
|
+
properties: {
|
|
1666
|
+
action: { type: "string", enum: ["client", "docs", "env"] }
|
|
1667
|
+
},
|
|
1668
|
+
required: ["action"]
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
case "analytics":
|
|
1672
|
+
return {
|
|
1673
|
+
name: "stackforge_analytics",
|
|
1674
|
+
description: "Inspect analytics setup.",
|
|
1675
|
+
parameters: {
|
|
1676
|
+
type: "object",
|
|
1677
|
+
properties: {
|
|
1678
|
+
action: { type: "string", enum: ["client", "env"] }
|
|
1679
|
+
},
|
|
1680
|
+
required: ["action"]
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
case "error-tracking":
|
|
1684
|
+
return {
|
|
1685
|
+
name: "stackforge_error-tracking",
|
|
1686
|
+
description: "Inspect error tracking setup.",
|
|
1687
|
+
parameters: {
|
|
1688
|
+
type: "object",
|
|
1689
|
+
properties: {
|
|
1690
|
+
action: { type: "string", enum: ["client", "env"] }
|
|
1691
|
+
},
|
|
1692
|
+
required: ["action"]
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
default:
|
|
1696
|
+
return {
|
|
1697
|
+
name: `stackforge_${tool}`,
|
|
1698
|
+
description: `Access ${tool} helpers for this project.`,
|
|
1699
|
+
parameters: {
|
|
1700
|
+
type: "object",
|
|
1701
|
+
properties: {
|
|
1702
|
+
action: { type: "string" }
|
|
1703
|
+
},
|
|
1704
|
+
required: ["action"]
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
function buildHints(config) {
|
|
1711
|
+
const hints = [];
|
|
1712
|
+
if (config.api.type === "trpc") hints.push("tRPC router at src/server/api/root.ts");
|
|
1713
|
+
if (config.api.type === "graphql") hints.push("GraphQL schema at src/graphql/schema.graphql");
|
|
1714
|
+
if (config.api.type === "rest") {
|
|
1715
|
+
hints.push(
|
|
1716
|
+
config.frontend.type === "nextjs" ? "REST route at app/api/hello/route.ts" : "REST server at src/server/index.ts"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
if (config.database.orm === "drizzle") hints.push("Drizzle schema at drizzle/schema.ts");
|
|
1720
|
+
if (config.database.orm === "prisma") hints.push("Prisma schema at prisma/schema.prisma");
|
|
1721
|
+
if (config.database.orm === "mongoose") hints.push("Mongoose connection at src/db/mongoose.ts");
|
|
1722
|
+
if (config.database.orm === "typeorm") hints.push("TypeORM data source at src/db/data-source.ts");
|
|
1723
|
+
if (config.ui.library === "mui") hints.push("MUI components docs in components/README.md");
|
|
1724
|
+
if (config.ui.library === "chakra") hints.push("Chakra UI docs in components/README.md");
|
|
1725
|
+
if (config.ui.library === "mantine") hints.push("Mantine docs in components/README.md");
|
|
1726
|
+
if (config.ui.library === "antd") hints.push("Ant Design docs in components/README.md");
|
|
1727
|
+
if (config.ui.library === "nextui") hints.push("NextUI docs in components/README.md");
|
|
1728
|
+
if (config.features.includes("analytics")) hints.push("Analytics client at src/lib/posthog.ts");
|
|
1729
|
+
if (config.features.includes("error-tracking")) hints.push("Error tracking at src/lib/sentry.ts");
|
|
1730
|
+
if (config.auth.provider === "nextauth") hints.push("NextAuth route at app/api/auth/[...nextauth]/route.ts");
|
|
1731
|
+
if (config.auth.provider === "clerk") hints.push("Clerk middleware at middleware.ts");
|
|
1732
|
+
if (config.auth.provider === "supabase") hints.push("Supabase client at src/lib/supabase.ts");
|
|
1733
|
+
return hints;
|
|
1734
|
+
}
|
|
1735
|
+
async function generateAiAgentConfigs(root, config, ctx) {
|
|
1736
|
+
if (!config.aiAgents || config.aiAgents.length === 0) return;
|
|
1737
|
+
const projectRoot = resolveProjectRoot(root, config);
|
|
1738
|
+
const agentsRoot = join9(projectRoot, ".ai-agents");
|
|
1739
|
+
await ensureDir(agentsRoot, ctx);
|
|
1740
|
+
const serversRoot = join9(agentsRoot, "servers");
|
|
1741
|
+
await ensureDir(serversRoot, ctx);
|
|
1742
|
+
const protocolsRoot = join9(agentsRoot, "protocols");
|
|
1743
|
+
await ensureDir(protocolsRoot, ctx);
|
|
1744
|
+
const tools = buildTools(config);
|
|
1745
|
+
const hints = buildHints(config);
|
|
1746
|
+
for (const agent of config.aiAgents) {
|
|
1747
|
+
const agentDir = join9(agentsRoot, agent);
|
|
1748
|
+
await ensureDir(agentDir, ctx);
|
|
1749
|
+
const ext = config.frontend.language === "ts" ? "ts" : "js";
|
|
1750
|
+
const contextJson = JSON.stringify(buildAgentContext(config), null, 2);
|
|
1751
|
+
await writeTextFile(join9(agentDir, "context.json"), contextJson + "\n", ctx);
|
|
1752
|
+
const toolsJson = JSON.stringify({ tools }, null, 2);
|
|
1753
|
+
await writeTextFile(join9(agentDir, "tools.json"), toolsJson + "\n", ctx);
|
|
1754
|
+
if (agent === "claude") {
|
|
1755
|
+
const content = JSON.stringify(
|
|
1756
|
+
{
|
|
1757
|
+
mcpServers: {
|
|
1758
|
+
stackforge: {
|
|
1759
|
+
command: "node",
|
|
1760
|
+
args: [`.ai-agents/servers/claude/mcp-server.${ext}`]
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
null,
|
|
1765
|
+
2
|
|
1766
|
+
);
|
|
1767
|
+
await writeTextFile(join9(agentDir, "claude_desktop_config.json"), content + "\n", ctx);
|
|
1768
|
+
const claudeRoot = join9(projectRoot, ".claude");
|
|
1769
|
+
await ensureDir(claudeRoot, ctx);
|
|
1770
|
+
await writeTextFile(join9(claudeRoot, "claude_desktop_config.json"), content + "\n", ctx);
|
|
1771
|
+
await writeTextFile(
|
|
1772
|
+
join9(claudeRoot, "README.md"),
|
|
1773
|
+
"StackForge generated this folder for Claude. Use claude_desktop_config.json to configure the MCP server. The server lives in .ai-agents/servers/claude/mcp-server.(ts|js).\n",
|
|
1774
|
+
ctx
|
|
1775
|
+
);
|
|
1776
|
+
const serverDir = join9(serversRoot, "claude");
|
|
1777
|
+
await ensureDir(serverDir, ctx);
|
|
1778
|
+
const serverContent = buildMcpServerContent(ext, tools, hints);
|
|
1779
|
+
await writeTextFile(join9(serverDir, `mcp-server.${ext}`), serverContent, ctx);
|
|
1780
|
+
const mcpSpec = JSON.stringify(
|
|
1781
|
+
{
|
|
1782
|
+
name: "stackforge",
|
|
1783
|
+
tools
|
|
1784
|
+
},
|
|
1785
|
+
null,
|
|
1786
|
+
2
|
|
1787
|
+
);
|
|
1788
|
+
await writeTextFile(join9(serverDir, "mcp.json"), mcpSpec + "\n", ctx);
|
|
1789
|
+
await writeTextFile(
|
|
1790
|
+
join9(serverDir, "README.md"),
|
|
1791
|
+
"Run the MCP server: `node mcp-server.js` (or .ts with tsx). Endpoints: /tools and /invoke.\n",
|
|
1792
|
+
ctx
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
if (agent === "copilot") {
|
|
1796
|
+
const content = JSON.stringify({ functions: buildFunctionDefinitions(tools) }, null, 2);
|
|
1797
|
+
await writeTextFile(join9(agentDir, "functions.json"), content + "\n", ctx);
|
|
1798
|
+
const serverDir = join9(serversRoot, "copilot");
|
|
1799
|
+
await ensureDir(serverDir, ctx);
|
|
1800
|
+
const serverContent = `export const functions = ${JSON.stringify(buildFunctionDefinitions(tools), null, 2)};
|
|
1801
|
+
|
|
1802
|
+
export function handleFunctionCall(name, args) {
|
|
1803
|
+
return { name, args, ok: true, message: 'Implement tool logic here.' };
|
|
1804
|
+
}
|
|
1805
|
+
`;
|
|
1806
|
+
await writeTextFile(join9(serverDir, `functions.${ext}`), serverContent, ctx);
|
|
1807
|
+
await writeTextFile(join9(serverDir, "functions.json"), content + "\n", ctx);
|
|
1808
|
+
}
|
|
1809
|
+
if (agent === "codex") {
|
|
1810
|
+
const content = JSON.stringify({ functions: buildFunctionDefinitions(tools) }, null, 2);
|
|
1811
|
+
await writeTextFile(join9(agentDir, "functions.json"), content + "\n", ctx);
|
|
1812
|
+
const codexRoot = join9(projectRoot, ".codex");
|
|
1813
|
+
await ensureDir(codexRoot, ctx);
|
|
1814
|
+
await writeTextFile(join9(codexRoot, "functions.json"), content + "\n", ctx);
|
|
1815
|
+
await writeTextFile(
|
|
1816
|
+
join9(codexRoot, "README.md"),
|
|
1817
|
+
"StackForge generated this folder for Codex. Use functions.json for OpenAI function calling integrations.\n",
|
|
1818
|
+
ctx
|
|
1819
|
+
);
|
|
1820
|
+
const serverDir = join9(serversRoot, "codex");
|
|
1821
|
+
await ensureDir(serverDir, ctx);
|
|
1822
|
+
const serverContent = `export const functions = ${JSON.stringify(buildFunctionDefinitions(tools), null, 2)};
|
|
1823
|
+
|
|
1824
|
+
export function handleFunctionCall(name, args) {
|
|
1825
|
+
return { name, args, ok: true, message: 'Implement tool logic here.' };
|
|
1826
|
+
}
|
|
1827
|
+
`;
|
|
1828
|
+
await writeTextFile(join9(serverDir, `functions.${ext}`), serverContent, ctx);
|
|
1829
|
+
await writeTextFile(join9(serverDir, "functions.json"), content + "\n", ctx);
|
|
1830
|
+
}
|
|
1831
|
+
if (agent === "gemini") {
|
|
1832
|
+
const content = JSON.stringify({ functions: buildFunctionDefinitions(tools) }, null, 2);
|
|
1833
|
+
await writeTextFile(join9(agentDir, "function_declarations.json"), content + "\n", ctx);
|
|
1834
|
+
await writeTextFile(join9(agentDir, "function-declarations.json"), content + "\n", ctx);
|
|
1835
|
+
const serverDir = join9(serversRoot, "gemini");
|
|
1836
|
+
await ensureDir(serverDir, ctx);
|
|
1837
|
+
const serverContent = `export const functions = ${JSON.stringify(buildFunctionDefinitions(tools), null, 2)};
|
|
1838
|
+
|
|
1839
|
+
export function handleFunctionCall(name, args) {
|
|
1840
|
+
return { name, args, ok: true, message: 'Implement tool logic here.' };
|
|
1841
|
+
}
|
|
1842
|
+
`;
|
|
1843
|
+
await writeTextFile(join9(serverDir, `functions.${ext}`), serverContent, ctx);
|
|
1844
|
+
await writeTextFile(join9(serverDir, "function_declarations.json"), content + "\n", ctx);
|
|
1845
|
+
await writeTextFile(join9(serverDir, "function-declarations.json"), content + "\n", ctx);
|
|
1846
|
+
}
|
|
1847
|
+
if (agent === "cursor") {
|
|
1848
|
+
const content = `# Cursor rules
|
|
1849
|
+
# See context.json for project stack details
|
|
1850
|
+
`;
|
|
1851
|
+
await writeTextFile(join9(agentDir, ".cursorrules"), content, ctx);
|
|
1852
|
+
await writeTextFile(join9(projectRoot, ".cursorrules"), content, ctx);
|
|
1853
|
+
const cursorRoot = join9(projectRoot, ".cursor");
|
|
1854
|
+
await ensureDir(cursorRoot, ctx);
|
|
1855
|
+
const extensions = JSON.stringify({ recommendations: ["cursor.cursor"] }, null, 2);
|
|
1856
|
+
await writeTextFile(join9(cursorRoot, "extensions.json"), extensions + "\n", ctx);
|
|
1857
|
+
const serverDir = join9(serversRoot, "cursor");
|
|
1858
|
+
await ensureDir(serverDir, ctx);
|
|
1859
|
+
const serverContent = `# Cursor uses .cursorrules for guidance.
|
|
1860
|
+
`;
|
|
1861
|
+
await writeTextFile(join9(serverDir, "README.md"), serverContent, ctx);
|
|
1862
|
+
}
|
|
1863
|
+
if (agent === "codeium") {
|
|
1864
|
+
const content = JSON.stringify({ protocol: "lsp", tools, hints }, null, 2);
|
|
1865
|
+
await writeTextFile(join9(agentDir, "server-config.json"), content + "\n", ctx);
|
|
1866
|
+
}
|
|
1867
|
+
if (agent === "windsurf") {
|
|
1868
|
+
const windsurfRoot = join9(projectRoot, ".windsurf");
|
|
1869
|
+
await ensureDir(windsurfRoot, ctx);
|
|
1870
|
+
const content = JSON.stringify({ protocol: "cascade", tools, hints }, null, 2);
|
|
1871
|
+
await writeTextFile(join9(windsurfRoot, "cascade.json"), content + "\n", ctx);
|
|
1872
|
+
}
|
|
1873
|
+
if (agent === "tabnine") {
|
|
1874
|
+
const tabnineRoot = join9(projectRoot, ".tabnine");
|
|
1875
|
+
await ensureDir(tabnineRoot, ctx);
|
|
1876
|
+
const content = JSON.stringify({ tools, hints }, null, 2);
|
|
1877
|
+
await writeTextFile(join9(tabnineRoot, "config.json"), content + "\n", ctx);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const protocolTools = tools;
|
|
1881
|
+
const openAiFunctions = buildFunctionDefinitions(protocolTools);
|
|
1882
|
+
const openAiSchema = JSON.stringify({ functions: openAiFunctions }, null, 2);
|
|
1883
|
+
await writeTextFile(join9(protocolsRoot, "openai-functions.json"), openAiSchema + "\n", ctx);
|
|
1884
|
+
const lspSchema = JSON.stringify(
|
|
1885
|
+
{
|
|
1886
|
+
version: "0.1",
|
|
1887
|
+
capabilities: {
|
|
1888
|
+
tools: protocolTools
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
null,
|
|
1892
|
+
2
|
|
1893
|
+
);
|
|
1894
|
+
await writeTextFile(join9(protocolsRoot, "lsp.json"), lspSchema + "\n", ctx);
|
|
1895
|
+
const docsDir = join9(projectRoot, "docs");
|
|
1896
|
+
await ensureDir(docsDir, ctx);
|
|
1897
|
+
const aiDoc = buildAiAgentsDoc(config);
|
|
1898
|
+
await writeTextFile(join9(docsDir, "AI_AGENTS.md"), aiDoc + "\n", ctx);
|
|
1899
|
+
}
|
|
1900
|
+
function resolveProjectRoot(root, config) {
|
|
1901
|
+
const candidate = join9(root, "stackforge.json");
|
|
1902
|
+
if (existsSync2(candidate)) return root;
|
|
1903
|
+
return join9(root, config.projectName);
|
|
1904
|
+
}
|
|
1905
|
+
function buildAiAgentsDoc(config) {
|
|
1906
|
+
const agentsList = config.aiAgents.map((agent) => `- ${agent}`).join("\n");
|
|
1907
|
+
const tools = buildTools(config).map((tool) => `- ${tool}`).join("\n");
|
|
1908
|
+
const hints = buildHints(config).map((hint) => `- ${hint}`).join("\n");
|
|
1909
|
+
return `# AI Agent Integrations
|
|
1910
|
+
|
|
1911
|
+
This project includes configuration files for the selected AI agents.
|
|
1912
|
+
|
|
1913
|
+
## Enabled Agents
|
|
1914
|
+
${agentsList || "- none"}
|
|
1915
|
+
|
|
1916
|
+
## Tools Enabled
|
|
1917
|
+
${tools || "- none"}
|
|
1918
|
+
|
|
1919
|
+
## Project Hints
|
|
1920
|
+
${hints || "- none"}
|
|
1921
|
+
|
|
1922
|
+
## Files
|
|
1923
|
+
- .ai-agents/<agent>/context.json
|
|
1924
|
+
- .ai-agents/<agent>/tools.json
|
|
1925
|
+
- .ai-agents/servers/<agent>/
|
|
1926
|
+
- .claude/claude_desktop_config.json (when Claude is enabled)
|
|
1927
|
+
- .codex/functions.json (when Codex is enabled)
|
|
1928
|
+
- .cursor/extensions.json and .cursorrules (when Cursor is enabled)
|
|
1929
|
+
- .windsurf/cascade.json (when Windsurf is enabled)
|
|
1930
|
+
- .tabnine/config.json (when Tabnine is enabled)
|
|
1931
|
+
|
|
1932
|
+
## Setup Notes
|
|
1933
|
+
- Claude Desktop: copy .ai-agents/claude/claude_desktop_config.json into your Claude config folder.
|
|
1934
|
+
- Claude MCP server: run .ai-agents/servers/claude/mcp-server.(ts|js).
|
|
1935
|
+
- Copilot: use .ai-agents/copilot/functions.json for function calling.
|
|
1936
|
+
- Codex: use .codex/functions.json for function calling.
|
|
1937
|
+
- Gemini: use .ai-agents/gemini/function_declarations.json in AI Studio.
|
|
1938
|
+
- Cursor: .ai-agents/cursor/.cursorrules is auto-discovered when opening the project.
|
|
1939
|
+
- Codeium: use .ai-agents/codeium/server-config.json for LSP tools.
|
|
1940
|
+
- Windsurf: use .windsurf/cascade.json for Cascade protocol.
|
|
1941
|
+
- Tabnine: use .tabnine/config.json for plugin configuration.
|
|
1942
|
+
|
|
1943
|
+
## Notes
|
|
1944
|
+
- Agent configs are generated based on stackforge.json.
|
|
1945
|
+
- Re-run \`stackforge configure-agents\` after changing your stack.
|
|
1946
|
+
`;
|
|
1947
|
+
}
|
|
1948
|
+
function buildMcpServerContent(ext, tools, hints) {
|
|
1949
|
+
const toolCases = [
|
|
1950
|
+
"case 'stackforge_database':\\n return { ok: true, message: 'Check DATABASE_URL in .env and review db clients in src/db or drizzle/' };",
|
|
1951
|
+
"case 'stackforge_orm':\\n return { ok: true, message: 'Review ORM schema or models in drizzle/ or prisma/ or src/db' };",
|
|
1952
|
+
"case 'stackforge_api':\\n return { ok: true, message: 'API routes in app/api and clients in src/api, src/graphql, or src/trpc' };",
|
|
1953
|
+
"case 'stackforge_auth':\\n return { ok: true, message: 'Auth routes in app/api/auth and auth helpers in auth/' };",
|
|
1954
|
+
"case 'stackforge_email':\\n return { ok: true, message: 'Email client at src/lib/resend.ts and docs in features/email' };",
|
|
1955
|
+
"case 'stackforge_storage':\\n return { ok: true, message: 'Storage docs in features/storage' };",
|
|
1956
|
+
"case 'stackforge_payments':\\n return { ok: true, message: 'Stripe client at src/lib/stripe.ts and docs in features/payments' };",
|
|
1957
|
+
"case 'stackforge_analytics':\\n return { ok: true, message: 'PostHog client at src/lib/posthog.ts' };",
|
|
1958
|
+
"case 'stackforge_error-tracking':\\n return { ok: true, message: 'Sentry client at src/lib/sentry.ts' };"
|
|
1959
|
+
].join("\\n ");
|
|
1960
|
+
const common = `const tools = ${JSON.stringify(tools, null, 2)};\\nconst hints = ${JSON.stringify(
|
|
1961
|
+
hints,
|
|
1962
|
+
null,
|
|
1963
|
+
2
|
|
1964
|
+
)};\\n\\nfunction handleTool(name${ext === "ts" ? ": string" : ""}, action${ext === "ts" ? ": string" : ""}) {\\n switch (name) {\\n ${toolCases}\\n default:\\n return { ok: false, message: 'Unknown tool' };\\n }\\n}\\n\\nconst server = ${ext === "ts" ? "createServer" : "http.createServer"}((req, res) => {\\n if (req.url === '/tools') {\\n res.writeHead(200, { 'Content-Type': 'application/json' });\\n res.end(JSON.stringify({ tools, hints }));\\n return;\\n }\\n if (req.url === '/invoke' && req.method === 'POST') {\\n let body = '';\\n req.on('data', (chunk) => (body += chunk));\\n req.on('end', () => {\\n try {\\n const payload = JSON.parse(body || '{}');\\n const name = payload.name || '';\\n const action = payload.arguments?.action || '';\\n const result = handleTool(name, action);\\n res.writeHead(200, { 'Content-Type': 'application/json' });\\n res.end(JSON.stringify(result));\\n } catch (err) {\\n res.writeHead(400, { 'Content-Type': 'application/json' });\\n res.end(JSON.stringify({ ok: false, message: 'Invalid JSON' }));\\n }\\n });\\n return;\\n }\\n res.writeHead(404);\\n res.end();\\n});\\n\\nconst port = Number(process.env.MCP_PORT || 7341);\\nserver.listen(port, () => {\\n console.log('MCP server listening on', port);\\n});\\n`;
|
|
1965
|
+
if (ext === "ts") {
|
|
1966
|
+
return `import { createServer } from 'node:http';\\n\\n${common}`;
|
|
1967
|
+
}
|
|
1968
|
+
return `const http = require('node:http');\\n\\n${common}`;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/generators/features/feature-files.ts
|
|
1972
|
+
import { join as join10 } from "path";
|
|
1973
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
1974
|
+
async function generateFeatureFiles(root, config, ctx) {
|
|
1975
|
+
const projectRoot = join10(root, config.projectName);
|
|
1976
|
+
const templatesRoot = fileURLToPath7(new URL("../../../templates", import.meta.url));
|
|
1977
|
+
const libDir = join10(projectRoot, "src", "lib");
|
|
1978
|
+
const isTs = config.frontend.language === "ts";
|
|
1979
|
+
if (config.features.includes("email")) {
|
|
1980
|
+
const dir = join10(projectRoot, "features", "email");
|
|
1981
|
+
await ensureDir(dir, ctx);
|
|
1982
|
+
await ensureDir(libDir, ctx);
|
|
1983
|
+
const readme = await readTextFile(join10(templatesRoot, "features", "email", "README.md"));
|
|
1984
|
+
const resendClient = await readTextFile(
|
|
1985
|
+
join10(templatesRoot, "features", "email", isTs ? "resend.ts" : "resend.js")
|
|
1986
|
+
);
|
|
1987
|
+
await writeTextFile(join10(dir, "README.md"), readme, ctx);
|
|
1988
|
+
await writeTextFile(join10(libDir, isTs ? "resend.ts" : "resend.js"), resendClient, ctx);
|
|
1989
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'RESEND_API_KEY=""', ctx);
|
|
1990
|
+
}
|
|
1991
|
+
if (config.features.includes("storage")) {
|
|
1992
|
+
const dir = join10(projectRoot, "features", "storage");
|
|
1993
|
+
await ensureDir(dir, ctx);
|
|
1994
|
+
await ensureDir(libDir, ctx);
|
|
1995
|
+
const readme = await readTextFile(join10(templatesRoot, "features", "storage", "README.md"));
|
|
1996
|
+
await writeTextFile(join10(dir, "README.md"), readme, ctx);
|
|
1997
|
+
const storageClient = await readTextFile(
|
|
1998
|
+
join10(templatesRoot, "features", "storage", isTs ? "storage.ts" : "storage.js")
|
|
1999
|
+
);
|
|
2000
|
+
await writeTextFile(join10(libDir, isTs ? "storage.ts" : "storage.js"), storageClient, ctx);
|
|
2001
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'CLOUDINARY_URL=""', ctx);
|
|
2002
|
+
}
|
|
2003
|
+
if (config.features.includes("payments")) {
|
|
2004
|
+
const dir = join10(projectRoot, "features", "payments");
|
|
2005
|
+
await ensureDir(dir, ctx);
|
|
2006
|
+
await ensureDir(libDir, ctx);
|
|
2007
|
+
const readme = await readTextFile(join10(templatesRoot, "features", "payments", "README.md"));
|
|
2008
|
+
const stripeClient = await readTextFile(
|
|
2009
|
+
join10(templatesRoot, "features", "payments", isTs ? "stripe.ts" : "stripe.js")
|
|
2010
|
+
);
|
|
2011
|
+
await writeTextFile(join10(dir, "README.md"), readme, ctx);
|
|
2012
|
+
await writeTextFile(join10(libDir, isTs ? "stripe.ts" : "stripe.js"), stripeClient, ctx);
|
|
2013
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'STRIPE_SECRET_KEY=""', ctx);
|
|
2014
|
+
}
|
|
2015
|
+
if (config.features.includes("analytics")) {
|
|
2016
|
+
const dir = join10(projectRoot, "features", "analytics");
|
|
2017
|
+
await ensureDir(dir, ctx);
|
|
2018
|
+
await ensureDir(libDir, ctx);
|
|
2019
|
+
const readme = await readTextFile(join10(templatesRoot, "features", "analytics", "README.md"));
|
|
2020
|
+
const client = await readTextFile(
|
|
2021
|
+
join10(templatesRoot, "features", "analytics", isTs ? "posthog.ts" : "posthog.js")
|
|
2022
|
+
);
|
|
2023
|
+
await writeTextFile(join10(dir, "README.md"), readme, ctx);
|
|
2024
|
+
await writeTextFile(join10(libDir, isTs ? "posthog.ts" : "posthog.js"), client, ctx);
|
|
2025
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'NEXT_PUBLIC_POSTHOG_KEY=""', ctx);
|
|
2026
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'NEXT_PUBLIC_POSTHOG_HOST=""', ctx);
|
|
2027
|
+
}
|
|
2028
|
+
if (config.features.includes("error-tracking")) {
|
|
2029
|
+
const dir = join10(projectRoot, "features", "error-tracking");
|
|
2030
|
+
await ensureDir(dir, ctx);
|
|
2031
|
+
await ensureDir(libDir, ctx);
|
|
2032
|
+
const readme = await readTextFile(join10(templatesRoot, "features", "error-tracking", "README.md"));
|
|
2033
|
+
const client = await readTextFile(
|
|
2034
|
+
join10(templatesRoot, "features", "error-tracking", isTs ? "sentry.ts" : "sentry.js")
|
|
2035
|
+
);
|
|
2036
|
+
await writeTextFile(join10(dir, "README.md"), readme, ctx);
|
|
2037
|
+
await writeTextFile(join10(libDir, isTs ? "sentry.ts" : "sentry.js"), client, ctx);
|
|
2038
|
+
await appendEnvLine(join10(projectRoot, ".env.example"), 'SENTRY_DSN=""', ctx);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/cli/run-generators.ts
|
|
2043
|
+
async function runGenerators(cwd, config, ctx) {
|
|
2044
|
+
await createProjectSkeleton(cwd, config, ctx);
|
|
2045
|
+
await generateFrontendFiles(cwd, config, ctx);
|
|
2046
|
+
await generateUiFiles(cwd, config, ctx);
|
|
2047
|
+
await generateDatabaseFiles(cwd, config, ctx);
|
|
2048
|
+
await generateAuthFiles(cwd, config, ctx);
|
|
2049
|
+
await generateApiFiles(cwd, config, ctx);
|
|
2050
|
+
await generateFeatureFiles(cwd, config, ctx);
|
|
2051
|
+
await generateAiAgentConfigs(cwd, config, ctx);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/cli/validators/config.ts
|
|
2055
|
+
function validateConfig(config) {
|
|
2056
|
+
if (!supported.frontend.includes(config.frontend.type)) {
|
|
2057
|
+
throw new Error(`Unsupported frontend: ${config.frontend.type}`);
|
|
2058
|
+
}
|
|
2059
|
+
if (!supported.ui.includes(config.ui.library)) {
|
|
2060
|
+
throw new Error(`Unsupported UI library: ${config.ui.library}`);
|
|
2061
|
+
}
|
|
2062
|
+
if (!supported.database.includes(config.database.provider)) {
|
|
2063
|
+
throw new Error(`Unsupported database: ${config.database.provider}`);
|
|
2064
|
+
}
|
|
2065
|
+
if (config.database.orm && !supported.orm.includes(config.database.orm)) {
|
|
2066
|
+
throw new Error(`Unsupported ORM: ${config.database.orm}`);
|
|
2067
|
+
}
|
|
2068
|
+
if (!supported.auth.includes(config.auth.provider)) {
|
|
2069
|
+
throw new Error(`Unsupported auth: ${config.auth.provider}`);
|
|
2070
|
+
}
|
|
2071
|
+
if (!supported.api.includes(config.api.type)) {
|
|
2072
|
+
throw new Error(`Unsupported API: ${config.api.type}`);
|
|
2073
|
+
}
|
|
2074
|
+
for (const feature of config.features) {
|
|
2075
|
+
if (!supported.features.includes(feature)) {
|
|
2076
|
+
throw new Error(`Unsupported feature: ${feature}`);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (config.auth.provider === "nextauth" && config.frontend.type !== "nextjs") {
|
|
2080
|
+
throw new Error("NextAuth requires Next.js.");
|
|
2081
|
+
}
|
|
2082
|
+
if (config.api.type === "trpc" && config.frontend.language !== "ts") {
|
|
2083
|
+
throw new Error("tRPC requires TypeScript.");
|
|
2084
|
+
}
|
|
2085
|
+
if (config.database.orm && config.database.provider === "none") {
|
|
2086
|
+
throw new Error("ORM requires a database provider.");
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/cli/validators/compatibility.ts
|
|
2091
|
+
function validateCompatibility(config) {
|
|
2092
|
+
if (config.auth.provider === "nextauth" && config.frontend.type !== "nextjs") {
|
|
2093
|
+
throw new Error("NextAuth requires Next.js.");
|
|
2094
|
+
}
|
|
2095
|
+
if (config.auth.provider === "clerk" && config.frontend.type !== "nextjs") {
|
|
2096
|
+
throw new Error("Clerk requires Next.js.");
|
|
2097
|
+
}
|
|
2098
|
+
if (config.api.type === "trpc" && config.frontend.language !== "ts") {
|
|
2099
|
+
throw new Error("tRPC requires TypeScript.");
|
|
2100
|
+
}
|
|
2101
|
+
if (config.database.orm === "typeorm") {
|
|
2102
|
+
const supported2 = ["postgres", "mysql", "sqlite"];
|
|
2103
|
+
if (!supported2.includes(config.database.provider)) {
|
|
2104
|
+
throw new Error("TypeORM requires postgres, mysql, or sqlite.");
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (config.features.includes("error-tracking") && config.frontend.type !== "nextjs") {
|
|
2108
|
+
throw new Error("Error tracking (Sentry) requires Next.js.");
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/utils/version-manager.ts
|
|
2113
|
+
function resolveVersions(options) {
|
|
2114
|
+
return versions;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/utils/dependency-resolver.ts
|
|
2118
|
+
function resolveDependencies(config, options) {
|
|
2119
|
+
resolveVersions(options);
|
|
2120
|
+
return collectDependencies(config);
|
|
2121
|
+
}
|
|
2122
|
+
function resolveScripts(config) {
|
|
2123
|
+
return collectScripts(config);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/cli/validators/dependencies.ts
|
|
2127
|
+
function validateDependencies(config) {
|
|
2128
|
+
const deps = resolveDependencies(config);
|
|
2129
|
+
const scripts = resolveScripts(config);
|
|
2130
|
+
if (Object.keys(scripts).length === 0) {
|
|
2131
|
+
throw new Error("No scripts resolved for the selected stack.");
|
|
2132
|
+
}
|
|
2133
|
+
if (Object.keys(deps.dependencies).length === 0 && Object.keys(deps.devDependencies).length === 0) {
|
|
2134
|
+
throw new Error("No dependencies resolved for the selected stack.");
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/utils/logger.ts
|
|
2139
|
+
var logger = {
|
|
2140
|
+
info(message) {
|
|
2141
|
+
console.log(message);
|
|
2142
|
+
},
|
|
2143
|
+
warn(message) {
|
|
2144
|
+
console.warn(message);
|
|
2145
|
+
},
|
|
2146
|
+
error(message) {
|
|
2147
|
+
console.error(message);
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// src/utils/install.ts
|
|
2152
|
+
import { exec } from "child_process";
|
|
2153
|
+
function getInstallCommand(pm) {
|
|
2154
|
+
switch (pm) {
|
|
2155
|
+
case "pnpm":
|
|
2156
|
+
return "pnpm install";
|
|
2157
|
+
case "yarn":
|
|
2158
|
+
return "yarn install";
|
|
2159
|
+
case "bun":
|
|
2160
|
+
return "bun install";
|
|
2161
|
+
default:
|
|
2162
|
+
return "npm install";
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function runInstall(pm, cwd) {
|
|
2166
|
+
const command = getInstallCommand(pm);
|
|
2167
|
+
return new Promise((resolve2, reject) => {
|
|
2168
|
+
const child = exec(command, { cwd }, (err) => {
|
|
2169
|
+
if (err) reject(err);
|
|
2170
|
+
else resolve2();
|
|
2171
|
+
});
|
|
2172
|
+
child.stdout?.pipe(process.stdout);
|
|
2173
|
+
child.stderr?.pipe(process.stderr);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/cli/commands/create.ts
|
|
2178
|
+
import { join as join11, resolve } from "path";
|
|
2179
|
+
function parseCsv(input) {
|
|
2180
|
+
if (!input) return void 0;
|
|
2181
|
+
return input.split(",").map((a) => a.trim()).filter(Boolean);
|
|
2182
|
+
}
|
|
2183
|
+
var createCommand = new Command("create").argument("[project-name]", "name of the project").option("--preset <name>", "use a preset config").option("--no-install", "skip dependency install").option("--dry-run", "print planned actions without writing files").option("--ai-agents <list>", "comma-separated list of AI agents to configure").option("--features <list>", "comma-separated list of features to include").option("--yes", "skip prompts and use defaults").option("--no-prompts", "alias for --yes").option("--out-dir <path>", "output directory (defaults to current working directory)").action(async (projectName, options) => {
|
|
2184
|
+
logger.info("Starting StackForge create flow...");
|
|
2185
|
+
const skipPrompts = Boolean(options.yes || options.noPrompts);
|
|
2186
|
+
const config = await promptForConfig({ projectName, preset: options.preset, skipPrompts });
|
|
2187
|
+
if (options.aiAgents) {
|
|
2188
|
+
config.aiAgents = parseCsv(options.aiAgents) ?? [];
|
|
2189
|
+
}
|
|
2190
|
+
if (options.features) {
|
|
2191
|
+
config.features = parseCsv(options.features) ?? [];
|
|
2192
|
+
}
|
|
2193
|
+
validateConfig(config);
|
|
2194
|
+
validateCompatibility(config);
|
|
2195
|
+
validateDependencies(config);
|
|
2196
|
+
const ctx = {
|
|
2197
|
+
dryRun: Boolean(options.dryRun),
|
|
2198
|
+
log: (message) => logger.info(message)
|
|
2199
|
+
};
|
|
2200
|
+
const outDir = options.outDir ? resolve(options.outDir) : process.cwd();
|
|
2201
|
+
await runGenerators(outDir, config, ctx);
|
|
2202
|
+
if (options.install !== false && !options.dryRun) {
|
|
2203
|
+
const projectRoot = join11(outDir, config.projectName);
|
|
2204
|
+
logger.info("Installing dependencies...");
|
|
2205
|
+
await runInstall(config.packageManager, projectRoot);
|
|
2206
|
+
}
|
|
2207
|
+
logger.info(`Project created: ${config.projectName}`);
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
// src/cli/commands/list.ts
|
|
2211
|
+
import { Command as Command2 } from "commander";
|
|
2212
|
+
var listCommand = new Command2("list").option("--available", "show available features").option("--category <name>", "filter by category").action(async (options) => {
|
|
2213
|
+
if (options.available) {
|
|
2214
|
+
logger.info("Available features:");
|
|
2215
|
+
logger.info(`frontend: ${supported.frontend.join(", ")}`);
|
|
2216
|
+
logger.info(`ui: ${supported.ui.join(", ")}`);
|
|
2217
|
+
logger.info(`database: ${supported.database.join(", ")}`);
|
|
2218
|
+
logger.info(`orm: ${supported.orm.join(", ")}`);
|
|
2219
|
+
logger.info(`auth: ${supported.auth.join(", ")}`);
|
|
2220
|
+
logger.info(`api: ${supported.api.join(", ")}`);
|
|
2221
|
+
logger.info(`features: ${supported.features.join(", ")}`);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const config = await readProjectConfig(process.cwd());
|
|
2225
|
+
if (!options.category) {
|
|
2226
|
+
logger.info(`frontend: ${config.frontend.type} (${config.frontend.language})`);
|
|
2227
|
+
logger.info(`ui: ${config.ui.library}`);
|
|
2228
|
+
logger.info(`database: ${config.database.provider}${config.database.orm ? " (" + config.database.orm + ")" : ""}`);
|
|
2229
|
+
logger.info(`auth: ${config.auth.provider}`);
|
|
2230
|
+
logger.info(`api: ${config.api.type}`);
|
|
2231
|
+
logger.info(`features: ${config.features.length ? config.features.join(", ") : "none"}`);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
switch (options.category) {
|
|
2235
|
+
case "frontend":
|
|
2236
|
+
logger.info(`frontend: ${config.frontend.type} (${config.frontend.language})`);
|
|
2237
|
+
break;
|
|
2238
|
+
case "ui":
|
|
2239
|
+
logger.info(`ui: ${config.ui.library}`);
|
|
2240
|
+
break;
|
|
2241
|
+
case "database":
|
|
2242
|
+
logger.info(`database: ${config.database.provider}${config.database.orm ? " (" + config.database.orm + ")" : ""}`);
|
|
2243
|
+
break;
|
|
2244
|
+
case "auth":
|
|
2245
|
+
logger.info(`auth: ${config.auth.provider}`);
|
|
2246
|
+
break;
|
|
2247
|
+
case "api":
|
|
2248
|
+
logger.info(`api: ${config.api.type}`);
|
|
2249
|
+
break;
|
|
2250
|
+
case "features":
|
|
2251
|
+
logger.info(`features: ${config.features.length ? config.features.join(", ") : "none"}`);
|
|
2252
|
+
break;
|
|
2253
|
+
default:
|
|
2254
|
+
logger.error(`Unknown category: ${options.category}`);
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2258
|
+
// src/cli/commands/add.ts
|
|
2259
|
+
import { Command as Command3 } from "commander";
|
|
2260
|
+
|
|
2261
|
+
// src/utils/package-json.ts
|
|
2262
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2263
|
+
async function readPackageJson(path) {
|
|
2264
|
+
const raw = await readFile4(path, "utf8");
|
|
2265
|
+
return JSON.parse(raw);
|
|
2266
|
+
}
|
|
2267
|
+
async function writePackageJson(path, pkg) {
|
|
2268
|
+
await writeFile4(path, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
2269
|
+
}
|
|
2270
|
+
function mergeScripts(pkg, scripts) {
|
|
2271
|
+
return { ...pkg, scripts: { ...pkg.scripts ?? {}, ...scripts } };
|
|
2272
|
+
}
|
|
2273
|
+
function mergeDependencies(pkg, deps) {
|
|
2274
|
+
return {
|
|
2275
|
+
...pkg,
|
|
2276
|
+
dependencies: { ...pkg.dependencies ?? {}, ...deps.dependencies },
|
|
2277
|
+
devDependencies: { ...pkg.devDependencies ?? {}, ...deps.devDependencies }
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function removeScripts(pkg, scriptsToRemove) {
|
|
2281
|
+
if (!pkg.scripts) return pkg;
|
|
2282
|
+
const next = { ...pkg.scripts };
|
|
2283
|
+
for (const key of Object.keys(scriptsToRemove)) {
|
|
2284
|
+
delete next[key];
|
|
2285
|
+
}
|
|
2286
|
+
return { ...pkg, scripts: next };
|
|
2287
|
+
}
|
|
2288
|
+
function removeDependencies(pkg, depsToRemove) {
|
|
2289
|
+
const nextDeps = { ...pkg.dependencies ?? {} };
|
|
2290
|
+
const nextDev = { ...pkg.devDependencies ?? {} };
|
|
2291
|
+
for (const key of Object.keys(depsToRemove.dependencies)) {
|
|
2292
|
+
delete nextDeps[key];
|
|
2293
|
+
}
|
|
2294
|
+
for (const key of Object.keys(depsToRemove.devDependencies)) {
|
|
2295
|
+
delete nextDev[key];
|
|
2296
|
+
}
|
|
2297
|
+
return { ...pkg, dependencies: nextDeps, devDependencies: nextDev };
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// src/utils/feature-update.ts
|
|
2301
|
+
function updateConfigForFeature(config, category, value, action) {
|
|
2302
|
+
const next = { ...config, database: { ...config.database }, ui: { ...config.ui }, auth: { ...config.auth }, api: { ...config.api } };
|
|
2303
|
+
switch (category) {
|
|
2304
|
+
case "ui":
|
|
2305
|
+
if (action === "add" && value && !supported.ui.includes(value)) {
|
|
2306
|
+
throw new Error(`Unsupported UI library: ${value}`);
|
|
2307
|
+
}
|
|
2308
|
+
next.ui.library = action === "remove" ? "none" : value;
|
|
2309
|
+
break;
|
|
2310
|
+
case "auth":
|
|
2311
|
+
if (action === "add" && value && !supported.auth.includes(value)) {
|
|
2312
|
+
throw new Error(`Unsupported auth provider: ${value}`);
|
|
2313
|
+
}
|
|
2314
|
+
next.auth.provider = action === "remove" ? "none" : value;
|
|
2315
|
+
break;
|
|
2316
|
+
case "api":
|
|
2317
|
+
if (action === "add" && value && !supported.api.includes(value)) {
|
|
2318
|
+
throw new Error(`Unsupported API type: ${value}`);
|
|
2319
|
+
}
|
|
2320
|
+
next.api.type = action === "remove" ? "none" : value;
|
|
2321
|
+
break;
|
|
2322
|
+
case "database":
|
|
2323
|
+
if (action === "add" && value && !supported.database.includes(value)) {
|
|
2324
|
+
throw new Error(`Unsupported database: ${value}`);
|
|
2325
|
+
}
|
|
2326
|
+
next.database.provider = action === "remove" ? "none" : value;
|
|
2327
|
+
if (action === "remove" || value === "none") next.database.orm = void 0;
|
|
2328
|
+
break;
|
|
2329
|
+
case "orm":
|
|
2330
|
+
if (action === "add" && value && !supported.orm.includes(value)) {
|
|
2331
|
+
throw new Error(`Unsupported ORM: ${value}`);
|
|
2332
|
+
}
|
|
2333
|
+
next.database.orm = action === "remove" ? void 0 : value;
|
|
2334
|
+
break;
|
|
2335
|
+
case "feature":
|
|
2336
|
+
if (action === "add" && value && !supported.features.includes(value)) {
|
|
2337
|
+
throw new Error(`Unsupported feature: ${value}`);
|
|
2338
|
+
}
|
|
2339
|
+
if (action === "add" && value) {
|
|
2340
|
+
next.features = Array.from(/* @__PURE__ */ new Set([...next.features, value]));
|
|
2341
|
+
} else if (action === "remove" && value) {
|
|
2342
|
+
next.features = next.features.filter((f) => f !== value);
|
|
2343
|
+
}
|
|
2344
|
+
break;
|
|
2345
|
+
default:
|
|
2346
|
+
throw new Error(`Unknown feature category: ${category}`);
|
|
2347
|
+
}
|
|
2348
|
+
return next;
|
|
2349
|
+
}
|
|
2350
|
+
function diffKeys(oldMap, newMap) {
|
|
2351
|
+
const diff = {};
|
|
2352
|
+
for (const key of Object.keys(oldMap)) {
|
|
2353
|
+
if (!(key in newMap)) diff[key] = oldMap[key];
|
|
2354
|
+
}
|
|
2355
|
+
return diff;
|
|
2356
|
+
}
|
|
2357
|
+
async function syncPackageJson(path, oldConfig, newConfig) {
|
|
2358
|
+
const pkg = await readPackageJson(path);
|
|
2359
|
+
const oldScripts = collectScripts(oldConfig);
|
|
2360
|
+
const newScripts = collectScripts(newConfig);
|
|
2361
|
+
const oldDeps = collectDependencies(oldConfig);
|
|
2362
|
+
const newDeps = collectDependencies(newConfig);
|
|
2363
|
+
const scriptsToRemove = diffKeys(oldScripts, newScripts);
|
|
2364
|
+
const depsToRemove = {
|
|
2365
|
+
dependencies: diffKeys(oldDeps.dependencies, newDeps.dependencies),
|
|
2366
|
+
devDependencies: diffKeys(oldDeps.devDependencies, newDeps.devDependencies)
|
|
2367
|
+
};
|
|
2368
|
+
let nextPkg = removeScripts(pkg, scriptsToRemove);
|
|
2369
|
+
nextPkg = removeDependencies(nextPkg, depsToRemove);
|
|
2370
|
+
nextPkg = mergeScripts(nextPkg, newScripts);
|
|
2371
|
+
nextPkg = mergeDependencies(nextPkg, newDeps);
|
|
2372
|
+
await writePackageJson(path, nextPkg);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/cli/commands/add.ts
|
|
2376
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
2377
|
+
function parseFeature(feature) {
|
|
2378
|
+
const [category, value] = feature.split(":");
|
|
2379
|
+
if (!category || !value) {
|
|
2380
|
+
throw new Error("Feature must be in the form category:value (e.g., auth:nextauth).");
|
|
2381
|
+
}
|
|
2382
|
+
return { category, value };
|
|
2383
|
+
}
|
|
2384
|
+
var addCommand = new Command3("add").argument("<feature>", "feature to add (category:value)").action(async (feature) => {
|
|
2385
|
+
const cwd = process.cwd();
|
|
2386
|
+
const { category, value } = parseFeature(feature);
|
|
2387
|
+
const current = await readProjectConfig(cwd);
|
|
2388
|
+
const next = updateConfigForFeature(current, category, value, "add");
|
|
2389
|
+
validateConfig(next);
|
|
2390
|
+
validateCompatibility(next);
|
|
2391
|
+
validateDependencies(next);
|
|
2392
|
+
await writeProjectConfig(cwd, next);
|
|
2393
|
+
await syncPackageJson(`${cwd}/package.json`, current, next);
|
|
2394
|
+
const root = dirname3(cwd);
|
|
2395
|
+
if (category === "ui") await generateUiFiles(root, next);
|
|
2396
|
+
if (category === "database" || category === "orm") await generateDatabaseFiles(root, next);
|
|
2397
|
+
if (category === "auth") await generateAuthFiles(root, next);
|
|
2398
|
+
if (category === "api") await generateApiFiles(root, next);
|
|
2399
|
+
if (category === "feature") await generateFeatureFiles(root, next);
|
|
2400
|
+
const readme = buildProjectReadme(next);
|
|
2401
|
+
await writeTextFile(join12(cwd, "README.md"), readme + "\n");
|
|
2402
|
+
logger.info(`Added ${feature}`);
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
// src/cli/commands/remove.ts
|
|
2406
|
+
import { Command as Command4 } from "commander";
|
|
2407
|
+
|
|
2408
|
+
// src/utils/feature-cleanup.ts
|
|
2409
|
+
import { join as join13 } from "path";
|
|
2410
|
+
async function cleanupFeature(cwd, config, category, value) {
|
|
2411
|
+
const root = cwd;
|
|
2412
|
+
if (category === "ui") {
|
|
2413
|
+
if (config.ui.library === "tailwind") {
|
|
2414
|
+
await removePath(join13(root, "tailwind.config.js"));
|
|
2415
|
+
await removePath(join13(root, "postcss.config.js"));
|
|
2416
|
+
await removePath(join13(root, "src", "styles.css"));
|
|
2417
|
+
await removePath(join13(root, "src", "components", "ui-demo.tsx"));
|
|
2418
|
+
await removePath(join13(root, "src", "components", "ui-demo.jsx"));
|
|
2419
|
+
}
|
|
2420
|
+
if (config.ui.library === "shadcn") {
|
|
2421
|
+
await removePath(join13(root, "components"));
|
|
2422
|
+
await removePath(join13(root, "components.json"));
|
|
2423
|
+
await removePath(join13(root, "src", "lib", "utils.ts"));
|
|
2424
|
+
await removePath(join13(root, "src", "lib", "utils.js"));
|
|
2425
|
+
await removePath(join13(root, "src", "components", "ui"));
|
|
2426
|
+
await removePath(join13(root, "src", "components", "ui-demo.tsx"));
|
|
2427
|
+
await removePath(join13(root, "src", "components", "ui-demo.jsx"));
|
|
2428
|
+
}
|
|
2429
|
+
if (config.ui.library === "mui" || config.ui.library === "chakra" || config.ui.library === "mantine" || config.ui.library === "antd" || config.ui.library === "nextui") {
|
|
2430
|
+
await removePath(join13(root, "components"));
|
|
2431
|
+
await removePath(join13(root, "src", "theme.ts"));
|
|
2432
|
+
await removePath(join13(root, "src", "theme.js"));
|
|
2433
|
+
await removePath(join13(root, "src", "components", "ui-demo.tsx"));
|
|
2434
|
+
await removePath(join13(root, "src", "components", "ui-demo.jsx"));
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (category === "auth") {
|
|
2438
|
+
if (config.auth.provider === "nextauth") {
|
|
2439
|
+
await removePath(join13(root, "app", "api", "auth"));
|
|
2440
|
+
await removePath(join13(root, "app", "auth", "protected"));
|
|
2441
|
+
await removePath(join13(root, "app", "auth", "signin"));
|
|
2442
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXTAUTH_SECRET");
|
|
2443
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXTAUTH_URL");
|
|
2444
|
+
}
|
|
2445
|
+
if (config.auth.provider === "clerk") {
|
|
2446
|
+
await removePath(join13(root, "middleware.ts"));
|
|
2447
|
+
await removePath(join13(root, "middleware.js"));
|
|
2448
|
+
await removePath(join13(root, "src", "lib", "clerk.ts"));
|
|
2449
|
+
await removePath(join13(root, "src", "lib", "clerk.js"));
|
|
2450
|
+
await removePath(join13(root, "app", "auth", "protected"));
|
|
2451
|
+
await removePath(join13(root, "app", "auth", "signin"));
|
|
2452
|
+
await removeEnvKey(join13(root, ".env.example"), "CLERK_SECRET_KEY");
|
|
2453
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
|
|
2454
|
+
}
|
|
2455
|
+
if (config.auth.provider === "supabase") {
|
|
2456
|
+
await removePath(join13(root, "src", "lib", "supabase.ts"));
|
|
2457
|
+
await removePath(join13(root, "src", "lib", "supabase.js"));
|
|
2458
|
+
await removePath(join13(root, "src", "lib", "supabase-server.ts"));
|
|
2459
|
+
await removePath(join13(root, "src", "lib", "supabase-server.js"));
|
|
2460
|
+
await removePath(join13(root, "app", "auth", "protected"));
|
|
2461
|
+
await removePath(join13(root, "app", "auth", "signin"));
|
|
2462
|
+
await removePath(join13(root, "src", "auth", "signin.tsx"));
|
|
2463
|
+
await removePath(join13(root, "src", "auth", "signin.jsx"));
|
|
2464
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXT_PUBLIC_SUPABASE_URL");
|
|
2465
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXT_PUBLIC_SUPABASE_ANON_KEY");
|
|
2466
|
+
}
|
|
2467
|
+
await removePath(join13(root, "auth"));
|
|
2468
|
+
}
|
|
2469
|
+
if (category === "api") {
|
|
2470
|
+
if (config.api.type === "rest") {
|
|
2471
|
+
await removePath(join13(root, "app", "api", "hello"));
|
|
2472
|
+
await removePath(join13(root, "app", "api", "users"));
|
|
2473
|
+
await removePath(join13(root, "app", "examples"));
|
|
2474
|
+
await removePath(join13(root, "src", "server"));
|
|
2475
|
+
await removePath(join13(root, "src", "api"));
|
|
2476
|
+
await removePath(join13(root, "src", "api", "client-usage.tsx"));
|
|
2477
|
+
await removePath(join13(root, "src", "api", "client-usage.jsx"));
|
|
2478
|
+
}
|
|
2479
|
+
if (config.api.type === "trpc") {
|
|
2480
|
+
await removePath(join13(root, "app", "api", "trpc"));
|
|
2481
|
+
await removePath(join13(root, "app", "examples"));
|
|
2482
|
+
await removePath(join13(root, "src", "server", "api"));
|
|
2483
|
+
await removePath(join13(root, "src", "trpc"));
|
|
2484
|
+
await removePath(join13(root, "src", "trpc", "client-usage.tsx"));
|
|
2485
|
+
await removePath(join13(root, "src", "trpc", "client-usage.jsx"));
|
|
2486
|
+
await removePath(join13(root, "src", "server"));
|
|
2487
|
+
}
|
|
2488
|
+
if (config.api.type === "graphql") {
|
|
2489
|
+
await removePath(join13(root, "app", "api", "graphql"));
|
|
2490
|
+
await removePath(join13(root, "app", "examples"));
|
|
2491
|
+
await removePath(join13(root, "src", "graphql"));
|
|
2492
|
+
await removePath(join13(root, "src", "server"));
|
|
2493
|
+
await removePath(join13(root, "src", "graphql", "client-usage.tsx"));
|
|
2494
|
+
await removePath(join13(root, "src", "graphql", "client-usage.jsx"));
|
|
2495
|
+
}
|
|
2496
|
+
await removePath(join13(root, "api"));
|
|
2497
|
+
}
|
|
2498
|
+
if (category === "database") {
|
|
2499
|
+
if (config.database.orm === "drizzle") {
|
|
2500
|
+
await removePath(join13(root, "drizzle.config.ts"));
|
|
2501
|
+
await removePath(join13(root, "drizzle"));
|
|
2502
|
+
}
|
|
2503
|
+
if (config.database.orm === "prisma") {
|
|
2504
|
+
await removePath(join13(root, "prisma"));
|
|
2505
|
+
await removePath(join13(root, "src", "db", "prisma.ts"));
|
|
2506
|
+
await removePath(join13(root, "src", "db", "prisma.js"));
|
|
2507
|
+
await removePath(join13(root, "src", "db", "prisma-example.ts"));
|
|
2508
|
+
await removePath(join13(root, "src", "db", "prisma-example.js"));
|
|
2509
|
+
}
|
|
2510
|
+
if (config.database.orm === "mongoose") {
|
|
2511
|
+
await removePath(join13(root, "src", "db", "mongoose.ts"));
|
|
2512
|
+
await removePath(join13(root, "src", "db", "mongoose.js"));
|
|
2513
|
+
await removePath(join13(root, "src", "db", "mongoose-model.ts"));
|
|
2514
|
+
await removePath(join13(root, "src", "db", "mongoose-model.js"));
|
|
2515
|
+
}
|
|
2516
|
+
if (config.database.orm === "typeorm") {
|
|
2517
|
+
await removePath(join13(root, "src", "db", "data-source.ts"));
|
|
2518
|
+
await removePath(join13(root, "src", "db", "data-source.js"));
|
|
2519
|
+
await removePath(join13(root, "src", "db", "entities"));
|
|
2520
|
+
await removePath(join13(root, "src", "db", "migrations"));
|
|
2521
|
+
}
|
|
2522
|
+
await removeEnvKey(join13(root, ".env.example"), "DATABASE_URL");
|
|
2523
|
+
await removeEnvKey(join13(root, ".env.example"), "NEON_API_KEY");
|
|
2524
|
+
await removeEnvKey(join13(root, ".env.example"), "NEON_PROJECT_ID");
|
|
2525
|
+
await removeEnvKey(join13(root, ".env.example"), "SUPABASE_URL");
|
|
2526
|
+
await removeEnvKey(join13(root, ".env.example"), "SUPABASE_ANON_KEY");
|
|
2527
|
+
}
|
|
2528
|
+
if (category === "orm") {
|
|
2529
|
+
if (config.database.orm === "drizzle") {
|
|
2530
|
+
await removePath(join13(root, "drizzle.config.ts"));
|
|
2531
|
+
await removePath(join13(root, "drizzle"));
|
|
2532
|
+
}
|
|
2533
|
+
if (config.database.orm === "prisma") {
|
|
2534
|
+
await removePath(join13(root, "prisma"));
|
|
2535
|
+
await removePath(join13(root, "src", "db", "prisma.ts"));
|
|
2536
|
+
await removePath(join13(root, "src", "db", "prisma.js"));
|
|
2537
|
+
await removePath(join13(root, "src", "db", "prisma-example.ts"));
|
|
2538
|
+
await removePath(join13(root, "src", "db", "prisma-example.js"));
|
|
2539
|
+
}
|
|
2540
|
+
if (config.database.orm === "mongoose") {
|
|
2541
|
+
await removePath(join13(root, "src", "db", "mongoose.ts"));
|
|
2542
|
+
await removePath(join13(root, "src", "db", "mongoose.js"));
|
|
2543
|
+
await removePath(join13(root, "src", "db", "mongoose-model.ts"));
|
|
2544
|
+
await removePath(join13(root, "src", "db", "mongoose-model.js"));
|
|
2545
|
+
}
|
|
2546
|
+
if (config.database.orm === "typeorm") {
|
|
2547
|
+
await removePath(join13(root, "src", "db", "data-source.ts"));
|
|
2548
|
+
await removePath(join13(root, "src", "db", "data-source.js"));
|
|
2549
|
+
await removePath(join13(root, "src", "db", "entities"));
|
|
2550
|
+
await removePath(join13(root, "src", "db", "migrations"));
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (category === "feature") {
|
|
2554
|
+
const targets = value ? [value] : config.features;
|
|
2555
|
+
if (targets.includes("email")) {
|
|
2556
|
+
await removePath(join13(root, "features", "email"));
|
|
2557
|
+
await removeEnvKey(join13(root, ".env.example"), "RESEND_API_KEY");
|
|
2558
|
+
await removePath(join13(root, "src", "lib", "resend.ts"));
|
|
2559
|
+
await removePath(join13(root, "src", "lib", "resend.js"));
|
|
2560
|
+
}
|
|
2561
|
+
if (targets.includes("storage")) {
|
|
2562
|
+
await removePath(join13(root, "features", "storage"));
|
|
2563
|
+
await removeEnvKey(join13(root, ".env.example"), "CLOUDINARY_URL");
|
|
2564
|
+
await removePath(join13(root, "src", "lib", "storage.ts"));
|
|
2565
|
+
await removePath(join13(root, "src", "lib", "storage.js"));
|
|
2566
|
+
}
|
|
2567
|
+
if (targets.includes("payments")) {
|
|
2568
|
+
await removePath(join13(root, "features", "payments"));
|
|
2569
|
+
await removeEnvKey(join13(root, ".env.example"), "STRIPE_SECRET_KEY");
|
|
2570
|
+
await removePath(join13(root, "src", "lib", "stripe.ts"));
|
|
2571
|
+
await removePath(join13(root, "src", "lib", "stripe.js"));
|
|
2572
|
+
}
|
|
2573
|
+
if (targets.includes("analytics")) {
|
|
2574
|
+
await removePath(join13(root, "features", "analytics"));
|
|
2575
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXT_PUBLIC_POSTHOG_KEY");
|
|
2576
|
+
await removeEnvKey(join13(root, ".env.example"), "NEXT_PUBLIC_POSTHOG_HOST");
|
|
2577
|
+
await removePath(join13(root, "src", "lib", "posthog.ts"));
|
|
2578
|
+
await removePath(join13(root, "src", "lib", "posthog.js"));
|
|
2579
|
+
}
|
|
2580
|
+
if (targets.includes("error-tracking")) {
|
|
2581
|
+
await removePath(join13(root, "features", "error-tracking"));
|
|
2582
|
+
await removeEnvKey(join13(root, ".env.example"), "SENTRY_DSN");
|
|
2583
|
+
await removePath(join13(root, "src", "lib", "sentry.ts"));
|
|
2584
|
+
await removePath(join13(root, "src", "lib", "sentry.js"));
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/cli/commands/remove.ts
|
|
2590
|
+
import { join as join14 } from "path";
|
|
2591
|
+
function parseFeature2(feature) {
|
|
2592
|
+
const [category, value] = feature.split(":");
|
|
2593
|
+
if (!category || !value) {
|
|
2594
|
+
throw new Error("Feature must be in the form category:value (e.g., auth:nextauth).");
|
|
2595
|
+
}
|
|
2596
|
+
return { category, value };
|
|
2597
|
+
}
|
|
2598
|
+
function assertRemovalTarget(category, value, current) {
|
|
2599
|
+
switch (category) {
|
|
2600
|
+
case "ui":
|
|
2601
|
+
if (current.ui.library !== value) {
|
|
2602
|
+
throw new Error(`UI library ${value} is not installed.`);
|
|
2603
|
+
}
|
|
2604
|
+
break;
|
|
2605
|
+
case "auth":
|
|
2606
|
+
if (current.auth.provider !== value) {
|
|
2607
|
+
throw new Error(`Auth provider ${value} is not installed.`);
|
|
2608
|
+
}
|
|
2609
|
+
break;
|
|
2610
|
+
case "api":
|
|
2611
|
+
if (current.api.type !== value) {
|
|
2612
|
+
throw new Error(`API type ${value} is not installed.`);
|
|
2613
|
+
}
|
|
2614
|
+
break;
|
|
2615
|
+
case "database":
|
|
2616
|
+
if (current.database.provider !== value) {
|
|
2617
|
+
throw new Error(`Database provider ${value} is not installed.`);
|
|
2618
|
+
}
|
|
2619
|
+
break;
|
|
2620
|
+
case "orm":
|
|
2621
|
+
if (current.database.orm !== value) {
|
|
2622
|
+
throw new Error(`ORM ${value} is not installed.`);
|
|
2623
|
+
}
|
|
2624
|
+
break;
|
|
2625
|
+
case "feature":
|
|
2626
|
+
if (!current.features.includes(value)) {
|
|
2627
|
+
throw new Error(`Feature ${value} is not installed.`);
|
|
2628
|
+
}
|
|
2629
|
+
break;
|
|
2630
|
+
default:
|
|
2631
|
+
throw new Error(`Unknown feature category: ${category}`);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
var removeCommand = new Command4("remove").argument("<feature>", "feature to remove (category:value)").action(async (feature) => {
|
|
2635
|
+
const cwd = process.cwd();
|
|
2636
|
+
const { category, value } = parseFeature2(feature);
|
|
2637
|
+
const current = await readProjectConfig(cwd);
|
|
2638
|
+
assertRemovalTarget(category, value, current);
|
|
2639
|
+
await cleanupFeature(cwd, current, category, value);
|
|
2640
|
+
const next = updateConfigForFeature(current, category, value, "remove");
|
|
2641
|
+
validateConfig(next);
|
|
2642
|
+
validateCompatibility(next);
|
|
2643
|
+
validateDependencies(next);
|
|
2644
|
+
await writeProjectConfig(cwd, next);
|
|
2645
|
+
await syncPackageJson(`${cwd}/package.json`, current, next);
|
|
2646
|
+
const readme = buildProjectReadme(next);
|
|
2647
|
+
await writeTextFile(join14(cwd, "README.md"), readme + "\n");
|
|
2648
|
+
logger.info(`Removed ${feature}`);
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
// src/cli/commands/update.ts
|
|
2652
|
+
import { Command as Command5 } from "commander";
|
|
2653
|
+
import { join as join15 } from "path";
|
|
2654
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2655
|
+
var updateCommand = new Command5("update").option("--check", "check for updates").option("--major", "allow major updates").option("--live", "compare against latest registry versions").action(async (options) => {
|
|
2656
|
+
const cwd = process.cwd();
|
|
2657
|
+
const config = await readProjectConfig(cwd);
|
|
2658
|
+
const pkgPath = join15(cwd, "package.json");
|
|
2659
|
+
if (options.check) {
|
|
2660
|
+
const pkg = JSON.parse(await readFile5(pkgPath, "utf8"));
|
|
2661
|
+
const expectedScripts = resolveScripts(config);
|
|
2662
|
+
const expectedDeps = resolveDependencies(config, { allowMajor: Boolean(options.major) });
|
|
2663
|
+
const issues = [];
|
|
2664
|
+
for (const key of Object.keys(expectedScripts)) {
|
|
2665
|
+
if (!pkg.scripts || !(key in pkg.scripts)) {
|
|
2666
|
+
issues.push(`Missing script: ${key}`);
|
|
2667
|
+
} else if (pkg.scripts[key] !== expectedScripts[key]) {
|
|
2668
|
+
issues.push(`Script mismatch: ${key}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
for (const key of Object.keys(expectedDeps.dependencies)) {
|
|
2672
|
+
if (!pkg.dependencies || !(key in pkg.dependencies)) {
|
|
2673
|
+
issues.push(`Missing dependency: ${key}`);
|
|
2674
|
+
} else if (pkg.dependencies[key] !== expectedDeps.dependencies[key]) {
|
|
2675
|
+
issues.push(`Dependency version mismatch: ${key}`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
for (const key of Object.keys(expectedDeps.devDependencies)) {
|
|
2679
|
+
if (!pkg.devDependencies || !(key in pkg.devDependencies)) {
|
|
2680
|
+
issues.push(`Missing devDependency: ${key}`);
|
|
2681
|
+
} else if (pkg.devDependencies[key] !== expectedDeps.devDependencies[key]) {
|
|
2682
|
+
issues.push(`Dev dependency version mismatch: ${key}`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
if (options.live) {
|
|
2686
|
+
const { fetchLatestVersion } = await import("./npm-registry-F7EVX3RR.js");
|
|
2687
|
+
const allDeps = {
|
|
2688
|
+
...expectedDeps.dependencies,
|
|
2689
|
+
...expectedDeps.devDependencies
|
|
2690
|
+
};
|
|
2691
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
2692
|
+
const latest = await fetchLatestVersion(name);
|
|
2693
|
+
if (latest && !version.includes(latest)) {
|
|
2694
|
+
issues.push(`Latest available: ${name}@${latest} (current ${version})`);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (issues.length === 0) {
|
|
2699
|
+
logger.info("No updates needed.");
|
|
2700
|
+
} else {
|
|
2701
|
+
for (const issue of issues) logger.warn(issue);
|
|
2702
|
+
}
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
await syncPackageJson(pkgPath, config, config);
|
|
2706
|
+
if (options.live) {
|
|
2707
|
+
const { fetchLatestVersion } = await import("./npm-registry-F7EVX3RR.js");
|
|
2708
|
+
const pkg = await readPackageJson(pkgPath);
|
|
2709
|
+
const allowMajor = Boolean(options.major);
|
|
2710
|
+
const updateMap = async (deps) => {
|
|
2711
|
+
if (!deps) return;
|
|
2712
|
+
for (const [name, current] of Object.entries(deps)) {
|
|
2713
|
+
const latest = await fetchLatestVersion(name);
|
|
2714
|
+
if (!latest) continue;
|
|
2715
|
+
const currentMajor = parseMajor(current);
|
|
2716
|
+
const latestMajor = parseMajor(latest);
|
|
2717
|
+
if (!allowMajor && currentMajor !== null && latestMajor !== null && latestMajor > currentMajor) {
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
deps[name] = `^${latest}`;
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
await updateMap(pkg.dependencies);
|
|
2724
|
+
await updateMap(pkg.devDependencies);
|
|
2725
|
+
await writePackageJson(pkgPath, pkg);
|
|
2726
|
+
}
|
|
2727
|
+
const readme = buildProjectReadme(config);
|
|
2728
|
+
await writeTextFile(join15(cwd, "README.md"), readme + "\n");
|
|
2729
|
+
logger.info("Project updated.");
|
|
2730
|
+
});
|
|
2731
|
+
function parseMajor(version) {
|
|
2732
|
+
const match = version.match(/(\d+)\./);
|
|
2733
|
+
if (!match) return null;
|
|
2734
|
+
return Number(match[1]);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/cli/commands/doctor.ts
|
|
2738
|
+
import { Command as Command6 } from "commander";
|
|
2739
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2740
|
+
import { join as join17 } from "path";
|
|
2741
|
+
|
|
2742
|
+
// src/utils/doctor.ts
|
|
2743
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2744
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2745
|
+
import { join as join16, dirname as dirname4, basename } from "path";
|
|
2746
|
+
function requiredEnvKeys(config) {
|
|
2747
|
+
const keys = [];
|
|
2748
|
+
if (config.database.provider !== "none") keys.push("DATABASE_URL");
|
|
2749
|
+
if (config.database.provider === "neon") keys.push("NEON_API_KEY", "NEON_PROJECT_ID");
|
|
2750
|
+
if (config.database.provider === "supabase") keys.push("SUPABASE_URL", "SUPABASE_ANON_KEY");
|
|
2751
|
+
if (config.auth.provider === "nextauth") keys.push("NEXTAUTH_SECRET", "NEXTAUTH_URL");
|
|
2752
|
+
if (config.auth.provider === "clerk") keys.push("CLERK_SECRET_KEY", "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
|
|
2753
|
+
if (config.auth.provider === "supabase") keys.push("NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY");
|
|
2754
|
+
if (config.features.includes("email")) keys.push("RESEND_API_KEY");
|
|
2755
|
+
if (config.features.includes("storage")) keys.push("CLOUDINARY_URL");
|
|
2756
|
+
if (config.features.includes("payments")) keys.push("STRIPE_SECRET_KEY");
|
|
2757
|
+
if (config.features.includes("analytics")) keys.push("NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST");
|
|
2758
|
+
if (config.features.includes("error-tracking")) keys.push("SENTRY_DSN");
|
|
2759
|
+
return keys;
|
|
2760
|
+
}
|
|
2761
|
+
function agentFiles(agent) {
|
|
2762
|
+
const files = ["context.json", "tools.json"];
|
|
2763
|
+
if (agent === "claude") files.push("claude_desktop_config.json");
|
|
2764
|
+
if (agent === "copilot") files.push("functions.json");
|
|
2765
|
+
if (agent === "codex") files.push("functions.json");
|
|
2766
|
+
if (agent === "gemini") files.push("function_declarations.json");
|
|
2767
|
+
if (agent === "cursor") files.push(".cursorrules");
|
|
2768
|
+
if (agent === "codeium") files.push("server-config.json");
|
|
2769
|
+
return files;
|
|
2770
|
+
}
|
|
2771
|
+
async function checkProject(cwd) {
|
|
2772
|
+
const configPath = join16(cwd, "stackforge.json");
|
|
2773
|
+
const pkgPath = join16(cwd, "package.json");
|
|
2774
|
+
const envPath = join16(cwd, ".env.example");
|
|
2775
|
+
const issues = [];
|
|
2776
|
+
if (!existsSync3(configPath)) {
|
|
2777
|
+
return {
|
|
2778
|
+
issues: ["Missing stackforge.json. Run from a StackForge project root."],
|
|
2779
|
+
config: await Promise.reject(new Error("Missing stackforge.json")),
|
|
2780
|
+
pkgPath,
|
|
2781
|
+
envPath,
|
|
2782
|
+
hasConfig: false,
|
|
2783
|
+
hasPackageJson: existsSync3(pkgPath)
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
if (!existsSync3(pkgPath)) {
|
|
2787
|
+
return {
|
|
2788
|
+
issues: ["Missing package.json"],
|
|
2789
|
+
config: await readProjectConfig(cwd),
|
|
2790
|
+
pkgPath,
|
|
2791
|
+
envPath,
|
|
2792
|
+
hasConfig: true,
|
|
2793
|
+
hasPackageJson: false
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
const config = await readProjectConfig(cwd);
|
|
2797
|
+
const pkg = JSON.parse(await readFile6(pkgPath, "utf8"));
|
|
2798
|
+
const requiredScripts = collectScripts(config);
|
|
2799
|
+
const requiredDeps = collectDependencies(config);
|
|
2800
|
+
for (const key of Object.keys(requiredScripts)) {
|
|
2801
|
+
if (!pkg.scripts || !(key in pkg.scripts)) {
|
|
2802
|
+
issues.push(`Missing script: ${key}`);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
for (const key of Object.keys(requiredDeps.dependencies)) {
|
|
2806
|
+
if (!pkg.dependencies || !(key in pkg.dependencies)) {
|
|
2807
|
+
issues.push(`Missing dependency: ${key}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
for (const key of Object.keys(requiredDeps.devDependencies)) {
|
|
2811
|
+
if (!pkg.devDependencies || !(key in pkg.devDependencies)) {
|
|
2812
|
+
issues.push(`Missing devDependency: ${key}`);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
let envContent = "";
|
|
2816
|
+
if (existsSync3(envPath)) {
|
|
2817
|
+
envContent = await readFile6(envPath, "utf8");
|
|
2818
|
+
}
|
|
2819
|
+
const envKeys = new Set(
|
|
2820
|
+
envContent.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0])
|
|
2821
|
+
);
|
|
2822
|
+
const requiredEnv = requiredEnvKeys(config);
|
|
2823
|
+
for (const key of requiredEnv) {
|
|
2824
|
+
if (!envKeys.has(key)) {
|
|
2825
|
+
issues.push(`Missing env key in .env.example: ${key}`);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
for (const agent of config.aiAgents) {
|
|
2829
|
+
const agentRoot = join16(cwd, ".ai-agents", agent);
|
|
2830
|
+
for (const file of agentFiles(agent)) {
|
|
2831
|
+
if (!existsSync3(join16(agentRoot, file))) {
|
|
2832
|
+
issues.push(`Missing AI agent file: .ai-agents/${agent}/${file}`);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
if (agent === "claude" && !existsSync3(join16(cwd, ".claude", "claude_desktop_config.json"))) {
|
|
2836
|
+
issues.push("Missing Claude config: .claude/claude_desktop_config.json");
|
|
2837
|
+
}
|
|
2838
|
+
if (agent === "codex" && !existsSync3(join16(cwd, ".codex", "functions.json"))) {
|
|
2839
|
+
issues.push("Missing Codex config: .codex/functions.json");
|
|
2840
|
+
}
|
|
2841
|
+
if (agent === "cursor" && !existsSync3(join16(cwd, ".cursor", "extensions.json"))) {
|
|
2842
|
+
issues.push("Missing Cursor config: .cursor/extensions.json");
|
|
2843
|
+
}
|
|
2844
|
+
if (agent === "windsurf" && !existsSync3(join16(cwd, ".windsurf", "cascade.json"))) {
|
|
2845
|
+
issues.push("Missing Windsurf config: .windsurf/cascade.json");
|
|
2846
|
+
}
|
|
2847
|
+
if (agent === "tabnine" && !existsSync3(join16(cwd, ".tabnine", "config.json"))) {
|
|
2848
|
+
issues.push("Missing Tabnine config: .tabnine/config.json");
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return {
|
|
2852
|
+
issues,
|
|
2853
|
+
config,
|
|
2854
|
+
pkgPath,
|
|
2855
|
+
envPath,
|
|
2856
|
+
hasConfig: true,
|
|
2857
|
+
hasPackageJson: true
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
async function fixProject(result) {
|
|
2861
|
+
const { config, pkgPath, envPath } = result;
|
|
2862
|
+
await syncPackageJson(pkgPath, config, config);
|
|
2863
|
+
const envContent = existsSync3(envPath) ? await readFile6(envPath, "utf8") : "";
|
|
2864
|
+
const envKeys = new Set(
|
|
2865
|
+
envContent.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0])
|
|
2866
|
+
);
|
|
2867
|
+
const missingEnv = requiredEnvKeys(config).filter((key) => !envKeys.has(key));
|
|
2868
|
+
for (const key of missingEnv) {
|
|
2869
|
+
await appendEnvLine(envPath, `${key}=""`);
|
|
2870
|
+
}
|
|
2871
|
+
if (config.aiAgents.length > 0) {
|
|
2872
|
+
const base = basename(process.cwd()) === config.projectName ? dirname4(process.cwd()) : process.cwd();
|
|
2873
|
+
await generateAiAgentConfigs(base, config);
|
|
2874
|
+
}
|
|
2875
|
+
const readme = buildProjectReadme(config);
|
|
2876
|
+
await writeTextFile(join16(process.cwd(), "README.md"), readme + "\n");
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// src/cli/commands/doctor.ts
|
|
2880
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2881
|
+
var doctorCommand = new Command6("doctor").option("--fix", "apply non-destructive fixes").action(async (options) => {
|
|
2882
|
+
const cwd = process.cwd();
|
|
2883
|
+
const configPath = join17(cwd, "stackforge.json");
|
|
2884
|
+
if (!existsSync4(configPath)) {
|
|
2885
|
+
logger.error("Missing stackforge.json. Run from a StackForge project root.");
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
let config;
|
|
2889
|
+
try {
|
|
2890
|
+
const raw = JSON.parse(await readFile7(configPath, "utf8"));
|
|
2891
|
+
const schemaVersion = raw._schemaVersion ?? 0;
|
|
2892
|
+
if (schemaVersion !== STACKFORGE_SCHEMA_VERSION) {
|
|
2893
|
+
logger.warn(`stackforge.json schema version ${schemaVersion} (expected ${STACKFORGE_SCHEMA_VERSION})`);
|
|
2894
|
+
} else {
|
|
2895
|
+
logger.info("stackforge.json OK");
|
|
2896
|
+
}
|
|
2897
|
+
config = await readProjectConfig(cwd);
|
|
2898
|
+
} catch (err) {
|
|
2899
|
+
logger.error("Failed to read stackforge.json");
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
const pkgPath = join17(cwd, "package.json");
|
|
2903
|
+
if (!existsSync4(pkgPath)) {
|
|
2904
|
+
logger.error("Missing package.json");
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const result = await checkProject(cwd);
|
|
2908
|
+
if (result.issues.length === 0) {
|
|
2909
|
+
logger.info("No issues found.");
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
for (const issue of result.issues) {
|
|
2913
|
+
logger.warn(issue);
|
|
2914
|
+
}
|
|
2915
|
+
if (options.fix) {
|
|
2916
|
+
await fixProject(result);
|
|
2917
|
+
logger.info("Applied fixes.");
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
// src/cli/commands/configure-agents.ts
|
|
2922
|
+
import { Command as Command7 } from "commander";
|
|
2923
|
+
var configureAgentsCommand = new Command7("configure-agents").option("--agents <list>", "comma-separated list of agents").action(async (options) => {
|
|
2924
|
+
const cwd = process.cwd();
|
|
2925
|
+
const config = await readProjectConfig(cwd);
|
|
2926
|
+
const agents = options.agents ? String(options.agents).split(",").map((a) => a.trim()).filter(Boolean) : config.aiAgents;
|
|
2927
|
+
const invalid = (agents ?? []).filter((a) => !supported.agents.includes(a));
|
|
2928
|
+
if (invalid.length) {
|
|
2929
|
+
throw new Error(`Unsupported agents: ${invalid.join(", ")}`);
|
|
2930
|
+
}
|
|
2931
|
+
const next = { ...config, aiAgents: agents };
|
|
2932
|
+
await writeProjectConfig(cwd, next);
|
|
2933
|
+
await generateAiAgentConfigs(cwd, next, { dryRun: false, log: logger.info });
|
|
2934
|
+
logger.info("AI agents configured.");
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
// src/cli/commands/add-agent.ts
|
|
2938
|
+
import { Command as Command8 } from "commander";
|
|
2939
|
+
var addAgentCommand = new Command8("add-agent").argument("<agent>", "agent to add").action(async (agent) => {
|
|
2940
|
+
if (!supported.agents.includes(agent)) {
|
|
2941
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
2942
|
+
}
|
|
2943
|
+
const cwd = process.cwd();
|
|
2944
|
+
const config = await readProjectConfig(cwd);
|
|
2945
|
+
const set = new Set(config.aiAgents ?? []);
|
|
2946
|
+
set.add(agent);
|
|
2947
|
+
const next = { ...config, aiAgents: Array.from(set) };
|
|
2948
|
+
await writeProjectConfig(cwd, next);
|
|
2949
|
+
await generateAiAgentConfigs(cwd, next, { dryRun: false, log: logger.info });
|
|
2950
|
+
logger.info(`Added agent: ${agent}`);
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
// src/cli/commands/remove-agent.ts
|
|
2954
|
+
import { Command as Command9 } from "commander";
|
|
2955
|
+
var removeAgentCommand = new Command9("remove-agent").argument("<agent>", "agent to remove").action(async (agent) => {
|
|
2956
|
+
if (!supported.agents.includes(agent)) {
|
|
2957
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
2958
|
+
}
|
|
2959
|
+
const cwd = process.cwd();
|
|
2960
|
+
const config = await readProjectConfig(cwd);
|
|
2961
|
+
const next = { ...config, aiAgents: (config.aiAgents ?? []).filter((a) => a !== agent) };
|
|
2962
|
+
await writeProjectConfig(cwd, next);
|
|
2963
|
+
await generateAiAgentConfigs(cwd, next, { dryRun: false, log: logger.info });
|
|
2964
|
+
logger.info(`Removed agent: ${agent}`);
|
|
2965
|
+
});
|
|
2966
|
+
|
|
2967
|
+
// src/cli/commands/list-agents.ts
|
|
2968
|
+
import { Command as Command10 } from "commander";
|
|
2969
|
+
var listAgentsCommand = new Command10("list-agents").action(async () => {
|
|
2970
|
+
const config = await readProjectConfig(process.cwd());
|
|
2971
|
+
const agents = config.aiAgents ?? [];
|
|
2972
|
+
if (agents.length === 0) {
|
|
2973
|
+
logger.info("No agents configured.");
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
logger.info(`Configured agents: ${agents.join(", ")}`);
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
// src/cli/commands/migrate.ts
|
|
2980
|
+
import { Command as Command11 } from "commander";
|
|
2981
|
+
import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
2982
|
+
import { join as join18 } from "path";
|
|
2983
|
+
var migrateCommand = new Command11("migrate").option("--dry-run", "show planned migration without writing").action(async (options) => {
|
|
2984
|
+
const cwd = process.cwd();
|
|
2985
|
+
const path = join18(cwd, "stackforge.json");
|
|
2986
|
+
const raw = await readFile8(path, "utf8");
|
|
2987
|
+
const parsed = JSON.parse(raw);
|
|
2988
|
+
const current = parsed._schemaVersion ?? 0;
|
|
2989
|
+
if (current === STACKFORGE_SCHEMA_VERSION) {
|
|
2990
|
+
logger.info("No migration needed.");
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
logger.info(`Migrating schema ${current} -> ${STACKFORGE_SCHEMA_VERSION}`);
|
|
2994
|
+
const migrated = migrateConfig(parsed);
|
|
2995
|
+
if (!options.dryRun) {
|
|
2996
|
+
await writeFile5(path, JSON.stringify(migrated, null, 2) + "\n", "utf8");
|
|
2997
|
+
logger.info("Migration complete.");
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
// src/cli/commands/list-presets.ts
|
|
3002
|
+
import { Command as Command12 } from "commander";
|
|
3003
|
+
var listPresetsCommand = new Command12("list-presets").option("--details", "show preset details").action((options) => {
|
|
3004
|
+
const presets2 = ["starter", "saas", "ecommerce", "blog", "api"];
|
|
3005
|
+
for (const name of presets2) {
|
|
3006
|
+
const preset = getPreset(name);
|
|
3007
|
+
if (!preset) {
|
|
3008
|
+
logger.info(`${name}: missing`);
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
if (options.details) {
|
|
3012
|
+
const features = preset.features && preset.features.length ? preset.features.join(", ") : "none";
|
|
3013
|
+
logger.info(`${name}: frontend=${preset.frontend?.type ?? "n/a"} ui=${preset.ui?.library ?? "n/a"} db=${preset.database?.provider ?? "n/a"} orm=${preset.database?.orm ?? "n/a"} auth=${preset.auth?.provider ?? "n/a"} api=${preset.api?.type ?? "n/a"} features=${features}`);
|
|
3014
|
+
} else {
|
|
3015
|
+
logger.info(`${name}: available`);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
|
|
3020
|
+
// src/cli/commands/use.ts
|
|
3021
|
+
import { Command as Command13 } from "commander";
|
|
3022
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
3023
|
+
var useCommand = new Command13("use").argument("<packageManager>", "npm | pnpm | yarn | bun").option("--no-install", "skip installing dependencies after switching").action(async (packageManager, options) => {
|
|
3024
|
+
if (!PACKAGE_MANAGERS.includes(packageManager)) {
|
|
3025
|
+
throw new Error(`Unsupported package manager: ${packageManager}`);
|
|
3026
|
+
}
|
|
3027
|
+
const cwd = process.cwd();
|
|
3028
|
+
const config = await readProjectConfig(cwd);
|
|
3029
|
+
const nextManager = packageManager;
|
|
3030
|
+
if (config.packageManager === nextManager) {
|
|
3031
|
+
logger.info(`Package manager already set to ${nextManager}.`);
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
await removeLockfiles(cwd);
|
|
3035
|
+
await writeProjectConfig(cwd, { ...config, packageManager: nextManager });
|
|
3036
|
+
if (options.install !== false) {
|
|
3037
|
+
await runInstall(nextManager, cwd);
|
|
3038
|
+
}
|
|
3039
|
+
logger.info(`Package manager switched to ${nextManager}.`);
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
// src/cli/commands/validate.ts
|
|
3043
|
+
import { Command as Command14 } from "commander";
|
|
3044
|
+
var validateCommand = new Command14("validate").description("validate project configuration and generated files").action(async () => {
|
|
3045
|
+
const result = await checkProject(process.cwd());
|
|
3046
|
+
if (result.issues.length === 0) {
|
|
3047
|
+
logger.info("Validation passed.");
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
for (const issue of result.issues) {
|
|
3051
|
+
logger.error(issue);
|
|
3052
|
+
}
|
|
3053
|
+
process.exitCode = 1;
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
// src/cli/commands/fix.ts
|
|
3057
|
+
import { Command as Command15 } from "commander";
|
|
3058
|
+
var fixCommand = new Command15("fix").description("apply safe fixes to project configuration").action(async () => {
|
|
3059
|
+
const result = await checkProject(process.cwd());
|
|
3060
|
+
if (result.issues.length === 0) {
|
|
3061
|
+
logger.info("No issues found.");
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
await fixProject(result);
|
|
3065
|
+
logger.info("Applied fixes.");
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
// src/cli/commands/upgrade.ts
|
|
3069
|
+
import { Command as Command16 } from "commander";
|
|
3070
|
+
import { join as join19, dirname as dirname5 } from "path";
|
|
3071
|
+
var upgradeCommand = new Command16("upgrade").option("--preset <name>", "preset to upgrade to").action(async (options) => {
|
|
3072
|
+
const cwd = process.cwd();
|
|
3073
|
+
const current = await readProjectConfig(cwd);
|
|
3074
|
+
const preset = getPreset(options.preset);
|
|
3075
|
+
if (!preset) {
|
|
3076
|
+
throw new Error("Unknown preset. Use stackforge list-presets.");
|
|
3077
|
+
}
|
|
3078
|
+
const next = {
|
|
3079
|
+
...current,
|
|
3080
|
+
...preset,
|
|
3081
|
+
preset: options.preset,
|
|
3082
|
+
projectName: current.projectName,
|
|
3083
|
+
packageManager: current.packageManager,
|
|
3084
|
+
aiAgents: current.aiAgents
|
|
3085
|
+
};
|
|
3086
|
+
if (current.frontend.type !== next.frontend.type || current.frontend.language !== next.frontend.language) {
|
|
3087
|
+
throw new Error("Preset upgrade does not support changing frontend type or language yet.");
|
|
3088
|
+
}
|
|
3089
|
+
validateConfig(next);
|
|
3090
|
+
validateCompatibility(next);
|
|
3091
|
+
validateDependencies(next);
|
|
3092
|
+
if (current.ui.library !== next.ui.library) {
|
|
3093
|
+
await cleanupFeature(cwd, current, "ui", current.ui.library);
|
|
3094
|
+
}
|
|
3095
|
+
if (current.database.provider !== next.database.provider) {
|
|
3096
|
+
await cleanupFeature(cwd, current, "database", current.database.provider);
|
|
3097
|
+
}
|
|
3098
|
+
if (current.database.orm !== next.database.orm) {
|
|
3099
|
+
await cleanupFeature(cwd, current, "orm", current.database.orm);
|
|
3100
|
+
}
|
|
3101
|
+
if (current.auth.provider !== next.auth.provider) {
|
|
3102
|
+
await cleanupFeature(cwd, current, "auth", current.auth.provider);
|
|
3103
|
+
}
|
|
3104
|
+
if (current.api.type !== next.api.type) {
|
|
3105
|
+
await cleanupFeature(cwd, current, "api", current.api.type);
|
|
3106
|
+
}
|
|
3107
|
+
const removedFeatures = current.features.filter((f) => !next.features.includes(f));
|
|
3108
|
+
for (const feature of removedFeatures) {
|
|
3109
|
+
await cleanupFeature(cwd, current, "feature", feature);
|
|
3110
|
+
}
|
|
3111
|
+
await writeProjectConfig(cwd, next);
|
|
3112
|
+
await syncPackageJson(join19(cwd, "package.json"), current, next);
|
|
3113
|
+
const root = dirname5(cwd);
|
|
3114
|
+
await generateFrontendFiles(root, next);
|
|
3115
|
+
await generateUiFiles(root, next);
|
|
3116
|
+
await generateDatabaseFiles(root, next);
|
|
3117
|
+
await generateAuthFiles(root, next);
|
|
3118
|
+
await generateApiFiles(root, next);
|
|
3119
|
+
await generateFeatureFiles(root, next);
|
|
3120
|
+
const readme = buildProjectReadme(next);
|
|
3121
|
+
await writeTextFile(join19(cwd, "README.md"), readme + "\n");
|
|
3122
|
+
logger.info(`Upgraded project to preset: ${options.preset}`);
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
// src/cli.ts
|
|
3126
|
+
var program = new Command17();
|
|
3127
|
+
program.name("create-stackforge").description("Universal full-stack boilerplate generator").version("0.0.0");
|
|
3128
|
+
program.addCommand(createCommand);
|
|
3129
|
+
program.addCommand(listCommand);
|
|
3130
|
+
program.addCommand(addCommand);
|
|
3131
|
+
program.addCommand(removeCommand);
|
|
3132
|
+
program.addCommand(updateCommand);
|
|
3133
|
+
program.addCommand(doctorCommand);
|
|
3134
|
+
program.addCommand(configureAgentsCommand);
|
|
3135
|
+
program.addCommand(addAgentCommand);
|
|
3136
|
+
program.addCommand(removeAgentCommand);
|
|
3137
|
+
program.addCommand(listAgentsCommand);
|
|
3138
|
+
program.addCommand(migrateCommand);
|
|
3139
|
+
program.addCommand(listPresetsCommand);
|
|
3140
|
+
program.addCommand(useCommand);
|
|
3141
|
+
program.addCommand(validateCommand);
|
|
3142
|
+
program.addCommand(fixCommand);
|
|
3143
|
+
program.addCommand(upgradeCommand);
|
|
3144
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
3145
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
3146
|
+
process.exitCode = 1;
|
|
3147
|
+
});
|
|
3148
|
+
//# sourceMappingURL=cli.js.map
|