@h-ai/cli 0.1.0-alpha.20 → 0.1.0-alpha.22

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 (182) hide show
  1. package/README.md +10 -7
  2. package/dist/{chunk-GSMXGUYF.js → chunk-5IGMJVV5.js} +56 -38
  3. package/dist/chunk-5IGMJVV5.js.map +1 -0
  4. package/dist/{chunk-3BBXDQ4Y.js → chunk-CFCXOKGV.js} +24 -2
  5. package/dist/chunk-CFCXOKGV.js.map +1 -0
  6. package/dist/cli-config-templates-5J5VKWWD.js +5 -0
  7. package/dist/{cli-config-templates-IGGO7P45.js.map → cli-config-templates-5J5VKWWD.js.map} +1 -1
  8. package/dist/cli-skill-templates-JMSV2SVW.js +5 -0
  9. package/dist/{cli-skill-templates-IMYZPQRG.js.map → cli-skill-templates-JMSV2SVW.js.map} +1 -1
  10. package/dist/index.js +567 -75
  11. package/dist/index.js.map +1 -1
  12. package/package.json +3 -2
  13. package/templates/README.md +40 -29
  14. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +16 -0
  15. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +38 -0
  16. package/templates/apps/api/apps/{{projectName}}-contract/src/app-contract.ts.hbs +27 -0
  17. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +33 -0
  18. package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +3 -0
  19. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +12 -0
  20. package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +18 -0
  21. package/templates/apps/api/apps/{{projectName}}-contract/tsconfig.json.hbs +12 -0
  22. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +11 -0
  23. package/templates/apps/api/apps/{{projectName}}-contract/vitest.config.ts.hbs +9 -0
  24. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +17 -0
  25. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +19 -0
  26. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +26 -0
  27. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +55 -0
  28. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +98 -0
  29. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +21 -0
  30. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +184 -0
  31. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/app-procedures.ts.hbs +49 -0
  32. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +18 -0
  33. package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +168 -0
  34. package/templates/apps/api/apps/{{projectName}}-service/tsconfig.json.hbs +16 -0
  35. package/templates/apps/api/apps/{{projectName}}-service/tsup.config.ts.hbs +11 -0
  36. package/templates/apps/api/apps/{{projectName}}-service/vitest.config.ts.hbs +9 -0
  37. package/templates/apps/api/e2e/api-service.spec.ts.hbs +18 -0
  38. package/templates/apps/api/eslint.config.js +32 -0
  39. package/templates/apps/api/package.json.hbs +29 -0
  40. package/templates/apps/api/playwright.config.ts.hbs +58 -0
  41. package/templates/apps/api/pnpm-workspace.yaml.hbs +14 -0
  42. package/templates/apps/api/tsconfig.base.json +15 -0
  43. package/templates/apps/fullstack/apps/{{projectName}}-app/.env.example.hbs +10 -0
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -7
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +18 -0
  46. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +1 -1
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +16 -0
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +6 -0
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +6 -0
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +51 -29
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +186 -0
  52. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.css.hbs +2 -0
  53. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +0 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +18 -0
  55. package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +14 -0
  56. package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +1 -5
  57. package/templates/apps/fullstack/apps/{{projectName}}-app/tsconfig.json.hbs +10 -4
  58. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +4 -3
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/.env.example.hbs +6 -0
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +20 -6
  61. package/templates/apps/fullstack/apps/{{projectName}}-desktop/eslint.config.js.hbs +1 -1
  62. package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +15 -0
  63. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +33 -30
  64. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +152 -0
  65. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.d.ts.hbs +1 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/main.ts.hbs +13 -0
  67. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +22 -0
  68. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/build.rs.hbs +6 -0
  69. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/capabilities/default.json.hbs +12 -0
  70. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/128x128.png +0 -0
  71. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/128x128@2x.png +0 -0
  72. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/32x32.png +0 -0
  73. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/icon.icns +0 -0
  74. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/icon.ico +0 -0
  75. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +16 -0
  76. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/main.rs.hbs +8 -0
  77. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +41 -0
  78. package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +2 -8
  79. package/templates/apps/fullstack/apps/{{projectName}}-desktop/tsconfig.json.hbs +9 -4
  80. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +44 -21
  81. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +15 -6
  82. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +1 -1
  83. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +15 -0
  84. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +25 -27
  85. package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +153 -0
  86. package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +0 -1
  87. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +14 -0
  88. package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +1 -5
  89. package/templates/apps/fullstack/apps/{{projectName}}-web/tsconfig.json.hbs +10 -4
  90. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +3 -2
  91. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +2 -1
  92. package/templates/apps/fullstack/eslint.config.js +0 -1
  93. package/templates/apps/fullstack/package.json.hbs +18 -17
  94. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +3 -3
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +2 -2
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +2 -2
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +1 -1
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/{server-app.test.ts.hbs → app-server.test.ts.hbs} +1 -1
  99. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +9 -8
  100. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +1 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +5 -1
  102. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +1 -1
  103. package/templates/apps/fullstack/packages/{{projectName}}-shared/tsconfig.json.hbs +1 -1
  104. package/templates/apps/fullstack/playwright.config.ts.hbs +41 -4
  105. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +15 -0
  106. package/templates/apps/mobile-app/.env.hbs +3 -0
  107. package/templates/apps/{android-app → mobile-app}/capacitor.config.ts.hbs +2 -2
  108. package/templates/apps/mobile-app/index.html.hbs +13 -0
  109. package/templates/apps/mobile-app/messages/en-US.json +36 -0
  110. package/templates/apps/mobile-app/messages/zh-CN.json +36 -0
  111. package/templates/apps/mobile-app/src/App.svelte.hbs +162 -0
  112. package/templates/apps/mobile-app/src/lib/api.ts.hbs +54 -0
  113. package/templates/apps/{android-app → mobile-app}/src/lib/capacitor.ts +5 -6
  114. package/templates/apps/mobile-app/src/main.ts.hbs +11 -0
  115. package/templates/base/.gitignore.hbs +16 -0
  116. package/templates/base/{eslint.config.js → eslint.config.js.hbs} +4 -1
  117. package/templates/base/package.json.hbs +52 -33
  118. package/templates/base/playwright.config.ts +30 -3
  119. package/templates/base/pnpm-workspace.yaml.hbs +14 -0
  120. package/templates/base/src/app.d.ts.hbs +3 -0
  121. package/templates/base/src/app.html.hbs +3 -0
  122. package/templates/base/svelte.config.js.hbs +43 -3
  123. package/templates/base/tests/scaffold.test.ts.hbs +7 -0
  124. package/templates/base/tsconfig.json.hbs +50 -0
  125. package/templates/base/vite.config.ts.hbs +10 -0
  126. package/templates/skills/AGENTS.md +57 -24
  127. package/templates/skills/CLAUDE.md +18 -7
  128. package/templates/skills/bridges/admin/AGENTS.md +66 -0
  129. package/templates/skills/bridges/admin/CLAUDE.md +19 -0
  130. package/templates/skills/bridges/admin/copilot-instructions.md +46 -0
  131. package/templates/skills/bridges/api/AGENTS.md +65 -0
  132. package/templates/skills/bridges/api/CLAUDE.md +19 -0
  133. package/templates/skills/bridges/api/copilot-instructions.md +46 -0
  134. package/templates/skills/bridges/fullstack/AGENTS.md +84 -0
  135. package/templates/skills/bridges/fullstack/CLAUDE.md +19 -0
  136. package/templates/skills/bridges/fullstack/copilot-instructions.md +54 -0
  137. package/templates/skills/bridges/h5/AGENTS.md +65 -0
  138. package/templates/skills/bridges/h5/CLAUDE.md +19 -0
  139. package/templates/skills/bridges/h5/copilot-instructions.md +44 -0
  140. package/templates/skills/bridges/mobile-app/AGENTS.md +77 -0
  141. package/templates/skills/bridges/mobile-app/CLAUDE.md +19 -0
  142. package/templates/skills/bridges/mobile-app/copilot-instructions.md +45 -0
  143. package/templates/skills/bridges/website/AGENTS.md +65 -0
  144. package/templates/skills/bridges/website/CLAUDE.md +19 -0
  145. package/templates/skills/bridges/website/copilot-instructions.md +44 -0
  146. package/templates/skills/copilot-instructions.md +27 -64
  147. package/templates/skills/hai-app-create/SKILL.md +1 -0
  148. package/templates/skills/hai-app-review/SKILL.md +1 -1
  149. package/templates/skills/hai-app-tests/SKILL.md +28 -3
  150. package/templates/skills/hai-capacitor/SKILL.md +14 -22
  151. package/templates/skills/hai-ui/SKILL.md +8 -2
  152. package/templates/skills/opencode.json +1 -6
  153. package/dist/chunk-3BBXDQ4Y.js.map +0 -1
  154. package/dist/chunk-GSMXGUYF.js.map +0 -1
  155. package/dist/cli-config-templates-IGGO7P45.js +0 -5
  156. package/dist/cli-skill-templates-IMYZPQRG.js +0 -5
  157. package/templates/apps/android-app/.env.hbs +0 -3
  158. package/templates/apps/android-app/messages/en-US.json +0 -24
  159. package/templates/apps/android-app/messages/zh-CN.json +0 -24
  160. package/templates/apps/android-app/src/lib/api.ts.hbs +0 -32
  161. package/templates/apps/android-app/src/routes/+layout.svelte.hbs +0 -43
  162. package/templates/apps/android-app/src/routes/+layout.ts +0 -7
  163. package/templates/apps/android-app/src/routes/+page.svelte.hbs +0 -38
  164. package/templates/apps/api/src/routes/+layout.svelte +0 -14
  165. package/templates/apps/api/src/routes/+page.svelte +0 -11
  166. package/templates/apps/api/src/routes/api/v1/health/+server.ts +0 -14
  167. package/templates/apps/api/src/routes/api/v1/items/+server.ts +0 -37
  168. package/templates/apps/api/src/routes/api/v1/items/[id]/+server.ts +0 -30
  169. package/templates/apps/api/tests/health.test.ts.hbs +0 -26
  170. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.html.hbs +0 -14
  171. package/templates/apps/fullstack/apps/{{projectName}}-app/src/routes/+layout.svelte.hbs +0 -18
  172. package/templates/apps/fullstack/apps/{{projectName}}-app/src/routes/+page.svelte.hbs +0 -162
  173. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.html.hbs +0 -14
  174. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/routes/+layout.svelte.hbs +0 -18
  175. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/routes/+page.svelte.hbs +0 -162
  176. package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.html.hbs +0 -14
  177. package/templates/apps/fullstack/apps/{{projectName}}-web/src/routes/+layout.svelte.hbs +0 -18
  178. package/templates/apps/fullstack/apps/{{projectName}}-web/src/routes/+page.svelte.hbs +0 -162
  179. package/templates/apps/fullstack/pnpm-workspace.yaml +0 -7
  180. package/templates/base/tsconfig.json +0 -20
  181. /package/templates/apps/fullstack/packages/{{projectName}}-serv/src/{server-app.ts.hbs → app-server.ts.hbs} +0 -0
  182. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/{theme.svelte.ts.hbs → theme-store.svelte.ts.hbs} +0 -0
