create-nexwire 4.1.2 → 4.2.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.
Files changed (52) hide show
  1. package/express/gitignore-stub +31 -24
  2. package/express/package.json +2 -2
  3. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
  4. package/express/src/database/migrations/sqlite/meta/_journal.json +12 -12
  5. package/express/src/framework/maker-cli/db/index.mjs +1 -1
  6. package/express/src/framework/maker-cli/deploy/core.mjs +0 -1
  7. package/express/src/framework/maker-cli/index.mjs +13 -3
  8. package/express/src/framework/maker-cli/module/core.mjs +121 -43
  9. package/express/src/framework/maker-cli/runtime/core.mjs +26 -7
  10. package/express/src/framework/maker-cli/runtime/index.mjs +120 -104
  11. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  12. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  13. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  14. package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  15. package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  16. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  17. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  18. package/express/src/framework/maker-cli/utils/help.mjs +374 -50
  19. package/express/src/framework/maker-cli/utils/naming.mjs +83 -20
  20. package/express/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  21. package/express/src/framework/queue/queue.ts +402 -342
  22. package/express/src/modules/auth/database/seeders/role.ts +18 -18
  23. package/express/src/modules/auth/database/seeders/user.ts +35 -35
  24. package/express/vitest.config.ts +38 -23
  25. package/hono/gitignore-stub +31 -24
  26. package/hono/package.json +2 -2
  27. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
  28. package/hono/src/database/migrations/sqlite/meta/_journal.json +12 -12
  29. package/hono/src/framework/database/paginate.ts +253 -253
  30. package/hono/src/framework/maker-cli/db/index.mjs +1 -1
  31. package/hono/src/framework/maker-cli/deploy/core.mjs +0 -1
  32. package/hono/src/framework/maker-cli/index.mjs +13 -3
  33. package/hono/src/framework/maker-cli/module/core.mjs +116 -38
  34. package/hono/src/framework/maker-cli/runtime/core.mjs +24 -6
  35. package/hono/src/framework/maker-cli/runtime/index.mjs +120 -104
  36. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  37. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  38. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  39. package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  40. package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  41. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  42. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  43. package/hono/src/framework/maker-cli/utils/help.mjs +374 -50
  44. package/hono/src/framework/maker-cli/utils/naming.mjs +83 -20
  45. package/hono/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  46. package/hono/src/framework/queue/queue.ts +402 -342
  47. package/hono/src/modules/auth/database/seeders/role.ts +18 -18
  48. package/hono/src/modules/auth/database/seeders/user.ts +35 -35
  49. package/hono/vitest.config.ts +38 -23
  50. package/package.json +1 -1
  51. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -73
  52. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -69
@@ -5,7 +5,7 @@ import { drizzleGenerateArgs, ensureDatabaseDirectory, ensureMigrationMeta, sync
5
5
  import { detectDialect, openApiEnabled } from "../utils/env-db.mjs";
6
6
  import { writeFiles } from "../utils/file-ops.mjs";
7
7
  import { hasFlag } from "../utils/flags.mjs";
8
- import { assertName, camelCase, pascal } from "../utils/naming.mjs";
8
+ import { assertName, camelCase, normalizeName, pascal, plural, snakeCase } from "../utils/naming.mjs";
9
9
  import { packageScript, runNodeScript } from "../utils/process.mjs";
10
10
 
11
11
  const stubsRoot = path.resolve(import.meta.dirname, "../stubs");
@@ -18,7 +18,6 @@ const STUBS = {
18
18
  },
19
19
  controller: {
20
20
  openapi: "controller/openapi.ts.stub",
21
- openapiWithModel: "controller/openapi.with-model.ts.stub",
22
21
  plain: "controller/plain.ts.stub",
23
22
  standalone: "controller/standalone.ts.stub"
24
23
  },
@@ -36,7 +35,8 @@ const STUBS = {
36
35
  }
37
36
  },
38
37
  seeder: {
39
- named: "seeder/name.ts.stub"
38
+ named: "seeder/name.ts.stub",
39
+ standalone: "seeder/standalone.ts.stub"
40
40
  },
