@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 +1 -1
- package/src/commands/config.js +67 -31
- package/src/commands/create.js +97 -5
- package/src/utils/ssh-key.js +34 -12
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -5,19 +5,21 @@ import {
|
|
|
5
5
|
maskValue,
|
|
6
6
|
getConfigPath,
|
|
7
7
|
} from "../utils/config-store.js";
|
|
8
|
-
import {
|
|
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(`
|
|
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: ${
|
|
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: "
|
|
42
|
-
message: "
|
|
43
|
-
validate: (v) => (v.
|
|
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
|
|
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
|
|
56
|
-
validate: (v) => (v.length > 0 ? true : "
|
|
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
|
|
115
|
+
const coolifyUrl = `http://${answers.serverIp}:${answers.coolifyPort}`;
|
|
81
116
|
|
|
82
117
|
const config = {
|
|
83
|
-
|
|
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
|
|
124
|
+
sshKeyPath,
|
|
89
125
|
};
|
|
90
126
|
|
|
91
127
|
saveConfig(config);
|
|
92
128
|
|
|
93
129
|
console.log(`\nConfiguration saved to ${getConfigPath()}`);
|
|
94
|
-
console.log(`
|
|
130
|
+
console.log(`Coolify URL: ${coolifyUrl}`);
|
|
95
131
|
console.log("\nDone! You can now run: webhikers create <project-name>");
|
|
96
132
|
}
|
package/src/commands/create.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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://${
|
|
176
|
+
console.log(` Domain: https://${domain}`);
|
|
85
177
|
console.log(` Dir: ${projectDir}`);
|
|
86
178
|
console.log("");
|
|
87
179
|
console.log(" MANUAL STEP:");
|
package/src/utils/ssh-key.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "os";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const SSH_DIR = join(homedir(), ".ssh");
|
|
6
6
|
|
|
7
|
-
export function
|
|
8
|
-
|
|
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
|
-
|
|
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
|
}
|