create-fullstack-scaffold 0.1.1 → 0.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 (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
package/dist/cli/index.js CHANGED
@@ -1,433 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { program } from 'commander';
3
- import { hc } from 'hono/client';
4
- import { z } from '@hono/zod-openapi';
5
- import fs, { readdirSync, statSync, existsSync, readFileSync } from 'fs';
6
- import path2, { join } from 'path';
7
- import os from 'os';
2
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
3
+ import path, { join } from 'path';
8
4
  import { fileURLToPath, pathToFileURL } from 'url';
9
- import fs2 from 'fs-extra';
5
+ import { Command } from 'commander';
10
6
  import chalk from 'chalk';
7
+ import { select } from '@inquirer/prompts';
8
+ import fs from 'fs-extra';
11
9
  import ora from 'ora';
12
10
 
13
- function createRPCClient(baseUrl) {
14
- return hc(baseUrl);
15
- }
16
-
17
- // src/cli/utils/api.ts
18
- var DEFAULT_BASE_URL = "http://localhost:3010";
19
- var globalBaseUrl = process.env.BIOMIMIC_API_URL || DEFAULT_BASE_URL;
20
- function setBaseUrl(url) {
21
- globalBaseUrl = url;
22
- }
23
- function getBaseUrl() {
24
- return globalBaseUrl;
25
- }
26
- function getClient() {
27
- return createRPCClient(globalBaseUrl);
28
- }
29
-
30
- // src/cli/utils/logger.ts
31
- var Logger = class {
32
- level = "info";
33
- constructor(options = {}) {
34
- if (options.debug || process.env.BIOMIMIC_DEBUG === "true") {
35
- this.level = "debug";
36
- } else if (options.verbose || process.env.BIOMIMIC_VERBOSE === "true") {
37
- this.level = "info";
38
- }
39
- }
40
- setLevel(level) {
41
- this.level = level;
42
- }
43
- shouldLog(level) {
44
- const levels = ["silent", "error", "warn", "info", "debug"];
45
- return levels.indexOf(level) <= levels.indexOf(this.level);
46
- }
47
- info(message, ...args) {
48
- if (this.shouldLog("info")) {
49
- process.stdout.write(`${message}${args.length ? " " + args.join(" ") : ""}
50
- `);
51
- }
52
- }
53
- debug(message, ...args) {
54
- if (this.shouldLog("debug")) {
55
- process.stdout.write(`[debug] ${message}${args.length ? " " + args.join(" ") : ""}
56
- `);
57
- }
58
- }
59
- warn(message) {
60
- if (this.shouldLog("warn")) {
61
- process.stderr.write(`[warn] ${message}
62
- `);
63
- }
64
- }
65
- error(message) {
66
- if (this.shouldLog("error")) {
67
- process.stderr.write(`[error] ${message}
68
- `);
69
- }
70
- }
71
- success(message, ...args) {
72
- if (this.shouldLog("info")) {
73
- process.stdout.write(`\u2713 ${message}${args.length ? " " + args.join(" ") : ""}
74
- `);
75
- }
76
- }
77
- fail(message) {
78
- if (this.shouldLog("error")) {
79
- process.stderr.write(`\u2717 ${message}
80
- `);
81
- }
82
- }
83
- };
84
- var logger = null;
85
- function createLogger(options = {}) {
86
- logger = new Logger(options);
87
- return logger;
88
- }
89
- function getLogger() {
90
- if (!logger) {
91
- logger = new Logger();
92
- }
93
- return logger;
94
- }
95
-
96
- // src/cli/utils/auto-command.ts
97
- function extractZodInfo(schema) {
98
- const def = schema._def;
99
- if (def.typeName === "ZodOptional") {
100
- const inner = schema._def.innerType;
101
- const info = extractZodInfo(inner);
102
- return { ...info, required: false };
103
- }
104
- if (def.typeName === "ZodDefault") {
105
- const innerDef = schema._def;
106
- const defaultValue = innerDef.defaultValue();
107
- const info = extractZodInfo(innerDef.innerType);
108
- return { ...info, required: false, defaultValue };
109
- }
110
- if (def.typeName === "ZodEnum") {
111
- const enumValues = [...schema._def.values];
112
- return { type: "enum", required: true, enumValues };
113
- }
114
- if (def.typeName === "ZodString") return { type: "string", required: true };
115
- if (def.typeName === "ZodNumber") return { type: "number", required: true };
116
- if (def.typeName === "ZodBoolean") return { type: "boolean", required: true };
117
- if (def.typeName === "ZodObject") return { type: "object", required: true };
118
- return { type: "string", required: true };
119
- }
120
- function schemaToOptions(schema) {
121
- const options = [];
122
- const shape = schema.shape;
123
- for (const [key, zodType] of Object.entries(shape)) {
124
- const info = extractZodInfo(zodType);
125
- const longFlag = key.replace(/([A-Z])/g, "-$1").toLowerCase();
126
- let flags = `--${longFlag} <value>`;
127
- if (info.type === "boolean") {
128
- flags = `--${longFlag}`;
129
- }
130
- let description = String(info.type);
131
- if (info.enumValues) {
132
- description = `${info.type} (${info.enumValues.join("|")})`;
133
- }
134
- options.push({
135
- flags,
136
- description,
137
- required: info.required,
138
- defaultValue: info.defaultValue
139
- });
140
- }
141
- return options;
142
- }
143
- function pathToApiCall(client, _method, path3) {
144
- const pathParts = path3.split("/").filter(Boolean);
145
- let current = client.api;
146
- for (const part of pathParts) {
147
- if (part.startsWith("{") && part.endsWith("}")) {
148
- continue;
149
- }
150
- current = current[part] || current[`:${part}`];
151
- }
152
- return current;
153
- }
154
- function createCommandFromRoute(config) {
155
- const options = [];
156
- const arguments_ = [];
157
- if (config.params) {
158
- const shape = config.params.shape;
159
- for (const [key] of Object.entries(shape)) {
160
- arguments_.push({
161
- name: key,
162
- description: `${key} parameter`,
163
- required: true
164
- });
165
- }
166
- }
167
- if (config.body) {
168
- const bodySchema = config.body;
169
- if (bodySchema.shape) {
170
- options.push(...schemaToOptions(bodySchema) || []);
171
- }
172
- }
173
- if (config.query) {
174
- options.push(...schemaToOptions(config.query) || []);
175
- }
176
- return {
177
- name: config.command,
178
- description: config.description,
179
- options,
180
- arguments: arguments_,
181
- action: async (opts, args) => {
182
- const logger2 = getLogger();
183
- const client = getClient();
184
- const param = {};
185
- if (config.params) {
186
- const shape = config.params.shape;
187
- const keys = Object.keys(shape);
188
- keys.forEach((key, i) => {
189
- param[key] = args[i] || String(opts[key]);
190
- });
191
- }
192
- const json = {};
193
- if (config.body && opts) {
194
- const bodySchema = config.body;
195
- if (bodySchema.shape) {
196
- const shape = bodySchema.shape;
197
- for (const key of Object.keys(shape)) {
198
- if (opts[key] !== void 0) {
199
- json[key] = opts[key];
200
- }
201
- }
202
- }
203
- }
204
- const query = {};
205
- if (config.query && opts) {
206
- const shape = config.query.shape;
207
- for (const key of Object.keys(shape)) {
208
- if (opts[key] !== void 0) {
209
- query[key] = String(opts[key]);
210
- }
211
- }
212
- }
213
- const apiCall = pathToApiCall(client, config.method, config.path);
214
- const methodCall = apiCall[`$${config.method.charAt(0).toUpperCase() + config.method.slice(1)}`] || apiCall.$get;
215
- const res = await methodCall.call(apiCall, {
216
- param: Object.keys(param).length > 0 ? param : void 0,
217
- query: Object.keys(query).length > 0 ? query : void 0,
218
- json: Object.keys(json).length > 0 ? json : void 0
219
- });
220
- const data = await res.json();
221
- logger2.info(JSON.stringify(data, null, 2));
222
- }
223
- };
224
- }
225
- function registerAutoCommand(program2, config) {
226
- const cmd = createCommandFromRoute(config);
227
- const command = program2.command(cmd.name).description(cmd.description).action(async (firstArg, opts) => {
228
- const args = typeof firstArg === "string" ? [firstArg] : [];
229
- const options = (typeof firstArg === "object" ? firstArg : opts) || {};
230
- await cmd.action(options, args);
231
- });
232
- cmd.options?.forEach((opt) => {
233
- if (opt.required) {
234
- command.requiredOption(opt.flags, opt.description, opt.defaultValue);
235
- } else {
236
- command.option(opt.flags, opt.description, opt.defaultValue);
237
- }
238
- });
239
- cmd.arguments?.forEach((arg) => {
240
- command.argument(arg.required ? `<${arg.name}>` : `[${arg.name}]`, arg.description);
241
- });
242
- }
243
- var TodoStatusSchema = z.enum(["pending", "in_progress", "completed"]);
244
- var todoRoutes = [
245
- {
246
- method: "get",
247
- path: "/todos",
248
- command: "list",
249
- description: "List all todos"
250
- },
251
- {
252
- method: "get",
253
- path: "/todos/{id}",
254
- command: "get",
255
- description: "Get a todo by ID",
256
- params: z.object({ id: z.string() })
257
- },
258
- {
259
- method: "post",
260
- path: "/todos",
261
- command: "create",
262
- description: "Create a new todo",
263
- body: z.object({
264
- title: z.string().min(1),
265
- description: z.string().optional()
266
- })
267
- },
268
- {
269
- method: "put",
270
- path: "/todos/{id}",
271
- command: "update",
272
- description: "Update a todo",
273
- params: z.object({ id: z.string() }),
274
- body: z.object({
275
- title: z.string().optional(),
276
- description: z.string().optional(),
277
- status: TodoStatusSchema.optional()
278
- })
279
- },
280
- {
281
- method: "delete",
282
- path: "/todos/{id}",
283
- command: "delete",
284
- description: "Delete a todo",
285
- params: z.object({ id: z.string() })
286
- }
287
- ];
288
- function registerTodoCommands(program2) {
289
- const todo = program2.command("todo").description("Todo management commands");
290
- for (const route of todoRoutes) {
291
- registerAutoCommand(todo, route);
292
- }
293
- }
294
-
295
- // src/cli/modules/notification/index.ts
296
- function registerNotificationCommands(program2) {
297
- const notification = program2.command("notification").description("Notification management commands");
298
- notification.command("list").description("List all notifications").option("--unread-only", "Show only unread notifications").option("--limit <number>", "Limit number of results", "20").action(async (options) => {
299
- const logger2 = getLogger();
300
- const client = getClient();
301
- const unreadOnly = Boolean(options.unreadOnly);
302
- const limit = parseInt(options.limit || "20");
303
- const res = await client.api.notifications.$get({
304
- query: { unreadOnly: String(unreadOnly), limit: String(limit) }
305
- });
306
- const data = await res.json();
307
- logger2.info(JSON.stringify(data, null, 2));
308
- });
309
- notification.command("create").description("Create a new notification").requiredOption("-t, --title <title>", "Notification title").requiredOption("-m, --message <message>", "Notification message").option("--type <type>", "Notification type (info|warning|success|error)", "info").action(async (options) => {
310
- const logger2 = getLogger();
311
- const client = getClient();
312
- const res = await client.api.notifications.$post({
313
- json: {
314
- type: options.type,
315
- title: options.title,
316
- message: options.message
317
- }
318
- });
319
- const data = await res.json();
320
- logger2.success("Notification created");
321
- logger2.info(JSON.stringify(data, null, 2));
322
- });
323
- notification.command("unread-count").description("Get unread notification count").action(async () => {
324
- const logger2 = getLogger();
325
- const client = getClient();
326
- const res = await client.api.notifications["unread-count"].$get();
327
- const data = await res.json();
328
- logger2.info(JSON.stringify(data, null, 2));
329
- });
330
- notification.command("mark-read").description("Mark a notification as read").argument("<id>", "Notification ID").action(async (id) => {
331
- const logger2 = getLogger();
332
- const client = getClient();
333
- const res = await client.api.notifications[":id"].read.$patch({ param: { id } });
334
- const data = await res.json();
335
- logger2.success("Notification marked as read");
336
- logger2.info(JSON.stringify(data, null, 2));
337
- });
338
- notification.command("delete").description("Delete a notification").argument("<id>", "Notification ID").action(async (id) => {
339
- const logger2 = getLogger();
340
- const client = getClient();
341
- const res = await client.api.notifications[":id"].$delete({ param: { id } });
342
- const data = await res.json();
343
- logger2.success("Notification deleted");
344
- logger2.info(JSON.stringify(data, null, 2));
345
- });
346
- }
347
- var CONFIG_DIR = path2.join(os.homedir(), ".biomimic");
348
- var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
349
- function loadConfig() {
350
- try {
351
- if (fs.existsSync(CONFIG_FILE)) {
352
- const content = fs.readFileSync(CONFIG_FILE, "utf-8");
353
- return JSON.parse(content);
354
- }
355
- } catch {
356
- }
357
- return { baseUrl: "http://localhost:3010" };
358
- }
359
- function saveConfig(config) {
360
- if (!fs.existsSync(CONFIG_DIR)) {
361
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
362
- }
363
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
364
- }
365
- function registerConfigCommands(program2) {
366
- const config = program2.command("config").description("CLI configuration and service management");
367
- config.command("get").description("Show current configuration").option("-k, --key <key>", "Get specific config key").action((options) => {
368
- const logger2 = getLogger();
369
- const cfg = loadConfig();
370
- if (options.key) {
371
- const value = cfg[options.key];
372
- logger2.info(`${options.key}: ${value ?? "not set"}`);
373
- } else {
374
- logger2.info(JSON.stringify(cfg, null, 2));
375
- }
376
- });
377
- config.command("set").description("Set configuration value").option("-u, --url <url>", "Set server URL").action((options) => {
378
- const logger2 = getLogger();
379
- const cfg = loadConfig();
380
- if (options.url) {
381
- cfg.baseUrl = options.url;
382
- setBaseUrl(options.url);
383
- logger2.success(`Server URL set to: ${options.url}`);
384
- }
385
- saveConfig(cfg);
386
- });
387
- config.command("url").description("Show or set server URL").argument("[url]", "New server URL").action((url) => {
388
- const logger2 = getLogger();
389
- if (url) {
390
- const cfg = loadConfig();
391
- cfg.baseUrl = url;
392
- setBaseUrl(url);
393
- saveConfig(cfg);
394
- logger2.success(`Server URL set to: ${url}`);
395
- } else {
396
- logger2.info(`Current server URL: ${getBaseUrl()}`);
397
- }
398
- });
399
- config.command("status").description("Check server connection status").action(async () => {
400
- const logger2 = getLogger();
401
- const client = getClient();
402
- try {
403
- const res = await client.health.$get();
404
- const data = await res.json();
405
- logger2.success("Server is reachable");
406
- logger2.info(JSON.stringify(data, null, 2));
407
- } catch (error) {
408
- logger2.error(`Server not reachable: ${getBaseUrl()}`);
409
- logger2.error(String(error));
410
- }
411
- });
412
- config.command("reset").description("Reset configuration to defaults").action(() => {
413
- const logger2 = getLogger();
414
- const defaultConfig = { baseUrl: "http://localhost:3010" };
415
- saveConfig(defaultConfig);
416
- setBaseUrl(defaultConfig.baseUrl);
417
- logger2.success("Configuration reset to defaults");
418
- });
419
- config.command("path").description("Show config file path").action(() => {
420
- const logger2 = getLogger();
421
- logger2.info(`Config file: ${CONFIG_FILE}`);
422
- });
423
- }
424
-
425
- // src/cli/modules/index.ts
426
- function registerModules(program2) {
427
- registerTodoCommands(program2);
428
- registerNotificationCommands(program2);
429
- registerConfigCommands(program2);
430
- }
431
11
  var tsImportFn;
432
12
  async function getTsImport() {
433
13
  if (tsImportFn) return tsImportFn;
@@ -525,11 +105,13 @@ function resolvePreset(preset, allManifests) {
525
105
  if (manifest.adminPages && manifest.adminPages.length > 0) hasAdmin = true;
526
106
  if (manifest.routes.admin && manifest.routes.admin.length > 0) hasAdmin = true;
527
107
  }
108
+ const isCliOnly = preset.id === "cli-only";
528
109
  return {
529
110
  preset,
530
111
  modules,
531
112
  hasAdmin,
532
- hasClient: true,
113
+ hasClient: !isCliOnly,
114
+ hasCli: true,
533
115
  hasSSE,
534
116
  hasWebSocket,
535
117
  hasPermission: modules.has("permission"),
@@ -563,13 +145,6 @@ function getAdminPages(resolved) {
563
145
  }
564
146
  return pages;
565
147
  }
566
- function getDefaultRoute(resolved) {
567
- const pages = getClientPages(resolved);
568
- if (pages.length > 0) {
569
- return pages[0].route;
570
- }
571
- return "/";
572
- }
573
148
 
574
149
  // src/generators/file-filter.ts
575
150
  function getExcludePatterns(resolved, allManifests) {
@@ -617,22 +192,54 @@ function getExcludePatterns(resolved, allManifests) {
617
192
  if (name === "notifications") {
618
193
  excludes.push("src/cli/modules/notification");
619
194
  }
195
+ if (name === "todos") {
196
+ excludes.push("src/cli/modules/todo");
197
+ excludes.push("src/server/__tests__/integration/todos-api.test.ts");
198
+ }
199
+ if (name === "auth") {
200
+ excludes.push("src/cli/modules/auth");
201
+ }
202
+ if (name === "plugin") {
203
+ excludes.push("src/cli/modules/plugin");
204
+ }
620
205
  }
621
206
  if (!resolved.modules.has("admin")) {
622
- excludes.push("src/client/components/AuthButton.tsx");
623
207
  excludes.push("src/admin");
624
208
  excludes.push("admin.html");
625
209
  excludes.push("auth-inject.html");
210
+ }
211
+ if (!resolved.hasClient) {
212
+ excludes.push("src/client");
213
+ excludes.push("index.html");
214
+ excludes.push("admin.html");
215
+ excludes.push("auth-inject.html");
216
+ excludes.push("src/admin");
217
+ excludes.push("vite.config.ts");
218
+ excludes.push("postcss.config.js");
219
+ excludes.push("tailwind.config.js");
220
+ }
221
+ if (!resolved.hasClient) {
222
+ excludes.push("src/client/components/AuthButton.tsx");
223
+ excludes.push("src/client/components/__tests__/AuthButton.test.tsx");
224
+ } else if (!resolved.modules.has("admin") && !resolved.modules.has("auth")) {
225
+ excludes.push("src/client/components/AuthButton.tsx");
626
226
  excludes.push("src/client/components/__tests__/AuthButton.test.tsx");
627
- excludes.push("src/cli");
628
227
  excludes.push("src/server/utils/auth.ts");
629
228
  }
630
- if (!resolved.hasPermission) {
229
+ excludes.push("src/client/preset-ui-config.ts");
230
+ if (!resolved.hasPermission && !resolved.modules.has("auth")) {
631
231
  excludes.push("src/server/utils/permission-utils.ts");
632
232
  excludes.push("src/server/middleware/__tests__/auth-simple.test.ts");
633
233
  excludes.push("src/server/middleware/__tests__/auth.test.ts");
634
234
  excludes.push("src/server/middleware/__tests__/error-response-format.test.ts");
635
235
  excludes.push("src/server/utils/__tests__/auth.test.ts");
236
+ } else if (!resolved.hasPermission && resolved.modules.has("auth")) {
237
+ excludes.push("src/server/utils/permission-utils.ts");
238
+ excludes.push("src/server/middleware/__tests__/auth-simple.test.ts");
239
+ excludes.push("src/server/middleware/__tests__/auth.test.ts");
240
+ excludes.push("src/server/middleware/__tests__/error-response-format.test.ts");
241
+ excludes.push("src/server/utils/__tests__/auth.test.ts");
242
+ excludes.push("src/server/module-auth/__tests__/auth-service.test.ts");
636
243
  } else if (!resolved.modules.has("admin")) {
637
244
  excludes.push("src/server/middleware/__tests__/error-response-format.test.ts");
638
245
  }
@@ -645,16 +252,29 @@ function getGeneratedFiles(resolved) {
645
252
  const files = [
646
253
  "src/server/route-registry.ts",
647
254
  "src/server/db/schema/index.ts",
648
- "src/client/App.tsx",
649
- "src/client/components/Navigation.tsx",
650
255
  "src/shared/modules/index.ts",
651
256
  "src/shared/schemas/index.ts",
652
257
  "src/server/middleware/index.ts",
653
- "src/client/components/index.ts"
258
+ "src/server/app.ts",
259
+ "src/cli/modules/index.ts"
654
260
  ];
655
- if (resolved.modules.has("admin")) {
261
+ if (resolved.hasClient) {
262
+ files.push(
263
+ "src/client/App.tsx",
264
+ "src/client/components/Navigation.tsx",
265
+ "src/client/components/index.ts",
266
+ "src/client/Layout.tsx",
267
+ "src/client/components/__tests__/App.test.tsx",
268
+ "src/client/components/__tests__/Navigation.test.tsx",
269
+ "src/client/main.tsx",
270
+ "src/client/preset-ui-config.ts"
271
+ );
272
+ }
273
+ if (resolved.hasClient && resolved.modules.has("admin")) {
656
274
  files.push("src/admin/App.tsx");
657
- files.push("src/cli/modules/index.ts");
275
+ }
276
+ if (resolved.hasClient && !resolved.modules.has("admin")) {
277
+ files.push("vite.config.ts");
658
278
  }
659
279
  if (!resolved.hasPermission) {
660
280
  files.push("src/server/middleware/auth.ts");
@@ -663,10 +283,6 @@ function getGeneratedFiles(resolved) {
663
283
  if (!resolved.modules.has("admin") && resolved.hasPermission) {
664
284
  files.push("src/server/utils/auth.ts");
665
285
  }
666
- files.push("src/server/app.ts");
667
- if (!resolved.modules.has("admin")) {
668
- files.push("vite.config.ts");
669
- }
670
286
  const seedModules = ["order", "ticket", "dispute", "content"];
671
287
  if (seedModules.some((m) => !resolved.modules.has(m)) || !resolved.hasPermission) {
672
288
  files.push("src/server/db/init.ts");
@@ -686,11 +302,14 @@ function generateRouteRegistry(resolved) {
686
302
  for (const [name, manifest] of moduleEntries) {
687
303
  const moduleDir = `module-${name}`;
688
304
  if (manifest.routes.client) {
689
- const { importPath, exportName } = manifest.routes.client;
690
- imports.push(
691
- `import { ${exportName} } from './${moduleDir}/${importPath.replace(/^\.\//, "")}'`
692
- );
693
- clientRoutes.push(` .route('/api', ${exportName})`);
305
+ const clientRouteList = Array.isArray(manifest.routes.client) ? manifest.routes.client : [manifest.routes.client];
306
+ for (const route of clientRouteList) {
307
+ const { importPath, exportName } = route;
308
+ imports.push(
309
+ `import { ${exportName} } from './${moduleDir}/${importPath.replace(/^\.\//, "")}'`
310
+ );
311
+ clientRoutes.push(` .route('/api', ${exportName})`);
312
+ }
694
313
  }
695
314
  if (manifest.routes.admin) {
696
315
  for (const route of manifest.routes.admin) {
@@ -747,130 +366,148 @@ function generateRouteRegistry(resolved) {
747
366
  return content;
748
367
  }
749
368
 
750
- // src/generators/client-app.ts
751
- function generateClientApp(resolved) {
752
- const pages = getClientPages(resolved);
753
- const defaultRoute = getDefaultRoute(resolved);
754
- const imports = [
755
- `import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'`,
756
- `import { Layout } from './Layout'`
757
- ];
758
- for (const page of pages) {
759
- imports.push(`import { ${page.name} } from './pages/${page.name}'`);
760
- }
761
- const routeElements = [];
762
- routeElements.push(
763
- ` <Route path="/" element={<Navigate to="${defaultRoute}" replace />} />`
764
- );
765
- for (const page of pages) {
766
- routeElements.push(
767
- ` <Route path="${page.route}" element={<${page.name} />} />`
768
- );
769
- }
770
- return `${imports.join("\n")}
771
-
772
- export function App() {
773
- return (
774
- <BrowserRouter>
775
- <Layout>
776
- <Routes>
777
- ${routeElements.join("\n")}
778
- </Routes>
779
- </Layout>
780
- </BrowserRouter>
781
- )
782
- }
783
- `;
784
- }
785
-
786
369
  // src/generators/client-navigation.ts
787
370
  var DEFAULT_ICON = "Circle";
788
371
  var ICON_MAP = {
789
372
  TodoPage: "CheckCircle",
790
373
  NotificationPage: "Bell",
791
374
  WebSocketPage: "Plug",
792
- ContentListPage: "FileText"
375
+ ContentListPage: "FileText",
376
+ PluginsPage: "Package",
377
+ CategoriesPage: "Grid",
378
+ SearchPage: "Search",
379
+ PublishPage: "Upload",
380
+ DeveloperDashboardPage: "Code",
381
+ PluginDetailPage: "Package",
382
+ TopicsPage: "Hash",
383
+ ProfilePage: "User",
384
+ DashboardPage: "LayoutDashboard",
385
+ SettingsPage: "Settings",
386
+ CartPage: "ShoppingCart",
387
+ OrdersPage: "Package"
793
388
  };
794
389
  function generateClientNavigation(resolved) {
795
390
  const pages = getClientPages(resolved).filter((p) => !p.route.includes(":"));
796
- const iconsNeeded = /* @__PURE__ */ new Set();
797
- iconsNeeded.add("Rocket");
798
- iconsNeeded.add("Github");
391
+ const hasAuth = resolved.modules.has("auth");
392
+ const navItems = [];
799
393
  for (const page of pages) {
800
394
  const icon = ICON_MAP[page.name] || DEFAULT_ICON;
801
- iconsNeeded.add(icon);
802
- }
803
- const routeKeys = [];
804
- const routeEntries = [];
805
- for (const page of pages) {
806
- const key = page.route.replace(/^\//, "").replace(/\//g, "-");
807
- routeKeys.push(`'${key}'`);
808
- const icon = ICON_MAP[page.name] || DEFAULT_ICON;
809
- const label = key === "todos" ? "Todo List" : key === "notifications" ? "Notifications" : key === "websocket" ? "WebSocket" : key.charAt(0).toUpperCase() + key.slice(1);
810
- const safeKey = /^[a-zA-Z0-9_]+$/.test(key) ? key : `'${key}'`;
811
- routeEntries.push(` ${safeKey}: { label: '${label}', icon: ${icon}, path: '${page.route}' },`);
395
+ const label = page.name === "TodoPage" ? "Todos" : page.name === "NotificationPage" ? "Notifications" : page.name === "WebSocketPage" ? "WebSocket" : page.name === "PluginsPage" ? "Plugins" : page.name === "CategoriesPage" ? "Categories" : page.name === "SearchPage" ? "Search" : page.name === "PublishPage" ? "Publish" : page.name === "DeveloperDashboardPage" ? "Developer" : page.name === "TopicsPage" ? "Topics" : page.name === "ProfilePage" ? "Profile" : page.name === "DashboardPage" ? "Dashboard" : page.name === "SettingsPage" ? "Settings" : page.name === "CartPage" ? "Cart" : page.name === "OrdersPage" ? "Orders" : page.route.replace(/^\//, "").charAt(0).toUpperCase() + page.route.replace(/^\//, "").slice(1);
396
+ navItems.push(` { label: '${label}', icon: '${icon}', path: '${page.route}' },`);
397
+ }
398
+ const authImport = hasAuth ? `
399
+ import { useAuthStore } from '../stores/authStore'` : "";
400
+ const authSection = hasAuth ? `
401
+ function AuthSection({ style }: { style: string }) {
402
+ const isAuthenticated = useAuthStore((state: any) => state.isAuthenticated)
403
+ const user = useAuthStore((state: any) => state.user)
404
+ const logout = useAuthStore((state: any) => state.logout)
405
+
406
+ if (style === 'none') return null
407
+
408
+ if (isAuthenticated) {
409
+ return (
410
+ <div className="flex items-center gap-2">
411
+ <span className="text-sm text-gray-600">{user?.username}</span>
412
+ <button onClick={logout} className="text-xs text-gray-400 hover:text-red-500">Sign Out</button>
413
+ </div>
414
+ )
812
415
  }
813
- const iconsStr = [...iconsNeeded].join(", ");
814
- const authButtonImport = resolved.modules.has("admin") ? `
815
- import { AuthButton } from './AuthButton'` : "";
816
- const authButtonElement = resolved.modules.has("admin") ? `
817
- <AuthButton />` : "";
818
- return `import { NavLink } from 'react-router-dom'
819
- import { ${iconsStr} } from 'lucide-react'${authButtonImport}
820
416
 
821
- type RouteKey = ${routeKeys.join(" | ")}
417
+ if (style === 'buttons') {
418
+ return (
419
+ <div className="flex items-center gap-2">
420
+ <Link
421
+ to="/login"
422
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
423
+ >
424
+ Sign In
425
+ </Link>
426
+ <Link
427
+ to="/register"
428
+ className="px-3 py-1 text-sm border border-gray-300 text-gray-700 rounded hover:bg-gray-50"
429
+ >
430
+ Sign Up
431
+ </Link>
432
+ </div>
433
+ )
434
+ }
822
435
 
823
- const routes: Record<
824
- RouteKey,
825
- { label: string; icon: React.FC<{ className?: string }>; path: string }
826
- > = {
827
- ${routeEntries.join("\n")}
436
+ return (
437
+ <Link to="/login" className="text-sm text-gray-500 hover:text-gray-900">
438
+ Login
439
+ </Link>
440
+ )
828
441
  }
442
+ ` : `
443
+ function AuthSection(_style: { style: string }) {
444
+ return null
445
+ }
446
+ `;
447
+ return `import { NavLink${hasAuth ? ", Link" : ""} } from 'react-router-dom'
448
+ import { Rocket, Sparkles } from 'lucide-react'${authImport}
449
+ import type { PresetTheme, NavigationConfig, ClientNavItem } from '../preset-ui-config'
450
+
451
+ interface NavigationProps {
452
+ preset?: string
453
+ items?: ClientNavItem[]
454
+ theme?: PresetTheme
455
+ navigation?: NavigationConfig
456
+ }
457
+ ${authSection}
458
+ export const Navigation: React.FC<NavigationProps> = ({
459
+ items,
460
+ theme,
461
+ navigation,
462
+ }) => {
463
+ const navItems = navigation?.navItems === 'none' ? [] : (items ?? [])
464
+ const primaryColor = theme?.primaryColor ?? '#6366f1'
465
+ const logoText = theme?.logoText ?? 'Biomimic'
466
+ const showLogo = navigation?.showLogo !== false
467
+ const authStyle = navigation?.authStyle ?? 'none'
829
468
 
830
- export const Navigation: React.FC = () => {
831
469
  return (
832
- <nav className="bg-white border-b border-gray-200 sticky top-0 z-50" data-testid="app-nav">
833
- <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
834
- <div className="flex items-center gap-8">
835
- <h1
836
- className="text-xl font-bold text-gray-900 flex items-center gap-2"
837
- data-testid="app-title"
838
- >
839
- <Rocket className="w-6 h-6 text-blue-500" />
840
- Biomimic App
841
- </h1>
842
- <div className="flex items-center gap-1">
843
- {(Object.keys(routes) as RouteKey[]).map(route => {
844
- const Icon = routes[route].icon
845
- return (
846
- <NavLink
847
- key={route}
848
- to={routes[route].path}
849
- data-testid={\`nav-\${route}-button\`}
850
- className={({ isActive }) =>
851
- \`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors \${
852
- isActive ? 'bg-blue-50 text-blue-600' : 'text-gray-600 hover:bg-gray-100'
853
- }\`
854
- }
855
- >
856
- <Icon className="w-4 h-4" />
857
- {routes[route].label}
858
- </NavLink>
859
- )
860
- })}
861
- </div>
862
- </div>
863
- <div className="flex items-center gap-4">${authButtonElement}
864
- <a
865
- href="https://github.com"
866
- target="_blank"
867
- rel="noopener noreferrer"
868
- data-testid="github-link"
869
- className="text-gray-400 hover:text-gray-600 transition-colors"
870
- >
871
- <Github className="w-5 h-5" />
872
- </a>
470
+ <nav className="hidden md:block bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50" data-testid="app-nav">
471
+ <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-3">
472
+ {showLogo && (
473
+ <NavLink to="/" className="flex items-center gap-2 group shrink-0" data-testid="app-title">
474
+ <div
475
+ className="w-8 h-8 rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow"
476
+ style={{ backgroundColor: primaryColor }}
477
+ >
478
+ <Rocket className="w-4 h-4 text-white" />
479
+ </div>
480
+ <span className="text-lg font-semibold text-gray-900 tracking-tight whitespace-nowrap">
481
+ {logoText}
482
+ </span>
483
+ <Sparkles className="w-3.5 h-3.5 shrink-0" style={{ color: primaryColor }} />
484
+ </NavLink>
485
+ )}
486
+
487
+ <div className="flex items-center gap-0.5 overflow-x-auto flex-1 min-w-0 scrollbar-hide">
488
+ {navItems.map(item => (
489
+ <NavLink
490
+ key={item.path}
491
+ to={item.path}
492
+ data-testid={\`nav-\${item.label.toLowerCase().replace(/\\s+/g, '-')}-button\`}
493
+ className={({ isActive }: { isActive: boolean }) =>
494
+ \`px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 shrink-0 whitespace-nowrap \${
495
+ isActive ? 'text-white' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-50'
496
+ }\`
497
+ }
498
+ style={
499
+ (({ isActive }: { isActive: boolean }) =>
500
+ isActive
501
+ ? { backgroundColor: \`\${primaryColor}15\`, color: primaryColor }
502
+ : undefined) as never
503
+ }
504
+ >
505
+ {item.label}
506
+ </NavLink>
507
+ ))}
873
508
  </div>
509
+
510
+ <AuthSection style={authStyle} />
874
511
  </div>
875
512
  </nav>
876
513
  )
@@ -878,6 +515,119 @@ export const Navigation: React.FC = () => {
878
515
  `;
879
516
  }
880
517
 
518
+ // src/generators/client-app-test.ts
519
+ function generateClientAppTest(resolved) {
520
+ const pages = getClientPages(resolved);
521
+ const mocks = pages.map(
522
+ (p) => `vi.mock('@client/pages/${p.name}', () => ({
523
+ ${p.name}: () => <div data-testid="${p.name.toLowerCase()}-page">${p.name}</div>,
524
+ }))`
525
+ ).join("\n\n ");
526
+ return `import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
527
+ import { render, screen, cleanup } from '@testing-library/react'
528
+ import '@testing-library/jest-dom'
529
+ import { App } from '@client/App'
530
+
531
+ ${mocks}
532
+
533
+ describe('App Component', () => {
534
+ beforeEach(() => {
535
+ vi.clearAllMocks()
536
+ })
537
+
538
+ afterEach(() => {
539
+ cleanup()
540
+ })
541
+
542
+ describe('Initial Render', () => {
543
+ it('should render navigation', () => {
544
+ render(<App />)
545
+ expect(screen.getByTestId('app-nav')).toBeInTheDocument()
546
+ })
547
+
548
+ it('should render main content area', () => {
549
+ render(<App />)
550
+ expect(screen.getByTestId('app-main')).toBeInTheDocument()
551
+ })
552
+
553
+ it('should render container', () => {
554
+ render(<App />)
555
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
556
+ })
557
+ })
558
+
559
+ describe('Navigation Links', () => {
560
+ it('should render footer', () => {
561
+ render(<App />)
562
+ expect(screen.getByTestId('app-footer')).toBeInTheDocument()
563
+ })
564
+ })
565
+ })
566
+ `;
567
+ }
568
+
569
+ // src/generators/client-navigation-test.ts
570
+ var LABEL_MAP = {
571
+ TodoPage: "Todos",
572
+ NotificationPage: "Notifications",
573
+ WebSocketPage: "WebSocket",
574
+ PluginsPage: "Plugins",
575
+ CategoriesPage: "Categories",
576
+ SearchPage: "Search",
577
+ PublishPage: "Publish",
578
+ DeveloperDashboardPage: "Developer",
579
+ TopicsPage: "Topics",
580
+ ProfilePage: "Profile",
581
+ DashboardPage: "Dashboard",
582
+ SettingsPage: "Settings",
583
+ CartPage: "Cart",
584
+ OrdersPage: "Orders"
585
+ };
586
+ function generateClientNavigationTest(resolved) {
587
+ const pages = getClientPages(resolved).filter((p) => !p.route.includes(":"));
588
+ const firstPage = pages[0];
589
+ if (!firstPage) {
590
+ return `import { describe, it, expect } from 'vitest'
591
+ import { render, screen } from '@testing-library/react'
592
+ import { BrowserRouter } from 'react-router-dom'
593
+ import { Navigation } from '../Navigation'
594
+
595
+ const renderWithRouter = (component: React.ReactNode) => {
596
+ return render(<BrowserRouter>{component}</BrowserRouter>)
597
+ }
598
+
599
+ describe('Navigation', () => {
600
+ it('should render navigation', () => {
601
+ renderWithRouter(<Navigation />)
602
+ expect(screen.getByTestId('app-nav')).toBeInTheDocument()
603
+ })
604
+ })
605
+ `;
606
+ }
607
+ const firstLabel = LABEL_MAP[firstPage.name] || firstPage.name.replace("Page", "");
608
+ return `import { describe, it, expect } from 'vitest'
609
+ import { render, screen } from '@testing-library/react'
610
+ import { BrowserRouter } from 'react-router-dom'
611
+ import { Navigation } from '../Navigation'
612
+
613
+ const renderWithRouter = (component: React.ReactNode) => {
614
+ return render(<BrowserRouter>{component}</BrowserRouter>)
615
+ }
616
+
617
+ describe('Navigation', () => {
618
+ it('should render navigation', () => {
619
+ renderWithRouter(<Navigation />)
620
+ expect(screen.getByTestId('app-nav')).toBeInTheDocument()
621
+ })
622
+
623
+ it('should render nav items', () => {
624
+ renderWithRouter(<Navigation />)
625
+ expect(screen.getByText('${firstLabel}')).toBeInTheDocument()
626
+ })
627
+ })
628
+ `;
629
+ }
630
+
881
631
  // src/generators/admin-app.ts
882
632
  function generateAdminApp(resolved) {
883
633
  if (!resolved.hasAdmin) return null;
@@ -906,7 +656,7 @@ function generateAdminApp(resolved) {
906
656
  );
907
657
  return `${imports.join("\n")}
908
658
 
909
- export const App: React.FC = () => {
659
+ export const App: React.FC<{ basePath?: string }> = ({ basePath = '/admin' }) => {
910
660
  return (
911
661
  <ConfigProvider
912
662
  theme={{
@@ -915,7 +665,7 @@ export const App: React.FC = () => {
915
665
  },
916
666
  }}
917
667
  >
918
- <BrowserRouter basename="/admin">
668
+ <BrowserRouter basename={basePath}>
919
669
  <Routes>
920
670
  ${publicRouteLines.join("\n")}
921
671
  <Route
@@ -943,8 +693,8 @@ ${protectedRouteElements.join("\n")}
943
693
  // src/generators/db-schema-barrel.ts
944
694
  function generateDbSchemaBarrel(resolved) {
945
695
  const files = getDbSchemaFiles(resolved);
946
- const exports$1 = files.map((f) => `export * from './${f}'`);
947
- return exports$1.join("\n") + "\n";
696
+ const exports = files.map((f) => `export * from './${f}'`);
697
+ return exports.join("\n") + "\n";
948
698
  }
949
699
 
950
700
  // src/generators/db-init.ts
@@ -1629,19 +1379,73 @@ var MODULE_EXPORTS = {
1629
1379
  "type PermissionInfo",
1630
1380
  "type UserPermissions"
1631
1381
  ]
1382
+ },
1383
+ auth: {
1384
+ namedExports: [
1385
+ "DeveloperProfileSchema",
1386
+ "LoginSchema",
1387
+ "RegisterSchema",
1388
+ "TokenResponseSchema",
1389
+ "type DeveloperProfile",
1390
+ "type LoginInput",
1391
+ "type RegisterInput",
1392
+ "type TokenResponse"
1393
+ ]
1394
+ },
1395
+ plugin: {
1396
+ namedExports: [
1397
+ "PluginSchema",
1398
+ "PluginStatusSchema",
1399
+ "CreatePluginSchema",
1400
+ "UpdatePluginSchema",
1401
+ "PluginVersionStatusSchema",
1402
+ "VersionSchema",
1403
+ "CategorySchema",
1404
+ "ReviewSchema",
1405
+ "CreateReviewSchema",
1406
+ "MarketplaceStatsSchema",
1407
+ "PluginListResponseSchema",
1408
+ "AdminPluginSchema",
1409
+ "AdminDashboardStatsSchema",
1410
+ "PluginListQuerySchema",
1411
+ "PluginSlugSchema",
1412
+ "type Plugin",
1413
+ "type PluginStatus",
1414
+ "type CreatePluginInput",
1415
+ "type UpdatePluginInput",
1416
+ "type PluginVersionStatus",
1417
+ "type Version",
1418
+ "type Category",
1419
+ "type Review",
1420
+ "type CreateReviewInput",
1421
+ "type MarketplaceStats",
1422
+ "type PluginListResponse",
1423
+ "type AdminPlugin",
1424
+ "type AdminDashboardStats",
1425
+ "type PluginListQuery"
1426
+ ]
1632
1427
  }
1633
1428
  };
1634
1429
  function generateSharedModulesIndex(resolved) {
1635
1430
  const lines = [];
1636
- const moduleOrder = ["chat", "todos", "file", "notifications", "admin", "permission"];
1637
- for (const moduleName of moduleOrder) {
1431
+ const moduleOrder2 = [
1432
+ "chat",
1433
+ "todos",
1434
+ "file",
1435
+ "notifications",
1436
+ "admin",
1437
+ "permission",
1438
+ "auth",
1439
+ "plugin"
1440
+ ];
1441
+ for (const moduleName of moduleOrder2) {
1638
1442
  if (!resolved.modules.has(moduleName)) continue;
1639
1443
  const manifest = resolved.modules.get(moduleName);
1640
1444
  const exportKey = manifest.sharedSchemas?.path ?? moduleName;
1641
- const exports$1 = MODULE_EXPORTS[exportKey];
1642
- if (!exports$1) continue;
1445
+ const exports = MODULE_EXPORTS[exportKey];
1446
+ if (!exports) continue;
1643
1447
  lines.push(`export {
1644
- ${exports$1.namedExports.join(",\n ")},
1448
+ ${exports.namedExports.join(",\n ")},
1645
1449
  } from './${exportKey}'`);
1646
1450
  }
1647
1451
  return lines.join("\n") + "\n";
@@ -1713,8 +1517,329 @@ var MODULE_EXPORTS2 = {
1713
1517
  "type NotificationId",
1714
1518
  "type UnreadCountEvent"
1715
1519
  ]
1520
+ },
1521
+ auth: {
1522
+ namedExports: [
1523
+ "DeveloperProfileSchema",
1524
+ "LoginSchema",
1525
+ "RegisterSchema",
1526
+ "TokenResponseSchema",
1527
+ "ProfileSchema",
1528
+ "type DeveloperProfile",
1529
+ "type LoginInput",
1530
+ "type RegisterInput",
1531
+ "type TokenResponse",
1532
+ "type Profile"
1533
+ ]
1534
+ },
1535
+ plugin: {
1536
+ namedExports: [
1537
+ "PluginSchema",
1538
+ "PluginStatusSchema",
1539
+ "CreatePluginSchema",
1540
+ "UpdatePluginSchema",
1541
+ "PluginVersionStatusSchema",
1542
+ "VersionSchema",
1543
+ "CategorySchema",
1544
+ "ReviewSchema",
1545
+ "CreateReviewSchema",
1546
+ "MarketplaceStatsSchema",
1547
+ "PluginListResponseSchema",
1548
+ "AdminPluginSchema",
1549
+ "AdminDashboardStatsSchema",
1550
+ "PluginListQuerySchema",
1551
+ "PluginSlugSchema",
1552
+ "PluginSearchQuerySchema",
1553
+ "PluginDeleteResponseSchema",
1554
+ "ReviewIdParamsSchema",
1555
+ "ReviewDeleteResponseSchema",
1556
+ "CategorySlugParamsSchema",
1557
+ "CategoryPluginsQuerySchema",
1558
+ "PluginListAdminSchema",
1559
+ "AdminListQuerySchema",
1560
+ "AdminListAllQuerySchema",
1561
+ "RejectPluginBodySchema",
1562
+ "BulkApproveBodySchema",
1563
+ "BulkRejectBodySchema",
1564
+ "BulkResponseSchema",
1565
+ "CreateCategoryBodySchema",
1566
+ "UpdateCategoryBodySchema",
1567
+ "CategoryIdParamsSchema",
1568
+ "CategoryIdResponseSchema",
1569
+ "type Plugin",
1570
+ "type PluginStatus",
1571
+ "type CreatePluginInput",
1572
+ "type UpdatePluginInput",
1573
+ "type PluginVersionStatus",
1574
+ "type Version",
1575
+ "type Category",
1576
+ "type Review",
1577
+ "type CreateReviewInput",
1578
+ "type MarketplaceStats",
1579
+ "type PluginListResponse",
1580
+ "type AdminPlugin",
1581
+ "type AdminDashboardStats",
1582
+ "type PluginListQuery"
1583
+ ]
1584
+ },
1585
+ admin: {
1586
+ namedExports: [
1587
+ "SystemStatsSchema",
1588
+ "HealthCheckSchema",
1589
+ "RecentActivityItemSchema",
1590
+ "RecentActivitySchema",
1591
+ "AuthUserSchema",
1592
+ "LoginRequestSchema",
1593
+ "LoginResponseSchema",
1594
+ "RegisterRequestSchema",
1595
+ "UserSchema",
1596
+ "UserListSchema",
1597
+ "UpdateUserRequestSchema",
1598
+ "CreateUserRequestSchema",
1599
+ "ClearTodosResultSchema",
1600
+ "SuccessSchema",
1601
+ "DownloadTokenSchema",
1602
+ "type SystemStats",
1603
+ "type HealthCheck",
1604
+ "type RecentActivityItem",
1605
+ "type AuthUserResponse",
1606
+ "type CreateUserRequest",
1607
+ "type LoginRequest",
1608
+ "type LoginResponse",
1609
+ "type RegisterRequest",
1610
+ "type User",
1611
+ "type UpdateUserRequest",
1612
+ "type ClearTodosResult"
1613
+ ]
1614
+ },
1615
+ audit: {
1616
+ namedExports: ["ResourceTypeSchema", "ActionTypeSchema", "AuditLogSchema", "type AuditLogType"]
1617
+ },
1618
+ captcha: {
1619
+ namedExports: [
1620
+ "CaptchaResponseSchema",
1621
+ "VerifyCaptchaRequestSchema",
1622
+ "CaptchaVerifyResponseSchema",
1623
+ "type CaptchaResponse",
1624
+ "type VerifyCaptchaRequest",
1625
+ "type CaptchaVerifyResponse"
1626
+ ]
1627
+ },
1628
+ cart: {
1629
+ namedExports: [
1630
+ "CartItemSchema",
1631
+ "CartSummarySchema",
1632
+ "CartResponseSchema",
1633
+ "AddCartItemSchema",
1634
+ "CartItemIdSchema",
1635
+ "type CartItem",
1636
+ "type CartSummary",
1637
+ "type CartResponse",
1638
+ "type AddCartItemInput"
1639
+ ]
1640
+ },
1641
+ community: {
1642
+ namedExports: [
1643
+ "TopicStatusSchema",
1644
+ "TopicTagSchema",
1645
+ "TopicAuthorSchema",
1646
+ "TopicSchema",
1647
+ "TopicsResponseSchema",
1648
+ "ProfileStatsSchema",
1649
+ "ActivityTypeSchema",
1650
+ "ProfileActivitySchema",
1651
+ "ProfileResponseSchema",
1652
+ "type TopicStatus",
1653
+ "type TopicTag",
1654
+ "type TopicAuthor",
1655
+ "type Topic",
1656
+ "type ProfileStats",
1657
+ "type ActivityType",
1658
+ "type ProfileActivity",
1659
+ "type ProfileResponse"
1660
+ ]
1661
+ },
1662
+ content: {
1663
+ namedExports: [
1664
+ "ContentCategorySchema",
1665
+ "ContentStatusSchema",
1666
+ "ContentSchema",
1667
+ "CreateContentSchema",
1668
+ "UpdateContentSchema",
1669
+ "ContentListSchema",
1670
+ "DeleteResultSchema",
1671
+ "type ContentCategory",
1672
+ "type ContentStatus",
1673
+ "type Content",
1674
+ "type CreateContentInput",
1675
+ "type UpdateContentInput",
1676
+ "type DeleteResult"
1677
+ ]
1678
+ },
1679
+ dashboard: {
1680
+ namedExports: [
1681
+ "DashboardStatSchema",
1682
+ "RevenueDataSchema",
1683
+ "ActivityStatusSchema",
1684
+ "ActivitySchema",
1685
+ "DashboardResponseSchema",
1686
+ "type DashboardStat",
1687
+ "type RevenueData",
1688
+ "type ActivityStatus",
1689
+ "type Activity",
1690
+ "type DashboardResponse"
1691
+ ]
1692
+ },
1693
+ dispute: {
1694
+ namedExports: [
1695
+ "DisputeTypeSchema",
1696
+ "DisputeStatusSchema",
1697
+ "DisputeSchema",
1698
+ "CreateDisputeSchema",
1699
+ "UpdateDisputeSchema",
1700
+ "ResolveDisputeSchema",
1701
+ "DisputeListSchema",
1702
+ "DeleteResultSchema",
1703
+ "type DisputeType",
1704
+ "type DisputeStatus",
1705
+ "type Dispute",
1706
+ "type CreateDisputeInput",
1707
+ "type UpdateDisputeInput",
1708
+ "type ResolveDisputeInput",
1709
+ "type DeleteResult"
1710
+ ]
1711
+ },
1712
+ order: {
1713
+ namedExports: [
1714
+ "OrderStatusSchema",
1715
+ "OrderSchema",
1716
+ "CreateOrderSchema",
1717
+ "UpdateOrderSchema",
1718
+ "OrderListSchema",
1719
+ "OrderQuerySchema",
1720
+ "DeleteResultSchema",
1721
+ "ProcessOrderSchema",
1722
+ "CancelOrderSchema",
1723
+ "RemoveCartItemResponseSchema",
1724
+ "ECommerceProductSchema",
1725
+ "ECommerceOrderStatusSchema",
1726
+ "ECommerceOrderSchema",
1727
+ "ECommerceOrderListSchema",
1728
+ "type OrderStatus",
1729
+ "type Order",
1730
+ "type CreateOrderInput",
1731
+ "type UpdateOrderInput",
1732
+ "type DeleteResult",
1733
+ "type ProcessOrderInput",
1734
+ "type CancelOrderInput",
1735
+ "type OrderQueryInput",
1736
+ "type RemoveCartItemResponse",
1737
+ "type ECommerceProduct",
1738
+ "type ECommerceOrderStatus",
1739
+ "type ECommerceOrder"
1740
+ ]
1741
+ },
1742
+ permission: {
1743
+ namedExports: [
1744
+ "RoleEnum",
1745
+ "PermissionEnum",
1746
+ "RoleInfoSchema",
1747
+ "PermissionInfoSchema",
1748
+ "UserPermissionsSchema",
1749
+ "MenuItemSchema",
1750
+ "PageActionSchema",
1751
+ "PagePermissionConfigSchema",
1752
+ "PermissionCategorySchema",
1753
+ "RoleListSchema",
1754
+ "PermissionListSchema",
1755
+ "MenuConfigSchema",
1756
+ "PagePermissionsSchema",
1757
+ "PermissionCategoriesSchema",
1758
+ "RoleLabelsSchema",
1759
+ "PermissionLabelsSchema",
1760
+ "PermissionInitSchema",
1761
+ "type RoleType",
1762
+ "type PermissionType",
1763
+ "type RoleInfo",
1764
+ "type PermissionInfo",
1765
+ "type UserPermissions",
1766
+ "type MenuItem",
1767
+ "type PageAction",
1768
+ "type PagePermissionConfig",
1769
+ "type PermissionCategory",
1770
+ "type PermissionInit",
1771
+ "Role",
1772
+ "Permission",
1773
+ "ROLE_PERMISSIONS",
1774
+ "ROLE_LABELS",
1775
+ "PERMISSION_LABELS",
1776
+ "PERMISSION_CATEGORIES",
1777
+ "getPermissionsByRole",
1778
+ "hasPermission",
1779
+ "hasAnyPermission",
1780
+ "hasAllPermissions"
1781
+ ]
1782
+ },
1783
+ role: {
1784
+ namedExports: [
1785
+ "RoleSchema",
1786
+ "CreateRoleSchema",
1787
+ "UpdateRoleSchema",
1788
+ "UpdateRolePermissionsSchema",
1789
+ "SuccessSchema",
1790
+ "type RoleType",
1791
+ "type CreateRoleType",
1792
+ "type UpdateRoleType"
1793
+ ]
1794
+ },
1795
+ ticket: {
1796
+ namedExports: [
1797
+ "TicketStatusSchema",
1798
+ "TicketPrioritySchema",
1799
+ "TicketCategorySchema",
1800
+ "TicketReplySchema",
1801
+ "TicketSchema",
1802
+ "CreateTicketSchema",
1803
+ "UpdateTicketSchema",
1804
+ "ReplyTicketSchema",
1805
+ "TicketListSchema",
1806
+ "DeleteResultSchema",
1807
+ "type TicketStatus",
1808
+ "type TicketPriority",
1809
+ "type TicketCategory",
1810
+ "type TicketReply",
1811
+ "type Ticket",
1812
+ "type CreateTicketInput",
1813
+ "type UpdateTicketInput",
1814
+ "type ReplyTicketInput",
1815
+ "type DeleteResult"
1816
+ ]
1716
1817
  }
1717
1818
  };
1819
+ var ADDITIONAL_PATHS_MAP = {
1820
+ permission: ["role", "audit"]
1821
+ };
1822
+ var STANDALONE_SHARED_MODULES = /* @__PURE__ */ new Set(["cart", "community", "dashboard"]);
1823
+ var moduleOrder = [
1824
+ "chat",
1825
+ "file",
1826
+ "todos",
1827
+ "notifications",
1828
+ "auth",
1829
+ "plugin",
1830
+ "admin",
1831
+ "audit",
1832
+ "captcha",
1833
+ "cart",
1834
+ "community",
1835
+ "content",
1836
+ "dashboard",
1837
+ "dispute",
1838
+ "order",
1839
+ "permission",
1840
+ "role",
1841
+ "ticket"
1842
+ ];
1718
1843
  function generateSharedSchemasIndex(resolved) {
1719
1844
  const header = `// Re-export interfaces from implementation files
1720
1845
  export type { WSClient, WSProtocol, WSStatus } from '../core/ws-client'
@@ -1740,25 +1865,49 @@ export {
1740
1865
  // Re-export modules
1741
1866
  `;
1742
1867
  const moduleLines = [];
1743
- const moduleOrder = ["chat", "file", "todos", "notifications"];
1868
+ const exportedNames = /* @__PURE__ */ new Set();
1744
1869
  for (const moduleName of moduleOrder) {
1745
- if (!resolved.modules.has(moduleName)) continue;
1746
- const exports$1 = MODULE_EXPORTS2[moduleName];
1747
- if (!exports$1) continue;
1748
- const manifest = resolved.modules.get(moduleName);
1749
- const importPath = manifest.sharedSchemas?.path ?? moduleName;
1870
+ const exports = MODULE_EXPORTS2[moduleName];
1871
+ if (!exports) continue;
1872
+ const shouldInclude = shouldIncludeModule(moduleName, resolved);
1873
+ if (!shouldInclude) continue;
1874
+ const uniqueExports = exports.namedExports.filter((name) => {
1875
+ if (exportedNames.has(name)) return false;
1876
+ exportedNames.add(name);
1877
+ return true;
1878
+ });
1879
+ if (uniqueExports.length === 0) continue;
1880
+ const importPath = getImportPath(moduleName, resolved);
1750
1881
  moduleLines.push(
1751
1882
  `export {
1752
- ${exports$1.namedExports.join(",\n ")},
1883
+ ${uniqueExports.join(",\n ")},
1753
1884
  } from '../modules/${importPath}'`
1754
1885
  );
1755
1886
  }
1756
1887
  return header + moduleLines.join("\n") + "\n";
1757
1888
  }
1889
+ function shouldIncludeModule(moduleName, resolved) {
1890
+ if (resolved.modules.has(moduleName)) return true;
1891
+ if (STANDALONE_SHARED_MODULES.has(moduleName)) return true;
1892
+ for (const [parentModule, additionalPaths] of Object.entries(ADDITIONAL_PATHS_MAP)) {
1893
+ if (additionalPaths.includes(moduleName) && resolved.modules.has(parentModule)) {
1894
+ return true;
1895
+ }
1896
+ }
1897
+ return false;
1898
+ }
1899
+ function getImportPath(moduleName, resolved) {
1900
+ const manifest = resolved.modules.get(moduleName);
1901
+ if (manifest?.sharedSchemas?.path) {
1902
+ return manifest.sharedSchemas.path;
1903
+ }
1904
+ return moduleName;
1905
+ }
1758
1906
 
1759
1907
  // src/generators/middleware-index.ts
1760
1908
  function generateMiddlewareIndex(resolved) {
1761
1909
  const lines = [];
1910
+ const hasAuthOrPermission = resolved.modules.has("auth") || resolved.hasPermission;
1762
1911
  lines.push(`export { corsMiddleware, createCorsMiddleware, type CorsOptions } from './cors'`);
1763
1912
  lines.push(
1764
1913
  `export { loggerMiddleware, createLoggerMiddleware, type LoggerOptions } from './logger'`
@@ -1770,7 +1919,7 @@ function generateMiddlewareIndex(resolved) {
1770
1919
  type ErrorHandlerOptions,
1771
1920
  } from './error-handler'`
1772
1921
  );
1773
- if (resolved.hasPermission) {
1922
+ if (hasAuthOrPermission) {
1774
1923
  lines.push(
1775
1924
  `export {
1776
1925
  authMiddleware,
@@ -1796,14 +1945,101 @@ function generateMiddlewareIndex(resolved) {
1796
1945
  lines.push(`export { permissionMiddleware } from './permission'`);
1797
1946
  }
1798
1947
  lines.push(`export { rateLimitMiddleware, type RateLimitOptions } from './rate-limit'`);
1799
- if (resolved.hasPermission) {
1948
+ if (hasAuthOrPermission) {
1800
1949
  lines.push(`export { getAuthUser } from '../utils/auth'`);
1801
1950
  }
1802
1951
  return lines.join("\n") + "\n";
1803
1952
  }
1804
1953
 
1805
1954
  // src/generators/auth-middleware.ts
1806
- function generateAuthMiddleware(_resolved) {
1955
+ function generateAuthMiddleware(resolved) {
1956
+ if (resolved.modules.has("auth") && !resolved.hasPermission) {
1957
+ return generateSimplifiedAuthMiddleware();
1958
+ }
1959
+ return generateNoopAuthMiddleware();
1960
+ }
1961
+ function generateSimplifiedAuthMiddleware() {
1962
+ return `import type { MiddlewareHandler } from 'hono'
1963
+ import { createModuleLoggerSync } from '../utils/logger'
1964
+
1965
+ export type UserRole = 'user' | 'admin'
1966
+
1967
+ export interface AuthUser {
1968
+ id: string
1969
+ username: string
1970
+ email: string
1971
+ role: UserRole
1972
+ avatar?: string
1973
+ }
1974
+
1975
+ export interface AuthMiddlewareOptions {
1976
+ requiredRole?: UserRole
1977
+ }
1978
+
1979
+ declare module 'hono' {
1980
+ interface ContextVariableMap {
1981
+ authUser: AuthUser
1982
+ }
1983
+ }
1984
+
1985
+ function extractToken(authHeader: string | undefined): string | null {
1986
+ if (!authHeader) return null
1987
+ if (!authHeader.startsWith('Bearer ')) return null
1988
+ return authHeader.slice(7)
1989
+ }
1990
+
1991
+ export function authMiddleware(_options: AuthMiddlewareOptions = {}): MiddlewareHandler {
1992
+ const log = createModuleLoggerSync('auth')
1993
+
1994
+ return async (c, next) => {
1995
+ const token = extractToken(c.req.header('Authorization'))
1996
+
1997
+ if (!token) {
1998
+ log.warn({ path: c.req.path, method: c.req.method }, 'Missing auth token')
1999
+ return c.json({ success: false, error: 'Authentication required', status: 401 }, 401)
2000
+ }
2001
+
2002
+ try {
2003
+ const jwt = await import('jsonwebtoken')
2004
+ const secretKey = process.env.AUTH_SECRET_KEY || 'dev-secret-key-change-in-production'
2005
+ const decoded = jwt.verify(token, secretKey) as {
2006
+ userId: string
2007
+ username: string
2008
+ email: string
2009
+ role: string
2010
+ }
2011
+
2012
+ const user: AuthUser = {
2013
+ id: decoded.userId,
2014
+ username: decoded.username,
2015
+ email: decoded.email,
2016
+ role: decoded.role as UserRole,
2017
+ }
2018
+
2019
+ c.set('authUser', user)
2020
+ log.info({ userId: user.id, path: c.req.path }, 'User authenticated')
2021
+ await next()
2022
+ } catch {
2023
+ log.warn({ path: c.req.path, method: c.req.method }, 'Invalid auth token')
2024
+ return c.json({ success: false, error: 'Invalid or expired token', status: 401 }, 401)
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ export function requireSuperAdminMiddleware(): MiddlewareHandler {
2030
+ return authMiddleware()
2031
+ }
2032
+
2033
+ export function requireCustomerServiceMiddleware(): MiddlewareHandler {
2034
+ return authMiddleware()
2035
+ }
2036
+
2037
+ export function requirePermissionsMiddleware(): MiddlewareHandler {
2038
+ return authMiddleware()
2039
+ }
2040
+ `;
2041
+ }
2042
+ function generateNoopAuthMiddleware() {
1807
2043
  return `import type { MiddlewareHandler } from 'hono'
1808
2044
  import { createModuleLoggerSync } from '../utils/logger'
1809
2045
 
@@ -1876,13 +2112,31 @@ export function requirePermissionsMiddleware(): MiddlewareHandler {
1876
2112
 
1877
2113
  // src/generators/auth-utils.ts
1878
2114
  function generateAuthUtils(resolved) {
1879
- if (!resolved.hasPermission) {
2115
+ const hasAuthOrPermission = resolved.modules.has("auth") || resolved.hasPermission;
2116
+ if (!hasAuthOrPermission) {
1880
2117
  return `import type { Context } from 'hono'
1881
2118
  import type { AuthUser } from '../middleware/auth'
1882
2119
 
1883
2120
  export function getAuthUser(c: Context): AuthUser {
1884
2121
  return c.get('authUser')
1885
2122
  }
2123
+ `;
2124
+ }
2125
+ if (resolved.modules.has("auth") && !resolved.hasPermission) {
2126
+ return `import type { Context } from 'hono'
2127
+ import type { AuthUser } from '../middleware/auth'
2128
+
2129
+ export function getAuthUser(c: Context): AuthUser {
2130
+ return c.get('authUser')
2131
+ }
2132
+
2133
+ export function getOptionalAuthUser(c: Context): AuthUser | null {
2134
+ try {
2135
+ return c.get('authUser')
2136
+ } catch {
2137
+ return null
2138
+ }
2139
+ }
1886
2140
  `;
1887
2141
  }
1888
2142
  return `import type { Context } from 'hono'
@@ -1975,7 +2229,7 @@ function generateClientComponentsIndex(resolved) {
1975
2229
  if (resolved.modules.has("chat")) {
1976
2230
  lines.push(`export { MessageCard } from './MessageCard'`);
1977
2231
  }
1978
- if (resolved.modules.has("admin")) {
2232
+ if (resolved.modules.has("admin") || resolved.modules.has("auth")) {
1979
2233
  lines.push(`export { AuthButton } from './AuthButton'`);
1980
2234
  }
1981
2235
  return lines.join("\n") + "\n";
@@ -1987,47 +2241,96 @@ function generateCliModulesIndex(resolved) {
1987
2241
  const registrations = [];
1988
2242
  if (resolved.modules.has("todos")) {
1989
2243
  modules.push("import { registerTodoCommands } from './todo'");
1990
- registrations.push("registerTodoCommands(program)");
2244
+ registrations.push("registerTodoCommands(site)");
1991
2245
  }
1992
2246
  if (resolved.modules.has("notifications")) {
1993
- modules.push(
1994
- "import { registerNotificationCommands } from './notification'"
1995
- );
1996
- registrations.push("registerNotificationCommands(program)");
2247
+ modules.push("import { registerNotificationCommands } from './notification'");
2248
+ registrations.push("registerNotificationCommands(site)");
2249
+ }
2250
+ if (resolved.modules.has("auth")) {
2251
+ modules.push("import { registerAuthCommands } from './auth'");
2252
+ registrations.push("registerAuthCommands(site)");
2253
+ }
2254
+ if (resolved.modules.has("plugin")) {
2255
+ modules.push("import { registerPluginCommands } from './plugin'");
2256
+ registrations.push("registerPluginCommands(site)");
1997
2257
  }
1998
2258
  modules.push("import { registerConfigCommands } from './config'");
1999
- registrations.push("registerConfigCommands(program)");
2000
- const imports = `import type { Command } from 'commander'
2001
- ${modules.join("\n")}`;
2002
- const exports$1 = modules.map((m) => {
2259
+ registrations.push("registerConfigCommands(site)");
2260
+ const exports = modules.map((m) => {
2003
2261
  const match = m.match(/\{ (\w+) \}/);
2004
2262
  return match ? match[1] : "";
2005
2263
  }).filter(Boolean);
2006
- return `${imports}
2264
+ return `import type { Core } from '@dyyz1993/xcli-core'
2265
+ ${modules.join("\n")}
2266
+
2267
+ /**
2268
+ * Register all builtin CLI commands to xcli-core.
2269
+ * Each register function receives a SiteInstance for command registration.
2270
+ */
2271
+ export function registerBuiltinCommands(app: Core) {
2272
+ const api = app.loader.getAPI()
2273
+
2274
+ const site = api.createSite({
2275
+ name: 'local-server',
2276
+ url: 'http://localhost:3010',
2277
+ })
2007
2278
 
2008
- export function registerModules(program: Command) {
2009
2279
  ${registrations.map((r) => ` ${r}`).join("\n")}
2010
2280
  }
2011
2281
 
2012
- export { ${exports$1.join(", ")} }
2282
+ export { ${exports.join(", ")} }
2013
2283
  `;
2014
2284
  }
2015
2285
 
2016
2286
  // src/generators/package-json.ts
2017
2287
  var MODULE_PACKAGES = {
2018
- admin: ["bcryptjs"]
2288
+ admin: ["bcryptjs"],
2289
+ auth: ["bcryptjs"]
2019
2290
  };
2020
2291
  var ADMIN_PANEL_PACKAGES = ["antd"];
2021
2292
  var CLI_PACKAGES = ["commander"];
2022
2293
  var UNUSED_PACKAGES = ["lodash-es", "chalk", "mysql2"];
2294
+ var CLIENT_PACKAGES = [
2295
+ "react",
2296
+ "react-dom",
2297
+ "react-helmet-async",
2298
+ "react-router-dom",
2299
+ "lucide-react",
2300
+ "zustand"
2301
+ ];
2302
+ var CLIENT_DEV_PACKAGES = [
2303
+ "@vitejs/plugin-react",
2304
+ "@testing-library/react",
2305
+ "@testing-library/jest-dom",
2306
+ "@testing-library/dom",
2307
+ "vite",
2308
+ "jsdom",
2309
+ "tailwindcss",
2310
+ "@tailwindcss/postcss",
2311
+ "postcss",
2312
+ "autoprefixer",
2313
+ "@playwright/test",
2314
+ "playwright",
2315
+ "@prerenderer/renderer-jsdom",
2316
+ "@prerenderer/renderer-puppeteer",
2317
+ "@prerenderer/rollup-plugin",
2318
+ "eventsource"
2319
+ ];
2320
+ var CLIENT_TYPE_PACKAGES = ["@types/react", "@types/react-dom"];
2023
2321
  function filterPackageJson(pkg, resolved) {
2024
2322
  const result = { ...pkg };
2025
2323
  const packagesToRemove = new Set(UNUSED_PACKAGES);
2324
+ const packageToModules = {};
2026
2325
  for (const [module, packages] of Object.entries(MODULE_PACKAGES)) {
2027
- if (!resolved.modules.has(module)) {
2028
- for (const pkg2 of packages) {
2029
- packagesToRemove.add(pkg2);
2030
- }
2326
+ for (const pkg2 of packages) {
2327
+ if (!packageToModules[pkg2]) packageToModules[pkg2] = [];
2328
+ packageToModules[pkg2].push(module);
2329
+ }
2330
+ }
2331
+ for (const [pkg2, modules] of Object.entries(packageToModules)) {
2332
+ if (!modules.some((m) => resolved.modules.has(m))) {
2333
+ packagesToRemove.add(pkg2);
2031
2334
  }
2032
2335
  }
2033
2336
  if (!resolved.modules.has("admin")) {
@@ -2035,8 +2338,11 @@ function filterPackageJson(pkg, resolved) {
2035
2338
  packagesToRemove.add(pkg2);
2036
2339
  }
2037
2340
  }
2038
- if (!resolved.modules.has("admin")) {
2039
- for (const pkg2 of CLI_PACKAGES) {
2341
+ for (const pkg2 of CLI_PACKAGES) {
2342
+ packagesToRemove.add(pkg2);
2343
+ }
2344
+ if (!resolved.hasClient) {
2345
+ for (const pkg2 of CLIENT_PACKAGES) {
2040
2346
  packagesToRemove.add(pkg2);
2041
2347
  }
2042
2348
  }
@@ -2052,8 +2358,37 @@ function filterPackageJson(pkg, resolved) {
2052
2358
  if (!resolved.modules.has("admin")) {
2053
2359
  delete devDeps["@testing-library/user-event"];
2054
2360
  }
2361
+ if (!resolved.hasClient) {
2362
+ for (const pkg2 of CLIENT_DEV_PACKAGES) {
2363
+ delete devDeps[pkg2];
2364
+ }
2365
+ for (const pkg2 of CLIENT_TYPE_PACKAGES) {
2366
+ delete devDeps[pkg2];
2367
+ }
2368
+ }
2055
2369
  result.devDependencies = devDeps;
2056
2370
  }
2371
+ if (!resolved.hasClient && result.scripts && typeof result.scripts === "object") {
2372
+ const scripts = { ...result.scripts };
2373
+ scripts["dev"] = "NODE_ENV=development node --import tsx src/server/entries/node.ts";
2374
+ scripts["build"] = "npm run build:server && npm run build:cli";
2375
+ scripts["build:all"] = "npm run build:server && npm run build:cli";
2376
+ delete scripts["build:client"];
2377
+ delete scripts["build:cloudflare"];
2378
+ delete scripts["preview"];
2379
+ delete scripts["dev:todo"];
2380
+ delete scripts["dev:plugin"];
2381
+ delete scripts["dev:ecommerce"];
2382
+ delete scripts["dev:community"];
2383
+ delete scripts["dev:saas"];
2384
+ delete scripts["dev:cf"];
2385
+ delete scripts["deploy:cf"];
2386
+ delete scripts["test:e2e"];
2387
+ delete scripts["test:e2e:ui"];
2388
+ delete scripts["test:e2e:debug"];
2389
+ delete scripts["test:full"];
2390
+ result.scripts = scripts;
2391
+ }
2057
2392
  return result;
2058
2393
  }
2059
2394
  function generateViteConfig(resolved, templateDir) {
@@ -2068,9 +2403,594 @@ function generateViteConfig(resolved, templateDir) {
2068
2403
  return content;
2069
2404
  }
2070
2405
 
2406
+ // src/generators/client-preset-ui-config.ts
2407
+ function getPresetType(presetId) {
2408
+ const map = {
2409
+ "todo-app": "todo",
2410
+ "xbrowser-marketplace": "plugin",
2411
+ ecommerce: "ecommerce",
2412
+ "fullstack-admin": "saas",
2413
+ minimal: "todo"
2414
+ };
2415
+ return map[presetId] || "todo";
2416
+ }
2417
+ function getThemeForPresetType(presetType) {
2418
+ const themes = {
2419
+ todo: {
2420
+ constName: "TODO_THEME",
2421
+ theme: `{
2422
+ primaryColor: '#6366f1',
2423
+ primaryHover: '#4f46e5',
2424
+ bgColor: '#ffffff',
2425
+ textColor: '#111827',
2426
+ secondaryBg: '#f9fafb',
2427
+ borderColor: '#e5e7eb',
2428
+ borderRadius: '12px',
2429
+ logoText: 'Biomimic',
2430
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2431
+ }`
2432
+ },
2433
+ plugin: {
2434
+ constName: "PLUGIN_MARKET_THEME",
2435
+ theme: `{
2436
+ primaryColor: '#3b82f6',
2437
+ primaryHover: '#2563eb',
2438
+ bgColor: '#ffffff',
2439
+ textColor: '#111827',
2440
+ secondaryBg: '#f0f9ff',
2441
+ borderColor: '#bae6fd',
2442
+ borderRadius: '12px',
2443
+ logoText: 'PluginHub',
2444
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2445
+ }`
2446
+ },
2447
+ ecommerce: {
2448
+ constName: "ECOMMERCE_THEME",
2449
+ theme: `{
2450
+ primaryColor: '#f59e0b',
2451
+ primaryHover: '#d97706',
2452
+ bgColor: '#ffffff',
2453
+ textColor: '#111827',
2454
+ secondaryBg: '#fffbeb',
2455
+ borderColor: '#fde68a',
2456
+ borderRadius: '12px',
2457
+ logoText: 'ShopMart',
2458
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2459
+ }`
2460
+ },
2461
+ saas: {
2462
+ constName: "SAAS_ADMIN_THEME",
2463
+ theme: `{
2464
+ primaryColor: '#1f2937',
2465
+ primaryHover: '#374151',
2466
+ bgColor: '#f9fafb',
2467
+ textColor: '#111827',
2468
+ secondaryBg: '#f3f4f6',
2469
+ borderColor: '#e5e7eb',
2470
+ borderRadius: '8px',
2471
+ logoText: 'AdminPanel',
2472
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2473
+ }`
2474
+ }
2475
+ };
2476
+ return themes[presetType] || themes.todo;
2477
+ }
2478
+ function getRoutesForPreset(presetType, resolved) {
2479
+ const hasModule = (m) => resolved.modules.has(m);
2480
+ const loginRoute = {
2481
+ path: "/login",
2482
+ importPath: "./pages/LoginPage",
2483
+ componentName: "LoginPage",
2484
+ label: "Login"
2485
+ };
2486
+ const registerRoute = {
2487
+ path: "/register",
2488
+ importPath: "./pages/RegisterPage",
2489
+ componentName: "RegisterPage",
2490
+ label: "Register"
2491
+ };
2492
+ const maybeAuthRoutes = hasModule("auth") ? [
2493
+ loginRoute,
2494
+ registerRoute,
2495
+ {
2496
+ path: "/profile",
2497
+ importPath: "./pages/ProfilePage",
2498
+ componentName: "ProfilePage",
2499
+ label: "Profile"
2500
+ }
2501
+ ] : [];
2502
+ switch (presetType) {
2503
+ case "todo": {
2504
+ const routes = [
2505
+ ...maybeAuthRoutes,
2506
+ {
2507
+ path: "/todos",
2508
+ importPath: "./pages/TodoPage",
2509
+ componentName: "TodoPage",
2510
+ label: "Todos"
2511
+ }
2512
+ ];
2513
+ if (hasModule("notifications")) {
2514
+ routes.push({
2515
+ path: "/notifications",
2516
+ importPath: "./pages/NotificationPage",
2517
+ componentName: "NotificationPage",
2518
+ label: "Notifications"
2519
+ });
2520
+ }
2521
+ if (hasModule("chat")) {
2522
+ routes.push({
2523
+ path: "/websocket",
2524
+ importPath: "./pages/WebSocketPage",
2525
+ componentName: "WebSocketPage",
2526
+ label: "WebSocket"
2527
+ });
2528
+ }
2529
+ return routes;
2530
+ }
2531
+ case "plugin": {
2532
+ const routes = [];
2533
+ if (hasModule("plugin")) {
2534
+ routes.push(
2535
+ {
2536
+ path: "/",
2537
+ importPath: "./pages/PluginsPage",
2538
+ componentName: "PluginsPage",
2539
+ label: "Home"
2540
+ },
2541
+ {
2542
+ path: "/plugins",
2543
+ importPath: "./pages/PluginsPage",
2544
+ componentName: "PluginsPage",
2545
+ label: "Plugins"
2546
+ },
2547
+ {
2548
+ path: "/plugins/:slug",
2549
+ importPath: "./pages/PluginDetailPage",
2550
+ componentName: "PluginDetailPage",
2551
+ label: "Plugin Detail"
2552
+ },
2553
+ {
2554
+ path: "/categories",
2555
+ importPath: "./pages/CategoriesPage",
2556
+ componentName: "CategoriesPage",
2557
+ label: "Categories"
2558
+ },
2559
+ {
2560
+ path: "/search",
2561
+ importPath: "./pages/SearchPage",
2562
+ componentName: "SearchPage",
2563
+ label: "Search"
2564
+ },
2565
+ {
2566
+ path: "/publish",
2567
+ importPath: "./pages/PublishPage",
2568
+ componentName: "PublishPage",
2569
+ label: "Publish"
2570
+ },
2571
+ {
2572
+ path: "/developer",
2573
+ importPath: "./pages/DeveloperDashboardPage",
2574
+ componentName: "DeveloperDashboardPage",
2575
+ label: "Developer"
2576
+ }
2577
+ );
2578
+ }
2579
+ routes.push(...maybeAuthRoutes);
2580
+ if (hasModule("notifications")) {
2581
+ routes.push({
2582
+ path: "/notifications",
2583
+ importPath: "./pages/NotificationPage",
2584
+ componentName: "NotificationPage",
2585
+ label: "Notifications"
2586
+ });
2587
+ }
2588
+ return routes;
2589
+ }
2590
+ case "ecommerce": {
2591
+ const routes = [];
2592
+ if (hasModule("content")) {
2593
+ routes.push(
2594
+ {
2595
+ path: "/",
2596
+ importPath: "./pages/ContentListPage",
2597
+ componentName: "ContentListPage",
2598
+ label: "Home"
2599
+ },
2600
+ {
2601
+ path: "/products",
2602
+ importPath: "./pages/ContentListPage",
2603
+ componentName: "ContentListPage",
2604
+ label: "Products"
2605
+ },
2606
+ {
2607
+ path: "/products/:id",
2608
+ importPath: "./pages/ContentDetailPage",
2609
+ componentName: "ContentDetailPage",
2610
+ label: "Product Detail"
2611
+ },
2612
+ {
2613
+ path: "/content",
2614
+ importPath: "./pages/ContentListPage",
2615
+ componentName: "ContentListPage",
2616
+ label: "Content"
2617
+ },
2618
+ {
2619
+ path: "/content/:id",
2620
+ importPath: "./pages/ContentDetailPage",
2621
+ componentName: "ContentDetailPage",
2622
+ label: "Content Detail"
2623
+ }
2624
+ );
2625
+ }
2626
+ if (hasModule("order")) {
2627
+ routes.push(
2628
+ {
2629
+ path: "/cart",
2630
+ importPath: "./pages/CartPage",
2631
+ componentName: "CartPage",
2632
+ label: "Cart"
2633
+ },
2634
+ {
2635
+ path: "/orders",
2636
+ importPath: "./pages/OrdersPage",
2637
+ componentName: "OrdersPage",
2638
+ label: "Orders"
2639
+ }
2640
+ );
2641
+ }
2642
+ routes.push(...maybeAuthRoutes);
2643
+ return routes;
2644
+ }
2645
+ case "saas": {
2646
+ const routes = [];
2647
+ if (hasModule("admin")) {
2648
+ routes.push(
2649
+ {
2650
+ path: "/dashboard",
2651
+ importPath: "./pages/DashboardPage",
2652
+ componentName: "DashboardPage",
2653
+ label: "Dashboard"
2654
+ },
2655
+ {
2656
+ path: "/settings",
2657
+ importPath: "./pages/SettingsPage",
2658
+ componentName: "SettingsPage",
2659
+ label: "Settings"
2660
+ }
2661
+ );
2662
+ }
2663
+ routes.push(...maybeAuthRoutes);
2664
+ return routes;
2665
+ }
2666
+ default:
2667
+ return [...maybeAuthRoutes];
2668
+ }
2669
+ }
2670
+ function getNavConfigForPreset(presetType, hasAuth) {
2671
+ switch (presetType) {
2672
+ case "todo":
2673
+ return {
2674
+ name: "Todo App",
2675
+ appType: "client",
2676
+ layout: "top-nav",
2677
+ navigationObj: hasAuth ? "{ visible: true, showLogo: true, showSearch: false, showCart: false, authStyle: 'buttons', navItems: 'desktop' }" : "{ visible: true, showLogo: true, showSearch: false, showCart: false, authStyle: 'none', navItems: 'desktop' }",
2678
+ desktopNav: [
2679
+ "{ label: 'Todos', icon: 'CheckSquare', path: '/todos' }",
2680
+ "{ label: 'SSE Demo', icon: 'Bell', path: '/notifications' }",
2681
+ "{ label: 'WebSocket', icon: 'Zap', path: '/websocket' }"
2682
+ ],
2683
+ mobileTabs: [
2684
+ "{ label: 'Todos', icon: 'CheckSquare', path: '/todos' }",
2685
+ "{ label: 'SSE', icon: 'Bell', path: '/notifications' }",
2686
+ "{ label: 'WS', icon: 'Zap', path: '/websocket' }"
2687
+ ],
2688
+ defaultRoute: "/todos"
2689
+ };
2690
+ case "plugin":
2691
+ return {
2692
+ name: "Plugin Market",
2693
+ appType: "client",
2694
+ layout: "top-nav",
2695
+ navigationObj: "{ visible: true, showLogo: true, showSearch: true, showCart: false, authStyle: 'text-link', navItems: 'desktop' }",
2696
+ desktopNav: [
2697
+ "{ label: 'Discover', icon: 'Compass', path: '/plugins' }",
2698
+ "{ label: 'Plugins', icon: 'Puzzle', path: '/plugins/list' }",
2699
+ "{ label: 'Categories', icon: 'Tags', path: '/categories' }",
2700
+ "{ label: 'Search', icon: 'Search', path: '/search' }",
2701
+ "{ label: 'Publish', icon: 'PlusCircle', path: '/publish' }",
2702
+ "{ label: 'Developer', icon: 'Code', path: '/developer' }"
2703
+ ],
2704
+ mobileTabs: [
2705
+ "{ label: 'Discover', icon: 'Compass', path: '/plugins' }",
2706
+ "{ label: 'Plugins', icon: 'Puzzle', path: '/plugins/list' }",
2707
+ "{ label: 'Categories', icon: 'Tags', path: '/categories' }",
2708
+ "{ label: 'Search', icon: 'Search', path: '/search' }",
2709
+ "{ label: 'My', icon: 'User', path: '/developer' }"
2710
+ ],
2711
+ defaultRoute: "/plugins"
2712
+ };
2713
+ case "ecommerce":
2714
+ return {
2715
+ name: "E-Commerce",
2716
+ appType: "client",
2717
+ layout: "top-nav",
2718
+ navigationObj: "{ visible: true, showLogo: true, showSearch: true, showCart: true, authStyle: 'icon', navItems: 'desktop' }",
2719
+ desktopNav: [
2720
+ "{ label: 'Home', icon: 'Home', path: '/' }",
2721
+ "{ label: 'Products', icon: 'ShoppingBag', path: '/products' }",
2722
+ "{ label: 'Cart', icon: 'ShoppingCart', path: '/cart' }",
2723
+ "{ label: 'Orders', icon: 'Package', path: '/orders' }",
2724
+ "{ label: 'Account', icon: 'User', path: '/content' }"
2725
+ ],
2726
+ mobileTabs: [
2727
+ "{ label: 'Home', icon: 'Home', path: '/' }",
2728
+ "{ label: 'Products', icon: 'ShoppingBag', path: '/products' }",
2729
+ "{ label: 'Cart', icon: 'ShoppingCart', path: '/cart' }",
2730
+ "{ label: 'Orders', icon: 'Package', path: '/orders' }",
2731
+ "{ label: 'Me', icon: 'User', path: '/content' }"
2732
+ ],
2733
+ defaultRoute: "/"
2734
+ };
2735
+ case "saas":
2736
+ return {
2737
+ name: "SaaS Admin",
2738
+ appType: "admin",
2739
+ layout: "minimal",
2740
+ navigationObj: "{ visible: false, showLogo: false, showSearch: false, showCart: false, authStyle: 'none', navItems: 'none' }",
2741
+ desktopNav: [
2742
+ "{ label: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }",
2743
+ "{ label: 'Settings', icon: 'Settings', path: '/settings' }"
2744
+ ],
2745
+ mobileTabs: [
2746
+ "{ label: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }",
2747
+ "{ label: 'Settings', icon: 'Settings', path: '/settings' }"
2748
+ ],
2749
+ defaultRoute: "/dashboard"
2750
+ };
2751
+ default:
2752
+ return {
2753
+ name: "App",
2754
+ appType: "client",
2755
+ layout: "top-nav",
2756
+ navigationObj: "{ visible: true, showLogo: true, showSearch: false, showCart: false, authStyle: 'none', navItems: 'desktop' }",
2757
+ desktopNav: ["{ label: 'Home', icon: 'Home', path: '/' }"],
2758
+ mobileTabs: ["{ label: 'Home', icon: 'Home', path: '/' }"],
2759
+ defaultRoute: "/"
2760
+ };
2761
+ }
2762
+ }
2763
+ function filterNavByModules(navItems, resolved) {
2764
+ const hasModule = (path3) => {
2765
+ if (path3 === "/todos") return resolved.modules.has("todos");
2766
+ if (path3 === "/notifications") return resolved.modules.has("notifications");
2767
+ if (path3 === "/websocket") return resolved.modules.has("chat");
2768
+ if (path3.startsWith("/plugins") || path3 === "/categories" || path3 === "/search" || path3 === "/publish" || path3 === "/developer")
2769
+ return resolved.modules.has("plugin");
2770
+ if (path3 === "/cart" || path3 === "/orders") return resolved.modules.has("order");
2771
+ if (path3.startsWith("/content") || path3 === "/products" || path3 === "/")
2772
+ return resolved.modules.has("content");
2773
+ if (path3 === "/topics" || path3 === "/popular" || path3 === "/profile")
2774
+ return resolved.modules.has("content");
2775
+ if (path3 === "/dashboard" || path3 === "/settings") return resolved.modules.has("admin");
2776
+ return true;
2777
+ };
2778
+ return navItems.filter((item) => {
2779
+ const pathMatch = item.match(/path:\s*'([^']+)'/);
2780
+ if (!pathMatch) return true;
2781
+ return hasModule(pathMatch[1]);
2782
+ });
2783
+ }
2784
+ function generatePresetUIConfig(resolved, presetId) {
2785
+ const presetType = getPresetType(presetId);
2786
+ const { constName, theme } = getThemeForPresetType(presetType);
2787
+ const hasAuth = resolved.modules.has("auth");
2788
+ const navConfig = getNavConfigForPreset(presetType, hasAuth);
2789
+ const routes = getRoutesForPreset(presetType, resolved);
2790
+ const desktopNav = filterNavByModules(navConfig.desktopNav, resolved);
2791
+ const mobileTabs = filterNavByModules(navConfig.mobileTabs, resolved);
2792
+ const routeDefs = routes.map((r) => {
2793
+ return ` {
2794
+ path: '${r.path}',
2795
+ component: lazy(() => import('${r.importPath}').then(m => ({ default: m.${r.componentName} }))),
2796
+ label: '${r.label}',
2797
+ }`;
2798
+ });
2799
+ return `import { lazy, type ComponentType } from 'react'
2800
+
2801
+ export type PresetType = '${presetType}'
2802
+
2803
+ export type AppType = 'client' | 'admin'
2804
+ export type LayoutType = 'top-nav' | 'minimal'
2805
+ export type AuthStyle = 'buttons' | 'text-link' | 'icon' | 'avatar' | 'none'
2806
+
2807
+ // ClientNavItem is intentionally simpler than admin MenuItem (no permissions/children needed for client nav)
2808
+ // eslint-disable-next-line local-rules/prefer-shared-types
2809
+ export interface ClientNavItem {
2810
+ label: string
2811
+ icon: string
2812
+ path: string
2813
+ }
2814
+
2815
+ export type TabItem = ClientNavItem
2816
+
2817
+ export interface NavigationConfig {
2818
+ visible: boolean
2819
+ showLogo: boolean
2820
+ showSearch: boolean
2821
+ showCart: boolean
2822
+ authStyle: AuthStyle
2823
+ navItems: 'desktop' | 'none'
2824
+ }
2825
+
2826
+ export interface PresetTheme {
2827
+ primaryColor: string
2828
+ primaryHover: string
2829
+ bgColor: string
2830
+ textColor: string
2831
+ secondaryBg: string
2832
+ borderColor: string
2833
+ borderRadius: string
2834
+ logoText: string
2835
+ fontFamily: string
2836
+ }
2837
+
2838
+ export interface RouteDef {
2839
+ path: string
2840
+ component: ComponentType<Record<string, unknown>> | null
2841
+ label: string
2842
+ }
2843
+
2844
+ export interface PresetUIConfig {
2845
+ id: PresetType
2846
+ name: string
2847
+ appType: AppType
2848
+ layout: LayoutType
2849
+ theme: PresetTheme
2850
+ navigation: NavigationConfig
2851
+ desktopNav: ClientNavItem[]
2852
+ mobileTabs: ClientNavItem[]
2853
+ routes: RouteDef[]
2854
+ defaultRoute: string
2855
+ }
2856
+
2857
+ const ${constName}: PresetTheme = ${theme}
2858
+
2859
+ export const PRESET_UI_CONFIGS: Record<PresetType, PresetUIConfig> = {
2860
+ ${presetType}: {
2861
+ id: '${presetType}',
2862
+ name: '${navConfig.name}',
2863
+ appType: '${navConfig.appType}',
2864
+ layout: '${navConfig.layout}',
2865
+ theme: ${constName},
2866
+ navigation: ${navConfig.navigationObj},
2867
+ desktopNav: [
2868
+ ${desktopNav.map((i) => ` ${i}`).join(",\n")}
2869
+ ],
2870
+ mobileTabs: [
2871
+ ${mobileTabs.map((i) => ` ${i}`).join(",\n")}
2872
+ ],
2873
+ defaultRoute: '${navConfig.defaultRoute}',
2874
+ routes: [
2875
+ ${routeDefs.join(",\n")}
2876
+ ],
2877
+ },
2878
+ }
2879
+
2880
+ export function getPresetUIConfig(id: string): PresetUIConfig {
2881
+ return PRESET_UI_CONFIGS[id as PresetType] ?? PRESET_UI_CONFIGS[Object.keys(PRESET_UI_CONFIGS)[0] as PresetType]
2882
+ }
2883
+
2884
+ export function getPresetUIConfigs(): Record<PresetType, PresetUIConfig> {
2885
+ return PRESET_UI_CONFIGS
2886
+ }
2887
+ `;
2888
+ }
2889
+
2890
+ // src/generators/client-main.ts
2891
+ function getPresetType2(presetId) {
2892
+ const map = {
2893
+ "todo-app": "todo",
2894
+ "xbrowser-marketplace": "plugin",
2895
+ ecommerce: "ecommerce",
2896
+ "fullstack-admin": "saas",
2897
+ minimal: "todo"
2898
+ };
2899
+ return map[presetId] || "todo";
2900
+ }
2901
+ function generateClientMain(resolved, presetId) {
2902
+ const presetType = getPresetType2(presetId);
2903
+ const isSaas = presetType === "saas";
2904
+ const authTokenBlock = isSaas ? "" : `
2905
+ if (preset !== 'saas') {
2906
+ try {
2907
+ const raw = localStorage.getItem('auth-token')
2908
+ const parsed = raw ? JSON.parse(raw) : null
2909
+ if (!parsed?.state?.token) {
2910
+ localStorage.setItem('auth-token', JSON.stringify({
2911
+ state: {
2912
+ token: 'user-token',
2913
+ isAuthenticated: true,
2914
+ user: { id: 'user-1', username: 'Demo User', role: 'USER' },
2915
+ loading: false,
2916
+ error: null,
2917
+ },
2918
+ version: 0,
2919
+ }))
2920
+ }
2921
+ } catch {
2922
+ localStorage.setItem('auth-token', JSON.stringify({
2923
+ state: { token: 'user-token', isAuthenticated: true, user: { id: 'user-1', username: 'Demo User', role: 'USER' }, loading: false, error: null },
2924
+ version: 0,
2925
+ }))
2926
+ }
2927
+ }
2928
+ `;
2929
+ if (isSaas) {
2930
+ return `import React from 'react'
2931
+ import ReactDOM from 'react-dom/client'
2932
+ import './index.css'
2933
+
2934
+ const AdminApp = React.lazy(() => import('@admin/App').then(m => ({ default: m.App })))
2935
+
2936
+ const RootApp = () => {
2937
+ return (
2938
+ <React.Suspense fallback={<div className="flex items-center justify-center h-screen text-gray-400">Loading...</div>}>
2939
+ <AdminApp basePath="/" />
2940
+ </React.Suspense>
2941
+ )
2942
+ }
2943
+
2944
+ ReactDOM.createRoot(document.getElementById('root')!).render(
2945
+ <React.StrictMode>
2946
+ <RootApp />
2947
+ </React.StrictMode>,
2948
+ )
2949
+
2950
+ if (typeof window !== 'undefined') {
2951
+ requestAnimationFrame(() => {
2952
+ setTimeout(() => {
2953
+ document.dispatchEvent(new CustomEvent('prerender-ready'))
2954
+ }, 100)
2955
+ })
2956
+ }
2957
+ `;
2958
+ }
2959
+ return `import React from 'react'
2960
+ import ReactDOM from 'react-dom/client'
2961
+ import { HelmetProvider } from 'react-helmet-async'
2962
+ import { App as ClientApp } from './App'
2963
+ import './index.css'
2964
+
2965
+ const preset = import.meta.env.VITE_PRESET || '${presetType}'
2966
+ ${authTokenBlock}
2967
+ const RootApp = () => {
2968
+ return (
2969
+ <HelmetProvider>
2970
+ <ClientApp presetId={preset} />
2971
+ </HelmetProvider>
2972
+ )
2973
+ }
2974
+
2975
+ ReactDOM.createRoot(document.getElementById('root')!).render(
2976
+ <React.StrictMode>
2977
+ <RootApp />
2978
+ </React.StrictMode>,
2979
+ )
2980
+
2981
+ if (typeof window !== 'undefined') {
2982
+ requestAnimationFrame(() => {
2983
+ setTimeout(() => {
2984
+ document.dispatchEvent(new CustomEvent('prerender-ready'))
2985
+ }, 100)
2986
+ })
2987
+ }
2988
+ `;
2989
+ }
2990
+
2071
2991
  // src/commands/create.ts
2072
2992
  var __filename$1 = fileURLToPath(import.meta.url);
2073
- var __dirname$1 = path2.dirname(__filename$1);
2993
+ var __dirname$1 = path.dirname(__filename$1);
2074
2994
  var TEMPLATE_PROJECT_NAME = "biomimic-todo-app";
2075
2995
  var TEMPLATE_DB_NAME = "biomimic-todo-db";
2076
2996
  var ScaffoldError = class extends Error {
@@ -2112,11 +3032,11 @@ function generateDbName(projectName) {
2112
3032
  return `${sanitized}-db`;
2113
3033
  }
2114
3034
  async function updateWranglerToml(targetDir, projectName) {
2115
- const wranglerPath = path2.join(targetDir, "wrangler.toml");
2116
- if (!await fs2.pathExists(wranglerPath)) {
3035
+ const wranglerPath = path.join(targetDir, "wrangler.toml");
3036
+ if (!await fs.pathExists(wranglerPath)) {
2117
3037
  return;
2118
3038
  }
2119
- let content = await fs2.readFile(wranglerPath, "utf-8");
3039
+ let content = await fs.readFile(wranglerPath, "utf-8");
2120
3040
  const dbName = generateDbName(projectName);
2121
3041
  content = content.replace(
2122
3042
  new RegExp(`^name = "${TEMPLATE_PROJECT_NAME}"`, "m"),
@@ -2130,43 +3050,43 @@ async function updateWranglerToml(targetDir, projectName) {
2130
3050
  /database_id = "[^"]+"/,
2131
3051
  `database_id = "" # TODO: Run 'wrangler d1 create ${dbName}' and paste the ID here`
2132
3052
  );
2133
- await fs2.writeFile(wranglerPath, content);
3053
+ await fs.writeFile(wranglerPath, content);
2134
3054
  }
2135
3055
  async function updatePackageJson(targetDir, projectName, resolved) {
2136
- const pkgJsonPath = path2.join(targetDir, "package.json");
2137
- if (!await fs2.pathExists(pkgJsonPath)) {
3056
+ const pkgJsonPath = path.join(targetDir, "package.json");
3057
+ if (!await fs.pathExists(pkgJsonPath)) {
2138
3058
  return;
2139
3059
  }
2140
- let pkgJson = await fs2.readJson(pkgJsonPath);
3060
+ let pkgJson = await fs.readJson(pkgJsonPath);
2141
3061
  pkgJson = filterPackageJson(pkgJson, resolved);
2142
3062
  pkgJson.name = projectName;
2143
3063
  if (pkgJson.bin) {
2144
3064
  delete pkgJson.bin;
2145
3065
  }
2146
- await fs2.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
3066
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2147
3067
  }
2148
3068
  async function updatePackageLockJson(targetDir, projectName) {
2149
- const lockFilePath = path2.join(targetDir, "package-lock.json");
2150
- if (!await fs2.pathExists(lockFilePath)) {
3069
+ const lockFilePath = path.join(targetDir, "package-lock.json");
3070
+ if (!await fs.pathExists(lockFilePath)) {
2151
3071
  return;
2152
3072
  }
2153
- const lockFile = await fs2.readJson(lockFilePath);
3073
+ const lockFile = await fs.readJson(lockFilePath);
2154
3074
  if (lockFile.name === TEMPLATE_PROJECT_NAME) {
2155
3075
  lockFile.name = projectName;
2156
3076
  }
2157
3077
  if (lockFile.packages?.[""]?.name === TEMPLATE_PROJECT_NAME) {
2158
3078
  lockFile.packages[""].name = projectName;
2159
3079
  }
2160
- await fs2.writeJson(lockFilePath, lockFile, { spaces: 2 });
3080
+ await fs.writeJson(lockFilePath, lockFile, { spaces: 2 });
2161
3081
  }
2162
3082
  async function updateReadme(targetDir, projectName) {
2163
- const readmePath = path2.join(targetDir, "README.md");
2164
- if (!await fs2.pathExists(readmePath)) {
3083
+ const readmePath = path.join(targetDir, "README.md");
3084
+ if (!await fs.pathExists(readmePath)) {
2165
3085
  return;
2166
3086
  }
2167
- let content = await fs2.readFile(readmePath, "utf-8");
3087
+ let content = await fs.readFile(readmePath, "utf-8");
2168
3088
  content = content.replace(/^# (.+)$/m, `# ${projectName}`);
2169
- await fs2.writeFile(readmePath, content);
3089
+ await fs.writeFile(readmePath, content);
2170
3090
  }
2171
3091
  async function createProject(projectNameOrOptions, useCurrentDir = false, preset) {
2172
3092
  let projectName;
@@ -2189,19 +3109,19 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2189
3109
  if (!currentDir) {
2190
3110
  validateProjectName(projectName);
2191
3111
  }
2192
- const templateDir = path2.join(__dirname$1, "../../template");
3112
+ const templateDir = path.join(__dirname$1, "../../template");
2193
3113
  let targetDir;
2194
3114
  if (currentDir) {
2195
3115
  targetDir = process.cwd();
2196
- projectName = path2.basename(targetDir);
3116
+ projectName = path.basename(targetDir);
2197
3117
  } else if (outputDir) {
2198
- targetDir = path2.resolve(outputDir);
2199
- if (await fs2.pathExists(targetDir)) {
3118
+ targetDir = path.resolve(outputDir);
3119
+ if (await fs.pathExists(targetDir)) {
2200
3120
  throw new ScaffoldError(`Directory ${outputDir} already exists`);
2201
3121
  }
2202
3122
  } else {
2203
- targetDir = path2.resolve(process.cwd(), projectName);
2204
- if (await fs2.pathExists(targetDir)) {
3123
+ targetDir = path.resolve(process.cwd(), projectName);
3124
+ if (await fs.pathExists(targetDir)) {
2205
3125
  throw new ScaffoldError(`Directory ${projectName} already exists`);
2206
3126
  }
2207
3127
  }
@@ -2228,16 +3148,16 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2228
3148
  for (const file of generatedFiles2) {
2229
3149
  console.log(` ${chalk.green("\u2713")} ${file}`);
2230
3150
  }
2231
- const gitignorePath2 = path2.join(templateDir, ".gitignore");
3151
+ const gitignorePath2 = path.join(templateDir, ".gitignore");
2232
3152
  let ignorePatterns2 = [];
2233
- if (await fs2.pathExists(gitignorePath2)) {
2234
- const gitignoreContent = await fs2.readFile(gitignorePath2, "utf-8");
3153
+ if (await fs.pathExists(gitignorePath2)) {
3154
+ const gitignoreContent = await fs.readFile(gitignorePath2, "utf-8");
2235
3155
  ignorePatterns2 = parseGitignore(gitignoreContent);
2236
3156
  }
2237
3157
  ignorePatterns2.push("node_modules", ".wrangler");
2238
3158
  const excludePatterns2 = getExcludePatterns(resolved, allManifests);
2239
3159
  let templateFileCount = 0;
2240
- const templateFiles = await fs2.readdir(templateDir, { recursive: true });
3160
+ const templateFiles = await fs.readdir(templateDir, { recursive: true });
2241
3161
  for (const file of templateFiles) {
2242
3162
  const relative = String(file);
2243
3163
  if (!relative) continue;
@@ -2272,21 +3192,21 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2272
3192
  }
2273
3193
  if (!currentDir) {
2274
3194
  const dirSpinner = ora("Creating project directory...").start();
2275
- await fs2.ensureDir(targetDir);
3195
+ await fs.ensureDir(targetDir);
2276
3196
  dirSpinner.succeed(chalk.green("Project directory created"));
2277
3197
  }
2278
3198
  const copySpinner = ora("Copying template files...").start();
2279
- const gitignorePath = path2.join(templateDir, ".gitignore");
3199
+ const gitignorePath = path.join(templateDir, ".gitignore");
2280
3200
  let ignorePatterns = [];
2281
- if (await fs2.pathExists(gitignorePath)) {
2282
- const gitignoreContent = await fs2.readFile(gitignorePath, "utf-8");
3201
+ if (await fs.pathExists(gitignorePath)) {
3202
+ const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
2283
3203
  ignorePatterns = parseGitignore(gitignoreContent);
2284
3204
  }
2285
3205
  ignorePatterns.push("node_modules", ".wrangler");
2286
3206
  const excludePatterns = getExcludePatterns(resolved, allManifests);
2287
- await fs2.copy(templateDir, targetDir, {
3207
+ await fs.copy(templateDir, targetDir, {
2288
3208
  filter: (src) => {
2289
- const relative = path2.relative(templateDir, src);
3209
+ const relative = path.relative(templateDir, src);
2290
3210
  if (relative === "") return true;
2291
3211
  const negated = ignorePatterns.filter((p) => p.startsWith("!"));
2292
3212
  const gitIgnored = ignorePatterns.filter((p) => !p.startsWith("!") && relative.startsWith(p));
@@ -2308,62 +3228,80 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2308
3228
  copySpinner.succeed(chalk.green("Template files copied"));
2309
3229
  const genSpinner = ora("Generating module-specific files...").start();
2310
3230
  const routeRegistryContent = generateRouteRegistry(resolved);
2311
- await fs2.writeFile(path2.join(targetDir, "src/server/route-registry.ts"), routeRegistryContent);
3231
+ await fs.writeFile(path.join(targetDir, "src/server/route-registry.ts"), routeRegistryContent);
2312
3232
  const dbSchemaContent = generateDbSchemaBarrel(resolved);
2313
- await fs2.writeFile(path2.join(targetDir, "src/server/db/schema/index.ts"), dbSchemaContent);
2314
- const clientAppContent = generateClientApp(resolved);
2315
- await fs2.writeFile(path2.join(targetDir, "src/client/App.tsx"), clientAppContent);
2316
- const clientNavContent = generateClientNavigation(resolved);
2317
- await fs2.writeFile(
2318
- path2.join(targetDir, "src/client/components/Navigation.tsx"),
2319
- clientNavContent
2320
- );
2321
- if (resolved.modules.has("admin")) {
3233
+ await fs.writeFile(path.join(targetDir, "src/server/db/schema/index.ts"), dbSchemaContent);
3234
+ if (resolved.hasClient) {
3235
+ const clientNavContent = generateClientNavigation(resolved);
3236
+ await fs.writeFile(
3237
+ path.join(targetDir, "src/client/components/Navigation.tsx"),
3238
+ clientNavContent
3239
+ );
3240
+ const clientAppTestContent = generateClientAppTest(resolved);
3241
+ await fs.ensureDir(path.join(targetDir, "src/client/components/__tests__"));
3242
+ await fs.writeFile(
3243
+ path.join(targetDir, "src/client/components/__tests__/App.test.tsx"),
3244
+ clientAppTestContent
3245
+ );
3246
+ const clientNavTestContent = generateClientNavigationTest(resolved);
3247
+ await fs.writeFile(
3248
+ path.join(targetDir, "src/client/components/__tests__/Navigation.test.tsx"),
3249
+ clientNavTestContent
3250
+ );
3251
+ const presetUIConfigContent = generatePresetUIConfig(resolved, selectedPreset.id);
3252
+ await fs.writeFile(
3253
+ path.join(targetDir, "src/client/preset-ui-config.ts"),
3254
+ presetUIConfigContent
3255
+ );
3256
+ const clientMainContent = generateClientMain(resolved, selectedPreset.id);
3257
+ await fs.writeFile(path.join(targetDir, "src/client/main.tsx"), clientMainContent);
3258
+ }
3259
+ if (resolved.hasClient && resolved.modules.has("admin")) {
2322
3260
  const adminAppContent = generateAdminApp(resolved);
2323
3261
  if (adminAppContent) {
2324
- await fs2.ensureDir(path2.join(targetDir, "src/admin"));
2325
- await fs2.writeFile(path2.join(targetDir, "src/admin/App.tsx"), adminAppContent);
3262
+ await fs.ensureDir(path.join(targetDir, "src/admin"));
3263
+ await fs.writeFile(path.join(targetDir, "src/admin/App.tsx"), adminAppContent);
2326
3264
  }
2327
3265
  }
2328
3266
  const serverAppContent = generateServerApp(resolved);
2329
- await fs2.writeFile(path2.join(targetDir, "src/server/app.ts"), serverAppContent);
3267
+ await fs.writeFile(path.join(targetDir, "src/server/app.ts"), serverAppContent);
2330
3268
  const generatedFiles = getGeneratedFiles(resolved);
2331
3269
  if (generatedFiles.includes("src/server/db/init.ts")) {
2332
3270
  const dbInitContent = generateDbInit(resolved);
2333
- await fs2.writeFile(path2.join(targetDir, "src/server/db/init.ts"), dbInitContent);
3271
+ await fs.writeFile(path.join(targetDir, "src/server/db/init.ts"), dbInitContent);
2334
3272
  }
2335
3273
  const sharedModulesContent = generateSharedModulesIndex(resolved);
2336
- await fs2.writeFile(path2.join(targetDir, "src/shared/modules/index.ts"), sharedModulesContent);
3274
+ await fs.writeFile(path.join(targetDir, "src/shared/modules/index.ts"), sharedModulesContent);
2337
3275
  const sharedSchemasContent = generateSharedSchemasIndex(resolved);
2338
- await fs2.writeFile(path2.join(targetDir, "src/shared/schemas/index.ts"), sharedSchemasContent);
3276
+ await fs.writeFile(path.join(targetDir, "src/shared/schemas/index.ts"), sharedSchemasContent);
2339
3277
  const middlewareIndexContent = generateMiddlewareIndex(resolved);
2340
- await fs2.writeFile(
2341
- path2.join(targetDir, "src/server/middleware/index.ts"),
3278
+ await fs.writeFile(
3279
+ path.join(targetDir, "src/server/middleware/index.ts"),
2342
3280
  middlewareIndexContent
2343
3281
  );
2344
3282
  if (generatedFiles.includes("src/server/middleware/auth.ts")) {
2345
3283
  const authMiddlewareContent = generateAuthMiddleware(resolved);
2346
- await fs2.writeFile(
2347
- path2.join(targetDir, "src/server/middleware/auth.ts"),
3284
+ await fs.writeFile(
3285
+ path.join(targetDir, "src/server/middleware/auth.ts"),
2348
3286
  authMiddlewareContent
2349
3287
  );
2350
3288
  }
2351
3289
  if (generatedFiles.includes("src/server/utils/auth.ts")) {
2352
3290
  const authUtilsContent = generateAuthUtils(resolved);
2353
- await fs2.writeFile(path2.join(targetDir, "src/server/utils/auth.ts"), authUtilsContent);
3291
+ await fs.writeFile(path.join(targetDir, "src/server/utils/auth.ts"), authUtilsContent);
2354
3292
  }
2355
- const clientComponentsContent = generateClientComponentsIndex(resolved);
2356
- await fs2.writeFile(
2357
- path2.join(targetDir, "src/client/components/index.ts"),
2358
- clientComponentsContent
2359
- );
2360
- if (resolved.modules.has("admin")) {
2361
- const cliModulesContent = generateCliModulesIndex(resolved);
2362
- await fs2.writeFile(path2.join(targetDir, "src/cli/modules/index.ts"), cliModulesContent);
3293
+ if (resolved.hasClient) {
3294
+ const clientComponentsContent = generateClientComponentsIndex(resolved);
3295
+ await fs.writeFile(
3296
+ path.join(targetDir, "src/client/components/index.ts"),
3297
+ clientComponentsContent
3298
+ );
2363
3299
  }
2364
- if (generatedFiles.includes("vite.config.ts")) {
3300
+ const cliModulesContent = generateCliModulesIndex(resolved);
3301
+ await fs.writeFile(path.join(targetDir, "src/cli/modules/index.ts"), cliModulesContent);
3302
+ if (resolved.hasClient && generatedFiles.includes("vite.config.ts")) {
2365
3303
  const viteConfigContent = generateViteConfig(resolved, templateDir);
2366
- await fs2.writeFile(path2.join(targetDir, "vite.config.ts"), viteConfigContent);
3304
+ await fs.writeFile(path.join(targetDir, "vite.config.ts"), viteConfigContent);
2367
3305
  }
2368
3306
  genSpinner.succeed(chalk.green("Module-specific files generated"));
2369
3307
  const pkgSpinner = ora("Configuring package.json...").start();
@@ -2388,14 +3326,24 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2388
3326
  console.log(chalk.white(` cd ${projectName}`));
2389
3327
  }
2390
3328
  console.log(chalk.white(" npm install"));
2391
- console.log(chalk.white(" npm run dev"));
2392
- console.log("");
2393
- console.log(chalk.yellow(" \u26A0\uFE0F Cloudflare Setup:"));
2394
- console.log(
2395
- chalk.white(` 1. Create D1 database: wrangler d1 create ${generateDbName(projectName)}`)
2396
- );
2397
- console.log(chalk.white(" 2. Copy the database ID to wrangler.toml"));
2398
- console.log(chalk.white(" 3. Deploy: npm run deploy:cf"));
3329
+ if (resolved.hasClient) {
3330
+ console.log(chalk.white(" npm run dev"));
3331
+ console.log("");
3332
+ console.log(chalk.yellow(" \u26A0\uFE0F Cloudflare Setup:"));
3333
+ console.log(
3334
+ chalk.white(` 1. Create D1 database: wrangler d1 create ${generateDbName(projectName)}`)
3335
+ );
3336
+ console.log(chalk.white(" 2. Copy the database ID to wrangler.toml"));
3337
+ console.log(chalk.white(" 3. Deploy: npm run deploy:cf"));
3338
+ } else {
3339
+ console.log(chalk.white(" npm run build"));
3340
+ console.log(chalk.white(" npm start"));
3341
+ console.log("");
3342
+ console.log(chalk.cyan(" CLI usage:"));
3343
+ console.log(chalk.white(" node dist/cli/index.js config status"));
3344
+ console.log(chalk.white(" node dist/cli/index.js todo list"));
3345
+ console.log(chalk.white(" node dist/cli/index.js --help"));
3346
+ }
2399
3347
  console.log("");
2400
3348
  console.log(chalk.gray(" Happy coding! \u{1F41F}"));
2401
3349
  console.log("");
@@ -2405,37 +3353,66 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
2405
3353
  }
2406
3354
  }
2407
3355
 
2408
- // src/cli/index.ts
2409
- var MODULE_COMMANDS = ["todo", "notification", "config"];
2410
- program.name("create-fullstack-scaffold").description("Create a new fullstack scaffolded project").version("0.1.1").argument("[project-name]", "Name of the project to create").option("-v, --verbose", "Enable verbose output").option("-u, --url <url>", "Server URL", "http://localhost:3010").option("-p, --preset <preset>", "Template preset to use", "fullstack-admin").option("-d, --dry-run", "Preview files without creating them").option("--current-dir", "Scaffold in the current directory").action(async (projectName, cmdOptions) => {
2411
- if (projectName && !MODULE_COMMANDS.includes(projectName)) {
2412
- const isCurrentDir = cmdOptions.currentDir === true || projectName === ".";
2413
- createLogger({ verbose: cmdOptions.verbose });
2414
- const options = {
2415
- projectName: isCurrentDir ? "." : projectName,
2416
- currentDir: isCurrentDir,
2417
- preset: cmdOptions.preset,
2418
- dryRun: cmdOptions.dryRun
2419
- };
2420
- await createProject(options).catch((err) => {
2421
- console.error(`Error: ${err.message}`);
2422
- process.exit(1);
2423
- });
2424
- return;
2425
- }
2426
- if (!projectName) {
2427
- program.outputHelp();
2428
- process.exit(1);
3356
+ // src/index.ts
3357
+ var __filename2 = fileURLToPath(import.meta.url);
3358
+ var __dirname2 = path.dirname(__filename2);
3359
+ var rootDir = __dirname2.endsWith(path.join("src")) || __dirname2.endsWith(path.join("dist")) ? path.resolve(__dirname2, "..") : path.resolve(__dirname2, "..", "..");
3360
+ var packageJson = JSON.parse(readFileSync(path.join(rootDir, "package.json"), "utf-8"));
3361
+ var program = new Command();
3362
+ program.name("create-fullstack-scaffold").description("Create a new full-stack scaffold app with Todo List example").version(packageJson.version).argument("[project-name]", "Name of your project").option("-c, --current-dir", "Create project in current directory").option(
3363
+ "-p, --preset <preset>",
3364
+ "Template preset to use (fullstack-admin, todo-app, cli-only, minimal)"
3365
+ ).option("-o, --output-dir <path>", "Output directory (defaults to project name)").option("--dry-run", "Show what would be generated without creating files").action(
3366
+ async (projectName = "my-fullstack-app", options) => {
3367
+ console.log("");
3368
+ console.log(chalk.cyan.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
3369
+ console.log(chalk.cyan.bold(" \u2551 Create Fullstack Scaffold App \u2551"));
3370
+ console.log(chalk.cyan.bold(" \u2551 React + Hono + Vite + Zustand + TS \u2551"));
3371
+ console.log(chalk.cyan.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
3372
+ console.log("");
3373
+ let preset = options.preset;
3374
+ if (!preset && process.stdin.isTTY) {
3375
+ const templateDir = path.join(__dirname2, "../template");
3376
+ const presets = await loadPresets(templateDir);
3377
+ preset = await select({
3378
+ message: "Choose a template preset:",
3379
+ choices: presets.map((p) => ({
3380
+ value: p.id,
3381
+ name: `${p.name} \u2014 ${p.description}`
3382
+ }))
3383
+ });
3384
+ }
3385
+ if (!preset) {
3386
+ preset = "fullstack-admin";
3387
+ }
3388
+ try {
3389
+ await createProject({
3390
+ projectName,
3391
+ currentDir: options.currentDir ?? false,
3392
+ preset,
3393
+ outputDir: options.outputDir,
3394
+ dryRun: options.dryRun ?? false
3395
+ });
3396
+ } catch (error) {
3397
+ if (error instanceof ScaffoldError) {
3398
+ console.error(chalk.red(` \u2716 ${error.message}`));
3399
+ process.exit(1);
3400
+ }
3401
+ throw error;
3402
+ }
2429
3403
  }
2430
- });
2431
- program.hook("preAction", (thisCommand) => {
2432
- const options = thisCommand.opts();
2433
- createLogger({ verbose: options.verbose });
2434
- if (options.url) {
2435
- setBaseUrl(options.url);
3404
+ );
3405
+ program.command("presets").description("List available template presets").action(async () => {
3406
+ const templateDir = path.join(__dirname2, "../template");
3407
+ const presets = await loadPresets(templateDir);
3408
+ console.log(chalk.cyan("\nAvailable presets:\n"));
3409
+ for (const preset of presets) {
3410
+ console.log(` ${chalk.green(preset.id.padEnd(20))} ${preset.name}`);
3411
+ console.log(` ${" ".repeat(20)} ${preset.description}`);
3412
+ console.log(` ${" ".repeat(20)} Modules: ${preset.modules.join(", ")}`);
3413
+ console.log();
2436
3414
  }
2437
3415
  });
2438
- registerModules(program);
2439
3416
  program.parse();
2440
3417
  //# sourceMappingURL=index.js.map
2441
3418
  //# sourceMappingURL=index.js.map