@webhikers/cli 1.1.3 → 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 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
- Configures Coolify API token, server credentials, and SSH key.
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 and configure the domain (`my-project.webhikers.site`)
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, then start developing:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhikers/cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "CLI for creating and deploying webhikers projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { existsSync, writeFileSync, readFileSync } from "fs";
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
- // Wait for GitHub to finish creating the repo from template
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("\nInstalling dependencies...");
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("\nGenerating .env...");
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 (PAYLOAD_SECRET + SITE_URL)");
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("\nSetting up Coolify deployment...");
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
- // Create project
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
- // Create application
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,28 +160,27 @@ 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
- // Set domain
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
- // Set environment variables
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,
170
- is_build_time: true,
173
+ is_preview: false,
171
174
  });
172
175
  await coolifyApi(config, "POST", `/applications/${appUuid}/envs`, {
173
176
  key: "SITE_URL",
174
177
  value: siteUrl,
175
- is_build_time: true,
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. Summary ---
193
- console.log("");
194
- console.log("=========================================");
195
- console.log(" Project created successfully!");
196
- console.log("=========================================");
197
- console.log("");
198
- console.log(` Repo: https://github.com/${GITHUB_ORG}/${name}`);
199
- console.log(` Domain: https://${domain}`);
200
- console.log(` Dir: ${projectDir}`);
201
- console.log("");
202
- console.log(" MANUAL STEP:");
203
- console.log(" Open Coolify UI → Application → Storages");
204
- console.log(" Add two volumes:");
205
- console.log(" 1. /app/data (SQLite database)");
206
- console.log(" 2. /app/public/media (uploaded images)");
207
- console.log("");
208
- console.log(` Ready! Run: cd ${name} && npm run dev`);
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
  }