bindler 1.1.0 → 1.2.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.
package/dist/cli.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
- import chalk26 from "chalk";
5
+ import chalk29 from "chalk";
6
6
 
7
7
  // src/commands/new.ts
8
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
9
- import { basename } from "path";
8
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
9
+ import { basename as basename2 } from "path";
10
10
  import inquirer from "inquirer";
11
11
  import chalk from "chalk";
12
12
 
@@ -140,6 +140,19 @@ function getDefaults() {
140
140
  const config = readConfig();
141
141
  return config.defaults;
142
142
  }
143
+ function listProjectsForEnv(env = "production") {
144
+ const config = readConfig();
145
+ return config.projects.map((project) => {
146
+ const envConfig = project.environments?.[env];
147
+ if (!envConfig) return project;
148
+ return {
149
+ ...project,
150
+ hostname: envConfig.hostname || project.hostname,
151
+ port: envConfig.port || project.port,
152
+ env: { ...project.env, ...envConfig.env }
153
+ };
154
+ });
155
+ }
143
156
 
144
157
  // src/lib/utils.ts
145
158
  import { execSync, spawn } from "child_process";
@@ -277,6 +290,60 @@ function getPortsTable() {
277
290
  // src/lib/nginx.ts
278
291
  import { existsSync as existsSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2 } from "fs";
279
292
  import { dirname as dirname2, join as join3 } from "path";
293
+ import { createHash } from "crypto";
294
+ function generateHtpasswdEntry(username, password) {
295
+ const result = execCommandSafe(`openssl passwd -apr1 '${password.replace(/'/g, "'\\''")}'`);
296
+ if (result.success && result.output) {
297
+ return `${username}:${result.output.trim()}`;
298
+ }
299
+ const hash = createHash("md5").update(password).digest("base64");
300
+ return `${username}:{PLAIN}${password}`;
301
+ }
302
+ function generateSecurityDirectives(project, indent) {
303
+ const lines = [];
304
+ const security = project.security;
305
+ if (!security) return lines;
306
+ if (security.basicAuth?.enabled) {
307
+ const realm = security.basicAuth.realm || "Restricted";
308
+ const htpasswdPath = security.basicAuth.htpasswdPath || join3(getGeneratedDir(), `htpasswd-${project.name}`);
309
+ lines.push(`${indent} auth_basic "${realm}";`);
310
+ lines.push(`${indent} auth_basic_user_file ${htpasswdPath};`);
311
+ }
312
+ if (security.ipBlocklist?.length) {
313
+ for (const ip of security.ipBlocklist) {
314
+ lines.push(`${indent} deny ${ip};`);
315
+ }
316
+ }
317
+ if (security.ipAllowlist?.length) {
318
+ for (const ip of security.ipAllowlist) {
319
+ lines.push(`${indent} allow ${ip};`);
320
+ }
321
+ lines.push(`${indent} deny all;`);
322
+ }
323
+ if (security.rateLimit?.enabled) {
324
+ const burst = security.rateLimit.burst || 20;
325
+ lines.push(`${indent} limit_req zone=limit_${project.name} burst=${burst} nodelay;`);
326
+ }
327
+ if (security.headers) {
328
+ const h = security.headers;
329
+ if (h.hsts) {
330
+ lines.push(`${indent} add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;`);
331
+ }
332
+ if (h.xFrameOptions) {
333
+ lines.push(`${indent} add_header X-Frame-Options "${h.xFrameOptions}" always;`);
334
+ }
335
+ if (h.xContentTypeOptions) {
336
+ lines.push(`${indent} add_header X-Content-Type-Options "nosniff" always;`);
337
+ }
338
+ if (h.xXssProtection) {
339
+ lines.push(`${indent} add_header X-XSS-Protection "1; mode=block" always;`);
340
+ }
341
+ if (h.contentSecurityPolicy) {
342
+ lines.push(`${indent} add_header Content-Security-Policy "${h.contentSecurityPolicy}" always;`);
343
+ }
344
+ }
345
+ return lines;
346
+ }
280
347
  function generateLocationBlock(project, indent = " ") {
281
348
  const lines = [];
282
349
  const locationPath = project.basePath || "/";
@@ -290,6 +357,7 @@ function generateLocationBlock(project, indent = " ") {
290
357
  }
291
358
  lines.push(`${indent} index index.html index.htm;`);
292
359
  lines.push(`${indent} try_files $uri $uri/ =404;`);
360
+ lines.push(...generateSecurityDirectives(project, indent));
293
361
  lines.push(`${indent}}`);
294
362
  } else if (project.type === "npm") {
295
363
  lines.push(`${indent}location ${locationPath} {`);
@@ -302,10 +370,22 @@ function generateLocationBlock(project, indent = " ") {
302
370
  lines.push(`${indent} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
303
371
  lines.push(`${indent} proxy_set_header X-Forwarded-Proto $scheme;`);
304
372
  lines.push(`${indent} proxy_cache_bypass $http_upgrade;`);
373
+ lines.push(...generateSecurityDirectives(project, indent));
305
374
  lines.push(`${indent}}`);
306
375
  }
307
376
  return lines;
308
377
  }
378
+ function generateHtpasswdFiles(projects) {
379
+ for (const project of projects) {
380
+ if (project.security?.basicAuth?.enabled && project.security.basicAuth.users?.length) {
381
+ const htpasswdPath = project.security.basicAuth.htpasswdPath || join3(getGeneratedDir(), `htpasswd-${project.name}`);
382
+ const entries = project.security.basicAuth.users.map(
383
+ (u) => generateHtpasswdEntry(u.username, u.password)
384
+ );
385
+ writeFileSync2(htpasswdPath, entries.join("\n") + "\n");
386
+ }
387
+ }
388
+ }
309
389
  function generateNginxConfig(config) {
310
390
  const { defaults, projects } = config;
311
391
  const listen = defaults.nginxListen;
@@ -314,6 +394,17 @@ function generateNginxConfig(config) {
314
394
  `# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
315
395
  ""
316
396
  ];
397
+ const rateLimitedProjects = projects.filter(
398
+ (p) => p.enabled !== false && p.security?.rateLimit?.enabled
399
+ );
400
+ if (rateLimitedProjects.length > 0) {
401
+ lines.push("# Rate limiting zones");
402
+ for (const project of rateLimitedProjects) {
403
+ const rps = project.security?.rateLimit?.requestsPerSecond || 10;
404
+ lines.push(`limit_req_zone $binary_remote_addr zone=limit_${project.name}:10m rate=${rps}r/s;`);
405
+ }
406
+ lines.push("");
407
+ }
317
408
  const hostGroups = /* @__PURE__ */ new Map();
318
409
  for (const project of projects) {
319
410
  if (project.enabled === false) {
@@ -550,6 +641,59 @@ function restartAllProjects(projects) {
550
641
  }));
551
642
  }
552
643
 
644
+ // src/lib/yaml.ts
645
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
646
+ import { join as join4, basename } from "path";
647
+ import yaml from "js-yaml";
648
+ var YAML_FILENAME = "bindler.yaml";
649
+ function readBindlerYaml(dir) {
650
+ const yamlPath = join4(dir, YAML_FILENAME);
651
+ if (!existsSync4(yamlPath)) {
652
+ return null;
653
+ }
654
+ try {
655
+ const content = readFileSync3(yamlPath, "utf-8");
656
+ const parsed = yaml.load(content);
657
+ return parsed || null;
658
+ } catch {
659
+ return null;
660
+ }
661
+ }
662
+ function yamlToProject(yamlConfig, dir) {
663
+ const project = {
664
+ name: yamlConfig.name || basename(dir),
665
+ type: yamlConfig.type || "static",
666
+ path: dir,
667
+ hostname: yamlConfig.hostname,
668
+ basePath: yamlConfig.basePath,
669
+ port: yamlConfig.port,
670
+ start: yamlConfig.start,
671
+ env: yamlConfig.env,
672
+ local: yamlConfig.local,
673
+ security: yamlConfig.security,
674
+ environments: yamlConfig.environments
675
+ };
676
+ for (const key of Object.keys(project)) {
677
+ if (project[key] === void 0) {
678
+ delete project[key];
679
+ }
680
+ }
681
+ return project;
682
+ }
683
+ function mergeYamlWithProject(project, yamlConfig) {
684
+ return {
685
+ ...project,
686
+ hostname: yamlConfig.hostname || project.hostname,
687
+ basePath: yamlConfig.basePath ?? project.basePath,
688
+ port: yamlConfig.port ?? project.port,
689
+ start: yamlConfig.start ?? project.start,
690
+ env: yamlConfig.env ? { ...project.env, ...yamlConfig.env } : project.env,
691
+ local: yamlConfig.local ?? project.local,
692
+ security: yamlConfig.security ?? project.security,
693
+ environments: yamlConfig.environments ?? project.environments
694
+ };
695
+ }
696
+
553
697
  // src/commands/new.ts
554
698
  async function newCommand(options) {
555
699
  console.log(chalk.dim("Checking prerequisites...\n"));
@@ -584,7 +728,14 @@ async function newCommand(options) {
584
728
  const defaults = getDefaults();
585
729
  let project = {};
586
730
  const cwd = process.cwd();
587
- const cwdName = basename(cwd);
731
+ const cwdName = basename2(cwd);
732
+ const initialPath = options.path || cwd;
733
+ const yamlConfig = existsSync5(initialPath) ? readBindlerYaml(initialPath) : null;
734
+ let yamlDefaults = {};
735
+ if (yamlConfig) {
736
+ console.log(chalk.cyan("Found bindler.yaml - using as defaults\n"));
737
+ yamlDefaults = yamlToProject(yamlConfig, initialPath);
738
+ }
588
739
  if (!options.name) {
589
740
  const answers = await inquirer.prompt([
590
741
  {
@@ -603,7 +754,7 @@ async function newCommand(options) {
603
754
  type: "input",
604
755
  name: "name",
605
756
  message: "Project name:",
606
- default: cwdName,
757
+ default: yamlDefaults.name || cwdName,
607
758
  validate: (input) => {
608
759
  if (!validateProjectName(input)) {
609
760
  return "Invalid project name. Use alphanumeric characters, dashes, and underscores.";
@@ -616,21 +767,22 @@ async function newCommand(options) {
616
767
  name: "type",
617
768
  message: "Project type:",
618
769
  choices: (answers2) => {
619
- const detected = existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
770
+ const detected = existsSync5(answers2.path) ? detectProjectType(answers2.path) : "static";
620
771
  return [
621
772
  { name: `npm (Node.js app)${detected === "npm" ? " - detected" : ""}`, value: "npm" },
622
773
  { name: `static (HTML/CSS/JS)${detected === "static" ? " - detected" : ""}`, value: "static" }
623
774
  ];
624
775
  },
625
776
  default: (answers2) => {
626
- return existsSync4(answers2.path) ? detectProjectType(answers2.path) : "static";
777
+ if (yamlDefaults.type) return yamlDefaults.type;
778
+ return existsSync5(answers2.path) ? detectProjectType(answers2.path) : "static";
627
779
  }
628
780
  },
629
781
  {
630
782
  type: "input",
631
783
  name: "hostname",
632
784
  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,
785
+ default: yamlDefaults.hostname || (options.local ? `${cwdName}.local` : void 0),
634
786
  validate: (input) => {
635
787
  if (!validateHostname(input)) {
636
788
  return "Invalid hostname format";
@@ -642,6 +794,7 @@ async function newCommand(options) {
642
794
  type: "input",
643
795
  name: "basePath",
644
796
  message: "Base path (leave empty for root, or e.g., /api):",
797
+ default: yamlDefaults.basePath || "",
645
798
  filter: (input) => {
646
799
  if (!input || input.trim() === "") return "";
647
800
  const trimmed = input.trim();
@@ -652,9 +805,11 @@ async function newCommand(options) {
652
805
  project = { ...answers };
653
806
  if (!project.basePath) delete project.basePath;
654
807
  if (options.local) project.local = true;
808
+ if (yamlDefaults.security) project.security = yamlDefaults.security;
809
+ if (yamlDefaults.environments) project.environments = yamlDefaults.environments;
655
810
  if (answers.type === "npm") {
656
- const scripts = existsSync4(answers.path) ? getPackageJsonScripts(answers.path) : [];
657
- const suggestedPort = findAvailablePort();
811
+ const scripts = existsSync5(answers.path) ? getPackageJsonScripts(answers.path) : [];
812
+ const suggestedPort = yamlDefaults.port || findAvailablePort();
658
813
  const npmAnswers = await inquirer.prompt([
659
814
  {
660
815
  type: "input",
@@ -678,7 +833,7 @@ async function newCommand(options) {
678
833
  ...scripts.map((s) => ({ name: `npm run ${s}`, value: `npm run ${s}` })),
679
834
  { name: "Custom command...", value: "__custom__" }
680
835
  ] : void 0,
681
- default: scripts.includes("start") ? "npm run start" : "npm start"
836
+ default: yamlDefaults.start || (scripts.includes("start") ? "npm run start" : "npm start")
682
837
  }
683
838
  ]);
684
839
  if (npmAnswers.start === "__custom__") {
@@ -734,7 +889,7 @@ async function newCommand(options) {
734
889
  console.error(chalk.red("Error: Invalid hostname"));
735
890
  process.exit(1);
736
891
  }
737
- if (!existsSync4(project.path)) {
892
+ if (!existsSync5(project.path)) {
738
893
  const createDir = options.name ? true : (await inquirer.prompt([
739
894
  {
740
895
  type: "confirm",
@@ -1193,9 +1348,9 @@ Run ${chalk8.cyan("sudo bindler apply")} to apply changes to nginx.`));
1193
1348
  }
1194
1349
 
1195
1350
  // src/commands/edit.ts
1196
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
1351
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
1197
1352
  import { tmpdir } from "os";
1198
- import { join as join4 } from "path";
1353
+ import { join as join5 } from "path";
1199
1354
  import chalk9 from "chalk";
1200
1355
  async function editCommand(name) {
1201
1356
  const project = getProject(name);
@@ -1204,8 +1359,8 @@ async function editCommand(name) {
1204
1359
  process.exit(1);
1205
1360
  }
1206
1361
  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");
1362
+ const tmpFile = join5(tmpdir(), `bindler-${name}-${Date.now()}.json`);
1363
+ writeFileSync4(tmpFile, JSON.stringify(project, null, 2) + "\n");
1209
1364
  console.log(chalk9.dim(`Opening ${name} config in ${editor}...`));
1210
1365
  const exitCode = await spawnInteractive(editor, [tmpFile]);
1211
1366
  if (exitCode !== 0) {
@@ -1215,7 +1370,7 @@ async function editCommand(name) {
1215
1370
  }
1216
1371
  let editedContent;
1217
1372
  try {
1218
- editedContent = readFileSync3(tmpFile, "utf-8");
1373
+ editedContent = readFileSync4(tmpFile, "utf-8");
1219
1374
  } catch (error) {
1220
1375
  console.error(chalk9.red("Failed to read edited file"));
1221
1376
  process.exit(1);
@@ -1303,6 +1458,7 @@ Run ${chalk10.cyan("sudo bindler apply")} to update nginx configuration.`));
1303
1458
 
1304
1459
  // src/commands/apply.ts
1305
1460
  import chalk11 from "chalk";
1461
+ import { existsSync as existsSync6 } from "fs";
1306
1462
 
1307
1463
  // src/lib/cloudflare.ts
1308
1464
  function isCloudflaredInstalled() {
@@ -1394,8 +1550,36 @@ function getTunnelInfo(tunnelName) {
1394
1550
 
1395
1551
  // src/commands/apply.ts
1396
1552
  async function applyCommand(options) {
1397
- const config = readConfig();
1553
+ let config = readConfig();
1398
1554
  const defaults = getDefaults();
1555
+ if (options.sync) {
1556
+ console.log(chalk11.dim("Syncing bindler.yaml from project directories...\n"));
1557
+ let synced = 0;
1558
+ for (const project of config.projects) {
1559
+ if (!existsSync6(project.path)) continue;
1560
+ const yamlConfig = readBindlerYaml(project.path);
1561
+ if (yamlConfig) {
1562
+ const merged = mergeYamlWithProject(project, yamlConfig);
1563
+ updateProject(project.name, merged);
1564
+ console.log(chalk11.green(` \u2713 Synced ${project.name} from bindler.yaml`));
1565
+ synced++;
1566
+ }
1567
+ }
1568
+ if (synced === 0) {
1569
+ console.log(chalk11.dim(" No bindler.yaml files found in project directories"));
1570
+ } else {
1571
+ console.log(chalk11.dim(`
1572
+ Synced ${synced} project(s)
1573
+ `));
1574
+ }
1575
+ config = readConfig();
1576
+ }
1577
+ if (options.env) {
1578
+ console.log(chalk11.dim(`Using ${options.env} environment configuration...
1579
+ `));
1580
+ const envProjects = listProjectsForEnv(options.env);
1581
+ config = { ...config, projects: envProjects };
1582
+ }
1399
1583
  if (config.projects.length === 0) {
1400
1584
  console.log(chalk11.yellow("No projects registered. Nothing to apply."));
1401
1585
  return;
@@ -1422,6 +1606,18 @@ Try running with sudo: ${chalk11.cyan("sudo bindler apply")}`));
1422
1606
  }
1423
1607
  process.exit(1);
1424
1608
  }
1609
+ const authProjects = config.projects.filter(
1610
+ (p) => p.security?.basicAuth?.enabled && p.security.basicAuth.users?.length
1611
+ );
1612
+ if (authProjects.length > 0) {
1613
+ console.log(chalk11.dim("Generating htpasswd files..."));
1614
+ try {
1615
+ generateHtpasswdFiles(config.projects);
1616
+ console.log(chalk11.green(` \u2713 Generated htpasswd files for ${authProjects.length} project(s)`));
1617
+ } catch (error) {
1618
+ console.log(chalk11.yellow(` ! Failed to generate htpasswd files: ${error}`));
1619
+ }
1620
+ }
1425
1621
  console.log(chalk11.dim("Testing nginx configuration..."));
1426
1622
  const testResult = testNginxConfig();
1427
1623
  if (!testResult.success) {
@@ -1511,7 +1707,7 @@ ${config.projects.length} project(s) configured:`));
1511
1707
 
1512
1708
  // src/commands/doctor.ts
1513
1709
  import chalk12 from "chalk";
1514
- import { existsSync as existsSync5 } from "fs";
1710
+ import { existsSync as existsSync7 } from "fs";
1515
1711
  async function doctorCommand() {
1516
1712
  console.log(chalk12.blue("Running diagnostics...\n"));
1517
1713
  const checks = [];
@@ -1649,7 +1845,7 @@ async function doctorCommand() {
1649
1845
  }
1650
1846
  }
1651
1847
  const defaults = getDefaults();
1652
- if (existsSync5(defaults.nginxManagedPath)) {
1848
+ if (existsSync7(defaults.nginxManagedPath)) {
1653
1849
  checks.push({
1654
1850
  name: "Nginx config file",
1655
1851
  status: "ok",
@@ -1663,7 +1859,7 @@ async function doctorCommand() {
1663
1859
  fix: "Run: sudo bindler apply"
1664
1860
  });
1665
1861
  }
1666
- if (existsSync5(defaults.projectsRoot)) {
1862
+ if (existsSync7(defaults.projectsRoot)) {
1667
1863
  checks.push({
1668
1864
  name: "Projects root",
1669
1865
  status: "ok",
@@ -1758,7 +1954,7 @@ async function infoCommand() {
1758
1954
  `));
1759
1955
  console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
1760
1956
  console.log(chalk14.white(" with Nginx and PM2\n"));
1761
- console.log(chalk14.dim(" Version: ") + chalk14.white("1.1.0"));
1957
+ console.log(chalk14.dim(" Version: ") + chalk14.white("1.2.0"));
1762
1958
  console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
1763
1959
  console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
1764
1960
  console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
@@ -1950,7 +2146,7 @@ Project: ${project.name} (${project.type})`));
1950
2146
  import chalk16 from "chalk";
1951
2147
  import inquirer3 from "inquirer";
1952
2148
  import { execSync as execSync2 } from "child_process";
1953
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2149
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1954
2150
  function detectOs() {
1955
2151
  const platform = process.platform;
1956
2152
  if (platform === "darwin") {
@@ -1961,8 +2157,8 @@ function detectOs() {
1961
2157
  }
1962
2158
  if (platform === "linux") {
1963
2159
  try {
1964
- if (existsSync6("/etc/os-release")) {
1965
- const osRelease = readFileSync4("/etc/os-release", "utf-8");
2160
+ if (existsSync8("/etc/os-release")) {
2161
+ const osRelease = readFileSync5("/etc/os-release", "utf-8");
1966
2162
  const lines = osRelease.split("\n");
1967
2163
  const info = {};
1968
2164
  for (const line of lines) {
@@ -1978,7 +2174,7 @@ function detectOs() {
1978
2174
  if (["ubuntu", "debian", "pop", "mint", "elementary"].includes(distro)) {
1979
2175
  packageManager = "apt";
1980
2176
  } else if (["fedora", "rhel", "centos", "rocky", "alma"].includes(distro)) {
1981
- packageManager = existsSync6("/usr/bin/dnf") ? "dnf" : "yum";
2177
+ packageManager = existsSync8("/usr/bin/dnf") ? "dnf" : "yum";
1982
2178
  } else if (["amzn"].includes(distro)) {
1983
2179
  packageManager = "yum";
1984
2180
  }
@@ -2346,8 +2542,8 @@ async function initCommand() {
2346
2542
  // src/commands/deploy.ts
2347
2543
  import chalk18 from "chalk";
2348
2544
  import { execSync as execSync3 } from "child_process";
2349
- import { existsSync as existsSync7 } from "fs";
2350
- import { join as join5 } from "path";
2545
+ import { existsSync as existsSync9 } from "fs";
2546
+ import { join as join6 } from "path";
2351
2547
  function runInDir(command, cwd) {
2352
2548
  try {
2353
2549
  const output = execSync3(command, { cwd, encoding: "utf-8", stdio: "pipe" });
@@ -2377,7 +2573,7 @@ async function deployCommand(name, options) {
2377
2573
  }
2378
2574
  process.exit(1);
2379
2575
  }
2380
- if (!existsSync7(project.path)) {
2576
+ if (!existsSync9(project.path)) {
2381
2577
  console.log(chalk18.red(`Project path does not exist: ${project.path}`));
2382
2578
  process.exit(1);
2383
2579
  }
@@ -2385,7 +2581,7 @@ async function deployCommand(name, options) {
2385
2581
  Deploying ${project.name}...
2386
2582
  `));
2387
2583
  if (!options.skipPull) {
2388
- const isGitRepo = existsSync7(join5(project.path, ".git"));
2584
+ const isGitRepo = existsSync9(join6(project.path, ".git"));
2389
2585
  if (isGitRepo) {
2390
2586
  console.log(chalk18.dim("Pulling latest changes..."));
2391
2587
  const result = runInDir("git pull", project.path);
@@ -2409,7 +2605,7 @@ Deploying ${project.name}...
2409
2605
  console.log(chalk18.dim(" - Skipped git pull (--skip-pull)"));
2410
2606
  }
2411
2607
  if (project.type === "npm" && !options.skipInstall) {
2412
- const hasPackageJson = existsSync7(join5(project.path, "package.json"));
2608
+ const hasPackageJson = existsSync9(join6(project.path, "package.json"));
2413
2609
  if (hasPackageJson) {
2414
2610
  console.log(chalk18.dim("Installing dependencies..."));
2415
2611
  const result = runInDir("npm install", project.path);
@@ -2444,7 +2640,7 @@ Deploying ${project.name}...
2444
2640
 
2445
2641
  // src/commands/backup.ts
2446
2642
  import chalk19 from "chalk";
2447
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2643
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
2448
2644
  import { dirname as dirname3, resolve } from "path";
2449
2645
  import { homedir as homedir2 } from "os";
2450
2646
  async function backupCommand(options) {
@@ -2453,7 +2649,7 @@ async function backupCommand(options) {
2453
2649
  const defaultPath = resolve(homedir2(), `bindler-backup-${timestamp}.json`);
2454
2650
  const outputPath = options.output || defaultPath;
2455
2651
  const dir = dirname3(outputPath);
2456
- if (!existsSync8(dir)) {
2652
+ if (!existsSync10(dir)) {
2457
2653
  mkdirSync4(dir, { recursive: true });
2458
2654
  }
2459
2655
  const backup = {
@@ -2461,7 +2657,7 @@ async function backupCommand(options) {
2461
2657
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2462
2658
  config
2463
2659
  };
2464
- writeFileSync4(outputPath, JSON.stringify(backup, null, 2) + "\n");
2660
+ writeFileSync5(outputPath, JSON.stringify(backup, null, 2) + "\n");
2465
2661
  console.log(chalk19.green(`
2466
2662
  \u2713 Backup saved to ${outputPath}
2467
2663
  `));
@@ -2481,13 +2677,13 @@ async function restoreCommand(file, options) {
2481
2677
  process.exit(1);
2482
2678
  }
2483
2679
  const filePath = resolve(file);
2484
- if (!existsSync8(filePath)) {
2680
+ if (!existsSync10(filePath)) {
2485
2681
  console.log(chalk19.red(`File not found: ${filePath}`));
2486
2682
  process.exit(1);
2487
2683
  }
2488
2684
  let backup;
2489
2685
  try {
2490
- const content = readFileSync5(filePath, "utf-8");
2686
+ const content = readFileSync6(filePath, "utf-8");
2491
2687
  backup = JSON.parse(content);
2492
2688
  } catch (error) {
2493
2689
  console.log(chalk19.red("Invalid backup file. Must be valid JSON."));
@@ -3094,14 +3290,340 @@ async function completionCommand(shell) {
3094
3290
  }
3095
3291
  }
3096
3292
 
3293
+ // src/commands/clone.ts
3294
+ import chalk26 from "chalk";
3295
+ import inquirer5 from "inquirer";
3296
+ async function cloneCommand(source, newName, options) {
3297
+ if (!source) {
3298
+ console.log(chalk26.red("Usage: bindler clone <source> <new-name>"));
3299
+ console.log(chalk26.dim("\nClones a project configuration with a new name"));
3300
+ console.log(chalk26.dim("\nExamples:"));
3301
+ console.log(chalk26.dim(" bindler clone myapp myapp-staging"));
3302
+ console.log(chalk26.dim(" bindler clone myapp myapp-v2 --hostname newapp.example.com"));
3303
+ console.log(chalk26.dim(" bindler clone myapp myapp-copy --path /var/www/newapp"));
3304
+ process.exit(1);
3305
+ }
3306
+ const sourceProject = getProject(source);
3307
+ if (!sourceProject) {
3308
+ console.log(chalk26.red(`Project "${source}" not found.`));
3309
+ console.log(chalk26.dim("\nAvailable projects:"));
3310
+ const projects = listProjects();
3311
+ for (const p of projects) {
3312
+ console.log(chalk26.dim(` - ${p.name}`));
3313
+ }
3314
+ process.exit(1);
3315
+ }
3316
+ let targetName = newName;
3317
+ if (!targetName) {
3318
+ const answer = await inquirer5.prompt([
3319
+ {
3320
+ type: "input",
3321
+ name: "name",
3322
+ message: "New project name:",
3323
+ default: `${source}-copy`,
3324
+ validate: (input) => {
3325
+ if (!validateProjectName(input)) {
3326
+ return "Invalid project name. Use alphanumeric characters, dashes, and underscores.";
3327
+ }
3328
+ if (getProject(input)) {
3329
+ return `Project "${input}" already exists`;
3330
+ }
3331
+ return true;
3332
+ }
3333
+ }
3334
+ ]);
3335
+ targetName = answer.name;
3336
+ } else {
3337
+ if (!validateProjectName(targetName)) {
3338
+ console.log(chalk26.red("Invalid project name. Use alphanumeric characters, dashes, and underscores."));
3339
+ process.exit(1);
3340
+ }
3341
+ if (getProject(targetName)) {
3342
+ console.log(chalk26.red(`Project "${targetName}" already exists.`));
3343
+ process.exit(1);
3344
+ }
3345
+ }
3346
+ let targetHostname = options.hostname;
3347
+ if (!targetHostname) {
3348
+ const answer = await inquirer5.prompt([
3349
+ {
3350
+ type: "input",
3351
+ name: "hostname",
3352
+ message: "Hostname for new project:",
3353
+ default: sourceProject.hostname.replace(source, targetName),
3354
+ validate: (input) => {
3355
+ if (!validateHostname(input)) {
3356
+ return "Invalid hostname format";
3357
+ }
3358
+ return true;
3359
+ }
3360
+ }
3361
+ ]);
3362
+ targetHostname = answer.hostname;
3363
+ } else {
3364
+ if (!validateHostname(targetHostname)) {
3365
+ console.log(chalk26.red("Invalid hostname format."));
3366
+ process.exit(1);
3367
+ }
3368
+ }
3369
+ const newProject = {
3370
+ ...sourceProject,
3371
+ name: targetName,
3372
+ hostname: targetHostname,
3373
+ path: options.path || sourceProject.path
3374
+ };
3375
+ if (newProject.type === "npm") {
3376
+ newProject.port = options.port || findAvailablePort();
3377
+ if (newProject.env?.PORT) {
3378
+ newProject.env = { ...newProject.env, PORT: String(newProject.port) };
3379
+ }
3380
+ }
3381
+ try {
3382
+ addProject(newProject);
3383
+ console.log(chalk26.green(`
3384
+ Project "${targetName}" cloned from "${source}"!
3385
+ `));
3386
+ console.log(chalk26.dim("Configuration:"));
3387
+ console.log(chalk26.dim(` Name: ${newProject.name}`));
3388
+ console.log(chalk26.dim(` Type: ${newProject.type}`));
3389
+ console.log(chalk26.dim(` Path: ${newProject.path}`));
3390
+ console.log(chalk26.dim(` Hostname: ${newProject.hostname}`));
3391
+ if (newProject.port) {
3392
+ console.log(chalk26.dim(` Port: ${newProject.port}`));
3393
+ }
3394
+ console.log(chalk26.dim(`
3395
+ Run ${chalk26.cyan("sudo bindler apply")} to update nginx configuration.`));
3396
+ if (newProject.type === "npm") {
3397
+ console.log(chalk26.dim(`Run ${chalk26.cyan(`bindler start ${targetName}`)} to start the application.`));
3398
+ }
3399
+ } catch (error) {
3400
+ console.error(chalk26.red(`Error: ${error instanceof Error ? error.message : error}`));
3401
+ process.exit(1);
3402
+ }
3403
+ }
3404
+
3405
+ // src/commands/dev.ts
3406
+ import chalk27 from "chalk";
3407
+ import { spawn as spawn3 } from "child_process";
3408
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
3409
+ import { basename as basename3, join as join7 } from "path";
3410
+ function getPackageJson(dir) {
3411
+ const pkgPath = join7(dir, "package.json");
3412
+ if (!existsSync11(pkgPath)) return null;
3413
+ try {
3414
+ return JSON.parse(readFileSync7(pkgPath, "utf-8"));
3415
+ } catch {
3416
+ return null;
3417
+ }
3418
+ }
3419
+ function getDevCommand(dir) {
3420
+ const pkg = getPackageJson(dir);
3421
+ if (!pkg?.scripts) return null;
3422
+ const scripts = pkg.scripts;
3423
+ if (scripts.dev) return "npm run dev";
3424
+ if (scripts["start:dev"]) return "npm run start:dev";
3425
+ if (scripts.watch) return "npm run watch";
3426
+ if (scripts.start) return "npm run start";
3427
+ return null;
3428
+ }
3429
+ async function devCommand(name, options) {
3430
+ const cwd = process.cwd();
3431
+ let project;
3432
+ let projectDir;
3433
+ if (name) {
3434
+ project = getProject(name);
3435
+ if (!project) {
3436
+ console.log(chalk27.red(`Project "${name}" not found.`));
3437
+ console.log(chalk27.dim("\nAvailable projects:"));
3438
+ const projects = listProjects();
3439
+ for (const p of projects) {
3440
+ console.log(chalk27.dim(` - ${p.name}`));
3441
+ }
3442
+ process.exit(1);
3443
+ }
3444
+ projectDir = project.path;
3445
+ } else {
3446
+ const projects = listProjects();
3447
+ project = projects.find((p) => p.path === cwd);
3448
+ if (!project) {
3449
+ const yamlConfig = readBindlerYaml(cwd);
3450
+ if (yamlConfig) {
3451
+ console.log(chalk27.cyan("Found bindler.yaml - creating temporary dev project\n"));
3452
+ const yamlProject = yamlToProject(yamlConfig, cwd);
3453
+ project = {
3454
+ name: yamlProject.name || basename3(cwd),
3455
+ type: yamlProject.type || "npm",
3456
+ path: cwd,
3457
+ hostname: options.hostname || yamlProject.hostname || `${basename3(cwd)}.local`,
3458
+ port: options.port || yamlProject.port || findAvailablePort(),
3459
+ start: yamlProject.start,
3460
+ local: true
3461
+ };
3462
+ } else {
3463
+ const pkg = getPackageJson(cwd);
3464
+ if (!pkg) {
3465
+ console.log(chalk27.red("No package.json found in current directory."));
3466
+ console.log(chalk27.dim("\nUsage: bindler dev [name]"));
3467
+ console.log(chalk27.dim(" Run in a project directory or specify a project name"));
3468
+ process.exit(1);
3469
+ }
3470
+ project = {
3471
+ name: pkg.name || basename3(cwd),
3472
+ type: "npm",
3473
+ path: cwd,
3474
+ hostname: options.hostname || `${basename3(cwd)}.local`,
3475
+ port: options.port || findAvailablePort(),
3476
+ local: true
3477
+ };
3478
+ }
3479
+ }
3480
+ projectDir = cwd;
3481
+ }
3482
+ if (project.type !== "npm") {
3483
+ console.log(chalk27.red("Dev mode is only supported for npm projects."));
3484
+ console.log(chalk27.dim("\nFor static projects, use a local web server:"));
3485
+ console.log(chalk27.dim(" npx serve " + project.path));
3486
+ process.exit(1);
3487
+ }
3488
+ if (!existsSync11(projectDir)) {
3489
+ console.log(chalk27.red(`Project directory not found: ${projectDir}`));
3490
+ process.exit(1);
3491
+ }
3492
+ const devCmd = getDevCommand(projectDir) || project.start || "npm start";
3493
+ const port = options.port || project.port || findAvailablePort();
3494
+ console.log(chalk27.blue(`
3495
+ Starting ${project.name} in dev mode...
3496
+ `));
3497
+ console.log(chalk27.dim(` Directory: ${projectDir}`));
3498
+ console.log(chalk27.dim(` Command: ${devCmd}`));
3499
+ console.log(chalk27.dim(` Port: ${port}`));
3500
+ console.log(chalk27.dim(` Hostname: ${project.hostname}`));
3501
+ if (project.hostname.endsWith(".local") || project.local) {
3502
+ console.log(chalk27.yellow(`
3503
+ Note: Add to /etc/hosts if not already:`));
3504
+ console.log(chalk27.cyan(` echo "127.0.0.1 ${project.hostname}" | sudo tee -a /etc/hosts`));
3505
+ }
3506
+ const defaults = getDefaults();
3507
+ const listenPort = defaults.nginxListen.split(":")[1] || "8080";
3508
+ console.log(chalk27.green(`
3509
+ Access at: http://${project.hostname}:${listenPort}`));
3510
+ console.log(chalk27.dim("Press Ctrl+C to stop\n"));
3511
+ console.log(chalk27.dim("---"));
3512
+ const env = {
3513
+ ...process.env,
3514
+ PORT: String(port),
3515
+ ...project.env
3516
+ };
3517
+ const [cmd, ...args] = devCmd.split(" ");
3518
+ const child = spawn3(cmd, args, {
3519
+ cwd: projectDir,
3520
+ env,
3521
+ stdio: "inherit",
3522
+ shell: true
3523
+ });
3524
+ child.on("error", (error) => {
3525
+ console.error(chalk27.red(`
3526
+ Failed to start: ${error.message}`));
3527
+ process.exit(1);
3528
+ });
3529
+ child.on("exit", (code) => {
3530
+ if (code !== 0) {
3531
+ console.log(chalk27.yellow(`
3532
+ Process exited with code ${code}`));
3533
+ }
3534
+ process.exit(code || 0);
3535
+ });
3536
+ process.on("SIGINT", () => {
3537
+ console.log(chalk27.dim("\n\nStopping dev server..."));
3538
+ child.kill("SIGINT");
3539
+ });
3540
+ process.on("SIGTERM", () => {
3541
+ child.kill("SIGTERM");
3542
+ });
3543
+ }
3544
+
3545
+ // src/lib/update-check.ts
3546
+ import chalk28 from "chalk";
3547
+ import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
3548
+ import { join as join8 } from "path";
3549
+ import { homedir as homedir3 } from "os";
3550
+ var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
3551
+ var CACHE_DIR = join8(homedir3(), ".config", "bindler");
3552
+ var CACHE_FILE = join8(CACHE_DIR, ".update-check");
3553
+ function readCache() {
3554
+ try {
3555
+ if (existsSync12(CACHE_FILE)) {
3556
+ return JSON.parse(readFileSync8(CACHE_FILE, "utf-8"));
3557
+ }
3558
+ } catch {
3559
+ }
3560
+ return { lastCheck: 0, latestVersion: null };
3561
+ }
3562
+ function writeCache(data) {
3563
+ try {
3564
+ if (!existsSync12(CACHE_DIR)) {
3565
+ mkdirSync5(CACHE_DIR, { recursive: true });
3566
+ }
3567
+ writeFileSync6(CACHE_FILE, JSON.stringify(data));
3568
+ } catch {
3569
+ }
3570
+ }
3571
+ async function fetchLatestVersion() {
3572
+ try {
3573
+ const controller = new AbortController();
3574
+ const timeout = setTimeout(() => controller.abort(), 3e3);
3575
+ const response = await fetch("https://registry.npmjs.org/bindler/latest", {
3576
+ signal: controller.signal
3577
+ });
3578
+ clearTimeout(timeout);
3579
+ if (!response.ok) return null;
3580
+ const data = await response.json();
3581
+ return data.version || null;
3582
+ } catch {
3583
+ return null;
3584
+ }
3585
+ }
3586
+ function compareVersions(current, latest) {
3587
+ const c = current.split(".").map(Number);
3588
+ const l = latest.split(".").map(Number);
3589
+ for (let i = 0; i < 3; i++) {
3590
+ if ((l[i] || 0) > (c[i] || 0)) return 1;
3591
+ if ((l[i] || 0) < (c[i] || 0)) return -1;
3592
+ }
3593
+ return 0;
3594
+ }
3595
+ async function checkForUpdates() {
3596
+ const cache = readCache();
3597
+ const now = Date.now();
3598
+ if (now - cache.lastCheck < CHECK_INTERVAL) {
3599
+ if (cache.latestVersion && compareVersions("1.2.0", cache.latestVersion) < 0) {
3600
+ showUpdateMessage(cache.latestVersion);
3601
+ }
3602
+ return;
3603
+ }
3604
+ fetchLatestVersion().then((latestVersion) => {
3605
+ writeCache({ lastCheck: now, latestVersion });
3606
+ if (latestVersion && compareVersions("1.2.0", latestVersion) < 0) {
3607
+ showUpdateMessage(latestVersion);
3608
+ }
3609
+ });
3610
+ }
3611
+ function showUpdateMessage(latestVersion) {
3612
+ console.log("");
3613
+ console.log(chalk28.yellow(` Update available: ${"1.2.0"} \u2192 ${latestVersion}`));
3614
+ console.log(chalk28.dim(` Run: npm update -g bindler`));
3615
+ console.log("");
3616
+ }
3617
+
3097
3618
  // src/cli.ts
3098
3619
  var program = new Command();
3099
- program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.0.0");
3100
- program.hook("preAction", () => {
3620
+ program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.2.0");
3621
+ program.hook("preAction", async () => {
3101
3622
  try {
3102
3623
  initConfig();
3103
3624
  } catch (error) {
3104
3625
  }
3626
+ checkForUpdates();
3105
3627
  });
3106
3628
  program.command("new").description("Create and register a new project").option("-n, --name <name>", "Project name").option("-t, --type <type>", "Project type (static or npm)", "static").option("-p, --path <path>", "Project directory path").option("-h, --hostname <hostname>", "Hostname for the project").option("-b, --base-path <path>", "Base path for path-based routing (e.g., /api)").option("--port <port>", "Port number (npm projects only)").option("-s, --start <command>", "Start command (npm projects only)").option("-l, --local", "Local project (skip Cloudflare, use .local hostname)").option("--apply", "Apply nginx config after creating").action(async (options) => {
3107
3629
  await newCommand(options);
@@ -3114,78 +3636,78 @@ program.command("status").description("Show detailed status of all projects").ac
3114
3636
  });
3115
3637
  program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
3116
3638
  if (!name && !options.all) {
3117
- console.log(chalk26.red("Usage: bindler start <name> or bindler start --all"));
3118
- console.log(chalk26.dim("\nExamples:"));
3119
- console.log(chalk26.dim(" bindler start myapp"));
3120
- console.log(chalk26.dim(" bindler start --all # start all npm projects"));
3639
+ console.log(chalk29.red("Usage: bindler start <name> or bindler start --all"));
3640
+ console.log(chalk29.dim("\nExamples:"));
3641
+ console.log(chalk29.dim(" bindler start myapp"));
3642
+ console.log(chalk29.dim(" bindler start --all # start all npm projects"));
3121
3643
  process.exit(1);
3122
3644
  }
3123
3645
  await startCommand(name, options);
3124
3646
  });
3125
3647
  program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
3126
3648
  if (!name && !options.all) {
3127
- console.log(chalk26.red("Usage: bindler stop <name> or bindler stop --all"));
3128
- console.log(chalk26.dim("\nExamples:"));
3129
- console.log(chalk26.dim(" bindler stop myapp"));
3130
- console.log(chalk26.dim(" bindler stop --all # stop all npm projects"));
3649
+ console.log(chalk29.red("Usage: bindler stop <name> or bindler stop --all"));
3650
+ console.log(chalk29.dim("\nExamples:"));
3651
+ console.log(chalk29.dim(" bindler stop myapp"));
3652
+ console.log(chalk29.dim(" bindler stop --all # stop all npm projects"));
3131
3653
  process.exit(1);
3132
3654
  }
3133
3655
  await stopCommand(name, options);
3134
3656
  });
3135
3657
  program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
3136
3658
  if (!name && !options.all) {
3137
- console.log(chalk26.red("Usage: bindler restart <name> or bindler restart --all"));
3138
- console.log(chalk26.dim("\nExamples:"));
3139
- console.log(chalk26.dim(" bindler restart myapp"));
3140
- console.log(chalk26.dim(" bindler restart --all # restart all npm projects"));
3659
+ console.log(chalk29.red("Usage: bindler restart <name> or bindler restart --all"));
3660
+ console.log(chalk29.dim("\nExamples:"));
3661
+ console.log(chalk29.dim(" bindler restart myapp"));
3662
+ console.log(chalk29.dim(" bindler restart --all # restart all npm projects"));
3141
3663
  process.exit(1);
3142
3664
  }
3143
3665
  await restartCommand(name, options);
3144
3666
  });
3145
3667
  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) => {
3146
3668
  if (!name) {
3147
- console.log(chalk26.red("Usage: bindler logs <name>"));
3148
- console.log(chalk26.dim("\nExamples:"));
3149
- console.log(chalk26.dim(" bindler logs myapp"));
3150
- console.log(chalk26.dim(" bindler logs myapp --follow"));
3151
- console.log(chalk26.dim(" bindler logs myapp --lines 500"));
3669
+ console.log(chalk29.red("Usage: bindler logs <name>"));
3670
+ console.log(chalk29.dim("\nExamples:"));
3671
+ console.log(chalk29.dim(" bindler logs myapp"));
3672
+ console.log(chalk29.dim(" bindler logs myapp --follow"));
3673
+ console.log(chalk29.dim(" bindler logs myapp --lines 500"));
3152
3674
  process.exit(1);
3153
3675
  }
3154
3676
  await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
3155
3677
  });
3156
3678
  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) => {
3157
3679
  if (!name) {
3158
- console.log(chalk26.red("Usage: bindler update <name> [options]"));
3159
- console.log(chalk26.dim("\nExamples:"));
3160
- console.log(chalk26.dim(" bindler update myapp --hostname newapp.example.com"));
3161
- console.log(chalk26.dim(" bindler update myapp --port 4000"));
3162
- console.log(chalk26.dim(" bindler update myapp --disable"));
3680
+ console.log(chalk29.red("Usage: bindler update <name> [options]"));
3681
+ console.log(chalk29.dim("\nExamples:"));
3682
+ console.log(chalk29.dim(" bindler update myapp --hostname newapp.example.com"));
3683
+ console.log(chalk29.dim(" bindler update myapp --port 4000"));
3684
+ console.log(chalk29.dim(" bindler update myapp --disable"));
3163
3685
  process.exit(1);
3164
3686
  }
3165
3687
  await updateCommand(name, options);
3166
3688
  });
3167
3689
  program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
3168
3690
  if (!name) {
3169
- console.log(chalk26.red("Usage: bindler edit <name>"));
3170
- console.log(chalk26.dim("\nOpens the project config in your $EDITOR"));
3171
- console.log(chalk26.dim("\nExample:"));
3172
- console.log(chalk26.dim(" bindler edit myapp"));
3691
+ console.log(chalk29.red("Usage: bindler edit <name>"));
3692
+ console.log(chalk29.dim("\nOpens the project config in your $EDITOR"));
3693
+ console.log(chalk29.dim("\nExample:"));
3694
+ console.log(chalk29.dim(" bindler edit myapp"));
3173
3695
  process.exit(1);
3174
3696
  }
3175
3697
  await editCommand(name);
3176
3698
  });
3177
3699
  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) => {
3178
3700
  if (!name) {
3179
- console.log(chalk26.red("Usage: bindler remove <name>"));
3180
- console.log(chalk26.dim("\nExamples:"));
3181
- console.log(chalk26.dim(" bindler remove myapp"));
3182
- console.log(chalk26.dim(" bindler remove myapp --force # skip confirmation"));
3183
- console.log(chalk26.dim(" bindler rm myapp # alias"));
3701
+ console.log(chalk29.red("Usage: bindler remove <name>"));
3702
+ console.log(chalk29.dim("\nExamples:"));
3703
+ console.log(chalk29.dim(" bindler remove myapp"));
3704
+ console.log(chalk29.dim(" bindler remove myapp --force # skip confirmation"));
3705
+ console.log(chalk29.dim(" bindler rm myapp # alias"));
3184
3706
  process.exit(1);
3185
3707
  }
3186
3708
  await removeCommand(name, options);
3187
3709
  });
3188
- 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) => {
3710
+ 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)").action(async (options) => {
3189
3711
  await applyCommand(options);
3190
3712
  });
3191
3713
  program.command("doctor").description("Run system diagnostics and check dependencies").action(async () => {
@@ -3199,10 +3721,10 @@ program.command("info").description("Show bindler information and stats").action
3199
3721
  });
3200
3722
  program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
3201
3723
  if (!hostname) {
3202
- console.log(chalk26.red("Usage: bindler check <hostname>"));
3203
- console.log(chalk26.dim("\nExamples:"));
3204
- console.log(chalk26.dim(" bindler check myapp.example.com"));
3205
- console.log(chalk26.dim(" bindler check myapp # uses project name"));
3724
+ console.log(chalk29.red("Usage: bindler check <hostname>"));
3725
+ console.log(chalk29.dim("\nExamples:"));
3726
+ console.log(chalk29.dim(" bindler check myapp.example.com"));
3727
+ console.log(chalk29.dim(" bindler check myapp # uses project name"));
3206
3728
  process.exit(1);
3207
3729
  }
3208
3730
  await checkCommand(hostname, options);
@@ -3240,5 +3762,11 @@ program.command("stats").description("Show CPU and memory stats for npm projects
3240
3762
  program.command("completion [shell]").description("Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
3241
3763
  await completionCommand(shell);
3242
3764
  });
3765
+ 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) => {
3766
+ await cloneCommand(source, newName, options);
3767
+ });
3768
+ 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) => {
3769
+ await devCommand(name, options);
3770
+ });
3243
3771
  program.parse();
3244
3772
  //# sourceMappingURL=cli.js.map