bindler 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1317 -583
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +4 -2
package/dist/cli.js CHANGED
@@ -1,14 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli.ts
4
10
  import { Command } from "commander";
5
- import chalk27 from "chalk";
11
+ import chalk30 from "chalk";
6
12
 
7
13
  // src/commands/new.ts
8
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
9
- import { basename } from "path";
14
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
15
+ import { basename as basename2 } from "path";
10
16
  import inquirer from "inquirer";
11
- import chalk from "chalk";
17
+ import chalk2 from "chalk";
12
18
 
13
19
  // src/lib/config.ts
14
20
  import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "fs";
@@ -140,6 +146,19 @@ function getDefaults() {
140
146
  const config = readConfig();
141
147
  return config.defaults;
142
148
  }
149
+ function listProjectsForEnv(env = "production") {
150
+ const config = readConfig();
151
+ return config.projects.map((project) => {
152
+ const envConfig = project.environments?.[env];
153
+ if (!envConfig) return project;
154
+ return {
155
+ ...project,
156
+ hostname: envConfig.hostname || project.hostname,
157
+ port: envConfig.port || project.port,
158
+ env: { ...project.env, ...envConfig.env }
159
+ };
160
+ });
161
+ }
143
162
 
144
163
  // src/lib/utils.ts
145
164
  import { execSync, spawn } from "child_process";
