@h-ai/cli 0.1.0-alpha.49 → 0.1.0-alpha.51

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 (28) hide show
  1. package/dist/index.js +52 -2
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/templates/apps/api/.dockerignore.hbs +14 -0
  5. package/templates/apps/api/Dockerfile.hbs +34 -0
  6. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +6 -0
  7. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
  8. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
  9. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +2 -0
  10. package/templates/apps/api/package.json.hbs +4 -0
  11. package/templates/apps/fullstack/.dockerignore.hbs +14 -0
  12. package/templates/apps/fullstack/Dockerfile.hbs +71 -0
  13. package/templates/apps/fullstack/docker/fullstack-server.mjs.hbs +194 -0
  14. package/templates/apps/fullstack/docker/nginx.conf.hbs +18 -0
  15. package/templates/apps/fullstack/package.json.hbs +8 -0
  16. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +7 -0
  17. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +2 -2
  18. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
  19. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  20. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +2 -1
  21. package/templates/base/.dockerignore.hbs +17 -0
  22. package/templates/base/Dockerfile.hbs +56 -0
  23. package/templates/base/docker/nginx.conf.hbs +18 -0
  24. package/templates/base/package.json.hbs +16 -6
  25. package/templates/base/svelte.config.js.hbs +0 -8
  26. package/templates/skills/hai-app-create/SKILL.md +13 -0
  27. package/templates/skills/hai-iam/SKILL.md +1 -1
  28. package/templates/skills/hai-ui/SKILL.md +7 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.49",
4
+ "version": "0.1.0-alpha.51",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.1.0",
35
35
  "prompts": "^2.4.2",
36
36
  "yaml": "^2.7.0",
37
- "@h-ai/core": "0.1.0-alpha.49"
37
+ "@h-ai/core": "0.1.0-alpha.51"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/fs-extra": "^11.0.4",
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .agents
4
+ .claude
5
+ .codex
6
+ .opencode
7
+ .turbo
8
+ build
9
+ dist
10
+ node_modules
11
+ playwright-report
12
+ test-results
13
+ **/data
14
+ *.log
@@ -0,0 +1,34 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-bookworm-slim AS build
3
+
4
+ ENV PNPM_HOME=/pnpm
5
+ ENV PATH=$PNPM_HOME:$PATH
6
+ WORKDIR /workspace
7
+
8
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
9
+ COPY . .
10
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
11
+ RUN pnpm build
12
+ RUN pnpm --filter {{api.servicePackageName}} --prod deploy /prod/app
13
+
14
+ FROM node:22-bookworm-slim AS runtime-base
15
+
16
+ ENV NODE_ENV=production
17
+ ENV HOST=0.0.0.0
18
+ ENV PORT=3000
19
+ WORKDIR /app
20
+
21
+ COPY --from=build --chown=node:node /prod/app ./
22
+ RUN mkdir -p /app/data && chown node:node /app/data
23
+
24
+ USER node
25
+ EXPOSE 3000
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
27
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
28
+ CMD ["node", "dist/index.js"]
29
+
30
+ FROM runtime-base AS runtime-with-data
31
+
32
+ COPY --from=embedded-data --chown=node:node . /app/data
33
+
34
+ FROM runtime-base AS runtime
@@ -9,6 +9,12 @@ pnpm --filter {{api.servicePackageName}} dev
9
9
  pnpm --filter {{api.servicePackageName}} test
