@webhikers/cli 1.0.1 → 1.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhikers/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for creating and deploying webhikers projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,19 +5,21 @@ import {
5
5
  maskValue,
6
6
  getConfigPath,
7
7
  } from "../utils/config-store.js";
8
- import { saveSSHKey, getSSHKeyPath } from "../utils/ssh-key.js";
8
+ import { findSSHKeys } from "../utils/ssh-key.js";
9
+
10
+ const DEFAULT_COOLIFY_PORT = "8000";
9
11
 
10
12
  export async function configCommand() {
11
13
  const existing = loadConfig();
12
14
 
13
15
  if (existing) {
14
16
  console.log("\nExisting configuration found:");
15
- console.log(` Coolify URL: ${existing.coolifyUrl || "—"}`);
17
+ console.log(` Server IP: ${existing.serverIp || "—"}`);
18
+ console.log(` Coolify Port: ${existing.coolifyPort || DEFAULT_COOLIFY_PORT}`);
16
19
  console.log(` Coolify Token: ${maskValue(existing.coolifyToken)}`);
17
20
  console.log(` Server UUID: ${maskValue(existing.serverUuid)}`);
18
- console.log(` Server IP: ${existing.serverIp || "—"}`);
19
21
  console.log(` SSH User: ${existing.sshUser || "—"}`);
20
- console.log(` SSH Key: ${getSSHKeyPath()}`);
22
+ console.log(` SSH Key: ${existing.sshKeyPath || "—"}`);
21
23
  console.log("");
22
24
 
23
25
  const { overwrite } = await inquirer.prompt([
@@ -35,62 +37,96 @@ export async function configCommand() {
35
37
  }
36
38
  }
37
39
 
40
+ // --- SSH Key selection ---
41
+ const sshKeys = findSSHKeys();
42
+ let sshKeyPath;
43
+
44
+ if (sshKeys.length > 0) {
45
+ const sshChoices = [
46
+ ...sshKeys,
47
+ { name: "Enter path manually", value: "__manual__" },
48
+ ];
49
+
50
+ const { selectedKey } = await inquirer.prompt([
51
+ {
52
+ type: "list",
53
+ name: "selectedKey",
54
+ message: "SSH Key auswählen:",
55
+ choices: sshChoices,
56
+ },
57
+ ]);
58
+
59
+ if (selectedKey === "__manual__") {
60
+ const { manualPath } = await inquirer.prompt([
61
+ {
62
+ type: "input",
63
+ name: "manualPath",
64
+ message: "SSH Key Pfad:",
65
+ validate: (v) => (v.length > 0 ? true : "Pfad ist erforderlich"),
66
+ },
67
+ ]);
68
+ sshKeyPath = manualPath;
69
+ } else {
70
+ sshKeyPath = selectedKey;
71
+ }
72
+ } else {
73
+ console.log("\nKein SSH Key gefunden. Erstelle einen:");
74
+ console.log(" ssh-keygen -t ed25519 -f ~/.ssh/hetzner");
75
+ console.log("Dann füge ~/.ssh/hetzner.pub bei Hetzner hinzu:");
76
+ console.log(" Hetzner Console → Security → SSH Keys → Add\n");
77
+ process.exit(1);
78
+ }
79
+
80
+ // --- Server + Coolify config ---
38
81
  const answers = await inquirer.prompt([
39
82
  {
40
83
  type: "input",
41
- name: "coolifyUrl",
42
- message: "Coolify URL (e.g. https://coolify.your-server.de):",
43
- validate: (v) => (v.startsWith("http") ? true : "Must start with http"),
84
+ name: "serverIp",
85
+ message: "Server IP-Adresse:\n (Hetzner Cloud Console → Server → IP-Adresse)\n ",
86
+ validate: (v) => (v.length > 0 ? true : "IP ist erforderlich"),
87
+ },
88
+ {
89
+ type: "input",
90
+ name: "coolifyPort",
91
+ message: "Coolify Port:\n (Standard ist 8000, Enter drücken wenn korrekt)\n ",
92
+ default: DEFAULT_COOLIFY_PORT,
44
93
  },
45
94
  {
46
95
  type: "password",
47
96
  name: "coolifyToken",
48
- message: "Coolify API Token:",
97
+ message: "Coolify API Token:\n (Coolify UI → Keys & Tokens → API Token erstellen)\n ",
49
98
  mask: "*",
50
- validate: (v) => (v.length > 0 ? true : "Token is required"),
99
+ validate: (v) => (v.length > 0 ? true : "Token ist erforderlich"),
51
100
  },
52
101
  {
53
102
  type: "input",
54
103
  name: "serverUuid",
55
- message: "Server UUID (from Coolify UI → Servers):",
56
- validate: (v) => (v.length > 0 ? true : "Server UUID is required"),
57
- },
58
- {
59
- type: "input",
60
- name: "serverIp",
61
- message: "Server IP address:",
62
- validate: (v) => (v.length > 0 ? true : "Server IP is required"),
104
+ message: "Server UUID:\n (Coolify UI → Servers → auf Server klicken → UUID steht in der URL)\n ",
105
+ validate: (v) => (v.length > 0 ? true : "UUID ist erforderlich"),
63
106
  },
64
107
  {
65
108
  type: "input",
66
109
  name: "sshUser",
67
- message: "SSH User:",
110
+ message: "SSH User:\n (Standard ist root, Enter drücken wenn korrekt)\n ",
68
111
  default: "root",
69
112
  },
70
- {
71
- type: "editor",
72
- name: "sshKey",
73
- message:
74
- "SSH Private Key (your editor will open — paste the key, save, and close):",
75
- validate: (v) =>
76
- v.includes("PRIVATE KEY") ? true : "Does not look like a valid SSH key",
77
- },
78
113
  ]);
79
114
 
80
- const keyPath = saveSSHKey(answers.sshKey);
115
+ const coolifyUrl = `http://${answers.serverIp}:${answers.coolifyPort}`;
81
116
 
82
117
  const config = {
83
- coolifyUrl: answers.coolifyUrl.replace(/\/+$/, ""),
118
+ serverIp: answers.serverIp,
119
+ coolifyPort: answers.coolifyPort,
120
+ coolifyUrl,
84
121
  coolifyToken: answers.coolifyToken,
85
122
  serverUuid: answers.serverUuid,
86
- serverIp: answers.serverIp,
87
123
  sshUser: answers.sshUser,
88
- sshKeyPath: keyPath,
124
+ sshKeyPath,
89
125
  };
90
126
 
91
127
  saveConfig(config);
92
128
 
93
129
  console.log(`\nConfiguration saved to ${getConfigPath()}`);
94
- console.log(`SSH key saved to ${keyPath}`);
130
+ console.log(`Coolify URL: ${coolifyUrl}`);
95
131
  console.log("\nDone! You can now run: webhikers create <project-name>");
96
132
  }
@@ -1,10 +1,11 @@
1
1
  import { execSync } from "child_process";
2
- import { existsSync, writeFileSync } from "fs";
2
+ import { existsSync, writeFileSync, readFileSync } from "fs";
3
3
  import { resolve } from "path";
4
4
  import { loadConfig, getConfigPath } from "../utils/config-store.js";
5
5
 
6
6
  const TEMPLATE_REPO = "Webhikers/nextjs-vibe-starter";
7
7
  const GITHUB_ORG = "Webhikers";
8
+ const DOMAIN_SUFFIX = "preview.webhikers.dev";
8
9
 
9
10
  function run(cmd, options = {}) {
10
11
  console.log(` → ${cmd}`);
@@ -15,6 +16,28 @@ function runCapture(cmd) {
15
16
  return execSync(cmd, { encoding: "utf-8" }).trim();
16
17
  }
17
18
 
19
+ async function coolifyApi(config, method, path, body) {
20
+ const url = `${config.coolifyUrl}/api/v1${path}`;
21
+ const options = {
22
+ method,
23
+ headers: {
24
+ Authorization: `Bearer ${config.coolifyToken}`,
25
+ "Content-Type": "application/json",
26
+ },
27
+ };
28
+ if (body) options.body = JSON.stringify(body);
29
+
30
+ const res = await fetch(url, options);
31
+ const data = await res.json();
32
+
33
+ if (!res.ok) {
34
+ throw new Error(
35
+ `Coolify API error (${res.status}): ${JSON.stringify(data)}`
36
+ );
37
+ }
38
+ return data;
39
+ }
40
+
18
41
  export async function createCommand(name) {
19
42
  console.log(`\nCreating project: ${name}\n`);
20
43
 
@@ -61,6 +84,8 @@ export async function createCommand(name) {
61
84
  // --- 4. Generate .env ---
62
85
  console.log("\nGenerating .env...");
63
86
  const payloadSecret = runCapture("openssl rand -hex 32");
87
+ const domain = `${name}.${DOMAIN_SUFFIX}`;
88
+ const siteUrl = `https://${domain}`;
64
89
  const envContent = [
65
90
  `PAYLOAD_SECRET=${payloadSecret}`,
66
91
  `SITE_URL=http://localhost:3000`,
@@ -70,18 +95,85 @@ export async function createCommand(name) {
70
95
  writeFileSync(resolve(projectDir, ".env"), envContent, "utf-8");
71
96
  console.log(" .env written (PAYLOAD_SECRET + SITE_URL)");
72
97
 
73
- // --- 5. Setup Coolify deployment ---
98
+ // --- 5. Get git remote URL ---
99
+ let gitRepo;
100
+ try {
101
+ gitRepo = runCapture("git remote get-url origin", { cwd: projectDir });
102
+ } catch {
103
+ console.error("Error: No git remote 'origin' found.");
104
+ process.exit(1);
105
+ }
106
+
107
+ // --- 6. Setup Coolify deployment ---
74
108
  console.log("\nSetting up Coolify deployment...");
75
- run(`npm run setup:deploy -- --name ${name}`, { cwd: projectDir });
109
+ console.log(` Domain: ${domain}`);
110
+ console.log(` Git repo: ${gitRepo}`);
111
+
112
+ // Create project
113
+ console.log(" Creating Coolify project...");
114
+ const project = await coolifyApi(config, "POST", "/projects", {
115
+ name,
116
+ description: `${name} — deployed via webhikers CLI`,
117
+ });
118
+ const projectUuid = project.uuid;
119
+ console.log(` Project created: ${projectUuid}`);
120
+
121
+ // Create application
122
+ console.log(" Creating application...");
123
+ const app = await coolifyApi(config, "POST", "/applications/public", {
124
+ project_uuid: projectUuid,
125
+ server_uuid: config.serverUuid,
126
+ environment_name: "production",
127
+ git_repository: gitRepo,
128
+ git_branch: "master",
129
+ build_pack: "dockerfile",
130
+ ports_exposes: "3000",
131
+ instant_deploy: false,
132
+ });
133
+ const appUuid = app.uuid;
134
+ console.log(` Application created: ${appUuid}`);
135
+
136
+ // Set domain
137
+ console.log(` Setting domain to ${domain}...`);
138
+ await coolifyApi(config, "PATCH", `/applications/${appUuid}`, {
139
+ domains: `https://${domain}`,
140
+ });
141
+
142
+ // Set environment variables
143
+ console.log(" Setting environment variables...");
144
+ await coolifyApi(config, "POST", `/applications/${appUuid}/envs`, {
145
+ key: "PAYLOAD_SECRET",
146
+ value: payloadSecret,
147
+ is_build_time: true,
148
+ });
149
+ await coolifyApi(config, "POST", `/applications/${appUuid}/envs`, {
150
+ key: "SITE_URL",
151
+ value: siteUrl,
152
+ is_build_time: true,
153
+ });
154
+
155
+ // --- 7. Write .deploy.json ---
156
+ const deployConfig = {
157
+ serverIp: config.serverIp,
158
+ domain,
159
+ coolifyProjectUuid: projectUuid,
160
+ coolifyAppUuid: appUuid,
161
+ };
162
+ writeFileSync(
163
+ resolve(projectDir, ".deploy.json"),
164
+ JSON.stringify(deployConfig, null, 2) + "\n",
165
+ "utf-8"
166
+ );
167
+ console.log(" .deploy.json written");
76
168
 
77
- // --- 6. Summary ---
169
+ // --- 8. Summary ---
78
170
  console.log("");
79
171
  console.log("=========================================");
80
172
  console.log(" Project created successfully!");
81
173
  console.log("=========================================");
82
174
  console.log("");
83
175
  console.log(` Repo: https://github.com/${GITHUB_ORG}/${name}`);
84
- console.log(` Domain: https://${name}.preview.webhikers.dev`);
176
+ console.log(` Domain: https://${domain}`);
85
177
  console.log(` Dir: ${projectDir}`);
86
178
  console.log("");
87
179
  console.log(" MANUAL STEP:");
@@ -1,17 +1,39 @@
1
- import { writeFileSync, chmodSync } from "fs";
1
+ import { readdirSync, readFileSync, existsSync } from "fs";
2
2
  import { join } from "path";
3
- import { getConfigDir } from "./config-store.js";
3
+ import { homedir } from "os";
4
4
 
5
- const SSH_KEY_FILENAME = "ssh_key";
5
+ const SSH_DIR = join(homedir(), ".ssh");
6
6
 
7
- export function getSSHKeyPath() {
8
- return join(getConfigDir(), SSH_KEY_FILENAME);
9
- }
7
+ export function findSSHKeys() {
8
+ if (!existsSync(SSH_DIR)) return [];
9
+
10
+ const files = readdirSync(SSH_DIR);
11
+ const keys = [];
12
+
13
+ for (const file of files) {
14
+ if (file.endsWith(".pub")) continue;
15
+ if (file === "known_hosts" || file === "config" || file === "authorized_keys") continue;
16
+ if (file.startsWith(".")) continue;
17
+
18
+ const privatePath = join(SSH_DIR, file);
19
+ const pubPath = privatePath + ".pub";
20
+
21
+ if (!existsSync(pubPath)) continue;
22
+
23
+ let comment = "";
24
+ try {
25
+ const pubContent = readFileSync(pubPath, "utf-8").trim();
26
+ const parts = pubContent.split(" ");
27
+ if (parts.length >= 3) {
28
+ comment = parts.slice(2).join(" ");
29
+ }
30
+ } catch {}
31
+
32
+ keys.push({
33
+ name: `~/.ssh/${file}${comment ? ` (${comment})` : ""}`,
34
+ value: privatePath,
35
+ });
36
+ }
10
37
 
11
- export function saveSSHKey(keyContent) {
12
- const keyPath = getSSHKeyPath();
13
- const normalized = keyContent.trim() + "\n";
14
- writeFileSync(keyPath, normalized, "utf-8");
15
- chmodSync(keyPath, 0o600);
16
- return keyPath;
38
+ return keys;
17
39
  }