@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
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.63",
4
+ "version": "0.1.0-alpha.65",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "0.1.0-alpha.63",
33
+ "@h-ai/core": "0.1.0-alpha.65",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
36
  "fast-wrap-ansi": "^0.2.2",
@@ -41,11 +41,12 @@
41
41
  "yaml": "^2.9.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@h-ai/cache": "0.1.0-alpha.63",
45
- "@h-ai/deploy": "0.1.0-alpha.63",
46
- "@h-ai/reach": "0.1.0-alpha.63",
47
- "@h-ai/reldb": "0.1.0-alpha.63",
48
- "@h-ai/storage": "0.1.0-alpha.63",
44
+ "@h-ai/cache": "0.1.0-alpha.65",
45
+ "@h-ai/crypto": "0.1.0-alpha.65",
46
+ "@h-ai/deploy": "0.1.0-alpha.65",
47
+ "@h-ai/reach": "0.1.0-alpha.65",
48
+ "@h-ai/reldb": "0.1.0-alpha.65",
49
+ "@h-ai/storage": "0.1.0-alpha.65",
49
50
  "@types/fs-extra": "^11.0.4",
50
51
  "@types/node": "^26.6.5",
51
52
  "@types/prompts": "^2.4.9",
@@ -62,6 +63,7 @@
62
63
  "lint": "eslint .",
63
64
  "lint:fix": "eslint . --fix",
64
65
  "test": "vitest run",
66
+ "test:deploy-gates": "node tests/run-scaffold-gates.mjs --deploy",
65
67
  "test:e2e": "pnpm test:scaffold-gates",
66
68
  "test:scaffold-gates": "pnpm build && node tests/run-scaffold-gates.mjs",
67
69
  "test:watch": "vitest",
@@ -32,6 +32,8 @@
32
32
  <title>{page.data.appConfig.name}</title>
33
33
  </svelte:head>
34
34
 
35
- <div class='min-h-screen bg-base-200'>
36
- {@render children()}
35
+ <div class='min-h-screen flex flex-col bg-base-200'>
36
+ <header class='navbar border-b border-base-300 bg-base-100 px-6'><span class='text-lg font-semibold'>{page.data.appConfig.name}</span></header>
37
+ <div class='flex-1'>{@render children()}</div>
38
+ <footer class='border-t border-base-300 bg-base-100 p-3 text-center text-xs text-base-content/60'>{page.data.appConfig.name} · {page.data.appConfig.version} · {page.data.appConfig.env}</footer>
37
39
  </div>
@@ -12,3 +12,8 @@ playwright-report
12
12
  test-results
13
13
  **/data
14
14
  *.log
15
+ # 仅排除本机私有配置,与 Git 处理一致;公共 .env 与 dev/test/prod 随上下文构建。
16
+ **/.env.local
17
+ **/.env.*.local
18
+ **/.env.runtime
19
+ **/.hai/deploy
@@ -5,10 +5,10 @@ dist
5
5
  build
6
6
  playwright-report
7
7
  test-results
8
- .env
9
- .env.*
10
- !.env.example
11
- !.env.example.hbs
12
8
 
13
9
  # hai deploy 产物(compose / .env.runtime / 镜像 tar / sidecar 凭据)
14
10
  .hai/
11
+
12
+ # 本机私有配置;公共 .env 与 dev/test/prod 环境文件纳入版本控制。
13
+ .env.local
14
+ .env.*.local
@@ -1,5 +1,11 @@
1
1
  # syntax=docker/dockerfile:1.7
2
2
  FROM node:24-bookworm-slim AS build
3
+ ARG SQLITE_PREBUILD_MIRROR
4
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
5
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
6
+ ARG HAI_CORE_ENV=production
7
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
8
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
3
9
 
4
10
  ENV PNPM_HOME=/pnpm
5
11
  ENV PATH=$PNPM_HOME:$PATH
@@ -12,12 +18,16 @@ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
12
18
  COPY . .
