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 +151 -30
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
106
|
+
function canAddProject(project) {
|
|
107
107
|
const config = readConfig();
|
|
108
108
|
if (config.projects.some((p) => p.name === project.name)) {
|
|
109
|
-
|
|
109
|
+
return { valid: false, error: `Project "${project.name}" already exists` };
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
2492
|
-
|
|
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:
|
|
3909
|
+
path: targetPath
|
|
3808
3910
|
};
|
|
3809
3911
|
if (newProject.type === "npm") {
|
|
3810
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:"));
|