@h-ai/cli 1.0.0-beta.18 → 1.0.0-beta.19

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 (194) hide show
  1. package/README.en.md +4 -0
  2. package/README.ja.md +4 -0
  3. package/README.md +4 -0
  4. package/dist/{chunk-6YZBC5MO.js → chunk-2ZL5ORQ2.js} +85 -25
  5. package/dist/chunk-2ZL5ORQ2.js.map +1 -0
  6. package/dist/{chunk-7NVRYYHN.js → chunk-L53P43B2.js} +129 -31
  7. package/dist/chunk-L53P43B2.js.map +1 -0
  8. package/dist/cli-config-templates-LWMD2I6V.js +5 -0
  9. package/dist/{cli-config-templates-QG5HOYR5.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
  10. package/dist/{cli-skill-templates-W4I6ZKYW.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
  11. package/dist/{cli-skill-templates-W4I6ZKYW.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +2747 -671
  14. package/dist/index.js.map +1 -1
  15. package/dist/metafile-esm.json +1 -1
  16. package/package.json +22 -22
  17. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
  18. package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
  19. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
  20. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
  21. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
  22. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
  23. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
  24. package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
  25. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  26. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +71 -32
  27. package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  28. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +131 -54
  29. package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
  30. package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +78 -31
  31. package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
  32. package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  33. package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
  34. package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
  35. package/templates/apps/api/eslint.config.js.hbs +3 -1
  36. package/templates/apps/api/images/serv.Dockerfile.hbs +3 -4
  37. package/templates/apps/api/playwright.config.ts.hbs +23 -19
  38. package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
  39. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -3
  40. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
  41. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
  42. package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
  43. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
  46. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
  48. package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
  49. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
  50. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
  51. package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
  52. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
  53. package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
  55. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +111 -26
  56. package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
  57. package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
  58. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +3 -4
  59. package/templates/apps/fullstack/images/web.Dockerfile.hbs +0 -2
  60. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
  61. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
  62. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  63. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
  64. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  65. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
  66. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
  67. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
  68. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
  69. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
  70. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +139 -49
  71. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
  72. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +139 -28
  73. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
  74. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +141 -25
  75. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
  76. package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  77. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
  78. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
  79. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
  80. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
  81. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
  82. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
  83. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
  84. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
  85. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
  86. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
  87. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
  88. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
  89. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
  90. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
  91. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
  92. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
  93. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
  94. package/templates/apps/fullstack/playwright.config.ts.hbs +9 -3
  95. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -15
  96. package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -21
  97. package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
  98. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
  99. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
  100. package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
  101. package/templates/apps/mobile-app/src/lib/api.ts.hbs +14 -3
  102. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
  103. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
  104. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
  105. package/templates/apps/website/scripts/static-website.mjs.hbs +16 -3
  106. package/templates/apps/website/src/routes/+layout.svelte.hbs +44 -23
  107. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
  108. package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
  109. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
  110. package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
  111. package/templates/base/e2e/app.spec.ts.hbs +49 -16
  112. package/templates/base/eslint-rules.js +44 -25
  113. package/templates/base/eslint.config.js.hbs +3 -1
  114. package/templates/base/images/web.Dockerfile.hbs +3 -4
  115. package/templates/base/package.json.hbs +5 -4
  116. package/templates/base/patches/daisyui-5.7.50.patch +8 -0
  117. package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
  118. package/templates/base/playwright.config.ts +3 -1
  119. package/templates/base/pnpm-workspace.yaml.hbs +33 -16
  120. package/templates/base/src/app.d.ts.hbs +1 -1
  121. package/templates/base/src/hooks.server.ts.hbs +36 -27
  122. package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
  123. package/templates/base/src/lib/server/init.ts.hbs +124 -51
  124. package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
  125. package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
  126. package/templates/base/svelte.config.js.hbs +10 -100
  127. package/templates/base/tests/scaffold.test.ts.hbs +12 -3
  128. package/templates/base/tsconfig.json.hbs +7 -4
  129. package/templates/base/vite.config.ts.hbs +108 -14
  130. package/templates/base/vitest.config.ts.hbs +6 -2
  131. package/templates/features/ai/routes/api/chat/+server.ts +3 -1
  132. package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
  133. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
  134. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
  135. package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
  136. package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
  137. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
  138. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
  139. package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
  140. package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
  141. package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
  142. package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
  143. package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
  144. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +11 -2
  145. package/templates/shared/browser-check/startup.js +32 -6
  146. package/templates/shared/browser-startup.spec.ts +3 -1
  147. package/templates/shared/build-metadata.ts +18 -4
  148. package/templates/shared/e2e-env.ts +22 -6
  149. package/templates/shared/native-name.mjs +9 -2
  150. package/templates/shared/public-base-path.mjs +17 -4
  151. package/templates/shared/run-e2e.mjs +40 -9
  152. package/templates/shared/run-node.mjs +15 -4
  153. package/templates/shared/run-tauri.mjs +37 -8
  154. package/templates/shared/vite-runtime-config.ts +9 -3
  155. package/templates/skills/AGENTS.md +10 -73
  156. package/templates/skills/bridges/admin/AGENTS.md +9 -65
  157. package/templates/skills/bridges/api/AGENTS.md +8 -59
  158. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
  159. package/templates/skills/bridges/h5/AGENTS.md +8 -63
  160. package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
  161. package/templates/skills/bridges/website/AGENTS.md +8 -63
  162. package/templates/skills/hai-ai/acp.md +2 -0
  163. package/templates/skills/hai-ai/guide.md +17 -6
  164. package/templates/skills/hai-ai/reference.md +21 -12
  165. package/templates/skills/hai-api-client/reference.md +1 -1
  166. package/templates/skills/hai-app-create/development.md +3 -1
  167. package/templates/skills/hai-app-create/project.md +33 -0
  168. package/templates/skills/hai-app-create/reference.md +8 -8
  169. package/templates/skills/hai-app-tests/reference.md +1 -1
  170. package/templates/skills/hai-build/SKILL.md +2 -2
  171. package/templates/skills/hai-build/paths.md +1 -1
  172. package/templates/skills/hai-build/reference.md +11 -7
  173. package/templates/skills/hai-capacitor/reference.md +2 -0
  174. package/templates/skills/hai-ci/SKILL.md +1 -0
  175. package/templates/skills/hai-core/reference.md +3 -0
  176. package/templates/skills/hai-crypto/reference.md +2 -0
  177. package/templates/skills/hai-deploy/SKILL.md +7 -1
  178. package/templates/skills/hai-deploy/middleware.md +3 -1
  179. package/templates/skills/hai-framework-sync/sync.md +2 -0
  180. package/templates/skills/hai-kit/reference.md +16 -4
  181. package/templates/skills/hai-payment/reference.md +2 -0
  182. package/templates/skills/hai-reach/SKILL.md +2 -1
  183. package/templates/skills/hai-reach/reference.md +1 -1
  184. package/templates/skills/hai-reldb/SKILL.md +1 -1
  185. package/templates/skills/hai-reldb/reference.md +4 -0
  186. package/templates/skills/hai-serv/reference.md +12 -2
  187. package/templates/skills/hai-storage/reference.md +12 -3
  188. package/templates/skills/hai-ui/SKILL.md +3 -2
  189. package/templates/skills/hai-ui/guide.md +1 -1
  190. package/templates/skills/hai-ui/reference.md +23 -35
  191. package/templates/skills/hai-vecdb/reference.md +1 -1
  192. package/dist/chunk-6YZBC5MO.js.map +0 -1
  193. package/dist/chunk-7NVRYYHN.js.map +0 -1
  194. package/dist/cli-config-templates-QG5HOYR5.js +0 -5
@@ -35,7 +35,13 @@ async function capture(page: Page, name: string, fallback: string) {
35
35
  await mkdir(evidence, { recursive: true })
36
36
  await expect(page.getByTestId('guide-loading')).toHaveCount(0)
37
37
  await page.evaluate(() => document.fonts.ready)
38
- await page.screenshot({ path: evidence ? path.join(evidence, name) : fallback, fullPage: true, animations: 'disabled' })
38
+ await page.screenshot({
39
+ path: evidence
40
+ ? path.join(evidence, name)
41
+ : fallback,
42
+ fullPage: true,
43
+ animations: 'disabled',
44
+ })
39
45
  }
40
46
 
41
47
  async function dismissToasts(page: Page) {
@@ -62,13 +68,18 @@ async function login(page: Page, username: string, password: string, keepGuide =
62
68
  async function userEditor(page: Page, username: string) {
63
69
  await page.getByTestId('users-search').locator('input').fill(username)
64
70
  await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
65
- const row = page.getByRole('row').filter({ has: page.getByText(`@${username}`, { exact: true }) })
71
+ const row = page.getByRole('row').filter({
72
+ has: page.getByText(`@${username}`, { exact: true }),
73
+ })
66
74
  await expect(row).toBeVisible()
67
75
  await row.getByRole('button', { name: /编辑|Edit/, exact: true }).click()
68
76
  return row
69
77
  }
70
78
 
71
- for (const scenario of [{ offset: -600_000, width: 1440 }, { offset: 600_000, width: 390 }]) {
79
+ for (const scenario of [
80
+ { offset: -600_000, width: 1440 },
81
+ { offset: 600_000, width: 390 },
82
+ ]) {
72
83
  test(`clock offset ${scenario.offset} displays synchronization advice and recovers after correction`, async ({ page }, testInfo) => {
73
84
  await page.setViewportSize({ width: scenario.width, height: 900 })
74
85
  await page.addInitScript((offset) => {
@@ -99,10 +110,11 @@ test('service health endpoint is ready', async ({ request }) => {
99
110
  })
100
111
 
101
112
  test('service rejects plaintext business requests', async ({ request }) => {
102
- const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, { data: { message: 'hello e2e' } })
113
+ const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
114
+ data: { message: 'hello e2e' },
115
+ })
103
116
  expect(response.status()).toBe(400)
104
117
  })
105
-
106
118
  {{#if fullstack.e2eFrontend}}
107
119
  test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
108
120
  await page.goto('./#/roles?source=direct')
@@ -289,7 +301,11 @@ const additionalFrontends = [
289
301
 
290
302
  for (const frontend of additionalFrontends) {
291
303
  test(`${frontend.target} preview supports login, tour, help and profile navigation`, async ({ browser }, testInfo) => {
292
- const page = await browser.newPage({ viewport: frontend.target === 'app' ? { width: 390, height: 844 } : { width: 1440, height: 960 } })
304
+ const page = await browser.newPage({
305
+ viewport: frontend.target === 'app'
306
+ ? { width: 390, height: 844 }
307
+ : { width: 1440, height: 960 },
308
+ })
293
309
  const pageErrors: string[] = []
294
310
  page.on('pageerror', error => pageErrors.push(error.message))
295
311
  try {
@@ -329,11 +345,17 @@ test('shared authentication supports public registration with configuration', as
329
345
  await mkdir(evidence, { recursive: true })
330
346
  await page.goto('./')
331
347
  await expect(page.getByTestId('login-form')).toBeVisible()
332
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
348
+ await page.screenshot({
349
+ path: path.join(evidence, '{{projectName}}-login.png'),
350
+ fullPage: true,
351
+ })
333
352
  await page.getByRole('button', { name: /立即注册|Register now/i }).click()
334
353
  const registerForm = page.locator('.register-form')
335
354
  await expect(registerForm.locator('#register-username')).toBeVisible()
336
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-register.png'), fullPage: true })
355
+ await page.screenshot({
356
+ path: path.join(evidence, '{{projectName}}-register.png'),
357
+ fullPage: true,
358
+ })
337
359
  const username = `signup${randomUUID().replaceAll('-', '').slice(0, 12)}`
338
360
  await registerForm.locator('#register-username').fill(username)
339
361
  await registerForm.locator('#register-email').fill(`${username}@example.com`)
@@ -347,7 +369,10 @@ test('shared authentication supports public registration with configuration', as
347
369
  const email = `${username}@example.com`
348
370
  await page.getByTestId('logout').click()
349
371
  await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
350
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-email.png'), fullPage: true })
372
+ await page.screenshot({
373
+ path: path.join(evidence, '{{projectName}}-email.png'),
374
+ fullPage: true,
375
+ })
351
376
  await page.locator('#login-username').fill(email)
352
377
  await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
353
378
  await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
@@ -363,14 +388,20 @@ test('shared authentication supports public registration with configuration', as
363
388
  await expect(page.getByTestId('settings-page')).toBeVisible()
364
389
  await page.getByTestId('logout').click()
365
390
  await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
366
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-forgot.png'), fullPage: true })
391
+ await page.screenshot({
392
+ path: path.join(evidence, '{{projectName}}-forgot.png'),
393
+ fullPage: true,
394
+ })
367
395
  await page.locator('input[type="email"]').fill(email)
368
396
  await page.locator('button[type="submit"]').click()
369
397
  await expect(page.locator('.hai-toast-item[data-variant="success"]')).toBeVisible()
370
398
  const reset = await received('password_reset')
371
399
  expect(Boolean(reset?.token)).toBe(true)
372
400
  await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
373
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-reset.png'), fullPage: true })
401
+ await page.screenshot({
402
+ path: path.join(evidence, '{{projectName}}-reset.png'),
403
+ fullPage: true,
404
+ })
374
405
  await page.goto(`./?token=${encodeURIComponent(reset?.token ?? '')}`)
375
406
  await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
376
407
  await page.locator('input[type="password"]').nth(0).fill('FullstackReset456')
@@ -401,7 +432,12 @@ test('frontend displays configured core metadata and restores the administrator
401
432
  const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
402
433
  if (evidence)
403
434
  await mkdir(evidence, { recursive: true })
404
- await page.screenshot({ path: evidence ? path.join(evidence, '{{projectName}}-users.png') : testInfo.outputPath('users.png'), fullPage: true })
435
+ await page.screenshot({
436
+ path: evidence
437
+ ? path.join(evidence, '{{projectName}}-users.png')
438
+ : testInfo.outputPath('users.png'),
439
+ fullPage: true,
440
+ })
405
441
  await page.getByTestId('logout').click()
406
442
  await expect(page.getByTestId('login-form')).toBeVisible()
407
443
  })
@@ -524,9 +560,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
524
560
  await page.setViewportSize({ width, height: 900 })
525
561
  if (width < 1024)
526
562
  await expect.poll(() => page.locator('aside').evaluate(sidebar => sidebar.getBoundingClientRect().right)).toBeLessThanOrEqual(1)
527
- const controls = await page.getByTestId('audit-advanced').evaluate(panel => [...panel.querySelectorAll('input')].map((element) => {
563
+ const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
564
+ ...panel.querySelectorAll('input'),
565
+ ].map((element) => {
528
566
  // 复合控件的点击框包含边框和留白,内部文字输入区不等于完整控件高度。
529
- const control = element.closest('.fieldset') ? element.parentElement! : element
567
+ const control = element.closest('.fieldset')
568
+ ? element.parentElement!
569
+ : element
530
570
  const rect = control.getBoundingClientRect()
531
571
  return { left: rect.left, right: rect.right, height: rect.height }
532
572
  }))
@@ -542,7 +582,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
542
582
  if (evidence)
543
583
  await mkdir(evidence, { recursive: true })
544
584
  const file = `{{projectName}}-audit-${width}.png`
545
- await page.screenshot({ path: evidence ? path.join(evidence, file) : testInfo.outputPath(file), fullPage: true, animations: 'disabled' })
585
+ await page.screenshot({
586
+ path: evidence
587
+ ? path.join(evidence, file)
588
+ : testInfo.outputPath(file),
589
+ fullPage: true,
590
+ animations: 'disabled',
591
+ })
546
592
  }
547
593
  }
548
594
  const reset = page.waitForResponse(response => new URL(response.url()).pathname.endsWith('/admin/audit/list'))
@@ -563,7 +609,11 @@ test('audit filters collapse without losing values and fit compact workspaces',
563
609
  await toggle.click()
564
610
  await page.getByTestId('audit-page').getByRole('button', { name: /详情|Details/ }).first().click()
565
611
  await expect(page.getByRole('dialog')).toBeVisible()
566
- await page.screenshot({ path: testInfo.outputPath('audit-short-table-details.png'), fullPage: true, animations: 'disabled' })
612
+ await page.screenshot({
613
+ path: testInfo.outputPath('audit-short-table-details.png'),
614
+ fullPage: true,
615
+ animations: 'disabled',
616
+ })
567
617
  })
568
618
 
569
619
  test('administrator manages every duty through one lifecycle and the shared password reset form', async ({ page, browser }, testInfo) => {
@@ -718,8 +768,16 @@ test('administrator and user complete account, avatar, preference, password and
718
768
  await userPage.getByRole('option', { name: 'English', exact: true }).click()
719
769
  await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
720
770
  await expect(userPage.locator('html')).toHaveAttribute('lang', 'en-US')
721
- const themes = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
722
- const selectedTheme = themes.includes('formal') ? 'formal' : themes.includes('high-contrast') ? 'high-contrast' : themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
771
+ const themes = [
772
+ {{#each fullstack.theme.themes}}
773
+ '{{this}}',
774
+ {{/each}}
775
+ ]
776
+ const selectedTheme = themes.includes('formal')
777
+ ? 'formal'
778
+ : themes.includes('high-contrast')
779
+ ? 'high-contrast'
780
+ : themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
723
781
  await userPage.getByTestId('profile-theme').click()
724
782
  await userPage.getByRole('option').nth(themes.indexOf(selectedTheme)).click()
725
783
  await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
@@ -738,7 +796,10 @@ test('administrator and user complete account, avatar, preference, password and
738
796
  await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
739
797
  await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe('light')
740
798
  await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-base-100').trim())).toBe('#e8f0df')
741
- await userPage.screenshot({ path: testInfo.outputPath('fullstack-eye-care.png'), animations: 'disabled' })
799
+ await userPage.screenshot({
800
+ path: testInfo.outputPath('fullstack-eye-care.png'),
801
+ animations: 'disabled',
802
+ })
742
803
  }
743
804
  await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
744
805
  await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
@@ -868,7 +929,11 @@ test('feedback submits an empty description and business operator can process an
868
929
  await dialog.locator('input[type=file]').setInputFiles([
869
930
  { name: 'attached-preview.png', mimeType: 'image/png', buffer: feedbackImage },
870
931
  { name: 'attached-large-log.txt', mimeType: 'text/plain', buffer: feedbackLog },
871
- { name: 'discarded.txt', mimeType: 'text/plain', buffer: Buffer.from('discarded local material') },
932
+ {
933
+ name: 'discarded.txt',
934
+ mimeType: 'text/plain',
935
+ buffer: Buffer.from('discarded local material'),
936
+ },
872
937
  ])
873
938
  const uploaded = dialog.locator('img[alt="attached-preview.png"]')
874
939
  await expect(uploaded).toBeVisible()
@@ -876,7 +941,10 @@ test('feedback submits an empty description and business operator can process an
876
941
  await expectDialogViewport()
877
942
  await expectUncovered(uploaded)
878
943
  await capture(page, 'feedback-image-before-mask.png', testInfo.outputPath('feedback-image-before-mask.png'))
879
- await uploaded.screenshot({ path: testInfo.outputPath('feedback-image-before-mask-detail.png'), animations: 'disabled' })
944
+ await uploaded.screenshot({
945
+ path: testInfo.outputPath('feedback-image-before-mask-detail.png'),
946
+ animations: 'disabled',
947
+ })
880
948
  const originalPreview = await uploaded.getAttribute('src')
881
949
  await dialog.getByRole('button', { name: /遮挡敏感区域|Mask sensitive area|機密情報を隠す/ }).last().click()
882
950
  await dialog.getByRole('button', { name: /应用标注|Apply annotation|注釈を適用/ }).last().click()
@@ -901,7 +969,10 @@ test('feedback submits an empty description and business operator can process an
901
969
  await expectDialogViewport()
902
970
  await expectUncovered(uploaded)
903
971
  await capture(page, 'feedback-preview.png', testInfo.outputPath('feedback-preview.png'))
904
- await uploaded.screenshot({ path: testInfo.outputPath('feedback-image-after-mask-detail.png'), animations: 'disabled' })
972
+ await uploaded.screenshot({
973
+ path: testInfo.outputPath('feedback-image-after-mask-detail.png'),
974
+ animations: 'disabled',
975
+ })
905
976
  // 回执只表示记录已受理;等这份大附件真正上传成功,再开始后台处理操作。
906
977
  const largeUpload = page.waitForResponse(response => response.request().method() === 'POST'
907
978
  && /\/feedback\/reports\/[^/]+\/attachments\/[^/]+$/.test(new URL(response.url()).pathname)
@@ -972,7 +1043,12 @@ test('feedback submits an empty description and business operator can process an
972
1043
  const serverManifest: { version: string } = JSON.parse(await readFile(path.resolve('packages/{{projectName}}-serv/package.json'), 'utf8'))
973
1044
  let expectedCommit = 'unknown'
974
1045
  try {
975
- execFileSync('git', ['ls-files', '--error-unmatch', '--', 'package.json'], { stdio: 'pipe', windowsHide: true })
1046
+ execFileSync('git', [
1047
+ 'ls-files',
1048
+ '--error-unmatch',
1049
+ '--',
1050
+ 'package.json',
1051
+ ], { stdio: 'pipe', windowsHide: true })
976
1052
  expectedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe', windowsHide: true }).trim()
977
1053
  }
978
1054
  catch { /* 无 Git 的源码归档必须明确保留 unknown,不冒用父工程提交。 */ }
@@ -981,7 +1057,10 @@ test('feedback submits an empty description and business operator can process an
981
1057
  expect(expectedCommit).toMatch(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i)
982
1058
  expect(browserManifest.version).not.toBe(serverManifest.version)
983
1059
  }
984
- for (const [metadata, version] of [[browserMetadata, browserManifest.version], [serverMetadata, serverManifest.version]] as const) {
1060
+ for (const [metadata, version] of [
1061
+ [browserMetadata, browserManifest.version],
1062
+ [serverMetadata, serverManifest.version],
1063
+ ] as const) {
985
1064
  expect(Object.keys(metadata).sort()).toEqual(['appVersion', 'buildId', 'commit'])
986
1065
  expect(metadata.appVersion).toBe(version)
987
1066
  expect(metadata.buildId).toMatch(/^[\w.+-]{1,160}$/)
@@ -993,7 +1072,10 @@ test('feedback submits an empty description and business operator can process an
993
1072
  expect(browserEnvironment?.data).toMatchObject(browserMetadata)
994
1073
  expect(serverEnvironment?.data.service).toMatchObject(serverMetadata)
995
1074
  expect(bundle.report).toMatchObject(browserMetadata)
996
- await testInfo.attach('feedback-build-metadata', { body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }), contentType: 'application/json' })
1075
+ await testInfo.attach('feedback-build-metadata', {
1076
+ body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }),
1077
+ contentType: 'application/json',
1078
+ })
997
1079
  expect(bundle.report.number).toBe(number)
998
1080
  expect(bundle.manifest.reportId).toBe(bundle.report.id)
999
1081
  expect(bundle.manifest.evidenceRevision).toBe(bundle.report.evidenceRevision)
@@ -1009,7 +1091,10 @@ test('feedback submits an empty description and business operator can process an
1009
1091
  throw new Error('The exported large log attachment is missing')
1010
1092
  expect(bundle.files[largeLog.path]).toEqual(new Uint8Array(feedbackLog))
1011
1093
  expect(bundle.manifest.files.some(file => file.name === 'discarded.txt')).toBe(false)
1012
- await testInfo.attach('feedback-export-masked', { body: Buffer.from(bundle.files[masked.path]!), contentType: 'image/png' })
1094
+ await testInfo.attach('feedback-export-masked', {
1095
+ body: Buffer.from(bundle.files[masked.path]!),
1096
+ contentType: 'image/png',
1097
+ })
1013
1098
  })
1014
1099
  {{/if}}
1015
1100
  {{/if}}
@@ -20,8 +20,13 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
20
20
  // 每例使用独立初始账号,真实完成服务端限制下的首次改密后才能建立普通会话。
21
21
  await expect(page.locator('#old-password')).toBeVisible()
22
22
  await expect(page.getByTestId('logout')).toHaveCount(0)
23
- await page.screenshot({ path: test.info().outputPath('initial-password-renewal.png'), fullPage: true })
24
- const form = page.locator('form').filter({ has: page.locator('#old-password') })
23
+ await page.screenshot({
24
+ path: test.info().outputPath('initial-password-renewal.png'),
25
+ fullPage: true,
26
+ })
27
+ const form = page.locator('form').filter({
28
+ has: page.locator('#old-password'),
29
+ })
25
30
  const error = page.locator('.hai-toast-item[data-variant=error]')
26
31
  await dismissToasts(page)
27
32
  // 同密码仍由服务端拒绝,显示安全原因与关联编号;本地确认不一致则直接提示字段。
@@ -34,7 +39,10 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
34
39
  await error.getByRole('button', { name: /^(查看排查信息|View diagnostic details|調査情報を表示)$/ }).click()
35
40
  await expect(error.locator('code')).toHaveText(/^[\w.:-]{1,128}$/)
36
41
  await expect(page.getByTestId('logout')).toHaveCount(0)
37
- await page.screenshot({ path: test.info().outputPath('initial-password-same.png'), fullPage: true })
42
+ await page.screenshot({
43
+ path: test.info().outputPath('initial-password-same.png'),
44
+ fullPage: true,
45
+ })
38
46
  await expect(form.locator('.alert-error')).toHaveCount(0)
39
47
  await dismissToasts(page)
40
48
  const password = `${initialPassword}-Renewed!`
@@ -100,13 +108,19 @@ test('server idle expiry opens normal login in place and resumes one rejected sa
100
108
  const dialog = page.getByTestId('session-recovery-dialog')
101
109
  await expect(dialog).toBeVisible()
102
110
  await expect(page.getByTestId('business-notice-input').locator('textarea')).toHaveValue(draft)
103
- await page.screenshot({ path: test.info().outputPath('idle-session-recovery.png'), fullPage: true })
111
+ await page.screenshot({
112
+ path: test.info().outputPath('idle-session-recovery.png'),
113
+ fullPage: true,
114
+ })
104
115
  const form = dialog.locator('form.login-form')
105
116
  await form.locator('input[type=password]').fill('Incorrect-test-password!')
106
117
  await form.locator('button[type=submit]').click()
107
118
  await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeVisible()
108
119
  await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeInViewport({ ratio: 1 })
109
- await page.screenshot({ path: test.info().outputPath('idle-session-recovery-error-toast.png'), fullPage: true })
120
+ await page.screenshot({
121
+ path: test.info().outputPath('idle-session-recovery-error-toast.png'),
122
+ fullPage: true,
123
+ })
110
124
  expect(statuses).toHaveLength(1)
111
125
  await form.locator('input[type=password]').fill(password)
112
126
  await form.locator('button[type=submit]').click()
@@ -3,7 +3,9 @@ import { haiQualityConfig } from './eslint-rules.js'
3
3
 
4
4
  export default antfu({
5
5
  // unused-imports 使用 TS 规则,JS 同样使用 TS parser 以保持 scope 契约一致。
6
- typescript: { files: [GLOB_SRC] },
6
+ typescript: {
7
+ files: [GLOB_SRC],
8
+ },
7
9
  formatters: true,
8
10
  ignores: [
9
11
  '**/build/**',
@@ -1,7 +1,5 @@
1
1
  ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
2
2
  FROM ${HAI_NODE_IMAGE} AS build
3
- ARG SQLITE_PREBUILD_MIRROR=
4
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
5
3
 
6
4
  ENV PNPM_HOME=/pnpm
7
5
  ENV PATH=$PNPM_HOME:$PATH
@@ -26,8 +24,9 @@ ARG HAI_BUILD_ID=
26
24
  ARG HAI_BUILD_COMMIT=unknown
27
25
  RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm --filter {{fullstack.servPackageName}}... --if-present build
28
26
 
29
- RUN --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
30
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
27
+ # SQLite 13 的原生二进制随 npm 包交付,在构建时验证实际加载。
28
+ RUN if [ -d node_modules/better-sqlite3 ]; then node -e "const Database=require('better-sqlite3');const db=new Database(':memory:');db.prepare('SELECT 1').get();db.close()"; fi
29
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
31
30
  RUN rm -f /prod/service/README* && find /prod/service/dist -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
32
31
 
33
32
  FROM ${HAI_NODE_IMAGE} AS service
@@ -2,8 +2,6 @@ ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
2
2
  ARG HAI_NGINX_IMAGE=nginxinc/nginx-unprivileged:1.29.8-alpine
3
3
  {{#if fullstack.frontends.web}}
4
4
  FROM ${HAI_NODE_IMAGE} AS build
5
- ARG SQLITE_PREBUILD_MIRROR=
6
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
7
5
 
8
6
  ENV PNPM_HOME=/pnpm
9
7
  ENV PATH=$PNPM_HOME:$PATH
@@ -22,54 +22,145 @@ import {
22
22
  /** mini 管理示例的业务路径;四类职责与权限由应用配置,可按项目需求调整。 */
23
23
  export const adminContract = {
24
24
  permissions: {
25
- list: apiContract.route({ method: 'POST', path: '/admin/permissions/list', tags: ['admin'] }).input(z.object({ page: z.number().int().min(1).default(1), pageSize: z.number().int().min(1).max(100).default(20) })).output(apiContract.haiResultSchema(z.object({ items: z.array(AdminPermissionPolicySchema), total: z.number() }))),
26
- update: apiContract.route({ method: 'POST', path: '/admin/permissions/update', tags: ['admin'] }).input(AdminPermissionPolicyInputSchema).output(apiContract.haiResultSchema(z.object({ saved: z.literal(true) }))),
25
+ list: apiContract.route({
26
+ method: 'POST',
27
+ path: '/admin/permissions/list',
28
+ tags: ['admin'],
29
+ }).input(z.object({
30
+ page: z.number().int().min(1).default(1),
31
+ pageSize: z.number().int().min(1).max(100).default(20),
32
+ })).output(apiContract.haiResultSchema(z.object({
33
+ items: z.array(AdminPermissionPolicySchema),
34
+ total: z.number(),
35
+ }))),
36
+ update: apiContract.route({
37
+ method: 'POST',
38
+ path: '/admin/permissions/update',
39
+ tags: ['admin'],
40
+ }).input(AdminPermissionPolicyInputSchema).output(apiContract.haiResultSchema(z.object({
41
+ saved: z.literal(true),
42
+ }))),
27
43
  },
28
44
  configuration: {
29
- get: apiContract.route({ method: 'POST', path: '/admin/configuration/get', tags: ['admin'] }).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
30
- update: apiContract.route({ method: 'POST', path: '/admin/configuration/update', tags: ['admin'] }).input(BusinessSettingsSchema).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
45
+ get: apiContract.route({
46
+ method: 'POST',
47
+ path: '/admin/configuration/get',
48
+ tags: ['admin'],
49
+ }).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
50
+ update: apiContract.route({
51
+ method: 'POST',
52
+ path: '/admin/configuration/update',
53
+ tags: ['admin'],
54
+ }).input(BusinessSettingsSchema).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
31
55
  },
32
56
  users: {
33
- resetPassword: apiContract.route({ method: 'POST', path: '/admin/users/reset-password', tags: ['admin'] })
57
+ resetPassword: apiContract.route({
58
+ method: 'POST',
59
+ path: '/admin/users/reset-password',
60
+ tags: ['admin'],
61
+ })
34
62
  .input(IamAdminResetPasswordInputSchema.strict())
35
- .output(apiContract.haiResultSchema(z.object({ reset: z.literal(true) }))),
36
- invite: apiContract.route({ method: 'POST', path: '/admin/users/invite', tags: ['admin'] }).input(AdminInviteUserSchema).output(apiContract.haiResultSchema(AdminUserSchema)),
37
- list: apiContract.route({ method: 'POST', path: '/admin/users/list', tags: ['admin'] })
63
+ .output(apiContract.haiResultSchema(z.object({
64
+ reset: z.literal(true),
65
+ }))),
66
+ invite: apiContract.route({
67
+ method: 'POST',
68
+ path: '/admin/users/invite',
69
+ tags: ['admin'],
70
+ }).input(AdminInviteUserSchema).output(apiContract.haiResultSchema(AdminUserSchema)),
71
+ list: apiContract.route({
72
+ method: 'POST',
73
+ path: '/admin/users/list',
74
+ tags: ['admin'],
75
+ })
38
76
  .input(AdminUsersInputSchema)
39
77
  .output(apiContract.haiResultSchema(AdminUsersSchema)),
40
- create: apiContract.route({ method: 'POST', path: '/admin/users/create', tags: ['admin'] })
78
+ create: apiContract.route({
79
+ method: 'POST',
80
+ path: '/admin/users/create',
81
+ tags: ['admin'],
82
+ })
41
83
  .input(AdminCreateUserSchema)
42
84
  .output(apiContract.haiResultSchema(AdminUserSchema)),
43
- update: apiContract.route({ method: 'POST', path: '/admin/users/update', tags: ['admin'] })
85
+ update: apiContract.route({
86
+ method: 'POST',
87
+ path: '/admin/users/update',
88
+ tags: ['admin'],
89
+ })
44
90
  .input(AdminUpdateUserSchema)
45
91
  .output(apiContract.haiResultSchema(AdminUserSchema)),
46
- delete: apiContract.route({ method: 'POST', path: '/admin/users/delete', tags: ['admin'] })
47
- .input(z.object({ id: z.string().min(1) }).strict())
48
- .output(apiContract.haiResultSchema(z.object({ deleted: z.literal(true) }))),
49
- avatar: apiContract.route({ method: 'POST', path: '/admin/users/avatar', tags: ['admin'] })
50
- .input(AvatarUploadSchema.extend({ id: z.string().min(1) }))
92
+ delete: apiContract.route({
93
+ method: 'POST',
94
+ path: '/admin/users/delete',
95
+ tags: ['admin'],
96
+ })
97
+ .input(z.object({
98
+ id: z.string().min(1),
99
+ }).strict())
100
+ .output(apiContract.haiResultSchema(z.object({
101
+ deleted: z.literal(true),
102
+ }))),
103
+ avatar: apiContract.route({
104
+ method: 'POST',
105
+ path: '/admin/users/avatar',
106
+ tags: ['admin'],
107
+ })
108
+ .input(AvatarUploadSchema.extend({
109
+ id: z.string().min(1),
110
+ }))
51
111
  .output(apiContract.haiResultSchema(AvatarOutputSchema)),
52
112
  },
53
113
  roles: {
54
- list: apiContract.route({ method: 'POST', path: '/admin/roles/list', tags: ['admin'] })
55
- .output(apiContract.haiResultSchema(z.object({ items: z.array(AdminRoleSchema) }))),
56
- update: apiContract.route({ method: 'POST', path: '/admin/roles/update', tags: ['admin'] })
57
- .input(z.object({ id: z.string().min(1), description: z.string().max(300) }).strict())
114
+ list: apiContract.route({
115
+ method: 'POST',
116
+ path: '/admin/roles/list',
117
+ tags: ['admin'],
118
+ })
119
+ .output(apiContract.haiResultSchema(z.object({
120
+ items: z.array(AdminRoleSchema),
121
+ }))),
122
+ update: apiContract.route({
123
+ method: 'POST',
124
+ path: '/admin/roles/update',
125
+ tags: ['admin'],
126
+ })
127
+ .input(z.object({
128
+ id: z.string().min(1),
129
+ description: z.string().max(300),
130
+ }).strict())
58
131
  .output(apiContract.haiResultSchema(AdminRoleSchema)),
59
132
  },
60
133
  audit: {
61
- backup: apiContract.route({ method: 'POST', path: '/admin/audit/backup', tags: ['admin'] }).input(z.object({ id: z.uuid() }).strict()).output(apiContract.haiResultSchema(AuditBackupReceiptSchema)),
62
- list: apiContract.route({ method: 'POST', path: '/admin/audit/list', tags: ['admin'] })
134
+ backup: apiContract.route({
135
+ method: 'POST',
136
+ path: '/admin/audit/backup',
137
+ tags: ['admin'],
138
+ }).input(z.object({
139
+ id: z.uuid(),
140
+ }).strict()).output(apiContract.haiResultSchema(AuditBackupReceiptSchema)),
141
+ list: apiContract.route({
142
+ method: 'POST',
143
+ path: '/admin/audit/list',
144
+ tags: ['admin'],
145
+ })
63
146
  .input(AdminAuditInputSchema)
64
147
  .output(apiContract.haiResultSchema(AdminAuditSchema)),
65
148
  },
66
149
  }
67
150
 
68
151
  export const profileContract = {
69
- update: apiContract.route({ method: 'POST', path: '/profile/update', tags: ['profile'] })
152
+ update: apiContract.route({
153
+ method: 'POST',
154
+ path: '/profile/update',
155
+ tags: ['profile'],
156
+ })
70
157
  .input(ProfileUpdateSchema)
71
158
  .output(apiContract.haiResultSchema(AdminUserSchema)),
72
- avatar: apiContract.route({ method: 'POST', path: '/profile/avatar', tags: ['profile'] })
159
+ avatar: apiContract.route({
160
+ method: 'POST',
161
+ path: '/profile/avatar',
162
+ tags: ['profile'],
163
+ })
73
164
  .input(AvatarUploadSchema)
74
165
  .output(apiContract.haiResultSchema(AvatarOutputSchema)),
75
166
  }