bindler 1.6.0 → 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 +467 -462
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,10 +11,10 @@ import { Command } from "commander";
|
|
|
11
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";
|
|
@@ -890,9 +890,296 @@ function runPreflightChecks(config) {
|
|
|
890
890
|
return result;
|
|
891
891
|
}
|
|
892
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
|
+
|
|
893
1180
|
// src/commands/new.ts
|
|
894
1181
|
async function newCommand(options) {
|
|
895
|
-
console.log(
|
|
1182
|
+
console.log(chalk3.dim("Checking prerequisites...\n"));
|
|
896
1183
|
const issues = [];
|
|
897
1184
|
if (!isNginxInstalled()) {
|
|
898
1185
|
issues.push("nginx is not installed. Install: brew install nginx (macOS) or apt install nginx (Linux)");
|
|
@@ -901,11 +1188,11 @@ async function newCommand(options) {
|
|
|
901
1188
|
issues.push("PM2 is not installed. Install: npm install -g pm2");
|
|
902
1189
|
}
|
|
903
1190
|
if (issues.length > 0) {
|
|
904
|
-
console.log(
|
|
1191
|
+
console.log(chalk3.red("Missing prerequisites:\n"));
|
|
905
1192
|
for (const issue of issues) {
|
|
906
|
-
console.log(
|
|
1193
|
+
console.log(chalk3.red(` \u2717 ${issue}`));
|
|
907
1194
|
}
|
|
908
|
-
console.log(
|
|
1195
|
+
console.log(chalk3.dim("\nRun `bindler doctor` for full diagnostics."));
|
|
909
1196
|
const { proceed } = await inquirer.prompt([
|
|
910
1197
|
{
|
|
911
1198
|
type: "confirm",
|
|
@@ -919,17 +1206,17 @@ async function newCommand(options) {
|
|
|
919
1206
|
}
|
|
920
1207
|
console.log("");
|
|
921
1208
|
} else {
|
|
922
|
-
console.log(
|
|
1209
|
+
console.log(chalk3.green("\u2713 Prerequisites OK\n"));
|
|
923
1210
|
}
|
|
924
1211
|
const defaults = getDefaults();
|
|
925
1212
|
let project = {};
|
|
926
1213
|
const cwd = process.cwd();
|
|
927
1214
|
const cwdName = basename2(cwd);
|
|
928
1215
|
const initialPath = options.path || cwd;
|
|
929
|
-
const yamlConfig =
|
|
1216
|
+
const yamlConfig = existsSync7(initialPath) ? readBindlerYaml(initialPath) : null;
|
|
930
1217
|
let yamlDefaults = {};
|
|
931
1218
|
if (yamlConfig) {
|
|
932
|
-
console.log(
|
|
1219
|
+
console.log(chalk3.cyan("Found bindler.yaml - using as defaults\n"));
|
|
933
1220
|
yamlDefaults = yamlToProject(yamlConfig, initialPath);
|
|
934
1221
|
}
|
|
935
1222
|
if (!options.name) {
|
|
@@ -963,7 +1250,7 @@ async function newCommand(options) {
|
|
|
963
1250
|
name: "type",
|
|
964
1251
|
message: "Project type:",
|
|
965
1252
|
choices: (answers2) => {
|
|
966
|
-
const detected =
|
|
1253
|
+
const detected = existsSync7(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
967
1254
|
return [
|
|
968
1255
|
{ name: `npm (Node.js app)${detected === "npm" ? " - detected" : ""}`, value: "npm" },
|
|
969
1256
|
{ name: `static (HTML/CSS/JS)${detected === "static" ? " - detected" : ""}`, value: "static" }
|
|
@@ -971,7 +1258,7 @@ async function newCommand(options) {
|
|
|
971
1258
|
},
|
|
972
1259
|
default: (answers2) => {
|
|
973
1260
|
if (yamlDefaults.type) return yamlDefaults.type;
|
|
974
|
-
return
|
|
1261
|
+
return existsSync7(answers2.path) ? detectProjectType(answers2.path) : "static";
|
|
975
1262
|
}
|
|
976
1263
|
},
|
|
977
1264
|
{
|
|
@@ -1004,7 +1291,7 @@ async function newCommand(options) {
|
|
|
1004
1291
|
if (yamlDefaults.security) project.security = yamlDefaults.security;
|
|
1005
1292
|
if (yamlDefaults.environments) project.environments = yamlDefaults.environments;
|
|
1006
1293
|
if (answers.type === "npm") {
|
|
1007
|
-
const scripts =
|
|
1294
|
+
const scripts = existsSync7(answers.path) ? getPackageJsonScripts(answers.path) : [];
|
|
1008
1295
|
let suggestedPort = yamlDefaults.port || findAvailablePort();
|
|
1009
1296
|
const portCheck = checkPortInUse(suggestedPort);
|
|
1010
1297
|
if (portCheck.inUse) {
|
|
@@ -1073,7 +1360,7 @@ async function newCommand(options) {
|
|
|
1073
1360
|
}
|
|
1074
1361
|
} else {
|
|
1075
1362
|
if (!options.hostname) {
|
|
1076
|
-
console.error(
|
|
1363
|
+
console.error(chalk3.red("Error: --hostname is required"));
|
|
1077
1364
|
process.exit(1);
|
|
1078
1365
|
}
|
|
1079
1366
|
project.name = options.name;
|
|
@@ -1091,13 +1378,13 @@ async function newCommand(options) {
|
|
|
1091
1378
|
const portCheck = checkPortInUse(port);
|
|
1092
1379
|
if (portCheck.inUse) {
|
|
1093
1380
|
const processInfo = portCheck.process ? ` by ${portCheck.process}` : "";
|
|
1094
|
-
console.log(
|
|
1381
|
+
console.log(chalk3.yellow(`Warning: Port ${port} is already in use${processInfo}`));
|
|
1095
1382
|
if (!options.port) {
|
|
1096
1383
|
for (let p = port + 1; p <= 9e3; p++) {
|
|
1097
1384
|
const check = checkPortInUse(p);
|
|
1098
1385
|
if (!check.inUse) {
|
|
1099
1386
|
port = p;
|
|
1100
|
-
console.log(
|
|
1387
|
+
console.log(chalk3.dim(` Using port ${port} instead`));
|
|
1101
1388
|
break;
|
|
1102
1389
|
}
|
|
1103
1390
|
}
|
|
@@ -1109,14 +1396,14 @@ async function newCommand(options) {
|
|
|
1109
1396
|
}
|
|
1110
1397
|
}
|
|
1111
1398
|
if (!validateProjectName(project.name)) {
|
|
1112
|
-
console.error(
|
|
1399
|
+
console.error(chalk3.red("Error: Invalid project name"));
|
|
1113
1400
|
process.exit(1);
|
|
1114
1401
|
}
|
|
1115
1402
|
if (!validateHostname(project.hostname)) {
|
|
1116
|
-
console.error(
|
|
1403
|
+
console.error(chalk3.red("Error: Invalid hostname"));
|
|
1117
1404
|
process.exit(1);
|
|
1118
1405
|
}
|
|
1119
|
-
if (!
|
|
1406
|
+
if (!existsSync7(project.path)) {
|
|
1120
1407
|
const createDir = options.name ? true : (await inquirer.prompt([
|
|
1121
1408
|
{
|
|
1122
1409
|
type: "confirm",
|
|
@@ -1127,7 +1414,7 @@ async function newCommand(options) {
|
|
|
1127
1414
|
])).create;
|
|
1128
1415
|
if (createDir) {
|
|
1129
1416
|
mkdirSync3(project.path, { recursive: true });
|
|
1130
|
-
console.log(
|
|
1417
|
+
console.log(chalk3.green(`Created directory: ${project.path}`));
|
|
1131
1418
|
}
|
|
1132
1419
|
}
|
|
1133
1420
|
const validationResult = validateProject(project);
|
|
@@ -1135,7 +1422,7 @@ async function newCommand(options) {
|
|
|
1135
1422
|
console.log("");
|
|
1136
1423
|
printValidationResult(validationResult);
|
|
1137
1424
|
if (!validationResult.valid) {
|
|
1138
|
-
console.log(
|
|
1425
|
+
console.log(chalk3.red("\n\u2717 Cannot add project due to validation errors."));
|
|
1139
1426
|
process.exit(1);
|
|
1140
1427
|
}
|
|
1141
1428
|
const { proceed } = await inquirer.prompt([
|
|
@@ -1147,52 +1434,57 @@ async function newCommand(options) {
|
|
|
1147
1434
|
}
|
|
1148
1435
|
]);
|
|
1149
1436
|
if (!proceed) {
|
|
1150
|
-
console.log(
|
|
1437
|
+
console.log(chalk3.yellow("Aborted."));
|
|
1151
1438
|
process.exit(0);
|
|
1152
1439
|
}
|
|
1153
1440
|
}
|
|
1154
1441
|
try {
|
|
1155
1442
|
addProject(project);
|
|
1156
|
-
console.log(
|
|
1443
|
+
console.log(chalk3.green(`
|
|
1157
1444
|
Project "${project.name}" added successfully!`));
|
|
1158
|
-
if (
|
|
1159
|
-
console.log(
|
|
1160
|
-
|
|
1161
|
-
console.log(chalk2.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
|
|
1162
|
-
console.log(chalk2.dim(`
|
|
1163
|
-
Run ${chalk2.cyan("sudo bindler apply")} to update nginx.`));
|
|
1164
|
-
console.log(chalk2.dim(`Then access at: ${chalk2.cyan(`http://${project.hostname}:8080`)}`));
|
|
1445
|
+
if (options.apply) {
|
|
1446
|
+
console.log("");
|
|
1447
|
+
await applyCommand({});
|
|
1165
1448
|
} else {
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
}
|
|
1168
1460
|
}
|
|
1169
1461
|
if (project.type === "npm") {
|
|
1170
|
-
console.log(
|
|
1462
|
+
console.log(chalk3.dim(`Run ${chalk3.cyan(`bindler start ${project.name}`)} to start the application.`));
|
|
1171
1463
|
}
|
|
1172
1464
|
} catch (error) {
|
|
1173
|
-
console.error(
|
|
1465
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1174
1466
|
process.exit(1);
|
|
1175
1467
|
}
|
|
1176
1468
|
}
|
|
1177
1469
|
|
|
1178
1470
|
// src/commands/list.ts
|
|
1179
|
-
import
|
|
1471
|
+
import chalk4 from "chalk";
|
|
1180
1472
|
import Table from "cli-table3";
|
|
1181
1473
|
async function listCommand() {
|
|
1182
1474
|
const projects = listProjects();
|
|
1183
1475
|
if (projects.length === 0) {
|
|
1184
|
-
console.log(
|
|
1185
|
-
console.log(
|
|
1476
|
+
console.log(chalk4.yellow("No projects registered."));
|
|
1477
|
+
console.log(chalk4.dim(`Run ${chalk4.cyan("bindler new")} to create one.`));
|
|
1186
1478
|
return;
|
|
1187
1479
|
}
|
|
1188
1480
|
const table = new Table({
|
|
1189
1481
|
head: [
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1482
|
+
chalk4.cyan("Name"),
|
|
1483
|
+
chalk4.cyan("Type"),
|
|
1484
|
+
chalk4.cyan("Hostname"),
|
|
1485
|
+
chalk4.cyan("Port"),
|
|
1486
|
+
chalk4.cyan("Path"),
|
|
1487
|
+
chalk4.cyan("Status")
|
|
1196
1488
|
],
|
|
1197
1489
|
style: {
|
|
1198
1490
|
head: [],
|
|
@@ -1204,15 +1496,15 @@ async function listCommand() {
|
|
|
1204
1496
|
if (project.type === "npm") {
|
|
1205
1497
|
const process2 = getProcessByName(project.name);
|
|
1206
1498
|
if (process2) {
|
|
1207
|
-
status = process2.status === "online" ?
|
|
1499
|
+
status = process2.status === "online" ? chalk4.green("online") : process2.status === "stopped" ? chalk4.yellow("stopped") : chalk4.red(process2.status);
|
|
1208
1500
|
} else {
|
|
1209
|
-
status =
|
|
1501
|
+
status = chalk4.dim("not started");
|
|
1210
1502
|
}
|
|
1211
1503
|
} else {
|
|
1212
|
-
status = project.enabled !== false ?
|
|
1504
|
+
status = project.enabled !== false ? chalk4.green("serving") : chalk4.yellow("disabled");
|
|
1213
1505
|
}
|
|
1214
1506
|
if (project.enabled === false) {
|
|
1215
|
-
status =
|
|
1507
|
+
status = chalk4.yellow("disabled");
|
|
1216
1508
|
}
|
|
1217
1509
|
table.push([
|
|
1218
1510
|
project.name,
|
|
@@ -1224,17 +1516,17 @@ async function listCommand() {
|
|
|
1224
1516
|
]);
|
|
1225
1517
|
}
|
|
1226
1518
|
console.log(table.toString());
|
|
1227
|
-
console.log(
|
|
1519
|
+
console.log(chalk4.dim(`
|
|
1228
1520
|
${projects.length} project(s) registered`));
|
|
1229
1521
|
}
|
|
1230
1522
|
|
|
1231
1523
|
// src/commands/status.ts
|
|
1232
|
-
import
|
|
1524
|
+
import chalk5 from "chalk";
|
|
1233
1525
|
import Table2 from "cli-table3";
|
|
1234
1526
|
async function statusCommand() {
|
|
1235
1527
|
const projects = listProjects();
|
|
1236
1528
|
if (projects.length === 0) {
|
|
1237
|
-
console.log(
|
|
1529
|
+
console.log(chalk5.yellow("No projects registered."));
|
|
1238
1530
|
return;
|
|
1239
1531
|
}
|
|
1240
1532
|
const statuses = [];
|
|
@@ -1258,12 +1550,12 @@ async function statusCommand() {
|
|
|
1258
1550
|
}
|
|
1259
1551
|
const table = new Table2({
|
|
1260
1552
|
head: [
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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")
|
|
1267
1559
|
],
|
|
1268
1560
|
style: {
|
|
1269
1561
|
head: [],
|
|
@@ -1276,25 +1568,25 @@ async function statusCommand() {
|
|
|
1276
1568
|
if (status.type === "npm") {
|
|
1277
1569
|
switch (status.pm2Status) {
|
|
1278
1570
|
case "online":
|
|
1279
|
-
pm2StatusStr =
|
|
1571
|
+
pm2StatusStr = chalk5.green("online");
|
|
1280
1572
|
break;
|
|
1281
1573
|
case "stopped":
|
|
1282
|
-
pm2StatusStr =
|
|
1574
|
+
pm2StatusStr = chalk5.yellow("stopped");
|
|
1283
1575
|
break;
|
|
1284
1576
|
case "errored":
|
|
1285
|
-
pm2StatusStr =
|
|
1577
|
+
pm2StatusStr = chalk5.red("errored");
|
|
1286
1578
|
break;
|
|
1287
1579
|
case "not_managed":
|
|
1288
|
-
pm2StatusStr =
|
|
1580
|
+
pm2StatusStr = chalk5.dim("not started");
|
|
1289
1581
|
break;
|
|
1290
1582
|
default:
|
|
1291
|
-
pm2StatusStr =
|
|
1583
|
+
pm2StatusStr = chalk5.dim(status.pm2Status || "-");
|
|
1292
1584
|
}
|
|
1293
|
-
portCheckStr = status.portListening ?
|
|
1585
|
+
portCheckStr = status.portListening ? chalk5.green("listening") : chalk5.red("not listening");
|
|
1294
1586
|
}
|
|
1295
1587
|
if (status.enabled === false) {
|
|
1296
|
-
pm2StatusStr =
|
|
1297
|
-
portCheckStr =
|
|
1588
|
+
pm2StatusStr = chalk5.yellow("disabled");
|
|
1589
|
+
portCheckStr = chalk5.dim("-");
|
|
1298
1590
|
}
|
|
1299
1591
|
table.push([
|
|
1300
1592
|
status.name,
|
|
@@ -1308,14 +1600,14 @@ async function statusCommand() {
|
|
|
1308
1600
|
console.log(table.toString());
|
|
1309
1601
|
const runningProcesses = getPm2List().filter((p) => p.name.startsWith("bindler:"));
|
|
1310
1602
|
if (runningProcesses.length > 0) {
|
|
1311
|
-
console.log(
|
|
1603
|
+
console.log(chalk5.bold("\nPM2 Process Details:"));
|
|
1312
1604
|
const detailTable = new Table2({
|
|
1313
1605
|
head: [
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1606
|
+
chalk5.cyan("Process"),
|
|
1607
|
+
chalk5.cyan("CPU"),
|
|
1608
|
+
chalk5.cyan("Memory"),
|
|
1609
|
+
chalk5.cyan("Uptime"),
|
|
1610
|
+
chalk5.cyan("Restarts")
|
|
1319
1611
|
],
|
|
1320
1612
|
style: {
|
|
1321
1613
|
head: [],
|
|
@@ -1336,198 +1628,198 @@ async function statusCommand() {
|
|
|
1336
1628
|
}
|
|
1337
1629
|
|
|
1338
1630
|
// src/commands/start.ts
|
|
1339
|
-
import
|
|
1631
|
+
import chalk6 from "chalk";
|
|
1340
1632
|
async function startCommand(name, options) {
|
|
1341
1633
|
if (options.all) {
|
|
1342
1634
|
const projects = listProjects();
|
|
1343
1635
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1344
1636
|
if (npmProjects.length === 0) {
|
|
1345
|
-
console.log(
|
|
1637
|
+
console.log(chalk6.yellow("No npm projects to start."));
|
|
1346
1638
|
return;
|
|
1347
1639
|
}
|
|
1348
|
-
console.log(
|
|
1640
|
+
console.log(chalk6.blue(`Starting ${npmProjects.length} npm project(s)...`));
|
|
1349
1641
|
const results = startAllProjects(npmProjects);
|
|
1350
1642
|
for (const result2 of results) {
|
|
1351
1643
|
if (result2.success) {
|
|
1352
|
-
console.log(
|
|
1644
|
+
console.log(chalk6.green(` \u2713 ${result2.name}`));
|
|
1353
1645
|
} else {
|
|
1354
|
-
console.log(
|
|
1646
|
+
console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1355
1647
|
}
|
|
1356
1648
|
}
|
|
1357
1649
|
const succeeded = results.filter((r) => r.success).length;
|
|
1358
|
-
console.log(
|
|
1650
|
+
console.log(chalk6.dim(`
|
|
1359
1651
|
${succeeded}/${results.length} started successfully`));
|
|
1360
1652
|
return;
|
|
1361
1653
|
}
|
|
1362
1654
|
if (!name) {
|
|
1363
|
-
console.error(
|
|
1655
|
+
console.error(chalk6.red("Error: Project name is required. Use --all to start all projects."));
|
|
1364
1656
|
process.exit(1);
|
|
1365
1657
|
}
|
|
1366
1658
|
const project = getProject(name);
|
|
1367
1659
|
if (!project) {
|
|
1368
|
-
console.error(
|
|
1660
|
+
console.error(chalk6.red(`Error: Project "${name}" not found`));
|
|
1369
1661
|
process.exit(1);
|
|
1370
1662
|
}
|
|
1371
1663
|
if (project.type !== "npm") {
|
|
1372
|
-
console.log(
|
|
1373
|
-
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."));
|
|
1374
1666
|
return;
|
|
1375
1667
|
}
|
|
1376
|
-
console.log(
|
|
1668
|
+
console.log(chalk6.blue(`Starting ${name}...`));
|
|
1377
1669
|
const result = startProject(project);
|
|
1378
1670
|
if (result.success) {
|
|
1379
|
-
console.log(
|
|
1380
|
-
console.log(
|
|
1381
|
-
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}`));
|
|
1382
1674
|
} else {
|
|
1383
|
-
console.error(
|
|
1675
|
+
console.error(chalk6.red(`\u2717 Failed to start ${name}: ${result.error}`));
|
|
1384
1676
|
process.exit(1);
|
|
1385
1677
|
}
|
|
1386
1678
|
}
|
|
1387
1679
|
|
|
1388
1680
|
// src/commands/stop.ts
|
|
1389
|
-
import
|
|
1681
|
+
import chalk7 from "chalk";
|
|
1390
1682
|
async function stopCommand(name, options) {
|
|
1391
1683
|
if (options.all) {
|
|
1392
1684
|
const projects = listProjects();
|
|
1393
1685
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1394
1686
|
if (npmProjects.length === 0) {
|
|
1395
|
-
console.log(
|
|
1687
|
+
console.log(chalk7.yellow("No npm projects to stop."));
|
|
1396
1688
|
return;
|
|
1397
1689
|
}
|
|
1398
|
-
console.log(
|
|
1690
|
+
console.log(chalk7.blue(`Stopping ${npmProjects.length} npm project(s)...`));
|
|
1399
1691
|
const results = stopAllProjects(npmProjects);
|
|
1400
1692
|
for (const result2 of results) {
|
|
1401
1693
|
if (result2.success) {
|
|
1402
|
-
console.log(
|
|
1694
|
+
console.log(chalk7.green(` \u2713 ${result2.name}`));
|
|
1403
1695
|
} else {
|
|
1404
|
-
console.log(
|
|
1696
|
+
console.log(chalk7.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1405
1697
|
}
|
|
1406
1698
|
}
|
|
1407
1699
|
const succeeded = results.filter((r) => r.success).length;
|
|
1408
|
-
console.log(
|
|
1700
|
+
console.log(chalk7.dim(`
|
|
1409
1701
|
${succeeded}/${results.length} stopped successfully`));
|
|
1410
1702
|
return;
|
|
1411
1703
|
}
|
|
1412
1704
|
if (!name) {
|
|
1413
|
-
console.error(
|
|
1705
|
+
console.error(chalk7.red("Error: Project name is required. Use --all to stop all projects."));
|
|
1414
1706
|
process.exit(1);
|
|
1415
1707
|
}
|
|
1416
1708
|
const project = getProject(name);
|
|
1417
1709
|
if (!project) {
|
|
1418
|
-
console.error(
|
|
1710
|
+
console.error(chalk7.red(`Error: Project "${name}" not found`));
|
|
1419
1711
|
process.exit(1);
|
|
1420
1712
|
}
|
|
1421
1713
|
if (project.type !== "npm") {
|
|
1422
|
-
console.log(
|
|
1714
|
+
console.log(chalk7.yellow(`Project "${name}" is a static site - no process to stop.`));
|
|
1423
1715
|
return;
|
|
1424
1716
|
}
|
|
1425
|
-
console.log(
|
|
1717
|
+
console.log(chalk7.blue(`Stopping ${name}...`));
|
|
1426
1718
|
const result = stopProject(name);
|
|
1427
1719
|
if (result.success) {
|
|
1428
|
-
console.log(
|
|
1720
|
+
console.log(chalk7.green(`\u2713 ${name} stopped successfully`));
|
|
1429
1721
|
} else {
|
|
1430
|
-
console.error(
|
|
1722
|
+
console.error(chalk7.red(`\u2717 Failed to stop ${name}: ${result.error}`));
|
|
1431
1723
|
process.exit(1);
|
|
1432
1724
|
}
|
|
1433
1725
|
}
|
|
1434
1726
|
|
|
1435
1727
|
// src/commands/restart.ts
|
|
1436
|
-
import
|
|
1728
|
+
import chalk8 from "chalk";
|
|
1437
1729
|
async function restartCommand(name, options) {
|
|
1438
1730
|
if (options.all) {
|
|
1439
1731
|
const projects = listProjects();
|
|
1440
1732
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1441
1733
|
if (npmProjects.length === 0) {
|
|
1442
|
-
console.log(
|
|
1734
|
+
console.log(chalk8.yellow("No npm projects to restart."));
|
|
1443
1735
|
return;
|
|
1444
1736
|
}
|
|
1445
|
-
console.log(
|
|
1737
|
+
console.log(chalk8.blue(`Restarting ${npmProjects.length} npm project(s)...`));
|
|
1446
1738
|
const results = restartAllProjects(npmProjects);
|
|
1447
1739
|
for (const result2 of results) {
|
|
1448
1740
|
if (result2.success) {
|
|
1449
|
-
console.log(
|
|
1741
|
+
console.log(chalk8.green(` \u2713 ${result2.name}`));
|
|
1450
1742
|
} else {
|
|
1451
|
-
console.log(
|
|
1743
|
+
console.log(chalk8.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1452
1744
|
}
|
|
1453
1745
|
}
|
|
1454
1746
|
const succeeded = results.filter((r) => r.success).length;
|
|
1455
|
-
console.log(
|
|
1747
|
+
console.log(chalk8.dim(`
|
|
1456
1748
|
${succeeded}/${results.length} restarted successfully`));
|
|
1457
1749
|
return;
|
|
1458
1750
|
}
|
|
1459
1751
|
if (!name) {
|
|
1460
|
-
console.error(
|
|
1752
|
+
console.error(chalk8.red("Error: Project name is required. Use --all to restart all projects."));
|
|
1461
1753
|
process2.exit(1);
|
|
1462
1754
|
}
|
|
1463
1755
|
const project = getProject(name);
|
|
1464
1756
|
if (!project) {
|
|
1465
|
-
console.error(
|
|
1757
|
+
console.error(chalk8.red(`Error: Project "${name}" not found`));
|
|
1466
1758
|
process2.exit(1);
|
|
1467
1759
|
}
|
|
1468
1760
|
if (project.type !== "npm") {
|
|
1469
|
-
console.log(
|
|
1761
|
+
console.log(chalk8.yellow(`Project "${name}" is a static site - no process to restart.`));
|
|
1470
1762
|
return;
|
|
1471
1763
|
}
|
|
1472
|
-
console.log(
|
|
1764
|
+
console.log(chalk8.blue(`Restarting ${name}...`));
|
|
1473
1765
|
const process2 = getProcessByName(name);
|
|
1474
1766
|
let result;
|
|
1475
1767
|
if (process2) {
|
|
1476
1768
|
result = restartProject(name);
|
|
1477
1769
|
} else {
|
|
1478
|
-
console.log(
|
|
1770
|
+
console.log(chalk8.dim("Process not running, starting..."));
|
|
1479
1771
|
result = startProject(project);
|
|
1480
1772
|
}
|
|
1481
1773
|
if (result.success) {
|
|
1482
|
-
console.log(
|
|
1774
|
+
console.log(chalk8.green(`\u2713 ${name} restarted successfully`));
|
|
1483
1775
|
} else {
|
|
1484
|
-
console.error(
|
|
1776
|
+
console.error(chalk8.red(`\u2717 Failed to restart ${name}: ${result.error}`));
|
|
1485
1777
|
process2.exit(1);
|
|
1486
1778
|
}
|
|
1487
1779
|
}
|
|
1488
1780
|
|
|
1489
1781
|
// src/commands/logs.ts
|
|
1490
|
-
import
|
|
1782
|
+
import chalk9 from "chalk";
|
|
1491
1783
|
async function logsCommand(name, options) {
|
|
1492
1784
|
const project = getProject(name);
|
|
1493
1785
|
if (!project) {
|
|
1494
|
-
console.error(
|
|
1786
|
+
console.error(chalk9.red(`Error: Project "${name}" not found`));
|
|
1495
1787
|
process2.exit(1);
|
|
1496
1788
|
}
|
|
1497
1789
|
if (project.type !== "npm") {
|
|
1498
|
-
console.log(
|
|
1499
|
-
console.log(
|
|
1500
|
-
console.log(
|
|
1501
|
-
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"));
|
|
1502
1794
|
return;
|
|
1503
1795
|
}
|
|
1504
1796
|
const process2 = getProcessByName(name);
|
|
1505
1797
|
if (!process2) {
|
|
1506
|
-
console.log(
|
|
1507
|
-
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.`));
|
|
1508
1800
|
return;
|
|
1509
1801
|
}
|
|
1510
1802
|
const lines = options.lines || 200;
|
|
1511
1803
|
const follow = options.follow || false;
|
|
1512
1804
|
if (follow) {
|
|
1513
|
-
console.log(
|
|
1805
|
+
console.log(chalk9.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
|
|
1514
1806
|
}
|
|
1515
1807
|
await showLogs(name, follow, lines);
|
|
1516
1808
|
}
|
|
1517
1809
|
|
|
1518
1810
|
// src/commands/update.ts
|
|
1519
|
-
import
|
|
1520
|
-
import { existsSync as
|
|
1811
|
+
import chalk10 from "chalk";
|
|
1812
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1521
1813
|
async function updateCommand(name, options) {
|
|
1522
1814
|
const project = getProject(name);
|
|
1523
1815
|
if (!project) {
|
|
1524
|
-
console.error(
|
|
1816
|
+
console.error(chalk10.red(`Error: Project "${name}" not found`));
|
|
1525
1817
|
process.exit(1);
|
|
1526
1818
|
}
|
|
1527
1819
|
const updates = {};
|
|
1528
1820
|
if (options.hostname) {
|
|
1529
1821
|
if (!validateHostname(options.hostname)) {
|
|
1530
|
-
console.error(
|
|
1822
|
+
console.error(chalk10.red("Error: Invalid hostname format"));
|
|
1531
1823
|
process.exit(1);
|
|
1532
1824
|
}
|
|
1533
1825
|
updates.hostname = options.hostname;
|
|
@@ -1535,11 +1827,11 @@ async function updateCommand(name, options) {
|
|
|
1535
1827
|
if (options.port) {
|
|
1536
1828
|
const port = parseInt(options.port, 10);
|
|
1537
1829
|
if (!validatePort(port)) {
|
|
1538
|
-
console.error(
|
|
1830
|
+
console.error(chalk10.red("Error: Invalid port. Use a number between 1024 and 65535."));
|
|
1539
1831
|
process.exit(1);
|
|
1540
1832
|
}
|
|
1541
1833
|
if (!isPortAvailable(port) && port !== project.port) {
|
|
1542
|
-
console.error(
|
|
1834
|
+
console.error(chalk10.red(`Error: Port ${port} is already in use by another project.`));
|
|
1543
1835
|
process.exit(1);
|
|
1544
1836
|
}
|
|
1545
1837
|
updates.port = port;
|
|
@@ -1549,20 +1841,20 @@ async function updateCommand(name, options) {
|
|
|
1549
1841
|
}
|
|
1550
1842
|
if (options.start) {
|
|
1551
1843
|
if (project.type !== "npm") {
|
|
1552
|
-
console.error(
|
|
1844
|
+
console.error(chalk10.red("Error: Start command only applies to npm projects"));
|
|
1553
1845
|
process.exit(1);
|
|
1554
1846
|
}
|
|
1555
1847
|
updates.start = options.start;
|
|
1556
1848
|
}
|
|
1557
1849
|
if (options.path) {
|
|
1558
|
-
if (!
|
|
1559
|
-
console.error(
|
|
1850
|
+
if (!existsSync8(options.path)) {
|
|
1851
|
+
console.error(chalk10.red(`Error: Path does not exist: ${options.path}`));
|
|
1560
1852
|
process.exit(1);
|
|
1561
1853
|
}
|
|
1562
1854
|
if (isProtectedPath(options.path)) {
|
|
1563
|
-
console.error(
|
|
1564
|
-
console.error(
|
|
1565
|
-
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.`));
|
|
1566
1858
|
process.exit(1);
|
|
1567
1859
|
}
|
|
1568
1860
|
updates.path = options.path;
|
|
@@ -1572,7 +1864,7 @@ async function updateCommand(name, options) {
|
|
|
1572
1864
|
for (const envStr of options.env) {
|
|
1573
1865
|
const [key, ...valueParts] = envStr.split("=");
|
|
1574
1866
|
if (!key) {
|
|
1575
|
-
console.error(
|
|
1867
|
+
console.error(chalk10.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
|
|
1576
1868
|
process.exit(1);
|
|
1577
1869
|
}
|
|
1578
1870
|
env[key] = valueParts.join("=");
|
|
@@ -1585,23 +1877,23 @@ async function updateCommand(name, options) {
|
|
|
1585
1877
|
updates.enabled = false;
|
|
1586
1878
|
}
|
|
1587
1879
|
if (Object.keys(updates).length === 0) {
|
|
1588
|
-
console.log(
|
|
1589
|
-
console.log(
|
|
1880
|
+
console.log(chalk10.yellow("No updates specified."));
|
|
1881
|
+
console.log(chalk10.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
|
|
1590
1882
|
return;
|
|
1591
1883
|
}
|
|
1592
1884
|
try {
|
|
1593
1885
|
updateProject(name, updates);
|
|
1594
|
-
console.log(
|
|
1886
|
+
console.log(chalk10.green(`\u2713 Project "${name}" updated successfully`));
|
|
1595
1887
|
for (const [key, value] of Object.entries(updates)) {
|
|
1596
|
-
console.log(
|
|
1888
|
+
console.log(chalk10.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
|
|
1597
1889
|
}
|
|
1598
|
-
console.log(
|
|
1599
|
-
Run ${
|
|
1890
|
+
console.log(chalk10.dim(`
|
|
1891
|
+
Run ${chalk10.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1600
1892
|
if (project.type === "npm" && (updates.port || updates.start || updates.env)) {
|
|
1601
|
-
console.log(
|
|
1893
|
+
console.log(chalk10.dim(`Run ${chalk10.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
|
|
1602
1894
|
}
|
|
1603
1895
|
} catch (error) {
|
|
1604
|
-
console.error(
|
|
1896
|
+
console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1605
1897
|
process.exit(1);
|
|
1606
1898
|
}
|
|
1607
1899
|
}
|
|
@@ -1610,20 +1902,20 @@ Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
|
1610
1902
|
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1611
1903
|
import { tmpdir } from "os";
|
|
1612
1904
|
import { join as join6 } from "path";
|
|
1613
|
-
import
|
|
1905
|
+
import chalk11 from "chalk";
|
|
1614
1906
|
async function editCommand(name) {
|
|
1615
1907
|
const project = getProject(name);
|
|
1616
1908
|
if (!project) {
|
|
1617
|
-
console.error(
|
|
1909
|
+
console.error(chalk11.red(`Error: Project "${name}" not found`));
|
|
1618
1910
|
process.exit(1);
|
|
1619
1911
|
}
|
|
1620
1912
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1621
1913
|
const tmpFile = join6(tmpdir(), `bindler-${name}-${Date.now()}.json`);
|
|
1622
1914
|
writeFileSync4(tmpFile, JSON.stringify(project, null, 2) + "\n");
|
|
1623
|
-
console.log(
|
|
1915
|
+
console.log(chalk11.dim(`Opening ${name} config in ${editor}...`));
|
|
1624
1916
|
const exitCode = await spawnInteractive(editor, [tmpFile]);
|
|
1625
1917
|
if (exitCode !== 0) {
|
|
1626
|
-
console.error(
|
|
1918
|
+
console.error(chalk11.red("Editor exited with error"));
|
|
1627
1919
|
unlinkSync(tmpFile);
|
|
1628
1920
|
process.exit(1);
|
|
1629
1921
|
}
|
|
@@ -1631,7 +1923,7 @@ async function editCommand(name) {
|
|
|
1631
1923
|
try {
|
|
1632
1924
|
editedContent = readFileSync4(tmpFile, "utf-8");
|
|
1633
1925
|
} catch (error) {
|
|
1634
|
-
console.error(
|
|
1926
|
+
console.error(chalk11.red("Failed to read edited file"));
|
|
1635
1927
|
process.exit(1);
|
|
1636
1928
|
} finally {
|
|
1637
1929
|
unlinkSync(tmpFile);
|
|
@@ -1640,44 +1932,44 @@ async function editCommand(name) {
|
|
|
1640
1932
|
try {
|
|
1641
1933
|
editedProject = JSON.parse(editedContent);
|
|
1642
1934
|
} catch (error) {
|
|
1643
|
-
console.error(
|
|
1935
|
+
console.error(chalk11.red("Error: Invalid JSON in edited file"));
|
|
1644
1936
|
process.exit(1);
|
|
1645
1937
|
}
|
|
1646
1938
|
if (editedProject.name !== project.name) {
|
|
1647
|
-
console.error(
|
|
1939
|
+
console.error(chalk11.red("Error: Cannot change project name via edit. Use a new project instead."));
|
|
1648
1940
|
process.exit(1);
|
|
1649
1941
|
}
|
|
1650
1942
|
const originalStr = JSON.stringify(project);
|
|
1651
1943
|
const editedStr = JSON.stringify(editedProject);
|
|
1652
1944
|
if (originalStr === editedStr) {
|
|
1653
|
-
console.log(
|
|
1945
|
+
console.log(chalk11.yellow("No changes made."));
|
|
1654
1946
|
return;
|
|
1655
1947
|
}
|
|
1656
1948
|
try {
|
|
1657
1949
|
const config = readConfig();
|
|
1658
1950
|
const index = config.projects.findIndex((p) => p.name === name);
|
|
1659
1951
|
if (index === -1) {
|
|
1660
|
-
console.error(
|
|
1952
|
+
console.error(chalk11.red("Error: Project not found"));
|
|
1661
1953
|
process.exit(1);
|
|
1662
1954
|
}
|
|
1663
1955
|
config.projects[index] = editedProject;
|
|
1664
1956
|
writeConfig(config);
|
|
1665
|
-
console.log(
|
|
1666
|
-
console.log(
|
|
1667
|
-
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.`));
|
|
1668
1960
|
} catch (error) {
|
|
1669
|
-
console.error(
|
|
1961
|
+
console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1670
1962
|
process.exit(1);
|
|
1671
1963
|
}
|
|
1672
1964
|
}
|
|
1673
1965
|
|
|
1674
1966
|
// src/commands/remove.ts
|
|
1675
1967
|
import inquirer2 from "inquirer";
|
|
1676
|
-
import
|
|
1968
|
+
import chalk12 from "chalk";
|
|
1677
1969
|
async function removeCommand(name, options) {
|
|
1678
1970
|
const project = getProject(name);
|
|
1679
1971
|
if (!project) {
|
|
1680
|
-
console.error(
|
|
1972
|
+
console.error(chalk12.red(`Error: Project "${name}" not found`));
|
|
1681
1973
|
process.exit(1);
|
|
1682
1974
|
}
|
|
1683
1975
|
if (!options.force) {
|
|
@@ -1690,338 +1982,51 @@ async function removeCommand(name, options) {
|
|
|
1690
1982
|
}
|
|
1691
1983
|
]);
|
|
1692
1984
|
if (!confirm) {
|
|
1693
|
-
console.log(
|
|
1985
|
+
console.log(chalk12.yellow("Cancelled."));
|
|
1694
1986
|
return;
|
|
1695
1987
|
}
|
|
1696
1988
|
}
|
|
1697
1989
|
if (project.type === "npm") {
|
|
1698
1990
|
const process2 = getProcessByName(name);
|
|
1699
1991
|
if (process2) {
|
|
1700
|
-
console.log(
|
|
1992
|
+
console.log(chalk12.dim("Stopping PM2 process..."));
|
|
1701
1993
|
deleteProject(name);
|
|
1702
1994
|
}
|
|
1703
1995
|
}
|
|
1704
1996
|
try {
|
|
1705
1997
|
removeProject(name);
|
|
1706
|
-
console.log(
|
|
1998
|
+
console.log(chalk12.green(`\u2713 Project "${name}" removed from registry`));
|
|
1707
1999
|
if (options.apply) {
|
|
1708
|
-
console.log(
|
|
2000
|
+
console.log(chalk12.dim("\nApplying nginx configuration..."));
|
|
1709
2001
|
const config = readConfig();
|
|
1710
2002
|
try {
|
|
1711
2003
|
writeNginxConfig(config);
|
|
1712
2004
|
const testResult = testNginxConfig();
|
|
1713
2005
|
if (testResult.success) {
|
|
1714
2006
|
reloadNginx();
|
|
1715
|
-
console.log(
|
|
2007
|
+
console.log(chalk12.green("\u2713 Nginx configuration updated"));
|
|
1716
2008
|
} else {
|
|
1717
|
-
console.log(
|
|
2009
|
+
console.log(chalk12.yellow("! Nginx config test failed, reload skipped"));
|
|
1718
2010
|
}
|
|
1719
2011
|
} catch (err) {
|
|
1720
|
-
console.log(
|
|
1721
|
-
console.log(
|
|
2012
|
+
console.log(chalk12.yellow(`! Failed to update nginx: ${err}`));
|
|
2013
|
+
console.log(chalk12.dim(" Try running: sudo bindler apply"));
|
|
1722
2014
|
}
|
|
1723
|
-
} else {
|
|
1724
|
-
console.log(chalk11.dim(`
|
|
1725
|
-
Run ${chalk11.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1726
|
-
}
|
|
1727
|
-
console.log(chalk11.yellow("\nNote: Project files were not deleted."));
|
|
1728
|
-
console.log(chalk11.dim(` Path: ${project.path}`));
|
|
1729
|
-
if (!project.local) {
|
|
1730
|
-
console.log(chalk11.yellow("\nCloudflare DNS route was not removed."));
|
|
1731
|
-
console.log(chalk11.dim(" Remove it manually from the Cloudflare dashboard:"));
|
|
1732
|
-
console.log(chalk11.dim(" https://dash.cloudflare.com \u2192 DNS \u2192 Records \u2192 Delete the CNAME for " + project.hostname));
|
|
1733
|
-
}
|
|
1734
|
-
} catch (error) {
|
|
1735
|
-
console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1736
|
-
process.exit(1);
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
// src/commands/apply.ts
|
|
1741
|
-
import chalk12 from "chalk";
|
|
1742
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1743
|
-
|
|
1744
|
-
// src/lib/cloudflare.ts
|
|
1745
|
-
function isCloudflaredInstalled() {
|
|
1746
|
-
const result = execCommandSafe("which cloudflared");
|
|
1747
|
-
return result.success;
|
|
1748
|
-
}
|
|
1749
|
-
function getCloudflaredVersion() {
|
|
1750
|
-
const result = execCommandSafe("cloudflared --version");
|
|
1751
|
-
if (result.success) {
|
|
1752
|
-
const match = result.output.match(/cloudflared version (\S+)/);
|
|
1753
|
-
return match ? match[1] : result.output;
|
|
1754
|
-
}
|
|
1755
|
-
return null;
|
|
1756
|
-
}
|
|
1757
|
-
function listTunnels() {
|
|
1758
|
-
const result = execCommandSafe("cloudflared tunnel list --output json");
|
|
1759
|
-
if (!result.success) {
|
|
1760
|
-
return [];
|
|
1761
|
-
}
|
|
1762
|
-
try {
|
|
1763
|
-
const tunnels = JSON.parse(result.output);
|
|
1764
|
-
return tunnels.map((t) => ({
|
|
1765
|
-
id: t.id,
|
|
1766
|
-
name: t.name,
|
|
1767
|
-
createdAt: t.created_at
|
|
1768
|
-
}));
|
|
1769
|
-
} catch {
|
|
1770
|
-
return [];
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
function getTunnelByName(name) {
|
|
1774
|
-
const tunnels = listTunnels();
|
|
1775
|
-
const tunnel = tunnels.find((t) => t.name === name);
|
|
1776
|
-
return tunnel ? { id: tunnel.id, name: tunnel.name } : null;
|
|
1777
|
-
}
|
|
1778
|
-
function routeDns(tunnelName, hostname) {
|
|
1779
|
-
const result = execCommandSafe(`cloudflared tunnel route dns "${tunnelName}" "${hostname}"`);
|
|
1780
|
-
if (!result.success) {
|
|
1781
|
-
if (result.error?.includes("already exists") || result.output?.includes("already exists")) {
|
|
1782
|
-
return { success: true, output: "DNS route already exists" };
|
|
1783
|
-
}
|
|
1784
|
-
return { success: false, error: result.error };
|
|
1785
|
-
}
|
|
1786
|
-
return { success: true, output: result.output };
|
|
1787
|
-
}
|
|
1788
|
-
function routeDnsForAllProjects() {
|
|
1789
|
-
const config = readConfig();
|
|
1790
|
-
const { tunnelName, applyCloudflareDnsRoutes } = config.defaults;
|
|
1791
|
-
if (!applyCloudflareDnsRoutes) {
|
|
1792
|
-
return [];
|
|
1793
|
-
}
|
|
1794
|
-
const results = [];
|
|
1795
|
-
for (const project of config.projects) {
|
|
1796
|
-
if (project.enabled === false) {
|
|
1797
|
-
continue;
|
|
1798
|
-
}
|
|
1799
|
-
if (project.local) {
|
|
1800
|
-
results.push({
|
|
1801
|
-
hostname: project.hostname,
|
|
1802
|
-
success: true,
|
|
1803
|
-
skipped: true,
|
|
1804
|
-
output: "Local project - skipped"
|
|
1805
|
-
});
|
|
1806
|
-
continue;
|
|
1807
|
-
}
|
|
1808
|
-
const result = routeDns(tunnelName, project.hostname);
|
|
1809
|
-
results.push({
|
|
1810
|
-
hostname: project.hostname,
|
|
1811
|
-
...result
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
return results;
|
|
1815
|
-
}
|
|
1816
|
-
function isTunnelRunning(tunnelName) {
|
|
1817
|
-
const result = execCommandSafe(`pgrep -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
1818
|
-
return result.success;
|
|
1819
|
-
}
|
|
1820
|
-
function getTunnelInfo(tunnelName) {
|
|
1821
|
-
const tunnel = getTunnelByName(tunnelName);
|
|
1822
|
-
if (!tunnel) {
|
|
1823
|
-
return { exists: false, running: false };
|
|
1824
|
-
}
|
|
1825
|
-
return {
|
|
1826
|
-
exists: true,
|
|
1827
|
-
running: isTunnelRunning(tunnelName),
|
|
1828
|
-
id: tunnel.id
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// src/commands/apply.ts
|
|
1833
|
-
async function applyCommand(options) {
|
|
1834
|
-
let config = readConfig();
|
|
1835
|
-
const defaults = getDefaults();
|
|
1836
|
-
if (options.sync) {
|
|
1837
|
-
console.log(chalk12.dim("Syncing bindler.yaml from project directories...\n"));
|
|
1838
|
-
let synced = 0;
|
|
1839
|
-
for (const project of config.projects) {
|
|
1840
|
-
if (!existsSync8(project.path)) continue;
|
|
1841
|
-
const yamlConfig = readBindlerYaml(project.path);
|
|
1842
|
-
if (yamlConfig) {
|
|
1843
|
-
const merged = mergeYamlWithProject(project, yamlConfig);
|
|
1844
|
-
updateProject(project.name, merged);
|
|
1845
|
-
console.log(chalk12.green(` \u2713 Synced ${project.name} from bindler.yaml`));
|
|
1846
|
-
synced++;
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
if (synced === 0) {
|
|
1850
|
-
console.log(chalk12.dim(" No bindler.yaml files found in project directories"));
|
|
1851
2015
|
} else {
|
|
1852
2016
|
console.log(chalk12.dim(`
|
|
1853
|
-
|
|
1854
|
-
`));
|
|
2017
|
+
Run ${chalk12.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1855
2018
|
}
|
|
1856
|
-
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
config = { ...config, projects: envProjects };
|
|
1863
|
-
}
|
|
1864
|
-
const hasProjects = config.projects.length > 0;
|
|
1865
|
-
console.log(chalk12.blue("Applying configuration...\n"));
|
|
1866
|
-
if (hasProjects && !options.skipChecks) {
|
|
1867
|
-
console.log(chalk12.dim("Running preflight checks..."));
|
|
1868
|
-
const checkResult = runPreflightChecks(config);
|
|
1869
|
-
if (!checkResult.valid) {
|
|
1870
|
-
printValidationResult(checkResult);
|
|
1871
|
-
console.log(chalk12.red("\n\u2717 Preflight checks failed. Fix the errors above before applying."));
|
|
1872
|
-
console.log(chalk12.dim(" Use --skip-checks to bypass (not recommended)"));
|
|
1873
|
-
process.exit(1);
|
|
1874
|
-
}
|
|
1875
|
-
if (checkResult.warnings.length > 0) {
|
|
1876
|
-
printValidationResult(checkResult);
|
|
1877
|
-
console.log("");
|
|
1878
|
-
} else {
|
|
1879
|
-
console.log(chalk12.green(" \u2713 Preflight checks passed"));
|
|
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));
|
|
1880
2025
|
}
|
|
1881
|
-
}
|
|
1882
|
-
console.log(chalk12.dim("Generating nginx configuration..."));
|
|
1883
|
-
if (options.dryRun) {
|
|
1884
|
-
const nginxConfig = generateNginxConfig(config);
|
|
1885
|
-
console.log(chalk12.cyan("\n--- Generated nginx config (dry-run) ---\n"));
|
|
1886
|
-
console.log(nginxConfig);
|
|
1887
|
-
console.log(chalk12.cyan("--- End of config ---\n"));
|
|
1888
|
-
console.log(chalk12.yellow("Dry run mode - no changes were made."));
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
try {
|
|
1892
|
-
const { path, content } = writeNginxConfig(config);
|
|
1893
|
-
console.log(chalk12.green(` \u2713 Wrote nginx config to ${path}`));
|
|
1894
2026
|
} catch (error) {
|
|
1895
|
-
|
|
1896
|
-
console.error(chalk12.red(` \u2717 Failed to write nginx config: ${errMsg}`));
|
|
1897
|
-
if (errMsg.includes("EACCES") || errMsg.includes("permission denied")) {
|
|
1898
|
-
console.log(chalk12.yellow(`
|
|
1899
|
-
Try running with sudo: ${chalk12.cyan("sudo bindler apply")}`));
|
|
1900
|
-
}
|
|
2027
|
+
console.error(chalk12.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1901
2028
|
process.exit(1);
|
|
1902
2029
|
}
|
|
1903
|
-
const authProjects = config.projects.filter(
|
|
1904
|
-
(p) => p.security?.basicAuth?.enabled && p.security.basicAuth.users?.length
|
|
1905
|
-
);
|
|
1906
|
-
if (authProjects.length > 0) {
|
|
1907
|
-
console.log(chalk12.dim("Generating htpasswd files..."));
|
|
1908
|
-
try {
|
|
1909
|
-
generateHtpasswdFiles(config.projects);
|
|
1910
|
-
console.log(chalk12.green(` \u2713 Generated htpasswd files for ${authProjects.length} project(s)`));
|
|
1911
|
-
} catch (error) {
|
|
1912
|
-
console.log(chalk12.yellow(` ! Failed to generate htpasswd files: ${error}`));
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
console.log(chalk12.dim("Testing nginx configuration..."));
|
|
1916
|
-
const testResult = testNginxConfig();
|
|
1917
|
-
if (!testResult.success) {
|
|
1918
|
-
console.error(chalk12.red(" \u2717 Nginx configuration test failed:"));
|
|
1919
|
-
console.error(chalk12.red(testResult.output));
|
|
1920
|
-
console.log(chalk12.yellow("\nConfiguration was written but nginx was NOT reloaded."));
|
|
1921
|
-
console.log(chalk12.dim("Fix the configuration and run `sudo bindler apply` again."));
|
|
1922
|
-
process.exit(1);
|
|
1923
|
-
}
|
|
1924
|
-
console.log(chalk12.green(" \u2713 Nginx configuration test passed"));
|
|
1925
|
-
if (!options.noReload) {
|
|
1926
|
-
console.log(chalk12.dim("Reloading nginx..."));
|
|
1927
|
-
const reloadResult = reloadNginx();
|
|
1928
|
-
if (!reloadResult.success) {
|
|
1929
|
-
console.error(chalk12.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
|
|
1930
|
-
console.log(chalk12.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
|
|
1931
|
-
process.exit(1);
|
|
1932
|
-
}
|
|
1933
|
-
console.log(chalk12.green(" \u2713 Nginx reloaded successfully"));
|
|
1934
|
-
const listenPort = parseInt(defaults.nginxListen.split(":").pop() || "80", 10);
|
|
1935
|
-
const portCheck = execCommandSafe(`lsof -i :${listenPort} -P -n 2>/dev/null | grep LISTEN | grep nginx`);
|
|
1936
|
-
if (!portCheck.success || !portCheck.output) {
|
|
1937
|
-
const isPrivilegedPort = listenPort < 1024;
|
|
1938
|
-
console.log(chalk12.yellow(`
|
|
1939
|
-
\u26A0 Nginx is not listening on port ${listenPort}`));
|
|
1940
|
-
if (isPrivilegedPort) {
|
|
1941
|
-
console.log(chalk12.dim(` Port ${listenPort} requires root privileges.
|
|
1942
|
-
`));
|
|
1943
|
-
console.log(chalk12.cyan(" Solutions:\n"));
|
|
1944
|
-
console.log(chalk12.white(" Option 1: Restart nginx with sudo (recommended for port 80)"));
|
|
1945
|
-
console.log(chalk12.dim(" sudo pkill nginx && sudo nginx\n"));
|
|
1946
|
-
console.log(chalk12.white(" Option 2: Use a non-privileged port (no sudo needed)"));
|
|
1947
|
-
console.log(chalk12.dim(" bindler config set nginxListen 8080"));
|
|
1948
|
-
console.log(chalk12.dim(" bindler apply"));
|
|
1949
|
-
console.log(chalk12.dim(" # Then access via http://hostname:8080\n"));
|
|
1950
|
-
} else {
|
|
1951
|
-
console.log(chalk12.dim(" Try restarting nginx: brew services restart nginx"));
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
} else {
|
|
1955
|
-
console.log(chalk12.yellow(" - Skipped nginx reload (--no-reload)"));
|
|
1956
|
-
}
|
|
1957
|
-
const isDirectMode = defaults.mode === "direct";
|
|
1958
|
-
if (!hasProjects) {
|
|
1959
|
-
} else if (isDirectMode) {
|
|
1960
|
-
console.log(chalk12.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
|
|
1961
|
-
} else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
|
|
1962
|
-
console.log(chalk12.dim("\nConfiguring Cloudflare DNS routes..."));
|
|
1963
|
-
if (!isCloudflaredInstalled()) {
|
|
1964
|
-
console.log(chalk12.yellow(" - cloudflared not installed, skipping DNS routes"));
|
|
1965
|
-
} else {
|
|
1966
|
-
const dnsResults = routeDnsForAllProjects();
|
|
1967
|
-
if (dnsResults.length === 0) {
|
|
1968
|
-
console.log(chalk12.dim(" No hostnames to route"));
|
|
1969
|
-
} else {
|
|
1970
|
-
for (const result of dnsResults) {
|
|
1971
|
-
if (result.skipped) {
|
|
1972
|
-
console.log(chalk12.dim(` - ${result.hostname} (local - skipped)`));
|
|
1973
|
-
} else if (result.success) {
|
|
1974
|
-
const msg = result.output?.includes("already exists") ? "exists" : "routed";
|
|
1975
|
-
console.log(chalk12.green(` \u2713 ${result.hostname} (${msg})`));
|
|
1976
|
-
} else {
|
|
1977
|
-
console.log(chalk12.red(` \u2717 ${result.hostname}: ${result.error}`));
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
} else if (options.noCloudflare) {
|
|
1983
|
-
console.log(chalk12.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
|
|
1984
|
-
}
|
|
1985
|
-
if (hasProjects && isDirectMode && defaults.sslEnabled && options.ssl !== false) {
|
|
1986
|
-
console.log(chalk12.dim("\nSetting up SSL certificates..."));
|
|
1987
|
-
const hostnames = config.projects.filter((p) => p.enabled !== false && !p.local).map((p) => p.hostname);
|
|
1988
|
-
if (hostnames.length === 0) {
|
|
1989
|
-
console.log(chalk12.dim(" No hostnames to secure"));
|
|
1990
|
-
} else {
|
|
1991
|
-
const certbotResult = execCommandSafe("which certbot");
|
|
1992
|
-
if (!certbotResult.success) {
|
|
1993
|
-
console.log(chalk12.yellow(" - certbot not installed, skipping SSL"));
|
|
1994
|
-
console.log(chalk12.dim(" Run: bindler setup --direct"));
|
|
1995
|
-
} else {
|
|
1996
|
-
for (const hostname of hostnames) {
|
|
1997
|
-
console.log(chalk12.dim(` Requesting certificate for ${hostname}...`));
|
|
1998
|
-
const email = defaults.sslEmail || "admin@" + hostname.split(".").slice(-2).join(".");
|
|
1999
|
-
const result = execCommandSafe(
|
|
2000
|
-
`sudo certbot --nginx -d ${hostname} --non-interactive --agree-tos --email ${email} 2>&1`
|
|
2001
|
-
);
|
|
2002
|
-
if (result.success || result.output?.includes("Certificate not yet due for renewal")) {
|
|
2003
|
-
console.log(chalk12.green(` \u2713 ${hostname} (secured)`));
|
|
2004
|
-
} else if (result.output?.includes("already exists")) {
|
|
2005
|
-
console.log(chalk12.green(` \u2713 ${hostname} (exists)`));
|
|
2006
|
-
} else {
|
|
2007
|
-
console.log(chalk12.yellow(` ! ${hostname}: ${result.error || "failed"}`));
|
|
2008
|
-
console.log(chalk12.dim(" Run manually: sudo certbot --nginx -d " + hostname));
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
console.log(chalk12.green("\n\u2713 Configuration applied successfully!"));
|
|
2015
|
-
if (hasProjects) {
|
|
2016
|
-
console.log(chalk12.dim(`
|
|
2017
|
-
${config.projects.length} project(s) configured:`));
|
|
2018
|
-
for (const project of config.projects) {
|
|
2019
|
-
const status = project.enabled !== false ? chalk12.green("enabled") : chalk12.yellow("disabled");
|
|
2020
|
-
console.log(chalk12.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
|
|
2021
|
-
}
|
|
2022
|
-
} else {
|
|
2023
|
-
console.log(chalk12.dim("\nNo projects configured. Nginx config cleared."));
|
|
2024
|
-
}
|
|
2025
2030
|
}
|
|
2026
2031
|
|
|
2027
2032
|
// src/commands/doctor.ts
|
|
@@ -2333,7 +2338,7 @@ async function infoCommand() {
|
|
|
2333
2338
|
`));
|
|
2334
2339
|
console.log(chalk15.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
2335
2340
|
console.log(chalk15.white(" with Nginx and PM2\n"));
|
|
2336
|
-
console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.
|
|
2341
|
+
console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.1"));
|
|
2337
2342
|
console.log(chalk15.dim(" Author: ") + chalk15.white("alfaoz"));
|
|
2338
2343
|
console.log(chalk15.dim(" License: ") + chalk15.white("MIT"));
|
|
2339
2344
|
console.log(chalk15.dim(" GitHub: ") + chalk15.cyan("https://github.com/alfaoz/bindler"));
|
|
@@ -4075,28 +4080,28 @@ async function checkForUpdates() {
|
|
|
4075
4080
|
const cache = readCache();
|
|
4076
4081
|
const now = Date.now();
|
|
4077
4082
|
if (now - cache.lastCheck < CHECK_INTERVAL) {
|
|
4078
|
-
if (cache.latestVersion && compareVersions("1.6.
|
|
4083
|
+
if (cache.latestVersion && compareVersions("1.6.1", cache.latestVersion) > 0) {
|
|
4079
4084
|
showUpdateMessage(cache.latestVersion);
|
|
4080
4085
|
}
|
|
4081
4086
|
return;
|
|
4082
4087
|
}
|
|
4083
4088
|
fetchLatestVersion().then((latestVersion) => {
|
|
4084
4089
|
writeCache({ lastCheck: now, latestVersion });
|
|
4085
|
-
if (latestVersion && compareVersions("1.6.
|
|
4090
|
+
if (latestVersion && compareVersions("1.6.1", latestVersion) > 0) {
|
|
4086
4091
|
showUpdateMessage(latestVersion);
|
|
4087
4092
|
}
|
|
4088
4093
|
});
|
|
4089
4094
|
}
|
|
4090
4095
|
function showUpdateMessage(latestVersion) {
|
|
4091
4096
|
console.log("");
|
|
4092
|
-
console.log(chalk30.yellow(` Update available: ${"1.6.
|
|
4097
|
+
console.log(chalk30.yellow(` Update available: ${"1.6.1"} \u2192 ${latestVersion}`));
|
|
4093
4098
|
console.log(chalk30.dim(` Run: npm update -g bindler`));
|
|
4094
4099
|
console.log("");
|
|
4095
4100
|
}
|
|
4096
4101
|
|
|
4097
4102
|
// src/cli.ts
|
|
4098
4103
|
var program = new Command();
|
|
4099
|
-
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.
|
|
4104
|
+
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.1");
|
|
4100
4105
|
program.hook("preAction", async () => {
|
|
4101
4106
|
try {
|
|
4102
4107
|
initConfig();
|