create-nexwire 4.0.0 → 4.1.1

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 (173) hide show
  1. package/README.md +5 -4
  2. package/express/README.md +13 -7
  3. package/express/package.json +2 -2
  4. package/express/src/config/session.ts +1 -1
  5. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  6. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  7. package/express/src/database/schema.ts +3 -3
  8. package/express/src/framework/cache/cache.ts +82 -81
  9. package/express/src/framework/database/seed.ts +1 -1
  10. package/express/src/framework/facade.ts +1 -24
  11. package/express/src/framework/http/app.ts +5 -5
  12. package/express/src/framework/http/openapi.ts +1 -1
  13. package/express/src/framework/http/router.ts +3 -18
  14. package/express/src/framework/http/types.ts +19 -0
  15. package/express/src/framework/http/validation.ts +18 -18
  16. package/express/src/framework/maker-cli/middleware/core.mjs +41 -47
  17. package/express/src/framework/maker-cli/module/core.mjs +920 -684
  18. package/express/src/framework/maker-cli/module/index.mjs +202 -140
  19. package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
  20. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +40 -14
  21. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +40 -15
  22. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +42 -16
  23. package/express/src/framework/maker-cli/stubs/controller/standalone.ts.stub +47 -0
  24. package/express/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  25. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  26. package/express/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  27. package/express/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  28. package/express/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  29. package/express/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  30. package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +50 -50
  31. package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  32. package/express/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  33. package/express/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  34. package/express/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +113 -0
  35. package/express/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  36. package/express/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  37. package/express/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  38. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  39. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  40. package/express/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  41. package/express/src/framework/maker-cli/utils/naming.mjs +56 -13
  42. package/express/src/framework/modules/discover.ts +57 -54
  43. package/express/src/framework/notification/index.ts +1 -1
  44. package/express/src/framework/realtime/broadcast.ts +46 -46
  45. package/express/src/framework/server.ts +2 -1
  46. package/express/src/framework/session/session.ts +102 -98
  47. package/express/src/framework/support/mail.ts +48 -48
  48. package/express/src/framework/support/status-codes.ts +75 -0
  49. package/express/src/middlewares/auth-middleware.ts +1 -1
  50. package/express/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
  51. package/express/src/modules/auth/__tests__/login.test.ts +171 -0
  52. package/express/src/modules/auth/__tests__/logout.test.ts +60 -0
  53. package/express/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
  54. package/express/src/modules/auth/__tests__/me.test.ts +91 -0
  55. package/express/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  56. package/express/src/modules/auth/__tests__/register.test.ts +291 -0
  57. package/express/src/modules/auth/__tests__/resetpass.test.ts +206 -0
  58. package/express/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
  59. package/express/src/modules/auth/controllers/auth.ts +254 -0
  60. package/express/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -15
  61. package/{hono/src/modules/auth/database/models/notifications.ts → express/src/modules/auth/database/models/notification.ts} +1 -1
  62. package/express/src/modules/auth/database/models/role.ts +1 -1
  63. package/express/src/modules/auth/database/models/user.ts +1 -1
  64. package/express/src/modules/auth/facade.ts +12 -0
  65. package/express/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +3 -4
  66. package/express/src/modules/auth/jobs/registeruser.ts +23 -23
  67. package/express/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  68. package/express/src/modules/auth/routes/role.ts +33 -33
  69. package/{hono/src/modules/auth/controllers/auth.schema.ts → express/src/modules/auth/schemas/auth.ts} +102 -102
  70. package/express/src/modules/auth/services/forgetpassword.ts +66 -0
  71. package/express/src/modules/auth/services/login.ts +63 -0
  72. package/express/src/modules/auth/services/logout.ts +25 -0
  73. package/express/src/modules/auth/services/logoutalldevices.ts +19 -0
  74. package/express/src/modules/auth/services/me.ts +31 -0
  75. package/express/src/modules/auth/services/refreshtoken.ts +110 -0
  76. package/express/src/modules/auth/services/register.ts +141 -0
  77. package/express/src/modules/auth/services/resetpassword.ts +69 -0
  78. package/express/src/modules/auth/services/verifyemail.ts +77 -0
  79. package/express/src/modules/auth/types/auth.ts +31 -0
  80. package/express/src/resources/src/assets/images/logo.png +0 -0
  81. package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -0
  82. package/express/src/resources/src/pages/auth/login.vue +1 -1
  83. package/express/src/storage/database/nexwire.sqlite +0 -0
  84. package/hono/README.md +13 -7
  85. package/hono/package.json +1 -1
  86. package/hono/src/config/session.ts +1 -1
  87. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  88. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  89. package/hono/src/database/schema.ts +3 -3
  90. package/hono/src/framework/cache/cache.ts +82 -81
  91. package/hono/src/framework/database/seed.ts +1 -1
  92. package/hono/src/framework/http/app.ts +5 -5
  93. package/hono/src/framework/http/validation.ts +21 -21
  94. package/hono/src/framework/maker-cli/middleware/core.mjs +40 -46
  95. package/hono/src/framework/maker-cli/module/core.mjs +401 -168
  96. package/hono/src/framework/maker-cli/module/index.mjs +73 -11
  97. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +28 -16
  98. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +28 -17
  99. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +28 -16
  100. package/hono/src/framework/maker-cli/stubs/controller/standalone.ts.stub +56 -0
  101. package/hono/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  102. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  103. package/hono/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  104. package/hono/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  105. package/hono/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  106. package/hono/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  107. package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -49
  108. package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  109. package/hono/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  110. package/hono/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  111. package/hono/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +112 -0
  112. package/hono/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  113. package/hono/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  114. package/hono/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  115. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  116. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  117. package/hono/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  118. package/hono/src/framework/maker-cli/utils/naming.mjs +56 -13
  119. package/hono/src/framework/modules/discover.ts +57 -54
  120. package/hono/src/framework/notification/index.ts +1 -1
  121. package/hono/src/framework/realtime/broadcast.ts +46 -46
  122. package/hono/src/framework/server.ts +1 -1
  123. package/hono/src/framework/session/session.ts +121 -116
  124. package/hono/src/middlewares/auth-middleware.ts +1 -1
  125. package/hono/src/modules/auth/__tests__/forgetpass.test.ts +170 -0
  126. package/hono/src/modules/auth/__tests__/login.test.ts +154 -0
  127. package/hono/src/modules/auth/__tests__/logout.test.ts +53 -0
  128. package/hono/src/modules/auth/__tests__/logoutalldevices.test.ts +34 -0
  129. package/hono/src/modules/auth/__tests__/me.test.ts +87 -0
  130. package/hono/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  131. package/hono/src/modules/auth/__tests__/register.test.ts +258 -0
  132. package/hono/src/modules/auth/__tests__/resetpass.test.ts +214 -0
  133. package/hono/src/modules/auth/__tests__/verifyemail.test.ts +189 -0
  134. package/hono/src/modules/auth/controllers/auth.ts +238 -0
  135. package/hono/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -25
  136. package/{express/src/modules/auth/database/models/notifications.ts → hono/src/modules/auth/database/models/notification.ts} +1 -1
  137. package/hono/src/modules/auth/database/models/role.ts +1 -1
  138. package/hono/src/modules/auth/database/models/user.ts +1 -1
  139. package/hono/src/modules/auth/facade.ts +12 -0
  140. package/hono/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +5 -6
  141. package/hono/src/modules/auth/jobs/registeruser.ts +23 -23
  142. package/hono/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  143. package/hono/src/modules/auth/routes/role.ts +33 -33
  144. package/{express/src/modules/auth/controllers/auth.schema.ts → hono/src/modules/auth/schemas/auth.ts} +102 -102
  145. package/hono/src/modules/auth/services/forgetpassword.ts +58 -0
  146. package/hono/src/modules/auth/services/login.ts +61 -0
  147. package/hono/src/modules/auth/services/logout.ts +24 -0
  148. package/hono/src/modules/auth/services/logoutalldevices.ts +19 -0
  149. package/hono/src/modules/auth/services/me.ts +31 -0
  150. package/hono/src/modules/auth/services/refreshtoken.ts +110 -0
  151. package/hono/src/modules/auth/services/register.ts +132 -0
  152. package/hono/src/modules/auth/services/resetpassword.ts +69 -0
  153. package/hono/src/modules/auth/services/verifyemail.ts +77 -0
  154. package/hono/src/modules/auth/types/auth.ts +31 -0
  155. package/hono/src/resources/src/assets/images/logo.png +0 -0
  156. package/hono/src/storage/database/nexwire.sqlite +0 -0
  157. package/package.json +1 -1
  158. package/express/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  159. package/express/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -418
  160. package/express/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  161. package/express/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  162. package/express/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  163. package/express/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  164. package/express/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  165. package/express/src/modules/auth/controllers/auth.controller.ts +0 -358
  166. package/hono/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  167. package/hono/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -505
  168. package/hono/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  169. package/hono/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  170. package/hono/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  171. package/hono/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  172. package/hono/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  173. package/hono/src/modules/auth/controllers/auth.controller.ts +0 -413
