@h-ai/cli 0.1.0-alpha.64 → 0.1.0-alpha.66

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 (122) hide show
  1. package/README.en.md +31 -9
  2. package/README.ja.md +30 -8
  3. package/README.md +38 -16
  4. package/dist/{chunk-X3RSYNBG.js → chunk-FWN5B54L.js} +94 -4
  5. package/dist/chunk-FWN5B54L.js.map +1 -0
  6. package/dist/chunk-QYX6V7H2.js +5613 -0
  7. package/dist/chunk-QYX6V7H2.js.map +1 -0
  8. package/dist/cli-config-templates-3XSFIXBP.js +5 -0
  9. package/dist/{cli-config-templates-QRF6B5T4.js.map → cli-config-templates-3XSFIXBP.js.map} +1 -1
  10. package/dist/{cli-skill-templates-EZFW7D43.js → cli-skill-templates-TSYEQTCY.js} +3 -3
  11. package/dist/{cli-skill-templates-EZFW7D43.js.map → cli-skill-templates-TSYEQTCY.js.map} +1 -1
  12. package/dist/index.js +733 -210
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-DUGLX4YB.js → src-RDQ2EIBT.js} +1054 -660
  15. package/dist/{src-DUGLX4YB.js.map → src-RDQ2EIBT.js.map} +1 -1
  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 +4 -0
  19. package/templates/apps/api/.gitignore.hbs +6 -5
  20. package/templates/apps/api/Dockerfile.hbs +5 -8
  21. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +2 -0
  22. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +2 -2
  23. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
  24. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
  25. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +8 -2
  26. package/templates/apps/api/hai.build.json.hbs +1 -0
  27. package/templates/apps/api/package.json.hbs +13 -7
  28. package/templates/apps/fullstack/.dockerignore.hbs +4 -3
  29. package/templates/apps/fullstack/.gitignore.hbs +24 -10
  30. package/templates/apps/fullstack/Dockerfile.hbs +6 -8
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +0 -3
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +5 -1
  33. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +8 -4
  34. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -6
  35. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +6 -2
  36. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
  37. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
  38. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +8 -4
  39. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -6
  40. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +5 -1
  41. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +8 -4
  42. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +24 -1
  43. package/templates/apps/fullstack/hai.build.json.hbs +7 -0
  44. package/templates/apps/fullstack/package.json.hbs +16 -11
  45. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -2
  46. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +2 -0
  47. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +1 -1
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +8 -14
  49. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -6
  50. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -8
  51. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
  52. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +8 -2
  53. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +4 -4
  54. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +4 -2
  55. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +2 -0
  56. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +14 -12
  57. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +1 -1
  58. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +17 -3
  59. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +15 -5
  60. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -1
  61. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +0 -1
  62. package/templates/apps/h5/src/routes/+layout.svelte.hbs +13 -13
  63. package/templates/apps/mobile-app/index.html.hbs +1 -0
  64. package/templates/apps/mobile-app/public/runtime-config.js +1 -0
  65. package/templates/apps/mobile-app/src/App.svelte.hbs +46 -117
  66. package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
  67. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
  68. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
  69. package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
  70. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
  71. package/templates/base/.dockerignore.hbs +4 -0
  72. package/templates/base/.gitignore.hbs +20 -5
  73. package/templates/base/Dockerfile.hbs +10 -11
  74. package/templates/base/docker/nginx.conf.hbs +6 -0
  75. package/templates/base/e2e/app.spec.ts.hbs +14 -0
  76. package/templates/base/hai.build.json.hbs +1 -0
  77. package/templates/base/package.json.hbs +25 -17
  78. package/templates/base/svelte.config.js.hbs +2 -0
  79. package/templates/base/tests/scaffold.test.ts.hbs +9 -3
  80. package/templates/base/vite.config.ts.hbs +11 -5
  81. package/templates/shared/.gitattributes +5 -0
  82. package/templates/shared/container-runtime-config.sh +30 -0
  83. package/templates/shared/hash-router.svelte.ts +83 -0
  84. package/templates/shared/run-e2e.mjs +65 -0
  85. package/templates/shared/vite-runtime-config.ts +54 -0
  86. package/templates/skills/AGENTS.md +15 -1
  87. package/templates/skills/bridges/admin/AGENTS.md +17 -1
  88. package/templates/skills/bridges/api/AGENTS.md +17 -3
  89. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +35 -11
  90. package/templates/skills/bridges/h5/AGENTS.md +17 -1
  91. package/templates/skills/bridges/mobile-app/AGENTS.md +19 -3
  92. package/templates/skills/bridges/website/AGENTS.md +17 -1
  93. package/templates/skills/hai-ai/reference.md +2 -2
  94. package/templates/skills/hai-app-create/SKILL.md +5 -3
  95. package/templates/skills/hai-app-create/development.md +128 -2
  96. package/templates/skills/hai-app-create/reference.md +4 -4
  97. package/templates/skills/hai-app-review/SKILL.md +2 -2
  98. package/templates/skills/hai-app-tests/SKILL.md +2 -1
  99. package/templates/skills/hai-app-tests/reference.md +1 -1
  100. package/templates/skills/hai-build/SKILL.md +16 -1
  101. package/templates/skills/hai-core/SKILL.md +2 -1
  102. package/templates/skills/hai-deploy/SKILL.md +52 -252
  103. package/templates/skills/hai-deploy/middleware.md +88 -0
  104. package/templates/skills/hai-framework-sync/SKILL.md +1 -1
  105. package/templates/skills/hai-framework-sync/sync.md +1 -1
  106. package/templates/skills/hai-reach/reference.md +8 -8
  107. package/templates/skills/hai-spec-implement/SKILL.md +12 -9
  108. package/templates/skills/hai-spec-implement/changes.md +67 -0
  109. package/templates/skills/hai-spec-implement/templates.md +25 -3
  110. package/templates/skills/hai-spec-implement/workflow.md +7 -6
  111. package/templates/skills/hai-spec-review/SKILL.md +14 -5
  112. package/templates/skills/hai-spec-review/checklist.md +4 -3
  113. package/templates/skills/hai-storage/SKILL.md +2 -0
  114. package/templates/skills/hai-ui/reference.md +2 -0
  115. package/dist/chunk-WW2MIO6T.js +0 -992
  116. package/dist/chunk-WW2MIO6T.js.map +0 -1
  117. package/dist/chunk-X3RSYNBG.js.map +0 -1
  118. package/dist/cli-config-templates-QRF6B5T4.js +0 -5
  119. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/AuditManagement.svelte.hbs → pages/audit.svelte.hbs} +0 -0
  120. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/RoleManagement.svelte.hbs → pages/roles.svelte.hbs} +0 -0
  121. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/ProfileSettings.svelte.hbs → pages/settings.svelte.hbs} +0 -0
  122. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/UserManagement.svelte.hbs → pages/users.svelte.hbs} +0 -0
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { generateConfigFile } from './chunk-WW2MIO6T.js';
3
- import { cliM } from './chunk-X3RSYNBG.js';
2
+ import { generateConfigFile, runtimeEnvironmentVariables, renderEnvironment, moduleEnvironmentVariables, parameterizeConfig } from './chunk-QYX6V7H2.js';
3
+ import { cliM } from './chunk-FWN5B54L.js';
4
4
  import './chunk-2H7UOFLK.js';
5
5
  import process4 from 'process';
6
6
  import { cac } from 'cac';
7
7
  import chalk from 'chalk';
8
- import path2, { resolve } from 'path';
9
- import { fileURLToPath } from 'url';
8
+ import path2, { resolve, join, dirname } from 'path';
9
+ import { fileURLToPath, pathToFileURL } from 'url';
10
10
  import { wrapAnsi } from 'fast-wrap-ansi';
11
11
  import fse from 'fs-extra';
12
12
  import Handlebars2 from 'handlebars';
@@ -14,10 +14,11 @@ import { core } from '@h-ai/core';
14
14
  import ora from 'ora';
15
15
  import prompts from 'prompts';
16
16
  import { Buffer } from 'buffer';
17
- import { execSync, execFileSync } from 'child_process';
18
- import { randomBytes } from 'crypto';
19
- import { stringify, parse } from 'yaml';
20
- import { existsSync, readFileSync } from 'fs';
17
+ import { execSync, execFileSync, spawn } from 'child_process';
18
+ import { randomBytes, createHash } from 'crypto';
19
+ import { stringify, parseDocument } from 'yaml';
20
+ import { readFileSync, mkdirSync, mkdtempSync, existsSync, createReadStream, writeFileSync, renameSync, rmSync } from 'fs';
21
+ import { createRequire } from 'module';
21
22
 
22
23
  var CAMEL_CASE_SEPARATOR_REGEX = /[-_\s]+(.)?/g;
23
24
  var LEADING_CHAR_REGEX = /^./;