@@ -277,6 +296,60 @@ function getPortsTable() {
277
296
  // src/lib/nginx.ts
278
297
  import { existsSync as existsSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2 } from "fs";
279
298
  import { dirname as dirname2, join as join3 } from "path";
299
+ import { createHash } from "crypto";
300
+ function generateHtpasswdEntry(username, password) {
301
+ const result = execCommandSafe(`openssl passwd -apr1 '${password.replace(/'/g, "'\\''")}'`);
302
+ if (result.success && result.output) {
303
+ return `${username}:${result.output.trim()}`;
304
+ }
305
+ const hash = createHash("md5").update(password).digest("base64");
306
+ return `${username}:{PLAIN}${password}`;
307
+ }
308
+ function generateSecurityDirectives(project, indent) {
309
+ const lines = [];
310
+ const security = project.security;
311
+ if (!security) return lines;
312
+ if (security.basicAuth?.enabled) {
313
+ const realm = security.basicAuth.realm || "Restricted";
314
+ const htpasswdPath = security.basicAuth.htpasswdPath || join3(getGeneratedDir(), `htpasswd-${project.name}`);
315
+ lines.push(`${indent} auth_basic "${realm}";`);
316
+ lines.push(`${indent} auth_basic_user_file ${htpasswdPath};`);
317
+ }
318
+ if (security.ipBlocklist?.length) {
319
+ for (const ip of security.ipBlocklist) {
320
+ lines.push(`${indent} deny ${ip};`);
321
+ }
322
+ }
323
+ if (security.ipAllowlist?.length) {
324
+ for (const ip of security.ipAllowlist) {
325
+ lines.push(`${indent} allow ${ip};`);
326
+ }
327
+ lines.push(`${indent} deny all;`);
328
+ }
329
+ if (security.rateLimit?.enabled) {
330
+ const burst = security.rateLimit.burst || 20;
331
+ lines.push(`${indent} limit_req zone=limit_${project.name} burst=${burst} nodelay;`);
332
+ }
333
+ if (security.headers) {
334
+ const h = security.headers;
335
+ if (h.hsts) {
336
+ lines.push(`${indent} add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;`);
337
+ }
338
+ if (h.xFrameOptions) {
339
+ lines.push(`${indent} add_header X-Frame-Options "${h.xFrameOptions}" always;`);
340
+ }
341
+ if (h.xContentTypeOptions) {
342
+ lines.push(`${indent} add_header X-Content-Type-Options "nosniff" always;`);
343
+ }
344
+ if (h.xXssProtection) {
345
+ lines.push(`${indent} add_header X-XSS-Protection "1; mode=block" always;`);
346
+ }
347
+ if (h.contentSecurityPolicy) {
348
+ lines.push(`${indent} add_header Content-Security-Policy "${h.contentSecurityPolicy}" always;`);
349
+ }
350
+ }
351
+ return lines;
352
+ }
280
353
  function generateLocationBlock(project, indent = " ") {
281
354
  const lines = [];
282
355
  const locationPath = project.basePath || "/";
@@ -290,6 +363,7 @@ function generateLocationBlock(project, indent = " ") {
290
363
  }
291
364
  lines.push(`${indent} index index.html index.htm;`);
292
365
  lines.push(`${indent} try_files $uri $uri/ =404;`);
366
+ lines.push(...generateSecurityDirectives(project, indent));
293
367
  lines.push(`${indent}}`);
294
368
  } else if (project.type === "npm") {
295
369
  lines.push(`${indent}location ${locationPath} {`);
@@ -302,10 +376,22 @@ function generateLocationBlock(project, indent = " ") {
302
376
  lines.push(`${indent} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
303
377
  lines.push(`${indent} proxy_set_header X-Forwarded-Proto $scheme;`);
304
378
  lines.push(`${indent} proxy_cache_bypass $http_upgrade;`);
379
+ lines.push(...generateSecurityDirectives(project, indent));
305
380
  lines.push(`${indent}}`);
306
381
  }
307
382
  return lines;
308
383
  }
384
+ function generateHtpasswdFiles(projects) {
385
+ for (const project of projects) {
386
+ if (project.security?.basicAuth?.enabled && project.security.basicAuth.users?.length) {
387
+ const htpasswdPath = project.security.basicAuth.htpasswdPath || join3(getGeneratedDir(), `htpasswd-${project.name}`);
388
+ const entries = project.security.basicAuth.users.map(
389
+ (u) => generateHtpasswdEntry(u.username, u.password)
390
+ );
391
+ writeFileSync2(htpasswdPath, entries.join("\n") + "\n");
392
+ }
393
+ }
394
+ }
309
395
  function generateNginxConfig(config) {
310
396
  const { defaults, projects } = config;
311
397
  const listen = defaults.nginxListen;
@@ -314,6 +400,17 @@ function generateNginxConfig(config) {
314
400
  `# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
315
401
  ""
316
402
  ];
403
+ const rateLimitedProjects = projects.filter(
404
+ (p) => p.enabled !== false && p.security?.rateLimit?.enabled
405
+ );
406
+ if (rateLimitedProjects.length > 0) {
407
+ lines.push("# Rate limiting zones");
408
+ for (const project of rateLimitedProjects) {
409
+ const rps = project.security?.rateLimit?.requestsPerSecond || 10;
410
+ lines.push(`limit_req_zone $binary_remote_addr zone=limit_${project.name}:10m rate=${rps}r/s;`);
411
+ }
412
+ lines.push("");
413
+ }
317
414
  const hostGroups = /* @__PURE__ */ new Map();
318
415
  for (const project of projects) {
319
416
  if (project.enabled === false) {
@@ -550,9 +647,228 @@ function restartAllProjects(projects) {
550
647
  }));
551
648
  }
552
649
 
650
+ // src/lib/yaml.ts
651
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
652
+ import { join as join4, basename } from "path";
653
+ import yaml from "js-yaml";
654
+ var YAML_FILENAME = "bindler.yaml";
655
+ function readBindlerYaml(dir) {
656
+ const yamlPath = join4(dir, YAML_FILENAME);
657
+ if (!existsSync4(yamlPath)) {
658
+ return null;
659
+ }
660
+ try {
661
+ const content = readFileSync3(yamlPath, "utf-8");
662
+ const parsed = yaml.load(content);
663
+ return parsed || null;
664
+ } catch {
665
+ return null;
666
+ }
667
+ }
668
+ function yamlToProject(yamlConfig, dir) {
669
+ const project = {
670
+ name: yamlConfig.name || basename(dir),
671
+ type: yamlConfig.type || "static",
672
+ path: dir,
673
+ hostname: yamlConfig.hostname,
674
+ basePath: yamlConfig.basePath,
675
+ port: yamlConfig.port,
676
+ start: yamlConfig.start,
677
+ env: yamlConfig.env,
678
+ local: yamlConfig.local,
679
+ security: yamlConfig.security,
680
+ environments: yamlConfig.environments
681
+ };
682
+ for (const key of Object.keys(project)) {
683
+ if (project[key] === void 0) {
684
+ delete project[key];
685
+ }
686
+ }
687
+ return project;
688
+ }
689
+ function mergeYamlWithProject(project, yamlConfig) {
690
+ return {
691
+ ...project,
692
+ hostname: yamlConfig.hostname || project.hostname,
693
+ basePath: yamlConfig.basePath ?? project.basePath,
694
+ port: yamlConfig.port ?? project.port,
695
+ start: yamlConfig.start ?? project.start,
696
+ env: yamlConfig.env ? { ...project.env, ...yamlConfig.env } : project.env,
697
+ local: yamlConfig.local ?? project.local,
698
+ security: yamlConfig.security ?? project.security,
699
+ environments: yamlConfig.environments ?? project.environments
700
+ };
701
+ }
702
+
703
+ // src/lib/validation.ts
704
+ import { existsSync as existsSync5, accessSync, constants } from "fs";
705
+ import { homedir as homedir2 } from "os";
706
+ import { join as join5 } from "path";
707
+ import chalk from "chalk";
708
+ var MACOS_PROTECTED_PATHS = [
709
+ join5(homedir2(), "Desktop"),
710
+ join5(homedir2(), "Documents"),
711
+ join5(homedir2(), "Downloads")
712
+ ];
713
+ function isProtectedPath(path) {
714
+ if (process.platform !== "darwin") return false;
715
+ const normalizedPath = path.replace(/\/+$/, "");
716
+ return MACOS_PROTECTED_PATHS.some(
717
+ (protected_) => normalizedPath === protected_ || normalizedPath.startsWith(protected_ + "/")
718
+ );
719
+ }
720
+ function checkPortAvailable(port) {
721
+ const result = execCommandSafe(`lsof -i :${port} -P -n 2>/dev/null | grep LISTEN | head -1`);
722
+ if (!result.success || !result.output) {
723
+ return { available: true };
724
+ }
725
+ const parts = result.output.trim().split(/\s+/);
726
+ const processName = parts[0] || "unknown";
727
+ return { available: false, usedBy: processName };
728
+ }
729
+ function validateProject(project) {
730
+ const errors = [];
731
+ const warnings = [];
732
+ if (!existsSync5(project.path)) {
733
+ errors.push(`Path does not exist: ${project.path}`);
734
+ }
735
+ if (isProtectedPath(project.path)) {
736
+ errors.push(
737
+ `Path is in a macOS protected folder (${project.path}). Nginx cannot access Desktop/Documents/Downloads without Full Disk Access. Move your project to ~/projects or grant nginx Full Disk Access in System Settings.`
738
+ );
739
+ }
740
+ if (project.type === "npm" && project.port) {
741
+ const portCheck = checkPortAvailable(project.port);
742
+ if (!portCheck.available) {
743
+ warnings.push(`Port ${project.port} is in use by ${portCheck.usedBy}`);
744
+ }
745
+ }
746
+ if (!project.hostname.match(/^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$/)) {
747
+ warnings.push(`Hostname "${project.hostname}" may be invalid`);
748
+ }
749
+ if (project.type === "npm" && !project.start) {
750
+ const pkgPath = join5(project.path, "package.json");
751
+ if (existsSync5(pkgPath)) {
752
+ try {
753
+ const pkg = JSON.parse(__require("fs").readFileSync(pkgPath, "utf-8"));
754
+ if (!pkg.scripts?.start) {
755
+ warnings.push('No start command specified and package.json has no "start" script');
756
+ }
757
+ } catch {
758
+ }
759
+ }
760
+ }
761
+ return {
762
+ valid: errors.length === 0,
763
+ errors,
764
+ warnings
765
+ };
766
+ }
767
+ function validateConfig(config) {
768
+ const errors = [];
769
+ const warnings = [];
770
+ const listenPort = parseInt(config.defaults.nginxListen.split(":").pop() || "80", 10);
771
+ if (listenPort === 80 || listenPort === 443) {
772
+ if (process.getuid && process.getuid() !== 0) {
773
+ warnings.push(
774
+ `Nginx is configured to listen on port ${listenPort}, which requires root/sudo. Run 'sudo bindler apply' or use a higher port like 8080.`
775
+ );
776
+ }
777
+ }
778
+ const nginxPath = config.defaults.nginxManagedPath;
779
+ const nginxDir = nginxPath.substring(0, nginxPath.lastIndexOf("/"));
780
+ if (!existsSync5(nginxDir)) {
781
+ errors.push(`Nginx config directory does not exist: ${nginxDir}`);
782
+ }
783
+ for (const project of config.projects) {
784
+ if (project.enabled === false) continue;
785
+ const projectResult = validateProject(project);
786
+ for (const err of projectResult.errors) {
787
+ errors.push(`[${project.name}] ${err}`);
788
+ }
789
+ for (const warn of projectResult.warnings) {
790
+ warnings.push(`[${project.name}] ${warn}`);
791
+ }
792
+ }
793
+ const hostnames = config.projects.filter((p) => p.enabled !== false).map((p) => p.hostname);
794
+ const duplicates = hostnames.filter((h, i) => hostnames.indexOf(h) !== i);
795
+ if (duplicates.length > 0) {
796
+ const uniqueDupes = [...new Set(duplicates)];
797
+ for (const dupe of uniqueDupes) {
798
+ const projects = config.projects.filter((p) => p.hostname === dupe).map((p) => p.name);
799
+ if (!projects.every((p, i, arr) => {
800
+ const proj = config.projects.find((x) => x.name === p);
801
+ const firstProj = config.projects.find((x) => x.name === arr[0]);
802
+ return proj?.basePath !== firstProj?.basePath || i === 0;
803
+ })) {
804
+ warnings.push(`Hostname "${dupe}" is shared by: ${projects.join(", ")} (ensure they have different base paths)`);
805
+ }
806
+ }
807
+ }
808
+ const ports = config.projects.filter((p) => p.enabled !== false && p.type === "npm" && p.port).map((p) => ({ name: p.name, port: p.port }));
809
+ const portMap = /* @__PURE__ */ new Map();
810
+ for (const { name, port } of ports) {
811
+ const existing = portMap.get(port) || [];
812
+ existing.push(name);
813
+ portMap.set(port, existing);
814
+ }
815
+ for (const [port, names] of portMap) {
816
+ if (names.length > 1) {
817
+ errors.push(`Port ${port} is used by multiple projects: ${names.join(", ")}`);
818
+ }
819
+ }
820
+ return {
821
+ valid: errors.length === 0,
822
+ errors,
823
+ warnings
824
+ };
825
+ }
826
+ function printValidationResult(result) {
827
+ if (result.errors.length > 0) {
828
+ console.log(chalk.red("\n\u2717 Validation errors:"));
829
+ for (const err of result.errors) {
830
+ console.log(chalk.red(` \u2022 ${err}`));
831
+ }
832
+ }
833
+ if (result.warnings.length > 0) {
834
+ console.log(chalk.yellow("\n\u26A0 Warnings:"));
835
+ for (const warn of result.warnings) {
836
+ console.log(chalk.yellow(` \u2022 ${warn}`));
837
+ }
838
+ }
839
+ if (result.valid && result.warnings.length === 0) {
840
+ console.log(chalk.green("\u2713 All checks passed"));
841
+ }
842
+ }
843
+ function runPreflightChecks(config) {
844
+ const result = validateConfig(config);
845
+ const nginxCheck = execCommandSafe("which nginx");
846
+ if (!nginxCheck.success) {
847
+ result.errors.push("Nginx is not installed. Run: brew install nginx");
848
+ }
849
+ const nginxRunning = execCommandSafe("pgrep nginx");
850
+ if (!nginxRunning.success) {
851
+ result.warnings.push("Nginx is not running. After apply, start it with: brew services start nginx");
852
+ }
853
+ const localProjects = config.projects.filter((p) => p.local && p.enabled !== false);
854
+ if (localProjects.length > 0) {
855
+ const hostsResult = execCommandSafe("cat /etc/hosts");
856
+ if (hostsResult.success) {
857
+ for (const project of localProjects) {
858
+ if (!hostsResult.output.includes(project.hostname)) {
859
+ result.warnings.push(
860
+ `Hostname "${project.hostname}" not in /etc/hosts. Add: 127.0.0.1 ${project.hostname}`
861
+ );
862
+ }
863
+ }
864
+ }
865
+ }
866
+ return result;
867
+ }
868
+
553
869
  // src/commands/new.ts
554
870
  async function newCommand(options) {
555
- console.log(chalk.dim("Checking prerequisites...\n"));
871
+ console.log(chalk2.dim("Checking prerequisites...\n"));
556
872
  const issues = [];
557
873
  if (!isNginxInstalled()) {
558
874
  issues.push("nginx is not installed. Install: brew install nginx (macOS) or apt install nginx (Linux)");
@@ -561,11 +877,11 @@ async function newCommand(options) {
561
877
  issues.push("PM2 is not installed. Install: npm install -g pm2");
562
878
  }
563
879
  if (issues.length > 0) {
564
- console.log(chalk.red("Missing prerequisites:\n"));
880
+ console.log(chalk2.red("Missing prerequisites:\n"));
565
881
  for (const issue of issues) {
566
- console.log(chalk.red(` \u2717 ${issue}`));
882
+ console.log(chalk2.red(` \u2717 ${issue}`));
567
883
  }
568
- console.log(chalk.dim("\nRun `bindler doctor` for full diagnostics."));
884
+ console.log(chalk2.dim("\nRun `bindler doctor` for full diagnostics."));
569
885
  const { proceed } = await inquirer.prompt([
570
886
  {
571
887
  type: "confirm",
@@ -579,12 +895,19 @@ async function newCommand(options) {
579
895
  }
580
896
  console.log("");
581
897
  } else {
582
- console.log(chalk.green("\u2713 Prerequisites OK\n"));
898
+ console.log(chalk2.green("\u2713 Prerequisites OK\n"));
583
899
  }
584
900
  const defaults = getDefaults();
585
901
  let project = {};
586
902
  const cwd = process.cwd();
587
- const cwdName = basename(cwd);
903
+ const cwdName = basename2(cwd);
904
+ const initialPath = options.path || cwd;
905
+ const yamlConfig = existsSync6(initialPath) ? readBindlerYaml(initialPath) : null;
906
+ let yamlDefaults = {};
907
+ if (yamlConfig) {
908
+ console.log(chalk2.cyan("Found bindler.yaml - using as defaults\n"));
909
+ yamlDefaults = yamlToProject(yamlConfig, initialPath);
910
+ }
588
911
  if (!options.name) {
589
912
  const answers = await inquirer.prompt([
590
913
  {
@@ -603,7 +926,7 @@ async function newCommand(options) {
603
926
  type: "input",
604
927
  name: "name",
605
928
  message: "Project name:",
606
- default: cwdName,
929
+ default: yamlDefaults.name || cwdName,
607
930
  validate: (input) => {
608
931
  if (!validateProjectName(input)) {
609
932
  return "Invalid project name. Use alphanumeric characters, dashes, and underscores.";
@@ -616,21 +939,22 @@ async function newCommand(options) {
616
939
  name: "type",
617
940
  message: "Project type:",
618
941
  choices: (answers2) => {
619
- const detected = existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
942
+ const detected = existsSync6(answers2.path) ? detectProjectType(answers2.path) : "static";
620
943
  return [
621
944
  { name: `npm (Node.js app)${detected === "npm" ? " - detected" : ""}`, value: "npm" },
622
945
  { name: `static (HTML/CSS/JS)${detected === "static" ? " - detected" : ""}`, value: "static" }
623
946
  ];
624
947
  },
625
948
  default: (answers2) => {
626
- return existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
949
+ if (yamlDefaults.type) return yamlDefaults.type;
950
+ return existsSync6(answers2.path) ? detectProjectType(answers2.path) : "static";
627
951
  }
628
952
  },
629
953
  {
630
954
  type: "input",
631
955
  name: "hostname",
632
956
  message: options.local ? "Hostname (e.g., myapp.local):" : "Hostname (e.g., mysite.example.com or example.com):",
633
- default: options.local ? `${cwdName}.local` : void 0,
957
+ default: yamlDefaults.hostname || (options.local ? `${cwdName}.local` : void 0),
634
958
  validate: (input) => {
635
959
  if (!validateHostname(input)) {
636
960
  return "Invalid hostname format";
@@ -642,6 +966,7 @@ async function newCommand(options) {
642
966
  type: "input",
643
967
  name: "basePath",
644
968
  message: "Base path (leave empty for root, or e.g., /api):",
969
+ default: yamlDefaults.basePath || "",
645
970
  filter: (input) => {
646
971
  if (!input || input.trim() === "") return "";
647
972
  const trimmed = input.trim();
@@ -652,9 +977,11 @@ async function newCommand(options) {
652
977
  project = { ...answers };
653
978
  if (!project.basePath) delete project.basePath;
654
979
  if (options.local) project.local = true;
980
+ if (yamlDefaults.security) project.security = yamlDefaults.security;
981
+ if (yamlDefaults.environments) project.environments = yamlDefaults.environments;
655
982
  if (answers.type === "npm") {
656
- const scripts = existsSync4(answers.path) ? getPackageJsonScripts(answers.path) : [];
657
- const suggestedPort = findAvailablePort();
983
+ const scripts = existsSync6(answers.path) ? getPackageJsonScripts(answers.path) : [];
984
+ const suggestedPort = yamlDefaults.port || findAvailablePort();
658
985
  const npmAnswers = await inquirer.prompt([
659
986
  {
660
987
  type: "input",
@@ -678,7 +1005,7 @@ async function newCommand(options) {
678
1005
  ...scripts.map((s) => ({ name: `npm run ${s}`, value: `npm run ${s}` })),
679
1006
  { name: "Custom command...", value: "__custom__" }
680
1007
  ] : void 0,
681
- default: scripts.includes("start") ? "npm run start" : "npm start"
1008
+ default: yamlDefaults.start || (scripts.includes("start") ? "npm run start" : "npm start")
682
1009
  }
683
1010
  ]);
684
1011
  if (npmAnswers.start === "__custom__") {
@@ -707,7 +1034,7 @@ async function newCommand(options) {
707
1034
  }
708
1035
  } else {
709
1036
  if (!options.hostname) {
710
- console.error(chalk.red("Error: --hostname is required"));
1037
+ console.error(chalk2.red("Error: --hostname is required"));
711
1038
  process.exit(1);
712
1039
  }
713
1040
  project.name = options.name;
@@ -727,14 +1054,14 @@ async function newCommand(options) {
727
1054
  }
728
1055
  }
729
1056
  if (!validateProjectName(project.name)) {
730
- console.error(chalk.red("Error: Invalid project name"));
1057
+ console.error(chalk2.red("Error: Invalid project name"));
731
1058
  process.exit(1);
732
1059
  }
733
1060
  if (!validateHostname(project.hostname)) {
734
- console.error(chalk.red("Error: Invalid hostname"));
1061
+ console.error(chalk2.red("Error: Invalid hostname"));
735
1062
  process.exit(1);
736
1063
  }
737
- if (!existsSync4(project.path)) {
1064
+ if (!existsSync6(project.path)) {
738
1065
  const createDir = options.name ? true : (await inquirer.prompt([
739
1066
  {
740
1067
  type: "confirm",
@@ -745,51 +1072,72 @@ async function newCommand(options) {
745
1072
  ])).create;
746
1073
  if (createDir) {
747
1074
  mkdirSync3(project.path, { recursive: true });
748
- console.log(chalk.green(`Created directory: ${project.path}`));
1075
+ console.log(chalk2.green(`Created directory: ${project.path}`));
1076
+ }
1077
+ }
1078
+ const validationResult = validateProject(project);
1079
+ if (!validationResult.valid || validationResult.warnings.length > 0) {
1080
+ console.log("");
1081
+ printValidationResult(validationResult);
1082
+ if (!validationResult.valid) {
1083
+ console.log(chalk2.red("\n\u2717 Cannot add project due to validation errors."));
1084
+ process.exit(1);
1085
+ }
1086
+ const { proceed } = await inquirer.prompt([
1087
+ {
1088
+ type: "confirm",
1089
+ name: "proceed",
1090
+ message: "Continue with these warnings?",
1091
+ default: true
1092
+ }
1093
+ ]);
1094
+ if (!proceed) {
1095
+ console.log(chalk2.yellow("Aborted."));
1096
+ process.exit(0);
749
1097
  }
750
1098
  }
751
1099
  try {
752
1100
  addProject(project);
753
- console.log(chalk.green(`
1101
+ console.log(chalk2.green(`
754
1102
  Project "${project.name}" added successfully!`));
755
1103
  if (project.local) {
756
- console.log(chalk.yellow(`
1104
+ console.log(chalk2.yellow(`
757
1105
  Local project - add to /etc/hosts:`));
758
- console.log(chalk.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
759
- console.log(chalk.dim(`
760
- Run ${chalk.cyan("sudo bindler apply")} to update nginx.`));
761
- console.log(chalk.dim(`Then access at: ${chalk.cyan(`http://${project.hostname}:8080`)}`));
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`)}`));
762
1110
  } else {
763
- console.log(chalk.dim(`
764
- Configuration saved. Run ${chalk.cyan("sudo bindler apply")} to update nginx and cloudflare.`));
1111
+ console.log(chalk2.dim(`
1112
+ Configuration saved. Run ${chalk2.cyan("sudo bindler apply")} to update nginx and cloudflare.`));
765
1113
  }
766
1114
  if (project.type === "npm") {
767
- console.log(chalk.dim(`Run ${chalk.cyan(`bindler start ${project.name}`)} to start the application.`));
1115
+ console.log(chalk2.dim(`Run ${chalk2.cyan(`bindler start ${project.name}`)} to start the application.`));
768
1116
  }
769
1117
  } catch (error) {
770
- console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1118
+ console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : error}`));
771
1119
  process.exit(1);
772
1120
  }
773
1121
  }
774
1122
 
775
1123
  // src/commands/list.ts
776
- import chalk2 from "chalk";
1124
+ import chalk3 from "chalk";
777
1125
  import Table from "cli-table3";
778
1126
  async function listCommand() {
779
1127
  const projects = listProjects();
780
1128
  if (projects.length === 0) {
781
- console.log(chalk2.yellow("No projects registered."));
782
- console.log(chalk2.dim(`Run ${chalk2.cyan("bindler new")} to create one.`));
1129
+ console.log(chalk3.yellow("No projects registered."));
1130
+ console.log(chalk3.dim(`Run ${chalk3.cyan("bindler new")} to create one.`));
783
1131
  return;
784
1132
  }
785
1133
  const table = new Table({
786
1134
  head: [
787
- chalk2.cyan("Name"),
788
- chalk2.cyan("Type"),
789
- chalk2.cyan("Hostname"),
790
- chalk2.cyan("Port"),
791
- chalk2.cyan("Path"),
792
- chalk2.cyan("Status")
1135
+ chalk3.cyan("Name"),
1136
+ chalk3.cyan("Type"),
1137
+ chalk3.cyan("Hostname"),
1138
+ chalk3.cyan("Port"),
1139
+ chalk3.cyan("Path"),
1140
+ chalk3.cyan("Status")
793
1141
  ],
794
1142
  style: {
795
1143
  head: [],
@@ -801,15 +1149,15 @@ async function listCommand() {
801
1149
  if (project.type === "npm") {
802
1150
  const process2 = getProcessByName(project.name);
803
1151
  if (process2) {
804
- status = process2.status === "online" ? chalk2.green("online") : process2.status === "stopped" ? chalk2.yellow("stopped") : chalk2.red(process2.status);
1152
+ status = process2.status === "online" ? chalk3.green("online") : process2.status === "stopped" ? chalk3.yellow("stopped") : chalk3.red(process2.status);
805
1153
  } else {
806
- status = chalk2.dim("not started");
1154
+ status = chalk3.dim("not started");
807
1155
  }
808
1156
  } else {
809
- status = project.enabled !== false ? chalk2.green("serving") : chalk2.yellow("disabled");
1157
+ status = project.enabled !== false ? chalk3.green("serving") : chalk3.yellow("disabled");
810
1158
  }
811
1159
  if (project.enabled === false) {
812
- status = chalk2.yellow("disabled");
1160
+ status = chalk3.yellow("disabled");
813
1161
  }
814
1162
  table.push([
815
1163
  project.name,
@@ -821,17 +1169,17 @@ async function listCommand() {
821
1169
  ]);
822
1170
  }
823
1171
  console.log(table.toString());
824
- console.log(chalk2.dim(`
1172
+ console.log(chalk3.dim(`
825
1173
  ${projects.length} project(s) registered`));
826
1174
  }
827
1175
 
828
1176
  // src/commands/status.ts
829
- import chalk3 from "chalk";
1177
+ import chalk4 from "chalk";
830
1178
  import Table2 from "cli-table3";
831
1179
  async function statusCommand() {
832
1180
  const projects = listProjects();
833
1181
  if (projects.length === 0) {
834
- console.log(chalk3.yellow("No projects registered."));
1182
+ console.log(chalk4.yellow("No projects registered."));
835
1183
  return;
836
1184
  }
837
1185
  const statuses = [];
@@ -855,12 +1203,12 @@ async function statusCommand() {
855
1203
  }
856
1204
  const table = new Table2({
857
1205
  head: [
858
- chalk3.cyan("Name"),
859
- chalk3.cyan("Type"),
860
- chalk3.cyan("Hostname"),
861
- chalk3.cyan("Port"),
862
- chalk3.cyan("PM2 Status"),
863
- chalk3.cyan("Port Check")
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")
864
1212
  ],
865
1213
  style: {
866
1214
  head: [],
@@ -873,25 +1221,25 @@ async function statusCommand() {
873
1221
  if (status.type === "npm") {
874
1222
  switch (status.pm2Status) {
875
1223
  case "online":
876
- pm2StatusStr = chalk3.green("online");
1224
+ pm2StatusStr = chalk4.green("online");
877
1225
  break;
878
1226
  case "stopped":
879
- pm2StatusStr = chalk3.yellow("stopped");
1227
+ pm2StatusStr = chalk4.yellow("stopped");
880
1228
  break;
881
1229
  case "errored":
882
- pm2StatusStr = chalk3.red("errored");
1230
+ pm2StatusStr = chalk4.red("errored");
883
1231
  break;
884
1232
  case "not_managed":
885
- pm2StatusStr = chalk3.dim("not started");
1233
+ pm2StatusStr = chalk4.dim("not started");
886
1234
  break;
887
1235
  default:
888
- pm2StatusStr = chalk3.dim(status.pm2Status || "-");
1236
+ pm2StatusStr = chalk4.dim(status.pm2Status || "-");
889
1237
  }
890
- portCheckStr = status.portListening ? chalk3.green("listening") : chalk3.red("not listening");
1238
+ portCheckStr = status.portListening ? chalk4.green("listening") : chalk4.red("not listening");
891
1239
  }
892
1240
  if (status.enabled === false) {
893
- pm2StatusStr = chalk3.yellow("disabled");
894
- portCheckStr = chalk3.dim("-");
1241
+ pm2StatusStr = chalk4.yellow("disabled");
1242
+ portCheckStr = chalk4.dim("-");
895
1243
  }
896
1244
  table.push([
897
1245
  status.name,
@@ -905,14 +1253,14 @@ async function statusCommand() {
905
1253
  console.log(table.toString());
906
1254
  const runningProcesses = getPm2List().filter((p) => p.name.startsWith("bindler:"));
907
1255
  if (runningProcesses.length > 0) {
908
- console.log(chalk3.bold("\nPM2 Process Details:"));
1256
+ console.log(chalk4.bold("\nPM2 Process Details:"));
909
1257
  const detailTable = new Table2({
910
1258
  head: [
911
- chalk3.cyan("Process"),
912
- chalk3.cyan("CPU"),
913
- chalk3.cyan("Memory"),
914
- chalk3.cyan("Uptime"),
915
- chalk3.cyan("Restarts")
1259
+ chalk4.cyan("Process"),
1260
+ chalk4.cyan("CPU"),
1261
+ chalk4.cyan("Memory"),
1262
+ chalk4.cyan("Uptime"),
1263
+ chalk4.cyan("Restarts")
916
1264
  ],
917
1265
  style: {
918
1266
  head: [],
@@ -933,197 +1281,198 @@ async function statusCommand() {
933
1281
  }
934
1282
 
935
1283
  // src/commands/start.ts
936
- import chalk4 from "chalk";
1284
+ import chalk5 from "chalk";
937
1285
  async function startCommand(name, options) {
938
1286
  if (options.all) {
939
1287
  const projects = listProjects();
940
1288
  const npmProjects = projects.filter((p) => p.type === "npm");
941
1289
  if (npmProjects.length === 0) {
942
- console.log(chalk4.yellow("No npm projects to start."));
1290
+ console.log(chalk5.yellow("No npm projects to start."));
943
1291
  return;
944
1292
  }
945
- console.log(chalk4.blue(`Starting ${npmProjects.length} npm project(s)...`));
1293
+ console.log(chalk5.blue(`Starting ${npmProjects.length} npm project(s)...`));
946
1294
  const results = startAllProjects(npmProjects);
947
1295
  for (const result2 of results) {
948
1296
  if (result2.success) {
949
- console.log(chalk4.green(` \u2713 ${result2.name}`));
1297
+ console.log(chalk5.green(` \u2713 ${result2.name}`));
950
1298
  } else {
951
- console.log(chalk4.red(` \u2717 ${result2.name}: ${result2.error}`));
1299
+ console.log(chalk5.red(` \u2717 ${result2.name}: ${result2.error}`));
952
1300
  }
953
1301
  }
954
1302
  const succeeded = results.filter((r) => r.success).length;
955
- console.log(chalk4.dim(`
1303
+ console.log(chalk5.dim(`
956
1304
  ${succeeded}/${results.length} started successfully`));
957
1305
  return;
958
1306
  }
959
1307
  if (!name) {
960
- console.error(chalk4.red("Error: Project name is required. Use --all to start all projects."));
1308
+ console.error(chalk5.red("Error: Project name is required. Use --all to start all projects."));
961
1309
  process.exit(1);
962
1310
  }
963
1311
  const project = getProject(name);
964
1312
  if (!project) {
965
- console.error(chalk4.red(`Error: Project "${name}" not found`));
1313
+ console.error(chalk5.red(`Error: Project "${name}" not found`));
966
1314
  process.exit(1);
967
1315
  }
968
1316
  if (project.type !== "npm") {
969
- console.log(chalk4.blue(`Project "${name}" is a static site - no process to start.`));
970
- console.log(chalk4.dim("Static sites are served directly by nginx."));
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."));
971
1319
  return;
972
1320
  }
973
- console.log(chalk4.blue(`Starting ${name}...`));
1321
+ console.log(chalk5.blue(`Starting ${name}...`));
974
1322
  const result = startProject(project);
975
1323
  if (result.success) {
976
- console.log(chalk4.green(`\u2713 ${name} started successfully`));
977
- console.log(chalk4.dim(` Port: ${project.port}`));
978
- console.log(chalk4.dim(` URL: https://${project.hostname}`));
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}`));
979
1327
  } else {
980
- console.error(chalk4.red(`\u2717 Failed to start ${name}: ${result.error}`));
1328
+ console.error(chalk5.red(`\u2717 Failed to start ${name}: ${result.error}`));
981
1329
  process.exit(1);
982
1330
  }
983
1331
  }
984
1332
 
985
1333
  // src/commands/stop.ts
986
- import chalk5 from "chalk";
1334
+ import chalk6 from "chalk";
987
1335
  async function stopCommand(name, options) {
988
1336
  if (options.all) {
989
1337
  const projects = listProjects();
990
1338
  const npmProjects = projects.filter((p) => p.type === "npm");
991
1339
  if (npmProjects.length === 0) {
992
- console.log(chalk5.yellow("No npm projects to stop."));
1340
+ console.log(chalk6.yellow("No npm projects to stop."));
993
1341
  return;
994
1342
  }
995
- console.log(chalk5.blue(`Stopping ${npmProjects.length} npm project(s)...`));
1343
+ console.log(chalk6.blue(`Stopping ${npmProjects.length} npm project(s)...`));
996
1344
  const results = stopAllProjects(npmProjects);
997
1345
  for (const result2 of results) {
998
1346
  if (result2.success) {
999
- console.log(chalk5.green(` \u2713 ${result2.name}`));
1347
+ console.log(chalk6.green(` \u2713 ${result2.name}`));
1000
1348
  } else {
1001
- console.log(chalk5.red(` \u2717 ${result2.name}: ${result2.error}`));
1349
+ console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
1002
1350
  }
1003
1351
  }
1004
1352
  const succeeded = results.filter((r) => r.success).length;
1005
- console.log(chalk5.dim(`
1353
+ console.log(chalk6.dim(`
1006
1354
  ${succeeded}/${results.length} stopped successfully`));
1007
1355
  return;
1008
1356
  }
1009
1357
  if (!name) {
1010
- console.error(chalk5.red("Error: Project name is required. Use --all to stop all projects."));
1358
+ console.error(chalk6.red("Error: Project name is required. Use --all to stop all projects."));
1011
1359
  process.exit(1);
1012
1360
  }
1013
1361
  const project = getProject(name);
1014
1362
  if (!project) {
1015
- console.error(chalk5.red(`Error: Project "${name}" not found`));
1363
+ console.error(chalk6.red(`Error: Project "${name}" not found`));
1016
1364
  process.exit(1);
1017
1365
  }
1018
1366
  if (project.type !== "npm") {
1019
- console.log(chalk5.yellow(`Project "${name}" is a static site - no process to stop.`));
1367
+ console.log(chalk6.yellow(`Project "${name}" is a static site - no process to stop.`));
1020
1368
  return;
1021
1369
  }
1022
- console.log(chalk5.blue(`Stopping ${name}...`));
1370
+ console.log(chalk6.blue(`Stopping ${name}...`));
1023
1371
  const result = stopProject(name);
1024
1372
  if (result.success) {
1025
- console.log(chalk5.green(`\u2713 ${name} stopped successfully`));
1373
+ console.log(chalk6.green(`\u2713 ${name} stopped successfully`));
1026
1374
  } else {
1027
- console.error(chalk5.red(`\u2717 Failed to stop ${name}: ${result.error}`));
1375
+ console.error(chalk6.red(`\u2717 Failed to stop ${name}: ${result.error}`));
1028
1376
  process.exit(1);
1029
1377
  }
1030
1378
  }
1031
1379
 
1032
1380
  // src/commands/restart.ts
1033
- import chalk6 from "chalk";
1381
+ import chalk7 from "chalk";
1034
1382
  async function restartCommand(name, options) {
1035
1383
  if (options.all) {
1036
1384
  const projects = listProjects();
1037
1385
  const npmProjects = projects.filter((p) => p.type === "npm");
1038
1386
  if (npmProjects.length === 0) {
1039
- console.log(chalk6.yellow("No npm projects to restart."));
1387
+ console.log(chalk7.yellow("No npm projects to restart."));
1040
1388
  return;
1041
1389
  }
1042
- console.log(chalk6.blue(`Restarting ${npmProjects.length} npm project(s)...`));
1390
+ console.log(chalk7.blue(`Restarting ${npmProjects.length} npm project(s)...`));
1043
1391
  const results = restartAllProjects(npmProjects);
1044
1392
  for (const result2 of results) {
1045
1393
  if (result2.success) {
1046
- console.log(chalk6.green(` \u2713 ${result2.name}`));
1394
+ console.log(chalk7.green(` \u2713 ${result2.name}`));
1047
1395
  } else {
1048
- console.log(chalk6.red(` \u2717 ${result2.name}: ${result2.error}`));
1396
+ console.log(chalk7.red(` \u2717 ${result2.name}: ${result2.error}`));
1049
1397
  }
1050
1398
  }
1051
1399
  const succeeded = results.filter((r) => r.success).length;
1052
- console.log(chalk6.dim(`
1400
+ console.log(chalk7.dim(`
1053
1401
  ${succeeded}/${results.length} restarted successfully`));
1054
1402
  return;
1055
1403
  }
1056
1404
  if (!name) {
1057
- console.error(chalk6.red("Error: Project name is required. Use --all to restart all projects."));
1405
+ console.error(chalk7.red("Error: Project name is required. Use --all to restart all projects."));
1058
1406
  process2.exit(1);
1059
1407
  }
1060
1408
  const project = getProject(name);
1061
1409
  if (!project) {
1062
- console.error(chalk6.red(`Error: Project "${name}" not found`));
1410
+ console.error(chalk7.red(`Error: Project "${name}" not found`));
1063
1411
  process2.exit(1);
1064
1412
  }
1065
1413
  if (project.type !== "npm") {
1066
- console.log(chalk6.yellow(`Project "${name}" is a static site - no process to restart.`));
1414
+ console.log(chalk7.yellow(`Project "${name}" is a static site - no process to restart.`));
1067
1415
  return;
1068
1416
  }
1069
- console.log(chalk6.blue(`Restarting ${name}...`));
1417
+ console.log(chalk7.blue(`Restarting ${name}...`));
1070
1418
  const process2 = getProcessByName(name);
1071
1419
  let result;
1072
1420
  if (process2) {
1073
1421
  result = restartProject(name);
1074
1422
  } else {
1075
- console.log(chalk6.dim("Process not running, starting..."));
1423
+ console.log(chalk7.dim("Process not running, starting..."));
1076
1424
  result = startProject(project);
1077
1425
  }
1078
1426
  if (result.success) {
1079
- console.log(chalk6.green(`\u2713 ${name} restarted successfully`));
1427
+ console.log(chalk7.green(`\u2713 ${name} restarted successfully`));
1080
1428
  } else {
1081
- console.error(chalk6.red(`\u2717 Failed to restart ${name}: ${result.error}`));
1429
+ console.error(chalk7.red(`\u2717 Failed to restart ${name}: ${result.error}`));
1082
1430
  process2.exit(1);
1083
1431
  }
1084
1432
  }
1085
1433
 
1086
1434
  // src/commands/logs.ts
1087
- import chalk7 from "chalk";
1435
+ import chalk8 from "chalk";
1088
1436
  async function logsCommand(name, options) {
1089
1437
  const project = getProject(name);
1090
1438
  if (!project) {
1091
- console.error(chalk7.red(`Error: Project "${name}" not found`));
1439
+ console.error(chalk8.red(`Error: Project "${name}" not found`));
1092
1440
  process2.exit(1);
1093
1441
  }
1094
1442
  if (project.type !== "npm") {
1095
- console.log(chalk7.yellow(`Project "${name}" is a static site - no logs available.`));
1096
- console.log(chalk7.dim("Check nginx access/error logs instead:"));
1097
- console.log(chalk7.dim(" /var/log/nginx/access.log"));
1098
- console.log(chalk7.dim(" /var/log/nginx/error.log"));
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"));
1099
1447
  return;
1100
1448
  }
1101
1449
  const process2 = getProcessByName(name);
1102
1450
  if (!process2) {
1103
- console.log(chalk7.yellow(`Project "${name}" has not been started yet.`));
1104
- console.log(chalk7.dim(`Run ${chalk7.cyan(`bindler start ${name}`)} first.`));
1451
+ console.log(chalk8.yellow(`Project "${name}" has not been started yet.`));
1452
+ console.log(chalk8.dim(`Run ${chalk8.cyan(`bindler start ${name}`)} first.`));
1105
1453
  return;
1106
1454
  }
1107
1455
  const lines = options.lines || 200;
1108
1456
  const follow = options.follow || false;
1109
1457
  if (follow) {
1110
- console.log(chalk7.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
1458
+ console.log(chalk8.dim(`Following logs for ${name}... (Ctrl+C to exit)`));
1111
1459
  }
1112
1460
  await showLogs(name, follow, lines);
1113
1461
  }
1114
1462
 
1115
1463
  // src/commands/update.ts
1116
- import chalk8 from "chalk";
1464
+ import chalk9 from "chalk";
1465
+ import { existsSync as existsSync7 } from "fs";
1117
1466
  async function updateCommand(name, options) {
1118
1467
  const project = getProject(name);
1119
1468
  if (!project) {
1120
- console.error(chalk8.red(`Error: Project "${name}" not found`));
1469
+ console.error(chalk9.red(`Error: Project "${name}" not found`));
1121
1470
  process.exit(1);
1122
1471
  }
1123
1472
  const updates = {};
1124
1473
  if (options.hostname) {
1125
1474
  if (!validateHostname(options.hostname)) {
1126
- console.error(chalk8.red("Error: Invalid hostname format"));
1475
+ console.error(chalk9.red("Error: Invalid hostname format"));
1127
1476
  process.exit(1);
1128
1477
  }
1129
1478
  updates.hostname = options.hostname;
@@ -1131,11 +1480,11 @@ async function updateCommand(name, options) {
1131
1480
  if (options.port) {
1132
1481
  const port = parseInt(options.port, 10);
1133
1482
  if (!validatePort(port)) {
1134
- console.error(chalk8.red("Error: Invalid port. Use a number between 1024 and 65535."));
1483
+ console.error(chalk9.red("Error: Invalid port. Use a number between 1024 and 65535."));
1135
1484
  process.exit(1);
1136
1485
  }
1137
1486
  if (!isPortAvailable(port) && port !== project.port) {
1138
- console.error(chalk8.red(`Error: Port ${port} is already in use by another project.`));
1487
+ console.error(chalk9.red(`Error: Port ${port} is already in use by another project.`));
1139
1488
  process.exit(1);
1140
1489
  }
1141
1490
  updates.port = port;
@@ -1145,12 +1494,22 @@ async function updateCommand(name, options) {
1145
1494
  }
1146
1495
  if (options.start) {
1147
1496
  if (project.type !== "npm") {
1148
- console.error(chalk8.red("Error: Start command only applies to npm projects"));
1497
+ console.error(chalk9.red("Error: Start command only applies to npm projects"));
1149
1498
  process.exit(1);
1150
1499
  }
1151
1500
  updates.start = options.start;
1152
1501
  }
1153
1502
  if (options.path) {
1503
+ if (!existsSync7(options.path)) {
1504
+ console.error(chalk9.red(`Error: Path does not exist: ${options.path}`));
1505
+ process.exit(1);
1506
+ }
1507
+ 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.`));
1511
+ process.exit(1);
1512
+ }
1154
1513
  updates.path = options.path;
1155
1514
  }
1156
1515
  if (options.env && options.env.length > 0) {
@@ -1158,7 +1517,7 @@ async function updateCommand(name, options) {
1158
1517
  for (const envStr of options.env) {
1159
1518
  const [key, ...valueParts] = envStr.split("=");
1160
1519
  if (!key) {
1161
- console.error(chalk8.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
1520
+ console.error(chalk9.red(`Error: Invalid env format: ${envStr}. Use KEY=value`));
1162
1521
  process.exit(1);
1163
1522
  }
1164
1523
  env[key] = valueParts.join("=");
@@ -1171,53 +1530,53 @@ async function updateCommand(name, options) {
1171
1530
  updates.enabled = false;
1172
1531
  }
1173
1532
  if (Object.keys(updates).length === 0) {
1174
- console.log(chalk8.yellow("No updates specified."));
1175
- console.log(chalk8.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
1533
+ console.log(chalk9.yellow("No updates specified."));
1534
+ console.log(chalk9.dim("Available options: --hostname, --port, --start, --path, --env, --enable, --disable"));
1176
1535
  return;
1177
1536
  }
1178
1537
  try {
1179
1538
  updateProject(name, updates);
1180
- console.log(chalk8.green(`\u2713 Project "${name}" updated successfully`));
1539
+ console.log(chalk9.green(`\u2713 Project "${name}" updated successfully`));
1181
1540
  for (const [key, value] of Object.entries(updates)) {
1182
- console.log(chalk8.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
1541
+ console.log(chalk9.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
1183
1542
  }
1184
- console.log(chalk8.dim(`
1185
- Run ${chalk8.cyan("sudo bindler apply")} to apply changes to nginx.`));
1543
+ console.log(chalk9.dim(`
1544
+ Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
1186
1545
  if (project.type === "npm" && (updates.port || updates.start || updates.env)) {
1187
- console.log(chalk8.dim(`Run ${chalk8.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
1546
+ console.log(chalk9.dim(`Run ${chalk9.cyan(`bindler restart ${name}`)} to apply changes to the running process.`));
1188
1547
  }
1189
1548
  } catch (error) {
1190
- console.error(chalk8.red(`Error: ${error instanceof Error ? error.message : error}`));
1549
+ console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : error}`));
1191
1550
  process.exit(1);
1192
1551
  }
1193
1552
  }
1194
1553
 
1195
1554
  // src/commands/edit.ts
1196
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
1555
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
1197
1556
  import { tmpdir } from "os";
1198
- import { join as join4 } from "path";
1199
- import chalk9 from "chalk";
1557
+ import { join as join6 } from "path";
1558
+ import chalk10 from "chalk";
1200
1559
  async function editCommand(name) {
1201
1560
  const project = getProject(name);
1202
1561
  if (!project) {
1203
- console.error(chalk9.red(`Error: Project "${name}" not found`));
1562
+ console.error(chalk10.red(`Error: Project "${name}" not found`));
1204
1563
  process.exit(1);
1205
1564
  }
1206
1565
  const editor = process.env.EDITOR || process.env.VISUAL || "vi";
1207
- const tmpFile = join4(tmpdir(), `bindler-${name}-${Date.now()}.json`);
1208
- writeFileSync3(tmpFile, JSON.stringify(project, null, 2) + "\n");
1209
- console.log(chalk9.dim(`Opening ${name} config in ${editor}...`));
1566
+ const tmpFile = join6(tmpdir(), `bindler-${name}-${Date.now()}.json`);
1567
+ writeFileSync4(tmpFile, JSON.stringify(project, null, 2) + "\n");
1568
+ console.log(chalk10.dim(`Opening ${name} config in ${editor}...`));
1210
1569
  const exitCode = await spawnInteractive(editor, [tmpFile]);
1211
1570
  if (exitCode !== 0) {
1212
- console.error(chalk9.red("Editor exited with error"));
1571
+ console.error(chalk10.red("Editor exited with error"));
1213
1572
  unlinkSync(tmpFile);
1214
1573
  process.exit(1);
1215
1574
  }
1216
1575
  let editedContent;
1217
1576
  try {
1218
- editedContent = readFileSync3(tmpFile, "utf-8");
1577
+ editedContent = readFileSync4(tmpFile, "utf-8");
1219
1578
  } catch (error) {
1220
- console.error(chalk9.red("Failed to read edited file"));
1579
+ console.error(chalk10.red("Failed to read edited file"));
1221
1580
  process.exit(1);
1222
1581
  } finally {
1223
1582
  unlinkSync(tmpFile);
@@ -1226,44 +1585,44 @@ async function editCommand(name) {
1226
1585
  try {
1227
1586
  editedProject = JSON.parse(editedContent);
1228
1587
  } catch (error) {
1229
- console.error(chalk9.red("Error: Invalid JSON in edited file"));
1588
+ console.error(chalk10.red("Error: Invalid JSON in edited file"));
1230
1589
  process.exit(1);
1231
1590
  }
1232
1591
  if (editedProject.name !== project.name) {
1233
- console.error(chalk9.red("Error: Cannot change project name via edit. Use a new project instead."));
1592
+ console.error(chalk10.red("Error: Cannot change project name via edit. Use a new project instead."));
1234
1593
  process.exit(1);
1235
1594
  }
1236
1595
  const originalStr = JSON.stringify(project);
1237
1596
  const editedStr = JSON.stringify(editedProject);
1238
1597
  if (originalStr === editedStr) {
1239
- console.log(chalk9.yellow("No changes made."));
1598
+ console.log(chalk10.yellow("No changes made."));
1240
1599
  return;
1241
1600
  }
1242
1601
  try {
1243
1602
  const config = readConfig();
1244
1603
  const index = config.projects.findIndex((p) => p.name === name);
1245
1604
  if (index === -1) {
1246
- console.error(chalk9.red("Error: Project not found"));
1605
+ console.error(chalk10.red("Error: Project not found"));
1247
1606
  process.exit(1);
1248
1607
  }
1249
1608
  config.projects[index] = editedProject;
1250
1609
  writeConfig(config);
1251
- console.log(chalk9.green(`\u2713 Project "${name}" updated successfully`));
1252
- console.log(chalk9.dim(`
1253
- Run ${chalk9.cyan("sudo bindler apply")} to apply changes to nginx.`));
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.`));
1254
1613
  } catch (error) {
1255
- console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : error}`));
1614
+ console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
1256
1615
  process.exit(1);
1257
1616
  }
1258
1617
  }
1259
1618
 
1260
1619
  // src/commands/remove.ts
1261
1620
  import inquirer2 from "inquirer";
1262
- import chalk10 from "chalk";
1621
+ import chalk11 from "chalk";
1263
1622
  async function removeCommand(name, options) {
1264
1623
  const project = getProject(name);
1265
1624
  if (!project) {
1266
- console.error(chalk10.red(`Error: Project "${name}" not found`));
1625
+ console.error(chalk11.red(`Error: Project "${name}" not found`));
1267
1626
  process.exit(1);
1268
1627
  }
1269
1628
  if (!options.force) {
@@ -1276,33 +1635,34 @@ async function removeCommand(name, options) {
1276
1635
  }
1277
1636
  ]);
1278
1637
  if (!confirm) {
1279
- console.log(chalk10.yellow("Cancelled."));
1638
+ console.log(chalk11.yellow("Cancelled."));
1280
1639
  return;
1281
1640
  }
1282
1641
  }
1283
1642
  if (project.type === "npm") {
1284
1643
  const process2 = getProcessByName(name);
1285
1644
  if (process2) {
1286
- console.log(chalk10.dim("Stopping PM2 process..."));
1645
+ console.log(chalk11.dim("Stopping PM2 process..."));
1287
1646
  deleteProject(name);
1288
1647
  }
1289
1648
  }
1290
1649
  try {
1291
1650
  removeProject(name);
1292
- console.log(chalk10.green(`\u2713 Project "${name}" removed from registry`));
1293
- console.log(chalk10.dim(`
1294
- Run ${chalk10.cyan("sudo bindler apply")} to update nginx configuration.`));
1295
- console.log(chalk10.yellow("\nNote: The project files and Cloudflare DNS routes were not removed."));
1296
- console.log(chalk10.dim(` Project path: ${project.path}`));
1297
- console.log(chalk10.dim(` To remove DNS route manually: cloudflared tunnel route dns --remove ${project.hostname}`));
1651
+ console.log(chalk11.green(`\u2713 Project "${name}" removed from registry`));
1652
+ console.log(chalk11.dim(`
1653
+ Run ${chalk11.cyan("sudo bindler apply")} to update nginx configuration.`));
1654
+ console.log(chalk11.yellow("\nNote: The project files and Cloudflare DNS routes were not removed."));
1655
+ console.log(chalk11.dim(` Project path: ${project.path}`));
1656
+ console.log(chalk11.dim(` To remove DNS route manually: cloudflared tunnel route dns --remove ${project.hostname}`));
1298
1657
  } catch (error) {
1299
- console.error(chalk10.red(`Error: ${error instanceof Error ? error.message : error}`));
1658
+ console.error(chalk11.red(`Error: ${error instanceof Error ? error.message : error}`));
1300
1659
  process.exit(1);
1301
1660
  }
1302
1661
  }
1303
1662
 
1304
1663
  // src/commands/apply.ts
1305
- import chalk11 from "chalk";
1664
+ import chalk12 from "chalk";
1665
+ import { existsSync as existsSync8 } from "fs";
1306
1666
 
1307
1667
  // src/lib/cloudflare.ts
1308
1668
  function isCloudflaredInstalled() {
@@ -1394,126 +1754,182 @@ function getTunnelInfo(tunnelName) {
1394
1754
 
1395
1755
  // src/commands/apply.ts
1396
1756
  async function applyCommand(options) {
1397
- const config = readConfig();
1757
+ let config = readConfig();
1398
1758
  const defaults = getDefaults();
1759
+ if (options.sync) {
1760
+ console.log(chalk12.dim("Syncing bindler.yaml from project directories...\n"));
1761
+ let synced = 0;
1762
+ for (const project of config.projects) {
1763
+ if (!existsSync8(project.path)) continue;
1764
+ const yamlConfig = readBindlerYaml(project.path);
1765
+ if (yamlConfig) {
1766
+ const merged = mergeYamlWithProject(project, yamlConfig);
1767
+ updateProject(project.name, merged);
1768
+ console.log(chalk12.green(` \u2713 Synced ${project.name} from bindler.yaml`));
1769
+ synced++;
1770
+ }
1771
+ }
1772
+ if (synced === 0) {
1773
+ console.log(chalk12.dim(" No bindler.yaml files found in project directories"));
1774
+ } else {
1775
+ console.log(chalk12.dim(`
1776
+ Synced ${synced} project(s)
1777
+ `));
1778
+ }
1779
+ config = readConfig();
1780
+ }
1781
+ if (options.env) {
1782
+ console.log(chalk12.dim(`Using ${options.env} environment configuration...
1783
+ `));
1784
+ const envProjects = listProjectsForEnv(options.env);
1785
+ config = { ...config, projects: envProjects };
1786
+ }
1399
1787
  if (config.projects.length === 0) {
1400
- console.log(chalk11.yellow("No projects registered. Nothing to apply."));
1788
+ console.log(chalk12.yellow("No projects registered. Nothing to apply."));
1401
1789
  return;
1402
1790
  }
1403
- console.log(chalk11.blue("Applying configuration...\n"));
1404
- console.log(chalk11.dim("Generating nginx configuration..."));
1791
+ console.log(chalk12.blue("Applying configuration...\n"));
1792
+ if (!options.skipChecks) {
1793
+ console.log(chalk12.dim("Running preflight checks..."));
1794
+ const checkResult = runPreflightChecks(config);
1795
+ if (!checkResult.valid) {
1796
+ printValidationResult(checkResult);
1797
+ console.log(chalk12.red("\n\u2717 Preflight checks failed. Fix the errors above before applying."));
1798
+ console.log(chalk12.dim(" Use --skip-checks to bypass (not recommended)"));
1799
+ process.exit(1);
1800
+ }
1801
+ if (checkResult.warnings.length > 0) {
1802
+ printValidationResult(checkResult);
1803
+ console.log("");
1804
+ } else {
1805
+ console.log(chalk12.green(" \u2713 Preflight checks passed"));
1806
+ }
1807
+ }
1808
+ console.log(chalk12.dim("Generating nginx configuration..."));
1405
1809
  if (options.dryRun) {
1406
1810
  const nginxConfig = generateNginxConfig(config);
1407
- console.log(chalk11.cyan("\n--- Generated nginx config (dry-run) ---\n"));
1811
+ console.log(chalk12.cyan("\n--- Generated nginx config (dry-run) ---\n"));
1408
1812
  console.log(nginxConfig);
1409
- console.log(chalk11.cyan("--- End of config ---\n"));
1410
- console.log(chalk11.yellow("Dry run mode - no changes were made."));
1813
+ console.log(chalk12.cyan("--- End of config ---\n"));
1814
+ console.log(chalk12.yellow("Dry run mode - no changes were made."));
1411
1815
  return;
1412
1816
  }
1413
1817
  try {
1414
1818
  const { path, content } = writeNginxConfig(config);
1415
- console.log(chalk11.green(` \u2713 Wrote nginx config to ${path}`));
1819
+ console.log(chalk12.green(` \u2713 Wrote nginx config to ${path}`));
1416
1820
  } catch (error) {
1417
1821
  const errMsg = error instanceof Error ? error.message : String(error);
1418
- console.error(chalk11.red(` \u2717 Failed to write nginx config: ${errMsg}`));
1822
+ console.error(chalk12.red(` \u2717 Failed to write nginx config: ${errMsg}`));
1419
1823
  if (errMsg.includes("EACCES") || errMsg.includes("permission denied")) {
1420
- console.log(chalk11.yellow(`
1421
- Try running with sudo: ${chalk11.cyan("sudo bindler apply")}`));
1824
+ console.log(chalk12.yellow(`
1825
+ Try running with sudo: ${chalk12.cyan("sudo bindler apply")}`));
1422
1826
  }
1423
1827
  process.exit(1);
1424
1828
  }
1425
- console.log(chalk11.dim("Testing nginx configuration..."));
1829
+ const authProjects = config.projects.filter(
1830
+ (p) => p.security?.basicAuth?.enabled && p.security.basicAuth.users?.length
1831
+ );
1832
+ if (authProjects.length > 0) {
1833
+ console.log(chalk12.dim("Generating htpasswd files..."));
1834
+ try {
1835
+ generateHtpasswdFiles(config.projects);
1836
+ console.log(chalk12.green(` \u2713 Generated htpasswd files for ${authProjects.length} project(s)`));
1837
+ } catch (error) {
1838
+ console.log(chalk12.yellow(` ! Failed to generate htpasswd files: ${error}`));
1839
+ }
1840
+ }
1841
+ console.log(chalk12.dim("Testing nginx configuration..."));
1426
1842
  const testResult = testNginxConfig();
1427
1843
  if (!testResult.success) {
1428
- console.error(chalk11.red(" \u2717 Nginx configuration test failed:"));
1429
- console.error(chalk11.red(testResult.output));
1430
- console.log(chalk11.yellow("\nConfiguration was written but nginx was NOT reloaded."));
1431
- console.log(chalk11.dim("Fix the configuration and run `sudo bindler apply` again."));
1844
+ console.error(chalk12.red(" \u2717 Nginx configuration test failed:"));
1845
+ console.error(chalk12.red(testResult.output));
1846
+ console.log(chalk12.yellow("\nConfiguration was written but nginx was NOT reloaded."));
1847
+ console.log(chalk12.dim("Fix the configuration and run `sudo bindler apply` again."));
1432
1848
  process.exit(1);
1433
1849
  }
1434
- console.log(chalk11.green(" \u2713 Nginx configuration test passed"));
1850
+ console.log(chalk12.green(" \u2713 Nginx configuration test passed"));
1435
1851
  if (!options.noReload) {
1436
- console.log(chalk11.dim("Reloading nginx..."));
1852
+ console.log(chalk12.dim("Reloading nginx..."));
1437
1853
  const reloadResult = reloadNginx();
1438
1854
  if (!reloadResult.success) {
1439
- console.error(chalk11.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
1440
- console.log(chalk11.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
1855
+ console.error(chalk12.red(` \u2717 Failed to reload nginx: ${reloadResult.error}`));
1856
+ console.log(chalk12.dim("You may need to reload nginx manually: sudo systemctl reload nginx"));
1441
1857
  process.exit(1);
1442
1858
  }
1443
- console.log(chalk11.green(" \u2713 Nginx reloaded successfully"));
1859
+ console.log(chalk12.green(" \u2713 Nginx reloaded successfully"));
1444
1860
  } else {
1445
- console.log(chalk11.yellow(" - Skipped nginx reload (--no-reload)"));
1861
+ console.log(chalk12.yellow(" - Skipped nginx reload (--no-reload)"));
1446
1862
  }
1447
1863
  const isDirectMode = defaults.mode === "direct";
1448
1864
  if (isDirectMode) {
1449
- console.log(chalk11.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
1865
+ console.log(chalk12.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
1450
1866
  } else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
1451
- console.log(chalk11.dim("\nConfiguring Cloudflare DNS routes..."));
1867
+ console.log(chalk12.dim("\nConfiguring Cloudflare DNS routes..."));
1452
1868
  if (!isCloudflaredInstalled()) {
1453
- console.log(chalk11.yellow(" - cloudflared not installed, skipping DNS routes"));
1869
+ console.log(chalk12.yellow(" - cloudflared not installed, skipping DNS routes"));
1454
1870
  } else {
1455
1871
  const dnsResults = routeDnsForAllProjects();
1456
1872
  if (dnsResults.length === 0) {
1457
- console.log(chalk11.dim(" No hostnames to route"));
1873
+ console.log(chalk12.dim(" No hostnames to route"));
1458
1874
  } else {
1459
1875
  for (const result of dnsResults) {
1460
1876
  if (result.skipped) {
1461
- console.log(chalk11.dim(` - ${result.hostname} (local - skipped)`));
1877
+ console.log(chalk12.dim(` - ${result.hostname} (local - skipped)`));
1462
1878
  } else if (result.success) {
1463
1879
  const msg = result.output?.includes("already exists") ? "exists" : "routed";
1464
- console.log(chalk11.green(` \u2713 ${result.hostname} (${msg})`));
1880
+ console.log(chalk12.green(` \u2713 ${result.hostname} (${msg})`));
1465
1881
  } else {
1466
- console.log(chalk11.red(` \u2717 ${result.hostname}: ${result.error}`));
1882
+ console.log(chalk12.red(` \u2717 ${result.hostname}: ${result.error}`));
1467
1883
  }
1468
1884
  }
1469
1885
  }
1470
1886
  }
1471
1887
  } else if (options.noCloudflare) {
1472
- console.log(chalk11.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
1888
+ console.log(chalk12.dim("\n - Skipped Cloudflare DNS routes (--no-cloudflare)"));
1473
1889
  }
1474
1890
  if (isDirectMode && defaults.sslEnabled && options.ssl !== false) {
1475
- console.log(chalk11.dim("\nSetting up SSL certificates..."));
1891
+ console.log(chalk12.dim("\nSetting up SSL certificates..."));
1476
1892
  const hostnames = config.projects.filter((p) => p.enabled !== false && !p.local).map((p) => p.hostname);
1477
1893
  if (hostnames.length === 0) {
1478
- console.log(chalk11.dim(" No hostnames to secure"));
1894
+ console.log(chalk12.dim(" No hostnames to secure"));
1479
1895
  } else {
1480
1896
  const certbotResult = execCommandSafe("which certbot");
1481
1897
  if (!certbotResult.success) {
1482
- console.log(chalk11.yellow(" - certbot not installed, skipping SSL"));
1483
- console.log(chalk11.dim(" Run: bindler setup --direct"));
1898
+ console.log(chalk12.yellow(" - certbot not installed, skipping SSL"));
1899
+ console.log(chalk12.dim(" Run: bindler setup --direct"));
1484
1900
  } else {
1485
1901
  for (const hostname of hostnames) {
1486
- console.log(chalk11.dim(` Requesting certificate for ${hostname}...`));
1902
+ console.log(chalk12.dim(` Requesting certificate for ${hostname}...`));
1487
1903
  const email = defaults.sslEmail || "admin@" + hostname.split(".").slice(-2).join(".");
1488
1904
  const result = execCommandSafe(
1489
1905
  `sudo certbot --nginx -d ${hostname} --non-interactive --agree-tos --email ${email} 2>&1`
1490
1906
  );
1491
1907
  if (result.success || result.output?.includes("Certificate not yet due for renewal")) {
1492
- console.log(chalk11.green(` \u2713 ${hostname} (secured)`));
1908
+ console.log(chalk12.green(` \u2713 ${hostname} (secured)`));
1493
1909
  } else if (result.output?.includes("already exists")) {
1494
- console.log(chalk11.green(` \u2713 ${hostname} (exists)`));
1910
+ console.log(chalk12.green(` \u2713 ${hostname} (exists)`));
1495
1911
  } else {
1496
- console.log(chalk11.yellow(` ! ${hostname}: ${result.error || "failed"}`));
1497
- console.log(chalk11.dim(" Run manually: sudo certbot --nginx -d " + hostname));
1912
+ console.log(chalk12.yellow(` ! ${hostname}: ${result.error || "failed"}`));
1913
+ console.log(chalk12.dim(" Run manually: sudo certbot --nginx -d " + hostname));
1498
1914
  }
1499
1915
  }
1500
1916
  }
1501
1917
  }
1502
1918
  }
1503
- console.log(chalk11.green("\n\u2713 Configuration applied successfully!"));
1504
- console.log(chalk11.dim(`
1919
+ console.log(chalk12.green("\n\u2713 Configuration applied successfully!"));
1920
+ console.log(chalk12.dim(`
1505
1921
  ${config.projects.length} project(s) configured:`));
1506
1922
  for (const project of config.projects) {
1507
- const status = project.enabled !== false ? chalk11.green("enabled") : chalk11.yellow("disabled");
1508
- console.log(chalk11.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
1923
+ const status = project.enabled !== false ? chalk12.green("enabled") : chalk12.yellow("disabled");
1924
+ console.log(chalk12.dim(` - ${project.name} \u2192 ${project.hostname} (${status})`));
1509
1925
  }
1510
1926
  }
1511
1927
 
1512
1928
  // src/commands/doctor.ts
1513
- import chalk12 from "chalk";
1514
- import { existsSync as existsSync5 } from "fs";
1929
+ import chalk13 from "chalk";
1930
+ import { existsSync as existsSync9 } from "fs";
1515
1931
  async function doctorCommand() {
1516
- console.log(chalk12.blue("Running diagnostics...\n"));
1932
+ console.log(chalk13.blue("Running diagnostics...\n"));
1517
1933
  const checks = [];
1518
1934
  const nodeVersion = process.version;
1519
1935
  const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
@@ -1649,7 +2065,7 @@ async function doctorCommand() {
1649
2065
  }
1650
2066
  }
1651
2067
  const defaults = getDefaults();
1652
- if (existsSync5(defaults.nginxManagedPath)) {
2068
+ if (existsSync9(defaults.nginxManagedPath)) {
1653
2069
  checks.push({
1654
2070
  name: "Nginx config file",
1655
2071
  status: "ok",
@@ -1663,7 +2079,7 @@ async function doctorCommand() {
1663
2079
  fix: "Run: sudo bindler apply"
1664
2080
  });
1665
2081
  }
1666
- if (existsSync5(defaults.projectsRoot)) {
2082
+ if (existsSync9(defaults.projectsRoot)) {
1667
2083
  checks.push({
1668
2084
  name: "Projects root",
1669
2085
  status: "ok",
@@ -1674,7 +2090,67 @@ async function doctorCommand() {
1674
2090
  name: "Projects root",
1675
2091
  status: "warning",
1676
2092
  message: `Not found at ${defaults.projectsRoot}`,
1677
- fix: `Create directory: sudo mkdir -p ${defaults.projectsRoot}`
2093
+ fix: `Create directory: mkdir -p ${defaults.projectsRoot}`
2094
+ });
2095
+ }
2096
+ const validation = validateConfig(config);
2097
+ if (validation.errors.length > 0) {
2098
+ for (const error of validation.errors) {
2099
+ checks.push({
2100
+ name: "Project validation",
2101
+ status: "error",
2102
+ message: error
2103
+ });
2104
+ }
2105
+ }
2106
+ if (validation.warnings.length > 0) {
2107
+ for (const warning of validation.warnings) {
2108
+ checks.push({
2109
+ name: "Project validation",
2110
+ status: "warning",
2111
+ message: warning
2112
+ });
2113
+ }
2114
+ }
2115
+ if (validation.valid && validation.warnings.length === 0) {
2116
+ checks.push({
2117
+ name: "Project validation",
2118
+ status: "ok",
2119
+ message: "All projects validated successfully"
2120
+ });
2121
+ }
2122
+ const localProjects = config.projects.filter((p) => p.local && p.enabled !== false);
2123
+ if (localProjects.length > 0) {
2124
+ const hostsResult = execCommandSafe("cat /etc/hosts");
2125
+ if (hostsResult.success) {
2126
+ const missingHosts = localProjects.filter(
2127
+ (p) => !hostsResult.output.includes(p.hostname)
2128
+ );
2129
+ if (missingHosts.length > 0) {
2130
+ for (const p of missingHosts) {
2131
+ checks.push({
2132
+ name: "Hosts file",
2133
+ status: "warning",
2134
+ message: `Missing entry for ${p.hostname}`,
2135
+ fix: `echo "127.0.0.1 ${p.hostname}" | sudo tee -a /etc/hosts`
2136
+ });
2137
+ }
2138
+ } else {
2139
+ checks.push({
2140
+ name: "Hosts file",
2141
+ status: "ok",
2142
+ message: `All ${localProjects.length} local hostname(s) configured`
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ const listenPort = parseInt(defaults.nginxListen.split(":").pop() || "80", 10);
2148
+ if (listenPort <= 1024 && process.getuid && process.getuid() !== 0) {
2149
+ checks.push({
2150
+ name: "Nginx port",
2151
+ status: "warning",
2152
+ message: `Port ${listenPort} requires root privileges`,
2153
+ fix: "Run: sudo bindler apply (or change nginxListen to a port > 1024)"
1678
2154
  });
1679
2155
  }
1680
2156
  } else {
@@ -1693,47 +2169,47 @@ async function doctorCommand() {
1693
2169
  switch (check.status) {
1694
2170
  case "ok":
1695
2171
  icon = "\u2713";
1696
- color = chalk12.green;
2172
+ color = chalk13.green;
1697
2173
  break;
1698
2174
  case "warning":
1699
2175
  icon = "!";
1700
- color = chalk12.yellow;
2176
+ color = chalk13.yellow;
1701
2177
  hasWarnings = true;
1702
2178
  break;
1703
2179
  case "error":
1704
2180
  icon = "\u2717";
1705
- color = chalk12.red;
2181
+ color = chalk13.red;
1706
2182
  hasErrors = true;
1707
2183
  break;
1708
2184
  }
1709
- console.log(`${color(icon)} ${chalk12.bold(check.name)}: ${check.message}`);
2185
+ console.log(`${color(icon)} ${chalk13.bold(check.name)}: ${check.message}`);
1710
2186
  if (check.fix) {
1711
- console.log(chalk12.dim(` Fix: ${check.fix}`));
2187
+ console.log(chalk13.dim(` Fix: ${check.fix}`));
1712
2188
  }
1713
2189
  }
1714
2190
  console.log("");
1715
2191
  if (hasErrors) {
1716
- console.log(chalk12.red("Some checks failed. Please fix the issues above."));
2192
+ console.log(chalk13.red("Some checks failed. Please fix the issues above."));
1717
2193
  process.exit(1);
1718
2194
  } else if (hasWarnings) {
1719
- console.log(chalk12.yellow("Some warnings detected. Review the suggestions above."));
2195
+ console.log(chalk13.yellow("Some warnings detected. Review the suggestions above."));
1720
2196
  } else {
1721
- console.log(chalk12.green("All checks passed!"));
2197
+ console.log(chalk13.green("All checks passed!"));
1722
2198
  }
1723
2199
  }
1724
2200
 
1725
2201
  // src/commands/ports.ts
1726
- import chalk13 from "chalk";
2202
+ import chalk14 from "chalk";
1727
2203
  import Table3 from "cli-table3";
1728
2204
  async function portsCommand() {
1729
2205
  const ports = getPortsTable();
1730
2206
  if (ports.length === 0) {
1731
- console.log(chalk13.yellow("No ports allocated."));
1732
- console.log(chalk13.dim("npm projects will have ports allocated when created."));
2207
+ console.log(chalk14.yellow("No ports allocated."));
2208
+ console.log(chalk14.dim("npm projects will have ports allocated when created."));
1733
2209
  return;
1734
2210
  }
1735
2211
  const table = new Table3({
1736
- head: [chalk13.cyan("Port"), chalk13.cyan("Project"), chalk13.cyan("Hostname")],
2212
+ head: [chalk14.cyan("Port"), chalk14.cyan("Project"), chalk14.cyan("Hostname")],
1737
2213
  style: {
1738
2214
  head: [],
1739
2215
  border: []
@@ -1743,25 +2219,25 @@ async function portsCommand() {
1743
2219
  table.push([String(entry.port), entry.project, entry.hostname]);
1744
2220
  }
1745
2221
  console.log(table.toString());
1746
- console.log(chalk13.dim(`
2222
+ console.log(chalk14.dim(`
1747
2223
  ${ports.length} port(s) allocated`));
1748
2224
  }
1749
2225
 
1750
2226
  // src/commands/info.ts
1751
- import chalk14 from "chalk";
2227
+ import chalk15 from "chalk";
1752
2228
  async function infoCommand() {
1753
- console.log(chalk14.bold.cyan(String.raw`
2229
+ console.log(chalk15.bold.cyan(String.raw`
1754
2230
  _ _ _ _
1755
2231
  | |_|_|___ _| | |___ ___
1756
2232
  | . | | | . | | -_| _|
1757
2233
  |___|_|_|_|___|_|___|_|
1758
2234
  `));
1759
- console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
1760
- console.log(chalk14.white(" with Nginx and PM2\n"));
1761
- console.log(chalk14.dim(" Version: ") + chalk14.white("1.1.1"));
1762
- console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
1763
- console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
1764
- console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
2235
+ console.log(chalk15.white(" Manage multiple projects behind Cloudflare Tunnel"));
2236
+ console.log(chalk15.white(" with Nginx and PM2\n"));
2237
+ console.log(chalk15.dim(" Version: ") + chalk15.white("1.3.0"));
2238
+ console.log(chalk15.dim(" Author: ") + chalk15.white("alfaoz"));
2239
+ console.log(chalk15.dim(" License: ") + chalk15.white("MIT"));
2240
+ console.log(chalk15.dim(" GitHub: ") + chalk15.cyan("https://github.com/alfaoz/bindler"));
1765
2241
  console.log("");
1766
2242
  if (configExists()) {
1767
2243
  const config = readConfig();
@@ -1769,23 +2245,23 @@ async function infoCommand() {
1769
2245
  const runningCount = pm2Processes.filter((p) => p.status === "online").length;
1770
2246
  const npmProjects = config.projects.filter((p) => p.type === "npm").length;
1771
2247
  const staticProjects = config.projects.filter((p) => p.type === "static").length;
1772
- console.log(chalk14.dim(" Config: ") + chalk14.white(getConfigPath()));
1773
- console.log(chalk14.dim(" Nginx: ") + chalk14.white(config.defaults.nginxManagedPath));
1774
- console.log(chalk14.dim(" Tunnel: ") + chalk14.white(config.defaults.tunnelName));
2248
+ console.log(chalk15.dim(" Config: ") + chalk15.white(getConfigPath()));
2249
+ console.log(chalk15.dim(" Nginx: ") + chalk15.white(config.defaults.nginxManagedPath));
2250
+ console.log(chalk15.dim(" Tunnel: ") + chalk15.white(config.defaults.tunnelName));
1775
2251
  console.log("");
1776
- console.log(chalk14.dim(" Projects: ") + chalk14.white(`${config.projects.length} total (${staticProjects} static, ${npmProjects} npm)`));
2252
+ console.log(chalk15.dim(" Projects: ") + chalk15.white(`${config.projects.length} total (${staticProjects} static, ${npmProjects} npm)`));
1777
2253
  if (npmProjects > 0) {
1778
- console.log(chalk14.dim(" Running: ") + chalk14.green(`${runningCount}/${npmProjects} npm apps online`));
2254
+ console.log(chalk15.dim(" Running: ") + chalk15.green(`${runningCount}/${npmProjects} npm apps online`));
1779
2255
  }
1780
2256
  } else {
1781
- console.log(chalk14.dim(" Config: ") + chalk14.yellow("Not initialized"));
1782
- console.log(chalk14.dim(" ") + chalk14.dim("Run `bindler new` to get started"));
2257
+ console.log(chalk15.dim(" Config: ") + chalk15.yellow("Not initialized"));
2258
+ console.log(chalk15.dim(" ") + chalk15.dim("Run `bindler new` to get started"));
1783
2259
  }
1784
2260
  console.log("");
1785
2261
  }
1786
2262
 
1787
2263
  // src/commands/check.ts
1788
- import chalk15 from "chalk";
2264
+ import chalk16 from "chalk";
1789
2265
  import { resolve4, resolve6, resolveCname } from "dns/promises";
1790
2266
  async function checkDns(hostname) {
1791
2267
  const result = {
@@ -1846,111 +2322,111 @@ async function checkCommand(hostnameOrName, options) {
1846
2322
  const hostname = project ? project.hostname : hostnameOrName;
1847
2323
  const basePath = project?.basePath || "/";
1848
2324
  const isLocal = project?.local || hostname.endsWith(".local") || hostname.endsWith(".localhost");
1849
- console.log(chalk15.blue(`
2325
+ console.log(chalk16.blue(`
1850
2326
  Checking ${hostname}...
1851
2327
  `));
1852
- console.log(chalk15.bold("DNS Resolution:"));
2328
+ console.log(chalk16.bold("DNS Resolution:"));
1853
2329
  const dns = await checkDns(hostname);
1854
2330
  if (!dns.resolved) {
1855
- console.log(chalk15.red(" \u2717 DNS not resolving"));
2331
+ console.log(chalk16.red(" \u2717 DNS not resolving"));
1856
2332
  if (isLocal) {
1857
- console.log(chalk15.dim(" Local hostname - add to /etc/hosts:"));
1858
- console.log(chalk15.cyan(` echo "127.0.0.1 ${hostname}" | sudo tee -a /etc/hosts`));
2333
+ console.log(chalk16.dim(" Local hostname - add to /etc/hosts:"));
2334
+ console.log(chalk16.cyan(` echo "127.0.0.1 ${hostname}" | sudo tee -a /etc/hosts`));
1859
2335
  } else {
1860
- console.log(chalk15.dim(" No DNS records found for this hostname."));
1861
- console.log(chalk15.dim(" If using Cloudflare Tunnel, run: bindler apply"));
1862
- console.log(chalk15.dim(" If using direct mode, add an A record pointing to your server IP."));
2336
+ console.log(chalk16.dim(" No DNS records found for this hostname."));
2337
+ console.log(chalk16.dim(" If using Cloudflare Tunnel, run: bindler apply"));
2338
+ console.log(chalk16.dim(" If using direct mode, add an A record pointing to your server IP."));
1863
2339
  }
1864
2340
  console.log("");
1865
2341
  return;
1866
2342
  }
1867
2343
  if (dns.cname.length > 0) {
1868
- console.log(chalk15.green(" \u2713 CNAME: ") + dns.cname.join(", "));
2344
+ console.log(chalk16.green(" \u2713 CNAME: ") + dns.cname.join(", "));
1869
2345
  if (dns.isCloudflare) {
1870
- console.log(chalk15.green(" \u2713 Points to Cloudflare Tunnel"));
2346
+ console.log(chalk16.green(" \u2713 Points to Cloudflare Tunnel"));
1871
2347
  }
1872
2348
  }
1873
2349
  if (dns.ipv4.length > 0) {
1874
- console.log(chalk15.green(" \u2713 A (IPv4): ") + dns.ipv4.join(", "));
2350
+ console.log(chalk16.green(" \u2713 A (IPv4): ") + dns.ipv4.join(", "));
1875
2351
  }
1876
2352
  if (dns.ipv6.length > 0) {
1877
- console.log(chalk15.green(" \u2713 AAAA (IPv6): ") + dns.ipv6.join(", "));
2353
+ console.log(chalk16.green(" \u2713 AAAA (IPv6): ") + dns.ipv6.join(", "));
1878
2354
  }
1879
2355
  console.log("");
1880
- console.log(chalk15.bold("HTTP Check:"));
2356
+ console.log(chalk16.bold("HTTP Check:"));
1881
2357
  const http = await checkHttp(hostname, basePath);
1882
2358
  if (!http.reachable) {
1883
- console.log(chalk15.red(" \u2717 Not reachable"));
2359
+ console.log(chalk16.red(" \u2717 Not reachable"));
1884
2360
  const err = http.error || "";
1885
2361
  if (err.includes("ECONNREFUSED")) {
1886
- console.log(chalk15.dim(" Connection refused - server not accepting connections"));
1887
- console.log(chalk15.yellow("\n Check:"));
1888
- console.log(chalk15.dim(" - Is nginx running? Run: bindler doctor"));
2362
+ console.log(chalk16.dim(" Connection refused - server not accepting connections"));
2363
+ console.log(chalk16.yellow("\n Check:"));
2364
+ console.log(chalk16.dim(" - Is nginx running? Run: bindler doctor"));
1889
2365
  if (project?.type === "npm") {
1890
- console.log(chalk15.dim(` - Is the app running? Run: bindler start ${project.name}`));
2366
+ console.log(chalk16.dim(` - Is the app running? Run: bindler start ${project.name}`));
1891
2367
  }
1892
2368
  } else if (err.includes("ENOTFOUND")) {
1893
- console.log(chalk15.dim(" Hostname not found"));
1894
- console.log(chalk15.yellow("\n Check:"));
1895
- console.log(chalk15.dim(" - DNS may not be propagated yet (wait a few minutes)"));
1896
- console.log(chalk15.dim(" - Verify DNS records are correct"));
2369
+ console.log(chalk16.dim(" Hostname not found"));
2370
+ console.log(chalk16.yellow("\n Check:"));
2371
+ console.log(chalk16.dim(" - DNS may not be propagated yet (wait a few minutes)"));
2372
+ console.log(chalk16.dim(" - Verify DNS records are correct"));
1897
2373
  } else if (err.includes("ETIMEDOUT") || err.includes("timeout")) {
1898
- console.log(chalk15.dim(" Connection timed out"));
1899
- console.log(chalk15.yellow("\n Check:"));
2374
+ console.log(chalk16.dim(" Connection timed out"));
2375
+ console.log(chalk16.yellow("\n Check:"));
1900
2376
  if (isLocal) {
1901
- console.log(chalk15.dim(" - Is nginx running on port 8080?"));
2377
+ console.log(chalk16.dim(" - Is nginx running on port 8080?"));
1902
2378
  } else {
1903
- console.log(chalk15.dim(" - Is your server/tunnel reachable from the internet?"));
1904
- console.log(chalk15.dim(" - Check firewall rules"));
2379
+ console.log(chalk16.dim(" - Is your server/tunnel reachable from the internet?"));
2380
+ console.log(chalk16.dim(" - Check firewall rules"));
1905
2381
  }
1906
2382
  } else if (err.includes("CERT") || err.includes("SSL") || err.includes("certificate")) {
1907
- console.log(chalk15.dim(" SSL/TLS certificate error"));
1908
- console.log(chalk15.yellow("\n Check:"));
1909
- console.log(chalk15.dim(" - Run: sudo bindler apply (to refresh SSL certs)"));
1910
- console.log(chalk15.dim(" - Or check certbot: sudo certbot certificates"));
2383
+ console.log(chalk16.dim(" SSL/TLS certificate error"));
2384
+ console.log(chalk16.yellow("\n Check:"));
2385
+ console.log(chalk16.dim(" - Run: sudo bindler apply (to refresh SSL certs)"));
2386
+ console.log(chalk16.dim(" - Or check certbot: sudo certbot certificates"));
1911
2387
  } else {
1912
- console.log(chalk15.dim(` Error: ${err}`));
1913
- console.log(chalk15.yellow("\n Possible issues:"));
2388
+ console.log(chalk16.dim(` Error: ${err}`));
2389
+ console.log(chalk16.yellow("\n Possible issues:"));
1914
2390
  if (!isLocal) {
1915
- console.log(chalk15.dim(" - Cloudflare tunnel not running"));
2391
+ console.log(chalk16.dim(" - Cloudflare tunnel not running"));
1916
2392
  }
1917
- console.log(chalk15.dim(" - Nginx not running or misconfigured"));
2393
+ console.log(chalk16.dim(" - Nginx not running or misconfigured"));
1918
2394
  if (project?.type === "npm") {
1919
- console.log(chalk15.dim(" - Project not started"));
2395
+ console.log(chalk16.dim(" - Project not started"));
1920
2396
  }
1921
2397
  }
1922
2398
  console.log("");
1923
2399
  return;
1924
2400
  }
1925
- const statusColor = http.statusCode < 400 ? chalk15.green : chalk15.red;
2401
+ const statusColor = http.statusCode < 400 ? chalk16.green : chalk16.red;
1926
2402
  console.log(statusColor(` \u2713 Status: ${http.statusCode}`));
1927
- console.log(chalk15.dim(` Response time: ${http.responseTime}ms`));
2403
+ console.log(chalk16.dim(` Response time: ${http.responseTime}ms`));
1928
2404
  if (http.redirectUrl) {
1929
- console.log(chalk15.dim(` Redirects to: ${http.redirectUrl}`));
2405
+ console.log(chalk16.dim(` Redirects to: ${http.redirectUrl}`));
1930
2406
  }
1931
2407
  console.log("");
1932
2408
  if (dns.resolved && http.reachable && http.statusCode < 400) {
1933
- console.log(chalk15.green("\u2713 All checks passed! Site is accessible."));
2409
+ console.log(chalk16.green("\u2713 All checks passed! Site is accessible."));
1934
2410
  } else if (dns.resolved && http.reachable) {
1935
- console.log(chalk15.yellow("! Site is reachable but returned an error status."));
2411
+ console.log(chalk16.yellow("! Site is reachable but returned an error status."));
1936
2412
  } else {
1937
- console.log(chalk15.red("\u2717 Some checks failed. See details above."));
2413
+ console.log(chalk16.red("\u2717 Some checks failed. See details above."));
1938
2414
  }
1939
2415
  if (project) {
1940
- console.log(chalk15.dim(`
2416
+ console.log(chalk16.dim(`
1941
2417
  Project: ${project.name} (${project.type})`));
1942
2418
  if (project.type === "npm") {
1943
- console.log(chalk15.dim(`Port: ${project.port}`));
2419
+ console.log(chalk16.dim(`Port: ${project.port}`));
1944
2420
  }
1945
2421
  }
1946
2422
  console.log("");
1947
2423
  }
1948
2424
 
1949
2425
  // src/commands/setup.ts
1950
- import chalk16 from "chalk";
2426
+ import chalk17 from "chalk";
1951
2427
  import inquirer3 from "inquirer";
1952
2428
  import { execSync as execSync2 } from "child_process";
1953
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2429
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
1954
2430
  function detectOs() {
1955
2431
  const platform = process.platform;
1956
2432
  if (platform === "darwin") {
@@ -1961,8 +2437,8 @@ function detectOs() {
1961
2437
  }
1962
2438
  if (platform === "linux") {
1963
2439
  try {
1964
- if (existsSync6("/etc/os-release")) {
1965
- const osRelease = readFileSync4("/etc/os-release", "utf-8");
2440
+ if (existsSync10("/etc/os-release")) {
2441
+ const osRelease = readFileSync5("/etc/os-release", "utf-8");
1966
2442
  const lines = osRelease.split("\n");
1967
2443
  const info = {};
1968
2444
  for (const line of lines) {
@@ -1978,7 +2454,7 @@ function detectOs() {
1978
2454
  if (["ubuntu", "debian", "pop", "mint", "elementary"].includes(distro)) {
1979
2455
  packageManager = "apt";
1980
2456
  } else if (["fedora", "rhel", "centos", "rocky", "alma"].includes(distro)) {
1981
- packageManager = existsSync6("/usr/bin/dnf") ? "dnf" : "yum";
2457
+ packageManager = existsSync10("/usr/bin/dnf") ? "dnf" : "yum";
1982
2458
  } else if (["amzn"].includes(distro)) {
1983
2459
  packageManager = "yum";
1984
2460
  }
@@ -1991,7 +2467,7 @@ function detectOs() {
1991
2467
  return { platform: "unknown" };
1992
2468
  }
1993
2469
  function runCommand(command, description) {
1994
- console.log(chalk16.dim(` \u2192 ${description}...`));
2470
+ console.log(chalk17.dim(` \u2192 ${description}...`));
1995
2471
  try {
1996
2472
  execSync2(command, { stdio: "inherit" });
1997
2473
  return true;
@@ -2000,7 +2476,7 @@ function runCommand(command, description) {
2000
2476
  }
2001
2477
  }
2002
2478
  async function installNginx(os) {
2003
- console.log(chalk16.blue("\nInstalling nginx...\n"));
2479
+ console.log(chalk17.blue("\nInstalling nginx...\n"));
2004
2480
  if (os.platform === "darwin") {
2005
2481
  return runCommand("brew install nginx", "Installing via Homebrew");
2006
2482
  }
@@ -2011,16 +2487,16 @@ async function installNginx(os) {
2011
2487
  if (os.platform === "linux" && (os.packageManager === "yum" || os.packageManager === "dnf")) {
2012
2488
  return runCommand(`sudo ${os.packageManager} install -y nginx`, "Installing nginx");
2013
2489
  }
2014
- console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
2015
- console.log(chalk16.dim(" Please install nginx manually."));
2490
+ console.log(chalk17.yellow(" Automatic installation not supported for your OS."));
2491
+ console.log(chalk17.dim(" Please install nginx manually."));
2016
2492
  return false;
2017
2493
  }
2018
2494
  async function installPm2() {
2019
- console.log(chalk16.blue("\nInstalling PM2...\n"));
2495
+ console.log(chalk17.blue("\nInstalling PM2...\n"));
2020
2496
  return runCommand("npm install -g pm2", "Installing via npm");
2021
2497
  }
2022
2498
  async function installCertbot(os) {
2023
- console.log(chalk16.blue("\nInstalling certbot (Let's Encrypt)...\n"));
2499
+ console.log(chalk17.blue("\nInstalling certbot (Let's Encrypt)...\n"));
2024
2500
  if (os.platform === "darwin") {
2025
2501
  return runCommand("brew install certbot", "Installing via Homebrew");
2026
2502
  }
@@ -2031,8 +2507,8 @@ async function installCertbot(os) {
2031
2507
  if (os.platform === "linux" && (os.packageManager === "yum" || os.packageManager === "dnf")) {
2032
2508
  return runCommand(`sudo ${os.packageManager} install -y certbot python3-certbot-nginx`, "Installing certbot");
2033
2509
  }
2034
- console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
2035
- console.log(chalk16.dim(" Please install certbot manually."));
2510
+ console.log(chalk17.yellow(" Automatic installation not supported for your OS."));
2511
+ console.log(chalk17.dim(" Please install certbot manually."));
2036
2512
  return false;
2037
2513
  }
2038
2514
  function isCertbotInstalled() {
@@ -2040,7 +2516,7 @@ function isCertbotInstalled() {
2040
2516
  return result.success;
2041
2517
  }
2042
2518
  async function installCloudflared(os) {
2043
- console.log(chalk16.blue("\nInstalling cloudflared...\n"));
2519
+ console.log(chalk17.blue("\nInstalling cloudflared...\n"));
2044
2520
  if (os.platform === "darwin") {
2045
2521
  return runCommand("brew install cloudflared", "Installing via Homebrew");
2046
2522
  }
@@ -2074,21 +2550,21 @@ async function installCloudflared(os) {
2074
2550
  );
2075
2551
  return runCommand(`sudo ${os.packageManager} install -y cloudflared`, "Installing cloudflared");
2076
2552
  }
2077
- console.log(chalk16.yellow(" Automatic installation not supported for your OS."));
2078
- console.log(chalk16.dim(" Visit: https://pkg.cloudflare.com/index.html"));
2553
+ console.log(chalk17.yellow(" Automatic installation not supported for your OS."));
2554
+ console.log(chalk17.dim(" Visit: https://pkg.cloudflare.com/index.html"));
2079
2555
  return false;
2080
2556
  }
2081
2557
  async function setupCommand(options = {}) {
2082
- console.log(chalk16.bold.cyan("\nBindler Setup\n"));
2558
+ console.log(chalk17.bold.cyan("\nBindler Setup\n"));
2083
2559
  const os = detectOs();
2084
- console.log(chalk16.dim(`Detected: ${os.distro || os.platform}${os.version ? ` ${os.version}` : ""}${os.codename ? ` (${os.codename})` : ""}`));
2560
+ console.log(chalk17.dim(`Detected: ${os.distro || os.platform}${os.version ? ` ${os.version}` : ""}${os.codename ? ` (${os.codename})` : ""}`));
2085
2561
  const isDirect = options.direct;
2086
2562
  if (isDirect) {
2087
- console.log(chalk16.cyan("\nDirect mode (VPS without Cloudflare Tunnel)"));
2088
- console.log(chalk16.dim("nginx will listen on port 80/443 directly\n"));
2563
+ console.log(chalk17.cyan("\nDirect mode (VPS without Cloudflare Tunnel)"));
2564
+ console.log(chalk17.dim("nginx will listen on port 80/443 directly\n"));
2089
2565
  } else {
2090
- console.log(chalk16.cyan("\nTunnel mode (via Cloudflare Tunnel)"));
2091
- console.log(chalk16.dim("nginx will listen on 127.0.0.1:8080\n"));
2566
+ console.log(chalk17.cyan("\nTunnel mode (via Cloudflare Tunnel)"));
2567
+ console.log(chalk17.dim("nginx will listen on 127.0.0.1:8080\n"));
2092
2568
  }
2093
2569
  const missing = [];
2094
2570
  if (!isNginxInstalled()) {
@@ -2107,11 +2583,11 @@ async function setupCommand(options = {}) {
2107
2583
  }
2108
2584
  }
2109
2585
  if (missing.length === 0) {
2110
- console.log(chalk16.green("\u2713 All dependencies are already installed!\n"));
2586
+ console.log(chalk17.green("\u2713 All dependencies are already installed!\n"));
2111
2587
  } else {
2112
- console.log(chalk16.yellow("Missing dependencies:\n"));
2588
+ console.log(chalk17.yellow("Missing dependencies:\n"));
2113
2589
  for (const dep of missing) {
2114
- console.log(chalk16.red(` \u2717 ${dep.name}`));
2590
+ console.log(chalk17.red(` \u2717 ${dep.name}`));
2115
2591
  }
2116
2592
  console.log("");
2117
2593
  const { toInstall } = await inquirer3.prompt([
@@ -2135,12 +2611,12 @@ async function setupCommand(options = {}) {
2135
2611
  results.push({ name: depName, success });
2136
2612
  }
2137
2613
  }
2138
- console.log(chalk16.bold("\n\nInstallation Summary:\n"));
2614
+ console.log(chalk17.bold("\n\nInstallation Summary:\n"));
2139
2615
  for (const result of results) {
2140
2616
  if (result.success) {
2141
- console.log(chalk16.green(` \u2713 ${result.name} installed`));
2617
+ console.log(chalk17.green(` \u2713 ${result.name} installed`));
2142
2618
  } else {
2143
- console.log(chalk16.red(` \u2717 ${result.name} failed`));
2619
+ console.log(chalk17.red(` \u2717 ${result.name} failed`));
2144
2620
  }
2145
2621
  }
2146
2622
  console.log("");
@@ -2182,32 +2658,32 @@ async function setupCommand(options = {}) {
2182
2658
  config.defaults.applyCloudflareDnsRoutes = true;
2183
2659
  }
2184
2660
  writeConfig(config);
2185
- console.log(chalk16.green("\n\u2713 Setup complete!\n"));
2186
- console.log(chalk16.dim(`Mode: ${config.defaults.mode}`));
2187
- console.log(chalk16.dim(`nginx listen: ${config.defaults.nginxListen}`));
2661
+ console.log(chalk17.green("\n\u2713 Setup complete!\n"));
2662
+ console.log(chalk17.dim(`Mode: ${config.defaults.mode}`));
2663
+ console.log(chalk17.dim(`nginx listen: ${config.defaults.nginxListen}`));
2188
2664
  if (config.defaults.sslEnabled) {
2189
- console.log(chalk16.dim(`SSL: enabled (${config.defaults.sslEmail})`));
2665
+ console.log(chalk17.dim(`SSL: enabled (${config.defaults.sslEmail})`));
2190
2666
  }
2191
- console.log(chalk16.dim("\nRun `bindler new` to create your first project."));
2667
+ console.log(chalk17.dim("\nRun `bindler new` to create your first project."));
2192
2668
  }
2193
2669
 
2194
2670
  // src/commands/init.ts
2195
- import chalk17 from "chalk";
2671
+ import chalk18 from "chalk";
2196
2672
  import inquirer4 from "inquirer";
2197
2673
  async function initCommand() {
2198
- console.log(chalk17.bold.cyan(`
2674
+ console.log(chalk18.bold.cyan(`
2199
2675
  _ _ _ _
2200
2676
  | |_|_|___ _| | |___ ___
2201
2677
  | . | | | . | | -_| _|
2202
2678
  |___|_|_|_|___|_|___|_|
2203
2679
  `));
2204
- console.log(chalk17.white(" Welcome to bindler!\n"));
2680
+ console.log(chalk18.white(" Welcome to bindler!\n"));
2205
2681
  if (configExists()) {
2206
2682
  const config2 = readConfig();
2207
- console.log(chalk17.yellow("Bindler is already initialized."));
2208
- console.log(chalk17.dim(` Config: ~/.config/bindler/config.json`));
2209
- console.log(chalk17.dim(` Mode: ${config2.defaults.mode || "tunnel"}`));
2210
- console.log(chalk17.dim(` Projects: ${config2.projects.length}`));
2683
+ console.log(chalk18.yellow("Bindler is already initialized."));
2684
+ console.log(chalk18.dim(` Config: ~/.config/bindler/config.json`));
2685
+ console.log(chalk18.dim(` Mode: ${config2.defaults.mode || "tunnel"}`));
2686
+ console.log(chalk18.dim(` Projects: ${config2.projects.length}`));
2211
2687
  console.log("");
2212
2688
  const { reinit } = await inquirer4.prompt([
2213
2689
  {
@@ -2218,11 +2694,11 @@ async function initCommand() {
2218
2694
  }
2219
2695
  ]);
2220
2696
  if (!reinit) {
2221
- console.log(chalk17.dim("\nRun `bindler new` to add a project."));
2697
+ console.log(chalk18.dim("\nRun `bindler new` to add a project."));
2222
2698
  return;
2223
2699
  }
2224
2700
  }
2225
- console.log(chalk17.bold("\n1. Choose your setup:\n"));
2701
+ console.log(chalk18.bold("\n1. Choose your setup:\n"));
2226
2702
  const { mode } = await inquirer4.prompt([
2227
2703
  {
2228
2704
  type: "list",
@@ -2244,19 +2720,19 @@ async function initCommand() {
2244
2720
  ]
2245
2721
  }
2246
2722
  ]);
2247
- console.log(chalk17.bold("\n2. Checking dependencies...\n"));
2723
+ console.log(chalk18.bold("\n2. Checking dependencies...\n"));
2248
2724
  const deps = {
2249
2725
  nginx: isNginxInstalled(),
2250
2726
  pm2: isPm2Installed(),
2251
2727
  cloudflared: isCloudflaredInstalled()
2252
2728
  };
2253
2729
  const nginxRunning = deps.nginx && isNginxRunning();
2254
- console.log(deps.nginx ? chalk17.green(" \u2713 nginx") : chalk17.red(" \u2717 nginx"));
2255
- console.log(deps.pm2 ? chalk17.green(" \u2713 pm2") : chalk17.red(" \u2717 pm2"));
2730
+ console.log(deps.nginx ? chalk18.green(" \u2713 nginx") : chalk18.red(" \u2717 nginx"));
2731
+ console.log(deps.pm2 ? chalk18.green(" \u2713 pm2") : chalk18.red(" \u2717 pm2"));
2256
2732
  if (mode === "tunnel") {
2257
- console.log(deps.cloudflared ? chalk17.green(" \u2713 cloudflared") : chalk17.red(" \u2717 cloudflared"));
2733
+ console.log(deps.cloudflared ? chalk18.green(" \u2713 cloudflared") : chalk18.red(" \u2717 cloudflared"));
2258
2734
  } else if (mode === "direct") {
2259
- console.log(chalk17.dim(" - cloudflared (not needed for direct mode)"));
2735
+ console.log(chalk18.dim(" - cloudflared (not needed for direct mode)"));
2260
2736
  }
2261
2737
  const missingDeps = !deps.nginx || !deps.pm2 || mode === "tunnel" && !deps.cloudflared;
2262
2738
  if (missingDeps) {
@@ -2273,7 +2749,7 @@ async function initCommand() {
2273
2749
  await setupCommand({ direct: mode === "direct" });
2274
2750
  }
2275
2751
  }
2276
- console.log(chalk17.bold("\n3. Configuration:\n"));
2752
+ console.log(chalk18.bold("\n3. Configuration:\n"));
2277
2753
  let tunnelName = "homelab";
2278
2754
  let sslEmail = "";
2279
2755
  if (mode === "tunnel") {
@@ -2325,29 +2801,29 @@ async function initCommand() {
2325
2801
  config.defaults.nginxListen = "127.0.0.1:8080";
2326
2802
  }
2327
2803
  writeConfig(config);
2328
- console.log(chalk17.green("\n\u2713 Bindler initialized!\n"));
2329
- console.log(chalk17.dim(" Mode: ") + chalk17.white(mode));
2330
- console.log(chalk17.dim(" Listen: ") + chalk17.white(config.defaults.nginxListen));
2804
+ console.log(chalk18.green("\n\u2713 Bindler initialized!\n"));
2805
+ console.log(chalk18.dim(" Mode: ") + chalk18.white(mode));
2806
+ console.log(chalk18.dim(" Listen: ") + chalk18.white(config.defaults.nginxListen));
2331
2807
  if (mode === "tunnel") {
2332
- console.log(chalk17.dim(" Tunnel: ") + chalk17.white(tunnelName));
2808
+ console.log(chalk18.dim(" Tunnel: ") + chalk18.white(tunnelName));
2333
2809
  }
2334
2810
  if (sslEmail) {
2335
- console.log(chalk17.dim(" SSL: ") + chalk17.white(sslEmail));
2811
+ console.log(chalk18.dim(" SSL: ") + chalk18.white(sslEmail));
2336
2812
  }
2337
- console.log(chalk17.bold("\nNext steps:\n"));
2338
- console.log(chalk17.dim(" 1. ") + chalk17.white("bindler new") + chalk17.dim(" # add your first project"));
2339
- console.log(chalk17.dim(" 2. ") + chalk17.white("bindler apply") + chalk17.dim(" # apply nginx config"));
2813
+ console.log(chalk18.bold("\nNext steps:\n"));
2814
+ console.log(chalk18.dim(" 1. ") + chalk18.white("bindler new") + chalk18.dim(" # add your first project"));
2815
+ console.log(chalk18.dim(" 2. ") + chalk18.white("bindler apply") + chalk18.dim(" # apply nginx config"));
2340
2816
  if (mode === "tunnel") {
2341
- console.log(chalk17.dim(" 3. ") + chalk17.white(`cloudflared tunnel run ${tunnelName}`) + chalk17.dim(" # start tunnel"));
2817
+ console.log(chalk18.dim(" 3. ") + chalk18.white(`cloudflared tunnel run ${tunnelName}`) + chalk18.dim(" # start tunnel"));
2342
2818
  }
2343
2819
  console.log("");
2344
2820
  }
2345
2821
 
2346
2822
  // src/commands/deploy.ts
2347
- import chalk18 from "chalk";
2823
+ import chalk19 from "chalk";
2348
2824
  import { execSync as execSync3 } from "child_process";
2349
- import { existsSync as existsSync7 } from "fs";
2350
- import { join as join5 } from "path";
2825
+ import { existsSync as existsSync11 } from "fs";
2826
+ import { join as join7 } from "path";
2351
2827
  function runInDir(command, cwd) {
2352
2828
  try {
2353
2829
  const output = execSync3(command, { cwd, encoding: "utf-8", stdio: "pipe" });
@@ -2359,101 +2835,101 @@ function runInDir(command, cwd) {
2359
2835
  }
2360
2836
  async function deployCommand(name, options) {
2361
2837
  if (!name) {
2362
- console.log(chalk18.red("Usage: bindler deploy <name>"));
2363
- console.log(chalk18.dim("\nDeploys a project: git pull + npm install + restart"));
2364
- console.log(chalk18.dim("\nExamples:"));
2365
- console.log(chalk18.dim(" bindler deploy myapp"));
2366
- console.log(chalk18.dim(" bindler deploy myapp --skip-install"));
2367
- console.log(chalk18.dim(" bindler deploy myapp --skip-pull"));
2838
+ console.log(chalk19.red("Usage: bindler deploy <name>"));
2839
+ console.log(chalk19.dim("\nDeploys a project: git pull + npm install + restart"));
2840
+ console.log(chalk19.dim("\nExamples:"));
2841
+ console.log(chalk19.dim(" bindler deploy myapp"));
2842
+ console.log(chalk19.dim(" bindler deploy myapp --skip-install"));
2843
+ console.log(chalk19.dim(" bindler deploy myapp --skip-pull"));
2368
2844
  process.exit(1);
2369
2845
  }
2370
2846
  const project = getProject(name);
2371
2847
  if (!project) {
2372
- console.log(chalk18.red(`Project "${name}" not found.`));
2373
- console.log(chalk18.dim("\nAvailable projects:"));
2848
+ console.log(chalk19.red(`Project "${name}" not found.`));
2849
+ console.log(chalk19.dim("\nAvailable projects:"));
2374
2850
  const projects = listProjects();
2375
2851
  for (const p of projects) {
2376
- console.log(chalk18.dim(` - ${p.name}`));
2852
+ console.log(chalk19.dim(` - ${p.name}`));
2377
2853
  }
2378
2854
  process.exit(1);
2379
2855
  }
2380
- if (!existsSync7(project.path)) {
2381
- console.log(chalk18.red(`Project path does not exist: ${project.path}`));
2856
+ if (!existsSync11(project.path)) {
2857
+ console.log(chalk19.red(`Project path does not exist: ${project.path}`));
2382
2858
  process.exit(1);
2383
2859
  }
2384
- console.log(chalk18.blue(`
2860
+ console.log(chalk19.blue(`
2385
2861
  Deploying ${project.name}...
2386
2862
  `));
2387
2863
  if (!options.skipPull) {
2388
- const isGitRepo = existsSync7(join5(project.path, ".git"));
2864
+ const isGitRepo = existsSync11(join7(project.path, ".git"));
2389
2865
  if (isGitRepo) {
2390
- console.log(chalk18.dim("Pulling latest changes..."));
2866
+ console.log(chalk19.dim("Pulling latest changes..."));
2391
2867
  const result = runInDir("git pull", project.path);
2392
2868
  if (result.success) {
2393
2869
  if (result.output.includes("Already up to date")) {
2394
- console.log(chalk18.green(" \u2713 Already up to date"));
2870
+ console.log(chalk19.green(" \u2713 Already up to date"));
2395
2871
  } else {
2396
- console.log(chalk18.green(" \u2713 Pulled latest changes"));
2872
+ console.log(chalk19.green(" \u2713 Pulled latest changes"));
2397
2873
  if (result.output) {
2398
- console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
2874
+ console.log(chalk19.dim(` ${result.output.split("\n")[0]}`));
2399
2875
  }
2400
2876
  }
2401
2877
  } else {
2402
- console.log(chalk18.yellow(" ! Git pull failed"));
2403
- console.log(chalk18.dim(` ${result.output}`));
2878
+ console.log(chalk19.yellow(" ! Git pull failed"));
2879
+ console.log(chalk19.dim(` ${result.output}`));
2404
2880
  }
2405
2881
  } else {
2406
- console.log(chalk18.dim(" - Not a git repository, skipping pull"));
2882
+ console.log(chalk19.dim(" - Not a git repository, skipping pull"));
2407
2883
  }
2408
2884
  } else {
2409
- console.log(chalk18.dim(" - Skipped git pull (--skip-pull)"));
2885
+ console.log(chalk19.dim(" - Skipped git pull (--skip-pull)"));
2410
2886
  }
2411
2887
  if (project.type === "npm" && !options.skipInstall) {
2412
- const hasPackageJson = existsSync7(join5(project.path, "package.json"));
2888
+ const hasPackageJson = existsSync11(join7(project.path, "package.json"));
2413
2889
  if (hasPackageJson) {
2414
- console.log(chalk18.dim("Installing dependencies..."));
2890
+ console.log(chalk19.dim("Installing dependencies..."));
2415
2891
  const result = runInDir("npm install", project.path);
2416
2892
  if (result.success) {
2417
- console.log(chalk18.green(" \u2713 Dependencies installed"));
2893
+ console.log(chalk19.green(" \u2713 Dependencies installed"));
2418
2894
  } else {
2419
- console.log(chalk18.yellow(" ! npm install failed"));
2420
- console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
2895
+ console.log(chalk19.yellow(" ! npm install failed"));
2896
+ console.log(chalk19.dim(` ${result.output.split("\n")[0]}`));
2421
2897
  }
2422
2898
  }
2423
2899
  } else if (options.skipInstall) {
2424
- console.log(chalk18.dim(" - Skipped npm install (--skip-install)"));
2900
+ console.log(chalk19.dim(" - Skipped npm install (--skip-install)"));
2425
2901
  }
2426
2902
  if (project.type === "npm" && !options.skipRestart) {
2427
- console.log(chalk18.dim("Restarting application..."));
2903
+ console.log(chalk19.dim("Restarting application..."));
2428
2904
  const result = restartProject(name);
2429
2905
  if (result.success) {
2430
- console.log(chalk18.green(" \u2713 Application restarted"));
2906
+ console.log(chalk19.green(" \u2713 Application restarted"));
2431
2907
  } else {
2432
- console.log(chalk18.yellow(` ! Restart failed: ${result.error}`));
2433
- console.log(chalk18.dim(` Try: bindler start ${name}`));
2908
+ console.log(chalk19.yellow(` ! Restart failed: ${result.error}`));
2909
+ console.log(chalk19.dim(` Try: bindler start ${name}`));
2434
2910
  }
2435
2911
  } else if (project.type === "static") {
2436
- console.log(chalk18.dim(" - Static project, no restart needed"));
2912
+ console.log(chalk19.dim(" - Static project, no restart needed"));
2437
2913
  } else if (options.skipRestart) {
2438
- console.log(chalk18.dim(" - Skipped restart (--skip-restart)"));
2914
+ console.log(chalk19.dim(" - Skipped restart (--skip-restart)"));
2439
2915
  }
2440
- console.log(chalk18.green(`
2916
+ console.log(chalk19.green(`
2441
2917
  \u2713 Deploy complete for ${project.name}
2442
2918
  `));
2443
2919
  }
2444
2920
 
2445
2921
  // src/commands/backup.ts
2446
- import chalk19 from "chalk";
2447
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2922
+ import chalk20 from "chalk";
2923
+ import { existsSync as existsSync12, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
2448
2924
  import { dirname as dirname3, resolve } from "path";
2449
- import { homedir as homedir2 } from "os";
2925
+ import { homedir as homedir3 } from "os";
2450
2926
  async function backupCommand(options) {
2451
2927
  const config = readConfig();
2452
2928
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2453
- const defaultPath = resolve(homedir2(), `bindler-backup-${timestamp}.json`);
2929
+ const defaultPath = resolve(homedir3(), `bindler-backup-${timestamp}.json`);
2454
2930
  const outputPath = options.output || defaultPath;
2455
2931
  const dir = dirname3(outputPath);
2456
- if (!existsSync8(dir)) {
2932
+ if (!existsSync12(dir)) {
2457
2933
  mkdirSync4(dir, { recursive: true });
2458
2934
  }
2459
2935
  const backup = {
@@ -2461,169 +2937,169 @@ async function backupCommand(options) {
2461
2937
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2462
2938
  config
2463
2939
  };
2464
- writeFileSync4(outputPath, JSON.stringify(backup, null, 2) + "\n");
2465
- console.log(chalk19.green(`
2940
+ writeFileSync5(outputPath, JSON.stringify(backup, null, 2) + "\n");
2941
+ console.log(chalk20.green(`
2466
2942
  \u2713 Backup saved to ${outputPath}
2467
2943
  `));
2468
- console.log(chalk19.dim(` Projects: ${config.projects.length}`));
2469
- console.log(chalk19.dim(` Mode: ${config.defaults.mode || "tunnel"}`));
2944
+ console.log(chalk20.dim(` Projects: ${config.projects.length}`));
2945
+ console.log(chalk20.dim(` Mode: ${config.defaults.mode || "tunnel"}`));
2470
2946
  console.log("");
2471
- console.log(chalk19.dim("Restore with:"));
2472
- console.log(chalk19.cyan(` bindler restore ${outputPath}`));
2947
+ console.log(chalk20.dim("Restore with:"));
2948
+ console.log(chalk20.cyan(` bindler restore ${outputPath}`));
2473
2949
  console.log("");
2474
2950
  }
2475
2951
  async function restoreCommand(file, options) {
2476
2952
  if (!file) {
2477
- console.log(chalk19.red("Usage: bindler restore <file>"));
2478
- console.log(chalk19.dim("\nExamples:"));
2479
- console.log(chalk19.dim(" bindler restore ~/bindler-backup.json"));
2480
- console.log(chalk19.dim(" bindler restore backup.json --force"));
2953
+ console.log(chalk20.red("Usage: bindler restore <file>"));
2954
+ console.log(chalk20.dim("\nExamples:"));
2955
+ console.log(chalk20.dim(" bindler restore ~/bindler-backup.json"));
2956
+ console.log(chalk20.dim(" bindler restore backup.json --force"));
2481
2957
  process.exit(1);
2482
2958
  }
2483
2959
  const filePath = resolve(file);
2484
- if (!existsSync8(filePath)) {
2485
- console.log(chalk19.red(`File not found: ${filePath}`));
2960
+ if (!existsSync12(filePath)) {
2961
+ console.log(chalk20.red(`File not found: ${filePath}`));
2486
2962
  process.exit(1);
2487
2963
  }
2488
2964
  let backup;
2489
2965
  try {
2490
- const content = readFileSync5(filePath, "utf-8");
2966
+ const content = readFileSync6(filePath, "utf-8");
2491
2967
  backup = JSON.parse(content);
2492
2968
  } catch (error) {
2493
- console.log(chalk19.red("Invalid backup file. Must be valid JSON."));
2969
+ console.log(chalk20.red("Invalid backup file. Must be valid JSON."));
2494
2970
  process.exit(1);
2495
2971
  }
2496
2972
  if (!backup.config || !backup.config.defaults || !Array.isArray(backup.config.projects)) {
2497
- console.log(chalk19.red("Invalid backup format. Missing config data."));
2973
+ console.log(chalk20.red("Invalid backup format. Missing config data."));
2498
2974
  process.exit(1);
2499
2975
  }
2500
- console.log(chalk19.blue("\nBackup info:\n"));
2501
- console.log(chalk19.dim(" Exported: ") + chalk19.white(backup.exportedAt || "unknown"));
2502
- console.log(chalk19.dim(" Projects: ") + chalk19.white(backup.config.projects.length));
2503
- console.log(chalk19.dim(" Mode: ") + chalk19.white(backup.config.defaults.mode || "tunnel"));
2976
+ console.log(chalk20.blue("\nBackup info:\n"));
2977
+ console.log(chalk20.dim(" Exported: ") + chalk20.white(backup.exportedAt || "unknown"));
2978
+ console.log(chalk20.dim(" Projects: ") + chalk20.white(backup.config.projects.length));
2979
+ console.log(chalk20.dim(" Mode: ") + chalk20.white(backup.config.defaults.mode || "tunnel"));
2504
2980
  console.log("");
2505
2981
  const currentConfig = readConfig();
2506
2982
  if (currentConfig.projects.length > 0 && !options.force) {
2507
- console.log(chalk19.yellow(`Warning: You have ${currentConfig.projects.length} existing project(s).`));
2508
- console.log(chalk19.dim("Use --force to overwrite.\n"));
2983
+ console.log(chalk20.yellow(`Warning: You have ${currentConfig.projects.length} existing project(s).`));
2984
+ console.log(chalk20.dim("Use --force to overwrite.\n"));
2509
2985
  process.exit(1);
2510
2986
  }
2511
2987
  writeConfig(backup.config);
2512
- console.log(chalk19.green("\u2713 Config restored!\n"));
2513
- console.log(chalk19.dim("Run `bindler apply` to apply nginx configuration."));
2988
+ console.log(chalk20.green("\u2713 Config restored!\n"));
2989
+ console.log(chalk20.dim("Run `bindler apply` to apply nginx configuration."));
2514
2990
  console.log("");
2515
2991
  }
2516
2992
 
2517
2993
  // src/commands/ssl.ts
2518
- import chalk20 from "chalk";
2994
+ import chalk21 from "chalk";
2519
2995
  async function sslCommand(hostname, options) {
2520
2996
  if (!hostname) {
2521
- console.log(chalk20.red("Usage: bindler ssl <hostname>"));
2522
- console.log(chalk20.dim("\nRequest SSL certificate for a hostname"));
2523
- console.log(chalk20.dim("\nExamples:"));
2524
- console.log(chalk20.dim(" bindler ssl myapp.example.com"));
2525
- console.log(chalk20.dim(" bindler ssl myapp # uses project hostname"));
2526
- console.log(chalk20.dim(" bindler ssl myapp --email me@example.com"));
2997
+ console.log(chalk21.red("Usage: bindler ssl <hostname>"));
2998
+ console.log(chalk21.dim("\nRequest SSL certificate for a hostname"));
2999
+ console.log(chalk21.dim("\nExamples:"));
3000
+ console.log(chalk21.dim(" bindler ssl myapp.example.com"));
3001
+ console.log(chalk21.dim(" bindler ssl myapp # uses project hostname"));
3002
+ console.log(chalk21.dim(" bindler ssl myapp --email me@example.com"));
2527
3003
  process.exit(1);
2528
3004
  }
2529
3005
  const project = getProject(hostname);
2530
3006
  const targetHostname = project ? project.hostname : hostname;
2531
3007
  const certbotCheck = execCommandSafe("which certbot");
2532
3008
  if (!certbotCheck.success) {
2533
- console.log(chalk20.red("certbot is not installed."));
2534
- console.log(chalk20.dim("\nInstall with:"));
2535
- console.log(chalk20.dim(" macOS: brew install certbot"));
2536
- console.log(chalk20.dim(" Linux: apt install certbot python3-certbot-nginx"));
3009
+ console.log(chalk21.red("certbot is not installed."));
3010
+ console.log(chalk21.dim("\nInstall with:"));
3011
+ console.log(chalk21.dim(" macOS: brew install certbot"));
3012
+ console.log(chalk21.dim(" Linux: apt install certbot python3-certbot-nginx"));
2537
3013
  process.exit(1);
2538
3014
  }
2539
3015
  const defaults = getDefaults();
2540
3016
  const email = options.email || defaults.sslEmail || `admin@${targetHostname.split(".").slice(-2).join(".")}`;
2541
- console.log(chalk20.blue(`
3017
+ console.log(chalk21.blue(`
2542
3018
  Requesting SSL certificate for ${targetHostname}...
2543
3019
  `));
2544
3020
  let cmd = `sudo certbot --nginx -d ${targetHostname} --non-interactive --agree-tos --email ${email}`;
2545
3021
  if (options.staging) {
2546
3022
  cmd += " --staging";
2547
- console.log(chalk20.yellow("Using staging server (test certificate)\n"));
3023
+ console.log(chalk21.yellow("Using staging server (test certificate)\n"));
2548
3024
  }
2549
- console.log(chalk20.dim(`Running: ${cmd}
3025
+ console.log(chalk21.dim(`Running: ${cmd}
2550
3026
  `));
2551
3027
  const result = execCommandSafe(cmd + " 2>&1");
2552
3028
  if (result.success) {
2553
- console.log(chalk20.green(`
3029
+ console.log(chalk21.green(`
2554
3030
  \u2713 SSL certificate installed for ${targetHostname}
2555
3031
  `));
2556
- console.log(chalk20.dim("Certificate will auto-renew via certbot timer."));
3032
+ console.log(chalk21.dim("Certificate will auto-renew via certbot timer."));
2557
3033
  } else if (result.output?.includes("Certificate not yet due for renewal")) {
2558
- console.log(chalk20.green(`
3034
+ console.log(chalk21.green(`
2559
3035
  \u2713 Certificate already exists and is valid
2560
3036
  `));
2561
- console.log(chalk20.dim("Use --force with certbot to renew early if needed."));
3037
+ console.log(chalk21.dim("Use --force with certbot to renew early if needed."));
2562
3038
  } else if (result.output?.includes("too many certificates")) {
2563
- console.log(chalk20.red("\n\u2717 Rate limit reached"));
2564
- console.log(chalk20.dim("Let's Encrypt limits certificates per domain."));
2565
- console.log(chalk20.dim("Try again later or use --staging for testing."));
3039
+ console.log(chalk21.red("\n\u2717 Rate limit reached"));
3040
+ console.log(chalk21.dim("Let's Encrypt limits certificates per domain."));
3041
+ console.log(chalk21.dim("Try again later or use --staging for testing."));
2566
3042
  } else if (result.output?.includes("Could not bind")) {
2567
- console.log(chalk20.red("\n\u2717 Port 80 is in use"));
2568
- console.log(chalk20.dim("Stop nginx temporarily or use webroot method."));
3043
+ console.log(chalk21.red("\n\u2717 Port 80 is in use"));
3044
+ console.log(chalk21.dim("Stop nginx temporarily or use webroot method."));
2569
3045
  } else {
2570
- console.log(chalk20.red("\n\u2717 Certificate request failed\n"));
3046
+ console.log(chalk21.red("\n\u2717 Certificate request failed\n"));
2571
3047
  if (result.output) {
2572
3048
  const lines = result.output.split("\n").filter(
2573
3049
  (l) => l.includes("Error") || l.includes("error") || l.includes("failed") || l.includes("Challenge")
2574
3050
  );
2575
3051
  for (const line of lines.slice(0, 5)) {
2576
- console.log(chalk20.dim(` ${line.trim()}`));
3052
+ console.log(chalk21.dim(` ${line.trim()}`));
2577
3053
  }
2578
3054
  }
2579
- console.log(chalk20.dim("\nCommon issues:"));
2580
- console.log(chalk20.dim(" - DNS not pointing to this server"));
2581
- console.log(chalk20.dim(" - Port 80 not accessible from internet"));
2582
- console.log(chalk20.dim(" - Firewall blocking HTTP validation"));
3055
+ console.log(chalk21.dim("\nCommon issues:"));
3056
+ console.log(chalk21.dim(" - DNS not pointing to this server"));
3057
+ console.log(chalk21.dim(" - Port 80 not accessible from internet"));
3058
+ console.log(chalk21.dim(" - Firewall blocking HTTP validation"));
2583
3059
  }
2584
3060
  console.log("");
2585
3061
  }
2586
3062
 
2587
3063
  // src/commands/tunnel.ts
2588
- import chalk21 from "chalk";
3064
+ import chalk22 from "chalk";
2589
3065
  import { execSync as execSync4, spawn as spawn2 } from "child_process";
2590
3066
  async function tunnelCommand(action, options) {
2591
3067
  if (!action) {
2592
- console.log(chalk21.red("Usage: bindler tunnel <action>"));
2593
- console.log(chalk21.dim("\nActions:"));
2594
- console.log(chalk21.dim(" status Show tunnel status"));
2595
- console.log(chalk21.dim(" start Start the tunnel"));
2596
- console.log(chalk21.dim(" stop Stop the tunnel"));
2597
- console.log(chalk21.dim(" login Authenticate with Cloudflare"));
2598
- console.log(chalk21.dim(" create Create a new tunnel"));
2599
- console.log(chalk21.dim(" list List all tunnels"));
2600
- console.log(chalk21.dim("\nExamples:"));
2601
- console.log(chalk21.dim(" bindler tunnel status"));
2602
- console.log(chalk21.dim(" bindler tunnel start"));
2603
- console.log(chalk21.dim(" bindler tunnel create --name mytunnel"));
3068
+ console.log(chalk22.red("Usage: bindler tunnel <action>"));
3069
+ console.log(chalk22.dim("\nActions:"));
3070
+ console.log(chalk22.dim(" status Show tunnel status"));
3071
+ console.log(chalk22.dim(" start Start the tunnel"));
3072
+ console.log(chalk22.dim(" stop Stop the tunnel"));
3073
+ console.log(chalk22.dim(" login Authenticate with Cloudflare"));
3074
+ console.log(chalk22.dim(" create Create a new tunnel"));
3075
+ console.log(chalk22.dim(" list List all tunnels"));
3076
+ console.log(chalk22.dim("\nExamples:"));
3077
+ console.log(chalk22.dim(" bindler tunnel status"));
3078
+ console.log(chalk22.dim(" bindler tunnel start"));
3079
+ console.log(chalk22.dim(" bindler tunnel create --name mytunnel"));
2604
3080
  process.exit(1);
2605
3081
  }
2606
3082
  if (!isCloudflaredInstalled()) {
2607
- console.log(chalk21.red("cloudflared is not installed."));
2608
- console.log(chalk21.dim("\nInstall with: bindler setup"));
3083
+ console.log(chalk22.red("cloudflared is not installed."));
3084
+ console.log(chalk22.dim("\nInstall with: bindler setup"));
2609
3085
  process.exit(1);
2610
3086
  }
2611
3087
  const defaults = getDefaults();
2612
3088
  const tunnelName = options.name || defaults.tunnelName;
2613
3089
  switch (action) {
2614
3090
  case "status": {
2615
- console.log(chalk21.blue("\nTunnel Status\n"));
3091
+ console.log(chalk22.blue("\nTunnel Status\n"));
2616
3092
  const info = getTunnelInfo(tunnelName);
2617
3093
  if (!info.exists) {
2618
- console.log(chalk21.yellow(`Tunnel "${tunnelName}" does not exist.`));
2619
- console.log(chalk21.dim(`
3094
+ console.log(chalk22.yellow(`Tunnel "${tunnelName}" does not exist.`));
3095
+ console.log(chalk22.dim(`
2620
3096
  Create with: bindler tunnel create --name ${tunnelName}`));
2621
3097
  } else {
2622
- console.log(chalk21.dim(" Name: ") + chalk21.white(tunnelName));
2623
- console.log(chalk21.dim(" ID: ") + chalk21.white(info.id || "unknown"));
2624
- console.log(chalk21.dim(" Running: ") + (info.running ? chalk21.green("yes") : chalk21.red("no")));
3098
+ console.log(chalk22.dim(" Name: ") + chalk22.white(tunnelName));
3099
+ console.log(chalk22.dim(" ID: ") + chalk22.white(info.id || "unknown"));
3100
+ console.log(chalk22.dim(" Running: ") + (info.running ? chalk22.green("yes") : chalk22.red("no")));
2625
3101
  if (!info.running) {
2626
- console.log(chalk21.dim(`
3102
+ console.log(chalk22.dim(`
2627
3103
  Start with: bindler tunnel start`));
2628
3104
  }
2629
3105
  }
@@ -2633,50 +3109,50 @@ Start with: bindler tunnel start`));
2633
3109
  case "start": {
2634
3110
  const info = getTunnelInfo(tunnelName);
2635
3111
  if (!info.exists) {
2636
- console.log(chalk21.red(`Tunnel "${tunnelName}" does not exist.`));
2637
- console.log(chalk21.dim(`Create with: bindler tunnel create`));
3112
+ console.log(chalk22.red(`Tunnel "${tunnelName}" does not exist.`));
3113
+ console.log(chalk22.dim(`Create with: bindler tunnel create`));
2638
3114
  process.exit(1);
2639
3115
  }
2640
3116
  if (info.running) {
2641
- console.log(chalk21.yellow(`Tunnel "${tunnelName}" is already running.`));
3117
+ console.log(chalk22.yellow(`Tunnel "${tunnelName}" is already running.`));
2642
3118
  process.exit(0);
2643
3119
  }
2644
- console.log(chalk21.blue(`Starting tunnel "${tunnelName}"...
3120
+ console.log(chalk22.blue(`Starting tunnel "${tunnelName}"...
2645
3121
  `));
2646
- console.log(chalk21.dim("Running in foreground. Press Ctrl+C to stop.\n"));
3122
+ console.log(chalk22.dim("Running in foreground. Press Ctrl+C to stop.\n"));
2647
3123
  const child = spawn2("cloudflared", ["tunnel", "run", tunnelName], {
2648
3124
  stdio: "inherit"
2649
3125
  });
2650
3126
  child.on("error", (err) => {
2651
- console.log(chalk21.red(`Failed to start tunnel: ${err.message}`));
3127
+ console.log(chalk22.red(`Failed to start tunnel: ${err.message}`));
2652
3128
  process.exit(1);
2653
3129
  });
2654
3130
  child.on("exit", (code) => {
2655
- console.log(chalk21.dim(`
3131
+ console.log(chalk22.dim(`
2656
3132
  Tunnel exited with code ${code}`));
2657
3133
  process.exit(code || 0);
2658
3134
  });
2659
3135
  break;
2660
3136
  }
2661
3137
  case "stop": {
2662
- console.log(chalk21.blue("Stopping tunnel...\n"));
3138
+ console.log(chalk22.blue("Stopping tunnel...\n"));
2663
3139
  const result = execCommandSafe(`pkill -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
2664
3140
  if (result.success) {
2665
- console.log(chalk21.green(`\u2713 Tunnel "${tunnelName}" stopped`));
3141
+ console.log(chalk22.green(`\u2713 Tunnel "${tunnelName}" stopped`));
2666
3142
  } else {
2667
- console.log(chalk21.yellow(`Tunnel "${tunnelName}" was not running.`));
3143
+ console.log(chalk22.yellow(`Tunnel "${tunnelName}" was not running.`));
2668
3144
  }
2669
3145
  console.log("");
2670
3146
  break;
2671
3147
  }
2672
3148
  case "login": {
2673
- console.log(chalk21.blue("Authenticating with Cloudflare...\n"));
2674
- console.log(chalk21.dim("A browser window will open. Follow the instructions.\n"));
3149
+ console.log(chalk22.blue("Authenticating with Cloudflare...\n"));
3150
+ console.log(chalk22.dim("A browser window will open. Follow the instructions.\n"));
2675
3151
  try {
2676
3152
  execSync4("cloudflared tunnel login", { stdio: "inherit" });
2677
- console.log(chalk21.green("\n\u2713 Authentication successful!"));
3153
+ console.log(chalk22.green("\n\u2713 Authentication successful!"));
2678
3154
  } catch {
2679
- console.log(chalk21.red("\n\u2717 Authentication failed or cancelled."));
3155
+ console.log(chalk22.red("\n\u2717 Authentication failed or cancelled."));
2680
3156
  }
2681
3157
  console.log("");
2682
3158
  break;
@@ -2685,69 +3161,69 @@ Tunnel exited with code ${code}`));
2685
3161
  const existingTunnels = listTunnels();
2686
3162
  const exists = existingTunnels.some((t) => t.name === tunnelName);
2687
3163
  if (exists) {
2688
- console.log(chalk21.yellow(`Tunnel "${tunnelName}" already exists.`));
2689
- console.log(chalk21.dim("\nUse a different name with --name"));
3164
+ console.log(chalk22.yellow(`Tunnel "${tunnelName}" already exists.`));
3165
+ console.log(chalk22.dim("\nUse a different name with --name"));
2690
3166
  process.exit(1);
2691
3167
  }
2692
- console.log(chalk21.blue(`Creating tunnel "${tunnelName}"...
3168
+ console.log(chalk22.blue(`Creating tunnel "${tunnelName}"...
2693
3169
  `));
2694
3170
  try {
2695
3171
  execSync4(`cloudflared tunnel create ${tunnelName}`, { stdio: "inherit" });
2696
- console.log(chalk21.green(`
3172
+ console.log(chalk22.green(`
2697
3173
  \u2713 Tunnel "${tunnelName}" created!`));
2698
- console.log(chalk21.dim("\nNext steps:"));
2699
- console.log(chalk21.dim(" 1. Create ~/.cloudflared/config.yml"));
2700
- console.log(chalk21.dim(" 2. Run: bindler tunnel start"));
3174
+ console.log(chalk22.dim("\nNext steps:"));
3175
+ console.log(chalk22.dim(" 1. Create ~/.cloudflared/config.yml"));
3176
+ console.log(chalk22.dim(" 2. Run: bindler tunnel start"));
2701
3177
  } catch {
2702
- console.log(chalk21.red("\n\u2717 Failed to create tunnel."));
2703
- console.log(chalk21.dim("Make sure you're logged in: bindler tunnel login"));
3178
+ console.log(chalk22.red("\n\u2717 Failed to create tunnel."));
3179
+ console.log(chalk22.dim("Make sure you're logged in: bindler tunnel login"));
2704
3180
  }
2705
3181
  console.log("");
2706
3182
  break;
2707
3183
  }
2708
3184
  case "list": {
2709
- console.log(chalk21.blue("\nCloudflare Tunnels\n"));
3185
+ console.log(chalk22.blue("\nCloudflare Tunnels\n"));
2710
3186
  const tunnels = listTunnels();
2711
3187
  if (tunnels.length === 0) {
2712
- console.log(chalk21.dim("No tunnels found."));
2713
- console.log(chalk21.dim("\nCreate one with: bindler tunnel create"));
3188
+ console.log(chalk22.dim("No tunnels found."));
3189
+ console.log(chalk22.dim("\nCreate one with: bindler tunnel create"));
2714
3190
  } else {
2715
3191
  for (const tunnel of tunnels) {
2716
3192
  const isDefault = tunnel.name === tunnelName;
2717
- const prefix = isDefault ? chalk21.cyan("\u2192 ") : " ";
2718
- console.log(prefix + chalk21.white(tunnel.name) + chalk21.dim(` (${tunnel.id.slice(0, 8)}...)`));
3193
+ const prefix = isDefault ? chalk22.cyan("\u2192 ") : " ";
3194
+ console.log(prefix + chalk22.white(tunnel.name) + chalk22.dim(` (${tunnel.id.slice(0, 8)}...)`));
2719
3195
  }
2720
- console.log(chalk21.dim(`
3196
+ console.log(chalk22.dim(`
2721
3197
  ${tunnels.length} tunnel(s)`));
2722
3198
  }
2723
3199
  console.log("");
2724
3200
  break;
2725
3201
  }
2726
3202
  default:
2727
- console.log(chalk21.red(`Unknown action: ${action}`));
2728
- console.log(chalk21.dim("Run `bindler tunnel` for usage."));
3203
+ console.log(chalk22.red(`Unknown action: ${action}`));
3204
+ console.log(chalk22.dim("Run `bindler tunnel` for usage."));
2729
3205
  process.exit(1);
2730
3206
  }
2731
3207
  }
2732
3208
 
2733
3209
  // src/commands/open.ts
2734
- import chalk22 from "chalk";
3210
+ import chalk23 from "chalk";
2735
3211
  import { exec } from "child_process";
2736
3212
  async function openCommand(name) {
2737
3213
  if (!name) {
2738
- console.log(chalk22.red("Usage: bindler open <name>"));
2739
- console.log(chalk22.dim("\nOpen a project in your browser"));
2740
- console.log(chalk22.dim("\nExamples:"));
2741
- console.log(chalk22.dim(" bindler open myapp"));
3214
+ console.log(chalk23.red("Usage: bindler open <name>"));
3215
+ console.log(chalk23.dim("\nOpen a project in your browser"));
3216
+ console.log(chalk23.dim("\nExamples:"));
3217
+ console.log(chalk23.dim(" bindler open myapp"));
2742
3218
  process.exit(1);
2743
3219
  }
2744
3220
  const project = getProject(name);
2745
3221
  if (!project) {
2746
- console.log(chalk22.red(`Project "${name}" not found.`));
2747
- console.log(chalk22.dim("\nAvailable projects:"));
3222
+ console.log(chalk23.red(`Project "${name}" not found.`));
3223
+ console.log(chalk23.dim("\nAvailable projects:"));
2748
3224
  const projects = listProjects();
2749
3225
  for (const p of projects) {
2750
- console.log(chalk22.dim(` - ${p.name}`));
3226
+ console.log(chalk23.dim(` - ${p.name}`));
2751
3227
  }
2752
3228
  process.exit(1);
2753
3229
  }
@@ -2768,7 +3244,7 @@ async function openCommand(name) {
2768
3244
  if (project.basePath && project.basePath !== "/") {
2769
3245
  url += project.basePath;
2770
3246
  }
2771
- console.log(chalk22.dim(`Opening ${url}...`));
3247
+ console.log(chalk23.dim(`Opening ${url}...`));
2772
3248
  const platform = process.platform;
2773
3249
  let cmd;
2774
3250
  if (platform === "darwin") {
@@ -2780,15 +3256,15 @@ async function openCommand(name) {
2780
3256
  }
2781
3257
  exec(cmd, (error) => {
2782
3258
  if (error) {
2783
- console.log(chalk22.yellow(`Could not open browser automatically.`));
2784
- console.log(chalk22.dim(`
2785
- Open manually: ${chalk22.cyan(url)}`));
3259
+ console.log(chalk23.yellow(`Could not open browser automatically.`));
3260
+ console.log(chalk23.dim(`
3261
+ Open manually: ${chalk23.cyan(url)}`));
2786
3262
  }
2787
3263
  });
2788
3264
  }
2789
3265
 
2790
3266
  // src/commands/health.ts
2791
- import chalk23 from "chalk";
3267
+ import chalk24 from "chalk";
2792
3268
  async function pingUrl(url, timeout = 5e3) {
2793
3269
  const start = Date.now();
2794
3270
  try {
@@ -2818,15 +3294,15 @@ async function healthCommand() {
2818
3294
  const projects = listProjects();
2819
3295
  const defaults = getDefaults();
2820
3296
  if (projects.length === 0) {
2821
- console.log(chalk23.yellow("\nNo projects registered."));
2822
- console.log(chalk23.dim("Run `bindler new` to add a project.\n"));
3297
+ console.log(chalk24.yellow("\nNo projects registered."));
3298
+ console.log(chalk24.dim("Run `bindler new` to add a project.\n"));
2823
3299
  return;
2824
3300
  }
2825
- console.log(chalk23.blue("\nHealth Check\n"));
3301
+ console.log(chalk24.blue("\nHealth Check\n"));
2826
3302
  const results = [];
2827
3303
  for (const project of projects) {
2828
3304
  if (project.enabled === false) {
2829
- console.log(chalk23.dim(` - ${project.name} (disabled)`));
3305
+ console.log(chalk24.dim(` - ${project.name} (disabled)`));
2830
3306
  continue;
2831
3307
  }
2832
3308
  const isLocal = project.local || project.hostname.endsWith(".local");
@@ -2845,33 +3321,33 @@ async function healthCommand() {
2845
3321
  if (project.basePath && project.basePath !== "/") {
2846
3322
  url += project.basePath;
2847
3323
  }
2848
- process.stdout.write(chalk23.dim(` Checking ${project.name}...`));
3324
+ process.stdout.write(chalk24.dim(` Checking ${project.name}...`));
2849
3325
  const result = await pingUrl(url);
2850
3326
  results.push({ name: project.name, hostname: project.hostname, ...result });
2851
3327
  process.stdout.write("\r\x1B[K");
2852
3328
  if (result.ok) {
2853
- console.log(chalk23.green(" \u2713 ") + chalk23.white(project.name) + chalk23.dim(` (${result.time}ms)`));
3329
+ console.log(chalk24.green(" \u2713 ") + chalk24.white(project.name) + chalk24.dim(` (${result.time}ms)`));
2854
3330
  } else if (result.status) {
2855
- console.log(chalk23.yellow(" ! ") + chalk23.white(project.name) + chalk23.dim(` (${result.status})`));
3331
+ console.log(chalk24.yellow(" ! ") + chalk24.white(project.name) + chalk24.dim(` (${result.status})`));
2856
3332
  } else {
2857
- console.log(chalk23.red(" \u2717 ") + chalk23.white(project.name) + chalk23.dim(` (${result.error})`));
3333
+ console.log(chalk24.red(" \u2717 ") + chalk24.white(project.name) + chalk24.dim(` (${result.error})`));
2858
3334
  }
2859
3335
  }
2860
3336
  const healthy = results.filter((r) => r.ok).length;
2861
3337
  const unhealthy = results.filter((r) => !r.ok).length;
2862
3338
  console.log("");
2863
3339
  if (unhealthy === 0) {
2864
- console.log(chalk23.green(`\u2713 All ${healthy} project(s) healthy`));
3340
+ console.log(chalk24.green(`\u2713 All ${healthy} project(s) healthy`));
2865
3341
  } else if (healthy === 0) {
2866
- console.log(chalk23.red(`\u2717 All ${unhealthy} project(s) down`));
3342
+ console.log(chalk24.red(`\u2717 All ${unhealthy} project(s) down`));
2867
3343
  } else {
2868
- console.log(chalk23.yellow(`! ${healthy} healthy, ${unhealthy} down`));
3344
+ console.log(chalk24.yellow(`! ${healthy} healthy, ${unhealthy} down`));
2869
3345
  }
2870
3346
  console.log("");
2871
3347
  }
2872
3348
 
2873
3349
  // src/commands/stats.ts
2874
- import chalk24 from "chalk";
3350
+ import chalk25 from "chalk";
2875
3351
  function formatBytes2(bytes) {
2876
3352
  if (bytes === 0) return "0 B";
2877
3353
  const k = 1024;
@@ -2894,15 +3370,15 @@ async function statsCommand() {
2894
3370
  const pm2Processes = getPm2List();
2895
3371
  const npmProjects = projects.filter((p) => p.type === "npm");
2896
3372
  if (npmProjects.length === 0) {
2897
- console.log(chalk24.yellow("\nNo npm projects registered."));
2898
- console.log(chalk24.dim("Stats are only available for npm projects.\n"));
3373
+ console.log(chalk25.yellow("\nNo npm projects registered."));
3374
+ console.log(chalk25.dim("Stats are only available for npm projects.\n"));
2899
3375
  return;
2900
3376
  }
2901
- console.log(chalk24.blue("\nProject Stats\n"));
3377
+ console.log(chalk25.blue("\nProject Stats\n"));
2902
3378
  console.log(
2903
- chalk24.dim(" ") + chalk24.dim("NAME".padEnd(20)) + chalk24.dim("STATUS".padEnd(10)) + chalk24.dim("CPU".padEnd(8)) + chalk24.dim("MEM".padEnd(10)) + chalk24.dim("UPTIME".padEnd(10)) + chalk24.dim("RESTARTS")
3379
+ chalk25.dim(" ") + chalk25.dim("NAME".padEnd(20)) + chalk25.dim("STATUS".padEnd(10)) + chalk25.dim("CPU".padEnd(8)) + chalk25.dim("MEM".padEnd(10)) + chalk25.dim("UPTIME".padEnd(10)) + chalk25.dim("RESTARTS")
2904
3380
  );
2905
- console.log(chalk24.dim(" " + "-".repeat(70)));
3381
+ console.log(chalk25.dim(" " + "-".repeat(70)));
2906
3382
  let totalCpu = 0;
2907
3383
  let totalMem = 0;
2908
3384
  for (const project of npmProjects) {
@@ -2911,11 +3387,11 @@ async function statsCommand() {
2911
3387
  const name = project.name.slice(0, 18).padEnd(20);
2912
3388
  if (!pm2Process) {
2913
3389
  console.log(
2914
- " " + chalk24.white(name) + chalk24.dim("not managed".padEnd(10)) + chalk24.dim("-".padEnd(8)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-")
3390
+ " " + chalk25.white(name) + chalk25.dim("not managed".padEnd(10)) + chalk25.dim("-".padEnd(8)) + chalk25.dim("-".padEnd(10)) + chalk25.dim("-".padEnd(10)) + chalk25.dim("-")
2915
3391
  );
2916
3392
  continue;
2917
3393
  }
2918
- const statusColor = pm2Process.status === "online" ? chalk24.green : chalk24.red;
3394
+ const statusColor = pm2Process.status === "online" ? chalk25.green : chalk25.red;
2919
3395
  const status = statusColor(pm2Process.status.padEnd(10));
2920
3396
  const cpu = `${pm2Process.cpu.toFixed(1)}%`.padEnd(8);
2921
3397
  const mem = formatBytes2(pm2Process.memory).padEnd(10);
@@ -2924,19 +3400,19 @@ async function statsCommand() {
2924
3400
  totalCpu += pm2Process.cpu;
2925
3401
  totalMem += pm2Process.memory;
2926
3402
  console.log(
2927
- " " + chalk24.white(name) + status + (pm2Process.cpu > 50 ? chalk24.yellow(cpu) : chalk24.dim(cpu)) + (pm2Process.memory > 500 * 1024 * 1024 ? chalk24.yellow(mem) : chalk24.dim(mem)) + chalk24.dim(uptime) + (pm2Process.restarts > 0 ? chalk24.yellow(restarts) : chalk24.dim(restarts))
3403
+ " " + chalk25.white(name) + status + (pm2Process.cpu > 50 ? chalk25.yellow(cpu) : chalk25.dim(cpu)) + (pm2Process.memory > 500 * 1024 * 1024 ? chalk25.yellow(mem) : chalk25.dim(mem)) + chalk25.dim(uptime) + (pm2Process.restarts > 0 ? chalk25.yellow(restarts) : chalk25.dim(restarts))
2928
3404
  );
2929
3405
  }
2930
3406
  const runningCount = pm2Processes.filter((p) => p.name.startsWith("bindler:") && p.status === "online").length;
2931
- console.log(chalk24.dim(" " + "-".repeat(70)));
3407
+ console.log(chalk25.dim(" " + "-".repeat(70)));
2932
3408
  console.log(
2933
- " " + chalk24.bold("TOTAL".padEnd(20)) + chalk24.dim(`${runningCount}/${npmProjects.length}`.padEnd(10)) + chalk24.dim(`${totalCpu.toFixed(1)}%`.padEnd(8)) + chalk24.dim(formatBytes2(totalMem).padEnd(10))
3409
+ " " + chalk25.bold("TOTAL".padEnd(20)) + chalk25.dim(`${runningCount}/${npmProjects.length}`.padEnd(10)) + chalk25.dim(`${totalCpu.toFixed(1)}%`.padEnd(8)) + chalk25.dim(formatBytes2(totalMem).padEnd(10))
2934
3410
  );
2935
3411
  console.log("");
2936
3412
  }
2937
3413
 
2938
3414
  // src/commands/completion.ts
2939
- import chalk25 from "chalk";
3415
+ import chalk26 from "chalk";
2940
3416
  var BASH_COMPLETION = `
2941
3417
  # bindler bash completion
2942
3418
  _bindler_completions() {
@@ -3069,12 +3545,12 @@ complete -c bindler -n '__fish_seen_subcommand_from tunnel' -a 'status start sto
3069
3545
  `;
3070
3546
  async function completionCommand(shell) {
3071
3547
  if (!shell) {
3072
- console.log(chalk25.red("Usage: bindler completion <shell>"));
3073
- console.log(chalk25.dim("\nSupported shells: bash, zsh, fish"));
3074
- console.log(chalk25.dim("\nSetup:"));
3075
- console.log(chalk25.dim(" bash: bindler completion bash >> ~/.bashrc"));
3076
- console.log(chalk25.dim(" zsh: bindler completion zsh >> ~/.zshrc"));
3077
- console.log(chalk25.dim(" fish: bindler completion fish > ~/.config/fish/completions/bindler.fish"));
3548
+ console.log(chalk26.red("Usage: bindler completion <shell>"));
3549
+ console.log(chalk26.dim("\nSupported shells: bash, zsh, fish"));
3550
+ console.log(chalk26.dim("\nSetup:"));
3551
+ console.log(chalk26.dim(" bash: bindler completion bash >> ~/.bashrc"));
3552
+ console.log(chalk26.dim(" zsh: bindler completion zsh >> ~/.zshrc"));
3553
+ console.log(chalk26.dim(" fish: bindler completion fish > ~/.config/fish/completions/bindler.fish"));
3078
3554
  process.exit(1);
3079
3555
  }
3080
3556
  switch (shell) {
@@ -3088,24 +3564,276 @@ async function completionCommand(shell) {
3088
3564
  console.log(FISH_COMPLETION.trim());
3089
3565
  break;
3090
3566
  default:
3091
- console.log(chalk25.red(`Unknown shell: ${shell}`));
3092
- console.log(chalk25.dim("Supported: bash, zsh, fish"));
3567
+ console.log(chalk26.red(`Unknown shell: ${shell}`));
3568
+ console.log(chalk26.dim("Supported: bash, zsh, fish"));
3569
+ process.exit(1);
3570
+ }
3571
+ }
3572
+
3573
+ // src/commands/clone.ts
3574
+ import chalk27 from "chalk";
3575
+ import inquirer5 from "inquirer";
3576
+ async function cloneCommand(source, newName, options) {
3577
+ if (!source) {
3578
+ console.log(chalk27.red("Usage: bindler clone <source> <new-name>"));
3579
+ console.log(chalk27.dim("\nClones a project configuration with a new name"));
3580
+ console.log(chalk27.dim("\nExamples:"));
3581
+ console.log(chalk27.dim(" bindler clone myapp myapp-staging"));
3582
+ console.log(chalk27.dim(" bindler clone myapp myapp-v2 --hostname newapp.example.com"));
3583
+ console.log(chalk27.dim(" bindler clone myapp myapp-copy --path /var/www/newapp"));
3584
+ process.exit(1);
3585
+ }
3586
+ const sourceProject = getProject(source);
3587
+ if (!sourceProject) {
3588
+ console.log(chalk27.red(`Project "${source}" not found.`));
3589
+ console.log(chalk27.dim("\nAvailable projects:"));
3590
+ const projects = listProjects();
3591
+ for (const p of projects) {
3592
+ console.log(chalk27.dim(` - ${p.name}`));
3593
+ }
3594
+ process.exit(1);
3595
+ }
3596
+ let targetName = newName;
3597
+ if (!targetName) {
3598
+ const answer = await inquirer5.prompt([
3599
+ {
3600
+ type: "input",
3601
+ name: "name",
3602
+ message: "New project name:",
3603
+ default: `${source}-copy`,
3604
+ validate: (input) => {
3605
+ if (!validateProjectName(input)) {
3606
+ return "Invalid project name. Use alphanumeric characters, dashes, and underscores.";
3607
+ }
3608
+ if (getProject(input)) {
3609
+ return `Project "${input}" already exists`;
3610
+ }
3611
+ return true;
3612
+ }
3613
+ }
3614
+ ]);
3615
+ targetName = answer.name;
3616
+ } else {
3617
+ if (!validateProjectName(targetName)) {
3618
+ console.log(chalk27.red("Invalid project name. Use alphanumeric characters, dashes, and underscores."));
3619
+ process.exit(1);
3620
+ }
3621
+ if (getProject(targetName)) {
3622
+ console.log(chalk27.red(`Project "${targetName}" already exists.`));
3623
+ process.exit(1);
3624
+ }
3625
+ }
3626
+ let targetHostname = options.hostname;
3627
+ if (!targetHostname) {
3628
+ const answer = await inquirer5.prompt([
3629
+ {
3630
+ type: "input",
3631
+ name: "hostname",
3632
+ message: "Hostname for new project:",
3633
+ default: sourceProject.hostname.replace(source, targetName),
3634
+ validate: (input) => {
3635
+ if (!validateHostname(input)) {
3636
+ return "Invalid hostname format";
3637
+ }
3638
+ return true;
3639
+ }
3640
+ }
3641
+ ]);
3642
+ targetHostname = answer.hostname;
3643
+ } else {
3644
+ if (!validateHostname(targetHostname)) {
3645
+ console.log(chalk27.red("Invalid hostname format."));
3646
+ process.exit(1);
3647
+ }
3648
+ }
3649
+ const newProject = {
3650
+ ...sourceProject,
3651
+ name: targetName,
3652
+ hostname: targetHostname,
3653
+ path: options.path || sourceProject.path
3654
+ };
3655
+ if (newProject.type === "npm") {
3656
+ newProject.port = options.port || findAvailablePort();
3657
+ if (newProject.env?.PORT) {
3658
+ newProject.env = { ...newProject.env, PORT: String(newProject.port) };
3659
+ }
3660
+ }
3661
+ try {
3662
+ addProject(newProject);
3663
+ console.log(chalk27.green(`
3664
+ Project "${targetName}" cloned from "${source}"!
3665
+ `));
3666
+ console.log(chalk27.dim("Configuration:"));
3667
+ console.log(chalk27.dim(` Name: ${newProject.name}`));
3668
+ console.log(chalk27.dim(` Type: ${newProject.type}`));
3669
+ console.log(chalk27.dim(` Path: ${newProject.path}`));
3670
+ console.log(chalk27.dim(` Hostname: ${newProject.hostname}`));
3671
+ if (newProject.port) {
3672
+ console.log(chalk27.dim(` Port: ${newProject.port}`));
3673
+ }
3674
+ console.log(chalk27.dim(`
3675
+ Run ${chalk27.cyan("sudo bindler apply")} to update nginx configuration.`));
3676
+ if (newProject.type === "npm") {
3677
+ console.log(chalk27.dim(`Run ${chalk27.cyan(`bindler start ${targetName}`)} to start the application.`));
3678
+ }
3679
+ } catch (error) {
3680
+ console.error(chalk27.red(`Error: ${error instanceof Error ? error.message : error}`));
3681
+ process.exit(1);
3682
+ }
3683
+ }
3684
+
3685
+ // src/commands/dev.ts
3686
+ import chalk28 from "chalk";
3687
+ import { spawn as spawn3 } from "child_process";
3688
+ import { existsSync as existsSync13, readFileSync as readFileSync7 } from "fs";
3689
+ import { basename as basename3, join as join8 } from "path";
3690
+ function getPackageJson(dir) {
3691
+ const pkgPath = join8(dir, "package.json");
3692
+ if (!existsSync13(pkgPath)) return null;
3693
+ try {
3694
+ return JSON.parse(readFileSync7(pkgPath, "utf-8"));
3695
+ } catch {
3696
+ return null;
3697
+ }
3698
+ }
3699
+ function getDevCommand(dir) {
3700
+ const pkg = getPackageJson(dir);
3701
+ if (!pkg?.scripts) return null;
3702
+ const scripts = pkg.scripts;
3703
+ if (scripts.dev) return "npm run dev";
3704
+ if (scripts["start:dev"]) return "npm run start:dev";
3705
+ if (scripts.watch) return "npm run watch";
3706
+ if (scripts.start) return "npm run start";
3707
+ return null;
3708
+ }
3709
+ async function devCommand(name, options) {
3710
+ const cwd = process.cwd();
3711
+ let project;
3712
+ let projectDir;
3713
+ if (name) {
3714
+ project = getProject(name);
3715
+ if (!project) {
3716
+ console.log(chalk28.red(`Project "${name}" not found.`));
3717
+ console.log(chalk28.dim("\nAvailable projects:"));
3718
+ const projects = listProjects();
3719
+ for (const p of projects) {
3720
+ console.log(chalk28.dim(` - ${p.name}`));
3721
+ }
3093
3722
  process.exit(1);
3723
+ }
3724
+ projectDir = project.path;
3725
+ } else {
3726
+ const projects = listProjects();
3727
+ project = projects.find((p) => p.path === cwd);
3728
+ if (!project) {
3729
+ const yamlConfig = readBindlerYaml(cwd);
3730
+ if (yamlConfig) {
3731
+ console.log(chalk28.cyan("Found bindler.yaml - creating temporary dev project\n"));
3732
+ const yamlProject = yamlToProject(yamlConfig, cwd);
3733
+ project = {
3734
+ name: yamlProject.name || basename3(cwd),
3735
+ type: yamlProject.type || "npm",
3736
+ path: cwd,
3737
+ hostname: options.hostname || yamlProject.hostname || `${basename3(cwd)}.local`,
3738
+ port: options.port || yamlProject.port || findAvailablePort(),
3739
+ start: yamlProject.start,
3740
+ local: true
3741
+ };
3742
+ } else {
3743
+ const pkg = getPackageJson(cwd);
3744
+ if (!pkg) {
3745
+ console.log(chalk28.red("No package.json found in current directory."));
3746
+ console.log(chalk28.dim("\nUsage: bindler dev [name]"));
3747
+ console.log(chalk28.dim(" Run in a project directory or specify a project name"));
3748
+ process.exit(1);
3749
+ }
3750
+ project = {
3751
+ name: pkg.name || basename3(cwd),
3752
+ type: "npm",
3753
+ path: cwd,
3754
+ hostname: options.hostname || `${basename3(cwd)}.local`,
3755
+ port: options.port || findAvailablePort(),
3756
+ local: true
3757
+ };
3758
+ }
3759
+ }
3760
+ projectDir = cwd;
3761
+ }
3762
+ if (project.type !== "npm") {
3763
+ console.log(chalk28.red("Dev mode is only supported for npm projects."));
3764
+ console.log(chalk28.dim("\nFor static projects, use a local web server:"));
3765
+ console.log(chalk28.dim(" npx serve " + project.path));
3766
+ process.exit(1);
3767
+ }
3768
+ if (!existsSync13(projectDir)) {
3769
+ console.log(chalk28.red(`Project directory not found: ${projectDir}`));
3770
+ process.exit(1);
3094
3771
  }
3772
+ const devCmd = getDevCommand(projectDir) || project.start || "npm start";
3773
+ const port = options.port || project.port || findAvailablePort();
3774
+ console.log(chalk28.blue(`
3775
+ Starting ${project.name} in dev mode...
3776
+ `));
3777
+ console.log(chalk28.dim(` Directory: ${projectDir}`));
3778
+ console.log(chalk28.dim(` Command: ${devCmd}`));
3779
+ console.log(chalk28.dim(` Port: ${port}`));
3780
+ console.log(chalk28.dim(` Hostname: ${project.hostname}`));
3781
+ if (project.hostname.endsWith(".local") || project.local) {
3782
+ console.log(chalk28.yellow(`
3783
+ Note: Add to /etc/hosts if not already:`));
3784
+ console.log(chalk28.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
3785
+ }
3786
+ const defaults = getDefaults();
3787
+ const listenPort = defaults.nginxListen.split(":")[1] || "8080";
3788
+ console.log(chalk28.green(`
3789
+ Access at: http://${project.hostname}:${listenPort}`));
3790
+ console.log(chalk28.dim("Press Ctrl+C to stop\n"));
3791
+ console.log(chalk28.dim("---"));
3792
+ const env = {
3793
+ ...process.env,
3794
+ PORT: String(port),
3795
+ ...project.env
3796
+ };
3797
+ const [cmd, ...args] = devCmd.split(" ");
3798
+ const child = spawn3(cmd, args, {
3799
+ cwd: projectDir,
3800
+ env,
3801
+ stdio: "inherit",
3802
+ shell: true
3803
+ });
3804
+ child.on("error", (error) => {
3805
+ console.error(chalk28.red(`
3806
+ Failed to start: ${error.message}`));
3807
+ process.exit(1);
3808
+ });
3809
+ child.on("exit", (code) => {
3810
+ if (code !== 0) {
3811
+ console.log(chalk28.yellow(`
3812
+ Process exited with code ${code}`));
3813
+ }
3814
+ process.exit(code || 0);
3815
+ });
3816
+ process.on("SIGINT", () => {
3817
+ console.log(chalk28.dim("\n\nStopping dev server..."));
3818
+ child.kill("SIGINT");
3819
+ });
3820
+ process.on("SIGTERM", () => {
3821
+ child.kill("SIGTERM");
3822
+ });
3095
3823
  }
3096
3824
 
3097
3825
  // src/lib/update-check.ts
3098
- import chalk26 from "chalk";
3099
- import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
3100
- import { join as join6 } from "path";
3101
- import { homedir as homedir3 } from "os";
3826
+ import chalk29 from "chalk";
3827
+ import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
3828
+ import { join as join9 } from "path";
3829
+ import { homedir as homedir4 } from "os";
3102
3830
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
3103
- var CACHE_DIR = join6(homedir3(), ".config", "bindler");
3104
- var CACHE_FILE = join6(CACHE_DIR, ".update-check");
3831
+ var CACHE_DIR = join9(homedir4(), ".config", "bindler");
3832
+ var CACHE_FILE = join9(CACHE_DIR, ".update-check");
3105
3833
  function readCache() {
3106
3834
  try {
3107
- if (existsSync9(CACHE_FILE)) {
3108
- return JSON.parse(readFileSync6(CACHE_FILE, "utf-8"));
3835
+ if (existsSync14(CACHE_FILE)) {
3836
+ return JSON.parse(readFileSync8(CACHE_FILE, "utf-8"));
3109
3837
  }
3110
3838
  } catch {
3111
3839
  }
@@ -3113,10 +3841,10 @@ function readCache() {
3113
3841
  }
3114
3842
  function writeCache(data) {
3115
3843
  try {
3116
- if (!existsSync9(CACHE_DIR)) {
3844
+ if (!existsSync14(CACHE_DIR)) {
3117
3845
  mkdirSync5(CACHE_DIR, { recursive: true });
3118
3846
  }
3119
- writeFileSync5(CACHE_FILE, JSON.stringify(data));
3847
+ writeFileSync6(CACHE_FILE, JSON.stringify(data));
3120
3848
  } catch {
3121
3849
  }
3122
3850
  }
@@ -3148,28 +3876,28 @@ async function checkForUpdates() {
3148
3876
  const cache = readCache();
3149
3877
  const now = Date.now();
3150
3878
  if (now - cache.lastCheck < CHECK_INTERVAL) {
3151
- if (cache.latestVersion && compareVersions("1.1.1", cache.latestVersion) < 0) {
3879
+ if (cache.latestVersion && compareVersions("1.3.0", cache.latestVersion) < 0) {
3152
3880
  showUpdateMessage(cache.latestVersion);
3153
3881
  }
3154
3882
  return;
3155
3883
  }
3156
3884
  fetchLatestVersion().then((latestVersion) => {
3157
3885
  writeCache({ lastCheck: now, latestVersion });
3158
- if (latestVersion && compareVersions("1.1.1", latestVersion) < 0) {
3886
+ if (latestVersion && compareVersions("1.3.0", latestVersion) < 0) {
3159
3887
  showUpdateMessage(latestVersion);
3160
3888
  }
3161
3889
  });
3162
3890
  }
3163
3891
  function showUpdateMessage(latestVersion) {
3164
3892
  console.log("");
3165
- console.log(chalk26.yellow(` Update available: ${"1.1.1"} \u2192 ${latestVersion}`));
3166
- console.log(chalk26.dim(` Run: npm update -g bindler`));
3893
+ console.log(chalk29.yellow(` Update available: ${"1.3.0"} \u2192 ${latestVersion}`));
3894
+ console.log(chalk29.dim(` Run: npm update -g bindler`));
3167
3895
  console.log("");
3168
3896
  }
3169
3897
 
3170
3898
  // src/cli.ts
3171
3899
  var program = new Command();
3172
- program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.1.1");
3900
+ program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.3.0");
3173
3901
  program.hook("preAction", async () => {
3174
3902
  try {
3175
3903
  initConfig();
@@ -3188,78 +3916,78 @@ program.command("status").description("Show detailed status of all projects").ac
3188
3916
  });
3189
3917
  program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
3190
3918
  if (!name && !options.all) {
3191
- console.log(chalk27.red("Usage: bindler start <name> or bindler start --all"));
3192
- console.log(chalk27.dim("\nExamples:"));
3193
- console.log(chalk27.dim(" bindler start myapp"));
3194
- console.log(chalk27.dim(" bindler start --all # start all npm projects"));
3919
+ console.log(chalk30.red("Usage: bindler start <name> or bindler start --all"));
3920
+ console.log(chalk30.dim("\nExamples:"));
3921
+ console.log(chalk30.dim(" bindler start myapp"));
3922
+ console.log(chalk30.dim(" bindler start --all # start all npm projects"));
3195
3923
  process.exit(1);
3196
3924
  }
3197
3925
  await startCommand(name, options);
3198
3926
  });
3199
3927
  program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
3200
3928
  if (!name && !options.all) {
3201
- console.log(chalk27.red("Usage: bindler stop <name> or bindler stop --all"));
3202
- console.log(chalk27.dim("\nExamples:"));
3203
- console.log(chalk27.dim(" bindler stop myapp"));
3204
- console.log(chalk27.dim(" bindler stop --all # stop all npm projects"));
3929
+ console.log(chalk30.red("Usage: bindler stop <name> or bindler stop --all"));
3930
+ console.log(chalk30.dim("\nExamples:"));
3931
+ console.log(chalk30.dim(" bindler stop myapp"));
3932
+ console.log(chalk30.dim(" bindler stop --all # stop all npm projects"));
3205
3933
  process.exit(1);
3206
3934
  }
3207
3935
  await stopCommand(name, options);
3208
3936
  });
3209
3937
  program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
3210
3938
  if (!name && !options.all) {
3211
- console.log(chalk27.red("Usage: bindler restart <name> or bindler restart --all"));
3212
- console.log(chalk27.dim("\nExamples:"));
3213
- console.log(chalk27.dim(" bindler restart myapp"));
3214
- console.log(chalk27.dim(" bindler restart --all # restart all npm projects"));
3939
+ console.log(chalk30.red("Usage: bindler restart <name> or bindler restart --all"));
3940
+ console.log(chalk30.dim("\nExamples:"));
3941
+ console.log(chalk30.dim(" bindler restart myapp"));
3942
+ console.log(chalk30.dim(" bindler restart --all # restart all npm projects"));
3215
3943
  process.exit(1);
3216
3944
  }
3217
3945
  await restartCommand(name, options);
3218
3946
  });
3219
3947
  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) => {
3220
3948
  if (!name) {
3221
- console.log(chalk27.red("Usage: bindler logs <name>"));
3222
- console.log(chalk27.dim("\nExamples:"));
3223
- console.log(chalk27.dim(" bindler logs myapp"));
3224
- console.log(chalk27.dim(" bindler logs myapp --follow"));
3225
- console.log(chalk27.dim(" bindler logs myapp --lines 500"));
3949
+ console.log(chalk30.red("Usage: bindler logs <name>"));
3950
+ console.log(chalk30.dim("\nExamples:"));
3951
+ console.log(chalk30.dim(" bindler logs myapp"));
3952
+ console.log(chalk30.dim(" bindler logs myapp --follow"));
3953
+ console.log(chalk30.dim(" bindler logs myapp --lines 500"));
3226
3954
  process.exit(1);
3227
3955
  }
3228
3956
  await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
3229
3957
  });
3230
3958
  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) => {
3231
3959
  if (!name) {
3232
- console.log(chalk27.red("Usage: bindler update <name> [options]"));
3233
- console.log(chalk27.dim("\nExamples:"));
3234
- console.log(chalk27.dim(" bindler update myapp --hostname newapp.example.com"));
3235
- console.log(chalk27.dim(" bindler update myapp --port 4000"));
3236
- console.log(chalk27.dim(" bindler update myapp --disable"));
3960
+ console.log(chalk30.red("Usage: bindler update <name> [options]"));
3961
+ console.log(chalk30.dim("\nExamples:"));
3962
+ console.log(chalk30.dim(" bindler update myapp --hostname newapp.example.com"));
3963
+ console.log(chalk30.dim(" bindler update myapp --port 4000"));
3964
+ console.log(chalk30.dim(" bindler update myapp --disable"));
3237
3965
  process.exit(1);
3238
3966
  }
3239
3967
  await updateCommand(name, options);
3240
3968
  });
3241
3969
  program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
3242
3970
  if (!name) {
3243
- console.log(chalk27.red("Usage: bindler edit <name>"));
3244
- console.log(chalk27.dim("\nOpens the project config in your $EDITOR"));
3245
- console.log(chalk27.dim("\nExample:"));
3246
- console.log(chalk27.dim(" bindler edit myapp"));
3971
+ console.log(chalk30.red("Usage: bindler edit <name>"));
3972
+ console.log(chalk30.dim("\nOpens the project config in your $EDITOR"));
3973
+ console.log(chalk30.dim("\nExample:"));
3974
+ console.log(chalk30.dim(" bindler edit myapp"));
3247
3975
  process.exit(1);
3248
3976
  }
3249
3977
  await editCommand(name);
3250
3978
  });
3251
3979
  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) => {
3252
3980
  if (!name) {
3253
- console.log(chalk27.red("Usage: bindler remove <name>"));
3254
- console.log(chalk27.dim("\nExamples:"));
3255
- console.log(chalk27.dim(" bindler remove myapp"));
3256
- console.log(chalk27.dim(" bindler remove myapp --force # skip confirmation"));
3257
- console.log(chalk27.dim(" bindler rm myapp # alias"));
3981
+ console.log(chalk30.red("Usage: bindler remove <name>"));
3982
+ console.log(chalk30.dim("\nExamples:"));
3983
+ console.log(chalk30.dim(" bindler remove myapp"));
3984
+ console.log(chalk30.dim(" bindler remove myapp --force # skip confirmation"));
3985
+ console.log(chalk30.dim(" bindler rm myapp # alias"));
3258
3986
  process.exit(1);
3259
3987
  }
3260
3988
  await removeCommand(name, options);
3261
3989
  });
3262
- program.command("apply").description("Generate and apply nginx configuration + Cloudflare DNS routes").option("-d, --dry-run", "Print config without applying").option("--no-reload", "Write config but do not reload nginx").option("--no-cloudflare", "Skip Cloudflare DNS route configuration").option("--no-ssl", "Skip SSL certificate setup (direct mode)").action(async (options) => {
3990
+ program.command("apply").description("Generate and apply nginx configuration + Cloudflare DNS routes").option("-d, --dry-run", "Print config without applying").option("--no-reload", "Write config but do not reload nginx").option("--no-cloudflare", "Skip Cloudflare DNS route configuration").option("--no-ssl", "Skip SSL certificate setup (direct mode)").option("--sync", "Sync bindler.yaml from project directories before applying").option("-e, --env <env>", "Use environment-specific config (staging, production)").option("--skip-checks", "Skip preflight validation checks (not recommended)").action(async (options) => {
3263
3991
  await applyCommand(options);
3264
3992
  });
3265
3993
  program.command("doctor").description("Run system diagnostics and check dependencies").action(async () => {
@@ -3273,10 +4001,10 @@ program.command("info").description("Show bindler information and stats").action
3273
4001
  });
3274
4002
  program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
3275
4003
  if (!hostname) {
3276
- console.log(chalk27.red("Usage: bindler check <hostname>"));
3277
- console.log(chalk27.dim("\nExamples:"));
3278
- console.log(chalk27.dim(" bindler check myapp.example.com"));
3279
- console.log(chalk27.dim(" bindler check myapp # uses project name"));
4004
+ console.log(chalk30.red("Usage: bindler check <hostname>"));
4005
+ console.log(chalk30.dim("\nExamples:"));
4006
+ console.log(chalk30.dim(" bindler check myapp.example.com"));
4007
+ console.log(chalk30.dim(" bindler check myapp # uses project name"));
3280
4008
  process.exit(1);
3281
4009
  }
3282
4010
  await checkCommand(hostname, options);
@@ -3314,5 +4042,11 @@ program.command("stats").description("Show CPU and memory stats for npm projects
3314
4042
  program.command("completion [shell]").description("Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
3315
4043
  await completionCommand(shell);
3316
4044
  });
4045
+ program.command("clone [source] [new-name]").description("Clone a project configuration with a new name").option("-h, --hostname <hostname>", "Hostname for the new project").option("-p, --path <path>", "Path for the new project").option("--port <port>", "Port for the new project (npm only)").action(async (source, newName, options) => {
4046
+ await cloneCommand(source, newName, options);
4047
+ });
4048
+ 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) => {
4049
+ await devCommand(name, options);
4050
+ });
3317
4051
  program.parse();
3318
4052
  //# sourceMappingURL=cli.js.map