@@ -0,0 +1,184 @@
1
+ {{#if features.ai}}import type { AIConfigInput } from '@h-ai/ai'
2
+ {{/if}}{{#if features.cache}}import type { CacheConfigInput } from '@h-ai/cache'
3
+ {{/if}}{{#if features.iam}}import type { IamConfigInput } from '@h-ai/iam'
4
+ {{/if}}{{#if features.crypto}}import type { ServConfig } from '@h-ai/serv'
5
+ {{/if}}{{#if features.storage}}import type { StorageConfigInput } from '@h-ai/storage'
6
+ {{/if}}{{#if features.ai}}import { ai, AIConfigSchema } from '@h-ai/ai'
7
+ {{/if}}{{#if features.cache}}import { cache, CacheConfigSchema } from '@h-ai/cache'
8
+ {{/if}}import { core } from '@h-ai/core'
9
+ {{#if features.crypto}}import { crypto } from '@h-ai/crypto'
10
+ {{/if}}{{#if features.iam}}import { iam, IamConfigSchema } from '@h-ai/iam'
11
+ {{/if}}{{#if features.db}}import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
12
+ {{/if}}import { ServConfigSchema } from '@h-ai/serv'
13
+ {{#if features.storage}}import { storage, StorageConfigSchema } from '@h-ai/storage'
14
+ {{/if}}{{#if features.vecdb}}import { vecdb, VecdbConfigSchema } from '@h-ai/vecdb'
15
+ {{/if}}
16
+
17
+ {{#if features.db}}type DbConfigInput = Parameters<typeof reldb.init>[0]
18
+ {{/if}}{{#if features.vecdb}}type VecdbConfigInput = Parameters<typeof vecdb.init>[0]
19
+ {{/if}}
20
+
21
+ let initialized = false
22
+ let initPromise: Promise<void> | null = null
23
+ {{#if features.crypto}}let cryptoInitialized = false
24
+ {{/if}}
25
+
26
+ export async function initApp(): Promise<void> {
27
+ if (initialized)
28
+ return
29
+ if (initPromise)
30
+ return initPromise
31
+
32
+ initPromise = doInit()
33
+ try {
34
+ await initPromise
35
+ initialized = true
36
+ }
37
+ finally {
38
+ initPromise = null
39
+ }
40
+ }
41
+
42
+ async function doInit(): Promise<void> {
43
+ core.init({
44
+ configDir: './config',
45
+ logging: { level: 'info' },
46
+ })
47
+
48
+ const servValidation = core.config.validate('serv', ServConfigSchema)
49
+ if (!servValidation.success) {
50
+ throw new Error(`Serv config invalid: ${servValidation.error.message}`)
51
+ }
52
+ {{#if features.crypto}}
53
+ const servConfig = core.config.getOrThrow<ServConfig>('serv')
54
+ if (servConfig.transport !== false) {
55
+ const cryptoResult = await crypto.init()
56
+ if (!cryptoResult.success) {
57
+ throw new Error(`Crypto initialization failed: ${cryptoResult.error.message}`)
58
+ }
59
+ cryptoInitialized = true
60
+ }
61
+ {{/if}}{{#if features.db}}
62
+
63
+ const dbValidation = core.config.validate('db', ReldbConfigSchema)
64
+ if (!dbValidation.success) {
65
+ throw new Error(`DB config invalid: ${dbValidation.error.message}`)
66
+ }
67
+ {{/if}}{{#if features.cache}}
68
+
69
+ const cacheValidation = core.config.validate('cache', CacheConfigSchema)
70
+ if (!cacheValidation.success) {
71
+ throw new Error(`Cache config invalid: ${cacheValidation.error.message}`)
72
+ }
73
+ {{/if}}{{#if features.vecdb}}
74
+
75
+ const vecdbValidation = core.config.validate('vecdb', VecdbConfigSchema)
76
+ if (!vecdbValidation.success) {
77
+ throw new Error(`VecDB config invalid: ${vecdbValidation.error.message}`)
78
+ }
79
+ {{/if}}{{#if features.iam}}
80
+
81
+ const iamValidation = core.config.validate('iam', IamConfigSchema)
82
+ if (!iamValidation.success) {
83
+ throw new Error(`IAM config invalid: ${iamValidation.error.message}`)
84
+ }
85
+ {{/if}}{{#if features.storage}}
86
+
87
+ const storageValidation = core.config.validate('storage', StorageConfigSchema)
88
+ if (!storageValidation.success) {
89
+ throw new Error(`Storage config invalid: ${storageValidation.error.message}`)
90
+ }
91
+ {{/if}}{{#if features.ai}}
92
+
93
+ const aiValidation = core.config.validate('ai', AIConfigSchema)
94
+ if (!aiValidation.success) {
95
+ throw new Error(`AI config invalid: ${aiValidation.error.message}`)
96
+ }
97
+ {{/if}}{{#if features.db}}
98
+
99
+ const dbConfig = core.config.getOrThrow<DbConfigInput>('db')
100
+ {{/if}}{{#if features.cache}} const cacheConfig = core.config.getOrThrow<CacheConfigInput>('cache')
101
+ {{/if}}{{#if features.vecdb}} const vecdbConfig = core.config.getOrThrow<VecdbConfigInput>('vecdb')
102
+ {{/if}}{{#if features.iam}} const iamConfig = core.config.getOrThrow<IamConfigInput>('iam')
103
+ {{/if}}{{#if features.storage}} const storageConfig = core.config.getOrThrow<StorageConfigInput>('storage')
104
+ {{/if}}{{#if features.ai}} const aiConfig = core.config.getOrThrow<AIConfigInput>('ai')
105
+ {{/if}}{{#if features.db}}
106
+
107
+ if (dbConfig.type === 'sqlite') {
108
+ const fs = await import('node:fs')
109
+ const path = await import('node:path')
110
+ const dbDir = path.dirname(dbConfig.database)
111
+ if (!fs.existsSync(dbDir)) {
112
+ fs.mkdirSync(dbDir, { recursive: true })
113
+ }
114
+ }
115
+
116
+ const dbResult = await reldb.init(dbConfig)
117
+ if (!dbResult.success) {
118
+ throw new Error(`Database initialization failed: ${dbResult.error.message}`)
119
+ }
120
+ {{/if}}{{#if features.cache}}
121
+
122
+ const cacheResult = await cache.init(cacheConfig)
123
+ if (!cacheResult.success) {
124
+ throw new Error(`Cache initialization failed: ${cacheResult.error.message}`)
125
+ }
126
+ {{/if}}{{#if features.vecdb}}
127
+
128
+ const vecdbResult = await vecdb.init(vecdbConfig)
129
+ if (!vecdbResult.success) {
130
+ throw new Error(`VecDB initialization failed: ${vecdbResult.error.message}`)
131
+ }
132
+ {{/if}}{{#if features.iam}}
133
+
134
+ const iamResult = await iam.init(iamConfig)
135
+ if (!iamResult.success) {
136
+ throw new Error(`IAM initialization failed: ${iamResult.error.message}`)
137
+ }
138
+ {{/if}}{{#if features.storage}}
139
+
140
+ const storageResult = await storage.init(storageConfig)
141
+ if (!storageResult.success) {
142
+ throw new Error(`Storage initialization failed: ${storageResult.error.message}`)
143
+ }
144
+ {{/if}}{{#if features.ai}}
145
+
146
+ const aiResult = await ai.init(aiConfig)
147
+ if (!aiResult.success) {
148
+ throw new Error(`AI initialization failed: ${aiResult.error.message}`)
149
+ }
150
+ {{/if}}
151
+
152
+ core.logger.info('API service initialized')
153
+ }
154
+
155
+ export async function closeApp(): Promise<void> {
156
+ if (!initialized && !initPromise)
157
+ return
158
+
159
+ const closers: Array<readonly [string, () => unknown]> = [
160
+ {{#if features.ai}} ['ai', () => ai.close()],
161
+ {{/if}}{{#if features.storage}} ['storage', () => storage.close()],
162
+ {{/if}}{{#if features.iam}} ['iam', () => iam.close()],
163
+ {{/if}}{{#if features.vecdb}} ['vecdb', () => vecdb.close()],
164
+ {{/if}}{{#if features.cache}} ['cache', () => cache.close()],
165
+ {{/if}}{{#if features.db}} ['reldb', () => reldb.close()],
166
+ {{/if}} ]
167
+ {{#if features.crypto}} if (cryptoInitialized) {
168
+ closers.push(['crypto', () => crypto.close()])
169
+ }
170
+ {{/if}}
171
+
172
+ for (const [name, close] of closers) {
173
+ try {
174
+ await close()
175
+ }
176
+ catch (error) {
177
+ core.logger.warn(`Module close failed: ${name}`, { error })
178
+ }
179
+ }
180
+
181
+ initialized = false
182
+ {{#if features.crypto}} cryptoInitialized = false
183
+ {{/if}} core.logger.info('API service closed')
184
+ }
@@ -0,0 +1,49 @@
1
+ import type { ServContext } from '@h-ai/serv'
2
+ {{#if features.iam}}import type { AppEchoInput, AppEchoOutputData, AppInfoOutputData } from '{{api.contractPackageName}}'
3
+ {{else}}import type { AppEchoOutputData, AppInfoOutputData } from '{{api.contractPackageName}}'
4
+ {{/if}}
5
+ import { ok } from '@h-ai/core'
6
+ import { serv } from '@h-ai/serv'
7
+ import { appContract } from '{{api.contractPackageName}}'
8
+
9
+ export interface AppProcedureDeps {
10
+ readonly info: {
11
+ readonly name: string
12
+ readonly version: string
13
+ readonly transportEnabled: boolean
14
+ }
15
+ readonly startedAt: number
16
+ }
17
+
18
+ const p = serv.implement(appContract).$context<ServContext>()
19
+
20
+ /** 创建应用自有 procedures。 */
21
+ export function createAppProcedures(deps: AppProcedureDeps) {
22
+ return p.router({
23
+ info: p.info.handler(() => {
24
+ const data: AppInfoOutputData = {
25
+ name: deps.info.name,
26
+ version: deps.info.version,
27
+ transportEnabled: deps.info.transportEnabled,
28
+ uptimeMs: Math.max(0, Date.now() - deps.startedAt),
29
+ }
30
+ return ok(data)
31
+ }),
32
+ echo: {{#if features.iam}}p.echo.handler(serv.requireAuth<AppEchoInput, AppEchoOutputData>(({ input, context }) => {
33
+ const data: AppEchoOutputData = {
34
+ message: input.message,
35
+ userId: context.session!.userId,
36
+ requestId: context.requestId,
37
+ timestamp: new Date().toISOString(),
38
+ }
39
+ return Promise.resolve(ok(data))
40
+ })){{else}}p.echo.handler(({ input, context }) => {
41
+ const data: AppEchoOutputData = {
42
+ message: input.message,
43
+ requestId: context.requestId,
44
+ timestamp: new Date().toISOString(),
45
+ }
46
+ return ok(data)
47
+ }){{/if}},
48
+ })
49
+ }
@@ -0,0 +1,18 @@
1
+ import type { AppProcedureDeps } from './app-procedures.js'
2
+ {{#if features.ai}}import { ai } from '@h-ai/ai'
3
+ {{/if}}{{#if features.iam}}import { iam } from '@h-ai/iam'
4
+ {{/if}}{{#if features.ai}}import { createAiProcedures } from '@h-ai/serv/features/ai'
5
+ {{/if}}{{#if features.iam}}import { createIamProcedures } from '@h-ai/serv/features/iam'
6
+ {{/if}}{{#if features.storage}}import { createStorageProcedures } from '@h-ai/serv/features/storage'
7
+ import { storage } from '@h-ai/storage'
8
+ {{/if}}import { createAppProcedures } from './app-procedures.js'
9
+
10
+ /** 创建应用级 procedures 聚合。 */
11
+ export function {{api.serviceProceduresFactoryName}}(deps: { app: AppProcedureDeps }) {
12
+ return {
13
+ {{#if features.iam}} iam: createIamProcedures({ iam }),
14
+ {{/if}}{{#if features.storage}} storage: createStorageProcedures({ storage }),
15
+ {{/if}}{{#if features.ai}} ai: createAiProcedures({ ai }),
16
+ {{/if}} app: createAppProcedures(deps.app),
17
+ }
18
+ }
@@ -0,0 +1,168 @@
1
+ import type { AddressInfo } from 'node:net'
2
+ import { mkdtempSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ {{#if features.ai}}import { ai } from '@h-ai/ai'
6
+ {{/if}}import { apiClient } from '@h-ai/api-client'
7
+ {{#if features.cache}}import { cache } from '@h-ai/cache'
8
+ {{/if}}import { core } from '@h-ai/core'
9
+ {{#if features.iam}}import { iam } from '@h-ai/iam'
10
+ {{/if}}{{#if features.db}}import { reldb } from '@h-ai/reldb'
11
+ {{/if}}import { serv } from '@h-ai/serv'
12
+ {{#if features.storage}}import { storage } from '@h-ai/storage'
13
+ {{/if}}{{#if features.vecdb}}import { vecdb } from '@h-ai/vecdb'
14
+ {{/if}}import { {{api.contractExportName}} } from '{{api.contractPackageName}}'
15
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
16
+
17
+ import { {{api.serviceAppFactoryName}} } from '../src/app.js'
18
+
19
+ let tempRoot = ''
20
+ let baseUrl = ''
21
+ let client: ReturnType<typeof apiClient.create<typeof {{api.contractExportName}}>>
22
+ let server: ReturnType<typeof serv.listen>
23
+ {{#if features.storage}}let storageRoot = ''
24
+ {{/if}}{{#if features.vecdb}}let vecdbRoot = ''
25
+ {{/if}}
26
+
27
+ {{#if features.iam}}
28
+ function uniqueUser(prefix = 'api') {
29
+ const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(-8)
30
+ return {
31
+ username: `${prefix}_${suffix}`,
32
+ password: 'TestPass123',
33
+ email: `${prefix}_${suffix}@test.local`,
34
+ }
35
+ }
36
+ {{/if}}
37
+
38
+ beforeAll(async () => {
39
+ tempRoot = mkdtempSync(join(tmpdir(), '{{projectName}}-service-test-'))
40
+ {{#if features.storage}} storageRoot = join(tempRoot, 'storage')
41
+ {{/if}}{{#if features.vecdb}} vecdbRoot = join(tempRoot, 'vecdb')
42
+ {{/if}}
43
+
44
+ core.init({
45
+ configDir: './config',
46
+ logging: { level: 'warn' },
47
+ })
48
+ {{#if features.db}}
49
+
50
+ const dbResult = await reldb.init({ type: 'sqlite', database: ':memory:' })
51
+ if (!dbResult.success)
52
+ throw new Error(`reldb: ${dbResult.error.message}`)
53
+ {{/if}}{{#if features.cache}}
54
+
55
+ const cacheResult = await cache.init({ type: 'memory' })
56
+ if (!cacheResult.success)
57
+ throw new Error(`cache: ${cacheResult.error.message}`)
58
+ {{/if}}{{#if features.vecdb}}
59
+
60
+ const vecdbResult = await vecdb.init({ type: 'lancedb', path: vecdbRoot })
61
+ if (!vecdbResult.success)
62
+ throw new Error(`vecdb: ${vecdbResult.error.message}`)
63
+ {{/if}}{{#if features.iam}}
64
+
65
+ const iamResult = await iam.init({})
66
+ if (!iamResult.success)
67
+ throw new Error(`iam: ${iamResult.error.message}`)
68
+ {{/if}}{{#if features.storage}}
69
+
70
+ const storageResult = await storage.init({ type: 'local', root: storageRoot })
71
+ if (!storageResult.success)
72
+ throw new Error(`storage: ${storageResult.error.message}`)
73
+ {{/if}}{{#if features.ai}}
74
+
75
+ const aiResult = await ai.init({})
76
+ if (!aiResult.success)
77
+ throw new Error(`ai: ${aiResult.error.message}`)
78
+ {{/if}}
79
+
80
+ const app = {{api.serviceAppFactoryName}}({
81
+ transport: 'disabled',{{#if features.iam}}
82
+ refreshCookie: 'disabled',{{/if}}
83
+ })
84
+ const listening = new Promise<AddressInfo>((resolve) => {
85
+ server = serv.listen(app, {
86
+ host: '127.0.0.1',
87
+ port: 0,
88
+ onListening: info => resolve(info as AddressInfo),
89
+ })
90
+ })
91
+ const address = await listening
92
+ baseUrl = `http://127.0.0.1:${address.port}`
93
+
94
+ client = apiClient.create({{api.contractExportName}})
95
+ const initResult = await client.init({
96
+ baseUrl: `${baseUrl}/api/v1`,
97
+ auth: { storage: apiClient.tokenStorage.memory() },
98
+ })
99
+ if (!initResult.success)
100
+ throw new Error(`client.init: ${initResult.error.message}`)
101
+ }, 30_000)
102
+
103
+ afterAll(async () => {
104
+ await client?.close()
105
+ await server?.close()
106
+ {{#if features.ai}} await ai.close()
107
+ {{/if}}{{#if features.storage}} await storage.close()
108
+ {{/if}}{{#if features.iam}} await iam.close()
109
+ {{/if}}{{#if features.vecdb}} await vecdb.close()
110
+ {{/if}}{{#if features.cache}} await cache.close()
111
+ {{/if}}{{#if features.db}} await reldb.close()
112
+ {{/if}} rmSync(tempRoot, { recursive: true, force: true })
113
+ })
114
+
115
+ describe('{{api.servicePackageName}}', () => {
116
+ it('returns health status', async () => {
117
+ const response = await fetch(`${baseUrl}/health`)
118
+
119
+ expect(response.status).toBe(200)
120
+ await expect(response.json()).resolves.toEqual({ status: 'ok' })
121
+ })
122
+
123
+ it('returns app info as HaiResult', async () => {
124
+ const result = await client.app.info()
125
+
126
+ expect(result.success).toBe(true)
127
+ if (result.success) {
128
+ expect(result.data.name).toContain('{{projectName}}')
129
+ expect(typeof result.data.transportEnabled).toBe('boolean')
130
+ }
131
+ })
132
+
133
+ it('validates echo input', async () => {
134
+ const result = await client.app.echo({ message: '' })
135
+
136
+ expect(result.success).toBe(false)
137
+ })
138
+
139
+ {{#if features.iam}}
140
+ it('registers a user and returns authenticated echo context', async () => {
141
+ const user = uniqueUser('svc')
142
+ const register = await client.iam.auth.register({
143
+ username: user.username,
144
+ password: user.password,
145
+ email: user.email,
146
+ })
147
+
148
+ expect(register.success).toBe(true)
149
+
150
+ const result = await client.app.echo({ message: 'hello unit' })
151
+ expect(result.success).toBe(true)
152
+ if (result.success) {
153
+ expect(result.data.message).toBe('hello unit')
154
+ expect(result.data.userId).toBeTruthy()
155
+ }
156
+ })
157
+ {{else}}
158
+ it('returns echo result as HaiResult', async () => {
159
+ const result = await client.app.echo({ message: 'hello unit' })
160
+
161
+ expect(result.success).toBe(true)
162
+ if (result.success) {
163
+ expect(result.data.message).toBe('hello unit')
164
+ expect(result.data.userId).toBeUndefined()
165
+ }
166
+ })
167
+ {{/if}}
168
+ })
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "moduleResolution": "bundler",
5
+ "types": ["node"],
6
+ "verbatimModuleSyntax": true,
7
+ "skipLibCheck": true
8
+ },
9
+ "include": [
10
+ "src/**/*.ts"
11
+ ],
12
+ "exclude": [
13
+ "node_modules",
14
+ "dist"
15
+ ]
16
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ target: 'node22',
7
+ sourcemap: true,
8
+ clean: true,
9
+ dts: false,
10
+ external: ['@lancedb/lancedb'],
11
+ })
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ },
9
+ })
@@ -0,0 +1,18 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ test('service health endpoint returns ok', async ({ request }) => {
4
+ const response = await request.get('/health')
5
+
6
+ expect(response.status()).toBe(200)
7
+ await expect(response.json()).resolves.toEqual({ status: 'ok' })
8
+ })
9
+
10
+ test('service app.info endpoint returns HaiResult', async ({ request }) => {
11
+ const response = await request.post('/api/v1/app/info')
12
+ const body = await response.json()
13
+
14
+ expect(response.status()).toBe(200)
15
+ expect(body.success).toBe(true)
16
+ expect(body.data.name).toContain('{{projectName}}')
17
+ expect(typeof body.data.transportEnabled).toBe('boolean')
18
+ })
@@ -0,0 +1,32 @@
1
+ import antfu from '@antfu/eslint-config'
2
+
3
+ export default antfu({
4
+ formatters: true,
5
+ ignores: [
6
+ '**/dist/**',
7
+ '**/node_modules/**',
8
+ '**/.agents/**',
9
+ '**/.github/**',
10
+ '**/AGENTS.md',
11
+ '**/CLAUDE.md',
12
+ '**/README.md',
13
+ '**/opencode.json',
14
+ '**/package.json',
15
+ ],
16
+ }, {
17
+ files: ['**/*.js', '**/*.mjs', '**/*.ts'],
18
+ rules: {
19
+ 'no-console': 'error',
20
+ 'ts/no-explicit-any': 'error',
21
+ },
22
+ }, {
23
+ files: ['**/tsconfig.json', '**/tsconfig.*.json'],
24
+ rules: {
25
+ 'jsonc/sort-keys': 'off',
26
+ },
27
+ }, {
28
+ files: ['**/index.ts'],
29
+ rules: {
30
+ 'perfectionist/sort-exports': 'off',
31
+ },
32
+ })
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "packageManager": "pnpm@9.15.0",
7
+ "description": "Typed hai API workspace with separated contract and service packages.",
8
+ "scripts": {
9
+ "dev": "pnpm --filter {{api.servicePackageName}} dev",
10
+ "compile": "pnpm typecheck && pnpm build",
11
+ "build": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present build",
12
+ "typecheck": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present typecheck",
13
+ "lint": "pnpm -r --if-present lint",
14
+ "test": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present test",
15
+ "test:e2e": "pnpm build && playwright test",
16
+ "clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
17
+ },
18
+ "devDependencies": {
19
+ "@antfu/eslint-config": "{{api.packageSpecifiers.antfuEslintConfig}}",
20
+ "@playwright/test": "{{api.packageSpecifiers.playwright}}",
21
+ "@types/node": "{{api.packageSpecifiers.nodeTypes}}",
22
+ "eslint": "{{api.packageSpecifiers.eslint}}",
23
+ "eslint-plugin-format": "{{api.packageSpecifiers.eslintPluginFormat}}",
24
+ "rimraf": "{{api.packageSpecifiers.rimraf}}",
25
+ "tsup": "{{api.packageSpecifiers.tsup}}",
26
+ "typescript": "{{api.packageSpecifiers.typescript}}",
27
+ "vitest": "{{api.packageSpecifiers.vitest}}"
28
+ }
29
+ }
@@ -0,0 +1,58 @@
1
+ import process from 'node:process'
2
+ import { defineConfig } from '@playwright/test'
3
+
4
+ const defaultBaseURL = 'http://127.0.0.1:3000'
5
+ const baseURL = process.env.BASE_URL || defaultBaseURL
6
+ const serviceHost = resolveHostFromBaseUrl(baseURL, '127.0.0.1')
7
+ const servicePort = resolvePortFromBaseUrl(baseURL, '3000')
8
+
9
+ function resolveHostFromBaseUrl(urlString: string, fallbackHost: string): string {
10
+ try {
11
+ const url = new URL(urlString)
12
+ return url.hostname || fallbackHost
13
+ }
14
+ catch {
15
+ return fallbackHost
16
+ }
17
+ }
18
+
19
+ function resolvePortFromBaseUrl(urlString: string, fallbackPort: string): string {
20
+ try {
21
+ const url = new URL(urlString)
22
+ if (url.port) {
23
+ return url.port
24
+ }
25
+
26
+ return url.protocol === 'https:' ? '443' : '80'
27
+ }
28
+ catch {
29
+ return fallbackPort
30
+ }
31
+ }
32
+
33
+ export default defineConfig({
34
+ testDir: './e2e',
35
+ fullyParallel: false,
36
+ forbidOnly: !!process.env.CI,
37
+ retries: process.env.CI ? 2 : 0,
38
+ workers: 1,
39
+ reporter: 'list',
40
+ timeout: 30_000,
41
+ use: {
42
+ baseURL,
43
+ channel: 'chrome',
44
+ trace: 'on-first-retry',
45
+ screenshot: 'only-on-failure',
46
+ },
47
+ webServer: [{
48
+ name: '{{api.servicePackageName}}',
49
+ command: 'pnpm --filter {{api.servicePackageName}} start',
50
+ env: {
51
+ HOST: serviceHost,
52
+ PORT: servicePort,
53
+ },
54
+ url: `${baseURL}/health`,
55
+ reuseExistingServer: false,
56
+ timeout: 120_000,
57
+ }],
58
+ })
@@ -0,0 +1,14 @@
1
+ {{! @skipIfEmpty }}
2
+ shellEmulator: true
3
+
4
+ trustPolicy: no-downgrade
5
+
6
+ packages:
7
+ - apps/*
8
+
9
+ {{#if useCatalogProtocol}}
10
+ catalog:
11
+ {{#each haiCatalogPackages}}
12
+ '{{@key}}': {{this}}
13
+ {{/each}}
14
+ {{/if}}
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "ignoreDeprecations": "6.0",
7
+ "resolveJsonModule": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "isolatedModules": true,
12
+ "verbatimModuleSyntax": true,
13
+ "skipLibCheck": true
14
+ }
15
+ }
@@ -0,0 +1,10 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if fullstack.frontends.app}}
3
+ # {{projectName}}-app — 环境变量
4
+ # Android 模拟器访问宿主机后端时通常使用 http://10.0.2.2:3000。
5
+
6
+ PUBLIC_API_BASE=http://localhost:3000
7
+
8
+ # 仅用于 Capacitor live reload 前端页面。
9
+ # CAPACITOR_SERVER_URL=http://localhost:{{fullstack.ports.app}}
10
+ {{/if}}