bindler 1.4.1 → 1.6.1
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 +672 -489
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,13 +8,13 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import
|
|
11
|
+
import chalk31 from "chalk";
|
|
12
12
|
|
|
13
13
|
// src/commands/new.ts
|
|
14
|
-
import { existsSync as
|
|
14
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
15
15
|
import { basename as basename2 } from "path";
|
|
16
16
|
import inquirer from "inquirer";
|
|
17
|
-
import
|
|
17
|
+
import chalk3 from "chalk";
|
|
18
18
|
|
|
19
19
|
// src/lib/config.ts
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "fs";
|
|
@@ -284,6 +284,25 @@ function isPortAvailable(port) {
|
|
|
284
284
|
const usedPorts = new Set(getUsedPorts());
|
|
285
285
|
return !usedPorts.has(port);
|
|
286
286
|
}
|
|
287
|
+
function checkPortInUse(port) {
|
|
288
|
+
const lsofResult = execCommandSafe(`lsof -i :${port} -P -n 2>/dev/null | grep LISTEN`);
|
|
289
|
+
if (lsofResult.success && lsofResult.output) {
|
|
290
|
+
const match = lsofResult.output.match(/^(\S+)\s+(\d+)/);
|
|
291
|
+
if (match) {
|
|
292
|
+
return { inUse: true, process: `${match[1]} (PID ${match[2]})` };
|
|
293
|
+
}
|
|
294
|
+
return { inUse: true };
|
|
295
|
+
}
|
|
296
|
+
const ssResult = execCommandSafe(`ss -tlnp 2>/dev/null | grep ":${port} "`);
|
|
297
|
+
if (ssResult.success && ssResult.output) {
|
|
298
|
+
const match = ssResult.output.match(/users:\(\("([^"]+)",pid=(\d+)/);
|
|
299
|
+
if (match) {
|
|
300
|
+
return { inUse: true, process: `${match[1]} (PID ${match[2]})` };
|
|
301
|
+
}
|
|
302
|
+
return { inUse: true };
|
|
303
|
+
}
|
|
304
|
+
return { inUse: false };
|
|
305
|
+
}
|
|
287
306
|
function getPortsTable() {
|
|
288
307
|
const config = readConfig();
|
|
289
308
|
return config.projects.filter((p) => p.type === "npm" && p.port).map((p) => ({
|
|
@@ -570,18 +589,17 @@ function startProject(project) {
|
|
|
570
589
|
}
|
|
571
590
|
const pm2Name = getPm2ProcessName(project.name);
|
|
572
591
|
const existingProcess = getProcessByName(project.name);
|
|
573
|
-
const envVars = [];
|
|
574
|
-
if (project.env) {
|
|
575
|
-
for (const [key, value] of Object.entries(project.env)) {
|
|
576
|
-
envVars.push(`${key}=${value}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
592
|
let command;
|
|
580
593
|
if (existingProcess) {
|
|
581
594
|
command = `pm2 restart "${pm2Name}"`;
|
|
582
595
|
} else {
|
|
583
|
-
|
|
584
|
-
|
|
596
|
+
let startCmd = project.start;
|
|
597
|
+
if (project.env && Object.keys(project.env).length > 0) {
|
|
598
|
+
const envPrefix = Object.entries(project.env).map(([key, value]) => `${key}=${escapeShellArg(value)}`).join(" ");
|
|
599
|
+
startCmd = `${envPrefix} ${project.start}`;
|
|
600
|
+
}
|
|
601
|
+
const escapedCmd = startCmd.replace(/'/g, "'\\''");
|
|
602
|
+
command = `pm2 start 'bash -c "${escapedCmd}"' --name "${pm2Name}" --cwd "${project.path}"`;
|
|
585
603
|
}
|
|
586
604
|
const result = execCommandSafe(command);
|
|
587
605
|
if (!result.success) {
|
|
@@ -590,6 +608,12 @@ function startProject(project) {
|
|
|
590
608
|
execCommandSafe("pm2 save");
|
|
591
609
|
return { success: true };
|
|
592
610
|
}
|
|
611
|
+
function escapeShellArg(arg) {
|
|
612
|
+
if (/[^a-zA-Z0-9_\-=]/.test(arg)) {
|
|
613
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
614
|
+
}
|
|
615
|
+
return arg;
|
|
616
|
+
}
|
|
593
617
|
function stopProject(name) {
|
|
594
618
|
const pm2Name = getPm2ProcessName(name);
|
|
595
619
|
const result = execCommandSafe(`pm2 stop "${pm2Name}"`);
|
|
@@ -866,9 +890,296 @@ function runPreflightChecks(config) {
|
|
|
866
890
|
return result;
|
|
867
891
|
}
|
|
868
892
|
|
|
893
|
+
// src/commands/apply.ts
|
|
894
|
+
import chalk2 from "chalk";
|
|
895
|
+
import { existsSync as existsSync6 } from "fs";
|
|
896
|
+
|
|
897
|
+
// src/lib/cloudflare.ts
|
|
898
|
+
function isCloudflaredInstalled() {
|
|
899
|
+
const result = execCommandSafe("which cloudflared");
|
|
900
|
+
return result.success;
|
|
901
|
+
}
|
|
902
|
+
function getCloudflaredVersion() {
|
|
903
|
+
const result = execCommandSafe("cloudflared --version");
|
|
904
|
+
if (result.success) {
|
|
905
|
+
const match = result.output.match(/cloudflared version (\S+)/);
|
|
906
|
+
return match ? match[1] : result.output;
|
|
907
|
+
}
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
function listTunnels() {
|
|
911
|
+
const result = execCommandSafe("cloudflared tunnel list --output json");
|
|
912
|
+
if (!result.success) {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const tunnels = JSON.parse(result.output);
|
|
917
|
+
return tunnels.map((t) => ({
|
|
918
|
+
id: t.id,
|
|
919
|
+
name: t.name,
|
|
920
|
+
createdAt: t.created_at
|
|
921
|
+
}));
|
|
922
|
+
} catch {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function getTunnelByName(name) {
|
|
927
|
+
const tunnels = listTunnels();
|
|
928
|
+
const tunnel = tunnels.find((t) => t.name === name);
|
|
929
|
+
return tunnel ? { id: tunnel.id, name: tunnel.name } : null;
|
|
930
|
+
}
|
|
931
|
+
function routeDns(tunnelName, hostname) {
|
|
932
|
+
const result = execCommandSafe(`cloudflared tunnel route dns "${tunnelName}" "${hostname}"`);
|
|
933
|
+
if (!result.success) {
|
|
934
|
+
if (result.error?.includes("already exists") || result.output?.includes("already exists")) {
|
|
935
|
+
return { success: true, output: "DNS route already exists" };
|
|
936
|
+
}
|
|
937
|
+
return { success: false, error: result.error };
|
|
938
|
+
}
|
|
939
|
+
return { success: true, output: result.output };
|
|
940
|
+
}
|
|
941
|
+
function routeDnsForAllProjects() {
|
|
942
|
+
const config = readConfig();
|
|
943
|
+
const { tunnelName, applyCloudflareDnsRoutes } = config.defaults;
|
|
944
|
+
if (!applyCloudflareDnsRoutes) {
|
|
945
|
+
return [];
|
|
946
|
+
}
|
|
947
|
+
const results = [];
|
|
948
|
+
for (const project of config.projects) {
|
|
949
|
+
if (project.enabled === false) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (project.local) {
|
|
953
|
+
results.push({
|
|
954
|
+
hostname: project.hostname,
|
|
955
|
+
success: true,
|
|
956
|
+
skipped: true,
|
|
957
|
+
output: "Local project - skipped"
|
|
958
|
+
});
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const result = routeDns(tunnelName, project.hostname);
|
|
962
|
+
results.push({
|
|
963
|
+
hostname: project.hostname,
|
|
964
|
+
...result
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return results;
|
|
968
|
+
}
|
|
969
|
+
function isTunnelRunning(tunnelName) {
|
|
970
|
+
const result = execCommandSafe(`pgrep -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
971
|
+
return result.success;
|
|
972
|
+
}
|
|
973
|
+
function getTunnelInfo(tunnelName) {
|
|
974
|
+
const tunnel = getTunnelByName(tunnelName);
|
|
975
|
+
if (!tunnel) {
|
|
976
|
+
return { exists: false, running: false };
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
exists: true,
|
|
980
|
+
running: isTunnelRunning(tunnelName),
|
|
981
|
+
id: tunnel.id
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/commands/apply.ts
|
|
986
|
+
async function applyCommand(options) {
|
|
987
|
+
let config = readConfig();
|
|
988
|
+
const defaults = getDefaults();
|
|
989
|
+
if (options.sync) {
|
|
990
|
+
console.log(chalk2.dim("Syncing bindler.yaml from project directories...\n"));
|
|
991
|
+
let synced = 0;
|
|
992
|
+
for (const project of config.projects) {
|
|
993
|
+
if (!existsSync6(project.path)) continue;
|
|
994
|
+
const yamlConfig = readBindlerYaml(project.path);
|
|
995
|
+
if (yamlConfig) {
|
|
996
|
+
const merged = mergeYamlWithProject(project, yamlConfig);
|
|
997
|
+
updateProject(project.name, merged);
|
|
998
|
+
console.log(chalk2.green(` \u2713 Synced ${project.name} from bindler.yaml`));
|
|
999
|
+
synced++;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (synced === 0) {
|
|
1003
|
+
console.log(chalk2.dim(" No bindler.yaml files found in project directories"));
|
|
1004
|
+
} else {
|
|
1005
|
+
console.log(chalk2.dim(`
|
|
1006
|
+
Synced ${synced} project(s)
|
|
1007
|
+
`));
|
|
1008
|
+
}
|
|
1009
|
+
config = readConfig();
|
|
1010
|
+
}
|
|
1011
|
+
if (options.env) {
|
|
1012
|
+
console.log(chalk2.dim(`Using ${options.env} environment configuration...
|
|
1013
|
+
`));
|
|
1014
|
+
const envProjects = listProjectsForEnv(options.env);
|
|
1015
|
+
config = { ...config, projects: envProjects };
|
|
1016
|
+
}
|
|
1017
|
+
const hasProjects = config.projects.length > 0;
|
|
1018
|
+
console.log(chalk2.blue("Applying configuration...\n"));
|
|
1019
|
+
if (hasProjects && !options.skipChecks) {
|
|
1020
|
+
console.log(chalk2.dim("Running preflight checks..."));
|
|
1021
|
+
const checkResult = runPreflightChecks(config);
|
|
1022
|
+
if (!checkResult.valid) {
|
|
1023
|
+
printValidationResult(checkResult);
|
|
1024
|
+
console.log(chalk2.red("\n\u2717 Preflight checks failed. Fix the errors above before applying."));
|
|
1025
|
+
console.log(chalk2.dim(" Use --skip-checks to bypass (not recommended)"));
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
if (checkResult.warnings.length > 0) {
|
|
1029
|
+
printValidationResult(checkResult);
|
|
1030
|
+
console.log("");
|
|
1031
|
+
} else {
|
|
1032
|
+
console.log(chalk2.green(" \u2713 Preflight checks passed"));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
console.log(chalk2.dim("Generating nginx configuration..."));
|
|
1036
|
+
if (options.dryRun) {
|
|
1037
|
+
const nginxConfig = generateNginxConfig(config);
|
|
1038
|
+
console.log(chalk2.cyan("\n--- Generated nginx config (dry-run) ---\n"));
|
|
1039
|
+
console.log(nginxConfig);
|
|
1040
|
+
console.log(chalk2.cyan("--- End of config ---\n"));
|
|
1041
|
+
console.log(chalk2.yellow("Dry run mode - no changes were made."));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const { path, content } = writeNginxConfig(config);
|
|
1046
|
+
console.log(chalk2.green(` \u2713 Wrote nginx config to ${path}`));
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1049
|
+
console.error(chalk2.red(` \u2717 Failed to write nginx config: ${errMsg}`));
|
|
1050
|
+
if (errMsg.includes("EACCES") || errMsg.includes("permission denied")) {
|
|
1051
|
+
console.log(chalk2.yellow(`
|
|
1052
|
+
Try running with sudo: ${chalk2.cyan("sudo bindler apply")}`));
|
|
1053
|
+
}
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
const authProjects = config.projects.filter(
|
|
1057
|
+
(p) => p.security?.basicAuth?.enabled && p.security.basicAuth.users?.length
|
|
1058
|
+
);
|
|
1059
|
+
if (authProjects.length > 0) {
|
|
1060
|
+
console.log(chalk2.dim("Generating htpasswd files..."));
|
|
1061
|
+
try {
|
|
1062
|
+
generateHtpasswdFiles(config.projects);
|
|
1063
|
+
console.log(chalk2.green(` \u2713 Generated htpasswd files for ${authProjects.length} project(s)`));
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
console.log(chalk2.yellow(` ! Failed to generate htpasswd files: ${error}`));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
console.log(chalk2.dim("Testing nginx configuration..."));
|
|
1069
|
+
const testResult = testNginxConfig();
|
|
1070
|
+
if (!testResult.success) {
|
|
1071
|
+
console.error(chalk2.red(" \u2717 Nginx configuration test failed:"));
|
|
1072
|
+
console.error(chalk2.red(testResult.output));
|
|
1073
|
+
console.log(chalk2.yellow("\nConfiguration was written but nginx was NOT reloaded."));
|
|
1074
|
+
console.log(chalk2.dim("Fix the configuration and run `sudo bindler apply` again."));
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
console.log(chalk2.green(" \u2713 Nginx configuration test passed"));
|
|
1078
|
+
if (!options.noReload) {
|
|
1079
|
+
console.log(chalk2.dim("Reloading nginx..."));
|
|
1080
|
+
const reloadResult = reloadNginx();
|
|
1081
|
+
if (!reloadResult.success) {
|
|
1082
|
+
console.error(chalk2.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
|
|
1083
|
+
console.log(chalk2.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
console.log(chalk2.green(" \u2713 Nginx reloaded successfully"));
|
|
1087
|
+
const listenPort = parseInt(defaults.nginxListen.split(":").pop() || "80", 10);
|
|
1088
|
+
const portCheck = execCommandSafe(`lsof -i :${listenPort} -P -n 2>/dev/null | grep LISTEN | grep nginx`);
|
|
1089
|
+
if (!portCheck.success || !portCheck.output) {
|
|
1090
|
+
const isPrivilegedPort = listenPort < 1024;
|
|
1091
|
+
console.log(chalk2.yellow(`
|
|
1092
|
+
\u26A0 Nginx is not listening on port ${listenPort}`));
|
|
1093
|
+
if (isPrivilegedPort) {
|
|
1094
|
+
console.log(chalk2.dim(` Port ${listenPort} requires root privileges.
|
|
1095
|
+
`));
|
|
1096
|
+
console.log(chalk2.cyan(" Solutions:\n"));
|
|
1097
|
+
console.log(chalk2.white(" Option 1: Restart nginx with sudo (recommended for port 80)"));
|
|
1098
|
+
console.log(chalk2.dim(" sudo pkill nginx && sudo nginx\n"));
|
|
1099
|
+
console.log(chalk2.white(" Option 2: Use a non-privileged port (no sudo needed)"));
|
|
1100
|
+
console.log(chalk2.dim(" bindler config set nginxListen 8080"));
|
|
1101
|
+
console.log(chalk2.dim(" bindler apply"));
|
|
1102
|
+
console.log(chalk2.dim(" # Then access via http://hostname:8080\n"));
|
|
1103
|
+
} else {
|
|
1104
|
+
console.log(chalk2.dim(" Try restarting nginx: brew services restart nginx"));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
console.log(chalk2.yellow(" - Skipped nginx reload (--no-reload)"));
|
|
1109
|
+
}
|
|
1110
|
+
const isDirectMode = defaults.mode === "direct";
|
|
1111
|
+
if (!hasProjects) {
|
|
1112
|
+
} else if (isDirectMode) {
|
|
1113
|
+
console.log(chalk2.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
|
|
1114
|
+
} else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
|
|
1115
|
+
console.log(chalk2.dim("\nConfiguring Cloudflare DNS routes..."));
|
|
1116
|
+
if (!isCloudflaredInstalled()) {
|
|
1117
|
+
console.log(chalk2.yellow(" - cloudflared not installed, skipping DNS routes"));
|
|
1118
|
+
} else {
|
|
1119
|
+
const dnsResults = routeDnsForAllProjects();
|
|
1120
|
+
if (dnsResults.length === 0) {
|
|
1121
|
+
console.log(chalk2.dim(" No hostnames to route"));
|
|
1122
|
+
} else {
|
|
1123
|
+
for (const result of dnsResults) {
|
|
1124
|
+
if (result.skipped) {
|
|
1125
|
+
console.log(chalk2.dim(` - ${result.hostname} (local - skipped)`));
|
|
1126
|
+
} else if (result.success) {
|
|
1127
|
+
const msg = result.output?.includes("already exists") ? "exists" : "routed";
|
|
1128
|
+
console.log(chalk2.green(` \u2713 ${result.hostname} (${msg})`));
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log(chalk2.red(` \u2717 ${result.hostname}: ${result.error}`));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
} else if (options.noCloudflare) {
|
|
1136
|
+
console.log(chalk2.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
|
|
1137
|
+
}
|
|
1138
|
+
if (hasProjects && isDirectMode && defaults.sslEnabled && options.ssl !== false) {
|
|
1139
|
+
console.log(chalk2.dim("\nSetting up SSL certificates..."));
|
|
1140
|
+
const hostnames = config.projects.filter((p) => p.enabled !== false && !p.local).map((p) => p.hostname);
|
|
1141
|
+
if (hostnames.length === 0) {
|
|
1142
|
+
console.log(chalk2.dim(" No hostnames to secure"));
|
|
1143
|
+
} else {
|
|
1144
|
+
const certbotResult = execCommandSafe("which certbot");
|
|
1145
|
+
if (!certbotResult.success) {
|
|
1146
|
+
console.log(chalk2.yellow(" - certbot not installed, skipping SSL"));
|
|
1147
|
+
console.log(chalk2.dim(" Run: bindler setup --direct"));
|
|
1148
|
+
} else {
|
|
1149
|
+
for (const hostname of hostnames) {
|
|
1150
|
+
console.log(chalk2.dim(` Requesting certificate for ${hostname}...`));
|
|
1151
|
+
const email = defaults.sslEmail || "admin@" + hostname.split(".").slice(-2).join(".");
|
|
1152
|
+
const result = execCommandSafe(
|
|
1153
|
+
`sudo certbot --nginx -d ${hostname} --non-interactive --agree-tos --email ${email} 2>&1`
|
|
1154
|
+
);
|
|
1155
|
+
if (result.success || result.output?.includes("Certificate not yet due for renewal")) {
|
|
1156
|
+
console.log(chalk2.green(` \u2713 ${hostname} (secured)`));
|
|
1157
|
+
} else if (result.output?.includes("already exists")) {
|
|
1158
|
+
console.log(chalk2.green(` \u2713 ${hostname} (exists)`));
|
|
1159
|
+
} else {
|
|
1160
|
+
console.log(chalk2.yellow(` ! ${hostname}: ${result.error || "failed"}`));
|
|
1161
|
+
console.log(chalk2.dim(" Run manually: sudo certbot --nginx -d " + hostname));
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
console.log(chalk2.green("\n\u2713 Configuration applied successfully!"));
|
|
1168
|
+
if (hasProjects) {
|
|
1169
|
+
console.log(chalk2.dim(`
|
|
1170
|
+
${config.projects.length} project(s) configured:`));
|
|
1171
|
+
for (const project of config.projects) {
|
|
1172
|
+
const status = project.enabled !== false ? chalk2.green("enabled") : chalk2.yellow("disabled");
|
|
1173
|
+
console.log(chalk2.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
|
|
1174
|
+
}
|
|
1175
|
+
} else {
|
|
1176
|
+
console.log(chalk2.dim("\nNo projects configured. Nginx config cleared."));
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
869
1180
|
// src/commands/new.ts
|
|
870
1181
|
async function newCommand(options) {
|
|
871
|
-
console.log(
|
|
1182
|
+
console.log(chalk3.dim("Checking prerequisites...\n"));
|
|
872
1183
|
const issues = [];
|
|
873
1184
|
if (!isNginxInstalled()) {
|
|
874
1185
|
issues.push("nginx is not installed. Install: brew install nginx (macOS) or apt install nginx (Linux)");
|
|
@@ -877,11 +1188,11 @@ async function newCommand(options) {
|
|
|
877
1188
|
issues.push("PM2 is not installed. Install: npm install -g pm2");
|
|
878
1189
|
}
|
|
879
1190
|
if (issues.length > 0) {
|
|
880
|
-
console.log(
|
|
1191
|
+
console.log(chalk3.red("Missing prerequisites:\n"));
|
|
881
1192
|
for (const issue of issues) {
|
|
882
|
-
console.log(
|
|
1193
|
+
console.log(chalk3.red(` \u2717 ${issue}`));
|
|
883
1194
|
}
|
|
884
|
-
console.log(
|
|
1195
|
+
console.log(chalk3.dim("\nRun `bindler doctor` for full diagnostics."));
|
|
885
1196
|
const { proceed } = await inquirer.prompt([
|
|
886
1197
|
{
|
|
887
1198
|
type: "confirm",
|
|
@@ -895,17 +1206,17 @@ async function newCommand(options) {
|
|
|
895
1206
|
}
|
|
896
1207
|
console.log("");
|
|
897
1208
|
} else {
|
|
898
|
-
console.log(
|
|
1209
|
+
console.log(chalk3.green("\u2713 Prerequisites OK\n"));
|
|
899
1210
|
}
|
|
900
1211
|
const defaults = getDefaults();
|
|
901
1212
|
let project = {};
|
|
902
1213
|
const cwd = process.cwd();
|
|
903
1214
|
const cwdName = basename2(cwd);
|
|
904
1215
|
const initialPath = options.path || cwd;
|
|
905
|
-
const yamlConfig =
|
|
1216
|
+
const yamlConfig = existsSync7(initialPath) ? readBindlerYaml(initialPath) : null;
|
|
906
1217
|
let yamlDefaults = {};
|
|
907
1218
|
if (yamlConfig) {
|
|
908
|
-
console.log(
|
|
1219
|
+
console.log(chalk3.cyan("Found bindler.yaml - using as defaults\n"));
|
|
909
1220
|
yamlDefaults = yamlToProject(yamlConfig, initialPath);
|
|
910
1221
|
}
|
|
911
1222
|
if (!options.name) {
|
|
@@ -939,7 +1250,7 @@ async function newCommand(options) {
|
|
|
939
1250
|
name: "type",
|
|
940
1251
|
message: "Project type:",
|
|
941
1252
|
choices: (answers2) => {
|
|
942
|
-
const detected =
|
|
1253
|
+
const detected = existsSync7(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
943
1254
|
return [
|
|
944
1255
|
{ name: `npm (Node.js app)${detected === "npm" ? " - detected" : ""}`, value: "npm" },
|
|
945
1256
|
{ name: `static (HTML/CSS/JS)${detected === "static" ? " - detected" : ""}`, value: "static" }
|
|
@@ -947,7 +1258,7 @@ async function newCommand(options) {
|
|
|
947
1258
|
},
|
|
948
1259
|
default: (answers2) => {
|
|
949
1260
|
if (yamlDefaults.type) return yamlDefaults.type;
|
|
950
|
-
return
|
|
1261
|
+
return existsSync7(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
951
1262
|
}
|
|
952
1263
|
},
|
|
953
1264
|
{
|
|
@@ -980,8 +1291,18 @@ async function newCommand(options) {
|
|
|
980
1291
|
if (yamlDefaults.security) project.security = yamlDefaults.security;
|
|
981
1292
|
if (yamlDefaults.environments) project.environments = yamlDefaults.environments;
|
|
982
1293
|
if (answers.type === "npm") {
|
|
983
|
-
const scripts =
|
|
984
|
-
|
|
1294
|
+
const scripts = existsSync7(answers.path) ? getPackageJsonScripts(answers.path) : [];
|
|
1295
|
+
let suggestedPort = yamlDefaults.port || findAvailablePort();
|
|
1296
|
+
const portCheck = checkPortInUse(suggestedPort);
|
|
1297
|
+
if (portCheck.inUse) {
|
|
1298
|
+
for (let p = suggestedPort + 1; p <= 9e3; p++) {
|
|
1299
|
+
const check = checkPortInUse(p);
|
|
1300
|
+
if (!check.inUse) {
|
|
1301
|
+
suggestedPort = p;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
985
1306
|
const npmAnswers = await inquirer.prompt([
|
|
986
1307
|
{
|
|
987
1308
|
type: "input",
|
|
@@ -993,6 +1314,11 @@ async function newCommand(options) {
|
|
|
993
1314
|
if (!validatePort(port)) {
|
|
994
1315
|
return "Invalid port. Use a number between 1024 and 65535.";
|
|
995
1316
|
}
|
|
1317
|
+
const inUseCheck = checkPortInUse(port);
|
|
1318
|
+
if (inUseCheck.inUse) {
|
|
1319
|
+
const processInfo = inUseCheck.process ? ` by ${inUseCheck.process}` : "";
|
|
1320
|
+
return `Port ${port} is already in use${processInfo}. Choose another port.`;
|
|
1321
|
+
}
|
|
996
1322
|
return true;
|
|
997
1323
|
},
|
|
998
1324
|
filter: (input) => parseInt(input, 10)
|
|
@@ -1034,7 +1360,7 @@ async function newCommand(options) {
|
|
|
1034
1360
|
}
|
|
1035
1361
|
} else {
|
|
1036
1362
|
if (!options.hostname) {
|
|
1037
|
-
console.error(
|
|
1363
|
+
console.error(chalk3.red("Error: --hostname is required"));
|
|
1038
1364
|
process.exit(1);
|
|
1039
1365
|
}
|
|
1040
1366
|
project.name = options.name;
|
|
@@ -1048,20 +1374,36 @@ async function newCommand(options) {
|
|
|
1048
1374
|
project.local = true;
|
|
1049
1375
|
}
|
|
1050
1376
|
if (project.type === "npm") {
|
|
1051
|
-
|
|
1377
|
+
let port = options.port || findAvailablePort();
|
|
1378
|
+
const portCheck = checkPortInUse(port);
|
|
1379
|
+
if (portCheck.inUse) {
|
|
1380
|
+
const processInfo = portCheck.process ? ` by ${portCheck.process}` : "";
|
|
1381
|
+
console.log(chalk3.yellow(`Warning: Port ${port} is already in use${processInfo}`));
|
|
1382
|
+
if (!options.port) {
|
|
1383
|
+
for (let p = port + 1; p <= 9e3; p++) {
|
|
1384
|
+
const check = checkPortInUse(p);
|
|
1385
|
+
if (!check.inUse) {
|
|
1386
|
+
port = p;
|
|
1387
|
+
console.log(chalk3.dim(` Using port ${port} instead`));
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
project.port = port;
|
|
1052
1394
|
project.start = options.start || "npm start";
|
|
1053
1395
|
project.env = { PORT: String(project.port) };
|
|
1054
1396
|
}
|
|
1055
1397
|
}
|
|
1056
1398
|
if (!validateProjectName(project.name)) {
|
|
1057
|
-
console.error(
|
|
1399
|
+
console.error(chalk3.red("Error: Invalid project name"));
|
|
1058
1400
|
process.exit(1);
|
|
1059
1401
|
}
|
|
1060
1402
|
if (!validateHostname(project.hostname)) {
|
|
1061
|
-
console.error(
|
|
1403
|
+
console.error(chalk3.red("Error: Invalid hostname"));
|
|
1062
1404
|
process.exit(1);
|
|
1063
1405
|
}
|
|
1064
|
-
if (!
|
|
1406
|
+
if (!existsSync7(project.path)) {
|
|
1065
1407
|
const createDir = options.name ? true : (await inquirer.prompt([
|
|
1066
1408
|
{
|
|
1067
1409
|
type: "confirm",
|
|
@@ -1072,7 +1414,7 @@ async function newCommand(options) {
|
|
|
1072
1414
|
])).create;
|
|
1073
1415
|
if (createDir) {
|
|
1074
1416
|
mkdirSync3(project.path, { recursive: true });
|
|
1075
|
-
console.log(
|
|
1417
|
+
console.log(chalk3.green(`Created directory: ${project.path}`));
|
|
1076
1418
|
}
|
|
1077
1419
|
}
|
|
1078
1420
|
const validationResult = validateProject(project);
|
|
@@ -1080,7 +1422,7 @@ async function newCommand(options) {
|
|
|
1080
1422
|
console.log("");
|
|
1081
1423
|
printValidationResult(validationResult);
|
|
1082
1424
|
if (!validationResult.valid) {
|
|
1083
|
-
console.log(
|
|
1425
|
+
console.log(chalk3.red("\n\u2717 Cannot add project due to validation errors."));
|
|
1084
1426
|
process.exit(1);
|
|
1085
1427
|
}
|
|
1086
1428
|
const { proceed } = await inquirer.prompt([
|
|
@@ -1092,52 +1434,57 @@ async function newCommand(options) {
|
|
|
1092
1434
|
}
|
|
1093
1435
|
]);
|
|
1094
1436
|
if (!proceed) {
|
|
1095
|
-
console.log(
|
|
1437
|
+
console.log(chalk3.yellow("Aborted."));
|
|
1096
1438
|
process.exit(0);
|
|
1097
1439
|
}
|
|
1098
1440
|
}
|
|
1099
1441
|
try {
|
|
1100
1442
|
addProject(project);
|
|
1101
|
-
console.log(
|
|
1443
|
+
console.log(chalk3.green(`
|
|
1102
1444
|
Project "${project.name}" added successfully!`));
|
|
1103
|
-
if (
|
|
1104
|
-
console.log(
|
|
1105
|
-
|
|
1106
|
-
console.log(chalk2.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
|
|
1107
|
-
console.log(chalk2.dim(`
|
|
1108
|
-
Run ${chalk2.cyan("sudo bindler apply")} to update nginx.`));
|
|
1109
|
-
console.log(chalk2.dim(`Then access at: ${chalk2.cyan(`http://${project.hostname}:8080`)}`));
|
|
1445
|
+
if (options.apply) {
|
|
1446
|
+
console.log("");
|
|
1447
|
+
await applyCommand({});
|
|
1110
1448
|
} else {
|
|
1111
|
-
|
|
1112
|
-
|
|
1449
|
+
if (project.local) {
|
|
1450
|
+
console.log(chalk3.yellow(`
|
|
1451
|
+
Local project - add to /etc/hosts:`));
|
|
1452
|
+
console.log(chalk3.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
|
|
1453
|
+
console.log(chalk3.dim(`
|
|
1454
|
+
Run ${chalk3.cyan("sudo bindler apply")} to update nginx.`));
|
|
1455
|
+
console.log(chalk3.dim(`Then access at: ${chalk3.cyan(`http://${project.hostname}:8080`)}`));
|
|
1456
|
+
} else {
|
|
1457
|
+
console.log(chalk3.dim(`
|
|
1458
|
+
Configuration saved. Run ${chalk3.cyan("sudo bindler apply")} to update nginx and cloudflare.`));
|
|
1459
|
+
}
|
|
1113
1460
|
}
|
|
1114
1461
|
if (project.type === "npm") {
|
|
1115
|
-
console.log(
|
|
1462
|
+
console.log(chalk3.dim(`Run ${chalk3.cyan(`bindler start ${project.name}`)} to start the application.`));
|
|
1116
1463
|
}
|
|
1117
1464
|
} catch (error) {
|
|
1118
|
-
console.error(
|
|
1465
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1119
1466
|
process.exit(1);
|
|
1120
1467
|
}
|
|
1121
1468
|
}
|
|
1122
1469
|
|
|
1123
1470
|
// src/commands/list.ts
|
|
1124
|
-
import
|
|
1471
|
+
import chalk4 from "chalk";
|
|
1125
1472
|
import Table from "cli-table3";
|
|
1126
1473
|
async function listCommand() {
|
|
1127
1474
|
const projects = listProjects();
|
|
1128
1475
|
if (projects.length === 0) {
|
|
1129
|
-
console.log(
|
|
1130
|
-
console.log(
|
|
1476
|
+
console.log(chalk4.yellow("No projects registered."));
|
|
1477
|
+
console.log(chalk4.dim(`Run ${chalk4.cyan("bindler new")} to create one.`));
|
|
1131
1478
|
return;
|
|
1132
1479
|
}
|
|
1133
1480
|
const table = new Table({
|
|
1134
1481
|
head: [
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1482
|
+
chalk4.cyan("Name"),
|
|
1483
|
+
chalk4.cyan("Type"),
|
|
1484
|
+
chalk4.cyan("Hostname"),
|
|
1485
|
+
chalk4.cyan("Port"),
|
|
1486
|
+
chalk4.cyan("Path"),
|
|
1487
|
+
chalk4.cyan("Status")
|
|
1141
1488
|
],
|
|
1142
1489
|
style: {
|
|
1143
1490
|
head: [],
|
|
@@ -1149,15 +1496,15 @@ async function listCommand() {
|
|
|
1149
1496
|
if (project.type === "npm") {
|
|
1150
1497
|
const process2 = getProcessByName(project.name);
|
|
1151
1498
|
if (process2) {
|
|
1152
|
-
status = process2.status === "online" ?
|
|
1499
|
+
status = process2.status === "online" ? chalk4.green("online") : process2.status === "stopped" ? chalk4.yellow("stopped") : chalk4.red(process2.status);
|
|
1153
1500
|
} else {
|
|
1154
|
-
status =
|
|
1501
|
+
status = chalk4.dim("not started");
|
|
1155
1502
|
}
|
|
1156
1503
|
} else {
|
|
1157
|
-
status = project.enabled !== false ?
|
|
1504
|
+
status = project.enabled !== false ? chalk4.green("serving") : chalk4.yellow("disabled");
|
|
1158
1505
|
}
|
|
1159
1506
|
if (project.enabled === false) {
|
|
1160
|
-
status =
|
|
1507
|
+
status = chalk4.yellow("disabled");
|
|
1161
1508
|
}
|
|
1162
1509
|
table.push([
|
|
1163
1510
|
project.name,
|
|
@@ -1169,17 +1516,17 @@ async function listCommand() {
|
|
|
1169
1516
|
]);
|
|
1170
1517
|
}
|
|
1171
1518
|
console.log(table.toString());
|
|
1172
|
-
console.log(
|
|
1519
|
+
console.log(chalk4.dim(`
|
|
1173
1520
|
${projects.length} project(s) registered`));
|
|
1174
1521
|
}
|
|
1175
1522
|
|
|
1176
1523
|
// src/commands/status.ts
|
|
1177
|
-
import
|
|
1524
|
+
import chalk5 from "chalk";
|
|
1178
1525
|
import Table2 from "cli-table3";
|
|
1179
1526
|
async function statusCommand() {
|
|
1180
1527
|
const projects = listProjects();
|
|
1181
1528
|
if (projects.length === 0) {
|
|
1182
|
-
console.log(
|
|
1529
|
+
console.log(chalk5.yellow("No projects registered."));
|
|
1183
1530
|
return;
|
|
1184
1531
|
}
|
|
1185
1532
|
const statuses = [];
|
|
@@ -1203,12 +1550,12 @@ async function statusCommand() {
|
|
|
1203
1550
|
}
|
|
1204
1551
|
const table = new Table2({
|
|
1205
1552
|
head: [
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1553
|
+
chalk5.cyan("Name"),
|
|
1554
|
+
chalk5.cyan("Type"),
|
|
1555
|
+
chalk5.cyan("Hostname"),
|
|
1556
|
+
chalk5.cyan("Port"),
|
|
1557
|
+
chalk5.cyan("PM2 Status"),
|
|
1558
|
+
chalk5.cyan("Port Check")
|
|
1212
1559
|
],
|
|
1213
1560
|
style: {
|
|
1214
1561
|
head: [],
|
|
@@ -1221,25 +1568,25 @@ async function statusCommand() {
|
|
|
1221
1568
|
if (status.type === "npm") {
|
|
1222
1569
|
switch (status.pm2Status) {
|
|
1223
1570
|
case "online":
|
|
1224
|
-
pm2StatusStr =
|
|
1571
|
+
pm2StatusStr = chalk5.green("online");
|
|
1225
1572
|
break;
|
|
1226
1573
|
case "stopped":
|
|
1227
|
-
pm2StatusStr =
|
|
1574
|
+
pm2StatusStr = chalk5.yellow("stopped");
|
|
1228
1575
|
break;
|
|
1229
1576
|
case "errored":
|
|
1230
|
-
pm2StatusStr =
|
|
1577
|
+
pm2StatusStr = chalk5.red("errored");
|
|
1231
1578
|
break;
|
|
1232
1579
|
case "not_managed":
|
|
1233
|
-
pm2StatusStr =
|
|
1580
|
+
pm2StatusStr = chalk5.dim("not started");
|
|
1234
1581
|
break;
|
|
1235
1582
|
default:
|
|
1236
|
-
pm2StatusStr =
|
|
1583
|
+
pm2StatusStr = chalk5.dim(status.pm2Status || "-");
|
|
1237
1584
|
}
|
|
1238
|
-
portCheckStr = status.portListening ?
|
|
1585
|
+
portCheckStr = status.portListening ? chalk5.green("listening") : chalk5.red("not listening");
|
|
1239
1586
|
}
|
|
1240
1587
|
if (status.enabled === false) {
|
|
1241
|
-
pm2StatusStr =
|
|
1242
|
-
portCheckStr =
|
|
1588
|
+
pm2StatusStr = chalk5.yellow("disabled");
|
|
1589
|
+
portCheckStr = chalk5.dim("-");
|
|
1243
1590
|
}
|
|
1244
1591
|
table.push([
|
|
1245
1592
|
status.name,
|
|
@@ -1253,14 +1600,14 @@ async function statusCommand() {
|
|
|
1253
1600
|
console.log(table.toString());
|
|
1254
1601
|
const runningProcesses = getPm2List().filter((p) => p.name.startsWith("bindler:"));
|
|
1255
1602
|
if (runningProcesses.length > 0) {
|
|
1256
|
-
console.log(
|
|
1603
|
+
console.log(chalk5.bold("\nPM2 Process Details:"));
|
|
1257
1604
|
const detailTable = new Table2({
|
|
1258
1605
|
head: [
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1606
|
+
chalk5.cyan("Process"),
|
|
1607
|
+
chalk5.cyan("CPU"),
|
|
1608
|
+
chalk5.cyan("Memory"),
|
|
1609
|
+
chalk5.cyan("Uptime"),
|
|
1610
|
+
chalk5.cyan("Restarts")
|
|
1264
1611
|
],
|
|
1265
1612
|
style: {
|
|
1266
1613
|
head: [],
|
|
@@ -1281,198 +1628,198 @@ async function statusCommand() {
|
|
|
1281
1628
|
}
|
|
1282
1629
|
|
|
1283
1630
|
// src/commands/start.ts
|
|
1284
|
-
import
|
|
1631
|
+
import chalk6 from "chalk";
|
|
1285
1632
|
async function startCommand(name, options) {
|
|
1286
1633
|
if (options.all) {
|
|
1287
1634
|
const projects = listProjects();
|
|
1288
1635
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1289
1636
|
if (npmProjects.length === 0) {
|
|
1290
|
-
console.log(
|
|
1637
|
+
console.log(chalk6.yellow("No npm projects to start."));
|
|
1291
1638
|
return;
|
|
1292
1639
|
}
|
|
1293
|
-
console.log(
|
|
1640
|
+
console.log(chalk6.blue(`Starting ${npmProjects.length} npm project(s)...`));
|
|
1294
1641
|
const results = startAllProjects(npmProjects);
|
|
1295
1642
|
for (const result2 of results) {
|
|
1296
1643
|
if (result2.success) {
|
|
1297
|
-
console.log(
|
|
1644
|
+
console.log(chalk6.green(` \u2713 ${result2.name}`));
|
|
1298
1645
|
} else {
|
|
1299
|
-
console.log(
|
|
1646
|
+
console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1300
1647
|
}
|
|
1301
1648
|
}
|
|
1302
1649
|
const succeeded = results.filter((r) => r.success).length;
|
|
1303
|
-
console.log(
|
|
1650
|
+
console.log(chalk6.dim(`
|
|
1304
1651
|
${succeeded}/${results.length} started successfully`));
|
|
1305
1652
|
return;
|
|
1306
1653
|
}
|
|
1307
1654
|
if (!name) {
|
|
1308
|
-
console.error(
|
|
1655
|
+
console.error(chalk6.red("Error: Project name is required. Use --all to start all projects."));
|
|
1309
1656
|
process.exit(1);
|
|
1310
1657
|
}
|
|
1311
1658
|
const project = getProject(name);
|
|
1312
1659
|
if (!project) {
|
|
1313
|
-
console.error(
|
|
1660
|
+
console.error(chalk6.red(`Error: Project "${name}" not found`));
|
|
1314
1661
|
process.exit(1);
|
|
1315
1662
|
}
|
|
1316
1663
|
if (project.type !== "npm") {
|
|
1317
|
-
console.log(
|
|
1318
|
-
console.log(
|
|
1664
|
+
console.log(chalk6.blue(`Project "${name}" is a static site - no process to start.`));
|
|
1665
|
+
console.log(chalk6.dim("Static sites are served directly by nginx."));
|
|
1319
1666
|
return;
|
|
1320
1667
|
}
|
|
1321
|
-
console.log(
|
|
1668
|
+
console.log(chalk6.blue(`Starting ${name}...`));
|
|
1322
1669
|
const result = startProject(project);
|
|
1323
1670
|
if (result.success) {
|
|
1324
|
-
console.log(
|
|
1325
|
-
console.log(
|
|
1326
|
-
console.log(
|
|
1671
|
+
console.log(chalk6.green(`\u2713 ${name} started successfully`));
|
|
1672
|
+
console.log(chalk6.dim(` Port: ${project.port}`));
|
|
1673
|
+
console.log(chalk6.dim(` URL: https://${project.hostname}`));
|
|
1327
1674
|
} else {
|
|
1328
|
-
console.error(
|
|
1675
|
+
console.error(chalk6.red(`\u2717 Failed to start ${name}: ${result.error}`));
|
|
1329
1676
|
process.exit(1);
|
|
1330
1677
|
}
|
|
1331
1678
|
}
|
|
1332
1679
|
|
|
1333
1680
|
// src/commands/stop.ts
|
|
1334
|
-
import
|
|
1681
|
+
import chalk7 from "chalk";
|
|
1335
1682
|
async function stopCommand(name, options) {
|
|
1336
1683
|
if (options.all) {
|
|
1337
1684
|
const projects = listProjects();
|
|
1338
1685
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1339
1686
|
if (npmProjects.length === 0) {
|
|
1340
|
-
console.log(
|
|
1687
|
+
console.log(chalk7.yellow("No npm projects to stop."));
|
|
1341
1688
|
return;
|
|
1342
1689
|
}
|
|
1343
|
-
console.log(
|
|
1690
|
+
console.log(chalk7.blue(`Stopping ${npmProjects.length} npm project(s)...`));
|
|
1344
1691
|
const results = stopAllProjects(npmProjects);
|
|
1345
1692
|
for (const result2 of results) {
|
|
1346
1693
|
if (result2.success) {
|
|
1347
|
-
console.log(
|
|
1694
|
+
console.log(chalk7.green(` \u2713 ${result2.name}`));
|
|
1348
1695
|
} else {
|
|
1349
|
-
console.log(
|
|
1696
|
+
console.log(chalk7.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1350
1697
|
}
|
|
1351
1698
|
}
|
|
1352
1699
|
const succeeded = results.filter((r) => r.success).length;
|
|
1353
|
-
console.log(
|
|
1700
|
+
console.log(chalk7.dim(`
|
|
1354
1701
|
${succeeded}/${results.length} stopped successfully`));
|
|
1355
1702
|
return;
|
|
1356
1703
|
}
|
|
1357
1704
|
if (!name) {
|
|
1358
|
-
console.error(
|
|
1705
|
+
console.error(chalk7.red("Error: Project name is required. Use --all to stop all projects."));
|
|
1359
1706
|
process.exit(1);
|
|
1360
1707
|
}
|
|
1361
1708
|
const project = getProject(name);
|
|
1362
1709
|
if (!project) {
|
|
1363
|
-
console.error(
|
|
1710
|
+
console.error(chalk7.red(`Error: Project "${name}" not found`));
|
|
1364
1711
|
process.exit(1);
|
|
1365
1712
|
}
|
|
1366
1713
|
if (project.type !== "npm") {
|
|
1367
|
-
console.log(
|
|
1714
|
+
console.log(chalk7.yellow(`Project "${name}" is a static site - no process to stop.`));
|
|
1368
1715
|
return;
|
|
1369
1716
|
}
|
|
1370
|
-
console.log(
|
|
1717
|
+
console.log(chalk7.blue(`Stopping ${name}...`));
|
|
1371
1718
|
const result = stopProject(name);
|
|
1372
1719
|
if (result.success) {
|
|
1373
|
-
console.log(
|
|
1720
|
+
console.log(chalk7.green(`\u2713 ${name} stopped successfully`));
|
|
1374
1721
|
} else {
|
|
1375
|
-
console.error(
|
|
1722
|
+
console.error(chalk7.red(`\u2717 Failed to stop ${name}: ${result.error}`));
|
|
1376
1723
|
process.exit(1);
|
|
1377
1724
|
}
|
|
1378
1725
|
}
|
|
1379
1726
|
|
|
1380
1727
|
// src/commands/restart.ts
|
|
1381
|
-
import
|
|
1728
|
+
import chalk8 from "chalk";
|
|
1382
1729
|
async function restartCommand(name, options) {
|
|
1383
1730
|
if (options.all) {
|
|
1384
1731
|
const projects = listProjects();
|
|
1385
1732
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1386
1733
|
if (npmProjects.length === 0) {
|
|
1387
|
-
console.log(
|
|
1734
|
+
console.log(chalk8.yellow("No npm projects to restart."));
|
|
1388
1735
|
return;
|
|
1389
1736
|
}
|
|
1390
|
-
console.log(
|
|
1737
|
+
console.log(chalk8.blue(`Restarting ${npmProjects.length} npm project(s)...`));
|
|
1391
1738
|
const results = restartAllProjects(npmProjects);
|
|
1392
1739
|
for (const result2 of results) {
|
|
1393
1740
|
if (result2.success) {
|
|
1394
|
-
console.log(
|
|
1741
|
+
console.log(chalk8.green(` \u2713 ${result2.name}`));
|
|
1395
1742
|
} else {
|
|
1396
|
-
console.log(
|
|
1743
|
+
console.log(chalk8.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1397
1744
|
}
|
|
1398
1745
|
}
|
|
1399
1746
|
const succeeded = results.filter((r) => r.success).length;
|
|
1400
|
-
console.log(
|
|
1747
|
+
console.log(chalk8.dim(`
|
|
1401
1748
|
${succeeded}/${results.length} restarted successfully`));
|
|
1402
1749
|
return;
|
|
1403
1750
|
}
|
|
1404
1751
|
if (!name) {
|
|
1405
|
-
console.error(
|
|
1752
|
+
console.error(chalk8.red("Error: Project name is required. Use --all to restart all projects."));
|
|
1406
1753
|
process2.exit(1);
|
|
1407
1754
|
}
|
|
1408
1755
|
const project = getProject(name);
|
|
1409
1756
|
if (!project) {
|
|
1410
|
-
console.error(
|
|
1757
|
+
console.error(chalk8.red(`Error: Project "${name}" not found`));
|
|
1411
1758
|
process2.exit(1);
|
|
1412
1759
|
}
|
|
1413
1760
|
if (project.type !== "npm") {
|
|
1414
|
-
console.log(
|
|
1761
|
+
console.log(chalk8.yellow(`Project "${name}" is a static site - no process to restart.`));
|
|
1415
1762
|
return;
|
|
1416
1763
|
}
|
|
1417
|
-
console.log(
|
|
1764
|
+
console.log(chalk8.blue(`Restarting ${name}...`));
|
|
1418
1765
|
const process2 = getProcessByName(name);
|
|
1419
1766
|
let result;
|
|
1420
1767
|
if (process2) {
|
|
1421
1768
|
result = restartProject(name);
|
|
1422
1769
|
} else {
|
|
1423
|
-
console.log(
|
|
1770
|
+
console.log(chalk8.dim("Process not running, starting..."));
|
|
1424
1771
|
result = startProject(project);
|
|
1425
1772
|
}
|
|
1426
1773
|
if (result.success) {
|
|
1427
|
-
console.log(
|
|
1774
|
+
console.log(chalk8.green(`\u2713 ${name} restarted successfully`));
|
|
1428
1775
|
} else {
|
|
1429
|
-
console.error(
|
|
1776
|
+
console.error(chalk8.red(`\u2717 Failed to restart ${name}: ${result.error}`));
|
|
1430
1777
|
process2.exit(1);
|
|
1431
1778
|
}
|
|
1432
1779
|
}
|
|
1433
1780
|
|
|
1434
1781
|
// src/commands/logs.ts
|
|
1435
|
-
import
|
|
1782
|
+
import chalk9 from "chalk";
|
|
1436
1783
|
async function logsCommand(name, options) {
|
|
1437
1784
|
const project = getProject(name);
|
|
1438
1785
|
if (!project) {
|
|
1439
|
-
console.error(
|
|
1786
|
+
console.error(chalk9.red(`Error: Project "${name}" not found`));
|
|
1440
1787
|
process2.exit(1);
|
|
1441
1788
|
}
|
|
1442
1789
|
if (project.type !== "npm") {
|
|
1443
|
-
console.log(
|
|
1444
|
-
console.log(
|
|
1445
|
-
console.log(
|
|
1446
|
-
console.log(
|
|
1790
|
+
console.log(chalk9.yellow(`Project "${name}" is a static site - no logs available.`));
|
|
1791
|
+
console.log(chalk9.dim("Check nginx access/error logs instead:"));
|
|
1792
|
+
console.log(chalk9.dim(" /var/log/nginx/access.log"));
|
|
1793
|
+
console.log(chalk9.dim(" /var/log/nginx/error.log"));
|
|
1447
1794
|
return;
|
|
1448
1795
|
}
|
|
1449
1796
|
const process2 = getProcessByName(name);
|
|
1450
1797
|
if (!process2) {
|
|
1451
|
-
console.log(
|
|
1452
|
-
console.log(
|
|
1798
|
+
console.log(chalk9.yellow(`Project "${name}" has not been started yet.`));
|
|
1799
|
+
console.log(chalk9.dim(`Run ${chalk9.cyan(`bindler start ${name}`)} first.`));
|
|
1453
1800
|
return;
|
|
1454
1801
|
}
|
|
1455
1802
|
const lines = options.lines || 200;
|
|
1456
1803
|
const follow = options.follow || false;
|
|
1457
1804
|
if (follow) {
|
|
1458
|
-
console.log(
|
|
1805
|
+
console.log(chalk9.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
|
|
1459
1806
|
}
|
|
1460
1807
|
await showLogs(name, follow, lines);
|
|
1461
1808
|
}
|
|
1462
1809
|
|
|
1463
1810
|
// src/commands/update.ts
|
|
1464
|
-
import
|
|
1465
|
-
import { existsSync as
|
|
1811
|
+
import chalk10 from "chalk";
|
|
1812
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1466
1813
|
async function updateCommand(name, options) {
|
|
1467
1814
|
const project = getProject(name);
|
|
1468
1815
|
if (!project) {
|
|
1469
|
-
console.error(
|
|
1816
|
+
console.error(chalk10.red(`Error: Project "${name}" not found`));
|
|
1470
1817
|
process.exit(1);
|
|
1471
1818
|
}
|
|
1472
1819
|
const updates = {};
|
|
1473
1820
|
if (options.hostname) {
|
|
1474
1821
|
if (!validateHostname(options.hostname)) {
|
|
1475
|
-
console.error(
|
|
1822
|
+
console.error(chalk10.red("Error: Invalid hostname format"));
|
|
1476
1823
|
process.exit(1);
|
|
1477
1824
|
}
|
|
1478
1825
|
updates.hostname = options.hostname;
|
|
@@ -1480,11 +1827,11 @@ async function updateCommand(name, options) {
|
|
|
1480
1827
|
if (options.port) {
|
|
1481
1828
|
const port = parseInt(options.port, 10);
|
|
1482
1829
|
if (!validatePort(port)) {
|
|
1483
|
-
console.error(
|
|
1830
|
+
console.error(chalk10.red("Error: Invalid port. Use a number between 1024 and 65535."));
|
|
1484
1831
|
process.exit(1);
|
|
1485
1832
|
}
|
|
1486
1833
|
if (!isPortAvailable(port) && port !== project.port) {
|
|
1487
|
-
console.error(
|
|
1834
|
+
console.error(chalk10.red(`Error: Port ${port} is already in use by another project.`));
|
|
1488
1835
|
process.exit(1);
|
|
1489
1836
|
}
|
|
1490
1837
|
updates.port = port;
|
|
@@ -1494,20 +1841,20 @@ async function updateCommand(name, options) {
|
|
|
1494
1841
|
}
|
|
1495
1842
|
if (options.start) {
|
|
1496
1843
|
if (project.type !== "npm") {
|
|
1497
|
-
console.error(
|
|
1844
|
+
console.error(chalk10.red("Error: Start command only applies to npm projects"));
|
|
1498
1845
|
process.exit(1);
|
|
1499
1846
|
}
|
|
1500
1847
|
updates.start = options.start;
|
|
1501
1848
|
}
|
|
1502
1849
|
if (options.path) {
|
|
1503
|
-
if (!
|
|
1504
|
-
console.error(
|
|
1850
|
+
if (!existsSync8(options.path)) {
|
|
1851
|
+
console.error(chalk10.red(`Error: Path does not exist: ${options.path}`));
|
|
1505
1852
|
process.exit(1);
|
|
1506
1853
|
}
|
|
1507
1854
|
if (isProtectedPath(options.path)) {
|
|
1508
|
-
console.error(
|
|
1509
|
-
console.error(
|
|
1510
|
-
console.error(
|
|
1855
|
+
console.error(chalk10.red(`Error: Path is in a macOS protected folder (Desktop/Documents/Downloads).`));
|
|
1856
|
+
console.error(chalk10.yellow(`Nginx cannot access these folders without Full Disk Access.`));
|
|
1857
|
+
console.error(chalk10.dim(`Move your project to ~/projects or another accessible location.`));
|
|
1511
1858
|
process.exit(1);
|
|
1512
1859
|
}
|
|
1513
1860
|
updates.path = options.path;
|
|
@@ -1517,7 +1864,7 @@ async function updateCommand(name, options) {
|
|
|
1517
1864
|
for (const envStr of options.env) {
|
|
1518
1865
|
const [key, ...valueParts] = envStr.split("=");
|
|
1519
1866
|
if (!key) {
|
|
1520
|
-
console.error(
|
|
1867
|
+
console.error(chalk10.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
|
|
1521
1868
|
process.exit(1);
|
|
1522
1869
|
}
|
|
1523
1870
|
env[key] = valueParts.join("=");
|
|
@@ -1530,23 +1877,23 @@ async function updateCommand(name, options) {
|
|
|
1530
1877
|
updates.enabled = false;
|
|
1531
1878
|
}
|
|
1532
1879
|
if (Object.keys(updates).length === 0) {
|
|
1533
|
-
console.log(
|
|
1534
|
-
console.log(
|
|
1880
|
+
console.log(chalk10.yellow("No updates specified."));
|
|
1881
|
+
console.log(chalk10.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
|
|
1535
1882
|
return;
|
|
1536
1883
|
}
|
|
1537
1884
|
try {
|
|
1538
1885
|
updateProject(name, updates);
|
|
1539
|
-
console.log(
|
|
1886
|
+
console.log(chalk10.green(`\u2713 Project "${name}" updated successfully`));
|
|
1540
1887
|
for (const [key, value] of Object.entries(updates)) {
|
|
1541
|
-
console.log(
|
|
1888
|
+
console.log(chalk10.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
|
|
1542
1889
|
}
|
|
1543
|
-
console.log(
|
|
1544
|
-
Run ${
|
|
1890
|
+
console.log(chalk10.dim(`
|
|
1891
|
+
Run ${chalk10.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1545
1892
|
if (project.type === "npm" && (updates.port || updates.start || updates.env)) {
|
|
1546
|
-
console.log(
|
|
1893
|
+
console.log(chalk10.dim(`Run ${chalk10.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
|
|
1547
1894
|
}
|
|
1548
1895
|
} catch (error) {
|
|
1549
|
-
console.error(
|
|
1896
|
+
console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1550
1897
|
process.exit(1);
|
|
1551
1898
|
}
|
|
1552
1899
|
}
|
|
@@ -1555,20 +1902,20 @@ Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
|
1555
1902
|
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1556
1903
|
import { tmpdir } from "os";
|
|
1557
1904
|
import { join as join6 } from "path";
|
|
1558
|
-
import
|
|
1905
|
+
import chalk11 from "chalk";
|
|
1559
1906
|
async function editCommand(name) {
|
|
1560
1907
|
const project = getProject(name);
|
|
1561
1908
|
if (!project) {
|
|
1562
|
-
console.error(
|
|
1909
|
+
console.error(chalk11.red(`Error: Project "${name}" not found`));
|
|
1563
1910
|
process.exit(1);
|
|
1564
1911
|
}
|
|
1565
1912
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1566
1913
|
const tmpFile = join6(tmpdir(), `bindler-${name}-${Date.now()}.json`);
|
|
1567
1914
|
writeFileSync4(tmpFile, JSON.stringify(project, null, 2) + "\n");
|
|
1568
|
-
console.log(
|
|
1915
|
+
console.log(chalk11.dim(`Opening ${name} config in ${editor}...`));
|
|
1569
1916
|
const exitCode = await spawnInteractive(editor, [tmpFile]);
|
|
1570
1917
|
if (exitCode !== 0) {
|
|
1571
|
-
console.error(
|
|
1918
|
+
console.error(chalk11.red("Editor exited with error"));
|
|
1572
1919
|
unlinkSync(tmpFile);
|
|
1573
1920
|
process.exit(1);
|
|
1574
1921
|
}
|
|
@@ -1576,7 +1923,7 @@ async function editCommand(name) {
|
|
|
1576
1923
|
try {
|
|
1577
1924
|
editedContent = readFileSync4(tmpFile, "utf-8");
|
|
1578
1925
|
} catch (error) {
|
|
1579
|
-
console.error(
|
|
1926
|
+
console.error(chalk11.red("Failed to read edited file"));
|
|
1580
1927
|
process.exit(1);
|
|
1581
1928
|
} finally {
|
|
1582
1929
|
unlinkSync(tmpFile);
|
|
@@ -1585,44 +1932,44 @@ async function editCommand(name) {
|
|
|
1585
1932
|
try {
|
|
1586
1933
|
editedProject = JSON.parse(editedContent);
|
|
1587
1934
|
} catch (error) {
|
|
1588
|
-
console.error(
|
|
1935
|
+
console.error(chalk11.red("Error: Invalid JSON in edited file"));
|
|
1589
1936
|
process.exit(1);
|
|
1590
1937
|
}
|
|
1591
1938
|
if (editedProject.name !== project.name) {
|
|
1592
|
-
console.error(
|
|
1939
|
+
console.error(chalk11.red("Error: Cannot change project name via edit. Use a new project instead."));
|
|
1593
1940
|
process.exit(1);
|
|
1594
1941
|
}
|
|
1595
1942
|
const originalStr = JSON.stringify(project);
|
|
1596
1943
|
const editedStr = JSON.stringify(editedProject);
|
|
1597
1944
|
if (originalStr === editedStr) {
|
|
1598
|
-
console.log(
|
|
1945
|
+
console.log(chalk11.yellow("No changes made."));
|
|
1599
1946
|
return;
|
|
1600
1947
|
}
|
|
1601
1948
|
try {
|
|
1602
1949
|
const config = readConfig();
|
|
1603
1950
|
const index = config.projects.findIndex((p) => p.name === name);
|
|
1604
1951
|
if (index === -1) {
|
|
1605
|
-
console.error(
|
|
1952
|
+
console.error(chalk11.red("Error: Project not found"));
|
|
1606
1953
|
process.exit(1);
|
|
1607
1954
|
}
|
|
1608
1955
|
config.projects[index] = editedProject;
|
|
1609
1956
|
writeConfig(config);
|
|
1610
|
-
console.log(
|
|
1611
|
-
console.log(
|
|
1612
|
-
Run ${
|
|
1957
|
+
console.log(chalk11.green(`\u2713 Project "${name}" updated successfully`));
|
|
1958
|
+
console.log(chalk11.dim(`
|
|
1959
|
+
Run ${chalk11.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1613
1960
|
} catch (error) {
|
|
1614
|
-
console.error(
|
|
1961
|
+
console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1615
1962
|
process.exit(1);
|
|
1616
1963
|
}
|
|
1617
1964
|
}
|
|
1618
1965
|
|
|
1619
1966
|
// src/commands/remove.ts
|
|
1620
1967
|
import inquirer2 from "inquirer";
|
|
1621
|
-
import
|
|
1968
|
+
import chalk12 from "chalk";
|
|
1622
1969
|
async function removeCommand(name, options) {
|
|
1623
1970
|
const project = getProject(name);
|
|
1624
1971
|
if (!project) {
|
|
1625
|
-
console.error(
|
|
1972
|
+
console.error(chalk12.red(`Error: Project "${name}" not found`));
|
|
1626
1973
|
process.exit(1);
|
|
1627
1974
|
}
|
|
1628
1975
|
if (!options.force) {
|
|
@@ -1635,318 +1982,51 @@ async function removeCommand(name, options) {
|
|
|
1635
1982
|
}
|
|
1636
1983
|
]);
|
|
1637
1984
|
if (!confirm) {
|
|
1638
|
-
console.log(
|
|
1985
|
+
console.log(chalk12.yellow("Cancelled."));
|
|
1639
1986
|
return;
|
|
1640
1987
|
}
|
|
1641
1988
|
}
|
|
1642
1989
|
if (project.type === "npm") {
|
|
1643
1990
|
const process2 = getProcessByName(name);
|
|
1644
1991
|
if (process2) {
|
|
1645
|
-
console.log(
|
|
1992
|
+
console.log(chalk12.dim("Stopping PM2 process..."));
|
|
1646
1993
|
deleteProject(name);
|
|
1647
1994
|
}
|
|
1648
1995
|
}
|
|
1649
1996
|
try {
|
|
1650
1997
|
removeProject(name);
|
|
1651
|
-
console.log(
|
|
1998
|
+
console.log(chalk12.green(`\u2713 Project "${name}" removed from registry`));
|
|
1652
1999
|
if (options.apply) {
|
|
1653
|
-
console.log(
|
|
2000
|
+
console.log(chalk12.dim("\nApplying nginx configuration..."));
|
|
1654
2001
|
const config = readConfig();
|
|
1655
2002
|
try {
|
|
1656
2003
|
writeNginxConfig(config);
|
|
1657
2004
|
const testResult = testNginxConfig();
|
|
1658
2005
|
if (testResult.success) {
|
|
1659
2006
|
reloadNginx();
|
|
1660
|
-
console.log(
|
|
2007
|
+
console.log(chalk12.green("\u2713 Nginx configuration updated"));
|
|
1661
2008
|
} else {
|
|
1662
|
-
console.log(
|
|
2009
|
+
console.log(chalk12.yellow("! Nginx config test failed, reload skipped"));
|
|
1663
2010
|
}
|
|
1664
2011
|
} catch (err) {
|
|
1665
|
-
console.log(
|
|
1666
|
-
console.log(
|
|
1667
|
-
}
|
|
1668
|
-
} else {
|
|
1669
|
-
console.log(chalk11.dim(`
|
|
1670
|
-
Run ${chalk11.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1671
|
-
}
|
|
1672
|
-
console.log(chalk11.yellow("\nNote: Project files were not deleted."));
|
|
1673
|
-
console.log(chalk11.dim(` Path: ${project.path}`));
|
|
1674
|
-
if (!project.local) {
|
|
1675
|
-
console.log(chalk11.yellow("\nCloudflare DNS route was not removed."));
|
|
1676
|
-
console.log(chalk11.dim(" Remove it manually from the Cloudflare dashboard:"));
|
|
1677
|
-
console.log(chalk11.dim(" https://dash.cloudflare.com \u2192 DNS \u2192 Records \u2192 Delete the CNAME for " + project.hostname));
|
|
1678
|
-
}
|
|
1679
|
-
} catch (error) {
|
|
1680
|
-
console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1681
|
-
process.exit(1);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
// src/commands/apply.ts
|
|
1686
|
-
import chalk12 from "chalk";
|
|
1687
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1688
|
-
|
|
1689
|
-
// src/lib/cloudflare.ts
|
|
1690
|
-
function isCloudflaredInstalled() {
|
|
1691
|
-
const result = execCommandSafe("which cloudflared");
|
|
1692
|
-
return result.success;
|
|
1693
|
-
}
|
|
1694
|
-
function getCloudflaredVersion() {
|
|
1695
|
-
const result = execCommandSafe("cloudflared --version");
|
|
1696
|
-
if (result.success) {
|
|
1697
|
-
const match = result.output.match(/cloudflared version (\S+)/);
|
|
1698
|
-
return match ? match[1] : result.output;
|
|
1699
|
-
}
|
|
1700
|
-
return null;
|
|
1701
|
-
}
|
|
1702
|
-
function listTunnels() {
|
|
1703
|
-
const result = execCommandSafe("cloudflared tunnel list --output json");
|
|
1704
|
-
if (!result.success) {
|
|
1705
|
-
return [];
|
|
1706
|
-
}
|
|
1707
|
-
try {
|
|
1708
|
-
const tunnels = JSON.parse(result.output);
|
|
1709
|
-
return tunnels.map((t) => ({
|
|
1710
|
-
id: t.id,
|
|
1711
|
-
name: t.name,
|
|
1712
|
-
createdAt: t.created_at
|
|
1713
|
-
}));
|
|
1714
|
-
} catch {
|
|
1715
|
-
return [];
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
function getTunnelByName(name) {
|
|
1719
|
-
const tunnels = listTunnels();
|
|
1720
|
-
const tunnel = tunnels.find((t) => t.name === name);
|
|
1721
|
-
return tunnel ? { id: tunnel.id, name: tunnel.name } : null;
|
|
1722
|
-
}
|
|
1723
|
-
function routeDns(tunnelName, hostname) {
|
|
1724
|
-
const result = execCommandSafe(`cloudflared tunnel route dns "${tunnelName}" "${hostname}"`);
|
|
1725
|
-
if (!result.success) {
|
|
1726
|
-
if (result.error?.includes("already exists") || result.output?.includes("already exists")) {
|
|
1727
|
-
return { success: true, output: "DNS route already exists" };
|
|
1728
|
-
}
|
|
1729
|
-
return { success: false, error: result.error };
|
|
1730
|
-
}
|
|
1731
|
-
return { success: true, output: result.output };
|
|
1732
|
-
}
|
|
1733
|
-
function routeDnsForAllProjects() {
|
|
1734
|
-
const config = readConfig();
|
|
1735
|
-
const { tunnelName, applyCloudflareDnsRoutes } = config.defaults;
|
|
1736
|
-
if (!applyCloudflareDnsRoutes) {
|
|
1737
|
-
return [];
|
|
1738
|
-
}
|
|
1739
|
-
const results = [];
|
|
1740
|
-
for (const project of config.projects) {
|
|
1741
|
-
if (project.enabled === false) {
|
|
1742
|
-
continue;
|
|
1743
|
-
}
|
|
1744
|
-
if (project.local) {
|
|
1745
|
-
results.push({
|
|
1746
|
-
hostname: project.hostname,
|
|
1747
|
-
success: true,
|
|
1748
|
-
skipped: true,
|
|
1749
|
-
output: "Local project - skipped"
|
|
1750
|
-
});
|
|
1751
|
-
continue;
|
|
1752
|
-
}
|
|
1753
|
-
const result = routeDns(tunnelName, project.hostname);
|
|
1754
|
-
results.push({
|
|
1755
|
-
hostname: project.hostname,
|
|
1756
|
-
...result
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
return results;
|
|
1760
|
-
}
|
|
1761
|
-
function isTunnelRunning(tunnelName) {
|
|
1762
|
-
const result = execCommandSafe(`pgrep -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
1763
|
-
return result.success;
|
|
1764
|
-
}
|
|
1765
|
-
function getTunnelInfo(tunnelName) {
|
|
1766
|
-
const tunnel = getTunnelByName(tunnelName);
|
|
1767
|
-
if (!tunnel) {
|
|
1768
|
-
return { exists: false, running: false };
|
|
1769
|
-
}
|
|
1770
|
-
return {
|
|
1771
|
-
exists: true,
|
|
1772
|
-
running: isTunnelRunning(tunnelName),
|
|
1773
|
-
id: tunnel.id
|
|
1774
|
-
};
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// src/commands/apply.ts
|
|
1778
|
-
async function applyCommand(options) {
|
|
1779
|
-
let config = readConfig();
|
|
1780
|
-
const defaults = getDefaults();
|
|
1781
|
-
if (options.sync) {
|
|
1782
|
-
console.log(chalk12.dim("Syncing bindler.yaml from project directories...\n"));
|
|
1783
|
-
let synced = 0;
|
|
1784
|
-
for (const project of config.projects) {
|
|
1785
|
-
if (!existsSync8(project.path)) continue;
|
|
1786
|
-
const yamlConfig = readBindlerYaml(project.path);
|
|
1787
|
-
if (yamlConfig) {
|
|
1788
|
-
const merged = mergeYamlWithProject(project, yamlConfig);
|
|
1789
|
-
updateProject(project.name, merged);
|
|
1790
|
-
console.log(chalk12.green(` \u2713 Synced ${project.name} from bindler.yaml`));
|
|
1791
|
-
synced++;
|
|
2012
|
+
console.log(chalk12.yellow(`! Failed to update nginx: ${err}`));
|
|
2013
|
+
console.log(chalk12.dim(" Try running: sudo bindler apply"));
|
|
1792
2014
|
}
|
|
1793
|
-
}
|
|
1794
|
-
if (synced === 0) {
|
|
1795
|
-
console.log(chalk12.dim(" No bindler.yaml files found in project directories"));
|
|
1796
2015
|
} else {
|
|
1797
2016
|
console.log(chalk12.dim(`
|
|
1798
|
-
|
|
1799
|
-
`));
|
|
1800
|
-
}
|
|
1801
|
-
config = readConfig();
|
|
1802
|
-
}
|
|
1803
|
-
if (options.env) {
|
|
1804
|
-
console.log(chalk12.dim(`Using ${options.env} environment configuration...
|
|
1805
|
-
`));
|
|
1806
|
-
const envProjects = listProjectsForEnv(options.env);
|
|
1807
|
-
config = { ...config, projects: envProjects };
|
|
1808
|
-
}
|
|
1809
|
-
const hasProjects = config.projects.length > 0;
|
|
1810
|
-
console.log(chalk12.blue("Applying configuration...\n"));
|
|
1811
|
-
if (hasProjects && !options.skipChecks) {
|
|
1812
|
-
console.log(chalk12.dim("Running preflight checks..."));
|
|
1813
|
-
const checkResult = runPreflightChecks(config);
|
|
1814
|
-
if (!checkResult.valid) {
|
|
1815
|
-
printValidationResult(checkResult);
|
|
1816
|
-
console.log(chalk12.red("\n\u2717 Preflight checks failed. Fix the errors above before applying."));
|
|
1817
|
-
console.log(chalk12.dim(" Use --skip-checks to bypass (not recommended)"));
|
|
1818
|
-
process.exit(1);
|
|
2017
|
+
Run ${chalk12.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1819
2018
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
console.log(chalk12.
|
|
2019
|
+
console.log(chalk12.yellow("\nNote: Project files were not deleted."));
|
|
2020
|
+
console.log(chalk12.dim(` Path: ${project.path}`));
|
|
2021
|
+
if (!project.local) {
|
|
2022
|
+
console.log(chalk12.yellow("\nCloudflare DNS route was not removed."));
|
|
2023
|
+
console.log(chalk12.dim(" Remove it manually from the Cloudflare dashboard:"));
|
|
2024
|
+
console.log(chalk12.dim(" https://dash.cloudflare.com \u2192 DNS \u2192 Records \u2192 Delete the CNAME for " + project.hostname));
|
|
1825
2025
|
}
|
|
1826
|
-
}
|
|
1827
|
-
console.log(chalk12.dim("Generating nginx configuration..."));
|
|
1828
|
-
if (options.dryRun) {
|
|
1829
|
-
const nginxConfig = generateNginxConfig(config);
|
|
1830
|
-
console.log(chalk12.cyan("\n--- Generated nginx config (dry-run) ---\n"));
|
|
1831
|
-
console.log(nginxConfig);
|
|
1832
|
-
console.log(chalk12.cyan("--- End of config ---\n"));
|
|
1833
|
-
console.log(chalk12.yellow("Dry run mode - no changes were made."));
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
try {
|
|
1837
|
-
const { path, content } = writeNginxConfig(config);
|
|
1838
|
-
console.log(chalk12.green(` \u2713 Wrote nginx config to ${path}`));
|
|
1839
2026
|
} catch (error) {
|
|
1840
|
-
|
|
1841
|
-
console.error(chalk12.red(` \u2717 Failed to write nginx config: ${errMsg}`));
|
|
1842
|
-
if (errMsg.includes("EACCES") || errMsg.includes("permission denied")) {
|
|
1843
|
-
console.log(chalk12.yellow(`
|
|
1844
|
-
Try running with sudo: ${chalk12.cyan("sudo bindler apply")}`));
|
|
1845
|
-
}
|
|
1846
|
-
process.exit(1);
|
|
1847
|
-
}
|
|
1848
|
-
const authProjects = config.projects.filter(
|
|
1849
|
-
(p) => p.security?.basicAuth?.enabled && p.security.basicAuth.users?.length
|
|
1850
|
-
);
|
|
1851
|
-
if (authProjects.length > 0) {
|
|
1852
|
-
console.log(chalk12.dim("Generating htpasswd files..."));
|
|
1853
|
-
try {
|
|
1854
|
-
generateHtpasswdFiles(config.projects);
|
|
1855
|
-
console.log(chalk12.green(` \u2713 Generated htpasswd files for ${authProjects.length} project(s)`));
|
|
1856
|
-
} catch (error) {
|
|
1857
|
-
console.log(chalk12.yellow(` ! Failed to generate htpasswd files: ${error}`));
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
console.log(chalk12.dim("Testing nginx configuration..."));
|
|
1861
|
-
const testResult = testNginxConfig();
|
|
1862
|
-
if (!testResult.success) {
|
|
1863
|
-
console.error(chalk12.red(" \u2717 Nginx configuration test failed:"));
|
|
1864
|
-
console.error(chalk12.red(testResult.output));
|
|
1865
|
-
console.log(chalk12.yellow("\nConfiguration was written but nginx was NOT reloaded."));
|
|
1866
|
-
console.log(chalk12.dim("Fix the configuration and run `sudo bindler apply` again."));
|
|
2027
|
+
console.error(chalk12.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1867
2028
|
process.exit(1);
|
|
1868
2029
|
}
|
|
1869
|
-
console.log(chalk12.green(" \u2713 Nginx configuration test passed"));
|
|
1870
|
-
if (!options.noReload) {
|
|
1871
|
-
console.log(chalk12.dim("Reloading nginx..."));
|
|
1872
|
-
const reloadResult = reloadNginx();
|
|
1873
|
-
if (!reloadResult.success) {
|
|
1874
|
-
console.error(chalk12.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
|
|
1875
|
-
console.log(chalk12.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
|
|
1876
|
-
process.exit(1);
|
|
1877
|
-
}
|
|
1878
|
-
console.log(chalk12.green(" \u2713 Nginx reloaded successfully"));
|
|
1879
|
-
} else {
|
|
1880
|
-
console.log(chalk12.yellow(" - Skipped nginx reload (--no-reload)"));
|
|
1881
|
-
}
|
|
1882
|
-
const isDirectMode = defaults.mode === "direct";
|
|
1883
|
-
if (!hasProjects) {
|
|
1884
|
-
} else if (isDirectMode) {
|
|
1885
|
-
console.log(chalk12.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
|
|
1886
|
-
} else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
|
|
1887
|
-
console.log(chalk12.dim("\nConfiguring Cloudflare DNS routes..."));
|
|
1888
|
-
if (!isCloudflaredInstalled()) {
|
|
1889
|
-
console.log(chalk12.yellow(" - cloudflared not installed, skipping DNS routes"));
|
|
1890
|
-
} else {
|
|
1891
|
-
const dnsResults = routeDnsForAllProjects();
|
|
1892
|
-
if (dnsResults.length === 0) {
|
|
1893
|
-
console.log(chalk12.dim(" No hostnames to route"));
|
|
1894
|
-
} else {
|
|
1895
|
-
for (const result of dnsResults) {
|
|
1896
|
-
if (result.skipped) {
|
|
1897
|
-
console.log(chalk12.dim(` - ${result.hostname} (local - skipped)`));
|
|
1898
|
-
} else if (result.success) {
|
|
1899
|
-
const msg = result.output?.includes("already exists") ? "exists" : "routed";
|
|
1900
|
-
console.log(chalk12.green(` \u2713 ${result.hostname} (${msg})`));
|
|
1901
|
-
} else {
|
|
1902
|
-
console.log(chalk12.red(` \u2717 ${result.hostname}: ${result.error}`));
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
} else if (options.noCloudflare) {
|
|
1908
|
-
console.log(chalk12.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
|
|
1909
|
-
}
|
|
1910
|
-
if (hasProjects && isDirectMode && defaults.sslEnabled && options.ssl !== false) {
|
|
1911
|
-
console.log(chalk12.dim("\nSetting up SSL certificates..."));
|
|
1912
|
-
const hostnames = config.projects.filter((p) => p.enabled !== false && !p.local).map((p) => p.hostname);
|
|
1913
|
-
if (hostnames.length === 0) {
|
|
1914
|
-
console.log(chalk12.dim(" No hostnames to secure"));
|
|
1915
|
-
} else {
|
|
1916
|
-
const certbotResult = execCommandSafe("which certbot");
|
|
1917
|
-
if (!certbotResult.success) {
|
|
1918
|
-
console.log(chalk12.yellow(" - certbot not installed, skipping SSL"));
|
|
1919
|
-
console.log(chalk12.dim(" Run: bindler setup --direct"));
|
|
1920
|
-
} else {
|
|
1921
|
-
for (const hostname of hostnames) {
|
|
1922
|
-
console.log(chalk12.dim(` Requesting certificate for ${hostname}...`));
|
|
1923
|
-
const email = defaults.sslEmail || "admin@" + hostname.split(".").slice(-2).join(".");
|
|
1924
|
-
const result = execCommandSafe(
|
|
1925
|
-
`sudo certbot --nginx -d ${hostname} --non-interactive --agree-tos --email ${email} 2>&1`
|
|
1926
|
-
);
|
|
1927
|
-
if (result.success || result.output?.includes("Certificate not yet due for renewal")) {
|
|
1928
|
-
console.log(chalk12.green(` \u2713 ${hostname} (secured)`));
|
|
1929
|
-
} else if (result.output?.includes("already exists")) {
|
|
1930
|
-
console.log(chalk12.green(` \u2713 ${hostname} (exists)`));
|
|
1931
|
-
} else {
|
|
1932
|
-
console.log(chalk12.yellow(` ! ${hostname}: ${result.error || "failed"}`));
|
|
1933
|
-
console.log(chalk12.dim(" Run manually: sudo certbot --nginx -d " + hostname));
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
console.log(chalk12.green("\n\u2713 Configuration applied successfully!"));
|
|
1940
|
-
if (hasProjects) {
|
|
1941
|
-
console.log(chalk12.dim(`
|
|
1942
|
-
${config.projects.length} project(s) configured:`));
|
|
1943
|
-
for (const project of config.projects) {
|
|
1944
|
-
const status = project.enabled !== false ? chalk12.green("enabled") : chalk12.yellow("disabled");
|
|
1945
|
-
console.log(chalk12.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
|
|
1946
|
-
}
|
|
1947
|
-
} else {
|
|
1948
|
-
console.log(chalk12.dim("\nNo projects configured. Nginx config cleared."));
|
|
1949
|
-
}
|
|
1950
2030
|
}
|
|
1951
2031
|
|
|
1952
2032
|
// src/commands/doctor.ts
|
|
@@ -2258,7 +2338,7 @@ async function infoCommand() {
|
|
|
2258
2338
|
`));
|
|
2259
2339
|
console.log(chalk15.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
2260
2340
|
console.log(chalk15.white(" with Nginx and PM2\n"));
|
|
2261
|
-
console.log(chalk15.dim(" Version: ") + chalk15.white("1.
|
|
2341
|
+
console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.1"));
|
|
2262
2342
|
console.log(chalk15.dim(" Author: ") + chalk15.white("alfaoz"));
|
|
2263
2343
|
console.log(chalk15.dim(" License: ") + chalk15.white("MIT"));
|
|
2264
2344
|
console.log(chalk15.dim(" GitHub: ") + chalk15.cyan("https://github.com/alfaoz/bindler"));
|
|
@@ -2723,12 +2803,18 @@ async function initCommand() {
|
|
|
2723
2803
|
}
|
|
2724
2804
|
}
|
|
2725
2805
|
console.log(chalk18.bold("\n1. Choose your setup:\n"));
|
|
2806
|
+
const hasCloudflared = isCloudflaredInstalled();
|
|
2807
|
+
const defaultMode = hasCloudflared ? "tunnel" : "direct";
|
|
2808
|
+
if (!hasCloudflared) {
|
|
2809
|
+
console.log(chalk18.dim(" cloudflared not detected - suggesting direct mode for VPS\n"));
|
|
2810
|
+
}
|
|
2726
2811
|
const { mode } = await inquirer4.prompt([
|
|
2727
2812
|
{
|
|
2728
2813
|
type: "list",
|
|
2729
2814
|
name: "mode",
|
|
2730
2815
|
message: "How will you expose your projects?",
|
|
2731
|
-
|
|
2816
|
+
default: defaultMode,
|
|
2817
|
+
choices: hasCloudflared ? [
|
|
2732
2818
|
{
|
|
2733
2819
|
name: "Cloudflare Tunnel (recommended for home servers)",
|
|
2734
2820
|
value: "tunnel"
|
|
@@ -2741,6 +2827,19 @@ async function initCommand() {
|
|
|
2741
2827
|
name: "Local only (development, no internet access)",
|
|
2742
2828
|
value: "local"
|
|
2743
2829
|
}
|
|
2830
|
+
] : [
|
|
2831
|
+
{
|
|
2832
|
+
name: "Direct (VPS with public IP, port 80/443) - recommended",
|
|
2833
|
+
value: "direct"
|
|
2834
|
+
},
|
|
2835
|
+
{
|
|
2836
|
+
name: "Cloudflare Tunnel (requires cloudflared)",
|
|
2837
|
+
value: "tunnel"
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
name: "Local only (development, no internet access)",
|
|
2841
|
+
value: "local"
|
|
2842
|
+
}
|
|
2744
2843
|
]
|
|
2745
2844
|
}
|
|
2746
2845
|
]);
|
|
@@ -3846,8 +3945,89 @@ Process exited with code ${code}`));
|
|
|
3846
3945
|
});
|
|
3847
3946
|
}
|
|
3848
3947
|
|
|
3849
|
-
// src/
|
|
3948
|
+
// src/commands/config.ts
|
|
3850
3949
|
import chalk29 from "chalk";
|
|
3950
|
+
var VALID_KEYS = [
|
|
3951
|
+
"nginxListen",
|
|
3952
|
+
"projectsRoot",
|
|
3953
|
+
"tunnelName",
|
|
3954
|
+
"mode",
|
|
3955
|
+
"applyCloudflareDnsRoutes",
|
|
3956
|
+
"sslEnabled",
|
|
3957
|
+
"sslEmail"
|
|
3958
|
+
];
|
|
3959
|
+
async function configCommand(action, key, value) {
|
|
3960
|
+
if (!action || action === "list") {
|
|
3961
|
+
const defaults = getDefaults();
|
|
3962
|
+
console.log(chalk29.blue("Current configuration:\n"));
|
|
3963
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
3964
|
+
console.log(` ${chalk29.cyan(k)}: ${chalk29.white(String(v))}`);
|
|
3965
|
+
}
|
|
3966
|
+
console.log(chalk29.dim("\nUse `bindler config set <key> <value>` to change a setting."));
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
if (action === "get") {
|
|
3970
|
+
if (!key) {
|
|
3971
|
+
console.error(chalk29.red("Usage: bindler config get <key>"));
|
|
3972
|
+
console.log(chalk29.dim(`
|
|
3973
|
+
Available keys: ${VALID_KEYS.join(", ")}`));
|
|
3974
|
+
process.exit(1);
|
|
3975
|
+
}
|
|
3976
|
+
const defaults = getDefaults();
|
|
3977
|
+
const configKey = key;
|
|
3978
|
+
if (!(configKey in defaults)) {
|
|
3979
|
+
console.error(chalk29.red(`Unknown config key: ${key}`));
|
|
3980
|
+
console.log(chalk29.dim(`
|
|
3981
|
+
Available keys: ${VALID_KEYS.join(", ")}`));
|
|
3982
|
+
process.exit(1);
|
|
3983
|
+
}
|
|
3984
|
+
console.log(defaults[configKey]);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
if (action === "set") {
|
|
3988
|
+
if (!key || value === void 0) {
|
|
3989
|
+
console.error(chalk29.red("Usage: bindler config set <key> <value>"));
|
|
3990
|
+
console.log(chalk29.dim(`
|
|
3991
|
+
Available keys: ${VALID_KEYS.join(", ")}`));
|
|
3992
|
+
console.log(chalk29.dim("\nExamples:"));
|
|
3993
|
+
console.log(chalk29.dim(" bindler config set nginxListen 8080"));
|
|
3994
|
+
console.log(chalk29.dim(" bindler config set mode tunnel"));
|
|
3995
|
+
process.exit(1);
|
|
3996
|
+
}
|
|
3997
|
+
const configKey = key;
|
|
3998
|
+
if (!VALID_KEYS.includes(configKey)) {
|
|
3999
|
+
console.error(chalk29.red(`Unknown config key: ${key}`));
|
|
4000
|
+
console.log(chalk29.dim(`
|
|
4001
|
+
Available keys: ${VALID_KEYS.join(", ")}`));
|
|
4002
|
+
process.exit(1);
|
|
4003
|
+
}
|
|
4004
|
+
const config = readConfig();
|
|
4005
|
+
let parsedValue = value;
|
|
4006
|
+
if (configKey === "applyCloudflareDnsRoutes" || configKey === "sslEnabled") {
|
|
4007
|
+
parsedValue = value === "true" || value === "1";
|
|
4008
|
+
} else if (configKey === "mode") {
|
|
4009
|
+
if (value !== "tunnel" && value !== "direct") {
|
|
4010
|
+
console.error(chalk29.red('Mode must be "tunnel" or "direct"'));
|
|
4011
|
+
process.exit(1);
|
|
4012
|
+
}
|
|
4013
|
+
parsedValue = value;
|
|
4014
|
+
}
|
|
4015
|
+
config.defaults[configKey] = parsedValue;
|
|
4016
|
+
writeConfig(config);
|
|
4017
|
+
console.log(chalk29.green(`\u2713 Set ${key} = ${parsedValue}`));
|
|
4018
|
+
console.log(chalk29.dim("\nRun `bindler apply` to apply changes."));
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
console.error(chalk29.red(`Unknown action: ${action}`));
|
|
4022
|
+
console.log(chalk29.dim("\nUsage:"));
|
|
4023
|
+
console.log(chalk29.dim(" bindler config # list all settings"));
|
|
4024
|
+
console.log(chalk29.dim(" bindler config get <key> # get a setting"));
|
|
4025
|
+
console.log(chalk29.dim(" bindler config set <key> <value> # set a setting"));
|
|
4026
|
+
process.exit(1);
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
// src/lib/update-check.ts
|
|
4030
|
+
import chalk30 from "chalk";
|
|
3851
4031
|
import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
3852
4032
|
import { join as join9 } from "path";
|
|
3853
4033
|
import { homedir as homedir4 } from "os";
|
|
@@ -3900,28 +4080,28 @@ async function checkForUpdates() {
|
|
|
3900
4080
|
const cache = readCache();
|
|
3901
4081
|
const now = Date.now();
|
|
3902
4082
|
if (now - cache.lastCheck < CHECK_INTERVAL) {
|
|
3903
|
-
if (cache.latestVersion && compareVersions("1.
|
|
4083
|
+
if (cache.latestVersion && compareVersions("1.6.1", cache.latestVersion) > 0) {
|
|
3904
4084
|
showUpdateMessage(cache.latestVersion);
|
|
3905
4085
|
}
|
|
3906
4086
|
return;
|
|
3907
4087
|
}
|
|
3908
4088
|
fetchLatestVersion().then((latestVersion) => {
|
|
3909
4089
|
writeCache({ lastCheck: now, latestVersion });
|
|
3910
|
-
if (latestVersion && compareVersions("1.
|
|
4090
|
+
if (latestVersion && compareVersions("1.6.1", latestVersion) > 0) {
|
|
3911
4091
|
showUpdateMessage(latestVersion);
|
|
3912
4092
|
}
|
|
3913
4093
|
});
|
|
3914
4094
|
}
|
|
3915
4095
|
function showUpdateMessage(latestVersion) {
|
|
3916
4096
|
console.log("");
|
|
3917
|
-
console.log(
|
|
3918
|
-
console.log(
|
|
4097
|
+
console.log(chalk30.yellow(` Update available: ${"1.6.1"} \u2192 ${latestVersion}`));
|
|
4098
|
+
console.log(chalk30.dim(` Run: npm update -g bindler`));
|
|
3919
4099
|
console.log("");
|
|
3920
4100
|
}
|
|
3921
4101
|
|
|
3922
4102
|
// src/cli.ts
|
|
3923
4103
|
var program = new Command();
|
|
3924
|
-
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.
|
|
4104
|
+
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.1");
|
|
3925
4105
|
program.hook("preAction", async () => {
|
|
3926
4106
|
try {
|
|
3927
4107
|
initConfig();
|
|
@@ -3940,73 +4120,73 @@ program.command("status").description("Show detailed status of all projects").ac
|
|
|
3940
4120
|
});
|
|
3941
4121
|
program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
|
|
3942
4122
|
if (!name && !options.all) {
|
|
3943
|
-
console.log(
|
|
3944
|
-
console.log(
|
|
3945
|
-
console.log(
|
|
3946
|
-
console.log(
|
|
4123
|
+
console.log(chalk31.red("Usage: bindler start <name> or bindler start --all"));
|
|
4124
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4125
|
+
console.log(chalk31.dim(" bindler start myapp"));
|
|
4126
|
+
console.log(chalk31.dim(" bindler start --all # start all npm projects"));
|
|
3947
4127
|
process.exit(1);
|
|
3948
4128
|
}
|
|
3949
4129
|
await startCommand(name, options);
|
|
3950
4130
|
});
|
|
3951
4131
|
program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
|
|
3952
4132
|
if (!name && !options.all) {
|
|
3953
|
-
console.log(
|
|
3954
|
-
console.log(
|
|
3955
|
-
console.log(
|
|
3956
|
-
console.log(
|
|
4133
|
+
console.log(chalk31.red("Usage: bindler stop <name> or bindler stop --all"));
|
|
4134
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4135
|
+
console.log(chalk31.dim(" bindler stop myapp"));
|
|
4136
|
+
console.log(chalk31.dim(" bindler stop --all # stop all npm projects"));
|
|
3957
4137
|
process.exit(1);
|
|
3958
4138
|
}
|
|
3959
4139
|
await stopCommand(name, options);
|
|
3960
4140
|
});
|
|
3961
4141
|
program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
|
|
3962
4142
|
if (!name && !options.all) {
|
|
3963
|
-
console.log(
|
|
3964
|
-
console.log(
|
|
3965
|
-
console.log(
|
|
3966
|
-
console.log(
|
|
4143
|
+
console.log(chalk31.red("Usage: bindler restart <name> or bindler restart --all"));
|
|
4144
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4145
|
+
console.log(chalk31.dim(" bindler restart myapp"));
|
|
4146
|
+
console.log(chalk31.dim(" bindler restart --all # restart all npm projects"));
|
|
3967
4147
|
process.exit(1);
|
|
3968
4148
|
}
|
|
3969
4149
|
await restartCommand(name, options);
|
|
3970
4150
|
});
|
|
3971
4151
|
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) => {
|
|
3972
4152
|
if (!name) {
|
|
3973
|
-
console.log(
|
|
3974
|
-
console.log(
|
|
3975
|
-
console.log(
|
|
3976
|
-
console.log(
|
|
3977
|
-
console.log(
|
|
4153
|
+
console.log(chalk31.red("Usage: bindler logs <name>"));
|
|
4154
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4155
|
+
console.log(chalk31.dim(" bindler logs myapp"));
|
|
4156
|
+
console.log(chalk31.dim(" bindler logs myapp --follow"));
|
|
4157
|
+
console.log(chalk31.dim(" bindler logs myapp --lines 500"));
|
|
3978
4158
|
process.exit(1);
|
|
3979
4159
|
}
|
|
3980
4160
|
await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
|
|
3981
4161
|
});
|
|
3982
4162
|
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) => {
|
|
3983
4163
|
if (!name) {
|
|
3984
|
-
console.log(
|
|
3985
|
-
console.log(
|
|
3986
|
-
console.log(
|
|
3987
|
-
console.log(
|
|
3988
|
-
console.log(
|
|
4164
|
+
console.log(chalk31.red("Usage: bindler update <name> [options]"));
|
|
4165
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4166
|
+
console.log(chalk31.dim(" bindler update myapp --hostname newapp.example.com"));
|
|
4167
|
+
console.log(chalk31.dim(" bindler update myapp --port 4000"));
|
|
4168
|
+
console.log(chalk31.dim(" bindler update myapp --disable"));
|
|
3989
4169
|
process.exit(1);
|
|
3990
4170
|
}
|
|
3991
4171
|
await updateCommand(name, options);
|
|
3992
4172
|
});
|
|
3993
4173
|
program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
|
|
3994
4174
|
if (!name) {
|
|
3995
|
-
console.log(
|
|
3996
|
-
console.log(
|
|
3997
|
-
console.log(
|
|
3998
|
-
console.log(
|
|
4175
|
+
console.log(chalk31.red("Usage: bindler edit <name>"));
|
|
4176
|
+
console.log(chalk31.dim("\nOpens the project config in your $EDITOR"));
|
|
4177
|
+
console.log(chalk31.dim("\nExample:"));
|
|
4178
|
+
console.log(chalk31.dim(" bindler edit myapp"));
|
|
3999
4179
|
process.exit(1);
|
|
4000
4180
|
}
|
|
4001
4181
|
await editCommand(name);
|
|
4002
4182
|
});
|
|
4003
4183
|
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) => {
|
|
4004
4184
|
if (!name) {
|
|
4005
|
-
console.log(
|
|
4006
|
-
console.log(
|
|
4007
|
-
console.log(
|
|
4008
|
-
console.log(
|
|
4009
|
-
console.log(
|
|
4185
|
+
console.log(chalk31.red("Usage: bindler remove <name>"));
|
|
4186
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4187
|
+
console.log(chalk31.dim(" bindler remove myapp"));
|
|
4188
|
+
console.log(chalk31.dim(" bindler remove myapp --force # skip confirmation"));
|
|
4189
|
+
console.log(chalk31.dim(" bindler rm myapp # alias"));
|
|
4010
4190
|
process.exit(1);
|
|
4011
4191
|
}
|
|
4012
4192
|
await removeCommand(name, options);
|
|
@@ -4025,10 +4205,10 @@ program.command("info").description("Show bindler information and stats").action
|
|
|
4025
4205
|
});
|
|
4026
4206
|
program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
|
|
4027
4207
|
if (!hostname) {
|
|
4028
|
-
console.log(
|
|
4029
|
-
console.log(
|
|
4030
|
-
console.log(
|
|
4031
|
-
console.log(
|
|
4208
|
+
console.log(chalk31.red("Usage: bindler check <hostname>"));
|
|
4209
|
+
console.log(chalk31.dim("\nExamples:"));
|
|
4210
|
+
console.log(chalk31.dim(" bindler check myapp.example.com"));
|
|
4211
|
+
console.log(chalk31.dim(" bindler check myapp # uses project name"));
|
|
4032
4212
|
process.exit(1);
|
|
4033
4213
|
}
|
|
4034
4214
|
await checkCommand(hostname, options);
|
|
@@ -4072,5 +4252,8 @@ program.command("clone [source] [new-name]").description("Clone a project config
|
|
|
4072
4252
|
program.command("dev [name]").description("Start a project in development mode with hot reload").option("-p, --port <port>", "Override port number").option("-h, --hostname <hostname>", "Override hostname").action(async (name, options) => {
|
|
4073
4253
|
await devCommand(name, options);
|
|
4074
4254
|
});
|
|
4255
|
+
program.command("config [action] [key] [value]").description("View or modify bindler configuration").action(async (action, key, value) => {
|
|
4256
|
+
await configCommand(action, key, value);
|
|
4257
|
+
});
|
|
4075
4258
|
program.parse();
|
|
4076
4259
|
//# sourceMappingURL=cli.js.map
|