@h-ai/deploy 0.1.0-alpha.55 → 0.1.0-alpha.56

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.
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { homedir } from 'os';
5
5
  import { join, dirname, relative, sep } from 'path';
6
6
  import process from 'process';
7
7
  import { parse, stringify } from 'yaml';
8
- import { execSync } from 'child_process';
9
- import { createHash } from 'crypto';
8
+ import { execSync, spawn } from 'child_process';
9
+ import { randomBytes, createHash } from 'crypto';
10
10
  import { Buffer } from 'buffer';
11
11
 
12
12
  // src/deploy-config.ts
@@ -38,7 +38,15 @@ var en_US_default = {
38
38
  deploy_fileUploadFailed: "File upload failed ({status})",
39
39
  deploy_provisionNoResult: "No connection info returned from {service}",
40
40
  deploy_recoveryFailure: "Deployment stopped at {stage}; inspect the recovery details.",
41
- deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use."
41
+ deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
42
+ deploy_commandTimeout: "Command timed out: {command}",
43
+ deploy_commandFailed: "Command failed ({command}): {error}",
44
+ deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
45
+ deploy_containerBuildFailed: "Container image build failed: {error}",
46
+ deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
47
+ deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
48
+ deploy_imageTransferFailed: "Image transfer failed: {error}",
49
+ deploy_healthCheckFailed: "Health check failed, app not ready at {url}"
42
50
  };
43
51
 
44
52
  // messages/zh-CN.json
@@ -68,7 +76,15 @@ var zh_CN_default = {
68
76
  deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09",
69
77
  deploy_provisionNoResult: "{service} \u672A\u8FD4\u56DE\u8FDE\u63A5\u4FE1\u606F",
70
78
  deploy_recoveryFailure: "\u90E8\u7F72\u5728 {stage} \u9636\u6BB5\u505C\u6B62\uFF0C\u8BF7\u67E5\u770B\u6062\u590D\u6E05\u5355\u3002",
71
- deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002"
79
+ deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
80
+ deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
81
+ deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
82
+ deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
83
+ deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
84
+ deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
85
+ deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
86
+ deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
87
+ deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA"
72
88
  };
73
89
 
74
90
  // src/deploy-i18n.ts
@@ -82,8 +98,47 @@ var VercelProviderConfigSchema = z.object({
82
98
  type: z.literal("vercel"),
83
99
  token: z.string().min(1, deployM("deploy_configTokenRequired"))
84
100
  });
101
+ var SshConfigSchema = z.object({
102
+ /** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
103
+ host: z.string().min(1),
104
+ /** SSH 端口 */
105
+ port: z.number().int().min(1).max(65535).default(22),
106
+ /** SSH 用户名 */
107
+ username: z.string().min(1),
108
+ /** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
109
+ identityFile: z.string().min(1).optional(),
110
+ /** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
111
+ knownHostsFile: z.string().min(1).optional(),
112
+ /** 严格主机密钥校验,默认开启(true→accept-new 首次信任并拒绝变更;false→关闭校验) */
113
+ strictHostKeyChecking: z.boolean().default(true),
114
+ /** 连接超时(秒) */
115
+ connectTimeoutSeconds: z.number().int().positive().default(10)
116
+ });
117
+ var PortExposeSchema = z.object({
118
+ type: z.literal("port").default("port"),
119
+ /** 容器内监听端口(adapter-node 默认 3000) */
120
+ containerPort: z.number().int().positive().default(3e3),
121
+ /** 宿主机映射端口 */
122
+ hostPort: z.number().int().positive()
123
+ });
124
+ var ExposeSchema = PortExposeSchema;
125
+ var DockerSshProviderConfigSchema = z.object({
126
+ type: z.literal("docker-ssh"),
127
+ /** SSH 连接参数 */
128
+ ssh: SshConfigSchema,
129
+ /** 远程主机运行参数 */
130
+ remote: z.object({
131
+ /** 远程应用根目录 */
132
+ baseDir: z.string().min(1).default("/opt/hai/apps"),
133
+ /** 远程容器运行时(auto 优先 podman) */
134
+ runtime: z.enum(["auto", "docker", "podman"]).default("auto")
135
+ }).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
136
+ /** 应用暴露方式 */
137
+ expose: ExposeSchema
138
+ });
85
139
  var ProviderConfigSchema = z.discriminatedUnion("type", [
86
- VercelProviderConfigSchema
140
+ VercelProviderConfigSchema,
141
+ DockerSshProviderConfigSchema
87
142
  ]);
