@h-ai/cli 0.1.0-alpha.63 → 0.1.0-alpha.65

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 (104) hide show
  1. package/README.en.md +20 -4
  2. package/README.ja.md +20 -4
  3. package/README.md +28 -12
  4. package/dist/chunk-47V5OT22.js +5639 -0
  5. package/dist/chunk-47V5OT22.js.map +1 -0
  6. package/dist/{chunk-X3RSYNBG.js → chunk-IPG3QNYK.js} +62 -4
  7. package/dist/chunk-IPG3QNYK.js.map +1 -0
  8. package/dist/cli-config-templates-SJ2MBIR7.js +5 -0
  9. package/dist/{cli-config-templates-QRF6B5T4.js.map → cli-config-templates-SJ2MBIR7.js.map} +1 -1
  10. package/dist/{cli-skill-templates-EZFW7D43.js → cli-skill-templates-A5VIR5J6.js} +3 -3
  11. package/dist/{cli-skill-templates-EZFW7D43.js.map → cli-skill-templates-A5VIR5J6.js.map} +1 -1
  12. package/dist/index.js +524 -66
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-DUGLX4YB.js → src-6PPBKQ6Q.js} +666 -290
  15. package/dist/src-6PPBKQ6Q.js.map +1 -0
  16. package/package.json +9 -7
  17. package/templates/apps/admin/src/routes/+layout.svelte.hbs +4 -2
  18. package/templates/apps/api/.dockerignore.hbs +5 -0
  19. package/templates/apps/api/.gitignore.hbs +4 -4
  20. package/templates/apps/api/Dockerfile.hbs +11 -1
  21. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +2 -0
  22. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
  23. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
  24. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +9 -2
  25. package/templates/apps/api/package.json.hbs +9 -0
  26. package/templates/apps/fullstack/.dockerignore.hbs +5 -3
  27. package/templates/apps/fullstack/.gitignore.hbs +22 -9
  28. package/templates/apps/fullstack/Dockerfile.hbs +11 -1
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +6 -0
  30. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +7 -3
  31. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -0
  32. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +7 -3
  33. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +6 -0
  34. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +7 -3
  35. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +93 -1
  36. package/templates/apps/fullstack/package.json.hbs +12 -0
  37. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -2
  38. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +2 -0
  39. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +1 -1
  40. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +8 -14
  41. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +6 -4
  42. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -8
  43. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
  44. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +9 -2
  45. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +4 -4
  46. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +6 -2
  47. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +2 -0
  48. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +19 -13
  49. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +1 -1
  50. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +46 -0
  51. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +17 -3
  52. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +15 -5
  53. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -1
  54. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +0 -1
  55. package/templates/apps/h5/src/routes/+layout.svelte.hbs +13 -13
  56. package/templates/apps/mobile-app/src/App.svelte.hbs +46 -117
  57. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
  58. package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
  59. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
  60. package/templates/base/.dockerignore.hbs +5 -0
  61. package/templates/base/.gitignore.hbs +18 -4
  62. package/templates/base/Dockerfile.hbs +17 -1
  63. package/templates/base/e2e/app.spec.ts.hbs +14 -0
  64. package/templates/base/package.json.hbs +14 -2
  65. package/templates/base/tests/scaffold.test.ts.hbs +9 -3
  66. package/templates/base/vite.config.ts.hbs +6 -4
  67. package/templates/shared/hash-router.svelte.ts +83 -0
  68. package/templates/skills/AGENTS.md +15 -1
  69. package/templates/skills/bridges/admin/AGENTS.md +17 -1
  70. package/templates/skills/bridges/api/AGENTS.md +17 -3
  71. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +35 -11
  72. package/templates/skills/bridges/h5/AGENTS.md +17 -1
  73. package/templates/skills/bridges/mobile-app/AGENTS.md +19 -3
  74. package/templates/skills/bridges/website/AGENTS.md +17 -1
  75. package/templates/skills/hai-ai/reference.md +2 -2
  76. package/templates/skills/hai-app-create/SKILL.md +5 -3
  77. package/templates/skills/hai-app-create/development.md +126 -2
  78. package/templates/skills/hai-app-create/reference.md +2 -2
  79. package/templates/skills/hai-app-review/SKILL.md +2 -2
  80. package/templates/skills/hai-app-tests/SKILL.md +2 -1
  81. package/templates/skills/hai-app-tests/reference.md +1 -1
  82. package/templates/skills/hai-core/SKILL.md +2 -1
  83. package/templates/skills/hai-deploy/SKILL.md +42 -34
  84. package/templates/skills/hai-deploy/middleware.md +94 -0
  85. package/templates/skills/hai-framework-sync/SKILL.md +1 -1
  86. package/templates/skills/hai-framework-sync/sync.md +1 -1
  87. package/templates/skills/hai-reach/reference.md +8 -8
  88. package/templates/skills/hai-spec-implement/SKILL.md +12 -9
  89. package/templates/skills/hai-spec-implement/changes.md +67 -0
  90. package/templates/skills/hai-spec-implement/templates.md +25 -3
  91. package/templates/skills/hai-spec-implement/workflow.md +7 -6
  92. package/templates/skills/hai-spec-review/SKILL.md +14 -5
  93. package/templates/skills/hai-spec-review/checklist.md +4 -3
  94. package/templates/skills/hai-storage/SKILL.md +2 -0
  95. package/templates/skills/hai-ui/SKILL.md +4 -0
  96. package/dist/chunk-WW2MIO6T.js +0 -992
  97. package/dist/chunk-WW2MIO6T.js.map +0 -1
  98. package/dist/chunk-X3RSYNBG.js.map +0 -1
  99. package/dist/cli-config-templates-QRF6B5T4.js +0 -5
  100. package/dist/src-DUGLX4YB.js.map +0 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +0 -57
  102. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/AuditManagement.svelte.hbs → pages/audit.svelte.hbs} +0 -0
  103. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/RoleManagement.svelte.hbs → pages/roles.svelte.hbs} +0 -0
  104. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/UserManagement.svelte.hbs → pages/users.svelte.hbs} +0 -0
