bindler 1.0.4 → 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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
- import chalk17 from "chalk";
5
+ import chalk26 from "chalk";
6
6
 
7
7
  // src/commands/new.ts
8
8
  import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
@@ -162,28 +162,28 @@ function execCommandSafe(command, options) {
162
162
  }
163
163
  }
164
164
  function spawnInteractive(command, args, options) {
165
- return new Promise((resolve) => {
165
+ return new Promise((resolve2) => {
166
166
  const child = spawn(command, args, {
167
167
  stdio: "inherit",
168
168
  ...options
169
169
  });
170
170
  child.on("close", (code) => {
171
- resolve(code);
171
+ resolve2(code);
172
172
  });
173
173
  });
174
174
  }
175
175
  function isPortListening(port, host = "127.0.0.1") {
176
- return new Promise((resolve) => {
176
+ return new Promise((resolve2) => {
177
177
  const socket = createConnection({ port, host }, () => {
178
178
  socket.destroy();
179
- resolve(true);
179
+ resolve2(true);
180
180
  });
181
181
  socket.on("error", () => {
182
- resolve(false);
182
+ resolve2(false);
183
183
  });
184
184
  socket.setTimeout(1e3, () => {
185
185
  socket.destroy();
186
- resolve(false);
186
+ resolve2(false);
187
187
  });
188
188
  });
189
189
  }
@@ -1758,7 +1758,7 @@ async function infoCommand() {
1758
1758
  `));
1759
1759
  console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
1760
1760
  console.log(chalk14.white(" with Nginx and PM2\n"));
1761
- console.log(chalk14.dim(" Version: ") + chalk14.white("1.0.4"));
1761
+ console.log(chalk14.dim(" Version: ") + chalk14.white("1.1.0"));
1762
1762
  console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
1763
1763
  console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
1764
1764
  console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
@@ -2191,6 +2191,909 @@ async function setupCommand(options = {}) {
2191
2191
  console.log(chalk16.dim("\nRun `bindler new` to create your first project."));
2192
2192
  }
2193
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
+
2194
3097
  // src/cli.ts
2195
3098
  var program = new Command();
2196
3099
  program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.0.0");
@@ -2211,73 +3114,73 @@ program.command("status").description("Show detailed status of all projects").ac
2211
3114
  });
2212
3115
  program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
2213
3116
  if (!name && !options.all) {
2214
- console.log(chalk17.red("Usage: bindler start <name> or bindler start --all"));
2215
- console.log(chalk17.dim("\nExamples:"));
2216
- console.log(chalk17.dim(" bindler start myapp"));
2217
- console.log(chalk17.dim(" bindler start --all # start all npm projects"));
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"));
2218
3121
  process.exit(1);
2219
3122
  }
2220
3123
  await startCommand(name, options);
2221
3124
  });
2222
3125
  program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
2223
3126
  if (!name && !options.all) {
2224
- console.log(chalk17.red("Usage: bindler stop <name> or bindler stop --all"));
2225
- console.log(chalk17.dim("\nExamples:"));
2226
- console.log(chalk17.dim(" bindler stop myapp"));
2227
- console.log(chalk17.dim(" bindler stop --all # stop all npm projects"));
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"));
2228
3131
  process.exit(1);
2229
3132
  }
2230
3133
  await stopCommand(name, options);
2231
3134
  });
2232
3135
  program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
2233
3136
  if (!name && !options.all) {
2234
- console.log(chalk17.red("Usage: bindler restart <name> or bindler restart --all"));
2235
- console.log(chalk17.dim("\nExamples:"));
2236
- console.log(chalk17.dim(" bindler restart myapp"));
2237
- console.log(chalk17.dim(" bindler restart --all # restart all npm projects"));
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"));
2238
3141
  process.exit(1);
2239
3142
  }
2240
3143
  await restartCommand(name, options);
2241
3144
  });
2242
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) => {
2243
3146
  if (!name) {
2244
- console.log(chalk17.red("Usage: bindler logs <name>"));
2245
- console.log(chalk17.dim("\nExamples:"));
2246
- console.log(chalk17.dim(" bindler logs myapp"));
2247
- console.log(chalk17.dim(" bindler logs myapp --follow"));
2248
- console.log(chalk17.dim(" bindler logs myapp --lines 500"));
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"));
2249
3152
  process.exit(1);
2250
3153
  }
2251
3154
  await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
2252
3155
  });
2253
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) => {
2254
3157
  if (!name) {
2255
- console.log(chalk17.red("Usage: bindler update <name> [options]"));
2256
- console.log(chalk17.dim("\nExamples:"));
2257
- console.log(chalk17.dim(" bindler update myapp --hostname newapp.example.com"));
2258
- console.log(chalk17.dim(" bindler update myapp --port 4000"));
2259
- console.log(chalk17.dim(" bindler update myapp --disable"));
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"));
2260
3163
  process.exit(1);
2261
3164
  }
2262
3165
  await updateCommand(name, options);
2263
3166
  });
2264
3167
  program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
2265
3168
  if (!name) {
2266
- console.log(chalk17.red("Usage: bindler edit <name>"));
2267
- console.log(chalk17.dim("\nOpens the project config in your $EDITOR"));
2268
- console.log(chalk17.dim("\nExample:"));
2269
- console.log(chalk17.dim(" bindler edit myapp"));
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"));
2270
3173
  process.exit(1);
2271
3174
  }
2272
3175
  await editCommand(name);
2273
3176
  });
2274
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) => {
2275
3178
  if (!name) {
2276
- console.log(chalk17.red("Usage: bindler remove <name>"));
2277
- console.log(chalk17.dim("\nExamples:"));
2278
- console.log(chalk17.dim(" bindler remove myapp"));
2279
- console.log(chalk17.dim(" bindler remove myapp --force # skip confirmation"));
2280
- console.log(chalk17.dim(" bindler rm myapp # alias"));
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"));
2281
3184
  process.exit(1);
2282
3185
  }
2283
3186
  await removeCommand(name, options);
@@ -2296,10 +3199,10 @@ program.command("info").description("Show bindler information and stats").action
2296
3199
  });
2297
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) => {
2298
3201
  if (!hostname) {
2299
- console.log(chalk17.red("Usage: bindler check <hostname>"));
2300
- console.log(chalk17.dim("\nExamples:"));
2301
- console.log(chalk17.dim(" bindler check myapp.example.com"));
2302
- console.log(chalk17.dim(" bindler check myapp # uses project name"));
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"));
2303
3206
  process.exit(1);
2304
3207
  }
2305
3208
  await checkCommand(hostname, options);
@@ -2307,5 +3210,35 @@ program.command("check [hostname]").description("Check DNS propagation and HTTP
2307
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) => {
2308
3211
  await setupCommand(options);
2309
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
+ });
2310
3243
  program.parse();
2311
3244
  //# sourceMappingURL=cli.js.map