@stackweld/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +498 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
- package/dist/__tests__/compatibility-scorer.test.js +226 -0
- package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
- package/dist/__tests__/rules-engine.test.d.ts +2 -0
- package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
- package/dist/__tests__/rules-engine.test.js +161 -0
- package/dist/__tests__/rules-engine.test.js.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
- package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
- package/dist/__tests__/stack-engine.test.d.ts +2 -0
- package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
- package/dist/__tests__/stack-engine.test.js +278 -0
- package/dist/__tests__/stack-engine.test.js.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +106 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/engine/compatibility-scorer.d.ts +37 -0
- package/dist/engine/compatibility-scorer.d.ts.map +1 -0
- package/dist/engine/compatibility-scorer.js +178 -0
- package/dist/engine/compatibility-scorer.js.map +1 -0
- package/dist/engine/compose-generator.d.ts +35 -0
- package/dist/engine/compose-generator.d.ts.map +1 -0
- package/dist/engine/compose-generator.js +95 -0
- package/dist/engine/compose-generator.js.map +1 -0
- package/dist/engine/cost-estimator.d.ts +22 -0
- package/dist/engine/cost-estimator.d.ts.map +1 -0
- package/dist/engine/cost-estimator.js +451 -0
- package/dist/engine/cost-estimator.js.map +1 -0
- package/dist/engine/env-analyzer.d.ts +36 -0
- package/dist/engine/env-analyzer.d.ts.map +1 -0
- package/dist/engine/env-analyzer.js +111 -0
- package/dist/engine/env-analyzer.js.map +1 -0
- package/dist/engine/health-checker.d.ts +20 -0
- package/dist/engine/health-checker.d.ts.map +1 -0
- package/dist/engine/health-checker.js +377 -0
- package/dist/engine/health-checker.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/infra-generator.d.ts +26 -0
- package/dist/engine/infra-generator.d.ts.map +1 -0
- package/dist/engine/infra-generator.js +751 -0
- package/dist/engine/infra-generator.js.map +1 -0
- package/dist/engine/migration-planner.d.ts +34 -0
- package/dist/engine/migration-planner.d.ts.map +1 -0
- package/dist/engine/migration-planner.js +427 -0
- package/dist/engine/migration-planner.js.map +1 -0
- package/dist/engine/performance-profiler.d.ts +22 -0
- package/dist/engine/performance-profiler.d.ts.map +1 -0
- package/dist/engine/performance-profiler.js +292 -0
- package/dist/engine/performance-profiler.js.map +1 -0
- package/dist/engine/plugin-loader.d.ts +36 -0
- package/dist/engine/plugin-loader.d.ts.map +1 -0
- package/dist/engine/plugin-loader.js +157 -0
- package/dist/engine/plugin-loader.js.map +1 -0
- package/dist/engine/preferences.d.ts +24 -0
- package/dist/engine/preferences.d.ts.map +1 -0
- package/dist/engine/preferences.js +62 -0
- package/dist/engine/preferences.js.map +1 -0
- package/dist/engine/rules-engine.d.ts +31 -0
- package/dist/engine/rules-engine.d.ts.map +1 -0
- package/dist/engine/rules-engine.js +179 -0
- package/dist/engine/rules-engine.js.map +1 -0
- package/dist/engine/runtime-manager.d.ts +65 -0
- package/dist/engine/runtime-manager.d.ts.map +1 -0
- package/dist/engine/runtime-manager.js +181 -0
- package/dist/engine/runtime-manager.js.map +1 -0
- package/dist/engine/scaffold-orchestrator.d.ts +103 -0
- package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
- package/dist/engine/scaffold-orchestrator.js +934 -0
- package/dist/engine/scaffold-orchestrator.js.map +1 -0
- package/dist/engine/stack-detector.d.ts +21 -0
- package/dist/engine/stack-detector.d.ts.map +1 -0
- package/dist/engine/stack-detector.js +313 -0
- package/dist/engine/stack-detector.js.map +1 -0
- package/dist/engine/stack-differ.d.ts +26 -0
- package/dist/engine/stack-differ.d.ts.map +1 -0
- package/dist/engine/stack-differ.js +80 -0
- package/dist/engine/stack-differ.js.map +1 -0
- package/dist/engine/stack-engine.d.ts +54 -0
- package/dist/engine/stack-engine.d.ts.map +1 -0
- package/dist/engine/stack-engine.js +186 -0
- package/dist/engine/stack-engine.js.map +1 -0
- package/dist/engine/stack-serializer.d.ts +32 -0
- package/dist/engine/stack-serializer.d.ts.map +1 -0
- package/dist/engine/stack-serializer.js +75 -0
- package/dist/engine/stack-serializer.js.map +1 -0
- package/dist/engine/standards-linter.d.ts +34 -0
- package/dist/engine/standards-linter.d.ts.map +1 -0
- package/dist/engine/standards-linter.js +162 -0
- package/dist/engine/standards-linter.js.map +1 -0
- package/dist/engine/tech-installer.d.ts +37 -0
- package/dist/engine/tech-installer.d.ts.map +1 -0
- package/dist/engine/tech-installer.js +508 -0
- package/dist/engine/tech-installer.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +33 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +6 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/stack.d.ts +29 -0
- package/dist/types/stack.d.ts.map +1 -0
- package/dist/types/stack.js +6 -0
- package/dist/types/stack.js.map +1 -0
- package/dist/types/technology.d.ts +47 -0
- package/dist/types/technology.d.ts.map +1 -0
- package/dist/types/technology.js +6 -0
- package/dist/types/technology.js.map +1 -0
- package/dist/types/template.d.ts +34 -0
- package/dist/types/template.d.ts.map +1 -0
- package/dist/types/template.js +6 -0
- package/dist/types/template.js.map +1 -0
- package/dist/types/validation.d.ts +20 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/__tests__/compatibility-scorer.test.ts +264 -0
- package/src/__tests__/rules-engine.test.ts +170 -0
- package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
- package/src/__tests__/stack-engine.test.ts +328 -0
- package/src/db/database.ts +112 -0
- package/src/db/index.ts +1 -0
- package/src/engine/compatibility-scorer.ts +222 -0
- package/src/engine/compose-generator.ts +134 -0
- package/src/engine/cost-estimator.ts +498 -0
- package/src/engine/env-analyzer.ts +156 -0
- package/src/engine/health-checker.ts +421 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/infra-generator.ts +837 -0
- package/src/engine/migration-planner.ts +496 -0
- package/src/engine/performance-profiler.ts +354 -0
- package/src/engine/plugin-loader.ts +216 -0
- package/src/engine/preferences.ts +85 -0
- package/src/engine/rules-engine.ts +204 -0
- package/src/engine/runtime-manager.ts +207 -0
- package/src/engine/scaffold-orchestrator.ts +1052 -0
- package/src/engine/stack-detector.ts +345 -0
- package/src/engine/stack-differ.ts +118 -0
- package/src/engine/stack-engine.ts +258 -0
- package/src/engine/stack-serializer.ts +95 -0
- package/src/engine/standards-linter.ts +210 -0
- package/src/engine/tech-installer.ts +650 -0
- package/src/index.ts +78 -0
- package/src/types/index.ts +10 -0
- package/src/types/project.ts +36 -0
- package/src/types/stack.ts +32 -0
- package/src/types/technology.ts +58 -0
- package/src/types/template.ts +37 -0
- package/src/types/validation.ts +22 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TechInstaller — Knows how to install and configure EVERY technology
|
|
3
|
+
* in a project directory. Each category has specific installation logic.
|
|
4
|
+
*
|
|
5
|
+
* Categories handled:
|
|
6
|
+
* - database: Docker service + connection env vars
|
|
7
|
+
* - orm: Initialize schema + connect to selected DB
|
|
8
|
+
* - auth: Install package + config files + env vars
|
|
9
|
+
* - styling: Install + configure (Tailwind, shadcn, etc.)
|
|
10
|
+
* - devops: Create config files (biome, vitest, playwright, etc.)
|
|
11
|
+
* - service: Docker service entry
|
|
12
|
+
*
|
|
13
|
+
* Frontend and Backend scaffolding is handled by the generate command directly.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { Technology } from "../types/index.js";
|
|
20
|
+
|
|
21
|
+
// ─── Types ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface InstallContext {
|
|
24
|
+
projectDir: string; // Root project directory
|
|
25
|
+
frontendDir: string | null; // frontend/ subdir (null if single project)
|
|
26
|
+
backendDir: string | null; // backend/ subdir (null if single project)
|
|
27
|
+
projectName: string;
|
|
28
|
+
isFullStack: boolean;
|
|
29
|
+
allTechs: Technology[];
|
|
30
|
+
runtime: "node" | "python" | "go" | "rust" | "bun" | "deno" | "php" | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InstallResult {
|
|
34
|
+
techId: string;
|
|
35
|
+
success: boolean;
|
|
36
|
+
message: string;
|
|
37
|
+
filesCreated: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Helpers ────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function run(cmd: string, cwd: string, timeout = 60_000): boolean {
|
|
43
|
+
try {
|
|
44
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function write(filePath: string, content: string): void {
|
|
52
|
+
const dir = path.dirname(filePath);
|
|
53
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasTech(ctx: InstallContext, id: string): boolean {
|
|
58
|
+
return ctx.allTechs.some((t) => t.id === id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getNodeDir(ctx: InstallContext): string {
|
|
62
|
+
return ctx.frontendDir || ctx.projectDir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getPythonDir(ctx: InstallContext): string {
|
|
66
|
+
return ctx.backendDir || ctx.projectDir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getDbUrl(ctx: InstallContext): string {
|
|
70
|
+
if (hasTech(ctx, "postgresql"))
|
|
71
|
+
return `postgresql://postgres:postgres@localhost:5432/${ctx.projectName}`;
|
|
72
|
+
if (hasTech(ctx, "mysql") || hasTech(ctx, "mariadb"))
|
|
73
|
+
return `mysql://root:root@localhost:3306/${ctx.projectName}`;
|
|
74
|
+
if (hasTech(ctx, "mongodb"))
|
|
75
|
+
return `mongodb://admin:admin@localhost:27017/${ctx.projectName}?authSource=admin`;
|
|
76
|
+
if (hasTech(ctx, "sqlite")) return "file:./prisma/dev.db";
|
|
77
|
+
if (hasTech(ctx, "turso")) return "libsql://your-db.turso.io";
|
|
78
|
+
if (hasTech(ctx, "neon")) return "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb";
|
|
79
|
+
return "file:./dev.db";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── ORM Installers ─────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function installPrisma(ctx: InstallContext): InstallResult {
|
|
85
|
+
const dir = getNodeDir(ctx);
|
|
86
|
+
const files: string[] = [];
|
|
87
|
+
const dbUrl = getDbUrl(ctx);
|
|
88
|
+
|
|
89
|
+
const provider = hasTech(ctx, "postgresql")
|
|
90
|
+
? "postgresql"
|
|
91
|
+
: hasTech(ctx, "mysql") || hasTech(ctx, "mariadb")
|
|
92
|
+
? "mysql"
|
|
93
|
+
: hasTech(ctx, "mongodb")
|
|
94
|
+
? "mongodb"
|
|
95
|
+
: "sqlite";
|
|
96
|
+
|
|
97
|
+
run(`npm install prisma @prisma/client`, dir);
|
|
98
|
+
run(`npx prisma init --datasource-provider ${provider}`, dir);
|
|
99
|
+
|
|
100
|
+
// Update schema if it was created
|
|
101
|
+
const schemaPath = path.join(dir, "prisma/schema.prisma");
|
|
102
|
+
if (fs.existsSync(schemaPath)) {
|
|
103
|
+
let schema = fs.readFileSync(schemaPath, "utf-8");
|
|
104
|
+
// Add a sample model
|
|
105
|
+
if (!schema.includes("model User")) {
|
|
106
|
+
schema += `\nmodel User {\n id String @id @default(cuid())\n email String @unique\n name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n`;
|
|
107
|
+
fs.writeFileSync(schemaPath, schema);
|
|
108
|
+
}
|
|
109
|
+
files.push("prisma/schema.prisma");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add DATABASE_URL to .env.example
|
|
113
|
+
const envPath = path.join(dir, ".env.example");
|
|
114
|
+
if (fs.existsSync(envPath)) {
|
|
115
|
+
let env = fs.readFileSync(envPath, "utf-8");
|
|
116
|
+
if (!env.includes("DATABASE_URL")) {
|
|
117
|
+
env += `\n# Prisma\nDATABASE_URL="${dbUrl}"\n`;
|
|
118
|
+
fs.writeFileSync(envPath, env);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
techId: "prisma",
|
|
124
|
+
success: true,
|
|
125
|
+
message: `Prisma initialized with ${provider}`,
|
|
126
|
+
filesCreated: files,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function installDrizzle(ctx: InstallContext): InstallResult {
|
|
131
|
+
const dir = getNodeDir(ctx);
|
|
132
|
+
const provider = hasTech(ctx, "postgresql")
|
|
133
|
+
? "pg"
|
|
134
|
+
: hasTech(ctx, "mysql")
|
|
135
|
+
? "mysql2"
|
|
136
|
+
: "better-sqlite3";
|
|
137
|
+
const drizzleDialect = hasTech(ctx, "postgresql")
|
|
138
|
+
? "postgresql"
|
|
139
|
+
: hasTech(ctx, "mysql")
|
|
140
|
+
? "mysql"
|
|
141
|
+
: "sqlite";
|
|
142
|
+
|
|
143
|
+
run(`npm install drizzle-orm ${provider}`, dir);
|
|
144
|
+
run(`npm install -D drizzle-kit`, dir);
|
|
145
|
+
|
|
146
|
+
write(
|
|
147
|
+
path.join(dir, "drizzle.config.ts"),
|
|
148
|
+
`${[
|
|
149
|
+
`import { defineConfig } from "drizzle-kit";`,
|
|
150
|
+
``,
|
|
151
|
+
`export default defineConfig({`,
|
|
152
|
+
` dialect: "${drizzleDialect}",`,
|
|
153
|
+
` schema: "./src/db/schema.ts",`,
|
|
154
|
+
` out: "./drizzle",`,
|
|
155
|
+
` dbCredentials: {`,
|
|
156
|
+
drizzleDialect === "sqlite" ? ` url: "./dev.db",` : ` url: process.env.DATABASE_URL!,`,
|
|
157
|
+
` },`,
|
|
158
|
+
`});`,
|
|
159
|
+
].join("\n")}\n`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
write(
|
|
163
|
+
path.join(dir, "src/db/schema.ts"),
|
|
164
|
+
`${[
|
|
165
|
+
`import { ${drizzleDialect === "postgresql" ? "pgTable, serial, text, timestamp" : drizzleDialect === "mysql" ? "mysqlTable, serial, text, timestamp" : "sqliteTable, integer, text"} } from "drizzle-orm/${drizzleDialect === "postgresql" ? "pg-core" : drizzleDialect === "mysql" ? "mysql-core" : "sqlite-core"}";`,
|
|
166
|
+
``,
|
|
167
|
+
`export const users = ${drizzleDialect === "postgresql" ? "pgTable" : drizzleDialect === "mysql" ? "mysqlTable" : "sqliteTable"}("users", {`,
|
|
168
|
+
drizzleDialect === "sqlite"
|
|
169
|
+
? ` id: integer("id").primaryKey({ autoIncrement: true }),\n email: text("email").notNull().unique(),\n name: text("name"),`
|
|
170
|
+
: ` id: serial("id").primaryKey(),\n email: text("email").notNull().unique(),\n name: text("name"),\n createdAt: timestamp("created_at").defaultNow(),`,
|
|
171
|
+
`});`,
|
|
172
|
+
].join("\n")}\n`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
techId: "drizzle",
|
|
177
|
+
success: true,
|
|
178
|
+
message: `Drizzle initialized with ${drizzleDialect}`,
|
|
179
|
+
filesCreated: ["drizzle.config.ts", "src/db/schema.ts"],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function installTypeorm(ctx: InstallContext): InstallResult {
|
|
184
|
+
const dir = getNodeDir(ctx);
|
|
185
|
+
run(`npm install typeorm reflect-metadata`, dir);
|
|
186
|
+
|
|
187
|
+
const dbType = hasTech(ctx, "postgresql")
|
|
188
|
+
? "postgres"
|
|
189
|
+
: hasTech(ctx, "mysql")
|
|
190
|
+
? "mysql"
|
|
191
|
+
: "sqlite";
|
|
192
|
+
|
|
193
|
+
write(
|
|
194
|
+
path.join(dir, "src/data-source.ts"),
|
|
195
|
+
`${[
|
|
196
|
+
`import "reflect-metadata";`,
|
|
197
|
+
`import { DataSource } from "typeorm";`,
|
|
198
|
+
``,
|
|
199
|
+
`export const AppDataSource = new DataSource({`,
|
|
200
|
+
` type: "${dbType}",`,
|
|
201
|
+
dbType === "sqlite" ? ` database: "./dev.db",` : ` url: process.env.DATABASE_URL,`,
|
|
202
|
+
` synchronize: true,`,
|
|
203
|
+
` logging: false,`,
|
|
204
|
+
` entities: ["src/entities/**/*.ts"],`,
|
|
205
|
+
` migrations: ["src/migrations/**/*.ts"],`,
|
|
206
|
+
`});`,
|
|
207
|
+
].join("\n")}\n`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
techId: "typeorm",
|
|
212
|
+
success: true,
|
|
213
|
+
message: `TypeORM initialized with ${dbType}`,
|
|
214
|
+
filesCreated: ["src/data-source.ts"],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function installSqlalchemy(ctx: InstallContext): InstallResult {
|
|
219
|
+
const dir = getPythonDir(ctx);
|
|
220
|
+
const pip = path.join(dir, ".venv/bin/pip");
|
|
221
|
+
if (fs.existsSync(pip)) {
|
|
222
|
+
run(`${pip} install sqlalchemy alembic`, dir);
|
|
223
|
+
run(`${path.join(dir, ".venv/bin/alembic")} init alembic`, dir);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
techId: "sqlalchemy",
|
|
227
|
+
success: true,
|
|
228
|
+
message: "SQLAlchemy + Alembic initialized",
|
|
229
|
+
filesCreated: ["alembic/", "alembic.ini"],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function installMongoose(ctx: InstallContext): InstallResult {
|
|
234
|
+
const dir = getNodeDir(ctx);
|
|
235
|
+
run(`npm install mongoose`, dir);
|
|
236
|
+
|
|
237
|
+
write(
|
|
238
|
+
path.join(dir, "src/db/connection.ts"),
|
|
239
|
+
`${[
|
|
240
|
+
`import mongoose from "mongoose";`,
|
|
241
|
+
``,
|
|
242
|
+
`export async function connectDB() {`,
|
|
243
|
+
` const url = process.env.MONGODB_URL || "mongodb://localhost:27017/${ctx.projectName}";`,
|
|
244
|
+
` await mongoose.connect(url);`,
|
|
245
|
+
` console.log("MongoDB connected");`,
|
|
246
|
+
`}`,
|
|
247
|
+
].join("\n")}\n`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
techId: "mongoose",
|
|
252
|
+
success: true,
|
|
253
|
+
message: "Mongoose configured",
|
|
254
|
+
filesCreated: ["src/db/connection.ts"],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── Auth Installers ────────────────────────────────
|
|
259
|
+
|
|
260
|
+
function installNextAuth(ctx: InstallContext): InstallResult {
|
|
261
|
+
const dir = getNodeDir(ctx);
|
|
262
|
+
run(`npm install next-auth@beta`, dir);
|
|
263
|
+
|
|
264
|
+
write(
|
|
265
|
+
path.join(dir, "src/auth.ts"),
|
|
266
|
+
`${[
|
|
267
|
+
`import NextAuth from "next-auth";`,
|
|
268
|
+
``,
|
|
269
|
+
`export const { handlers, signIn, signOut, auth } = NextAuth({`,
|
|
270
|
+
` providers: [],`,
|
|
271
|
+
`});`,
|
|
272
|
+
].join("\n")}\n`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
write(
|
|
276
|
+
path.join(dir, "src/app/api/auth/[...nextauth]/route.ts"),
|
|
277
|
+
`${[`import { handlers } from "@/auth";`, `export const { GET, POST } = handlers;`].join(
|
|
278
|
+
"\n",
|
|
279
|
+
)}\n`,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
techId: "nextauth",
|
|
284
|
+
success: true,
|
|
285
|
+
message: "NextAuth.js configured",
|
|
286
|
+
filesCreated: ["src/auth.ts", "src/app/api/auth/[...nextauth]/route.ts"],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function installClerk(ctx: InstallContext): InstallResult {
|
|
291
|
+
const dir = getNodeDir(ctx);
|
|
292
|
+
run(`npm install @clerk/nextjs`, dir);
|
|
293
|
+
|
|
294
|
+
write(
|
|
295
|
+
path.join(dir, "src/middleware.ts"),
|
|
296
|
+
`${[
|
|
297
|
+
`import { clerkMiddleware } from "@clerk/nextjs/server";`,
|
|
298
|
+
`export default clerkMiddleware();`,
|
|
299
|
+
`export const config = { matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"] };`,
|
|
300
|
+
].join("\n")}\n`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
techId: "clerk",
|
|
305
|
+
success: true,
|
|
306
|
+
message: "Clerk configured",
|
|
307
|
+
filesCreated: ["src/middleware.ts"],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function installLucia(ctx: InstallContext): InstallResult {
|
|
312
|
+
const dir = getNodeDir(ctx);
|
|
313
|
+
run(`npm install lucia`, dir);
|
|
314
|
+
return { techId: "lucia", success: true, message: "Lucia installed", filesCreated: [] };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function installSupabaseAuth(ctx: InstallContext): InstallResult {
|
|
318
|
+
const dir = getNodeDir(ctx);
|
|
319
|
+
run(`npm install @supabase/supabase-js`, dir);
|
|
320
|
+
|
|
321
|
+
write(
|
|
322
|
+
path.join(dir, "src/lib/supabase.ts"),
|
|
323
|
+
`${[
|
|
324
|
+
`import { createClient } from "@supabase/supabase-js";`,
|
|
325
|
+
``,
|
|
326
|
+
`export const supabase = createClient(`,
|
|
327
|
+
` process.env.NEXT_PUBLIC_SUPABASE_URL!,`,
|
|
328
|
+
` process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,`,
|
|
329
|
+
`);`,
|
|
330
|
+
].join("\n")}\n`,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
techId: "supabase-auth",
|
|
335
|
+
success: true,
|
|
336
|
+
message: "Supabase Auth configured",
|
|
337
|
+
filesCreated: ["src/lib/supabase.ts"],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Styling Installers ─────────────────────────────
|
|
342
|
+
|
|
343
|
+
function installTailwind(ctx: InstallContext): InstallResult {
|
|
344
|
+
const dir = getNodeDir(ctx);
|
|
345
|
+
// Only install if not already present (create-next-app --tailwind already does it)
|
|
346
|
+
const pkgPath = path.join(dir, "package.json");
|
|
347
|
+
if (fs.existsSync(pkgPath)) {
|
|
348
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
349
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
350
|
+
if (!allDeps.tailwindcss) {
|
|
351
|
+
run(`npm install -D tailwindcss @tailwindcss/vite`, dir);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
techId: "tailwindcss",
|
|
356
|
+
success: true,
|
|
357
|
+
message: "Tailwind CSS configured",
|
|
358
|
+
filesCreated: [],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function installShadcn(ctx: InstallContext): InstallResult {
|
|
363
|
+
const dir = getNodeDir(ctx);
|
|
364
|
+
run(`npx shadcn@latest init -y`, dir);
|
|
365
|
+
return {
|
|
366
|
+
techId: "shadcn-ui",
|
|
367
|
+
success: true,
|
|
368
|
+
message: "shadcn/ui initialized",
|
|
369
|
+
filesCreated: ["components.json"],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function installDaisyUI(ctx: InstallContext): InstallResult {
|
|
374
|
+
const dir = getNodeDir(ctx);
|
|
375
|
+
run(`npm install -D daisyui`, dir);
|
|
376
|
+
return { techId: "daisyui", success: true, message: "DaisyUI installed", filesCreated: [] };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function installChakraUI(ctx: InstallContext): InstallResult {
|
|
380
|
+
const dir = getNodeDir(ctx);
|
|
381
|
+
run(`npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion`, dir);
|
|
382
|
+
return { techId: "chakra-ui", success: true, message: "Chakra UI installed", filesCreated: [] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function installMaterialUI(ctx: InstallContext): InstallResult {
|
|
386
|
+
const dir = getNodeDir(ctx);
|
|
387
|
+
run(`npm install @mui/material @emotion/react @emotion/styled`, dir);
|
|
388
|
+
return {
|
|
389
|
+
techId: "material-ui",
|
|
390
|
+
success: true,
|
|
391
|
+
message: "Material UI installed",
|
|
392
|
+
filesCreated: [],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── DevOps Installers ──────────────────────────────
|
|
397
|
+
|
|
398
|
+
function installVitest(ctx: InstallContext): InstallResult {
|
|
399
|
+
const dir = getNodeDir(ctx);
|
|
400
|
+
run(`npm install -D vitest`, dir);
|
|
401
|
+
|
|
402
|
+
write(
|
|
403
|
+
path.join(dir, "vitest.config.ts"),
|
|
404
|
+
`${[
|
|
405
|
+
`import { defineConfig } from "vitest/config";`,
|
|
406
|
+
``,
|
|
407
|
+
`export default defineConfig({`,
|
|
408
|
+
` test: {`,
|
|
409
|
+
` globals: true,`,
|
|
410
|
+
` },`,
|
|
411
|
+
`});`,
|
|
412
|
+
].join("\n")}\n`,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
techId: "vitest",
|
|
417
|
+
success: true,
|
|
418
|
+
message: "Vitest configured",
|
|
419
|
+
filesCreated: ["vitest.config.ts"],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function installJest(ctx: InstallContext): InstallResult {
|
|
424
|
+
const dir = getNodeDir(ctx);
|
|
425
|
+
run(`npm install -D jest ts-jest @types/jest`, dir);
|
|
426
|
+
|
|
427
|
+
write(
|
|
428
|
+
path.join(dir, "jest.config.ts"),
|
|
429
|
+
`${[`export default {`, ` preset: "ts-jest",`, ` testEnvironment: "node",`, `};`].join(
|
|
430
|
+
"\n",
|
|
431
|
+
)}\n`,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
techId: "jest",
|
|
436
|
+
success: true,
|
|
437
|
+
message: "Jest configured",
|
|
438
|
+
filesCreated: ["jest.config.ts"],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function installPlaywright(ctx: InstallContext): InstallResult {
|
|
443
|
+
const dir = getNodeDir(ctx);
|
|
444
|
+
run(`npm install -D @playwright/test`, dir);
|
|
445
|
+
|
|
446
|
+
write(
|
|
447
|
+
path.join(dir, "playwright.config.ts"),
|
|
448
|
+
`${[
|
|
449
|
+
`import { defineConfig } from "@playwright/test";`,
|
|
450
|
+
``,
|
|
451
|
+
`export default defineConfig({`,
|
|
452
|
+
` testDir: "./tests",`,
|
|
453
|
+
` use: { baseURL: "http://localhost:3000" },`,
|
|
454
|
+
`});`,
|
|
455
|
+
].join("\n")}\n`,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
techId: "playwright",
|
|
460
|
+
success: true,
|
|
461
|
+
message: "Playwright configured",
|
|
462
|
+
filesCreated: ["playwright.config.ts"],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function installCypress(ctx: InstallContext): InstallResult {
|
|
467
|
+
const dir = getNodeDir(ctx);
|
|
468
|
+
run(`npm install -D cypress`, dir);
|
|
469
|
+
|
|
470
|
+
write(
|
|
471
|
+
path.join(dir, "cypress.config.ts"),
|
|
472
|
+
`${[
|
|
473
|
+
`import { defineConfig } from "cypress";`,
|
|
474
|
+
``,
|
|
475
|
+
`export default defineConfig({`,
|
|
476
|
+
` e2e: { baseUrl: "http://localhost:3000" },`,
|
|
477
|
+
`});`,
|
|
478
|
+
].join("\n")}\n`,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
techId: "cypress",
|
|
483
|
+
success: true,
|
|
484
|
+
message: "Cypress configured",
|
|
485
|
+
filesCreated: ["cypress.config.ts"],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function installBiome(ctx: InstallContext): InstallResult {
|
|
490
|
+
const dir = getNodeDir(ctx);
|
|
491
|
+
run(`npm install -D @biomejs/biome`, dir);
|
|
492
|
+
|
|
493
|
+
write(
|
|
494
|
+
path.join(dir, "biome.json"),
|
|
495
|
+
`${JSON.stringify(
|
|
496
|
+
{
|
|
497
|
+
$schema: "https://biomejs.dev/schemas/1.9.0/schema.json",
|
|
498
|
+
formatter: { indentStyle: "space", indentWidth: 2, lineWidth: 100 },
|
|
499
|
+
linter: { enabled: true, rules: { recommended: true } },
|
|
500
|
+
organizeImports: { enabled: true },
|
|
501
|
+
},
|
|
502
|
+
null,
|
|
503
|
+
2,
|
|
504
|
+
)}\n`,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
techId: "biome",
|
|
509
|
+
success: true,
|
|
510
|
+
message: "Biome configured",
|
|
511
|
+
filesCreated: ["biome.json"],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function installEslint(ctx: InstallContext): InstallResult {
|
|
516
|
+
const dir = getNodeDir(ctx);
|
|
517
|
+
// Don't install if already present from scaffold (Next.js includes it)
|
|
518
|
+
const configExists =
|
|
519
|
+
fs.existsSync(path.join(dir, "eslint.config.mjs")) ||
|
|
520
|
+
fs.existsSync(path.join(dir, ".eslintrc.json")) ||
|
|
521
|
+
fs.existsSync(path.join(dir, "eslint.config.js"));
|
|
522
|
+
if (!configExists) {
|
|
523
|
+
run(`npm install -D eslint @eslint/js typescript-eslint`, dir);
|
|
524
|
+
write(
|
|
525
|
+
path.join(dir, "eslint.config.js"),
|
|
526
|
+
`${[
|
|
527
|
+
`import js from "@eslint/js";`,
|
|
528
|
+
`import tseslint from "typescript-eslint";`,
|
|
529
|
+
``,
|
|
530
|
+
`export default tseslint.config(`,
|
|
531
|
+
` js.configs.recommended,`,
|
|
532
|
+
` ...tseslint.configs.recommended,`,
|
|
533
|
+
`);`,
|
|
534
|
+
].join("\n")}\n`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
return { techId: "eslint", success: true, message: "ESLint configured", filesCreated: [] };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function installPrettier(ctx: InstallContext): InstallResult {
|
|
541
|
+
const dir = getNodeDir(ctx);
|
|
542
|
+
run(`npm install -D prettier`, dir);
|
|
543
|
+
write(
|
|
544
|
+
path.join(dir, ".prettierrc"),
|
|
545
|
+
`${JSON.stringify({ semi: true, singleQuote: false, tabWidth: 2, trailingComma: "all" }, null, 2)}\n`,
|
|
546
|
+
);
|
|
547
|
+
return {
|
|
548
|
+
techId: "prettier",
|
|
549
|
+
success: true,
|
|
550
|
+
message: "Prettier configured",
|
|
551
|
+
filesCreated: [".prettierrc"],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function installZod(ctx: InstallContext): InstallResult {
|
|
556
|
+
const dir = getNodeDir(ctx);
|
|
557
|
+
run(`npm install zod`, dir);
|
|
558
|
+
return { techId: "zod", success: true, message: "Zod installed", filesCreated: [] };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function installStorybook(ctx: InstallContext): InstallResult {
|
|
562
|
+
const dir = getNodeDir(ctx);
|
|
563
|
+
run(`npx storybook@latest init --no-dev`, dir, 120_000);
|
|
564
|
+
return {
|
|
565
|
+
techId: "storybook",
|
|
566
|
+
success: true,
|
|
567
|
+
message: "Storybook initialized",
|
|
568
|
+
filesCreated: [".storybook/"],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function installGithubActions(_ctx: InstallContext): InstallResult {
|
|
573
|
+
// Already handled by scaffold-orchestrator (ci.yml)
|
|
574
|
+
return {
|
|
575
|
+
techId: "github-actions",
|
|
576
|
+
success: true,
|
|
577
|
+
message: "GitHub Actions CI configured",
|
|
578
|
+
filesCreated: [".github/workflows/ci.yml"],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ─── Main installer ─────────────────────────────────
|
|
583
|
+
|
|
584
|
+
const INSTALLERS: Record<string, (ctx: InstallContext) => InstallResult> = {
|
|
585
|
+
// ORMs
|
|
586
|
+
prisma: installPrisma,
|
|
587
|
+
drizzle: installDrizzle,
|
|
588
|
+
typeorm: installTypeorm,
|
|
589
|
+
sqlalchemy: installSqlalchemy,
|
|
590
|
+
mongoose: installMongoose,
|
|
591
|
+
|
|
592
|
+
// Auth
|
|
593
|
+
nextauth: installNextAuth,
|
|
594
|
+
clerk: installClerk,
|
|
595
|
+
lucia: installLucia,
|
|
596
|
+
"supabase-auth": installSupabaseAuth,
|
|
597
|
+
|
|
598
|
+
// Styling
|
|
599
|
+
tailwindcss: installTailwind,
|
|
600
|
+
"shadcn-ui": installShadcn,
|
|
601
|
+
"chakra-ui": installChakraUI,
|
|
602
|
+
"material-ui": installMaterialUI,
|
|
603
|
+
daisyui: installDaisyUI,
|
|
604
|
+
|
|
605
|
+
// DevOps
|
|
606
|
+
vitest: installVitest,
|
|
607
|
+
jest: installJest,
|
|
608
|
+
playwright: installPlaywright,
|
|
609
|
+
cypress: installCypress,
|
|
610
|
+
biome: installBiome,
|
|
611
|
+
eslint: installEslint,
|
|
612
|
+
prettier: installPrettier,
|
|
613
|
+
zod: installZod,
|
|
614
|
+
storybook: installStorybook,
|
|
615
|
+
"github-actions": installGithubActions,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Install all selected technologies that have installers.
|
|
620
|
+
* Skips runtimes (already handled), frontends/backends (handled by generate),
|
|
621
|
+
* databases (handled by docker-compose), and services (handled by docker-compose).
|
|
622
|
+
*/
|
|
623
|
+
export function installTechnologies(ctx: InstallContext): InstallResult[] {
|
|
624
|
+
const results: InstallResult[] = [];
|
|
625
|
+
|
|
626
|
+
// Install in dependency order: styling → orm → auth → devops
|
|
627
|
+
const order = ["styling", "orm", "auth", "devops"];
|
|
628
|
+
|
|
629
|
+
for (const phase of order) {
|
|
630
|
+
for (const tech of ctx.allTechs) {
|
|
631
|
+
if (tech.category !== phase) continue;
|
|
632
|
+
const installer = INSTALLERS[tech.id];
|
|
633
|
+
if (installer) {
|
|
634
|
+
try {
|
|
635
|
+
const result = installer(ctx);
|
|
636
|
+
results.push(result);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
results.push({
|
|
639
|
+
techId: tech.id,
|
|
640
|
+
success: false,
|
|
641
|
+
message: err instanceof Error ? err.message : "Install failed",
|
|
642
|
+
filesCreated: [],
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return results;
|
|
650
|
+
}
|