@@ -1,6 +1,9 @@
1
1
  <script lang='ts'>
2
2
  {{#if features.iam}}
3
3
  import type { IamPublicConfig } from '@h-ai/api-contract'
4
+ {{/if}}
5
+ import type { Component } from 'svelte'
6
+ {{#if features.iam}}
4
7
  import { apiClient } from '@h-ai/api-client'
5
8
  {{/if}}
6
9
  import { applyTheme, getSavedTheme, setGlobalLocale } from '@h-ai/ui'
@@ -8,9 +11,11 @@
8
11
  import { AuthPanel } from '@h-ai/ui/components/scenes/iam/index'
9
12
  {{/if}}
10
13
  import { onMount } from 'svelte'
11
- import { closeApi, initApi, resolveApiBase, usesNativeTokenStorage } from './lib/api.js'
14
+
15
+ import { closeApi, initApi, usesNativeTokenStorage } from './lib/api.js'
12
16
  import { appConfig, initClientCore } from './lib/app-config.js'
13
17
  import { initCapacitor, isNativeApp } from './lib/capacitor.js'
18
+ import { HashRouter } from './lib/navigation/hash-router.svelte.js'
14
19
  import * as m from './lib/paraglide/messages.js'
15
20
  import { getLocale } from './lib/paraglide/runtime.js'
16
21
 
@@ -31,11 +36,12 @@
31
36
  }
32
37
  {{/if}}
33
38
 
34
- type TabId = 'home' | 'tasks' | 'profile'
39
+ const pages = import.meta.glob<{ default: Component<{ nativeRuntime: boolean, nativeTokenStorage: boolean }> }>('./lib/pages/**/*.svelte')
40
+ const router = new HashRouter(Object.keys(pages), '/home')
41
+ const pageComponent = $derived(pages[router.file]())
35
42
 
36
43
  let booting = $state(true)
37
44
  let bootError = $state<string | null>(null)
38
- let activeTab = $state<TabId>('home')
39
45
  let nativeRuntime = $state(false)
40
46
  let nativeTokenStorage = $state(false)
41
47
 
@@ -74,136 +80,59 @@
74
80
  }
75
81
  }
76
82
 
83
+ /** 切换底部导航;例如 handleTabChange('tasks') 会写入 #/tasks。 */
77
84
  function handleTabChange(id: string): void {
78
- if (id === 'home' || id === 'tasks' || id === 'profile')
79
- activeTab = id
85
+ router.navigate(`/${id}`)
80
86
  }
81
87
 
82
88
  onMount(() => {
89
+ const stopRouting = router.start()
83
90
  {{#if features.iam}}
84
91
  resetToken = new URL(location.href).searchParams.get('token') ?? ''
85
92
  {{/if}}
86
93
  void bootstrap()
87
94
 
88
95
  return () => {
96
+ stopRouting()
89
97
  void closeApi()
90
98
  }
91
99
  })
92
100
  </script>
93
101
 
94
- {#if booting}
95
- <div class='hai-mobile-viewport'>
96
- <div class='hai-mobile-shell max-w-lg'>
97
- <div class='flex min-h-screen items-center justify-center'>
98
- <span class='loading loading-spinner loading-lg'></span>
99
- </div>
100
- </div>
101
- </div>
102
- {:else if bootError}
103
- <div class='hai-mobile-viewport'>
104
- <div class='hai-mobile-shell max-w-lg'>
105
- <div class='flex min-h-screen flex-col items-center justify-center gap-3 p-6 text-center'>
106
- <span class='icon-[tabler--alert-triangle] text-error text-4xl'></span>
107
- <h1 class='text-lg font-bold'>{m.boot_failed_title()}</h1>
108
- <p class='text-base-content/60 text-sm'>{bootError}</p>
109
- <Button variant='primary' size='sm' onclick={() => location.reload()}>{m.common_retry()}</Button>
110
- </div>
111
- </div>
112
- </div>
102
+ <div class='hai-mobile-viewport'>
103
+ <div class='hai-mobile-shell max-w-lg'>
104
+ <AppBar title={appConfig.name} safeArea fixed={false} class='bg-base-100/95 backdrop-blur border-base-content/8' />
105
+ <main class='hai-mobile-main'>
106
+ {#if booting}
107
+ <div class='grid min-h-64 place-items-center'><span class='loading loading-spinner loading-lg'></span></div>
108
+ {:else if bootError}
109
+ <section class='flex min-h-64 flex-col items-center justify-center gap-3 p-6 text-center'>
110
+ <h1 class='text-lg font-bold'>{m.boot_failed_title()}</h1>
111
+ <p class='text-base-content/60 text-sm'>{bootError}</p>
112
+ <Button variant='primary' size='sm' onclick={() => location.reload()}>{m.common_retry()}</Button>
113
+ </section>
113
114
  {{#if features.iam}}
114
- {:else if !authenticated}
115
- <main>
116
- <AuthPanel config={authConfig} actions={authActions} {resetToken} initialMode={resetToken ? 'reset' : 'login'} onauthenticated={handleAuthenticated} />
117
- </main>
115
+ {:else if !authenticated}
116
+ <AuthPanel config={authConfig} actions={authActions} {resetToken} initialMode={resetToken ? 'reset' : 'login'} onauthenticated={handleAuthenticated} />
117
+ {{/if}}
118
+ {:else}
119
+ {#await pageComponent}
120
+ <div class='grid min-h-64 place-items-center'><span class='loading loading-spinner'></span></div>
121
+ {:then { default: Page }}
122
+ <Page {nativeRuntime} {nativeTokenStorage} />
123
+ {/await}
124
+ {/if}
125
+ </main>
126
+ <footer class='border-t border-base-content/8 bg-base-100 p-2 text-center text-xs text-base-content/50'>
127
+ {appConfig.name} · {appConfig.version} · {appConfig.env}
128
+ </footer>
129
+ {{#if features.iam}}
130
+ {#if authenticated && !booting && !bootError}
131
+ {{/if}}
132
+ <BottomNav items={navItems} active={router.path.slice(1)} onchange={handleTabChange} safeArea centered maxWidth='lg' />
133
+ {{#if features.iam}}
134
+ {/if}
118
135
  {{/if}}
119
- {:else}
120
- <div class='hai-mobile-viewport'>
121
- <div class='hai-mobile-shell max-w-lg'>
122
- <AppBar title={appConfig.name} safeArea fixed={false} class='bg-base-100/95 backdrop-blur border-base-content/8' />
123
-
124
- <main class='hai-mobile-main'>
125
- {#if activeTab === 'home'}
126
- <section class='space-y-4 p-4 pb-6'>
127
- <div class='bg-primary text-primary-content rounded-2xl p-5 shadow-sm'>
128
- <p class='text-sm opacity-75'>{m.hero_eyebrow()}</p>
129
- <h1 class='mt-1 text-2xl font-bold'>{m.hero_title()}</h1>
130
- <p class='mt-2 text-sm opacity-80'>{m.hero_subtitle()}</p>
131
- </div>
132
-
133
- <div class='grid grid-cols-2 gap-3'>
134
- <Card padding='sm' shadow='sm'>
135
- <div class='flex flex-col gap-2'>
136
- <span class='icon-[tabler--device-mobile-code] text-primary text-2xl'></span>
137
- <span class='text-sm font-medium'>{m.feature_native_title()}</span>
138
- <span class='text-base-content/60 text-xs'>{m.feature_native_desc()}</span>
139
- </div>
140
- </Card>
141
- <Card padding='sm' shadow='sm'>
142
- <div class='flex flex-col gap-2'>
143
- <span class='icon-[tabler--api-app] text-primary text-2xl'></span>
144
- <span class='text-sm font-medium'>{m.feature_api_title()}</span>
145
- <span class='text-base-content/60 text-xs'>{m.feature_api_desc()}</span>
146
- </div>
147
- </Card>
148
- </div>
149
-
150
- <Card title={m.status_title()} shadow='sm'>
151
- <p class='mb-3 text-xs text-base-content/50'>{appConfig.name} · {appConfig.version} · {appConfig.env}</p>
152
- <div class='grid grid-cols-2 gap-3 text-sm'>
153
- <div>
154
- <p class='text-base-content/50'>{m.status_api_label()}</p>
155
- <p class='font-medium break-all'>{resolveApiBase()}</p>
156
- </div>
157
- <div>
158
- <p class='text-base-content/50'>{m.status_runtime_label()}</p>
159
- <Badge variant={nativeRuntime ? 'success' : 'warning'} size='sm'>
160
- {nativeRuntime ? m.status_runtime_native() : m.status_runtime_web()}
161
- </Badge>
162
- </div>
163
- <div class='col-span-2'>
164
- <p class='text-base-content/50'>{m.status_token_label()}</p>
165
- <p class='font-medium'>{nativeTokenStorage ? m.status_token_native() : m.status_token_memory()}</p>
166
- </div>
167
- </div>
168
- </Card>
169
- </section>
170
- {:else if activeTab === 'tasks'}
171
- <section class='space-y-4 p-4 pb-6'>
172
- <Card title={m.tasks_title()} shadow='sm'>
173
- <div class='space-y-3'>
174
- {#each [m.tasks_sync(), m.tasks_polish(), m.tasks_release()] as task (task)}
175
- <div class='bg-base-200 rounded-xl flex items-center gap-3 p-3'>
176
- <span class='icon-[tabler--circle-check] text-success text-xl'></span>
177
- <span class='text-sm font-medium'>{task}</span>
178
- </div>
179
- {/each}
180
- </div>
181
- </Card>
182
- </section>
183
- {:else}
184
- <section class='space-y-4 p-4 pb-6'>
185
- <Card title={m.profile_title()} shadow='sm'>
186
- <div class='space-y-3 text-sm'>
187
- <div class='bg-base-200 rounded-xl p-3'>{m.profile_language()}</div>
188
- <div class='bg-base-200 rounded-xl p-3'>{m.profile_theme()}</div>
189
- <div class='bg-base-200 rounded-xl p-3'>{m.profile_secure()}</div>
190
- </div>
191
- </Card>
192
- </section>
193
- {/if}
194
- </main>
195
-
196
- <BottomNav
197
- items={navItems}
198
- active={activeTab}
199
- onchange={handleTabChange}
200
- safeArea
201
- centered
202
- maxWidth='lg'
203
- class='border-base-content/8 shadow-[0_-12px_32px_rgba(15,23,42,0.08)]'
204
- />
205
- </div>
206
136
  </div>
207
- {/if}
208
-
137
+ </div>
209
138
  <ToastContainer />
@@ -0,0 +1,58 @@
1
+ <script lang='ts'>
2
+ import { resolveApiBase } from '../api.js'
3
+ import { appConfig } from '../app-config.js'
4
+ import * as m from '../paraglide/messages.js'
5
+ /** 首页展示的平台运行状态;例如 { nativeRuntime: true, nativeTokenStorage: true }。 */
6
+ interface Props {
7
+ /** 是否运行于原生壳。 */
8
+ nativeRuntime: boolean
9
+ /** 是否使用原生安全凭据存储。 */
10
+ nativeTokenStorage: boolean
11
+ }
12
+ const { nativeRuntime, nativeTokenStorage }: Props = $props()
13
+ </script>
14
+
15
+ <section class='space-y-4 p-4 pb-6'>
16
+ <div class='bg-primary text-primary-content rounded-2xl p-5 shadow-sm'>
17
+ <p class='text-sm opacity-75'>{m.hero_eyebrow()}</p>
18
+ <h1 class='mt-1 text-2xl font-bold'>{m.hero_title()}</h1>
19
+ <p class='mt-2 text-sm opacity-80'>{m.hero_subtitle()}</p>
20
+ </div>
21
+
22
+ <div class='grid grid-cols-2 gap-3'>
23
+ <Card padding='sm' shadow='sm'>
24
+ <div class='flex flex-col gap-2'>
25
+ <span class='icon-[tabler--device-mobile-code] text-primary text-2xl'></span>
26
+ <span class='text-sm font-medium'>{m.feature_native_title()}</span>
27
+ <span class='text-base-content/60 text-xs'>{m.feature_native_desc()}</span>
28
+ </div>
29
+ </Card>
30
+ <Card padding='sm' shadow='sm'>
31
+ <div class='flex flex-col gap-2'>
32
+ <span class='icon-[tabler--api-app] text-primary text-2xl'></span>
33
+ <span class='text-sm font-medium'>{m.feature_api_title()}</span>
34
+ <span class='text-base-content/60 text-xs'>{m.feature_api_desc()}</span>
35
+ </div>
36
+ </Card>
37
+ </div>
38
+
39
+ <Card title={m.status_title()} shadow='sm'>
40
+ <p class='mb-3 text-xs text-base-content/50'>{appConfig.name} · {appConfig.version} · {appConfig.env}</p>
41
+ <div class='grid grid-cols-2 gap-3 text-sm'>
42
+ <div>
43
+ <p class='text-base-content/50'>{m.status_api_label()}</p>
44
+ <p class='font-medium break-all'>{resolveApiBase()}</p>
45
+ </div>
46
+ <div>
47
+ <p class='text-base-content/50'>{m.status_runtime_label()}</p>
48
+ <Badge variant={nativeRuntime ? 'success' : 'warning'} size='sm'>
49
+ {nativeRuntime ? m.status_runtime_native() : m.status_runtime_web()}
50
+ </Badge>
51
+ </div>
52
+ <div class='col-span-2'>
53
+ <p class='text-base-content/50'>{m.status_token_label()}</p>
54
+ <p class='font-medium'>{nativeTokenStorage ? m.status_token_native() : m.status_token_memory()}</p>
55
+ </div>
56
+ </div>
57
+ </Card>
58
+ </section>
@@ -0,0 +1,13 @@
1
+ <script lang='ts'>
2
+ import * as m from '../paraglide/messages.js'
3
+ </script>
4
+
5
+ <section class='space-y-4 p-4 pb-6'>
6
+ <Card title={m.profile_title()} shadow='sm'>
7
+ <div class='space-y-3 text-sm'>
8
+ <div class='bg-base-200 rounded-xl p-3'>{m.profile_language()}</div>
9
+ <div class='bg-base-200 rounded-xl p-3'>{m.profile_theme()}</div>
10
+ <div class='bg-base-200 rounded-xl p-3'>{m.profile_secure()}</div>
11
+ </div>
12
+ </Card>
13
+ </section>
@@ -0,0 +1,16 @@
1
+ <script lang='ts'>
2
+ import * as m from '../paraglide/messages.js'
3
+ </script>
4
+
5
+ <section class='space-y-4 p-4 pb-6'>
6
+ <Card title={m.tasks_title()} shadow='sm'>
7
+ <div class='space-y-3'>
8
+ {#each [m.tasks_sync(), m.tasks_polish(), m.tasks_release()] as task (task)}
9
+ <div class='bg-base-200 rounded-xl flex items-center gap-3 p-3'>
10
+ <span class='icon-[tabler--circle-check] text-success text-xl'></span>
11
+ <span class='text-sm font-medium'>{task}</span>
12
+ </div>
13
+ {/each}
14
+ </div>
15
+ </Card>
16
+ </section>
@@ -15,3 +15,8 @@ playwright-report
15
15
  test-results
16
16
  **/data
17
17
  *.log
18
+ # 仅排除本机私有配置,与 Git 处理一致;公共 .env 与 dev/test/prod 随上下文构建。
19
+ **/.env.local
20
+ **/.env.*.local
21
+ **/.env.runtime
22
+ **/.hai/deploy
@@ -1,19 +1,33 @@
1
+ # 构建产物目录
1
2
  node_modules
2
3
  {{#unless isSvelteOnlyApp}}
3
4
  .svelte-kit
4
5
  {{/unless}}
5
6
  build
6
7
  dist
8
+ .turbo
7
9
  android
8
10
  ios
9
- .env
10
- .env.*
11
- !.env.example
11
+ **/src/lib/paraglide
12
+ **/src-tauri/gen/
13
+ **/src-tauri/target/
14
+ **/hash-core/target/
15
+
16
+ # 日志测试目录
12
17
  *.log
13
- .DS_Store
14
18
  test-results/
15
19
  playwright-report/
20
+
21
+
22
+ # 系统文件
23
+ .DS_Store
24
+
25
+ # 本地数据目录
16
26
  **/data/
17
27
 
18
28
  # hai deploy 产物(compose / .env.runtime / 镜像 tar / sidecar 凭据)
19
29
  .hai/
30
+
31
+ # 本机私有配置;公共 .env 与 dev/test/prod 环境文件纳入版本控制。
32
+ .env.local
33
+ .env.*.local
@@ -1,6 +1,12 @@
1
1
  # syntax=docker/dockerfile:1.7
2
2
  {{#if isSvelteOnlyApp}}
3
3
  FROM node:24-bookworm-slim AS build
4
+ ARG SQLITE_PREBUILD_MIRROR
5
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
6
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
7
+ ARG HAI_CORE_ENV=production
8
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
9
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
4
10
 
5
11
  ENV PNPM_HOME=/pnpm
6
12
  ENV PATH=$PNPM_HOME:$PATH
@@ -24,6 +30,12 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
24
30
  CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
25
31
  {{else}}
26
32
  FROM node:24-bookworm-slim AS build
33
+ ARG SQLITE_PREBUILD_MIRROR
34
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
35
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
36
+ ARG HAI_CORE_ENV=production
37
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
38
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
27
39
 
28
40
  ENV PNPM_HOME=/pnpm
29
41
  ENV PATH=$PNPM_HOME:$PATH
@@ -36,12 +48,16 @@ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
36
48
  COPY . .
37
49
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
38
50
  RUN pnpm build
51
+ RUN 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
39
52
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{projectName}} --prod /prod/app
40
53
 
41
54
  FROM node:24-bookworm-slim AS runtime-base
55
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
56
+ ARG HAI_CORE_ENV=production
42
57
 
43
58
  ENV NODE_ENV=production
44
- ENV HAI_CORE_ENV=production
59
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
60
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
45
61
  ENV HOST=0.0.0.0
46
62
  ENV PORT=3000
47
63
  WORKDIR /app
@@ -21,14 +21,26 @@ test('home page renders from browser', async ({ page }) => {
21
21
  await expect(page).toHaveTitle(process.env.HAI_CORE_NAME ?? '{{projectName}}')
22
22
  await expect(pageErrors).toEqual([])
23
23
  await expect(page.locator('main, section').first()).toBeVisible()
24
+ await expect(page.locator('header').first()).toBeVisible()
25
+ await expect(page.locator('footer').first()).toBeAttached()
24
26
  {{#if_eq appType 'h5'}}
25
27
  // 在手机宽度验证导航真正切换路由,避免样式失效但首页渲染测试仍通过。
26
28
  const nav = page.locator('nav')
27
29
  await nav.getByRole('button', { name: /发现|Discover/i, exact: true }).click()
28
30
  await expect(page).toHaveURL(/\/discover$/)
31
+ await page.reload()
32
+ await expect(page).toHaveURL(/\/discover$/)
33
+ await expect(page.getByRole('heading', { name: /发现|Discover/i })).toBeVisible()
29
34
  await nav.getByRole('button', { name: /首页|Home/i, exact: true }).click()
30
35
  await expect(page).toHaveURL(/\/$/)
31
36
  expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
37
+ {{/if_eq}}
38
+ {{#if_eq appType 'website'}}
39
+ await page.goto('/about')
40
+ await page.reload()
41
+ await expect(page).toHaveURL(/\/about$/)
42
+ await expect(page.locator('header')).toBeVisible()
43
+ await expect(page.locator('footer')).toBeVisible()
32
44
  {{/if_eq}}
33
45
  const evidence = process.env.HAI_AUTH_EVIDENCE_DIR || path.resolve('test-results/auth-screenshots')
34
46
  await mkdir(evidence, { recursive: true })
@@ -50,6 +62,8 @@ test('configured IAM registration and password login', async ({ page, context })
50
62
  const authUrl = '{{#if isCapacitorApp}}/{{else}}/auth/login{{/if}}'
51
63
  await page.goto(authUrl)
52
64
  await expect(page.locator('#login-username')).toBeVisible()
65
+ await expect(page.locator('header').first()).toBeVisible()
66
+ await expect(page.locator('footer')).toBeVisible()
53
67
  await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
54
68
  await page.getByRole('{{#if isCapacitorApp}}button{{else}}link{{/if}}', { name: /立即注册|Register now/i }).click()
55
69
  await expect(page.locator('input[name="username"]')).toBeVisible()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{projectName}}",
4
- "version": "0.1.0-alpha.63",
4
+ "version": "0.1.0-alpha.65",
5
5
  "private": true,
6
6
  "packageManager": "pnpm@12.11.2",
7
7
  "description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
@@ -21,11 +21,20 @@
21
21
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
22
22
  {{/if}}
23
23
  "dev": "vite dev",
24
+ "dev:test": "vite dev --mode test",
25
+ "dev:prod": "vite dev --mode prod",
24
26
  "build": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vite build",
27
+ "build:prod": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vite build --mode prod",
28
+ {{#unless isSvelteOnlyApp}}
29
+ "start": "node --env-file=.env --env-file=.env.prod --env-file-if-exists=.env.local build/index.js",
30
+ "start:test": "node --env-file=.env --env-file=.env.test --env-file-if-exists=.env.local build/index.js",
31
+ "start:prod": "pnpm start",
32
+ {{/unless}}
25
33
  "preview": "vite preview",
26
- "check": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}{{#if isSvelteOnlyApp}}svelte-check --tsconfig ./tsconfig.json{{else}}svelte-kit sync && svelte-check --tsconfig ./tsconfig.json{{/if}}",
34
+ "check": "pnpm typecheck",
27
35
  "typecheck": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}{{#if isSvelteOnlyApp}}svelte-check --tsconfig ./tsconfig.json{{else}}svelte-kit sync && svelte-check --tsconfig ./tsconfig.json{{/if}}",
28
36
  "lint": "eslint .",
37
+ "lint:fix": "eslint . --fix",
29
38
  "test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
30
39
  "test:watch": "vitest",
31
40
  "test:e2e": "playwright test",
@@ -79,6 +88,9 @@
79
88
  {{/if}}
80
89
  },
81
90
  "devDependencies": {
91
+ {{#if isSvelteOnlyApp}}
92
+ "@h-ai/core": "{{haiPackageSpecifier}}",
93
+ {{/if}}
82
94
  "@antfu/eslint-config": "{{versions.antfuEslintConfig}}",
83
95
  "@playwright/test": "{{versions.playwright}}",
84
96
  {{#if isCapacitorApp}}
@@ -3,6 +3,9 @@ import type { CoreConfig } from '@h-ai/core'
3
3
  {{/unless}}
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { join } from 'node:path'
6
+ {{#unless isSvelteOnlyApp}}
7
+ import process from 'node:process'
8
+ {{/unless}}
6
9
  import { fileURLToPath } from 'node:url'
7
10
  {{#unless isSvelteOnlyApp}}
8
11
  import { core } from '@h-ai/core'
@@ -43,9 +46,12 @@ describe('generated scaffold', () => {
43
46
  {{#unless isCapacitorApp}}
44
47
 
45
48
  it('ships an application config loaded at startup', async () => {
46
- const coreConfig = await readFile(join(rootDir, 'config/_core.yml'), 'utf-8')
47
- expect(coreConfig).toContain('name: {{projectName}}')
48
- expect(coreConfig).toContain('defaultLocale: {{defaultLocale}}')
49
+ const result = core.config.load<CoreConfig>('core', join(rootDir, 'config/_core.yml'))
50
+ expect(result.success).toBe(true)
51
+ if (!result.success)
52
+ return
53
+ expect(result.data.name).toBe(process.env.HAI_CORE_NAME ?? '{{projectName}}')
54
+ expect(result.data.defaultLocale).toBe(process.env.HAI_CORE_DEFAULTLOCALE ?? '{{defaultLocale}}')
49
55
  })
50
56
  {{/unless}}
51
57
  {{#unless isSvelteOnlyApp}}
@@ -6,9 +6,7 @@ import type { CoreConfig } from '@h-ai/core'
6
6
  {{/if}}
7
7
  import path from 'node:path'
8
8
  import process from 'node:process'
9
- {{#if isSvelteOnlyApp}}
10
9
  import { core } from '@h-ai/core'
11
- {{/if}}
12
10
  {{#if hasUi}}
13
11
  import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
14
12
  {{/if}}
@@ -23,10 +21,13 @@ import { sveltekit } from '@sveltejs/kit/vite'
23
21
  {{#if hasUi}}
24
22
  import tailwindcss from '@tailwindcss/vite'
25
23
  {{/if}}
26
- import { defineConfig, loadEnv } from 'vite'
24
+ import { defineConfig } from 'vite'
27
25
 
28
26
  export default defineConfig(({ mode }) => {
29
- const env = loadEnv(mode, process.cwd(), '')
27
+ const envResult = core.config.loadEnvFiles({ mode, envDir: process.cwd() })
28
+ if (!envResult.success)
29
+ throw new Error(envResult.error.message)
30
+ const env = envResult.data
30
31
  Object.assign(process.env, env)
31
32
  {{#if isSvelteOnlyApp}}
32
33
  // 浏览器无法读取 YAML;构建时通过 core 解析,仅注入明确可公开的字段。
@@ -42,6 +43,7 @@ export default defineConfig(({ mode }) => {
42
43
  {{/if}}
43
44
 
44
45
  return {
46
+ envDir: false,
45
47
  envPrefix: ['VITE_', 'PUBLIC_'],
46
48
  {{#if isSvelteOnlyApp}}
47
49
  define: { __APP_CORE_CONFIG__: JSON.stringify(appConfig) },
@@ -0,0 +1,83 @@
1
+ import { SvelteMap, SvelteURL } from 'svelte/reactivity'
2
+
3
+ /**
4
+ * 页面目录驱动的 hash 导航;仅管理 URL,不包含业务权限或页面加载逻辑。
5
+ * @example
6
+ * ```ts
7
+ * const pages = import.meta.glob('../pages/*.svelte')
8
+ * const router = new HashRouter(Object.keys(pages), '/home')
9
+ * const stop = router.start(() => void loadCurrentPage())
10
+ * router.navigate('/reports/weekly?period=month')
11
+ * stop()
12
+ * ```
13
+ */
14
+ export class HashRouter {
15
+ /** 当前页面路径,例如 `/reports/weekly`。 */
16
+ path = $state('')
17
+ /** 当前页面查询串,例如 `?period=month`。 */
18
+ search = $state('')
19
+ private readonly files = new SvelteMap<string, string>()
20
+
21
+ private readonly fallback: string
22
+
23
+ /**
24
+ * 建立页面文件到 URL 的映射,新增页面无需维护路由表。
25
+ * @param pageFiles - import.meta.glob 返回的文件名
26
+ * @param fallback - 空路径和未知页面的回退路径
27
+ * @example new HashRouter(Object.keys(import.meta.glob('../pages/*.svelte')), '/home')
28
+ */
29
+ constructor(pageFiles: string[], fallback: string) {
30
+ this.fallback = fallback
31
+ for (const file of pageFiles) {
32
+ const route = file.replace(/^.*\/pages/, '').replace(/\.svelte$/, '').replace(/\/index$/, '') || '/'
33
+ this.files.set(route, file)
34
+ }
35
+ if (!this.files.has(fallback))
36
+ throw new Error('Hash router fallback page is missing')
37
+ this.path = fallback
38
+ }
39
+
40
+ /** 当前路径对应的页面文件;可用于索引 import.meta.glob 的惰性加载器。 */
41
+ get file(): string {
42
+ return this.files.get(this.path) ?? this.files.get(this.fallback)!
43
+ }
44
+
45
+ /**
46
+ * 恢复地址栏状态并监听链接、前进与后退;卸载时调用返回的清理函数。
47
+ * @param onChange - URL 导航完成后的业务通知
48
+ * @returns 移除事件监听器的函数
49
+ * @example const stop = router.start(() => void store.navigate(router.path.slice(1)))
50
+ */
51
+ start(onChange: () => void = () => {}): () => void {
52
+ const synchronize = () => {
53
+ this.navigate(location.hash.slice(1), true)
54
+ onChange()
55
+ }
56
+ synchronize()
57
+ window.addEventListener('hashchange', synchronize)
58
+ return () => {
59
+ window.removeEventListener('hashchange', synchronize)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 导航到页面;未知路径回退,查询串随 hash 保留并可在刷新后恢复。
65
+ * @param target - 以 `/` 开头的本地页面路径和查询串
66
+ * @param replace - 是否替换当前历史项
67
+ * @example router.navigate('/settings?section=profile')
68
+ */
69
+ navigate(target: string, replace = false): void {
70
+ const local = target.startsWith('/') && !target.startsWith('//') && !target.includes('\\')
71
+ const url = new SvelteURL(local ? target : this.fallback, 'https://hash.invalid')
72
+ const known = this.files.has(url.pathname)
73
+ this.path = known ? url.pathname : this.fallback
74
+ this.search = known ? url.search : ''
75
+ const hash = `#${this.path}${this.search}`
76
+ if (location.hash !== hash) {
77
+ if (replace)
78
+ history.replaceState(history.state, '', hash)
79
+ else
80
+ history.pushState(history.state, '', hash)
81
+ }
82
+ }
83
+ }
@@ -2,13 +2,20 @@
2
2
 
3
3
  > 通用 AI 编程助手入口。优先阅读 `README.md`、现有测试与 `.agents/skills/`;本文件同时作为 `hai add` / AI 支持回填时的 generic fallback。
4
4
 
5
+ ## 需求与模板
6
+
7
+ - 业务行为以用户当前明确要求及其指定的需求/设计文档为准。本文件、README、skills、示例、生成代码及其测试中的模板默认行为是起点,不是不可变的业务约束;角色代码、权限模型、页面流程、字段、限额、存储方式与架构均可按实际需求调整。
8
+ - 需求与模板不同,审查时记录为模板适配项,不据此判定需求错误或阻塞审查,也不要求用户额外授权“覆盖模板”;实施时在已有授权范围内修改或移除冲突逻辑,并同步 contract、鉴权、数据迁移、界面、配置、测试、文档及本文件中的过期规则。
9
+ - 未说明的行为可参考现有实现并注明假设,不能升级为强制需求。只有业务要求自身矛盾、与已确认项目决策的优先级不明,或存在真实技术/安全障碍时才提出具体问题;已明确的规则不重复要求选择。
10
+ - 业务规则与技术契约分别判断:可用 API 以当前安装版本的公开类型为准;输入校验、服务端授权、资源隔离和密钥保护仍须落实。需求涉及替换技术方案时说明影响并同步迁移,不能用保留模板默认值代替解决问题。
11
+
5
12
  ## 行为契约
6
13
 
7
14
  需求/设计文档审查或从文档生成代码时,读取 [hai-spec-review](.agents/skills/hai-spec-review/SKILL.md),严格确认完备性、歧义、技术正确性与命名一致性;影响行为的问题未解决前不判定文档可生成。普通小改动不重审无关文档。
8
15
 
9
16
  升级 hai-framework 到最新/指定版本、比较版本差异或同步框架 skills 时,先读 [框架升级与同步 Skill](.agents/skills/hai-framework-sync/SKILL.md),执行版本发现、差异分析、用户选择/阻塞处理、迁移与验证;仅要求比较时不修改应用。
10
17
 
11
- 根据文档规划、实施、继续或返修任务时,读取 [hai-spec-implement](.agents/skills/hai-spec-implement/SKILL.md):审查后按可验收功能和依赖拆分,维护 docs 下的清单及 docs/tasks 下的任务信息,每项独立分支 TDD、验证并本地合并;返修复用原任务与分支。仅规划不实施,指定任务先核验依赖。
18
+ 根据文档规划、实施、继续、返修或按已提交文档变更同步实现时,读取 [hai-spec-implement](.agents/skills/hai-spec-implement/SKILL.md):维护 docs 清单与任务记录,比较已验收与目标文档 Git 快照,更新原任务或新建任务,每项独立分支 TDD、验证并本地合并;全部差异与整体验收通过后才推进基线。返修复用原任务与分支。仅规划不实施,指定任务先核验依赖。
12
19
 
13
20
  普通新增、修改、修复任务开始前,先读 [应用开发 Skill](.agents/skills/hai-app-create/SKILL.md) 和 [开发规范与框架能力路由](.agents/skills/hai-app-create/development.md);再按任务读取模块 Skill 及其相邻参考文件。架构/构建、审查、测试分别读取 hai-build、hai-app-review、hai-app-tests,不加载整个 skill 树。
14
21
 
@@ -36,6 +43,13 @@
36
43
  3. 改动公共类型、配置、模板或共享逻辑时,同步更新所有调用方与文档。
37
44
  4. 每次改动后按影响范围运行质量门禁并记录结果。
38
45
 
46
+ ## 代码注释与示例
47
+
48
+ - 尽可能补充有信息量的中文 JSDoc:所有 export 的函数/类/方法、核心算法与业务流程、所有数据库表/列、接口/type/schema 及其字段都必须说明用途、单位、约束和关联关系。核心逻辑解释决策原因、边界、权限与失败路径,不逐行复述代码。
49
+ - 函数按实际契约写 @param、@returns,仅确实抛错时写 @throws;异步、HaiResult 与事务失败行为须说明。类型/表结构说明字段含义、可空性、默认值、主外键和唯一约束。
50
+ - 上述函数和结构的 JSDoc 均提供可理解的 @example:函数给调用及结果判断,结构给合法数据,表给典型行或查询示例。省略上下文要标明,不使用真实凭据。示例随实现同步,不能只堆叠没有信息量的注释。
51
+ - 调用及结构示例见 [.agents/skills/hai-app-create/development.md](.agents/skills/hai-app-create/development.md) 的 JSDoc 注释一节。
52
+
39
53
  ## 质量门禁
40
54
 
41
55
  先核对 package.json,按影响范围执行已有脚本;文档检查引用,UI/路由变更再运行 E2E。未运行项说明原因。