bindler 1.6.0 → 1.6.2
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 +502 -466
- 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,229 @@ async function statusCommand() {
|
|
|
1336
1628
|
}
|
|
1337
1629
|
|
|
1338
1630
|
// src/commands/start.ts
|
|
1339
|
-
import
|
|
1631
|
+
import chalk6 from "chalk";
|
|
1632
|
+
var CRASH_CHECK_DELAY = 2500;
|
|
1633
|
+
function sleep(ms) {
|
|
1634
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1635
|
+
}
|
|
1340
1636
|
async function startCommand(name, options) {
|
|
1341
1637
|
if (options.all) {
|
|
1342
1638
|
const projects = listProjects();
|
|
1343
1639
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1344
1640
|
if (npmProjects.length === 0) {
|
|
1345
|
-
console.log(
|
|
1641
|
+
console.log(chalk6.yellow("No npm projects to start."));
|
|
1346
1642
|
return;
|
|
1347
1643
|
}
|
|
1348
|
-
console.log(
|
|
1644
|
+
console.log(chalk6.blue(`Starting ${npmProjects.length} npm project(s)...`));
|
|
1349
1645
|
const results = startAllProjects(npmProjects);
|
|
1350
1646
|
for (const result2 of results) {
|
|
1351
1647
|
if (result2.success) {
|
|
1352
|
-
console.log(
|
|
1648
|
+
console.log(chalk6.green(` \u2713 ${result2.name}`));
|
|
1353
1649
|
} else {
|
|
1354
|
-
console.log(
|
|
1650
|
+
console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1355
1651
|
}
|
|
1356
1652
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1653
|
+
console.log(chalk6.dim("\nChecking for crashes..."));
|
|
1654
|
+
await sleep(CRASH_CHECK_DELAY);
|
|
1655
|
+
const crashed = [];
|
|
1656
|
+
for (const project2 of npmProjects) {
|
|
1657
|
+
const process2 = getProcessByName(project2.name);
|
|
1658
|
+
if (process2 && process2.status !== "online") {
|
|
1659
|
+
crashed.push(project2.name);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (crashed.length > 0) {
|
|
1663
|
+
console.log(chalk6.red(`
|
|
1664
|
+
\u2717 ${crashed.length} project(s) crashed: ${crashed.join(", ")}`));
|
|
1665
|
+
console.log(chalk6.dim(` Run: bindler logs <name> to see errors`));
|
|
1666
|
+
} else {
|
|
1667
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
1668
|
+
console.log(chalk6.green(`\u2713 ${succeeded}/${results.length} running`));
|
|
1669
|
+
}
|
|
1360
1670
|
return;
|
|
1361
1671
|
}
|
|
1362
1672
|
if (!name) {
|
|
1363
|
-
console.error(
|
|
1673
|
+
console.error(chalk6.red("Error: Project name is required. Use --all to start all projects."));
|
|
1364
1674
|
process.exit(1);
|
|
1365
1675
|
}
|
|
1366
1676
|
const project = getProject(name);
|
|
1367
1677
|
if (!project) {
|
|
1368
|
-
console.error(
|
|
1678
|
+
console.error(chalk6.red(`Error: Project "${name}" not found`));
|
|
1369
1679
|
process.exit(1);
|
|
1370
1680
|
}
|
|
1371
1681
|
if (project.type !== "npm") {
|
|
1372
|
-
console.log(
|
|
1373
|
-
console.log(
|
|
1682
|
+
console.log(chalk6.blue(`Project "${name}" is a static site - no process to start.`));
|
|
1683
|
+
console.log(chalk6.dim("Static sites are served directly by nginx."));
|
|
1374
1684
|
return;
|
|
1375
1685
|
}
|
|
1376
|
-
console.log(
|
|
1686
|
+
console.log(chalk6.blue(`Starting ${name}...`));
|
|
1377
1687
|
const result = startProject(project);
|
|
1378
|
-
if (result.success) {
|
|
1379
|
-
console.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1688
|
+
if (!result.success) {
|
|
1689
|
+
console.error(chalk6.red(`\u2717 Failed to start ${name}: ${result.error}`));
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
console.log(chalk6.dim("Waiting for process to stabilize..."));
|
|
1693
|
+
await sleep(CRASH_CHECK_DELAY);
|
|
1694
|
+
const processStatus = getProcessByName(name);
|
|
1695
|
+
if (!processStatus || processStatus.status !== "online") {
|
|
1696
|
+
console.error(chalk6.red(`
|
|
1697
|
+
\u2717 ${name} crashed immediately after starting`));
|
|
1698
|
+
console.log(chalk6.dim("\nShowing recent logs:\n"));
|
|
1699
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
1700
|
+
await showLogs(name, false, 30);
|
|
1701
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
1702
|
+
console.log(chalk6.yellow(`
|
|
1703
|
+
Fix the error and try again: bindler start ${name}`));
|
|
1384
1704
|
process.exit(1);
|
|
1385
1705
|
}
|
|
1706
|
+
console.log(chalk6.green(`\u2713 ${name} started successfully`));
|
|
1707
|
+
console.log(chalk6.dim(` Port: ${project.port}`));
|
|
1708
|
+
console.log(chalk6.dim(` URL: https://${project.hostname}`));
|
|
1386
1709
|
}
|
|
1387
1710
|
|
|
1388
1711
|
// src/commands/stop.ts
|
|
1389
|
-
import
|
|
1712
|
+
import chalk7 from "chalk";
|
|
1390
1713
|
async function stopCommand(name, options) {
|
|
1391
1714
|
if (options.all) {
|
|
1392
1715
|
const projects = listProjects();
|
|
1393
1716
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1394
1717
|
if (npmProjects.length === 0) {
|
|
1395
|
-
console.log(
|
|
1718
|
+
console.log(chalk7.yellow("No npm projects to stop."));
|
|
1396
1719
|
return;
|
|
1397
1720
|
}
|
|
1398
|
-
console.log(
|
|
1721
|
+
console.log(chalk7.blue(`Stopping ${npmProjects.length} npm project(s)...`));
|
|
1399
1722
|
const results = stopAllProjects(npmProjects);
|
|
1400
1723
|
for (const result2 of results) {
|
|
1401
1724
|
if (result2.success) {
|
|
1402
|
-
console.log(
|
|
1725
|
+
console.log(chalk7.green(` \u2713 ${result2.name}`));
|
|
1403
1726
|
} else {
|
|
1404
|
-
console.log(
|
|
1727
|
+
console.log(chalk7.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1405
1728
|
}
|
|
1406
1729
|
}
|
|
1407
1730
|
const succeeded = results.filter((r) => r.success).length;
|
|
1408
|
-
console.log(
|
|
1731
|
+
console.log(chalk7.dim(`
|
|
1409
1732
|
${succeeded}/${results.length} stopped successfully`));
|
|
1410
1733
|
return;
|
|
1411
1734
|
}
|
|
1412
1735
|
if (!name) {
|
|
1413
|
-
console.error(
|
|
1736
|
+
console.error(chalk7.red("Error: Project name is required. Use --all to stop all projects."));
|
|
1414
1737
|
process.exit(1);
|
|
1415
1738
|
}
|
|
1416
1739
|
const project = getProject(name);
|
|
1417
1740
|
if (!project) {
|
|
1418
|
-
console.error(
|
|
1741
|
+
console.error(chalk7.red(`Error: Project "${name}" not found`));
|
|
1419
1742
|
process.exit(1);
|
|
1420
1743
|
}
|
|
1421
1744
|
if (project.type !== "npm") {
|
|
1422
|
-
console.log(
|
|
1745
|
+
console.log(chalk7.yellow(`Project "${name}" is a static site - no process to stop.`));
|
|
1423
1746
|
return;
|
|
1424
1747
|
}
|
|
1425
|
-
console.log(
|
|
1748
|
+
console.log(chalk7.blue(`Stopping ${name}...`));
|
|
1426
1749
|
const result = stopProject(name);
|
|
1427
1750
|
if (result.success) {
|
|
1428
|
-
console.log(
|
|
1751
|
+
console.log(chalk7.green(`\u2713 ${name} stopped successfully`));
|
|
1429
1752
|
} else {
|
|
1430
|
-
console.error(
|
|
1753
|
+
console.error(chalk7.red(`\u2717 Failed to stop ${name}: ${result.error}`));
|
|
1431
1754
|
process.exit(1);
|
|
1432
1755
|
}
|
|
1433
1756
|
}
|
|
1434
1757
|
|
|
1435
1758
|
// src/commands/restart.ts
|
|
1436
|
-
import
|
|
1759
|
+
import chalk8 from "chalk";
|
|
1437
1760
|
async function restartCommand(name, options) {
|
|
1438
1761
|
if (options.all) {
|
|
1439
1762
|
const projects = listProjects();
|
|
1440
1763
|
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
1441
1764
|
if (npmProjects.length === 0) {
|
|
1442
|
-
console.log(
|
|
1765
|
+
console.log(chalk8.yellow("No npm projects to restart."));
|
|
1443
1766
|
return;
|
|
1444
1767
|
}
|
|
1445
|
-
console.log(
|
|
1768
|
+
console.log(chalk8.blue(`Restarting ${npmProjects.length} npm project(s)...`));
|
|
1446
1769
|
const results = restartAllProjects(npmProjects);
|
|
1447
1770
|
for (const result2 of results) {
|
|
1448
1771
|
if (result2.success) {
|
|
1449
|
-
console.log(
|
|
1772
|
+
console.log(chalk8.green(` \u2713 ${result2.name}`));
|
|
1450
1773
|
} else {
|
|
1451
|
-
console.log(
|
|
1774
|
+
console.log(chalk8.red(` \u2717 ${result2.name}: ${result2.error}`));
|
|
1452
1775
|
}
|
|
1453
1776
|
}
|
|
1454
1777
|
const succeeded = results.filter((r) => r.success).length;
|
|
1455
|
-
console.log(
|
|
1778
|
+
console.log(chalk8.dim(`
|
|
1456
1779
|
${succeeded}/${results.length} restarted successfully`));
|
|
1457
1780
|
return;
|
|
1458
1781
|
}
|
|
1459
1782
|
if (!name) {
|
|
1460
|
-
console.error(
|
|
1783
|
+
console.error(chalk8.red("Error: Project name is required. Use --all to restart all projects."));
|
|
1461
1784
|
process2.exit(1);
|
|
1462
1785
|
}
|
|
1463
1786
|
const project = getProject(name);
|
|
1464
1787
|
if (!project) {
|
|
1465
|
-
console.error(
|
|
1788
|
+
console.error(chalk8.red(`Error: Project "${name}" not found`));
|
|
1466
1789
|
process2.exit(1);
|
|
1467
1790
|
}
|
|
1468
1791
|
if (project.type !== "npm") {
|
|
1469
|
-
console.log(
|
|
1792
|
+
console.log(chalk8.yellow(`Project "${name}" is a static site - no process to restart.`));
|
|
1470
1793
|
return;
|
|
1471
1794
|
}
|
|
1472
|
-
console.log(
|
|
1795
|
+
console.log(chalk8.blue(`Restarting ${name}...`));
|
|
1473
1796
|
const process2 = getProcessByName(name);
|
|
1474
1797
|
let result;
|
|
1475
1798
|
if (process2) {
|
|
1476
1799
|
result = restartProject(name);
|
|
1477
1800
|
} else {
|
|
1478
|
-
console.log(
|
|
1801
|
+
console.log(chalk8.dim("Process not running, starting..."));
|
|
1479
1802
|
result = startProject(project);
|
|
1480
1803
|
}
|
|
1481
1804
|
if (result.success) {
|
|
1482
|
-
console.log(
|
|
1805
|
+
console.log(chalk8.green(`\u2713 ${name} restarted successfully`));
|
|
1483
1806
|
} else {
|
|
1484
|
-
console.error(
|
|
1807
|
+
console.error(chalk8.red(`\u2717 Failed to restart ${name}: ${result.error}`));
|
|
1485
1808
|
process2.exit(1);
|
|
1486
1809
|
}
|
|
1487
1810
|
}
|
|
1488
1811
|
|
|
1489
1812
|
// src/commands/logs.ts
|
|
1490
|
-
import
|
|
1813
|
+
import chalk9 from "chalk";
|
|
1491
1814
|
async function logsCommand(name, options) {
|
|
1492
1815
|
const project = getProject(name);
|
|
1493
1816
|
if (!project) {
|
|
1494
|
-
console.error(
|
|
1817
|
+
console.error(chalk9.red(`Error: Project "${name}" not found`));
|
|
1495
1818
|
process2.exit(1);
|
|
1496
1819
|
}
|
|
1497
1820
|
if (project.type !== "npm") {
|
|
1498
|
-
console.log(
|
|
1499
|
-
console.log(
|
|
1500
|
-
console.log(
|
|
1501
|
-
console.log(
|
|
1821
|
+
console.log(chalk9.yellow(`Project "${name}" is a static site - no logs available.`));
|
|
1822
|
+
console.log(chalk9.dim("Check nginx access/error logs instead:"));
|
|
1823
|
+
console.log(chalk9.dim(" /var/log/nginx/access.log"));
|
|
1824
|
+
console.log(chalk9.dim(" /var/log/nginx/error.log"));
|
|
1502
1825
|
return;
|
|
1503
1826
|
}
|
|
1504
1827
|
const process2 = getProcessByName(name);
|
|
1505
1828
|
if (!process2) {
|
|
1506
|
-
console.log(
|
|
1507
|
-
console.log(
|
|
1829
|
+
console.log(chalk9.yellow(`Project "${name}" has not been started yet.`));
|
|
1830
|
+
console.log(chalk9.dim(`Run ${chalk9.cyan(`bindler start ${name}`)} first.`));
|
|
1508
1831
|
return;
|
|
1509
1832
|
}
|
|
1510
1833
|
const lines = options.lines || 200;
|
|
1511
1834
|
const follow = options.follow || false;
|
|
1512
1835
|
if (follow) {
|
|
1513
|
-
console.log(
|
|
1836
|
+
console.log(chalk9.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
|
|
1514
1837
|
}
|
|
1515
1838
|
await showLogs(name, follow, lines);
|
|
1516
1839
|
}
|
|
1517
1840
|
|
|
1518
1841
|
// src/commands/update.ts
|
|
1519
|
-
import
|
|
1520
|
-
import { existsSync as
|
|
1842
|
+
import chalk10 from "chalk";
|
|
1843
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1521
1844
|
async function updateCommand(name, options) {
|
|
1522
1845
|
const project = getProject(name);
|
|
1523
1846
|
if (!project) {
|
|
1524
|
-
console.error(
|
|
1847
|
+
console.error(chalk10.red(`Error: Project "${name}" not found`));
|
|
1525
1848
|
process.exit(1);
|
|
1526
1849
|
}
|
|
1527
1850
|
const updates = {};
|
|
1528
1851
|
if (options.hostname) {
|
|
1529
1852
|
if (!validateHostname(options.hostname)) {
|
|
1530
|
-
console.error(
|
|
1853
|
+
console.error(chalk10.red("Error: Invalid hostname format"));
|
|
1531
1854
|
process.exit(1);
|
|
1532
1855
|
}
|
|
1533
1856
|
updates.hostname = options.hostname;
|
|
@@ -1535,11 +1858,11 @@ async function updateCommand(name, options) {
|
|
|
1535
1858
|
if (options.port) {
|
|
1536
1859
|
const port = parseInt(options.port, 10);
|
|
1537
1860
|
if (!validatePort(port)) {
|
|
1538
|
-
console.error(
|
|
1861
|
+
console.error(chalk10.red("Error: Invalid port. Use a number between 1024 and 65535."));
|
|
1539
1862
|
process.exit(1);
|
|
1540
1863
|
}
|
|
1541
1864
|
if (!isPortAvailable(port) && port !== project.port) {
|
|
1542
|
-
console.error(
|
|
1865
|
+
console.error(chalk10.red(`Error: Port ${port} is already in use by another project.`));
|
|
1543
1866
|
process.exit(1);
|
|
1544
1867
|
}
|
|
1545
1868
|
updates.port = port;
|
|
@@ -1549,20 +1872,20 @@ async function updateCommand(name, options) {
|
|
|
1549
1872
|
}
|
|
1550
1873
|
if (options.start) {
|
|
1551
1874
|
if (project.type !== "npm") {
|
|
1552
|
-
console.error(
|
|
1875
|
+
console.error(chalk10.red("Error: Start command only applies to npm projects"));
|
|
1553
1876
|
process.exit(1);
|
|
1554
1877
|
}
|
|
1555
1878
|
updates.start = options.start;
|
|
1556
1879
|
}
|
|
1557
1880
|
if (options.path) {
|
|
1558
|
-
if (!
|
|
1559
|
-
console.error(
|
|
1881
|
+
if (!existsSync8(options.path)) {
|
|
1882
|
+
console.error(chalk10.red(`Error: Path does not exist: ${options.path}`));
|
|
1560
1883
|
process.exit(1);
|
|
1561
1884
|
}
|
|
1562
1885
|
if (isProtectedPath(options.path)) {
|
|
1563
|
-
console.error(
|
|
1564
|
-
console.error(
|
|
1565
|
-
console.error(
|
|
1886
|
+
console.error(chalk10.red(`Error: Path is in a macOS protected folder (Desktop/Documents/Downloads).`));
|
|
1887
|
+
console.error(chalk10.yellow(`Nginx cannot access these folders without Full Disk Access.`));
|
|
1888
|
+
console.error(chalk10.dim(`Move your project to ~/projects or another accessible location.`));
|
|
1566
1889
|
process.exit(1);
|
|
1567
1890
|
}
|
|
1568
1891
|
updates.path = options.path;
|
|
@@ -1572,7 +1895,7 @@ async function updateCommand(name, options) {
|
|
|
1572
1895
|
for (const envStr of options.env) {
|
|
1573
1896
|
const [key, ...valueParts] = envStr.split("=");
|
|
1574
1897
|
if (!key) {
|
|
1575
|
-
console.error(
|
|
1898
|
+
console.error(chalk10.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
|
|
1576
1899
|
process.exit(1);
|
|
1577
1900
|
}
|
|
1578
1901
|
env[key] = valueParts.join("=");
|
|
@@ -1585,23 +1908,23 @@ async function updateCommand(name, options) {
|
|
|
1585
1908
|
updates.enabled = false;
|
|
1586
1909
|
}
|
|
1587
1910
|
if (Object.keys(updates).length === 0) {
|
|
1588
|
-
console.log(
|
|
1589
|
-
console.log(
|
|
1911
|
+
console.log(chalk10.yellow("No updates specified."));
|
|
1912
|
+
console.log(chalk10.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
|
|
1590
1913
|
return;
|
|
1591
1914
|
}
|
|
1592
1915
|
try {
|
|
1593
1916
|
updateProject(name, updates);
|
|
1594
|
-
console.log(
|
|
1917
|
+
console.log(chalk10.green(`\u2713 Project "${name}" updated successfully`));
|
|
1595
1918
|
for (const [key, value] of Object.entries(updates)) {
|
|
1596
|
-
console.log(
|
|
1919
|
+
console.log(chalk10.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
|
|
1597
1920
|
}
|
|
1598
|
-
console.log(
|
|
1599
|
-
Run ${
|
|
1921
|
+
console.log(chalk10.dim(`
|
|
1922
|
+
Run ${chalk10.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1600
1923
|
if (project.type === "npm" && (updates.port || updates.start || updates.env)) {
|
|
1601
|
-
console.log(
|
|
1924
|
+
console.log(chalk10.dim(`Run ${chalk10.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
|
|
1602
1925
|
}
|
|
1603
1926
|
} catch (error) {
|
|
1604
|
-
console.error(
|
|
1927
|
+
console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1605
1928
|
process.exit(1);
|
|
1606
1929
|
}
|
|
1607
1930
|
}
|
|
@@ -1610,20 +1933,20 @@ Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
|
1610
1933
|
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1611
1934
|
import { tmpdir } from "os";
|
|
1612
1935
|
import { join as join6 } from "path";
|
|
1613
|
-
import
|
|
1936
|
+
import chalk11 from "chalk";
|
|
1614
1937
|
async function editCommand(name) {
|
|
1615
1938
|
const project = getProject(name);
|
|
1616
1939
|
if (!project) {
|
|
1617
|
-
console.error(
|
|
1940
|
+
console.error(chalk11.red(`Error: Project "${name}" not found`));
|
|
1618
1941
|
process.exit(1);
|
|
1619
1942
|
}
|
|
1620
1943
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1621
1944
|
const tmpFile = join6(tmpdir(), `bindler-${name}-${Date.now()}.json`);
|
|
1622
1945
|
writeFileSync4(tmpFile, JSON.stringify(project, null, 2) + "\n");
|
|
1623
|
-
console.log(
|
|
1946
|
+
console.log(chalk11.dim(`Opening ${name} config in ${editor}...`));
|
|
1624
1947
|
const exitCode = await spawnInteractive(editor, [tmpFile]);
|
|
1625
1948
|
if (exitCode !== 0) {
|
|
1626
|
-
console.error(
|
|
1949
|
+
console.error(chalk11.red("Editor exited with error"));
|
|
1627
1950
|
unlinkSync(tmpFile);
|
|
1628
1951
|
process.exit(1);
|
|
1629
1952
|
}
|
|
@@ -1631,7 +1954,7 @@ async function editCommand(name) {
|
|
|
1631
1954
|
try {
|
|
1632
1955
|
editedContent = readFileSync4(tmpFile, "utf-8");
|
|
1633
1956
|
} catch (error) {
|
|
1634
|
-
console.error(
|
|
1957
|
+
console.error(chalk11.red("Failed to read edited file"));
|
|
1635
1958
|
process.exit(1);
|
|
1636
1959
|
} finally {
|
|
1637
1960
|
unlinkSync(tmpFile);
|
|
@@ -1640,44 +1963,44 @@ async function editCommand(name) {
|
|
|
1640
1963
|
try {
|
|
1641
1964
|
editedProject = JSON.parse(editedContent);
|
|
1642
1965
|
} catch (error) {
|
|
1643
|
-
console.error(
|
|
1966
|
+
console.error(chalk11.red("Error: Invalid JSON in edited file"));
|
|
1644
1967
|
process.exit(1);
|
|
1645
1968
|
}
|
|
1646
1969
|
if (editedProject.name !== project.name) {
|
|
1647
|
-
console.error(
|
|
1970
|
+
console.error(chalk11.red("Error: Cannot change project name via edit. Use a new project instead."));
|
|
1648
1971
|
process.exit(1);
|
|
1649
1972
|
}
|
|
1650
1973
|
const originalStr = JSON.stringify(project);
|
|
1651
1974
|
const editedStr = JSON.stringify(editedProject);
|
|
1652
1975
|
if (originalStr === editedStr) {
|
|
1653
|
-
console.log(
|
|
1976
|
+
console.log(chalk11.yellow("No changes made."));
|
|
1654
1977
|
return;
|
|
1655
1978
|
}
|
|
1656
1979
|
try {
|
|
1657
1980
|
const config = readConfig();
|
|
1658
1981
|
const index = config.projects.findIndex((p) => p.name === name);
|
|
1659
1982
|
if (index === -1) {
|
|
1660
|
-
console.error(
|
|
1983
|
+
console.error(chalk11.red("Error: Project not found"));
|
|
1661
1984
|
process.exit(1);
|
|
1662
1985
|
}
|
|
1663
1986
|
config.projects[index] = editedProject;
|
|
1664
1987
|
writeConfig(config);
|
|
1665
|
-
console.log(
|
|
1666
|
-
console.log(
|
|
1667
|
-
Run ${
|
|
1988
|
+
console.log(chalk11.green(`\u2713 Project "${name}" updated successfully`));
|
|
1989
|
+
console.log(chalk11.dim(`
|
|
1990
|
+
Run ${chalk11.cyan("sudo bindler apply")} to apply changes to nginx.`));
|
|
1668
1991
|
} catch (error) {
|
|
1669
|
-
console.error(
|
|
1992
|
+
console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1670
1993
|
process.exit(1);
|
|
1671
1994
|
}
|
|
1672
1995
|
}
|
|
1673
1996
|
|
|
1674
1997
|
// src/commands/remove.ts
|
|
1675
1998
|
import inquirer2 from "inquirer";
|
|
1676
|
-
import
|
|
1999
|
+
import chalk12 from "chalk";
|
|
1677
2000
|
async function removeCommand(name, options) {
|
|
1678
2001
|
const project = getProject(name);
|
|
1679
2002
|
if (!project) {
|
|
1680
|
-
console.error(
|
|
2003
|
+
console.error(chalk12.red(`Error: Project "${name}" not found`));
|
|
1681
2004
|
process.exit(1);
|
|
1682
2005
|
}
|
|
1683
2006
|
if (!options.force) {
|
|
@@ -1690,338 +2013,51 @@ async function removeCommand(name, options) {
|
|
|
1690
2013
|
}
|
|
1691
2014
|
]);
|
|
1692
2015
|
if (!confirm) {
|
|
1693
|
-
console.log(
|
|
2016
|
+
console.log(chalk12.yellow("Cancelled."));
|
|
1694
2017
|
return;
|
|
1695
2018
|
}
|
|
1696
2019
|
}
|
|
1697
2020
|
if (project.type === "npm") {
|
|
1698
2021
|
const process2 = getProcessByName(name);
|
|
1699
2022
|
if (process2) {
|
|
1700
|
-
console.log(
|
|
2023
|
+
console.log(chalk12.dim("Stopping PM2 process..."));
|
|
1701
2024
|
deleteProject(name);
|
|
1702
2025
|
}
|
|
1703
2026
|
}
|
|
1704
2027
|
try {
|
|
1705
2028
|
removeProject(name);
|
|
1706
|
-
console.log(
|
|
2029
|
+
console.log(chalk12.green(`\u2713 Project "${name}" removed from registry`));
|
|
1707
2030
|
if (options.apply) {
|
|
1708
|
-
console.log(
|
|
2031
|
+
console.log(chalk12.dim("\nApplying nginx configuration..."));
|
|
1709
2032
|
const config = readConfig();
|
|
1710
2033
|
try {
|
|
1711
2034
|
writeNginxConfig(config);
|
|
1712
2035
|
const testResult = testNginxConfig();
|
|
1713
2036
|
if (testResult.success) {
|
|
1714
2037
|
reloadNginx();
|
|
1715
|
-
console.log(
|
|
2038
|
+
console.log(chalk12.green("\u2713 Nginx configuration updated"));
|
|
1716
2039
|
} else {
|
|
1717
|
-
console.log(
|
|
2040
|
+
console.log(chalk12.yellow("! Nginx config test failed, reload skipped"));
|
|
1718
2041
|
}
|
|
1719
2042
|
} catch (err) {
|
|
1720
|
-
console.log(
|
|
1721
|
-
console.log(
|
|
1722
|
-
}
|
|
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++;
|
|
2043
|
+
console.log(chalk12.yellow(`! Failed to update nginx: ${err}`));
|
|
2044
|
+
console.log(chalk12.dim(" Try running: sudo bindler apply"));
|
|
1847
2045
|
}
|
|
1848
|
-
}
|
|
1849
|
-
if (synced === 0) {
|
|
1850
|
-
console.log(chalk12.dim(" No bindler.yaml files found in project directories"));
|
|
1851
2046
|
} else {
|
|
1852
2047
|
console.log(chalk12.dim(`
|
|
1853
|
-
|
|
1854
|
-
`));
|
|
2048
|
+
Run ${chalk12.cyan("sudo bindler apply")} to update nginx configuration.`));
|
|
1855
2049
|
}
|
|
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"));
|
|
2050
|
+
console.log(chalk12.yellow("\nNote: Project files were not deleted."));
|
|
2051
|
+
console.log(chalk12.dim(` Path: ${project.path}`));
|
|
2052
|
+
if (!project.local) {
|
|
2053
|
+
console.log(chalk12.yellow("\nCloudflare DNS route was not removed."));
|
|
2054
|
+
console.log(chalk12.dim(" Remove it manually from the Cloudflare dashboard:"));
|
|
2055
|
+
console.log(chalk12.dim(" https://dash.cloudflare.com \u2192 DNS \u2192 Records \u2192 Delete the CNAME for " + project.hostname));
|
|
1880
2056
|
}
|
|
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
2057
|
} 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
|
-
}
|
|
1901
|
-
process.exit(1);
|
|
1902
|
-
}
|
|
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."));
|
|
2058
|
+
console.error(chalk12.red(`Error: ${error instanceof Error ? error.message : error}`));
|
|
1922
2059
|
process.exit(1);
|
|
1923
2060
|
}
|
|
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
2061
|
}
|
|
2026
2062
|
|
|
2027
2063
|
// src/commands/doctor.ts
|
|
@@ -2333,7 +2369,7 @@ async function infoCommand() {
|
|
|
2333
2369
|
`));
|
|
2334
2370
|
console.log(chalk15.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
2335
2371
|
console.log(chalk15.white(" with Nginx and PM2\n"));
|
|
2336
|
-
console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.
|
|
2372
|
+
console.log(chalk15.dim(" Version: ") + chalk15.white("1.6.2"));
|
|
2337
2373
|
console.log(chalk15.dim(" Author: ") + chalk15.white("alfaoz"));
|
|
2338
2374
|
console.log(chalk15.dim(" License: ") + chalk15.white("MIT"));
|
|
2339
2375
|
console.log(chalk15.dim(" GitHub: ") + chalk15.cyan("https://github.com/alfaoz/bindler"));
|
|
@@ -4075,28 +4111,28 @@ async function checkForUpdates() {
|
|
|
4075
4111
|
const cache = readCache();
|
|
4076
4112
|
const now = Date.now();
|
|
4077
4113
|
if (now - cache.lastCheck < CHECK_INTERVAL) {
|
|
4078
|
-
if (cache.latestVersion && compareVersions("1.6.
|
|
4114
|
+
if (cache.latestVersion && compareVersions("1.6.2", cache.latestVersion) > 0) {
|
|
4079
4115
|
showUpdateMessage(cache.latestVersion);
|
|
4080
4116
|
}
|
|
4081
4117
|
return;
|
|
4082
4118
|
}
|
|
4083
4119
|
fetchLatestVersion().then((latestVersion) => {
|
|
4084
4120
|
writeCache({ lastCheck: now, latestVersion });
|
|
4085
|
-
if (latestVersion && compareVersions("1.6.
|
|
4121
|
+
if (latestVersion && compareVersions("1.6.2", latestVersion) > 0) {
|
|
4086
4122
|
showUpdateMessage(latestVersion);
|
|
4087
4123
|
}
|
|
4088
4124
|
});
|
|
4089
4125
|
}
|
|
4090
4126
|
function showUpdateMessage(latestVersion) {
|
|
4091
4127
|
console.log("");
|
|
4092
|
-
console.log(chalk30.yellow(` Update available: ${"1.6.
|
|
4128
|
+
console.log(chalk30.yellow(` Update available: ${"1.6.2"} \u2192 ${latestVersion}`));
|
|
4093
4129
|
console.log(chalk30.dim(` Run: npm update -g bindler`));
|
|
4094
4130
|
console.log("");
|
|
4095
4131
|
}
|
|
4096
4132
|
|
|
4097
4133
|
// src/cli.ts
|
|
4098
4134
|
var program = new Command();
|
|
4099
|
-
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.
|
|
4135
|
+
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.6.2");
|
|
4100
4136
|
program.hook("preAction", async () => {
|
|
4101
4137
|
try {
|
|
4102
4138
|
initConfig();
|