@@ -5,25 +5,28 @@ 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, pascal } from "../utils/naming.mjs";
8
+ import { assertName, camelCase, pascal } 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");
12
12
 
13
13
  /** Template stubs used for module file generation. */
14
14
  const STUBS = {
15
+ schema: {
16
+ openapi: "schema/name.ts.stub",
17
+ plain: "schema/name.plain.ts.stub"
18
+ },
15
19
  controller: {
16
20
  openapi: "controller/openapi.ts.stub",
17
21
  openapiWithModel: "controller/openapi.with-model.ts.stub",
18
22
  plain: "controller/plain.ts.stub",
19
- schema: {
20
- openapi: "controller/schema.ts.stub",
21
- plain: "controller/schema.plain.ts.stub"
22
- }
23
+ standalone: "controller/standalone.ts.stub"
23
24
  },
24
25
  route: {
25
26
  api: "route/api.ts.stub",
26
- plain: "route/plain.ts.stub"
27
+ plain: "route/plain.ts.stub",
28
+ standaloneApi: "route/standalone.api.ts.stub",
29
+ standalonePlain: "route/standalone.plain.ts.stub"
27
30
  },
28
31
  model: {
29
32
  named: {
@@ -35,16 +38,21 @@ const STUBS = {
35
38
  seeder: {
36
39
  named: "seeder/name.ts.stub"
37
40
  },
38
- example: {
39
- schema: {
40
- openapi: "example/schema.ts.stub",
41
- plain: "example/schema.plain.ts.stub"
42
- },
43
- controller: "example/controller.ts.stub",
44
- routeApi: "example/route.api.ts.stub",
45
- routePlain: "example/route.plain.ts.stub",
46
- job: "example/job.ts.stub",
47
- console: "example/console.ts.stub"
41
+ service: {
42
+ named: "service/name.ts.stub",
43
+ plain: "service/name.plain.ts.stub"
44
+ },
45
+ facade: {
46
+ named: "facade/name.ts.stub"
47
+ },
48
+ helper: {
49
+ named: "helper/name.ts.stub"
50
+ },
51
+ middleware: {
52
+ local: "middleware/name.local.ts.stub"
53
+ },
54
+ type: {
55
+ named: "type/name.ts.stub"
48
56
  },
49
57
  job: {
50
58
  named: "job/name.ts.stub"
@@ -86,7 +94,95 @@ async function stub(name, values = {}) {
86
94
 
87
95
  /** Resolve the canonical module root path inside src/modules. */
88
96
  function moduleRoot(moduleName) {
89
- return path.resolve(process.cwd(), "src/modules", moduleName);
97
+ const base = path.resolve(process.cwd(), "src/modules");
98
+ const root = path.resolve(base, moduleName);
99
+ if (root !== base && !root.startsWith(base + path.sep)) {
100
+ throw new Error(`Module path escapes src/modules: ${moduleName}`);
101
+ }
102
+ return root;
103
+ }
104
+
105
+ /**
106
+ * Leaf folder name of a module. `admin/post` -> `post`, `post` -> `post`.
107
+ * File names and class names always come from the leaf; imports come from the
108
+ * full path.
109
+ */
110
+ function leafName(moduleName) {
111
+ const segments = String(moduleName).split("/").filter(Boolean);
112
+ return segments[segments.length - 1] || String(moduleName);
113
+ }
114
+
115
+ /**
116
+ * Panel prefix of a module, everything before the leaf. `admin/post` -> `admin`,
117
+ * `admin/reporting/deep` -> `admin/reporting`, `post` -> "".
118
+ */
119
+ function panelOf(moduleName) {
120
+ const segments = String(moduleName).split("/").filter(Boolean);
121
+ return segments.slice(0, -1).join("/");
122
+ }
123
+
124
+ /**
125
+ * Scalar tag for a module. Panels become a nested tag group so Scalar renders
126
+ * `Admin / Post` instead of flattening everything under one panel.
127
+ */
128
+ function moduleLabel(moduleName) {
129
+ return String(moduleName)
130
+ .split("/")
131
+ .filter(Boolean)
132
+ .map((segment) => pascal(segment))
133
+ .join(" / ");
134
+ }
135
+
136
+ /**
137
+ * Parse `--path=<panel>` into a normalized module prefix.
138
+ * A panel is only a folder separator, so nested values are allowed:
139
+ * `--path=admin` and `--path=admin/reporting` both validate.
140
+ */
141
+ function parsePanelPath(flags) {
142
+ const matches = flags.filter((flag) => flag.startsWith("--path"));
143
+ if (matches.length === 0) return "";
144
+ if (matches.length > 1) throw new Error("--path can only be given once.");
145
+ const raw = matches[0].slice("--path".length).replace(/^=/, "").trim();
146
+ if (!raw) throw new Error("--path needs a value, e.g. --path=admin");
147
+ const segments = raw
148
+ .split(/[\\/]+/)
149
+ .filter(Boolean)
150
+ .map((segment) => segment.toLowerCase());
151
+ if (segments.length === 0) throw new Error(`Invalid --path value: ${raw}`);
152
+ for (const segment of segments) {
153
+ if (!/^[a-z0-9][a-z0-9-_]*$/.test(segment)) {
154
+ throw new Error(`Invalid --path segment "${segment}". Use lowercase letters, numbers and dashes.`);
155
+ }
156
+ }
157
+ return segments.join("/");
158
+ }
159
+
160
+ /** True when the path is a directory holding at least one entry. */
161
+ async function dirHasFiles(target) {
162
+ try {
163
+ const entries = await fs.readdir(target);
164
+ return entries.length > 0;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Resolve the module a subcommand targets.
172
+ *
173
+ * `rawModule` is always the leaf name the developer typed. Subcommands accept the
174
+ * same `--path=<panel>` flag as `module:make`, so `panelme --path=admin` addresses
175
+ * src/modules/admin/panelme instead of failing with "module does not exist".
176
+ * Flags may arrive either in the positional slot or in extraFlags depending on
177
+ * whether a name was given, so every candidate string is scanned.
178
+ */
179
+ function resolveModuleRef(rawModule, ...flagGroups) {
180
+ const raw = assertName(rawModule, "Module name");
181
+ const panel = parsePanelPath(flagGroups.flat().filter((value) => typeof value === "string"));
182
+ const moduleName = panel ? `${panel}/${raw}` : raw;
183
+ // Derive the leaf from the resolved path so `admin/post` passed directly as the
184
+ // module argument still yields `post` for filenames and default names.
185
+ return { moduleName, leaf: leafName(moduleName) };
90
186
  }
91
187
 
92
188
  /** Ensure a module directory exists, throwing a helpful error if not. */
@@ -104,43 +200,87 @@ async function assertModuleExists(moduleName) {
104
200
  return root;
105
201
  }
106
202
 
107
- /** Generate controller and schema file content for a module. */
108
- async function controllerFiles(moduleName, controllerName = moduleName, options = {}) {
109
- const { includeModelImport = false } = options;
203
+ /** Build the placeholder substitution map shared by module file generators. */
204
+ function moduleFileVars(moduleName, controller) {
205
+ return {
206
+ module: moduleName,
207
+ controller,
208
+ ClassName: pascal(controller),
209
+ name: leafName(moduleName),
210
+ tableVariable: `${controller}s`
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Generate a controller file for a module.
216
+ * When a matching service exists the controller delegates to it; otherwise it
217
+ * falls back to the standalone shape so the file never imports a missing module.
218
+ */
219
+ async function controllerFile(moduleName, controllerName, openApi, withService) {
110
220
  const controller = controllerName.trim().toLowerCase();
111
- const openApi = openApiEnabled();
112
- const controllerStub = openApi
113
- ? includeModelImport
114
- ? STUBS.controller.openapiWithModel
115
- : STUBS.controller.openapi
116
- : STUBS.controller.plain;
117
- const schemaStub = openApi ? STUBS.controller.schema.openapi : STUBS.controller.schema.plain;
221
+ const controllerStub = withService
222
+ ? openApi
223
+ ? STUBS.controller.openapi
224
+ : STUBS.controller.plain
225
+ : STUBS.controller.standalone;
226
+ return await stub(controllerStub, moduleFileVars(moduleName, controller));
227
+ }
228
+
229
+ /** Generate a schema file for a module. */
230
+ async function schemaFile(moduleName, controllerName, openApi) {
231
+ const controller = controllerName.trim().toLowerCase();
232
+ const schemaStub = openApi ? STUBS.schema.openapi : STUBS.schema.plain;
233
+ return await stub(schemaStub, moduleFileVars(moduleName, controller));
234
+ }
235
+
236
+ /** Generate a service file for a module. withModel picks the Drizzle-backed variant. */
237
+ async function serviceFile(moduleName, controllerName, withModel) {
238
+ const controller = controllerName.trim().toLowerCase();
239
+ const serviceStub = withModel ? STUBS.service.named : STUBS.service.plain;
240
+ return await stub(serviceStub, moduleFileVars(moduleName, controller));
241
+ }
242
+
243
+ /** Build the full default scaffold for module:make. */
244
+ async function moduleFiles(moduleName, openApi) {
245
+ const name = leafName(moduleName);
246
+ 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;
118
249
  return {
119
- [`controllers/${controller}.schema.ts`]: await stub(schemaStub, {
120
- module: moduleName,
121
- controller,
122
- ClassName: pascal(controller),
123
- name: moduleName
124
- }),
125
- [`controllers/${controller}.controller.ts`]: await stub(controllerStub, {
126
- module: moduleName,
127
- controller,
128
- ClassName: pascal(controller),
129
- name: moduleName,
130
- tableVariable: `${controller}s`
131
- })
250
+ "facade.ts": await stub(STUBS.facade.named, vars),
251
+ [`controllers/${name}.ts`]: await stub(controllerStub, vars),
252
+ [`schemas/${name}.ts`]: await schemaFile(moduleName, name, openApi),
253
+ // module:make also writes database/models/<name>.ts, so the service can query it.
254
+ [`services/${name}.ts`]: await serviceFile(moduleName, name, true)
132
255
  };
133
256
  }
134
257
 
135
- /** Generate a route file for a module. */
258
+ /**
259
+ * Generate a wired route file. Only module:make uses this: it owns the whole
260
+ * module, so the controller and schema it just created are known to exist.
261
+ */
136
262
  async function routeTemplate(moduleName, controllerName = moduleName) {
137
- const controller = controllerName.trim().toLowerCase();
263
+ const controller = leafName(controllerName);
138
264
  const routeStub = openApiEnabled() ? STUBS.route.api : STUBS.route.plain;
139
265
  return await stub(routeStub, {
140
266
  module: moduleName,
141
267
  controller,
142
268
  ClassName: pascal(controller),
143
- ModuleClass: pascal(moduleName)
269
+ ModuleClass: moduleLabel(moduleName)
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Generate a barebone route file that wires to nothing. module:make-route uses
275
+ * this so the developer decides which controller and schema to attach.
276
+ */
277
+ async function standaloneRouteTemplate(moduleName, routeName) {
278
+ const routeStub = openApiEnabled() ? STUBS.route.standaloneApi : STUBS.route.standalonePlain;
279
+ return await stub(routeStub, {
280
+ module: moduleName,
281
+ controller: routeName,
282
+ ClassName: pascal(routeName),
283
+ ModuleClass: moduleLabel(moduleName)
144
284
  });
145
285
  }
146
286
 
@@ -168,14 +308,6 @@ async function namedSeederTemplate(moduleName, modelName, className) {
168
308
  });
169
309
  }
170
310
 
171
- /** Comment out every line of content (used for seeders generated without --force). */
172
- function asCommentedSeeder(content) {
173
- return content
174
- .split("\n")
175
- .map((line) => (line ? `// ${line}` : "//"))
176
- .join("\n");
177
- }
178
-
179
311
  /** Check if a file path exists on disk. */
180
312
  async function pathExists(filePath) {
181
313
  try {
@@ -186,40 +318,22 @@ async function pathExists(filePath) {
186
318
  }
187
319
  }
188
320
 
189
- /** Resolve the best controller name for a route. Prefers explicit name, falls back to most recently modified controller. */
190
- async function resolveRouteControllerName(moduleRootPath, moduleName, preferredName = "") {
191
- const preferred = preferredName ? preferredName.trim().toLowerCase() : "";
192
- if (preferred) {
193
- const preferredController = path.join(moduleRootPath, "controllers", `${preferred}.controller.ts`);
194
- const preferredSchema = path.join(moduleRootPath, "controllers", `${preferred}.schema.ts`);
195
- if ((await pathExists(preferredController)) && (await pathExists(preferredSchema))) return preferred;
321
+ /**
322
+ * Modules scaffolded before 4.1.0 kept schemas and services inside controllers/
323
+ * and used suffixed filenames. Returns the first legacy path that still exists
324
+ * so lookups resolve for existing projects.
325
+ */
326
+ function legacyPath(moduleRootPath, folder, baseName) {
327
+ if (folder === "schemas") {
328
+ return path.join(moduleRootPath, "controllers", `${baseName}.schema.ts`);
196
329
  }
197
-
198
- const controllerDir = path.join(moduleRootPath, "controllers");
199
- let entries = [];
200
- try {
201
- entries = await fs.readdir(controllerDir, { withFileTypes: true });
202
- } catch {
203
- return moduleName;
204
- }
205
-
206
- let latest = "";
207
- let latestMtime = -1;
208
- for (const entry of entries) {
209
- if (!entry.isFile() || !entry.name.endsWith(".controller.ts")) continue;
210
- const baseName = entry.name.replace(/\.controller\.ts$/, "");
211
- const schemaPath = path.join(controllerDir, `${baseName}.schema.ts`);
212
- if (!(await pathExists(schemaPath))) continue;
213
- const fullPath = path.join(controllerDir, entry.name);
214
- const stats = await fs.stat(fullPath);
215
- const mtime = stats.mtimeMs || 0;
216
- if (mtime > latestMtime) {
217
- latest = baseName;
218
- latestMtime = mtime;
219
- }
330
+ if (folder === "helpers") {
331
+ return path.join(moduleRootPath, "controllers", `${baseName}.helpers.ts`);
220
332
  }
221
-
222
- return latest || moduleName;
333
+ if (folder === "services") {
334
+ return path.join(moduleRootPath, "controllers", `${baseName}.service.ts`);
335
+ }
336
+ return path.join(moduleRootPath, "controllers", `${baseName}.controller.ts`);
223
337
  }
224
338
 
225
339
  /** Write a file with safety checks (dry-run support, force overwrite protection). */
@@ -235,53 +349,97 @@ async function writeFileSafe(filePath, content, flags = [], label = "File") {
235
349
  await fs.writeFile(filePath, content);
236
350
  }
237
351
 
238
- /** Generate a complete module scaffold (controller, route, models, seeders). */
239
- export async function makeModule(rawName) {
240
- const name = assertName(rawName, "Module name");
241
- const root = path.resolve(process.cwd(), "src/modules", name);
242
- const controller = await controllerFiles(name, name, { includeModelImport: true });
243
- const route = await routeTemplate(name);
244
- const dialect = detectDialect();
245
- const model = await namedModelTemplate(name, name, dialect);
246
- const seeder = await namedSeederTemplate(name, name, name);
247
- await writeFiles(root, { ...controller, "routes/api.ts": route });
248
- await fs.mkdir(path.join(root, "database", "models"), { recursive: true });
249
- await fs.mkdir(path.join(root, "database", "seeders"), { recursive: true });
250
- await fs.writeFile(path.join(root, "database", "models", `${name}.ts`), model);
251
- await fs.writeFile(path.join(root, "database", "seeders", `${name}.ts`), asCommentedSeeder(seeder));
252
- console.log(`Module ready: ${name}`);
253
- console.log("Database files: models/{module}.ts, seeders/{module}.ts (commented)");
352
+ /**
353
+ * Generate a complete module scaffold (facade, schema, service, controller, route, model, seeder).
354
+ * Pass `--path=<panel>` to nest the module under a panel folder, e.g.
355
+ * `module:make post --path=admin` writes to src/modules/admin/post/.
356
+ */
357
+ export async function makeModule(rawName, flags = []) {
358
+ const result = await createModule(rawName, flags);
359
+ reportModule(result);
360
+ return result;
361
+ }
362
+
363
+ /**
364
+ * Create several modules in one run: `module:make post tag testme`.
365
+ *
366
+ * A name that cannot be created is reported and the batch continues, so one bad
367
+ * argument never costs you the modules that were fine. The caller decides the
368
+ * exit code from `failed`.
369
+ */
370
+ export async function makeModules(rawNames, flags = []) {
371
+ const created = [];
372
+ const failed = [];
373
+ for (const rawName of rawNames) {
374
+ try {
375
+ created.push(await createModule(rawName, flags));
376
+ } catch (error) {
377
+ failed.push({ name: rawName, reason: error.message });
378
+ }
379
+ }
380
+
381
+ if (created.length) {
382
+ console.log(`Module${created.length > 1 ? "s" : ""} ready: ${created.map((item) => item.moduleName).join(", ")}`);
383
+ const panel = created[0].panel;
384
+ if (panel) {
385
+ console.log(` panel ${panel} -> ${created.map((item) => `/api/${item.moduleName}`).join(", ")}`);
386
+ }
387
+ }
388
+ for (const item of failed) {
389
+ console.log(` skipped ${item.name}: ${item.reason}`);
390
+ }
391
+
392
+ return { created, failed };
393
+ }
394
+
395
+ /** Print the per-module summary for a single-module run. */
396
+ function reportModule({ moduleName, panel, leaf }) {
397
+ console.log(`Module ready: ${moduleName}`);
398
+ if (panel) console.log(`Panel: ${panel} -> /api/${moduleName}`);
399
+ console.log(`Database files: models/${leaf}.ts, seeders/${leaf}.ts`);
254
400
  console.log(`Route style: ${openApiEnabled() ? "openapi" : "plain"}`);
401
+ if (panel) {
402
+ console.log(`Fix unresolved imports with: bun maker alias`);
403
+ }
255
404
  }
256
405
 
257
- /** Generate a one-shot example module with queue/broadcast/scheduler cases. */
258
- export async function makeExampleModule(rawName = "example") {
259
- const moduleName = assertName(rawName || "example", "Module name");
260
- const root = path.resolve(process.cwd(), "src/modules", moduleName);
406
+ /**
407
+ * Scaffold one module.
408
+ *
409
+ * Refuses to touch a module folder that already has content. Without this guard
410
+ * writeFiles overwrites unconditionally, so re-running module:make on an
411
+ * existing module silently destroys the developer's edits to the controller,
412
+ * service, schema and route.
413
+ */
414
+ async function createModule(rawName, flags = []) {
415
+ const leaf = assertName(rawName, "Module name");
416
+ const panel = parsePanelPath(flags);
417
+ const moduleName = panel ? `${panel}/${leaf}` : leaf;
418
+ const root = moduleRoot(moduleName);
419
+ const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
420
+ if (!force && (await dirHasFiles(root))) {
421
+ throw new Error(`Module already exists: src/modules/${moduleName}. Re-run with --force to overwrite its files.`);
422
+ }
261
423
  const openApi = openApiEnabled();
262
- await writeFiles(root, {
263
- [`controllers/${moduleName}.schema.ts`]: await stub(openApi ? STUBS.example.schema.openapi : STUBS.example.schema.plain, {
264
- module: moduleName
265
- }),
266
- [`controllers/${moduleName}.controller.ts`]: await stub(STUBS.example.controller, {
267
- module: moduleName
268
- }),
269
- "routes/api.ts": await stub(openApi ? STUBS.example.routeApi : STUBS.example.routePlain, { module: moduleName }),
270
- [`jobs/${moduleName}.ts`]: await stub(STUBS.example.job, { module: moduleName }),
271
- [`console/${moduleName}.ts`]: await stub(STUBS.example.console, { module: moduleName })
272
- });
424
+ const files = await moduleFiles(moduleName, openApi);
425
+ const route = await routeTemplate(moduleName);
426
+ const dialect = detectDialect();
427
+ const model = await namedModelTemplate(moduleName, leaf, dialect);
428
+ const seeder = await namedSeederTemplate(moduleName, leaf, leaf);
429
+ await writeFiles(root, { ...files, "routes/index.ts": route });
273
430
  await fs.mkdir(path.join(root, "database", "models"), { recursive: true });
274
431
  await fs.mkdir(path.join(root, "database", "seeders"), { recursive: true });
275
- console.log(`Example module ready: ${moduleName}`);
276
- console.log(`Route style: ${openApi ? "openapi" : "plain"}`);
432
+ await fs.writeFile(path.join(root, "database", "models", `${leaf}.ts`), model);
433
+ await fs.writeFile(path.join(root, "database", "seeders", `${leaf}.ts`), seeder);
434
+ return { moduleName, panel, leaf };
277
435
  }
278
436
 
279
437
  /** Generate a route file for an existing module. */
280
438
  export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = []) {
281
- const moduleName = assertName(rawModule, "Module name");
439
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawControllerOrFlag, extraFlags);
282
440
  const root = await assertModuleExists(moduleName);
283
441
  const flags = [];
284
- let routeName = moduleName;
442
+ let routeName = leaf;
285
443
  let controllerExplicit = false;
286
444
  if (rawControllerOrFlag) {
287
445
  if (rawControllerOrFlag.startsWith("--")) flags.push(rawControllerOrFlag);
@@ -291,9 +449,8 @@ export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = [])
291
449
  }
292
450
  }
293
451
  flags.push(...extraFlags);
294
- const controllerName = await resolveRouteControllerName(root, moduleName, routeName);
295
- const route = await routeTemplate(moduleName, controllerName);
296
- const routeFile = controllerExplicit ? `${routeName}.ts` : "api.ts";
452
+ const route = await standaloneRouteTemplate(moduleName, routeName);
453
+ const routeFile = controllerExplicit ? `${routeName}.ts` : "index.ts";
297
454
  const routePath = path.join(root, `routes/${routeFile}`);
298
455
  const dryRun = hasFlag(flags, "--dry-run");
299
456
  const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
@@ -302,7 +459,7 @@ export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = [])
302
459
  try {
303
460
  await fs.access(routePath);
304
461
  exists = true;
305
- } catch {}
462
+ } catch { }
306
463
  if (exists && !force)
307
464
  throw new Error(`Route file already exists: ${path.relative(process.cwd(), routePath)}. Re-run with --force to overwrite.`);
308
465
  await fs.mkdir(path.dirname(routePath), { recursive: true });
@@ -311,31 +468,36 @@ export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = [])
311
468
  }
312
469
 
313
470
  /** Generate a notification module with controller, routes, and job. */
314
- export async function makeNotificationModule(rawName = "notification") {
315
- const moduleName = assertName(rawName, "Module name");
316
- const root = path.resolve(process.cwd(), "src/modules", moduleName);
471
+ export async function makeNotificationModule(rawName = "notification", flags = []) {
472
+ const { moduleName, leaf } = resolveModuleRef(rawName, flags);
473
+ const root = moduleRoot(moduleName);
474
+ const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
475
+ if (!force && (await dirHasFiles(root))) {
476
+ throw new Error(`Module already exists: src/modules/${moduleName}. Re-run with --force to overwrite its files.`);
477
+ }
317
478
  const openApi = openApiEnabled();
318
479
 
319
480
  await writeFiles(root, {
320
- [`controllers/${moduleName}.controller.ts`]: await stub(STUBS.notification.controller, {
481
+ [`controllers/${leaf}.ts`]: await stub(STUBS.notification.controller, {
321
482
  module: moduleName
322
483
  }),
323
- [`controllers/${moduleName}.schema.ts`]: await stub(openApi ? STUBS.notification.schema.openapi : STUBS.notification.schema.plain, {
484
+ [`schemas/${leaf}.ts`]: await stub(openApi ? STUBS.notification.schema.openapi : STUBS.notification.schema.plain, {
324
485
  module: moduleName
325
486
  }),
326
- "routes/api.ts": await stub(openApi ? STUBS.notification.routeApi : STUBS.notification.routePlain, { module: moduleName }),
327
- [`jobs/${moduleName}.ts`]: await stub(STUBS.notification.job, { module: moduleName })
487
+ "routes/index.ts": await stub(openApi ? STUBS.notification.routeApi : STUBS.notification.routePlain, { module: moduleName }),
488
+ [`jobs/${leaf}.ts`]: await stub(STUBS.notification.job, { module: moduleName })
328
489
  });
329
490
 
330
491
  console.log(`Notification module ready: ${moduleName}`);
492
+ if (panelOf(moduleName)) console.log(`Panel: ${panelOf(moduleName)} -> /api/${moduleName}`);
331
493
  console.log(`Route style: ${openApi ? "openapi" : "plain"}`);
332
494
  console.log("See docs (packages/docs/guide/notification.md) for Vue UI integration.");
333
495
  }
334
496
 
335
497
  /** Soft-delete a module by moving it to storage trash. */
336
498
  export async function deleteModule(rawName, flags = []) {
337
- const name = assertName(rawName, "Module name");
338
- if (name === "notification") {
499
+ const { moduleName: name, leaf } = resolveModuleRef(rawName, flags);
500
+ if (leaf === "notification") {
339
501
  console.log("Use `bun maker module:delete-notification` to remove the notification module (removes UI files too).");
340
502
  return;
341
503
  }
@@ -352,7 +514,8 @@ export async function deleteModule(rawName, flags = []) {
352
514
  if (!stats.isDirectory()) throw new Error(`Module path is not a directory: src/modules/${name}`);
353
515
  const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
354
516
  const stamp = new Date().toISOString().replace(/[.:]/g, "-");
355
- const trashPath = path.join(trashRoot, `${name}-${stamp}`);
517
+ // Trash keeps the panel path so admin/post stays distinguishable from admin/user.
518
+ const trashPath = path.join(trashRoot, `${name.replaceAll("/", "-")}-${stamp}`);
356
519
  const dryRun = hasFlag(flags, "--dry-run");
357
520
  const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
358
521
  if (dryRun) return;
@@ -373,11 +536,11 @@ export async function deleteModule(rawName, flags = []) {
373
536
 
374
537
  /** Remove a notification module (backend only — moves to trash). */
375
538
  export async function deleteNotificationModule(rawName = "notification", flags = []) {
376
- const moduleName = assertName(rawName, "Module name");
539
+ const { moduleName, leaf } = resolveModuleRef(rawName, flags);
377
540
  const dryRun = hasFlag(flags, "--dry-run");
378
541
  const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
379
542
 
380
- const modulePath = path.resolve(process.cwd(), "src/modules", moduleName);
543
+ const modulePath = moduleRoot(moduleName);
381
544
 
382
545
  if (dryRun) {
383
546
  const exists = await fs
@@ -390,7 +553,7 @@ export async function deleteNotificationModule(rawName = "notification", flags =
390
553
 
391
554
  if (!confirmed) {
392
555
  throw new Error(
393
- `Refusing to delete notification module without confirmation. Re-run with: bun maker module:delete-notification ${moduleName} --yes`
556
+ `Refusing to delete notification module without confirmation. Re-run with: bun maker module:delete-notification ${leaf} --yes${panelOf(moduleName) ? ` --path=${panelOf(moduleName)}` : ""}`
394
557
  );
395
558
  }
396
559
 
@@ -453,28 +616,93 @@ export async function cleanModuleTrash(rawName, flags = []) {
453
616
  for (const name of matches) await fs.rm(path.join(trashRoot, name), { recursive: true, force: true });
454
617
  }
455
618
 
456
- /** Generate a controller for an existing module. */
457
- export async function makeController(rawModule, rawControllerOrFlag, extraFlags = []) {
458
- const moduleName = assertName(rawModule, "Module name");
619
+ /**
620
+ * Split "<module> <nameOrFlag> [extraFlags]" into a validated name plus flags.
621
+ * Shared by the per-file commands so they all accept --force/--dry-run the same way.
622
+ */
623
+ function parseNameArg(rawNameOrFlag, extraFlags, kindLabel, fallback) {
459
624
  const flags = [];
460
- let controllerName = moduleName;
461
- if (rawControllerOrFlag) {
462
- if (rawControllerOrFlag.startsWith("--")) flags.push(rawControllerOrFlag);
463
- else controllerName = assertName(rawControllerOrFlag, "Controller name");
625
+ let name = fallback;
626
+ if (rawNameOrFlag) {
627
+ if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
628
+ else name = assertName(rawNameOrFlag, kindLabel);
464
629
  }
465
630
  flags.push(...extraFlags);
631
+ return { name, flags };
632
+ }
633
+
634
+ /** Generate a controller for an existing module. */
635
+ export async function makeController(rawModule, rawControllerOrFlag, extraFlags = []) {
636
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawControllerOrFlag, extraFlags);
637
+ const { name, flags } = parseNameArg(rawControllerOrFlag, extraFlags, "Controller name", leaf);
466
638
  const root = await assertModuleExists(moduleName);
467
- const files = await controllerFiles(moduleName, controllerName);
468
- for (const [relativePath, content] of Object.entries(files)) {
469
- await writeFileSafe(path.join(root, relativePath), content, flags, "Controller file");
470
- }
639
+ const lower = name.toLowerCase();
640
+ const hasService = (await pathExists(path.join(root, "services", `${lower}.ts`))) || (await pathExists(legacyPath(root, "services", lower)));
641
+ const content = await controllerFile(moduleName, name, openApiEnabled(), hasService);
642
+ await writeFileSafe(path.join(root, `controllers/${lower}.ts`), content, flags, "Controller file");
643
+ }
644
+
645
+ /** Generate a schema file for an existing module. */
646
+ export async function makeSchema(rawModule, rawNameOrFlag, extraFlags = []) {
647
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
648
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Schema name", leaf);
649
+ const root = await assertModuleExists(moduleName);
650
+ const content = await schemaFile(moduleName, name, openApiEnabled());
651
+ await writeFileSafe(path.join(root, `schemas/${name.toLowerCase()}.ts`), content, flags, "Schema file");
652
+ }
653
+
654
+ /**
655
+ * Generate a service file for an existing module.
656
+ * Pass --with-model when the module has database/models/<name>.ts so the
657
+ * service queries the table instead of returning the empty defaults.
658
+ */
659
+ export async function makeService(rawModule, rawNameOrFlag, extraFlags = []) {
660
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
661
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Service name", leaf);
662
+ const root = await assertModuleExists(moduleName);
663
+ const lower = name.toLowerCase();
664
+ const withModel = hasFlag(flags, "--with-model") || (await pathExists(path.join(root, "database", "models", `${lower}.ts`)));
665
+ const content = await serviceFile(moduleName, name, withModel);
666
+ await writeFileSafe(path.join(root, `services/${lower}.ts`), content, flags, "Service file");
667
+ }
668
+
669
+ /** Generate a helper file for an existing module. */
670
+ export async function makeHelper(rawModule, rawNameOrFlag, extraFlags = []) {
671
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
672
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Helper name", leaf);
673
+ const root = await assertModuleExists(moduleName);
674
+ const content = await stub(STUBS.helper.named, { module: moduleName, name, Name: pascal(name) });
675
+ await writeFileSafe(path.join(root, `helpers/${name}.ts`), content, flags, "Helper file");
676
+ }
677
+
678
+ /** Generate a module-local middleware file for an existing module. */
679
+ export async function makeLocalMiddleware(rawModule, rawNameOrFlag, extraFlags = []) {
680
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
681
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Middleware name", leaf);
682
+ const root = await assertModuleExists(moduleName);
683
+ const content = await stub(STUBS.middleware.local, {
684
+ module: moduleName,
685
+ name,
686
+ camel: camelCase(name),
687
+ Name: pascal(name)
688
+ });
689
+ await writeFileSafe(path.join(root, `middlewares/${name}.ts`), content, flags, "Middleware file");
690
+ }
691
+
692
+ /** Generate a types file for an existing module. */
693
+ export async function makeType(rawModule, rawNameOrFlag, extraFlags = []) {
694
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
695
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Type name", leaf);
696
+ const root = await assertModuleExists(moduleName);
697
+ const content = await stub(STUBS.type.named, { module: moduleName, name, Name: pascal(name) });
698
+ await writeFileSafe(path.join(root, `types/${name}.ts`), content, flags, "Type file");
471
699
  }
472
700
 
473
701
  /** Generate a model file for an existing module. */
474
702
  export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
475
- const moduleName = assertName(rawModule, "Module name");
703
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
476
704
  const flags = [];
477
- let name = moduleName;
705
+ let name = leaf;
478
706
  if (rawNameOrFlag) {
479
707
  if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
480
708
  else name = assertName(rawNameOrFlag, "Model name");
@@ -488,9 +716,9 @@ export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
488
716
 
489
717
  /** Generate a seeder file for an existing module model. */
490
718
  export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
491
- const moduleName = assertName(rawModule, "Module name");
719
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
492
720
  const flags = [];
493
- let name = moduleName;
721
+ let name = leaf;
494
722
  if (rawNameOrFlag) {
495
723
  if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
496
724
  else name = assertName(rawNameOrFlag, "Seeder name");
@@ -503,34 +731,35 @@ export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
503
731
  try {
504
732
  await fs.access(modelPath);
505
733
  } catch {
506
- modelName = moduleName;
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;
507
738
  modelPath = path.join(root, "database", "models", `${modelName}.ts`);
508
739
 
509
740
  try {
510
741
  await fs.access(modelPath);
511
742
  console.log(`Model '${name}' not found, using module model '${modelName}' for seeder '${name}'.`);
512
743
  } catch {
744
+ const panel = panelOf(moduleName);
745
+ const suggest = panel ? `${leaf} ${name} --path=${panel}` : `${leaf} ${name}`;
513
746
  throw new Error(
514
- `Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${moduleName} ${name}`
747
+ `Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${suggest}`
515
748
  );
516
749
  }
517
750
  }
518
751
 
519
- await writeFileSafe(
520
- path.join(root, `database/seeders/${name}.ts`),
521
- await namedSeederTemplate(moduleName, modelName, name),
522
- flags,
523
- "Seeder file"
524
- );
752
+ const seeder = await namedSeederTemplate(moduleName, modelName, name);
753
+ await writeFileSafe(path.join(root, `database/seeders/${name}.ts`), seeder, flags, "Seeder file");
525
754
 
526
755
  console.log(`Seeder ready: ${moduleName}/${name}`);
527
756
  }
528
757
 
529
758
  /** Generate a job file for an existing module. */
530
759
  export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
531
- const moduleName = assertName(rawModule, "Module name");
760
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
532
761
  const flags = [];
533
- let name = moduleName;
762
+ let name = leaf;
534
763
  if (rawNameOrFlag) {
535
764
  if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
536
765
  else name = assertName(rawNameOrFlag, "Job name");
@@ -542,9 +771,9 @@ export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
542
771
 
543
772
  /** Generate a schedule/console file for an existing module. */
544
773
  export async function makeSchedule(rawModule, rawNameOrFlag, extraFlags = []) {
545
- const moduleName = assertName(rawModule, "Module name");
774
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
546
775
  const flags = [];
547
- let name = moduleName;
776
+ let name = leaf;
548
777
  if (rawNameOrFlag) {
549
778
  if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
550
779
  else name = assertName(rawNameOrFlag, "Schedule name");
@@ -580,7 +809,8 @@ export async function listModules() {
580
809
  }
581
810
 
582
811
  /** Check that a module has at least one seeder file. */
583
- export async function assertModuleHasSeeders(moduleName) {
812
+ export async function assertModuleHasSeeders(rawModule, flags = []) {
813
+ const { moduleName } = resolveModuleRef(rawModule, flags);
584
814
  await assertModuleExists(moduleName);
585
815
  const files = await glob(`${moduleName}/database/seeders/*.{ts,js}`, {
586
816
  cwd: path.resolve(process.cwd(), "src/modules"),
@@ -628,7 +858,10 @@ async function generateModuleSchemaTemp(moduleName) {
628
858
  const exports = [];
629
859
  const tempDir = path.resolve(process.cwd(), "src/storage/tmp");
630
860
  await fs.mkdir(tempDir, { recursive: true });
631
- const tempSchemaPath = path.join(tempDir, `schema.${moduleName}.${Date.now()}.ts`);
861
+ // Flatten the panel path: "admin/testme" must not become a nested
862
+ // storage/tmp/schema.admin/testme.<ts>.ts path.
863
+ const slug = moduleName.replaceAll("/", "-");
864
+ const tempSchemaPath = path.join(tempDir, `schema.${slug}.${Date.now()}.ts`);
632
865
  const tempSchemaDir = path.dirname(tempSchemaPath);
633
866
  for (const file of files.sort()) {
634
867
  const absoluteFile = path.join(backendSrc, file);
@@ -645,7 +878,7 @@ async function generateModuleSchemaTemp(moduleName) {
645
878
 
646
879
  /** Generate and run migrations for a single module using a temporary schema. */
647
880
  export async function runModuleMigrate(rawModuleName, rawArgs = []) {
648
- const moduleName = assertName(rawModuleName, "Module name");
881
+ const { moduleName } = resolveModuleRef(rawModuleName, rawArgs);
649
882
  await syncMigrationDialect();
650
883
  await ensureMigrationMeta();
651
884
  await ensureDatabaseDirectory();
@@ -661,15 +894,15 @@ export async function runModuleMigrate(rawModuleName, rawArgs = []) {
661
894
  } finally {
662
895
  if (previousSchema == null) delete process.env.DRIZZLE_SCHEMA;
663
896
  else process.env.DRIZZLE_SCHEMA = previousSchema;
664
- if (!keepTemp) await fs.rm(tempSchemaPath, { force: true }).catch(() => {});
897
+ if (!keepTemp) await fs.rm(tempSchemaPath, { force: true }).catch(() => { });
665
898
  }
666
899
  }
667
900
 
668
901
  /** Generate a unit test file for a module. */
669
902
  export async function makeTest(rawModule, rawNameOrFlag, extraFlags = []) {
670
- const moduleName = assertName(rawModule, "Module name");
903
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
671
904
  const flags = [];
672
- let name = moduleName;
905
+ let name = leaf;
673
906
  if (rawNameOrFlag) {
674
907
  if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
675
908
  else name = assertName(rawNameOrFlag, "Test name");