@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.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/bin/ajke.ts +80 -0
- package/bin/commands/add.ts +155 -0
- package/bin/commands/generate.ts +74 -0
- package/bin/commands/new.ts +457 -0
- package/bin/generators/module.generator.ts +266 -0
- package/bin/utils/config.ts +43 -0
- package/bin/utils/paths.ts +32 -0
- package/bin/utils/wrangler.ts +216 -0
- package/dist/bin/ajke.js +1150 -0
- package/dist/bin/ajke.js.map +1 -0
- package/package.json +34 -0
package/dist/bin/ajke.js
ADDED
|
@@ -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
|