@hazejs/cli 0.0.0-stage → 0.1.2

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/index.mjs ADDED
@@ -0,0 +1,826 @@
1
+ import { createRequire } from "node:module";
2
+ import { defineCommand, runMain } from "citty";
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import process from "node:process";
7
+ import { createDevServer, loadHazeConfig } from "@hazejs/vite";
8
+ import { consola } from "consola";
9
+ import { colors } from "consola/utils";
10
+ import { mkdir, writeFile } from "node:fs/promises";
11
+ import { fileURLToPath } from "node:url";
12
+ // src/args.ts
13
+ const rootArg = { root: {
14
+ type: "string",
15
+ description: "项目根目录",
16
+ default: "."
17
+ } };
18
+ const serverArgs = {
19
+ port: {
20
+ type: "string",
21
+ alias: "p",
22
+ description: "监听端口(默认读取 PORT 或 haze.config.ts)"
23
+ },
24
+ host: {
25
+ type: "string",
26
+ description: "监听地址"
27
+ }
28
+ };
29
+ function resolveRoot(root) {
30
+ return resolve(process.cwd(), root);
31
+ }
32
+ function parsePort(port) {
33
+ if (port === void 0) return void 0;
34
+ const value = Number(port);
35
+ if (!Number.isInteger(value) || value < 0) throw new Error(`Invalid port: ${port}`);
36
+ return value;
37
+ }
38
+ /** srvx 在监听所有地址时返回 `[::]` / `0.0.0.0`,展示为 localhost 便于点击 */
39
+ function displayUrl(url) {
40
+ return url.replace(/\/\/(?:\[::\]|0\.0\.0\.0)/, "//localhost");
41
+ }
42
+ // src/resolve.ts
43
+ /**
44
+ * 从项目目录向上查找已安装包的 package.json。
45
+ * 不使用 require.resolve('<pkg>/package.json'):部分包(如 drizzle-kit)未在 exports 中暴露它。
46
+ */
47
+ function findPackage(root, pkg) {
48
+ for (let dir = root;; dir = dirname(dir)) {
49
+ const file = join(dir, "node_modules", pkg, "package.json");
50
+ if (existsSync(file)) return {
51
+ dir: dirname(file),
52
+ json: JSON.parse(readFileSync(file, "utf8"))
53
+ };
54
+ if (dirname(dir) === dir) return void 0;
55
+ }
56
+ }
57
+ /** 包的可执行文件路径 */
58
+ function binOf(root, pkg, bin = pkg.split("/").pop()) {
59
+ const found = findPackage(root, pkg);
60
+ if (!found) throw new Error(`${pkg} is not installed. Run: pnpm add -D ${pkg}`);
61
+ const bins = found.json.bin;
62
+ const path = typeof bins === "string" ? bins : bins?.[bin];
63
+ if (!path) throw new Error(`${pkg} has no "${bin}" executable`);
64
+ return join(found.dir, path);
65
+ }
66
+ // src/commands/db.ts
67
+ /** 启动(不监听端口的)开发服务器以读取配置;db:migrate / db:seed 也在应用的模块图中执行 */
68
+ async function withApp$1(root, run) {
69
+ const { srcDir } = await loadHazeConfig(root);
70
+ const server = await createDevServer({
71
+ root,
72
+ listen: false
73
+ });
74
+ try {
75
+ if (!server.app) throw new Error("Application failed to start");
76
+ const database = server.app.config.get("database", void 0);
77
+ if (!database?.connections) throw new Error("config/database.ts is missing or invalid");
78
+ return await run(server, database, srcDir);
79
+ } finally {
80
+ await server.close();
81
+ }
82
+ }
83
+ function targetOf(database, srcDir, name = database.default) {
84
+ const config = database.connections[name];
85
+ if (!config) throw new Error(`Database connection "${name}" is not defined in config/database.ts`);
86
+ if (config.driver === "mongodb") throw new MongoConnectionError(name);
87
+ return {
88
+ name,
89
+ config,
90
+ dialect: config.driver === "mysql" ? "mysql" : [
91
+ "sqlite",
92
+ "turso",
93
+ "d1"
94
+ ].includes(config.driver) ? "sqlite" : "postgresql",
95
+ schema: resolve(srcDir, config.schema ?? "database/schema"),
96
+ migrations: resolve(srcDir, config.migrations ?? (name === database.default ? "database/migrations" : `database/migrations/${name}`))
97
+ };
98
+ }
99
+ var MongoConnectionError = class extends Error {
100
+ constructor(name) {
101
+ super(`Connection "${name}" is MongoDB, which has no SQL schema or migrations`);
102
+ }
103
+ };
104
+ const connectionArg$1 = { connection: {
105
+ type: "string",
106
+ alias: "c",
107
+ description: "连接名(默认 config/database.ts 中的 default)"
108
+ } };
109
+ const generateCommand = defineCommand({
110
+ meta: {
111
+ name: "db:generate",
112
+ description: "根据 database/schema 的表结构变化生成迁移(drizzle-kit)"
113
+ },
114
+ args: {
115
+ ...rootArg,
116
+ ...connectionArg$1,
117
+ name: {
118
+ type: "string",
119
+ description: "迁移名称"
120
+ },
121
+ custom: {
122
+ type: "boolean",
123
+ description: "生成空白迁移,手写 SQL",
124
+ default: false
125
+ }
126
+ },
127
+ async run({ args }) {
128
+ const root = resolveRoot(args.root);
129
+ const target = await withApp$1(root, async (_server, database, srcDir) => targetOf(database, srcDir, args.connection));
130
+ const kit = binOf(root, "drizzle-kit");
131
+ const kitArgs = [
132
+ "generate",
133
+ `--dialect=${target.dialect}`,
134
+ `--schema=${relative(root, target.schema)}/*.ts`,
135
+ `--out=${relative(root, target.migrations)}`,
136
+ ...args.name ? [`--name=${args.name}`] : [],
137
+ ...args.custom ? ["--custom"] : []
138
+ ];
139
+ const code = await new Promise((done) => {
140
+ spawn(process.execPath, [kit, ...kitArgs], {
141
+ cwd: root,
142
+ stdio: "inherit"
143
+ }).on("close", (code) => done(code ?? 1));
144
+ });
145
+ process.exitCode = code;
146
+ }
147
+ });
148
+ const migrateCommand = defineCommand({
149
+ meta: {
150
+ name: "db:migrate",
151
+ description: "执行尚未运行的迁移"
152
+ },
153
+ args: {
154
+ ...rootArg,
155
+ ...connectionArg$1,
156
+ all: {
157
+ type: "boolean",
158
+ description: "对所有连接执行",
159
+ default: false
160
+ }
161
+ },
162
+ async run({ args }) {
163
+ await withApp$1(resolveRoot(args.root), async (server, _database, srcDir) => {
164
+ const results = await (await server.import("@hazejs/orm")).runMigrations({
165
+ srcDir,
166
+ connection: args.connection,
167
+ all: args.all
168
+ });
169
+ for (const result of results) if (result.skipped === "mongodb") consola.info(`[${result.connection}] MongoDB has no migrations, skipped`);
170
+ else if (result.skipped) consola.warn(`[${result.connection}] no migrations in ${result.folder}, run haze db:generate first`);
171
+ else consola.success(`[${result.connection}] migrated: ${result.applied} of ${result.total} applied (${result.folder})`);
172
+ });
173
+ }
174
+ });
175
+ const seedCommand = defineCommand({
176
+ meta: {
177
+ name: "db:seed",
178
+ description: "按文件名顺序执行 database/seeders 下的数据填充"
179
+ },
180
+ args: {
181
+ ...rootArg,
182
+ file: {
183
+ type: "string",
184
+ description: "只执行指定文件(不含扩展名)"
185
+ }
186
+ },
187
+ async run({ args }) {
188
+ const root = resolveRoot(args.root);
189
+ await withApp$1(root, async (server, _database, srcDir) => {
190
+ const dir = join(srcDir, "database", "seeders");
191
+ const files = existsSync(dir) ? readdirSync(dir).filter((file) => /\.[cm]?[jt]s$/.test(file) && (!args.file || file.replace(/\.[^.]+$/, "") === args.file)).sort() : [];
192
+ if (!files.length) {
193
+ consola.warn(`No seeders found in ${relative(root, dir)}`);
194
+ return;
195
+ }
196
+ for (const file of files) {
197
+ const module = await server.import(join(dir, file));
198
+ if (typeof module.default !== "function") throw new TypeError(`${file} must export a default function`);
199
+ await module.default(server.app);
200
+ consola.success(`Seeded ${file}`);
201
+ }
202
+ });
203
+ }
204
+ });
205
+ // src/make/naming.ts
206
+ /** `admin/user-profile`、`Admin/UserProfileController` → { dir: 'admin', name: 'UserProfile' } */
207
+ function parseName(input, suffix = "") {
208
+ const segments = input.replace(/\\/g, "/").split("/").filter(Boolean);
209
+ const last = segments.pop();
210
+ if (!last) throw new Error("Name is required");
211
+ let name = pascalCase(last);
212
+ if (suffix && name.endsWith(suffix) && name !== suffix) name = name.slice(0, -suffix.length);
213
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error(`Invalid name "${input}"`);
214
+ return {
215
+ dir: segments.map(kebabCase).join("/"),
216
+ name
217
+ };
218
+ }
219
+ function pascalCase(value) {
220
+ return value.replace(/[-_\s]+(.)?/g, (_, char) => (char ?? "").toUpperCase()).replace(/^./, (char) => char.toUpperCase());
221
+ }
222
+ function camelCase(value) {
223
+ const pascal = pascalCase(value);
224
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
225
+ }
226
+ function kebabCase(value) {
227
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
228
+ }
229
+ /** 简单的英文复数,用于路由路径提示 */
230
+ function plural(value) {
231
+ if (/(?:[sxz]|ch|sh)$/.test(value)) return `${value}es`;
232
+ if (/[^aeiou]y$/.test(value)) return `${value.slice(0, -1)}ies`;
233
+ return `${value}s`;
234
+ }
235
+ // src/make/generator.ts
236
+ /** 内置 stub 目录:构建产物(dist/)与源码(src/make/)位于不同深度,向上查找 */
237
+ const STUBS_DIR = ["../stubs", "../../stubs"].map((path) => fileURLToPath(new URL(path, import.meta.url))).find((dir) => existsSync(dir));
238
+ const KINDS = {
239
+ controller: {
240
+ description: "控制器",
241
+ suffix: "Controller",
242
+ dir: "app/controller",
243
+ base: "src",
244
+ file: "class",
245
+ stub: "controller",
246
+ hint: (v) => `在 route/*.ts 中注册:r.get('/${v.routePath}', [${v.className}, 'index'])`
247
+ },
248
+ service: {
249
+ description: "业务服务",
250
+ suffix: "Service",
251
+ dir: "app/service",
252
+ base: "src",
253
+ file: "class",
254
+ stub: "service"
255
+ },
256
+ repository: {
257
+ description: "数据访问层",
258
+ suffix: "Repository",
259
+ dir: "app/repository",
260
+ base: "src",
261
+ file: "class",
262
+ stub: "repository"
263
+ },
264
+ middleware: {
265
+ description: "中间件",
266
+ suffix: "",
267
+ dir: "app/middleware",
268
+ base: "src",
269
+ file: "class",
270
+ stub: "middleware",
271
+ hint: (v) => `在 config/middleware.ts 的 alias 中注册:${v.camelName}: ${v.className}`
272
+ },
273
+ interceptor: {
274
+ description: "出站拦截器",
275
+ suffix: "Interceptor",
276
+ dir: "app/interceptor",
277
+ base: "src",
278
+ file: "kebab",
279
+ flat: true,
280
+ stub: "interceptor",
281
+ hint: () => "app/interceptor 下的拦截器按文件名顺序自动加载"
282
+ },
283
+ provider: {
284
+ description: "服务提供者",
285
+ suffix: "Provider",
286
+ dir: "app/provider",
287
+ base: "src",
288
+ file: "class",
289
+ stub: "provider",
290
+ hint: () => "app/provider 下的提供者会被自动加载"
291
+ },
292
+ event: {
293
+ description: "事件",
294
+ suffix: "",
295
+ dir: "app/event",
296
+ base: "src",
297
+ file: "class",
298
+ flat: true,
299
+ stub: "event",
300
+ hint: (v) => `触发:await ${v.className}.emit({ ... });监听:haze make:listener <Name>`
301
+ },
302
+ listener: {
303
+ description: "事件监听器",
304
+ suffix: "",
305
+ dir: "app/listener",
306
+ base: "src",
307
+ file: "class",
308
+ flat: true,
309
+ stub: "listener",
310
+ hint: () => "app/listener 下的监听器自动注册;把 import 的事件改为要监听的事件"
311
+ },
312
+ job: {
313
+ description: "队列任务",
314
+ suffix: "",
315
+ dir: "app/job",
316
+ base: "src",
317
+ file: "class",
318
+ flat: true,
319
+ stub: "job",
320
+ hint: (v) => `投递:await ${v.className}.dispatch({ ... });消费:haze queue:work`
321
+ },
322
+ policy: {
323
+ description: "授权策略",
324
+ suffix: "Policy",
325
+ dir: "app/policy",
326
+ base: "src",
327
+ file: "class",
328
+ flat: true,
329
+ stub: "policy",
330
+ hint: (v) => `app/policy 下的策略自动注册:await ctx.authorize('update', ${v.camelName}) / .middleware('can:update,${v.name},id')`
331
+ },
332
+ validate: {
333
+ description: "请求校验",
334
+ suffix: "Schema",
335
+ dir: "app/validate",
336
+ base: "src",
337
+ file: "kebab",
338
+ stub: "validate"
339
+ },
340
+ test: {
341
+ description: "测试",
342
+ suffix: "Test",
343
+ dir: "tests",
344
+ base: "root",
345
+ file: "kebab",
346
+ ext: ".test.ts",
347
+ stub: "test",
348
+ hint: () => "运行:haze test"
349
+ },
350
+ config: {
351
+ description: "配置文件",
352
+ suffix: "Config",
353
+ dir: "config",
354
+ base: "src",
355
+ file: "camel",
356
+ flat: true,
357
+ stub: "config",
358
+ hint: (v) => `读取:config('${v.camelName}.enabled')`
359
+ },
360
+ route: {
361
+ description: "路由文件",
362
+ suffix: "Routes",
363
+ dir: "route",
364
+ base: "src",
365
+ file: "kebab",
366
+ flat: true,
367
+ stub: "route"
368
+ },
369
+ seeder: {
370
+ description: "数据填充",
371
+ suffix: "Seeder",
372
+ dir: "database/seeders",
373
+ base: "src",
374
+ file: "kebab",
375
+ flat: true,
376
+ stub: "seeder",
377
+ hint: () => "运行:haze db:seed"
378
+ }
379
+ };
380
+ async function make(options) {
381
+ const kind = KINDS[options.kind];
382
+ const { dir, name } = parseName(options.name, kind.suffix);
383
+ if (kind.flat && dir) throw new Error(`${kind.dir}/ 只自动加载第一层文件,${options.kind} 名称不能包含子目录:${options.name}`);
384
+ const vars = {
385
+ name,
386
+ className: name + (kind.file === "class" ? kind.suffix : ""),
387
+ camelName: camelCase(name),
388
+ kebabName: kebabCase(name),
389
+ envName: kebabCase(name).replace(/-/g, "_").toUpperCase(),
390
+ routePath: plural(kebabCase(name))
391
+ };
392
+ const fileName = kind.file === "class" ? vars.className : kind.file === "camel" ? vars.camelName : vars.kebabName;
393
+ const base = kind.base === "root" ? options.root : options.srcDir;
394
+ const kindDir = options.app && kind.base === "src" ? `app/${kebabCase(options.app)}/${kind.dir.replace(/^app\//, "")}` : kind.dir;
395
+ const file = join(base, kindDir, dir, fileName + (kind.ext ?? ".ts"));
396
+ if (existsSync(file) && !options.force) throw new Error(`${file} already exists. Use --force to overwrite.`);
397
+ const stubName = options.variant ?? kind.stub;
398
+ const { content, custom } = readStub(options.root, stubName);
399
+ await mkdir(dirname(file), { recursive: true });
400
+ await writeFile(file, render(content, vars));
401
+ return {
402
+ file,
403
+ stub: stubName,
404
+ custom,
405
+ hint: options.app ? kind.hint?.(vars)?.replace("route/*.ts", `app/${kebabCase(options.app)}/route/*.ts`) : kind.hint?.(vars)
406
+ };
407
+ }
408
+ /** 优先使用项目根目录 stubs/<name>.stub,便于团队定制 */
409
+ function readStub(root, stubName) {
410
+ const custom = join(root, "stubs", `${stubName}.stub`);
411
+ if (existsSync(custom)) return {
412
+ content: readFileSync(custom, "utf8"),
413
+ custom: true
414
+ };
415
+ const builtin = join(STUBS_DIR, `${stubName}.stub`);
416
+ if (!existsSync(builtin)) throw new Error(`Stub "${stubName}" not found`);
417
+ return {
418
+ content: readFileSync(builtin, "utf8"),
419
+ custom: false
420
+ };
421
+ }
422
+ /** 生成模型(app/model)与对应的 drizzle 表结构(database/schema,已存在时跳过) */
423
+ async function makeModel(options) {
424
+ const { dir, name } = parseName(options.name, "Model");
425
+ const tableName = options.table ?? plural(kebabCase(name)).replace(/-/g, "_");
426
+ const vars = {
427
+ name,
428
+ className: name,
429
+ tableName,
430
+ tableVar: camelCase(tableName),
431
+ softDelete: String(options.softDelete ?? false)
432
+ };
433
+ const modelDir = options.app ? `app/${kebabCase(options.app)}/model` : "app/model";
434
+ const model = join(options.srcDir, modelDir, dir, `${name}.ts`);
435
+ if (existsSync(model) && !options.force) throw new Error(`${model} already exists. Use --force to overwrite.`);
436
+ const mongo = options.dialect === "mongodb";
437
+ const schema = join(options.srcDir, "database/schema", `${tableName}.ts`);
438
+ const writeSchema = !mongo && !existsSync(schema);
439
+ await mkdir(dirname(model), { recursive: true });
440
+ await writeFile(model, render(readStub(options.root, mongo ? "model.mongodb" : "model").content, vars));
441
+ if (writeSchema) {
442
+ await mkdir(dirname(schema), { recursive: true });
443
+ await writeFile(schema, render(readStub(options.root, `schema.${options.dialect ?? "sqlite"}`).content, vars));
444
+ }
445
+ return {
446
+ model,
447
+ schema: writeSchema ? schema : void 0,
448
+ hint: mongo ? "MongoDB 无需迁移;索引可通过 (await Mongo.db()).collection(...).createIndex() 管理" : "修改表结构后运行:haze db:generate && haze db:migrate"
449
+ };
450
+ }
451
+ function render(content, vars) {
452
+ return content.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => vars[key] ?? match);
453
+ }
454
+ // src/commands/make.ts
455
+ function makeCommand(kind) {
456
+ return defineCommand({
457
+ meta: {
458
+ name: `make:${kind}`,
459
+ description: `生成${KINDS[kind].description}`
460
+ },
461
+ args: {
462
+ name: {
463
+ type: "positional",
464
+ description: "名称,可带子目录,例如 admin/User",
465
+ required: true
466
+ },
467
+ ...rootArg,
468
+ force: {
469
+ type: "boolean",
470
+ description: "文件已存在时覆盖",
471
+ default: false
472
+ },
473
+ app: {
474
+ type: "string",
475
+ alias: "a",
476
+ description: "多应用:生成到 app/<app>/ 下"
477
+ },
478
+ ...kind === "controller" && { resource: {
479
+ type: "boolean",
480
+ alias: "r",
481
+ description: "生成 RESTful 资源控制器",
482
+ default: false
483
+ } }
484
+ },
485
+ async run({ args }) {
486
+ const root = resolveRoot(args.root);
487
+ const { srcDir } = await loadHazeConfig(root);
488
+ const result = await make({
489
+ root,
490
+ srcDir,
491
+ kind,
492
+ name: args.name,
493
+ force: args.force,
494
+ variant: variantOf(kind, root, args),
495
+ app: args.app
496
+ });
497
+ consola.success(`Created ${colors.cyan(relative(process.cwd(), result.file))}${result.custom ? colors.dim(` (stubs/${result.stub}.stub)`) : ""}`);
498
+ if (result.hint) consola.info(result.hint);
499
+ }
500
+ });
501
+ }
502
+ function variantOf(kind, root, args) {
503
+ if (kind === "controller" && args.resource) return "controller.resource";
504
+ if (kind === "validate" && usesOnly(root, "zod", "valibot")) return "validate.zod";
505
+ }
506
+ function usesOnly(root, wanted, other) {
507
+ const file = join(root, "package.json");
508
+ if (!existsSync(file)) return false;
509
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
510
+ const deps = {
511
+ ...pkg.dependencies,
512
+ ...pkg.devDependencies
513
+ };
514
+ return wanted in deps && !(other in deps);
515
+ }
516
+ const makeCommands = Object.fromEntries(Object.keys(KINDS).map((kind) => [`make:${kind}`, makeCommand(kind)]));
517
+ const makeModelCommand = defineCommand({
518
+ meta: {
519
+ name: "make:model",
520
+ description: "生成模型与 drizzle 表结构(database/schema)"
521
+ },
522
+ args: {
523
+ name: {
524
+ type: "positional",
525
+ description: "模型名,例如 User 或 OrderItem",
526
+ required: true
527
+ },
528
+ ...rootArg,
529
+ table: {
530
+ type: "string",
531
+ description: "表名(默认为复数下划线形式,例如 order_items)"
532
+ },
533
+ dialect: {
534
+ type: "string",
535
+ description: "sqlite | postgres | mysql | mongodb(默认按已安装的驱动推断)"
536
+ },
537
+ softDelete: {
538
+ type: "boolean",
539
+ description: "启用软删除",
540
+ default: false
541
+ },
542
+ app: {
543
+ type: "string",
544
+ alias: "a",
545
+ description: "多应用:模型生成到 app/<app>/model"
546
+ },
547
+ force: {
548
+ type: "boolean",
549
+ description: "文件已存在时覆盖",
550
+ default: false
551
+ }
552
+ },
553
+ async run({ args }) {
554
+ const root = resolveRoot(args.root);
555
+ const { srcDir } = await loadHazeConfig(root);
556
+ const dialect = args.dialect ?? detectDialect(root);
557
+ if (![
558
+ "sqlite",
559
+ "postgres",
560
+ "mysql",
561
+ "mongodb"
562
+ ].includes(dialect)) throw new Error(`Unknown dialect "${dialect}"`);
563
+ const result = await makeModel({
564
+ root,
565
+ srcDir,
566
+ name: args.name,
567
+ table: args.table,
568
+ dialect,
569
+ softDelete: args.softDelete,
570
+ app: args.app,
571
+ force: args.force
572
+ });
573
+ consola.success(`Created ${colors.cyan(relative(process.cwd(), result.model))}`);
574
+ if (result.schema) consola.success(`Created ${colors.cyan(relative(process.cwd(), result.schema))} ${colors.dim(`(${dialect})`)}`);
575
+ consola.info(result.hint);
576
+ }
577
+ });
578
+ function detectDialect(root) {
579
+ if (usesOnly(root, "mongodb", "@libsql/client") && usesOnly(root, "mongodb", "mysql2") && usesOnly(root, "mongodb", "postgres")) return "mongodb";
580
+ if (usesOnly(root, "mysql2", "@libsql/client")) return "mysql";
581
+ if (usesOnly(root, "postgres", "@libsql/client") || usesOnly(root, "@electric-sql/pglite", "@libsql/client")) return "postgres";
582
+ return "sqlite";
583
+ }
584
+ // src/commands/worker.ts
585
+ /** 在应用的模块图中运行(不监听端口);keepAlive 时保持运行,文件变化后应用热替换 */
586
+ async function withApp(root, run, keepAlive = false) {
587
+ const server = await createDevServer({
588
+ root,
589
+ listen: false
590
+ });
591
+ if (!server.app) {
592
+ await server.close();
593
+ throw new Error("Application failed to start");
594
+ }
595
+ const core = await server.import("@hazejs/core");
596
+ try {
597
+ return await run(server, core);
598
+ } finally {
599
+ if (!keepAlive) await server.close();
600
+ }
601
+ }
602
+ /** 等待 Ctrl+C / SIGTERM,然后执行清理 */
603
+ function untilSignal(cleanup) {
604
+ return new Promise((resolve) => {
605
+ const stop = async () => {
606
+ consola.info("Stopping…");
607
+ await cleanup();
608
+ resolve();
609
+ };
610
+ process.once("SIGINT", stop);
611
+ process.once("SIGTERM", stop);
612
+ });
613
+ }
614
+ const connectionArg = { connection: {
615
+ type: "string",
616
+ description: "队列连接(默认 config/queue.ts 的 default)"
617
+ } };
618
+ const queueWorkCommand = defineCommand({
619
+ meta: {
620
+ name: "queue:work",
621
+ description: "消费队列任务(任务代码修改后自动生效)"
622
+ },
623
+ args: {
624
+ ...rootArg,
625
+ ...connectionArg,
626
+ queue: {
627
+ type: "string",
628
+ alias: "q",
629
+ description: "队列名,逗号分隔(按优先级),默认 default"
630
+ },
631
+ concurrency: {
632
+ type: "string",
633
+ alias: "c",
634
+ description: "并发数,默认 1",
635
+ default: "1"
636
+ }
637
+ },
638
+ async run({ args }) {
639
+ await withApp(resolveRoot(args.root), async (server, core) => {
640
+ const manager = () => server.app.make(core.QueueManager);
641
+ const queues = args.queue?.split(",").map((name) => name.trim()).filter(Boolean) ?? ["default"];
642
+ const worker = await manager().work({
643
+ connection: args.connection,
644
+ queues,
645
+ concurrency: Number(args.concurrency),
646
+ resolve: manager
647
+ });
648
+ consola.success(`Processing ${queues.join(", ")} (${manager().config.connections[args.connection ?? manager().config.default]?.driver} connection, concurrency ${args.concurrency})`);
649
+ await untilSignal(async () => {
650
+ await worker.stop();
651
+ await server.close();
652
+ });
653
+ }, true);
654
+ }
655
+ });
656
+ const queueFailedCommand = defineCommand({
657
+ meta: {
658
+ name: "queue:failed",
659
+ description: "列出失败的任务"
660
+ },
661
+ args: {
662
+ ...rootArg,
663
+ ...connectionArg
664
+ },
665
+ async run({ args }) {
666
+ await withApp(resolveRoot(args.root), async (server, core) => {
667
+ const failed = await server.app.make(core.QueueManager).failed(args.connection);
668
+ if (!failed.length) {
669
+ consola.info("No failed jobs");
670
+ return;
671
+ }
672
+ for (const job of failed) console.log(`${job.id} ${job.job} [${job.queue}] ${new Date(job.failedAt).toISOString()} attempts=${job.attempts}\n ${job.error.split("\n")[0]}`);
673
+ });
674
+ }
675
+ });
676
+ const queueRetryCommand = defineCommand({
677
+ meta: {
678
+ name: "queue:retry",
679
+ description: "重新投递失败的任务(不传 id 为全部)"
680
+ },
681
+ args: {
682
+ ...rootArg,
683
+ ...connectionArg,
684
+ id: {
685
+ type: "positional",
686
+ required: false,
687
+ description: "任务 ID"
688
+ }
689
+ },
690
+ async run({ args }) {
691
+ await withApp(resolveRoot(args.root), async (server, core) => {
692
+ const count = await server.app.make(core.QueueManager).retry(args.id, args.connection);
693
+ consola.success(`Retried ${count} job(s)`);
694
+ });
695
+ }
696
+ });
697
+ const queueForgetCommand = defineCommand({
698
+ meta: {
699
+ name: "queue:forget",
700
+ description: "删除一个失败的任务"
701
+ },
702
+ args: {
703
+ ...rootArg,
704
+ ...connectionArg,
705
+ id: {
706
+ type: "positional",
707
+ required: true,
708
+ description: "任务 ID"
709
+ }
710
+ },
711
+ async run({ args }) {
712
+ await withApp(resolveRoot(args.root), async (server, core) => {
713
+ if (await server.app.make(core.QueueManager).forget(args.id, args.connection)) consola.success(`Removed ${args.id}`);
714
+ else consola.warn(`No failed job ${args.id}`);
715
+ });
716
+ }
717
+ });
718
+ const queueFlushCommand = defineCommand({
719
+ meta: {
720
+ name: "queue:flush",
721
+ description: "清空失败的任务"
722
+ },
723
+ args: {
724
+ ...rootArg,
725
+ ...connectionArg
726
+ },
727
+ async run({ args }) {
728
+ await withApp(resolveRoot(args.root), async (server, core) => {
729
+ consola.success(`Removed ${await server.app.make(core.QueueManager).flush(args.connection)} failed job(s)`);
730
+ });
731
+ }
732
+ });
733
+ const scheduleListCommand = defineCommand({
734
+ meta: {
735
+ name: "schedule:list",
736
+ description: "列出定时任务与下次执行时间"
737
+ },
738
+ args: rootArg,
739
+ async run({ args }) {
740
+ await withApp(resolveRoot(args.root), async (server, core) => {
741
+ const scheduler = server.app.make(core.Scheduler);
742
+ const tasks = scheduler.tasks();
743
+ if (!tasks.length) {
744
+ consola.info("No scheduled tasks (schedule/*.ts)");
745
+ return;
746
+ }
747
+ const width = Math.max(...tasks.map((task) => task.name.length));
748
+ for (const task of tasks) {
749
+ const flags = [task.runsOnOneServer && "onOneServer", task.preventsOverlap && "withoutOverlapping"].filter(Boolean).join(", ");
750
+ const timezone = task.timezoneName ?? scheduler.config.timezone ?? "UTC";
751
+ console.log(`${task.name.padEnd(width)} ${task.expression.padEnd(15)} ${timezone.padEnd(15)} next ${task.nextRun(/* @__PURE__ */ new Date(), scheduler.config.timezone)?.toISOString() ?? "-"}${flags ? ` (${flags})` : ""}`);
752
+ }
753
+ });
754
+ }
755
+ });
756
+ const scheduleRunCommand = defineCommand({
757
+ meta: {
758
+ name: "schedule:run",
759
+ description: "执行当前分钟到期的定时任务(配合系统 cron 每分钟调用)"
760
+ },
761
+ args: rootArg,
762
+ async run({ args }) {
763
+ await withApp(resolveRoot(args.root), async (server, core) => {
764
+ const runs = await server.app.make(core.Scheduler).runDue();
765
+ if (!runs.length) consola.info("No scheduled tasks are due");
766
+ for (const run of runs) consola[run.status === "failed" ? "error" : run.status === "ok" ? "success" : "info"](`${run.name}: ${run.status}${run.reason ? ` (${run.reason})` : ""}`);
767
+ if (runs.some((run) => run.status === "failed")) process.exitCode = 1;
768
+ });
769
+ }
770
+ });
771
+ const scheduleWorkCommand = defineCommand({
772
+ meta: {
773
+ name: "schedule:work",
774
+ description: "常驻执行定时任务(每分钟整点检查)"
775
+ },
776
+ args: rootArg,
777
+ async run({ args }) {
778
+ await withApp(resolveRoot(args.root), async (server, core) => {
779
+ const controller = new AbortController();
780
+ consola.success(`Scheduler started (${server.app.make(core.Scheduler).tasks().length} task(s))`);
781
+ const loop = (async () => {
782
+ while (!controller.signal.aborted) await server.app.make(core.Scheduler).work(AbortSignal.any([controller.signal, AbortSignal.timeout(61e3)]));
783
+ })();
784
+ await untilSignal(async () => {
785
+ controller.abort();
786
+ await loop;
787
+ await server.close();
788
+ });
789
+ }, true);
790
+ }
791
+ });
792
+ // src/index.ts
793
+ const { version } = createRequire(import.meta.url)("../package.json");
794
+ const main = defineCommand({
795
+ meta: {
796
+ name: "hazejs",
797
+ version,
798
+ description: "hazejs Node 服务端框架命令行"
799
+ },
800
+ subCommands: {
801
+ "dev": () => import("./dev-B4V8ouF6.mjs").then((m) => m.default),
802
+ "build": () => import("./build-1rAV1kEg.mjs").then((m) => m.default),
803
+ "start": () => import("./start-DYW9Ar9y.mjs").then((m) => m.default),
804
+ "preview": () => import("./preview-B6OYX5qM.mjs").then((m) => m.default),
805
+ "key:generate": () => import("./key-CmUJM4SG.mjs").then((m) => m.default),
806
+ "test": () => import("./test-CUMzRaAD.mjs").then((m) => m.default),
807
+ "route:list": () => import("./routes-DF0y4tVg.mjs").then((m) => m.default),
808
+ "typegen": () => import("./typegen-BljZvG5y.mjs").then((m) => m.default),
809
+ "stub:publish": () => import("./stub-publish-BvtOsp9D.mjs").then((m) => m.default),
810
+ ...makeCommands,
811
+ "make:model": makeModelCommand,
812
+ "db:generate": generateCommand,
813
+ "db:migrate": migrateCommand,
814
+ "db:seed": seedCommand,
815
+ "queue:work": queueWorkCommand,
816
+ "queue:failed": queueFailedCommand,
817
+ "queue:retry": queueRetryCommand,
818
+ "queue:forget": queueForgetCommand,
819
+ "queue:flush": queueFlushCommand,
820
+ "schedule:list": scheduleListCommand,
821
+ "schedule:run": scheduleRunCommand,
822
+ "schedule:work": scheduleWorkCommand
823
+ }
824
+ });
825
+ runMain(main);
826
+ export { resolveRoot as a, parsePort as i, binOf as n, rootArg as o, displayUrl as r, serverArgs as s, STUBS_DIR as t };