@action-llama/action-llama 0.12.1 → 0.12.2

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 (52) hide show
  1. package/dist/build-info.json +1 -1
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/chat.js.map +1 -1
  4. package/dist/cli/commands/kill.d.ts.map +1 -1
  5. package/dist/cli/commands/kill.js +1 -0
  6. package/dist/cli/commands/kill.js.map +1 -1
  7. package/dist/cli/commands/logs.js +3 -3
  8. package/dist/cli/commands/logs.js.map +1 -1
  9. package/dist/cli/commands/pause.d.ts.map +1 -1
  10. package/dist/cli/commands/pause.js +1 -0
  11. package/dist/cli/commands/pause.js.map +1 -1
  12. package/dist/cli/commands/push.d.ts.map +1 -1
  13. package/dist/cli/commands/push.js +0 -1
  14. package/dist/cli/commands/push.js.map +1 -1
  15. package/dist/cli/commands/resume.d.ts.map +1 -1
  16. package/dist/cli/commands/resume.js +1 -0
  17. package/dist/cli/commands/resume.js.map +1 -1
  18. package/dist/cli/commands/status.js +2 -2
  19. package/dist/cli/commands/status.js.map +1 -1
  20. package/dist/cli/gateway-client.d.ts +4 -2
  21. package/dist/cli/gateway-client.d.ts.map +1 -1
  22. package/dist/cli/gateway-client.js +6 -4
  23. package/dist/cli/gateway-client.js.map +1 -1
  24. package/dist/cloud/vps/hetzner-api.d.ts +133 -0
  25. package/dist/cloud/vps/hetzner-api.d.ts.map +1 -0
  26. package/dist/cloud/vps/hetzner-api.js +95 -0
  27. package/dist/cloud/vps/hetzner-api.js.map +1 -0
  28. package/dist/cloud/vps/provision.d.ts.map +1 -1
  29. package/dist/cloud/vps/provision.js +412 -2
  30. package/dist/cloud/vps/provision.js.map +1 -1
  31. package/dist/cloud/vps/teardown.d.ts.map +1 -1
  32. package/dist/cloud/vps/teardown.js +11 -1
  33. package/dist/cloud/vps/teardown.js.map +1 -1
  34. package/dist/credentials/builtins/hetzner-api-key.d.ts +4 -0
  35. package/dist/credentials/builtins/hetzner-api-key.d.ts.map +1 -0
  36. package/dist/credentials/builtins/hetzner-api-key.js +24 -0
  37. package/dist/credentials/builtins/hetzner-api-key.js.map +1 -0
  38. package/dist/credentials/builtins/index.d.ts.map +1 -1
  39. package/dist/credentials/builtins/index.js +2 -0
  40. package/dist/credentials/builtins/index.js.map +1 -1
  41. package/dist/remote/push.d.ts +0 -1
  42. package/dist/remote/push.d.ts.map +1 -1
  43. package/dist/remote/push.js +31 -7
  44. package/dist/remote/push.js.map +1 -1
  45. package/dist/scheduler/index.js +1 -1
  46. package/dist/scheduler/index.js.map +1 -1
  47. package/dist/shared/config.d.ts +2 -0
  48. package/dist/shared/config.d.ts.map +1 -1
  49. package/dist/shared/config.js.map +1 -1
  50. package/docs/cloud.md +2 -1
  51. package/docs/vps-deployment.md +11 -1
  52. package/package.json +1 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Hetzner Cloud API v1 client.