41
41
  service: {
42
42
  named: "service/name.ts.stub",
@@ -185,6 +185,35 @@ function resolveModuleRef(rawModule, ...flagGroups) {
185
185
  return { moduleName, leaf: leafName(moduleName) };
186
186
  }
187
187
 
188
+ /**
189
+ * Find an existing module directory whose folder name spells the requested
190
+ * module differently, only in the separators used (`my_module` vs `my-module`).
191
+ *
192
+ * Names are canonicalised to kebab-case, so a module scaffolded before this
193
+ * happened can live in a folder the canonical name no longer resolves to.
194
+ * Without this lookup, normalising the name would lock a developer out of their
195
+ * own module. Matching is segment by segment so a panel path such as
196
+ * `admin/reporting` keeps working, and each candidate is a real directory
197
+ * inside src/modules, so the result cannot escape it.
198
+ */
199
+ async function resolveExistingModuleDir(moduleName) {
200
+ let current = path.resolve(process.cwd(), "src/modules");
201
+ for (const segment of moduleName.split("/").filter(Boolean)) {
202
+ let entries;
203
+ try {
204
+ entries = await fs.readdir(current, { withFileTypes: true });
205
+ } catch {
206
+ return "";
207
+ }
208
+ const directories = entries.filter((entry) => entry.isDirectory());
209
+ const exact = directories.find((entry) => entry.name === segment);
210
+ const loose = exact ?? directories.find((entry) => normalizeName(entry.name) === normalizeName(segment));
211
+ if (!loose) return "";
212
+ current = path.join(current, loose.name);
213
+ }
214
+ return current;
215
+ }
216
+
188
217
  /** Ensure a module directory exists, throwing a helpful error if not. */
189
218
  async function assertModuleExists(moduleName) {
190
219
  const root = moduleRoot(moduleName);
@@ -192,6 +221,8 @@ async function assertModuleExists(moduleName) {
192
221
  try {
193
222
  stats = await fs.stat(root);
194
223
  } catch {
224
+ const legacy = await resolveExistingModuleDir(moduleName);
225
+ if (legacy) return legacy;
195
226
  throw new Error(`Module does not exist: ${moduleName}. Create it first with: bun maker module:make ${moduleName}`);
196
227
  }
197
228
  if (!stats.isDirectory()) {
@@ -200,14 +231,28 @@ async function assertModuleExists(moduleName) {
200
231
  return root;
201
232
  }
202
233
 
203
- /** Build the placeholder substitution map shared by module file generators. */
234
+ /**
235
+ * Build the placeholder substitution map shared by module file generators.
236
+ *
237
+ * The same developer-supplied name reaches four different places in a stub, and
238
+ * each needs a different shape: import paths use the kebab file name, the
239
+ * service identifier is camelCase, the table identifier is camelCase plural and
240
+ * the SQL table name is snake_case plural. Deriving all of them from one
241
+ * canonical name is what keeps a multi-word name from landing in an identifier
242
+ * position as `game-configService`, which parses as subtraction.
243
+ */
204
244
  function moduleFileVars(moduleName, controller) {
245
+ const base = normalizeName(controller);
205
246
  return {
206
247
  module: moduleName,
207
- controller,
208
- ClassName: pascal(controller),
248
+ // File name on disk and therefore every import path that points at it.
249
+ controller: base,
250
+ // Identifier the stubs export and call.
251
+ ServiceName: `${camelCase(base)}Service`,
252
+ ClassName: pascal(base),
209
253
  name: leafName(moduleName),
210
- tableVariable: `${controller}s`
254
+ tableName: plural(snakeCase(base)),
255
+ tableVariable: plural(camelCase(base))
211
256
  };
212
257
  }
213
258
 
@@ -244,8 +289,7 @@ async function serviceFile(moduleName, controllerName, withModel) {
244
289
  async function moduleFiles(moduleName, openApi) {
245
290
  const name = leafName(moduleName);
246
291
  const vars = moduleFileVars(moduleName, name);
247
- // The with-model controller variant only exists for the openapi style.
248
- const controllerStub = openApi ? STUBS.controller.openapiWithModel : STUBS.controller.plain;
292
+ const controllerStub = openApi ? STUBS.controller.openapi : STUBS.controller.plain;
249
293
  return {
250
294
  "facade.ts": await stub(STUBS.facade.named, vars),
251
295
  [`controllers/${name}.ts`]: await stub(controllerStub, vars),
@@ -286,25 +330,28 @@ async function standaloneRouteTemplate(moduleName, routeName) {
286
330
 
287
331
  /** Generate a named model file for a module. */
288
332
  async function namedModelTemplate(moduleName, name, dialect) {
333
+ const base = normalizeName(name);
289
334
  return await stub(STUBS.model.named[dialect], {
290
335
  module: moduleName,
291
- name,
292
- controller: name,
293
- ClassName: pascal(name),
294
- tableName: `${name}s`,
295
- tableVariable: `${name}s`
336
+ name: base,
337
+ controller: base,
338
+ ClassName: pascal(base),
339
+ tableName: plural(snakeCase(base)),
340
+ tableVariable: plural(camelCase(base))
296
341
  });
297
342
  }
298
343
 
299
344
  /** Generate a seeder file for a module model. */
300
345
  async function namedSeederTemplate(moduleName, modelName, className) {
346
+ const base = normalizeName(modelName);
301
347
  return await stub(STUBS.seeder.named, {
302
348
  module: moduleName,
303
- name: modelName,
349
+ // Import path: the model file is named after the model, in kebab-case.
350
+ name: base,
304
351
  controller: className,
305
352
  ClassName: pascal(className),
306
- tableName: `${modelName}s`,
307
- tableVariable: `${modelName}s`
353
+ tableName: plural(snakeCase(base)),
354
+ tableVariable: plural(camelCase(base))
308
355
  });
309
356
  }
310
357
 
@@ -714,7 +761,16 @@ export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
714
761
  await writeFileSafe(modelPath, await namedModelTemplate(moduleName, name, dialect), flags, "Model file");
715
762
  }
716
763
 
717
- /** Generate a seeder file for an existing module model. */
764
+ /**
765
+ * Generate a seeder file.
766
+ *
767
+ * A seeder does not have to own the table it writes to, and does not have to
768
+ * write to only one table, so a missing model is not fatal. When the model is
769
+ * there we import it and pre-wire the insert; when it is not we emit the
770
+ * standalone shape and let the developer choose what to write. That is the
771
+ * same trade the controller makes when there is no service, and the service
772
+ * makes when there is no model. Seeder was the only generator that refused.
773
+ */
718
774
  export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
719
775
  const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
720
776
  const flags = [];
@@ -725,36 +781,58 @@ export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
725
781
  }
726
782
  flags.push(...extraFlags);
727
783
  const root = await assertModuleExists(moduleName);
728
- let modelName = name;
729
- let modelPath = path.join(root, "database", "models", `${modelName}.ts`);
730
-
731
- try {
732
- await fs.access(modelPath);
733
- } catch {
734
- // Fall back to the module's own model. This must use the leaf, not
735
- // moduleName: for a panel module moduleName is "admin/testme", so using it
736
- // would probe database/models/admin/testme.ts and never match.
737
- modelName = leaf;
738
- modelPath = path.join(root, "database", "models", `${modelName}.ts`);
739
784
 
740
- try {
741
- await fs.access(modelPath);
785
+ // The fallback probes the leaf, not moduleName: for a panel module moduleName
786
+ // is "admin/testme", so using it would look for database/models/admin/testme.ts
787
+ // and never match.
788
+ const modelName = await existingModelName(root, name, leaf);
789
+ let seeder;
790
+ if (modelName) {
791
+ seeder = await namedSeederTemplate(moduleName, modelName, name);
792
+ if (modelName !== name) {
742
793
  console.log(`Model '${name}' not found, using module model '${modelName}' for seeder '${name}'.`);
743
- } catch {
744
- const panel = panelOf(moduleName);
745
- const suggest = panel ? `${leaf} ${name} --path=${panel}` : `${leaf} ${name}`;
746
- throw new Error(
747
- `Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${suggest}`
748
- );
749
794
  }
795
+ } else {
796
+ seeder = await stub(STUBS.seeder.standalone, {
797
+ module: moduleName,
798
+ name,
799
+ ClassName: pascal(name),
800
+ // The stub shows this as the conventional table name, so derive it the
801
+ // same way the bound stub does - it is what a model of this name exports.
802
+ tableVariable: plural(camelCase(name))
803
+ });
804
+ const available = await listModels(root);
805
+ console.log(`No model '${name}' in module ${moduleName}, so this is a standalone seeder: it imports no model and writes nothing until you fill it in.`);
806
+ console.log(
807
+ available.length
808
+ ? ` Models in this module: ${available.join(", ")}. Run module:make-seeder ${moduleName} <one of these> for a seeder with the model already imported.`
809
+ : ` This module has no models yet. Create one with: bun maker module:make-model ${moduleName} ${name}`
810
+ );
750
811
  }
751
812
 
752
- const seeder = await namedSeederTemplate(moduleName, modelName, name);
753
813
  await writeFileSafe(path.join(root, `database/seeders/${name}.ts`), seeder, flags, "Seeder file");
754
814
 
755
815
  console.log(`Seeder ready: ${moduleName}/${name}`);
756
816
  }
757
817
 
818
+ /** The model a seeder binds to: the requested one, else the module's own. Null when neither exists. */
819
+ async function existingModelName(root, name, leaf) {
820
+ for (const candidate of [name, leaf]) {
821
+ if (await pathExists(path.join(root, "database", "models", `${candidate}.ts`))) return candidate;
822
+ }
823
+ return null;
824
+ }
825
+
826
+ /** Model file names in a module, without the extension. Empty when there are none. */
827
+ async function listModels(root) {
828
+ try {
829
+ const entries = await fs.readdir(path.join(root, "database", "models"));
830
+ return entries.filter((f) => f.endsWith(".ts")).map((f) => f.slice(0, -3));
831
+ } catch {
832
+ return [];
833
+ }
834
+ }
835
+
758
836
  /** Generate a job file for an existing module. */
759
837
  export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
760
838
  const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
@@ -498,24 +498,42 @@ export async function runVite(configPath) {
498
498
  await runCommand(localBin("vite"), ["--config", configPath]);
499
499
  }
500
500
 
501
- /** Run Vitest tests (backend). */
501
+ /**
502
+ * Strip the argument separator npm inserts.
503
+ *
504
+ * `npm run maker test -- posts` reaches us as `test posts`, but
505
+ * `npm run maker test -- -- posts` reaches us as `test -- posts`, because npm
506
+ * eats one separator and forwards the other. Vitest reads a bare `--` as a
507
+ * positional filter that matches no test file, which silently runs the whole
508
+ * suite instead of the one you asked for - so the command appears to work while
509
+ * doing something else. Dropping a leading `--` makes both spellings behave the
510
+ * same, and makes the passthrough behave identically across package managers.
511
+ */
512
+ function vitestArgs(rawArgs) {
513
+ return rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
514
+ }
515
+
516
+ /** Run Vitest tests once (backend). */
502
517
  export async function runTest(rawArgs = []) {
503
- await runCommand(localBin("vitest"), rawArgs);
518
+ // "run" is required. Bare `vitest` is watch mode, which never exits - so
519
+ // without this, `maker test` hung in any interactive terminal while CI and
520
+ // piped runs exited normally, and the two looked identical from a log.
521
+ await runCommand(localBin("vitest"), ["run", ...vitestArgs(rawArgs)]);
504
522
  }
505
523
 
506
524
  /** Run Vitest in watch mode (backend). */
507
525
  export async function runTestWatch(rawArgs = []) {
508
- await runCommand(localBin("vitest"), rawArgs);
526
+ await runCommand(localBin("vitest"), vitestArgs(rawArgs));
509
527
  }
510
528
 
511
- /** Run Vitest with coverage (backend). */
529
+ /** Run Vitest once with coverage (backend). */
512
530
  export async function runTestCoverage(rawArgs = []) {
513
- await runCommand(localBin("vitest"), ["--coverage", ...rawArgs]);
531
+ await runCommand(localBin("vitest"), ["run", "--coverage", ...vitestArgs(rawArgs)]);
514
532
  }
515
533
 
516
534
  /** Run Vitest in UI mode (backend). */
517
535
  export async function runTestUI(rawArgs = []) {
518
- await runCommand(localBin("vitest"), ["--ui", ...rawArgs]);
536
+ await runCommand(localBin("vitest"), ["--ui", ...vitestArgs(rawArgs)]);
519
537
  }
520
538
 
521
539
  /** Clear Vite cache directories. */
@@ -1,104 +1,120 @@
1
- import { clearViteCache, runRuntime, runTest, runTestCoverage, runTestUI, runTestWatch, runUi, runVite } from "./core.mjs";
2
-
3
- /** Register runtime commands (dev, serve, queue, schedule, UI tools) on the CLI program. */
4
- export function registerRuntimeCommands(program, rawArgs) {
5
- program
6
- .command("dev")
7
- .description("Start API + Vue 3 UI plus optional workers/tools")
8
- .option("--view <tools>", "Comma-separated UI tools to launch (redis,maildev,studio)")
9
- .option("--viewer <tools>", "Alias for --view (npm expands --view to --viewer)")
10
- .option("--with <tools>", "Alias for --view")
11
- .option("--with-redis-view", "Launch Redis Commander")
12
- .option("--with-maildev", "Launch MailDev")
13
- .option("--with-db-studio", "Launch Drizzle Studio")
14
- .allowUnknownOption(true)
15
- .action(async () => runRuntime("dev", rawArgs));
16
- program
17
- .command("serve")
18
- .description("Start HTTP server (dist first if built, else src)")
19
- .option("--prod", "Run production build")
20
- .option("--runtime <name>", "Runtime: node or bun", "node")
21
- .option("--watch", "Watch for file changes (src mode)")
22
- .option("--src", "Force source mode even if dist exists")
23
- .allowUnknownOption(true)
24
- .action(async () => runRuntime("serve", rawArgs));
25
- program
26
- .command("queue:work")
27
- .description("Start queue worker (dist first if built, else src)")
28
- .option("--queue <name>", "Queue name", "default")
29
- .option("--prod", "Run production build")
30
- .option("--runtime <name>", "Runtime: node or bun", "node")
31
- .option("--src", "Force source mode even if dist exists")
32
- .allowUnknownOption(true)
33
- .action(async () => runRuntime("queue:work", rawArgs));
34
- program
35
- .command("queue:clear")
36
- .description("Clear all queue keys")
37
- .allowUnknownOption(true)
38
- .action(async () => runRuntime("queue:clear", rawArgs));
39
- program
40
- .command("schedule:work")
41
- .description("Start scheduler worker (dist first if built, else src)")
42
- .option("--prod", "Run production build")
43
- .option("--runtime <name>", "Runtime: node or bun", "node")
44
- .option("--src", "Force source mode even if dist exists")
45
- .allowUnknownOption(true)
46
- .action(async () => runRuntime("schedule:work", rawArgs));
47
- program
48
- .command("maildev:view")
49
- .description("Start MailDev SMTP and web UI")
50
- .allowUnknownOption(true)
51
- .action(async () => runUi("maildev:view"));
52
- program
53
- .command("redis:view")
54
- .description("Start Redis Commander UI")
55
- .allowUnknownOption(true)
56
- .action(async () => runUi("redis:view"));
57
- program
58
- .command("ui:dev")
59
- .description("Start Vue 3 UI from src/resources")
60
- .allowUnknownOption(true)
61
- .action(async () => runVite("src/resources/vite.config.ts"));
62
- program
63
- .command("admin:dev")
64
- .description("Start admin UI dev server")
65
- .allowUnknownOption(true)
66
- .action(async () => runVite("src/resources/vite.config.ts"));
67
- program
68
- .command("vite:cache:clear")
69
- .description("Clear Vite cache folders (cross-platform)")
70
- .allowUnknownOption(true)
71
- .action(async () => clearViteCache());
72
- program
73
- .command("test")
74
- .description("Run Vitest backend tests")
75
- .allowUnknownOption(true)
76
- .action(async () => {
77
- const args = process.argv.slice(3);
78
- await runTest(args);
79
- });
80
- program
81
- .command("test:watch")
82
- .description("Run Vitest backend tests in watch mode")
83
- .allowUnknownOption(true)
84
- .action(async () => {
85
- const args = process.argv.slice(3);
86
- await runTestWatch(args);
87
- });
88
- program
89
- .command("test:coverage")
90
- .description("Run Vitest backend tests with code coverage")
91
- .allowUnknownOption(true)
92
- .action(async () => {
93
- const args = process.argv.slice(3);
94
- await runTestCoverage(args);
95
- });
96
- program
97
- .command("test:ui")
98
- .description("Run Vitest backend tests in UI mode")
99
- .allowUnknownOption(true)
100
- .action(async () => {
101
- const args = process.argv.slice(3);
102
- await runTestUI(args);
103
- });
104
- }
1
+ import { clearViteCache, runRuntime, runTest, runTestCoverage, runTestUI, runTestWatch, runUi, runVite } from "./core.mjs";
2
+ import { initProjectCommands, PROJECT_COMMANDS_PATH } from "../utils/project-commands.mjs";
3
+
4
+ /** Register runtime commands (dev, serve, queue, schedule, UI tools) on the CLI program. */
5
+ export function registerRuntimeCommands(program, rawArgs) {
6
+ program
7
+ .command("dev")
8
+ .description("Start API + Vue 3 UI plus optional workers/tools")
9
+ .option("--view <tools>", "Comma-separated UI tools to launch (redis,maildev,studio)")
10
+ .option("--viewer <tools>", "Alias for --view (npm expands --view to --viewer)")
11
+ .option("--with <tools>", "Alias for --view")
12
+ .option("--with-redis-view", "Launch Redis Commander")
13
+ .option("--with-maildev", "Launch MailDev")
14
+ .option("--with-db-studio", "Launch Drizzle Studio")
15
+ .allowUnknownOption(true)
16
+ .action(async () => runRuntime("dev", rawArgs));
17
+ program
18
+ .command("serve")
19
+ .description("Start HTTP server (dist first if built, else src)")
20
+ .option("--prod", "Run production build")
21
+ .option("--runtime <name>", "Runtime: node or bun", "node")
22
+ .option("--watch", "Watch for file changes (src mode)")
23
+ .option("--src", "Force source mode even if dist exists")
24
+ .allowUnknownOption(true)
25
+ .action(async () => runRuntime("serve", rawArgs));
26
+ program
27
+ .command("queue:work")
28
+ .description("Start queue worker (dist first if built, else src)")
29
+ .option("--queue <name>", "Queue name", "default")
30
+ .option("--prod", "Run production build")
31
+ .option("--runtime <name>", "Runtime: node or bun", "node")
32
+ .option("--src", "Force source mode even if dist exists")
33
+ .allowUnknownOption(true)
34
+ .action(async () => runRuntime("queue:work", rawArgs));
35
+ program
36
+ .command("queue:clear")
37
+ .description("Clear all queue keys")
38
+ .allowUnknownOption(true)
39
+ .action(async () => runRuntime("queue:clear", rawArgs));
40
+ program
41
+ .command("schedule:work")
42
+ .description("Start scheduler worker (dist first if built, else src)")
43
+ .option("--prod", "Run production build")
44
+ .option("--runtime <name>", "Runtime: node or bun", "node")
45
+ .option("--src", "Force source mode even if dist exists")
46
+ .allowUnknownOption(true)
47
+ .action(async () => runRuntime("schedule:work", rawArgs));
48
+ program
49
+ .command("maildev:view")
50
+ .description("Start MailDev SMTP and web UI")
51
+ .allowUnknownOption(true)
52
+ .action(async () => runUi("maildev:view"));
53
+ program
54
+ .command("redis:view")
55
+ .description("Start Redis Commander UI")
56
+ .allowUnknownOption(true)
57
+ .action(async () => runUi("redis:view"));
58
+ program
59
+ .command("ui:dev")
60
+ .description("Start Vue 3 UI from src/resources")
61
+ .allowUnknownOption(true)
62
+ .action(async () => runVite("src/resources/vite.config.ts"));
63
+ program
64
+ .command("admin:dev")
65
+ .description("Start admin UI dev server")
66
+ .allowUnknownOption(true)
67
+ .action(async () => runVite("src/resources/vite.config.ts"));
68
+ program
69
+ .command("vite:cache:clear")
70
+ .description("Clear Vite cache folders (cross-platform)")
71
+ .allowUnknownOption(true)
72
+ .action(async () => clearViteCache());
73
+ program
74
+ .command("maker:init")
75
+ .description(`Create ${PROJECT_COMMANDS_PATH} for your own commands`)
76
+ .option("--force", "Overwrite an existing file")
77
+ .allowUnknownOption(true)
78
+ .action(async (options) => {
79
+ const { created, overwritten } = await initProjectCommands(process.cwd(), { force: Boolean(options?.force) });
80
+ // created = written into a new file; overwritten = replaced under --force.
81
+ // Neither means the file was already there and was left alone.
82
+ console.log(
83
+ created || overwritten
84
+ ? `Add your commands to ${PROJECT_COMMANDS_PATH}, then run: maker --help`
85
+ : `${PROJECT_COMMANDS_PATH} already exists and was left alone. Edit it to add commands, or re-run with --force to replace it.`
86
+ );
87
+ });
88
+ program
89
+ .command("test")
90
+ .description("Run Vitest backend tests")
91
+ .allowUnknownOption(true)
92
+ .action(async () => {
93
+ const args = process.argv.slice(3);
94
+ await runTest(args);
95
+ });
96
+ program
97
+ .command("test:watch")
98
+ .description("Run Vitest backend tests in watch mode")
99
+ .allowUnknownOption(true)
100
+ .action(async () => {
101
+ const args = process.argv.slice(3);
102
+ await runTestWatch(args);
103
+ });
104
+ program
105
+ .command("test:coverage")
106
+ .description("Run Vitest backend tests with code coverage")
107
+ .allowUnknownOption(true)
108
+ .action(async () => {
109
+ const args = process.argv.slice(3);
110
+ await runTestCoverage(args);
111
+ });
112
+ program
113
+ .command("test:ui")
114
+ .description("Run Vitest backend tests in UI mode")
115
+ .allowUnknownOption(true)
116
+ .action(async () => {
117
+ const args = process.argv.slice(3);
118
+ await runTestUI(args);
119
+ });
120
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Handler } from "hono";
2
2
  import { HttpStatusCodes } from "@/framework/facade.js";
3
- import { {{controller}}Service } from "@/modules/{{module}}/services/{{controller}}.js";
3
+ import { {{ServiceName}} } from "@/modules/{{module}}/services/{{controller}}.js";
4
4
 
5
5
  /**
6
6
  * Why: Returns every {{controller}} row for list screens.
@@ -8,7 +8,7 @@ import { {{controller}}Service } from "@/modules/{{module}}/services/{{controlle
8
8
  * Where: GET /{{controller}} route.
9
9
  */
10
10
  export const index: Handler = async (c: any) => {
11
- const data = await {{controller}}Service.list();
11
+ const data = await {{ServiceName}}.list();
12
12
 
13
13
  return c.json({ message: "{{controller}} list fetched successfully", data });
14
14
  };
@@ -20,7 +20,7 @@ export const index: Handler = async (c: any) => {
20
20
  */
21
21
  export const show: Handler = async (c: any) => {
22
22
  const { id } = c.req.valid("param");
23
- const data = await {{controller}}Service.findOne(id);
23
+ const data = await {{ServiceName}}.findOne(id);
24
24
 
25
25
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
26
26
 
@@ -34,7 +34,7 @@ export const show: Handler = async (c: any) => {
34
34
  */
35
35
  export const store: Handler = async (c: any) => {
36
36
  const body = c.req.valid("json");
37
- const data = await {{controller}}Service.create(body);
37
+ const data = await {{ServiceName}}.create(body);
38
38
 
39
39
  return c.json({ message: "{{controller}} created successfully", data }, HttpStatusCodes.CREATED);
40
40
  };
@@ -47,7 +47,7 @@ export const store: Handler = async (c: any) => {
47
47
  export const update: Handler = async (c: any) => {
48
48
  const { id } = c.req.valid("param");
49
49
  const body = c.req.valid("json");
50
- const data = await {{controller}}Service.update(id, body);
50
+ const data = await {{ServiceName}}.update(id, body);
51
51
 
52
52
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
53
53
 
@@ -61,7 +61,7 @@ export const update: Handler = async (c: any) => {
61
61
  */
62
62
  export const destroy: Handler = async (c: any) => {
63
63
  const { id } = c.req.valid("param");
64
- const data = await {{controller}}Service.remove(id);
64
+ const data = await {{ServiceName}}.remove(id);
65
65
 
66
66
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
67
67
 
@@ -2,7 +2,7 @@ import type { Handler } from "hono";
2
2
  import { HttpStatusCodes } from "@/framework/facade.js";
3
3
  import { validate } from "@/framework/facade.js";
4
4
  import { Create{{ClassName}}Schema, Update{{ClassName}}Schema, {{ClassName}}IdParamsSchema } from "@/modules/{{module}}/schemas/{{controller}}.js";
5
- import { {{controller}}Service } from "@/modules/{{module}}/services/{{controller}}.js";
5
+ import { {{ServiceName}} } from "@/modules/{{module}}/services/{{controller}}.js";
6
6
 
7
7
  /**
8
8
  * Why: Returns every {{controller}} row for list screens.
@@ -10,7 +10,7 @@ import { {{controller}}Service } from "@/modules/{{module}}/services/{{controlle
10
10
  * Where: GET /{{controller}} route.
11
11
  */
12
12
  export const index: Handler = async (c: any) => {
13
- const data = await {{controller}}Service.list();
13
+ const data = await {{ServiceName}}.list();
14
14
 
15
15
  return c.json({ message: "{{controller}} list fetched successfully", data });
16
16
  };
@@ -22,7 +22,7 @@ export const index: Handler = async (c: any) => {
22
22
  */
23
23
  export const show: Handler = async (c: any) => {
24
24
  const params = await validate({{ClassName}}IdParamsSchema, c.req.param());
25
- const data = await {{controller}}Service.findOne(params.id);
25
+ const data = await {{ServiceName}}.findOne(params.id);
26
26
 
27
27
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
28
28
 
@@ -36,7 +36,7 @@ export const show: Handler = async (c: any) => {
36
36
  */
37
37
  export const store: Handler = async (c: any) => {
38
38
  const body = await validate(Create{{ClassName}}Schema, await c.req.json());
39
- const data = await {{controller}}Service.create(body);
39
+ const data = await {{ServiceName}}.create(body);
40
40
 
41
41
  return c.json({ message: "{{controller}} created successfully", data }, HttpStatusCodes.CREATED);
42
42
  };
@@ -49,7 +49,7 @@ export const store: Handler = async (c: any) => {
49
49
  export const update: Handler = async (c: any) => {
50
50
  const params = await validate({{ClassName}}IdParamsSchema, c.req.param());
51
51
  const body = await validate(Update{{ClassName}}Schema, await c.req.json());
52
- const data = await {{controller}}Service.update(params.id, body);
52
+ const data = await {{ServiceName}}.update(params.id, body);
53
53
 
54
54
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
55
55
 
@@ -63,7 +63,7 @@ export const update: Handler = async (c: any) => {
63
63
  */
64
64
  export const destroy: Handler = async (c: any) => {
65
65
  const params = await validate({{ClassName}}IdParamsSchema, c.req.param());
66
- const data = await {{controller}}Service.remove(params.id);
66
+ const data = await {{ServiceName}}.remove(params.id);
67
67
 
68
68
  if (!data) return c.json({ message: "{{controller}} not found" }, HttpStatusCodes.NOT_FOUND);
69
69
 
@@ -9,6 +9,6 @@
9
9
  * When: Replace this with real logic and rename it.
10
10
  * Where: Anywhere inside the {{module}} module.
11
11
  */
12
- export function {{name}}Helper<T>(value: T): T {
12
+ export function {{Name}}Helper<T>(value: T): T {
13
13
  return value;
14
14
  }