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
@@ -0,0 +1,255 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import {
3
+ Card,
4
+ Button,
5
+ Space,
6
+ Tag,
7
+ Empty,
8
+ Spin,
9
+ Input,
10
+ Modal,
11
+ Descriptions,
12
+ message,
13
+ Popconfirm,
14
+ } from 'antd'
15
+ import { CheckCircle, XCircle, ExternalLink, ArrowRight } from 'lucide-react'
16
+ import { apiClient, api } from '../services/apiClient'
17
+ import type { Plugin } from '@shared/modules/plugins'
18
+
19
+ const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
20
+ pending: { color: 'orange', label: '待审核' },
21
+ approved: { color: 'green', label: '已通过' },
22
+ rejected: { color: 'red', label: '已拒绝' },
23
+ }
24
+
25
+ export const PluginReviewPage: React.FC = () => {
26
+ const [pending, setPending] = useState<Plugin[]>([])
27
+ const [loading, setLoading] = useState(false)
28
+ const [currentIndex, setCurrentIndex] = useState(0)
29
+ const [rejectModalVisible, setRejectModalVisible] = useState(false)
30
+ const [rejectReason, setRejectReason] = useState('')
31
+
32
+ const fetchPending = useCallback(async () => {
33
+ setLoading(true)
34
+ try {
35
+ const result = await api(
36
+ apiClient.api.plugins.$get({ query: { status: 'pending', limit: 50, page: 1 } })
37
+ )
38
+ .withLoading()
39
+ .json()
40
+ setPending(result.plugins)
41
+ setCurrentIndex(0)
42
+ } catch {
43
+ // handled by api-request
44
+ } finally {
45
+ setLoading(false)
46
+ }
47
+ }, [])
48
+
49
+ useEffect(() => {
50
+ fetchPending()
51
+ }, [fetchPending])
52
+
53
+ const current = pending[currentIndex]
54
+
55
+ const handleApprove = async () => {
56
+ if (!current) return
57
+ try {
58
+ await api(apiClient.api.plugins[':slug'].approve.$put({ param: { slug: current.slug } }))
59
+ .withLoading('审核通过中...')
60
+ .json()
61
+ message.success(`插件 "${current.name}" 已通过审核`)
62
+ setPending(prev => prev.filter(p => p.id !== current.id))
63
+ setCurrentIndex(prev => Math.min(prev, Math.max(0, pending.length - 2)))
64
+ } catch {
65
+ // handled
66
+ }
67
+ }
68
+
69
+ const openRejectModal = () => {
70
+ setRejectReason('')
71
+ setRejectModalVisible(true)
72
+ }
73
+
74
+ const handleReject = async () => {
75
+ if (!current || !rejectReason.trim()) {
76
+ message.warning('请填写拒绝原因')
77
+ return
78
+ }
79
+ try {
80
+ await api(
81
+ apiClient.api.plugins[':slug'].reject.$put({
82
+ param: { slug: current.slug },
83
+ json: { reason: rejectReason },
84
+ })
85
+ )
86
+ .withLoading('拒绝中...')
87
+ .json()
88
+ message.success(`插件 "${current.name}" 已拒绝`)
89
+ setPending(prev => prev.filter(p => p.id !== current.id))
90
+ setCurrentIndex(prev => Math.min(prev, Math.max(0, pending.length - 2)))
91
+ setRejectModalVisible(false)
92
+ } catch {
93
+ // handled
94
+ }
95
+ }
96
+
97
+ const goNext = () => setCurrentIndex(prev => Math.min(prev + 1, pending.length - 1))
98
+ const goPrev = () => setCurrentIndex(prev => Math.max(prev - 1, 0))
99
+
100
+ if (loading) {
101
+ return (
102
+ <div className="flex justify-center items-center h-64">
103
+ <Spin size="large" tip="加载待审核插件..." />
104
+ </div>
105
+ )
106
+ }
107
+
108
+ return (
109
+ <div>
110
+ <div className="flex justify-between items-center mb-6">
111
+ <div>
112
+ <h1 className="text-2xl font-bold text-gray-900">插件审核</h1>
113
+ <p className="text-gray-600 mt-1">
114
+ 待审核插件:{pending.length} 个
115
+ {pending.length > 0 && ` (${currentIndex + 1}/${pending.length})`}
116
+ </p>
117
+ </div>
118
+ <Button onClick={fetchPending}>刷新列表</Button>
119
+ </div>
120
+
121
+ {pending.length === 0 ? (
122
+ <Card>
123
+ <Empty description="没有待审核的插件" />
124
+ </Card>
125
+ ) : (
126
+ current && (
127
+ <Card
128
+ className="shadow-sm"
129
+ title={
130
+ <div className="flex items-center gap-3">
131
+ <span className="text-lg font-semibold">{current.name}</span>
132
+ <Tag color={STATUS_CONFIG[current.status]?.color}>
133
+ {STATUS_CONFIG[current.status]?.label}
134
+ </Tag>
135
+ {current.featured && <Tag color="gold">推荐</Tag>}
136
+ </div>
137
+ }
138
+ extra={
139
+ <Space>
140
+ <Button
141
+ onClick={goPrev}
142
+ disabled={currentIndex === 0}
143
+ icon={<ArrowRight className="w-4 h-4 rotate-180" />}
144
+ >
145
+ 上一个
146
+ </Button>
147
+ <Button
148
+ onClick={goNext}
149
+ disabled={currentIndex >= pending.length - 1}
150
+ icon={<ArrowRight className="w-4 h-4" />}
151
+ >
152
+ 下一个
153
+ </Button>
154
+ </Space>
155
+ }
156
+ >
157
+ <Descriptions bordered column={2}>
158
+ <Descriptions.Item label="Slug">
159
+ <code className="text-xs">{current.slug}</code>
160
+ </Descriptions.Item>
161
+ <Descriptions.Item label="作者">{current.authorName}</Descriptions.Item>
162
+ <Descriptions.Item label="版本">
163
+ <code className="text-xs">{current.version}</code>
164
+ </Descriptions.Item>
165
+ <Descriptions.Item label="下载量">{current.downloadCount}</Descriptions.Item>
166
+ <Descriptions.Item label="描述" span={2}>
167
+ {current.description}
168
+ </Descriptions.Item>
169
+ {current.repositoryUrl && (
170
+ <Descriptions.Item label="仓库地址">
171
+ <a
172
+ href={current.repositoryUrl}
173
+ target="_blank"
174
+ rel="noopener noreferrer"
175
+ className="flex items-center gap-1"
176
+ >
177
+ <ExternalLink className="w-3 h-3" /> {current.repositoryUrl}
178
+ </a>
179
+ </Descriptions.Item>
180
+ )}
181
+ {current.homepageUrl && (
182
+ <Descriptions.Item label="主页">
183
+ <a
184
+ href={current.homepageUrl}
185
+ target="_blank"
186
+ rel="noopener noreferrer"
187
+ className="flex items-center gap-1"
188
+ >
189
+ <ExternalLink className="w-3 h-3" /> {current.homepageUrl}
190
+ </a>
191
+ </Descriptions.Item>
192
+ )}
193
+ {current.license && (
194
+ <Descriptions.Item label="许可证">{current.license}</Descriptions.Item>
195
+ )}
196
+ {current.npmPackage && (
197
+ <Descriptions.Item label="NPM 包">
198
+ <code className="text-xs">{current.npmPackage}</code>
199
+ </Descriptions.Item>
200
+ )}
201
+ {current.tags && current.tags.length > 0 && (
202
+ <Descriptions.Item label="标签" span={2}>
203
+ {current.tags.map(tag => (
204
+ <Tag key={tag}>{tag}</Tag>
205
+ ))}
206
+ </Descriptions.Item>
207
+ )}
208
+ </Descriptions>
209
+
210
+ <div className="flex justify-end gap-3 mt-6 pt-4 border-t">
211
+ <Popconfirm
212
+ title="确定通过此插件审核?"
213
+ onConfirm={handleApprove}
214
+ okText="通过"
215
+ cancelText="取消"
216
+ >
217
+ <Button type="primary" icon={<CheckCircle className="w-4 h-4" />}>
218
+ 通过审核
219
+ </Button>
220
+ </Popconfirm>
221
+ <Button danger icon={<XCircle className="w-4 h-4" />} onClick={openRejectModal}>
222
+ 拒绝
223
+ </Button>
224
+ </div>
225
+ </Card>
226
+ )
227
+ )}
228
+
229
+ <Modal
230
+ title={`拒绝插件${current ? ` - ${current.name}` : ''}`}
231
+ open={rejectModalVisible}
232
+ onOk={handleReject}
233
+ onCancel={() => {
234
+ setRejectModalVisible(false)
235
+ setRejectReason('')
236
+ }}
237
+ okText="确认拒绝"
238
+ cancelText="取消"
239
+ okButtonProps={{ danger: true }}
240
+ >
241
+ <div className="mb-4">
242
+ <p className="text-sm text-gray-500 mb-2">请填写拒绝原因:</p>
243
+ <Input.TextArea
244
+ rows={4}
245
+ value={rejectReason}
246
+ onChange={e => setRejectReason(e.target.value)}
247
+ placeholder="请输入拒绝原因..."
248
+ maxLength={500}
249
+ showCount
250
+ />
251
+ </div>
252
+ </Modal>
253
+ </div>
254
+ )
255
+ }
@@ -29,6 +29,15 @@ export const SystemLogsPage: React.FC = () => {
29
29
  resourceType: '',
30
30
  })
