bindler 1.4.1 → 1.6.1

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