13
19
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
14
20
  RUN pnpm build
21
+ 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
15
22
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{api.servicePackageName}} --prod /prod/app
16
23
 
17
24
  FROM node:24-bookworm-slim AS runtime-base
25
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
26
+ ARG HAI_CORE_ENV=production
18
27
 
19
28
  ENV NODE_ENV=production
20
- ENV HAI_CORE_ENV=production
29
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
30
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
21
31
  ENV HOST=0.0.0.0
22
32
  ENV PORT=3000
23
33
  WORKDIR /app
@@ -19,8 +19,10 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "tsup",
22
+ "check": "pnpm typecheck",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "lint": "eslint .",
25
+ "lint:fix": "eslint . --fix",
24
26
  "test": "vitest run",
25
27
  "test:watch": "vitest",
26
28
  "clean": "rimraf dist .turbo"
@@ -4,8 +4,8 @@
4
4
 
5
5
  name: {{projectName}} API Service
6
6
  version: 0.1.0
7
- env: ${HAI_ENV:development}
8
- debug: ${HAI_DEBUG:false}
7
+ env: ${HAI_CORE_ENV:development}
8
+ debug: ${HAI_CORE_DEBUG:false}
9
9
 
10
10
  logging:
11
11
  level: info
@@ -18,10 +18,10 @@ http:
18
18
  rpc: false
19
19
 
