bindler 1.0.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/cli.js +2211 -0
- package/dist/cli.js.map +1 -0
- package/package.json +51 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/new.ts
|
|
7
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
8
|
+
import { basename } from "path";
|
|
9
|
+
import inquirer from "inquirer";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
// src/lib/config.ts
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
var CONFIG_DIR = join(homedir(), ".config", "bindler");
|
|
17
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
18
|
+
var GENERATED_DIR = join(CONFIG_DIR, "generated");
|
|
19
|
+
var BACKUP_DIR = join(CONFIG_DIR, "backups");
|
|
20
|
+
function getConfigPath() {
|
|
21
|
+
return CONFIG_PATH;
|
|
22
|
+
}
|
|
23
|
+
function getGeneratedDir() {
|
|
24
|
+
return GENERATED_DIR;
|
|
25
|
+
}
|
|
26
|
+
function getNginxConfigPath() {
|
|
27
|
+
const isMac = process.platform === "darwin";
|
|
28
|
+
if (isMac) {
|
|
29
|
+
if (existsSync("/opt/homebrew/etc/nginx/servers")) {
|
|
30
|
+
return "/opt/homebrew/etc/nginx/servers/bindler.conf";
|
|
31
|
+
}
|
|
32
|
+
if (existsSync("/usr/local/etc/nginx/servers")) {
|
|
33
|
+
return "/usr/local/etc/nginx/servers/bindler.conf";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "/etc/nginx/conf.d/bindler.conf";
|
|
37
|
+
}
|
|
38
|
+
function getDefaultConfig() {
|
|
39
|
+
return {
|
|
40
|
+
version: 1,
|
|
41
|
+
defaults: {
|
|
42
|
+
projectsRoot: join(homedir(), "projects"),
|
|
43
|
+
nginxManagedPath: getNginxConfigPath(),
|
|
44
|
+
nginxListen: "127.0.0.1:8080",
|
|
45
|
+
tunnelName: "homelab",
|
|
46
|
+
applyCloudflareDnsRoutes: true,
|
|
47
|
+
mode: "tunnel"
|
|
48
|
+
},
|
|
49
|
+
projects: []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function ensureConfigDirs() {
|
|
53
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
54
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
if (!existsSync(GENERATED_DIR)) {
|
|
57
|
+
mkdirSync(GENERATED_DIR, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
if (!existsSync(BACKUP_DIR)) {
|
|
60
|
+
mkdirSync(BACKUP_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function configExists() {
|
|
64
|
+
return existsSync(CONFIG_PATH);
|
|
65
|
+
}
|
|
66
|
+
function readConfig() {
|
|
67
|
+
if (!configExists()) {
|
|
68
|
+
return getDefaultConfig();
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
72
|
+
const config = JSON.parse(content);
|
|
73
|
+
return config;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Failed to read config: ${error instanceof Error ? error.message : error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeConfig(config) {
|
|
79
|
+
ensureConfigDirs();
|
|
80
|
+
if (configExists()) {
|
|
81
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
82
|
+
const backupPath = join(BACKUP_DIR, `config-${timestamp}.json`);
|
|
83
|
+
copyFileSync(CONFIG_PATH, backupPath);
|
|
84
|
+
}
|
|
85
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
86
|
+
}
|
|
87
|
+
function initConfig() {
|
|
88
|
+
if (configExists()) {
|
|
89
|
+
return readConfig();
|
|
90
|
+
}
|
|
91
|
+
const config = getDefaultConfig();
|
|
92
|
+
writeConfig(config);
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
function getProject(name) {
|
|
96
|
+
const config = readConfig();
|
|
97
|
+
return config.projects.find((p) => p.name === name);
|
|
98
|
+
}
|
|
99
|
+
function addProject(project) {
|
|
100
|
+
const config = readConfig();
|
|
101
|
+
if (config.projects.some((p) => p.name === project.name)) {
|
|
102
|
+
throw new Error(`Project "${project.name}" already exists`);
|
|
103
|
+
}
|
|
104
|
+
if (config.projects.some((p) => p.hostname === project.hostname)) {
|
|
105
|
+
throw new Error(`Hostname "${project.hostname}" is already in use`);
|
|
106
|
+
}
|
|
107
|
+
config.projects.push(project);
|
|
108
|
+
writeConfig(config);
|
|
109
|
+
}
|
|
110
|
+
function updateProject(name, updates) {
|
|
111
|
+
const config = readConfig();
|
|
112
|
+
const index = config.projects.findIndex((p) => p.name === name);
|
|
113
|
+
if (index === -1) {
|
|
114
|
+
throw new Error(`Project "${name}" not found`);
|
|
115
|
+
}
|
|
116
|
+
if (updates.hostname && updates.hostname !== config.projects[index].hostname) {
|
|
117
|
+
if (config.projects.some((p) => p.hostname === updates.hostname)) {
|
|
118
|
+
throw new Error(`Hostname "${updates.hostname}" is already in use`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
config.projects[index] = { ...config.projects[index], ...updates };
|
|
122
|
+
writeConfig(config);
|
|
123
|
+
}
|
|
124
|
+
function removeProject(name) {
|
|
125
|
+
const config = readConfig();
|
|
126
|
+
const index = config.projects.findIndex((p) => p.name === name);
|
|
127
|
+
if (index === -1) {
|
|
128
|
+
throw new Error(`Project "${name}" not found`);
|
|
129
|
+
}
|
|
130
|
+
const [removed] = config.projects.splice(index, 1);
|
|
131
|
+
writeConfig(config);
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
function listProjects() {
|
|
135
|
+
const config = readConfig();
|
|
136
|
+
return config.projects;
|
|
137
|
+
}
|
|
138
|
+
function getDefaults() {
|
|
139
|
+
const config = readConfig();
|
|
140
|
+
return config.defaults;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/lib/utils.ts
|
|
144
|
+
import { execSync, spawn } from "child_process";
|
|
145
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
146
|
+
import { join as join2 } from "path";
|
|
147
|
+
import { createConnection } from "net";
|
|
148
|
+
function execCommandSafe(command, options) {
|
|
149
|
+
try {
|
|
150
|
+
const output = execSync(command, {
|
|
151
|
+
encoding: "utf-8",
|
|
152
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
153
|
+
...options
|
|
154
|
+
}).trim();
|
|
155
|
+
return { success: true, output };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof Error && "stderr" in error) {
|
|
158
|
+
return { success: false, output: "", error: error.stderr || error.message };
|
|
159
|
+
}
|
|
160
|
+
return { success: false, output: "", error: String(error) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function spawnInteractive(command, args, options) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const child = spawn(command, args, {
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
...options
|
|
168
|
+
});
|
|
169
|
+
child.on("close", (code) => {
|
|
170
|
+
resolve(code);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function isPortListening(port, host = "127.0.0.1") {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const socket = createConnection({ port, host }, () => {
|
|
177
|
+
socket.destroy();
|
|
178
|
+
resolve(true);
|
|
179
|
+
});
|
|
180
|
+
socket.on("error", () => {
|
|
181
|
+
resolve(false);
|
|
182
|
+
});
|
|
183
|
+
socket.setTimeout(1e3, () => {
|
|
184
|
+
socket.destroy();
|
|
185
|
+
resolve(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function detectProjectType(path) {
|
|
190
|
+
const packageJsonPath = join2(path, "package.json");
|
|
191
|
+
return existsSync2(packageJsonPath) ? "npm" : "static";
|
|
192
|
+
}
|
|
193
|
+
function getPackageJsonScripts(path) {
|
|
194
|
+
const packageJsonPath = join2(path, "package.json");
|
|
195
|
+
if (!existsSync2(packageJsonPath)) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const content = readFileSync2(packageJsonPath, "utf-8");
|
|
200
|
+
const pkg = JSON.parse(content);
|
|
201
|
+
return Object.keys(pkg.scripts || {});
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function validateHostname(hostname) {
|
|
207
|
+
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
|
208
|
+
return hostnameRegex.test(hostname) && hostname.length <= 253;
|
|
209
|
+
}
|
|
210
|
+
function validateProjectName(name) {
|
|
211
|
+
const nameRegex = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
212
|
+
return nameRegex.test(name) && name.length <= 64;
|
|
213
|
+
}
|
|
214
|
+
function validatePort(port) {
|
|
215
|
+
return Number.isInteger(port) && port >= 1024 && port <= 65535;
|
|
216
|
+
}
|
|
217
|
+
function formatUptime(ms) {
|
|
218
|
+
const seconds = Math.floor(ms / 1e3);
|
|
219
|
+
const minutes = Math.floor(seconds / 60);
|
|
220
|
+
const hours = Math.floor(minutes / 60);
|
|
221
|
+
const days = Math.floor(hours / 24);
|
|
222
|
+
if (days > 0) {
|
|
223
|
+
return `${days}d ${hours % 24}h`;
|
|
224
|
+
}
|
|
225
|
+
if (hours > 0) {
|
|
226
|
+
return `${hours}h ${minutes % 60}m`;
|
|
227
|
+
}
|
|
228
|
+
if (minutes > 0) {
|
|
229
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
230
|
+
}
|
|
231
|
+
return `${seconds}s`;
|
|
232
|
+
}
|
|
233
|
+
function formatBytes(bytes) {
|
|
234
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
235
|
+
let value = bytes;
|
|
236
|
+
let unitIndex = 0;
|
|
237
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
238
|
+
value /= 1024;
|
|
239
|
+
unitIndex++;
|
|
240
|
+
}
|
|
241
|
+
return `${value.toFixed(1)}${units[unitIndex]}`;
|
|
242
|
+
}
|
|
243
|
+
function getPm2ProcessName(projectName) {
|
|
244
|
+
return `bindler:${projectName}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/lib/ports.ts
|
|
248
|
+
var PORT_RANGE_START = 3e3;
|
|
249
|
+
var PORT_RANGE_END = 9e3;
|
|
250
|
+
function getUsedPorts() {
|
|
251
|
+
const config = readConfig();
|
|
252
|
+
return config.projects.filter((p) => p.type === "npm" && p.port).map((p) => p.port);
|
|
253
|
+
}
|
|
254
|
+
function findAvailablePort(startFrom = PORT_RANGE_START) {
|
|
255
|
+
const usedPorts = new Set(getUsedPorts());
|
|
256
|
+
for (let port = startFrom; port <= PORT_RANGE_END; port++) {
|
|
257
|
+
if (!usedPorts.has(port)) {
|
|
258
|
+
return port;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`No available ports in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
|
|
262
|
+
}
|
|
263
|
+
function isPortAvailable(port) {
|
|
264
|
+
const usedPorts = new Set(getUsedPorts());
|
|
265
|
+
return !usedPorts.has(port);
|
|
266
|
+
}
|
|
267
|
+
function getPortsTable() {
|
|
268
|
+
const config = readConfig();
|
|
269
|
+
return config.projects.filter((p) => p.type === "npm" && p.port).map((p) => ({
|
|
270
|
+
port: p.port,
|
|
271
|
+
project: p.name,
|
|
272
|
+
hostname: p.hostname
|
|
273
|
+
})).sort((a, b) => a.port - b.port);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/lib/nginx.ts
|
|
277
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
278
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
279
|
+
function generateLocationBlock(project, indent = " ") {
|
|
280
|
+
const lines = [];
|
|
281
|
+
const locationPath = project.basePath || "/";
|
|
282
|
+
const isRootLocation = locationPath === "/";
|
|
283
|
+
if (project.type === "static") {
|
|
284
|
+
lines.push(`${indent}location ${locationPath} {`);
|
|
285
|
+
if (isRootLocation) {
|
|
286
|
+
lines.push(`${indent} root ${project.path};`);
|
|
287
|
+
} else {
|
|
288
|
+
lines.push(`${indent} alias ${project.path}/;`);
|
|
289
|
+
}
|
|
290
|
+
lines.push(`${indent} index index.html index.htm;`);
|
|
291
|
+
lines.push(`${indent} try_files $uri $uri/ =404;`);
|
|
292
|
+
lines.push(`${indent}}`);
|
|
293
|
+
} else if (project.type === "npm") {
|
|
294
|
+
lines.push(`${indent}location ${locationPath} {`);
|
|
295
|
+
lines.push(`${indent} proxy_pass http://127.0.0.1:${project.port};`);
|
|
296
|
+
lines.push(`${indent} proxy_http_version 1.1;`);
|
|
297
|
+
lines.push(`${indent} proxy_set_header Upgrade $http_upgrade;`);
|
|
298
|
+
lines.push(`${indent} proxy_set_header Connection 'upgrade';`);
|
|
299
|
+
lines.push(`${indent} proxy_set_header Host $host;`);
|
|
300
|
+
lines.push(`${indent} proxy_set_header X-Real-IP $remote_addr;`);
|
|
301
|
+
lines.push(`${indent} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
|
|
302
|
+
lines.push(`${indent} proxy_set_header X-Forwarded-Proto $scheme;`);
|
|
303
|
+
lines.push(`${indent} proxy_cache_bypass $http_upgrade;`);
|
|
304
|
+
lines.push(`${indent}}`);
|
|
305
|
+
}
|
|
306
|
+
return lines;
|
|
307
|
+
}
|
|
308
|
+
function generateNginxConfig(config) {
|
|
309
|
+
const { defaults, projects } = config;
|
|
310
|
+
const listen = defaults.nginxListen;
|
|
311
|
+
const lines = [
|
|
312
|
+
"# Generated by bindler - DO NOT EDIT MANUALLY",
|
|
313
|
+
`# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
314
|
+
""
|
|
315
|
+
];
|
|
316
|
+
const hostGroups = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const project of projects) {
|
|
318
|
+
if (project.enabled === false) {
|
|
319
|
+
lines.push(`# Project "${project.name}" is disabled`);
|
|
320
|
+
lines.push("");
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const existing = hostGroups.get(project.hostname) || [];
|
|
324
|
+
existing.push(project);
|
|
325
|
+
hostGroups.set(project.hostname, existing);
|
|
326
|
+
}
|
|
327
|
+
for (const [hostname, hostProjects] of hostGroups) {
|
|
328
|
+
const projectNames = hostProjects.map((p) => p.name).join(", ");
|
|
329
|
+
lines.push(`# Hostname: ${hostname} (${projectNames})`);
|
|
330
|
+
lines.push("server {");
|
|
331
|
+
lines.push(` listen ${listen};`);
|
|
332
|
+
lines.push(` server_name ${hostname};`);
|
|
333
|
+
lines.push("");
|
|
334
|
+
const sortedProjects = [...hostProjects].sort((a, b) => {
|
|
335
|
+
const pathA = a.basePath || "/";
|
|
336
|
+
const pathB = b.basePath || "/";
|
|
337
|
+
return pathB.length - pathA.length;
|
|
338
|
+
});
|
|
339
|
+
for (const project of sortedProjects) {
|
|
340
|
+
lines.push(` # Project: ${project.name} (${project.type})`);
|
|
341
|
+
lines.push(...generateLocationBlock(project));
|
|
342
|
+
lines.push("");
|
|
343
|
+
}
|
|
344
|
+
lines.push("}");
|
|
345
|
+
lines.push("");
|
|
346
|
+
}
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
function writeNginxConfig(config, dryRun = false) {
|
|
350
|
+
const nginxConfig = generateNginxConfig(config);
|
|
351
|
+
const targetPath = config.defaults.nginxManagedPath;
|
|
352
|
+
if (dryRun) {
|
|
353
|
+
return { path: targetPath, content: nginxConfig };
|
|
354
|
+
}
|
|
355
|
+
const targetDir = dirname2(targetPath);
|
|
356
|
+
if (!existsSync3(targetDir)) {
|
|
357
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
358
|
+
}
|
|
359
|
+
if (existsSync3(targetPath)) {
|
|
360
|
+
const backupDir = join3(getGeneratedDir(), "backups");
|
|
361
|
+
if (!existsSync3(backupDir)) {
|
|
362
|
+
mkdirSync2(backupDir, { recursive: true });
|
|
363
|
+
}
|
|
364
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
365
|
+
const backupPath = join3(backupDir, `nginx-${timestamp}.conf`);
|
|
366
|
+
copyFileSync2(targetPath, backupPath);
|
|
367
|
+
}
|
|
368
|
+
const generatedPath = join3(getGeneratedDir(), "nginx.conf");
|
|
369
|
+
writeFileSync2(generatedPath, nginxConfig);
|
|
370
|
+
writeFileSync2(targetPath, nginxConfig);
|
|
371
|
+
return { path: targetPath, content: nginxConfig };
|
|
372
|
+
}
|
|
373
|
+
function testNginxConfig() {
|
|
374
|
+
const result = execCommandSafe("nginx -t 2>&1");
|
|
375
|
+
return {
|
|
376
|
+
success: result.success || result.output.includes("syntax is ok"),
|
|
377
|
+
output: result.output || result.error || ""
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function reloadNginx() {
|
|
381
|
+
const isMac = process.platform === "darwin";
|
|
382
|
+
if (isMac) {
|
|
383
|
+
let result2 = execCommandSafe("brew services reload nginx");
|
|
384
|
+
if (result2.success) {
|
|
385
|
+
return { success: true };
|
|
386
|
+
}
|
|
387
|
+
result2 = execCommandSafe("nginx -s reload");
|
|
388
|
+
if (result2.success) {
|
|
389
|
+
return { success: true };
|
|
390
|
+
}
|
|
391
|
+
return { success: false, error: result2.error || "Failed to reload nginx. Try: brew services restart nginx" };
|
|
392
|
+
}
|
|
393
|
+
let result = execCommandSafe("sudo systemctl reload nginx");
|
|
394
|
+
if (result.success) {
|
|
395
|
+
return { success: true };
|
|
396
|
+
}
|
|
397
|
+
result = execCommandSafe("sudo service nginx reload");
|
|
398
|
+
if (result.success) {
|
|
399
|
+
return { success: true };
|
|
400
|
+
}
|
|
401
|
+
result = execCommandSafe("sudo nginx -s reload");
|
|
402
|
+
if (result.success) {
|
|
403
|
+
return { success: true };
|
|
404
|
+
}
|
|
405
|
+
return { success: false, error: result.error || "Failed to reload nginx" };
|
|
406
|
+
}
|
|
407
|
+
function isNginxInstalled() {
|
|
408
|
+
const result = execCommandSafe("which nginx");
|
|
409
|
+
return result.success;
|
|
410
|
+
}
|
|
411
|
+
function isNginxRunning() {
|
|
412
|
+
const isMac = process.platform === "darwin";
|
|
413
|
+
if (isMac) {
|
|
414
|
+
let result2 = execCommandSafe("brew services list | grep nginx");
|
|
415
|
+
if (result2.success && result2.output.includes("started")) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
let result2 = execCommandSafe("systemctl is-active nginx");
|
|
420
|
+
if (result2.success && result2.output === "active") {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const result = execCommandSafe("pgrep nginx");
|
|
425
|
+
return result.success;
|
|
426
|
+
}
|
|
427
|
+
function getNginxVersion() {
|
|
428
|
+
const result = execCommandSafe("nginx -v 2>&1");
|
|
429
|
+
if (result.success || result.output) {
|
|
430
|
+
const match = (result.output || result.error || "").match(/nginx\/(\S+)/);
|
|
431
|
+
return match ? match[1] : null;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/lib/pm2.ts
|
|
437
|
+
function isPm2Installed() {
|
|
438
|
+
const result = execCommandSafe("which pm2");
|
|
439
|
+
return result.success;
|
|
440
|
+
}
|
|
441
|
+
function getPm2List() {
|
|
442
|
+
const result = execCommandSafe("pm2 jlist");
|
|
443
|
+
if (!result.success) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const processes = JSON.parse(result.output);
|
|
448
|
+
return processes.map((p) => ({
|
|
449
|
+
name: p.name,
|
|
450
|
+
pm_id: p.pm_id,
|
|
451
|
+
status: p.pm2_env?.status,
|
|
452
|
+
cpu: p.monit ? p.monit.cpu : 0,
|
|
453
|
+
memory: p.monit ? p.monit.memory : 0,
|
|
454
|
+
uptime: p.pm2_env?.pm_uptime ? Date.now() - p.pm2_env.pm_uptime : 0,
|
|
455
|
+
restarts: p.pm2_env?.restart_time || 0
|
|
456
|
+
}));
|
|
457
|
+
} catch {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function getProcessByName(name) {
|
|
462
|
+
const pm2Name = getPm2ProcessName(name);
|
|
463
|
+
const processes = getPm2List();
|
|
464
|
+
return processes.find((p) => p.name === pm2Name);
|
|
465
|
+
}
|
|
466
|
+
function startProject(project) {
|
|
467
|
+
if (project.type !== "npm") {
|
|
468
|
+
return { success: false, error: "Only npm projects can be started with PM2" };
|
|
469
|
+
}
|
|
470
|
+
if (!project.start) {
|
|
471
|
+
return { success: false, error: "No start command configured" };
|
|
472
|
+
}
|
|
473
|
+
const pm2Name = getPm2ProcessName(project.name);
|
|
474
|
+
const existingProcess = getProcessByName(project.name);
|
|
475
|
+
const envVars = [];
|
|
476
|
+
if (project.env) {
|
|
477
|
+
for (const [key, value] of Object.entries(project.env)) {
|
|
478
|
+
envVars.push(`${key}=${value}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
let command;
|
|
482
|
+
if (existingProcess) {
|
|
483
|
+
command = `pm2 restart "${pm2Name}"`;
|
|
484
|
+
} else {
|
|
485
|
+
const envFlags = envVars.length > 0 ? envVars.map((e) => `--env ${e}`).join(" ") : "";
|
|
486
|
+
command = `pm2 start --name "${pm2Name}" --cwd "${project.path}" ${envFlags} -- bash -lc "${project.start}"`;
|
|
487
|
+
}
|
|
488
|
+
const result = execCommandSafe(command);
|
|
489
|
+
if (!result.success) {
|
|
490
|
+
return { success: false, error: result.error };
|
|
491
|
+
}
|
|
492
|
+
execCommandSafe("pm2 save");
|
|
493
|
+
return { success: true };
|
|
494
|
+
}
|
|
495
|
+
function stopProject(name) {
|
|
496
|
+
const pm2Name = getPm2ProcessName(name);
|
|
497
|
+
const result = execCommandSafe(`pm2 stop "${pm2Name}"`);
|
|
498
|
+
if (!result.success) {
|
|
499
|
+
return { success: false, error: result.error };
|
|
500
|
+
}
|
|
501
|
+
execCommandSafe("pm2 save");
|
|
502
|
+
return { success: true };
|
|
503
|
+
}
|
|
504
|
+
function restartProject(name) {
|
|
505
|
+
const pm2Name = getPm2ProcessName(name);
|
|
506
|
+
const result = execCommandSafe(`pm2 restart "${pm2Name}"`);
|
|
507
|
+
if (!result.success) {
|
|
508
|
+
return { success: false, error: result.error };
|
|
509
|
+
}
|
|
510
|
+
execCommandSafe("pm2 save");
|
|
511
|
+
return { success: true };
|
|
512
|
+
}
|
|
513
|
+
function deleteProject(name) {
|
|
514
|
+
const pm2Name = getPm2ProcessName(name);
|
|
515
|
+
const result = execCommandSafe(`pm2 delete "${pm2Name}"`);
|
|
516
|
+
if (!result.success) {
|
|
517
|
+
if (result.error?.includes("not found")) {
|
|
518
|
+
return { success: true };
|
|
519
|
+
}
|
|
520
|
+
return { success: false, error: result.error };
|
|
521
|
+
}
|
|
522
|
+
execCommandSafe("pm2 save");
|
|
523
|
+
return { success: true };
|
|
524
|
+
}
|
|
525
|
+
async function showLogs(name, follow = false, lines = 200) {
|
|
526
|
+
const pm2Name = getPm2ProcessName(name);
|
|
527
|
+
const args = ["logs", pm2Name, "--lines", String(lines)];
|
|
528
|
+
if (!follow) {
|
|
529
|
+
args.push("--nostream");
|
|
530
|
+
}
|
|
531
|
+
return spawnInteractive("pm2", args);
|
|
532
|
+
}
|
|
533
|
+
function startAllProjects(projects) {
|
|
534
|
+
return projects.filter((p) => p.type === "npm").map((project) => ({
|
|
535
|
+
name: project.name,
|
|
536
|
+
...startProject(project)
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
function stopAllProjects(projects) {
|
|
540
|
+
return projects.filter((p) => p.type === "npm").map((project) => ({
|
|
541
|
+
name: project.name,
|
|
542
|
+
...stopProject(project.name)
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
function restartAllProjects(projects) {
|
|
546
|
+
return projects.filter((p) => p.type === "npm").map((project) => ({
|
|
547
|
+
name: project.name,
|
|
548
|
+
...restartProject(project.name)
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/commands/new.ts
|
|
553
|
+
async function newCommand(options) {
|
|
554
|
+
console.log(chalk.dim("Checking prerequisites...\n"));
|
|
555
|
+
const issues = [];
|
|
556
|
+
if (!isNginxInstalled()) {
|
|
557
|
+
issues.push("nginx is not installed. Install: brew install nginx (macOS) or apt install nginx (Linux)");
|
|
558
|
+
}
|
|
559
|
+
if (!isPm2Installed()) {
|
|
560
|
+
issues.push("PM2 is not installed. Install: npm install -g pm2");
|
|
561
|
+
}
|
|
562
|
+
if (issues.length > 0) {
|
|
563
|
+
console.log(chalk.red("Missing prerequisites:\n"));
|
|
564
|
+
for (const issue of issues) {
|
|
565
|
+
console.log(chalk.red(` \u2717 ${issue}`));
|
|
566
|
+
}
|
|
567
|
+
console.log(chalk.dim("\nRun `bindler doctor` for full diagnostics."));
|
|
568
|
+
const { proceed } = await inquirer.prompt([
|
|
569
|
+
{
|
|
570
|
+
type: "confirm",
|
|
571
|
+
name: "proceed",
|
|
572
|
+
message: "Continue anyway?",
|
|
573
|
+
default: false
|
|
574
|
+
}
|
|
575
|
+
]);
|
|
576
|
+
if (!proceed) {
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
console.log("");
|
|
580
|
+
} else {
|
|
581
|
+
console.log(chalk.green("\u2713 Prerequisites OK\n"));
|
|
582
|
+
}
|
|
583
|
+
const defaults = getDefaults();
|
|
584
|
+
let project = {};
|
|
585
|
+
const cwd = process.cwd();
|
|
586
|
+
const cwdName = basename(cwd);
|
|
587
|
+
if (!options.name) {
|
|
588
|
+
const answers = await inquirer.prompt([
|
|
589
|
+
{
|
|
590
|
+
type: "input",
|
|
591
|
+
name: "path",
|
|
592
|
+
message: "Project path:",
|
|
593
|
+
default: cwd,
|
|
594
|
+
validate: (input) => {
|
|
595
|
+
if (!input) {
|
|
596
|
+
return "Path is required";
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
type: "input",
|
|
603
|
+
name: "name",
|
|
604
|
+
message: "Project name:",
|
|
605
|
+
default: cwdName,
|
|
606
|
+
validate: (input) => {
|
|
607
|
+
if (!validateProjectName(input)) {
|
|
608
|
+
return "Invalid project name. Use alphanumeric characters, dashes, and underscores.";
|
|
609
|
+
}
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
type: "list",
|
|
615
|
+
name: "type",
|
|
616
|
+
message: "Project type:",
|
|
617
|
+
choices: (answers2) => {
|
|
618
|
+
const detected = existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
619
|
+
return [
|
|
620
|
+
{ name: `npm (Node.js app)${detected === "npm" ? " - detected" : ""}`, value: "npm" },
|
|
621
|
+
{ name: `static (HTML/CSS/JS)${detected === "static" ? " - detected" : ""}`, value: "static" }
|
|
622
|
+
];
|
|
623
|
+
},
|
|
624
|
+
default: (answers2) => {
|
|
625
|
+
return existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
type: "input",
|
|
630
|
+
name: "hostname",
|
|
631
|
+
message: options.local ? "Hostname (e.g., myapp.local):" : "Hostname (e.g., mysite.example.com or example.com):",
|
|
632
|
+
default: options.local ? `${cwdName}.local` : void 0,
|
|
633
|
+
validate: (input) => {
|
|
634
|
+
if (!validateHostname(input)) {
|
|
635
|
+
return "Invalid hostname format";
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
type: "input",
|
|
642
|
+
name: "basePath",
|
|
643
|
+
message: "Base path (leave empty for root, or e.g., /api):",
|
|
644
|
+
filter: (input) => {
|
|
645
|
+
if (!input || input.trim() === "") return "";
|
|
646
|
+
const trimmed = input.trim();
|
|
647
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
]);
|
|
651
|
+
project = { ...answers };
|
|
652
|
+
if (!project.basePath) delete project.basePath;
|
|
653
|
+
if (options.local) project.local = true;
|
|
654
|
+
if (answers.type === "npm") {
|
|
655
|
+
const scripts = existsSync4(answers.path) ? getPackageJsonScripts(answers.path) : [];
|
|
656
|
+
const suggestedPort = findAvailablePort();
|
|
657
|
+
const npmAnswers = await inquirer.prompt([
|
|
658
|
+
{
|
|
659
|
+
type: "input",
|
|
660
|
+
name: "port",
|
|
661
|
+
message: "Port number:",
|
|
662
|
+
default: suggestedPort,
|
|
663
|
+
validate: (input) => {
|
|
664
|
+
const port = parseInt(input, 10);
|
|
665
|
+
if (!validatePort(port)) {
|
|
666
|
+
return "Invalid port. Use a number between 1024 and 65535.";
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
},
|
|
670
|
+
filter: (input) => parseInt(input, 10)
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
type: scripts.length > 0 ? "list" : "input",
|
|
674
|
+
name: "start",
|
|
675
|
+
message: "Start command:",
|
|
676
|
+
choices: scripts.length > 0 ? [
|
|
677
|
+
...scripts.map((s) => ({ name: `npm run ${s}`, value: `npm run ${s}` })),
|
|
678
|
+
{ name: "Custom command...", value: "__custom__" }
|
|
679
|
+
] : void 0,
|
|
680
|
+
default: scripts.includes("start") ? "npm run start" : "npm start"
|
|
681
|
+
}
|
|
682
|
+
]);
|
|
683
|
+
if (npmAnswers.start === "__custom__") {
|
|
684
|
+
const customAnswer = await inquirer.prompt([
|
|
685
|
+
{
|
|
686
|
+
type: "input",
|
|
687
|
+
name: "start",
|
|
688
|
+
message: "Enter custom start command:"
|
|
689
|
+
}
|
|
690
|
+
]);
|
|
691
|
+
npmAnswers.start = customAnswer.start;
|
|
692
|
+
}
|
|
693
|
+
project.port = npmAnswers.port;
|
|
694
|
+
project.start = npmAnswers.start;
|
|
695
|
+
const envAnswer = await inquirer.prompt([
|
|
696
|
+
{
|
|
697
|
+
type: "confirm",
|
|
698
|
+
name: "setPortEnv",
|
|
699
|
+
message: `Set PORT=${npmAnswers.port} environment variable?`,
|
|
700
|
+
default: true
|
|
701
|
+
}
|
|
702
|
+
]);
|
|
703
|
+
if (envAnswer.setPortEnv) {
|
|
704
|
+
project.env = { PORT: String(npmAnswers.port) };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
if (!options.hostname) {
|
|
709
|
+
console.error(chalk.red("Error: --hostname is required"));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
project.name = options.name;
|
|
713
|
+
project.type = options.type || "static";
|
|
714
|
+
project.path = options.path || cwd;
|
|
715
|
+
project.hostname = options.hostname;
|
|
716
|
+
if (options.basePath) {
|
|
717
|
+
project.basePath = options.basePath.startsWith("/") ? options.basePath : `/${options.basePath}`;
|
|
718
|
+
}
|
|
719
|
+
if (options.local) {
|
|
720
|
+
project.local = true;
|
|
721
|
+
}
|
|
722
|
+
if (project.type === "npm") {
|
|
723
|
+
project.port = options.port || findAvailablePort();
|
|
724
|
+
project.start = options.start || "npm start";
|
|
725
|
+
project.env = { PORT: String(project.port) };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!validateProjectName(project.name)) {
|
|
729
|
+
console.error(chalk.red("Error: Invalid project name"));
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
if (!validateHostname(project.hostname)) {
|
|
733
|
+
console.error(chalk.red("Error: Invalid hostname"));
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
if (!existsSync4(project.path)) {
|
|
737
|
+
const createDir = options.name ? true : (await inquirer.prompt([
|
|
738
|
+
{
|
|
739
|
+
type: "confirm",
|
|
740
|
+
name: "create",
|
|
741
|
+
message: `Directory ${project.path} does not exist. Create it?`,
|
|
742
|
+
default: true
|
|
743
|
+
}
|
|
744
|
+
])).create;
|
|
745
|
+
if (createDir) {
|
|
746
|
+
mkdirSync3(project.path, { recursive: true });
|
|
747
|
+
console.log(chalk.green(`Created directory: ${project.path}`));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
addProject(project);
|
|
752
|
+
console.log(chalk.green(`
|
|
753
|
+
Project "${project.name}" added successfully!`));
|
|
754
|
+
if (project.local) {
|
|
755
|
+
console.log(chalk.yellow(`
|
|
756
|
+
Local project - add to /etc/hosts:`));
|
|
757
|
+
console.log(chalk.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
|
|
758
|
+
console.log(chalk.dim(`
|
|
759
|
+
Run ${chalk.cyan("sudo bindler apply")} to update nginx.`));
|
|
760
|
+
console.log(chalk.dim(`Then access at: ${chalk.cyan(`http://${project.hostname}:8080`)}`));
|
|
761
|
+
} else {
|
|
762
|
+
console.log(chalk.dim(`
|
|
763
|
+
Configuration saved. Run ${chalk.cyan("sudo bindler apply")} to update nginx and cloudflare.`));
|
|
764
|
+
}
|
|
765
|
+
if (project.type === "npm") {
|
|
766
|
+
console.log(chalk.dim(`Run ${chalk.cyan(`bindler start ${project.name}`)} to start the application.`));
|
|
767
|
+
}
|
|
768
|
+
} catch (error) {
|
|
769
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/commands/list.ts
|
|
775
|
+
import chalk2 from "chalk";
|
|
776
|
+
import Table from "cli-table3";
|
|
777
|
+
async function listCommand() {
|
|
778
|
+
const projects = listProjects();
|
|
779
|
+
if (projects.length === 0) {
|
|
780
|
+
console.log(chalk2.yellow("No projects registered."));
|
|
781
|
+
console.log(chalk2.dim(`Run ${chalk2.cyan("bindler new")} to create one.`));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const table = new Table({
|
|
785
|
+
head: [
|
|
786
|
+
chalk2.cyan("Name"),
|
|
787
|
+
chalk2.cyan("Type"),
|
|
788
|
+
chalk2.cyan("Hostname"),
|
|
789
|
+
chalk2.cyan("Port"),
|
|
790
|
+
chalk2.cyan("Path"),
|
|
791
|
+
chalk2.cyan("Status")
|
|
792
|
+
],
|
|
793
|
+
style: {
|
|
794
|
+
head: [],
|
|
795
|
+
border: []
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
for (const project of projects) {
|
|
799
|
+
let status = "-";
|
|
800
|
+
if (project.type === "npm") {
|
|
801
|
+
const process2 = getProcessByName(project.name);
|
|
802
|
+
if (process2) {
|
|
803
|
+
status = process2.status === "online" ? chalk2.green("online") : process2.status === "stopped" ? chalk2.yellow("stopped") : chalk2.red(process2.status);
|
|
804
|
+
} else {
|
|
805
|
+
status = chalk2.dim("not started");
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
status = project.enabled !== false ? chalk2.green("serving") : chalk2.yellow("disabled");
|
|
809
|
+
}
|
|
810
|
+
if (project.enabled === false) {
|
|
811
|
+
status = chalk2.yellow("disabled");
|
|
812
|
+
}
|
|
813
|
+
table.push([
|
|
814
|
+
project.name,
|
|
815
|
+
project.type,
|
|
816
|
+
project.hostname,
|
|
817
|
+
project.port?.toString() || "-",
|
|
818
|
+
project.path,
|
|
819
|
+
status
|
|
820
|
+
]);
|
|
821
|
+
}
|
|
822
|
+
console.log(table.toString());
|
|
823
|
+
console.log(chalk2.dim(`
|
|
824
|
+
${projects.length} project(s) registered`));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/commands/status.ts
|
|
828
|
+
import chalk3 from "chalk";
|
|
829
|
+
import Table2 from "cli-table3";
|
|
830
|
+
async function statusCommand() {
|
|
831
|
+
const projects = listProjects();
|
|
832
|
+
if (projects.length === 0) {
|
|
833
|
+
console.log(chalk3.yellow("No projects registered."));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const statuses = [];
|
|
837
|
+
for (const project of projects) {
|
|
838
|
+
const status = { ...project };
|
|
839
|
+
if (project.type === "npm") {
|
|
840
|
+
const process2 = getProcessByName(project.name);
|
|
841
|
+
status.pm2Name = getPm2ProcessName(project.name);
|
|
842
|
+
if (process2) {
|
|
843
|
+
status.pm2Status = process2.status;
|
|
844
|
+
} else {
|
|
845
|
+
status.pm2Status = "not_managed";
|
|
846
|
+
}
|
|
847
|
+
if (project.port) {
|
|
848
|
+
status.portListening = await isPortListening(project.port);
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
status.pm2Status = "n/a";
|
|
852
|
+
}
|
|
853
|
+
statuses.push(status);
|
|
854
|
+
}
|
|
855
|
+
const table = new Table2({
|
|
856
|
+
head: [
|
|
857
|
+
chalk3.cyan("Name"),
|
|
858
|
+
chalk3.cyan("Type"),
|
|
859
|
+
chalk3.cyan("Hostname"),
|
|
860
|
+
chalk3.cyan("Port"),
|
|
861
|
+
chalk3.cyan("PM2 Status"),
|
|
862
|
+
chalk3.cyan("Port Check")
|
|
863
|
+
],
|
|
864
|
+
style: {
|
|
865
|
+
head: [],
|
|
866
|
+
border: []
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
for (const status of statuses) {
|
|
870
|
+
let pm2StatusStr = "-";
|
|
871
|
+
let portCheckStr = "-";
|
|
872
|
+
if (status.type === "npm") {
|
|
873
|
+
switch (status.pm2Status) {
|
|
874
|
+
case "online":
|
|
875
|
+
pm2StatusStr = chalk3.green("online");
|
|
876
|
+
break;
|
|
877
|
+
case "stopped":
|
|
878
|
+
pm2StatusStr = chalk3.yellow("stopped");
|
|
879
|
+
break;
|
|
880
|
+
case "errored":
|
|
881
|
+
pm2StatusStr = chalk3.red("errored");
|
|
882
|
+
break;
|
|
883
|
+
case "not_managed":
|
|
884
|
+
pm2StatusStr = chalk3.dim("not started");
|
|
885
|
+
break;
|
|
886
|
+
default:
|
|
887
|
+
pm2StatusStr = chalk3.dim(status.pm2Status || "-");
|
|
888
|
+
}
|
|
889
|
+
portCheckStr = status.portListening ? chalk3.green("listening") : chalk3.red("not listening");
|
|
890
|
+
}
|
|
891
|
+
if (status.enabled === false) {
|
|
892
|
+
pm2StatusStr = chalk3.yellow("disabled");
|
|
893
|
+
portCheckStr = chalk3.dim("-");
|
|
894
|
+
}
|
|
895
|
+
table.push([
|
|
896
|
+
status.name,
|
|
897
|
+
status.type,
|
|
898
|
+
status.hostname,
|
|
899
|
+
status.port?.toString() || "-",
|
|
900
|
+
pm2StatusStr,
|
|
901
|
+
portCheckStr
|
|
902
|
+
]);
|
|
903
|
+
}
|
|
904
|
+
console.log(table.toString());
|
|
905
|
+
const runningProcesses = getPm2List().filter((p) => p.name.startsWith("bindler:"));
|
|
906
|
+
if (runningProcesses.length > 0) {
|
|
907
|
+
console.log(chalk3.bold("\nPM2 Process Details:"));
|
|
908
|
+
const detailTable = new Table2({
|
|
909
|
+
head: [
|
|
910
|
+
chalk3.cyan("Process"),
|
|
911
|
+
chalk3.cyan("CPU"),
|
|
912
|
+
chalk3.cyan("Memory"),
|
|
913
|
+
chalk3.cyan("Uptime"),
|
|
914
|
+
chalk3.cyan("Restarts")
|
|
915
|
+
],
|
|
916
|
+
style: {
|
|
917
|
+
head: [],
|
|
918
|
+
border: []
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
for (const proc of runningProcesses) {
|
|
922
|
+
detailTable.push([
|
|
923
|
+
proc.name.replace("bindler:", ""),
|
|
924
|
+
`${proc.cpu}%`,
|
|
925
|
+
formatBytes(proc.memory),
|
|
926
|
+
formatUptime(proc.uptime),
|
|
927
|
+
String(proc.restarts)
|
|
928
|
+
]);
|
|
929
|
+
}
|
|
930
|
+
console.log(detailTable.toString());
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/commands/start.ts
|
|
935
|
+
import chalk4 from "chalk";
|
|
936
|
+
async function startCommand(name, options) {
|
|
937
|
+
if (options.all) {
|
|
938
|
+
const projects = listProjects();
|
|
939
|
+
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
940
|
+
if (npmProjects.length === 0) {
|
|
941
|
+
console.log(chalk4.yellow("No npm projects to start."));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
console.log(chalk4.blue(`Starting ${npmProjects.length} npm project(s)...`));
|
|
945
|
+
const results = startAllProjects(npmProjects);
|
|
946
|
+
for (const result2 of results) {
|
|
947
|
+
if (result2.success) {
|
|
948
|
+
console.log(chalk4.green(` \u2713 ${result2.name}`));
|
|
949
|
+
} else {
|
|
950
|
+
console.log(chalk4.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
954
|
+
console.log(chalk4.dim(`
|
|
955
|
+
${succeeded}/${results.length} started successfully`));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (!name) {
|
|
959
|
+
console.error(chalk4.red("Error: Project name is required. Use --all to start all projects."));
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
const project = getProject(name);
|
|
963
|
+
if (!project) {
|
|
964
|
+
console.error(chalk4.red(`Error: Project "${name}" not found`));
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
if (project.type !== "npm") {
|
|
968
|
+
console.log(chalk4.blue(`Project "${name}" is a static site - no process to start.`));
|
|
969
|
+
console.log(chalk4.dim("Static sites are served directly by nginx."));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.log(chalk4.blue(`Starting ${name}...`));
|
|
973
|
+
const result = startProject(project);
|
|
974
|
+
if (result.success) {
|
|
975
|
+
console.log(chalk4.green(`\u2713 ${name} started successfully`));
|
|
976
|
+
console.log(chalk4.dim(` Port: ${project.port}`));
|
|
977
|
+
console.log(chalk4.dim(` URL: https://${project.hostname}`));
|
|
978
|
+
} else {
|
|
979
|
+
console.error(chalk4.red(`\u2717 Failed to start ${name}: ${result.error}`));
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/commands/stop.ts
|
|
985
|
+
import chalk5 from "chalk";
|
|
986
|
+
async function stopCommand(name, options) {
|
|
987
|
+
if (options.all) {
|
|
988
|
+
const projects = listProjects();
|
|
989
|
+
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
990
|
+
if (npmProjects.length === 0) {
|
|
991
|
+
console.log(chalk5.yellow("No npm projects to stop."));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.log(chalk5.blue(`Stopping ${npmProjects.length} npm project(s)...`));
|
|
995
|
+
const results = stopAllProjects(npmProjects);
|
|
996
|
+
for (const result2 of results) {
|
|
997
|
+
if (result2.success) {
|
|
998
|
+
console.log(chalk5.green(` \u2713 ${result2.name}`));
|
|
999
|
+
} else {
|
|
1000
|
+
console.log(chalk5.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
1004
|
+
console.log(chalk5.dim(`
|
|
1005
|
+
${succeeded}/${results.length} stopped successfully`));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (!name) {
|
|
1009
|
+
console.error(chalk5.red("Error: Project name is required. Use --all to stop all projects."));
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
const project = getProject(name);
|
|
1013
|
+
if (!project) {
|
|
1014
|
+
console.error(chalk5.red(`Error: Project "${name}" not found`));
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
if (project.type !== "npm") {
|
|
1018
|
+
console.log(chalk5.yellow(`Project "${name}" is a static site - no process to stop.`));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
console.log(chalk5.blue(`Stopping ${name}...`));
|
|
1022
|
+
const result = stopProject(name);
|
|
1023
|
+
if (result.success) {
|
|
1024
|
+
console.log(chalk5.green(`\u2713 ${name} stopped successfully`));
|
|
1025
|
+
} else {
|
|
1026
|
+
console.error(chalk5.red(`\u2717 Failed to stop ${name}: ${result.error}`));
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/commands/restart.ts
|
|
1032
|
+
import chalk6 from "chalk";
|
|
1033
|
+
async function restartCommand(name, options) {
|
|
1034
|
+
if (options.all) {
|
|
1035
|
+
const projects = listProjects();
|
|
1036
|
+
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1037
|
+
if (npmProjects.length === 0) {
|
|
1038
|
+
console.log(chalk6.yellow("No npm projects to restart."));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
console.log(chalk6.blue(`Restarting ${npmProjects.length} npm project(s)...`));
|
|
1042
|
+
const results = restartAllProjects(npmProjects);
|
|
1043
|
+
for (const result2 of results) {
|
|
1044
|
+
if (result2.success) {
|
|
1045
|
+
console.log(chalk6.green(` \u2713 ${result2.name}`));
|
|
1046
|
+
} else {
|
|
1047
|
+
console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
1051
|
+
console.log(chalk6.dim(`
|
|
1052
|
+
${succeeded}/${results.length} restarted successfully`));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (!name) {
|
|
1056
|
+
console.error(chalk6.red("Error: Project name is required. Use --all to restart all projects."));
|
|
1057
|
+
process2.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
const project = getProject(name);
|
|
1060
|
+
if (!project) {
|
|
1061
|
+
console.error(chalk6.red(`Error: Project "${name}" not found`));
|
|
1062
|
+
process2.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
if (project.type !== "npm") {
|
|
1065
|
+
console.log(chalk6.yellow(`Project "${name}" is a static site - no process to restart.`));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
console.log(chalk6.blue(`Restarting ${name}...`));
|
|
1069
|
+
const process2 = getProcessByName(name);
|
|
1070
|
+
let result;
|
|
1071
|
+
if (process2) {
|
|
1072
|
+
result = restartProject(name);
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log(chalk6.dim("Process not running, starting..."));
|
|
1075
|
+
result = startProject(project);
|
|
1076
|
+
}
|
|
1077
|
+
if (result.success) {
|
|
1078
|
+
console.log(chalk6.green(`\u2713 ${name} restarted successfully`));
|
|
1079
|
+
} else {
|
|
1080
|
+
console.error(chalk6.red(`\u2717 Failed to restart ${name}: ${result.error}`));
|
|
1081
|
+
process2.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/commands/logs.ts
|
|
1086
|
+
import chalk7 from "chalk";
|
|
1087
|
+
async function logsCommand(name, options) {
|
|
1088
|
+
const project = getProject(name);
|
|
1089
|
+
if (!project) {
|
|
1090
|
+
console.error(chalk7.red(`Error: Project "${name}" not found`));
|
|
1091
|
+
process2.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
if (project.type !== "npm") {
|
|
1094
|
+
console.log(chalk7.yellow(`Project "${name}" is a static site - no logs available.`));
|
|
1095
|
+
console.log(chalk7.dim("Check nginx access/error logs instead:"));
|
|
1096
|
+
console.log(chalk7.dim(" /var/log/nginx/access.log"));
|
|
1097
|
+
console.log(chalk7.dim(" /var/log/nginx/error.log"));
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const process2 = getProcessByName(name);
|
|
1101
|
+
if (!process2) {
|
|
1102
|
+
console.log(chalk7.yellow(`Project "${name}" has not been started yet.`));
|
|
1103
|
+
console.log(chalk7.dim(`Run ${chalk7.cyan(`bindler start ${name}`)} first.`));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const lines = options.lines || 200;
|
|
1107
|
+
const follow = options.follow || false;
|
|
1108
|
+
if (follow) {
|
|
1109
|
+
console.log(chalk7.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
|
|
1110
|
+
}
|
|
1111
|
+
await showLogs(name, follow, lines);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/commands/update.ts
|
|
1115
|
+
import chalk8 from "chalk";
|
|
1116
|
+
async function updateCommand(name, options) {
|
|
1117
|
+
const project = getProject(name);
|
|
1118
|
+
if (!project) {
|
|
1119
|
+
console.error(chalk8.red(`Error: Project "${name}" not found`));
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
const updates = {};
|
|
1123
|
+
if (options.hostname) {
|
|
1124
|
+
if (!validateHostname(options.hostname)) {
|
|
1125
|
+
console.error(chalk8.red("Error: Invalid hostname format"));
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
updates.hostname = options.hostname;
|
|
1129
|
+
}
|
|
1130
|
+
if (options.port) {
|
|
1131
|
+
const port = parseInt(options.port, 10);
|
|
1132
|
+
if (!validatePort(port)) {
|
|
1133
|
+
console.error(chalk8.red("Error: Invalid port. Use a number between 1024 and 65535."));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
if (!isPortAvailable(port) && port !== project.port) {
|
|
1137
|
+
console.error(chalk8.red(`Error: Port ${port} is already in use by another project.`));
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
updates.port = port;
|
|
1141
|
+
if (project.env?.PORT) {
|
|
1142
|
+
updates.env = { ...project.env, PORT: String(port) };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (options.start) {
|
|
1146
|
+
if (project.type !== "npm") {
|
|
1147
|
+
console.error(chalk8.red("Error: Start command only applies to npm projects"));
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
updates.start = options.start;
|
|
1151
|
+
}
|
|
1152
|
+
if (options.path) {
|
|
1153
|
+
updates.path = options.path;
|
|
1154
|
+
}
|
|
1155
|
+
if (options.env && options.env.length > 0) {
|
|
1156
|
+
const env = { ...project.env || {} };
|
|
1157
|
+
for (const envStr of options.env) {
|
|
1158
|
+
const [key, ...valueParts] = envStr.split("=");
|
|
1159
|
+
if (!key) {
|
|
1160
|
+
console.error(chalk8.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
env[key] = valueParts.join("=");
|
|
1164
|
+
}
|
|
1165
|
+
updates.env = env;
|
|
1166
|
+
}
|
|
1167
|
+
if (options.enable) {
|
|
1168
|
+
updates.enabled = true;
|
|
1169
|
+
} else if (options.disable) {
|
|
1170
|
+
updates.enabled = false;
|
|
1171
|
+
}
|
|
1172
|
+
if (Object.keys(updates).length === 0) {
|
|
1173
|
+
console.log(chalk8.yellow("No updates specified."));
|
|
1174
|
+
console.log(chalk8.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
updateProject(name, updates);
|
|
1179
|
+
console.log(chalk8.green(`\u2713 Project "${name}" updated successfully`));
|
|
1180
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1181
|
+
console.log(chalk8.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
|
|
1182
|
+
}
|
|
1183
|
+
console.log(chalk8.dim(`
|
|
1184
|
+
Run ${chalk8.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1185
|
+
if (project.type === "npm" && (updates.port || updates.start || updates.env)) {
|
|
1186
|
+
console.log(chalk8.dim(`Run ${chalk8.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
|
|
1187
|
+
}
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
console.error(chalk8.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/commands/edit.ts
|
|
1195
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
1196
|
+
import { tmpdir } from "os";
|
|
1197
|
+
import { join as join4 } from "path";
|
|
1198
|
+
import chalk9 from "chalk";
|
|
1199
|
+
async function editCommand(name) {
|
|
1200
|
+
const project = getProject(name);
|
|
1201
|
+
if (!project) {
|
|
1202
|
+
console.error(chalk9.red(`Error: Project "${name}" not found`));
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1206
|
+
const tmpFile = join4(tmpdir(), `bindler-${name}-${Date.now()}.json`);
|
|
1207
|
+
writeFileSync3(tmpFile, JSON.stringify(project, null, 2) + "\n");
|
|
1208
|
+
console.log(chalk9.dim(`Opening ${name} config in ${editor}...`));
|
|
1209
|
+
const exitCode = await spawnInteractive(editor, [tmpFile]);
|
|
1210
|
+
if (exitCode !== 0) {
|
|
1211
|
+
console.error(chalk9.red("Editor exited with error"));
|
|
1212
|
+
unlinkSync(tmpFile);
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
let editedContent;
|
|
1216
|
+
try {
|
|
1217
|
+
editedContent = readFileSync3(tmpFile, "utf-8");
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
console.error(chalk9.red("Failed to read edited file"));
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
} finally {
|
|
1222
|
+
unlinkSync(tmpFile);
|
|
1223
|
+
}
|
|
1224
|
+
let editedProject;
|
|
1225
|
+
try {
|
|
1226
|
+
editedProject = JSON.parse(editedContent);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
console.error(chalk9.red("Error: Invalid JSON in edited file"));
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
if (editedProject.name !== project.name) {
|
|
1232
|
+
console.error(chalk9.red("Error: Cannot change project name via edit. Use a new project instead."));
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
const originalStr = JSON.stringify(project);
|
|
1236
|
+
const editedStr = JSON.stringify(editedProject);
|
|
1237
|
+
if (originalStr === editedStr) {
|
|
1238
|
+
console.log(chalk9.yellow("No changes made."));
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
const config = readConfig();
|
|
1243
|
+
const index = config.projects.findIndex((p) => p.name === name);
|
|
1244
|
+
if (index === -1) {
|
|
1245
|
+
console.error(chalk9.red("Error: Project not found"));
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
config.projects[index] = editedProject;
|
|
1249
|
+
writeConfig(config);
|
|
1250
|
+
console.log(chalk9.green(`\u2713 Project "${name}" updated successfully`));
|
|
1251
|
+
console.log(chalk9.dim(`
|
|
1252
|
+
Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/commands/remove.ts
|
|
1260
|
+
import inquirer2 from "inquirer";
|
|
1261
|
+
import chalk10 from "chalk";
|
|
1262
|
+
async function removeCommand(name, options) {
|
|
1263
|
+
const project = getProject(name);
|
|
1264
|
+
if (!project) {
|
|
1265
|
+
console.error(chalk10.red(`Error: Project "${name}" not found`));
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
}
|
|
1268
|
+
if (!options.force) {
|
|
1269
|
+
const { confirm } = await inquirer2.prompt([
|
|
1270
|
+
{
|
|
1271
|
+
type: "confirm",
|
|
1272
|
+
name: "confirm",
|
|
1273
|
+
message: `Are you sure you want to remove project "${name}" (${project.hostname})?`,
|
|
1274
|
+
default: false
|
|
1275
|
+
}
|
|
1276
|
+
]);
|
|
1277
|
+
if (!confirm) {
|
|
1278
|
+
console.log(chalk10.yellow("Cancelled."));
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (project.type === "npm") {
|
|
1283
|
+
const process2 = getProcessByName(name);
|
|
1284
|
+
if (process2) {
|
|
1285
|
+
console.log(chalk10.dim("Stopping PM2 process..."));
|
|
1286
|
+
deleteProject(name);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
removeProject(name);
|
|
1291
|
+
console.log(chalk10.green(`\u2713 Project "${name}" removed from registry`));
|
|
1292
|
+
console.log(chalk10.dim(`
|
|
1293
|
+
Run ${chalk10.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1294
|
+
console.log(chalk10.yellow("\nNote: The project files and Cloudflare DNS routes were not removed."));
|
|
1295
|
+
console.log(chalk10.dim(` Project path: ${project.path}`));
|
|
1296
|
+
console.log(chalk10.dim(` To remove DNS route manually: cloudflared tunnel route dns --remove ${project.hostname}`));
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/commands/apply.ts
|
|
1304
|
+
import chalk11 from "chalk";
|
|
1305
|
+
|
|
1306
|
+
// src/lib/cloudflare.ts
|
|
1307
|
+
function isCloudflaredInstalled() {
|
|
1308
|
+
const result = execCommandSafe("which cloudflared");
|
|
1309
|
+
return result.success;
|
|
1310
|
+
}
|
|
1311
|
+
function getCloudflaredVersion() {
|
|
1312
|
+
const result = execCommandSafe("cloudflared --version");
|
|
1313
|
+
if (result.success) {
|
|
1314
|
+
const match = result.output.match(/cloudflared version (\S+)/);
|
|
1315
|
+
return match ? match[1] : result.output;
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
function listTunnels() {
|
|
1320
|
+
const result = execCommandSafe("cloudflared tunnel list --output json");
|
|
1321
|
+
if (!result.success) {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
const tunnels = JSON.parse(result.output);
|
|
1326
|
+
return tunnels.map((t) => ({
|
|
1327
|
+
id: t.id,
|
|
1328
|
+
name: t.name,
|
|
1329
|
+
createdAt: t.created_at
|
|
1330
|
+
}));
|
|
1331
|
+
} catch {
|
|
1332
|
+
return [];
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function getTunnelByName(name) {
|
|
1336
|
+
const tunnels = listTunnels();
|
|
1337
|
+
const tunnel = tunnels.find((t) => t.name === name);
|
|
1338
|
+
return tunnel ? { id: tunnel.id, name: tunnel.name } : null;
|
|
1339
|
+
}
|
|
1340
|
+
function routeDns(tunnelName, hostname) {
|
|
1341
|
+
const result = execCommandSafe(`cloudflared tunnel route dns "${tunnelName}" "${hostname}"`);
|
|
1342
|
+
if (!result.success) {
|
|
1343
|
+
if (result.error?.includes("already exists") || result.output?.includes("already exists")) {
|
|
1344
|
+
return { success: true, output: "DNS route already exists" };
|
|
1345
|
+
}
|
|
1346
|
+
return { success: false, error: result.error };
|
|
1347
|
+
}
|
|
1348
|
+
return { success: true, output: result.output };
|
|
1349
|
+
}
|
|
1350
|
+
function routeDnsForAllProjects() {
|
|
1351
|
+
const config = readConfig();
|
|
1352
|
+
const { tunnelName, applyCloudflareDnsRoutes } = config.defaults;
|
|
1353
|
+
if (!applyCloudflareDnsRoutes) {
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
const results = [];
|
|
1357
|
+
for (const project of config.projects) {
|
|
1358
|
+
if (project.enabled === false) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (project.local) {
|
|
1362
|
+
results.push({
|
|
1363
|
+
hostname: project.hostname,
|
|
1364
|
+
success: true,
|
|
1365
|
+
skipped: true,
|
|
1366
|
+
output: "Local project - skipped"
|
|
1367
|
+
});
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
const result = routeDns(tunnelName, project.hostname);
|
|
1371
|
+
results.push({
|
|
1372
|
+
hostname: project.hostname,
|
|
1373
|
+
...result
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
return results;
|
|
1377
|
+
}
|
|
1378
|
+
function isTunnelRunning(tunnelName) {
|
|
1379
|
+
const result = execCommandSafe(`pgrep -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
1380
|
+
return result.success;
|
|
1381
|
+
}
|
|
1382
|
+
function getTunnelInfo(tunnelName) {
|
|
1383
|
+
const tunnel = getTunnelByName(tunnelName);
|
|
1384
|
+
if (!tunnel) {
|
|
1385
|
+
return { exists: false, running: false };
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
exists: true,
|
|
1389
|
+
running: isTunnelRunning(tunnelName),
|
|
1390
|
+
id: tunnel.id
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/commands/apply.ts
|
|
1395
|
+
async function applyCommand(options) {
|
|
1396
|
+
const config = readConfig();
|
|
1397
|
+
const defaults = getDefaults();
|
|
1398
|
+
if (config.projects.length === 0) {
|
|
1399
|
+
console.log(chalk11.yellow("No projects registered. Nothing to apply."));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
console.log(chalk11.blue("Applying configuration...\n"));
|
|
1403
|
+
console.log(chalk11.dim("Generating nginx configuration..."));
|
|
1404
|
+
if (options.dryRun) {
|
|
1405
|
+
const nginxConfig = generateNginxConfig(config);
|
|
1406
|
+
console.log(chalk11.cyan("\n--- Generated nginx config (dry-run) ---\n"));
|
|
1407
|
+
console.log(nginxConfig);
|
|
1408
|
+
console.log(chalk11.cyan("--- End of config ---\n"));
|
|
1409
|
+
console.log(chalk11.yellow("Dry run mode - no changes were made."));
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
const { path, content } = writeNginxConfig(config);
|
|
1414
|
+
console.log(chalk11.green(` \u2713 Wrote nginx config to ${path}`));
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1417
|
+
console.error(chalk11.red(` \u2717 Failed to write nginx config: ${errMsg}`));
|
|
1418
|
+
if (errMsg.includes("EACCES") || errMsg.includes("permission denied")) {
|
|
1419
|
+
console.log(chalk11.yellow(`
|
|
1420
|
+
Try running with sudo: ${chalk11.cyan("sudo bindler apply")}`));
|
|
1421
|
+
}
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
console.log(chalk11.dim("Testing nginx configuration..."));
|
|
1425
|
+
const testResult = testNginxConfig();
|
|
1426
|
+
if (!testResult.success) {
|
|
1427
|
+
console.error(chalk11.red(" \u2717 Nginx configuration test failed:"));
|
|
1428
|
+
console.error(chalk11.red(testResult.output));
|
|
1429
|
+
console.log(chalk11.yellow("\nConfiguration was written but nginx was NOT reloaded."));
|
|
1430
|
+
console.log(chalk11.dim("Fix the configuration and run `sudo bindler apply` again."));
|
|
1431
|
+
process.exit(1);
|
|
1432
|
+
}
|
|
1433
|
+
console.log(chalk11.green(" \u2713 Nginx configuration test passed"));
|
|
1434
|
+
if (!options.noReload) {
|
|
1435
|
+
console.log(chalk11.dim("Reloading nginx..."));
|
|
1436
|
+
const reloadResult = reloadNginx();
|
|
1437
|
+
if (!reloadResult.success) {
|
|
1438
|
+
console.error(chalk11.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
|
|
1439
|
+
console.log(chalk11.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
console.log(chalk11.green(" \u2713 Nginx reloaded successfully"));
|
|
1443
|
+
} else {
|
|
1444
|
+
console.log(chalk11.yellow(" - Skipped nginx reload (--no-reload)"));
|
|
1445
|
+
}
|
|
1446
|
+
const isDirectMode = defaults.mode === "direct";
|
|
1447
|
+
if (isDirectMode) {
|
|
1448
|
+
console.log(chalk11.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
|
|
1449
|
+
} else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
|
|
1450
|
+
console.log(chalk11.dim("\nConfiguring Cloudflare DNS routes..."));
|
|
1451
|
+
if (!isCloudflaredInstalled()) {
|
|
1452
|
+
console.log(chalk11.yellow(" - cloudflared not installed, skipping DNS routes"));
|
|
1453
|
+
} else {
|
|
1454
|
+
const dnsResults = routeDnsForAllProjects();
|
|
1455
|
+
if (dnsResults.length === 0) {
|
|
1456
|
+
console.log(chalk11.dim(" No hostnames to route"));
|
|
1457
|
+
} else {
|
|
1458
|
+
for (const result of dnsResults) {
|
|
1459
|
+
if (result.skipped) {
|
|
1460
|
+
console.log(chalk11.dim(` - ${result.hostname} (local - skipped)`));
|
|
1461
|
+
} else if (result.success) {
|
|
1462
|
+
const msg = result.output?.includes("already exists") ? "exists" : "routed";
|
|
1463
|
+
console.log(chalk11.green(` \u2713 ${result.hostname} (${msg})`));
|
|
1464
|
+
} else {
|
|
1465
|
+
console.log(chalk11.red(` \u2717 ${result.hostname}: ${result.error}`));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
} else if (options.noCloudflare) {
|
|
1471
|
+
console.log(chalk11.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
|
|
1472
|
+
}
|
|
1473
|
+
if (isDirectMode && defaults.sslEnabled && options.ssl !== false) {
|
|
1474
|
+
console.log(chalk11.dim("\nSetting up SSL certificates..."));
|
|
1475
|
+
const hostnames = config.projects.filter((p) => p.enabled !== false && !p.local).map((p) => p.hostname);
|
|
1476
|
+
if (hostnames.length === 0) {
|
|
1477
|
+
console.log(chalk11.dim(" No hostnames to secure"));
|
|
1478
|
+
} else {
|
|
1479
|
+
const certbotResult = execCommandSafe("which certbot");
|
|
1480
|
+
if (!certbotResult.success) {
|
|
1481
|
+
console.log(chalk11.yellow(" - certbot not installed, skipping SSL"));
|
|
1482
|
+
console.log(chalk11.dim(" Run: bindler setup --direct"));
|
|
1483
|
+
} else {
|
|
1484
|
+
for (const hostname of hostnames) {
|
|
1485
|
+
console.log(chalk11.dim(` Requesting certificate for ${hostname}...`));
|
|
1486
|
+
const email = defaults.sslEmail || "admin@" + hostname.split(".").slice(-2).join(".");
|
|
1487
|
+
const result = execCommandSafe(
|
|
1488
|
+
`sudo certbot --nginx -d ${hostname} --non-interactive --agree-tos --email ${email} 2>&1`
|
|
1489
|
+
);
|
|
1490
|
+
if (result.success || result.output?.includes("Certificate not yet due for renewal")) {
|
|
1491
|
+
console.log(chalk11.green(` \u2713 ${hostname} (secured)`));
|
|
1492
|
+
} else if (result.output?.includes("already exists")) {
|
|
1493
|
+
console.log(chalk11.green(` \u2713 ${hostname} (exists)`));
|
|
1494
|
+
} else {
|
|
1495
|
+
console.log(chalk11.yellow(` ! ${hostname}: ${result.error || "failed"}`));
|
|
1496
|
+
console.log(chalk11.dim(" Run manually: sudo certbot --nginx -d " + hostname));
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
console.log(chalk11.green("\n\u2713 Configuration applied successfully!"));
|
|
1503
|
+
console.log(chalk11.dim(`
|
|
1504
|
+
${config.projects.length} project(s) configured:`));
|
|
1505
|
+
for (const project of config.projects) {
|
|
1506
|
+
const status = project.enabled !== false ? chalk11.green("enabled") : chalk11.yellow("disabled");
|
|
1507
|
+
console.log(chalk11.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/commands/doctor.ts
|
|
1512
|
+
import chalk12 from "chalk";
|
|
1513
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1514
|
+
async function doctorCommand() {
|
|
1515
|
+
console.log(chalk12.blue("Running diagnostics...\n"));
|
|
1516
|
+
const checks = [];
|
|
1517
|
+
const nodeVersion = process.version;
|
|
1518
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
1519
|
+
checks.push({
|
|
1520
|
+
name: "Node.js",
|
|
1521
|
+
status: nodeMajor >= 18 ? "ok" : "warning",
|
|
1522
|
+
message: `Version ${nodeVersion}`,
|
|
1523
|
+
fix: nodeMajor < 18 ? "Upgrade to Node.js 18 or later" : void 0
|
|
1524
|
+
});
|
|
1525
|
+
if (isNginxInstalled()) {
|
|
1526
|
+
const version = getNginxVersion();
|
|
1527
|
+
if (isNginxRunning()) {
|
|
1528
|
+
checks.push({
|
|
1529
|
+
name: "nginx",
|
|
1530
|
+
status: "ok",
|
|
1531
|
+
message: `Installed (${version || "version unknown"}) and running`
|
|
1532
|
+
});
|
|
1533
|
+
const isMac = process.platform === "darwin";
|
|
1534
|
+
if (isMac) {
|
|
1535
|
+
const currentUser = process.env.USER || "";
|
|
1536
|
+
const psResult = execCommandSafe('ps aux | grep "nginx: worker" | grep -v grep | head -1');
|
|
1537
|
+
if (psResult.success && psResult.output) {
|
|
1538
|
+
const nginxUser = psResult.output.split(/\s+/)[0];
|
|
1539
|
+
if (nginxUser && nginxUser !== currentUser && (nginxUser === "nobody" || nginxUser === "root")) {
|
|
1540
|
+
checks.push({
|
|
1541
|
+
name: "nginx user",
|
|
1542
|
+
status: "warning",
|
|
1543
|
+
message: `nginx running as "${nginxUser}" (should be "${currentUser}")`,
|
|
1544
|
+
fix: `Fix permissions: sudo chown -R ${currentUser}:staff /opt/homebrew/var/log/nginx /opt/homebrew/var/run && sudo killall nginx && brew services start nginx`
|
|
1545
|
+
});
|
|
1546
|
+
} else {
|
|
1547
|
+
checks.push({
|
|
1548
|
+
name: "nginx user",
|
|
1549
|
+
status: "ok",
|
|
1550
|
+
message: `Running as "${nginxUser}"`
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
} else {
|
|
1556
|
+
const isMac = process.platform === "darwin";
|
|
1557
|
+
checks.push({
|
|
1558
|
+
name: "nginx",
|
|
1559
|
+
status: "warning",
|
|
1560
|
+
message: `Installed (${version || "version unknown"}) but not running`,
|
|
1561
|
+
fix: isMac ? "Start nginx: brew services start nginx" : "Start nginx: sudo systemctl start nginx"
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
} else {
|
|
1565
|
+
checks.push({
|
|
1566
|
+
name: "nginx",
|
|
1567
|
+
status: "error",
|
|
1568
|
+
message: "Not installed",
|
|
1569
|
+
fix: "Run `bindler setup` to install"
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (isPm2Installed()) {
|
|
1573
|
+
const processes = getPm2List();
|
|
1574
|
+
const bindlerProcesses = processes.filter((p) => p.name.startsWith("bindler:"));
|
|
1575
|
+
checks.push({
|
|
1576
|
+
name: "PM2",
|
|
1577
|
+
status: "ok",
|
|
1578
|
+
message: `Installed, ${bindlerProcesses.length} bindler process(es) managed`
|
|
1579
|
+
});
|
|
1580
|
+
} else {
|
|
1581
|
+
checks.push({
|
|
1582
|
+
name: "PM2",
|
|
1583
|
+
status: "error",
|
|
1584
|
+
message: "Not installed",
|
|
1585
|
+
fix: "Run `bindler setup` to install"
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
if (isCloudflaredInstalled()) {
|
|
1589
|
+
const version = getCloudflaredVersion();
|
|
1590
|
+
checks.push({
|
|
1591
|
+
name: "cloudflared",
|
|
1592
|
+
status: "ok",
|
|
1593
|
+
message: `Installed (${version || "version unknown"})`
|
|
1594
|
+
});
|
|
1595
|
+
if (configExists()) {
|
|
1596
|
+
const defaults = getDefaults();
|
|
1597
|
+
const tunnelInfo = getTunnelInfo(defaults.tunnelName);
|
|
1598
|
+
if (tunnelInfo.exists) {
|
|
1599
|
+
checks.push({
|
|
1600
|
+
name: "Cloudflare Tunnel",
|
|
1601
|
+
status: tunnelInfo.running ? "ok" : "warning",
|
|
1602
|
+
message: tunnelInfo.running ? `Tunnel "${defaults.tunnelName}" exists and running` : `Tunnel "${defaults.tunnelName}" exists but not running`,
|
|
1603
|
+
fix: !tunnelInfo.running ? `Start tunnel: cloudflared tunnel run ${defaults.tunnelName}` : void 0
|
|
1604
|
+
});
|
|
1605
|
+
} else {
|
|
1606
|
+
checks.push({
|
|
1607
|
+
name: "Cloudflare Tunnel",
|
|
1608
|
+
status: "warning",
|
|
1609
|
+
message: `Tunnel "${defaults.tunnelName}" not found`,
|
|
1610
|
+
fix: `Create tunnel: cloudflared tunnel create ${defaults.tunnelName}`
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
checks.push({
|
|
1616
|
+
name: "cloudflared",
|
|
1617
|
+
status: "warning",
|
|
1618
|
+
message: "Not installed (optional, for Cloudflare Tunnel)",
|
|
1619
|
+
fix: "Run `bindler setup` or visit https://pkg.cloudflare.com/index.html"
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
if (configExists()) {
|
|
1623
|
+
const config = readConfig();
|
|
1624
|
+
checks.push({
|
|
1625
|
+
name: "Bindler config",
|
|
1626
|
+
status: "ok",
|
|
1627
|
+
message: `${config.projects.length} project(s) registered`
|
|
1628
|
+
});
|
|
1629
|
+
const isMac = process.platform === "darwin";
|
|
1630
|
+
if (isMac && config.projects.length > 0) {
|
|
1631
|
+
const homeDir = process.env.HOME || "";
|
|
1632
|
+
const protectedPaths = ["/Desktop/", "/Documents/", "/Downloads/"];
|
|
1633
|
+
const protectedProjects = config.projects.filter(
|
|
1634
|
+
(p) => protectedPaths.some((pp) => p.path.startsWith(homeDir + pp))
|
|
1635
|
+
);
|
|
1636
|
+
if (protectedProjects.length > 0) {
|
|
1637
|
+
const currentUser = process.env.USER || "";
|
|
1638
|
+
const psResult = execCommandSafe('ps aux | grep "nginx: worker" | grep -v grep | head -1');
|
|
1639
|
+
const nginxUser = psResult.success && psResult.output ? psResult.output.split(/\s+/)[0] : "";
|
|
1640
|
+
if (nginxUser && nginxUser !== currentUser) {
|
|
1641
|
+
checks.push({
|
|
1642
|
+
name: "Protected folders",
|
|
1643
|
+
status: "warning",
|
|
1644
|
+
message: `${protectedProjects.length} project(s) in ~/Desktop, ~/Documents, or ~/Downloads`,
|
|
1645
|
+
fix: `nginx must run as "${currentUser}" to access these folders. Run: sudo chown -R ${currentUser}:staff /opt/homebrew/var/log/nginx /opt/homebrew/var/run && sudo killall nginx && brew services start nginx`
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const defaults = getDefaults();
|
|
1651
|
+
if (existsSync5(defaults.nginxManagedPath)) {
|
|
1652
|
+
checks.push({
|
|
1653
|
+
name: "Nginx config file",
|
|
1654
|
+
status: "ok",
|
|
1655
|
+
message: `Exists at ${defaults.nginxManagedPath}`
|
|
1656
|
+
});
|
|
1657
|
+
} else {
|
|
1658
|
+
checks.push({
|
|
1659
|
+
name: "Nginx config file",
|
|
1660
|
+
status: "warning",
|
|
1661
|
+
message: `Not found at ${defaults.nginxManagedPath}`,
|
|
1662
|
+
fix: "Run: sudo bindler apply"
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
if (existsSync5(defaults.projectsRoot)) {
|
|
1666
|
+
checks.push({
|
|
1667
|
+
name: "Projects root",
|
|
1668
|
+
status: "ok",
|
|
1669
|
+
message: `Exists at ${defaults.projectsRoot}`
|
|
1670
|
+
});
|
|
1671
|
+
} else {
|
|
1672
|
+
checks.push({
|
|
1673
|
+
name: "Projects root",
|
|
1674
|
+
status: "warning",
|
|
1675
|
+
message: `Not found at ${defaults.projectsRoot}`,
|
|
1676
|
+
fix: `Create directory: sudo mkdir -p ${defaults.projectsRoot}`
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
} else {
|
|
1680
|
+
checks.push({
|
|
1681
|
+
name: "Bindler config",
|
|
1682
|
+
status: "warning",
|
|
1683
|
+
message: "Not initialized",
|
|
1684
|
+
fix: "Run: bindler new (to create first project)"
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
let hasErrors = false;
|
|
1688
|
+
let hasWarnings = false;
|
|
1689
|
+
for (const check of checks) {
|
|
1690
|
+
let icon;
|
|
1691
|
+
let color;
|
|
1692
|
+
switch (check.status) {
|
|
1693
|
+
case "ok":
|
|
1694
|
+
icon = "\u2713";
|
|
1695
|
+
color = chalk12.green;
|
|
1696
|
+
break;
|
|
1697
|
+
case "warning":
|
|
1698
|
+
icon = "!";
|
|
1699
|
+
color = chalk12.yellow;
|
|
1700
|
+
hasWarnings = true;
|
|
1701
|
+
break;
|
|
1702
|
+
case "error":
|
|
1703
|
+
icon = "\u2717";
|
|
1704
|
+
color = chalk12.red;
|
|
1705
|
+
hasErrors = true;
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
console.log(`${color(icon)} ${chalk12.bold(check.name)}: ${check.message}`);
|
|
1709
|
+
if (check.fix) {
|
|
1710
|
+
console.log(chalk12.dim(` Fix: ${check.fix}`));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
console.log("");
|
|
1714
|
+
if (hasErrors) {
|
|
1715
|
+
console.log(chalk12.red("Some checks failed. Please fix the issues above."));
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
} else if (hasWarnings) {
|
|
1718
|
+
console.log(chalk12.yellow("Some warnings detected. Review the suggestions above."));
|
|
1719
|
+
} else {
|
|
1720
|
+
console.log(chalk12.green("All checks passed!"));
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/commands/ports.ts
|
|
1725
|
+
import chalk13 from "chalk";
|
|
1726
|
+
import Table3 from "cli-table3";
|
|
1727
|
+
async function portsCommand() {
|
|
1728
|
+
const ports = getPortsTable();
|
|
1729
|
+
if (ports.length === 0) {
|
|
1730
|
+
console.log(chalk13.yellow("No ports allocated."));
|
|
1731
|
+
console.log(chalk13.dim("npm projects will have ports allocated when created."));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const table = new Table3({
|
|
1735
|
+
head: [chalk13.cyan("Port"), chalk13.cyan("Project"), chalk13.cyan("Hostname")],
|
|
1736
|
+
style: {
|
|
1737
|
+
head: [],
|
|
1738
|
+
border: []
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
for (const entry of ports) {
|
|
1742
|
+
table.push([String(entry.port), entry.project, entry.hostname]);
|
|
1743
|
+
}
|
|
1744
|
+
console.log(table.toString());
|
|
1745
|
+
console.log(chalk13.dim(`
|
|
1746
|
+
${ports.length} port(s) allocated`));
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/commands/info.ts
|
|
1750
|
+
import chalk14 from "chalk";
|
|
1751
|
+
async function infoCommand() {
|
|
1752
|
+
console.log(chalk14.bold.cyan(String.raw`
|
|
1753
|
+
_ _ _ _
|
|
1754
|
+
| |_|_|___ _| | |___ ___
|
|
1755
|
+
| . | | | . | | -_| _|
|
|
1756
|
+
|___|_|_|_|___|_|___|_|
|
|
1757
|
+
`));
|
|
1758
|
+
console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
1759
|
+
console.log(chalk14.white(" with Nginx and PM2\n"));
|
|
1760
|
+
console.log(chalk14.dim(" Version: ") + chalk14.white("1.0.0"));
|
|
1761
|
+
console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
|
|
1762
|
+
console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
|
|
1763
|
+
console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
|
|
1764
|
+
console.log("");
|
|
1765
|
+
if (configExists()) {
|
|
1766
|
+
const config = readConfig();
|
|
1767
|
+
const pm2Processes = getPm2List().filter((p) => p.name.startsWith("bindler:"));
|
|
1768
|
+
const runningCount = pm2Processes.filter((p) => p.status === "online").length;
|
|
1769
|
+
const npmProjects = config.projects.filter((p) => p.type === "npm").length;
|
|
1770
|
+
const staticProjects = config.projects.filter((p) => p.type === "static").length;
|
|
1771
|
+
console.log(chalk14.dim(" Config: ") + chalk14.white(getConfigPath()));
|
|
1772
|
+
console.log(chalk14.dim(" Nginx: ") + chalk14.white(config.defaults.nginxManagedPath));
|
|
1773
|
+
console.log(chalk14.dim(" Tunnel: ") + chalk14.white(config.defaults.tunnelName));
|
|
1774
|
+
console.log("");
|
|
1775
|
+
console.log(chalk14.dim(" Projects: ") + chalk14.white(`${config.projects.length} total (${staticProjects} static, ${npmProjects} npm)`));
|
|
1776
|
+
if (npmProjects > 0) {
|
|
1777
|
+
console.log(chalk14.dim(" Running: ") + chalk14.green(`${runningCount}/${npmProjects} npm apps online`));
|
|
1778
|
+
}
|
|
1779
|
+
} else {
|
|
1780
|
+
console.log(chalk14.dim(" Config: ") + chalk14.yellow("Not initialized"));
|
|
1781
|
+
console.log(chalk14.dim(" ") + chalk14.dim("Run `bindler new` to get started"));
|
|
1782
|
+
}
|
|
1783
|
+
console.log("");
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/commands/check.ts
|
|
1787
|
+
import chalk15 from "chalk";
|
|
1788
|
+
import { resolve4, resolve6, resolveCname } from "dns/promises";
|
|
1789
|
+
async function checkDns(hostname) {
|
|
1790
|
+
const result = {
|
|
1791
|
+
resolved: false,
|
|
1792
|
+
ipv4: [],
|
|
1793
|
+
ipv6: [],
|
|
1794
|
+
cname: [],
|
|
1795
|
+
isCloudflare: false
|
|
1796
|
+
};
|
|
1797
|
+
try {
|
|
1798
|
+
result.ipv4 = await resolve4(hostname);
|
|
1799
|
+
result.resolved = true;
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
result.ipv6 = await resolve6(hostname);
|
|
1804
|
+
result.resolved = true;
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
result.cname = await resolveCname(hostname);
|
|
1809
|
+
result.resolved = true;
|
|
1810
|
+
result.isCloudflare = result.cname.some(
|
|
1811
|
+
(c) => c.includes("cfargotunnel.com") || c.includes("cloudflare")
|
|
1812
|
+
);
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
return result;
|
|
1816
|
+
}
|
|
1817
|
+
async function checkHttp(hostname, path = "/") {
|
|
1818
|
+
const url = `https://${hostname}${path}`;
|
|
1819
|
+
const startTime = Date.now();
|
|
1820
|
+
try {
|
|
1821
|
+
const controller = new AbortController();
|
|
1822
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1823
|
+
const response = await fetch(url, {
|
|
1824
|
+
method: "HEAD",
|
|
1825
|
+
redirect: "manual",
|
|
1826
|
+
signal: controller.signal
|
|
1827
|
+
});
|
|
1828
|
+
clearTimeout(timeout);
|
|
1829
|
+
const responseTime = Date.now() - startTime;
|
|
1830
|
+
return {
|
|
1831
|
+
reachable: true,
|
|
1832
|
+
statusCode: response.status,
|
|
1833
|
+
redirectUrl: response.headers.get("location") || void 0,
|
|
1834
|
+
responseTime
|
|
1835
|
+
};
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
return {
|
|
1838
|
+
reachable: false,
|
|
1839
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async function checkCommand(hostnameOrName, options) {
|
|
1844
|
+
const project = getProject(hostnameOrName);
|
|
1845
|
+
const hostname = project ? project.hostname : hostnameOrName;
|
|
1846
|
+
const basePath = project?.basePath || "/";
|
|
1847
|
+
console.log(chalk15.blue(`
|
|
1848
|
+
Checking ${hostname}...
|
|
1849
|
+
`));
|
|
1850
|
+
console.log(chalk15.bold("DNS Resolution:"));
|
|
1851
|
+
const dns = await checkDns(hostname);
|
|
1852
|
+
if (!dns.resolved) {
|
|
1853
|
+
console.log(chalk15.red(" \u2717 DNS not resolving"));
|
|
1854
|
+
console.log(chalk15.dim(" The hostname does not have any DNS records."));
|
|
1855
|
+
console.log(chalk15.dim(" Run: cloudflared tunnel route dns <tunnel> " + hostname));
|
|
1856
|
+
console.log("");
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (dns.cname.length > 0) {
|
|
1860
|
+
console.log(chalk15.green(" \u2713 CNAME: ") + dns.cname.join(", "));
|
|
1861
|
+
if (dns.isCloudflare) {
|
|
1862
|
+
console.log(chalk15.green(" \u2713 Points to Cloudflare Tunnel"));
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (dns.ipv4.length > 0) {
|
|
1866
|
+
console.log(chalk15.green(" \u2713 A (IPv4): ") + dns.ipv4.join(", "));
|
|
1867
|
+
}
|
|
1868
|
+
if (dns.ipv6.length > 0) {
|
|
1869
|
+
console.log(chalk15.green(" \u2713 AAAA (IPv6): ") + dns.ipv6.join(", "));
|
|
1870
|
+
}
|
|
1871
|
+
console.log("");
|
|
1872
|
+
console.log(chalk15.bold("HTTP Check:"));
|
|
1873
|
+
const http = await checkHttp(hostname, basePath);
|
|
1874
|
+
if (!http.reachable) {
|
|
1875
|
+
console.log(chalk15.red(" \u2717 Not reachable"));
|
|
1876
|
+
console.log(chalk15.dim(` Error: ${http.error}`));
|
|
1877
|
+
console.log("");
|
|
1878
|
+
console.log(chalk15.yellow("Possible issues:"));
|
|
1879
|
+
console.log(chalk15.dim(" - Cloudflare tunnel not running"));
|
|
1880
|
+
console.log(chalk15.dim(" - Nginx not running or misconfigured"));
|
|
1881
|
+
console.log(chalk15.dim(" - Project not started (if npm app)"));
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const statusColor = http.statusCode < 400 ? chalk15.green : chalk15.red;
|
|
1885
|
+
console.log(statusColor(` \u2713 Status: ${http.statusCode}`));
|
|
1886
|
+
console.log(chalk15.dim(` Response time: ${http.responseTime}ms`));
|
|
1887
|
+
if (http.redirectUrl) {
|
|
1888
|
+
console.log(chalk15.dim(` Redirects to: ${http.redirectUrl}`));
|
|
1889
|
+
}
|
|
1890
|
+
console.log("");
|
|
1891
|
+
if (dns.resolved && http.reachable && http.statusCode < 400) {
|
|
1892
|
+
console.log(chalk15.green("\u2713 All checks passed! Site is accessible."));
|
|
1893
|
+
} else if (dns.resolved && http.reachable) {
|
|
1894
|
+
console.log(chalk15.yellow("! Site is reachable but returned an error status."));
|
|
1895
|
+
} else {
|
|
1896
|
+
console.log(chalk15.red("\u2717 Some checks failed. See details above."));
|
|
1897
|
+
}
|
|
1898
|
+
if (project) {
|
|
1899
|
+
console.log(chalk15.dim(`
|
|
1900
|
+
Project: ${project.name} (${project.type})`));
|
|
1901
|
+
if (project.type === "npm") {
|
|
1902
|
+
console.log(chalk15.dim(`Port: ${project.port}`));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
console.log("");
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/commands/setup.ts
|
|
1909
|
+
import chalk16 from "chalk";
|
|
1910
|
+
import inquirer3 from "inquirer";
|
|
1911
|
+
import { execSync as execSync2 } from "child_process";
|
|
1912
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
1913
|
+
function detectOs() {
|
|
1914
|
+
const platform = process.platform;
|
|
1915
|
+
if (platform === "darwin") {
|
|
1916
|
+
return { platform: "darwin", distro: "macOS", packageManager: "brew" };
|
|
1917
|
+
}
|
|
1918
|
+
if (platform === "win32") {
|
|
1919
|
+
return { platform: "win32", distro: "Windows", packageManager: "winget" };
|
|
1920
|
+
}
|
|
1921
|
+
if (platform === "linux") {
|
|
1922
|
+
try {
|
|
1923
|
+
if (existsSync6("/etc/os-release")) {
|
|
1924
|
+
const osRelease = readFileSync4("/etc/os-release", "utf-8");
|
|
1925
|
+
const lines = osRelease.split("\n");
|
|
1926
|
+
const info = {};
|
|
1927
|
+
for (const line of lines) {
|
|
1928
|
+
const [key, ...valueParts] = line.split("=");
|
|
1929
|
+
if (key && valueParts.length > 0) {
|
|
1930
|
+
info[key] = valueParts.join("=").replace(/"/g, "");
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const distro = info["ID"] || "linux";
|
|
1934
|
+
const version = info["VERSION_ID"];
|
|
1935
|
+
const codename = info["VERSION_CODENAME"];
|
|
1936
|
+
let packageManager;
|
|
1937
|
+
if (["ubuntu", "debian", "pop", "mint", "elementary"].includes(distro)) {
|
|
1938
|
+
packageManager = "apt";
|
|
1939
|
+
} else if (["fedora", "rhel", "centos", "rocky", "alma"].includes(distro)) {
|
|
1940
|
+
packageManager = existsSync6("/usr/bin/dnf") ? "dnf" : "yum";
|
|
1941
|
+
} else if (["amzn"].includes(distro)) {
|
|
1942
|
+
packageManager = "yum";
|
|
1943
|
+
}
|
|
1944
|
+
return { platform: "linux", distro, version, codename, packageManager };
|
|
1945
|
+
}
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
return { platform: "linux", distro: "unknown" };
|
|
1949
|
+
}
|
|
1950
|
+
return { platform: "unknown" };
|
|
1951
|
+
}
|
|
1952
|
+
function runCommand(command, description) {
|
|
1953
|
+
console.log(chalk16.dim(` \u2192 ${description}...`));
|
|
1954
|
+
try {
|
|
1955
|
+
execSync2(command, { stdio: "inherit" });
|
|
1956
|
+
return true;
|
|
1957
|
+
} catch {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async function installNginx(os) {
|
|
1962
|
+
console.log(chalk16.blue("\nInstalling nginx...\n"));
|
|
1963
|
+
if (os.platform === "darwin") {
|
|
1964
|
+
return runCommand("brew install nginx", "Installing via Homebrew");
|
|
1965
|
+
}
|
|
1966
|
+
if (os.platform === "linux" && os.packageManager === "apt") {
|
|
1967
|
+
runCommand("sudo apt-get update", "Updating package list");
|
|
1968
|
+
return runCommand("sudo apt-get install -y nginx", "Installing nginx");
|
|
1969
|
+
}
|
|
1970
|
+
if (os.platform === "linux" && (os.packageManager === "yum" || os.packageManager === "dnf")) {
|
|
1971
|
+
return runCommand(`sudo ${os.packageManager} install -y nginx`, "Installing nginx");
|
|
1972
|
+
}
|
|
1973
|
+
console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
|
|
1974
|
+
console.log(chalk16.dim(" Please install nginx manually."));
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
async function installPm2() {
|
|
1978
|
+
console.log(chalk16.blue("\nInstalling PM2...\n"));
|
|
1979
|
+
return runCommand("npm install -g pm2", "Installing via npm");
|
|
1980
|
+
}
|
|
1981
|
+
async function installCertbot(os) {
|
|
1982
|
+
console.log(chalk16.blue("\nInstalling certbot (Let's Encrypt)...\n"));
|
|
1983
|
+
if (os.platform === "darwin") {
|
|
1984
|
+
return runCommand("brew install certbot", "Installing via Homebrew");
|
|
1985
|
+
}
|
|
1986
|
+
if (os.platform === "linux" && os.packageManager === "apt") {
|
|
1987
|
+
runCommand("sudo apt-get update", "Updating package list");
|
|
1988
|
+
return runCommand("sudo apt-get install -y certbot python3-certbot-nginx", "Installing certbot");
|
|
1989
|
+
}
|
|
1990
|
+
if (os.platform === "linux" && (os.packageManager === "yum" || os.packageManager === "dnf")) {
|
|
1991
|
+
return runCommand(`sudo ${os.packageManager} install -y certbot python3-certbot-nginx`, "Installing certbot");
|
|
1992
|
+
}
|
|
1993
|
+
console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
|
|
1994
|
+
console.log(chalk16.dim(" Please install certbot manually."));
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
function isCertbotInstalled() {
|
|
1998
|
+
const result = execCommandSafe("which certbot");
|
|
1999
|
+
return result.success;
|
|
2000
|
+
}
|
|
2001
|
+
async function installCloudflared(os) {
|
|
2002
|
+
console.log(chalk16.blue("\nInstalling cloudflared...\n"));
|
|
2003
|
+
if (os.platform === "darwin") {
|
|
2004
|
+
return runCommand("brew install cloudflared", "Installing via Homebrew");
|
|
2005
|
+
}
|
|
2006
|
+
if (os.platform === "win32") {
|
|
2007
|
+
return runCommand("winget install --id Cloudflare.cloudflared", "Installing via winget");
|
|
2008
|
+
}
|
|
2009
|
+
if (os.platform === "linux" && os.packageManager === "apt") {
|
|
2010
|
+
runCommand(
|
|
2011
|
+
"sudo mkdir -p --mode=0755 /usr/share/keyrings",
|
|
2012
|
+
"Creating keyrings directory"
|
|
2013
|
+
);
|
|
2014
|
+
runCommand(
|
|
2015
|
+
"curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null",
|
|
2016
|
+
"Adding Cloudflare GPG key"
|
|
2017
|
+
);
|
|
2018
|
+
let repoLine = "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main";
|
|
2019
|
+
if (os.codename) {
|
|
2020
|
+
repoLine = `deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared ${os.codename} main`;
|
|
2021
|
+
}
|
|
2022
|
+
runCommand(
|
|
2023
|
+
`echo '${repoLine}' | sudo tee /etc/apt/sources.list.d/cloudflared.list`,
|
|
2024
|
+
"Adding Cloudflare repository"
|
|
2025
|
+
);
|
|
2026
|
+
runCommand("sudo apt-get update", "Updating package list");
|
|
2027
|
+
return runCommand("sudo apt-get install -y cloudflared", "Installing cloudflared");
|
|
2028
|
+
}
|
|
2029
|
+
if (os.platform === "linux" && (os.packageManager === "yum" || os.packageManager === "dnf")) {
|
|
2030
|
+
runCommand(
|
|
2031
|
+
"curl -fsSl https://pkg.cloudflare.com/cloudflared.repo | sudo tee /etc/yum.repos.d/cloudflared.repo",
|
|
2032
|
+
"Adding Cloudflare repository"
|
|
2033
|
+
);
|
|
2034
|
+
return runCommand(`sudo ${os.packageManager} install -y cloudflared`, "Installing cloudflared");
|
|
2035
|
+
}
|
|
2036
|
+
console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
|
|
2037
|
+
console.log(chalk16.dim(" Visit: https://pkg.cloudflare.com/index.html"));
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
async function setupCommand(options = {}) {
|
|
2041
|
+
console.log(chalk16.bold.cyan("\nBindler Setup\n"));
|
|
2042
|
+
const os = detectOs();
|
|
2043
|
+
console.log(chalk16.dim(`Detected: ${os.distro || os.platform}${os.version ? ` ${os.version}` : ""}${os.codename ? ` (${os.codename})` : ""}`));
|
|
2044
|
+
const isDirect = options.direct;
|
|
2045
|
+
if (isDirect) {
|
|
2046
|
+
console.log(chalk16.cyan("\nDirect mode (VPS without Cloudflare Tunnel)"));
|
|
2047
|
+
console.log(chalk16.dim("nginx will listen on port 80/443 directly\n"));
|
|
2048
|
+
} else {
|
|
2049
|
+
console.log(chalk16.cyan("\nTunnel mode (via Cloudflare Tunnel)"));
|
|
2050
|
+
console.log(chalk16.dim("nginx will listen on 127.0.0.1:8080\n"));
|
|
2051
|
+
}
|
|
2052
|
+
const missing = [];
|
|
2053
|
+
if (!isNginxInstalled()) {
|
|
2054
|
+
missing.push({ name: "nginx", install: () => installNginx(os) });
|
|
2055
|
+
}
|
|
2056
|
+
if (!isPm2Installed()) {
|
|
2057
|
+
missing.push({ name: "PM2", install: () => installPm2() });
|
|
2058
|
+
}
|
|
2059
|
+
if (isDirect) {
|
|
2060
|
+
if (!isCertbotInstalled()) {
|
|
2061
|
+
missing.push({ name: "certbot", install: () => installCertbot(os) });
|
|
2062
|
+
}
|
|
2063
|
+
} else {
|
|
2064
|
+
if (!isCloudflaredInstalled()) {
|
|
2065
|
+
missing.push({ name: "cloudflared", install: () => installCloudflared(os) });
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (missing.length === 0) {
|
|
2069
|
+
console.log(chalk16.green("\u2713 All dependencies are already installed!\n"));
|
|
2070
|
+
} else {
|
|
2071
|
+
console.log(chalk16.yellow("Missing dependencies:\n"));
|
|
2072
|
+
for (const dep of missing) {
|
|
2073
|
+
console.log(chalk16.red(` \u2717 ${dep.name}`));
|
|
2074
|
+
}
|
|
2075
|
+
console.log("");
|
|
2076
|
+
const { toInstall } = await inquirer3.prompt([
|
|
2077
|
+
{
|
|
2078
|
+
type: "checkbox",
|
|
2079
|
+
name: "toInstall",
|
|
2080
|
+
message: "Select dependencies to install:",
|
|
2081
|
+
choices: missing.map((dep) => ({
|
|
2082
|
+
name: dep.name,
|
|
2083
|
+
value: dep.name,
|
|
2084
|
+
checked: true
|
|
2085
|
+
}))
|
|
2086
|
+
}
|
|
2087
|
+
]);
|
|
2088
|
+
if (toInstall.length > 0) {
|
|
2089
|
+
const results = [];
|
|
2090
|
+
for (const depName of toInstall) {
|
|
2091
|
+
const dep = missing.find((m) => m.name === depName);
|
|
2092
|
+
if (dep) {
|
|
2093
|
+
const success = await dep.install();
|
|
2094
|
+
results.push({ name: depName, success });
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
console.log(chalk16.bold("\n\nInstallation Summary:\n"));
|
|
2098
|
+
for (const result of results) {
|
|
2099
|
+
if (result.success) {
|
|
2100
|
+
console.log(chalk16.green(` \u2713 ${result.name} installed`));
|
|
2101
|
+
} else {
|
|
2102
|
+
console.log(chalk16.red(` \u2717 ${result.name} failed`));
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
console.log("");
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const config = configExists() ? readConfig() : initConfig();
|
|
2109
|
+
if (isDirect) {
|
|
2110
|
+
config.defaults.mode = "direct";
|
|
2111
|
+
config.defaults.nginxListen = "80";
|
|
2112
|
+
config.defaults.applyCloudflareDnsRoutes = false;
|
|
2113
|
+
const { enableSsl } = await inquirer3.prompt([
|
|
2114
|
+
{
|
|
2115
|
+
type: "confirm",
|
|
2116
|
+
name: "enableSsl",
|
|
2117
|
+
message: "Enable SSL with Let's Encrypt?",
|
|
2118
|
+
default: true
|
|
2119
|
+
}
|
|
2120
|
+
]);
|
|
2121
|
+
if (enableSsl) {
|
|
2122
|
+
const { email } = await inquirer3.prompt([
|
|
2123
|
+
{
|
|
2124
|
+
type: "input",
|
|
2125
|
+
name: "email",
|
|
2126
|
+
message: "Email for Let's Encrypt notifications:",
|
|
2127
|
+
validate: (input) => {
|
|
2128
|
+
if (!input || !input.includes("@")) {
|
|
2129
|
+
return "Please enter a valid email address";
|
|
2130
|
+
}
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
]);
|
|
2135
|
+
config.defaults.sslEnabled = true;
|
|
2136
|
+
config.defaults.sslEmail = email;
|
|
2137
|
+
}
|
|
2138
|
+
} else {
|
|
2139
|
+
config.defaults.mode = "tunnel";
|
|
2140
|
+
config.defaults.nginxListen = "127.0.0.1:8080";
|
|
2141
|
+
config.defaults.applyCloudflareDnsRoutes = true;
|
|
2142
|
+
}
|
|
2143
|
+
writeConfig(config);
|
|
2144
|
+
console.log(chalk16.green("\n\u2713 Setup complete!\n"));
|
|
2145
|
+
console.log(chalk16.dim(`Mode: ${config.defaults.mode}`));
|
|
2146
|
+
console.log(chalk16.dim(`nginx listen: ${config.defaults.nginxListen}`));
|
|
2147
|
+
if (config.defaults.sslEnabled) {
|
|
2148
|
+
console.log(chalk16.dim(`SSL: enabled (${config.defaults.sslEmail})`));
|
|
2149
|
+
}
|
|
2150
|
+
console.log(chalk16.dim("\nRun `bindler new` to create your first project."));
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/cli.ts
|
|
2154
|
+
var program = new Command();
|
|
2155
|
+
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.0.0");
|
|
2156
|
+
program.hook("preAction", () => {
|
|
2157
|
+
try {
|
|
2158
|
+
initConfig();
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
program.command("new").description("Create and register a new project").option("-n, --name <name>", "Project name").option("-t, --type <type>", "Project type (static or npm)", "static").option("-p, --path <path>", "Project directory path").option("-h, --hostname <hostname>", "Hostname for the project").option("-b, --base-path <path>", "Base path for path-based routing (e.g., /api)").option("--port <port>", "Port number (npm projects only)").option("-s, --start <command>", "Start command (npm projects only)").option("-l, --local", "Local project (skip Cloudflare, use .local hostname)").option("--apply", "Apply nginx config after creating").action(async (options) => {
|
|
2163
|
+
await newCommand(options);
|
|
2164
|
+
});
|
|
2165
|
+
program.command("list").alias("ls").description("List all registered projects").action(async () => {
|
|
2166
|
+
await listCommand();
|
|
2167
|
+
});
|
|
2168
|
+
program.command("status").description("Show detailed status of all projects").action(async () => {
|
|
2169
|
+
await statusCommand();
|
|
2170
|
+
});
|
|
2171
|
+
program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
|
|
2172
|
+
await startCommand(name, options);
|
|
2173
|
+
});
|
|
2174
|
+
program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
|
|
2175
|
+
await stopCommand(name, options);
|
|
2176
|
+
});
|
|
2177
|
+
program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
|
|
2178
|
+
await restartCommand(name, options);
|
|
2179
|
+
});
|
|
2180
|
+
program.command("logs <name>").description("Show logs for an npm project").option("-f, --follow", "Follow log output").option("-l, --lines <n>", "Number of lines to show", "200").action(async (name, options) => {
|
|
2181
|
+
await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
|
|
2182
|
+
});
|
|
2183
|
+
program.command("update <name>").description("Update project configuration").option("-h, --hostname <hostname>", "New hostname").option("--port <port>", "New port number").option("-s, --start <command>", "New start command").option("-p, --path <path>", "New project path").option("-e, --env <vars...>", "Environment variables (KEY=value)").option("--enable", "Enable the project").option("--disable", "Disable the project").action(async (name, options) => {
|
|
2184
|
+
await updateCommand(name, options);
|
|
2185
|
+
});
|
|
2186
|
+
program.command("edit <name>").description("Edit project configuration in $EDITOR").action(async (name) => {
|
|
2187
|
+
await editCommand(name);
|
|
2188
|
+
});
|
|
2189
|
+
program.command("remove <name>").alias("rm").description("Remove a project from registry").option("-f, --force", "Skip confirmation").option("--apply", "Apply nginx config after removing").action(async (name, options) => {
|
|
2190
|
+
await removeCommand(name, options);
|
|
2191
|
+
});
|
|
2192
|
+
program.command("apply").description("Generate and apply nginx configuration + Cloudflare DNS routes").option("-d, --dry-run", "Print config without applying").option("--no-reload", "Write config but do not reload nginx").option("--no-cloudflare", "Skip Cloudflare DNS route configuration").option("--no-ssl", "Skip SSL certificate setup (direct mode)").action(async (options) => {
|
|
2193
|
+
await applyCommand(options);
|
|
2194
|
+
});
|
|
2195
|
+
program.command("doctor").description("Run system diagnostics and check dependencies").action(async () => {
|
|
2196
|
+
await doctorCommand();
|
|
2197
|
+
});
|
|
2198
|
+
program.command("ports").description("Show allocated ports").action(async () => {
|
|
2199
|
+
await portsCommand();
|
|
2200
|
+
});
|
|
2201
|
+
program.command("info").description("Show bindler information and stats").action(async () => {
|
|
2202
|
+
await infoCommand();
|
|
2203
|
+
});
|
|
2204
|
+
program.command("check <hostname>").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
|
|
2205
|
+
await checkCommand(hostname, options);
|
|
2206
|
+
});
|
|
2207
|
+
program.command("setup").description("Install missing dependencies (nginx, PM2, cloudflared)").option("--direct", "Direct mode for VPS (no Cloudflare Tunnel, use port 80/443)").action(async (options) => {
|
|
2208
|
+
await setupCommand(options);
|
|
2209
|
+
});
|
|
2210
|
+
program.parse();
|
|
2211
|
+
//# sourceMappingURL=cli.js.map
|