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