88
143
  var NeonServiceSchema = z.object({
89
144
  provisioner: z.literal("neon"),
@@ -121,7 +176,11 @@ var ServicesConfigSchema = z.object({
121
176
  }).optional();
122
177
  var DeployConfigSchema = z.object({
123
178
  provider: ProviderConfigSchema,
124
- services: ServicesConfigSchema
179
+ services: ServicesConfigSchema,
180
+ /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
181
+ container: z.object({
182
+ runtime: z.enum(["auto", "docker", "podman"]).default("auto")
183
+ }).optional()
125
184
  });
126
185
  var DeployErrorInfo = {
127
186
  DEPLOY_FAILED: "001:500",
@@ -137,7 +196,13 @@ var DeployErrorInfo = {
137
196
  ENV_VAR_FAILED: "011:500",
138
197
  UNSUPPORTED_TYPE: "012:400",
139
198
  CONFIG_ERROR: "013:500",
140
- CREDENTIAL_ERROR: "014:500"
199
+ CREDENTIAL_ERROR: "014:500",
200
+ CONTAINER_RUNTIME_NOT_FOUND: "015:500",
201
+ CONTAINER_BUILD_FAILED: "016:500",
202
+ REMOTE_CONNECT_FAILED: "017:500",
203
+ REMOTE_COMMAND_FAILED: "018:500",
204
+ IMAGE_TRANSFER_FAILED: "019:500",
205
+ HEALTH_CHECK_FAILED: "020:500"
141
206
  };
142
207
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
143
208
 
@@ -355,55 +420,61 @@ function hasDependency(pkg, name) {
355
420
  }
356
421
  function detectRequiredServices(configDir) {
357
422
  const services = [];
423
+ const backends = {};
358
424
  if (!existsSync(configDir)) {
359
- return services;
425
+ return { services, backends };
360
426
  }
361
427
  const files = readdirSync(configDir);
362
- if (files.includes("_db.yml") || files.includes("_db.yaml")) {
363
- const dbConfig = readYaml(join(configDir, files.find((f) => f.startsWith("_db."))));
428
+ const dbFile = files.find((f) => f.startsWith("_db."));
429
+ if (dbFile) {
430
+ const dbConfig = readYaml(join(configDir, dbFile));
364
431
  if (dbConfig !== null) {
365
432
  services.push("db");
433
+ backends.db = dbConfig.type ?? "postgresql";
366
434
  }
367
435
  }
368
- if (files.includes("_cache.yml") || files.includes("_cache.yaml")) {
369
- const cacheConfig = readYaml(join(configDir, files.find((f) => f.startsWith("_cache."))));
370
- if (cacheConfig !== null) {
371
- const cacheType = cacheConfig.type;
372
- if (cacheType === "redis") {
373
- services.push("cache");
374
- }
436
+ const cacheFile = files.find((f) => f.startsWith("_cache."));
437
+ if (cacheFile) {
438
+ const cacheConfig = readYaml(join(configDir, cacheFile));
439
+ if (cacheConfig !== null && cacheConfig.type === "redis") {
440
+ services.push("cache");
441
+ backends.cache = "redis";
442
+ }
443
+ }
444
+ const storageFile = files.find((f) => f.startsWith("_storage."));
445
+ if (storageFile) {
446
+ const storageConfig = readYaml(join(configDir, storageFile));
447
+ if (storageConfig !== null && detectStorageIsS3(storageConfig)) {
448
+ services.push("storage");
449
+ backends.storage = "s3";
375
450
  }
376
451
  }
377
- if (files.includes("_storage.yml") || files.includes("_storage.yaml")) {
378
- const storageConfig = readYaml(join(configDir, files.find((f) => f.startsWith("_storage."))));
379
- if (storageConfig !== null) {
380
- detectStorageServices(storageConfig, services);
452
+ const vecdbFile = files.find((f) => f.startsWith("_vecdb."));
453
+ if (vecdbFile) {
454
+ const vecdbConfig = readYaml(join(configDir, vecdbFile));
455
+ if (vecdbConfig !== null) {
456
+ services.push("vecdb");
457
+ backends.vecdb = vecdbConfig.type ?? "qdrant";
381
458
  }
382
459
  }
383
- if (files.includes("_reach.yml") || files.includes("_reach.yaml")) {
384
- const reachConfig = readYaml(join(configDir, files.find((f) => f.startsWith("_reach."))));
460
+ const reachFile = files.find((f) => f.startsWith("_reach."));
461
+ if (reachFile) {
462
+ const reachConfig = readYaml(join(configDir, reachFile));
385
463
  if (reachConfig !== null) {
386
464
  detectReachServices(reachConfig, services);
387
465
  }
388
466
  }
389
- return services;
467
+ return { services, backends };
390
468
  }
391
- function detectStorageServices(config, services) {
469
+ function detectStorageIsS3(config) {
392
470
  if (config.type === "s3") {
393
- services.push("storage");
394
- return;
471
+ return true;
395
472
  }
396
473
  const providers = config.providers;
397
474
  if (providers === void 0) {
398
- return;
399
- }
400
- for (const providerConfig of Object.values(providers)) {
401
- const typed = providerConfig;
402
- if (typed?.type === "s3") {
403
- services.push("storage");
404
- return;
405
- }
475
+ return false;
406
476
  }
477
+ return Object.values(providers).some((p) => p?.type === "s3");
407
478
  }
408
479
  function detectReachServices(config, services) {
409
480
  const providers = config.providers;
@@ -437,24 +508,29 @@ async function scanApp(appDir) {
437
508
  const appName = extractAppName(pkg);
438
509
  const isSvelteKit = existsSync(join(appDir, "svelte.config.js")) || existsSync(join(appDir, "svelte.config.ts"));
439
510
  const adapterInstalled = hasDependency(pkg, "@sveltejs/adapter-vercel");
511
+ const nodeAdapterInstalled = hasDependency(pkg, "@sveltejs/adapter-node");
440
512
  const configDir = join(appDir, "config");
441
- const configServices = detectRequiredServices(configDir);
513
+ const configDetection = detectRequiredServices(configDir);
442
514
  const depServices = detectServicesFromDependencies(pkg);
443
- const requiredServices = Array.from(/* @__PURE__ */ new Set([...configServices, ...depServices]));
515
+ const requiredServices = Array.from(/* @__PURE__ */ new Set([...configDetection.services, ...depServices]));
444
516
  const scripts = pkg.scripts;
445
517
  const buildCommand = scripts?.build ?? "pnpm build";
446
518
  const scanResult = {
447
519
  appName,
448
520
  isSvelteKit,
449
521
  adapterInstalled,
522
+ nodeAdapterInstalled,
450
523
  requiredServices,
524
+ serviceBackends: configDetection.backends,
451
525
  buildCommand
452
526
  };
453
527
  logger3.info("Application scanned", {
454
528
  appName,
455
529
  isSvelteKit,
456
530
  adapterInstalled,
457
- requiredServices
531
+ nodeAdapterInstalled,
532
+ requiredServices,
533
+ serviceBackends: configDetection.backends
458
534
  });
459
535
  return ok(scanResult);
460
536
  } catch (error) {
@@ -472,7 +548,8 @@ function detectServicesFromDependencies(pkg) {
472
548
  const depMap = {
473
549
  "@h-ai/reldb": "db",
474
550
  "@h-ai/cache": "cache",
475
- "@h-ai/storage": "storage"
551
+ "@h-ai/storage": "storage",
552
+ "@h-ai/vecdb": "vecdb"
476
553
  };
477
554
  const services = [];
478
555
  for (const [depName, serviceType] of Object.entries(depMap)) {
@@ -490,7 +567,667 @@ function extractAppName(pkg) {
490
567
  }
491
568
  return name;
492
569
  }
493
- var logger4 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
570
+ var IMAGES = {
571
+ postgres: "postgres:16-alpine",
572
+ mysql: "mysql:8.4",
573
+ redis: "redis:7-alpine",
574
+ minio: "minio/minio:RELEASE.2025-09-07T16-13-09Z",
575
+ mc: "minio/mc:RELEASE.2025-08-13T08-35-41Z",
576
+ qdrant: "qdrant/qdrant:v1.12.4",
577
+ pgvector: "pgvector/pgvector:pg17",
578
+ chroma: "chromadb/chroma:0.5.23"
579
+ };
580
+ function generatePassword() {
581
+ return randomBytes(24).toString("base64url");
582
+ }
583
+ function resolveSidecarSecrets(appDir, projectName) {
584
+ const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
585
+ if (existsSync(secretFile)) {
586
+ try {
587
+ return JSON.parse(readFileSync(secretFile, "utf-8"));
588
+ } catch {
589
+ }
590
+ }
591
+ const secrets = {
592
+ dbPassword: generatePassword(),
593
+ storagePassword: generatePassword()
594
+ };
595
+ mkdirSync(dirname(secretFile), { recursive: true });
596
+ writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
597
+ return secrets;
598
+ }
599
+ function addAppDataVolume(plan, projectName) {
600
+ const mount = `${projectName}-data:/app/data`;
601
+ if (!plan.appVolumes.includes(mount)) {
602
+ plan.appVolumes.push(mount);
603
+ plan.volumes[`${projectName}-data`] = null;
604
+ }
605
+ }
606
+ function planPostgresDb(plan, projectName, password, net) {
607
+ plan.services.push("db");
608
+ plan.dependsOn.push("db");
609
+ plan.composeServices.db = {
610
+ image: IMAGES.postgres,
611
+ container_name: `hai-${projectName}-db`,
612
+ restart: "unless-stopped",
613
+ env_file: [".env.runtime"],
614
+ volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
615
+ networks: net
616
+ };
617
+ plan.volumes[`${projectName}-db-data`] = null;
618
+ plan.env.POSTGRES_USER = "hai";
619
+ plan.env.POSTGRES_PASSWORD = password;
620
+ plan.env.POSTGRES_DB = "hai";
621
+ plan.env.HAI_DB = JSON.stringify({ type: "postgresql", url: `postgresql://hai:${password}@db:5432/hai`, database: "hai" });
622
+ plan.secrets.push(password);
623
+ }
624
+ function planMysqlDb(plan, projectName, password, net) {
625
+ plan.services.push("db");
626
+ plan.dependsOn.push("db");
627
+ plan.composeServices.db = {
628
+ image: IMAGES.mysql,
629
+ container_name: `hai-${projectName}-db`,
630
+ restart: "unless-stopped",
631
+ env_file: [".env.runtime"],
632
+ volumes: [`${projectName}-db-data:/var/lib/mysql`],
633
+ networks: net
634
+ };
635
+ plan.volumes[`${projectName}-db-data`] = null;
636
+ plan.env.MYSQL_ROOT_PASSWORD = password;
637
+ plan.env.MYSQL_DATABASE = "hai";
638
+ plan.env.MYSQL_USER = "hai";
639
+ plan.env.MYSQL_PASSWORD = password;
640
+ plan.env.HAI_DB = JSON.stringify({ type: "mysql", url: `mysql://hai:${password}@db:3306/hai`, database: "hai" });
641
+ plan.secrets.push(password);
642
+ }
643
+ function planRedisCache(plan, projectName, net) {
644
+ plan.services.push("cache");
645
+ plan.dependsOn.push("cache");
646
+ plan.composeServices.cache = {
647
+ image: IMAGES.redis,
648
+ container_name: `hai-${projectName}-cache`,
649
+ restart: "unless-stopped",
650
+ networks: net
651
+ };
652
+ plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
653
+ }
654
+ function planMinioStorage(plan, projectName, password, net) {
655
+ plan.services.push("storage");
656
+ plan.dependsOn.push("storage");
657
+ plan.composeServices.storage = {
658
+ image: IMAGES.minio,
659
+ container_name: `hai-${projectName}-storage`,
660
+ restart: "unless-stopped",
661
+ command: ["server", "/data"],
662
+ env_file: [".env.runtime"],
663
+ volumes: [`${projectName}-storage-data:/data`],
664
+ networks: net
665
+ };
666
+ plan.composeServices["storage-init"] = {
667
+ image: IMAGES.mc,
668
+ depends_on: ["storage"],
669
+ env_file: [".env.runtime"],
670
+ entrypoint: [
671
+ "sh",
672
+ "-c",
673
+ 'until mc alias set s3 http://storage:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do sleep 1; done; mc mb -p s3/hai; exit 0'
674
+ ],
675
+ networks: net
676
+ };
677
+ plan.volumes[`${projectName}-storage-data`] = null;
678
+ plan.env.MINIO_ROOT_USER = "hai";
679
+ plan.env.MINIO_ROOT_PASSWORD = password;
680
+ plan.env.HAI_STORAGE = JSON.stringify({
681
+ type: "s3",
682
+ endpoint: "http://storage:9000",
683
+ bucket: "hai",
684
+ region: "us-east-1",
685
+ accessKeyId: "hai",
686
+ secretAccessKey: password,
687
+ forcePathStyle: true
688
+ });
689
+ plan.secrets.push(password);
690
+ }
691
+ function planQdrantVecdb(plan, projectName, net) {
692
+ plan.services.push("vecdb");
693
+ plan.dependsOn.push("vecdb");
694
+ plan.composeServices.vecdb = {
695
+ image: IMAGES.qdrant,
696
+ container_name: `hai-${projectName}-vecdb`,
697
+ restart: "unless-stopped",
698
+ volumes: [`${projectName}-vecdb-data:/qdrant/storage`],
699
+ networks: net
700
+ };
701
+ plan.volumes[`${projectName}-vecdb-data`] = null;
702
+ plan.env.HAI_VECDB = JSON.stringify({ type: "qdrant", url: "http://vecdb:6333" });
703
+ }
704
+ function planPgvectorVecdb(plan, projectName, password, net) {
705
+ plan.services.push("vecdb");
706
+ plan.dependsOn.push("vecdb");
707
+ plan.composeServices.vecdb = {
708
+ image: IMAGES.pgvector,
709
+ container_name: `hai-${projectName}-vecdb`,
710
+ restart: "unless-stopped",
711
+ env_file: [".env.runtime"],
712
+ volumes: [`${projectName}-vecdb-data:/var/lib/postgresql/data`],
713
+ networks: net
714
+ };
715
+ plan.volumes[`${projectName}-vecdb-data`] = null;
716
+ plan.env.POSTGRES_USER = "hai";
717
+ plan.env.POSTGRES_PASSWORD = password;
718
+ plan.env.POSTGRES_DB = "hai";
719
+ plan.env.HAI_VECDB = JSON.stringify({ type: "pgvector", url: `postgresql://hai:${password}@vecdb:5432/hai`, database: "hai" });
720
+ plan.secrets.push(password);
721
+ }
722
+ function planChromaVecdb(plan, projectName, net) {
723
+ plan.services.push("vecdb");
724
+ plan.dependsOn.push("vecdb");
725
+ plan.composeServices.vecdb = {
726
+ image: IMAGES.chroma,
727
+ container_name: `hai-${projectName}-vecdb`,
728
+ restart: "unless-stopped",
729
+ volumes: [`${projectName}-vecdb-data:/data`],
730
+ networks: net
731
+ };
732
+ plan.volumes[`${projectName}-vecdb-data`] = null;
733
+ plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
734
+ }
735
+ function planSidecars(appDir, projectName, scan, provisionEnv) {
736
+ const plan = {
737
+ services: [],
738
+ composeServices: {},
739
+ volumes: {},
740
+ appVolumes: [],
741
+ env: {},
742
+ dependsOn: [],
743
+ secrets: []
744
+ };
745
+ const secrets = resolveSidecarSecrets(appDir, projectName);
746
+ const net = ["hai-app"];
747
+ const backends = scan.serviceBackends;
748
+ for (const service of scan.requiredServices) {
749
+ if (service === "db" && !provisionEnv.HAI_DB) {
750
+ const backend = backends.db ?? "postgresql";
751
+ if (backend === "mysql")
752
+ planMysqlDb(plan, projectName, secrets.dbPassword, net);
753
+ else if (backend === "sqlite")
754
+ addAppDataVolume(plan, projectName);
755
+ else
756
+ planPostgresDb(plan, projectName, secrets.dbPassword, net);
757
+ } else if (service === "cache" && !provisionEnv.HAI_CACHE) {
758
+ planRedisCache(plan, projectName, net);
759
+ } else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
760
+ planMinioStorage(plan, projectName, secrets.storagePassword, net);
761
+ } else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
762
+ const backend = backends.vecdb ?? "qdrant";
763
+ if (backend === "pgvector")
764
+ planPgvectorVecdb(plan, projectName, secrets.dbPassword, net);
765
+ else if (backend === "chroma")
766
+ planChromaVecdb(plan, projectName, net);
767
+ else if (backend === "lancedb")
768
+ addAppDataVolume(plan, projectName);
769
+ else
770
+ planQdrantVecdb(plan, projectName, net);
771
+ }
772
+ }
773
+ return plan;
774
+ }
775
+ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort) {
776
+ const sidecar = planSidecars(appDir, projectName, scan, provisionEnv);
777
+ const env = {
778
+ NODE_ENV: "production",
779
+ HOST: "0.0.0.0",
780
+ PORT: String(containerPort),
781
+ ...sidecar.env,
782
+ ...provisionEnv
783
+ };
784
+ return { env, sidecar };
785
+ }
786
+ function generateComposeFile(options) {
787
+ const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
788
+ const app = {
789
+ image: imageRef,
790
+ container_name: `hai-${projectName}-app`,
791
+ restart: "unless-stopped",
792
+ // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
793
+ pull_policy: "never",
794
+ env_file: [".env.runtime"],
795
+ ports: [`${hostPort}:${containerPort}`],
796
+ networks: ["hai-app"]
797
+ };
798
+ if (sidecar.appVolumes.length > 0)
799
+ app.volumes = sidecar.appVolumes;
800
+ if (sidecar.dependsOn.length > 0)
801
+ app.depends_on = sidecar.dependsOn;
802
+ const compose = {
803
+ services: {
804
+ app,
805
+ ...sidecar.composeServices
806
+ },
807
+ networks: {
808
+ "hai-app": { name: `hai-${projectName}` }
809
+ }
810
+ };
811
+ if (Object.keys(sidecar.volumes).length > 0)
812
+ compose.volumes = sidecar.volumes;
813
+ return stringify(compose);
814
+ }
815
+ function serializeEnvFile(env) {
816
+ return `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n")}
817
+ `;
818
+ }
819
+ function generateDockerignore() {
820
+ return `**/node_modules
821
+ **/dist
822
+ **/build
823
+ **/.turbo
824
+ **/.svelte-kit
825
+ **/coverage
826
+ **/test-results
827
+ **/playwright-report
828
+ **/.hai/deploy
829
+ .git
830
+ `;
831
+ }
832
+ function generateFallbackDockerfile(packageName, containerPort) {
833
+ return `# syntax=docker/dockerfile:1.7
834
+ FROM node:22-bookworm-slim AS build
835
+ ENV PNPM_HOME=/pnpm
836
+ ENV PATH=$PNPM_HOME:$PATH
837
+ ENV CI=true
838
+ WORKDIR /workspace
839
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
840
+ COPY . .
841
+ RUN pnpm install --frozen-lockfile --ignore-scripts
842
+ RUN pnpm --filter ${packageName} build
843
+ RUN pnpm --filter ${packageName} --prod deploy /prod/app
844
+
845
+ FROM node:22-bookworm-slim AS runtime
846
+ ENV NODE_ENV=production
847
+ ENV HOST=0.0.0.0
848
+ ENV PORT=${containerPort}
849
+ WORKDIR /app
850
+ COPY --from=build --chown=node:node /prod/app ./
851
+ USER node
852
+ EXPOSE ${containerPort}
853
+ CMD ["node", "build"]
854
+ `;
855
+ }
856
+ var logger4 = core.logger.child({ module: "deploy", scope: "command" });
857
+ function maskSecrets(text, secrets) {
858
+ if (!secrets || secrets.length === 0)
859
+ return text;
860
+ let masked = text;
861
+ for (const secret of secrets) {
862
+ if (secret && secret.length > 0)
863
+ masked = masked.split(secret).join("***");
864
+ }
865
+ return masked;
866
+ }
867
+ function runCommand(command, args, options = {}) {
868
+ return new Promise((resolve) => {
869
+ const child = spawn(command, args, {
870
+ cwd: options.cwd,
871
+ env: options.env ? { ...process.env, ...options.env } : process.env,
872
+ shell: false
873
+ });
874
+ let stdout = "";
875
+ let stderr = "";
876
+ let settled = false;
877
+ let timer;
878
+ const finish = (result) => {
879
+ if (settled)
880
+ return;
881
+ settled = true;
882
+ if (timer)
883
+ clearTimeout(timer);
884
+ resolve(result);
885
+ };
886
+ if (options.timeoutMs && options.timeoutMs > 0) {
887
+ timer = setTimeout(() => {
888
+ child.kill("SIGKILL");
889
+ logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
890
+ finish(err(
891
+ HaiDeployError.REMOTE_COMMAND_FAILED,
892
+ deployM("deploy_commandTimeout", { params: { command } })
893
+ ));
894
+ }, options.timeoutMs);
895
+ }
896
+ child.stdout.on("data", (chunk) => {
897
+ stdout += chunk.toString("utf-8");
898
+ });
899
+ child.stderr.on("data", (chunk) => {
900
+ stderr += chunk.toString("utf-8");
901
+ });
902
+ child.on("error", (error) => {
903
+ logger4.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
904
+ finish(err(
905
+ HaiDeployError.REMOTE_COMMAND_FAILED,
906
+ deployM("deploy_commandFailed", {
907
+ params: { command, error: maskSecrets(error.message, options.secrets) }
908
+ }),
909
+ error
910
+ ));
911
+ });
912
+ child.on("close", (code) => {
913
+ const exitCode = code ?? -1;
914
+ if (exitCode === 0) {
915
+ finish(ok({ code: exitCode, stdout, stderr }));
916
+ return;
917
+ }
918
+ logger4.error("Command exited with non-zero code", {
919
+ command,
920
+ code: exitCode,
921
+ stderr: maskSecrets(stderr, options.secrets).slice(-500)
922
+ });
923
+ finish(err(
924
+ HaiDeployError.REMOTE_COMMAND_FAILED,
925
+ deployM("deploy_commandFailed", {
926
+ params: { command, error: maskSecrets(stderr || `exit ${exitCode}`, options.secrets).slice(-500) }
927
+ })
928
+ ));
929
+ });
930
+ if (options.input !== void 0) {
931
+ child.stdin.write(options.input);
932
+ child.stdin.end();
933
+ } else {
934
+ child.stdin.end();
935
+ }
936
+ });
937
+ }
938
+
939
+ // src/container/deploy-container-runtime.ts
940
+ var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
941
+ async function detectLocalRuntime(preferred = "auto") {
942
+ const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
943
+ for (const runtime of candidates) {
944
+ const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
945
+ if (probe.success) {
946
+ logger5.info("Local container runtime detected", { runtime });
947
+ return ok(runtime);
948
+ }
949
+ }
950
+ return err(
951
+ HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
952
+ deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
953
+ );
954
+ }
955
+ async function buildImage(runtime, options) {
956
+ const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
957
+ if (runtime === "podman")
958
+ args.push("--format", "docker", "--jobs", "1");
959
+ args.push(options.contextDir);
960
+ logger5.info("Building container image", { runtime, imageRef: options.imageRef });
961
+ const result = await runCommand(runtime, args, { cwd: options.contextDir });
962
+ if (!result.success) {
963
+ return err(
964
+ HaiDeployError.CONTAINER_BUILD_FAILED,
965
+ deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
966
+ result.error
967
+ );
968
+ }
969
+ return ok(void 0);
970
+ }
971
+ async function saveImage(runtime, imageRef, tarPath) {
972
+ logger5.info("Saving container image", { runtime, imageRef, tarPath });
973
+ const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
974
+ if (!result.success) {
975
+ return err(
976
+ HaiDeployError.CONTAINER_BUILD_FAILED,
977
+ deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
978
+ result.error
979
+ );
980
+ }
981
+ return ok(void 0);
982
+ }
983
+ var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
984
+ function buildConnectOptions(config) {
985
+ const args = ["-o", "BatchMode=yes"];
986
+ args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
987
+ args.push("-o", `StrictHostKeyChecking=${config.strictHostKeyChecking ? "accept-new" : "no"}`);
988
+ if (config.knownHostsFile)
989
+ args.push("-o", `UserKnownHostsFile=${config.knownHostsFile}`);
990
+ if (config.identityFile) {
991
+ args.push("-i", config.identityFile);
992
+ args.push("-o", "IdentitiesOnly=yes");
993
+ }
994
+ return args;
995
+ }
996
+ function buildSshExecArgs(config, remoteCommand) {
997
+ return [
998
+ ...buildConnectOptions(config),
999
+ "-p",
1000
+ String(config.port),
1001
+ `${config.username}@${config.host}`,
1002
+ remoteCommand
1003
+ ];
1004
+ }
1005
+ function buildScpUploadArgs(config, localPath, remotePath) {
1006
+ return [
1007
+ ...buildConnectOptions(config),
1008
+ "-P",
1009
+ String(config.port),
1010
+ localPath,
1011
+ `${config.username}@${config.host}:${remotePath}`
1012
+ ];
1013
+ }
1014
+ function createSshExecutor(config) {
1015
+ const exec = async (remoteCommand, options) => {
1016
+ return runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
1017
+ timeoutMs: options?.timeoutMs,
1018
+ secrets: options?.secrets
1019
+ });
1020
+ };
1021
+ const upload = async (localPath, remotePath) => {
1022
+ const args = buildScpUploadArgs(config, localPath, remotePath);
1023
+ const result = await runCommand("scp", args, {});
1024
+ if (!result.success) {
1025
+ return err(
1026
+ HaiDeployError.IMAGE_TRANSFER_FAILED,
1027
+ deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
1028
+ result.error
1029
+ );
1030
+ }
1031
+ return ok(void 0);
1032
+ };
1033
+ const ping = async () => {
1034
+ logger6.debug("SSH preflight", { host: config.host, port: config.port });
1035
+ const result = await exec("true", { timeoutMs: 2e4 });
1036
+ if (!result.success) {
1037
+ return err(
1038
+ HaiDeployError.REMOTE_CONNECT_FAILED,
1039
+ deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }),
1040
+ result.error
1041
+ );
1042
+ }
1043
+ return ok(void 0);
1044
+ };
1045
+ return { exec, upload, ping };
1046
+ }
1047
+
1048
+ // src/providers/deploy-provider-docker-ssh.ts
1049
+ var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1050
+ var HEALTH_POLL_INTERVAL = 3e3;
1051
+ var HEALTH_MAX_ATTEMPTS = 40;
1052
+ function makeDeploymentId() {
1053
+ const now = /* @__PURE__ */ new Date();
1054
+ const pad = (n) => String(n).padStart(2, "0");
1055
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
1056
+ return `${stamp}-${randomBytes(3).toString("hex")}`;
1057
+ }
1058
+ function parseLoadedImageRef(stdout, fallback) {
1059
+ const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
1060
+ return match ? match[1] : fallback;
1061
+ }
1062
+ function sleep(ms) {
1063
+ return new Promise((resolve) => setTimeout(resolve, ms));
1064
+ }
1065
+ function readPackageName(appDir) {
1066
+ try {
1067
+ const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
1068
+ return pkg.name ?? "app";
1069
+ } catch {
1070
+ return "app";
1071
+ }
1072
+ }
1073
+ function resolveWorkspaceRoot(appDir) {
1074
+ if (existsSync(join(appDir, "pnpm-workspace.yaml")))
1075
+ return appDir;
1076
+ let current = appDir;
1077
+ let parent = dirname(current);
1078
+ while (parent !== current) {
1079
+ if (existsSync(join(parent, "pnpm-workspace.yaml")))
1080
+ return parent;
1081
+ current = parent;
1082
+ parent = dirname(parent);
1083
+ }
1084
+ return appDir;
1085
+ }
1086
+ function createDockerSshProvider(config, localRuntimePref = "auto") {
1087
+ const ssh = createSshExecutor(config.ssh);
1088
+ const baseDir = config.remote.baseDir;
1089
+ async function detectRemoteRuntime() {
1090
+ const candidates = config.remote.runtime === "auto" ? ["podman", "docker"] : [config.remote.runtime];
1091
+ for (const runtime of candidates) {
1092
+ const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
1093
+ if (!versionResult.success)
1094
+ continue;
1095
+ const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
1096
+ if (composeResult.success) {
1097
+ logger7.info("Remote container runtime detected", { runtime });
1098
+ return ok(runtime);
1099
+ }
1100
+ }
1101
+ return err(
1102
+ HaiDeployError.REMOTE_COMMAND_FAILED,
1103
+ deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
1104
+ );
1105
+ }
1106
+ async function healthCheck(url) {
1107
+ for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
1108
+ try {
1109
+ const response = await fetch(url, { signal: AbortSignal.timeout(5e3) });
1110
+ if (response.ok)
1111
+ return true;
1112
+ } catch {
1113
+ }
1114
+ await sleep(HEALTH_POLL_INTERVAL);
1115
+ }
1116
+ return false;
1117
+ }
1118
+ const deploy2 = async (context) => {
1119
+ const { appDir, projectName, scan, provisionEnv } = context;
1120
+ const deploymentId = makeDeploymentId();
1121
+ const containerPort = config.expose.containerPort;
1122
+ const hostPort = config.expose.hostPort;
1123
+ const localImageTag = `hai-${projectName}:${deploymentId}`;
1124
+ const runtimeResult = await detectLocalRuntime(localRuntimePref);
1125
+ if (!runtimeResult.success)
1126
+ return err(runtimeResult.error);
1127
+ const localRuntime = runtimeResult.data;
1128
+ const artifactDir = join(appDir, ".hai", "deploy");
1129
+ mkdirSync(artifactDir, { recursive: true });
1130
+ const appDockerfile = join(appDir, "Dockerfile");
1131
+ let dockerfilePath;
1132
+ let contextDir;
1133
+ if (existsSync(appDockerfile)) {
1134
+ dockerfilePath = appDockerfile;
1135
+ contextDir = appDir;
1136
+ } else {
1137
+ dockerfilePath = join(artifactDir, "Dockerfile");
1138
+ writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort));
1139
+ writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
1140
+ contextDir = resolveWorkspaceRoot(appDir);
1141
+ logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
1142
+ }
1143
+ const buildResult = await buildImage(localRuntime, {
1144
+ contextDir,
1145
+ dockerfilePath,
1146
+ imageRef: localImageTag
1147
+ });
1148
+ if (!buildResult.success)
1149
+ return err(buildResult.error);
1150
+ const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort);
1151
+ const envPath = join(artifactDir, ".env.runtime");
1152
+ writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
1153
+ const tarPath = join(artifactDir, `${projectName}.tar`);
1154
+ const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
1155
+ if (!saveResult.success)
1156
+ return err(saveResult.error);
1157
+ const pingResult = await ssh.ping();
1158
+ if (!pingResult.success)
1159
+ return err(pingResult.error);
1160
+ const remoteRuntimeResult = await detectRemoteRuntime();
1161
+ if (!remoteRuntimeResult.success)
1162
+ return err(remoteRuntimeResult.error);
1163
+ const remoteRuntime = remoteRuntimeResult.data;
1164
+ const remoteDir = `${baseDir}/${projectName}`;
1165
+ const prepareResult = await ssh.exec(`mkdir -p ${remoteDir} && chmod 700 ${remoteDir}`);
1166
+ if (!prepareResult.success)
1167
+ return err(prepareResult.error);
1168
+ const remoteTar = `${remoteDir}/image.tar`;
1169
+ const uploadTarResult = await ssh.upload(tarPath, remoteTar);
1170
+ if (!uploadTarResult.success)
1171
+ return err(uploadTarResult.error);
1172
+ const loadResult = await ssh.exec(`${remoteRuntime} load -i ${remoteTar}`, { timeoutMs: 3e5 });
1173
+ if (!loadResult.success)
1174
+ return err(loadResult.error);
1175
+ const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
1176
+ const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
1177
+ if (loadedRef !== remoteImageRef) {
1178
+ const tagResult = await ssh.exec(`${remoteRuntime} tag ${loadedRef} ${remoteImageRef}`);
1179
+ if (!tagResult.success)
1180
+ return err(tagResult.error);
1181
+ }
1182
+ const composeContent = generateComposeFile({
1183
+ projectName,
1184
+ imageRef: remoteImageRef,
1185
+ hostPort,
1186
+ containerPort,
1187
+ sidecar
1188
+ });
1189
+ const composePath = join(artifactDir, "compose.yml");
1190
+ writeFileSync(composePath, composeContent);
1191
+ const uploadComposeResult = await ssh.upload(composePath, `${remoteDir}/compose.yml`);
1192
+ if (!uploadComposeResult.success)
1193
+ return err(uploadComposeResult.error);
1194
+ const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
1195
+ if (!uploadEnvResult.success)
1196
+ return err(uploadEnvResult.error);
1197
+ const secretMask = sidecar.secrets;
1198
+ const chmodResult = await ssh.exec(`chmod 600 ${remoteDir}/.env.runtime`, { secrets: secretMask });
1199
+ if (!chmodResult.success)
1200
+ return err(chmodResult.error);
1201
+ const upResult = await ssh.exec(
1202
+ `cd ${remoteDir} && ${remoteRuntime} compose -p hai-${projectName} up -d`,
1203
+ { timeoutMs: 3e5, secrets: secretMask }
1204
+ );
1205
+ if (!upResult.success)
1206
+ return err(upResult.error);
1207
+ await ssh.exec(`rm -f ${remoteTar}`);
1208
+ const url = `http://${config.ssh.host}:${hostPort}`;
1209
+ logger7.info("Running health check", { url });
1210
+ const healthy = await healthCheck(`${url}/`);
1211
+ if (!healthy) {
1212
+ return err(
1213
+ HaiDeployError.HEALTH_CHECK_FAILED,
1214
+ deployM("deploy_healthCheckFailed", { params: { url } })
1215
+ );
1216
+ }
1217
+ const result = {
1218
+ url,
1219
+ deploymentId,
1220
+ status: "ready",
1221
+ provider: "docker-ssh",
1222
+ healthCheck: "passed",
1223
+ envVarsSet: Object.keys(env)
1224
+ };
1225
+ logger7.info("docker-ssh deployment complete", { url, deploymentId });
1226
+ return ok(result);
1227
+ };
1228
+ return { name: "docker-ssh", deploy: deploy2 };
1229
+ }
1230
+ var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
494
1231
  var VERCEL_API = "https://api.vercel.com";
495
1232
  var POLL_INTERVAL = 3e3;
496
1233
  var MAX_POLL_ATTEMPTS = 60;
@@ -542,7 +1279,7 @@ function createVercelProvider() {
542
1279
  return {
543
1280
  name: "vercel",
544
1281
  async authenticate(apiToken) {
545
- logger4.debug("Authenticating with Vercel");
1282
+ logger8.debug("Authenticating with Vercel");
546
1283
  try {
547
1284
  const user = await vercelFetch(
548
1285
  apiToken,
@@ -550,10 +1287,10 @@ function createVercelProvider() {
550
1287
  );
551
1288
  token = apiToken;
552
1289
  const username = user.user.username || user.user.email;
553
- logger4.info("Vercel authenticated", { user: username });
1290
+ logger8.info("Vercel authenticated", { user: username });
554
1291
  return ok(username);
555
1292
  } catch (error) {
556
- logger4.error("Vercel authentication failed", { error });
1293
+ logger8.error("Vercel authentication failed", { error });
557
1294
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error));
558
1295
  }
559
1296
  },
@@ -564,11 +1301,11 @@ function createVercelProvider() {
564
1301
  deployM("deploy_authRequired")
565
1302
  );
566
1303
  }
567
- logger4.debug("Creating Vercel project", { projectName });
1304
+ logger8.debug("Creating Vercel project", { projectName });
568
1305
  try {
569
1306
  const existingId = await findExistingProject(token, projectName);
570
1307
  if (existingId !== null) {
571
- logger4.info("Vercel project found", { projectName, projectId: existingId });
1308
+ logger8.info("Vercel project found", { projectName, projectId: existingId });
572
1309
  return ok(existingId);
573
1310
  }
574
1311
  try {
@@ -583,13 +1320,13 @@ function createVercelProvider() {
583
1320
  })
584
1321
  }
585
1322
  );
586
- logger4.info("Vercel project created", { projectName, projectId: project.id });
1323
+ logger8.info("Vercel project created", { projectName, projectId: project.id });
587
1324
  return ok(project.id);
588
1325
  } catch (error) {
589
1326
  if (error instanceof VercelApiError && error.status === 409) {
590
1327
  const concurrentProjectId = await findExistingProject(token, projectName);
591
1328
  if (concurrentProjectId !== null) {
592
- logger4.info("Vercel project reused after create conflict", {
1329
+ logger8.info("Vercel project reused after create conflict", {
593
1330
  projectName,
594
1331
  projectId: concurrentProjectId
595
1332
  });
@@ -599,7 +1336,7 @@ function createVercelProvider() {
599
1336
  throw error;
600
1337
  }
601
1338
  } catch (error) {
602
- logger4.error("Vercel project creation failed", { projectName, error });
1339
+ logger8.error("Vercel project creation failed", { projectName, error });
603
1340
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error));
604
1341
  }
605
1342
  },
@@ -610,7 +1347,7 @@ function createVercelProvider() {
610
1347
  deployM("deploy_authRequired")
611
1348
  );
612
1349
  }