10
10
  ```
11
11
 
12
+ ## 容器运行
13
+
14
+ - 根目录运行 `pnpm docker:build` / `pnpm podman:build`;镜像监听 `0.0.0.0:3000`。
15
+ - 配置默认打包自本目录 `config/`,可在运行时只读挂载到 `/app/config`,敏感值通过环境变量注入。
16
+ - `data/` 可作为 SQLite/本地存储目录;`pnpm docker:build:data` / `pnpm podman:build:data` 会将其固化为镜像初始数据,生产写入应挂载 `/app/data` volume。
17
+
12
18
  ## 关键目录
13
19
 
14
20
  - `config/`:service 配置
@@ -4,8 +4,8 @@
4
4
 
5
5
  name: {{projectName}} API Service
6
6
  version: 0.1.0
7
- env: development
8
- debug: false
7
+ env: ${HAI_ENV:development}
8
+ debug: ${HAI_DEBUG:false}
9
9
 
10
10
  defaultLocale: {{defaultLocale}}
11
11
  supportedLocales:
@@ -18,10 +18,10 @@ http:
18
18
  rpc: false
19
19
 
20
20
  cors:
21
- origin: '*'
22
- nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
23
- {{#if features.iam}} credentials: true
24
- {{else}} credentials: false
21
+ origin: ${HAI_CORS_ORIGIN:*}
22
+ nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
23
+ {{#if features.iam}} credentials: ${HAI_CORS_CREDENTIALS:true}
24
+ {{else}} credentials: ${HAI_CORS_CREDENTIALS:false}
25
25
  {{/if}} allowedHeaders:
26
26
  - Authorization
27
27
  - Content-Type
@@ -14,6 +14,8 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "files": [
16
16
  "dist",
17
+ "config",
18
+ "data",
17
19
  "README.md"
18
20
  ],
19
21
  "scripts": {
@@ -9,6 +9,10 @@
9
9
  "dev": "pnpm --filter {{api.servicePackageName}} dev",
10
10
  "compile": "pnpm typecheck && pnpm build",
11
11
  "build": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present build",
12
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
13
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
14
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
15
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
12
16
  "typecheck": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present typecheck",
13
17
  "lint": "pnpm -r --if-present lint",
14
18
  "test": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present test",
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .agents
4
+ .claude
5
+ .codex
6
+ .opencode
7
+ .turbo
8
+ **/build
9
+ **/dist
10
+ **/node_modules
11
+ playwright-report
12
+ test-results
13
+ **/data
14
+ *.log
@@ -0,0 +1,71 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-bookworm-slim AS build
3
+
4
+ ARG PUBLIC_API_BASE
5
+ ENV PNPM_HOME=/pnpm
6
+ ENV PATH=$PNPM_HOME:$PATH
7
+ ENV PUBLIC_API_BASE=$PUBLIC_API_BASE
8
+ WORKDIR /workspace
9
+
10
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
11
+ COPY . .
12
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
13
+ RUN pnpm build
14
+ RUN pnpm --filter {{fullstack.servPackageName}} --prod deploy /prod/service
15
+
16
+ FROM node:22-bookworm-slim AS service
17
+
18
+ ENV NODE_ENV=production
19
+ ENV HOST=0.0.0.0
20
+ ENV PORT=3000
21
+ WORKDIR /app
22
+
23
+ COPY --from=build --chown=node:node /prod/service ./
24
+ RUN mkdir -p /app/data && chown node:node /app/data
25
+
26
+ USER node
27
+ EXPOSE 3000
28
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
29
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
30
+ CMD ["node", "dist/index.js"]
31
+
32
+ {{#if fullstack.frontends.web}}
33
+ FROM nginxinc/nginx-unprivileged:1.29-alpine AS web
34
+
35
+ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
36
+ COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
37
+
38
+ EXPOSE 8080
39
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
40
+ CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
41
+
42
+ FROM node:22-bookworm-slim AS runtime-base
43
+
44
+ ENV NODE_ENV=production
45
+ ENV HAI_WEB_PORT=8080
46
+ ENV HAI_SERV_PORT=3000
47
+ WORKDIR /app
48
+
49
+ COPY --from=build --chown=node:node /prod/service ./service
50
+ COPY --from=build --chown=node:node /workspace/apps/{{projectName}}-web/dist ./web
51
+ COPY --chown=node:node docker/fullstack-server.mjs ./fullstack-server.mjs
52
+ RUN mkdir -p /app/service/data && chown node:node /app/service/data
53
+
54
+ USER node
55
+ EXPOSE 8080
56
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
57
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.HAI_WEB_PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
58
+ CMD ["node", "fullstack-server.mjs"]
59
+
60
+ FROM runtime-base AS runtime-with-data
61
+
62
+ COPY --from=embedded-data --chown=node:node . /app/service/data
63
+
64
+ FROM runtime-base AS runtime
65
+ {{else}}
66
+ FROM service AS runtime-with-data
67
+
68
+ COPY --from=embedded-data --chown=node:node . /app/data
69
+
70
+ FROM service AS runtime
71
+ {{/if}}
@@ -0,0 +1,194 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if fullstack.frontends.web}}
3
+ import { spawn } from 'node:child_process'
4
+ import { createReadStream } from 'node:fs'
5
+ import { readFile, stat } from 'node:fs/promises'
6
+ import { createServer, request as createProxyRequest } from 'node:http'
7
+ import { extname, relative, resolve, sep } from 'node:path'
8
+ import process from 'node:process'
9
+
10
+ const serviceRoot = '/app/service'
11
+ const webRoot = '/app/web'
12
+ const publicPort = readPort('HAI_WEB_PORT', 8080)
13
+ const servicePort = readPort('HAI_SERV_PORT', 3000)
14
+ const indexHtml = await readFile(resolve(webRoot, 'index.html'), 'utf8')
15
+ const indexWithRuntimeApi = injectRuntimeApiOrigin(indexHtml)
16
+ let shuttingDown = false
17
+
18
+ // 服务进程只监听容器回环地址,所有外部流量统一经过同源 Web 入口。
19
+ const service = spawn(process.execPath, ['dist/index.js'], {
20
+ cwd: serviceRoot,
21
+ env: {
22
+ ...process.env,
23
+ HOST: '127.0.0.1',
24
+ PORT: String(servicePort),
25
+ },
26
+ stdio: 'inherit',
27
+ })
28
+
29
+ const server = createServer(async (request, response) => {
30
+ const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`)
31
+
32
+ if (isServicePath(requestUrl.pathname)) {
33
+ proxyToService(request, response)
34
+ return
35
+ }
36
+
37
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
38
+ response.writeHead(405, { Allow: 'GET, HEAD' })
39
+ response.end()
40
+ return
41
+ }
42
+
43
+ await serveWebAsset(requestUrl.pathname, request.method === 'HEAD', response)
44
+ })
45
+
46
+ server.listen(publicPort, '0.0.0.0', () => {
47
+ process.stdout.write(`Fullstack container listening on 0.0.0.0:${publicPort}\n`)
48
+ })
49
+
50
+ service.once('error', (error) => {
51
+ process.stderr.write(`Service process failed to start: ${error.message}\n`)
52
+ void shutdown('service-error')
53
+ })
54
+
55
+ service.once('exit', (code, signal) => {
56
+ if (shuttingDown)
57
+ return
58
+ process.stderr.write(`Service process exited unexpectedly (code=${String(code)}, signal=${String(signal)})\n`)
59
+ process.exitCode = code ?? 1
60
+ server.close()
61
+ })
62
+
63
+ for (const signal of ['SIGINT', 'SIGTERM']) {
64
+ process.once(signal, () => {
65
+ void shutdown(signal)
66
+ })
67
+ }
68
+
69
+ function readPort(name, fallback) {
70
+ const raw = process.env[name]
71
+ if (raw === undefined || raw === '')
72
+ return fallback
73
+ const value = Number(raw)
74
+ if (!Number.isInteger(value) || value < 1 || value > 65535)
75
+ throw new Error(`${name} must be an integer between 1 and 65535`)
76
+ return value
77
+ }
78
+
79
+ function isServicePath(pathname) {
80
+ return pathname === '/health'
81
+ || pathname === '/ready'
82
+ || pathname === '/openapi.json'
83
+ || pathname.startsWith('/api/')
84
+ || pathname.startsWith('/docs')
85
+ || pathname.startsWith('/_hai/')
86
+ }
87
+
88
+ function proxyToService(request, response) {
89
+ const proxyRequest = createProxyRequest({
90
+ host: '127.0.0.1',
91
+ port: servicePort,
92
+ method: request.method,
93
+ path: request.url,
94
+ headers: {
95
+ ...request.headers,
96
+ host: `127.0.0.1:${servicePort}`,
97
+ 'x-forwarded-host': request.headers.host ?? '',
98
+ 'x-forwarded-proto': request.headers['x-forwarded-proto'] ?? 'http',
99
+ },
100
+ }, (proxyResponse) => {
101
+ response.writeHead(proxyResponse.statusCode ?? 502, proxyResponse.headers)
102
+ proxyResponse.pipe(response)
103
+ })
104
+
105
+ proxyRequest.once('error', (error) => {
106
+ if (response.headersSent) {
107
+ response.destroy(error)
108
+ return
109
+ }
110
+ response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
111
+ response.end('Service unavailable')
112
+ })
113
+ request.pipe(proxyRequest)
114
+ }
115
+
116
+ async function serveWebAsset(pathname, headOnly, response) {
117
+ let decodedPath
118
+ try {
119
+ decodedPath = decodeURIComponent(pathname)
120
+ }
121
+ catch {
122
+ response.writeHead(400)
123
+ response.end()
124
+ return
125
+ }
126
+
127
+ const candidate = resolve(webRoot, `.${decodedPath}`)
128
+ const relativePath = relative(webRoot, candidate)
129
+ if (relativePath.startsWith(`..${sep}`) || relativePath === '..') {
130
+ response.writeHead(403)
131
+ response.end()
132
+ return
133
+ }
134
+
135
+ try {
136
+ const file = await stat(candidate)
137
+ if (file.isFile()) {
138
+ response.writeHead(200, {
139
+ 'content-length': file.size,
140
+ 'content-type': contentType(candidate),
141
+ 'x-content-type-options': 'nosniff',
142
+ })
143
+ if (headOnly)
144
+ response.end()
145
+ else
146
+ createReadStream(candidate).pipe(response)
147
+ return
148
+ }
149
+ }
150
+ catch {
151
+ // SPA 路由没有对应静态文件时回退到 index.html。
152
+ }
153
+
154
+ const body = Buffer.from(indexWithRuntimeApi)
155
+ response.writeHead(200, {
156
+ 'content-length': body.length,
157
+ 'content-type': 'text/html; charset=utf-8',
158
+ 'x-content-type-options': 'nosniff',
159
+ })
160
+ response.end(headOnly ? undefined : body)
161
+ }
162
+
163
+ function injectRuntimeApiOrigin(html) {
164
+ const script = `<script>(()=>{const p=new URLSearchParams(location.search);if(!p.has('apiBase')){p.set('apiBase',location.origin);history.replaceState(null,'',location.pathname+'?'+p+location.hash)}})()</script>`
165
+ return html.includes('<head>') ? html.replace('<head>', `<head>${script}`) : `${script}${html}`
166
+ }
167
+
168
+ function contentType(file) {
169
+ return {
170
+ '.css': 'text/css; charset=utf-8',
171
+ '.html': 'text/html; charset=utf-8',
172
+ '.ico': 'image/x-icon',
173
+ '.js': 'text/javascript; charset=utf-8',
174
+ '.json': 'application/json; charset=utf-8',
175
+ '.map': 'application/json; charset=utf-8',
176
+ '.png': 'image/png',
177
+ '.svg': 'image/svg+xml',
178
+ '.webp': 'image/webp',
179
+ '.woff': 'font/woff',
180
+ '.woff2': 'font/woff2',
181
+ }[extname(file).toLowerCase()] ?? 'application/octet-stream'
182
+ }
183
+
184
+ async function shutdown(signal) {
185
+ if (shuttingDown)
186
+ return
187
+ shuttingDown = true
188
+ process.stdout.write(`Stopping fullstack container (${signal})\n`)
189
+ server.close()
190
+ service.kill('SIGTERM')
191
+ const forceExit = setTimeout(() => service.kill('SIGKILL'), 10_000)
192
+ forceExit.unref()
193
+ }
194
+ {{/if}}
@@ -0,0 +1,18 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if fullstack.frontends.web}}
3
+ server {
4
+ listen 8080;
5
+ server_name _;
6
+ root /usr/share/nginx/html;
7
+
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ location = /health {
13
+ access_log off;
14
+ add_header Content-Type text/plain;
15
+ return 200 'ok';
16
+ }
17
+ }
18
+ {{/if}}
@@ -11,6 +11,14 @@
11
11
  "dev": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev",
