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
@@ -1,86 +1,102 @@
1
- import { Command } from 'commander'
1
+ import type { SiteInstance } from '@dyyz1993/xcli-core'
2
+ import { ok, fail } from '@dyyz1993/xcli-core'
3
+ import { z } from 'zod'
2
4
  import { getClient } from '@cli/utils/api'
3
- import { getLogger } from '@cli/utils/logger'
4
5
 
5
- export function registerNotificationCommands(program: Command) {
6
- const notification = program
7
- .command('notification')
8
- .description('Notification management commands')
6
+ export function registerNotificationCommands(site: SiteInstance) {
7
+ site.command('list', {
8
+ description: 'List all notifications',
9
+ parameters: z.object({
10
+ 'unread-only': z.boolean().default(false).describe('Show only unread'),
11
+ limit: z.coerce.number().default(20).describe('Limit results'),
12
+ }),
13
+ handler: async (params: unknown) => {
14
+ const p = params as { 'unread-only': boolean; limit: number }
15
+ try {
16
+ const client = getClient()
17
+ const res = await client.api.notifications.$get({
18
+ query: { unreadOnly: String(p['unread-only']), limit: String(p.limit) },
19
+ })
20
+ const data = await res.json()
21
+ return ok(data)
22
+ } catch (err) {
23
+ return fail(err instanceof Error ? err.message : 'Failed to list notifications')
24
+ }
25
+ },
26
+ })
9
27
 
10
- notification
11
- .command('list')
12
- .description('List all notifications')
13
- .option('--unread-only', 'Show only unread notifications')
14
- .option('--limit <number>', 'Limit number of results', '20')
15
- .action(async (options: { unreadOnly?: boolean; limit?: string }) => {
16
- const logger = getLogger()
17
- const client = getClient()
18
- const unreadOnly = Boolean(options.unreadOnly)
19
- const limit = parseInt(options.limit || '20')
28
+ site.command('create', {
29
+ description: 'Create a new notification',
30
+ parameters: z.object({
31
+ title: z.string().min(1).describe('Notification title'),
32
+ message: z.string().min(1).describe('Notification message'),
33
+ type: z.enum(['info', 'warning', 'success', 'error']).default('info').describe('Type'),
34
+ }),
35
+ handler: async (params: unknown) => {
36
+ const p = params as {
37
+ title: string
38
+ message: string
39
+ type: 'info' | 'warning' | 'success' | 'error'
40
+ }
41
+ try {
42
+ const client = getClient()
43
+ const res = await client.api.notifications.$post({ json: p })
44
+ const data = await res.json()
45
+ return ok(data, ['Notification created'])
46
+ } catch (err) {
47
+ return fail(err instanceof Error ? err.message : 'Failed to create notification')
48
+ }
49
+ },
50
+ })
20
51
 
21
- const res = await client.api.notifications.$get({
22
- query: { unreadOnly: String(unreadOnly), limit: String(limit) },
23
- })
24
- const data = await res.json()
25
- logger.info(JSON.stringify(data, null, 2))
26
- })
52
+ site.command('unread-count', {
53
+ description: 'Get unread notification count',
54
+ parameters: z.object({}),
55
+ handler: async () => {
56
+ try {
57
+ const client = getClient()
58
+ const res = await client.api.notifications['unread-count'].$get()
59
+ const data = await res.json()
60
+ return ok(data)
61
+ } catch (err) {
62
+ return fail(err instanceof Error ? err.message : 'Failed to get unread count')
63
+ }
64
+ },
65
+ })
27
66
 
28
- notification
29
- .command('create')
30
- .description('Create a new notification')
31
- .requiredOption('-t, --title <title>', 'Notification title')
32
- .requiredOption('-m, --message <message>', 'Notification message')
33
- .option('--type <type>', 'Notification type (info|warning|success|error)', 'info')
34
- .action(async (options: { title: string; message: string; type: string }) => {
35
- const logger = getLogger()
36
- const client = getClient()
67
+ site.command('mark-read', {
68
+ description: 'Mark a notification as read',
69
+ parameters: z.object({
70
+ id: z.string().describe('Notification ID'),
71
+ }),
72
+ handler: async (params: unknown) => {
73
+ const p = params as { id: string }
74
+ try {
75
+ const client = getClient()
76
+ const res = await client.api.notifications[':id'].read.$patch({ param: { id: p.id } })
77
+ const data = await res.json()
78
+ return ok(data, ['Marked as read'])
79
+ } catch (err) {
80
+ return fail(err instanceof Error ? err.message : 'Failed to mark as read')
81
+ }
82
+ },
83
+ })
37
84
 
38
- const res = await client.api.notifications.$post({
39
- json: {
40
- type: options.type as 'info' | 'warning' | 'success' | 'error',
41
- title: options.title,
42
- message: options.message,
43
- },
44
- })
45
- const data = await res.json()
46
- logger.success('Notification created')
47
- logger.info(JSON.stringify(data, null, 2))
48
- })
49
-
50
- notification
51
- .command('unread-count')
52
- .description('Get unread notification count')
53
- .action(async () => {
54
- const logger = getLogger()
55
- const client = getClient()
56
- const res = await client.api.notifications['unread-count'].$get()
57
- const data = await res.json()
58
- logger.info(JSON.stringify(data, null, 2))
59
- })
60
-
61
- notification
62
- .command('mark-read')
63
- .description('Mark a notification as read')
64
- .argument('<id>', 'Notification ID')
65
- .action(async (id: string) => {
66
- const logger = getLogger()
67
- const client = getClient()
68
- const res = await client.api.notifications[':id'].read.$patch({ param: { id } })
69
- const data = await res.json()
70
- logger.success('Notification marked as read')
71
- logger.info(JSON.stringify(data, null, 2))
72
- })
73
-
74
- notification
75
- .command('delete')
76
- .description('Delete a notification')
77
- .argument('<id>', 'Notification ID')
78
- .action(async (id: string) => {
79
- const logger = getLogger()
80
- const client = getClient()
81
- const res = await client.api.notifications[':id'].$delete({ param: { id } })
82
- const data = await res.json()
83
- logger.success('Notification deleted')
84
- logger.info(JSON.stringify(data, null, 2))
85
- })
85
+ site.command('delete', {
86
+ description: 'Delete a notification',
87
+ parameters: z.object({
88
+ id: z.string().describe('Notification ID'),
89
+ }),
90
+ handler: async (params: unknown) => {
91
+ const p = params as { id: string }
92
+ try {
93
+ const client = getClient()
94
+ const res = await client.api.notifications[':id'].$delete({ param: { id: p.id } })
95
+ const data = await res.json()
96
+ return ok(data, ['Notification deleted'])
97
+ } catch (err) {
98
+ return fail(err instanceof Error ? err.message : 'Failed to delete notification')
99
+ }
100
+ },
101
+ })
86
102
  }
@@ -0,0 +1,111 @@
1
+ import type { SiteInstance } from '@dyyz1993/xcli-core'
2
+ import { ok, fail } from '@dyyz1993/xcli-core'
3
+ import { z } from 'zod'
4
+ import { getClient } from '@cli/utils/api'
5
+
6
+ export function registerPluginCommands(site: SiteInstance) {
7
+ site.command('list', {
8
+ description: 'List all plugins',
9
+ parameters: z.object({
10
+ limit: z.coerce.number().default(20).describe('Limit results'),
11
+ page: z.coerce.number().default(1).describe('Page number'),
12
+ }),
13
+ handler: async (params: unknown) => {
14
+ const p = params as { limit: number; page: number }
15
+ try {
16
+ const client = getClient()
17
+ const res = await client.api.plugins.$get({
18
+ query: { limit: String(p.limit), page: String(p.page) },
19
+ })
20
+ const data = await res.json()
21
+ return ok(data)
22
+ } catch (err) {
23
+ return fail(err instanceof Error ? err.message : 'Failed to list plugins')
24
+ }
25
+ },
26
+ })
27
+
28
+ site.command('search', {
29
+ description: 'Search plugins',
30
+ parameters: z.object({
31
+ query: z.string().min(1).describe('Search query'),
32
+ }),
33
+ handler: async (params: unknown) => {
34
+ const p = params as { query: string }
35
+ try {
36
+ const client = getClient()
37
+ const res = await client.api.plugins.search.$get({ query: { q: p.query } })
38
+ const data = await res.json()
39
+ return ok(data)
40
+ } catch (err) {
41
+ return fail(err instanceof Error ? err.message : 'Failed to search plugins')
42
+ }
43
+ },
44
+ })
45
+
46
+ site.command('get', {
47
+ description: 'Get plugin details',
48
+ parameters: z.object({
49
+ slug: z.string().describe('Plugin slug'),
50
+ }),
51
+ handler: async (params: unknown) => {
52
+ const p = params as { slug: string }
53
+ try {
54
+ const client = getClient()
55
+ const res = await client.api.plugins[':slug'].$get({ param: { slug: p.slug } })
56
+ const data = await res.json()
57
+ return ok(data)
58
+ } catch (err) {
59
+ return fail(err instanceof Error ? err.message : 'Failed to get plugin')
60
+ }
61
+ },
62
+ })
63
+
64
+ site.command('install', {
65
+ description: 'Track plugin install',
66
+ parameters: z.object({
67
+ slug: z.string().describe('Plugin slug'),
68
+ }),
69
+ handler: async (params: unknown) => {
70
+ const p = params as { slug: string }
71
+ try {
72
+ const client = getClient()
73
+ const res = await client.api.plugins[':slug'].install.$post({ param: { slug: p.slug } })
74
+ const data = await res.json()
75
+ return ok(data, ['Install tracked'])
76
+ } catch (err) {
77
+ return fail(err instanceof Error ? err.message : 'Failed to track install')
78
+ }
79
+ },
80
+ })
81
+
82
+ site.command('categories', {
83
+ description: 'List plugin categories',
84
+ parameters: z.object({}),
85
+ handler: async () => {
86
+ try {
87
+ const client = getClient()
88
+ const res = await client.api.categories.$get()
89
+ const data = await res.json()
90
+ return ok(data)
91
+ } catch (err) {
92
+ return fail(err instanceof Error ? err.message : 'Failed to list categories')
93
+ }
94
+ },
95
+ })
96
+
97
+ site.command('stats', {
98
+ description: 'Show marketplace statistics',
99
+ parameters: z.object({}),
100
+ handler: async () => {
101
+ try {
102
+ const client = getClient()
103
+ const res = await client.api.stats.$get()
104
+ const data = await res.json()
105
+ return ok(data)
106
+ } catch (err) {
107
+ return fail(err instanceof Error ? err.message : 'Failed to get stats')
108
+ }
109
+ },
110
+ })
111
+ }
@@ -1,60 +1,108 @@
1
- import { Command } from 'commander'
2
- import { registerAutoCommand, type RouteConfig } from '@cli/utils/auto-command'
3
- import { z } from '@hono/zod-openapi'
1
+ import type { SiteInstance } from '@dyyz1993/xcli-core'
2
+ import { ok, fail } from '@dyyz1993/xcli-core'
3
+ import { z } from 'zod'
4
+ import { getClient } from '@cli/utils/api'
4
5
 
5
- const TodoStatusSchema = z.enum(['pending', 'in_progress', 'completed'])
6
-
7
- const todoRoutes: RouteConfig[] = [
8
- {
9
- method: 'get',
10
- path: '/todos',
11
- command: 'list',
6
+ export function registerTodoCommands(site: SiteInstance) {
7
+ site.command('list', {
12
8
  description: 'List all todos',
13
- },
14
- {
15
- method: 'get',
16
- path: '/todos/{id}',
17
- command: 'get',
9
+ parameters: z.object({
10
+ limit: z.coerce.number().default(20).describe('Limit results'),
11
+ }),
12
+ handler: async () => {
13
+ try {
14
+ const client = getClient()
15
+ const res = await client.api.todos.$get()
16
+ const data = await res.json()
17
+ return ok(data)
18
+ } catch (err) {
19
+ return fail(err instanceof Error ? err.message : 'Failed to list todos')
20
+ }
21
+ },
22
+ })
23
+
24
+ site.command('get', {
18
25
  description: 'Get a todo by ID',
19
- params: z.object({ id: z.string() }),
20
- },
21
- {
22
- method: 'post',
23
- path: '/todos',
24
- command: 'create',
26
+ parameters: z.object({
27
+ id: z.string().describe('Todo ID'),
28
+ }),
29
+ handler: async (params: unknown) => {
30
+ const p = params as { id: string }
31
+ try {
32
+ const client = getClient()
33
+ const res = await client.api.todos[':id'].$get({ param: { id: p.id } })
34
+ const data = await res.json()
35
+ return ok(data)
36
+ } catch (err) {
37
+ return fail(err instanceof Error ? err.message : 'Failed to get todo')
38
+ }
39
+ },
40
+ })
41
+
42
+ site.command('create', {
25
43
  description: 'Create a new todo',
26
- body: z.object({
27
- title: z.string().min(1),
28
- description: z.string().optional(),
44
+ parameters: z.object({
45
+ title: z.string().min(1).describe('Todo title'),
46
+ description: z.string().optional().describe('Todo description'),
29
47
  }),
30
- },
31
- {
32
- method: 'put',
33
- path: '/todos/{id}',
34
- command: 'update',
48
+ handler: async (params: unknown) => {
49
+ const p = params as { title: string; description?: string }
50
+ try {
51
+ const client = getClient()
52
+ const res = await client.api.todos.$post({ json: p })
53
+ const data = await res.json()
54
+ return ok(data, ['Todo created'])
55
+ } catch (err) {
56
+ return fail(err instanceof Error ? err.message : 'Failed to create todo')
57
+ }
58
+ },
59
+ })
60
+
61
+ site.command('update', {
35
62
  description: 'Update a todo',
36
- params: z.object({ id: z.string() }),
37
- body: z.object({
38
- title: z.string().optional(),
39
- description: z.string().optional(),
40
- status: TodoStatusSchema.optional(),
63
+ parameters: z.object({
64
+ id: z.string().describe('Todo ID'),
65
+ title: z.string().optional().describe('New title'),
66
+ description: z.string().optional().describe('New description'),
67
+ status: z.enum(['pending', 'in_progress', 'completed']).optional().describe('New status'),
41
68
  }),
42
- },
43
- {
44
- method: 'delete',
45
- path: '/todos/{id}',
46
- command: 'delete',
47
- description: 'Delete a todo',
48
- params: z.object({ id: z.string() }),
49
- },
50
- ]
51
-
52
- export function registerTodoCommands(program: Command) {
53
- const todo = program.command('todo').description('Todo management commands')
69
+ handler: async (params: unknown) => {
70
+ const p = params as {
71
+ id: string
72
+ title?: string
73
+ description?: string
74
+ status?: 'pending' | 'in_progress' | 'completed'
75
+ }
76
+ try {
77
+ const { id, ...body } = p
78
+ const client = getClient()
79
+ const res = await client.api.todos[':id'].$put({
80
+ param: { id },
81
+ json: body,
82
+ })
83
+ const data = await res.json()
84
+ return ok(data, ['Todo updated'])
85
+ } catch (err) {
86
+ return fail(err instanceof Error ? err.message : 'Failed to update todo')
87
+ }
88
+ },
89
+ })
54
90
 
55
- for (const route of todoRoutes) {
56
- registerAutoCommand(todo, route)
57
- }
91
+ site.command('delete', {
92
+ description: 'Delete a todo',
93
+ parameters: z.object({
94
+ id: z.string().describe('Todo ID'),
95
+ }),
96
+ handler: async (params: unknown) => {
97
+ const p = params as { id: string }
98
+ try {
99
+ const client = getClient()
100
+ const res = await client.api.todos[':id'].$delete({ param: { id: p.id } })
101
+ const data = await res.json()
102
+ return ok(data, ['Todo deleted'])
103
+ } catch (err) {
104
+ return fail(err instanceof Error ? err.message : 'Failed to delete todo')
105
+ }
106
+ },
107
+ })
58
108
  }
59
-
60
- export { todoRoutes }
@@ -1,4 +1,7 @@
1
- import { Command } from 'commander'
1
+ // NOTE: This file is preserved for Phase 2 RPC auto-mapping migration.
2
+ // The registerAutoCommand function will be rewritten to use xcli-core's site.command().
3
+ // The utility functions (extractZodInfo, createCommandFromRoute, etc.) remain usable.
4
+
2
5
  import type { ZodType, ZodObject, ZodOptional } from 'zod'
3
6
  import { getClient } from './api'
4
7
  import { getLogger } from './logger'
@@ -201,29 +204,8 @@ export function createCommandFromRoute(config: RouteConfig): CliCommandConfig {
201
204
  }
202
205
  }
203
206
 
204
- export function registerAutoCommand(program: Command, config: RouteConfig) {
205
- const cmd = createCommandFromRoute(config)
206
-
207
- const command = program
208
- .command(cmd.name)
209
- .description(cmd.description)
210
- .action(async (firstArg, opts) => {
211
- const args = typeof firstArg === 'string' ? [firstArg] : []
212
- const options = (typeof firstArg === 'object' ? firstArg : opts) || {}
213
- await cmd.action(options, args)
214
- })
215
-
216
- cmd.options?.forEach(opt => {
217
- if (opt.required) {
218
- command.requiredOption(opt.flags, opt.description, opt.defaultValue)
219
- } else {
220
- command.option(opt.flags, opt.description, opt.defaultValue)
221
- }
222
- })
223
-
224
- cmd.arguments?.forEach(arg => {
225
- command.argument(arg.required ? `<${arg.name}>` : `[${arg.name}]`, arg.description)
226
- })
227
- }
207
+ // NOTE: registerAutoCommand was removed during xcli-core migration (Phase 1).
208
+ // It will be replaced with a xcli-core version in Phase 2 (RPC auto-mapping).
228
209
 
229
210
  export { type RouteConfig, type CliCommandConfig }
211
+ export { extractZodInfo, schemaToOptions, pathToApiCall }
@@ -1,8 +1,10 @@
1
1
  export { createLogger, getLogger, Logger, type LogLevel, type LoggerOptions } from './logger'
2
2
  export { setBaseUrl, getBaseUrl, getClient } from './api'
3
3
  export {
4
- registerAutoCommand,
5
4
  createCommandFromRoute,
5
+ extractZodInfo,
6
+ schemaToOptions,
7
+ pathToApiCall,
6
8
  type RouteConfig,
7
9
  type CliCommandConfig,
8
10
  } from './auto-command'
@@ -1,24 +1,44 @@
1
1
  import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
2
2
  import { Layout } from './Layout'
3
- import { TodoPage } from './pages/TodoPage'
4
- import { NotificationPage } from './pages/NotificationPage'
5
- import { WebSocketPage } from './pages/WebSocketPage'
6
- import { ContentListPage } from './pages/ContentListPage'
7
- import { ContentDetailPage } from './pages/ContentDetailPage'
3
+ import { PresetProvider } from './contexts/PresetContext'
4
+ import { getPresetUIConfig, type RouteDef } from './preset-ui-config'
5
+
6
+ export const App: React.FC<{ presetId?: string }> = ({ presetId = 'todo' }) => {
7
+ const config = getPresetUIConfig(presetId)
8
+ const { theme, desktopNav, mobileTabs, defaultRoute, routes, layout, navigation } = config
8
9
 
9
- export const App: React.FC = () => {
10
10
  return (
11
11
  <BrowserRouter>
12
- <Layout>
13
- <Routes>
14
- <Route path="/" element={<Navigate to="/todos" replace />} />
15
- <Route path="/todos" element={<TodoPage />} />
16
- <Route path="/notifications" element={<NotificationPage />} />
17
- <Route path="/websocket" element={<WebSocketPage />} />
18
- <Route path="/content" element={<ContentListPage />} />
19
- <Route path="/content/:id" element={<ContentDetailPage />} />
20
- </Routes>
21
- </Layout>
12
+ <PresetProvider value={presetId}>
13
+ <Layout
14
+ preset={presetId}
15
+ layout={layout}
16
+ theme={theme}
17
+ navigation={navigation}
18
+ desktopNav={desktopNav}
19
+ mobileTabs={mobileTabs}
20
+ >
21
+ <Routes>
22
+ <Route path="/" element={<Navigate to={defaultRoute} replace />} />
23
+ {routes
24
+ .filter(
25
+ (r): r is RouteDef & { component: NonNullable<RouteDef['component']> } =>
26
+ r.component !== null
27
+ )
28
+ .map(route => (
29
+ <Route key={route.path} path={route.path} element={<route.component />} />
30
+ ))}
31
+ <Route
32
+ path="*"
33
+ element={
34
+ <div className="flex items-center justify-center min-h-[50vh] text-gray-400">
35
+ 404 - Page not found
36
+ </div>
37
+ }
38
+ />
39
+ </Routes>
40
+ </Layout>
41
+ </PresetProvider>
22
42
  </BrowserRouter>
23
43
  )
24
44
  }
@@ -1,27 +1,84 @@
1
1
  import { ReactNode } from 'react'
2
2
  import { Navigation } from './components/Navigation'
3
3
  import { Footer } from './components/Footer'
4
+ import { BottomTabBar } from './components/BottomTabBar'
5
+ import type {
6
+ PresetTheme,
7
+ ClientNavItem,
8
+ TabItem,
9
+ LayoutType,
10
+ NavigationConfig,
11
+ } from './preset-ui-config'
12
+
13
+ export type PresetType = string
4
14
 
5
15
  interface LayoutProps {
6
16
  children: ReactNode
7
17
  className?: string
8
- showNavigation?: boolean
9
- showFooter?: boolean
18
+ preset?: PresetType
19
+ layout?: LayoutType
20
+ theme?: PresetTheme
21
+ navigation?: NavigationConfig
22
+ desktopNav?: ClientNavItem[]
23
+ mobileTabs?: TabItem[]
10
24
  }
11
25
 
12
26
  export const Layout: React.FC<LayoutProps> = ({
13
27
  children,
14
- className = '',
15
- showNavigation = true,
16
- showFooter = true,
28
+ className: classNameProp = '',
29
+ preset = 'todo',
30
+ layout = 'top-nav',
31
+ theme,
32
+ navigation,
33
+ desktopNav,
34
+ mobileTabs,
17
35
  }) => {
36
+ const cssVars = theme
37
+ ? ({
38
+ '--preset-primary': theme.primaryColor,
39
+ '--preset-primary-hover': theme.primaryHover,
40
+ '--preset-bg': theme.bgColor,
41
+ '--preset-text': theme.textColor,
42
+ '--preset-secondary-bg': theme.secondaryBg,
43
+ '--preset-border': theme.borderColor,
44
+ '--preset-radius': theme.borderRadius,
45
+ } as React.CSSProperties)
46
+ : undefined
47
+
48
+ const navVisible = navigation?.visible !== false
49
+ const showFooter = layout === 'top-nav'
50
+
18
51
  return (
19
- <div className={`min-h-screen bg-gray-50 ${className}`} data-testid="app-container">
20
- {showNavigation && <Navigation />}
21
- <main className="py-8" data-testid="app-main">
22
- {children}
23
- </main>
52
+ <div
53
+ className={`min-h-screen bg-white text-gray-900 ${classNameProp}`}
54
+ style={{
55
+ fontFamily:
56
+ theme?.fontFamily ?? "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
57
+ ...cssVars,
58
+ overflowX: 'hidden',
59
+ }}
60
+ data-testid="app-container"
61
+ >
62
+ {navVisible && layout === 'top-nav' && (
63
+ <Navigation preset={preset} items={desktopNav} theme={theme} navigation={navigation} />
64
+ )}
65
+
66
+ {layout === 'minimal' ? (
67
+ <main data-testid="app-main">{children}</main>
68
+ ) : (
69
+ <main
70
+ className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pb-24 md:pb-8"
71
+ data-testid="app-main"
72
+ >
73
+ {children}
74
+ </main>
75
+ )}
76
+
24
77
  {showFooter && <Footer />}
78
+
79
+ {mobileTabs && mobileTabs.length > 0 && layout !== 'minimal' && (
80
+ <BottomTabBar tabs={mobileTabs} theme={theme} />
81
+ )}
25
82
  </div>
26
83
  )
27
84
  }