bindler 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +1005 -12
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import chalk26 from "chalk";
|
|
5
6
|
|
|
6
7
|
// src/commands/new.ts
|
|
7
8
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -161,28 +162,28 @@ function execCommandSafe(command, options) {
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
function spawnInteractive(command, args, options) {
|
|
164
|
-
return new Promise((
|
|
165
|
+
return new Promise((resolve2) => {
|
|
165
166
|
const child = spawn(command, args, {
|
|
166
167
|
stdio: "inherit",
|
|
167
168
|
...options
|
|
168
169
|
});
|
|
169
170
|
child.on("close", (code) => {
|
|
170
|
-
|
|
171
|
+
resolve2(code);
|
|
171
172
|
});
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
175
|
function isPortListening(port, host = "127.0.0.1") {
|
|
175
|
-
return new Promise((
|
|
176
|
+
return new Promise((resolve2) => {
|
|
176
177
|
const socket = createConnection({ port, host }, () => {
|
|
177
178
|
socket.destroy();
|
|
178
|
-
|
|
179
|
+
resolve2(true);
|
|
179
180
|
});
|
|
180
181
|
socket.on("error", () => {
|
|
181
|
-
|
|
182
|
+
resolve2(false);
|
|
182
183
|
});
|
|
183
184
|
socket.setTimeout(1e3, () => {
|
|
184
185
|
socket.destroy();
|
|
185
|
-
|
|
186
|
+
resolve2(false);
|
|
186
187
|
});
|
|
187
188
|
});
|
|
188
189
|
}
|
|
@@ -1757,7 +1758,7 @@ async function infoCommand() {
|
|
|
1757
1758
|
`));
|
|
1758
1759
|
console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
1759
1760
|
console.log(chalk14.white(" with Nginx and PM2\n"));
|
|
1760
|
-
console.log(chalk14.dim(" Version: ") + chalk14.white("1.0
|
|
1761
|
+
console.log(chalk14.dim(" Version: ") + chalk14.white("1.1.0"));
|
|
1761
1762
|
console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
|
|
1762
1763
|
console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
|
|
1763
1764
|
console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
|
|
@@ -2190,6 +2191,909 @@ async function setupCommand(options = {}) {
|
|
|
2190
2191
|
console.log(chalk16.dim("\nRun `bindler new` to create your first project."));
|
|
2191
2192
|
}
|
|
2192
2193
|
|
|
2194
|
+
// src/commands/init.ts
|
|
2195
|
+
import chalk17 from "chalk";
|
|
2196
|
+
import inquirer4 from "inquirer";
|
|
2197
|
+
async function initCommand() {
|
|
2198
|
+
console.log(chalk17.bold.cyan(`
|
|
2199
|
+
_ _ _ _
|
|
2200
|
+
| |_|_|___ _| | |___ ___
|
|
2201
|
+
| . | | | . | | -_| _|
|
|
2202
|
+
|___|_|_|_|___|_|___|_|
|
|
2203
|
+
`));
|
|
2204
|
+
console.log(chalk17.white(" Welcome to bindler!\n"));
|
|
2205
|
+
if (configExists()) {
|
|
2206
|
+
const config2 = readConfig();
|
|
2207
|
+
console.log(chalk17.yellow("Bindler is already initialized."));
|
|
2208
|
+
console.log(chalk17.dim(` Config: ~/.config/bindler/config.json`));
|
|
2209
|
+
console.log(chalk17.dim(` Mode: ${config2.defaults.mode || "tunnel"}`));
|
|
2210
|
+
console.log(chalk17.dim(` Projects: ${config2.projects.length}`));
|
|
2211
|
+
console.log("");
|
|
2212
|
+
const { reinit } = await inquirer4.prompt([
|
|
2213
|
+
{
|
|
2214
|
+
type: "confirm",
|
|
2215
|
+
name: "reinit",
|
|
2216
|
+
message: "Reconfigure bindler?",
|
|
2217
|
+
default: false
|
|
2218
|
+
}
|
|
2219
|
+
]);
|
|
2220
|
+
if (!reinit) {
|
|
2221
|
+
console.log(chalk17.dim("\nRun `bindler new` to add a project."));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
console.log(chalk17.bold("\n1. Choose your setup:\n"));
|
|
2226
|
+
const { mode } = await inquirer4.prompt([
|
|
2227
|
+
{
|
|
2228
|
+
type: "list",
|
|
2229
|
+
name: "mode",
|
|
2230
|
+
message: "How will you expose your projects?",
|
|
2231
|
+
choices: [
|
|
2232
|
+
{
|
|
2233
|
+
name: "Cloudflare Tunnel (recommended for home servers)",
|
|
2234
|
+
value: "tunnel"
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
name: "Direct (VPS with public IP, port 80/443)",
|
|
2238
|
+
value: "direct"
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
name: "Local only (development, no internet access)",
|
|
2242
|
+
value: "local"
|
|
2243
|
+
}
|
|
2244
|
+
]
|
|
2245
|
+
}
|
|
2246
|
+
]);
|
|
2247
|
+
console.log(chalk17.bold("\n2. Checking dependencies...\n"));
|
|
2248
|
+
const deps = {
|
|
2249
|
+
nginx: isNginxInstalled(),
|
|
2250
|
+
pm2: isPm2Installed(),
|
|
2251
|
+
cloudflared: isCloudflaredInstalled()
|
|
2252
|
+
};
|
|
2253
|
+
const nginxRunning = deps.nginx && isNginxRunning();
|
|
2254
|
+
console.log(deps.nginx ? chalk17.green(" \u2713 nginx") : chalk17.red(" \u2717 nginx"));
|
|
2255
|
+
console.log(deps.pm2 ? chalk17.green(" \u2713 pm2") : chalk17.red(" \u2717 pm2"));
|
|
2256
|
+
if (mode === "tunnel") {
|
|
2257
|
+
console.log(deps.cloudflared ? chalk17.green(" \u2713 cloudflared") : chalk17.red(" \u2717 cloudflared"));
|
|
2258
|
+
} else if (mode === "direct") {
|
|
2259
|
+
console.log(chalk17.dim(" - cloudflared (not needed for direct mode)"));
|
|
2260
|
+
}
|
|
2261
|
+
const missingDeps = !deps.nginx || !deps.pm2 || mode === "tunnel" && !deps.cloudflared;
|
|
2262
|
+
if (missingDeps) {
|
|
2263
|
+
console.log("");
|
|
2264
|
+
const { install } = await inquirer4.prompt([
|
|
2265
|
+
{
|
|
2266
|
+
type: "confirm",
|
|
2267
|
+
name: "install",
|
|
2268
|
+
message: "Install missing dependencies?",
|
|
2269
|
+
default: true
|
|
2270
|
+
}
|
|
2271
|
+
]);
|
|
2272
|
+
if (install) {
|
|
2273
|
+
await setupCommand({ direct: mode === "direct" });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
console.log(chalk17.bold("\n3. Configuration:\n"));
|
|
2277
|
+
let tunnelName = "homelab";
|
|
2278
|
+
let sslEmail = "";
|
|
2279
|
+
if (mode === "tunnel") {
|
|
2280
|
+
const answers = await inquirer4.prompt([
|
|
2281
|
+
{
|
|
2282
|
+
type: "input",
|
|
2283
|
+
name: "tunnelName",
|
|
2284
|
+
message: "Cloudflare tunnel name:",
|
|
2285
|
+
default: "homelab"
|
|
2286
|
+
}
|
|
2287
|
+
]);
|
|
2288
|
+
tunnelName = answers.tunnelName;
|
|
2289
|
+
}
|
|
2290
|
+
if (mode === "direct") {
|
|
2291
|
+
const answers = await inquirer4.prompt([
|
|
2292
|
+
{
|
|
2293
|
+
type: "confirm",
|
|
2294
|
+
name: "enableSsl",
|
|
2295
|
+
message: "Enable SSL with Let's Encrypt?",
|
|
2296
|
+
default: true
|
|
2297
|
+
}
|
|
2298
|
+
]);
|
|
2299
|
+
if (answers.enableSsl) {
|
|
2300
|
+
const emailAnswer = await inquirer4.prompt([
|
|
2301
|
+
{
|
|
2302
|
+
type: "input",
|
|
2303
|
+
name: "email",
|
|
2304
|
+
message: "Email for SSL certificates:",
|
|
2305
|
+
validate: (input) => input.includes("@") || "Enter a valid email"
|
|
2306
|
+
}
|
|
2307
|
+
]);
|
|
2308
|
+
sslEmail = emailAnswer.email;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const config = configExists() ? readConfig() : initConfig();
|
|
2312
|
+
config.defaults.mode = mode === "local" ? "tunnel" : mode;
|
|
2313
|
+
config.defaults.tunnelName = tunnelName;
|
|
2314
|
+
config.defaults.applyCloudflareDnsRoutes = mode === "tunnel";
|
|
2315
|
+
if (mode === "direct") {
|
|
2316
|
+
config.defaults.nginxListen = "80";
|
|
2317
|
+
if (sslEmail) {
|
|
2318
|
+
config.defaults.sslEnabled = true;
|
|
2319
|
+
config.defaults.sslEmail = sslEmail;
|
|
2320
|
+
}
|
|
2321
|
+
} else if (mode === "local") {
|
|
2322
|
+
config.defaults.nginxListen = "127.0.0.1:8080";
|
|
2323
|
+
config.defaults.applyCloudflareDnsRoutes = false;
|
|
2324
|
+
} else {
|
|
2325
|
+
config.defaults.nginxListen = "127.0.0.1:8080";
|
|
2326
|
+
}
|
|
2327
|
+
writeConfig(config);
|
|
2328
|
+
console.log(chalk17.green("\n\u2713 Bindler initialized!\n"));
|
|
2329
|
+
console.log(chalk17.dim(" Mode: ") + chalk17.white(mode));
|
|
2330
|
+
console.log(chalk17.dim(" Listen: ") + chalk17.white(config.defaults.nginxListen));
|
|
2331
|
+
if (mode === "tunnel") {
|
|
2332
|
+
console.log(chalk17.dim(" Tunnel: ") + chalk17.white(tunnelName));
|
|
2333
|
+
}
|
|
2334
|
+
if (sslEmail) {
|
|
2335
|
+
console.log(chalk17.dim(" SSL: ") + chalk17.white(sslEmail));
|
|
2336
|
+
}
|
|
2337
|
+
console.log(chalk17.bold("\nNext steps:\n"));
|
|
2338
|
+
console.log(chalk17.dim(" 1. ") + chalk17.white("bindler new") + chalk17.dim(" # add your first project"));
|
|
2339
|
+
console.log(chalk17.dim(" 2. ") + chalk17.white("bindler apply") + chalk17.dim(" # apply nginx config"));
|
|
2340
|
+
if (mode === "tunnel") {
|
|
2341
|
+
console.log(chalk17.dim(" 3. ") + chalk17.white(`cloudflared tunnel run ${tunnelName}`) + chalk17.dim(" # start tunnel"));
|
|
2342
|
+
}
|
|
2343
|
+
console.log("");
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/commands/deploy.ts
|
|
2347
|
+
import chalk18 from "chalk";
|
|
2348
|
+
import { execSync as execSync3 } from "child_process";
|
|
2349
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2350
|
+
import { join as join5 } from "path";
|
|
2351
|
+
function runInDir(command, cwd) {
|
|
2352
|
+
try {
|
|
2353
|
+
const output = execSync3(command, { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
2354
|
+
return { success: true, output: output.trim() };
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
const err = error;
|
|
2357
|
+
return { success: false, output: err.stderr || err.message || "Unknown error" };
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async function deployCommand(name, options) {
|
|
2361
|
+
if (!name) {
|
|
2362
|
+
console.log(chalk18.red("Usage: bindler deploy <name>"));
|
|
2363
|
+
console.log(chalk18.dim("\nDeploys a project: git pull + npm install + restart"));
|
|
2364
|
+
console.log(chalk18.dim("\nExamples:"));
|
|
2365
|
+
console.log(chalk18.dim(" bindler deploy myapp"));
|
|
2366
|
+
console.log(chalk18.dim(" bindler deploy myapp --skip-install"));
|
|
2367
|
+
console.log(chalk18.dim(" bindler deploy myapp --skip-pull"));
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
const project = getProject(name);
|
|
2371
|
+
if (!project) {
|
|
2372
|
+
console.log(chalk18.red(`Project "${name}" not found.`));
|
|
2373
|
+
console.log(chalk18.dim("\nAvailable projects:"));
|
|
2374
|
+
const projects = listProjects();
|
|
2375
|
+
for (const p of projects) {
|
|
2376
|
+
console.log(chalk18.dim(` - ${p.name}`));
|
|
2377
|
+
}
|
|
2378
|
+
process.exit(1);
|
|
2379
|
+
}
|
|
2380
|
+
if (!existsSync7(project.path)) {
|
|
2381
|
+
console.log(chalk18.red(`Project path does not exist: ${project.path}`));
|
|
2382
|
+
process.exit(1);
|
|
2383
|
+
}
|
|
2384
|
+
console.log(chalk18.blue(`
|
|
2385
|
+
Deploying ${project.name}...
|
|
2386
|
+
`));
|
|
2387
|
+
if (!options.skipPull) {
|
|
2388
|
+
const isGitRepo = existsSync7(join5(project.path, ".git"));
|
|
2389
|
+
if (isGitRepo) {
|
|
2390
|
+
console.log(chalk18.dim("Pulling latest changes..."));
|
|
2391
|
+
const result = runInDir("git pull", project.path);
|
|
2392
|
+
if (result.success) {
|
|
2393
|
+
if (result.output.includes("Already up to date")) {
|
|
2394
|
+
console.log(chalk18.green(" \u2713 Already up to date"));
|
|
2395
|
+
} else {
|
|
2396
|
+
console.log(chalk18.green(" \u2713 Pulled latest changes"));
|
|
2397
|
+
if (result.output) {
|
|
2398
|
+
console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
} else {
|
|
2402
|
+
console.log(chalk18.yellow(" ! Git pull failed"));
|
|
2403
|
+
console.log(chalk18.dim(` ${result.output}`));
|
|
2404
|
+
}
|
|
2405
|
+
} else {
|
|
2406
|
+
console.log(chalk18.dim(" - Not a git repository, skipping pull"));
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
console.log(chalk18.dim(" - Skipped git pull (--skip-pull)"));
|
|
2410
|
+
}
|
|
2411
|
+
if (project.type === "npm" && !options.skipInstall) {
|
|
2412
|
+
const hasPackageJson = existsSync7(join5(project.path, "package.json"));
|
|
2413
|
+
if (hasPackageJson) {
|
|
2414
|
+
console.log(chalk18.dim("Installing dependencies..."));
|
|
2415
|
+
const result = runInDir("npm install", project.path);
|
|
2416
|
+
if (result.success) {
|
|
2417
|
+
console.log(chalk18.green(" \u2713 Dependencies installed"));
|
|
2418
|
+
} else {
|
|
2419
|
+
console.log(chalk18.yellow(" ! npm install failed"));
|
|
2420
|
+
console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
} else if (options.skipInstall) {
|
|
2424
|
+
console.log(chalk18.dim(" - Skipped npm install (--skip-install)"));
|
|
2425
|
+
}
|
|
2426
|
+
if (project.type === "npm" && !options.skipRestart) {
|
|
2427
|
+
console.log(chalk18.dim("Restarting application..."));
|
|
2428
|
+
const result = restartProject(name);
|
|
2429
|
+
if (result.success) {
|
|
2430
|
+
console.log(chalk18.green(" \u2713 Application restarted"));
|
|
2431
|
+
} else {
|
|
2432
|
+
console.log(chalk18.yellow(` ! Restart failed: ${result.error}`));
|
|
2433
|
+
console.log(chalk18.dim(` Try: bindler start ${name}`));
|
|
2434
|
+
}
|
|
2435
|
+
} else if (project.type === "static") {
|
|
2436
|
+
console.log(chalk18.dim(" - Static project, no restart needed"));
|
|
2437
|
+
} else if (options.skipRestart) {
|
|
2438
|
+
console.log(chalk18.dim(" - Skipped restart (--skip-restart)"));
|
|
2439
|
+
}
|
|
2440
|
+
console.log(chalk18.green(`
|
|
2441
|
+
\u2713 Deploy complete for ${project.name}
|
|
2442
|
+
`));
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/backup.ts
|
|
2446
|
+
import chalk19 from "chalk";
|
|
2447
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
2448
|
+
import { dirname as dirname3, resolve } from "path";
|
|
2449
|
+
import { homedir as homedir2 } from "os";
|
|
2450
|
+
async function backupCommand(options) {
|
|
2451
|
+
const config = readConfig();
|
|
2452
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
2453
|
+
const defaultPath = resolve(homedir2(), `bindler-backup-${timestamp}.json`);
|
|
2454
|
+
const outputPath = options.output || defaultPath;
|
|
2455
|
+
const dir = dirname3(outputPath);
|
|
2456
|
+
if (!existsSync8(dir)) {
|
|
2457
|
+
mkdirSync4(dir, { recursive: true });
|
|
2458
|
+
}
|
|
2459
|
+
const backup = {
|
|
2460
|
+
version: 1,
|
|
2461
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2462
|
+
config
|
|
2463
|
+
};
|
|
2464
|
+
writeFileSync4(outputPath, JSON.stringify(backup, null, 2) + "\n");
|
|
2465
|
+
console.log(chalk19.green(`
|
|
2466
|
+
\u2713 Backup saved to ${outputPath}
|
|
2467
|
+
`));
|
|
2468
|
+
console.log(chalk19.dim(` Projects: ${config.projects.length}`));
|
|
2469
|
+
console.log(chalk19.dim(` Mode: ${config.defaults.mode || "tunnel"}`));
|
|
2470
|
+
console.log("");
|
|
2471
|
+
console.log(chalk19.dim("Restore with:"));
|
|
2472
|
+
console.log(chalk19.cyan(` bindler restore ${outputPath}`));
|
|
2473
|
+
console.log("");
|
|
2474
|
+
}
|
|
2475
|
+
async function restoreCommand(file, options) {
|
|
2476
|
+
if (!file) {
|
|
2477
|
+
console.log(chalk19.red("Usage: bindler restore <file>"));
|
|
2478
|
+
console.log(chalk19.dim("\nExamples:"));
|
|
2479
|
+
console.log(chalk19.dim(" bindler restore ~/bindler-backup.json"));
|
|
2480
|
+
console.log(chalk19.dim(" bindler restore backup.json --force"));
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
const filePath = resolve(file);
|
|
2484
|
+
if (!existsSync8(filePath)) {
|
|
2485
|
+
console.log(chalk19.red(`File not found: ${filePath}`));
|
|
2486
|
+
process.exit(1);
|
|
2487
|
+
}
|
|
2488
|
+
let backup;
|
|
2489
|
+
try {
|
|
2490
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
2491
|
+
backup = JSON.parse(content);
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
console.log(chalk19.red("Invalid backup file. Must be valid JSON."));
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
if (!backup.config || !backup.config.defaults || !Array.isArray(backup.config.projects)) {
|
|
2497
|
+
console.log(chalk19.red("Invalid backup format. Missing config data."));
|
|
2498
|
+
process.exit(1);
|
|
2499
|
+
}
|
|
2500
|
+
console.log(chalk19.blue("\nBackup info:\n"));
|
|
2501
|
+
console.log(chalk19.dim(" Exported: ") + chalk19.white(backup.exportedAt || "unknown"));
|
|
2502
|
+
console.log(chalk19.dim(" Projects: ") + chalk19.white(backup.config.projects.length));
|
|
2503
|
+
console.log(chalk19.dim(" Mode: ") + chalk19.white(backup.config.defaults.mode || "tunnel"));
|
|
2504
|
+
console.log("");
|
|
2505
|
+
const currentConfig = readConfig();
|
|
2506
|
+
if (currentConfig.projects.length > 0 && !options.force) {
|
|
2507
|
+
console.log(chalk19.yellow(`Warning: You have ${currentConfig.projects.length} existing project(s).`));
|
|
2508
|
+
console.log(chalk19.dim("Use --force to overwrite.\n"));
|
|
2509
|
+
process.exit(1);
|
|
2510
|
+
}
|
|
2511
|
+
writeConfig(backup.config);
|
|
2512
|
+
console.log(chalk19.green("\u2713 Config restored!\n"));
|
|
2513
|
+
console.log(chalk19.dim("Run `bindler apply` to apply nginx configuration."));
|
|
2514
|
+
console.log("");
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/commands/ssl.ts
|
|
2518
|
+
import chalk20 from "chalk";
|
|
2519
|
+
async function sslCommand(hostname, options) {
|
|
2520
|
+
if (!hostname) {
|
|
2521
|
+
console.log(chalk20.red("Usage: bindler ssl <hostname>"));
|
|
2522
|
+
console.log(chalk20.dim("\nRequest SSL certificate for a hostname"));
|
|
2523
|
+
console.log(chalk20.dim("\nExamples:"));
|
|
2524
|
+
console.log(chalk20.dim(" bindler ssl myapp.example.com"));
|
|
2525
|
+
console.log(chalk20.dim(" bindler ssl myapp # uses project hostname"));
|
|
2526
|
+
console.log(chalk20.dim(" bindler ssl myapp --email me@example.com"));
|
|
2527
|
+
process.exit(1);
|
|
2528
|
+
}
|
|
2529
|
+
const project = getProject(hostname);
|
|
2530
|
+
const targetHostname = project ? project.hostname : hostname;
|
|
2531
|
+
const certbotCheck = execCommandSafe("which certbot");
|
|
2532
|
+
if (!certbotCheck.success) {
|
|
2533
|
+
console.log(chalk20.red("certbot is not installed."));
|
|
2534
|
+
console.log(chalk20.dim("\nInstall with:"));
|
|
2535
|
+
console.log(chalk20.dim(" macOS: brew install certbot"));
|
|
2536
|
+
console.log(chalk20.dim(" Linux: apt install certbot python3-certbot-nginx"));
|
|
2537
|
+
process.exit(1);
|
|
2538
|
+
}
|
|
2539
|
+
const defaults = getDefaults();
|
|
2540
|
+
const email = options.email || defaults.sslEmail || `admin@${targetHostname.split(".").slice(-2).join(".")}`;
|
|
2541
|
+
console.log(chalk20.blue(`
|
|
2542
|
+
Requesting SSL certificate for ${targetHostname}...
|
|
2543
|
+
`));
|
|
2544
|
+
let cmd = `sudo certbot --nginx -d ${targetHostname} --non-interactive --agree-tos --email ${email}`;
|
|
2545
|
+
if (options.staging) {
|
|
2546
|
+
cmd += " --staging";
|
|
2547
|
+
console.log(chalk20.yellow("Using staging server (test certificate)\n"));
|
|
2548
|
+
}
|
|
2549
|
+
console.log(chalk20.dim(`Running: ${cmd}
|
|
2550
|
+
`));
|
|
2551
|
+
const result = execCommandSafe(cmd + " 2>&1");
|
|
2552
|
+
if (result.success) {
|
|
2553
|
+
console.log(chalk20.green(`
|
|
2554
|
+
\u2713 SSL certificate installed for ${targetHostname}
|
|
2555
|
+
`));
|
|
2556
|
+
console.log(chalk20.dim("Certificate will auto-renew via certbot timer."));
|
|
2557
|
+
} else if (result.output?.includes("Certificate not yet due for renewal")) {
|
|
2558
|
+
console.log(chalk20.green(`
|
|
2559
|
+
\u2713 Certificate already exists and is valid
|
|
2560
|
+
`));
|
|
2561
|
+
console.log(chalk20.dim("Use --force with certbot to renew early if needed."));
|
|
2562
|
+
} else if (result.output?.includes("too many certificates")) {
|
|
2563
|
+
console.log(chalk20.red("\n\u2717 Rate limit reached"));
|
|
2564
|
+
console.log(chalk20.dim("Let's Encrypt limits certificates per domain."));
|
|
2565
|
+
console.log(chalk20.dim("Try again later or use --staging for testing."));
|
|
2566
|
+
} else if (result.output?.includes("Could not bind")) {
|
|
2567
|
+
console.log(chalk20.red("\n\u2717 Port 80 is in use"));
|
|
2568
|
+
console.log(chalk20.dim("Stop nginx temporarily or use webroot method."));
|
|
2569
|
+
} else {
|
|
2570
|
+
console.log(chalk20.red("\n\u2717 Certificate request failed\n"));
|
|
2571
|
+
if (result.output) {
|
|
2572
|
+
const lines = result.output.split("\n").filter(
|
|
2573
|
+
(l) => l.includes("Error") || l.includes("error") || l.includes("failed") || l.includes("Challenge")
|
|
2574
|
+
);
|
|
2575
|
+
for (const line of lines.slice(0, 5)) {
|
|
2576
|
+
console.log(chalk20.dim(` ${line.trim()}`));
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
console.log(chalk20.dim("\nCommon issues:"));
|
|
2580
|
+
console.log(chalk20.dim(" - DNS not pointing to this server"));
|
|
2581
|
+
console.log(chalk20.dim(" - Port 80 not accessible from internet"));
|
|
2582
|
+
console.log(chalk20.dim(" - Firewall blocking HTTP validation"));
|
|
2583
|
+
}
|
|
2584
|
+
console.log("");
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/commands/tunnel.ts
|
|
2588
|
+
import chalk21 from "chalk";
|
|
2589
|
+
import { execSync as execSync4, spawn as spawn2 } from "child_process";
|
|
2590
|
+
async function tunnelCommand(action, options) {
|
|
2591
|
+
if (!action) {
|
|
2592
|
+
console.log(chalk21.red("Usage: bindler tunnel <action>"));
|
|
2593
|
+
console.log(chalk21.dim("\nActions:"));
|
|
2594
|
+
console.log(chalk21.dim(" status Show tunnel status"));
|
|
2595
|
+
console.log(chalk21.dim(" start Start the tunnel"));
|
|
2596
|
+
console.log(chalk21.dim(" stop Stop the tunnel"));
|
|
2597
|
+
console.log(chalk21.dim(" login Authenticate with Cloudflare"));
|
|
2598
|
+
console.log(chalk21.dim(" create Create a new tunnel"));
|
|
2599
|
+
console.log(chalk21.dim(" list List all tunnels"));
|
|
2600
|
+
console.log(chalk21.dim("\nExamples:"));
|
|
2601
|
+
console.log(chalk21.dim(" bindler tunnel status"));
|
|
2602
|
+
console.log(chalk21.dim(" bindler tunnel start"));
|
|
2603
|
+
console.log(chalk21.dim(" bindler tunnel create --name mytunnel"));
|
|
2604
|
+
process.exit(1);
|
|
2605
|
+
}
|
|
2606
|
+
if (!isCloudflaredInstalled()) {
|
|
2607
|
+
console.log(chalk21.red("cloudflared is not installed."));
|
|
2608
|
+
console.log(chalk21.dim("\nInstall with: bindler setup"));
|
|
2609
|
+
process.exit(1);
|
|
2610
|
+
}
|
|
2611
|
+
const defaults = getDefaults();
|
|
2612
|
+
const tunnelName = options.name || defaults.tunnelName;
|
|
2613
|
+
switch (action) {
|
|
2614
|
+
case "status": {
|
|
2615
|
+
console.log(chalk21.blue("\nTunnel Status\n"));
|
|
2616
|
+
const info = getTunnelInfo(tunnelName);
|
|
2617
|
+
if (!info.exists) {
|
|
2618
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" does not exist.`));
|
|
2619
|
+
console.log(chalk21.dim(`
|
|
2620
|
+
Create with: bindler tunnel create --name ${tunnelName}`));
|
|
2621
|
+
} else {
|
|
2622
|
+
console.log(chalk21.dim(" Name: ") + chalk21.white(tunnelName));
|
|
2623
|
+
console.log(chalk21.dim(" ID: ") + chalk21.white(info.id || "unknown"));
|
|
2624
|
+
console.log(chalk21.dim(" Running: ") + (info.running ? chalk21.green("yes") : chalk21.red("no")));
|
|
2625
|
+
if (!info.running) {
|
|
2626
|
+
console.log(chalk21.dim(`
|
|
2627
|
+
Start with: bindler tunnel start`));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
console.log("");
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
case "start": {
|
|
2634
|
+
const info = getTunnelInfo(tunnelName);
|
|
2635
|
+
if (!info.exists) {
|
|
2636
|
+
console.log(chalk21.red(`Tunnel "${tunnelName}" does not exist.`));
|
|
2637
|
+
console.log(chalk21.dim(`Create with: bindler tunnel create`));
|
|
2638
|
+
process.exit(1);
|
|
2639
|
+
}
|
|
2640
|
+
if (info.running) {
|
|
2641
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" is already running.`));
|
|
2642
|
+
process.exit(0);
|
|
2643
|
+
}
|
|
2644
|
+
console.log(chalk21.blue(`Starting tunnel "${tunnelName}"...
|
|
2645
|
+
`));
|
|
2646
|
+
console.log(chalk21.dim("Running in foreground. Press Ctrl+C to stop.\n"));
|
|
2647
|
+
const child = spawn2("cloudflared", ["tunnel", "run", tunnelName], {
|
|
2648
|
+
stdio: "inherit"
|
|
2649
|
+
});
|
|
2650
|
+
child.on("error", (err) => {
|
|
2651
|
+
console.log(chalk21.red(`Failed to start tunnel: ${err.message}`));
|
|
2652
|
+
process.exit(1);
|
|
2653
|
+
});
|
|
2654
|
+
child.on("exit", (code) => {
|
|
2655
|
+
console.log(chalk21.dim(`
|
|
2656
|
+
Tunnel exited with code ${code}`));
|
|
2657
|
+
process.exit(code || 0);
|
|
2658
|
+
});
|
|
2659
|
+
break;
|
|
2660
|
+
}
|
|
2661
|
+
case "stop": {
|
|
2662
|
+
console.log(chalk21.blue("Stopping tunnel...\n"));
|
|
2663
|
+
const result = execCommandSafe(`pkill -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
2664
|
+
if (result.success) {
|
|
2665
|
+
console.log(chalk21.green(`\u2713 Tunnel "${tunnelName}" stopped`));
|
|
2666
|
+
} else {
|
|
2667
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" was not running.`));
|
|
2668
|
+
}
|
|
2669
|
+
console.log("");
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
case "login": {
|
|
2673
|
+
console.log(chalk21.blue("Authenticating with Cloudflare...\n"));
|
|
2674
|
+
console.log(chalk21.dim("A browser window will open. Follow the instructions.\n"));
|
|
2675
|
+
try {
|
|
2676
|
+
execSync4("cloudflared tunnel login", { stdio: "inherit" });
|
|
2677
|
+
console.log(chalk21.green("\n\u2713 Authentication successful!"));
|
|
2678
|
+
} catch {
|
|
2679
|
+
console.log(chalk21.red("\n\u2717 Authentication failed or cancelled."));
|
|
2680
|
+
}
|
|
2681
|
+
console.log("");
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
case "create": {
|
|
2685
|
+
const existingTunnels = listTunnels();
|
|
2686
|
+
const exists = existingTunnels.some((t) => t.name === tunnelName);
|
|
2687
|
+
if (exists) {
|
|
2688
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" already exists.`));
|
|
2689
|
+
console.log(chalk21.dim("\nUse a different name with --name"));
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
console.log(chalk21.blue(`Creating tunnel "${tunnelName}"...
|
|
2693
|
+
`));
|
|
2694
|
+
try {
|
|
2695
|
+
execSync4(`cloudflared tunnel create ${tunnelName}`, { stdio: "inherit" });
|
|
2696
|
+
console.log(chalk21.green(`
|
|
2697
|
+
\u2713 Tunnel "${tunnelName}" created!`));
|
|
2698
|
+
console.log(chalk21.dim("\nNext steps:"));
|
|
2699
|
+
console.log(chalk21.dim(" 1. Create ~/.cloudflared/config.yml"));
|
|
2700
|
+
console.log(chalk21.dim(" 2. Run: bindler tunnel start"));
|
|
2701
|
+
} catch {
|
|
2702
|
+
console.log(chalk21.red("\n\u2717 Failed to create tunnel."));
|
|
2703
|
+
console.log(chalk21.dim("Make sure you're logged in: bindler tunnel login"));
|
|
2704
|
+
}
|
|
2705
|
+
console.log("");
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
case "list": {
|
|
2709
|
+
console.log(chalk21.blue("\nCloudflare Tunnels\n"));
|
|
2710
|
+
const tunnels = listTunnels();
|
|
2711
|
+
if (tunnels.length === 0) {
|
|
2712
|
+
console.log(chalk21.dim("No tunnels found."));
|
|
2713
|
+
console.log(chalk21.dim("\nCreate one with: bindler tunnel create"));
|
|
2714
|
+
} else {
|
|
2715
|
+
for (const tunnel of tunnels) {
|
|
2716
|
+
const isDefault = tunnel.name === tunnelName;
|
|
2717
|
+
const prefix = isDefault ? chalk21.cyan("\u2192 ") : " ";
|
|
2718
|
+
console.log(prefix + chalk21.white(tunnel.name) + chalk21.dim(` (${tunnel.id.slice(0, 8)}...)`));
|
|
2719
|
+
}
|
|
2720
|
+
console.log(chalk21.dim(`
|
|
2721
|
+
${tunnels.length} tunnel(s)`));
|
|
2722
|
+
}
|
|
2723
|
+
console.log("");
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
default:
|
|
2727
|
+
console.log(chalk21.red(`Unknown action: ${action}`));
|
|
2728
|
+
console.log(chalk21.dim("Run `bindler tunnel` for usage."));
|
|
2729
|
+
process.exit(1);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/commands/open.ts
|
|
2734
|
+
import chalk22 from "chalk";
|
|
2735
|
+
import { exec } from "child_process";
|
|
2736
|
+
async function openCommand(name) {
|
|
2737
|
+
if (!name) {
|
|
2738
|
+
console.log(chalk22.red("Usage: bindler open <name>"));
|
|
2739
|
+
console.log(chalk22.dim("\nOpen a project in your browser"));
|
|
2740
|
+
console.log(chalk22.dim("\nExamples:"));
|
|
2741
|
+
console.log(chalk22.dim(" bindler open myapp"));
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
const project = getProject(name);
|
|
2745
|
+
if (!project) {
|
|
2746
|
+
console.log(chalk22.red(`Project "${name}" not found.`));
|
|
2747
|
+
console.log(chalk22.dim("\nAvailable projects:"));
|
|
2748
|
+
const projects = listProjects();
|
|
2749
|
+
for (const p of projects) {
|
|
2750
|
+
console.log(chalk22.dim(` - ${p.name}`));
|
|
2751
|
+
}
|
|
2752
|
+
process.exit(1);
|
|
2753
|
+
}
|
|
2754
|
+
const defaults = getDefaults();
|
|
2755
|
+
const isLocal = project.local || project.hostname.endsWith(".local");
|
|
2756
|
+
const isDirect = defaults.mode === "direct";
|
|
2757
|
+
let url;
|
|
2758
|
+
if (isLocal) {
|
|
2759
|
+
const port = defaults.nginxListen.includes(":") ? defaults.nginxListen.split(":")[1] : defaults.nginxListen;
|
|
2760
|
+
url = `http://${project.hostname}:${port}`;
|
|
2761
|
+
} else if (isDirect && defaults.sslEnabled) {
|
|
2762
|
+
url = `https://${project.hostname}`;
|
|
2763
|
+
} else if (isDirect) {
|
|
2764
|
+
url = `http://${project.hostname}`;
|
|
2765
|
+
} else {
|
|
2766
|
+
url = `https://${project.hostname}`;
|
|
2767
|
+
}
|
|
2768
|
+
if (project.basePath && project.basePath !== "/") {
|
|
2769
|
+
url += project.basePath;
|
|
2770
|
+
}
|
|
2771
|
+
console.log(chalk22.dim(`Opening ${url}...`));
|
|
2772
|
+
const platform = process.platform;
|
|
2773
|
+
let cmd;
|
|
2774
|
+
if (platform === "darwin") {
|
|
2775
|
+
cmd = `open "${url}"`;
|
|
2776
|
+
} else if (platform === "win32") {
|
|
2777
|
+
cmd = `start "${url}"`;
|
|
2778
|
+
} else {
|
|
2779
|
+
cmd = `xdg-open "${url}"`;
|
|
2780
|
+
}
|
|
2781
|
+
exec(cmd, (error) => {
|
|
2782
|
+
if (error) {
|
|
2783
|
+
console.log(chalk22.yellow(`Could not open browser automatically.`));
|
|
2784
|
+
console.log(chalk22.dim(`
|
|
2785
|
+
Open manually: ${chalk22.cyan(url)}`));
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/commands/health.ts
|
|
2791
|
+
import chalk23 from "chalk";
|
|
2792
|
+
async function pingUrl(url, timeout = 5e3) {
|
|
2793
|
+
const start = Date.now();
|
|
2794
|
+
try {
|
|
2795
|
+
const controller = new AbortController();
|
|
2796
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2797
|
+
const response = await fetch(url, {
|
|
2798
|
+
method: "HEAD",
|
|
2799
|
+
redirect: "follow",
|
|
2800
|
+
signal: controller.signal
|
|
2801
|
+
});
|
|
2802
|
+
clearTimeout(timeoutId);
|
|
2803
|
+
return {
|
|
2804
|
+
ok: response.ok,
|
|
2805
|
+
status: response.status,
|
|
2806
|
+
time: Date.now() - start
|
|
2807
|
+
};
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
const err = error;
|
|
2810
|
+
return {
|
|
2811
|
+
ok: false,
|
|
2812
|
+
error: err.name === "AbortError" ? "timeout" : err.message,
|
|
2813
|
+
time: Date.now() - start
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
async function healthCommand() {
|
|
2818
|
+
const projects = listProjects();
|
|
2819
|
+
const defaults = getDefaults();
|
|
2820
|
+
if (projects.length === 0) {
|
|
2821
|
+
console.log(chalk23.yellow("\nNo projects registered."));
|
|
2822
|
+
console.log(chalk23.dim("Run `bindler new` to add a project.\n"));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
console.log(chalk23.blue("\nHealth Check\n"));
|
|
2826
|
+
const results = [];
|
|
2827
|
+
for (const project of projects) {
|
|
2828
|
+
if (project.enabled === false) {
|
|
2829
|
+
console.log(chalk23.dim(` - ${project.name} (disabled)`));
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
const isLocal = project.local || project.hostname.endsWith(".local");
|
|
2833
|
+
const isDirect = defaults.mode === "direct";
|
|
2834
|
+
let url;
|
|
2835
|
+
if (isLocal) {
|
|
2836
|
+
const port = defaults.nginxListen.includes(":") ? defaults.nginxListen.split(":")[1] : defaults.nginxListen;
|
|
2837
|
+
url = `http://${project.hostname}:${port}`;
|
|
2838
|
+
} else if (isDirect && defaults.sslEnabled) {
|
|
2839
|
+
url = `https://${project.hostname}`;
|
|
2840
|
+
} else if (isDirect) {
|
|
2841
|
+
url = `http://${project.hostname}`;
|
|
2842
|
+
} else {
|
|
2843
|
+
url = `https://${project.hostname}`;
|
|
2844
|
+
}
|
|
2845
|
+
if (project.basePath && project.basePath !== "/") {
|
|
2846
|
+
url += project.basePath;
|
|
2847
|
+
}
|
|
2848
|
+
process.stdout.write(chalk23.dim(` Checking ${project.name}...`));
|
|
2849
|
+
const result = await pingUrl(url);
|
|
2850
|
+
results.push({ name: project.name, hostname: project.hostname, ...result });
|
|
2851
|
+
process.stdout.write("\r\x1B[K");
|
|
2852
|
+
if (result.ok) {
|
|
2853
|
+
console.log(chalk23.green(" \u2713 ") + chalk23.white(project.name) + chalk23.dim(` (${result.time}ms)`));
|
|
2854
|
+
} else if (result.status) {
|
|
2855
|
+
console.log(chalk23.yellow(" ! ") + chalk23.white(project.name) + chalk23.dim(` (${result.status})`));
|
|
2856
|
+
} else {
|
|
2857
|
+
console.log(chalk23.red(" \u2717 ") + chalk23.white(project.name) + chalk23.dim(` (${result.error})`));
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
const healthy = results.filter((r) => r.ok).length;
|
|
2861
|
+
const unhealthy = results.filter((r) => !r.ok).length;
|
|
2862
|
+
console.log("");
|
|
2863
|
+
if (unhealthy === 0) {
|
|
2864
|
+
console.log(chalk23.green(`\u2713 All ${healthy} project(s) healthy`));
|
|
2865
|
+
} else if (healthy === 0) {
|
|
2866
|
+
console.log(chalk23.red(`\u2717 All ${unhealthy} project(s) down`));
|
|
2867
|
+
} else {
|
|
2868
|
+
console.log(chalk23.yellow(`! ${healthy} healthy, ${unhealthy} down`));
|
|
2869
|
+
}
|
|
2870
|
+
console.log("");
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/commands/stats.ts
|
|
2874
|
+
import chalk24 from "chalk";
|
|
2875
|
+
function formatBytes2(bytes) {
|
|
2876
|
+
if (bytes === 0) return "0 B";
|
|
2877
|
+
const k = 1024;
|
|
2878
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
2879
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2880
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
2881
|
+
}
|
|
2882
|
+
function formatUptime2(ms) {
|
|
2883
|
+
const seconds = Math.floor(ms / 1e3);
|
|
2884
|
+
const minutes = Math.floor(seconds / 60);
|
|
2885
|
+
const hours = Math.floor(minutes / 60);
|
|
2886
|
+
const days = Math.floor(hours / 24);
|
|
2887
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
2888
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
2889
|
+
if (minutes > 0) return `${minutes}m`;
|
|
2890
|
+
return `${seconds}s`;
|
|
2891
|
+
}
|
|
2892
|
+
async function statsCommand() {
|
|
2893
|
+
const projects = listProjects();
|
|
2894
|
+
const pm2Processes = getPm2List();
|
|
2895
|
+
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
2896
|
+
if (npmProjects.length === 0) {
|
|
2897
|
+
console.log(chalk24.yellow("\nNo npm projects registered."));
|
|
2898
|
+
console.log(chalk24.dim("Stats are only available for npm projects.\n"));
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
console.log(chalk24.blue("\nProject Stats\n"));
|
|
2902
|
+
console.log(
|
|
2903
|
+
chalk24.dim(" ") + chalk24.dim("NAME".padEnd(20)) + chalk24.dim("STATUS".padEnd(10)) + chalk24.dim("CPU".padEnd(8)) + chalk24.dim("MEM".padEnd(10)) + chalk24.dim("UPTIME".padEnd(10)) + chalk24.dim("RESTARTS")
|
|
2904
|
+
);
|
|
2905
|
+
console.log(chalk24.dim(" " + "-".repeat(70)));
|
|
2906
|
+
let totalCpu = 0;
|
|
2907
|
+
let totalMem = 0;
|
|
2908
|
+
for (const project of npmProjects) {
|
|
2909
|
+
const pm2Name = `bindler:${project.name}`;
|
|
2910
|
+
const pm2Process = pm2Processes.find((p) => p.name === pm2Name);
|
|
2911
|
+
const name = project.name.slice(0, 18).padEnd(20);
|
|
2912
|
+
if (!pm2Process) {
|
|
2913
|
+
console.log(
|
|
2914
|
+
" " + chalk24.white(name) + chalk24.dim("not managed".padEnd(10)) + chalk24.dim("-".padEnd(8)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-")
|
|
2915
|
+
);
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const statusColor = pm2Process.status === "online" ? chalk24.green : chalk24.red;
|
|
2919
|
+
const status = statusColor(pm2Process.status.padEnd(10));
|
|
2920
|
+
const cpu = `${pm2Process.cpu.toFixed(1)}%`.padEnd(8);
|
|
2921
|
+
const mem = formatBytes2(pm2Process.memory).padEnd(10);
|
|
2922
|
+
const uptime = formatUptime2(pm2Process.uptime).padEnd(10);
|
|
2923
|
+
const restarts = String(pm2Process.restarts);
|
|
2924
|
+
totalCpu += pm2Process.cpu;
|
|
2925
|
+
totalMem += pm2Process.memory;
|
|
2926
|
+
console.log(
|
|
2927
|
+
" " + chalk24.white(name) + status + (pm2Process.cpu > 50 ? chalk24.yellow(cpu) : chalk24.dim(cpu)) + (pm2Process.memory > 500 * 1024 * 1024 ? chalk24.yellow(mem) : chalk24.dim(mem)) + chalk24.dim(uptime) + (pm2Process.restarts > 0 ? chalk24.yellow(restarts) : chalk24.dim(restarts))
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
const runningCount = pm2Processes.filter((p) => p.name.startsWith("bindler:") && p.status === "online").length;
|
|
2931
|
+
console.log(chalk24.dim(" " + "-".repeat(70)));
|
|
2932
|
+
console.log(
|
|
2933
|
+
" " + chalk24.bold("TOTAL".padEnd(20)) + chalk24.dim(`${runningCount}/${npmProjects.length}`.padEnd(10)) + chalk24.dim(`${totalCpu.toFixed(1)}%`.padEnd(8)) + chalk24.dim(formatBytes2(totalMem).padEnd(10))
|
|
2934
|
+
);
|
|
2935
|
+
console.log("");
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/commands/completion.ts
|
|
2939
|
+
import chalk25 from "chalk";
|
|
2940
|
+
var BASH_COMPLETION = `
|
|
2941
|
+
# bindler bash completion
|
|
2942
|
+
_bindler_completions() {
|
|
2943
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
2944
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
2945
|
+
|
|
2946
|
+
local commands="new list ls status start stop restart logs update edit remove rm apply doctor ports info check setup init deploy backup restore ssl tunnel open health stats completion"
|
|
2947
|
+
|
|
2948
|
+
case "\${prev}" in
|
|
2949
|
+
bindler)
|
|
2950
|
+
COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
|
|
2951
|
+
return 0
|
|
2952
|
+
;;
|
|
2953
|
+
start|stop|restart|logs|update|edit|remove|rm|deploy|open|check)
|
|
2954
|
+
local projects=$(bindler list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
|
|
2955
|
+
COMPREPLY=($(compgen -W "\${projects}" -- "\${cur}"))
|
|
2956
|
+
return 0
|
|
2957
|
+
;;
|
|
2958
|
+
tunnel)
|
|
2959
|
+
COMPREPLY=($(compgen -W "status start stop login create list" -- "\${cur}"))
|
|
2960
|
+
return 0
|
|
2961
|
+
;;
|
|
2962
|
+
esac
|
|
2963
|
+
|
|
2964
|
+
COMPREPLY=()
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
complete -F _bindler_completions bindler
|
|
2968
|
+
`;
|
|
2969
|
+
var ZSH_COMPLETION = `
|
|
2970
|
+
#compdef bindler
|
|
2971
|
+
|
|
2972
|
+
_bindler() {
|
|
2973
|
+
local -a commands
|
|
2974
|
+
commands=(
|
|
2975
|
+
'new:Create and register a new project'
|
|
2976
|
+
'list:List all registered projects'
|
|
2977
|
+
'ls:List all registered projects'
|
|
2978
|
+
'status:Show detailed status of all projects'
|
|
2979
|
+
'start:Start an npm project with PM2'
|
|
2980
|
+
'stop:Stop an npm project'
|
|
2981
|
+
'restart:Restart an npm project'
|
|
2982
|
+
'logs:Show logs for an npm project'
|
|
2983
|
+
'update:Update project configuration'
|
|
2984
|
+
'edit:Edit project configuration in \\$EDITOR'
|
|
2985
|
+
'remove:Remove a project from registry'
|
|
2986
|
+
'rm:Remove a project from registry'
|
|
2987
|
+
'apply:Generate and apply nginx configuration'
|
|
2988
|
+
'doctor:Run system diagnostics'
|
|
2989
|
+
'ports:Show allocated ports'
|
|
2990
|
+
'info:Show bindler information'
|
|
2991
|
+
'check:Check DNS and HTTP for a hostname'
|
|
2992
|
+
'setup:Install missing dependencies'
|
|
2993
|
+
'init:Initialize bindler'
|
|
2994
|
+
'deploy:Deploy a project (git pull + install + restart)'
|
|
2995
|
+
'backup:Backup configuration'
|
|
2996
|
+
'restore:Restore configuration'
|
|
2997
|
+
'ssl:Request SSL certificate'
|
|
2998
|
+
'tunnel:Manage Cloudflare tunnel'
|
|
2999
|
+
'open:Open project in browser'
|
|
3000
|
+
'health:Check health of all projects'
|
|
3001
|
+
'stats:Show CPU and memory stats'
|
|
3002
|
+
'completion:Generate shell completions'
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
local -a project_commands
|
|
3006
|
+
project_commands=(start stop restart logs update edit remove rm deploy open check)
|
|
3007
|
+
|
|
3008
|
+
_arguments -C \\
|
|
3009
|
+
'1: :->command' \\
|
|
3010
|
+
'*: :->args'
|
|
3011
|
+
|
|
3012
|
+
case "$state" in
|
|
3013
|
+
command)
|
|
3014
|
+
_describe -t commands 'bindler command' commands
|
|
3015
|
+
;;
|
|
3016
|
+
args)
|
|
3017
|
+
case "$words[2]" in
|
|
3018
|
+
start|stop|restart|logs|update|edit|remove|rm|deploy|open|check)
|
|
3019
|
+
local -a projects
|
|
3020
|
+
projects=(\${(f)"$(bindler list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)"})
|
|
3021
|
+
_describe -t projects 'project' projects
|
|
3022
|
+
;;
|
|
3023
|
+
tunnel)
|
|
3024
|
+
local -a tunnel_actions
|
|
3025
|
+
tunnel_actions=(status start stop login create list)
|
|
3026
|
+
_describe -t actions 'action' tunnel_actions
|
|
3027
|
+
;;
|
|
3028
|
+
esac
|
|
3029
|
+
;;
|
|
3030
|
+
esac
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
_bindler
|
|
3034
|
+
`;
|
|
3035
|
+
var FISH_COMPLETION = `
|
|
3036
|
+
# bindler fish completion
|
|
3037
|
+
complete -c bindler -f
|
|
3038
|
+
|
|
3039
|
+
# Commands
|
|
3040
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'new' -d 'Create and register a new project'
|
|
3041
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'list ls' -d 'List all registered projects'
|
|
3042
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'status' -d 'Show detailed status'
|
|
3043
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'start' -d 'Start an npm project'
|
|
3044
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'stop' -d 'Stop an npm project'
|
|
3045
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'restart' -d 'Restart an npm project'
|
|
3046
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'logs' -d 'Show logs'
|
|
3047
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'update' -d 'Update project configuration'
|
|
3048
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'edit' -d 'Edit project in $EDITOR'
|
|
3049
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'remove rm' -d 'Remove a project'
|
|
3050
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'apply' -d 'Apply nginx configuration'
|
|
3051
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'doctor' -d 'Run diagnostics'
|
|
3052
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'ports' -d 'Show allocated ports'
|
|
3053
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'info' -d 'Show bindler info'
|
|
3054
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'check' -d 'Check DNS and HTTP'
|
|
3055
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'setup' -d 'Install dependencies'
|
|
3056
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'init' -d 'Initialize bindler'
|
|
3057
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'deploy' -d 'Deploy a project'
|
|
3058
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'backup' -d 'Backup configuration'
|
|
3059
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'restore' -d 'Restore configuration'
|
|
3060
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'ssl' -d 'Request SSL certificate'
|
|
3061
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'tunnel' -d 'Manage Cloudflare tunnel'
|
|
3062
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'open' -d 'Open in browser'
|
|
3063
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'health' -d 'Check health'
|
|
3064
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'stats' -d 'Show stats'
|
|
3065
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'completion' -d 'Generate completions'
|
|
3066
|
+
|
|
3067
|
+
# Tunnel subcommands
|
|
3068
|
+
complete -c bindler -n '__fish_seen_subcommand_from tunnel' -a 'status start stop login create list'
|
|
3069
|
+
`;
|
|
3070
|
+
async function completionCommand(shell) {
|
|
3071
|
+
if (!shell) {
|
|
3072
|
+
console.log(chalk25.red("Usage: bindler completion <shell>"));
|
|
3073
|
+
console.log(chalk25.dim("\nSupported shells: bash, zsh, fish"));
|
|
3074
|
+
console.log(chalk25.dim("\nSetup:"));
|
|
3075
|
+
console.log(chalk25.dim(" bash: bindler completion bash >> ~/.bashrc"));
|
|
3076
|
+
console.log(chalk25.dim(" zsh: bindler completion zsh >> ~/.zshrc"));
|
|
3077
|
+
console.log(chalk25.dim(" fish: bindler completion fish > ~/.config/fish/completions/bindler.fish"));
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
switch (shell) {
|
|
3081
|
+
case "bash":
|
|
3082
|
+
console.log(BASH_COMPLETION.trim());
|
|
3083
|
+
break;
|
|
3084
|
+
case "zsh":
|
|
3085
|
+
console.log(ZSH_COMPLETION.trim());
|
|
3086
|
+
break;
|
|
3087
|
+
case "fish":
|
|
3088
|
+
console.log(FISH_COMPLETION.trim());
|
|
3089
|
+
break;
|
|
3090
|
+
default:
|
|
3091
|
+
console.log(chalk25.red(`Unknown shell: ${shell}`));
|
|
3092
|
+
console.log(chalk25.dim("Supported: bash, zsh, fish"));
|
|
3093
|
+
process.exit(1);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2193
3097
|
// src/cli.ts
|
|
2194
3098
|
var program = new Command();
|
|
2195
3099
|
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.0.0");
|
|
@@ -2209,24 +3113,76 @@ program.command("status").description("Show detailed status of all projects").ac
|
|
|
2209
3113
|
await statusCommand();
|
|
2210
3114
|
});
|
|
2211
3115
|
program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
|
|
3116
|
+
if (!name && !options.all) {
|
|
3117
|
+
console.log(chalk26.red("Usage: bindler start <name> or bindler start --all"));
|
|
3118
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3119
|
+
console.log(chalk26.dim(" bindler start myapp"));
|
|
3120
|
+
console.log(chalk26.dim(" bindler start --all # start all npm projects"));
|
|
3121
|
+
process.exit(1);
|
|
3122
|
+
}
|
|
2212
3123
|
await startCommand(name, options);
|
|
2213
3124
|
});
|
|
2214
3125
|
program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
|
|
3126
|
+
if (!name && !options.all) {
|
|
3127
|
+
console.log(chalk26.red("Usage: bindler stop <name> or bindler stop --all"));
|
|
3128
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3129
|
+
console.log(chalk26.dim(" bindler stop myapp"));
|
|
3130
|
+
console.log(chalk26.dim(" bindler stop --all # stop all npm projects"));
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
2215
3133
|
await stopCommand(name, options);
|
|
2216
3134
|
});
|
|
2217
3135
|
program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
|
|
3136
|
+
if (!name && !options.all) {
|
|
3137
|
+
console.log(chalk26.red("Usage: bindler restart <name> or bindler restart --all"));
|
|
3138
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3139
|
+
console.log(chalk26.dim(" bindler restart myapp"));
|
|
3140
|
+
console.log(chalk26.dim(" bindler restart --all # restart all npm projects"));
|
|
3141
|
+
process.exit(1);
|
|
3142
|
+
}
|
|
2218
3143
|
await restartCommand(name, options);
|
|
2219
3144
|
});
|
|
2220
|
-
program.command("logs
|
|
3145
|
+
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) => {
|
|
3146
|
+
if (!name) {
|
|
3147
|
+
console.log(chalk26.red("Usage: bindler logs <name>"));
|
|
3148
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3149
|
+
console.log(chalk26.dim(" bindler logs myapp"));
|
|
3150
|
+
console.log(chalk26.dim(" bindler logs myapp --follow"));
|
|
3151
|
+
console.log(chalk26.dim(" bindler logs myapp --lines 500"));
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
2221
3154
|
await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
|
|
2222
3155
|
});
|
|
2223
|
-
program.command("update
|
|
3156
|
+
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) => {
|
|
3157
|
+
if (!name) {
|
|
3158
|
+
console.log(chalk26.red("Usage: bindler update <name> [options]"));
|
|
3159
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3160
|
+
console.log(chalk26.dim(" bindler update myapp --hostname newapp.example.com"));
|
|
3161
|
+
console.log(chalk26.dim(" bindler update myapp --port 4000"));
|
|
3162
|
+
console.log(chalk26.dim(" bindler update myapp --disable"));
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
}
|
|
2224
3165
|
await updateCommand(name, options);
|
|
2225
3166
|
});
|
|
2226
|
-
program.command("edit
|
|
3167
|
+
program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
|
|
3168
|
+
if (!name) {
|
|
3169
|
+
console.log(chalk26.red("Usage: bindler edit <name>"));
|
|
3170
|
+
console.log(chalk26.dim("\nOpens the project config in your $EDITOR"));
|
|
3171
|
+
console.log(chalk26.dim("\nExample:"));
|
|
3172
|
+
console.log(chalk26.dim(" bindler edit myapp"));
|
|
3173
|
+
process.exit(1);
|
|
3174
|
+
}
|
|
2227
3175
|
await editCommand(name);
|
|
2228
3176
|
});
|
|
2229
|
-
program.command("remove
|
|
3177
|
+
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) => {
|
|
3178
|
+
if (!name) {
|
|
3179
|
+
console.log(chalk26.red("Usage: bindler remove <name>"));
|
|
3180
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3181
|
+
console.log(chalk26.dim(" bindler remove myapp"));
|
|
3182
|
+
console.log(chalk26.dim(" bindler remove myapp --force # skip confirmation"));
|
|
3183
|
+
console.log(chalk26.dim(" bindler rm myapp # alias"));
|
|
3184
|
+
process.exit(1);
|
|
3185
|
+
}
|
|
2230
3186
|
await removeCommand(name, options);
|
|
2231
3187
|
});
|
|
2232
3188
|
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) => {
|
|
@@ -2241,11 +3197,48 @@ program.command("ports").description("Show allocated ports").action(async () =>
|
|
|
2241
3197
|
program.command("info").description("Show bindler information and stats").action(async () => {
|
|
2242
3198
|
await infoCommand();
|
|
2243
3199
|
});
|
|
2244
|
-
program.command("check
|
|
3200
|
+
program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
|
|
3201
|
+
if (!hostname) {
|
|
3202
|
+
console.log(chalk26.red("Usage: bindler check <hostname>"));
|
|
3203
|
+
console.log(chalk26.dim("\nExamples:"));
|
|
3204
|
+
console.log(chalk26.dim(" bindler check myapp.example.com"));
|
|
3205
|
+
console.log(chalk26.dim(" bindler check myapp # uses project name"));
|
|
3206
|
+
process.exit(1);
|
|
3207
|
+
}
|
|
2245
3208
|
await checkCommand(hostname, options);
|
|
2246
3209
|
});
|
|
2247
3210
|
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) => {
|
|
2248
3211
|
await setupCommand(options);
|
|
2249
3212
|
});
|
|
3213
|
+
program.command("init").description("Initialize bindler with interactive setup wizard").action(async () => {
|
|
3214
|
+
await initCommand();
|
|
3215
|
+
});
|
|
3216
|
+
program.command("deploy [name]").description("Deploy a project (git pull + npm install + restart)").option("--skip-pull", "Skip git pull").option("--skip-install", "Skip npm install").option("--skip-restart", "Skip restart").action(async (name, options) => {
|
|
3217
|
+
await deployCommand(name, options);
|
|
3218
|
+
});
|
|
3219
|
+
program.command("backup").description("Backup bindler configuration").option("-o, --output <file>", "Output file path").action(async (options) => {
|
|
3220
|
+
await backupCommand(options);
|
|
3221
|
+
});
|
|
3222
|
+
program.command("restore [file]").description("Restore bindler configuration from backup").option("-f, --force", "Overwrite existing config").action(async (file, options) => {
|
|
3223
|
+
await restoreCommand(file, options);
|
|
3224
|
+
});
|
|
3225
|
+
program.command("ssl [hostname]").description("Request SSL certificate for a hostname").option("-e, --email <email>", "Email for Let's Encrypt").option("--staging", "Use staging server (for testing)").action(async (hostname, options) => {
|
|
3226
|
+
await sslCommand(hostname, options);
|
|
3227
|
+
});
|
|
3228
|
+
program.command("tunnel [action]").description("Manage Cloudflare tunnel (status, start, stop, login, create, list)").option("-n, --name <name>", "Tunnel name").action(async (action, options) => {
|
|
3229
|
+
await tunnelCommand(action, options);
|
|
3230
|
+
});
|
|
3231
|
+
program.command("open [name]").description("Open a project in your browser").action(async (name) => {
|
|
3232
|
+
await openCommand(name);
|
|
3233
|
+
});
|
|
3234
|
+
program.command("health").description("Check health of all projects").action(async () => {
|
|
3235
|
+
await healthCommand();
|
|
3236
|
+
});
|
|
3237
|
+
program.command("stats").description("Show CPU and memory stats for npm projects").action(async () => {
|
|
3238
|
+
await statsCommand();
|
|
3239
|
+
});
|
|
3240
|
+
program.command("completion [shell]").description("Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
|
|
3241
|
+
await completionCommand(shell);
|
|
3242
|
+
});
|
|
2250
3243
|
program.parse();
|
|
2251
3244
|
//# sourceMappingURL=cli.js.map
|