12
12
  "compile": "pnpm typecheck && pnpm build",
13
13
  "build": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
14
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
15
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest .",
16
+ "docker:build:service": "docker build --target service -t {{projectName}}-serv:latest ."{{#if fullstack.frontends.web}},
17
+ "docker:build:web": "docker build --target web -t {{projectName}}-web:latest ."{{/if}},
18
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
19
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest .",
20
+ "podman:build:service": "podman build --format docker --jobs=1 --target service -t {{projectName}}-serv:latest ."{{#if fullstack.frontends.web}},
21
+ "podman:build:web": "podman build --format docker --jobs=1 --target web -t {{projectName}}-web:latest ."{{/if}},
14
22
  "package": "pnpm build{{#if fullstack.frontends.web}} && pnpm --filter {{projectName}}-web package{{/if}}{{#if fullstack.frontends.app}} && pnpm --filter {{projectName}}-app package{{/if}}{{#if fullstack.frontends.desktop}} && pnpm --filter {{projectName}}-desktop package{{/if}}",
15
23
  "deploy": "pnpm package",
16
24
  "typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
@@ -15,3 +15,10 @@
15
15
 
16
16
  - `pnpm --filter {{fullstack.servPackageName}} dev`
17
17
  - `pnpm --filter {{fullstack.servPackageName}} test`
18
+
19
+ ## 容器运行
20
+
21
+ - 根目录 `pnpm docker:build` / `pnpm podman:build` 生成 Web + Service 同源镜像;`pnpm docker:build:service` / `pnpm podman:build:service` 只生成本服务镜像。
22
+ - 合并镜像的配置挂载点为 `/app/service/config`,数据目录为 `/app/service/data`;service-only 镜像对应 `/app/config` 与 `/app/data`。
23
+ - YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
24
+ - 将 SQLite/本地存储放入本目录 `data/` 后运行 `pnpm docker:build:data` / `pnpm podman:build:data`,可生成带初始数据的镜像,持续写入仍应使用 volume。
@@ -8,7 +8,7 @@
8
8
 
9
9
  name: {{fullstack.servPackageName}}
10
10
  version: 0.1.0
11
- env: development
12
- debug: false
11
+ env: ${HAI_ENV:development}
12
+ debug: ${HAI_DEBUG:false}
13
13
 
14
14
  defaultLocale: {{fullstack.i18n.baseLocale}}
@@ -21,9 +21,9 @@ http:
21
21
  rpc: false
22
22
 
23
23
  cors:
24
- origin: '*'
25
- nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
26
- credentials: false
24
+ origin: ${HAI_CORS_ORIGIN:*}
25
+ nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
26
+ credentials: ${HAI_CORS_CREDENTIALS:false}
27
27
  allowedHeaders:
28
28
  - Authorization
29
29
  - Content-Type
@@ -11,6 +11,12 @@
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "config",
16
+ "data",
17
+ "dist",
18
+ "README.md"
19
+ ],
14
20
  "scripts": {
15
21
  "build": "tsup",
16
22
  "typecheck": "tsc --noEmit",
@@ -38,7 +38,8 @@ export async function fetchAppInfo(options: FetchAppInfoOptions = {}): Promise<A
38
38
  }
39
39
 
40
40
  function resolveDefaultApiOrigin(): string {
41
- return import.meta.env.PUBLIC_API_BASE ?? DEFAULT_API_ORIGIN
41
+ return import.meta.env.PUBLIC_API_BASE
42
+ || (typeof globalThis.location === 'undefined' ? DEFAULT_API_ORIGIN : globalThis.location.origin)
42
43
  }
43
44
 
44
45
  function normalizeApiBaseUrl(value: string): string {
@@ -0,0 +1,17 @@
1
+ .git
2
+ .github
3
+ .agents
4
+ .claude
5
+ .codex
6
+ .opencode
7
+ .svelte-kit
8
+ .turbo
9
+ android
10
+ build
11
+ dist
12
+ ios
13
+ node_modules
14
+ playwright-report
15
+ test-results
16
+ **/data
17
+ *.log
@@ -0,0 +1,56 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ {{#if isSvelteOnlyApp}}
3
+ FROM node:22-bookworm-slim AS build
4
+
5
+ ENV PNPM_HOME=/pnpm
6
+ ENV PATH=$PNPM_HOME:$PATH
7
+ WORKDIR /workspace
8
+
9
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
10
+ COPY . .
11
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
12
+ RUN pnpm build
13
+
14
+ FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime
15
+
16
+ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
17
+ COPY --from=build /workspace/dist /usr/share/nginx/html
18
+
19
+ EXPOSE 8080
20
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
21
+ CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
22
+ {{else}}
23
+ FROM node:22-bookworm-slim AS build
24
+
25
+ ENV PNPM_HOME=/pnpm
26
+ ENV PATH=$PNPM_HOME:$PATH
27
+ WORKDIR /workspace
28
+
29
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
30
+ COPY . .
31
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
32
+ RUN pnpm build
33
+ RUN pnpm --filter {{projectName}} --prod deploy /prod/app
34
+
35
+ FROM node:22-bookworm-slim AS runtime-base
36
+
37
+ ENV NODE_ENV=production
38
+ ENV HOST=0.0.0.0
39
+ ENV PORT=3000
40
+ WORKDIR /app
41
+
42
+ COPY --from=build --chown=node:node /prod/app ./
43
+ RUN mkdir -p /app/data && chown node:node /app/data
44
+
45
+ USER node
46
+ EXPOSE 3000
47
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
48
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
49
+ CMD ["node", "build"]
50
+
51
+ FROM runtime-base AS runtime-with-data
52
+
53
+ COPY --from=embedded-data --chown=node:node . /app/data
54
+
55
+ FROM runtime-base AS runtime
56
+ {{/if}}
@@ -0,0 +1,18 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if isSvelteOnlyApp}}
3
+ server {
4
+ listen 8080;
5
+ server_name _;
6
+ root /usr/share/nginx/html;
7
+
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ location = /health {
13
+ access_log off;
14
+ add_header Content-Type text/plain;
15
+ return 200 'ok';
16
+ }
17
+ }
18
+ {{/if}}
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "{{projectName}}",
3
- "version": "0.1.0-alpha.49",
3
+ "version": "0.1.0-alpha.51",
4
4
  "private": true,
5
5
  "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}}",
6
6
  "keywords": ["hai", {{#unless isSvelteOnlyApp}}"sveltekit", {{/unless}}"svelte5", "{{appType}}"],
7
7
  "license": "Apache-2.0",
8
8
  "type": "module",
9
+ {{#unless isSvelteOnlyApp}}
10
+ "files": [
11
+ "build",
12
+ "config",
13
+ "data",
14
+ "package.json"
15
+ ],
16
+ {{/unless}}
9
17
  "scripts": {
10
18
  {{#if hasI18n}}
11
19
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
@@ -19,7 +27,13 @@
19
27
  "test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
20
28
  "test:watch": "vitest",
21
29
  "test:e2e": "playwright test",
22
- "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}"{{#if isCapacitorApp}},
30
+ "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}",
31
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
32
+ {{#unless isSvelteOnlyApp}}
33
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest .",
34
+ {{/unless}}
35
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest ."{{#unless isSvelteOnlyApp}},
36
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest ."{{/unless}}{{#if isCapacitorApp}},
23
37
  "cap:add:android": "cap add android",
24
38
  "cap:add:ios": "cap add ios",
25
39
  "cap:sync": "pnpm build && cap sync",
@@ -78,11 +92,7 @@
78
92
  "@capacitor/push-notifications": "^8.0.1",
79
93
  "@capacitor/status-bar": "^8.0.1",
80
94
  {{else}}
81
- {{#if_eq appType 'api'}}
82
95
  "@sveltejs/adapter-node": "^5.5.4",
83
- {{else}}
84
- "@sveltejs/adapter-auto": "^7.0.1",
85
- {{/if_eq}}
86
96
  {{/if}}
87
97
  {{#unless isSvelteOnlyApp}}
88
98
  "@sveltejs/kit": "^2.61.1",
@@ -8,15 +8,7 @@ import process from 'node:process'
8
8
  import { autoImportHaiUi } from '@h-ai/ui/auto-import'
9
9
  {{/if}}
10
10
  {{#unless isSvelteOnlyApp}}
11
- {{#if isCapacitorApp}}
12
- import adapter from '@sveltejs/adapter-static'
13
- {{else}}
14
- {{#if_eq appType 'api'}}
15
11
  import adapter from '@sveltejs/adapter-node'
16
- {{else}}
17
- import adapter from '@sveltejs/adapter-auto'
18
- {{/if_eq}}
19
- {{/if}}
20
12
  {{/unless}}
21
13
  import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
22
14