3
+ * Plain fetch() wrapper — no SDK dependency.
4
+ */
5
+ const BASE_URL = "https://api.hetzner.cloud/v1";
6
+ class HetznerApiError extends Error {
7
+ statusCode;
8
+ constructor(statusCode, message) {
9
+ super(message);
10
+ this.statusCode = statusCode;
11
+ this.name = "HetznerApiError";
12
+ }
13
+ }
14
+ async function hetznerFetch(apiKey, path, options = {}) {
15
+ const res = await fetch(`${BASE_URL}${path}`, {
16
+ ...options,
17
+ headers: {
18
+ Authorization: `Bearer ${apiKey}`,
19
+ "Content-Type": "application/json",
20
+ ...options.headers,
21
+ },
22
+ });
23
+ if (!res.ok) {
24
+ const body = await res.json().catch(() => ({ error: { message: "Unknown error" } }));
25
+ const errorMessage = body.error?.message || `HTTP ${res.status}`;
26
+ throw new HetznerApiError(res.status, `Hetzner API ${path} failed: ${errorMessage}`);
27
+ }
28
+ // Some endpoints return 204 No Content
29
+ if (res.status === 204)
30
+ return undefined;
31
+ return res.json();
32
+ }
33
+ export async function listLocations(apiKey) {
34
+ const data = await hetznerFetch(apiKey, "/locations");
35
+ return data.locations;
36
+ }
37
+ export async function listServerTypes(apiKey) {
38
+ const data = await hetznerFetch(apiKey, "/server_types");
39
+ // Hetzner API includes supported locations per server type
40
+ return data.server_types;
41
+ }
42
+ export async function listImages(apiKey) {
43
+ // Filter to only OS images (not snapshots/backups)
44
+ const data = await hetznerFetch(apiKey, "/images?type=system");
45
+ return data.images;
46
+ }
47
+ export async function listSshKeys(apiKey) {
48
+ const data = await hetznerFetch(apiKey, "/ssh_keys");
49
+ return data.ssh_keys;
50
+ }
51
+ export async function createSshKey(apiKey, name, publicKey) {
52
+ const data = await hetznerFetch(apiKey, "/ssh_keys", {
53
+ method: "POST",
54
+ body: JSON.stringify({ name, public_key: publicKey }),
55
+ });
56
+ return data.ssh_key;
57
+ }
58
+ export async function createServer(apiKey, opts) {
59
+ const data = await hetznerFetch(apiKey, "/servers", {
60
+ method: "POST",
61
+ body: JSON.stringify(opts),
62
+ });
63
+ return data.server;
64
+ }
65
+ export async function getServer(apiKey, serverId) {
66
+ const data = await hetznerFetch(apiKey, `/servers/${serverId}`);
67
+ return data.server;
68
+ }
69
+ export async function deleteServer(apiKey, serverId) {
70
+ await hetznerFetch(apiKey, `/servers/${serverId}`, { method: "DELETE" });
71
+ }
72
+ // --- Firewalls ---
73
+ export async function listFirewalls(apiKey) {
74
+ const data = await hetznerFetch(apiKey, "/firewalls");
75
+ return data.firewalls;
76
+ }
77
+ export async function createFirewall(apiKey, name, rules) {
78
+ const data = await hetznerFetch(apiKey, "/firewalls", {
79
+ method: "POST",
80
+ body: JSON.stringify({ name, rules }),
81
+ });
82
+ return data.firewall;
83
+ }
84
+ export async function applyFirewallToServer(apiKey, firewallId, serverId) {
85
+ await hetznerFetch(apiKey, `/firewalls/${firewallId}/actions/apply_to_resources`, {
86
+ method: "POST",
87
+ body: JSON.stringify({
88
+ apply_to: [{
89
+ type: "server",
90
+ server: { id: serverId },
91
+ }],
92
+ }),
93
+ });
94
+ }
95
+ //# sourceMappingURL=hetzner-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hetzner-api.js","sourceRoot":"","sources":["../../../src/cloud/vps/hetzner-api.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"provision.d.ts","sourceRoot":"","sources":["../../../src/cloud/vps/provision.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgDH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE3E,wBAAsB,aAAa,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAiBlH"}
1
+ {"version":3,"file":"provision.d.ts","sourceRoot":"","sources":["../../../src/cloud/vps/provision.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgDH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE3E,wBAAsB,aAAa,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAsBlH"}
@@ -51,14 +51,18 @@ export async function setupVpsCloud(onInstanceCreated) {
51
51
  choices: [
52
52
  { name: "Connect to an existing server (any provider)", value: "existing" },
53
53
  { name: "Provision a new Vultr VPS", value: "vultr" },
54
+ { name: "Provision a new Hetzner VPS", value: "hetzner" },
54
55
  ],
55
56
  });
56
57
  if (mode === "existing") {
57
58
  return setupExistingServer();
58
59
  }
