@typokit/cli 0.1.4
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/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +13 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +42 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +302 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +106 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +536 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/generate.d.ts +65 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +430 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +579 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/migrate.d.ts +70 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +570 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/scaffold.d.ts +70 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +483 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/test.d.ts +56 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +248 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +33 -0
- package/dist/logger.js.map +1 -0
- package/package.json +33 -0
- package/src/bin.ts +22 -0
- package/src/commands/build.ts +433 -0
- package/src/commands/dev.ts +822 -0
- package/src/commands/generate.ts +640 -0
- package/src/commands/inspect.ts +885 -0
- package/src/commands/migrate.ts +800 -0
- package/src/commands/scaffold.ts +627 -0
- package/src/commands/test.ts +353 -0
- package/src/config.ts +93 -0
- package/src/dev.test.ts +285 -0
- package/src/env.d.ts +86 -0
- package/src/generate.test.ts +304 -0
- package/src/index.test.ts +217 -0
- package/src/index.ts +397 -0
- package/src/inspect.test.ts +411 -0
- package/src/logger.ts +49 -0
- package/src/migrate.test.ts +205 -0
- package/src/scaffold.test.ts +256 -0
- package/src/test.test.ts +230 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
// @typokit/cli — Scaffold Commands (init, add route, add service)
|
|
2
|
+
|
|
3
|
+
import type { CliLogger } from "../logger.js";
|
|
4
|
+
|
|
5
|
+
export interface ScaffoldCommandOptions {
|
|
6
|
+
/** Project root directory */
|
|
7
|
+
rootDir: string;
|
|
8
|
+
/** Logger instance */
|
|
9
|
+
logger: CliLogger;
|
|
10
|
+
/** Scaffold subcommand: "init", "route", "service" */
|
|
11
|
+
subcommand: string;
|
|
12
|
+
/** Positional arguments (e.g., route/service name) */
|
|
13
|
+
positional: string[];
|
|
14
|
+
/** CLI flags */
|
|
15
|
+
flags: Record<string, string | boolean>;
|
|
16
|
+
/** Whether verbose mode is enabled */
|
|
17
|
+
verbose: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScaffoldResult {
|
|
21
|
+
/** Whether the command succeeded */
|
|
22
|
+
success: boolean;
|
|
23
|
+
/** Files created */
|
|
24
|
+
filesCreated: string[];
|
|
25
|
+
/** Duration in milliseconds */
|
|
26
|
+
duration: number;
|
|
27
|
+
/** Errors encountered */
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InitOptions {
|
|
32
|
+
/** Project name */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Server adapter to use */
|
|
35
|
+
server: "native" | "fastify" | "hono" | "express";
|
|
36
|
+
/** Database adapter to use */
|
|
37
|
+
db: "drizzle" | "kysely" | "prisma" | "raw" | "none";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Generate a route contracts.ts template */
|
|
41
|
+
export function generateRouteContracts(name: string): string {
|
|
42
|
+
const pascalName = toPascalCase(name);
|
|
43
|
+
return `// Route contracts for ${name}
|
|
44
|
+
import type { RouteContract } from "@typokit/types";
|
|
45
|
+
|
|
46
|
+
/** ${pascalName} item type */
|
|
47
|
+
export interface ${pascalName} {
|
|
48
|
+
id: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create ${pascalName} request body */
|
|
54
|
+
export interface Create${pascalName}Body {
|
|
55
|
+
// TODO: Define create fields
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Update ${pascalName} request body */
|
|
59
|
+
export interface Update${pascalName}Body {
|
|
60
|
+
// TODO: Define update fields
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Route contracts for /${name} */
|
|
64
|
+
export interface ${pascalName}Routes {
|
|
65
|
+
"GET /${name}": RouteContract<Record<string, never>, { limit?: number; offset?: number }, never, ${pascalName}[]>;
|
|
66
|
+
"GET /${name}/:id": RouteContract<{ id: string }, never, never, ${pascalName}>;
|
|
67
|
+
"POST /${name}": RouteContract<Record<string, never>, never, Create${pascalName}Body, ${pascalName}>;
|
|
68
|
+
"PUT /${name}/:id": RouteContract<{ id: string }, never, Update${pascalName}Body, ${pascalName}>;
|
|
69
|
+
"DELETE /${name}/:id": RouteContract<{ id: string }, never, never, void>;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Generate a route handlers.ts template */
|
|
75
|
+
export function generateRouteHandlers(name: string): string {
|
|
76
|
+
const pascalName = toPascalCase(name);
|
|
77
|
+
return `// Route handlers for ${name}
|
|
78
|
+
import type { RouteHandler, RequestContext } from "@typokit/types";
|
|
79
|
+
import type { ${pascalName}, Create${pascalName}Body, Update${pascalName}Body } from "./contracts.ts";
|
|
80
|
+
|
|
81
|
+
/** List all ${name} */
|
|
82
|
+
export const list${pascalName}: RouteHandler = async (ctx: RequestContext) => {
|
|
83
|
+
const _query = ctx.query as { limit?: number; offset?: number };
|
|
84
|
+
// TODO: Implement list logic
|
|
85
|
+
return { status: 200, body: [] as ${pascalName}[] };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Get a single ${name} by ID */
|
|
89
|
+
export const get${pascalName}: RouteHandler = async (ctx: RequestContext) => {
|
|
90
|
+
const { id } = ctx.params as { id: string };
|
|
91
|
+
// TODO: Implement get logic
|
|
92
|
+
return { status: 200, body: { id } as ${pascalName} };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Create a new ${name} */
|
|
96
|
+
export const create${pascalName}: RouteHandler = async (ctx: RequestContext) => {
|
|
97
|
+
const _body = ctx.body as Create${pascalName}Body;
|
|
98
|
+
// TODO: Implement create logic
|
|
99
|
+
return { status: 201, body: {} as ${pascalName} };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Update an existing ${name} */
|
|
103
|
+
export const update${pascalName}: RouteHandler = async (ctx: RequestContext) => {
|
|
104
|
+
const { id } = ctx.params as { id: string };
|
|
105
|
+
const _body = ctx.body as Update${pascalName}Body;
|
|
106
|
+
// TODO: Implement update logic
|
|
107
|
+
return { status: 200, body: { id } as ${pascalName} };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Delete a ${name} */
|
|
111
|
+
export const delete${pascalName}: RouteHandler = async (ctx: RequestContext) => {
|
|
112
|
+
const { id } = ctx.params as { id: string };
|
|
113
|
+
// TODO: Implement delete logic
|
|
114
|
+
void id;
|
|
115
|
+
return { status: 204, body: undefined };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Default export: all handlers for registration in app.ts */
|
|
119
|
+
export default {
|
|
120
|
+
"GET /${name}": list${pascalName},
|
|
121
|
+
"GET /${name}/:id": get${pascalName},
|
|
122
|
+
"POST /${name}": create${pascalName},
|
|
123
|
+
"PUT /${name}/:id": update${pascalName},
|
|
124
|
+
"DELETE /${name}/:id": delete${pascalName},
|
|
125
|
+
};
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Generate a route middleware.ts template */
|
|
130
|
+
export function generateRouteMiddleware(name: string): string {
|
|
131
|
+
return `// Route-specific middleware for ${name}
|
|
132
|
+
import type { MiddlewareFn } from "@typokit/types";
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Example middleware for ${name} routes.
|
|
136
|
+
* Add route-specific middleware here (e.g., authorization, rate limiting).
|
|
137
|
+
*/
|
|
138
|
+
export const ${toCamelCase(name)}Middleware: MiddlewareFn = async (ctx, next) => {
|
|
139
|
+
// TODO: Implement route-specific middleware
|
|
140
|
+
return next(ctx);
|
|
141
|
+
};
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Generate a service template */
|
|
146
|
+
export function generateService(name: string): string {
|
|
147
|
+
const pascalName = toPascalCase(name);
|
|
148
|
+
return `// ${pascalName} service — business logic layer
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ${pascalName}Service handles business logic for ${name}.
|
|
152
|
+
* Keep handlers thin — put complex logic here.
|
|
153
|
+
*/
|
|
154
|
+
export class ${pascalName}Service {
|
|
155
|
+
/**
|
|
156
|
+
* Example method. Replace with actual business logic.
|
|
157
|
+
*/
|
|
158
|
+
async execute(): Promise<void> {
|
|
159
|
+
// TODO: Implement ${name} business logic
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Singleton instance for convenience */
|
|
164
|
+
export const ${toCamelCase(name)}Service = new ${pascalName}Service();
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Generate package.json for a new project */
|
|
169
|
+
export function generatePackageJson(options: InitOptions): string {
|
|
170
|
+
const deps: Record<string, string> = {
|
|
171
|
+
"@typokit/core": "^0.1.0",
|
|
172
|
+
"@typokit/types": "^0.1.0",
|
|
173
|
+
"@typokit/errors": "^0.1.0",
|
|
174
|
+
"@typokit/cli": "^0.1.0",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (options.server !== "native") {
|
|
178
|
+
deps[`@typokit/server-${options.server}`] = "^0.1.0";
|
|
179
|
+
} else {
|
|
180
|
+
deps["@typokit/server-native"] = "^0.1.0";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (options.db !== "none") {
|
|
184
|
+
deps[`@typokit/db-${options.db}`] = "^0.1.0";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
JSON.stringify(
|
|
189
|
+
{
|
|
190
|
+
name: options.name,
|
|
191
|
+
version: "0.1.0",
|
|
192
|
+
type: "module",
|
|
193
|
+
private: true,
|
|
194
|
+
scripts: {
|
|
195
|
+
build: "typokit build",
|
|
196
|
+
dev: "typokit dev",
|
|
197
|
+
test: "typokit test",
|
|
198
|
+
"generate:db": "typokit generate:db",
|
|
199
|
+
"generate:client": "typokit generate:client",
|
|
200
|
+
typecheck: "tsc --noEmit",
|
|
201
|
+
},
|
|
202
|
+
dependencies: deps,
|
|
203
|
+
devDependencies: {
|
|
204
|
+
typescript: "^5.7.0",
|
|
205
|
+
"@typokit/transform-native": "^0.1.0",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
null,
|
|
209
|
+
2,
|
|
210
|
+
) + "\n"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Generate tsconfig.json for a new project */
|
|
215
|
+
export function generateTsconfig(): string {
|
|
216
|
+
return (
|
|
217
|
+
JSON.stringify(
|
|
218
|
+
{
|
|
219
|
+
compilerOptions: {
|
|
220
|
+
target: "ES2022",
|
|
221
|
+
module: "NodeNext",
|
|
222
|
+
moduleResolution: "NodeNext",
|
|
223
|
+
allowImportingTsExtensions: true,
|
|
224
|
+
rewriteRelativeImportExtensions: true,
|
|
225
|
+
strict: true,
|
|
226
|
+
esModuleInterop: true,
|
|
227
|
+
skipLibCheck: true,
|
|
228
|
+
outDir: "dist",
|
|
229
|
+
rootDir: "src",
|
|
230
|
+
declaration: true,
|
|
231
|
+
declarationMap: true,
|
|
232
|
+
sourceMap: true,
|
|
233
|
+
},
|
|
234
|
+
include: ["src"],
|
|
235
|
+
},
|
|
236
|
+
null,
|
|
237
|
+
2,
|
|
238
|
+
) + "\n"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Generate the main app.ts for a new project */
|
|
243
|
+
export function generateAppTs(options: InitOptions): string {
|
|
244
|
+
const serverImport =
|
|
245
|
+
options.server === "native"
|
|
246
|
+
? `import { nativeServer } from "@typokit/server-native";`
|
|
247
|
+
: `import { ${options.server}Server } from "@typokit/server-${options.server}";`;
|
|
248
|
+
|
|
249
|
+
const serverValue =
|
|
250
|
+
options.server === "native"
|
|
251
|
+
? "nativeServer()"
|
|
252
|
+
: `${options.server}Server()`;
|
|
253
|
+
|
|
254
|
+
return `// Application entry point — explicit route registration
|
|
255
|
+
import { createApp } from "@typokit/core";
|
|
256
|
+
${serverImport}
|
|
257
|
+
|
|
258
|
+
export const app = createApp({
|
|
259
|
+
server: ${serverValue},
|
|
260
|
+
middleware: [],
|
|
261
|
+
routes: [
|
|
262
|
+
// Register route modules here:
|
|
263
|
+
// { prefix: "/users", handlers: usersHandlers },
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Start the server
|
|
268
|
+
app.listen({ port: 3000 }).then(() => {
|
|
269
|
+
console.log("Server running on http://localhost:3000");
|
|
270
|
+
});
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Generate the types.ts seed file for a new project */
|
|
275
|
+
export function generateTypesTs(): string {
|
|
276
|
+
return `// Schema type definitions — the single source of truth
|
|
277
|
+
// TypoKit generates validation, DB schema, OpenAPI, and client types from these interfaces.
|
|
278
|
+
// See: https://github.com/typokit/typokit#schema-types
|
|
279
|
+
|
|
280
|
+
/** @table */
|
|
281
|
+
export interface Example {
|
|
282
|
+
/** @id @generated */
|
|
283
|
+
id: string;
|
|
284
|
+
name: string;
|
|
285
|
+
createdAt: string;
|
|
286
|
+
updatedAt: string;
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Convert kebab-case or snake_case name to PascalCase */
|
|
292
|
+
export function toPascalCase(name: string): string {
|
|
293
|
+
return name
|
|
294
|
+
.split(/[-_]/)
|
|
295
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
296
|
+
.join("");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Convert kebab-case or snake_case name to camelCase */
|
|
300
|
+
export function toCamelCase(name: string): string {
|
|
301
|
+
const pascal = toPascalCase(name);
|
|
302
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Execute `typokit init` — create a new project from template.
|
|
307
|
+
*/
|
|
308
|
+
export async function scaffoldInit(
|
|
309
|
+
rootDir: string,
|
|
310
|
+
options: InitOptions,
|
|
311
|
+
logger: CliLogger,
|
|
312
|
+
): Promise<ScaffoldResult> {
|
|
313
|
+
const start = Date.now();
|
|
314
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
315
|
+
join: (...args: string[]) => string;
|
|
316
|
+
};
|
|
317
|
+
const { mkdirSync, writeFileSync, existsSync } = (await import(
|
|
318
|
+
/* @vite-ignore */ "fs"
|
|
319
|
+
)) as {
|
|
320
|
+
mkdirSync: (p: string, o?: { recursive?: boolean }) => void;
|
|
321
|
+
writeFileSync: (p: string, data: string) => void;
|
|
322
|
+
existsSync: (p: string) => boolean;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const projectDir = join(rootDir, options.name);
|
|
326
|
+
const filesCreated: string[] = [];
|
|
327
|
+
const errors: string[] = [];
|
|
328
|
+
|
|
329
|
+
// Check if directory already exists with content
|
|
330
|
+
if (existsSync(projectDir)) {
|
|
331
|
+
errors.push(`Directory "${options.name}" already exists`);
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
filesCreated,
|
|
335
|
+
duration: Date.now() - start,
|
|
336
|
+
errors,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// Create directory structure per Section 4.4
|
|
342
|
+
const dirs = [
|
|
343
|
+
projectDir,
|
|
344
|
+
join(projectDir, "src"),
|
|
345
|
+
join(projectDir, "src", "routes"),
|
|
346
|
+
join(projectDir, "src", "middleware"),
|
|
347
|
+
join(projectDir, "src", "services"),
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
for (const dir of dirs) {
|
|
351
|
+
mkdirSync(dir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Write package.json
|
|
355
|
+
const pkgPath = join(projectDir, "package.json");
|
|
356
|
+
writeFileSync(pkgPath, generatePackageJson(options));
|
|
357
|
+
filesCreated.push(pkgPath);
|
|
358
|
+
logger.info(`Created ${pkgPath}`);
|
|
359
|
+
|
|
360
|
+
// Write tsconfig.json
|
|
361
|
+
const tscPath = join(projectDir, "tsconfig.json");
|
|
362
|
+
writeFileSync(tscPath, generateTsconfig());
|
|
363
|
+
filesCreated.push(tscPath);
|
|
364
|
+
logger.info(`Created ${tscPath}`);
|
|
365
|
+
|
|
366
|
+
// Write src/app.ts
|
|
367
|
+
const appPath = join(projectDir, "src", "app.ts");
|
|
368
|
+
writeFileSync(appPath, generateAppTs(options));
|
|
369
|
+
filesCreated.push(appPath);
|
|
370
|
+
logger.info(`Created ${appPath}`);
|
|
371
|
+
|
|
372
|
+
// Write src/types.ts
|
|
373
|
+
const typesPath = join(projectDir, "src", "types.ts");
|
|
374
|
+
writeFileSync(typesPath, generateTypesTs());
|
|
375
|
+
filesCreated.push(typesPath);
|
|
376
|
+
logger.info(`Created ${typesPath}`);
|
|
377
|
+
|
|
378
|
+
logger.info(`\nProject "${options.name}" created successfully!`);
|
|
379
|
+
logger.info(`\n cd ${options.name}`);
|
|
380
|
+
logger.info(" npm install");
|
|
381
|
+
logger.info(" typokit dev\n");
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
success: true,
|
|
385
|
+
filesCreated,
|
|
386
|
+
duration: Date.now() - start,
|
|
387
|
+
errors,
|
|
388
|
+
};
|
|
389
|
+
} catch (err: unknown) {
|
|
390
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
391
|
+
errors.push(msg);
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
filesCreated,
|
|
395
|
+
duration: Date.now() - start,
|
|
396
|
+
errors,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Execute `typokit add route <name>` — scaffold a route module.
|
|
403
|
+
*/
|
|
404
|
+
export async function scaffoldRoute(
|
|
405
|
+
rootDir: string,
|
|
406
|
+
name: string,
|
|
407
|
+
logger: CliLogger,
|
|
408
|
+
): Promise<ScaffoldResult> {
|
|
409
|
+
const start = Date.now();
|
|
410
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
411
|
+
join: (...args: string[]) => string;
|
|
412
|
+
};
|
|
413
|
+
const { mkdirSync, writeFileSync, existsSync } = (await import(
|
|
414
|
+
/* @vite-ignore */ "fs"
|
|
415
|
+
)) as {
|
|
416
|
+
mkdirSync: (p: string, o?: { recursive?: boolean }) => void;
|
|
417
|
+
writeFileSync: (p: string, data: string) => void;
|
|
418
|
+
existsSync: (p: string) => boolean;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const filesCreated: string[] = [];
|
|
422
|
+
const errors: string[] = [];
|
|
423
|
+
|
|
424
|
+
if (!name) {
|
|
425
|
+
errors.push("Route name is required. Usage: typokit add route <name>");
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
filesCreated,
|
|
429
|
+
duration: Date.now() - start,
|
|
430
|
+
errors,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const routeDir = join(rootDir, "src", "routes", name);
|
|
435
|
+
|
|
436
|
+
if (existsSync(routeDir)) {
|
|
437
|
+
errors.push(
|
|
438
|
+
`Route directory "${name}" already exists at src/routes/${name}`,
|
|
439
|
+
);
|
|
440
|
+
return {
|
|
441
|
+
success: false,
|
|
442
|
+
filesCreated,
|
|
443
|
+
duration: Date.now() - start,
|
|
444
|
+
errors,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
mkdirSync(routeDir, { recursive: true });
|
|
450
|
+
|
|
451
|
+
// contracts.ts — route type contracts
|
|
452
|
+
const contractsPath = join(routeDir, "contracts.ts");
|
|
453
|
+
writeFileSync(contractsPath, generateRouteContracts(name));
|
|
454
|
+
filesCreated.push(contractsPath);
|
|
455
|
+
logger.info(`Created ${contractsPath}`);
|
|
456
|
+
|
|
457
|
+
// handlers.ts — handler implementations
|
|
458
|
+
const handlersPath = join(routeDir, "handlers.ts");
|
|
459
|
+
writeFileSync(handlersPath, generateRouteHandlers(name));
|
|
460
|
+
filesCreated.push(handlersPath);
|
|
461
|
+
logger.info(`Created ${handlersPath}`);
|
|
462
|
+
|
|
463
|
+
// middleware.ts — route-specific middleware
|
|
464
|
+
const middlewarePath = join(routeDir, "middleware.ts");
|
|
465
|
+
writeFileSync(middlewarePath, generateRouteMiddleware(name));
|
|
466
|
+
filesCreated.push(middlewarePath);
|
|
467
|
+
logger.info(`Created ${middlewarePath}`);
|
|
468
|
+
|
|
469
|
+
logger.info(`\nRoute "${name}" scaffolded at src/routes/${name}/`);
|
|
470
|
+
logger.info(" Don't forget to register it in src/app.ts!\n");
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
success: true,
|
|
474
|
+
filesCreated,
|
|
475
|
+
duration: Date.now() - start,
|
|
476
|
+
errors,
|
|
477
|
+
};
|
|
478
|
+
} catch (err: unknown) {
|
|
479
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
480
|
+
errors.push(msg);
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
filesCreated,
|
|
484
|
+
duration: Date.now() - start,
|
|
485
|
+
errors,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Execute `typokit add service <name>` — scaffold a service file.
|
|
492
|
+
*/
|
|
493
|
+
export async function scaffoldService(
|
|
494
|
+
rootDir: string,
|
|
495
|
+
name: string,
|
|
496
|
+
logger: CliLogger,
|
|
497
|
+
): Promise<ScaffoldResult> {
|
|
498
|
+
const start = Date.now();
|
|
499
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
500
|
+
join: (...args: string[]) => string;
|
|
501
|
+
};
|
|
502
|
+
const { mkdirSync, writeFileSync, existsSync } = (await import(
|
|
503
|
+
/* @vite-ignore */ "fs"
|
|
504
|
+
)) as {
|
|
505
|
+
mkdirSync: (p: string, o?: { recursive?: boolean }) => void;
|
|
506
|
+
writeFileSync: (p: string, data: string) => void;
|
|
507
|
+
existsSync: (p: string) => boolean;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const filesCreated: string[] = [];
|
|
511
|
+
const errors: string[] = [];
|
|
512
|
+
|
|
513
|
+
if (!name) {
|
|
514
|
+
errors.push("Service name is required. Usage: typokit add service <name>");
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
filesCreated,
|
|
518
|
+
duration: Date.now() - start,
|
|
519
|
+
errors,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const servicesDir = join(rootDir, "src", "services");
|
|
524
|
+
const servicePath = join(servicesDir, `${name}.service.ts`);
|
|
525
|
+
|
|
526
|
+
if (existsSync(servicePath)) {
|
|
527
|
+
errors.push(
|
|
528
|
+
`Service "${name}" already exists at src/services/${name}.service.ts`,
|
|
529
|
+
);
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
filesCreated,
|
|
533
|
+
duration: Date.now() - start,
|
|
534
|
+
errors,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
mkdirSync(servicesDir, { recursive: true });
|
|
540
|
+
|
|
541
|
+
writeFileSync(servicePath, generateService(name));
|
|
542
|
+
filesCreated.push(servicePath);
|
|
543
|
+
logger.info(`Created ${servicePath}`);
|
|
544
|
+
|
|
545
|
+
logger.info(
|
|
546
|
+
`\nService "${name}" scaffolded at src/services/${name}.service.ts\n`,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
filesCreated,
|
|
552
|
+
duration: Date.now() - start,
|
|
553
|
+
errors,
|
|
554
|
+
};
|
|
555
|
+
} catch (err: unknown) {
|
|
556
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
557
|
+
errors.push(msg);
|
|
558
|
+
return {
|
|
559
|
+
success: false,
|
|
560
|
+
filesCreated,
|
|
561
|
+
duration: Date.now() - start,
|
|
562
|
+
errors,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Execute scaffold commands dispatcher.
|
|
569
|
+
*/
|
|
570
|
+
export async function executeScaffold(
|
|
571
|
+
options: ScaffoldCommandOptions,
|
|
572
|
+
): Promise<ScaffoldResult> {
|
|
573
|
+
const { rootDir, logger, subcommand, positional, flags } = options;
|
|
574
|
+
|
|
575
|
+
if (subcommand === "init") {
|
|
576
|
+
const name =
|
|
577
|
+
positional[0] ??
|
|
578
|
+
(typeof flags["name"] === "string" ? flags["name"] : "my-app");
|
|
579
|
+
const server = parseServerFlag(flags["server"]);
|
|
580
|
+
const db = parseDbFlag(flags["db"]);
|
|
581
|
+
|
|
582
|
+
return scaffoldInit(rootDir, { name, server, db }, logger);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (subcommand === "route") {
|
|
586
|
+
const name = positional[0] ?? "";
|
|
587
|
+
return scaffoldRoute(rootDir, name, logger);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (subcommand === "service") {
|
|
591
|
+
const name = positional[0] ?? "";
|
|
592
|
+
return scaffoldService(rootDir, name, logger);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
success: false,
|
|
597
|
+
filesCreated: [],
|
|
598
|
+
duration: 0,
|
|
599
|
+
errors: [
|
|
600
|
+
`Unknown scaffold subcommand: "${subcommand}". Use: init, route, service`,
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Parse server adapter flag */
|
|
606
|
+
function parseServerFlag(
|
|
607
|
+
value: string | boolean | undefined,
|
|
608
|
+
): InitOptions["server"] {
|
|
609
|
+
if (typeof value === "string") {
|
|
610
|
+
const valid = ["native", "fastify", "hono", "express"] as const;
|
|
611
|
+
if (valid.includes(value as (typeof valid)[number])) {
|
|
612
|
+
return value as InitOptions["server"];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return "native";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Parse database adapter flag */
|
|
619
|
+
function parseDbFlag(value: string | boolean | undefined): InitOptions["db"] {
|
|
620
|
+
if (typeof value === "string") {
|
|
621
|
+
const valid = ["drizzle", "kysely", "prisma", "raw", "none"] as const;
|
|
622
|
+
if (valid.includes(value as (typeof valid)[number])) {
|
|
623
|
+
return value as InitOptions["db"];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return "none";
|
|
627
|
+
}
|