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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- package/template/package-lock.json +0 -14554
|
@@ -1,86 +1,102 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { z } from '
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
title: z.string().optional(),
|
|
39
|
-
description: z.string().optional(),
|
|
40
|
-
status:
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|