20
20
  cors:
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}
21
+ origin: ${HAI_SERV_CORS_ORIGIN:*}
22
+ nativeOrigins: ${HAI_SERV_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
23
+ {{#if features.iam}} credentials: ${HAI_SERV_CORS_CREDENTIALS:true}
24
+ {{else}} credentials: ${HAI_SERV_CORS_CREDENTIALS:false}
25
25
  {{/if}} allowedHeaders:
26
26
  - Authorization
27
27
  - Content-Type
@@ -21,13 +21,20 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsup",
24
+ "build:prod": "pnpm build",
25
+ "check": "pnpm typecheck",
24
26
  "typecheck": "tsc --noEmit",
25
27
  "lint": "eslint .",
28
+ "lint:fix": "eslint . --fix",
26
29
  "test": "vitest run",
27
30
  "test:watch": "vitest",
28
31
  "clean": "rimraf dist .turbo",
29
- "dev": "pnpm build && node dist/index.js",
30
- "start": "node dist/index.js"
32
+ "dev": "pnpm build && node --env-file=.env --env-file=.env.dev --env-file-if-exists=.env.local dist/index.js",
33
+ "start": "node --env-file=.env --env-file=.env.prod --env-file-if-exists=.env.local dist/index.js",
34
+ "dev:test": "pnpm build && node --env-file=.env --env-file=.env.test --env-file-if-exists=.env.local dist/index.js",
35
+ "dev:prod": "pnpm build && pnpm start",
36
+ "start:test": "node --env-file=.env --env-file=.env.test --env-file-if-exists=.env.local dist/index.js",
37
+ "start:prod": "pnpm start"
31
38
  },
32
39
  "dependencies": {
33
40
  "@h-ai/core": "{{haiPackageSpecifier}}",
@@ -8,15 +8,24 @@
8
8
  "description": "Typed hai API workspace with separated contract and service packages.",
9
9
  "scripts": {
10
10
  "dev": "pnpm --filter {{api.servicePackageName}} dev",
11
+ "start": "pnpm --filter {{api.servicePackageName}} start",
12
+ "start:test": "pnpm --filter {{api.servicePackageName}} start:test",
13
+ "start:prod": "pnpm --filter {{api.servicePackageName}} start:prod",
14
+ "dev:test": "pnpm --filter {{api.servicePackageName}} dev:test",
15
+ "dev:prod": "pnpm --filter {{api.servicePackageName}} dev:prod",
11
16
  "compile": "pnpm typecheck && pnpm build",
12
17
  "build": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present build",
13
18
  "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
14
19
  "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
15
20
  "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
16
21
  "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
22
+ "check": "pnpm typecheck",
17
23
  "typecheck": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present typecheck",
18
24
  "lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
25
+ "lint:fix": "eslint *.js *.ts e2e --fix && pnpm -r --if-present lint:fix",
19
26
  "test": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present test",
27
+ "test:watch": "pnpm -r --parallel --if-present test:watch",
28
+ "build:prod": "pnpm build",
20
29
  "test:e2e": "pnpm build && playwright test",
21
30
  "clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
22
31
  },
@@ -6,9 +6,6 @@ Dockerfile
6
6
  .codex
7
7
  .opencode
8
8
  .turbo
9
- .env
10
- .env.*
11
- !.env.example
12
9
  **/build
13
10
  **/dist
14
11
  **/node_modules
@@ -17,3 +14,8 @@ playwright-report
17
14
  test-results
18
15
  **/data
19
16
  *.log
17
+ # 仅排除本机私有配置,与 Git 处理一致;公共 .env 与 dev/test/prod 随上下文构建。
18
+ **/.env.local
19
+ **/.env.*.local
20
+ **/.env.runtime
21
+ **/.hai/deploy
@@ -1,17 +1,30 @@
1
+ # 构建产物目录
1
2
  node_modules
2
- **/data/
3
- .turbo
4
- dist
5
3
  build
4
+ dist
5
+ .turbo
6
+ android
7
+ ios
6
8
  **/src/lib/paraglide
7
- playwright-report
8
- test-results
9
- .env
10
- .env.*
11
- !.env.example
12
- !.env.example.hbs
13
9
  **/src-tauri/gen/
14
10
  **/src-tauri/target/
11
+ **/hash-core/target/
12
+
13
+ # 日志测试目录
14
+ *.log
15
+ test-results/
16
+ playwright-report/
17
+
18
+
19
+ # 系统文件
20
+ .DS_Store
21
+
22
+ # 本地数据目录
23
+ **/data/
15
24
 
16
25
  # hai deploy 产物(compose / .env.runtime / 镜像 tar / sidecar 凭据)
17
26
  .hai/
27
+
28
+ # 本机私有配置;公共 .env 与 dev/test/prod 环境文件纳入版本控制。
29
+ .env.local
30
+ .env.*.local
@@ -1,5 +1,11 @@
1
1
  # syntax=docker/dockerfile:1.7
2
2
  FROM node:24-bookworm-slim AS build
3
+ ARG SQLITE_PREBUILD_MIRROR
4
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
5
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
6
+ ARG HAI_CORE_ENV=production
7
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
8
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
3
9
 
4
10
  ENV PNPM_HOME=/pnpm
5
11
  ENV PATH=$PNPM_HOME:$PATH
@@ -15,12 +21,16 @@ RUN pnpm build
15
21
 
16
22
  FROM build AS service-build
17
23
 
24
+ 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
18
25
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
19
26
 
20
27
  FROM node:24-bookworm-slim AS service
28
+ ARG HAI_DEPLOY_ENVIRONMENT=prod
29
+ ARG HAI_CORE_ENV=production
21
30
 
22
31
  ENV NODE_ENV=production
23
- ENV HAI_CORE_ENV=production
32
+ ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
33
+ ENV HAI_CORE_ENV=$HAI_CORE_ENV
24
34
  ENV HOST=0.0.0.0
25
35
  ENV PORT=3000
26
36
  WORKDIR /app
@@ -9,13 +9,18 @@
9
9
  "scripts": {
10
10
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
11
11
  "paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
12
+ "dev:test": "vite --mode test",
13
+ "dev:prod": "vite --mode prod",
12
14
  "dev": "vite dev --host 127.0.0.1 --port {{fullstack.ports.app}}",
13
15
  "build": "pnpm paraglide:compile && vite build",
16
+ "build:prod": "pnpm paraglide:compile && vite build --mode prod",
14
17
  "package": "pnpm cap:sync",
15
18
  "deploy": "pnpm package",
16
19
  "preview": "vite preview --host 127.0.0.1 --port {{fullstack.ports.app}} --strictPort",
20
+ "check": "pnpm typecheck",
17
21
  "typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
18
22
  "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
19
24
  "test": "pnpm paraglide:compile && vitest run --passWithNoTests",
20
25
  "test:watch": "pnpm paraglide:compile && vitest",
21
26
  "clean": "rimraf dist build android ios .turbo src/lib/paraglide",
@@ -50,6 +55,7 @@
50
55
  "@capacitor/preferences": "{{fullstack.packageSpecifiers.capacitorPreferences}}",
51
56
  "@capacitor/push-notifications": "{{fullstack.packageSpecifiers.capacitorPushNotifications}}",
52
57
  "@capacitor/status-bar": "{{fullstack.packageSpecifiers.capacitorStatusBar}}",
58
+ "@h-ai/core": "{{haiPackageSpecifier}}",
53
59
  "@iconify-json/tabler": "{{fullstack.packageSpecifiers.iconifyJsonTabler}}",
54
60
  "@iconify/tailwind4": "{{fullstack.packageSpecifiers.iconifyTailwind}}",
55
61
  "@inlang/paraglide-js": "{{fullstack.packageSpecifiers.inlangParaglide}}",
@@ -2,21 +2,25 @@
2
2
  {{#if fullstack.frontends.app}}
3
3
  import process from 'node:process'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { core } from '@h-ai/core'
5
6
  import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
6
7
  import { paraglideVitePlugin } from '@inlang/paraglide-js'
7
8
  import { svelte } from '@sveltejs/vite-plugin-svelte'
8
9
  import tailwindcss from '@tailwindcss/vite'
9
- import { loadEnv } from 'vite'
10
10
  import { defineConfig } from 'vitest/config'
11
11
 
12
12
  const envDir = fileURLToPath(new URL('../../', import.meta.url))
13
13
 
14
14
  export default defineConfig(({ mode }) => {
15
- const env = loadEnv(mode, envDir, 'PUBLIC_')
15
+ const envResult = core.config.loadEnvFiles({ mode, envDir })
16
+ if (!envResult.success)
17
+ throw new Error(envResult.error.message)
18
+ const env = envResult.data
19
+ Object.assign(process.env, env)
16
20
  const target = (env.PUBLIC_API_BASE || process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000').replace(/\/api\/v1\/?$/, '')
17
21
  const proxy = { '/api': { target, changeOrigin: true } }
18
22
  return {
19
- envDir,
23
+ envDir: false,
20
24
  server: { proxy },
21
25
  preview: { proxy },
22
26
  build: {
@@ -9,11 +9,15 @@
9
9
  "scripts": {
10
10
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
11
11
  "paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
12
+ "dev:test": "vite --mode test",
13
+ "dev:prod": "vite --mode prod",
12
14
  "dev": "vite dev --host 127.0.0.1 --port {{fullstack.ports.desktop}}",
13
15
  "build": "pnpm paraglide:compile && vite build",
16
+ "build:prod": "pnpm paraglide:compile && vite build --mode prod",
14
17
  "package": "pnpm tauri:build",
15
18
  "deploy": "pnpm package",
16
19
  "preview": "vite preview --host 127.0.0.1 --port {{fullstack.ports.desktop}} --strictPort",
20
+ "check": "pnpm typecheck",
17
21
  "typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
18
22
  "lint": "eslint src vite.config.ts svelte.config.js",
19
23
  "lint:fix": "eslint src vite.config.ts svelte.config.js --fix",
@@ -31,6 +35,7 @@
31
35
  },
32
36
  "devDependencies": {
33
37
  "@antfu/eslint-config": "{{fullstack.packageSpecifiers.antfuEslintConfig}}",
38
+ "@h-ai/core": "{{haiPackageSpecifier}}",
34
39
  "@iconify-json/tabler": "{{fullstack.packageSpecifiers.iconifyJsonTabler}}",
35
40
  "@iconify/tailwind4": "{{fullstack.packageSpecifiers.iconifyTailwind}}",
36
41
  "@inlang/paraglide-js": "{{fullstack.packageSpecifiers.inlangParaglide}}",
@@ -2,23 +2,27 @@
2
2
  {{#if fullstack.frontends.desktop}}
3
3
  import process from 'node:process'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { core } from '@h-ai/core'
5
6
  import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
6
7
  import { paraglideVitePlugin } from '@inlang/paraglide-js'
7
8
  import { svelte } from '@sveltejs/vite-plugin-svelte'
8
9
  import tailwindcss from '@tailwindcss/vite'
9
- import { loadEnv } from 'vite'
10
10
  import { defineConfig } from 'vitest/config'
11
11
 
12
12
  const host = process.env.TAURI_DEV_HOST
13
13
  const envDir = fileURLToPath(new URL('../../', import.meta.url))
14
14
 
15
15
  export default defineConfig(({ mode }) => {
16
- const env = loadEnv(mode, envDir, 'PUBLIC_')
16
+ const envResult = core.config.loadEnvFiles({ mode, envDir })
17
+ if (!envResult.success)
18
+ throw new Error(envResult.error.message)
19
+ const env = envResult.data
20
+ Object.assign(process.env, env)
17
21
  const target = (env.PUBLIC_API_BASE || process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000').replace(/\/api\/v1\/?$/, '')
18
22
  const proxy = { '/api': { target, changeOrigin: true } }
19
23
 
20
24
  return {
21
- envDir,
25
+ envDir: false,
22
26
  preview: { proxy },
23
27
  build: {
24
28
  rolldownOptions: {
@@ -9,13 +9,18 @@
9
9
  "scripts": {
10
10
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
11
11
  "paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
12
+ "dev:test": "vite --mode test",
13
+ "dev:prod": "vite --mode prod",
12
14
  "dev": "vite dev --host 127.0.0.1 --port {{fullstack.ports.web}}",
13
15
  "build": "pnpm paraglide:compile && vite build",
16
+ "build:prod": "pnpm paraglide:compile && vite build --mode prod",
14
17
  "package": "pnpm build",
15
18
  "deploy": "pnpm package",
16
19
  "preview": "vite preview --host 127.0.0.1 --port {{fullstack.ports.web}} --strictPort",
20
+ "check": "pnpm typecheck",
17
21
  "typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
18
22
  "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
19
24
  "test": "vitest run --passWithNoTests",
20
25
  "test:watch": "vitest",
21
26
  "clean": "rimraf dist build .turbo src/lib/paraglide"
@@ -26,6 +31,7 @@
26
31
  },
27
32
  "devDependencies": {
28
33
  "@antfu/eslint-config": "{{fullstack.packageSpecifiers.antfuEslintConfig}}",
34
+ "@h-ai/core": "{{haiPackageSpecifier}}",
29
35
  "@iconify-json/tabler": "{{fullstack.packageSpecifiers.iconifyJsonTabler}}",
30
36
  "@iconify/tailwind4": "{{fullstack.packageSpecifiers.iconifyTailwind}}",
31
37
  "@inlang/paraglide-js": "{{fullstack.packageSpecifiers.inlangParaglide}}",
@@ -2,21 +2,25 @@
2
2
  {{#if fullstack.frontends.web}}
3
3
  import process from 'node:process'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { core } from '@h-ai/core'
5
6
  import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
6
7
  import { paraglideVitePlugin } from '@inlang/paraglide-js'
7
8
  import { svelte } from '@sveltejs/vite-plugin-svelte'
8
9
  import tailwindcss from '@tailwindcss/vite'
9
- import { loadEnv } from 'vite'
10
10
  import { defineConfig } from 'vitest/config'
11
11
 
12
12
  const envDir = fileURLToPath(new URL('../../', import.meta.url))
13
13
 
14
14
  export default defineConfig(({ mode }) => {
15
- const env = loadEnv(mode, envDir, 'PUBLIC_')
15
+ const envResult = core.config.loadEnvFiles({ mode, envDir })
16
+ if (!envResult.success)
17
+ throw new Error(envResult.error.message)
18
+ const env = envResult.data
19
+ Object.assign(process.env, env)
16
20
  const target = (env.PUBLIC_API_BASE || process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000').replace(/\/api\/v1\/?$/, '')
17
21
  const proxy = { '/api': { target, changeOrigin: true } }
18
22
  return {
19
- envDir,
23
+ envDir: false,
20
24
  server: { proxy },
21
25
  preview: { proxy },
22
26
  build: {
@@ -61,6 +61,98 @@ test('service rejects plaintext business requests', async ({ request }) => {
61
61
  })
62
62
 
63
63
  {{#if fullstack.e2eFrontend}}
64
+ test('hash routing restores direct links, refresh and browser history', async ({ page }) => {
65
+ await page.goto('/#/roles?source=direct')
66
+ await login(page, 'admin', adminPassword)
67
+ await expect(page.getByTestId('roles-page')).toBeVisible()
68
+ await expect(page).toHaveURL(/#\/roles\?source=direct$/)
69
+ await page.reload()
70
+ await expect(page.getByTestId('roles-page')).toBeVisible()
71
+ await expect(page.locator('header')).toBeVisible()
72
+ await expect(page.locator('footer')).toBeVisible()
73
+ await page.getByTestId('nav-settings').click()
74
+ await expect(page).toHaveURL(/#\/settings$/)
75
+ await page.goBack()
76
+ await expect(page.getByTestId('roles-page')).toBeVisible()
77
+ await page.goForward()
78
+ await expect(page.getByTestId('profile-form')).toBeVisible()
79
+ await page.goto('/#/unknown-page')
80
+ await expect(page).toHaveURL(/#\/users$/)
81
+ await expect(page.getByTestId('users-page')).toBeVisible()
82
+ })
83
+
84
+ test('branded authentication, profile layout and blur validation work across screen sizes', async ({ page }, testInfo) => {
85
+ test.setTimeout(60_000)
86
+ await page.setViewportSize({ width: 1440, height: 960 })
87
+ await page.goto('/')
88
+ await expect(page.getByRole('heading', { name: configuredName, exact: true })).toBeVisible()
89
+ await expect(page.locator('header')).toBeVisible()
90
+ await expect(page.locator('footer')).toBeVisible()
91
+ await capture(page, '{{projectName}}-login-desktop.png', testInfo.outputPath('login-desktop.png'))
92
+ await page.getByRole('button', { name: /立即注册|Register now/, exact: true }).click()
93
+ await expect(page.locator('.register-form')).toBeVisible()
94
+ await capture(page, '{{projectName}}-register-desktop.png', testInfo.outputPath('register-desktop.png'))
95
+ await page.setViewportSize({ width: 390, height: 844 })
96
+ await capture(page, '{{projectName}}-register-mobile.png', testInfo.outputPath('register-mobile.png'))
97
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
98
+ await page.reload()
99
+ await capture(page, '{{projectName}}-login-mobile.png', testInfo.outputPath('login-mobile.png'))
100
+ await page.setViewportSize({ width: 1440, height: 960 })
101
+ await login(page, 'admin', adminPassword)
102
+ await page.getByTestId('nav-settings').click()
103
+ const profile = page.getByTestId('profile-form')
104
+ await expect(profile).toBeVisible()
105
+ const email = profile.locator('input[name="email"]')
106
+ await email.fill('invalid-email')
107
+ await email.press('Tab')
108
+ await expect(email).toHaveAttribute('aria-invalid', 'true')
109
+ await capture(page, '{{projectName}}-profile-blur-error.png', testInfo.outputPath('profile-blur-error.png'))
110
+ await email.fill('')
111
+ await expect(email).not.toHaveAttribute('aria-invalid')
112
+ await capture(page, '{{projectName}}-settings-desktop.png', testInfo.outputPath('settings-desktop.png'))
113
+ for (const width of [768, 390]) {
114
+ await page.setViewportSize({ width, height: 960 })
115
+ const fields = await profile.locator('.hai-form-field').evaluateAll(elements => elements.map((field) => {
116
+ const label = field.querySelector('label')!.getBoundingClientRect()
117
+ const control = field.querySelector('input')!.getBoundingClientRect()
118
+ return { labelBottom: label.bottom, controlTop: control.top, controlRight: control.right }
119
+ }))
120
+ for (const field of fields) {
121
+ expect(field.labelBottom).toBeLessThanOrEqual(field.controlTop)
122
+ expect(field.controlRight).toBeLessThanOrEqual(width)
123
+ }
124
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
125
+ await capture(page, `{{projectName}}-settings-${width}.png`, testInfo.outputPath(`settings-${width}.png`))
126
+ }
127
+ await page.getByTestId('password-form').scrollIntoViewIfNeeded()
128
+ await capture(page, '{{projectName}}-password-mobile.png', testInfo.outputPath('password-mobile.png'))
129
+ await page.setViewportSize({ width: 1440, height: 960 })
130
+ await page.getByTestId('nav-users').click()
131
+ await page.getByTestId('create-user').click()
132
+ const username = page.getByTestId('user-username').locator('input')
133
+ await username.focus()
134
+ await username.press('Tab')
135
+ await expect(username).toHaveAttribute('aria-invalid', 'true')
136
+ await username.fill('a')
137
+ await expect(username).toHaveAttribute('aria-invalid', 'true')
138
+ await page.getByTestId('user-password').locator('input').fill('short')
139
+ await page.getByTestId('user-password').locator('input').press('Tab')
140
+ await expect(page.getByTestId('user-password').locator('input')).toHaveAttribute('aria-invalid', 'true')
141
+ await capture(page, '{{projectName}}-create-blur-error.png', testInfo.outputPath('create-blur-error.png'))
142
+ await page.getByTestId('user-editor').getByRole('button', { name: /取消|Cancel/, exact: true }).click()
143
+ await page.getByTestId('create-user').click()
144
+ await expect(page.getByTestId('user-editor').locator('[aria-invalid="true"]')).toHaveCount(0)
145
+ await page.getByTestId('user-editor').getByRole('button', { name: /取消|Cancel/, exact: true }).click()
146
+ await userEditor(page, 'admin')
147
+ const editEmail = page.getByTestId('user-email').locator('input')
148
+ await editEmail.fill('invalid-email')
149
+ await editEmail.press('Tab')
150
+ await expect(editEmail).toHaveAttribute('aria-invalid', 'true')
151
+ await capture(page, '{{projectName}}-edit-blur-error.png', testInfo.outputPath('edit-blur-error.png'))
152
+ await editEmail.fill('admin@example.com')
153
+ await expect(editEmail).not.toHaveAttribute('aria-invalid')
154
+ })
155
+
64
156
  test('first-use tour, help search and settings navigation work on desktop and mobile', async ({ page }, testInfo) => {
65
157
  await page.goto('/')
66
158
  await login(page, 'admin', adminPassword, true)
@@ -109,7 +201,7 @@ const additionalFrontends = [
109
201
  {{#each fullstack.frontendApps}}
110
202
  {
111
203
  target: '{{target}}',
112
- url: process.env.HAI_E2E_{{target}}_URL || 'http://127.0.0.1:{{port}}',
204
+ url: process.env['HAI_E2E_{{target}}_URL'.toUpperCase()] || 'http://127.0.0.1:{{port}}',
113
205
  },
114
206
  {{/each}}
115
207
  ].filter(frontend => frontend.target !== '{{fullstack.e2eFrontend.target}}')
@@ -10,6 +10,12 @@
10
10
  "i18n:compile": "pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile{{#each fullstack.frontendApps}} && pnpm --filter {{packageName}} paraglide:compile{{/each}}",
11
11
  "postinstall": "pnpm i18n:compile",
12
12
  "dev": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev",
13
+ "start": "pnpm --filter {{fullstack.servPackageName}} start",
14
+ "start:test": "pnpm --filter {{fullstack.servPackageName}} start:test",
15
+ "start:prod": "pnpm --filter {{fullstack.servPackageName}} start:prod",
16
+ "dev:test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:test",
17
+ "dev:prod": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:prod",
18
+ "build:prod": "pnpm build && pnpm -r --if-present build:prod",
13
19
  "compile": "pnpm typecheck && pnpm build",
14
20
  "build": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
15
21
  "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
@@ -20,9 +26,12 @@
20
26
  "podman:build:web": "podman build --format docker --jobs=1 --target web -t {{projectName}}-web:latest ."{{/if}},
21
27
  "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}}",
22
28
  "deploy": "pnpm package",
29
+ "check": "pnpm typecheck",
23
30
  "typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
24
31
  "lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
32
+ "lint:fix": "eslint *.js *.ts e2e --fix && pnpm -r --if-present lint:fix",
25
33
  "test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present test",
34
+ "test:watch": "pnpm -r --parallel --if-present test:watch",
26
35
  "test:e2e": "pnpm build && playwright test",
27
36
  "clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
28
37
  },
@@ -32,6 +41,9 @@
32
41
  "{{fullstack.sharedPackageName}}": "workspace:*"
33
42
  },
34
43
  "devDependencies": {
44
+ {{#if features.deploy}}
45
+ "@h-ai/deploy": "{{haiPackageSpecifier}}",
46
+ {{/if}}
35
47
  "@antfu/eslint-config": "{{fullstack.packageSpecifiers.antfuEslintConfig}}",
36
48
  "@playwright/test": "{{fullstack.packageSpecifiers.playwright}}",
37
49
  "@sveltejs/vite-plugin-svelte": "{{fullstack.packageSpecifiers.sveltePlugin}}",
@@ -1,12 +1,12 @@
1
1
  # {{fullstack.contractPackageName}}
2
2
 
3
- 共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract。
3
+ 共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract。业务 schema、角色、流程及限额是模板初始方案,按用户需求修改,并同步服务端授权、调用方、测试与说明;不要求需求沿用默认业务。
4
4
 
5
5
  ## 骨架说明
6
6
 
7
7
  - `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
8
8
  - `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
9
- - `src/admin-schemas.ts` / `src/admin-contract.ts`:管理员用户、固定角色、分页审计、个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,通过服务端配置启用注册、密码/LDAP/验证码登录及密码重置。
9
+ - `src/admin-schemas.ts` / `src/admin-contract.ts`:示例管理员用户、初始 admin/user 角色、分页审计、个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,通过服务端配置启用注册、密码/LDAP/验证码登录及密码重置。
10
10
  - `src/{{projectName}}-contract.ts`:工程级 contract,供 `@h-ai/serv` 与 `@h-ai/api-client` 共用。
11
11
 
12
12
  只使用一次的输出 Schema 直接内联到 `.output(...)`;同一 contract 文件内重复使用时才保留私有常量。
@@ -14,8 +14,10 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "scripts": {
16
16
  "build": "tsup",
17
+ "check": "pnpm typecheck",
17
18
  "typecheck": "tsc --noEmit",
18
19
  "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
19
21
  "test": "vitest run",
20
22
  "test:watch": "vitest",
21
23
  "clean": "rimraf dist .turbo"