bindler 1.6.2 → 1.8.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
@@ -103,14 +103,31 @@ function getProject(name) {
103
103
  const config = readConfig();
104
104
  return config.projects.find((p) => p.name === name);
105
105
  }
106
- function addProject(project) {
106
+ function canAddProject(project) {
107
107
  const config = readConfig();
108
108
  if (config.projects.some((p) => p.name === project.name)) {
109
- throw new Error(`Project "${project.name}" already exists`);
109
+ return { valid: false, error: `Project "${project.name}" already exists` };
110
110
  }
111
- if (config.projects.some((p) => p.hostname === project.hostname)) {
112
- throw new Error(`Hostname "${project.hostname}" is already in use`);
111
+ const conflictingProject = config.projects.find(
112
+ (p) => p.hostname === project.hostname && (p.basePath || "/") === (project.basePath || "/")
113
+ );
114
+ if (conflictingProject) {
115
+ if (project.basePath || conflictingProject.basePath) {
116
+ return {
117
+ valid: false,
118
+ error: `Hostname "${project.hostname}" with base path "${project.basePath || "/"}" conflicts with project "${conflictingProject.name}"`
119
+ };
120
+ }
121
+ return { valid: false, error: `Hostname "${project.hostname}" is already in use by project "${conflictingProject.name}"` };
113
122
  }
123
+ return { valid: true };
124
+ }
125
+ function addProject(project) {
126
+ const check = canAddProject(project);
127
+ if (!check.valid) {
128
+ throw new Error(check.error);
129
+ }
130
+ const config = readConfig();
114
131
  config.projects.push(project);
115
132
  writeConfig(config);
116
133
  }
@@ -741,14 +758,30 @@ function isProtectedPath(path) {
741
758
  (protected_) => normalizedPath === protected_ || normalizedPath.startsWith(protected_ + "/")
742
759
  );
743
760
  }
744
- function checkPortAvailable(port) {
761
+ function checkPortAvailable(port, projectName) {
745
762
  const result = execCommandSafe(`lsof -i :${port} -P -n 2>/dev/null | grep LISTEN | head -1`);
746
763
  if (!result.success || !result.output) {
747
764
  return { available: true };
748
765
  }
749
766
  const parts = result.output.trim().split(/\s+/);
750
767
  const processName = parts[0] || "unknown";
751
- return { available: false, usedBy: processName };
768
+ const pid = parts[1] || "";
769
+ if (projectName && pid) {
770
+ const pm2Check = execCommandSafe(`pm2 jlist 2>/dev/null`);
771
+ if (pm2Check.success && pm2Check.output) {
772
+ try {
773
+ const processes = JSON.parse(pm2Check.output);
774
+ const bindlerProcess = processes.find(
775
+ (p) => p.name === `bindler:${projectName}` && String(p.pid) === pid
776
+ );
777
+ if (bindlerProcess) {
778
+ return { available: false, usedBy: processName, isOwnProcess: true };
779
+ }
780
+ } catch {
781
+ }
782
+ }
783
+ }
784
+ return { available: false, usedBy: processName, isOwnProcess: false };
752
785
  }
753
786
  function validateProject(project) {
754
787
  const errors = [];
@@ -762,8 +795,8 @@ function validateProject(project) {
762
795
  );
763
796
  }
764
797
  if (project.type === "npm" && project.port) {
765
- const portCheck = checkPortAvailable(project.port);
766
- if (!portCheck.available) {
798
+ const portCheck = checkPortAvailable(project.port, project.name);
799
+ if (!portCheck.available && !portCheck.isOwnProcess) {
767
800
  warnings.push(`Port ${project.port} is in use by ${portCheck.usedBy}`);
768
801
  }
769
802
  }
@@ -1396,11 +1429,16 @@ async function newCommand(options) {
1396
1429
  }
1397
1430
  }
1398
1431
  if (!validateProjectName(project.name)) {
1399
- console.error(chalk3.red("Error: Invalid project name"));
1432
+ console.error(chalk3.red("Error: Invalid project name. Use alphanumeric characters, dashes, and underscores only."));
1400
1433
  process.exit(1);
1401
1434
  }
1402
1435
  if (!validateHostname(project.hostname)) {
1403
- console.error(chalk3.red("Error: Invalid hostname"));
1436
+ console.error(chalk3.red("Error: Invalid hostname format"));
1437
+ process.exit(1);
1438
+ }
1439
+ const conflictCheck = canAddProject(project);
1440
+ if (!conflictCheck.valid) {
1441
+ console.error(chalk3.red(`Error: ${conflictCheck.error}`));
1404
1442
  process.exit(1);
1405
1443
  }
1406
1444
  if (!existsSync7(project.path)) {
@@ -1470,13 +1508,42 @@ Configuration saved. Run ${chalk3.cyan("sudo bindler apply")} to update nginx an
1470
1508
  // src/commands/list.ts
1471
1509
  import chalk4 from "chalk";
1472
1510
  import Table from "cli-table3";
1473
- async function listCommand() {
1511
+ async function listCommand(options = {}) {
1474
1512
  const projects = listProjects();
1475
1513
  if (projects.length === 0) {
1514
+ if (options.json) {
1515
+ console.log("[]");
1516
+ return;
1517
+ }
1476
1518
  console.log(chalk4.yellow("No projects registered."));
1477
1519
  console.log(chalk4.dim(`Run ${chalk4.cyan("bindler new")} to create one.`));
1478
1520
  return;
1479
1521
  }
1522
+ if (options.json) {
1523
+ const output = projects.map((project) => {
1524
+ let status = "n/a";
1525
+ if (project.type === "npm") {
1526
+ const process2 = getProcessByName(project.name);
1527
+ status = process2?.status || "not_started";
1528
+ } else {
1529
+ status = project.enabled !== false ? "serving" : "disabled";
1530
+ }
1531
+ if (project.enabled === false) status = "disabled";
1532
+ return {
1533
+ name: project.name,
1534
+ type: project.type,
1535
+ hostname: project.hostname,
1536
+ basePath: project.basePath,
1537
+ port: project.port,
1538
+ path: project.path,
1539
+ status,
1540
+ local: project.local,
1541
+ enabled: project.enabled !== false
1542
+ };
1543
+ });
1544
+ console.log(JSON.stringify(output, null, 2));
1545
+ return;
1546
+ }
1480
1547
  const table = new Table({
1481
1548
  head: [
1482
1549
  chalk4.cyan("Name"),
@@ -1745,6 +1812,15 @@ ${succeeded}/${results.length} stopped successfully`));
1745
1812
  console.log(chalk7.yellow(`Project "${name}" is a static site - no process to stop.`));
1746
1813
  return;
1747
1814
  }
1815
+ const processStatus = getProcessByName(name);
1816
+ if (!processStatus) {
1817
+ console.log(chalk7.yellow(`${name} is not managed by PM2 (never started)`));
1818
+ return;
1819
+ }
1820
+ if (processStatus.status === "stopped") {
1821
+ console.log(chalk7.yellow(`${name} is already stopped`));
1822
+ return;
1823
+ }
1748
1824
  console.log(chalk7.blue(`Stopping ${name}...`));
1749
1825
  const result = stopProject(name);
1750
1826
  if (result.success) {
@@ -1848,7 +1924,11 @@ async function updateCommand(name, options) {
1848
1924
  process.exit(1);
1849
1925
  }
1850
1926
  const updates = {};
1851
- if (options.hostname) {
1927
+ if (options.hostname !== void 0) {
1928
+ if (!options.hostname || options.hostname.trim() === "") {
1929
+ console.error(chalk10.red("Error: Hostname cannot be empty"));
1930
+ process.exit(1);
1931
+ }
1852
1932
  if (!validateHostname(options.hostname)) {
1853
1933
  console.error(chalk10.red("Error: Invalid hostname format"));
1854
1934
  process.exit(1);
@@ -2369,7 +2449,7 @@ async function infoCommand() {
2369
2449
  `));
2370
2450
  console.log(chalk15.white(" Manage multiple projects behind Cloudflare Tunnel"));
2371
2451
  console.log(chalk15.white(" with Nginx and PM2\n"));
2372
- console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.2"));
2452
+ console.log(chalk15.dim(" Version: ") + chalk15.white("1.8.0"));
2373
2453
  console.log(chalk15.dim(" Author: ") + chalk15.white("alfaoz"));
2374
2454
  console.log(chalk15.dim(" License: ") + chalk15.white("MIT"));
2375
2455
  console.log(chalk15.dim(" GitHub: ") + chalk15.cyan("https://github.com/alfaoz/bindler"));
@@ -2426,8 +2506,9 @@ async function checkDns(hostname) {
2426
2506
  }
2427
2507
  return result;
2428
2508
  }
2429
- async function checkHttp(hostname, path = "/") {
2430
- const url = `https://${hostname}${path}`;
2509
+ async function checkHttp(hostname, path = "/", useHttp = false) {
2510
+ const protocol = useHttp ? "http" : "https";
2511
+ const url = `${protocol}://${hostname}${path}`;
2431
2512
  const startTime = Date.now();
2432
2513
  try {
2433
2514
  const controller = new AbortController();
@@ -2443,12 +2524,14 @@ async function checkHttp(hostname, path = "/") {
2443
2524
  reachable: true,
2444
2525
  statusCode: response.status,
2445
2526
  redirectUrl: response.headers.get("location") || void 0,
2446
- responseTime
2527
+ responseTime,
2528
+ protocol
2447
2529
  };
2448
2530
  } catch (error) {
2449
2531
  return {
2450
2532
  reachable: false,
2451
- error: error instanceof Error ? error.message : String(error)
2533
+ error: error instanceof Error ? error.message : String(error),
2534
+ protocol
2452
2535
  };
2453
2536
  }
2454
2537
  }
@@ -2488,8 +2571,9 @@ Checking ${hostname}...
2488
2571
  console.log(chalk16.green(" \u2713 AAAA (IPv6): ") + dns.ipv6.join(", "));
2489
2572
  }
2490
2573
  console.log("");
2491
- console.log(chalk16.bold("HTTP Check:"));
2492
- const http = await checkHttp(hostname, basePath);
2574
+ const useHttp = options.http ?? isLocal;
2575
+ console.log(chalk16.bold(`HTTP Check (${useHttp ? "HTTP" : "HTTPS"}):`));
2576
+ const http = await checkHttp(hostname, basePath, useHttp);
2493
2577
  if (!http.reachable) {
2494
2578
  console.log(chalk16.red(" \u2717 Not reachable"));
2495
2579
  const err = http.error || "";
@@ -3800,14 +3884,48 @@ async function cloneCommand(source, newName, options) {
3800
3884
  process.exit(1);
3801
3885
  }
3802
3886
  }
3887
+ let targetPath = options.path;
3888
+ if (!targetPath) {
3889
+ const answer = await inquirer5.prompt([
3890
+ {
3891
+ type: "input",
3892
+ name: "path",
3893
+ message: "Path for new project:",
3894
+ default: sourceProject.path.replace(source, targetName),
3895
+ validate: (input) => {
3896
+ if (!input || input.trim() === "") {
3897
+ return "Path is required";
3898
+ }
3899
+ return true;
3900
+ }
3901
+ }
3902
+ ]);
3903
+ targetPath = answer.path;
3904
+ }
3803
3905
  const newProject = {
3804
3906
  ...sourceProject,
3805
3907
  name: targetName,
3806
3908
  hostname: targetHostname,
3807
- path: options.path || sourceProject.path
3909
+ path: targetPath
3808
3910
  };
3809
3911
  if (newProject.type === "npm") {
3810
- newProject.port = options.port || findAvailablePort();
3912
+ let port = options.port || findAvailablePort();
3913
+ const portCheck = checkPortInUse(port);
3914
+ if (portCheck.inUse) {
3915
+ const processInfo = portCheck.process ? ` by ${portCheck.process}` : "";
3916
+ console.log(chalk27.yellow(`Warning: Port ${port} is already in use${processInfo}`));
3917
+ if (!options.port) {
3918
+ for (let p = port + 1; p <= 9e3; p++) {
3919
+ const check = checkPortInUse(p);
3920
+ if (!check.inUse) {
3921
+ port = p;
3922
+ console.log(chalk27.dim(` Using port ${port} instead`));
3923
+ break;
3924
+ }
3925
+ }
3926
+ }
3927
+ }
3928
+ newProject.port = port;
3811
3929
  if (newProject.env?.PORT) {
3812
3930
  newProject.env = { ...newProject.env, PORT: String(newProject.port) };
3813
3931
  }
@@ -3938,9 +4056,12 @@ Note: Add to /etc/hosts if not already:`));
3938
4056
  console.log(chalk28.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
3939
4057
  }
3940
4058
  const defaults = getDefaults();
3941
- const listenPort = defaults.nginxListen.split(":")[1] || "8080";
4059
+ const listenParts = defaults.nginxListen.split(":");
4060
+ const listenPort = listenParts.length > 1 ? listenParts[1] : listenParts[0];
4061
+ const isDefaultPort = listenPort === "80" || listenPort === "443";
4062
+ const accessUrl = isDefaultPort ? `http://${project.hostname}` : `http://${project.hostname}:${listenPort}`;
3942
4063
  console.log(chalk28.green(`
3943
- Access at: http://${project.hostname}:${listenPort}`));
4064
+ Access at: ${accessUrl}`));
3944
4065
  console.log(chalk28.dim("Press Ctrl+C to stop\n"));
3945
4066
  console.log(chalk28.dim("---"));
3946
4067
  const env = {
@@ -4111,28 +4232,28 @@ async function checkForUpdates() {
4111
4232
  const cache = readCache();
4112
4233
  const now = Date.now();
4113
4234
  if (now - cache.lastCheck < CHECK_INTERVAL) {
4114
- if (cache.latestVersion && compareVersions("1.6.2", cache.latestVersion) > 0) {
4235
+ if (cache.latestVersion && compareVersions("1.8.0", cache.latestVersion) > 0) {
4115
4236
  showUpdateMessage(cache.latestVersion);
4116
4237
  }
4117
4238
  return;
4118
4239
  }
4119
4240
  fetchLatestVersion().then((latestVersion) => {
4120
4241
  writeCache({ lastCheck: now, latestVersion });
4121
- if (latestVersion && compareVersions("1.6.2", latestVersion) > 0) {
4242
+ if (latestVersion && compareVersions("1.8.0", latestVersion) > 0) {
4122
4243
  showUpdateMessage(latestVersion);
4123
4244
  }
4124
4245
  });
4125
4246
  }
4126
4247
  function showUpdateMessage(latestVersion) {
4127
4248
  console.log("");
4128
- console.log(chalk30.yellow(` Update available: ${"1.6.2"} \u2192 ${latestVersion}`));
4249
+ console.log(chalk30.yellow(` Update available: ${"1.8.0"} \u2192 ${latestVersion}`));
4129
4250
  console.log(chalk30.dim(` Run: npm update -g bindler`));
4130
4251
  console.log("");
4131
4252
  }
4132
4253
 
4133
4254
  // src/cli.ts
4134
4255
  var program = new Command();
4135
- program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.2");
4256
+ program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.8.0");
4136
4257
  program.hook("preAction", async () => {
4137
4258
  try {
4138
4259
  initConfig();
@@ -4143,8 +4264,8 @@ program.hook("preAction", async () => {
4143
4264
  program.command("new").description("Create and register a new project").option("-n, --name <name>", "Project name").option("-t, --type <type>", "Project type (static or npm)", "static").option("-p, --path <path>", "Project directory path").option("-h, --hostname <hostname>", "Hostname for the project").option("-b, --base-path <path>", "Base path for path-based routing (e.g., /api)").option("--port <port>", "Port number (npm projects only)").option("-s, --start <command>", "Start command (npm projects only)").option("-l, --local", "Local project (skip Cloudflare, use .local hostname)").option("--apply", "Apply nginx config after creating").action(async (options) => {
4144
4265
  await newCommand(options);
4145
4266
  });
4146
- program.command("list").alias("ls").description("List all registered projects").action(async () => {
4147
- await listCommand();
4267
+ program.command("list").alias("ls").description("List all registered projects").option("--json", "Output as JSON (for scripting)").action(async (options) => {
4268
+ await listCommand(options);
4148
4269
  });
4149
4270
  program.command("status").description("Show detailed status of all projects").action(async () => {
4150
4271
  await statusCommand();
@@ -4234,7 +4355,7 @@ program.command("ports").description("Show allocated ports").action(async () =>
4234
4355
  program.command("info").description("Show bindler information and stats").action(async () => {
4235
4356
  await infoCommand();
4236
4357
  });
4237
- program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
4358
+ program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").option("--http", "Use HTTP instead of HTTPS (auto-enabled for .local hostnames)").action(async (hostname, options) => {
4238
4359
  if (!hostname) {
4239
4360
  console.log(chalk31.red("Usage: bindler check <hostname>"));
4240
4361
  console.log(chalk31.dim("\nExamples:"));