@kozojs/cli 0.1.4 → 0.1.6
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/lib/index.js +1180 -20
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -35,11 +35,23 @@ var import_execa = require("execa");
|
|
|
35
35
|
var import_fs_extra = __toESM(require("fs-extra"));
|
|
36
36
|
var import_node_path = __toESM(require("path"));
|
|
37
37
|
async function scaffoldProject(options) {
|
|
38
|
-
const { projectName, database, packageSource, template } = options;
|
|
38
|
+
const { projectName, runtime, database, packageSource, template, frontend, extras } = options;
|
|
39
39
|
const projectDir = import_node_path.default.resolve(process.cwd(), projectName);
|
|
40
|
-
const kozoCoreDep = packageSource === "local" ? "workspace:*" : "^0.
|
|
40
|
+
const kozoCoreDep = packageSource === "local" ? "workspace:*" : "^0.2.0";
|
|
41
|
+
if (frontend !== "none") {
|
|
42
|
+
await scaffoldFullstackProject(projectDir, projectName, kozoCoreDep, runtime, database, frontend, extras, template);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
41
45
|
if (template === "complete") {
|
|
42
|
-
await scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep);
|
|
46
|
+
await scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, runtime);
|
|
47
|
+
if (extras.includes("docker")) await createDockerfile(projectDir, runtime);
|
|
48
|
+
if (extras.includes("github-actions")) await createGitHubActions(projectDir);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (template === "api-only") {
|
|
52
|
+
await scaffoldApiOnlyTemplate(projectDir, projectName, kozoCoreDep, runtime);
|
|
53
|
+
if (extras.includes("docker")) await createDockerfile(projectDir, runtime);
|
|
54
|
+
if (extras.includes("github-actions")) await createGitHubActions(projectDir);
|
|
43
55
|
return;
|
|
44
56
|
}
|
|
45
57
|
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes"));
|
|
@@ -399,7 +411,7 @@ export default async ({ body, services: { db } }: HandlerContext<Body>) => {
|
|
|
399
411
|
`;
|
|
400
412
|
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "users", "post.ts"), postUsersRoute);
|
|
401
413
|
}
|
|
402
|
-
async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep) {
|
|
414
|
+
async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, runtime) {
|
|
403
415
|
await import_fs_extra.default.ensureDir(projectDir);
|
|
404
416
|
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src"));
|
|
405
417
|
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "schemas"));
|
|
@@ -662,7 +674,6 @@ These optimizations make Kozo competitive with Fastify while providing:
|
|
|
662
674
|
|
|
663
675
|
## Documentation
|
|
664
676
|
|
|
665
|
-
- [Kozo Documentation](https://kozo.dev/docs)
|
|
666
677
|
- [Zod Schema Validation](https://zod.dev)
|
|
667
678
|
- [Hono Framework](https://hono.dev)
|
|
668
679
|
|
|
@@ -1074,6 +1085,1127 @@ export function registerPostRoutes(app: Kozo) {
|
|
|
1074
1085
|
`;
|
|
1075
1086
|
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "posts", "index.ts"), postRoutes);
|
|
1076
1087
|
}
|
|
1088
|
+
async function scaffoldApiOnlyTemplate(projectDir, projectName, kozoCoreDep, runtime) {
|
|
1089
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src"));
|
|
1090
|
+
const packageJson = {
|
|
1091
|
+
name: projectName,
|
|
1092
|
+
version: "1.0.0",
|
|
1093
|
+
type: "module",
|
|
1094
|
+
scripts: {
|
|
1095
|
+
dev: runtime === "bun" ? "bun --watch src/index.ts" : "tsx watch src/index.ts",
|
|
1096
|
+
build: "tsc",
|
|
1097
|
+
start: runtime === "bun" ? "bun src/index.ts" : "node dist/index.js"
|
|
1098
|
+
},
|
|
1099
|
+
dependencies: {
|
|
1100
|
+
"@kozojs/core": kozoCoreDep,
|
|
1101
|
+
hono: "^4.6.0",
|
|
1102
|
+
zod: "^3.23.0",
|
|
1103
|
+
...runtime === "node" && { "@hono/node-server": "^1.13.0" }
|
|
1104
|
+
},
|
|
1105
|
+
devDependencies: {
|
|
1106
|
+
"@types/node": "^22.0.0",
|
|
1107
|
+
...runtime !== "bun" && { tsx: "^4.19.0" },
|
|
1108
|
+
typescript: "^5.6.0"
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), packageJson, { spaces: 2 });
|
|
1112
|
+
const tsconfig = {
|
|
1113
|
+
compilerOptions: {
|
|
1114
|
+
target: "ES2022",
|
|
1115
|
+
module: "ESNext",
|
|
1116
|
+
moduleResolution: "bundler",
|
|
1117
|
+
strict: true,
|
|
1118
|
+
esModuleInterop: true,
|
|
1119
|
+
skipLibCheck: true,
|
|
1120
|
+
outDir: "dist",
|
|
1121
|
+
rootDir: "src"
|
|
1122
|
+
},
|
|
1123
|
+
include: ["src/**/*"],
|
|
1124
|
+
exclude: ["node_modules", "dist"]
|
|
1125
|
+
};
|
|
1126
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
1127
|
+
const indexTs = `import { createKozo } from '@kozojs/core';
|
|
1128
|
+
import { z } from 'zod';
|
|
1129
|
+
|
|
1130
|
+
const app = createKozo({ port: 3000 });
|
|
1131
|
+
|
|
1132
|
+
// Health check
|
|
1133
|
+
app.get('/health', {}, () => ({
|
|
1134
|
+
status: 'ok',
|
|
1135
|
+
timestamp: new Date().toISOString(),
|
|
1136
|
+
}));
|
|
1137
|
+
|
|
1138
|
+
// Example endpoint with validation
|
|
1139
|
+
app.get('/hello/:name', {
|
|
1140
|
+
params: z.object({ name: z.string() }),
|
|
1141
|
+
response: z.object({ message: z.string() }),
|
|
1142
|
+
}, (c) => ({
|
|
1143
|
+
message: \`Hello, \${c.params.name}!\`,
|
|
1144
|
+
}));
|
|
1145
|
+
|
|
1146
|
+
console.log('\u{1F525} Kozo running on http://localhost:3000');
|
|
1147
|
+
app.listen();
|
|
1148
|
+
`;
|
|
1149
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
|
|
1150
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
|
|
1151
|
+
}
|
|
1152
|
+
async function createDockerfile(projectDir, runtime) {
|
|
1153
|
+
const dockerfile = runtime === "bun" ? `FROM oven/bun:1 as builder
|
|
1154
|
+
WORKDIR /app
|
|
1155
|
+
COPY package.json bun.lockb* ./
|
|
1156
|
+
RUN bun install --frozen-lockfile
|
|
1157
|
+
|
|
1158
|
+
COPY . .
|
|
1159
|
+
RUN bun run build
|
|
1160
|
+
|
|
1161
|
+
FROM oven/bun:1-slim
|
|
1162
|
+
WORKDIR /app
|
|
1163
|
+
COPY --from=builder /app/dist ./dist
|
|
1164
|
+
COPY --from=builder /app/package.json ./
|
|
1165
|
+
EXPOSE 3000
|
|
1166
|
+
CMD ["bun", "dist/index.js"]
|
|
1167
|
+
` : `FROM node:20-alpine as builder
|
|
1168
|
+
WORKDIR /app
|
|
1169
|
+
COPY package*.json ./
|
|
1170
|
+
RUN npm ci
|
|
1171
|
+
|
|
1172
|
+
COPY . .
|
|
1173
|
+
RUN npm run build
|
|
1174
|
+
|
|
1175
|
+
FROM node:20-alpine
|
|
1176
|
+
WORKDIR /app
|
|
1177
|
+
COPY --from=builder /app/dist ./dist
|
|
1178
|
+
COPY --from=builder /app/package*.json ./
|
|
1179
|
+
RUN npm ci --omit=dev
|
|
1180
|
+
EXPOSE 3000
|
|
1181
|
+
CMD ["node", "dist/index.js"]
|
|
1182
|
+
`;
|
|
1183
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "Dockerfile"), dockerfile);
|
|
1184
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".dockerignore"), "node_modules\ndist\n.git\n");
|
|
1185
|
+
}
|
|
1186
|
+
async function createGitHubActions(projectDir) {
|
|
1187
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, ".github", "workflows"));
|
|
1188
|
+
const workflow = `name: CI
|
|
1189
|
+
|
|
1190
|
+
on:
|
|
1191
|
+
push:
|
|
1192
|
+
branches: [main]
|
|
1193
|
+
pull_request:
|
|
1194
|
+
branches: [main]
|
|
1195
|
+
|
|
1196
|
+
jobs:
|
|
1197
|
+
build:
|
|
1198
|
+
runs-on: ubuntu-latest
|
|
1199
|
+
steps:
|
|
1200
|
+
- uses: actions/checkout@v4
|
|
1201
|
+
- uses: actions/setup-node@v4
|
|
1202
|
+
with:
|
|
1203
|
+
node-version: '20'
|
|
1204
|
+
cache: 'npm'
|
|
1205
|
+
- run: npm ci
|
|
1206
|
+
- run: npm run build
|
|
1207
|
+
- run: npm test --if-present
|
|
1208
|
+
`;
|
|
1209
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".github", "workflows", "ci.yml"), workflow);
|
|
1210
|
+
}
|
|
1211
|
+
async function scaffoldFullstackProject(projectDir, projectName, kozoCoreDep, runtime, database, frontend, extras, template) {
|
|
1212
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "routes"));
|
|
1213
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "data"));
|
|
1214
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "web", "src", "lib"));
|
|
1215
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, ".vscode"));
|
|
1216
|
+
const rootPackageJson = {
|
|
1217
|
+
name: projectName,
|
|
1218
|
+
private: true,
|
|
1219
|
+
scripts: {
|
|
1220
|
+
dev: "pnpm run --parallel dev",
|
|
1221
|
+
build: "pnpm run --recursive build"
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), rootPackageJson, { spaces: 2 });
|
|
1225
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "pnpm-workspace.yaml"), `packages:
|
|
1226
|
+
- 'apps/*'
|
|
1227
|
+
`);
|
|
1228
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), "node_modules/\ndist/\n.env\n*.log\n");
|
|
1229
|
+
await scaffoldFullstackApi(projectDir, projectName, kozoCoreDep, runtime);
|
|
1230
|
+
await scaffoldFullstackWeb(projectDir, projectName, frontend);
|
|
1231
|
+
await scaffoldFullstackReadme(projectDir, projectName);
|
|
1232
|
+
if (extras.includes("docker")) await createDockerfile(import_node_path.default.join(projectDir, "apps", "api"), runtime);
|
|
1233
|
+
if (extras.includes("github-actions")) await createGitHubActions(projectDir);
|
|
1234
|
+
}
|
|
1235
|
+
async function scaffoldFullstackApi(projectDir, projectName, kozoCoreDep, runtime) {
|
|
1236
|
+
const apiDir = import_node_path.default.join(projectDir, "apps", "api");
|
|
1237
|
+
const apiPackageJson = {
|
|
1238
|
+
name: `@${projectName}/api`,
|
|
1239
|
+
version: "1.0.0",
|
|
1240
|
+
type: "module",
|
|
1241
|
+
scripts: {
|
|
1242
|
+
dev: runtime === "bun" ? "bun --watch src/index.ts" : "tsx watch src/index.ts",
|
|
1243
|
+
build: "tsc"
|
|
1244
|
+
},
|
|
1245
|
+
dependencies: {
|
|
1246
|
+
"@kozojs/core": kozoCoreDep,
|
|
1247
|
+
hono: "^4.6.0",
|
|
1248
|
+
zod: "^3.23.0",
|
|
1249
|
+
...runtime === "node" && { "@hono/node-server": "^1.13.0" }
|
|
1250
|
+
},
|
|
1251
|
+
devDependencies: {
|
|
1252
|
+
"@types/node": "^22.0.0",
|
|
1253
|
+
...runtime !== "bun" && { tsx: "^4.19.0" },
|
|
1254
|
+
typescript: "^5.6.0"
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(apiDir, "package.json"), apiPackageJson, { spaces: 2 });
|
|
1258
|
+
const tsconfig = {
|
|
1259
|
+
compilerOptions: {
|
|
1260
|
+
target: "ES2022",
|
|
1261
|
+
module: "ESNext",
|
|
1262
|
+
moduleResolution: "bundler",
|
|
1263
|
+
strict: true,
|
|
1264
|
+
esModuleInterop: true,
|
|
1265
|
+
skipLibCheck: true,
|
|
1266
|
+
outDir: "dist",
|
|
1267
|
+
rootDir: "src"
|
|
1268
|
+
},
|
|
1269
|
+
include: ["src/**/*"],
|
|
1270
|
+
exclude: ["node_modules", "dist"]
|
|
1271
|
+
};
|
|
1272
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(apiDir, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
1273
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "index.ts"), `import { createKozo } from '@kozojs/core';
|
|
1274
|
+
import { registerRoutes } from './routes';
|
|
1275
|
+
|
|
1276
|
+
const app = createKozo({ port: 3000 });
|
|
1277
|
+
|
|
1278
|
+
registerRoutes(app);
|
|
1279
|
+
|
|
1280
|
+
export type AppType = typeof app;
|
|
1281
|
+
|
|
1282
|
+
console.log('\u{1F525} ${projectName} API running on http://localhost:3000');
|
|
1283
|
+
console.log('\u{1F4DA} Endpoints: /api/health, /api/users, /api/posts, /api/tasks, /api/stats');
|
|
1284
|
+
app.listen();
|
|
1285
|
+
`);
|
|
1286
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "data", "index.ts"), `import { z } from 'zod';
|
|
1287
|
+
|
|
1288
|
+
export const UserSchema = z.object({
|
|
1289
|
+
id: z.string(),
|
|
1290
|
+
name: z.string(),
|
|
1291
|
+
email: z.string().email(),
|
|
1292
|
+
role: z.enum(['admin', 'user']),
|
|
1293
|
+
createdAt: z.string().optional(),
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
export const PostSchema = z.object({
|
|
1297
|
+
id: z.string(),
|
|
1298
|
+
title: z.string(),
|
|
1299
|
+
content: z.string(),
|
|
1300
|
+
authorId: z.string(),
|
|
1301
|
+
published: z.boolean(),
|
|
1302
|
+
createdAt: z.string().optional(),
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
export const TaskSchema = z.object({
|
|
1306
|
+
id: z.string(),
|
|
1307
|
+
title: z.string(),
|
|
1308
|
+
completed: z.boolean(),
|
|
1309
|
+
priority: z.enum(['low', 'medium', 'high']),
|
|
1310
|
+
createdAt: z.string(),
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
export const users: z.infer<typeof UserSchema>[] = [
|
|
1314
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin', createdAt: new Date().toISOString() },
|
|
1315
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com', role: 'user', createdAt: new Date().toISOString() },
|
|
1316
|
+
];
|
|
1317
|
+
|
|
1318
|
+
export const posts: z.infer<typeof PostSchema>[] = [
|
|
1319
|
+
{ id: '1', title: 'Hello World', content: 'First post!', authorId: '1', published: true, createdAt: new Date().toISOString() },
|
|
1320
|
+
{ id: '2', title: 'Draft', content: 'Work in progress', authorId: '2', published: false, createdAt: new Date().toISOString() },
|
|
1321
|
+
];
|
|
1322
|
+
|
|
1323
|
+
export const tasks: z.infer<typeof TaskSchema>[] = [
|
|
1324
|
+
{ id: '1', title: 'Setup project', completed: true, priority: 'high', createdAt: new Date().toISOString() },
|
|
1325
|
+
{ id: '2', title: 'Write tests', completed: false, priority: 'medium', createdAt: new Date().toISOString() },
|
|
1326
|
+
{ id: '3', title: 'Deploy', completed: false, priority: 'low', createdAt: new Date().toISOString() },
|
|
1327
|
+
];
|
|
1328
|
+
`);
|
|
1329
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "index.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1330
|
+
import { registerHealthRoutes } from './health';
|
|
1331
|
+
import { registerUserRoutes } from './users';
|
|
1332
|
+
import { registerPostRoutes } from './posts';
|
|
1333
|
+
import { registerTaskRoutes } from './tasks';
|
|
1334
|
+
import { registerToolRoutes } from './tools';
|
|
1335
|
+
|
|
1336
|
+
export function registerRoutes(app: Kozo) {
|
|
1337
|
+
registerHealthRoutes(app);
|
|
1338
|
+
registerUserRoutes(app);
|
|
1339
|
+
registerPostRoutes(app);
|
|
1340
|
+
registerTaskRoutes(app);
|
|
1341
|
+
registerToolRoutes(app);
|
|
1342
|
+
}
|
|
1343
|
+
`);
|
|
1344
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "health.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1345
|
+
import { users, posts, tasks } from '../data';
|
|
1346
|
+
|
|
1347
|
+
export function registerHealthRoutes(app: Kozo) {
|
|
1348
|
+
app.get('/api/health', {}, () => ({
|
|
1349
|
+
status: 'ok',
|
|
1350
|
+
timestamp: new Date().toISOString(),
|
|
1351
|
+
version: '1.0.0',
|
|
1352
|
+
uptime: process.uptime(),
|
|
1353
|
+
}));
|
|
1354
|
+
|
|
1355
|
+
app.get('/api/stats', {}, () => ({
|
|
1356
|
+
users: users.length,
|
|
1357
|
+
posts: posts.length,
|
|
1358
|
+
tasks: tasks.length,
|
|
1359
|
+
publishedPosts: posts.filter(p => p.published).length,
|
|
1360
|
+
completedTasks: tasks.filter(t => t.completed).length,
|
|
1361
|
+
}));
|
|
1362
|
+
}
|
|
1363
|
+
`);
|
|
1364
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "users.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1365
|
+
import { z } from 'zod';
|
|
1366
|
+
import { users, UserSchema } from '../data';
|
|
1367
|
+
|
|
1368
|
+
export function registerUserRoutes(app: Kozo) {
|
|
1369
|
+
app.get('/api/users', {
|
|
1370
|
+
response: z.array(UserSchema),
|
|
1371
|
+
}, () => users);
|
|
1372
|
+
|
|
1373
|
+
app.get('/api/users/:id', {
|
|
1374
|
+
params: z.object({ id: z.string() }),
|
|
1375
|
+
response: UserSchema,
|
|
1376
|
+
}, (c) => {
|
|
1377
|
+
const user = users.find(u => u.id === c.params.id);
|
|
1378
|
+
if (!user) throw new Error('User not found');
|
|
1379
|
+
return user;
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
app.post('/api/users', {
|
|
1383
|
+
body: z.object({
|
|
1384
|
+
name: z.string().min(1),
|
|
1385
|
+
email: z.string().email(),
|
|
1386
|
+
role: z.enum(['admin', 'user']).optional(),
|
|
1387
|
+
}),
|
|
1388
|
+
response: UserSchema,
|
|
1389
|
+
}, (c) => {
|
|
1390
|
+
const newUser = {
|
|
1391
|
+
id: String(Date.now()),
|
|
1392
|
+
name: c.body.name,
|
|
1393
|
+
email: c.body.email,
|
|
1394
|
+
role: c.body.role || 'user' as const,
|
|
1395
|
+
createdAt: new Date().toISOString(),
|
|
1396
|
+
};
|
|
1397
|
+
users.push(newUser);
|
|
1398
|
+
return newUser;
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
app.put('/api/users/:id', {
|
|
1402
|
+
params: z.object({ id: z.string() }),
|
|
1403
|
+
body: z.object({
|
|
1404
|
+
name: z.string().min(1).optional(),
|
|
1405
|
+
email: z.string().email().optional(),
|
|
1406
|
+
role: z.enum(['admin', 'user']).optional(),
|
|
1407
|
+
}),
|
|
1408
|
+
response: UserSchema,
|
|
1409
|
+
}, (c) => {
|
|
1410
|
+
const idx = users.findIndex(u => u.id === c.params.id);
|
|
1411
|
+
if (idx === -1) throw new Error('User not found');
|
|
1412
|
+
users[idx] = { ...users[idx], ...c.body };
|
|
1413
|
+
return users[idx];
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
app.delete('/api/users/:id', {
|
|
1417
|
+
params: z.object({ id: z.string() }),
|
|
1418
|
+
}, (c) => {
|
|
1419
|
+
const idx = users.findIndex(u => u.id === c.params.id);
|
|
1420
|
+
if (idx === -1) throw new Error('User not found');
|
|
1421
|
+
users.splice(idx, 1);
|
|
1422
|
+
return { success: true, message: 'User deleted' };
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
`);
|
|
1426
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "posts.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1427
|
+
import { z } from 'zod';
|
|
1428
|
+
import { posts, PostSchema } from '../data';
|
|
1429
|
+
|
|
1430
|
+
export function registerPostRoutes(app: Kozo) {
|
|
1431
|
+
app.get('/api/posts', {
|
|
1432
|
+
query: z.object({ published: z.coerce.boolean().optional() }),
|
|
1433
|
+
response: z.array(PostSchema),
|
|
1434
|
+
}, (c) => {
|
|
1435
|
+
if (c.query.published !== undefined) {
|
|
1436
|
+
return posts.filter(p => p.published === c.query.published);
|
|
1437
|
+
}
|
|
1438
|
+
return posts;
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
app.get('/api/posts/:id', {
|
|
1442
|
+
params: z.object({ id: z.string() }),
|
|
1443
|
+
response: PostSchema,
|
|
1444
|
+
}, (c) => {
|
|
1445
|
+
const post = posts.find(p => p.id === c.params.id);
|
|
1446
|
+
if (!post) throw new Error('Post not found');
|
|
1447
|
+
return post;
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
app.post('/api/posts', {
|
|
1451
|
+
body: z.object({
|
|
1452
|
+
title: z.string().min(1),
|
|
1453
|
+
content: z.string(),
|
|
1454
|
+
authorId: z.string(),
|
|
1455
|
+
published: z.boolean().optional(),
|
|
1456
|
+
}),
|
|
1457
|
+
response: PostSchema,
|
|
1458
|
+
}, (c) => {
|
|
1459
|
+
const newPost = {
|
|
1460
|
+
id: String(Date.now()),
|
|
1461
|
+
title: c.body.title,
|
|
1462
|
+
content: c.body.content,
|
|
1463
|
+
authorId: c.body.authorId,
|
|
1464
|
+
published: c.body.published ?? false,
|
|
1465
|
+
createdAt: new Date().toISOString(),
|
|
1466
|
+
};
|
|
1467
|
+
posts.push(newPost);
|
|
1468
|
+
return newPost;
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
app.put('/api/posts/:id', {
|
|
1472
|
+
params: z.object({ id: z.string() }),
|
|
1473
|
+
body: z.object({
|
|
1474
|
+
title: z.string().min(1).optional(),
|
|
1475
|
+
content: z.string().optional(),
|
|
1476
|
+
published: z.boolean().optional(),
|
|
1477
|
+
}),
|
|
1478
|
+
response: PostSchema,
|
|
1479
|
+
}, (c) => {
|
|
1480
|
+
const idx = posts.findIndex(p => p.id === c.params.id);
|
|
1481
|
+
if (idx === -1) throw new Error('Post not found');
|
|
1482
|
+
posts[idx] = { ...posts[idx], ...c.body };
|
|
1483
|
+
return posts[idx];
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
app.delete('/api/posts/:id', {
|
|
1487
|
+
params: z.object({ id: z.string() }),
|
|
1488
|
+
}, (c) => {
|
|
1489
|
+
const idx = posts.findIndex(p => p.id === c.params.id);
|
|
1490
|
+
if (idx === -1) throw new Error('Post not found');
|
|
1491
|
+
posts.splice(idx, 1);
|
|
1492
|
+
return { success: true, message: 'Post deleted' };
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
`);
|
|
1496
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "tasks.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1497
|
+
import { z } from 'zod';
|
|
1498
|
+
import { tasks, TaskSchema } from '../data';
|
|
1499
|
+
|
|
1500
|
+
export function registerTaskRoutes(app: Kozo) {
|
|
1501
|
+
app.get('/api/tasks', {
|
|
1502
|
+
query: z.object({
|
|
1503
|
+
completed: z.coerce.boolean().optional(),
|
|
1504
|
+
priority: z.enum(['low', 'medium', 'high']).optional(),
|
|
1505
|
+
}),
|
|
1506
|
+
response: z.array(TaskSchema),
|
|
1507
|
+
}, (c) => {
|
|
1508
|
+
let result = [...tasks];
|
|
1509
|
+
if (c.query.completed !== undefined) {
|
|
1510
|
+
result = result.filter(t => t.completed === c.query.completed);
|
|
1511
|
+
}
|
|
1512
|
+
if (c.query.priority) {
|
|
1513
|
+
result = result.filter(t => t.priority === c.query.priority);
|
|
1514
|
+
}
|
|
1515
|
+
return result;
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
app.get('/api/tasks/:id', {
|
|
1519
|
+
params: z.object({ id: z.string() }),
|
|
1520
|
+
response: TaskSchema,
|
|
1521
|
+
}, (c) => {
|
|
1522
|
+
const task = tasks.find(t => t.id === c.params.id);
|
|
1523
|
+
if (!task) throw new Error('Task not found');
|
|
1524
|
+
return task;
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
app.post('/api/tasks', {
|
|
1528
|
+
body: z.object({
|
|
1529
|
+
title: z.string().min(1),
|
|
1530
|
+
priority: z.enum(['low', 'medium', 'high']).optional(),
|
|
1531
|
+
}),
|
|
1532
|
+
response: TaskSchema,
|
|
1533
|
+
}, (c) => {
|
|
1534
|
+
const newTask = {
|
|
1535
|
+
id: String(Date.now()),
|
|
1536
|
+
title: c.body.title,
|
|
1537
|
+
completed: false,
|
|
1538
|
+
priority: c.body.priority || 'medium' as const,
|
|
1539
|
+
createdAt: new Date().toISOString(),
|
|
1540
|
+
};
|
|
1541
|
+
tasks.push(newTask);
|
|
1542
|
+
return newTask;
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
app.put('/api/tasks/:id', {
|
|
1546
|
+
params: z.object({ id: z.string() }),
|
|
1547
|
+
body: z.object({
|
|
1548
|
+
title: z.string().min(1).optional(),
|
|
1549
|
+
completed: z.boolean().optional(),
|
|
1550
|
+
priority: z.enum(['low', 'medium', 'high']).optional(),
|
|
1551
|
+
}),
|
|
1552
|
+
response: TaskSchema,
|
|
1553
|
+
}, (c) => {
|
|
1554
|
+
const idx = tasks.findIndex(t => t.id === c.params.id);
|
|
1555
|
+
if (idx === -1) throw new Error('Task not found');
|
|
1556
|
+
tasks[idx] = { ...tasks[idx], ...c.body };
|
|
1557
|
+
return tasks[idx];
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
app.patch('/api/tasks/:id/toggle', {
|
|
1561
|
+
params: z.object({ id: z.string() }),
|
|
1562
|
+
response: TaskSchema,
|
|
1563
|
+
}, (c) => {
|
|
1564
|
+
const idx = tasks.findIndex(t => t.id === c.params.id);
|
|
1565
|
+
if (idx === -1) throw new Error('Task not found');
|
|
1566
|
+
tasks[idx].completed = !tasks[idx].completed;
|
|
1567
|
+
return tasks[idx];
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
app.delete('/api/tasks/:id', {
|
|
1571
|
+
params: z.object({ id: z.string() }),
|
|
1572
|
+
}, (c) => {
|
|
1573
|
+
const idx = tasks.findIndex(t => t.id === c.params.id);
|
|
1574
|
+
if (idx === -1) throw new Error('Task not found');
|
|
1575
|
+
tasks.splice(idx, 1);
|
|
1576
|
+
return { success: true, message: 'Task deleted' };
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
`);
|
|
1580
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "tools.ts"), `import type { Kozo } from '@kozojs/core';
|
|
1581
|
+
import { z } from 'zod';
|
|
1582
|
+
|
|
1583
|
+
export function registerToolRoutes(app: Kozo) {
|
|
1584
|
+
app.get('/api/echo', {
|
|
1585
|
+
query: z.object({ message: z.string() }),
|
|
1586
|
+
}, (c) => ({
|
|
1587
|
+
echo: c.query.message,
|
|
1588
|
+
timestamp: new Date().toISOString(),
|
|
1589
|
+
}));
|
|
1590
|
+
|
|
1591
|
+
app.post('/api/validate', {
|
|
1592
|
+
body: z.object({
|
|
1593
|
+
email: z.string().email(),
|
|
1594
|
+
age: z.number().min(0).max(150),
|
|
1595
|
+
}),
|
|
1596
|
+
}, (c) => ({
|
|
1597
|
+
valid: true,
|
|
1598
|
+
data: c.body,
|
|
1599
|
+
}));
|
|
1600
|
+
}
|
|
1601
|
+
`);
|
|
1602
|
+
}
|
|
1603
|
+
async function scaffoldFullstackWeb(projectDir, projectName, frontend) {
|
|
1604
|
+
const webDir = import_node_path.default.join(projectDir, "apps", "web");
|
|
1605
|
+
const packageJson = {
|
|
1606
|
+
name: `@${projectName}/web`,
|
|
1607
|
+
version: "1.0.0",
|
|
1608
|
+
type: "module",
|
|
1609
|
+
scripts: {
|
|
1610
|
+
dev: "vite",
|
|
1611
|
+
build: "vite build",
|
|
1612
|
+
preview: "vite preview"
|
|
1613
|
+
},
|
|
1614
|
+
dependencies: {
|
|
1615
|
+
react: "^18.2.0",
|
|
1616
|
+
"react-dom": "^18.2.0",
|
|
1617
|
+
"@tanstack/react-query": "^5.0.0",
|
|
1618
|
+
hono: "^4.6.0",
|
|
1619
|
+
"lucide-react": "^0.460.0"
|
|
1620
|
+
},
|
|
1621
|
+
devDependencies: {
|
|
1622
|
+
"@types/react": "^18.2.0",
|
|
1623
|
+
"@types/react-dom": "^18.2.0",
|
|
1624
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
1625
|
+
typescript: "^5.6.0",
|
|
1626
|
+
vite: "^5.0.0",
|
|
1627
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
1628
|
+
tailwindcss: "^4.0.0"
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(webDir, "package.json"), packageJson, { spaces: 2 });
|
|
1632
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "vite.config.ts"), `import { defineConfig } from 'vite';
|
|
1633
|
+
import react from '@vitejs/plugin-react';
|
|
1634
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
1635
|
+
|
|
1636
|
+
export default defineConfig({
|
|
1637
|
+
plugins: [react(), tailwindcss()],
|
|
1638
|
+
server: {
|
|
1639
|
+
proxy: {
|
|
1640
|
+
'/api': 'http://localhost:3000',
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1644
|
+
`);
|
|
1645
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "index.html"), `<!DOCTYPE html>
|
|
1646
|
+
<html lang="en">
|
|
1647
|
+
<head>
|
|
1648
|
+
<meta charset="UTF-8" />
|
|
1649
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1650
|
+
<title>${projectName}</title>
|
|
1651
|
+
</head>
|
|
1652
|
+
<body>
|
|
1653
|
+
<div id="root"></div>
|
|
1654
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
1655
|
+
</body>
|
|
1656
|
+
</html>
|
|
1657
|
+
`);
|
|
1658
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "index.css"), `@import "tailwindcss";
|
|
1659
|
+
|
|
1660
|
+
body {
|
|
1661
|
+
background-color: rgb(15 23 42);
|
|
1662
|
+
color: rgb(241 245 249);
|
|
1663
|
+
}
|
|
1664
|
+
`);
|
|
1665
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "lib", "client.ts"), `import { hc } from 'hono/client';
|
|
1666
|
+
import type { AppType } from '@${projectName}/api';
|
|
1667
|
+
|
|
1668
|
+
// Type-safe RPC client - changes in API break frontend at compile time!
|
|
1669
|
+
export const client = hc<AppType>('/');
|
|
1670
|
+
|
|
1671
|
+
/* Usage:
|
|
1672
|
+
const res = await client.api.users.$get();
|
|
1673
|
+
const users = await res.json(); // Fully typed!
|
|
1674
|
+
*/
|
|
1675
|
+
`);
|
|
1676
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "main.tsx"), `import React from 'react';
|
|
1677
|
+
import ReactDOM from 'react-dom/client';
|
|
1678
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1679
|
+
import App from './App';
|
|
1680
|
+
import './index.css';
|
|
1681
|
+
|
|
1682
|
+
const queryClient = new QueryClient({
|
|
1683
|
+
defaultOptions: {
|
|
1684
|
+
queries: {
|
|
1685
|
+
refetchOnWindowFocus: false,
|
|
1686
|
+
retry: 1,
|
|
1687
|
+
},
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
1692
|
+
<React.StrictMode>
|
|
1693
|
+
<QueryClientProvider client={queryClient}>
|
|
1694
|
+
<App />
|
|
1695
|
+
</QueryClientProvider>
|
|
1696
|
+
</React.StrictMode>
|
|
1697
|
+
);
|
|
1698
|
+
`);
|
|
1699
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "App.tsx"), generateFullReactApp(projectName));
|
|
1700
|
+
}
|
|
1701
|
+
function generateFullReactApp(projectName) {
|
|
1702
|
+
return `import { useState } from 'react';
|
|
1703
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
1704
|
+
import {
|
|
1705
|
+
Activity, Users, FileText, CheckSquare, Send, Trash2, Edit2,
|
|
1706
|
+
Plus, Server, Zap, Heart
|
|
1707
|
+
} from 'lucide-react';
|
|
1708
|
+
|
|
1709
|
+
const API_BASE = '/api';
|
|
1710
|
+
|
|
1711
|
+
type Tab = 'health' | 'users' | 'posts' | 'tasks' | 'tools';
|
|
1712
|
+
|
|
1713
|
+
interface ApiResponse {
|
|
1714
|
+
status: number;
|
|
1715
|
+
data: unknown;
|
|
1716
|
+
time: number;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
async function fetchApi(endpoint: string, options?: RequestInit): Promise<ApiResponse> {
|
|
1720
|
+
const start = performance.now();
|
|
1721
|
+
const res = await fetch(\`\${API_BASE}\${endpoint}\`, {
|
|
1722
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1723
|
+
...options,
|
|
1724
|
+
});
|
|
1725
|
+
const time = Math.round(performance.now() - start);
|
|
1726
|
+
const data = await res.json();
|
|
1727
|
+
return { status: res.status, data, time };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function Badge({ children, variant = 'default' }: { children: React.ReactNode; variant?: 'default' | 'success' | 'warning' | 'error' }) {
|
|
1731
|
+
const colors = {
|
|
1732
|
+
default: 'bg-slate-700 text-slate-200',
|
|
1733
|
+
success: 'bg-emerald-900 text-emerald-300',
|
|
1734
|
+
warning: 'bg-amber-900 text-amber-300',
|
|
1735
|
+
error: 'bg-red-900 text-red-300',
|
|
1736
|
+
};
|
|
1737
|
+
return <span className={\`px-2 py-0.5 rounded text-xs font-medium \${colors[variant]}\`}>{children}</span>;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function Card({ title, icon: Icon, children }: { title: string; icon: React.ElementType; children: React.ReactNode }) {
|
|
1741
|
+
return (
|
|
1742
|
+
<div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
1743
|
+
<div className="px-4 py-3 border-b border-slate-700 flex items-center gap-2">
|
|
1744
|
+
<Icon className="w-4 h-4 text-blue-400" />
|
|
1745
|
+
<h3 className="font-medium text-slate-200">{title}</h3>
|
|
1746
|
+
</div>
|
|
1747
|
+
<div className="p-4">{children}</div>
|
|
1748
|
+
</div>
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function ResponseDisplay({ response, loading }: { response: ApiResponse | null; loading: boolean }) {
|
|
1753
|
+
if (loading) return <div className="text-slate-400 text-sm">Loading...</div>;
|
|
1754
|
+
if (!response) return null;
|
|
1755
|
+
|
|
1756
|
+
const isSuccess = response.status >= 200 && response.status < 300;
|
|
1757
|
+
return (
|
|
1758
|
+
<div className="mt-3 p-3 bg-slate-900 rounded border border-slate-700">
|
|
1759
|
+
<div className="flex items-center gap-2 mb-2">
|
|
1760
|
+
<Badge variant={isSuccess ? 'success' : 'error'}>{response.status}</Badge>
|
|
1761
|
+
<span className="text-xs text-slate-500">{response.time}ms</span>
|
|
1762
|
+
</div>
|
|
1763
|
+
<pre className="text-xs text-slate-300 overflow-auto max-h-48">
|
|
1764
|
+
{JSON.stringify(response.data, null, 2)}
|
|
1765
|
+
</pre>
|
|
1766
|
+
</div>
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function HealthPanel() {
|
|
1771
|
+
const [response, setResponse] = useState<ApiResponse | null>(null);
|
|
1772
|
+
const [loading, setLoading] = useState(false);
|
|
1773
|
+
|
|
1774
|
+
const checkHealth = async () => {
|
|
1775
|
+
setLoading(true);
|
|
1776
|
+
const res = await fetchApi('/health');
|
|
1777
|
+
setResponse(res);
|
|
1778
|
+
setLoading(false);
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
const checkStats = async () => {
|
|
1782
|
+
setLoading(true);
|
|
1783
|
+
const res = await fetchApi('/stats');
|
|
1784
|
+
setResponse(res);
|
|
1785
|
+
setLoading(false);
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
return (
|
|
1789
|
+
<Card title="Health & Stats" icon={Heart}>
|
|
1790
|
+
<div className="flex gap-2 mb-3">
|
|
1791
|
+
<button onClick={checkHealth} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition">
|
|
1792
|
+
Check Health
|
|
1793
|
+
</button>
|
|
1794
|
+
<button onClick={checkStats} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition">
|
|
1795
|
+
Get Stats
|
|
1796
|
+
</button>
|
|
1797
|
+
</div>
|
|
1798
|
+
<ResponseDisplay response={response} loading={loading} />
|
|
1799
|
+
</Card>
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function UsersPanel() {
|
|
1804
|
+
const queryClient = useQueryClient();
|
|
1805
|
+
const [response, setResponse] = useState<ApiResponse | null>(null);
|
|
1806
|
+
const [loading, setLoading] = useState(false);
|
|
1807
|
+
const [newUser, setNewUser] = useState({ name: '', email: '', role: 'user' as const });
|
|
1808
|
+
|
|
1809
|
+
const { data: users, isLoading } = useQuery({
|
|
1810
|
+
queryKey: ['users'],
|
|
1811
|
+
queryFn: async () => {
|
|
1812
|
+
const res = await fetchApi('/users');
|
|
1813
|
+
return res.data as Array<{ id: string; name: string; email: string; role: string }>;
|
|
1814
|
+
},
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
const createUser = async () => {
|
|
1818
|
+
if (!newUser.name || !newUser.email) return;
|
|
1819
|
+
setLoading(true);
|
|
1820
|
+
const res = await fetchApi('/users', {
|
|
1821
|
+
method: 'POST',
|
|
1822
|
+
body: JSON.stringify(newUser),
|
|
1823
|
+
});
|
|
1824
|
+
setResponse(res);
|
|
1825
|
+
setLoading(false);
|
|
1826
|
+
setNewUser({ name: '', email: '', role: 'user' });
|
|
1827
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
const deleteUser = async (id: string) => {
|
|
1831
|
+
setLoading(true);
|
|
1832
|
+
const res = await fetchApi(\`/users/\${id}\`, { method: 'DELETE' });
|
|
1833
|
+
setResponse(res);
|
|
1834
|
+
setLoading(false);
|
|
1835
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
return (
|
|
1839
|
+
<Card title="Users" icon={Users}>
|
|
1840
|
+
<div className="space-y-4">
|
|
1841
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1842
|
+
<input
|
|
1843
|
+
placeholder="Name"
|
|
1844
|
+
value={newUser.name}
|
|
1845
|
+
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
|
1846
|
+
className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
1847
|
+
/>
|
|
1848
|
+
<input
|
|
1849
|
+
placeholder="Email"
|
|
1850
|
+
value={newUser.email}
|
|
1851
|
+
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
|
1852
|
+
className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
1853
|
+
/>
|
|
1854
|
+
<button onClick={createUser} disabled={loading} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition flex items-center justify-center gap-1">
|
|
1855
|
+
<Plus className="w-4 h-4" /> Add User
|
|
1856
|
+
</button>
|
|
1857
|
+
</div>
|
|
1858
|
+
|
|
1859
|
+
<div className="space-y-2">
|
|
1860
|
+
{isLoading ? (
|
|
1861
|
+
<div className="text-slate-400 text-sm">Loading users...</div>
|
|
1862
|
+
) : (
|
|
1863
|
+
users?.map((user) => (
|
|
1864
|
+
<div key={user.id} className="flex items-center justify-between p-2 bg-slate-900 rounded border border-slate-700">
|
|
1865
|
+
<div>
|
|
1866
|
+
<span className="font-medium">{user.name}</span>
|
|
1867
|
+
<span className="text-slate-400 text-sm ml-2">{user.email}</span>
|
|
1868
|
+
<Badge variant={user.role === 'admin' ? 'warning' : 'default'}>{user.role}</Badge>
|
|
1869
|
+
</div>
|
|
1870
|
+
<button onClick={() => deleteUser(user.id)} className="p-1 text-red-400 hover:text-red-300 transition">
|
|
1871
|
+
<Trash2 className="w-4 h-4" />
|
|
1872
|
+
</button>
|
|
1873
|
+
</div>
|
|
1874
|
+
))
|
|
1875
|
+
)}
|
|
1876
|
+
</div>
|
|
1877
|
+
|
|
1878
|
+
<ResponseDisplay response={response} loading={loading} />
|
|
1879
|
+
</div>
|
|
1880
|
+
</Card>
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function TasksPanel() {
|
|
1885
|
+
const queryClient = useQueryClient();
|
|
1886
|
+
const [response, setResponse] = useState<ApiResponse | null>(null);
|
|
1887
|
+
const [loading, setLoading] = useState(false);
|
|
1888
|
+
const [newTask, setNewTask] = useState({ title: '', priority: 'medium' as const });
|
|
1889
|
+
|
|
1890
|
+
const { data: tasks, isLoading } = useQuery({
|
|
1891
|
+
queryKey: ['tasks'],
|
|
1892
|
+
queryFn: async () => {
|
|
1893
|
+
const res = await fetchApi('/tasks');
|
|
1894
|
+
return res.data as Array<{ id: string; title: string; completed: boolean; priority: string }>;
|
|
1895
|
+
},
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
const createTask = async () => {
|
|
1899
|
+
if (!newTask.title) return;
|
|
1900
|
+
setLoading(true);
|
|
1901
|
+
const res = await fetchApi('/tasks', {
|
|
1902
|
+
method: 'POST',
|
|
1903
|
+
body: JSON.stringify(newTask),
|
|
1904
|
+
});
|
|
1905
|
+
setResponse(res);
|
|
1906
|
+
setLoading(false);
|
|
1907
|
+
setNewTask({ title: '', priority: 'medium' });
|
|
1908
|
+
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const toggleTask = async (id: string) => {
|
|
1912
|
+
setLoading(true);
|
|
1913
|
+
const res = await fetchApi(\`/tasks/\${id}/toggle\`, { method: 'PATCH' });
|
|
1914
|
+
setResponse(res);
|
|
1915
|
+
setLoading(false);
|
|
1916
|
+
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
const deleteTask = async (id: string) => {
|
|
1920
|
+
setLoading(true);
|
|
1921
|
+
const res = await fetchApi(\`/tasks/\${id}\`, { method: 'DELETE' });
|
|
1922
|
+
setResponse(res);
|
|
1923
|
+
setLoading(false);
|
|
1924
|
+
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
const priorityColor = (p: string) => {
|
|
1928
|
+
switch (p) {
|
|
1929
|
+
case 'high': return 'error';
|
|
1930
|
+
case 'medium': return 'warning';
|
|
1931
|
+
default: return 'default';
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
return (
|
|
1936
|
+
<Card title="Tasks" icon={CheckSquare}>
|
|
1937
|
+
<div className="space-y-4">
|
|
1938
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1939
|
+
<input
|
|
1940
|
+
placeholder="Task title"
|
|
1941
|
+
value={newTask.title}
|
|
1942
|
+
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
|
1943
|
+
className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
1944
|
+
/>
|
|
1945
|
+
<select
|
|
1946
|
+
value={newTask.priority}
|
|
1947
|
+
onChange={(e) => setNewTask({ ...newTask, priority: e.target.value as 'low' | 'medium' | 'high' })}
|
|
1948
|
+
className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
1949
|
+
>
|
|
1950
|
+
<option value="low">Low</option>
|
|
1951
|
+
<option value="medium">Medium</option>
|
|
1952
|
+
<option value="high">High</option>
|
|
1953
|
+
</select>
|
|
1954
|
+
<button onClick={createTask} disabled={loading} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition flex items-center justify-center gap-1">
|
|
1955
|
+
<Plus className="w-4 h-4" /> Add Task
|
|
1956
|
+
</button>
|
|
1957
|
+
</div>
|
|
1958
|
+
|
|
1959
|
+
<div className="space-y-2">
|
|
1960
|
+
{isLoading ? (
|
|
1961
|
+
<div className="text-slate-400 text-sm">Loading tasks...</div>
|
|
1962
|
+
) : (
|
|
1963
|
+
tasks?.map((task) => (
|
|
1964
|
+
<div key={task.id} className="flex items-center justify-between p-2 bg-slate-900 rounded border border-slate-700">
|
|
1965
|
+
<div className="flex items-center gap-2">
|
|
1966
|
+
<input
|
|
1967
|
+
type="checkbox"
|
|
1968
|
+
checked={task.completed}
|
|
1969
|
+
onChange={() => toggleTask(task.id)}
|
|
1970
|
+
className="rounded"
|
|
1971
|
+
/>
|
|
1972
|
+
<span className={task.completed ? 'line-through text-slate-500' : ''}>{task.title}</span>
|
|
1973
|
+
<Badge variant={priorityColor(task.priority) as 'default' | 'success' | 'warning' | 'error'}>{task.priority}</Badge>
|
|
1974
|
+
</div>
|
|
1975
|
+
<button onClick={() => deleteTask(task.id)} className="p-1 text-red-400 hover:text-red-300 transition">
|
|
1976
|
+
<Trash2 className="w-4 h-4" />
|
|
1977
|
+
</button>
|
|
1978
|
+
</div>
|
|
1979
|
+
))
|
|
1980
|
+
)}
|
|
1981
|
+
</div>
|
|
1982
|
+
|
|
1983
|
+
<ResponseDisplay response={response} loading={loading} />
|
|
1984
|
+
</div>
|
|
1985
|
+
</Card>
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function ToolsPanel() {
|
|
1990
|
+
const [echoMessage, setEchoMessage] = useState('');
|
|
1991
|
+
const [validateData, setValidateData] = useState({ email: '', age: '' });
|
|
1992
|
+
const [response, setResponse] = useState<ApiResponse | null>(null);
|
|
1993
|
+
const [loading, setLoading] = useState(false);
|
|
1994
|
+
|
|
1995
|
+
const testEcho = async () => {
|
|
1996
|
+
if (!echoMessage) return;
|
|
1997
|
+
setLoading(true);
|
|
1998
|
+
const res = await fetchApi(\`/echo?message=\${encodeURIComponent(echoMessage)}\`);
|
|
1999
|
+
setResponse(res);
|
|
2000
|
+
setLoading(false);
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
const testValidate = async () => {
|
|
2004
|
+
setLoading(true);
|
|
2005
|
+
const res = await fetchApi('/validate', {
|
|
2006
|
+
method: 'POST',
|
|
2007
|
+
body: JSON.stringify({
|
|
2008
|
+
email: validateData.email,
|
|
2009
|
+
age: parseInt(validateData.age) || 0,
|
|
2010
|
+
}),
|
|
2011
|
+
});
|
|
2012
|
+
setResponse(res);
|
|
2013
|
+
setLoading(false);
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
return (
|
|
2017
|
+
<Card title="Test Tools" icon={Zap}>
|
|
2018
|
+
<div className="space-y-6">
|
|
2019
|
+
<div>
|
|
2020
|
+
<h4 className="text-sm font-medium text-slate-300 mb-2">Echo Endpoint</h4>
|
|
2021
|
+
<div className="flex gap-2">
|
|
2022
|
+
<input
|
|
2023
|
+
placeholder="Message to echo"
|
|
2024
|
+
value={echoMessage}
|
|
2025
|
+
onChange={(e) => setEchoMessage(e.target.value)}
|
|
2026
|
+
className="flex-1 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
2027
|
+
/>
|
|
2028
|
+
<button onClick={testEcho} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition flex items-center gap-1">
|
|
2029
|
+
<Send className="w-4 h-4" /> Echo
|
|
2030
|
+
</button>
|
|
2031
|
+
</div>
|
|
2032
|
+
</div>
|
|
2033
|
+
|
|
2034
|
+
<div>
|
|
2035
|
+
<h4 className="text-sm font-medium text-slate-300 mb-2">Validation Endpoint</h4>
|
|
2036
|
+
<div className="flex gap-2">
|
|
2037
|
+
<input
|
|
2038
|
+
placeholder="Email"
|
|
2039
|
+
value={validateData.email}
|
|
2040
|
+
onChange={(e) => setValidateData({ ...validateData, email: e.target.value })}
|
|
2041
|
+
className="flex-1 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
2042
|
+
/>
|
|
2043
|
+
<input
|
|
2044
|
+
placeholder="Age"
|
|
2045
|
+
type="number"
|
|
2046
|
+
value={validateData.age}
|
|
2047
|
+
onChange={(e) => setValidateData({ ...validateData, age: e.target.value })}
|
|
2048
|
+
className="w-24 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
|
|
2049
|
+
/>
|
|
2050
|
+
<button onClick={testValidate} className="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 rounded text-sm font-medium transition flex items-center gap-1">
|
|
2051
|
+
<CheckSquare className="w-4 h-4" /> Validate
|
|
2052
|
+
</button>
|
|
2053
|
+
</div>
|
|
2054
|
+
</div>
|
|
2055
|
+
|
|
2056
|
+
<ResponseDisplay response={response} loading={loading} />
|
|
2057
|
+
</div>
|
|
2058
|
+
</Card>
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
export default function App() {
|
|
2063
|
+
const [activeTab, setActiveTab] = useState<Tab>('health');
|
|
2064
|
+
|
|
2065
|
+
const tabs: { id: Tab; label: string; icon: React.ElementType }[] = [
|
|
2066
|
+
{ id: 'health', label: 'Health', icon: Activity },
|
|
2067
|
+
{ id: 'users', label: 'Users', icon: Users },
|
|
2068
|
+
{ id: 'tasks', label: 'Tasks', icon: CheckSquare },
|
|
2069
|
+
{ id: 'tools', label: 'Tools', icon: Zap },
|
|
2070
|
+
];
|
|
2071
|
+
|
|
2072
|
+
return (
|
|
2073
|
+
<div className="min-h-screen p-6">
|
|
2074
|
+
<div className="max-w-4xl mx-auto">
|
|
2075
|
+
<header className="mb-8">
|
|
2076
|
+
<div className="flex items-center gap-3 mb-2">
|
|
2077
|
+
<Server className="w-8 h-8 text-blue-400" />
|
|
2078
|
+
<h1 className="text-3xl font-bold">Kozo API Tester</h1>
|
|
2079
|
+
</div>
|
|
2080
|
+
<p className="text-slate-400">Test the ${projectName} API endpoints with this interactive UI</p>
|
|
2081
|
+
</header>
|
|
2082
|
+
|
|
2083
|
+
<nav className="flex gap-1 mb-6 bg-slate-800 p-1 rounded-lg">
|
|
2084
|
+
{tabs.map(({ id, label, icon: Icon }) => (
|
|
2085
|
+
<button
|
|
2086
|
+
key={id}
|
|
2087
|
+
onClick={() => setActiveTab(id)}
|
|
2088
|
+
className={\`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition \${
|
|
2089
|
+
activeTab === id
|
|
2090
|
+
? 'bg-blue-600 text-white'
|
|
2091
|
+
: 'text-slate-400 hover:text-white hover:bg-slate-700'
|
|
2092
|
+
}\`}
|
|
2093
|
+
>
|
|
2094
|
+
<Icon className="w-4 h-4" />
|
|
2095
|
+
{label}
|
|
2096
|
+
</button>
|
|
2097
|
+
))}
|
|
2098
|
+
</nav>
|
|
2099
|
+
|
|
2100
|
+
<main>
|
|
2101
|
+
{activeTab === 'health' && <HealthPanel />}
|
|
2102
|
+
{activeTab === 'users' && <UsersPanel />}
|
|
2103
|
+
{activeTab === 'tasks' && <TasksPanel />}
|
|
2104
|
+
{activeTab === 'tools' && <ToolsPanel />}
|
|
2105
|
+
</main>
|
|
2106
|
+
|
|
2107
|
+
<footer className="mt-8 pt-6 border-t border-slate-800 text-center text-slate-500 text-sm">
|
|
2108
|
+
Built with Kozo + React + TailwindCSS
|
|
2109
|
+
</footer>
|
|
2110
|
+
</div>
|
|
2111
|
+
</div>
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
`;
|
|
2115
|
+
}
|
|
2116
|
+
async function scaffoldFullstackReadme(projectDir, projectName) {
|
|
2117
|
+
const readme = `# ${projectName}
|
|
2118
|
+
|
|
2119
|
+
Full-stack application built with **Kozo** framework and React.
|
|
2120
|
+
|
|
2121
|
+
## Project Structure
|
|
2122
|
+
|
|
2123
|
+
\`\`\`
|
|
2124
|
+
apps/
|
|
2125
|
+
\u251C\u2500\u2500 api/ # Backend Kozo
|
|
2126
|
+
\u2502 \u2514\u2500\u2500 src/
|
|
2127
|
+
\u2502 \u251C\u2500\u2500 data/ # Schemas e dati in-memory
|
|
2128
|
+
\u2502 \u2502 \u2514\u2500\u2500 index.ts
|
|
2129
|
+
\u2502 \u251C\u2500\u2500 routes/ # Routes organizzate per risorsa
|
|
2130
|
+
\u2502 \u2502 \u251C\u2500\u2500 health.ts # Health check e stats
|
|
2131
|
+
\u2502 \u2502 \u251C\u2500\u2500 users.ts # CRUD Users
|
|
2132
|
+
\u2502 \u2502 \u251C\u2500\u2500 posts.ts # CRUD Posts
|
|
2133
|
+
\u2502 \u2502 \u251C\u2500\u2500 tasks.ts # CRUD Tasks
|
|
2134
|
+
\u2502 \u2502 \u251C\u2500\u2500 tools.ts # Utility endpoints
|
|
2135
|
+
\u2502 \u2502 \u2514\u2500\u2500 index.ts # Route registration
|
|
2136
|
+
\u2502 \u2514\u2500\u2500 index.ts # Entry point
|
|
2137
|
+
\u2514\u2500\u2500 web/ # Frontend React
|
|
2138
|
+
\u2514\u2500\u2500 src/
|
|
2139
|
+
\u251C\u2500\u2500 App.tsx # UI principale con tabs
|
|
2140
|
+
\u251C\u2500\u2500 main.tsx # Entry point
|
|
2141
|
+
\u2514\u2500\u2500 index.css # TailwindCSS v4
|
|
2142
|
+
\`\`\`
|
|
2143
|
+
|
|
2144
|
+
## Technologies
|
|
2145
|
+
|
|
2146
|
+
- **Backend**: Kozo (Hono-based), TypeScript, Zod
|
|
2147
|
+
- **Frontend**: React 18, TanStack Query, TailwindCSS v4, Lucide Icons
|
|
2148
|
+
- **Build**: Vite, pnpm workspace
|
|
2149
|
+
|
|
2150
|
+
## Installation
|
|
2151
|
+
|
|
2152
|
+
\`\`\`bash
|
|
2153
|
+
pnpm install
|
|
2154
|
+
\`\`\`
|
|
2155
|
+
|
|
2156
|
+
## Development
|
|
2157
|
+
|
|
2158
|
+
\`\`\`bash
|
|
2159
|
+
# Start API and Web in parallel
|
|
2160
|
+
pnpm dev
|
|
2161
|
+
|
|
2162
|
+
# Or separately:
|
|
2163
|
+
pnpm --filter @${projectName}/api dev # API on http://localhost:3000
|
|
2164
|
+
pnpm --filter @${projectName}/web dev # Web on http://localhost:5173
|
|
2165
|
+
\`\`\`
|
|
2166
|
+
|
|
2167
|
+
## API Endpoints
|
|
2168
|
+
|
|
2169
|
+
### Health & Stats
|
|
2170
|
+
- \`GET /api/health\` - Health check with uptime
|
|
2171
|
+
- \`GET /api/stats\` - Global statistics
|
|
2172
|
+
|
|
2173
|
+
### Users (Full CRUD)
|
|
2174
|
+
- \`GET /api/users\` - List all users
|
|
2175
|
+
- \`GET /api/users/:id\` - Get user by ID
|
|
2176
|
+
- \`POST /api/users\` - Create user
|
|
2177
|
+
- \`PUT /api/users/:id\` - Update user
|
|
2178
|
+
- \`DELETE /api/users/:id\` - Delete user
|
|
2179
|
+
|
|
2180
|
+
### Posts (Full CRUD)
|
|
2181
|
+
- \`GET /api/posts?published=true\` - List posts (optional filter)
|
|
2182
|
+
- \`GET /api/posts/:id\` - Get post by ID
|
|
2183
|
+
- \`POST /api/posts\` - Create post
|
|
2184
|
+
- \`PUT /api/posts/:id\` - Update post
|
|
2185
|
+
- \`DELETE /api/posts/:id\` - Delete post
|
|
2186
|
+
|
|
2187
|
+
### Tasks (Full CRUD)
|
|
2188
|
+
- \`GET /api/tasks?completed=true&priority=high\` - List tasks (optional filters)
|
|
2189
|
+
- \`GET /api/tasks/:id\` - Get task by ID
|
|
2190
|
+
- \`POST /api/tasks\` - Create task
|
|
2191
|
+
- \`PUT /api/tasks/:id\` - Update task
|
|
2192
|
+
- \`PATCH /api/tasks/:id/toggle\` - Toggle completion
|
|
2193
|
+
- \`DELETE /api/tasks/:id\` - Delete task
|
|
2194
|
+
|
|
2195
|
+
### Tools
|
|
2196
|
+
- \`GET /api/echo?message=hello\` - Echo test
|
|
2197
|
+
- \`POST /api/validate\` - Zod validation (email + age)
|
|
2198
|
+
|
|
2199
|
+
## Type Safety
|
|
2200
|
+
|
|
2201
|
+
The frontend uses Hono RPC client for type-safe API calls:
|
|
2202
|
+
\`\`\`typescript
|
|
2203
|
+
const res = await client.api.users.$get();
|
|
2204
|
+
const users = await res.json(); // Fully typed!
|
|
2205
|
+
\`\`\`
|
|
2206
|
+
`;
|
|
2207
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "README.md"), readme);
|
|
2208
|
+
}
|
|
1077
2209
|
|
|
1078
2210
|
// src/utils/ascii-art.ts
|
|
1079
2211
|
var import_picocolors = __toESM(require("picocolors"));
|
|
@@ -1113,39 +2245,64 @@ async function newCommand(projectName) {
|
|
|
1113
2245
|
}
|
|
1114
2246
|
});
|
|
1115
2247
|
},
|
|
2248
|
+
runtime: () => p.select({
|
|
2249
|
+
message: "Target runtime",
|
|
2250
|
+
options: [
|
|
2251
|
+
{ value: "node", label: "Node.js / Docker", hint: "Maximum compatibility (default)" },
|
|
2252
|
+
{ value: "cloudflare", label: "Cloudflare Workers", hint: "Edge-native, global deployment" },
|
|
2253
|
+
{ value: "bun", label: "Bun", hint: "Maximum local speed" }
|
|
2254
|
+
],
|
|
2255
|
+
initialValue: "node"
|
|
2256
|
+
}),
|
|
1116
2257
|
template: () => p.select({
|
|
1117
2258
|
message: "Template",
|
|
1118
2259
|
options: [
|
|
1119
2260
|
{ value: "complete", label: "Complete Server", hint: "Full production-ready app (Auth, CRUD, Stats)" },
|
|
1120
2261
|
{ value: "starter", label: "Starter", hint: "Minimal setup with database" },
|
|
1121
|
-
{ value: "
|
|
1122
|
-
{ value: "ecommerce", label: "E-commerce", hint: "Products + Orders (coming soon)" }
|
|
2262
|
+
{ value: "api-only", label: "API Only", hint: "Minimal, no database" }
|
|
1123
2263
|
]
|
|
1124
2264
|
}),
|
|
1125
2265
|
database: ({ results }) => {
|
|
1126
|
-
if (results.template === "complete") {
|
|
2266
|
+
if (results.template === "complete" || results.template === "api-only") {
|
|
1127
2267
|
return Promise.resolve("none");
|
|
1128
2268
|
}
|
|
1129
2269
|
return p.select({
|
|
1130
|
-
message: "Database
|
|
2270
|
+
message: "Database",
|
|
1131
2271
|
options: [
|
|
1132
|
-
{ value: "
|
|
1133
|
-
{ value: "
|
|
1134
|
-
{ value: "
|
|
2272
|
+
{ value: "sqlite", label: "SQLite / Turso", hint: "Perfect for Edge and local dev" },
|
|
2273
|
+
{ value: "postgresql", label: "PostgreSQL + Drizzle", hint: "Standard Enterprise" },
|
|
2274
|
+
{ value: "mysql", label: "MySQL + Drizzle", hint: "PlanetScale compatible" }
|
|
1135
2275
|
]
|
|
1136
2276
|
});
|
|
1137
2277
|
},
|
|
2278
|
+
frontend: () => p.select({
|
|
2279
|
+
message: "Frontend",
|
|
2280
|
+
options: [
|
|
2281
|
+
{ value: "none", label: "None (API only)", hint: "Backend microservice" },
|
|
2282
|
+
{ value: "react", label: "React (Vite + TanStack Query)", hint: "Full-stack type-safe" },
|
|
2283
|
+
{ value: "solid", label: "SolidJS (Vite)", hint: "Performance purist choice" },
|
|
2284
|
+
{ value: "vue", label: "Vue (Vite)", hint: "Progressive framework" }
|
|
2285
|
+
],
|
|
2286
|
+
initialValue: "none"
|
|
2287
|
+
}),
|
|
2288
|
+
extras: () => p.multiselect({
|
|
2289
|
+
message: "Extras",
|
|
2290
|
+
options: [
|
|
2291
|
+
{ value: "docker", label: "Docker", hint: "Multi-stage Dockerfile" },
|
|
2292
|
+
{ value: "github-actions", label: "GitHub Actions", hint: "CI/CD pipeline" },
|
|
2293
|
+
{ value: "auth", label: "Authentication", hint: "JWT middleware + login routes" }
|
|
2294
|
+
],
|
|
2295
|
+
required: false
|
|
2296
|
+
}),
|
|
1138
2297
|
packageSource: () => p.select({
|
|
1139
|
-
message: "Package source
|
|
2298
|
+
message: "Package source",
|
|
1140
2299
|
options: [
|
|
1141
|
-
{ value: "npm", label: "npm registry", hint: "
|
|
1142
|
-
{ value: "local", label: "Local workspace", hint: "Link to
|
|
2300
|
+
{ value: "npm", label: "npm registry", hint: "Published version (recommended)" },
|
|
2301
|
+
{ value: "local", label: "Local workspace", hint: "Link to monorepo (dev only)" }
|
|
1143
2302
|
],
|
|
1144
2303
|
initialValue: "npm"
|
|
1145
2304
|
}),
|
|
1146
|
-
install: () =>
|
|
1147
|
-
return Promise.resolve(true);
|
|
1148
|
-
}
|
|
2305
|
+
install: () => Promise.resolve(true)
|
|
1149
2306
|
},
|
|
1150
2307
|
{
|
|
1151
2308
|
onCancel: () => {
|
|
@@ -1159,8 +2316,11 @@ async function newCommand(projectName) {
|
|
|
1159
2316
|
try {
|
|
1160
2317
|
await scaffoldProject({
|
|
1161
2318
|
projectName: project.name,
|
|
1162
|
-
|
|
2319
|
+
runtime: project.runtime,
|
|
1163
2320
|
template: project.template,
|
|
2321
|
+
database: project.database,
|
|
2322
|
+
frontend: project.frontend,
|
|
2323
|
+
extras: project.extras,
|
|
1164
2324
|
packageSource: project.packageSource
|
|
1165
2325
|
});
|
|
1166
2326
|
s.stop("Project structure created!");
|
|
@@ -1197,7 +2357,7 @@ ${import_picocolors2.default.bold("Next steps:")}
|
|
|
1197
2357
|
${import_picocolors2.default.cyan(`cd ${project.name}`)}
|
|
1198
2358
|
${!project.install ? import_picocolors2.default.cyan("pnpm install") + "\n " : ""}${import_picocolors2.default.cyan("pnpm dev")}
|
|
1199
2359
|
|
|
1200
|
-
${import_picocolors2.default.dim("Documentation:")} ${import_picocolors2.default.underline("https://
|
|
2360
|
+
${import_picocolors2.default.dim("Documentation:")} ${import_picocolors2.default.underline("https://kozojs.dev/docs")}
|
|
1201
2361
|
`);
|
|
1202
2362
|
}
|
|
1203
2363
|
|