613
- logger4.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
1350
+ logger8.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
614
1351
  try {
615
1352
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
616
1353
  key,
@@ -626,10 +1363,10 @@ function createVercelProvider() {
626
1363
  body: JSON.stringify(envEntries)
627
1364
  }
628
1365
  );
629
- logger4.info("Vercel env vars set", { projectId, count: envEntries.length });
1366
+ logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
630
1367
  return ok(void 0);
631
1368
  } catch (error) {
632
- logger4.error("Vercel env vars setup failed", { projectId, error });
1369
+ logger8.error("Vercel env vars setup failed", { projectId, error });
633
1370
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error));
634
1371
  }
635
1372
  },
@@ -640,7 +1377,7 @@ function createVercelProvider() {
640
1377
  deployM("deploy_authRequired")
641
1378
  );
642
1379
  }
643
- logger4.debug("Deploying to Vercel", { projectId, outputDir });
1380
+ logger8.debug("Deploying to Vercel", { projectId, outputDir });
644
1381
  try {
645
1382
  const filePaths = collectFiles(outputDir, outputDir);
646
1383
  const files = [];
@@ -652,7 +1389,7 @@ function createVercelProvider() {
652
1389
  await uploadFile(token, content, sha);
653
1390
  files.push({ file: filePath, sha, size });
654
1391
  }
655
- logger4.debug("Files uploaded", { count: files.length });
1392
+ logger8.debug("Files uploaded", { count: files.length });
656
1393
  const deployment = await vercelFetch(
657
1394
  token,
658
1395
  "/v13/deployments",
@@ -672,14 +1409,14 @@ function createVercelProvider() {
672
1409
  status: finalStatus,
673
1410
  envVarsSet: []
674
1411
  };
675
- logger4.info("Vercel deployment complete", {
1412
+ logger8.info("Vercel deployment complete", {
676
1413
  deploymentId: deployment.id,
677
1414
  url: result.url,
678
1415
  status: finalStatus
679
1416
  });
680
1417
  return ok(result);
681
1418
  } catch (error) {
682
- logger4.error("Vercel deployment failed", { projectId, error });
1419
+ logger8.error("Vercel deployment failed", { projectId, error });
683
1420
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error));
684
1421
  }
685
1422
  }