@@ -121,7 +122,7 @@ Handlebars2.registerHelper("yaml", (key, value) => stringify({ [key]: value }).t
121
122
  Handlebars2.registerHelper("jsString", (value) => `'${JSON.stringify(value).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`);
122
123
  Handlebars2.registerHelper("moduleConfig", function(moduleKey, enabled) {
123
124
  const content = generateConfigFile(moduleKey, this.moduleConfigs).trimEnd();
124
- const configured = this.appType === "fullstack" && moduleKey === "iam" ? content.replace("seedDefaultData: true", "seedDefaultData: false").replace("variant: card", "variant: split") : content;
125
+ const configured = this.appType === "fullstack" && moduleKey === "iam" ? content.replace("HAI_IAM_SEEDDEFAULTDATA:true", "HAI_IAM_SEEDDEFAULTDATA:false").replace("variant: card", "variant: split") : content;
125
126
  return enabled ? configured : `# # \u672A\u542F\u7528\uFF1A\u5B89\u88C5\u5BF9\u5E94\u6A21\u5757\u5E76\u5728\u670D\u52A1\u7AEF runtime \u4E2D\u521D\u59CB\u5316\uFF0C\u518D\u53D6\u6D88\u6240\u9700\u914D\u7F6E\u7684\u6CE8\u91CA\u3002
126
127
  ${configured.split("\n").map((line) => line ? `# ${line}` : "").join("\n")}`;
127
128
  });
@@ -207,6 +208,7 @@ var HAI_CATALOG_PACKAGE_NAMES = [
207
208
  "@h-ai/audit",
208
209
  "@h-ai/cache",
209
210
  "@h-ai/capacitor",
211
+ "@h-ai/cli",
210
212
  "@h-ai/core",
211
213
  "@h-ai/crypto",
212
214
  "@h-ai/datapipe",
@@ -387,6 +389,7 @@ function buildFullstackPackageSpecifiers(useCatalogProtocol) {
387
389
  function buildApiWorkspaceCatalogPackages(featureMap) {
388
390
  const packageNames = [
389
391
  ...API_WORKSPACE_EXTERNAL_CATALOG_PACKAGE_NAMES,
392
+ "@h-ai/cli",
390
393
  "@h-ai/api-client",
391
394
  "@h-ai/api-contract",
392
395
  "@h-ai/core",
@@ -421,7 +424,7 @@ function buildApiWorkspaceCatalogPackages(featureMap) {
421
424
  return pickCatalogPackages(packageNames, FULLSTACK_CATALOG_PACKAGES);
422
425
  }
423
426
  function buildAppHaiCatalogPackages(options) {
424
- const packageNames = [];
427
+ const packageNames = ["@h-ai/cli"];
425
428
  if (options.isSvelteOnlyApp) {
426
429
  packageNames.push("@h-ai/api-client", "@h-ai/api-contract", "@h-ai/capacitor", "@h-ai/ui");
427
430
  return pickCatalogPackages(packageNames, HAI_CATALOG_PACKAGES);
@@ -465,9 +468,10 @@ function buildAppHaiCatalogPackages(options) {
465
468
  packageNames.push("@h-ai/capacitor");
466
469
  return pickCatalogPackages(packageNames, HAI_CATALOG_PACKAGES);
467
470
  }
468
- function buildFullstackCatalogPackages(frontends) {
471
+ function buildFullstackCatalogPackages(frontends, deploy = false) {
469
472
  const packageNames = [
470
473
  ...FULLSTACK_ROOT_CATALOG_PACKAGE_NAMES,
474
+ "@h-ai/cli",
471
475
  ...FULLSTACK_CONTRACT_CATALOG_PACKAGE_NAMES,
472
476
  ...FULLSTACK_SERV_CATALOG_PACKAGE_NAMES,
473
477
  ...FULLSTACK_SHARED_CATALOG_PACKAGE_NAMES
@@ -478,6 +482,8 @@ function buildFullstackCatalogPackages(frontends) {
478
482
  packageNames.push(...FULLSTACK_APP_CATALOG_PACKAGE_NAMES);
479
483
  if (frontends.includes("desktop"))
480
484
  packageNames.push(...FULLSTACK_DESKTOP_CATALOG_PACKAGE_NAMES);
485
+ if (deploy)
486
+ packageNames.push("@h-ai/deploy");
481
487
  return pickCatalogPackages(packageNames, FULLSTACK_CATALOG_PACKAGES);
482
488
  }
483
489
  var FEATURE_ROUTE_DIRS = {
@@ -594,7 +600,7 @@ function buildTemplateContext(options) {
594
600
  const selectedFrontends = options.frontends ?? DEFAULT_FULLSTACK_FRONTENDS;
595
601
  let haiCatalogPackages;
596
602
  if (isFullstack) {
597
- haiCatalogPackages = buildFullstackCatalogPackages(selectedFrontends);
603
+ haiCatalogPackages = buildFullstackCatalogPackages(selectedFrontends, featureMap.deploy);
598
604
  } else if (isApi) {
599
605
  haiCatalogPackages = buildApiWorkspaceCatalogPackages(featureMap);
600
606
  } else {
@@ -769,6 +775,16 @@ async function generateFromTemplates(projectPath, context) {
769
775
  }
770
776
  }
771
777
  await renderDynamicFiles(root, projectPath, context);
778
+ await fse.copy(path2.join(root, "shared", "run-e2e.mjs"), path2.join(projectPath, "scripts", "run-e2e.mjs"));
779
+ await fse.copy(path2.join(root, "shared", ".gitattributes"), path2.join(projectPath, ".gitattributes"));
780
+ if (context.isSvelteOnlyApp) {
781
+ await fse.copy(path2.join(root, "shared", "vite-runtime-config.ts"), path2.join(projectPath, "scripts", "vite-runtime-config.ts"));
782
+ await fse.copy(path2.join(root, "shared", "container-runtime-config.sh"), path2.join(projectPath, "docker", "19-runtime-config.sh"));
783
+ }
784
+ if (context.isSvelteOnlyApp || context.appType === "fullstack") {
785
+ const navigationDir = context.appType === "fullstack" ? path2.join(projectPath, "packages", `${context.projectName}-shared`, "src", "lib", "navigation") : path2.join(projectPath, "src", "lib", "navigation");
786
+ await fse.copy(path2.join(root, "shared", "hash-router.svelte.ts"), path2.join(navigationDir, "hash-router.svelte.ts"));
787
+ }
772
788
  if (context.hasI18n) {
773
789
  const i18nDir = path2.join(root, "i18n");
774
790
  if (await fse.pathExists(i18nDir)) {
@@ -839,7 +855,8 @@ async function renderHbsInDir(dir, destRoot, context) {
839
855
  continue;
840
856
  }
841
857
  const destPath = path2.join(destRoot, outRelPath);
842
- const normalizedRendered = normalizeRenderedContent(rendered, outRelPath);
858
+ const configMatch = /(?:^|[/\\])config[/\\]_([^/\\]+)\.ya?ml$/.exec(outRelPath);
859
+ const normalizedRendered = normalizeRenderedContent(configMatch ? parameterizeConfig(configMatch[1], rendered) : rendered, outRelPath);
843
860
  await fse.ensureDir(path2.dirname(destPath));
844
861
  await fse.writeFile(destPath, normalizedRendered, "utf-8");
845
862
  }
@@ -1124,7 +1141,7 @@ async function createProject(options) {
1124
1141
  });
1125
1142
  await generateFromTemplates(projectPath, context);
1126
1143
  {
1127
- const { generateConfigFile: generateConfigFile2, generateEnvExample } = await import('./cli-config-templates-QRF6B5T4.js');
1144
+ const { generateConfigFile: generateConfigFile2, generateEnvExample } = await import('./cli-config-templates-3XSFIXBP.js');
1128
1145
  const configs = resolvedOptions.moduleConfigs;
1129
1146
  const isApiWorkspace = resolvedOptions.appType === "api";
1130
1147
  const isFullstack = resolvedOptions.appType === "fullstack";
@@ -1145,26 +1162,60 @@ async function createProject(options) {
1145
1162
  generateConfigFile2("core", configs)
1146
1163
  );
1147
1164
  }
1148
- const envExample = isApiWorkspace ? `${generateEnvExample(resolvedOptions.features, configs)}
1149
- # =============================================================================
1150
- # Service Runtime
1151
- # =============================================================================
1152
- PORT=3000
1153
- ` : isFullstack ? `${generateEnvExample(resolvedOptions.features, configs, true)}
1154
- # First-start administrator password. The CLI generates a random value in .env.
1165
+ const configContents = {};
1166
+ for (const file of await fse.readdir(configDir)) {
1167
+ if (/^_.*\.yml$/.test(file))
1168
+ configContents[file.slice(1, -4)] = await fse.readFile(path2.join(configDir, file), "utf8");
1169
+ }
1170
+ const envFeatures = resolvedOptions.features;
1171
+ const envContents = generateEnvExample(envFeatures, configs, isFullstack, configContents);
1172
+ let envExample = isApiWorkspace ? envContents : isFullstack ? `${envContents}
1173
+ # First-start administrator password; must satisfy the IAM password policy.
1155
1174
  HAI_ADMIN_DEFAULT_PASSWORD=
1156
1175
 
1157
- # Local development API. Use your public API origin for deployment/native clients.
1176
+ # Public API origin; never include credentials.
1158
1177
  PUBLIC_API_BASE=http://127.0.0.1:3000
1159
- ` : generateEnvExample(resolvedOptions.features, configs);
1178
+ ` : context.isSvelteOnlyApp ? `${envContents}
1179
+ # Public API URL including /api/v1; never include credentials.
1180
+ PUBLIC_API_BASE=http://10.0.2.2:3000/api/v1
1181
+ ` : envContents;
1182
+ const runtimeVariables = /* @__PURE__ */ new Map();
1183
+ for (const file of await fse.readdir(projectPath, { recursive: true, encoding: "utf8" })) {
1184
+ if (!/\.(?:ts|js|mjs|svelte|envsh)$/.test(file))
1185
+ continue;
1186
+ for (const variable of runtimeEnvironmentVariables(await fse.readFile(path2.join(projectPath, file), "utf8")))
1187
+ runtimeVariables.set(variable.name, variable);
1188
+ }
1189
+ const existingNames = new Set([...envExample.matchAll(/^(?:#\s*)?([A-Z][A-Z0-9_]*)=/gm)].map((match) => match[1]));
1190
+ const runtimeExample = renderEnvironment([...runtimeVariables.values()].filter((variable) => !existingNames.has(variable.name)));
1191
+ if (runtimeExample)
1192
+ envExample += `
1193
+ # \u53EF\u9009\u8FD0\u884C\u5165\u53E3\uFF08\u6309\u7528\u9014\u6CE8\u5165\uFF0C\u4E0D\u8FDB\u5165\u516C\u5171\u9ED8\u8BA4\u503C\uFF09\uFF1A
1194
+ ${runtimeExample.split("\n").map((line) => line && !line.startsWith("#") ? `# ${line}` : line).join("\n")}
1195
+ `;
1160
1196
  await writeFile(envExamplePath, envExample);
1197
+ const envDirectory = path2.dirname(envExamplePath);
1198
+ const sharedVariables = envExample.split("\n").filter((line) => /^[A-Z][A-Z0-9_]*=/.test(line));
1199
+ await writeFile(path2.join(envDirectory, ".env"), `# \u516C\u5171\u57FA\u7840\u914D\u7F6E\uFF08\u53EF\u63D0\u4EA4\uFF09\uFF1B\u5B8C\u6574\u5B57\u6BB5\u8BF4\u660E\u89C1 .env.example\u3002
1200
+ ${sharedVariables.join("\n")}
1201
+ `);
1202
+ for (const [mode, environment] of [["dev", "development"], ["test", "test"], ["prod", "production"]]) {
1203
+ await writeFile(path2.join(envDirectory, `.env.${mode}`), `# \u516C\u5171 ${mode} \u73AF\u5883\u914D\u7F6E\uFF1B\u672C\u673A\u79C1\u6709\u503C\u653E .env.local\uFF0C\u8FDB\u7A0B\u73AF\u5883\u53D8\u91CF\u4F18\u5148\u3002
1204
+ HAI_CORE_ENV=${environment}
1205
+ HAI_CORE_DEBUG=false
1206
+ HAI_CORE_LOGGING_LEVEL=${mode === "test" ? "warn" : mode === "dev" ? "debug" : "info"}
1207
+ `);
1208
+ }
1161
1209
  if (isFullstack) {
1162
1210
  const password = `Aa1!${randomBytes(24).toString("base64url")}`;
1163
- await fse.writeFile(path2.join(projectPath, ".env"), envExample.replace("HAI_ADMIN_DEFAULT_PASSWORD=\n", `HAI_ADMIN_DEFAULT_PASSWORD=${password}
1164
- `), { mode: 384 });
1211
+ await fse.writeFile(path2.join(projectPath, ".env.local"), `# \u672C\u673A\u79C1\u6709\u914D\u7F6E\uFF0C\u4E0D\u63D0\u4EA4 Git\u3002
1212
+ HAI_ADMIN_DEFAULT_PASSWORD=${password}
1213
+ `, { mode: 384 });
1214
+ } else {
1215
+ await writeFile(path2.join(envDirectory, ".env.local"), "# \u672C\u673A\u79C1\u6709\u914D\u7F6E\uFF0C\u4E0D\u63D0\u4EA4 Git\uFF1B\u8986\u76D6\u516C\u5171\u57FA\u7840\u548C\u73AF\u5883\u914D\u7F6E\u3002\n");
1165
1216
  }
1166
1217
  }
1167
- const { generateSkillFiles } = await import('./cli-skill-templates-EZFW7D43.js');
1218
+ const { generateSkillFiles } = await import('./cli-skill-templates-TSYEQTCY.js');
1168
1219
  await generateSkillFiles(
1169
1220
  projectPath,
1170
1221
  resolvedOptions.features,
@@ -1277,7 +1328,7 @@ async function resolveOptions(options) {
1277
1328
  let selectedFeatures = [];
1278
1329
  const appTypeDefaults = APP_TYPES[selectedAppType].defaultFeatures;
1279
1330
  if (selectedAppType === "fullstack") {
1280
- selectedFeatures = resolveFeatureDependencies(appTypeDefaults);
1331
+ selectedFeatures = resolveFeatureDependencies([...appTypeDefaults, ...options.features?.includes("deploy") ? ["deploy"] : []]);
1281
1332
  } else if (selectedTemplate === "custom" && !options.features && useDefaults) {
1282
1333
  selectedFeatures = resolveFeatureDependencies(appTypeDefaults);
1283
1334
  } else if (selectedTemplate === "custom" && !options.features) {
@@ -1685,39 +1736,44 @@ function generateAiDevelopmentReadme() {
1685
1736
 
1686
1737
  \u4ECE\u672C\u9879\u76EE\u6839\u76EE\u5F55\u6253\u5F00\u6240\u7528\u7F16\u7801\u5DE5\u5177\uFF0C\u5148\u8BFB\u53D6 [AGENTS.md](./AGENTS.md)\u3002Claude Code \u901A\u8FC7 [CLAUDE.md](./CLAUDE.md) \u52A0\u8F7D\u9879\u76EE\u89C4\u5219\uFF1BGitHub Copilot \u4F7F\u7528 [.github/copilot-instructions.md](./.github/copilot-instructions.md)\uFF0COpenCode \u4F7F\u7528 AGENTS.md \u548C [opencode.json](./opencode.json)\u3002
1687
1738
 
1739
+ \u4E1A\u52A1\u4EE5\u7528\u6237\u5F53\u524D\u660E\u786E\u8981\u6C42\u53CA\u5176\u6307\u5B9A\u7684\u9700\u6C42/\u8BBE\u8BA1\u6587\u6863\u4E3A\u51C6\u3002\u6A21\u677F AGENTS\u3001README\u3001skills\u3001\u751F\u6210\u4EE3\u7801\u4E0E\u6D4B\u8BD5\u4E2D\u7684\u89D2\u8272/\u6743\u9650\u3001\u6D41\u7A0B\u3001\u9650\u989D\u548C\u67B6\u6784\u662F\u53EF\u8C03\u6574\u7684\u521D\u59CB\u65B9\u6848\uFF1B\u5BA1\u67E5\u5C06\u5DEE\u5F02\u5217\u4E3A\u6A21\u677F\u9002\u914D\u9879\uFF0C\u5B9E\u65BD\u6309\u6388\u6743\u540C\u6B65\u51B2\u7A81\u4EE3\u7801\u3001\u6D4B\u8BD5\u4E0E\u8FC7\u671F\u89C4\u5219\uFF0C\u4E0D\u8981\u6C42\u628A\u9700\u6C42\u6539\u56DE\u6A21\u677F\u3002\u6846\u67B6 API \u4EE5\u5F53\u524D\u5B89\u88C5\u7248\u672C\u516C\u5F00\u5951\u7EA6\u4E3A\u51C6\uFF0C\u670D\u52A1\u7AEF\u6388\u6743\u3001\u8D44\u6E90\u9694\u79BB\u3001\u8F93\u5165\u6821\u9A8C\u548C\u51ED\u636E\u4FDD\u62A4\u4ECD\u9700\u843D\u5B9E\u3002
1740
+
1688
1741
  Codex\u3001Cursor\u3001Copilot\u3001OpenCode \u4ECE .agents/skills/ \u53D1\u73B0\u5171\u4EAB\u6280\u80FD\uFF1BClaude Code \u901A\u8FC7 CLAUDE.md \u5F15\u7528 AGENTS.md\uFF0C\u5E76\u6309\u9879\u76EE\u6307\u5F15\u8BFB\u53D6 .agents/skills/ \u7684\u540C\u4E00\u4EFD\u6B63\u6587\u3002\u9700\u6C42\u6216\u8BBE\u8BA1\u6587\u6863\u5728\u751F\u6210\u524D\u4F7F\u7528 [\u6587\u6863\u5BA1\u67E5 Skill](./.agents/skills/hai-spec-review/SKILL.md) \u4E25\u683C\u68C0\u67E5\u5B8C\u5907\u6027\u3001\u6B67\u4E49\u3001\u6280\u672F\u6B63\u786E\u6027\u548C\u547D\u540D\u4E00\u81F4\u6027\uFF0C\u95EE\u9898\u89E3\u51B3\u540E\u518D\u751F\u6210\u3002\u666E\u901A\u65B0\u589E\u3001\u4FEE\u6539\u3001\u4FEE\u590D\u4EFB\u52A1\u4F7F\u7528 [\u5E94\u7528\u5F00\u53D1 Skill](./.agents/skills/hai-app-create/SKILL.md) \u548C [\u5F00\u53D1\u89C4\u8303\u4E0E\u6846\u67B6\u80FD\u529B\u8DEF\u7531](./.agents/skills/hai-app-create/development.md)\uFF1B\u5177\u4F53\u6A21\u5757\u518D\u6309\u9700\u8BFB\u53D6\uFF0C\u4E0D\u80FD\u628A Skill \u5DF2\u4EA4\u4ED8\u5F53\u6210\u4F9D\u8D56\u5DF2\u5B89\u88C5\u6216\u914D\u7F6E\u5DF2\u542F\u7528\u3002
1689
1742
 
1690
- \u6839\u636E\u6587\u6863\u89C4\u5212\u3001\u5B9E\u65BD\u6216\u8FD4\u4FEE\u65F6\uFF0C\u4F7F\u7528 [\u6587\u6863\u5B9E\u65BD Skill](./.agents/skills/hai-spec-implement/SKILL.md)\uFF1A\u6309\u53EF\u9A8C\u6536\u529F\u80FD\u62C6\u5206\uFF0C\u7EF4\u62A4 docs/task-list.md \u4E0E docs/tasks/ \u8BB0\u5F55\uFF0C\u6BCF\u9879\u5728\u72EC\u7ACB\u5206\u652F\u8D70 TDD\u3001\u9A8C\u8BC1\u4E0E\u672C\u5730\u5408\u5E76\uFF1B\u8FD4\u4FEE\u6CBF\u7528\u539F\u4EFB\u52A1\u4E0E\u5206\u652F\u5E76\u8FFD\u52A0\u8BC1\u636E\u3002\u4EC5\u89C4\u5212\u4E0D\u5F00\u59CB\u5B9E\u73B0\uFF0C\u6307\u5B9A\u4EFB\u52A1\u5148\u6838\u9A8C\u4F9D\u8D56\u3002
1743
+ \u6839\u636E\u6587\u6863\u89C4\u5212\u3001\u5B9E\u65BD\u3001\u8FD4\u4FEE\u6216\u6309\u5DF2\u63D0\u4EA4\u6587\u6863\u5DEE\u5F02\u540C\u6B65\u5B9E\u73B0\u65F6\uFF0C\u4F7F\u7528 [\u6587\u6863\u5B9E\u65BD Skill](./.agents/skills/hai-spec-implement/SKILL.md)\uFF1A\u7EF4\u62A4 docs/task-list.md \u4E0E docs/tasks/\uFF0C\u8BB0\u5F55\u5DF2\u9A8C\u6536\u6587\u6863\u548C\u5B9E\u73B0\u7684 Git SHA\uFF0C\u6BD4\u8F83\u56FA\u5B9A\u76EE\u6807\u7248\u672C\u540E\u66F4\u65B0\u539F\u4EFB\u52A1\u6216\u65B0\u5EFA\u4EFB\u52A1\uFF0C\u6BCF\u9879\u72EC\u7ACB\u5206\u652F TDD\u3001\u9A8C\u8BC1\u5E76\u672C\u5730\u5408\u5E76\uFF1B\u5168\u90E8\u5DEE\u5F02\u4E0E\u6574\u4F53\u9A8C\u6536\u901A\u8FC7\u540E\u624D\u63A8\u8FDB\u57FA\u7EBF\u3002\u8FD4\u4FEE\u6CBF\u7528\u539F\u4EFB\u52A1\u4E0E\u5206\u652F\u3002\u4EC5\u89C4\u5212\u4E0D\u5F00\u59CB\u5B9E\u73B0\uFF0C\u6307\u5B9A\u4EFB\u52A1\u5148\u6838\u9A8C\u4F9D\u8D56\u3002
1691
1744
 
1692
1745
  \u4F18\u5148\u590D\u7528\u9002\u7528\u7684 hai-framework \u516C\u5F00 API \u4E0E\u5DF2\u6709\u4E1A\u52A1\u5B9E\u73B0\u3002\u5B8C\u6210\u65F6\u8BF4\u660E\u590D\u7528\u7684\u80FD\u529B\u3001\u76F8\u5173\u80FD\u529B\u672A\u91C7\u7528\u7684\u539F\u56E0\uFF0C\u4EE5\u53CA\u5B89\u5168\u8FB9\u754C\u548C\u7C7B\u578B\u3001\u914D\u7F6E\u3001\u6587\u6848\u3001\u8C03\u7528\u65B9\u7684\u540C\u6B65\u60C5\u51B5\u3002\u8D28\u91CF\u9A8C\u8BC1\u6309\u5F53\u524D package.json \u6267\u884C typecheck \u2192 lint \u2192 build \u2192 test\uFF1BUI\u3001\u8DEF\u7531\u4E0E\u7AEF\u5230\u7AEF\u6D41\u7A0B\u53E6\u8FD0\u884C test:e2e\uFF0C\u672A\u8FD0\u884C\u9879\u8BF4\u660E\u539F\u56E0\u3002\u5B89\u5168\u4E0E\u8D28\u91CF\u89C4\u5219\u65E2\u8981\u6309\u89C4\u8303\u5BA1\u67E5\uFF0C\u4E5F\u8981\u8FD0\u884C\u95E8\u7981\u9A8C\u8BC1\u3002
1693
1746
 
1694
- \u5DF2\u6709 hai \u9879\u76EE\u53EF\u8FD0\u884C hai init \u8865\u9F50\u7F3A\u5931\u7684 AI \u652F\u6301\u6587\u4EF6\u4E0E\u53C2\u8003\u8D44\u6E90\uFF1B\u5DF2\u6709 AGENTS.md\u3001Skill \u548C\u81EA\u5B9A\u4E49\u89C4\u5219\u4F1A\u4FDD\u7559\uFF0C\u5347\u7EA7\u65F6\u9700\u5C06\u4E0A\u8FF0\u666E\u901A\u5F00\u53D1\u5165\u53E3\u4E0E\u89C4\u8303\u663E\u5F0F\u5408\u5E76\u5230\u65E7\u6307\u5F15\u3002
1747
+ \u5DF2\u6709 hai \u9879\u76EE\u53EF\u8FD0\u884C hai init \u8865\u9F50\u7F3A\u5931\u7684 AI \u652F\u6301\u6587\u4EF6\u4E0E\u53C2\u8003\u8D44\u6E90\uFF1B\u5DF2\u6709 AGENTS.md\u3001Skill \u548C\u81EA\u5B9A\u4E49\u89C4\u5219\u4F1A\u4FDD\u7559\uFF0C\u5347\u7EA7\u65F6\u9700\u5C06\u4E0A\u8FF0\u9700\u6C42\u4F18\u5148\u89C4\u5219\u3001\u666E\u901A\u5F00\u53D1\u5165\u53E3\u4E0E\u89C4\u8303\u663E\u5F0F\u5408\u5E76\u5230\u65E7\u6307\u5F15\uFF0C\u5E76\u4FEE\u6B63\u5DF2\u88AB\u9700\u6C42\u66FF\u4EE3\u7684\u6A21\u677F\u7EA6\u675F\u3002
1695
1748
 
1696
1749
  `;
1697
1750
  }
1751
+ function generateEnvironmentReadme() {
1752
+ return `## \u5206\u73AF\u5883\u914D\u7F6E
1753
+
1754
+ \u8FD0\u884C\u53C2\u6570\u5728 \`.env.example\` \u4E2D\u9010\u9879\u8BF4\u660E\u7528\u9014\u3001\u679A\u4E3E\u548C\u8303\u56F4\u3002\`.env\` \u4E3A\u516C\u5171\u57FA\u7840\uFF0C\`.env.dev\` / \`.env.test\` / \`.env.prod\` \u4E3A\u73AF\u5883\u8986\u76D6\uFF0C\u5747\u63D0\u4EA4 Git\uFF1B\`.env.local\` \u4E3A\u672C\u673A\u79C1\u6709\u8986\u76D6\uFF0C\u4E0D\u63D0\u4EA4 Git\u3002\u4F18\u5148\u7EA7\u4E3A\u8FDB\u7A0B\u73AF\u5883 > \`.env.local\` > \u73AF\u5883\u6587\u4EF6 > \`.env\`\u3002YAML \u663E\u5F0F\u5F15\u7528 \`HAI_<\u914D\u7F6E\u540D>_<\u8DEF\u5F84>\` \u5E76\u4FDD\u7559\u672C\u5730\u56DE\u9000\uFF0CcamelCase \u4E0D\u62C6\u8BCD\u3002\u5B8C\u6574\u547D\u4EE4\u3001\u52A0\u8F7D\u4F18\u5148\u7EA7\u548C Provider \u5207\u6362\u89C1 [\u73AF\u5883\u914D\u7F6E](.agents/skills/hai-app-create/development.md#\u73AF\u5883\u914D\u7F6E)\u3002
1755
+
1756
+ | \u7528\u9014 | \u547D\u4EE4 |
1757
+ | --- | --- |
1758
+ | \u5F00\u53D1 / \u6D4B\u8BD5\u6A21\u5F0F / \u751F\u4EA7\u6A21\u5F0F\u9884\u89C8 | \`pnpm dev\` / \`dev:test\` / \`dev:prod\` |
1759
+ | \u7C7B\u578B\u68C0\u67E5\uFF08check \u4E3A\u522B\u540D\uFF09 | \`pnpm typecheck\` |
1760
+ | \u68C0\u67E5 / \u81EA\u52A8\u4FEE\u590D\u683C\u5F0F | \`pnpm lint\` / \`lint:fix\` |
1761
+ | \u6E90\u7801\u6784\u5EFA / \u955C\u50CF\u6784\u5EFA | \`pnpm build\` / \`pnpm build --target docker\` |
1762
+ | \u5355\u5143\u6D4B\u8BD5 / \u76D1\u542C / \u6D4F\u89C8\u5668\u9A8C\u6536 | \`pnpm test\` / \`test:watch\` / \`test:e2e\` |
1763
+ | Node \u670D\u52A1\u6309\u73AF\u5883\u542F\u52A8\uFF08\u6784\u5EFA\u540E\uFF09 | \`pnpm start\` / \`start:test\` / \`start:prod\` |
1764
+
1765
+ Vite \u5BA2\u6237\u7AEF\u4F7F\u7528 \`preview\` \u9884\u89C8\u9759\u6001\u4EA7\u7269\uFF0CNode \u670D\u52A1\u4F7F\u7528 \`start\`\uFF1B\u7EAF contract/shared \u5E93\u53EA\u63D0\u4F9B\u9002\u7528\u7684\u6784\u5EFA\u548C\u8D28\u91CF\u547D\u4EE4\u3002API workspace \u7684\u73AF\u5883\u6587\u4EF6\u4F4D\u4E8E \`apps/<name>-service\`\uFF0Cfullstack \u4F4D\u4E8E\u5DE5\u7A0B\u6839\u3002`;
1766
+ }
1698
1767
  function generateReadme(name, appTypeLabel, pm, appType) {
1699
1768
  const install = getInstallCommand(pm);
1700
- const isStaticImage = appType === "mobile-app";
1701
- const containerPort = isStaticImage ? 8080 : 3e3;
1702
- const runtimeOptions = isStaticImage ? "" : ` --env-file .env --mount type=bind,source=/absolute/path/to/config,target=/app/config,readonly --mount type=volume,source=${name}-data,target=/app/data`;
1703
- const embeddedDataBuild = isStaticImage ? "" : `
1704
- # \u5E26 data/ \u521D\u59CB\u6570\u636E\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1705
- ${getRunCommand(pm, "docker:build:data")}
1706
- ${getRunCommand(pm, "podman:build:data")}`;
1707
- const runtimeNotes = isStaticImage ? "- \u8BE5\u6A21\u677F\u751F\u6210\u9759\u6001 Web \u955C\u50CF\uFF0C\u4E0D\u5305\u542B\u53EF\u5199\u6570\u636E\u5E93\u76EE\u5F55\u3002" : `\u4EE5\u4E0A\u4E09\u4E2A\u8FD0\u884C\u65F6\u53C2\u6570\u5747\u53EF\u6309\u9700\u5220\u9664\uFF1A
1708
-
1709
- | \u53C2\u6570 | \u7528\u9014 | \u53EF\u9009\u6027 |
1710
- | --- | --- | --- |
1711
- | \`--env-file .env\` | \u6279\u91CF\u4F20\u5165\u73AF\u5883\u53D8\u91CF\uFF1B\u4E5F\u53EF\u6539\u7528\u591A\u4E2A \`-e NAME=value\`\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u548C YAML \u9ED8\u8BA4\u503C\u3002 |
1712
- | \`--mount type=bind,...target=/app/config,readonly\` | \u7528\u5BBF\u4E3B\u673A\u914D\u7F6E\u76EE\u5F55\u8986\u76D6\u955C\u50CF\u5185 \`/app/config\`\uFF1B\u5FC5\u987B\u628A \`source\` \u6539\u6210\u771F\u5B9E\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\`readonly\` \u9632\u6B62\u5BB9\u5668\u4FEE\u6539\u914D\u7F6E\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u81EA\u5E26\u914D\u7F6E\u3002 |
1713
- | \`--mount type=volume,...target=/app/data\` | \u521B\u5EFA\u6216\u590D\u7528 named volume\uFF0C\u6301\u4E45\u5316 SQLite \u548C\u672C\u5730\u5B58\u50A8\u3002 | \u53EF\u9009\u4F46\u751F\u4EA7\u73AF\u5883\u5EFA\u8BAE\u4FDD\u7559\uFF1B\u5220\u9664\u540E\u6570\u636E\u53EA\u5199\u5165\u5BB9\u5668\u5C42\uFF0C\u4F7F\u7528 \`--rm\` \u5220\u9664\u5BB9\u5668\u65F6\u4F1A\u4E22\u5931\u3002 |
1714
-
1715
- - \u5E26\u6570\u636E\u6784\u5EFA\u4F1A\u628A\u5F53\u524D \`data/\`\uFF08\u4F8B\u5982\u9884\u7F6E SQLite \u6587\u4EF6\uFF09\u56FA\u5316\u4E3A\u955C\u50CF\u521D\u59CB\u5FEB\u7167\uFF1B\u8FD0\u884C\u540E\u4EA7\u751F\u7684\u65B0\u6570\u636E\u4ECD\u5E94\u5199\u5165 \`/app/data\` volume\u3002`;
1716
- const testPath = isStaticImage ? "/health" : "/";
1717
1769
  return `# ${name}
1718
1770
 
1719
1771
  \u57FA\u4E8E hai Agent Framework \u6784\u5EFA\u7684${appTypeLabel}\u5E94\u7528\u3002
1720
1772
 
1773
+ ## \u9875\u9762\u5E03\u5C40\u4E0E\u8DEF\u7531
1774
+
1775
+ \u9875\u9762\u7EDF\u4E00\u7EE7\u627F\u6839\u5E03\u5C40\u7684 header/footer\u3002${appType === "mobile-app" ? "\u9875\u9762\u653E src/lib/pages/**/*.svelte\uFF0Chash \u8DEF\u7531\u81EA\u52A8\u53D1\u73B0\uFF0C\u5237\u65B0\u4E0E\u524D\u8FDB\u540E\u9000\u6062\u590D\u5F53\u524D\u9875\u3002" : "\u4F7F\u7528 SvelteKit \u81EA\u52A8\u6587\u4EF6\u8DEF\u7531\uFF0C\u65B0\u589E +page.svelte \u65E0\u9700\u7EF4\u62A4\u8DEF\u7531\u8868\uFF1B\u5237\u65B0\u5B9A\u4F4D\u4E0E\u670D\u52A1\u7AEF\u9274\u6743\u4FDD\u6301\u4E00\u81F4\u3002"}
1776
+
1721
1777
  ## \u5F00\u53D1
1722
1778
 
1723
1779
  \`\`\`bash
@@ -1725,6 +1781,8 @@ ${install}
1725
1781
  ${getRunCommand(pm, "dev")}
1726
1782
  \`\`\`
1727
1783
 
1784
+ ${generateEnvironmentReadme()}
1785
+
1728
1786
  ## \u6784\u5EFA
1729
1787
 
1730
1788
  \`\`\`bash
@@ -1733,31 +1791,20 @@ ${getRunCommand(pm, "i18n:compile")}
1733
1791
  ${getRunCommand(pm, "preview")}
1734
1792
  \`\`\`
1735
1793
 
1736
- ## \u5BB9\u5668\u8FD0\u884C
1737
-
1738
- ### \u6784\u5EFA\u955C\u50CF
1794
+ ## \u6784\u5EFA\u4E0E\u90E8\u7F72
1739
1795
 
1740
1796
  \`\`\`bash
1741
- # \u666E\u901A\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1742
- ${getRunCommand(pm, "docker:build")}
1743
- ${getRunCommand(pm, "podman:build")}${embeddedDataBuild}
1744
- \`\`\`
1745
-
1746
- ### \u8FD0\u884C\u955C\u50CF
1747
-
1748
- \`\`\`bash
1749
- # Docker\uFF1B\u4F7F\u7528 Podman \u65F6\u5C06 docker \u66FF\u6362\u4E3A podman
1750
- docker run --rm -d --name ${name} -p ${containerPort}:${containerPort}${runtimeOptions} ${name}:latest
1751
- \`\`\`
1752
-
1753
- ### \u6D4B\u8BD5\u955C\u50CF
1754
-
1755
- \`\`\`bash
1756
- curl --fail http://127.0.0.1:${containerPort}${testPath}
1757
- docker stop ${name}
1797
+ # \u9ED8\u8BA4\u53EA\u7F16\u8BD1\u6E90\u7801\uFF1B\u4E0D\u9009\u62E9\u90E8\u7F72\u73AF\u5883
1798
+ ${pm} build
1799
+ # \u6784\u5EFA\u5E76\u5BFC\u51FA API/Web \u955C\u50CF\uFF0Cauto \u4F18\u5148 Podman\u3001\u5176\u6B21 Docker
1800
+ ${pm} build --target docker
1801
+ # \u9009\u62E9\u73AF\u5883\u3001\u4E2D\u95F4\u4EF6\u5E76\u4E0A\u4F20\u4E00\u4E2A Compose tar\uFF1B\u4E0D\u4F1A\u91CD\u65B0\u6784\u5EFA
1802
+ ${pm} exec hai deploy . --env test
1803
+ # \u65E0\u6CD5\u8FDE\u63A5\u670D\u52A1\u5668\u65F6\u53EA\u751F\u6210\u5305\uFF0C\u6309\u8F93\u51FA\u8BF4\u660E\u624B\u5DE5\u642C\u8FD0\u548C\u542F\u52A8
1804
+ ${pm} exec hai deploy . --env prod --bundle-only
1758
1805
  \`\`\`
1759
1806
 
1760
- ${runtimeNotes}
1807
+ \u955C\u50CF\u6784\u5EFA\u914D\u7F6E\u89C1 \`hai.build.json\`\u3002\u6784\u5EFA\u4EA7\u7269\u4FDD\u5B58\u5728 \`.hai/build/docker/<buildId>\`\uFF0C\u53EF\u7528 \`--artifact-dir\` \u9009\u62E9\u540C\u4E00\u4EFD\u4EA7\u7269\u90E8\u7F72\u5230 test/prod\u3002\u90E8\u7F72\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u4E0D\u80FD\u63D0\u4EA4 Git\u3002\u73AF\u5883\u5DF2\u914D\u7F6E\u5B8C\u5907\u53EF\u52A0 \`--skip-middleware\`\u3002\u670D\u52A1\u8FD0\u884C\u6570\u636E\u7531 volume \u6301\u4E45\u5316\uFF0C\u955C\u50CF\u4E0D\u6253\u5305\u672C\u673A data\u3002API/Web \u8054\u5408\u90E8\u7F72\u516C\u5F00 Web \u5165\u53E3\u5E76\u4EE3\u7406\u540C\u6E90 API\uFF1B\u6CA1\u6709 Web \u4EA7\u7269\u65F6\u53EA\u90E8\u7F72\u670D\u52A1\u3002
1761
1808
 
1762
1809
  ${generateAiDevelopmentReadme()}## \u6587\u6863
1763
1810
 
@@ -1782,6 +1829,8 @@ ${pm} install
1782
1829
  ${pm} dev
1783
1830
  \`\`\`
1784
1831
 
1832
+ ${generateEnvironmentReadme()}
1833
+
1785
1834
  ## \u8D28\u91CF\u95E8\u7981
1786
1835
 
1787
1836
  \`\`\`bash
@@ -1792,50 +1841,20 @@ ${pm} test
1792
1841
  ${pm} test:e2e
1793
1842
  \`\`\`
1794
1843
 
1795
- ## \u5BB9\u5668\u8FD0\u884C
1796
-
1797
- ### \u6784\u5EFA\u955C\u50CF
1844
+ ## \u6784\u5EFA\u4E0E\u90E8\u7F72
1798
1845
 
1799
1846
  \`\`\`bash
1800
- # \u666E\u901A\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1801
- ${pm} docker:build
1802
- ${pm} podman:build
1803
-
1804
- # \u5C06 apps/${name}-service/data \u4F5C\u4E3A\u955C\u50CF\u521D\u59CB\u6570\u636E\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1805
- ${pm} docker:build:data
1806
- ${pm} podman:build:data
1807
- \`\`\`
1808
-
1809
- ### \u8FD0\u884C\u955C\u50CF
1810
-
1811
- \`\`\`bash
1812
- # \u4F7F\u7528 Podman \u65F6\u5C06 docker \u66FF\u6362\u4E3A podman
1813
- docker run --rm -d --name ${name} -p 3000:3000 --env-file .env \\
1814
- --mount type=bind,source=/absolute/path/to/config,target=/app/config,readonly \\
1815
- --mount type=volume,source=${name}-data,target=/app/data \\
1816
- ${name}:latest
1817
- \`\`\`
1818
-
1819
- ### \u6D4B\u8BD5\u955C\u50CF
1820
-
1821
- \`\`\`bash
1822
- curl --fail http://127.0.0.1:3000/health
1823
- curl --fail http://127.0.0.1:3000/ready
1824
- curl --fail --request POST http://127.0.0.1:3000/api/v1/app/info \\
1825
- --header "Content-Type: application/json" --data '{}'
1826
- docker stop ${name}
1847
+ # \u9ED8\u8BA4\u53EA\u7F16\u8BD1\u6E90\u7801\uFF1B\u4E0D\u9009\u62E9\u90E8\u7F72\u73AF\u5883
1848
+ ${pm} build
1849
+ # \u6784\u5EFA\u5E76\u5BFC\u51FA API/Web \u955C\u50CF\uFF0Cauto \u4F18\u5148 Podman\u3001\u5176\u6B21 Docker
1850
+ ${pm} build --target docker
1851
+ # \u9009\u62E9\u73AF\u5883\u3001\u4E2D\u95F4\u4EF6\u5E76\u4E0A\u4F20\u4E00\u4E2A Compose tar\uFF1B\u4E0D\u4F1A\u91CD\u65B0\u6784\u5EFA
1852
+ ${pm} exec hai deploy apps/${name}-service --env test
1853
+ # \u65E0\u6CD5\u8FDE\u63A5\u670D\u52A1\u5668\u65F6\u53EA\u751F\u6210\u5305\uFF0C\u6309\u8F93\u51FA\u8BF4\u660E\u624B\u5DE5\u642C\u8FD0\u548C\u542F\u52A8
1854
+ ${pm} exec hai deploy apps/${name}-service --env prod --bundle-only
1827
1855
  \`\`\`
1828
1856
 
1829
- - \u914D\u7F6E\u9ED8\u8BA4\u6765\u81EA \`apps/${name}-service/config\`\uFF1B\u8FD0\u884C\u65F6\u53EF\u53EA\u8BFB\u6302\u8F7D\u5230 \`/app/config\` \u8986\u76D6\uFF0C\u5E76\u7528 YAML \u7684 \`\${ENV_NAME:default}\` \u5360\u4F4D\u7B26\u63A5\u6536\u73AF\u5883\u53D8\u91CF\u3002
1830
- - \u5E26\u6570\u636E\u6784\u5EFA\u4F1A\u628A \`apps/${name}-service/data\` \u56FA\u5316\u4E3A\u955C\u50CF\u521D\u59CB\u6570\u636E\uFF1B\u751F\u4EA7\u5199\u5165\u4ECD\u5EFA\u8BAE\u4F7F\u7528 \`/app/data\` volume\u3002
1831
-
1832
- \u4EE5\u4E0A\u4E09\u4E2A\u8FD0\u884C\u65F6\u53C2\u6570\u5747\u53EF\u6309\u9700\u5220\u9664\uFF1A
1833
-
1834
- | \u53C2\u6570 | \u7528\u9014 | \u53EF\u9009\u6027 |
1835
- | --- | --- | --- |
1836
- | \`--env-file .env\` | \u6279\u91CF\u4F20\u5165\u73AF\u5883\u53D8\u91CF\uFF1B\u4E5F\u53EF\u6539\u7528\u591A\u4E2A \`-e NAME=value\`\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u548C YAML \u9ED8\u8BA4\u503C\u3002 |
1837
- | \`--mount type=bind,...target=/app/config,readonly\` | \u7528\u5BBF\u4E3B\u673A\u914D\u7F6E\u76EE\u5F55\u8986\u76D6\u955C\u50CF\u5185 \`/app/config\`\uFF1B\u5FC5\u987B\u628A \`source\` \u6539\u6210\u771F\u5B9E\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\`readonly\` \u9632\u6B62\u5BB9\u5668\u4FEE\u6539\u914D\u7F6E\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u81EA\u5E26\u914D\u7F6E\u3002 |
1838
- | \`--mount type=volume,...target=/app/data\` | \u521B\u5EFA\u6216\u590D\u7528 named volume\uFF0C\u6301\u4E45\u5316 SQLite \u548C\u672C\u5730\u5B58\u50A8\u3002 | \u53EF\u9009\u4F46\u751F\u4EA7\u73AF\u5883\u5EFA\u8BAE\u4FDD\u7559\uFF1B\u5220\u9664\u540E\u6570\u636E\u53EA\u5199\u5165\u5BB9\u5668\u5C42\uFF0C\u4F7F\u7528 \`--rm\` \u5220\u9664\u5BB9\u5668\u65F6\u4F1A\u4E22\u5931\u3002 |
1857
+ \u955C\u50CF\u6784\u5EFA\u914D\u7F6E\u89C1 \`hai.build.json\`\u3002\u6784\u5EFA\u4EA7\u7269\u4FDD\u5B58\u5728 \`.hai/build/docker/<buildId>\`\uFF0C\u53EF\u7528 \`--artifact-dir\` \u9009\u62E9\u540C\u4E00\u4EFD\u4EA7\u7269\u90E8\u7F72\u5230 test/prod\u3002\u90E8\u7F72\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u4E0D\u80FD\u63D0\u4EA4 Git\u3002\u73AF\u5883\u5DF2\u914D\u7F6E\u5B8C\u5907\u53EF\u52A0 \`--skip-middleware\`\u3002\u670D\u52A1\u8FD0\u884C\u6570\u636E\u7531 volume \u6301\u4E45\u5316\uFF0C\u955C\u50CF\u4E0D\u6253\u5305\u672C\u673A data\u3002API/Web \u8054\u5408\u90E8\u7F72\u516C\u5F00 Web \u5165\u53E3\u5E76\u4EE3\u7406\u540C\u6E90 API\uFF1B\u6CA1\u6709 Web \u4EA7\u7269\u65F6\u53EA\u90E8\u7F72\u670D\u52A1\u3002
1839
1858
 
1840
1859
  ## \u6269\u5C55\u5EFA\u8BAE
1841
1860
 
@@ -1853,20 +1872,6 @@ ${generateAiDevelopmentReadme()}## \u6587\u6863
1853
1872
  }
1854
1873
  function generateFullstackReadme(name, pm, frontends) {
1855
1874
  const enabledFrontends = frontends.map((target) => FRONTEND_TARGETS[target].name).join(" / ");
1856
- const webBuildCommands = frontends.includes("web") ? `
1857
- # Web \u9759\u6001\u955C\u50CF\uFF08\u4E8C\u9009\u4E00\uFF09
1858
- ${pm} docker:build:web
1859
- ${pm} podman:build:web` : "";
1860
- const webRunCommand = frontends.includes("web") ? `
1861
-
1862
- # Web\uFF1BPUBLIC_API_BASE \u5728\u5BB9\u5668\u542F\u52A8\u65F6\u6CE8\u5165\uFF0C\u4E0D\u9700\u8981\u91CD\u65B0\u6784\u5EFA\u955C\u50CF
1863
- docker run --rm -d --name ${name}-web -p 8080:8080 \\
1864
- -e PUBLIC_API_BASE=https://api.example.com \\
1865
- ${name}-web:latest` : "";
1866
- const webTestCommands = frontends.includes("web") ? `curl --fail http://127.0.0.1:8080/health
1867
- curl --fail http://127.0.0.1:8080/runtime-config.js
1868
- ` : "";
1869
- const containerNames = frontends.includes("web") ? `${name} ${name}-web` : name;
1870
1875
  return `# ${name}
1871
1876
 
1872
1877
  \u57FA\u4E8E hai Agent Framework \u6784\u5EFA\u7684\u524D\u540E\u7AEF\u5206\u79BB\u591A\u5305\u5DE5\u7A0B\u3002
@@ -1883,11 +1888,17 @@ curl --fail http://127.0.0.1:8080/runtime-config.js
1883
1888
 
1884
1889
  \u5DF2\u542F\u7528\u524D\u7AEF\uFF1A${enabledFrontends || "Web / App / Desktop"}
1885
1890
 
1891
+ ## \u9875\u9762\u5E03\u5C40\u4E0E\u8DEF\u7531
1892
+
1893
+ Web/App/Desktop \u5171\u4EAB AppShell \u7684 header/footer\u3002\u65B0\u589E\u9875\u9762\u653E shared/src/lib/pages/**/*.svelte\uFF0C\u7531 HashRouter \u81EA\u52A8\u53D1\u73B0\u5E76\u6309\u9700\u52A0\u8F7D\uFF1B\u83DC\u5355\u53EA\u7EF4\u62A4\u5165\u53E3\u3002\u5730\u5740\u4F7F\u7528 #/\u9875\u9762\u8DEF\u5F84\uFF0C\u652F\u6301\u5237\u65B0\u3001\u67E5\u8BE2\u4E32\u4E0E\u6D4F\u89C8\u5668\u524D\u8FDB\u540E\u9000\uFF1BAPI \u6743\u9650\u4ECD\u7531\u670D\u52A1\u7AEF\u6821\u9A8C\u3002
1894
+
1886
1895
  ## \u9ED8\u8BA4\u7BA1\u7406\u529F\u80FD
1887
1896
 
1888
- \u9ED8\u8BA4\u542F\u7528 IAM\u3001reldb\u3001cache\u3001audit\u3001storage \u548C crypto\u3002\u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u7528\u6237\uFF08\u65B0\u589E\u3001\u7F16\u8F91\u3001\u7981\u7528\u3001\u5220\u9664\u3001\u89D2\u8272\u5206\u914D\u3001\u5934\u50CF\u4E0A\u4F20\uFF09\u3001\u67E5\u770B\u4E24\u79CD\u56FA\u5B9A\u89D2\u8272\u53CA\u4EBA\u6570\u3001\u7EF4\u62A4\u89D2\u8272\u8BF4\u660E\u3001\u5206\u9875\u7B5B\u9009\u5BA1\u8BA1\u65E5\u5FD7\uFF1B\u666E\u901A\u7528\u6237\u53EA\u80FD\u7EF4\u62A4\u4E2A\u4EBA\u8D44\u6599\u3001\u5934\u50CF\u3001\u8BED\u8A00\u3001\u4E3B\u9898\u548C\u5BC6\u7801\u3002\u5BC6\u7801\u4FEE\u6539\u540E\u5168\u90E8\u4F1A\u8BDD\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002
1897
+ \u4EE5\u4E0B\u63CF\u8FF0\u53EF\u8FD0\u884C\u793A\u4F8B\u7684\u521D\u59CB\u4E1A\u52A1\uFF0C\u53EF\u6309\u5B9E\u9645\u9700\u6C42\u8C03\u6574\u6216\u79FB\u9664\uFF0C\u5E76\u540C\u6B65\u4EE3\u7801\u3001\u6570\u636E\u3001\u9274\u6743\u3001\u754C\u9762\u3001\u6D4B\u8BD5\u548C\u6587\u6863\u3002
1898
+
1899
+ \u9ED8\u8BA4\u542F\u7528 IAM\u3001reldb\u3001cache\u3001audit\u3001storage \u548C crypto\u3002\u521D\u59CB\u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u7528\u6237\uFF08\u65B0\u589E\u3001\u7F16\u8F91\u3001\u7981\u7528\u3001\u5220\u9664\u3001\u89D2\u8272\u5206\u914D\u3001\u5934\u50CF\u4E0A\u4F20\uFF09\u3001\u67E5\u770B admin/user \u89D2\u8272\u53CA\u4EBA\u6570\u3001\u7EF4\u62A4\u89D2\u8272\u8BF4\u660E\u3001\u5206\u9875\u7B5B\u9009\u5BA1\u8BA1\u65E5\u5FD7\uFF1B\u521D\u59CB\u666E\u901A\u7528\u6237\u8FDB\u5165\u4E2A\u4EBA\u8D44\u6599\u3001\u5934\u50CF\u3001\u8BED\u8A00\u3001\u4E3B\u9898\u548C\u5BC6\u7801\u8BBE\u7F6E\u3002\u9ED8\u8BA4\u5BC6\u7801\u4FEE\u6539\u540E\u5168\u90E8\u4F1A\u8BDD\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u767B\u5F55\uFF1B\u89D2\u8272\u6A21\u578B\u3001\u8BBF\u95EE\u8303\u56F4\u548C\u4F1A\u8BDD\u7B56\u7565\u7531\u4E1A\u52A1\u9700\u6C42\u51B3\u5B9A\u3002
1889
1900
 
1890
- CLI \u5DF2\u5728\u6839\u76EE\u5F55\u751F\u6210 \`.env\`\uFF0C\u5176\u4E2D \`HAI_ADMIN_DEFAULT_PASSWORD\` \u662F\u6BCF\u4E2A\u9879\u76EE\u72EC\u7ACB\u751F\u6210\u7684\u968F\u673A\u5F3A\u5BC6\u7801\u3002\u76F4\u63A5\u8FD0\u884C \`dev\` / \`start\` \u5373\u53EF\uFF0C\u9996\u6B21\u767B\u5F55\u4F7F\u7528\u7528\u6237\u540D \`admin\` \u548C\u8BE5\u5BC6\u7801\uFF1B\u4E5F\u53EF\u901A\u8FC7\u540C\u540D\u73AF\u5883\u53D8\u91CF\u8986\u76D6\u3002\u5BC6\u7801\u987B\u6EE1\u8DB3 \`_iam.yml\` \u7B56\u7565\uFF0C\u542F\u52A8\u5931\u8D25\u4F1A\u663E\u793A\u5177\u4F53\u6821\u9A8C\u539F\u56E0\u53CA\u9519\u8BEF\u7801\u3002\`.env\` \u5DF2\u88AB Git \u548C\u5BB9\u5668\u6784\u5EFA\u5FFD\u7565\uFF0C\`.env.example\` \u4E0D\u542B\u771F\u5B9E\u5BC6\u7801\uFF1B\u90E8\u7F72\u65F6\u8BBE\u7F6E\u76EE\u6807\u73AF\u5883\u81EA\u5DF1\u7684\u5BC6\u7801\u3002\u5DF2\u6709\u7528\u6237\u65F6\u4E0D\u4F1A\u91CD\u590D\u521B\u5EFA\u7BA1\u7406\u5458\u6216\u91CD\u8BBE\u5BC6\u7801\u3002
1901
+ CLI \u5DF2\u5728\u6839\u76EE\u5F55\u751F\u6210 \`.env.local\`\uFF0C\u5176\u4E2D \`HAI_ADMIN_DEFAULT_PASSWORD\` \u662F\u6BCF\u4E2A\u9879\u76EE\u72EC\u7ACB\u751F\u6210\u7684\u968F\u673A\u5F3A\u5BC6\u7801\u3002\u76F4\u63A5\u8FD0\u884C \`dev\` / \`start\` \u5373\u53EF\uFF0C\u9996\u6B21\u767B\u5F55\u4F7F\u7528\u7528\u6237\u540D \`admin\` \u548C\u8BE5\u5BC6\u7801\uFF1B\u4E5F\u53EF\u901A\u8FC7\u540C\u540D\u73AF\u5883\u53D8\u91CF\u8986\u76D6\u3002\u5BC6\u7801\u987B\u6EE1\u8DB3 \`_iam.yml\` \u7B56\u7565\uFF0C\u542F\u52A8\u5931\u8D25\u4F1A\u663E\u793A\u5177\u4F53\u6821\u9A8C\u539F\u56E0\u53CA\u9519\u8BEF\u7801\u3002\`.env.local\` \u5DF2\u88AB Git \u548C\u5BB9\u5668\u6784\u5EFA\u5FFD\u7565\uFF0C\`.env.example\` \u4E0D\u542B\u771F\u5B9E\u5BC6\u7801\uFF1B\u90E8\u7F72\u65F6\u8BBE\u7F6E\u76EE\u6807\u73AF\u5883\u81EA\u5DF1\u7684\u5BC6\u7801\u3002\u5DF2\u6709\u7528\u6237\u65F6\u4E0D\u4F1A\u91CD\u590D\u521B\u5EFA\u7BA1\u7406\u5458\u6216\u91CD\u8BBE\u5BC6\u7801\u3002
1891
1902
 
1892
1903
  \u672C\u5730\u5F00\u53D1\u9ED8\u8BA4 \`PUBLIC_API_BASE=http://127.0.0.1:3000\`\uFF0CVite \u4F7F\u7528\u540C\u6E90\u4EE3\u7406\uFF1B\u90E8\u7F72\u65F6\u6539\u4E3A\u5B9E\u9645\u516C\u7F51 API Origin\uFF0CAndroid \u6A21\u62DF\u5668\u8BBF\u95EE\u5BBF\u4E3B\u673A\u65F6\u4F7F\u7528 \`http://10.0.2.2:3000\`\u3002\u4E0D\u8981\u5C06\u5BC6\u94A5\u653E\u5165\u4EFB\u4F55 \`PUBLIC_\` \u6216 \`VITE_\` \u53D8\u91CF\u3002
1893
1904
 
@@ -1899,82 +1910,42 @@ CLI \u5DF2\u5728\u6839\u76EE\u5F55\u751F\u6210 \`.env\`\uFF0C\u5176\u4E2D \`HAI_
1899
1910
 
1900
1911
  \u670D\u52A1\u7AEF \`packages/${name}-serv/config/\` \u59CB\u7EC8\u4FDD\u7559 core\u3001serv\u3001db\u3001cache\u3001iam\u3001storage\u3001audit\u3001reach\u3001ai\u3001vecdb\u3001payment\u3001scheduler\u3001deploy \u5171 13 \u4EFD YAML\u3002\u5DF2\u7528\u6A21\u5757\u4FDD\u7559\u5DE5\u4F5C\u9ED8\u8BA4\u503C\uFF0C\u672A\u542F\u7528\u6A21\u5757\u6574\u4EFD\u6CE8\u91CA\uFF0C\u53EF\u9009\u5B57\u6BB5\u548C\u66FF\u4EE3 Provider \u6309\u5757\u6CE8\u91CA\uFF1B\u6839 \`.env.example\` \u4E5F\u4FDD\u7559\u672A\u542F\u7528\u6A21\u5757\u7684\u51ED\u636E\u793A\u4F8B\u3002Reach \u9ED8\u8BA4\u4E3A\u5F00\u53D1\u63A7\u5236\u53F0\u901A\u77E5\uFF0C\u751F\u4EA7\u6309 \`_reach.yml\` \u6CE8\u91CA\u66FF\u6362\u4E3A SMTP\u3001\u77ED\u4FE1\u6216 API Provider\uFF0C\u5E76\u4FDD\u6301\u540D\u79F0\u552F\u4E00\u3002
1901
1912
 
1902
- \u81EA\u52A8\u751F\u6210\u7684 \`.env\` \u9ED8\u8BA4\u53EA\u542F\u7528\u521D\u59CB\u7BA1\u7406\u5458\u5BC6\u7801\u548C \`PUBLIC_API_BASE\`\uFF0C\u6A21\u5757\u73AF\u5883\u8986\u76D6\u4FDD\u7559\u6CE8\u91CA\uFF0CYAML \u662F\u9ED8\u8BA4\u503C\u6765\u6E90\u3002\u9700\u8981\u73AF\u5883\u8986\u76D6\u65F6\u663E\u5F0F\u53D6\u6D88\u5BF9\u5E94\u53D8\u91CF\u7684\u6CE8\u91CA\uFF1B\u7F16\u8F91 YAML \u65F6\u540C\u6B65\u6838\u5BF9\u5DF2\u6709\u8986\u76D6\u3002
1913
+ \u81EA\u52A8\u751F\u6210\u7684 \`.env\` \u5305\u542B\u5DF2\u542F\u7528\u6A21\u5757\u7684\u73AF\u5883\u53C2\u6570\uFF1BYAML \u663E\u5F0F\u5F15\u7528\u76F8\u540C\u53D8\u91CF\u5E76\u4FDD\u7559\u672C\u5730\u56DE\u9000\u3002\u7F16\u8F91\u73AF\u5883\u53C2\u6570\u4F18\u5148\u4FEE\u6539\u5BF9\u5E94 \`.env\`\uFF1B\u672A\u542F\u7528\u6A21\u5757\u4ECD\u4FDD\u7559\u6CE8\u91CA\u3002
1903
1914
 
1904
1915
  \u6269\u5C55\u65F6\u5148\u5728\u670D\u52A1\u7AEF workspace \u5B89\u88C5\u9700\u8981\u7684\u5305\uFF0C\u5728 \`src/server/runtime.ts\` \u6309\u6A21\u5757\u516C\u5F00 Schema \u6821\u9A8C\u914D\u7F6E\u3001\u521D\u59CB\u5316\u5E76\u53CD\u5E8F\u5173\u95ED\uFF0C\u518D\u63A5\u5165 contract/procedure \u548C\u754C\u9762\u3002\u6574\u4EFD\u6CE8\u91CA\u6587\u4EF6\u5148\u53BB\u6389\u4E00\u5C42\u5916\u5C42\u6CE8\u91CA\uFF0C\u53EF\u9009\u5757\u7EE7\u7EED\u6309\u9700\u5F00\u542F\uFF1B\u914D\u7F6E\u6587\u4EF6\u5B58\u5728\u4E0D\u4EE3\u8868\u4E1A\u52A1\u529F\u80FD\u5DF2\u63A5\u5165\u3002\`crypto.init()\` \u65E0\u914D\u7F6E\u53C2\u6570\uFF0Cdatapipe \u4E3A\u7EAF\u51FD\u6570\uFF0C\u4E0D\u751F\u6210\u865A\u6784 YAML\u3002
1905
1916
 
1906
1917
  \u73AF\u5883\u53D8\u91CF\u53EA\u8986\u76D6 YAML \u5DF2\u6709\u8DEF\u5F84\uFF0C\u6CE8\u91CA\u952E\u987B\u5148\u542F\u7528\uFF0C\u6216\u7528 \`HAI_DB\` / \`HAI_CACHE\` \u7B49\u6574\u4F53\u5BF9\u8C61\u8986\u76D6\u3002camelCase \u4E0D\u62C6\u8BCD\uFF0C\u4F8B\u5982 \`connectTimeout\` \u5BF9\u5E94 \`HAI_CACHE_CONNECTTIMEOUT\`\u3002\u4FEE\u6539\u914D\u7F6E\u540E\u91CD\u542F\u670D\u52A1\uFF1B\u8BED\u8A00\u5217\u8868\u5728\u5404\u524D\u7AEF/shared \u7684 \`project.inlang/settings.json\` \u4E2D\u540C\u6B65\u3002\`**/data/\` \u5FFD\u7565\u4EFB\u610F\u5C42\u7EA7\u7684\u6570\u636E\u76EE\u5F55\u53CA \`.gitkeep\`\uFF0C\u8FD0\u884C\u6570\u636E\u4E0D\u5165 Git\uFF0C\u8FD0\u884C\u65F6\u81EA\u52A8\u521B\u5EFA\u76EE\u5F55\u3002
1907
1918
 
1908
- ## \u5F00\u53D1\u3001\u7F16\u8BD1\u4E0E\u6253\u5305
1919
+ ${generateEnvironmentReadme()}
1920
+
1921
+ ## \u5F00\u53D1\u4E0E\u6784\u5EFA
1909
1922
 
1910
1923
  \`\`\`bash
1911
1924
  ${pm} install
1912
1925
  ${pm} dev
1913
-
1914
- # \u4E00\u952E\u7F16\u8BD1\uFF1A\u7C7B\u578B\u68C0\u67E5 + \u5168\u5DE5\u4F5C\u533A\u6784\u5EFA
1915
- ${pm} compile
1916
-
1917
- # \u4E00\u952E\u6253\u5305\uFF1AWeb build\u3001App Capacitor sync\u3001Desktop Tauri build\uFF08\u6309\u542F\u7528\u524D\u7AEF\u6267\u884C\uFF09
1918
- ${pm} package
1919
-
1920
- # \u90E8\u7F72\u5165\u53E3\uFF1A\u9ED8\u8BA4\u6267\u884C package\uFF0C\u53EF\u66FF\u6362\u4E3A\u56E2\u961F\u81EA\u5DF1\u7684 CI/CD \u53D1\u5E03\u811A\u672C
1921
- ${pm} deploy
1922
- \`\`\`
1923
-
1924
- ## \u5BB9\u5668\u8FD0\u884C
1925
-
1926
- \u9ED8\u8BA4\u955C\u50CF\u76F4\u63A5\u8FD0\u884C API Service\uFF0C\u5916\u90E8\u7AEF\u53E3\u4E3A 3000\u3002
1927
-
1928
- ### \u6784\u5EFA\u955C\u50CF
1929
-
1930
- \`\`\`bash
1931
- # Service \u666E\u901A\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1932
- ${pm} docker:build
1933
- ${pm} podman:build
1934
-
1935
- # \u5C06 packages/${name}-serv/data \u4F5C\u4E3A\u955C\u50CF\u521D\u59CB\u6570\u636E\u6784\u5EFA\uFF08\u4E8C\u9009\u4E00\uFF09
1936
- ${pm} docker:build:data
1937
- ${pm} podman:build:data
1938
- ${webBuildCommands}
1926
+ ${pm} typecheck
1927
+ ${pm} build
1928
+ ${pm} test
1929
+ ${pm} test:e2e
1930
+ ${frontends.includes("desktop") ? `${pm} build --target desktop --platform windows # Windows \u4E3B\u673A\uFF1BLinux/macOS \u5728\u5BF9\u5E94\u4E3B\u673A\u6784\u5EFA` : ""}
1939
1931
  \`\`\`
1940
1932
 
1941
- ### \u8FD0\u884C\u955C\u50CF
1942
-
1943
- \`\`\`bash
1944
- # Service\uFF1B\u4F7F\u7528 Podman \u65F6\u5C06 docker \u66FF\u6362\u4E3A podman
1945
- docker run --rm -d --name ${name} -p 3000:3000 --env-file .env \\
1946
- --mount type=bind,source=/absolute/path/to/config,target=/app/config,readonly \\
1947
- --mount type=volume,source=${name}-data,target=/app/data \\
1948
- ${name}:latest
1949
- ${webRunCommand}
1950
- \`\`\`
1933
+ Tauri \u9700\u8981\u76EE\u6807\u7CFB\u7EDF\u7684 Rust target\u3001SDK \u548C\u6253\u5305\u5DE5\u5177\u3002\u6D4B\u8BD5\u4EA7\u751F\u7684\u4E34\u65F6\u5DE5\u7A0B\u3001\u622A\u56FE\u3001\u62A5\u544A\u3001\u5BB9\u5668\u3001\u955C\u50CF\u4E0E\u8FDB\u7A0B\u5E94\u5728 finally/teardown \u4E2D\u81EA\u52A8\u6E05\u7406\uFF0C\u5931\u8D25\u4E5F\u6267\u884C\u3002
1951
1934
 
1952
- ### \u6D4B\u8BD5\u955C\u50CF
1935
+ ## \u6784\u5EFA\u4E0E\u90E8\u7F72
1953
1936
 
1954
1937
  \`\`\`bash
1955
- curl --fail http://127.0.0.1:3000/health
1956
- curl --fail http://127.0.0.1:3000/ready
1957
- ${webTestCommands}docker stop ${containerNames}
1938
+ # \u9ED8\u8BA4\u53EA\u7F16\u8BD1\u6E90\u7801\uFF1B\u4E0D\u9009\u62E9\u90E8\u7F72\u73AF\u5883
1939
+ ${pm} build
1940
+ # \u6784\u5EFA\u5E76\u5BFC\u51FA API/Web \u955C\u50CF\uFF0Cauto \u4F18\u5148 Podman\u3001\u5176\u6B21 Docker
1941
+ ${pm} build --target docker
1942
+ # \u9009\u62E9\u73AF\u5883\u3001\u4E2D\u95F4\u4EF6\u5E76\u4E0A\u4F20\u4E00\u4E2A Compose tar\uFF1B\u4E0D\u4F1A\u91CD\u65B0\u6784\u5EFA
1943
+ ${pm} exec hai deploy packages/${name}-serv --env test
1944
+ # \u65E0\u6CD5\u8FDE\u63A5\u670D\u52A1\u5668\u65F6\u53EA\u751F\u6210\u5305\uFF0C\u6309\u8F93\u51FA\u8BF4\u660E\u624B\u5DE5\u642C\u8FD0\u548C\u542F\u52A8
1945
+ ${pm} exec hai deploy packages/${name}-serv --env prod --bundle-only
1958
1946
  \`\`\`
1959
1947
 
1960
- - \u914D\u7F6E\u9ED8\u8BA4\u6765\u81EA \`packages/${name}-serv/config\`\uFF0C\u4E5F\u53EF\u6302\u8F7D\u5230 \`/app/config\`\uFF1BYAML \u4E2D\u7684 \`\${ENV_NAME:default}\` \u53EF\u7531 \`--env-file\` \u6216 \`-e\` \u6CE8\u5165\u3002
1961
- - \u5E26\u6570\u636E\u6784\u5EFA\u4F1A\u628A \`packages/${name}-serv/data\` \u56FA\u5316\u4E3A\u955C\u50CF\u521D\u59CB\u6570\u636E\uFF1B\u6301\u4E45\u5316\u5199\u5165\u4ECD\u5E94\u4F7F\u7528 volume\u3002
1962
- - Service \u7531 \`@h-ai/serv\` \u76F4\u63A5\u5BF9\u5916\u76D1\u542C\uFF0C\u4E0D\u989D\u5916\u542F\u52A8 Web \u8F6C\u53D1\u5C42\u3002
1963
- ${frontends.includes("web") ? `- Web \u4F7F\u7528\u72EC\u7ACB\u9759\u6001\u955C\u50CF\u5E76\u652F\u6301\u4E00\u6B21\u6784\u5EFA\u3001\u591A\u73AF\u5883\u90E8\u7F72\uFF1B\u6784\u5EFA\u53EA\u9700\u8FD0\u884C \`${pm} docker:build:web\`\uFF0C\u542F\u52A8\u5BB9\u5668\u65F6\u901A\u8FC7 \`-e PUBLIC_API_BASE=https://api.example.com\` \u6307\u5411 Service \u5BF9\u5916\u5730\u5740\u3002\u672A\u8BBE\u7F6E\u65F6\u9ED8\u8BA4\u4F7F\u7528 Web \u9875\u9762\u81EA\u8EAB origin\u3002` : ""}
1964
-
1965
- \u4EE5\u4E0A\u4E09\u4E2A Service \u8FD0\u884C\u65F6\u53C2\u6570\u5747\u53EF\u6309\u9700\u5220\u9664\uFF1A
1966
-
1967
- | \u53C2\u6570 | \u7528\u9014 | \u53EF\u9009\u6027 |
1968
- | --- | --- | --- |
1969
- | \`--env-file .env\` | \u6279\u91CF\u4F20\u5165\u73AF\u5883\u53D8\u91CF\uFF1B\u4E5F\u53EF\u6539\u7528\u591A\u4E2A \`-e NAME=value\`\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u548C YAML \u9ED8\u8BA4\u503C\u3002 |
1970
- | \`--mount type=bind,...target=/app/config,readonly\` | \u7528\u5BBF\u4E3B\u673A\u914D\u7F6E\u76EE\u5F55\u8986\u76D6\u955C\u50CF\u5185 \`/app/config\`\uFF1B\u5FC5\u987B\u628A \`source\` \u6539\u6210\u771F\u5B9E\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\`readonly\` \u9632\u6B62\u5BB9\u5668\u4FEE\u6539\u914D\u7F6E\u3002 | \u53EF\u9009\uFF1B\u5220\u9664\u540E\u4F7F\u7528\u955C\u50CF\u81EA\u5E26\u914D\u7F6E\u3002 |
1971
- | \`--mount type=volume,...target=/app/data\` | \u521B\u5EFA\u6216\u590D\u7528 named volume\uFF0C\u6301\u4E45\u5316 SQLite \u548C\u672C\u5730\u5B58\u50A8\u3002 | \u53EF\u9009\u4F46\u751F\u4EA7\u73AF\u5883\u5EFA\u8BAE\u4FDD\u7559\uFF1B\u5220\u9664\u540E\u6570\u636E\u53EA\u5199\u5165\u5BB9\u5668\u5C42\uFF0C\u4F7F\u7528 \`--rm\` \u5220\u9664\u5BB9\u5668\u65F6\u4F1A\u4E22\u5931\u3002 |
1972
-
1973
- \u524D\u7AEF\u5355\u72EC\u4EA4\u4ED8\u547D\u4EE4\uFF1A
1974
-
1975
- - Web\uFF1A\`${pm} --filter ${name}-web build\` / \`${pm} --filter ${name}-web package\`
1976
- - App\uFF1A\`${pm} --filter ${name}-app package\`\uFF0CAndroid/iOS \u53EF\u7528 \`cap:build:android:debug\` / \`cap:build:android:release\` / \`cap:build:ios\`
1977
- - Desktop\uFF1A\`${pm} --filter ${name}-desktop tauri:dev\` / \`${pm} --filter ${name}-desktop package\`
1948
+ \u955C\u50CF\u6784\u5EFA\u914D\u7F6E\u89C1 \`hai.build.json\`\u3002\u6784\u5EFA\u4EA7\u7269\u4FDD\u5B58\u5728 \`.hai/build/docker/<buildId>\`\uFF0C\u53EF\u7528 \`--artifact-dir\` \u9009\u62E9\u540C\u4E00\u4EFD\u4EA7\u7269\u90E8\u7F72\u5230 test/prod\u3002\u90E8\u7F72\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u4E0D\u80FD\u63D0\u4EA4 Git\u3002\u73AF\u5883\u5DF2\u914D\u7F6E\u5B8C\u5907\u53EF\u52A0 \`--skip-middleware\`\u3002\u670D\u52A1\u8FD0\u884C\u6570\u636E\u7531 volume \u6301\u4E45\u5316\uFF0C\u955C\u50CF\u4E0D\u6253\u5305\u672C\u673A data\u3002API/Web \u8054\u5408\u90E8\u7F72\u516C\u5F00 Web \u5165\u53E3\u5E76\u4EE3\u7406\u540C\u6E90 API\uFF1B\u6CA1\u6709 Web \u4EA7\u7269\u65F6\u53EA\u90E8\u7F72\u670D\u52A1\u3002
1978
1949
 
1979
1950
  ## i18n \u4E0E shared \u534F\u540C
1980
1951
 
@@ -2118,7 +2089,7 @@ async function addModule(options) {
2118
2089
  core.logger.info(`\u53EF\u7528\u6A21\u5757: ${Object.keys(MODULE_MAP).join(", ")}`);
2119
2090
  return;
2120
2091
  }
2121
- const { generateProjectAiSupport, resolveSkillNamesForPackages } = await import('./cli-skill-templates-EZFW7D43.js');
2092
+ const { generateProjectAiSupport, resolveSkillNamesForPackages } = await import('./cli-skill-templates-TSYEQTCY.js');
2122
2093
  if (moduleDef.packages.every((p) => project.haiPackages.includes(p))) {
2123
2094
  const generatedFiles2 = await generateProjectAiSupport(
2124
2095
  cwd,
@@ -2224,21 +2195,510 @@ async function addModule(options) {
2224
2195
  throw error;
2225
2196
  }
2226
2197
  }
2198
+ async function run(command, args, cwd, quiet = false) {
2199
+ await new Promise((resolve3, reject) => {
2200
+ const env = Object.fromEntries(Object.entries(process4.env).filter(([key]) => !/^(?:HAI_|PUBLIC_|VITE_)/.test(key)));
2201
+ env.NODE_ENV = "production";
2202
+ if (env.CI === "1" || env.CI === "0")
2203
+ env.CI = env.CI === "1" ? "true" : "false";
2204
+ const child = spawn(command, args, { cwd, env, windowsHide: true, stdio: quiet ? "ignore" : "inherit", shell: false });
2205
+ child.once("error", reject);
2206
+ child.once("exit", (code) => code === 0 ? resolve3() : reject(new Error(cliM("cli_build_failed", { params: { command, code: String(code) } }))));
2207
+ });
2208
+ }
2209
+ async function pnpm(args, cwd) {
2210
+ const entry = process4.env.npm_execpath;
2211
+ if (entry && /(?:^|[/\\])pnpm(?:-native)?(?:\.exe|\.[cm]?js)?$/i.test(entry))
2212
+ return /\.[cm]?js$/i.test(entry) ? run(process4.execPath, [entry, ...args], cwd) : run(entry, args, cwd);
2213
+ if (process4.platform === "win32") {
2214
+ const directories = (process4.env.PATH ?? "").split(";");
2215
+ for (const directory of directories) {
2216
+ const executable = join(directory, "pnpm.exe");
2217
+ if (existsSync(executable))
2218
+ return run(executable, args, cwd);
2219
+ for (const relative of ["node_modules/pnpm/bin/pnpm.cjs", "pnpm.cjs"]) {
2220
+ const candidate = join(directory, relative);
2221
+ if (existsSync(candidate))
2222
+ return run(process4.execPath, [candidate, ...args], cwd);
2223
+ }
2224
+ }
2225
+ throw new Error(cliM("cli_build_use_pnpm"));
2226
+ }
2227
+ return run("pnpm", args, cwd);
2228
+ }
2229
+ async function buildCommand(options) {
2230
+ const cwd = resolve(options.cwd ?? process4.cwd());
2231
+ const target = options.target ?? "compile";
2232
+ if (!["compile", "docker", "desktop"].includes(target))
2233
+ throw new Error(cliM("cli_build_target_invalid"));
2234
+ if (target !== "docker" && (options.runtime !== void 0 || options.network !== void 0 || options.sqlitePrebuildMirror !== void 0 || options.httpProxy === false) || target !== "desktop" && (options.platform !== void 0 || options.arch !== void 0)) {
2235
+ throw new Error(cliM("cli_build_options_invalid"));
2236
+ }
2237
+ if (target === "compile") {
2238
+ await pnpm(["run", "compile"], cwd);
2239
+ return;
2240
+ }
2241
+ const metadata = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
2242
+ const appVersion = metadata.version;
2243
+ if (typeof appVersion !== "string" || appVersion.length > 80 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i.test(appVersion))
2244
+ throw new Error(cliM("cli_build_version_invalid"));
2245
+ const config = JSON.parse(readFileSync(join(cwd, "hai.build.json"), "utf8"));
2246
+ if (target === "desktop") {
2247
+ const platforms = { win32: "windows", linux: "linux", darwin: "macos" };
2248
+ const host = platforms[process4.platform];
2249
+ const platform = options.platform ?? host;
2250
+ const arch = options.arch ?? process4.arch;
2251
+ if (!platform || platform !== host || !["x64", "arm64"].includes(arch))
2252
+ throw new Error(cliM("cli_build_native_host"));
2253
+ if (!config.desktop?.length)
2254
+ throw new Error(cliM("cli_build_desktop_missing"));
2255
+ const rustTarget = `${arch === "x64" ? "x86_64" : "aarch64"}-${platform === "windows" ? "pc-windows-msvc" : platform === "macos" ? "apple-darwin" : "unknown-linux-gnu"}`;
2256
+ const bundles = platform === "windows" ? "nsis" : platform === "macos" ? "app,dmg" : "deb,appimage";
2257
+ for (const directory of config.desktop)
2258
+ await pnpm(["exec", "tauri", "build", "--target", rustTarget, "--bundles", bundles, "--config", JSON.stringify({ version: appVersion })], resolve(cwd, directory));
2259
+ core.logger.info(cliM("cli_build_native_ready", { params: { target: rustTarget } }));
2260
+ return;
2261
+ }
2262
+ if (!Array.isArray(config.images) || !config.images.length || config.images.filter((image) => image.name === "app").length !== 1 || config.images.length > 2 || new Set(config.images.map((image) => image.name)).size !== config.images.length || config.images.some((image) => !["app", "web"].includes(image.name) || image.containerPort !== void 0 && (!Number.isInteger(image.containerPort) || image.containerPort < 1 || image.containerPort > 65535))) {
2263
+ throw new Error(cliM("cli_build_images_missing"));
2264
+ }
2265
+ const candidates = options.runtime && options.runtime !== "auto" ? [options.runtime] : ["podman", "docker"];
2266
+ if (candidates.some((runtime2) => !["podman", "docker"].includes(runtime2)))
2267
+ throw new Error(cliM("cli_build_runtime_missing"));
2268
+ let runtime;
2269
+ for (const candidate of candidates) {
2270
+ try {
2271
+ await run(candidate, ["version"], cwd, true);
2272
+ runtime = candidate;
2273
+ break;
2274
+ } catch {
2275
+ }
2276
+ }
2277
+ if (!runtime)
2278
+ throw new Error(cliM("cli_build_runtime_missing"));
2279
+ if (options.sqlitePrebuildMirror) {
2280
+ const url = new URL(options.sqlitePrebuildMirror);
2281
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
2282
+ throw new Error(cliM("cli_build_mirror_invalid"));
2283
+ }
2284
+ const buildId = `${appVersion}-${Date.now()}-${randomBytes(3).toString("hex")}`;
2285
+ const artifact = { version: 2, appVersion, buildId, images: [] };
2286
+ const parent = join(cwd, ".hai/build/docker");
2287
+ mkdirSync(parent, { recursive: true });
2288
+ const staging = mkdtempSync(join(parent, ".building-"));
2289
+ const built = [];
2290
+ try {
2291
+ for (const image of config.images) {
2292
+ if (!["app", "web"].includes(image.name))
2293
+ throw new Error(cliM("cli_build_images_missing"));
2294
+ const context = resolve(cwd, image.context ?? ".");
2295
+ const imageRef = `localhost/hai-${basenameSafe(cwd)}-${image.name}:${buildId.toLowerCase().replaceAll("+", "_")}`;
2296
+ const args = ["build", "-f", resolve(context, image.dockerfile ?? "Dockerfile"), "-t", imageRef];
2297
+ if (options.network !== void 0)
2298
+ args.push("--network", options.network);
2299
+ const ignore = `${resolve(context, image.dockerfile ?? "Dockerfile")}.dockerignore`;
2300
+ if (runtime === "podman" && existsSync(ignore))
2301
+ args.push("--ignorefile", ignore);
2302
+ if (runtime === "podman") {
2303
+ args.push("--format", "docker", "--jobs", "1", ...options.httpProxy === false ? ["--http-proxy=false"] : []);
2304
+ if (options.httpProxy !== false) {
2305
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "FTP_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "ftp_proxy", "no_proxy"]) {
2306
+ if (process4.env[key])
2307
+ args.push("--build-arg", `${key}=${process4.env[key]}`);
2308
+ }
2309
+ }
2310
+ }
2311
+ if (image.target)
2312
+ args.push("--target", image.target);
2313
+ if (options.sqlitePrebuildMirror)
2314
+ args.push("--build-arg", `SQLITE_PREBUILD_MIRROR=${options.sqlitePrebuildMirror}`);
2315
+ built.push(imageRef);
2316
+ await run(runtime, [...args, context], cwd);
2317
+ const file = `${image.name}-${appVersion}.tar`;
2318
+ await run(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, file), imageRef], cwd);
2319
+ const hash = createHash("sha256");
2320
+ for await (const chunk of createReadStream(join(staging, file)))
2321
+ hash.update(chunk);
2322
+ artifact.images.push({ name: image.name, file, imageRef, sha256: hash.digest("hex"), ...image.containerPort ? { containerPort: image.containerPort } : {} });
2323
+ }
2324
+ const output = join(parent, buildId);
2325
+ writeFileSync(join(staging, "manifest.json"), `${JSON.stringify(artifact, null, 2)}
2326
+ `);
2327
+ renameSync(staging, output);
2328
+ writeFileSync(join(parent, `latest-${buildId}`), `${buildId}
2329
+ `);
2330
+ renameSync(join(parent, `latest-${buildId}`), join(parent, "latest"));
2331
+ core.logger.info(cliM("cli_build_ready", { params: { path: output, runtime } }));
2332
+ } catch (error) {
2333
+ for (const imageRef of built) {
2334
+ try {
2335
+ await run(runtime, ["image", "rm", imageRef], cwd, true);
2336
+ } catch {
2337
+ }
2338
+ }
2339
+ throw error;
2340
+ } finally {
2341
+ rmSync(staging, { recursive: true, force: true });
2342
+ }
2343
+ }
2344
+ function basenameSafe(directory) {
2345
+ return directory.slice(dirname(directory).length + 1).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "") || "app";
2346
+ }
2347
+ function displayDeployValue(name, value) {
2348
+ if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
2349
+ return cliM("cli_deploy_secret");
2350
+ try {
2351
+ const parsed2 = JSON.parse(value);
2352
+ if (parsed2 && typeof parsed2 === "object") {
2353
+ const sanitize = (object) => {
2354
+ if (Array.isArray(object))
2355
+ return object.map(sanitize);
2356
+ if (object && typeof object === "object")
2357
+ return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, /PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(key.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(key) ? cliM("cli_deploy_secret") : typeof item === "string" ? displayDeployValue(key, item) : sanitize(item)]));
2358
+ return object;
2359
+ };
2360
+ return JSON.stringify(sanitize(parsed2));
2361
+ }
2362
+ } catch {
2363
+ }
2364
+ try {
2365
+ const url = new URL(value);
2366
+ if (url.username || url.password) {
2367
+ url.username = "***";
2368
+ url.password = "***";
2369
+ }
2370
+ for (const key of url.searchParams.keys()) {
2371
+ if (/password|secret|token|key|authorization/i.test(key))
2372
+ url.searchParams.set(key, "***");
2373
+ }
2374
+ return url.toString();
2375
+ } catch {
2376
+ return value;
2377
+ }
2378
+ }
2379
+ var CLOUD_FIELDS = {
2380
+ "neon": ["apiKey"],
2381
+ "upstash": ["email", "apiKey"],
2382
+ "cloudflare-r2": ["accountId", "apiToken", "accessKeyId", "secretAccessKey"],
2383
+ "resend": ["apiKey", "from"],
2384
+ "aliyun": ["accessKeyId", "accessKeySecret", "signName"]
2385
+ };
2386
+ function targetConfig(requirement, environment) {
2387
+ const result = {};
2388
+ const parent = environment.targetValues[requirement.envPrefix];
2389
+ if (parent) {
2390
+ try {
2391
+ Object.assign(result, JSON.parse(parent));
2392
+ } catch {
2393
+ return {};
2394
+ }
2395
+ }
2396
+ for (const [key, value] of Object.entries(environment.targetValues)) {
2397
+ if (key.startsWith(`${requirement.envPrefix}_`))
2398
+ result[key.slice(requirement.envPrefix.length + 1).toLowerCase()] = value;
2399
+ }
2400
+ return result;
2401
+ }
2402
+ function canKeepMiddleware(requirement, backend, environment) {
2403
+ const target = targetConfig(requirement, environment);
2404
+ if (requirement.service === "ldap" && backend.backend === "disabled")
2405
+ return environment.targetValues.HAI_IAM_LOGIN_LDAP === "false";
2406
+ if (requirement.service === "ldap") {
2407
+ if (!Object.keys(target).length)
2408
+ return false;
2409
+ } else if (target.type !== backend.backend) {
2410
+ return false;
2411
+ }
2412
+ return backend.fields.filter((field) => field.required && !field.secret).every((field) => {
2413
+ if ((target.url || requirement.config.url && Object.keys(target).length) && ["host", "port"].includes(field.name))
2414
+ return true;
2415
+ return target[field.name] !== void 0 && target[field.name] !== "" || target[field.name.toLowerCase()] !== void 0 && target[field.name.toLowerCase()] !== "";
2416
+ }) && backend.fields.filter((field) => field.required && field.secret).every((field) => Boolean(requirement.config[field.name] || environment.values[`${requirement.envPrefix}_${field.name.toUpperCase()}`]));
2417
+ }
2418
+ async function ask(question) {
2419
+ const answer = await prompts({ ...question, name: "value" });
2420
+ if (answer.value === void 0)
2421
+ throw new Error(cliM("cli_deploy_cancelled"));
2422
+ return answer.value;
2423
+ }
2424
+ function validateField(value, field) {
2425
+ if (value === "")
2426
+ return field.required ? cliM("cli_deploy_required") : true;
2427
+ if (field.kind === "port")
2428
+ return /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535 || cliM("cli_deploy_invalid_value");
2429
+ if (field.kind === "number")
2430
+ return /^\d+$/.test(value) && Number(value) >= (field.minimum ?? 0) && Number(value) <= (field.maximum ?? Number.MAX_SAFE_INTEGER) || cliM("cli_deploy_invalid_value");
2431
+ if (field.kind === "boolean")
2432
+ return ["true", "false"].includes(value) || cliM("cli_deploy_invalid_value");
2433
+ if (field.kind === "url") {
2434
+ try {
2435
+ return Boolean(new URL(value).protocol);
2436
+ } catch {
2437
+ return cliM("cli_deploy_invalid_value");
2438
+ }
2439
+ }
2440
+ if (field.name === "ssl") {
2441
+ const parsed2 = parseDocument(value);
2442
+ const ssl = parsed2.errors.length ? void 0 : parsed2.toJS();
2443
+ return typeof ssl === "boolean" || ["require", "prefer", "allow", "disable"].includes(String(ssl)) || ssl !== null && typeof ssl === "object" && !Array.isArray(ssl) || cliM("cli_deploy_invalid_value");
2444
+ }
2445
+ if (field.name === "headers") {
2446
+ try {
2447
+ const headers = JSON.parse(value);
2448
+ return headers !== null && typeof headers === "object" && !Array.isArray(headers) && Object.values(headers).every((item) => typeof item === "string") || cliM("cli_deploy_invalid_value");
2449
+ } catch {
2450
+ return cliM("cli_deploy_invalid_value");
2451
+ }
2452
+ }
2453
+ if (field.name === "method")
2454
+ return ["POST", "PUT"].includes(value) || cliM("cli_deploy_invalid_value");
2455
+ return true;
2456
+ }
2457
+ function ensureConfigNode(appDir, requirement, backend) {
2458
+ if (requirement.service === "ldap" && backend === "disabled")
2459
+ return;
2460
+ const directory = join(appDir, "config");
2461
+ mkdirSync(directory, { recursive: true });
2462
+ const path7 = join(directory, `_${requirement.configName}.yml`);
2463
+ const document = parseDocument(existsSync(path7) ? readFileSync(path7, "utf8") : "{}\n");
2464
+ if (document.errors.length)
2465
+ throw new Error(cliM("cli_deploy_invalid_value"));
2466
+ if (requirement.configPath.length && !document.hasIn(requirement.configPath))
2467
+ document.setIn(requirement.configPath, requirement.configName === "reach" ? { name: requirement.config.name ?? requirement.service, type: backend } : {});
2468
+ if (requirement.service === "ldap") {
2469
+ if (!document.hasIn(["login"]))
2470
+ document.setIn(["login"], {});
2471
+ if (!document.hasIn(["login", "ldap"]))
2472
+ document.setIn(["login", "ldap"], `\${HAI_IAM_LOGIN_LDAP:false}`);
2473
+ }
2474
+ const content = document.toString();
2475
+ if (!existsSync(path7) || readFileSync(path7, "utf8") !== content)
2476
+ writeFileSync(path7, content);
2477
+ }
2478
+ function connectionEnv(requirement, backend, config) {
2479
+ if (requirement.service === "ldap")
2480
+ return backend === "disabled" ? { HAI_IAM_LOGIN_LDAP: "false" } : { HAI_IAM_LOGIN_LDAP: "true", [requirement.envPrefix]: JSON.stringify(config) };
2481
+ return { [requirement.envPrefix]: JSON.stringify({ ...config, type: backend }) };
2482
+ }
2483
+ async function configureDeployMiddleware(module, scan, environment, config, yes = false) {
2484
+ const modes = {};
2485
+ const services = {};
2486
+ const existingServices = config.services && typeof config.services === "object" ? config.services : {};
2487
+ const provider = config.provider;
2488
+ for (const requirement of scan.middlewares ?? []) {
2489
+ const choices = module.DEPLOY_MIDDLEWARE[requirement.service];
2490
+ const current = choices.find((item) => item.backend === requirement.backend);
2491
+ if (!current)
2492
+ throw new Error(cliM("cli_deploy_cloud_unsupported", { params: { backend: requirement.backend } }));
2493
+ core.logger.info(cliM("cli_deploy_middleware", { params: { service: requirement.service, backend: current.label, purpose: module.describeMiddlewareBackend(current), source: requirement.source } }));
2494
+ const canKeep = canKeepMiddleware(requirement, current, environment);
2495
+ const mode = yes ? canKeep ? "keep" : "default" : await ask({
2496
+ type: "select",
2497
+ name: "value",
2498
+ message: cliM("cli_deploy_action", { params: { service: requirement.service } }),
2499
+ choices: [
2500
+ { title: cliM("cli_deploy_default"), value: "default" },
2501
+ { title: cliM("cli_deploy_custom"), value: "custom" },
2502
+ ...canKeep ? [{ title: cliM("cli_deploy_keep"), value: "keep" }] : []
2503
+ ]
2504
+ });
2505
+ modes[requirement.envPrefix] = mode;
2506
+ const previous = modes[requirement.service];
2507
+ modes[requirement.service] = previous === "default" || mode === "default" ? "default" : previous === "custom" || mode === "custom" ? "custom" : "keep";
2508
+ if (mode === "keep") {
2509
+ if (!canKeep)
2510
+ throw new Error(cliM("cli_deploy_missing_keep", { params: { service: requirement.service } }));
2511
+ continue;
2512
+ }
2513
+ const backendName = yes ? current.backend : String(await ask({ type: "select", name: "value", message: cliM("cli_deploy_backend", { params: { service: requirement.service } }), initial: choices.indexOf(current), choices: choices.map((item) => ({ title: `${item.label} \u2014 ${module.describeMiddlewareBackend(item)}`, value: item.backend })) }));
2514
+ const backend = choices.find((item) => item.backend === backendName);
2515
+ if (!backend)
2516
+ throw new Error(cliM("cli_deploy_invalid_value"));
2517
+ requirement.backend = backendName;
2518
+ const updates = {};
2519
+ if (mode === "default" && backendName === "s3" && provider?.type === "docker-ssh") {
2520
+ const container = config.container && typeof config.container === "object" ? config.container : {};
2521
+ const currentEngine = container.s3Engine === "minio" ? "minio" : "rustfs";
2522
+ const engine = yes ? currentEngine : String(await ask({ type: "select", name: "value", message: cliM("cli_deploy_s3_engine"), initial: currentEngine === "rustfs" ? 0 : 1, choices: [{ title: "RustFS", value: "rustfs" }, { title: "MinIO", value: "minio" }] }));
2523
+ if (!["rustfs", "minio"].includes(engine))
2524
+ throw new Error(cliM("cli_deploy_invalid_value"));
2525
+ config.container = { ...container, s3Engine: engine };
2526
+ updates.HAI_DEPLOY_CONTAINER_S3ENGINE = engine;
2527
+ const deploymentFile = join(scan.appDir ?? ".", "config", "_deploy.yml");
2528
+ const deployment = parseDocument(readFileSync(deploymentFile, "utf8"));
2529
+ if (deployment.errors.length)
2530
+ throw new Error(cliM("cli_deploy_invalid_value"));
2531
+ if (!deployment.hasIn(["container", "s3Engine"])) {
2532
+ if (!deployment.hasIn(["container"]))
2533
+ deployment.setIn(["container"], {});
2534
+ deployment.setIn(["container", "s3Engine"], `\${HAI_DEPLOY_CONTAINER_S3ENGINE:rustfs}`);
2535
+ writeFileSync(deploymentFile, deployment.toString());
2536
+ }
2537
+ }
2538
+ if (mode === "default" && provider?.type === "vercel") {
2539
+ if (!backend.provisioner)
2540
+ throw new Error(cliM("cli_deploy_cloud_unsupported", { params: { backend: backend.label } }));
2541
+ const prior = existingServices[requirement.service];
2542
+ const priorConfig = prior && typeof prior === "object" ? prior : {};
2543
+ const cloud = { provisioner: backend.provisioner };
2544
+ for (const name of CLOUD_FIELDS[backend.provisioner]) {
2545
+ const key = `HAI_DEPLOY_SERVICES_${requirement.service.toUpperCase()}_${name.toUpperCase()}`;
2546
+ const initial = String(priorConfig[name] ?? environment.values[key] ?? "");
2547
+ const value = yes ? initial : String(await ask({ type: /key|token|secret/i.test(name) ? "password" : "text", name: "value", message: key, initial, validate: (input) => Boolean(input) || cliM("cli_deploy_required") }));
2548
+ if (!value)
2549
+ throw new Error(`${key}: ${cliM("cli_deploy_required")}`);
2550
+ cloud[name] = value;
2551
+ updates[key] = value;
2552
+ }
2553
+ services[requirement.service] = cloud;
2554
+ } else {
2555
+ if (mode === "default" && (requirement.service === "sms" || backendName === "api"))
2556
+ throw new Error(cliM("cli_deploy_cloud_unsupported", { params: { backend: backend.label } }));
2557
+ const connection = mode === "custom" && backendName === current.backend ? { ...requirement.config } : requirement.configName === "reach" ? { name: requirement.config.name ?? requirement.service } : {};
2558
+ for (const field of backend.fields) {
2559
+ const previous2 = backendName === current.backend ? requirement.config[field.name] : void 0;
2560
+ const initial = previous2 === void 0 ? field.defaultValue ?? "" : typeof previous2 === "object" ? JSON.stringify(previous2) : String(previous2);
2561
+ const variable = `${requirement.envPrefix}_${field.name.toUpperCase()}`;
2562
+ const info = moduleEnvironmentVariables(requirement.configName).find((item) => item.name === variable);
2563
+ const value = mode === "custom" && !yes ? String(await ask({ type: field.secret ? "password" : "text", name: "value", message: cliM("cli_deploy_field", { params: { name: variable, description: info?.description ?? field.name, constraint: info?.constraint ?? field.kind ?? "string" } }), initial, validate: (input) => validateField(String(input), field) })) : initial;
2564
+ if (value !== "") {
2565
+ connection[field.name] = field.kind === "number" || field.kind === "port" ? Number(value) : field.kind === "boolean" ? value === "true" : field.name === "headers" || field.name === "ssl" ? parseDocument(value).toJS() : value;
2566
+ } else {
2567
+ delete connection[field.name];
2568
+ }
2569
+ }
2570
+ Object.assign(updates, connectionEnv(requirement, backendName, connection));
2571
+ }
2572
+ ensureConfigNode(scan.appDir ?? ".", requirement, backendName);
2573
+ const saved = module.saveDeploymentEnvironment(environment, updates);
2574
+ if (!saved.success)
2575
+ throw new Error(saved.error.message);
2576
+ for (const [name, value] of Object.entries(updates)) {
2577
+ core.logger.info(cliM("cli_deploy_saved", { params: { name, value: displayDeployValue(name, value) } }));
2578
+ }
2579
+ }
2580
+ config.services = Object.keys(services).length ? services : void 0;
2581
+ return modes;
2582
+ }
2583
+
2584
+ // src/commands/cli-deploy.ts
2585
+ function configObject(value) {
2586
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2587
+ }
2588
+ async function configureDeployTarget(module, config, environment, nonInteractive, bundleOnly) {
2589
+ if (bundleOnly) {
2590
+ const existing = configObject(config.provider);
2591
+ if (existing.type && existing.type !== "docker-ssh")
2592
+ throw new Error(cliM("cli_deploy_bundle_provider"));
2593
+ config.provider = { type: "docker-ssh", remote: { runtime: "auto" }, expose: { hostPort: 3e3 }, ...existing };
2594
+ }
2595
+ const current = module.ProviderConfigSchema.safeParse(config.provider);
2596
+ if (current.success && (bundleOnly || current.data.type !== "docker-ssh" || current.data.ssh)) {
2597
+ config.provider = current.data;
2598
+ core.logger.info(cliM("cli_deploy_target_reused", { params: { target: current.data.type === "docker-ssh" ? current.data.ssh ? `${current.data.ssh.username}@${current.data.ssh.host}:${current.data.ssh.port}` : cliM("cli_deploy_bundle_local") : "Vercel" } }));
2599
+ return;
2600
+ }
2601
+ if (nonInteractive)
2602
+ throw new Error(cliM("cli_deploy_target_missing", { params: { fields: current.success ? "provider.ssh" : current.error.issues.map((issue) => `provider.${issue.path.join(".")}`).join(", ") } }));
2603
+ const original = configObject(config.provider);
2604
+ const platform = await prompts({
2605
+ type: "select",
2606
+ name: "value",
2607
+ message: cliM("cli_deploy_target_platform"),
2608
+ choices: [{ title: cliM("cli_deploy_target_ssh"), value: "docker-ssh" }, { title: "Vercel", value: "vercel" }],
2609
+ initial: original.type === "vercel" ? 1 : 0
2610
+ });
2611
+ if (!platform.value)
2612
+ throw new Error(cliM("cli_deploy_cancelled"));
2613
+ const input = async (name, initial, options = {}) => {
2614
+ const answer = await prompts({
2615
+ type: options.secret ? "password" : "text",
2616
+ name: "value",
2617
+ message: cliM("cli_deploy_target_field", { params: { name } }),
2618
+ initial: options.secret ? void 0 : initial === void 0 ? "" : String(initial),
2619
+ validate: (value) => options.port ? /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535 || cliM("cli_deploy_invalid_value") : options.optional || !!value.trim() || cliM("cli_deploy_invalid_value")
2620
+ });
2621
+ if (answer.value === void 0)
2622
+ throw new Error(cliM("cli_deploy_cancelled"));
2623
+ return String(answer.value).trim();
2624
+ };
2625
+ let provider;
2626
+ if (platform.value === "docker-ssh") {
2627
+ core.logger.info(cliM("cli_deploy_target_ssh_hint"));
2628
+ const ssh = configObject(original.ssh);
2629
+ const remote = configObject(original.remote);
2630
+ const expose = configObject(original.expose);
2631
+ const host = await input("HAI_DEPLOY_PROVIDER_SSH_HOST", ssh.host);
2632
+ const port = Number(await input("HAI_DEPLOY_PROVIDER_SSH_PORT", ssh.port ?? 22, { port: true }));
2633
+ const username = await input("HAI_DEPLOY_PROVIDER_SSH_USERNAME", ssh.username);
2634
+ const identityFile = await input("HAI_DEPLOY_PROVIDER_SSH_IDENTITYFILE", ssh.identityFile, { optional: true });
2635
+ const knownHostsFile = await input("HAI_DEPLOY_PROVIDER_SSH_KNOWNHOSTSFILE", ssh.knownHostsFile, { optional: true });
2636
+ const baseDir = await input("HAI_DEPLOY_PROVIDER_REMOTE_BASEDIR", remote.baseDir || "/opt/hai/apps");
2637
+ const runtime = await prompts({ type: "select", name: "value", message: cliM("cli_deploy_target_runtime"), choices: ["auto", "podman", "docker"].map((value) => ({ title: value, value })), initial: ["auto", "podman", "docker"].indexOf(String(remote.runtime ?? "auto")) });
2638
+ if (!runtime.value)
2639
+ throw new Error(cliM("cli_deploy_cancelled"));
2640
+ const hostPort = Number(await input("HAI_DEPLOY_PROVIDER_EXPOSE_HOSTPORT", expose.hostPort || 3e3, { port: true }));
2641
+ const containerPort = Number(await input("HAI_DEPLOY_PROVIDER_EXPOSE_CONTAINERPORT", expose.containerPort || 3e3, { port: true }));
2642
+ provider = {
2643
+ type: "docker-ssh",
2644
+ ssh: { ...ssh, host, port, username, identityFile: identityFile || void 0, knownHostsFile: knownHostsFile || void 0 },
2645
+ remote: { ...remote, baseDir, runtime: runtime.value },
2646
+ expose: { type: "port", hostPort, containerPort }
2647
+ };
2648
+ } else {
2649
+ provider = { type: "vercel", token: await input("HAI_DEPLOY_PROVIDER_TOKEN", void 0, { secret: true }) };
2650
+ }
2651
+ const validated = module.ProviderConfigSchema.safeParse(provider);
2652
+ if (!validated.success)
2653
+ throw new Error(cliM("cli_deploy_invalid_value"));
2654
+ const saved = module.saveDeploymentEnvironment(environment, { HAI_DEPLOY_PROVIDER: JSON.stringify(validated.data) }, { private: true });
2655
+ if (!saved.success)
2656
+ throw new Error(saved.error.message);
2657
+ config.provider = validated.data;
2658
+ core.logger.info(cliM("cli_deploy_target_saved"));
2659
+ }
2227
2660
  async function deployCommand(options) {
2228
2661
  const spinner = ora();
2229
2662
  const cwd = options.cwd ?? process4.cwd();
2230
- const appDir = resolve(cwd, options.appDir ?? ".");
2663
+ let appDir = resolve(cwd, options.appDir ?? ".");
2231
2664
  let closeDeploy = null;
2665
+ const originalEnv = { ...process4.env };
2232
2666
  try {
2667
+ let environmentName = options.env;
2668
+ if (!environmentName) {
2669
+ if (!process4.stdin.isTTY)
2670
+ throw new Error(cliM("cli_deploy_environment_hint"));
2671
+ const answer = await prompts({ type: "text", name: "environment", message: cliM("cli_deploy_environment"), initial: "prod" });
2672
+ environmentName = answer.environment;
2673
+ if (!environmentName)
2674
+ throw new Error(cliM("cli_deploy_cancelled"));
2675
+ }
2676
+ if (!options.yes && !process4.stdin.isTTY)
2677
+ throw new Error(cliM("cli_deploy_environment_hint"));
2233
2678
  let deployModule;
2234
2679
  try {
2235
- deployModule = await import('./src-DUGLX4YB.js');
2680
+ const localRequire = createRequire(resolve(appDir, "package.json"));
2681
+ let localModule;
2682
+ try {
2683
+ localModule = localRequire.resolve("@h-ai/deploy");
2684
+ } catch {
2685
+ }
2686
+ deployModule = localModule ? await import(pathToFileURL(localModule).href) : await import('./src-RDQ2EIBT.js');
2236
2687
  } catch {
2237
2688
  core.logger.error(chalk.red(cliM("cli_deploy_module_missing")));
2238
2689
  core.logger.info(chalk.cyan(" pnpm add @h-ai/deploy"));
2239
2690
  throw new Error(cliM("cli_deploy_module_missing"));
2240
2691
  }
2241
2692
  const { deploy } = deployModule;
2693
+ const scanResult = await deploy.scan(appDir, { environment: environmentName, envDir: options.envDir ? resolve(cwd, options.envDir) : void 0 });
2694
+ if (!scanResult.success)
2695
+ throw new Error(cliM("cli_deploy_scan_failed", { params: { message: scanResult.error.message } }));
2696
+ const scan = scanResult.data;
2697
+ appDir = scan.appDir ?? appDir;
2698
+ const environment = deployModule.loadDeploymentEnvironment({ environment: environmentName, envDir: scan.envDir ?? appDir });
2699
+ if (!environment.success)
2700
+ throw new Error(environment.error.message);
2701
+ Object.assign(process4.env, environment.data.values);
2242
2702
  spinner.start(cliM("cli_deploy_load_credentials"));
2243
2703
  const credResult = deploy.credentials.load();
2244
2704
  if (!credResult.success) {
@@ -2252,19 +2712,44 @@ async function deployCommand(options) {
2252
2712
  core.logger.info(chalk.cyan(cliM("cli_deploy_config_hint")));
2253
2713
  throw new Error(cliM("cli_deploy_config_missing", { params: { path: configPath } }));
2254
2714
  }
2255
- const configContent = readFileSync(configPath, "utf-8");
2256
- const rawConfig = interpolateEnvFallback(configContent);
2257
- const deployConfig = parse(rawConfig);
2715
+ const loadedConfig = deployModule.loadDeploymentConfig("deploy", configPath, process4.env);
2716
+ if (!loadedConfig.success)
2717
+ throw new Error(loadedConfig.error.message);
2718
+ const deployConfig = loadedConfig.data;
2258
2719
  spinner.succeed(cliM("cli_deploy_config_loaded"));
2259
- spinner.start(cliM("cli_deploy_scanning"));
2260
- const scanResult = await deploy.scan(appDir);
2261
- if (!scanResult.success) {
2262
- throw new Error(cliM("cli_deploy_scan_failed", { params: { message: scanResult.error.message } }));
2263
- }
2264
- const scan = scanResult.data;
2720
+ await configureDeployTarget(deployModule, deployConfig, environment.data, options.yes, options.bundleOnly);
2265
2721
  spinner.succeed(cliM("cli_deploy_scanned", { params: { name: scan.appName, sveltekit: String(scan.isSvelteKit), services: scan.requiredServices.join(", ") || cliM("cli_deploy_none") } }));
2722
+ let skipMiddleware = options.skipMiddleware;
2723
+ if (!skipMiddleware && !options.yes && scan.requiredServices.length) {
2724
+ const action = await prompts({
2725
+ type: "select",
2726
+ name: "value",
2727
+ message: cliM("cli_deploy_middleware_plan"),
2728
+ choices: [{ title: cliM("cli_deploy_middleware_configure"), value: "configure" }, { title: cliM("cli_deploy_middleware_skip"), value: "skip" }]
2729
+ });
2730
+ if (!action.value)
2731
+ throw new Error(cliM("cli_deploy_cancelled"));
2732
+ skipMiddleware = action.value === "skip";
2733
+ }
2734
+ if (skipMiddleware)
2735
+ core.logger.info(cliM("cli_deploy_middleware_skipped"));
2736
+ const serviceModes = skipMiddleware ? Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) : await configureDeployMiddleware(deployModule, scan, environment.data, deployConfig, options.yes);
2737
+ if (skipMiddleware)
2738
+ deployConfig.services = void 0;
2739
+ for (const key of Object.keys(process4.env)) {
2740
+ if (originalEnv[key] === void 0)
2741
+ delete process4.env[key];
2742
+ }
2743
+ Object.assign(process4.env, originalEnv);
2744
+ const refreshed = deployModule.loadDeploymentEnvironment({ environment: environment.data.environment, envDir: environment.data.envDir });
2745
+ if (!refreshed.success)
2746
+ throw new Error(refreshed.error.message);
2747
+ Object.assign(process4.env, refreshed.data.values);
2266
2748
  spinner.start(cliM("cli_deploy_initializing"));
2267
- const initResult = await deploy.init(deployConfig);
2749
+ const validatedConfig = deployModule.DeployConfigSchema.safeParse(deployConfig);
2750
+ if (!validatedConfig.success)
2751
+ throw new Error(cliM("cli_deploy_init_failed", { params: { message: cliM("cli_deploy_invalid_value") } }));
2752
+ const initResult = await deploy.init(validatedConfig.data);
2268
2753
  if (!initResult.success) {
2269
2754
  throw new Error(cliM("cli_deploy_init_failed", { params: { message: initResult.error.message } }));
2270
2755
  }
@@ -2273,8 +2758,13 @@ async function deployCommand(options) {
2273
2758
  spinner.start(cliM("cli_deploy_deploying"));
2274
2759
  const deployResult = await deploy.deployApp(appDir, {
2275
2760
  projectName: options.projectName,
2276
- skipProvision: options.skipProvision,
2277
- skipBuild: options.skipBuild
2761
+ skipMiddleware,
2762
+ artifactDir: options.artifactDir ? resolve(cwd, options.artifactDir) : void 0,
2763
+ bundleOnly: options.bundleOnly,
2764
+ output: options.output ? resolve(cwd, options.output) : void 0,
2765
+ environment: environment.data.environment,
2766
+ envDir: environment.data.envDir,
2767
+ serviceModes
2278
2768
  });
2279
2769
  if (!deployResult.success) {
2280
2770
  if (deployResult.error.ext?.recovery) {
@@ -2284,9 +2774,29 @@ async function deployCommand(options) {
2284
2774
  }
2285
2775
  throw new Error(cliM("cli_deploy_failed", { params: { message: deployResult.error.message } }));
2286
2776
  }
2287
- spinner.succeed(chalk.green(cliM("cli_deploy_success")));
2777
+ spinner.succeed(chalk.green(cliM(deployResult.data.status === "packaged" ? "cli_deploy_bundle_ready" : "cli_deploy_success")));
2778
+ for (const key of Object.keys(process4.env)) {
2779
+ if (originalEnv[key] === void 0)
2780
+ delete process4.env[key];
2781
+ }
2782
+ Object.assign(process4.env, originalEnv);
2783
+ const completedEnvironment = deployModule.loadDeploymentEnvironment({ environment: environment.data.environment, envDir: environment.data.envDir });
2784
+ if (!completedEnvironment.success)
2785
+ throw new Error(completedEnvironment.error.message);
2786
+ for (const requirement of scan.middlewares ?? []) {
2787
+ if ((serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service]) === "keep")
2788
+ continue;
2789
+ const value = completedEnvironment.data.values[requirement.envPrefix];
2790
+ if (value !== void 0)
2791
+ core.logger.info(cliM("cli_deploy_saved", { params: { name: requirement.envPrefix, value: displayDeployValue(requirement.envPrefix, value) } }));
2792
+ }
2288
2793
  core.logger.info("");
2289
- core.logger.info(chalk.cyan(cliM("cli_deploy_url", { params: { url: deployResult.data.url } })));
2794
+ if (deployResult.data.bundlePath)
2795
+ core.logger.info(cliM("cli_deploy_bundle_path", { params: { path: deployResult.data.bundlePath } }));
2796
+ if (deployResult.data.status === "packaged")
2797
+ core.logger.info(deployResult.data.instructions ?? "");
2798
+ else
2799
+ core.logger.info(chalk.cyan(cliM("cli_deploy_url", { params: { url: deployResult.data.url } })));
2290
2800
  core.logger.info(chalk.gray(cliM("cli_deploy_id", { params: { id: deployResult.data.deploymentId } })));
2291
2801
  if (deployResult.data.envVarsSet.length > 0) {
2292
2802
  core.logger.info(chalk.gray(cliM("cli_deploy_env", { params: { names: deployResult.data.envVarsSet.join(", ") } })));
@@ -2305,13 +2815,13 @@ async function deployCommand(options) {
2305
2815
  process4.exitCode = 1;
2306
2816
  }
2307
2817
  }
2818
+ for (const key of Object.keys(process4.env)) {
2819
+ if (originalEnv[key] === void 0)
2820
+ delete process4.env[key];
2821
+ }
2822
+ Object.assign(process4.env, originalEnv);
2308
2823
  }
2309
2824
  }
2310
- function interpolateEnvFallback(content) {
2311
- return content.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, key, defaultValue) => {
2312
- return process4.env[key] ?? defaultValue ?? "";
2313
- });
2314
- }
2315
2825
  var GENERATORS = {
2316
2826
  page: {
2317
2827
  description: "SvelteKit \u9875\u9762",
@@ -2653,7 +3163,7 @@ async function initProject(options) {
2653
3163
  const { project, existing, missing } = result;
2654
3164
  spinner.succeed(`\u68C0\u6D4B\u5230\u9879\u76EE: ${chalk.cyan(project.name)}`);
2655
3165
  if (project.isHaiProject) {
2656
- const { generateProjectAiSupport, resolveSkillNamesForPackages } = await import('./cli-skill-templates-EZFW7D43.js');
3166
+ const { generateProjectAiSupport, resolveSkillNamesForPackages } = await import('./cli-skill-templates-TSYEQTCY.js');
2657
3167
  const generatedFiles = await generateProjectAiSupport(cwd, resolveSkillNamesForPackages(project.haiPackages), {
2658
3168
  overwriteSkills: false,
2659
3169
  overwriteBridgeFiles: false
@@ -2797,13 +3307,26 @@ cli.command("generate [type] [name]", "\u751F\u6210\u4EE3\u7801").alias("g").opt
2797
3307
  process4.exit(1);
2798
3308
  }
2799
3309
  });
2800
- cli.command("deploy [appDir]", "\u90E8\u7F72\u5E94\u7528\u5230\u4E91\u5E73\u53F0").option("--project-name <name>", "\u9879\u76EE\u540D\u79F0\uFF08\u9ED8\u8BA4\u4ECE package.json \u8BFB\u53D6\uFF09").option("--skip-provision", "\u8DF3\u8FC7\u57FA\u7840\u8BBE\u65BD\u5F00\u901A").option("--skip-build", "\u8DF3\u8FC7\u6784\u5EFA").action(async (appDir, options) => {
3310
+ cli.command("build", "\u6784\u5EFA\u6E90\u7801\u3001Docker \u955C\u50CF\u6216 Tauri \u5B89\u88C5\u5305\uFF08\u4E0D\u533A\u5206\u90E8\u7F72\u73AF\u5883\uFF09").option("--target <target>", "\u4EA7\u7269\u7C7B\u578B\uFF1Acompile\uFF08\u9ED8\u8BA4\uFF09/ docker / desktop").option("--runtime <runtime>", "\u672C\u5730\u5BB9\u5668\u5F15\u64CE\uFF1Aauto\uFF08\u4F18\u5148 Podman\uFF09/ podman / docker").option("--network <network>", "\u5BB9\u5668\u6784\u5EFA\u7F51\u7EDC\uFF08\u9ED8\u8BA4\u5F15\u64CE\u8BBE\u7F6E\uFF09\uFF0C\u4F8B\u5982 rootless Podman \u7684 pasta").option("--platform <platform>", "Tauri \u76EE\u6807\u7CFB\u7EDF\uFF1Awindows / linux / macos\uFF08\u5728\u5BF9\u5E94\u7CFB\u7EDF\u6784\u5EFA\uFF09").option("--arch <arch>", "Tauri \u76EE\u6807\u67B6\u6784\uFF1Ax64 / arm64").option("--sqlite-prebuild-mirror <url>", "SQLite \u9884\u7F16\u8BD1\u4E8C\u8FDB\u5236\u955C\u50CF\u6839\u5730\u5740").option("--no-http-proxy", "Podman \u6784\u5EFA\u4E0D\u7EE7\u627F\u672C\u673A\u4EE3\u7406").action(async (options) => {
3311
+ try {
3312
+ await buildCommand(options);
3313
+ } catch (error) {
3314
+ writeCliMessage(error instanceof Error ? error.message : String(error));
3315
+ process4.exitCode = 1;
3316
+ }
3317
+ });
3318
+ cli.command("deploy [appDir]", "\u90E8\u7F72\u5E94\u7528\u5230\u4E91\u5E73\u53F0").option("--env <environment>", "\u90E8\u7F72\u73AF\u5883\uFF08test / prod \u7B49\uFF0C\u5FC5\u987B\u5B58\u5728\u5BF9\u5E94\u73AF\u5883\u6587\u4EF6\uFF09").option("--env-dir <path>", "\u73AF\u5883\u6587\u4EF6\u6240\u5728\u76EE\u5F55\uFF08\u9ED8\u8BA4\u81EA\u52A8\u67E5\u627E\uFF09").option("-y, --yes", "\u6309\u5F53\u524D\u540E\u7AEF\u9ED8\u8BA4\u914D\u7F6E\u90E8\u7F72\uFF0C\u76EE\u6807\u73AF\u5883\u5DF2\u6709\u5B8C\u6574\u914D\u7F6E\u65F6\u4FDD\u6301\u4E0D\u53D8").option("--project-name <name>", "\u9879\u76EE\u540D\u79F0\uFF08\u9ED8\u8BA4\u670D\u52A1\u5305\u540D\uFF0C\u7F3A\u5931\u65F6\u4E3A\u76EE\u5F55\u540D\uFF1B\u9644\u52A0\u73AF\u5883\u540E\u7F00\uFF09").option("--skip-middleware", "\u914D\u7F6E\u5DF2\u5B8C\u5907\uFF0C\u8DF3\u8FC7\u5168\u90E8\u4E2D\u95F4\u4EF6\u5904\u7406\uFF0C\u76F4\u63A5\u90E8\u7F72").option("--artifact-dir <path>", "\u5DF2\u6709\u955C\u50CF\u6784\u5EFA\u4EA7\u7269\u76EE\u5F55\uFF08\u542B manifest.json\uFF09").option("--bundle-only", "\u53EA\u751F\u6210 Compose \u90E8\u7F72 tar\uFF0C\u624B\u5DE5\u642C\u8FD0\uFF1B\u4E0D\u8FDE\u63A5 SSH").option("--output <path>", "\u90E8\u7F72 tar \u8F93\u51FA\u8DEF\u5F84\uFF0C\u81EA\u52A8\u8865\u5E94\u7528\u7248\u672C\u540E\u7F00").action(async (appDir, options) => {
2801
3319
  try {
2802
3320
  await deployCommand({
2803
3321
  appDir,
3322
+ env: options.env,
3323
+ envDir: options.envDir,
3324
+ yes: options.yes,
2804
3325
  projectName: options.projectName,
2805
- skipProvision: options.skipProvision,
2806
- skipBuild: options.skipBuild,
3326
+ skipMiddleware: options.skipMiddleware,
3327
+ artifactDir: options.artifactDir,
3328
+ bundleOnly: options.bundleOnly,
3329
+ output: options.output,
2807
3330
  verbose: options.verbose,
2808
3331
  cwd: options.cwd
2809
3332
  });
@@ -2823,7 +3346,7 @@ cli.command("g:api <name>", "\u751F\u6210 API").action(async (name, options) =>
2823
3346
  cli.command("g:model <name>", "\u751F\u6210\u6570\u636E\u6A21\u578B").action(async (name, options) => {
2824
3347
  await generate({ type: "model", name, cwd: options.cwd });
2825
3348
  });
2826
- cli.version(VERSION);
3349
+ cli.version(VERSION, "-V, --version");
2827
3350
  cli.help();
2828
3351
  var parsed = cli.parse();
2829
3352
  if (!cli.matchedCommand && !parsed.options.help && !parsed.options.version) {