31
31
 
32
+ const safeFormatJson = (val: string | null | undefined): string => {
33
+ if (!val) return '-'
34
+ try {
35
+ return JSON.stringify(JSON.parse(val), null, 2)
36
+ } catch {
37
+ return val
38
+ }
39
+ }
40
+
32
41
  useEffect(() => {
33
42
  fetchLogs()
34
43
  }, [fetchLogs])
@@ -199,14 +208,14 @@ export const SystemLogsPage: React.FC = () => {
199
208
  {selectedLog.oldValue && (
200
209
  <Descriptions.Item label="旧值">
201
210
  <pre style={{ margin: 0, maxHeight: '200px', overflow: 'auto' }}>
202
- {JSON.stringify(JSON.parse(selectedLog.oldValue), null, 2)}
211
+ {safeFormatJson(selectedLog.oldValue)}
203
212
  </pre>
204
213
  </Descriptions.Item>
205
214
  )}
206
215
  {selectedLog.newValue && (
207
216
  <Descriptions.Item label="新值">
208
217
  <pre style={{ margin: 0, maxHeight: '200px', overflow: 'auto' }}>
209
- {JSON.stringify(JSON.parse(selectedLog.newValue), null, 2)}
218
+ {safeFormatJson(selectedLog.newValue)}
210
219
  </pre>
211
220
  </Descriptions.Item>
212
221
  )}
@@ -53,6 +53,8 @@ export const TicketsPage: React.FC = () => {
53
53
  const result = await response.json()
54
54
  if (result.success) {
55
55
  setTickets(result.data)
56
+ } else {
57
+ message.error(result.error || 'Failed to load tickets')
56
58
  }
57
59
  } catch {
58
60
  message.error('获取工单列表失败')
@@ -137,12 +137,13 @@ export const UsersPage: React.FC = () => {
137
137
  }
138
138
 
139
139
  const getStatusTag = (status: string) => {
140
- const statusConfig = {
140
+ const statusConfig: Record<string, { color: string; text: string }> = {
141
141
  active: { color: 'green', text: '正常' },
142
142
  inactive: { color: 'orange', text: '未激活' },
143
143
  locked: { color: 'red', text: '已锁定' },
144
144
  }
145
- const config = statusConfig[status as keyof typeof statusConfig]
145
+ const config = statusConfig[status]
146
+ if (!config) return <Tag>{status}</Tag>
146
147
  return <Tag color={config.color}>{config.text}</Tag>
147
148
  }
148
149
 
@@ -10,12 +10,14 @@ interface AdminState {
10
10
  isAuthenticated: boolean
11
11
  stats: SystemStats | null
12
12
  loading: boolean
13
+ error: string | null
13
14
 
14
15
  login: (username: string, password: string) => Promise<LoginResponse>
15
16
  logout: () => void
16
17
  fetchStats: () => Promise<void>
17
18
  setUser: (user: AuthUserResponse) => void
18
19
  setToken: (token: string) => void
20
+ clearError: () => void
19
21
  }
20
22
 
21
23
  export const useAdminStore = create<AdminState>()(
@@ -26,6 +28,7 @@ export const useAdminStore = create<AdminState>()(
26
28
  isAuthenticated: false,
27
29
  stats: null,
28
30
  loading: false,
31
+ error: null,
29
32
 
30
33
  login: async (username: string, password: string) => {
31
34
  set({ loading: true })
@@ -70,11 +73,13 @@ export const useAdminStore = create<AdminState>()(
70
73
  }
71
74
  } catch (error) {
72
75
  console.error('Failed to fetch stats:', error)
76
+ set({ error: error instanceof Error ? error.message : 'Failed to fetch stats' })
73
77
  }
74
78
  },
75
79
 
76
80
  setUser: (user: AuthUserResponse) => set({ user }),
77
81
  setToken: (token: string) => set({ token }),
82
+ clearError: () => set({ error: null }),
78
83
  }),
79
84
  {
80
85
  name: 'admin-storage',
@@ -1,22 +1,20 @@
1
- import { program } from 'commander'
2
- import { registerModules } from './modules'
3
- import { setBaseUrl } from './utils/api'
4
- import { createLogger } from './utils/logger'
1
+ import { Core, type CoreConfig } from '@dyyz1993/xcli-core'
2
+ import { registerBuiltinCommands } from './modules'
5
3
 
6
- program
7
- .name('biomimic')
8
- .description('Biomimic CLI - RPC service & code generation tools')
9
- .version('0.1.0')
10
- .option('-v, --verbose', 'Enable verbose output')
11
- .option('-u, --url <url>', 'Server URL', 'http://localhost:3010')
12
- .hook('preAction', thisCommand => {
13
- const options = thisCommand.opts()
14
- createLogger({ verbose: options.verbose })
15
- if (options.url) {
16
- setBaseUrl(options.url)
17
- }
18
- })
4
+ const coreConfig: CoreConfig = {
5
+ name: 'biomimic',
6
+ version: '0.1.0',
7
+ description: 'Biomimic CLI - RPC service & code generation tools',
8
+ configDirName: '.biomimic',
9
+ envPrefix: 'BIOMIMIC',
10
+ pluginDirs: [],
11
+ }
19
12
 
20
- registerModules(program)
13
+ const app = new Core(coreConfig)
21
14
 
22
- program.parse()
15
+ // Register builtin commands (todo/notification/config modules)
16
+ registerBuiltinCommands(app)
17
+
18
+ // Execute CLI
19
+ const exitCode = await app.run(process.argv.slice(2))
20
+ process.exit(exitCode)
@@ -0,0 +1,65 @@
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 registerAuthCommands(site: SiteInstance) {
7
+ site.command('register', {
8
+ description: 'Register a new developer account',
9
+ parameters: z.object({
10
+ username: z.string().min(2).max(50).describe('Username'),
11
+ email: z.string().email().describe('Email'),
12
+ password: z.string().min(6).describe('Password'),
13
+ }),
14
+ handler: async (params: unknown) => {
15
+ const p = params as { username: string; email: string; password: string }
16
+ try {
17
+ const client = getClient()
18
+ const res = await client.api.auth.register.$post({ json: p })
19
+ const data = await res.json()
20
+ return ok(data, ['Registration successful'])
21
+ } catch (err) {
22
+ return fail(err instanceof Error ? err.message : 'Failed to register')
23
+ }
24
+ },
25
+ })
26
+
27
+ site.command('login', {
28
+ description: 'Login to get API key',
29
+ parameters: z.object({
30
+ account: z.string().describe('Email or username'),
31
+ password: z.string().min(6).describe('Password'),
32
+ }),
33
+ handler: async (params: unknown) => {
34
+ const p = params as { account: string; password: string }
35
+ try {
36
+ const client = getClient()
37
+ const res = await client.api.auth.login.$post({ json: p })
38
+ const data = await res.json()
39
+ return ok(data, ['Login successful'])
40
+ } catch (err) {
41
+ return fail(err instanceof Error ? err.message : 'Failed to login')
42
+ }
43
+ },
44
+ })
45
+
46
+ site.command('verify', {
47
+ description: 'Verify API key',
48
+ parameters: z.object({
49
+ token: z.string().describe('API key to verify'),
50
+ }),
51
+ handler: async (params: unknown) => {
52
+ const p = params as { token: string }
53
+ try {
54
+ const client = getClient()
55
+ const res = await client.api.auth.verify.$get({
56
+ headers: { Authorization: `Bearer ${p.token}` },
57
+ })
58
+ const data = await res.json()
59
+ return ok(data)
60
+ } catch (err) {
61
+ return fail(err instanceof Error ? err.message : 'Failed to verify')
62
+ }
63
+ },
64
+ })
65
+ }
@@ -1,9 +1,10 @@
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 { getBaseUrl, setBaseUrl, getClient } from '@cli/utils/api'
3
- import { getLogger } from '@cli/utils/logger'
4
- import fs from 'fs'
5
- import path from 'path'
6
- import os from 'os'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import os from 'node:os'
7
8
 
8
9
  const CONFIG_DIR = path.join(os.homedir(), '.biomimic')
9
10
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
@@ -37,96 +38,92 @@ function saveConfig(config: Config) {
37
38
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
38
39
  }
39
40
 
40
- export function registerConfigCommands(program: Command) {
41
- const config = program.command('config').description('CLI configuration and service management')
42
-
43
- config
44
- .command('get')
45
- .description('Show current configuration')
46
- .option('-k, --key <key>', 'Get specific config key')
47
- .action((options: { key?: string }) => {
48
- const logger = getLogger()
41
+ export function registerConfigCommands(site: SiteInstance) {
42
+ site.command('config-get', {
43
+ description: 'Show current configuration',
44
+ parameters: z.object({
45
+ key: z.string().optional().describe('Get specific config key'),
46
+ }),
47
+ handler: async (params: unknown) => {
48
+ const p = params as { key?: string }
49
49
  const cfg = loadConfig()
50
-
51
- if (options.key) {
52
- const value = cfg[options.key]
53
- logger.info(`${options.key}: ${value ?? 'not set'}`)
54
- } else {
55
- logger.info(JSON.stringify(cfg, null, 2))
50
+ if (p.key) {
51
+ const value = cfg[p.key]
52
+ return ok({ [p.key]: value ?? 'not set' })
56
53
  }
57
- })
58
-
59
- config
60
- .command('set')
61
- .description('Set configuration value')
62
- .option('-u, --url <url>', 'Set server URL')
63
- .action((options: { url?: string }) => {
64
- const logger = getLogger()
54
+ return ok(cfg)
55
+ },
56
+ })
57
+
58
+ site.command('config-set', {
59
+ description: 'Set configuration value',
60
+ parameters: z.object({
61
+ url: z.string().optional().describe('Set server URL'),
62
+ }),
63
+ handler: async (params: unknown) => {
64
+ const p = params as { url?: string }
65
65
  const cfg = loadConfig()
66
-
67
- if (options.url) {
68
- cfg.baseUrl = options.url
69
- setBaseUrl(options.url)
70
- logger.success(`Server URL set to: ${options.url}`)
66
+ if (p.url) {
67
+ cfg.baseUrl = p.url
68
+ setBaseUrl(p.url)
71
69
  }
72
-
73
70
  saveConfig(cfg)
74
- })
75
-
76
- config
77
- .command('url')
78
- .description('Show or set server URL')
79
- .argument('[url]', 'New server URL')
80
- .action((url?: string) => {
81
- const logger = getLogger()
82
- if (url) {
71
+ return ok(cfg, ['Configuration saved'])
72
+ },
73
+ })
74
+
75
+ site.command('config-url', {
76
+ description: 'Show or set server URL',
77
+ parameters: z.object({
78
+ url: z.string().optional().describe('New server URL'),
79
+ }),
80
+ handler: async (params: unknown) => {
81
+ const p = params as { url?: string }
82
+ if (p.url) {
83
83
  const cfg = loadConfig()
84
- cfg.baseUrl = url
85
- setBaseUrl(url)
84
+ cfg.baseUrl = p.url
85
+ setBaseUrl(p.url)
86
86
  saveConfig(cfg)
87
- logger.success(`Server URL set to: ${url}`)
88
- } else {
89
- logger.info(`Current server URL: ${getBaseUrl()}`)
87
+ return ok({ url: p.url }, [`Server URL set to: ${p.url}`])
90
88
  }
91
- })
92
-
93
- config
94
- .command('status')
95
- .description('Check server connection status')
96
- .action(async () => {
97
- const logger = getLogger()
98
- const client = getClient()
99
-
89
+ return ok({ url: getBaseUrl() })
90
+ },
91
+ })
92
+
93
+ site.command('config-status', {
94
+ description: 'Check server connection status',
95
+ parameters: z.object({}),
96
+ handler: async () => {
100
97
  try {
98
+ const client = getClient()
101
99
  type HealthClient = { health: { $get: () => Promise<Response> } }
102
100
  const res = await (client as unknown as HealthClient).health.$get()
103
101
  const data = await res.json()
104
- logger.success('Server is reachable')
105
- logger.info(JSON.stringify(data, null, 2))
102
+ return ok(data, ['Server is reachable'])
106
103
  } catch (error) {
107
- logger.error(`Server not reachable: ${getBaseUrl()}`)
108
- logger.error(String(error))
104
+ return fail(`Server not reachable: ${getBaseUrl()} - ${String(error)}`)
109
105
  }
110
- })
106
+ },
107
+ })
111
108
 
112
- config
113
- .command('reset')
114
- .description('Reset configuration to defaults')
115
- .action(() => {
116
- const logger = getLogger()
109
+ site.command('config-reset', {
110
+ description: 'Reset configuration to defaults',
111
+ parameters: z.object({}),
112
+ handler: async () => {
117
113
  const defaultConfig: Config = { baseUrl: 'http://localhost:3010' }
118
114
  saveConfig(defaultConfig)
119
115
  setBaseUrl(defaultConfig.baseUrl)
120
- logger.success('Configuration reset to defaults')
121
- })
122
-
123
- config
124
- .command('path')
125
- .description('Show config file path')
126
- .action(() => {
127
- const logger = getLogger()
128
- logger.info(`Config file: ${CONFIG_FILE}`)
129
- })
116
+ return ok(defaultConfig, ['Configuration reset to defaults'])
117
+ },
118
+ })
119
+
120
+ site.command('config-path', {
121
+ description: 'Show config file path',
122
+ parameters: z.object({}),
123
+ handler: async () => {
124
+ return ok({ path: CONFIG_FILE })
125
+ },
126
+ })
130
127
  }
131
128
 
132
129
  export { loadConfig, saveConfig, CONFIG_FILE }
@@ -1,12 +1,34 @@
1
- import type { Command } from 'commander'
1
+ import type { Core } from '@dyyz1993/xcli-core'
2
2
  import { registerTodoCommands } from './todo'
3
3
  import { registerNotificationCommands } from './notification'
4
4
  import { registerConfigCommands } from './config'
5
+ import { registerPluginCommands } from './plugin'
6
+ import { registerAuthCommands } from './auth'
5
7
 
6
- export function registerModules(program: Command) {
7
- registerTodoCommands(program)
8
- registerNotificationCommands(program)
9
- registerConfigCommands(program)
8
+ /**
9
+ * Register all builtin CLI commands to xcli-core.
10
+ * Each register function receives a SiteInstance for command registration.
11
+ */
12
+ export function registerBuiltinCommands(app: Core) {
13
+ const api = app.loader.getAPI()
14
+
15
+ // Create a builtin site representing the local server
16
+ const site = api.createSite({
17
+ name: 'local-server',
18
+ url: 'http://localhost:3010',
19
+ })
20
+
21
+ registerTodoCommands(site)
22
+ registerNotificationCommands(site)
23
+ registerConfigCommands(site)
24
+ registerPluginCommands(site)
25
+ registerAuthCommands(site)
10
26
  }
11
27
 
12
- export { registerTodoCommands, registerNotificationCommands, registerConfigCommands }
28
+ export {
29
+ registerTodoCommands,
30
+ registerNotificationCommands,
31
+ registerConfigCommands,
32
+ registerPluginCommands,
33
+ registerAuthCommands,
34
+ }