@@ -728,7 +1465,7 @@ async function pollDeploymentStatus(token, deploymentId) {
728
1465
  }
729
1466
  return "building";
730
1467
  }
731
- var logger5 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
1468
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
732
1469
  function createAliyunProvisioner() {
733
1470
  let accessKey = null;
734
1471
  let secretKey = null;
@@ -737,7 +1474,7 @@ function createAliyunProvisioner() {
737
1474
  name: "aliyun",
738
1475
  serviceType: "sms",
739
1476
  async authenticate(credentials) {
740
- logger5.debug("Authenticating with Aliyun");
1477
+ logger9.debug("Authenticating with Aliyun");
741
1478
  try {
742
1479
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
743
1480
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -747,10 +1484,10 @@ function createAliyunProvisioner() {
747
1484
  accessKey = akId;
748
1485
  secretKey = akSecret;
749
1486
  signName = credentials.signName;
750
- logger5.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
1487
+ logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
751
1488
  return ok(akId);
752
1489
  } catch (error) {
753
- logger5.error("Aliyun authentication failed", { error });
1490
+ logger9.error("Aliyun authentication failed", { error });
754
1491
  return err(
755
1492
  HaiDeployError.AUTH_FAILED,
756
1493
  deployM("deploy_authFailed", {
@@ -767,7 +1504,7 @@ function createAliyunProvisioner() {
767
1504
  deployM("deploy_authRequired")
768
1505
  );
769
1506
  }
770
- logger5.debug("Provisioning Aliyun SMS service");
1507
+ logger9.debug("Provisioning Aliyun SMS service");
771
1508
  return ok({
772
1509
  serviceType: "sms",
773
1510
  provisionerName: "aliyun",
@@ -780,7 +1517,7 @@ function createAliyunProvisioner() {
780
1517
  }
781
1518
  };
782
1519
  }
783
- var logger6 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
1520
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
784
1521
  var NEON_API = "https://console.neon.tech/api/v2";
785
1522
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
786
1523
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -839,7 +1576,7 @@ function createNeonProvisioner() {
839
1576
  name: "neon",
840
1577
  serviceType: "db",
841
1578
  async authenticate(credentials) {
842
- logger6.debug("Authenticating with Neon");
1579
+ logger10.debug("Authenticating with Neon");
843
1580
  try {
844
1581
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
845
1582
  if (!apiToken) {
@@ -847,10 +1584,10 @@ function createNeonProvisioner() {
847
1584
  }
848
1585
  await listNeonProjects(apiToken);
849
1586
  token = apiToken;
850
- logger6.info("Neon authenticated");
1587
+ logger10.info("Neon authenticated");
851
1588
  return ok("neon-user");
852
1589
  } catch (error) {
853
- logger6.error("Neon authentication failed", { error });
1590
+ logger10.error("Neon authentication failed", { error });
854
1591
  return err(
855
1592
  HaiDeployError.AUTH_FAILED,
856
1593
  deployM("deploy_authFailed", {
@@ -867,7 +1604,7 @@ function createNeonProvisioner() {
867
1604
  deployM("deploy_authRequired")
868
1605
  );
869
1606
  }
870
- logger6.debug("Provisioning Neon database", { appName });
1607
+ logger10.debug("Provisioning Neon database", { appName });
871
1608
  let resource;
872
1609
  try {
873
1610
  const projectName = `${appName}-db`;
@@ -875,7 +1612,7 @@ function createNeonProvisioner() {
875
1612
  if (existingProject !== null) {
876
1613
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
877
1614
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
878
- logger6.info("Neon project reused", {
1615
+ logger10.info("Neon project reused", {
879
1616
  projectId: existingProject.id,
880
1617
  projectName
881
1618
  });
@@ -897,10 +1634,10 @@ function createNeonProvisioner() {
897
1634
  if (!projectId || !connectionUri) {
898
1635
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
899
1636
  }
900
- logger6.info("Neon database provisioned", { projectId });
1637
+ logger10.info("Neon database provisioned", { projectId });
901
1638
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
902
1639
  } catch (error) {
903
- logger6.error("Neon provisioning failed", { appName });
1640
+ logger10.error("Neon provisioning failed", { appName });
904
1641
  if (resource) {
905
1642
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
906
1643
  projectName: appName,
@@ -922,7 +1659,7 @@ function createNeonProvisioner() {
922
1659
  }
923
1660
  };
924
1661
  }
925
- var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
1662
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
926
1663
  var CF_API = "https://api.cloudflare.com/client/v4";
927
1664
  function createR2Provisioner() {
928
1665
  let token = null;
@@ -933,7 +1670,7 @@ function createR2Provisioner() {
933
1670
  name: "r2",
934
1671
  serviceType: "storage",
935
1672
  async authenticate(credentials) {
936
- logger7.debug("Authenticating with Cloudflare");
1673
+ logger11.debug("Authenticating with Cloudflare");
937
1674
  try {
938
1675
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
939
1676
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -950,10 +1687,10 @@ function createR2Provisioner() {
950
1687
  token = apiTok;
951
1688
  accessKey = credentials.accessKeyId;
952
1689
  secretKey = credentials.secretAccessKey;
953
- logger7.info("Cloudflare R2 authenticated", { accountId: acctId });
1690
+ logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
954
1691
  return ok(acctId);
955
1692
  } catch (error) {
956
- logger7.error("Cloudflare R2 authentication failed", { error });
1693
+ logger11.error("Cloudflare R2 authentication failed", { error });
957
1694
  return err(
958
1695
  HaiDeployError.AUTH_FAILED,
959
1696
  deployM("deploy_authFailed", {
@@ -971,7 +1708,7 @@ function createR2Provisioner() {
971
1708
  );
972
1709
  }
973
1710
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
974
- logger7.debug("Provisioning R2 bucket", { appName, bucketName });
1711
+ logger11.debug("Provisioning R2 bucket", { appName, bucketName });
975
1712
  try {
976
1713
  const createRes = await fetch(
977
1714
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -987,7 +1724,7 @@ function createR2Provisioner() {
987
1724
  if (!createRes.ok && createRes.status !== 409) {
988
1725
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
989
1726
  }
990
- logger7.info("R2 bucket provisioned", { bucketName });
1727
+ logger11.info("R2 bucket provisioned", { bucketName });
991
1728
  return ok({
992
1729
  serviceType: "storage",
993
1730
  provisionerName: "r2",
@@ -1005,7 +1742,7 @@ function createR2Provisioner() {
1005
1742
  resourceInfo: `r2-bucket:${bucketName}`
1006
1743
  });
1007
1744
  } catch (error) {
1008
- logger7.error("R2 provisioning failed", { appName, error });
1745
+ logger11.error("R2 provisioning failed", { appName, error });
1009
1746
  return err(
1010
1747
  HaiDeployError.PROVISION_FAILED,
1011
1748
  deployM("deploy_provisionFailed", {
@@ -1020,7 +1757,7 @@ function createR2Provisioner() {
1020
1757
  }
1021
1758
  };
1022
1759
  }
1023
- var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
1760
+ var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
1024
1761
  var RESEND_API = "https://api.resend.com";
1025
1762
  function createResendProvisioner() {
1026
1763
  let token = null;
@@ -1029,7 +1766,7 @@ function createResendProvisioner() {
1029
1766
  name: "resend",
1030
1767
  serviceType: "email",
1031
1768
  async authenticate(credentials) {
1032
- logger8.debug("Authenticating with Resend");
1769
+ logger12.debug("Authenticating with Resend");
1033
1770
  try {
1034
1771
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
1035
1772
  if (!apiToken || !credentials.from) {
@@ -1043,10 +1780,10 @@ function createResendProvisioner() {
1043
1780
  }
1044
1781
  token = apiToken;
1045
1782
  from = credentials.from;
1046
- logger8.info("Resend authenticated");
1783
+ logger12.info("Resend authenticated");
1047
1784
  return ok("resend-user");
1048
1785
  } catch (error) {
1049
- logger8.error("Resend authentication failed", { error });
1786
+ logger12.error("Resend authentication failed", { error });
1050
1787
  return err(
1051
1788
  HaiDeployError.AUTH_FAILED,
1052
1789
  deployM("deploy_authFailed", {
@@ -1063,7 +1800,7 @@ function createResendProvisioner() {
1063
1800
  deployM("deploy_authRequired")
1064
1801
  );
1065
1802
  }
1066
- logger8.debug("Provisioning Resend email service");
1803
+ logger12.debug("Provisioning Resend email service");
1067
1804
  return ok({
1068
1805
  serviceType: "email",
1069
1806
  provisionerName: "resend",
@@ -1076,7 +1813,7 @@ function createResendProvisioner() {
1076
1813
  }
1077
1814
  };
1078
1815
  }
1079
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
1816
+ var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
1080
1817
  var UPSTASH_API = "https://api.upstash.com";
1081
1818
  function createUpstashAuthHeader(email, apiKey) {
1082
1819
  return `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
@@ -1127,7 +1864,7 @@ function createUpstashProvisioner() {
1127
1864
  name: "upstash",
1128
1865
  serviceType: "cache",
1129
1866
  async authenticate(credentials) {
1130
- logger9.debug("Authenticating with Upstash");
1867
+ logger13.debug("Authenticating with Upstash");
1131
1868
  try {
1132
1869
  const userEmail = credentials.email ?? "";
1133
1870
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -1137,10 +1874,10 @@ function createUpstashProvisioner() {
1137
1874
  await listUpstashDatabases(userEmail, userKey);
1138
1875
  email = userEmail;
1139
1876
  apiKey = userKey;
1140
- logger9.info("Upstash authenticated", { email: userEmail });
1877
+ logger13.info("Upstash authenticated", { email: userEmail });
1141
1878
  return ok(userEmail);
1142
1879
  } catch (error) {
1143
- logger9.error("Upstash authentication failed", { error });
1880
+ logger13.error("Upstash authentication failed", { error });
1144
1881
  return err(
1145
1882
  HaiDeployError.AUTH_FAILED,
1146
1883
  deployM("deploy_authFailed", {
@@ -1157,7 +1894,7 @@ function createUpstashProvisioner() {
1157
1894
  deployM("deploy_authRequired")
1158
1895
  );
1159
1896
  }
1160
- logger9.debug("Provisioning Upstash Redis", { appName });
1897
+ logger13.debug("Provisioning Upstash Redis", { appName });
1161
1898
  let resource;
1162
1899
  try {
1163
1900
  const databaseName = `${appName}-cache`;
@@ -1169,7 +1906,7 @@ function createUpstashProvisioner() {
1169
1906
  }
1170
1907
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
1171
1908
  const detail = await getUpstashDatabase(email, apiKey, databaseId);
1172
- logger9.info("Upstash Redis reused", { databaseId, databaseName });
1909
+ logger13.info("Upstash Redis reused", { databaseId, databaseName });
1173
1910
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
1174
1911
  }
1175
1912
  const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
@@ -1182,10 +1919,10 @@ function createUpstashProvisioner() {
1182
1919
  });
1183
1920
  if (data.database_id)
1184
1921
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
1185
- logger9.info("Upstash Redis provisioned", { databaseId: data.database_id });
1922
+ logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
1186
1923
  return ok(buildUpstashProvisionResult(data, "created"));
1187
1924
  } catch (error) {
1188
- logger9.error("Upstash provisioning failed", { appName });
1925
+ logger13.error("Upstash provisioning failed", { appName });
1189
1926
  if (resource) {
1190
1927
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
1191
1928
  projectName: appName,
@@ -1209,9 +1946,10 @@ function createUpstashProvisioner() {
1209
1946
  }
1210
1947
 
1211
1948
  // src/deploy-main.ts
1212
- var logger10 = core.logger.child({ module: "deploy", scope: "main" });
1949
+ var logger14 = core.logger.child({ module: "deploy", scope: "main" });
1213
1950
  var currentConfig = null;
1214
1951
  var currentProvider = null;
1952
+ var currentContainerProvider = null;
1215
1953
  var currentProvisioners = /* @__PURE__ */ new Map();
1216
1954
  var initInProgress = false;
1217
1955
  var notInitialized = core.module.createNotInitializedKit(
@@ -1257,11 +1995,27 @@ var deployCredentials = {
1257
1995
  save: (key, value) => saveCredential(key, value),
1258
1996
  saveAll: (entries) => saveCredentials(entries)
1259
1997
  };
1998
+ function normalizeProjectName(name) {
1999
+ const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2000
+ return normalized || "app";
2001
+ }
2002
+ function mergeProvisionEnv(results) {
2003
+ let env = {};
2004
+ const reachProviders = [];
2005
+ for (const prov of results) {
2006
+ if (prov.envVars.HAI_REACH_PROVIDERS)
2007
+ reachProviders.push(...JSON.parse(prov.envVars.HAI_REACH_PROVIDERS));
2008
+ env = { ...env, ...prov.envVars };
2009
+ }
2010
+ if (reachProviders.length > 0)
2011
+ env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
2012
+ return env;
2013
+ }
1260
2014
  var deploy = {
1261
2015
  credentials: deployCredentials,
1262
2016
  async init(config) {
1263
2017
  if (initInProgress) {
1264
- logger10.warn("Deploy init already in progress, skipping concurrent call");
2018
+ logger14.warn("Deploy init already in progress, skipping concurrent call");
1265
2019
  return err(
1266
2020
  HaiDeployError.CONFIG_ERROR,
1267
2021
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -1269,14 +2023,14 @@ var deploy = {
1269
2023
  }
1270
2024
  initInProgress = true;
1271
2025
  try {
1272
- if (currentProvider) {
1273
- logger10.warn("Deploy module is already initialized, reinitializing");
2026
+ if (currentProvider || currentContainerProvider) {
2027
+ logger14.warn("Deploy module is already initialized, reinitializing");
1274
2028
  await deploy.close();
1275
2029
  }
1276
- logger10.info("Initializing deploy module");
2030
+ logger14.info("Initializing deploy module");
1277
2031
  const parseResult = DeployConfigSchema.safeParse(config);
1278
2032
  if (!parseResult.success) {
1279
- logger10.error("Deploy config validation failed", { error: parseResult.error.message });
2033
+ logger14.error("Deploy config validation failed", { error: parseResult.error.message });
1280
2034
  return err(
1281
2035
  HaiDeployError.CONFIG_ERROR,
1282
2036
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -1285,12 +2039,19 @@ var deploy = {
1285
2039
  }
1286
2040
  const parsed = parseResult.data;
1287
2041
  try {
1288
- const provider = createProviderByType(parsed.provider.type);
1289
- const authResult = await provider.authenticate(parsed.provider.token);
1290
- if (!authResult.success) {
1291
- return err(authResult.error);
2042
+ let provider = null;
2043
+ let containerProvider = null;
2044
+ if (parsed.provider.type === "docker-ssh") {
2045
+ containerProvider = createDockerSshProvider(parsed.provider, parsed.container?.runtime);
2046
+ logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
2047
+ } else {
2048
+ provider = createProviderByType(parsed.provider.type);
2049
+ const authResult = await provider.authenticate(parsed.provider.token);
2050
+ if (!authResult.success) {
2051
+ return err(authResult.error);
2052
+ }
2053
+ logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
1292
2054
  }
1293
- logger10.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
1294
2055
  currentProvisioners.clear();
1295
2056
  if (parsed.services) {
1296
2057
  const serviceEntries = Object.entries(parsed.services);
@@ -1301,14 +2062,14 @@ var deploy = {
1301
2062
  const credentials = extractCredentials(serviceConfig);
1302
2063
  const provAuthResult = await provisioner.authenticate(credentials);
1303
2064
  if (!provAuthResult.success) {
1304
- logger10.error("Provisioner authentication failed", {
2065
+ logger14.error("Provisioner authentication failed", {
1305
2066
  service: serviceType,
1306
2067
  provisioner: serviceConfig.provisioner
1307
2068
  });
1308
2069
  currentProvisioners.clear();
1309
2070
  return err(provAuthResult.error);
1310
2071
  }
1311
- logger10.info("Provisioner authenticated", {
2072
+ logger14.info("Provisioner authenticated", {
1312
2073
  service: serviceType,
1313
2074
  user: provAuthResult.data
1314
2075
  });
@@ -1316,17 +2077,19 @@ var deploy = {
1316
2077
  }
1317
2078
  }
1318
2079
  currentProvider = provider;
2080
+ currentContainerProvider = containerProvider;
1319
2081
  currentConfig = parsed;
1320
- logger10.info("Deploy module initialized", {
2082
+ logger14.info("Deploy module initialized", {
1321
2083
  provider: parsed.provider.type,
1322
2084
  provisioners: [...currentProvisioners.keys()]
1323
2085
  });
1324
2086
  return ok(void 0);
1325
2087
  } catch (error) {
1326
2088
  currentProvider = null;
2089
+ currentContainerProvider = null;
1327
2090
  currentConfig = null;
1328
2091
  currentProvisioners.clear();
1329
- logger10.error("Deploy module initialization failed", { error });
2092
+ logger14.error("Deploy module initialization failed", { error });
1330
2093
  return err(
1331
2094
  HaiDeployError.CONFIG_ERROR,
1332
2095
  deployM("deploy_configError", {
@@ -1340,37 +2103,38 @@ var deploy = {
1340
2103
  }
1341
2104
  },
1342
2105
  async close() {
1343
- if (!currentProvider) {
1344
- logger10.info("Deploy module already closed, skipping");
2106
+ if (!currentProvider && !currentContainerProvider) {
2107
+ logger14.info("Deploy module already closed, skipping");
1345
2108
  return;
1346
2109
  }
1347
- logger10.info("Closing deploy module");
2110
+ logger14.info("Closing deploy module");
1348
2111
  currentProvider = null;
2112
+ currentContainerProvider = null;
1349
2113
  currentConfig = null;
1350
2114
  currentProvisioners.clear();
1351
- logger10.info("Deploy module closed");
2115
+ logger14.info("Deploy module closed");
1352
2116
  },
1353
2117
  get config() {
1354
2118
  return currentConfig;
1355
2119
  },
1356
2120
  get isInitialized() {
1357
- return currentProvider !== null;
2121
+ return currentProvider !== null || currentContainerProvider !== null;
1358
2122
  },
1359
2123
  async scan(appDir) {
1360
- logger10.debug("Scanning application", { appDir });
2124
+ logger14.debug("Scanning application", { appDir });
1361
2125
  return scanApp(appDir);
1362
2126
  },
1363
2127
  async provisionAll(projectName) {
1364
- if (!currentProvider) {
2128
+ if (!currentProvider && !currentContainerProvider) {
1365
2129
  return notInitialized.result();
1366
2130
  }
1367
- logger10.info("Provisioning all services", { projectName, count: currentProvisioners.size });
2131
+ logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
1368
2132
  const results = [];
1369
2133
  for (const [serviceType, provisioner] of currentProvisioners) {
1370
- logger10.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
2134
+ logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
1371
2135
  const provResult = await provisioner.provision(projectName);
1372
2136
  if (!provResult.success) {
1373
- logger10.error("Service provisioning failed", {
2137
+ logger14.error("Service provisioning failed", {
1374
2138
  serviceType,
1375
2139
  provisioner: provisioner.name,
1376
2140
  code: provResult.error.code
@@ -1387,21 +2151,27 @@ var deploy = {
1387
2151
  });
1388
2152
  }
1389
2153
  results.push(provResult.data);
1390
- logger10.info("Service provisioned", { serviceType, provisioner: provisioner.name });
2154
+ logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
1391
2155
  }
1392
2156
  return ok(results);
1393
2157
  },
1394
2158
  async deployApp(appDir, options) {
1395
- if (!currentProvider) {
2159
+ if (!currentProvider && !currentContainerProvider) {
1396
2160
  return notInitialized.result();
1397
2161
  }
1398
- logger10.info("Starting deployment", { appDir, options });
2162
+ logger14.info("Starting deployment", { appDir, options });
1399
2163
  const scanResult = await scanApp(appDir);
1400
2164
  if (!scanResult.success) {
1401
2165
  return err(scanResult.error);
1402
2166
  }
1403
2167
  const scan = scanResult.data;
1404
2168
  const projectName = options?.projectName ?? scan.appName;
2169
+ if (currentContainerProvider) {
2170
+ return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options);
2171
+ }
2172
+ if (!currentProvider) {
2173
+ return notInitialized.result();
2174
+ }
1405
2175
  if (scan.isSvelteKit && !scan.adapterInstalled) {
1406
2176
  return err(
1407
2177
  HaiDeployError.ADAPTER_MISSING,
@@ -1418,14 +2188,7 @@ var deploy = {
1418
2188
  return err(provResults.error);
1419
2189
  }
1420
2190
  recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
1421
- const reachProviders = [];
1422
- for (const prov of provResults.data) {
1423
- if (prov.envVars.HAI_REACH_PROVIDERS)
1424
- reachProviders.push(...JSON.parse(prov.envVars.HAI_REACH_PROVIDERS));
1425
- allEnvVars = { ...allEnvVars, ...prov.envVars };
1426
- }
1427
- if (reachProviders.length > 0)
1428
- allEnvVars.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
2191
+ allEnvVars = mergeProvisionEnv(provResults.data);
1429
2192
  }
1430
2193
  const projectResult = await currentProvider.createProject(projectName);
1431
2194
  if (!projectResult.success) {
@@ -1454,7 +2217,7 @@ var deploy = {
1454
2217
  ...deployResult.data,
1455
2218
  envVarsSet: Object.keys(allEnvVars)
1456
2219
  };
1457
- logger10.info("Deployment completed", {
2220
+ logger14.info("Deployment completed", {
1458
2221
  url: result.url,
1459
2222
  deploymentId: result.deploymentId,
1460
2223
  status: result.status
@@ -1462,6 +2225,24 @@ var deploy = {
1462
2225
  return ok(result);
1463
2226
  }
1464
2227
  };
2228
+ async function deployContainerApp(provider, appDir, scan, rawProjectName, options) {
2229
+ const projectName = normalizeProjectName(rawProjectName);
2230
+ if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
2231
+ return err(
2232
+ HaiDeployError.ADAPTER_MISSING,
2233
+ deployM("deploy_adapterMissing", { params: { adapter: "@sveltejs/adapter-node" } })
2234
+ );
2235
+ }
2236
+ let provisionEnv = {};
2237
+ if (!options?.skipProvision) {
2238
+ const provResults = await deploy.provisionAll(projectName);
2239
+ if (!provResults.success) {
2240
+ return err(provResults.error);
2241
+ }
2242
+ provisionEnv = mergeProvisionEnv(provResults.data);
2243
+ }
2244
+ return provider.deploy({ appDir, projectName, scan, provisionEnv });
2245
+ }
1465
2246
 
1466
2247
  export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, getDeployRecovery };
1467
2248
  //# sourceMappingURL=index.js.map