59
- // Offer Cloudflare HTTPS before Vultr provisioning
60
+ // Offer Cloudflare HTTPS before VPS provisioning
60
61
  const cfConfig = await promptCloudflareHttps();
61
- return provisionVultr(onInstanceCreated, cfConfig);
62
+ if (mode === "vultr") {
63
+ return provisionVultr(onInstanceCreated, cfConfig);
64
+ }
65
+ return provisionHetzner(onInstanceCreated, cfConfig);
62
66
  }
63
67
  /**
64
68
  * Prompt for optional Cloudflare HTTPS setup.
@@ -574,4 +578,410 @@ async function provisionVultr(onInstanceCreated, cfConfig) {
574
578
  }
575
579
  return result;
576
580
  }
581
+ async function provisionHetzner(onInstanceCreated, cfConfig) {
582
+ // 1. Read Hetzner API key — prompt inline if missing
583
+ const backend = new FilesystemBackend();
584
+ let apiKeyValue = await backend.read("hetzner_api_key", "default", "api_key");
585
+ if (!apiKeyValue) {
586
+ console.log("Hetzner API key not found.");
587
+ const enteredKey = await password({
588
+ message: "Enter your Hetzner API key (from https://console.hetzner.cloud → Security → API tokens):",
589
+ mask: "*",
590
+ validate: (v) => v.trim() ? true : "API key is required",
591
+ });
592
+ apiKeyValue = enteredKey.trim();
593
+ await writeCredentialField("hetzner_api_key", "default", "api_key", apiKeyValue);
594
+ console.log("Hetzner API key saved.");
595
+ }
596
+ const { listLocations, listServerTypes, listImages, listSshKeys, createSshKey, createServer, getServer, listFirewalls, createFirewall, applyFirewallToServer, } = await import("./hetzner-api.js");
597
+ // Fetch server types + locations + OS images + SSH keys in parallel
598
+ console.log("\nFetching Hetzner catalog...");
599
+ const [serverTypes, locations, allImages, existingKeys] = await Promise.all([
600
+ listServerTypes(apiKeyValue),
601
+ listLocations(apiKeyValue),
602
+ listImages(apiKeyValue),
603
+ listSshKeys(apiKeyValue),
604
+ ]);
605
+ // Filter to x86 Linux images
606
+ const linuxImages = allImages.filter((img) => img.architecture === "x86" &&
607
+ !img.deprecated &&
608
+ (img.os_flavor === "ubuntu" || img.os_flavor === "debian" || img.os_flavor === "fedora" || img.os_flavor === "centos" || img.os_flavor === "rocky" || img.os_flavor === "alma"));
609
+ // Step-based wizard: Esc goes back to the previous step
610
+ // Steps: 0=Server Type, 1=Location, 2=OS, 3=SSH key
611
+ let step = 0;
612
+ let serverTypeChoice = "";
613
+ let locationChoice = "";
614
+ let imageChoice = "";
615
+ let sshKeyId = 0;
616
+ let sshKeyPath = VPS_CONSTANTS.DEFAULT_SSH_KEY_PATH;
617
+ while (step < 4) {
618
+ if (step === 0) {
619
+ // Pick server type first (searchable)
620
+ const typeChoices = serverTypes
621
+ .sort((a, b) => {
622
+ // Sort by monthly price (parse from string)
623
+ const priceA = parseFloat(a.prices[0]?.price_monthly.gross || "0");
624
+ const priceB = parseFloat(b.prices[0]?.price_monthly.gross || "0");
625
+ return priceA - priceB;
626
+ })
627
+ .map((st) => {
628
+ const price = st.prices[0]?.price_monthly.gross || "0";
629
+ return {
630
+ name: `${st.name} — ${st.cores} vCPU / ${st.memory}GB RAM / ${st.disk}GB SSD — €${price}/mo`,
631
+ value: st.name,
632
+ };
633
+ });
634
+ const result = await searchWithEsc({ message: "Server Type:", choices: typeChoices });
635
+ if (result === null)
636
+ return null; // Esc at first step → abort
637
+ serverTypeChoice = result;
638
+ step++;
639
+ }
640
+ else if (step === 1) {
641
+ // Pick location (filtered to where the selected server type is available)
642
+ const selectedType = serverTypes.find((st) => st.name === serverTypeChoice);
643
+ const availableLocationIds = new Set(selectedType.available_locations || []);
644
+ const locationChoices = locations
645
+ .filter((loc) => availableLocationIds.has(loc.id))
646
+ .sort((a, b) => a.city.localeCompare(b.city))
647
+ .map((loc) => ({
648
+ name: `${loc.city}, ${loc.country} (${loc.name})`,
649
+ value: loc.name,
650
+ }));
651
+ if (locationChoices.length === 0) {
652
+ console.error("This server type is not available in any location.");
653
+ step--;
654
+ continue;
655
+ }
656
+ const result = await searchWithEsc({ message: "Location:", choices: locationChoices });
657
+ if (result === null) {
658
+ step--;
659
+ continue;
660
+ }
661
+ locationChoice = result;
662
+ step++;
663
+ }
664
+ else if (step === 2) {
665
+ // Pick OS image
666
+ const imageChoices = linuxImages
667
+ .sort((a, b) => {
668
+ // Prefer Ubuntu, then Debian, then alphabetical
669
+ const rank = (img) => {
670
+ if (img.os_flavor === "ubuntu")
671
+ return 0;
672
+ if (img.os_flavor === "debian")
673
+ return 1;
674
+ return 2;
675
+ };
676
+ return rank(a) - rank(b) || a.description.localeCompare(b.description);
677
+ })
678
+ .map((img) => ({
679
+ name: img.description,
680
+ value: img.name,
681
+ }));
682
+ // Auto-select Ubuntu 22.04 if available
683
+ const ubuntu2204 = linuxImages.find((img) => img.name === "ubuntu-22.04");
684
+ if (ubuntu2204) {
685
+ imageChoice = ubuntu2204.name;
686
+ console.log(`OS: ${ubuntu2204.description} (auto-selected)`);
687
+ }
688
+ else {
689
+ const result = await searchWithEsc({ message: "OS image:", choices: imageChoices });
690
+ if (result === null) {
691
+ step--;
692
+ continue;
693
+ }
694
+ imageChoice = result;
695
+ }
696
+ step++;
697
+ }
698
+ else if (step === 3) {
699
+ // SSH key — use vps_ssh credential, Hetzner keys, or create new
700
+ const { loadCredentialFields, credentialExists: credExists } = await import("../../shared/credentials.js");
701
+ const { promptCredential } = await import("../../credentials/prompter.js");
702
+ const { resolveCredential } = await import("../../credentials/registry.js");
703
+ // Check for existing vps_ssh credential
704
+ const hasVpsSsh = await credExists("vps_ssh", "default");
705
+ const vpsSshFields = hasVpsSsh ? await loadCredentialFields("vps_ssh", "default") : undefined;
706
+ // Build choices
707
+ const keyChoices = [];
708
+ if (vpsSshFields?.public_key) {
709
+ const preview = vpsSshFields.public_key.slice(0, 40) + "...";
710
+ keyChoices.push({ name: `Action Llama VPS key (${preview})`, value: "__al_credential__" });
711
+ }
712
+ for (const k of existingKeys) {
713
+ keyChoices.push({
714
+ name: `Hetzner: ${k.name} (${k.public_key.slice(0, 30)}...)`,
715
+ value: String(k.id),
716
+ });
717
+ }
718
+ keyChoices.push({ name: "Set up a new VPS SSH key", value: "__new__" });
719
+ const result = await searchWithEsc({ message: "SSH key:", choices: keyChoices });
720
+ if (result === null) {
721
+ step--;
722
+ continue;
723
+ }
724
+ const vpsSshKeyPath = resolve(credentialDir("vps_ssh", "default"), "private_key");
725
+ if (result === "__new__") {
726
+ // Run the vps_ssh credential prompt
727
+ const def = resolveCredential("vps_ssh");
728
+ const promptResult = await promptCredential(def, "default");
729
+ if (!promptResult) {
730
+ continue; // User cancelled — stay on this step
731
+ }
732
+ // Persist the credential before uploading to Hetzner
733
+ await writeCredentialFields("vps_ssh", "default", promptResult.values);
734
+ sshKeyPath = vpsSshKeyPath;
735
+ const pubKey = promptResult.values.public_key;
736
+ // Upload to Hetzner
737
+ const uploaded = await createSshKey(apiKeyValue, "action-llama", pubKey);
738
+ sshKeyId = uploaded.id;
739
+ console.log("SSH key uploaded to Hetzner.");
740
+ }
741
+ else if (result === "__al_credential__") {
742
+ sshKeyPath = vpsSshKeyPath;
743
+ // Upload the existing vps_ssh public key to Hetzner if not already there
744
+ const pubKey = vpsSshFields.public_key;
745
+ const alreadyOnHetzner = existingKeys.find((k) => k.public_key.trim() === pubKey.trim());
746
+ if (alreadyOnHetzner) {
747
+ sshKeyId = alreadyOnHetzner.id;
748
+ }
749
+ else {
750
+ const uploaded = await createSshKey(apiKeyValue, "action-llama", pubKey);
751
+ sshKeyId = uploaded.id;
752
+ console.log("VPS SSH key uploaded to Hetzner.");
753
+ }
754
+ }
755
+ else {
756
+ sshKeyId = parseInt(result);
757
+ }
758
+ step++;
759
+ }
760
+ }
761
+ // Set up Hetzner firewall (allow SSH + gateway inbound)
762
+ const AL_FW_NAME = "action-llama";
763
+ let firewallId;
764
+ console.log("\nConfiguring Hetzner firewall...");
765
+ const existingFirewalls = await listFirewalls(apiKeyValue);
766
+ const existingFw = existingFirewalls.find((fw) => fw.name === AL_FW_NAME);
767
+ if (existingFw) {
768
+ firewallId = existingFw.id;
769
+ }
770
+ else {
771
+ // Create firewall with SSH + web ports
772
+ const webPorts = cfConfig
773
+ ? [
774
+ { port: "80", description: "HTTP redirect" },
775
+ { port: "443", description: "HTTPS" },
776
+ ]
777
+ : [
778
+ { port: String(VPS_CONSTANTS.DEFAULT_GATEWAY_PORT), description: "Gateway" },
779
+ ];
780
+ const rules = [
781
+ {
782
+ direction: "in",
783
+ protocol: "tcp",
784
+ source_ips: ["0.0.0.0/0", "::/0"],
785
+ port: "22",
786
+ description: "SSH",
787
+ },
788
+ ...webPorts.map((wp) => ({
789
+ direction: "in",
790
+ protocol: "tcp",
791
+ source_ips: ["0.0.0.0/0", "::/0"],
792
+ port: wp.port,
793
+ description: wp.description,
794
+ })),
795
+ ];
796
+ const firewall = await createFirewall(apiKeyValue, AL_FW_NAME, rules);
797
+ firewallId = firewall.id;
798
+ console.log("Hetzner firewall created.");
799
+ }
800
+ // Create server
801
+ console.log("Provisioning Hetzner server...");
802
+ const server = await createServer(apiKeyValue, {
803
+ name: "action-llama",
804
+ server_type: serverTypeChoice,
805
+ location: locationChoice,
806
+ image: imageChoice,
807
+ ssh_keys: [sshKeyId],
808
+ user_data: VPS_CONSTANTS.CLOUD_INIT_SCRIPT,
809
+ firewalls: firewallId ? [firewallId] : undefined,
810
+ labels: { "managed-by": "action-llama" },
811
+ });
812
+ console.log(`Server ${server.id} created.`);
813
+ // Persist immediately so the instance can be deprovisioned even if we're interrupted
814
+ const partialResult = {
815
+ provider: "vps",
816
+ host: "PENDING",
817
+ hetznerServerId: server.id,
818
+ hetznerLocation: locationChoice,
819
+ };
820
+ if (onInstanceCreated)
821
+ onInstanceCreated(partialResult);
822
+ // Poll until active + SSH available + Docker installed
823
+ console.log("Waiting for server to become active...");
824
+ const sshConfig = {
825
+ host: "",
826
+ user: VPS_CONSTANTS.DEFAULT_SSH_USER,
827
+ port: VPS_CONSTANTS.DEFAULT_SSH_PORT,
828
+ keyPath: sshKeyPath,
829
+ };
830
+ const startTime = Date.now();
831
+ const maxWaitMs = 10 * 60 * 1000; // 10 minutes
832
+ while (Date.now() - startTime < maxWaitMs) {
833
+ const current = await getServer(apiKeyValue, server.id);
834
+ if (current.status === "running" && current.public_net.ipv4.ip) {
835
+ sshConfig.host = current.public_net.ipv4.ip;
836
+ // Update persisted config with real IP as soon as we know it
837
+ if (partialResult.host === "PENDING") {
838
+ partialResult.host = current.public_net.ipv4.ip;
839
+ if (onInstanceCreated)
840
+ onInstanceCreated(partialResult);
841
+ console.log(`Server running at ${current.public_net.ipv4.ip}. Waiting for SSH...`);
842
+ }
843
+ // Wait for SSH
844
+ const sshReady = await testConnection(sshConfig);
845
+ if (sshReady) {
846
+ // Check if cloud-init has finished installing Node.js + Docker
847
+ const nodeCheck = await sshExec(sshConfig, "node --version");
848
+ const dockerCheck = await sshExec(sshConfig, "docker info --format '{{.ServerVersion}}'");
849
+ if (nodeCheck.exitCode === 0 && dockerCheck.exitCode === 0) {
850
+ console.log(`Node.js ${nodeCheck.stdout.trim()}, Docker ${dockerCheck.stdout.trim()} ready on VPS.`);
851
+ break;
852
+ }
853
+ console.log("Waiting for cloud-init to complete (Node.js + Docker)...");
854
+ }
855
+ }
856
+ else {
857
+ process.stdout.write(".");
858
+ }
859
+ await new Promise((r) => setTimeout(r, 10_000));
860
+ }
861
+ if (!sshConfig.host || sshConfig.host === "0.0.0.0") {
862
+ console.error("\nTimed out waiting for VPS to become ready.");
863
+ console.error(`Server ${server.id} was created. Use 'al env deprov' to clean up.`);
864
+ return null;
865
+ }
866
+ // Final SSH check
867
+ const ok = await testConnection(sshConfig);
868
+ if (!ok) {
869
+ console.error("\nVPS is active but SSH connection failed.");
870
+ console.error(`Server ${server.id} at ${sshConfig.host} was created. Use 'al env deprov' to clean up.`);
871
+ return null;
872
+ }
873
+ const shouldContinue = await confirm({
874
+ message: `VPS ready at ${sshConfig.host}. Continue with setup?`,
875
+ default: true,
876
+ });
877
+ if (!shouldContinue)
878
+ return null;
879
+ const result = {
880
+ provider: "vps",
881
+ host: sshConfig.host,
882
+ hetznerServerId: server.id,
883
+ hetznerLocation: locationChoice,
884
+ gatewayUrl: `http://${sshConfig.host}:${VPS_CONSTANTS.DEFAULT_GATEWAY_PORT}`,
885
+ };
886
+ if (sshKeyPath !== VPS_CONSTANTS.DEFAULT_SSH_KEY_PATH)
887
+ result.sshKeyPath = sshKeyPath;
888
+ // Post-VPS Cloudflare HTTPS setup (same as Vultr)
889
+ if (cfConfig) {
890
+ try {
891
+ result.cloudflareHostname = cfConfig.hostname;
892
+ result.cloudflareZoneId = cfConfig.zoneId;
893
+ const { upsertDnsRecord, createOriginCertificate, setSslMode, } = await import("./cloudflare-api.js");
894
+ const { installNginx, configureNginx } = await import("./nginx.js");
895
+ // 1. DNS record
896
+ console.log(`\nCreating DNS record: ${cfConfig.hostname} → ${sshConfig.host}...`);
897
+ try {
898
+ const dnsRecord = await upsertDnsRecord(cfConfig.apiToken, cfConfig.zoneId, cfConfig.hostname, sshConfig.host, true);
899
+ result.cloudflareDnsRecordId = dnsRecord.id;
900
+ console.log(`DNS record created (${dnsRecord.id}).`);
901
+ }
902
+ catch (err) {
903
+ console.error(`Warning: DNS record creation failed: ${err.message}`);
904
+ console.error(`Falling back to http://${sshConfig.host}:${VPS_CONSTANTS.DEFAULT_GATEWAY_PORT}`);
905
+ return result;
906
+ }
907
+ // 2. Origin CA certificate
908
+ let cert, key;
909
+ try {
910
+ const existingCert = await backend.read("cloudflare_origin_cert", cfConfig.hostname, "certificate");
911
+ const existingKey = await backend.read("cloudflare_origin_cert", cfConfig.hostname, "private_key");
912
+ const shouldGenerate = existingCert && existingKey
913
+ ? await confirm({ message: "Origin CA certificate already exists. Regenerate?", default: false })
914
+ : true;
915
+ if (shouldGenerate) {
916
+ console.log("Generating Cloudflare Origin CA certificate...");
917
+ const originCert = await createOriginCertificate(cfConfig.apiToken, [cfConfig.hostname], 5475);
918
+ cert = originCert.certificate;
919
+ key = originCert.private_key;
920
+ console.log("Origin CA certificate generated.");
921
+ await writeCredentialFields("cloudflare_origin_cert", cfConfig.hostname, {
922
+ certificate: cert,
923
+ private_key: key,
924
+ });
925
+ console.log(`Origin CA cert saved to credentials (cloudflare_origin_cert/${cfConfig.hostname}).`);
926
+ }
927
+ else {
928
+ cert = existingCert;
929
+ key = existingKey;
930
+ console.log("Using existing Origin CA certificate.");
931
+ }
932
+ }
933
+ catch (err) {
934
+ console.error(`Warning: Origin CA certificate creation failed: ${err.message}`);
935
+ console.error("You can generate one manually at https://dash.cloudflare.com → SSL/TLS → Origin Server.");
936
+ return result;
937
+ }
938
+ // 3. Install nginx
939
+ console.log("Installing nginx...");
940
+ try {
941
+ await installNginx(sshConfig);
942
+ console.log("nginx installed.");
943
+ }
944
+ catch (err) {
945
+ console.error(`Warning: nginx installation failed: ${err.message}`);
946
+ console.error(`SSH into the server and install manually: ssh ${sshConfig.user}@${sshConfig.host}`);
947
+ return result;
948
+ }
949
+ // 4. Configure nginx with cert
950
+ console.log("Configuring nginx TLS reverse proxy...");
951
+ try {
952
+ await configureNginx(sshConfig, cfConfig.hostname, cert, key, VPS_CONSTANTS.DEFAULT_GATEWAY_PORT);
953
+ console.log("nginx configured.");
954
+ }
955
+ catch (err) {
956
+ console.error(`Warning: nginx configuration failed: ${err.message}`);
957
+ console.error(`SSH into the server to configure manually: ssh ${sshConfig.user}@${sshConfig.host}`);
958
+ return result;
959
+ }
960
+ // 5. Set SSL mode to strict
961
+ console.log("Setting Cloudflare SSL mode to strict...");
962
+ try {
963
+ await setSslMode(cfConfig.apiToken, cfConfig.zoneId, "strict");
964
+ console.log("SSL mode set to strict.");
965
+ }
966
+ catch (err) {
967
+ console.error(`Warning: Failed to set SSL mode: ${err.message}`);
968
+ console.error("Set SSL/TLS mode to 'Full (strict)' manually in the Cloudflare dashboard.");
969
+ }
970
+ // 6. Verify nginx is proxying correctly
971
+ const healthCheck = await sshExec(sshConfig, "curl -sf http://localhost/health", 10_000);
972
+ if (healthCheck.exitCode === 0) {
973
+ console.log("nginx reverse proxy verified.");
974
+ }
975
+ else {
976
+ console.log("Note: nginx health check returned non-zero (gateway may not be running yet).");
977
+ }
978
+ result.gatewayUrl = `https://${cfConfig.hostname}`;
979
+ }
980
+ catch (err) {
981
+ console.error(`Cloudflare HTTPS setup encountered an error: ${err.message}`);
982
+ console.error(`VPS is still available at ${sshConfig.host}. HTTPS can be configured manually.`);
983
+ }
984
+ }
985
+ return result;
986
+ }
577
987
  //# sourceMappingURL=provision.js.map