@webhikers/cli 1.1.4 → 1.1.5
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/README.md +96 -4
- package/package.json +1 -1
- package/src/commands/create.js +66 -50
package/README.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# @webhikers/cli
|
|
2
2
|
|
|
3
|
-
CLI for creating and deploying webhikers projects.
|
|
3
|
+
CLI for creating and deploying webhikers projects on Hetzner/Coolify.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before using this CLI, you need:
|
|
8
|
+
|
|
9
|
+
1. **Hetzner Cloud Server** with Coolify installed
|
|
10
|
+
2. **Wildcard DNS** `*.preview.webhikers.dev` pointing to your server IP
|
|
11
|
+
3. **GitHub CLI** (`gh`) installed and authenticated
|
|
12
|
+
4. **Node.js** >= 18
|
|
13
|
+
5. **SSH Key** on your machine, added to the Hetzner server
|
|
14
|
+
|
|
15
|
+
### Server Setup (one-time)
|
|
16
|
+
|
|
17
|
+
1. Create a Hetzner CX23 server (Ubuntu 24.04)
|
|
18
|
+
2. Add your SSH public key during server creation
|
|
19
|
+
3. SSH into the server: `ssh -i ~/.ssh/your-key root@SERVER_IP`
|
|
20
|
+
4. Install Coolify: `curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash`
|
|
21
|
+
5. Open `http://SERVER_IP:8000`, create admin account
|
|
22
|
+
6. Enable API access: Settings → API Settings → API Access checkbox
|
|
23
|
+
7. Create API token: Keys & Tokens → create token with root permissions
|
|
24
|
+
8. Note your Server UUID: Servers → click server → UUID is in the URL
|
|
25
|
+
|
|
26
|
+
### DNS Setup (one-time)
|
|
27
|
+
|
|
28
|
+
Add a wildcard A record for your domain:
|
|
29
|
+
|
|
30
|
+
| Type | Name | Value |
|
|
31
|
+
|------|------|-------|
|
|
32
|
+
| A | `*.preview` | `SERVER_IP` |
|
|
33
|
+
|
|
34
|
+
This makes every project available at `project-name.preview.webhikers.dev`.
|
|
4
35
|
|
|
5
36
|
## Install
|
|
6
37
|
|
|
@@ -14,7 +45,18 @@ npm install -g @webhikers/cli
|
|
|
14
45
|
webhikers config
|
|
15
46
|
```
|
|
16
47
|
|
|
17
|
-
|
|
48
|
+
You will be asked for:
|
|
49
|
+
|
|
50
|
+
| Prompt | Where to find it |
|
|
51
|
+
|--------|-----------------|
|
|
52
|
+
| Server IP | Hetzner Cloud Console → Server |
|
|
53
|
+
| Coolify Port | Default: 8000 |
|
|
54
|
+
| Coolify API Token | Coolify UI → Keys & Tokens |
|
|
55
|
+
| Server UUID | Coolify UI → Servers → UUID in URL |
|
|
56
|
+
| SSH User | Default: root |
|
|
57
|
+
| SSH Key | Select from detected keys on your machine |
|
|
58
|
+
|
|
59
|
+
Config is saved to `~/.config/webhikers/config.json`.
|
|
18
60
|
|
|
19
61
|
## Create a new project
|
|
20
62
|
|
|
@@ -23,17 +65,67 @@ webhikers create my-project
|
|
|
23
65
|
```
|
|
24
66
|
|
|
25
67
|
This will:
|
|
68
|
+
|
|
26
69
|
1. Create a private GitHub repo from the `nextjs-vibe-starter` template
|
|
27
70
|
2. Clone it and install dependencies
|
|
28
71
|
3. Generate `.env` with a random `PAYLOAD_SECRET`
|
|
29
|
-
4. Create a Coolify project
|
|
72
|
+
4. Create a Coolify project with domain `my-project.preview.webhikers.dev`
|
|
73
|
+
5. Set environment variables in Coolify
|
|
74
|
+
6. Trigger the first deploy
|
|
30
75
|
|
|
31
|
-
After creation, add persistent volumes in Coolify UI
|
|
76
|
+
After creation, add persistent volumes in Coolify UI:
|
|
77
|
+
|
|
78
|
+
1. Open Coolify → Projects → select your project
|
|
79
|
+
2. Go to **Storages** tab
|
|
80
|
+
3. Add volume: `/app/data` (SQLite database)
|
|
81
|
+
4. Add volume: `/app/public/media` (uploaded images)
|
|
82
|
+
5. Redeploy the application
|
|
83
|
+
|
|
84
|
+
Then start local development:
|
|
32
85
|
|
|
33
86
|
```bash
|
|
34
87
|
cd my-project && npm run dev
|
|
35
88
|
```
|
|
36
89
|
|
|
90
|
+
## Architecture
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
~/.config/webhikers/
|
|
94
|
+
config.json ← Coolify token, server IP, SSH config (global)
|
|
95
|
+
ssh_key ← SSH private key (chmod 600)
|
|
96
|
+
|
|
97
|
+
project/
|
|
98
|
+
.env ← PAYLOAD_SECRET + SITE_URL (local dev, gitignored)
|
|
99
|
+
.deploy.json ← Server IP, domain, Coolify UUIDs (gitignored)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- **Local dev:** `npm run dev` → localhost:3000
|
|
103
|
+
- **Sync from prod:** `npm run sync:pull` (git pull + DB + media)
|
|
104
|
+
- **Deploy:** `npm run sync:push` (media push + git push → Coolify builds)
|
|
105
|
+
- **Coolify** handles Docker build, Payload migrations, and seed on every deploy
|
|
106
|
+
|
|
107
|
+
## Adding a team member
|
|
108
|
+
|
|
109
|
+
Your team member creates their own SSH key and sends you the public key (`.pub` file). Then:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ssh -i ~/.ssh/your-key root@SERVER_IP
|
|
113
|
+
echo "their-public-key-content" >> /root/.ssh/authorized_keys
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
They install the CLI and run `webhikers config` with the same server details.
|
|
117
|
+
|
|
118
|
+
## Scaling
|
|
119
|
+
|
|
120
|
+
| Server | RAM | Capacity |
|
|
121
|
+
|--------|-----|----------|
|
|
122
|
+
| CX23 | 4 GB | 5-10 sites |
|
|
123
|
+
| CX33 | 8 GB | 20-30 sites |
|
|
124
|
+
| CX43 | 16 GB | 50-80 sites |
|
|
125
|
+
| CX53 | 32 GB | 100-150 sites |
|
|
126
|
+
|
|
127
|
+
Upgrade via Hetzner Console (30 sec, no data loss). For 150+ sites, add a second server — Coolify manages multiple servers natively.
|
|
128
|
+
|
|
37
129
|
## License
|
|
38
130
|
|
|
39
131
|
MIT
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import { existsSync, writeFileSync
|
|
2
|
+
import { existsSync, writeFileSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import { loadConfig, getConfigPath } from "../utils/config-store.js";
|
|
5
5
|
|
|
@@ -7,8 +7,18 @@ const TEMPLATE_REPO = "Webhikers/nextjs-vibe-starter";
|
|
|
7
7
|
const GITHUB_ORG = "Webhikers";
|
|
8
8
|
const DOMAIN_SUFFIX = "preview.webhikers.dev";
|
|
9
9
|
|
|
10
|
+
// ANSI colors
|
|
11
|
+
const c = {
|
|
12
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
13
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
14
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
15
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
16
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
17
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
18
|
+
};
|
|
19
|
+
|
|
10
20
|
function run(cmd, options = {}) {
|
|
11
|
-
console.log(` → ${cmd}`);
|
|
21
|
+
console.log(c.dim(` → ${cmd}`));
|
|
12
22
|
return execSync(cmd, { stdio: "inherit", ...options });
|
|
13
23
|
}
|
|
14
24
|
|
|
@@ -39,12 +49,12 @@ async function coolifyApi(config, method, path, body) {
|
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export async function createCommand(name) {
|
|
42
|
-
console.log(`\nCreating project: ${name}\n`);
|
|
52
|
+
console.log(c.bold(`\nCreating project: ${name}\n`));
|
|
43
53
|
|
|
44
54
|
// --- 1. Check prerequisites ---
|
|
45
55
|
const config = loadConfig();
|
|
46
56
|
if (!config) {
|
|
47
|
-
console.error(`Error: ${getConfigPath()} not found.`);
|
|
57
|
+
console.error(c.red(`Error: ${getConfigPath()} not found.`));
|
|
48
58
|
console.error("Run 'webhikers config' first.");
|
|
49
59
|
process.exit(1);
|
|
50
60
|
}
|
|
@@ -52,7 +62,7 @@ export async function createCommand(name) {
|
|
|
52
62
|
try {
|
|
53
63
|
runCapture("which gh");
|
|
54
64
|
} catch {
|
|
55
|
-
console.error("Error: GitHub CLI (gh) not found.");
|
|
65
|
+
console.error(c.red("Error: GitHub CLI (gh) not found."));
|
|
56
66
|
console.error("Install it: https://cli.github.com");
|
|
57
67
|
process.exit(1);
|
|
58
68
|
}
|
|
@@ -60,21 +70,19 @@ export async function createCommand(name) {
|
|
|
60
70
|
try {
|
|
61
71
|
runCapture("gh auth status");
|
|
62
72
|
} catch {
|
|
63
|
-
console.error("Error: Not logged into GitHub CLI.");
|
|
73
|
+
console.error(c.red("Error: Not logged into GitHub CLI."));
|
|
64
74
|
console.error("Run: gh auth login");
|
|
65
75
|
process.exit(1);
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
// --- 2. Create GitHub repo from template ---
|
|
69
|
-
console.log("Creating GitHub repo from template...");
|
|
79
|
+
console.log(c.cyan("1/6 Creating GitHub repo from template..."));
|
|
70
80
|
|
|
71
|
-
// Create repo first (without --clone, to avoid race condition)
|
|
72
81
|
run(
|
|
73
82
|
`gh repo create ${GITHUB_ORG}/${name} --template ${TEMPLATE_REPO} --private`
|
|
74
83
|
);
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
console.log(" Waiting for GitHub to prepare the repository...");
|
|
85
|
+
console.log(c.dim(" Waiting for GitHub to prepare the repository..."));
|
|
78
86
|
const maxRetries = 12;
|
|
79
87
|
let cloned = false;
|
|
80
88
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -84,28 +92,30 @@ export async function createCommand(name) {
|
|
|
84
92
|
cloned = true;
|
|
85
93
|
break;
|
|
86
94
|
} catch {
|
|
87
|
-
console.log(` Retrying clone... (${i + 1}/${maxRetries})`);
|
|
95
|
+
console.log(c.dim(` Retrying clone... (${i + 1}/${maxRetries})`));
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
if (!cloned) {
|
|
92
|
-
console.error("Error: Could not clone repo after 60 seconds.");
|
|
100
|
+
console.error(c.red("Error: Could not clone repo after 60 seconds."));
|
|
93
101
|
console.error(`Try manually: gh repo clone ${GITHUB_ORG}/${name}`);
|
|
94
102
|
process.exit(1);
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
const projectDir = resolve(process.cwd(), name);
|
|
98
106
|
if (!existsSync(projectDir)) {
|
|
99
|
-
console.error(`Error: Expected directory ${projectDir} not found.`);
|
|
107
|
+
console.error(c.red(`Error: Expected directory ${projectDir} not found.`));
|
|
100
108
|
process.exit(1);
|
|
101
109
|
}
|
|
110
|
+
console.log(c.green(" ✓ Repo created and cloned"));
|
|
102
111
|
|
|
103
112
|
// --- 3. npm install ---
|
|
104
|
-
console.log("\
|
|
113
|
+
console.log(c.cyan("\n2/6 Installing dependencies..."));
|
|
105
114
|
run("npm install", { cwd: projectDir });
|
|
115
|
+
console.log(c.green(" ✓ Dependencies installed"));
|
|
106
116
|
|
|
107
117
|
// --- 4. Generate .env ---
|
|
108
|
-
console.log("\
|
|
118
|
+
console.log(c.cyan("\n3/6 Generating .env..."));
|
|
109
119
|
const payloadSecret = runCapture("openssl rand -hex 32");
|
|
110
120
|
const domain = `${name}.${DOMAIN_SUFFIX}`;
|
|
111
121
|
const siteUrl = `https://${domain}`;
|
|
@@ -116,33 +126,29 @@ export async function createCommand(name) {
|
|
|
116
126
|
].join("\n");
|
|
117
127
|
|
|
118
128
|
writeFileSync(resolve(projectDir, ".env"), envContent, "utf-8");
|
|
119
|
-
console.log(" .env written
|
|
129
|
+
console.log(c.green(" ✓ .env written"));
|
|
120
130
|
|
|
121
131
|
// --- 5. Get git remote URL ---
|
|
122
132
|
let gitRepo;
|
|
123
133
|
try {
|
|
124
134
|
gitRepo = runCapture("git remote get-url origin", { cwd: projectDir });
|
|
125
135
|
} catch {
|
|
126
|
-
console.error("Error: No git remote 'origin' found.");
|
|
136
|
+
console.error(c.red("Error: No git remote 'origin' found."));
|
|
127
137
|
process.exit(1);
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
// --- 6. Setup Coolify deployment ---
|
|
131
|
-
console.log("\
|
|
132
|
-
console.log(` Domain: ${domain}`);
|
|
133
|
-
console.log(` Git repo: ${gitRepo}`);
|
|
141
|
+
console.log(c.cyan("\n4/6 Setting up Coolify deployment..."));
|
|
142
|
+
console.log(` Domain: ${c.bold(domain)}`);
|
|
134
143
|
|
|
135
|
-
|
|
136
|
-
console.log(" Creating Coolify project...");
|
|
144
|
+
console.log(c.dim(" Creating Coolify project..."));
|
|
137
145
|
const project = await coolifyApi(config, "POST", "/projects", {
|
|
138
146
|
name,
|
|
139
147
|
description: `${name} - deployed via webhikers CLI`,
|
|
140
148
|
});
|
|
141
149
|
const projectUuid = project.uuid;
|
|
142
|
-
console.log(` Project created: ${projectUuid}`);
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
console.log(" Creating application...");
|
|
151
|
+
console.log(c.dim(" Creating application..."));
|
|
146
152
|
const app = await coolifyApi(config, "POST", "/applications/public", {
|
|
147
153
|
project_uuid: projectUuid,
|
|
148
154
|
server_uuid: config.serverUuid,
|
|
@@ -154,16 +160,13 @@ export async function createCommand(name) {
|
|
|
154
160
|
instant_deploy: false,
|
|
155
161
|
});
|
|
156
162
|
const appUuid = app.uuid;
|
|
157
|
-
console.log(` Application created: ${appUuid}`);
|
|
158
163
|
|
|
159
|
-
|
|
160
|
-
console.log(` Setting domain to ${domain}...`);
|
|
164
|
+
console.log(c.dim(` Setting domain...`));
|
|
161
165
|
await coolifyApi(config, "PATCH", `/applications/${appUuid}`, {
|
|
162
166
|
domains: `https://${domain}`,
|
|
163
167
|
});
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
console.log(" Setting environment variables...");
|
|
169
|
+
console.log(c.dim(" Setting environment variables..."));
|
|
167
170
|
await coolifyApi(config, "POST", `/applications/${appUuid}/envs`, {
|
|
168
171
|
key: "PAYLOAD_SECRET",
|
|
169
172
|
value: payloadSecret,
|
|
@@ -174,8 +177,10 @@ export async function createCommand(name) {
|
|
|
174
177
|
value: siteUrl,
|
|
175
178
|
is_preview: false,
|
|
176
179
|
});
|
|
180
|
+
console.log(c.green(" ✓ Coolify project configured"));
|
|
177
181
|
|
|
178
182
|
// --- 7. Write .deploy.json ---
|
|
183
|
+
console.log(c.cyan("\n5/6 Writing .deploy.json..."));
|
|
179
184
|
const deployConfig = {
|
|
180
185
|
serverIp: config.serverIp,
|
|
181
186
|
domain,
|
|
@@ -187,24 +192,35 @@ export async function createCommand(name) {
|
|
|
187
192
|
JSON.stringify(deployConfig, null, 2) + "\n",
|
|
188
193
|
"utf-8"
|
|
189
194
|
);
|
|
190
|
-
console.log(" .deploy.json written");
|
|
191
|
-
|
|
192
|
-
// --- 8.
|
|
193
|
-
console.log("");
|
|
194
|
-
|
|
195
|
-
console.log("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
console.log(
|
|
199
|
-
console.log(
|
|
200
|
-
console.log(
|
|
201
|
-
|
|
202
|
-
console.log("
|
|
203
|
-
console.log(
|
|
204
|
-
console.log("
|
|
205
|
-
console.log(
|
|
206
|
-
|
|
207
|
-
console.log("");
|
|
208
|
-
console.log(
|
|
209
|
-
console.log("
|
|
195
|
+
console.log(c.green(" ✓ .deploy.json written"));
|
|
196
|
+
|
|
197
|
+
// --- 8. Trigger first deploy ---
|
|
198
|
+
console.log(c.cyan("\n6/6 Triggering first deploy..."));
|
|
199
|
+
await coolifyApi(config, "POST", `/applications/${appUuid}/start`);
|
|
200
|
+
console.log(c.green(" ✓ Deploy triggered on Coolify"));
|
|
201
|
+
|
|
202
|
+
// --- 9. Summary ---
|
|
203
|
+
console.log("\n" + c.green("═══════════════════════════════════════════"));
|
|
204
|
+
console.log(c.green(c.bold(" Project created successfully!")));
|
|
205
|
+
console.log(c.green("═══════════════════════════════════════════\n"));
|
|
206
|
+
|
|
207
|
+
console.log(` ${c.bold("Repo:")} https://github.com/${GITHUB_ORG}/${name}`);
|
|
208
|
+
console.log(` ${c.bold("Domain:")} https://${domain}`);
|
|
209
|
+
console.log(` ${c.bold("Coolify:")} ${config.coolifyUrl}`);
|
|
210
|
+
console.log(` ${c.bold("Dir:")} ${projectDir}`);
|
|
211
|
+
|
|
212
|
+
console.log(c.yellow("\n ┌─────────────────────────────────────────────┐"));
|
|
213
|
+
console.log(c.yellow(" │ MANUAL STEP: Add persistent volumes │"));
|
|
214
|
+
console.log(c.yellow(" ├─────────────────────────────────────────────┤"));
|
|
215
|
+
console.log(c.yellow(" │ │"));
|
|
216
|
+
console.log(c.yellow(" │ 1. Open Coolify UI → Projects │"));
|
|
217
|
+
console.log(c.yellow(` │ 2. Select "${name}" │`));
|
|
218
|
+
console.log(c.yellow(" │ 3. Go to Storages tab │"));
|
|
219
|
+
console.log(c.yellow(" │ 4. Add volume: /app/data (SQLite DB) │"));
|
|
220
|
+
console.log(c.yellow(" │ 5. Add volume: /app/public/media (Uploads) │"));
|
|
221
|
+
console.log(c.yellow(" │ 6. Redeploy the application │"));
|
|
222
|
+
console.log(c.yellow(" │ │"));
|
|
223
|
+
console.log(c.yellow(" └─────────────────────────────────────────────┘"));
|
|
224
|
+
|
|
225
|
+
console.log(`\n ${c.bold("Next:")} cd ${name} && npm run dev\n`);
|
|
210
226
|
}
|