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.
- package/dist/cli.js +1317 -583
- package/dist/cli.js.map +1 -1
- 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
|
|
11
|
+
import chalk30 from "chalk";
|
|
6
12
|
|
|
7
13
|
// src/commands/new.ts
|
|
8
|
-
import { existsSync as
|
|
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
|
|
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(
|
|
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(
|
|
880
|
+
console.log(chalk2.red("Missing prerequisites:\n"));
|
|
565
881
|
for (const issue of issues) {
|
|
566
|
-
console.log(
|
|
882
|
+
console.log(chalk2.red(` \u2717 ${issue}`));
|
|
567
883
|
}
|
|
568
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
1061
|
+
console.error(chalk2.red("Error: Invalid hostname"));
|
|
735
1062
|
process.exit(1);
|
|
736
1063
|
}
|
|
737
|
-
if (!
|
|
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(
|
|
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(
|
|
1101
|
+
console.log(chalk2.green(`
|
|
754
1102
|
Project "${project.name}" added successfully!`));
|
|
755
1103
|
if (project.local) {
|
|
756
|
-
console.log(
|
|
1104
|
+
console.log(chalk2.yellow(`
|
|
757
1105
|
Local project - add to /etc/hosts:`));
|
|
758
|
-
console.log(
|
|
759
|
-
console.log(
|
|
760
|
-
Run ${
|
|
761
|
-
console.log(
|
|
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(
|
|
764
|
-
Configuration saved. Run ${
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
782
|
-
console.log(
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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" ?
|
|
1152
|
+
status = process2.status === "online" ? chalk3.green("online") : process2.status === "stopped" ? chalk3.yellow("stopped") : chalk3.red(process2.status);
|
|
805
1153
|
} else {
|
|
806
|
-
status =
|
|
1154
|
+
status = chalk3.dim("not started");
|
|
807
1155
|
}
|
|
808
1156
|
} else {
|
|
809
|
-
status = project.enabled !== false ?
|
|
1157
|
+
status = project.enabled !== false ? chalk3.green("serving") : chalk3.yellow("disabled");
|
|
810
1158
|
}
|
|
811
1159
|
if (project.enabled === false) {
|
|
812
|
-
status =
|
|
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(
|
|
1172
|
+
console.log(chalk3.dim(`
|
|
825
1173
|
${projects.length} project(s) registered`));
|
|
826
1174
|
}
|
|
827
1175
|
|
|
828
1176
|
// src/commands/status.ts
|
|
829
|
-
import
|
|
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(
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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 =
|
|
1224
|
+
pm2StatusStr = chalk4.green("online");
|
|
877
1225
|
break;
|
|
878
1226
|
case "stopped":
|
|
879
|
-
pm2StatusStr =
|
|
1227
|
+
pm2StatusStr = chalk4.yellow("stopped");
|
|
880
1228
|
break;
|
|
881
1229
|
case "errored":
|
|
882
|
-
pm2StatusStr =
|
|
1230
|
+
pm2StatusStr = chalk4.red("errored");
|
|
883
1231
|
break;
|
|
884
1232
|
case "not_managed":
|
|
885
|
-
pm2StatusStr =
|
|
1233
|
+
pm2StatusStr = chalk4.dim("not started");
|
|
886
1234
|
break;
|
|
887
1235
|
default:
|
|
888
|
-
pm2StatusStr =
|
|
1236
|
+
pm2StatusStr = chalk4.dim(status.pm2Status || "-");
|
|
889
1237
|
}
|
|
890
|
-
portCheckStr = status.portListening ?
|
|
1238
|
+
portCheckStr = status.portListening ? chalk4.green("listening") : chalk4.red("not listening");
|
|
891
1239
|
}
|
|
892
1240
|
if (status.enabled === false) {
|
|
893
|
-
pm2StatusStr =
|
|
894
|
-
portCheckStr =
|
|
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(
|
|
1256
|
+
console.log(chalk4.bold("\nPM2 Process Details:"));
|
|
909
1257
|
const detailTable = new Table2({
|
|
910
1258
|
head: [
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
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(
|
|
1290
|
+
console.log(chalk5.yellow("No npm projects to start."));
|
|
943
1291
|
return;
|
|
944
1292
|
}
|
|
945
|
-
console.log(
|
|
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(
|
|
1297
|
+
console.log(chalk5.green(` \u2713 ${result2.name}`));
|
|
950
1298
|
} else {
|
|
951
|
-
console.log(
|
|
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(
|
|
1303
|
+
console.log(chalk5.dim(`
|
|
956
1304
|
${succeeded}/${results.length} started successfully`));
|
|
957
1305
|
return;
|
|
958
1306
|
}
|
|
959
1307
|
if (!name) {
|
|
960
|
-
console.error(
|
|
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(
|
|
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(
|
|
970
|
-
console.log(
|
|
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(
|
|
1321
|
+
console.log(chalk5.blue(`Starting ${name}...`));
|
|
974
1322
|
const result = startProject(project);
|
|
975
1323
|
if (result.success) {
|
|
976
|
-
console.log(
|
|
977
|
-
console.log(
|
|
978
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
1340
|
+
console.log(chalk6.yellow("No npm projects to stop."));
|
|
993
1341
|
return;
|
|
994
1342
|
}
|
|
995
|
-
console.log(
|
|
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(
|
|
1347
|
+
console.log(chalk6.green(` \u2713 ${result2.name}`));
|
|
1000
1348
|
} else {
|
|
1001
|
-
console.log(
|
|
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(
|
|
1353
|
+
console.log(chalk6.dim(`
|
|
1006
1354
|
${succeeded}/${results.length} stopped successfully`));
|
|
1007
1355
|
return;
|
|
1008
1356
|
}
|
|
1009
1357
|
if (!name) {
|
|
1010
|
-
console.error(
|
|
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(
|
|
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(
|
|
1367
|
+
console.log(chalk6.yellow(`Project "${name}" is a static site - no process to stop.`));
|
|
1020
1368
|
return;
|
|
1021
1369
|
}
|
|
1022
|
-
console.log(
|
|
1370
|
+
console.log(chalk6.blue(`Stopping ${name}...`));
|
|
1023
1371
|
const result = stopProject(name);
|
|
1024
1372
|
if (result.success) {
|
|
1025
|
-
console.log(
|
|
1373
|
+
console.log(chalk6.green(`\u2713 ${name} stopped successfully`));
|
|
1026
1374
|
} else {
|
|
1027
|
-
console.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
|
|
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(
|
|
1387
|
+
console.log(chalk7.yellow("No npm projects to restart."));
|
|
1040
1388
|
return;
|
|
1041
1389
|
}
|
|
1042
|
-
console.log(
|
|
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(
|
|
1394
|
+
console.log(chalk7.green(` \u2713 ${result2.name}`));
|
|
1047
1395
|
} else {
|
|
1048
|
-
console.log(
|
|
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(
|
|
1400
|
+
console.log(chalk7.dim(`
|
|
1053
1401
|
${succeeded}/${results.length} restarted successfully`));
|
|
1054
1402
|
return;
|
|
1055
1403
|
}
|
|
1056
1404
|
if (!name) {
|
|
1057
|
-
console.error(
|
|
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(
|
|
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(
|
|
1414
|
+
console.log(chalk7.yellow(`Project "${name}" is a static site - no process to restart.`));
|
|
1067
1415
|
return;
|
|
1068
1416
|
}
|
|
1069
|
-
console.log(
|
|
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(
|
|
1423
|
+
console.log(chalk7.dim("Process not running, starting..."));
|
|
1076
1424
|
result = startProject(project);
|
|
1077
1425
|
}
|
|
1078
1426
|
if (result.success) {
|
|
1079
|
-
console.log(
|
|
1427
|
+
console.log(chalk7.green(`\u2713 ${name} restarted successfully`));
|
|
1080
1428
|
} else {
|
|
1081
|
-
console.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
|
|
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(
|
|
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(
|
|
1096
|
-
console.log(
|
|
1097
|
-
console.log(
|
|
1098
|
-
console.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(
|
|
1104
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1175
|
-
console.log(
|
|
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(
|
|
1539
|
+
console.log(chalk9.green(`\u2713 Project "${name}" updated successfully`));
|
|
1181
1540
|
for (const [key, value] of Object.entries(updates)) {
|
|
1182
|
-
console.log(
|
|
1541
|
+
console.log(chalk9.dim(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`));
|
|
1183
1542
|
}
|
|
1184
|
-
console.log(
|
|
1185
|
-
Run ${
|
|
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(
|
|
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(
|
|
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
|
|
1555
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1197
1556
|
import { tmpdir } from "os";
|
|
1198
|
-
import { join as
|
|
1199
|
-
import
|
|
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(
|
|
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 =
|
|
1208
|
-
|
|
1209
|
-
console.log(
|
|
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(
|
|
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 =
|
|
1577
|
+
editedContent = readFileSync4(tmpFile, "utf-8");
|
|
1219
1578
|
} catch (error) {
|
|
1220
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1252
|
-
console.log(
|
|
1253
|
-
Run ${
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1293
|
-
console.log(
|
|
1294
|
-
Run ${
|
|
1295
|
-
console.log(
|
|
1296
|
-
console.log(
|
|
1297
|
-
console.log(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
1788
|
+
console.log(chalk12.yellow("No projects registered. Nothing to apply."));
|
|
1401
1789
|
return;
|
|
1402
1790
|
}
|
|
1403
|
-
console.log(
|
|
1404
|
-
|
|
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(
|
|
1811
|
+
console.log(chalk12.cyan("\n--- Generated nginx config (dry-run) ---\n"));
|
|
1408
1812
|
console.log(nginxConfig);
|
|
1409
|
-
console.log(
|
|
1410
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
1421
|
-
Try running with sudo: ${
|
|
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
|
-
|
|
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(
|
|
1429
|
-
console.error(
|
|
1430
|
-
console.log(
|
|
1431
|
-
console.log(
|
|
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(
|
|
1850
|
+
console.log(chalk12.green(" \u2713 Nginx configuration test passed"));
|
|
1435
1851
|
if (!options.noReload) {
|
|
1436
|
-
console.log(
|
|
1852
|
+
console.log(chalk12.dim("Reloading nginx..."));
|
|
1437
1853
|
const reloadResult = reloadNginx();
|
|
1438
1854
|
if (!reloadResult.success) {
|
|
1439
|
-
console.error(
|
|
1440
|
-
console.log(
|
|
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(
|
|
1859
|
+
console.log(chalk12.green(" \u2713 Nginx reloaded successfully"));
|
|
1444
1860
|
} else {
|
|
1445
|
-
console.log(
|
|
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(
|
|
1865
|
+
console.log(chalk12.dim("\n - Direct mode: skipping Cloudflare DNS routes"));
|
|
1450
1866
|
} else if (!options.noCloudflare && defaults.applyCloudflareDnsRoutes) {
|
|
1451
|
-
console.log(
|
|
1867
|
+
console.log(chalk12.dim("\nConfiguring Cloudflare DNS routes..."));
|
|
1452
1868
|
if (!isCloudflaredInstalled()) {
|
|
1453
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
1880
|
+
console.log(chalk12.green(` \u2713 ${result.hostname} (${msg})`));
|
|
1465
1881
|
} else {
|
|
1466
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1483
|
-
console.log(
|
|
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(
|
|
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(
|
|
1908
|
+
console.log(chalk12.green(` \u2713 ${hostname} (secured)`));
|
|
1493
1909
|
} else if (result.output?.includes("already exists")) {
|
|
1494
|
-
console.log(
|
|
1910
|
+
console.log(chalk12.green(` \u2713 ${hostname} (exists)`));
|
|
1495
1911
|
} else {
|
|
1496
|
-
console.log(
|
|
1497
|
-
console.log(
|
|
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(
|
|
1504
|
-
console.log(
|
|
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 ?
|
|
1508
|
-
console.log(
|
|
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
|
|
1514
|
-
import { existsSync as
|
|
1929
|
+
import chalk13 from "chalk";
|
|
1930
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1515
1931
|
async function doctorCommand() {
|
|
1516
|
-
console.log(
|
|
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 (
|
|
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 (
|
|
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:
|
|
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 =
|
|
2172
|
+
color = chalk13.green;
|
|
1697
2173
|
break;
|
|
1698
2174
|
case "warning":
|
|
1699
2175
|
icon = "!";
|
|
1700
|
-
color =
|
|
2176
|
+
color = chalk13.yellow;
|
|
1701
2177
|
hasWarnings = true;
|
|
1702
2178
|
break;
|
|
1703
2179
|
case "error":
|
|
1704
2180
|
icon = "\u2717";
|
|
1705
|
-
color =
|
|
2181
|
+
color = chalk13.red;
|
|
1706
2182
|
hasErrors = true;
|
|
1707
2183
|
break;
|
|
1708
2184
|
}
|
|
1709
|
-
console.log(`${color(icon)} ${
|
|
2185
|
+
console.log(`${color(icon)} ${chalk13.bold(check.name)}: ${check.message}`);
|
|
1710
2186
|
if (check.fix) {
|
|
1711
|
-
console.log(
|
|
2187
|
+
console.log(chalk13.dim(` Fix: ${check.fix}`));
|
|
1712
2188
|
}
|
|
1713
2189
|
}
|
|
1714
2190
|
console.log("");
|
|
1715
2191
|
if (hasErrors) {
|
|
1716
|
-
console.log(
|
|
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(
|
|
2195
|
+
console.log(chalk13.yellow("Some warnings detected. Review the suggestions above."));
|
|
1720
2196
|
} else {
|
|
1721
|
-
console.log(
|
|
2197
|
+
console.log(chalk13.green("All checks passed!"));
|
|
1722
2198
|
}
|
|
1723
2199
|
}
|
|
1724
2200
|
|
|
1725
2201
|
// src/commands/ports.ts
|
|
1726
|
-
import
|
|
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(
|
|
1732
|
-
console.log(
|
|
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: [
|
|
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(
|
|
2222
|
+
console.log(chalk14.dim(`
|
|
1747
2223
|
${ports.length} port(s) allocated`));
|
|
1748
2224
|
}
|
|
1749
2225
|
|
|
1750
2226
|
// src/commands/info.ts
|
|
1751
|
-
import
|
|
2227
|
+
import chalk15 from "chalk";
|
|
1752
2228
|
async function infoCommand() {
|
|
1753
|
-
console.log(
|
|
2229
|
+
console.log(chalk15.bold.cyan(String.raw`
|
|
1754
2230
|
_ _ _ _
|
|
1755
2231
|
| |_|_|___ _| | |___ ___
|
|
1756
2232
|
| . | | | . | | -_| _|
|
|
1757
2233
|
|___|_|_|_|___|_|___|_|
|
|
1758
2234
|
`));
|
|
1759
|
-
console.log(
|
|
1760
|
-
console.log(
|
|
1761
|
-
console.log(
|
|
1762
|
-
console.log(
|
|
1763
|
-
console.log(
|
|
1764
|
-
console.log(
|
|
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(
|
|
1773
|
-
console.log(
|
|
1774
|
-
console.log(
|
|
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(
|
|
2252
|
+
console.log(chalk15.dim(" Projects: ") + chalk15.white(`${config.projects.length} total (${staticProjects} static, ${npmProjects} npm)`));
|
|
1777
2253
|
if (npmProjects > 0) {
|
|
1778
|
-
console.log(
|
|
2254
|
+
console.log(chalk15.dim(" Running: ") + chalk15.green(`${runningCount}/${npmProjects} npm apps online`));
|
|
1779
2255
|
}
|
|
1780
2256
|
} else {
|
|
1781
|
-
console.log(
|
|
1782
|
-
console.log(
|
|
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
|
|
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(
|
|
2325
|
+
console.log(chalk16.blue(`
|
|
1850
2326
|
Checking ${hostname}...
|
|
1851
2327
|
`));
|
|
1852
|
-
console.log(
|
|
2328
|
+
console.log(chalk16.bold("DNS Resolution:"));
|
|
1853
2329
|
const dns = await checkDns(hostname);
|
|
1854
2330
|
if (!dns.resolved) {
|
|
1855
|
-
console.log(
|
|
2331
|
+
console.log(chalk16.red(" \u2717 DNS not resolving"));
|
|
1856
2332
|
if (isLocal) {
|
|
1857
|
-
console.log(
|
|
1858
|
-
console.log(
|
|
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(
|
|
1861
|
-
console.log(
|
|
1862
|
-
console.log(
|
|
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(
|
|
2344
|
+
console.log(chalk16.green(" \u2713 CNAME: ") + dns.cname.join(", "));
|
|
1869
2345
|
if (dns.isCloudflare) {
|
|
1870
|
-
console.log(
|
|
2346
|
+
console.log(chalk16.green(" \u2713 Points to Cloudflare Tunnel"));
|
|
1871
2347
|
}
|
|
1872
2348
|
}
|
|
1873
2349
|
if (dns.ipv4.length > 0) {
|
|
1874
|
-
console.log(
|
|
2350
|
+
console.log(chalk16.green(" \u2713 A (IPv4): ") + dns.ipv4.join(", "));
|
|
1875
2351
|
}
|
|
1876
2352
|
if (dns.ipv6.length > 0) {
|
|
1877
|
-
console.log(
|
|
2353
|
+
console.log(chalk16.green(" \u2713 AAAA (IPv6): ") + dns.ipv6.join(", "));
|
|
1878
2354
|
}
|
|
1879
2355
|
console.log("");
|
|
1880
|
-
console.log(
|
|
2356
|
+
console.log(chalk16.bold("HTTP Check:"));
|
|
1881
2357
|
const http = await checkHttp(hostname, basePath);
|
|
1882
2358
|
if (!http.reachable) {
|
|
1883
|
-
console.log(
|
|
2359
|
+
console.log(chalk16.red(" \u2717 Not reachable"));
|
|
1884
2360
|
const err = http.error || "";
|
|
1885
2361
|
if (err.includes("ECONNREFUSED")) {
|
|
1886
|
-
console.log(
|
|
1887
|
-
console.log(
|
|
1888
|
-
console.log(
|
|
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(
|
|
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(
|
|
1894
|
-
console.log(
|
|
1895
|
-
console.log(
|
|
1896
|
-
console.log(
|
|
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(
|
|
1899
|
-
console.log(
|
|
2374
|
+
console.log(chalk16.dim(" Connection timed out"));
|
|
2375
|
+
console.log(chalk16.yellow("\n Check:"));
|
|
1900
2376
|
if (isLocal) {
|
|
1901
|
-
console.log(
|
|
2377
|
+
console.log(chalk16.dim(" - Is nginx running on port 8080?"));
|
|
1902
2378
|
} else {
|
|
1903
|
-
console.log(
|
|
1904
|
-
console.log(
|
|
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(
|
|
1908
|
-
console.log(
|
|
1909
|
-
console.log(
|
|
1910
|
-
console.log(
|
|
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(
|
|
1913
|
-
console.log(
|
|
2388
|
+
console.log(chalk16.dim(` Error: ${err}`));
|
|
2389
|
+
console.log(chalk16.yellow("\n Possible issues:"));
|
|
1914
2390
|
if (!isLocal) {
|
|
1915
|
-
console.log(
|
|
2391
|
+
console.log(chalk16.dim(" - Cloudflare tunnel not running"));
|
|
1916
2392
|
}
|
|
1917
|
-
console.log(
|
|
2393
|
+
console.log(chalk16.dim(" - Nginx not running or misconfigured"));
|
|
1918
2394
|
if (project?.type === "npm") {
|
|
1919
|
-
console.log(
|
|
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 ?
|
|
2401
|
+
const statusColor = http.statusCode < 400 ? chalk16.green : chalk16.red;
|
|
1926
2402
|
console.log(statusColor(` \u2713 Status: ${http.statusCode}`));
|
|
1927
|
-
console.log(
|
|
2403
|
+
console.log(chalk16.dim(` Response time: ${http.responseTime}ms`));
|
|
1928
2404
|
if (http.redirectUrl) {
|
|
1929
|
-
console.log(
|
|
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(
|
|
2409
|
+
console.log(chalk16.green("\u2713 All checks passed! Site is accessible."));
|
|
1934
2410
|
} else if (dns.resolved && http.reachable) {
|
|
1935
|
-
console.log(
|
|
2411
|
+
console.log(chalk16.yellow("! Site is reachable but returned an error status."));
|
|
1936
2412
|
} else {
|
|
1937
|
-
console.log(
|
|
2413
|
+
console.log(chalk16.red("\u2717 Some checks failed. See details above."));
|
|
1938
2414
|
}
|
|
1939
2415
|
if (project) {
|
|
1940
|
-
console.log(
|
|
2416
|
+
console.log(chalk16.dim(`
|
|
1941
2417
|
Project: ${project.name} (${project.type})`));
|
|
1942
2418
|
if (project.type === "npm") {
|
|
1943
|
-
console.log(
|
|
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
|
|
2426
|
+
import chalk17 from "chalk";
|
|
1951
2427
|
import inquirer3 from "inquirer";
|
|
1952
2428
|
import { execSync as execSync2 } from "child_process";
|
|
1953
|
-
import { existsSync as
|
|
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 (
|
|
1965
|
-
const osRelease =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
2015
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
2035
|
-
console.log(
|
|
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(
|
|
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(
|
|
2078
|
-
console.log(
|
|
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(
|
|
2558
|
+
console.log(chalk17.bold.cyan("\nBindler Setup\n"));
|
|
2083
2559
|
const os = detectOs();
|
|
2084
|
-
console.log(
|
|
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(
|
|
2088
|
-
console.log(
|
|
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(
|
|
2091
|
-
console.log(
|
|
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(
|
|
2586
|
+
console.log(chalk17.green("\u2713 All dependencies are already installed!\n"));
|
|
2111
2587
|
} else {
|
|
2112
|
-
console.log(
|
|
2588
|
+
console.log(chalk17.yellow("Missing dependencies:\n"));
|
|
2113
2589
|
for (const dep of missing) {
|
|
2114
|
-
console.log(
|
|
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(
|
|
2614
|
+
console.log(chalk17.bold("\n\nInstallation Summary:\n"));
|
|
2139
2615
|
for (const result of results) {
|
|
2140
2616
|
if (result.success) {
|
|
2141
|
-
console.log(
|
|
2617
|
+
console.log(chalk17.green(` \u2713 ${result.name} installed`));
|
|
2142
2618
|
} else {
|
|
2143
|
-
console.log(
|
|
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(
|
|
2186
|
-
console.log(
|
|
2187
|
-
console.log(
|
|
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(
|
|
2665
|
+
console.log(chalk17.dim(`SSL: enabled (${config.defaults.sslEmail})`));
|
|
2190
2666
|
}
|
|
2191
|
-
console.log(
|
|
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
|
|
2671
|
+
import chalk18 from "chalk";
|
|
2196
2672
|
import inquirer4 from "inquirer";
|
|
2197
2673
|
async function initCommand() {
|
|
2198
|
-
console.log(
|
|
2674
|
+
console.log(chalk18.bold.cyan(`
|
|
2199
2675
|
_ _ _ _
|
|
2200
2676
|
| |_|_|___ _| | |___ ___
|
|
2201
2677
|
| . | | | . | | -_| _|
|
|
2202
2678
|
|___|_|_|_|___|_|___|_|
|
|
2203
2679
|
`));
|
|
2204
|
-
console.log(
|
|
2680
|
+
console.log(chalk18.white(" Welcome to bindler!\n"));
|
|
2205
2681
|
if (configExists()) {
|
|
2206
2682
|
const config2 = readConfig();
|
|
2207
|
-
console.log(
|
|
2208
|
-
console.log(
|
|
2209
|
-
console.log(
|
|
2210
|
-
console.log(
|
|
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(
|
|
2697
|
+
console.log(chalk18.dim("\nRun `bindler new` to add a project."));
|
|
2222
2698
|
return;
|
|
2223
2699
|
}
|
|
2224
2700
|
}
|
|
2225
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
2255
|
-
console.log(deps.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 ?
|
|
2733
|
+
console.log(deps.cloudflared ? chalk18.green(" \u2713 cloudflared") : chalk18.red(" \u2717 cloudflared"));
|
|
2258
2734
|
} else if (mode === "direct") {
|
|
2259
|
-
console.log(
|
|
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(
|
|
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(
|
|
2329
|
-
console.log(
|
|
2330
|
-
console.log(
|
|
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(
|
|
2808
|
+
console.log(chalk18.dim(" Tunnel: ") + chalk18.white(tunnelName));
|
|
2333
2809
|
}
|
|
2334
2810
|
if (sslEmail) {
|
|
2335
|
-
console.log(
|
|
2811
|
+
console.log(chalk18.dim(" SSL: ") + chalk18.white(sslEmail));
|
|
2336
2812
|
}
|
|
2337
|
-
console.log(
|
|
2338
|
-
console.log(
|
|
2339
|
-
console.log(
|
|
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(
|
|
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
|
|
2823
|
+
import chalk19 from "chalk";
|
|
2348
2824
|
import { execSync as execSync3 } from "child_process";
|
|
2349
|
-
import { existsSync as
|
|
2350
|
-
import { join as
|
|
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(
|
|
2363
|
-
console.log(
|
|
2364
|
-
console.log(
|
|
2365
|
-
console.log(
|
|
2366
|
-
console.log(
|
|
2367
|
-
console.log(
|
|
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(
|
|
2373
|
-
console.log(
|
|
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(
|
|
2852
|
+
console.log(chalk19.dim(` - ${p.name}`));
|
|
2377
2853
|
}
|
|
2378
2854
|
process.exit(1);
|
|
2379
2855
|
}
|
|
2380
|
-
if (!
|
|
2381
|
-
console.log(
|
|
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(
|
|
2860
|
+
console.log(chalk19.blue(`
|
|
2385
2861
|
Deploying ${project.name}...
|
|
2386
2862
|
`));
|
|
2387
2863
|
if (!options.skipPull) {
|
|
2388
|
-
const isGitRepo =
|
|
2864
|
+
const isGitRepo = existsSync11(join7(project.path, ".git"));
|
|
2389
2865
|
if (isGitRepo) {
|
|
2390
|
-
console.log(
|
|
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(
|
|
2870
|
+
console.log(chalk19.green(" \u2713 Already up to date"));
|
|
2395
2871
|
} else {
|
|
2396
|
-
console.log(
|
|
2872
|
+
console.log(chalk19.green(" \u2713 Pulled latest changes"));
|
|
2397
2873
|
if (result.output) {
|
|
2398
|
-
console.log(
|
|
2874
|
+
console.log(chalk19.dim(` ${result.output.split("\n")[0]}`));
|
|
2399
2875
|
}
|
|
2400
2876
|
}
|
|
2401
2877
|
} else {
|
|
2402
|
-
console.log(
|
|
2403
|
-
console.log(
|
|
2878
|
+
console.log(chalk19.yellow(" ! Git pull failed"));
|
|
2879
|
+
console.log(chalk19.dim(` ${result.output}`));
|
|
2404
2880
|
}
|
|
2405
2881
|
} else {
|
|
2406
|
-
console.log(
|
|
2882
|
+
console.log(chalk19.dim(" - Not a git repository, skipping pull"));
|
|
2407
2883
|
}
|
|
2408
2884
|
} else {
|
|
2409
|
-
console.log(
|
|
2885
|
+
console.log(chalk19.dim(" - Skipped git pull (--skip-pull)"));
|
|
2410
2886
|
}
|
|
2411
2887
|
if (project.type === "npm" && !options.skipInstall) {
|
|
2412
|
-
const hasPackageJson =
|
|
2888
|
+
const hasPackageJson = existsSync11(join7(project.path, "package.json"));
|
|
2413
2889
|
if (hasPackageJson) {
|
|
2414
|
-
console.log(
|
|
2890
|
+
console.log(chalk19.dim("Installing dependencies..."));
|
|
2415
2891
|
const result = runInDir("npm install", project.path);
|
|
2416
2892
|
if (result.success) {
|
|
2417
|
-
console.log(
|
|
2893
|
+
console.log(chalk19.green(" \u2713 Dependencies installed"));
|
|
2418
2894
|
} else {
|
|
2419
|
-
console.log(
|
|
2420
|
-
console.log(
|
|
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(
|
|
2900
|
+
console.log(chalk19.dim(" - Skipped npm install (--skip-install)"));
|
|
2425
2901
|
}
|
|
2426
2902
|
if (project.type === "npm" && !options.skipRestart) {
|
|
2427
|
-
console.log(
|
|
2903
|
+
console.log(chalk19.dim("Restarting application..."));
|
|
2428
2904
|
const result = restartProject(name);
|
|
2429
2905
|
if (result.success) {
|
|
2430
|
-
console.log(
|
|
2906
|
+
console.log(chalk19.green(" \u2713 Application restarted"));
|
|
2431
2907
|
} else {
|
|
2432
|
-
console.log(
|
|
2433
|
-
console.log(
|
|
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(
|
|
2912
|
+
console.log(chalk19.dim(" - Static project, no restart needed"));
|
|
2437
2913
|
} else if (options.skipRestart) {
|
|
2438
|
-
console.log(
|
|
2914
|
+
console.log(chalk19.dim(" - Skipped restart (--skip-restart)"));
|
|
2439
2915
|
}
|
|
2440
|
-
console.log(
|
|
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
|
|
2447
|
-
import { existsSync as
|
|
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
|
|
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(
|
|
2929
|
+
const defaultPath = resolve(homedir3(), `bindler-backup-${timestamp}.json`);
|
|
2454
2930
|
const outputPath = options.output || defaultPath;
|
|
2455
2931
|
const dir = dirname3(outputPath);
|
|
2456
|
-
if (!
|
|
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
|
-
|
|
2465
|
-
console.log(
|
|
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(
|
|
2469
|
-
console.log(
|
|
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(
|
|
2472
|
-
console.log(
|
|
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(
|
|
2478
|
-
console.log(
|
|
2479
|
-
console.log(
|
|
2480
|
-
console.log(
|
|
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 (!
|
|
2485
|
-
console.log(
|
|
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 =
|
|
2966
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
2491
2967
|
backup = JSON.parse(content);
|
|
2492
2968
|
} catch (error) {
|
|
2493
|
-
console.log(
|
|
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(
|
|
2973
|
+
console.log(chalk20.red("Invalid backup format. Missing config data."));
|
|
2498
2974
|
process.exit(1);
|
|
2499
2975
|
}
|
|
2500
|
-
console.log(
|
|
2501
|
-
console.log(
|
|
2502
|
-
console.log(
|
|
2503
|
-
console.log(
|
|
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(
|
|
2508
|
-
console.log(
|
|
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(
|
|
2513
|
-
console.log(
|
|
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
|
|
2994
|
+
import chalk21 from "chalk";
|
|
2519
2995
|
async function sslCommand(hostname, options) {
|
|
2520
2996
|
if (!hostname) {
|
|
2521
|
-
console.log(
|
|
2522
|
-
console.log(
|
|
2523
|
-
console.log(
|
|
2524
|
-
console.log(
|
|
2525
|
-
console.log(
|
|
2526
|
-
console.log(
|
|
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(
|
|
2534
|
-
console.log(
|
|
2535
|
-
console.log(
|
|
2536
|
-
console.log(
|
|
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(
|
|
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(
|
|
3023
|
+
console.log(chalk21.yellow("Using staging server (test certificate)\n"));
|
|
2548
3024
|
}
|
|
2549
|
-
console.log(
|
|
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(
|
|
3029
|
+
console.log(chalk21.green(`
|
|
2554
3030
|
\u2713 SSL certificate installed for ${targetHostname}
|
|
2555
3031
|
`));
|
|
2556
|
-
console.log(
|
|
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(
|
|
3034
|
+
console.log(chalk21.green(`
|
|
2559
3035
|
\u2713 Certificate already exists and is valid
|
|
2560
3036
|
`));
|
|
2561
|
-
console.log(
|
|
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(
|
|
2564
|
-
console.log(
|
|
2565
|
-
console.log(
|
|
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(
|
|
2568
|
-
console.log(
|
|
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(
|
|
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(
|
|
3052
|
+
console.log(chalk21.dim(` ${line.trim()}`));
|
|
2577
3053
|
}
|
|
2578
3054
|
}
|
|
2579
|
-
console.log(
|
|
2580
|
-
console.log(
|
|
2581
|
-
console.log(
|
|
2582
|
-
console.log(
|
|
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
|
|
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(
|
|
2593
|
-
console.log(
|
|
2594
|
-
console.log(
|
|
2595
|
-
console.log(
|
|
2596
|
-
console.log(
|
|
2597
|
-
console.log(
|
|
2598
|
-
console.log(
|
|
2599
|
-
console.log(
|
|
2600
|
-
console.log(
|
|
2601
|
-
console.log(
|
|
2602
|
-
console.log(
|
|
2603
|
-
console.log(
|
|
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(
|
|
2608
|
-
console.log(
|
|
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(
|
|
3091
|
+
console.log(chalk22.blue("\nTunnel Status\n"));
|
|
2616
3092
|
const info = getTunnelInfo(tunnelName);
|
|
2617
3093
|
if (!info.exists) {
|
|
2618
|
-
console.log(
|
|
2619
|
-
console.log(
|
|
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(
|
|
2623
|
-
console.log(
|
|
2624
|
-
console.log(
|
|
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(
|
|
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(
|
|
2637
|
-
console.log(
|
|
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(
|
|
3117
|
+
console.log(chalk22.yellow(`Tunnel "${tunnelName}" is already running.`));
|
|
2642
3118
|
process.exit(0);
|
|
2643
3119
|
}
|
|
2644
|
-
console.log(
|
|
3120
|
+
console.log(chalk22.blue(`Starting tunnel "${tunnelName}"...
|
|
2645
3121
|
`));
|
|
2646
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3141
|
+
console.log(chalk22.green(`\u2713 Tunnel "${tunnelName}" stopped`));
|
|
2666
3142
|
} else {
|
|
2667
|
-
console.log(
|
|
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(
|
|
2674
|
-
console.log(
|
|
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(
|
|
3153
|
+
console.log(chalk22.green("\n\u2713 Authentication successful!"));
|
|
2678
3154
|
} catch {
|
|
2679
|
-
console.log(
|
|
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(
|
|
2689
|
-
console.log(
|
|
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(
|
|
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(
|
|
3172
|
+
console.log(chalk22.green(`
|
|
2697
3173
|
\u2713 Tunnel "${tunnelName}" created!`));
|
|
2698
|
-
console.log(
|
|
2699
|
-
console.log(
|
|
2700
|
-
console.log(
|
|
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(
|
|
2703
|
-
console.log(
|
|
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(
|
|
3185
|
+
console.log(chalk22.blue("\nCloudflare Tunnels\n"));
|
|
2710
3186
|
const tunnels = listTunnels();
|
|
2711
3187
|
if (tunnels.length === 0) {
|
|
2712
|
-
console.log(
|
|
2713
|
-
console.log(
|
|
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 ?
|
|
2718
|
-
console.log(prefix +
|
|
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(
|
|
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(
|
|
2728
|
-
console.log(
|
|
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
|
|
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(
|
|
2739
|
-
console.log(
|
|
2740
|
-
console.log(
|
|
2741
|
-
console.log(
|
|
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(
|
|
2747
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
2784
|
-
console.log(
|
|
2785
|
-
Open manually: ${
|
|
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
|
|
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(
|
|
2822
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3329
|
+
console.log(chalk24.green(" \u2713 ") + chalk24.white(project.name) + chalk24.dim(` (${result.time}ms)`));
|
|
2854
3330
|
} else if (result.status) {
|
|
2855
|
-
console.log(
|
|
3331
|
+
console.log(chalk24.yellow(" ! ") + chalk24.white(project.name) + chalk24.dim(` (${result.status})`));
|
|
2856
3332
|
} else {
|
|
2857
|
-
console.log(
|
|
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(
|
|
3340
|
+
console.log(chalk24.green(`\u2713 All ${healthy} project(s) healthy`));
|
|
2865
3341
|
} else if (healthy === 0) {
|
|
2866
|
-
console.log(
|
|
3342
|
+
console.log(chalk24.red(`\u2717 All ${unhealthy} project(s) down`));
|
|
2867
3343
|
} else {
|
|
2868
|
-
console.log(
|
|
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
|
|
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(
|
|
2898
|
-
console.log(
|
|
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(
|
|
3377
|
+
console.log(chalk25.blue("\nProject Stats\n"));
|
|
2902
3378
|
console.log(
|
|
2903
|
-
|
|
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(
|
|
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
|
-
" " +
|
|
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" ?
|
|
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
|
-
" " +
|
|
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(
|
|
3407
|
+
console.log(chalk25.dim(" " + "-".repeat(70)));
|
|
2932
3408
|
console.log(
|
|
2933
|
-
" " +
|
|
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
|
|
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(
|
|
3073
|
-
console.log(
|
|
3074
|
-
console.log(
|
|
3075
|
-
console.log(
|
|
3076
|
-
console.log(
|
|
3077
|
-
console.log(
|
|
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(
|
|
3092
|
-
console.log(
|
|
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
|
|
3099
|
-
import { existsSync as
|
|
3100
|
-
import { join as
|
|
3101
|
-
import { homedir as
|
|
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 =
|
|
3104
|
-
var CACHE_FILE =
|
|
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 (
|
|
3108
|
-
return JSON.parse(
|
|
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 (!
|
|
3844
|
+
if (!existsSync14(CACHE_DIR)) {
|
|
3117
3845
|
mkdirSync5(CACHE_DIR, { recursive: true });
|
|
3118
3846
|
}
|
|
3119
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
3166
|
-
console.log(
|
|
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.
|
|
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(
|
|
3192
|
-
console.log(
|
|
3193
|
-
console.log(
|
|
3194
|
-
console.log(
|
|
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(
|
|
3202
|
-
console.log(
|
|
3203
|
-
console.log(
|
|
3204
|
-
console.log(
|
|
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(
|
|
3212
|
-
console.log(
|
|
3213
|
-
console.log(
|
|
3214
|
-
console.log(
|
|
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(
|
|
3222
|
-
console.log(
|
|
3223
|
-
console.log(
|
|
3224
|
-
console.log(
|
|
3225
|
-
console.log(
|
|
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(
|
|
3233
|
-
console.log(
|
|
3234
|
-
console.log(
|
|
3235
|
-
console.log(
|
|
3236
|
-
console.log(
|
|
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(
|
|
3244
|
-
console.log(
|
|
3245
|
-
console.log(
|
|
3246
|
-
console.log(
|
|
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(
|
|
3254
|
-
console.log(
|
|
3255
|
-
console.log(
|
|
3256
|
-
console.log(
|
|
3257
|
-
console.log(
|
|
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(
|
|
3277
|
-
console.log(
|
|
3278
|
-
console.log(
|
|
3279
|
-
console.log(
|
|
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
|