@ajke/cli 0.1.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.
@@ -0,0 +1,1150 @@
1
+ #!/usr/bin/env tsx
2
+ #!/usr/bin/env node
3
+
4
+ // bin/commands/new.ts
5
+ import { mkdirSync, writeFileSync, existsSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ import { execSync } from "child_process";
8
+ function pascal(name) {
9
+ return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
10
+ }
11
+ function kebab(name) {
12
+ return name.replace(/([A-Z])/g, "-$1").replace(/^-/, "").replace(/_/g, "-").toLowerCase();
13
+ }
14
+ function packageJsonTemplate(name) {
15
+ return JSON.stringify(
16
+ {
17
+ name: kebab(name),
18
+ version: "0.0.1",
19
+ private: true,
20
+ type: "module",
21
+ scripts: {
22
+ dev: "vite dev --host --port 4001",
23
+ "dev:cf": "wrangler dev --port 4001",
24
+ build: "vite build",
25
+ deploy: "vite build && wrangler deploy",
26
+ "cf-typegen": "wrangler types --env-interface CloudflareBindings",
27
+ test: "vitest --run",
28
+ "test:watch": "vitest --watch",
29
+ "db:gen": "drizzle-kit generate",
30
+ "db:migrate": "wrangler d1 migrations apply DB --local",
31
+ "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
32
+ check: "tsc --noEmit",
33
+ ajke: "tsx node_modules/@ajke/cli/bin/ajke.ts"
34
+ },
35
+ dependencies: {
36
+ hono: "^4.12.0",
37
+ "@ajke/core": "^0.1.0",
38
+ "reflect-metadata": "^0.2.2",
39
+ tsyringe: "^4.10.0",
40
+ zod: "^4.0.0",
41
+ "drizzle-orm": "^0.45.0",
42
+ ulid: "^3.0.0"
43
+ },
44
+ devDependencies: {
45
+ "@ajke/cli": "^0.1.0",
46
+ "@cloudflare/vite-plugin": "^1.36.0",
47
+ "@cloudflare/workers-types": "^4.0.0",
48
+ "@cloudflare/vitest-pool-workers": "^0.16.0",
49
+ "@types/node": "^22.0.0",
50
+ "drizzle-kit": "^0.31.0",
51
+ typescript: "^5.0.0",
52
+ vite: "^6.0.0",
53
+ vitest: "^4.1.0",
54
+ tsx: "^4.0.0",
55
+ wrangler: "^4.0.0"
56
+ }
57
+ },
58
+ null,
59
+ 2
60
+ );
61
+ }
62
+ function wranglerTemplate(name) {
63
+ return `{
64
+ "$schema": "node_modules/wrangler/config-schema.json",
65
+ "name": "${kebab(name)}",
66
+ "main": "./src/index.ts",
67
+ "compatibility_date": "2025-06-17",
68
+ "compatibility_flags": ["nodejs_compat"],
69
+ "vars": {
70
+ "MODE": "development"
71
+ },
72
+ "observability": { "enabled": true },
73
+ "d1_databases": [
74
+ {
75
+ "binding": "DB",
76
+ "database_name": "REPLACE_WITH_YOUR_D1_NAME",
77
+ "database_id": "00000000-0000-0000-0000-000000000001",
78
+ "migrations_dir": "migrations",
79
+ "preview_database_id": "00000000-0000-0000-0000-000000000002"
80
+ }
81
+ ],
82
+ "dev": { "port": 4001 }
83
+ }
84
+ `;
85
+ }
86
+ function tsconfigTemplate() {
87
+ return `{
88
+ "compilerOptions": {
89
+ "target": "ESNext",
90
+ "module": "ESNext",
91
+ "moduleResolution": "Bundler",
92
+ "strict": true,
93
+ "skipLibCheck": true,
94
+ "experimentalDecorators": true,
95
+ "emitDecoratorMetadata": true,
96
+ "lib": ["ESNext", "DOM"],
97
+ "types": ["vite/client", "@cloudflare/workers-types"],
98
+ "baseUrl": ".",
99
+ "paths": {
100
+ "@app/*": ["src/*"]
101
+ }
102
+ },
103
+ "include": ["src/**/*", "worker-configuration.d.ts"],
104
+ "exclude": ["node_modules", "dist"]
105
+ }
106
+ `;
107
+ }
108
+ function viteConfigTemplate() {
109
+ return `import { defineConfig } from "vite";
110
+ import { cloudflare } from "@cloudflare/vite-plugin";
111
+ import { fileURLToPath, URL } from "node:url";
112
+
113
+ export default defineConfig({
114
+ plugins: [cloudflare()],
115
+ resolve: {
116
+ alias: {
117
+ "@app": fileURLToPath(new URL("./src", import.meta.url)),
118
+ },
119
+ },
120
+ });
121
+ `;
122
+ }
123
+ function workerTypesTemplate() {
124
+ return `// Generated by \`pnpm cf-typegen\`. Do not edit manually.
125
+ /// <reference types="@cloudflare/vitest-pool-workers/types" />
126
+ interface CloudflareBindings {
127
+ DB: D1Database;
128
+ MODE: string;
129
+ }
130
+
131
+ declare namespace Cloudflare {
132
+ interface Env extends CloudflareBindings {
133
+ TEST_MIGRATIONS: string;
134
+ }
135
+ }
136
+ `;
137
+ }
138
+ function vitestConfigTemplate() {
139
+ return `import path from "path";
140
+ import { defineConfig } from "vitest/config";
141
+ import { cloudflarePool, cloudflareTest, readD1Migrations } from "@cloudflare/vitest-pool-workers";
142
+
143
+ export default defineConfig(async () => {
144
+ const migrations = await readD1Migrations("./migrations");
145
+ const poolOptions = {
146
+ wrangler: { configPath: "./wrangler.jsonc" },
147
+ miniflare: {
148
+ bindings: { TEST_MIGRATIONS: JSON.stringify(migrations) },
149
+ },
150
+ };
151
+ return {
152
+ plugins: [cloudflareTest(poolOptions)],
153
+ resolve: {
154
+ alias: { "@app": path.resolve("./src") },
155
+ },
156
+ test: {
157
+ pool: cloudflarePool(poolOptions),
158
+ },
159
+ };
160
+ });
161
+ `;
162
+ }
163
+ function indexTemplate() {
164
+ return `import "reflect-metadata";
165
+ import { app } from "./app.module";
166
+
167
+ export default {
168
+ async fetch(request: Request, env: CloudflareBindings, ctx: ExecutionContext): Promise<Response> {
169
+ return app.fetch(request, env, ctx);
170
+ },
171
+ };
172
+ `;
173
+ }
174
+ function appModuleTemplate(name) {
175
+ const p = pascal(name);
176
+ return `import { cors } from "hono/cors";
177
+ import { secureHeaders } from "hono/secure-headers";
178
+
179
+ import { Module, createModule } from "@ajke/core";
180
+ import { requestLogger } from "@ajke/core/middleware";
181
+ import { HelloModule } from "./modules/app/hello/hello.module";
182
+
183
+ @Module({
184
+ imports: [HelloModule],
185
+ })
186
+ export class AppModule {}
187
+
188
+ const app = createModule(AppModule, {
189
+ middlewares: [cors(), secureHeaders(), requestLogger],
190
+ });
191
+
192
+ app.get("/", (c) => c.json({ success: true, status: "healthy", timestamp: new Date().toISOString() }));
193
+ app.get("/health", (c) => c.json({ success: true, status: "healthy", timestamp: new Date().toISOString() }));
194
+
195
+ export { app };
196
+ `;
197
+ }
198
+ function helloModuleTemplate() {
199
+ return `import { Module } from "@ajke/core";
200
+ import { HelloController } from "./hello.controller";
201
+ import { HelloService } from "./hello.service";
202
+
203
+ @Module({
204
+ controllers: [HelloController],
205
+ providers: [HelloService],
206
+ })
207
+ export class HelloModule {}
208
+ `;
209
+ }
210
+ function helloControllerTemplate() {
211
+ return `import type { Context } from "hono";
212
+ import { Controller, Get, Inject } from "@ajke/core";
213
+ import { ResponseUtil } from "@ajke/core";
214
+ import { HelloService } from "./hello.service";
215
+
216
+ @Controller("/hello")
217
+ export class HelloController {
218
+ constructor(@Inject(HelloService) private helloService: HelloService) {}
219
+
220
+ @Get()
221
+ async greet(c: Context) {
222
+ const message = this.helloService.greet();
223
+ return ResponseUtil.success(c, { message });
224
+ }
225
+ }
226
+ `;
227
+ }
228
+ function helloServiceTemplate() {
229
+ return `import { Injectable } from "@ajke/core";
230
+
231
+ @Injectable()
232
+ export class HelloService {
233
+ greet() {
234
+ return "Hello from Ajke!";
235
+ }
236
+ }
237
+ `;
238
+ }
239
+ function dbSchemaTemplate() {
240
+ return `// Register all your entity tables here so Drizzle can build relations and
241
+ // drizzle-kit can generate migrations from a single source of truth.
242
+ export const schema = {};
243
+ `;
244
+ }
245
+ function dbConnectionTemplate() {
246
+ return `import { drizzle } from "drizzle-orm/d1";
247
+ import type { Context } from "hono";
248
+ import { schema } from "./schema";
249
+ export function getDb(c: Context) {
250
+ return drizzle(c.env.DB, { schema: schema });
251
+ }
252
+ `;
253
+ }
254
+ function gitignoreTemplate() {
255
+ return `dist/
256
+ node_modules/
257
+ .wrangler
258
+ .env
259
+ .env.production
260
+ .dev.vars
261
+ .DS_Store
262
+ coverage/
263
+ *.log
264
+ `;
265
+ }
266
+ function wiltConfigTemplate() {
267
+ return `import { defineConfig } from "@ajke/core/config";
268
+
269
+ export default defineConfig({
270
+ // Directory where generated modules are placed (relative to project root)
271
+ modulesDir: "src/modules/app",
272
+
273
+ // Default files scaffolded by \`ajke generate module <name>\`
274
+ // Remove "test" to skip test files, or pass --no-test at the CLI
275
+ generate: {
276
+ files: ["module", "controller", "service", "dto", "entity", "test"],
277
+ },
278
+ });
279
+ `;
280
+ }
281
+ function envExampleTemplate() {
282
+ return `CLOUDFLARE_ACCOUNT_ID=
283
+ CLOUDFLARE_TOKEN=
284
+
285
+ CLOUDFLARE_DATABASE_ID=
286
+ `;
287
+ }
288
+ function drizzleConfigTemplate() {
289
+ return `import { defineConfig } from "drizzle-kit";
290
+
291
+ export default defineConfig({
292
+ schema: "./src/database/schema.ts",
293
+ out: "./migrations",
294
+ dialect: "sqlite",
295
+ driver: "d1-http",
296
+ dbCredentials: {
297
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
298
+ databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
299
+ token: process.env.CLOUDFLARE_TOKEN!,
300
+ },
301
+ verbose: true,
302
+ strict: true,
303
+ });
304
+ `;
305
+ }
306
+ function migrationTemplate() {
307
+ return `-- Initial migration
308
+ -- Add your tables here
309
+ `;
310
+ }
311
+ function migrationMetaTemplate(name) {
312
+ return JSON.stringify(
313
+ {
314
+ version: "5",
315
+ dialect: "sqlite",
316
+ entries: [
317
+ {
318
+ idx: 0,
319
+ version: "5",
320
+ when: Date.now(),
321
+ tag: "0000_initial",
322
+ breakpoints: true
323
+ }
324
+ ]
325
+ },
326
+ null,
327
+ 2
328
+ );
329
+ }
330
+ function write(path, content) {
331
+ writeFileSync(path, content, "utf-8");
332
+ console.log(` \u2714 ${path.replace(process.cwd() + "/", "")}`);
333
+ }
334
+ function dir(path) {
335
+ mkdirSync(path, { recursive: true });
336
+ }
337
+ function runNew(args) {
338
+ const [name] = args;
339
+ if (!name) {
340
+ console.error(" \u2717 Usage: ajke new <project-name>");
341
+ process.exit(1);
342
+ }
343
+ const projectDir = resolve(process.cwd(), name);
344
+ if (existsSync(projectDir)) {
345
+ console.error(` \u2717 Directory already exists: ${projectDir}`);
346
+ process.exit(1);
347
+ }
348
+ console.log(`
349
+ Creating Ajke app "${name}"...
350
+ `);
351
+ dir(join(projectDir, "src/modules/app/hello"));
352
+ dir(join(projectDir, "src/database"));
353
+ dir(join(projectDir, "migrations/meta"));
354
+ write(join(projectDir, "package.json"), packageJsonTemplate(name));
355
+ write(join(projectDir, "wrangler.jsonc"), wranglerTemplate(name));
356
+ write(join(projectDir, "tsconfig.json"), tsconfigTemplate());
357
+ write(join(projectDir, "vite.config.ts"), viteConfigTemplate());
358
+ write(join(projectDir, "vitest.config.ts"), vitestConfigTemplate());
359
+ write(join(projectDir, "worker-configuration.d.ts"), workerTypesTemplate());
360
+ write(join(projectDir, "ajke.config.ts"), wiltConfigTemplate());
361
+ write(join(projectDir, ".gitignore"), gitignoreTemplate());
362
+ write(join(projectDir, ".env.example"), envExampleTemplate());
363
+ write(join(projectDir, "drizzle.config.ts"), drizzleConfigTemplate());
364
+ write(join(projectDir, "src/index.ts"), indexTemplate());
365
+ write(join(projectDir, "src/app.module.ts"), appModuleTemplate(name));
366
+ write(join(projectDir, "src/modules/app/hello/hello.module.ts"), helloModuleTemplate());
367
+ write(join(projectDir, "src/modules/app/hello/hello.controller.ts"), helloControllerTemplate());
368
+ write(join(projectDir, "src/modules/app/hello/hello.service.ts"), helloServiceTemplate());
369
+ write(join(projectDir, "src/database/schema.ts"), dbSchemaTemplate());
370
+ write(join(projectDir, "src/database/connection.ts"), dbConnectionTemplate());
371
+ write(join(projectDir, "migrations/0000_initial.sql"), migrationTemplate());
372
+ write(join(projectDir, "migrations/meta/_journal.json"), migrationMetaTemplate(name));
373
+ console.log("\n Installing dependencies...\n");
374
+ try {
375
+ const pm = detectPackageManager();
376
+ execSync(`${pm} install`, { cwd: projectDir, stdio: "inherit" });
377
+ } catch {
378
+ console.log(" \u26A0 Could not auto-install. Run `pnpm install` manually.\n");
379
+ }
380
+ console.log(`
381
+ \u2714 Project created!
382
+
383
+ Next steps:
384
+ cd ${name}
385
+ cp .env.example .env # fill in Cloudflare credentials
386
+ # Edit wrangler.jsonc: set database_name + database_id
387
+ pnpm db:migrate # apply local D1 migrations
388
+ pnpm dev # http://localhost:4001
389
+
390
+ Try it:
391
+ curl http://localhost:4001/health
392
+ curl http://localhost:4001/hello
393
+
394
+ Generate a new module:
395
+ pnpm ajke generate module posts
396
+ `);
397
+ }
398
+ function detectPackageManager() {
399
+ try {
400
+ execSync("pnpm --version", { stdio: "ignore" });
401
+ return "pnpm";
402
+ } catch {
403
+ }
404
+ try {
405
+ execSync("bun --version", { stdio: "ignore" });
406
+ return "bun";
407
+ } catch {
408
+ }
409
+ return "npm";
410
+ }
411
+
412
+ // bin/generators/module.generator.ts
413
+ import { writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
414
+ import { join as join3 } from "path";
415
+
416
+ // bin/utils/paths.ts
417
+ import { join as join2 } from "path";
418
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
419
+ var PROJECT_ROOT = process.cwd();
420
+ var SRC_DIR = join2(PROJECT_ROOT, "src");
421
+ var MODULES_DIR = join2(SRC_DIR, "modules");
422
+ var APP_MODULES_DIR = join2(MODULES_DIR, "app");
423
+ var WRANGLER_JSONC = join2(PROJECT_ROOT, "wrangler.jsonc");
424
+ var APP_MODULE_FILE = join2(SRC_DIR, "app.module.ts");
425
+ function wiltImportPath() {
426
+ return "@ajke/core";
427
+ }
428
+ function ensureDir(dir2) {
429
+ if (!existsSync2(dir2)) {
430
+ mkdirSync2(dir2, { recursive: true });
431
+ }
432
+ }
433
+
434
+ // bin/generators/module.generator.ts
435
+ function toPascalCase(name) {
436
+ return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
437
+ }
438
+ function toCamelCase(name) {
439
+ const pascal2 = toPascalCase(name);
440
+ return pascal2.charAt(0).toLowerCase() + pascal2.slice(1);
441
+ }
442
+ function toKebabCase(name) {
443
+ return name.replace(/([A-Z])/g, "-$1").replace(/^-/, "").replace(/_/g, "-").toLowerCase();
444
+ }
445
+ function pluralize(name) {
446
+ return name.endsWith("s") ? name : name + "s";
447
+ }
448
+ function moduleTemplate(name, wiltPath) {
449
+ const pascal2 = toPascalCase(name);
450
+ const kebab2 = toKebabCase(name);
451
+ return `import { Module } from "${wiltPath}";
452
+ import { ${pascal2}Controller } from "./${kebab2}.controller";
453
+ import { ${pascal2}Service } from "./${kebab2}.service";
454
+
455
+ @Module({
456
+ controllers: [${pascal2}Controller],
457
+ providers: [${pascal2}Service],
458
+ exports: [${pascal2}Service],
459
+ })
460
+ export class ${pascal2}Module {}
461
+ `;
462
+ }
463
+ function entityTemplate(name) {
464
+ const camel = toCamelCase(name);
465
+ const kebab2 = toKebabCase(name);
466
+ const pascal2 = toPascalCase(name);
467
+ const entityVar = pluralize(camel);
468
+ const tableName = pluralize(kebab2);
469
+ return `import { sqliteTable, integer } from "drizzle-orm/sqlite-core";
470
+
471
+ export const ${entityVar} = sqliteTable("${tableName}", {
472
+ id: integer("id").primaryKey({ autoIncrement: true }),
473
+ createdAt: integer("created_at", { mode: "timestamp" })
474
+ .$defaultFn(() => new Date())
475
+ .notNull(),
476
+ });
477
+
478
+ export type ${pascal2} = typeof ${entityVar}.$inferSelect;
479
+ export type New${pascal2} = typeof ${entityVar}.$inferInsert;
480
+ `;
481
+ }
482
+ function controllerTemplate(name, wiltPath) {
483
+ const pascal2 = toPascalCase(name);
484
+ const camel = toCamelCase(name);
485
+ const kebab2 = toKebabCase(name);
486
+ return `import type { Context } from "hono";
487
+ import { Controller, Get, Inject } from "${wiltPath}";
488
+ import { ResponseUtil } from "${wiltPath}";
489
+ import { ${pascal2}Service } from "./${kebab2}.service";
490
+
491
+ @Controller("/${kebab2}")
492
+ export class ${pascal2}Controller {
493
+ constructor(@Inject(${pascal2}Service) private ${camel}Service: ${pascal2}Service) {}
494
+
495
+ @Get()
496
+ async getAll(c: Context) {
497
+ const data = await this.${camel}Service.findAll();
498
+ return ResponseUtil.success(c, data);
499
+ }
500
+ }
501
+ `;
502
+ }
503
+ function serviceTemplate(name, wiltPath) {
504
+ const pascal2 = toPascalCase(name);
505
+ return `import { Injectable } from "${wiltPath}";
506
+ import { createDatabase } from "../../../database/connection";
507
+
508
+ @Injectable()
509
+ export class ${pascal2}Service {
510
+
511
+ private getDatabase(env: CloudflareBindings) {
512
+ return createDatabase(env.DB);
513
+ }
514
+
515
+ async findAll() {
516
+ return [];
517
+ }
518
+ }
519
+ `;
520
+ }
521
+ function testTemplate(name) {
522
+ const kebab2 = toKebabCase(name);
523
+ const pascal2 = toPascalCase(name);
524
+ const table = pluralize(kebab2.replace(/-/g, "_"));
525
+ return `import { describe, it, expect, beforeAll, afterEach } from "vitest";
526
+ import { SELF, env, applyD1Migrations } from "cloudflare:test";
527
+ import { ${pascal2}Service } from "./${kebab2}.service";
528
+
529
+ const BASE = "http://localhost";
530
+ const service = new ${pascal2}Service();
531
+
532
+ beforeAll(async () => {
533
+ await applyD1Migrations(env.DB, JSON.parse(env.TEST_MIGRATIONS));
534
+ });
535
+
536
+ afterEach(async () => {
537
+ await env.DB.prepare("DELETE FROM ${table}").run();
538
+ });
539
+
540
+ describe("${pascal2}Service", () => {
541
+ it("should be defined", () => {
542
+ expect(service).toBeDefined();
543
+ });
544
+ });
545
+
546
+ describe("GET /${kebab2}", () => {
547
+ it("returns an empty list", async () => {
548
+ const res = await SELF.fetch(\`\${BASE}/${kebab2}\`);
549
+ expect(res.status).toBe(200);
550
+ const { data } = await res.json() as { data: unknown[] };
551
+ expect(data).toEqual([]);
552
+ });
553
+ });
554
+ `;
555
+ }
556
+ function dtoTemplate(name) {
557
+ const pascal2 = toPascalCase(name);
558
+ return `import { z } from "zod";
559
+
560
+ export const Create${pascal2}Dto = z.object({
561
+ // TODO: define your fields
562
+ });
563
+
564
+ export const Update${pascal2}Dto = Create${pascal2}Dto.partial();
565
+
566
+ export type Create${pascal2}DtoType = z.infer<typeof Create${pascal2}Dto>;
567
+ export type Update${pascal2}DtoType = z.infer<typeof Update${pascal2}Dto>;
568
+ `;
569
+ }
570
+ function generateModule(name, options = {}) {
571
+ const kebab2 = toKebabCase(name);
572
+ const pascal2 = toPascalCase(name);
573
+ const basedir = options.modulesDir ?? APP_MODULES_DIR;
574
+ const moduleDir = options.dir ?? join3(basedir, kebab2);
575
+ let filesToGen = options.files ?? options.defaultFiles ?? ["module", "controller", "service", "dto", "entity", "test"];
576
+ if (options.noTest) filesToGen = filesToGen.filter((f) => f !== "test");
577
+ if (existsSync3(moduleDir)) {
578
+ console.error(` \u2717 Directory already exists: ${moduleDir}`);
579
+ process.exit(1);
580
+ }
581
+ ensureDir(moduleDir);
582
+ const wiltPath = wiltImportPath();
583
+ const hasEntity = filesToGen.includes("entity");
584
+ const fileMap = {
585
+ module: moduleTemplate(name, wiltPath),
586
+ controller: controllerTemplate(name, wiltPath),
587
+ service: serviceTemplate(name, wiltPath),
588
+ dto: dtoTemplate(name),
589
+ entity: entityTemplate(name),
590
+ test: testTemplate(name)
591
+ };
592
+ const extMap = {
593
+ module: `${kebab2}.module.ts`,
594
+ controller: `${kebab2}.controller.ts`,
595
+ service: `${kebab2}.service.ts`,
596
+ dto: `${kebab2}.dto.ts`,
597
+ entity: `${kebab2}.entity.ts`,
598
+ test: `${kebab2}.test.ts`
599
+ };
600
+ for (const file of filesToGen) {
601
+ const filePath = join3(moduleDir, extMap[file]);
602
+ writeFileSync2(filePath, fileMap[file], "utf-8");
603
+ console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
604
+ }
605
+ const relModuleDir = moduleDir.replace(process.cwd() + "/", "");
606
+ const entityLine = hasEntity ? `
607
+ 2. Add the entity to src/database/schema.ts:
608
+
609
+ import { ${pluralize(toCamelCase(name))} } from "@app/${relModuleDir.replace(/^src\//, "")}/${kebab2}.entity";
610
+
611
+ export const schema = {
612
+ ...existing,
613
+ ${pluralize(toCamelCase(name))},
614
+ };
615
+ ` : "";
616
+ console.log(`
617
+ Next steps \u2014
618
+
619
+ 1. Register the module in src/app.module.ts:
620
+
621
+ import { ${pascal2}Module } from "./${relModuleDir.replace(/^src\//, "")}/${kebab2}.module";
622
+
623
+ @Module({
624
+ imports: [..., ${pascal2}Module],
625
+ })
626
+ export class AppModule {}
627
+ ${entityLine} Then run \`pnpm db:gen\` to generate a migration.
628
+ `);
629
+ }
630
+ function generateService(name, dir2, modulesDir) {
631
+ const kebab2 = toKebabCase(name);
632
+ const basedir = modulesDir ?? APP_MODULES_DIR;
633
+ const targetDir = dir2 ?? join3(basedir, kebab2);
634
+ const wiltPath = wiltImportPath();
635
+ const filePath = join3(targetDir, `${kebab2}.service.ts`);
636
+ if (existsSync3(filePath)) {
637
+ console.error(` \u2717 File already exists: ${filePath}`);
638
+ process.exit(1);
639
+ }
640
+ ensureDir(targetDir);
641
+ writeFileSync2(filePath, serviceTemplate(name, wiltPath), "utf-8");
642
+ console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
643
+ }
644
+ function generateController(name, dir2, modulesDir) {
645
+ const kebab2 = toKebabCase(name);
646
+ const basedir = modulesDir ?? APP_MODULES_DIR;
647
+ const targetDir = dir2 ?? join3(basedir, kebab2);
648
+ const wiltPath = wiltImportPath();
649
+ const filePath = join3(targetDir, `${kebab2}.controller.ts`);
650
+ if (existsSync3(filePath)) {
651
+ console.error(` \u2717 File already exists: ${filePath}`);
652
+ process.exit(1);
653
+ }
654
+ ensureDir(targetDir);
655
+ writeFileSync2(filePath, controllerTemplate(name, wiltPath), "utf-8");
656
+ console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
657
+ }
658
+
659
+ // bin/utils/config.ts
660
+ import { existsSync as existsSync4 } from "fs";
661
+ import { join as join4, resolve as resolve2 } from "path";
662
+ async function loadConfig(cwd = process.cwd()) {
663
+ let userConfig = {};
664
+ for (const name of ["ajke.config.ts", "ajke.config.js", "ajke.config.mjs", "wilt.config.ts", "wilt.config.js", "wilt.config.mjs"]) {
665
+ const configPath = join4(cwd, name);
666
+ if (existsSync4(configPath)) {
667
+ try {
668
+ const mod = await import(configPath);
669
+ userConfig = mod.default ?? {};
670
+ } catch {
671
+ }
672
+ break;
673
+ }
674
+ }
675
+ return {
676
+ modulesDir: resolve2(cwd, userConfig.modulesDir ?? "src/modules/app"),
677
+ srcDir: resolve2(cwd, userConfig.srcDir ?? "src"),
678
+ generate: {
679
+ files: userConfig.generate?.files ?? ["module", "controller", "service", "dto", "entity"]
680
+ }
681
+ };
682
+ }
683
+
684
+ // bin/commands/generate.ts
685
+ var USAGE = `
686
+ Usage:
687
+ pnpm ajke generate module <name> [--path <dir>] [--no-test]
688
+ pnpm ajke generate service <name> [--path <dir>]
689
+ pnpm ajke generate controller <name> [--path <dir>]
690
+
691
+ Aliases:
692
+ g m \u2192 generate module
693
+ g s \u2192 generate service
694
+ g c \u2192 generate controller
695
+
696
+ Flags:
697
+ --no-test Skip generating the .test.ts file for a module
698
+
699
+ Examples:
700
+ pnpm ajke generate module payment
701
+ pnpm ajke g m payment
702
+ pnpm ajke generate module payment --no-test
703
+ pnpm ajke generate service payment --path src/modules/app/payment
704
+ `.trim();
705
+ function parsePath(args) {
706
+ const idx = args.indexOf("--path");
707
+ if (idx === -1) return { args };
708
+ const path = args[idx + 1];
709
+ const cleaned = args.filter((_, i) => i !== idx && i !== idx + 1);
710
+ return { args: cleaned, path };
711
+ }
712
+ function parseNoTest(args) {
713
+ const idx = args.indexOf("--no-test");
714
+ if (idx === -1) return { args, noTest: false };
715
+ return { args: args.filter((_, i) => i !== idx), noTest: true };
716
+ }
717
+ async function runGenerate(args) {
718
+ const { args: withoutPath, path: targetPath } = parsePath(args);
719
+ const { args: cleanArgs, noTest } = parseNoTest(withoutPath);
720
+ const [subcommand, name] = cleanArgs;
721
+ if (!subcommand || !name) {
722
+ console.error(` \u2717 Missing arguments.
723
+
724
+ ${USAGE}`);
725
+ process.exit(1);
726
+ }
727
+ const config = await loadConfig();
728
+ const sub = subcommand.toLowerCase();
729
+ if (sub === "module" || sub === "m") {
730
+ console.log(`
731
+ Generating module "${name}"...
732
+ `);
733
+ generateModule(name, {
734
+ dir: targetPath,
735
+ modulesDir: config.modulesDir,
736
+ defaultFiles: config.generate.files,
737
+ noTest
738
+ });
739
+ } else if (sub === "service" || sub === "s") {
740
+ console.log(`
741
+ Generating service "${name}"...
742
+ `);
743
+ generateService(name, targetPath, config.modulesDir);
744
+ } else if (sub === "controller" || sub === "c") {
745
+ console.log(`
746
+ Generating controller "${name}"...
747
+ `);
748
+ generateController(name, targetPath, config.modulesDir);
749
+ } else {
750
+ console.error(` \u2717 Unknown generate subcommand: "${subcommand}"
751
+
752
+ ${USAGE}`);
753
+ process.exit(1);
754
+ }
755
+ }
756
+
757
+ // bin/utils/wrangler.ts
758
+ import { readFileSync, writeFileSync as writeFileSync3 } from "fs";
759
+ function stripComments(text) {
760
+ return text.split("\n").map((line) => line.replace(/\s*\/\/.*$/, "")).join("\n");
761
+ }
762
+ function readWrangler() {
763
+ const raw = readFileSync(WRANGLER_JSONC, "utf-8");
764
+ return JSON.parse(stripComments(raw));
765
+ }
766
+ function writeWrangler(config) {
767
+ const header = `{
768
+ "$schema": "node_modules/wrangler/config-schema.json",`;
769
+ const body = JSON.stringify(config, null, 2);
770
+ const bodyWithoutSchema = body.replace(/^\{/, "").replace(/\s+"?\$schema"?\s*:\s*"[^"]*",?\n/, "\n");
771
+ writeFileSync3(WRANGLER_JSONC, header + bodyWithoutSchema, "utf-8");
772
+ }
773
+ function addD1(binding, databaseName) {
774
+ const config = readWrangler();
775
+ if (!config.d1_databases) config.d1_databases = [];
776
+ const alreadyExists = config.d1_databases.some(
777
+ (db) => db.binding === binding
778
+ );
779
+ if (alreadyExists) {
780
+ console.error(` \u2717 D1 binding "${binding}" already exists in wrangler.jsonc`);
781
+ process.exit(1);
782
+ }
783
+ config.d1_databases.push({
784
+ binding,
785
+ database_name: databaseName,
786
+ database_id: "00000000-0000-0000-0000-000000000000",
787
+ migrations_dir: "migrations"
788
+ });
789
+ writeWrangler(config);
790
+ console.log(` \u2714 Added D1 binding "${binding}" (database: ${databaseName})`);
791
+ console.log(` ! Remember to replace database_id with your actual D1 database ID.`);
792
+ }
793
+ function addR2(binding, bucketName) {
794
+ const config = readWrangler();
795
+ if (!config.r2_buckets) config.r2_buckets = [];
796
+ if (config.r2_buckets.some((b) => b.binding === binding)) {
797
+ console.error(` \u2717 R2 binding "${binding}" already exists in wrangler.jsonc`);
798
+ process.exit(1);
799
+ }
800
+ config.r2_buckets.push({
801
+ binding,
802
+ bucket_name: bucketName,
803
+ preview_bucket_name: `${bucketName}-preview`
804
+ });
805
+ writeWrangler(config);
806
+ console.log(` \u2714 Added R2 binding "${binding}" (bucket: ${bucketName})`);
807
+ }
808
+ function addKv(binding) {
809
+ const config = readWrangler();
810
+ if (!config.kv_namespaces) config.kv_namespaces = [];
811
+ if (config.kv_namespaces.some((k) => k.binding === binding)) {
812
+ console.error(` \u2717 KV binding "${binding}" already exists in wrangler.jsonc`);
813
+ process.exit(1);
814
+ }
815
+ config.kv_namespaces.push({
816
+ binding,
817
+ id: "00000000000000000000000000000000"
818
+ });
819
+ writeWrangler(config);
820
+ console.log(` \u2714 Added KV namespace binding "${binding}"`);
821
+ console.log(` ! Remember to replace id with your actual KV namespace ID.`);
822
+ }
823
+ function addQueue(binding, queueName) {
824
+ const config = readWrangler();
825
+ if (!config.queues) config.queues = { producers: [], consumers: [] };
826
+ if (!config.queues.producers) config.queues.producers = [];
827
+ if (!config.queues.consumers) config.queues.consumers = [];
828
+ if (config.queues.producers.some((p) => p.binding === binding)) {
829
+ console.error(` \u2717 Queue producer binding "${binding}" already exists in wrangler.jsonc`);
830
+ process.exit(1);
831
+ }
832
+ config.queues.producers.push({ binding, queue: queueName });
833
+ config.queues.consumers.push({
834
+ queue: queueName,
835
+ max_batch_size: 10,
836
+ max_batch_timeout: 10,
837
+ max_retries: 3
838
+ });
839
+ writeWrangler(config);
840
+ console.log(` \u2714 Added Queue binding "${binding}" (queue: ${queueName})`);
841
+ }
842
+ function addAi(binding) {
843
+ const config = readWrangler();
844
+ if (config.ai) {
845
+ console.error(` \u2717 AI binding already exists in wrangler.jsonc`);
846
+ process.exit(1);
847
+ }
848
+ config.ai = { binding };
849
+ writeWrangler(config);
850
+ console.log(` \u2714 Added AI binding "${binding}"`);
851
+ }
852
+ function addDurableObject(binding, className) {
853
+ const config = readWrangler();
854
+ if (!config.durable_objects) config.durable_objects = { bindings: [] };
855
+ if (!config.durable_objects.bindings) config.durable_objects.bindings = [];
856
+ if (config.durable_objects.bindings.some((b) => b.name === binding)) {
857
+ console.error(` \u2717 Durable Object binding "${binding}" already exists in wrangler.jsonc`);
858
+ process.exit(1);
859
+ }
860
+ config.durable_objects.bindings.push({
861
+ name: binding,
862
+ class_name: className
863
+ });
864
+ if (!config.migrations) config.migrations = [];
865
+ const existingClasses = config.migrations.flatMap(
866
+ (m) => m.new_sqlite_classes ?? m.new_classes ?? []
867
+ );
868
+ if (!existingClasses.includes(className)) {
869
+ const nextTag = `v${config.migrations.length + 1}`;
870
+ config.migrations.push({
871
+ tag: nextTag,
872
+ new_sqlite_classes: [className]
873
+ });
874
+ console.log(` ! Added migration entry "${nextTag}" for ${className}.`);
875
+ }
876
+ writeWrangler(config);
877
+ console.log(` \u2714 Added Durable Object binding "${binding}" (class: ${className})`);
878
+ }
879
+ function addVectorize(binding, indexName, dimensions = 1536) {
880
+ const config = readWrangler();
881
+ if (!config.vectorize) config.vectorize = [];
882
+ if (config.vectorize.some((v) => v.binding === binding)) {
883
+ console.error(` \u2717 Vectorize binding "${binding}" already exists in wrangler.jsonc`);
884
+ process.exit(1);
885
+ }
886
+ config.vectorize.push({
887
+ binding,
888
+ index_name: indexName,
889
+ dimensions,
890
+ metric: "cosine"
891
+ });
892
+ writeWrangler(config);
893
+ console.log(` \u2714 Added Vectorize binding "${binding}" (index: ${indexName}, dimensions: ${dimensions})`);
894
+ console.log(` ! Create the index with: wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=cosine`);
895
+ }
896
+ function addBrowser(binding) {
897
+ const config = readWrangler();
898
+ if (config.browser) {
899
+ console.error(` \u2717 Browser binding already exists in wrangler.jsonc`);
900
+ process.exit(1);
901
+ }
902
+ config.browser = { binding };
903
+ writeWrangler(config);
904
+ console.log(` \u2714 Added Browser rendering binding "${binding}"`);
905
+ }
906
+ function addHyperdrive(binding, connectionString) {
907
+ const config = readWrangler();
908
+ if (!config.hyperdrive) config.hyperdrive = [];
909
+ if (config.hyperdrive.some((h) => h.binding === binding)) {
910
+ console.error(` \u2717 Hyperdrive binding "${binding}" already exists in wrangler.jsonc`);
911
+ process.exit(1);
912
+ }
913
+ config.hyperdrive.push({
914
+ binding,
915
+ id: "00000000000000000000000000000000",
916
+ localConnectionString: connectionString
917
+ });
918
+ writeWrangler(config);
919
+ console.log(` \u2714 Added Hyperdrive binding "${binding}"`);
920
+ console.log(` ! Remember to replace id with your actual Hyperdrive config ID.`);
921
+ }
922
+
923
+ // bin/commands/add.ts
924
+ var USAGE2 = `
925
+ Usage:
926
+ pnpm ajke add d1 <BINDING_NAME> <database-name>
927
+ pnpm ajke add r2 <BINDING_NAME> <bucket-name>
928
+ pnpm ajke add kv <BINDING_NAME>
929
+ pnpm ajke add queue <BINDING_NAME> <queue-name>
930
+ pnpm ajke add ai <BINDING_NAME>
931
+ pnpm ajke add durable-object <BINDING_NAME> <ClassName>
932
+ pnpm ajke add vectorize <BINDING_NAME> <index-name> [dimensions]
933
+ pnpm ajke add browser <BINDING_NAME>
934
+ pnpm ajke add hyperdrive <BINDING_NAME> <connection-string>
935
+
936
+ Examples:
937
+ pnpm ajke add d1 PAYMENTS_DB payments-db
938
+ pnpm ajke add r2 ASSETS_BUCKET my-assets
939
+ pnpm ajke add kv SESSION_KV
940
+ pnpm ajke add queue MAIL_QUEUE mailer
941
+ pnpm ajke add ai AI
942
+ pnpm ajke add durable-object CHAT_ROOM ChatRoom
943
+ pnpm ajke add vectorize VECTOR_IDX my-index 1536
944
+ pnpm ajke add browser BROWSER
945
+ pnpm ajke add hyperdrive HYPERDRIVE postgres://user:pass@host/db
946
+ `.trim();
947
+ function runAdd(args) {
948
+ const [service, ...rest] = args;
949
+ if (!service) {
950
+ console.error(` \u2717 Missing service type.
951
+
952
+ ${USAGE2}`);
953
+ process.exit(1);
954
+ }
955
+ const svc = service.toLowerCase();
956
+ switch (svc) {
957
+ case "d1": {
958
+ const [binding, dbName] = rest;
959
+ if (!binding || !dbName) {
960
+ console.error(` \u2717 Usage: pnpm ajke add d1 <BINDING_NAME> <database-name>`);
961
+ process.exit(1);
962
+ }
963
+ console.log(`
964
+ Adding D1 binding to wrangler.jsonc...
965
+ `);
966
+ addD1(binding, dbName);
967
+ break;
968
+ }
969
+ case "r2": {
970
+ const [binding, bucketName] = rest;
971
+ if (!binding || !bucketName) {
972
+ console.error(` \u2717 Usage: pnpm ajke add r2 <BINDING_NAME> <bucket-name>`);
973
+ process.exit(1);
974
+ }
975
+ console.log(`
976
+ Adding R2 binding to wrangler.jsonc...
977
+ `);
978
+ addR2(binding, bucketName);
979
+ break;
980
+ }
981
+ case "kv": {
982
+ const [binding] = rest;
983
+ if (!binding) {
984
+ console.error(` \u2717 Usage: pnpm ajke add kv <BINDING_NAME>`);
985
+ process.exit(1);
986
+ }
987
+ console.log(`
988
+ Adding KV namespace binding to wrangler.jsonc...
989
+ `);
990
+ addKv(binding);
991
+ break;
992
+ }
993
+ case "queue": {
994
+ const [binding, queueName] = rest;
995
+ if (!binding || !queueName) {
996
+ console.error(` \u2717 Usage: pnpm ajke add queue <BINDING_NAME> <queue-name>`);
997
+ process.exit(1);
998
+ }
999
+ console.log(`
1000
+ Adding Queue binding to wrangler.jsonc...
1001
+ `);
1002
+ addQueue(binding, queueName);
1003
+ break;
1004
+ }
1005
+ case "ai": {
1006
+ const [binding] = rest;
1007
+ if (!binding) {
1008
+ console.error(` \u2717 Usage: pnpm ajke add ai <BINDING_NAME>`);
1009
+ process.exit(1);
1010
+ }
1011
+ console.log(`
1012
+ Adding AI binding to wrangler.jsonc...
1013
+ `);
1014
+ addAi(binding);
1015
+ break;
1016
+ }
1017
+ case "durable-object":
1018
+ case "do": {
1019
+ const [binding, className] = rest;
1020
+ if (!binding || !className) {
1021
+ console.error(` \u2717 Usage: pnpm ajke add durable-object <BINDING_NAME> <ClassName>`);
1022
+ process.exit(1);
1023
+ }
1024
+ console.log(`
1025
+ Adding Durable Object binding to wrangler.jsonc...
1026
+ `);
1027
+ addDurableObject(binding, className);
1028
+ break;
1029
+ }
1030
+ case "vectorize":
1031
+ case "vector": {
1032
+ const [binding, indexName, rawDimensions] = rest;
1033
+ if (!binding || !indexName) {
1034
+ console.error(` \u2717 Usage: pnpm ajke add vectorize <BINDING_NAME> <index-name> [dimensions]`);
1035
+ process.exit(1);
1036
+ }
1037
+ const dimensions = rawDimensions ? parseInt(rawDimensions, 10) : 1536;
1038
+ console.log(`
1039
+ Adding Vectorize binding to wrangler.jsonc...
1040
+ `);
1041
+ addVectorize(binding, indexName, dimensions);
1042
+ break;
1043
+ }
1044
+ case "browser": {
1045
+ const [binding] = rest;
1046
+ if (!binding) {
1047
+ console.error(` \u2717 Usage: pnpm ajke add browser <BINDING_NAME>`);
1048
+ process.exit(1);
1049
+ }
1050
+ console.log(`
1051
+ Adding Browser rendering binding to wrangler.jsonc...
1052
+ `);
1053
+ addBrowser(binding);
1054
+ break;
1055
+ }
1056
+ case "hyperdrive": {
1057
+ const [binding, connectionString] = rest;
1058
+ if (!binding || !connectionString) {
1059
+ console.error(` \u2717 Usage: pnpm ajke add hyperdrive <BINDING_NAME> <connection-string>`);
1060
+ process.exit(1);
1061
+ }
1062
+ console.log(`
1063
+ Adding Hyperdrive binding to wrangler.jsonc...
1064
+ `);
1065
+ addHyperdrive(binding, connectionString);
1066
+ break;
1067
+ }
1068
+ default: {
1069
+ console.error(` \u2717 Unknown service type: "${service}"
1070
+
1071
+ ${USAGE2}`);
1072
+ process.exit(1);
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ // bin/ajke.ts
1078
+ var BANNER = `
1079
+ \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1080
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
1081
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557
1082
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D
1083
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1084
+ \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1085
+ Ajke CLI \u2014 Cloudflare Workers & Hono Framework
1086
+ `.trim();
1087
+ var HELP = `
1088
+ ${BANNER}
1089
+
1090
+ Usage:
1091
+ pnpm ajke <command> [subcommand] [args...]
1092
+
1093
+ Commands:
1094
+ new Create a new Ajke project
1095
+ generate (g) Scaffold module, service, or controller files
1096
+ add Add a Cloudflare service binding to wrangler.jsonc
1097
+ help Show this help message
1098
+
1099
+ Generate subcommands:
1100
+ module (m) Scaffold a full module (module + controller + service + dto)
1101
+ service (s) Scaffold a service file
1102
+ controller (c) Scaffold a controller file
1103
+
1104
+ Add subcommands:
1105
+ d1 D1 database binding
1106
+ r2 R2 bucket binding
1107
+ kv KV namespace binding
1108
+ queue Queue producer + consumer
1109
+ ai Workers AI binding
1110
+ durable-object Durable Object binding + migration
1111
+ vectorize Vectorize index binding
1112
+ browser Browser rendering binding
1113
+ hyperdrive Hyperdrive binding
1114
+
1115
+ Examples:
1116
+ pnpm ajke new my-app
1117
+ pnpm ajke generate module payment
1118
+ pnpm ajke g m payment
1119
+ pnpm ajke add d1 PAYMENTS_DB payments-db
1120
+ pnpm ajke add r2 ASSETS_BUCKET my-assets
1121
+ pnpm ajke add kv SESSION_KV
1122
+ pnpm ajke add queue MAIL_QUEUE mailer
1123
+ pnpm ajke add ai AI
1124
+ pnpm ajke add durable-object CHAT_ROOM ChatRoom
1125
+ `.trim();
1126
+ async function main() {
1127
+ const args = process.argv.slice(2);
1128
+ const [command, ...rest] = args;
1129
+ if (!command || command === "help" || command === "--help" || command === "-h") {
1130
+ console.log(`
1131
+ ${HELP}
1132
+ `);
1133
+ process.exit(0);
1134
+ }
1135
+ const cmd = command.toLowerCase();
1136
+ if (cmd === "new" || cmd === "n") {
1137
+ runNew(rest);
1138
+ } else if (cmd === "generate" || cmd === "g") {
1139
+ await runGenerate(rest);
1140
+ } else if (cmd === "add") {
1141
+ runAdd(rest);
1142
+ } else {
1143
+ console.error(` \u2717 Unknown command: "${command}"
1144
+ `);
1145
+ console.log(HELP);
1146
+ process.exit(1);
1147
+ }
1148
+ }
1149
+ main();
1150
+ //# sourceMappingURL=ajke.js.map