bindler 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1052 -45
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk27 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/new.ts
|
|
8
8
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -162,28 +162,28 @@ function execCommandSafe(command, options) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
function spawnInteractive(command, args, options) {
|
|
165
|
-
return new Promise((
|
|
165
|
+
return new Promise((resolve2) => {
|
|
166
166
|
const child = spawn(command, args, {
|
|
167
167
|
stdio: "inherit",
|
|
168
168
|
...options
|
|
169
169
|
});
|
|
170
170
|
child.on("close", (code) => {
|
|
171
|
-
|
|
171
|
+
resolve2(code);
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
function isPortListening(port, host = "127.0.0.1") {
|
|
176
|
-
return new Promise((
|
|
176
|
+
return new Promise((resolve2) => {
|
|
177
177
|
const socket = createConnection({ port, host }, () => {
|
|
178
178
|
socket.destroy();
|
|
179
|
-
|
|
179
|
+
resolve2(true);
|
|
180
180
|
});
|
|
181
181
|
socket.on("error", () => {
|
|
182
|
-
|
|
182
|
+
resolve2(false);
|
|
183
183
|
});
|
|
184
184
|
socket.setTimeout(1e3, () => {
|
|
185
185
|
socket.destroy();
|
|
186
|
-
|
|
186
|
+
resolve2(false);
|
|
187
187
|
});
|
|
188
188
|
});
|
|
189
189
|
}
|
|
@@ -1758,7 +1758,7 @@ async function infoCommand() {
|
|
|
1758
1758
|
`));
|
|
1759
1759
|
console.log(chalk14.white(" Manage multiple projects behind Cloudflare Tunnel"));
|
|
1760
1760
|
console.log(chalk14.white(" with Nginx and PM2\n"));
|
|
1761
|
-
console.log(chalk14.dim(" Version: ") + chalk14.white("1.
|
|
1761
|
+
console.log(chalk14.dim(" Version: ") + chalk14.white("1.1.1"));
|
|
1762
1762
|
console.log(chalk14.dim(" Author: ") + chalk14.white("alfaoz"));
|
|
1763
1763
|
console.log(chalk14.dim(" License: ") + chalk14.white("MIT"));
|
|
1764
1764
|
console.log(chalk14.dim(" GitHub: ") + chalk14.cyan("https://github.com/alfaoz/bindler"));
|
|
@@ -2191,14 +2191,991 @@ async function setupCommand(options = {}) {
|
|
|
2191
2191
|
console.log(chalk16.dim("\nRun `bindler new` to create your first project."));
|
|
2192
2192
|
}
|
|
2193
2193
|
|
|
2194
|
+
// src/commands/init.ts
|
|
2195
|
+
import chalk17 from "chalk";
|
|
2196
|
+
import inquirer4 from "inquirer";
|
|
2197
|
+
async function initCommand() {
|
|
2198
|
+
console.log(chalk17.bold.cyan(`
|
|
2199
|
+
_ _ _ _
|
|
2200
|
+
| |_|_|___ _| | |___ ___
|
|
2201
|
+
| . | | | . | | -_| _|
|
|
2202
|
+
|___|_|_|_|___|_|___|_|
|
|
2203
|
+
`));
|
|
2204
|
+
console.log(chalk17.white(" Welcome to bindler!\n"));
|
|
2205
|
+
if (configExists()) {
|
|
2206
|
+
const config2 = readConfig();
|
|
2207
|
+
console.log(chalk17.yellow("Bindler is already initialized."));
|
|
2208
|
+
console.log(chalk17.dim(` Config: ~/.config/bindler/config.json`));
|
|
2209
|
+
console.log(chalk17.dim(` Mode: ${config2.defaults.mode || "tunnel"}`));
|
|
2210
|
+
console.log(chalk17.dim(` Projects: ${config2.projects.length}`));
|
|
2211
|
+
console.log("");
|
|
2212
|
+
const { reinit } = await inquirer4.prompt([
|
|
2213
|
+
{
|
|
2214
|
+
type: "confirm",
|
|
2215
|
+
name: "reinit",
|
|
2216
|
+
message: "Reconfigure bindler?",
|
|
2217
|
+
default: false
|
|
2218
|
+
}
|
|
2219
|
+
]);
|
|
2220
|
+
if (!reinit) {
|
|
2221
|
+
console.log(chalk17.dim("\nRun `bindler new` to add a project."));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
console.log(chalk17.bold("\n1. Choose your setup:\n"));
|
|
2226
|
+
const { mode } = await inquirer4.prompt([
|
|
2227
|
+
{
|
|
2228
|
+
type: "list",
|
|
2229
|
+
name: "mode",
|
|
2230
|
+
message: "How will you expose your projects?",
|
|
2231
|
+
choices: [
|
|
2232
|
+
{
|
|
2233
|
+
name: "Cloudflare Tunnel (recommended for home servers)",
|
|
2234
|
+
value: "tunnel"
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
name: "Direct (VPS with public IP, port 80/443)",
|
|
2238
|
+
value: "direct"
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
name: "Local only (development, no internet access)",
|
|
2242
|
+
value: "local"
|
|
2243
|
+
}
|
|
2244
|
+
]
|
|
2245
|
+
}
|
|
2246
|
+
]);
|
|
2247
|
+
console.log(chalk17.bold("\n2. Checking dependencies...\n"));
|
|
2248
|
+
const deps = {
|
|
2249
|
+
nginx: isNginxInstalled(),
|
|
2250
|
+
pm2: isPm2Installed(),
|
|
2251
|
+
cloudflared: isCloudflaredInstalled()
|
|
2252
|
+
};
|
|
2253
|
+
const nginxRunning = deps.nginx && isNginxRunning();
|
|
2254
|
+
console.log(deps.nginx ? chalk17.green(" \u2713 nginx") : chalk17.red(" \u2717 nginx"));
|
|
2255
|
+
console.log(deps.pm2 ? chalk17.green(" \u2713 pm2") : chalk17.red(" \u2717 pm2"));
|
|
2256
|
+
if (mode === "tunnel") {
|
|
2257
|
+
console.log(deps.cloudflared ? chalk17.green(" \u2713 cloudflared") : chalk17.red(" \u2717 cloudflared"));
|
|
2258
|
+
} else if (mode === "direct") {
|
|
2259
|
+
console.log(chalk17.dim(" - cloudflared (not needed for direct mode)"));
|
|
2260
|
+
}
|
|
2261
|
+
const missingDeps = !deps.nginx || !deps.pm2 || mode === "tunnel" && !deps.cloudflared;
|
|
2262
|
+
if (missingDeps) {
|
|
2263
|
+
console.log("");
|
|
2264
|
+
const { install } = await inquirer4.prompt([
|
|
2265
|
+
{
|
|
2266
|
+
type: "confirm",
|
|
2267
|
+
name: "install",
|
|
2268
|
+
message: "Install missing dependencies?",
|
|
2269
|
+
default: true
|
|
2270
|
+
}
|
|
2271
|
+
]);
|
|
2272
|
+
if (install) {
|
|
2273
|
+
await setupCommand({ direct: mode === "direct" });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
console.log(chalk17.bold("\n3. Configuration:\n"));
|
|
2277
|
+
let tunnelName = "homelab";
|
|
2278
|
+
let sslEmail = "";
|
|
2279
|
+
if (mode === "tunnel") {
|
|
2280
|
+
const answers = await inquirer4.prompt([
|
|
2281
|
+
{
|
|
2282
|
+
type: "input",
|
|
2283
|
+
name: "tunnelName",
|
|
2284
|
+
message: "Cloudflare tunnel name:",
|
|
2285
|
+
default: "homelab"
|
|
2286
|
+
}
|
|
2287
|
+
]);
|
|
2288
|
+
tunnelName = answers.tunnelName;
|
|
2289
|
+
}
|
|
2290
|
+
if (mode === "direct") {
|
|
2291
|
+
const answers = await inquirer4.prompt([
|
|
2292
|
+
{
|
|
2293
|
+
type: "confirm",
|
|
2294
|
+
name: "enableSsl",
|
|
2295
|
+
message: "Enable SSL with Let's Encrypt?",
|
|
2296
|
+
default: true
|
|
2297
|
+
}
|
|
2298
|
+
]);
|
|
2299
|
+
if (answers.enableSsl) {
|
|
2300
|
+
const emailAnswer = await inquirer4.prompt([
|
|
2301
|
+
{
|
|
2302
|
+
type: "input",
|
|
2303
|
+
name: "email",
|
|
2304
|
+
message: "Email for SSL certificates:",
|
|
2305
|
+
validate: (input) => input.includes("@") || "Enter a valid email"
|
|
2306
|
+
}
|
|
2307
|
+
]);
|
|
2308
|
+
sslEmail = emailAnswer.email;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const config = configExists() ? readConfig() : initConfig();
|
|
2312
|
+
config.defaults.mode = mode === "local" ? "tunnel" : mode;
|
|
2313
|
+
config.defaults.tunnelName = tunnelName;
|
|
2314
|
+
config.defaults.applyCloudflareDnsRoutes = mode === "tunnel";
|
|
2315
|
+
if (mode === "direct") {
|
|
2316
|
+
config.defaults.nginxListen = "80";
|
|
2317
|
+
if (sslEmail) {
|
|
2318
|
+
config.defaults.sslEnabled = true;
|
|
2319
|
+
config.defaults.sslEmail = sslEmail;
|
|
2320
|
+
}
|
|
2321
|
+
} else if (mode === "local") {
|
|
2322
|
+
config.defaults.nginxListen = "127.0.0.1:8080";
|
|
2323
|
+
config.defaults.applyCloudflareDnsRoutes = false;
|
|
2324
|
+
} else {
|
|
2325
|
+
config.defaults.nginxListen = "127.0.0.1:8080";
|
|
2326
|
+
}
|
|
2327
|
+
writeConfig(config);
|
|
2328
|
+
console.log(chalk17.green("\n\u2713 Bindler initialized!\n"));
|
|
2329
|
+
console.log(chalk17.dim(" Mode: ") + chalk17.white(mode));
|
|
2330
|
+
console.log(chalk17.dim(" Listen: ") + chalk17.white(config.defaults.nginxListen));
|
|
2331
|
+
if (mode === "tunnel") {
|
|
2332
|
+
console.log(chalk17.dim(" Tunnel: ") + chalk17.white(tunnelName));
|
|
2333
|
+
}
|
|
2334
|
+
if (sslEmail) {
|
|
2335
|
+
console.log(chalk17.dim(" SSL: ") + chalk17.white(sslEmail));
|
|
2336
|
+
}
|
|
2337
|
+
console.log(chalk17.bold("\nNext steps:\n"));
|
|
2338
|
+
console.log(chalk17.dim(" 1. ") + chalk17.white("bindler new") + chalk17.dim(" # add your first project"));
|
|
2339
|
+
console.log(chalk17.dim(" 2. ") + chalk17.white("bindler apply") + chalk17.dim(" # apply nginx config"));
|
|
2340
|
+
if (mode === "tunnel") {
|
|
2341
|
+
console.log(chalk17.dim(" 3. ") + chalk17.white(`cloudflared tunnel run ${tunnelName}`) + chalk17.dim(" # start tunnel"));
|
|
2342
|
+
}
|
|
2343
|
+
console.log("");
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/commands/deploy.ts
|
|
2347
|
+
import chalk18 from "chalk";
|
|
2348
|
+
import { execSync as execSync3 } from "child_process";
|
|
2349
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2350
|
+
import { join as join5 } from "path";
|
|
2351
|
+
function runInDir(command, cwd) {
|
|
2352
|
+
try {
|
|
2353
|
+
const output = execSync3(command, { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
2354
|
+
return { success: true, output: output.trim() };
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
const err = error;
|
|
2357
|
+
return { success: false, output: err.stderr || err.message || "Unknown error" };
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async function deployCommand(name, options) {
|
|
2361
|
+
if (!name) {
|
|
2362
|
+
console.log(chalk18.red("Usage: bindler deploy <name>"));
|
|
2363
|
+
console.log(chalk18.dim("\nDeploys a project: git pull + npm install + restart"));
|
|
2364
|
+
console.log(chalk18.dim("\nExamples:"));
|
|
2365
|
+
console.log(chalk18.dim(" bindler deploy myapp"));
|
|
2366
|
+
console.log(chalk18.dim(" bindler deploy myapp --skip-install"));
|
|
2367
|
+
console.log(chalk18.dim(" bindler deploy myapp --skip-pull"));
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
const project = getProject(name);
|
|
2371
|
+
if (!project) {
|
|
2372
|
+
console.log(chalk18.red(`Project "${name}" not found.`));
|
|
2373
|
+
console.log(chalk18.dim("\nAvailable projects:"));
|
|
2374
|
+
const projects = listProjects();
|
|
2375
|
+
for (const p of projects) {
|
|
2376
|
+
console.log(chalk18.dim(` - ${p.name}`));
|
|
2377
|
+
}
|
|
2378
|
+
process.exit(1);
|
|
2379
|
+
}
|
|
2380
|
+
if (!existsSync7(project.path)) {
|
|
2381
|
+
console.log(chalk18.red(`Project path does not exist: ${project.path}`));
|
|
2382
|
+
process.exit(1);
|
|
2383
|
+
}
|
|
2384
|
+
console.log(chalk18.blue(`
|
|
2385
|
+
Deploying ${project.name}...
|
|
2386
|
+
`));
|
|
2387
|
+
if (!options.skipPull) {
|
|
2388
|
+
const isGitRepo = existsSync7(join5(project.path, ".git"));
|
|
2389
|
+
if (isGitRepo) {
|
|
2390
|
+
console.log(chalk18.dim("Pulling latest changes..."));
|
|
2391
|
+
const result = runInDir("git pull", project.path);
|
|
2392
|
+
if (result.success) {
|
|
2393
|
+
if (result.output.includes("Already up to date")) {
|
|
2394
|
+
console.log(chalk18.green(" \u2713 Already up to date"));
|
|
2395
|
+
} else {
|
|
2396
|
+
console.log(chalk18.green(" \u2713 Pulled latest changes"));
|
|
2397
|
+
if (result.output) {
|
|
2398
|
+
console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
} else {
|
|
2402
|
+
console.log(chalk18.yellow(" ! Git pull failed"));
|
|
2403
|
+
console.log(chalk18.dim(` ${result.output}`));
|
|
2404
|
+
}
|
|
2405
|
+
} else {
|
|
2406
|
+
console.log(chalk18.dim(" - Not a git repository, skipping pull"));
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
console.log(chalk18.dim(" - Skipped git pull (--skip-pull)"));
|
|
2410
|
+
}
|
|
2411
|
+
if (project.type === "npm" && !options.skipInstall) {
|
|
2412
|
+
const hasPackageJson = existsSync7(join5(project.path, "package.json"));
|
|
2413
|
+
if (hasPackageJson) {
|
|
2414
|
+
console.log(chalk18.dim("Installing dependencies..."));
|
|
2415
|
+
const result = runInDir("npm install", project.path);
|
|
2416
|
+
if (result.success) {
|
|
2417
|
+
console.log(chalk18.green(" \u2713 Dependencies installed"));
|
|
2418
|
+
} else {
|
|
2419
|
+
console.log(chalk18.yellow(" ! npm install failed"));
|
|
2420
|
+
console.log(chalk18.dim(` ${result.output.split("\n")[0]}`));
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
} else if (options.skipInstall) {
|
|
2424
|
+
console.log(chalk18.dim(" - Skipped npm install (--skip-install)"));
|
|
2425
|
+
}
|
|
2426
|
+
if (project.type === "npm" && !options.skipRestart) {
|
|
2427
|
+
console.log(chalk18.dim("Restarting application..."));
|
|
2428
|
+
const result = restartProject(name);
|
|
2429
|
+
if (result.success) {
|
|
2430
|
+
console.log(chalk18.green(" \u2713 Application restarted"));
|
|
2431
|
+
} else {
|
|
2432
|
+
console.log(chalk18.yellow(` ! Restart failed: ${result.error}`));
|
|
2433
|
+
console.log(chalk18.dim(` Try: bindler start ${name}`));
|
|
2434
|
+
}
|
|
2435
|
+
} else if (project.type === "static") {
|
|
2436
|
+
console.log(chalk18.dim(" - Static project, no restart needed"));
|
|
2437
|
+
} else if (options.skipRestart) {
|
|
2438
|
+
console.log(chalk18.dim(" - Skipped restart (--skip-restart)"));
|
|
2439
|
+
}
|
|
2440
|
+
console.log(chalk18.green(`
|
|
2441
|
+
\u2713 Deploy complete for ${project.name}
|
|
2442
|
+
`));
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/backup.ts
|
|
2446
|
+
import chalk19 from "chalk";
|
|
2447
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
2448
|
+
import { dirname as dirname3, resolve } from "path";
|
|
2449
|
+
import { homedir as homedir2 } from "os";
|
|
2450
|
+
async function backupCommand(options) {
|
|
2451
|
+
const config = readConfig();
|
|
2452
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
2453
|
+
const defaultPath = resolve(homedir2(), `bindler-backup-${timestamp}.json`);
|
|
2454
|
+
const outputPath = options.output || defaultPath;
|
|
2455
|
+
const dir = dirname3(outputPath);
|
|
2456
|
+
if (!existsSync8(dir)) {
|
|
2457
|
+
mkdirSync4(dir, { recursive: true });
|
|
2458
|
+
}
|
|
2459
|
+
const backup = {
|
|
2460
|
+
version: 1,
|
|
2461
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2462
|
+
config
|
|
2463
|
+
};
|
|
2464
|
+
writeFileSync4(outputPath, JSON.stringify(backup, null, 2) + "\n");
|
|
2465
|
+
console.log(chalk19.green(`
|
|
2466
|
+
\u2713 Backup saved to ${outputPath}
|
|
2467
|
+
`));
|
|
2468
|
+
console.log(chalk19.dim(` Projects: ${config.projects.length}`));
|
|
2469
|
+
console.log(chalk19.dim(` Mode: ${config.defaults.mode || "tunnel"}`));
|
|
2470
|
+
console.log("");
|
|
2471
|
+
console.log(chalk19.dim("Restore with:"));
|
|
2472
|
+
console.log(chalk19.cyan(` bindler restore ${outputPath}`));
|
|
2473
|
+
console.log("");
|
|
2474
|
+
}
|
|
2475
|
+
async function restoreCommand(file, options) {
|
|
2476
|
+
if (!file) {
|
|
2477
|
+
console.log(chalk19.red("Usage: bindler restore <file>"));
|
|
2478
|
+
console.log(chalk19.dim("\nExamples:"));
|
|
2479
|
+
console.log(chalk19.dim(" bindler restore ~/bindler-backup.json"));
|
|
2480
|
+
console.log(chalk19.dim(" bindler restore backup.json --force"));
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
const filePath = resolve(file);
|
|
2484
|
+
if (!existsSync8(filePath)) {
|
|
2485
|
+
console.log(chalk19.red(`File not found: ${filePath}`));
|
|
2486
|
+
process.exit(1);
|
|
2487
|
+
}
|
|
2488
|
+
let backup;
|
|
2489
|
+
try {
|
|
2490
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
2491
|
+
backup = JSON.parse(content);
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
console.log(chalk19.red("Invalid backup file. Must be valid JSON."));
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
if (!backup.config || !backup.config.defaults || !Array.isArray(backup.config.projects)) {
|
|
2497
|
+
console.log(chalk19.red("Invalid backup format. Missing config data."));
|
|
2498
|
+
process.exit(1);
|
|
2499
|
+
}
|
|
2500
|
+
console.log(chalk19.blue("\nBackup info:\n"));
|
|
2501
|
+
console.log(chalk19.dim(" Exported: ") + chalk19.white(backup.exportedAt || "unknown"));
|
|
2502
|
+
console.log(chalk19.dim(" Projects: ") + chalk19.white(backup.config.projects.length));
|
|
2503
|
+
console.log(chalk19.dim(" Mode: ") + chalk19.white(backup.config.defaults.mode || "tunnel"));
|
|
2504
|
+
console.log("");
|
|
2505
|
+
const currentConfig = readConfig();
|
|
2506
|
+
if (currentConfig.projects.length > 0 && !options.force) {
|
|
2507
|
+
console.log(chalk19.yellow(`Warning: You have ${currentConfig.projects.length} existing project(s).`));
|
|
2508
|
+
console.log(chalk19.dim("Use --force to overwrite.\n"));
|
|
2509
|
+
process.exit(1);
|
|
2510
|
+
}
|
|
2511
|
+
writeConfig(backup.config);
|
|
2512
|
+
console.log(chalk19.green("\u2713 Config restored!\n"));
|
|
2513
|
+
console.log(chalk19.dim("Run `bindler apply` to apply nginx configuration."));
|
|
2514
|
+
console.log("");
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/commands/ssl.ts
|
|
2518
|
+
import chalk20 from "chalk";
|
|
2519
|
+
async function sslCommand(hostname, options) {
|
|
2520
|
+
if (!hostname) {
|
|
2521
|
+
console.log(chalk20.red("Usage: bindler ssl <hostname>"));
|
|
2522
|
+
console.log(chalk20.dim("\nRequest SSL certificate for a hostname"));
|
|
2523
|
+
console.log(chalk20.dim("\nExamples:"));
|
|
2524
|
+
console.log(chalk20.dim(" bindler ssl myapp.example.com"));
|
|
2525
|
+
console.log(chalk20.dim(" bindler ssl myapp # uses project hostname"));
|
|
2526
|
+
console.log(chalk20.dim(" bindler ssl myapp --email me@example.com"));
|
|
2527
|
+
process.exit(1);
|
|
2528
|
+
}
|
|
2529
|
+
const project = getProject(hostname);
|
|
2530
|
+
const targetHostname = project ? project.hostname : hostname;
|
|
2531
|
+
const certbotCheck = execCommandSafe("which certbot");
|
|
2532
|
+
if (!certbotCheck.success) {
|
|
2533
|
+
console.log(chalk20.red("certbot is not installed."));
|
|
2534
|
+
console.log(chalk20.dim("\nInstall with:"));
|
|
2535
|
+
console.log(chalk20.dim(" macOS: brew install certbot"));
|
|
2536
|
+
console.log(chalk20.dim(" Linux: apt install certbot python3-certbot-nginx"));
|
|
2537
|
+
process.exit(1);
|
|
2538
|
+
}
|
|
2539
|
+
const defaults = getDefaults();
|
|
2540
|
+
const email = options.email || defaults.sslEmail || `admin@${targetHostname.split(".").slice(-2).join(".")}`;
|
|
2541
|
+
console.log(chalk20.blue(`
|
|
2542
|
+
Requesting SSL certificate for ${targetHostname}...
|
|
2543
|
+
`));
|
|
2544
|
+
let cmd = `sudo certbot --nginx -d ${targetHostname} --non-interactive --agree-tos --email ${email}`;
|
|
2545
|
+
if (options.staging) {
|
|
2546
|
+
cmd += " --staging";
|
|
2547
|
+
console.log(chalk20.yellow("Using staging server (test certificate)\n"));
|
|
2548
|
+
}
|
|
2549
|
+
console.log(chalk20.dim(`Running: ${cmd}
|
|
2550
|
+
`));
|
|
2551
|
+
const result = execCommandSafe(cmd + " 2>&1");
|
|
2552
|
+
if (result.success) {
|
|
2553
|
+
console.log(chalk20.green(`
|
|
2554
|
+
\u2713 SSL certificate installed for ${targetHostname}
|
|
2555
|
+
`));
|
|
2556
|
+
console.log(chalk20.dim("Certificate will auto-renew via certbot timer."));
|
|
2557
|
+
} else if (result.output?.includes("Certificate not yet due for renewal")) {
|
|
2558
|
+
console.log(chalk20.green(`
|
|
2559
|
+
\u2713 Certificate already exists and is valid
|
|
2560
|
+
`));
|
|
2561
|
+
console.log(chalk20.dim("Use --force with certbot to renew early if needed."));
|
|
2562
|
+
} else if (result.output?.includes("too many certificates")) {
|
|
2563
|
+
console.log(chalk20.red("\n\u2717 Rate limit reached"));
|
|
2564
|
+
console.log(chalk20.dim("Let's Encrypt limits certificates per domain."));
|
|
2565
|
+
console.log(chalk20.dim("Try again later or use --staging for testing."));
|
|
2566
|
+
} else if (result.output?.includes("Could not bind")) {
|
|
2567
|
+
console.log(chalk20.red("\n\u2717 Port 80 is in use"));
|
|
2568
|
+
console.log(chalk20.dim("Stop nginx temporarily or use webroot method."));
|
|
2569
|
+
} else {
|
|
2570
|
+
console.log(chalk20.red("\n\u2717 Certificate request failed\n"));
|
|
2571
|
+
if (result.output) {
|
|
2572
|
+
const lines = result.output.split("\n").filter(
|
|
2573
|
+
(l) => l.includes("Error") || l.includes("error") || l.includes("failed") || l.includes("Challenge")
|
|
2574
|
+
);
|
|
2575
|
+
for (const line of lines.slice(0, 5)) {
|
|
2576
|
+
console.log(chalk20.dim(` ${line.trim()}`));
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
console.log(chalk20.dim("\nCommon issues:"));
|
|
2580
|
+
console.log(chalk20.dim(" - DNS not pointing to this server"));
|
|
2581
|
+
console.log(chalk20.dim(" - Port 80 not accessible from internet"));
|
|
2582
|
+
console.log(chalk20.dim(" - Firewall blocking HTTP validation"));
|
|
2583
|
+
}
|
|
2584
|
+
console.log("");
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/commands/tunnel.ts
|
|
2588
|
+
import chalk21 from "chalk";
|
|
2589
|
+
import { execSync as execSync4, spawn as spawn2 } from "child_process";
|
|
2590
|
+
async function tunnelCommand(action, options) {
|
|
2591
|
+
if (!action) {
|
|
2592
|
+
console.log(chalk21.red("Usage: bindler tunnel <action>"));
|
|
2593
|
+
console.log(chalk21.dim("\nActions:"));
|
|
2594
|
+
console.log(chalk21.dim(" status Show tunnel status"));
|
|
2595
|
+
console.log(chalk21.dim(" start Start the tunnel"));
|
|
2596
|
+
console.log(chalk21.dim(" stop Stop the tunnel"));
|
|
2597
|
+
console.log(chalk21.dim(" login Authenticate with Cloudflare"));
|
|
2598
|
+
console.log(chalk21.dim(" create Create a new tunnel"));
|
|
2599
|
+
console.log(chalk21.dim(" list List all tunnels"));
|
|
2600
|
+
console.log(chalk21.dim("\nExamples:"));
|
|
2601
|
+
console.log(chalk21.dim(" bindler tunnel status"));
|
|
2602
|
+
console.log(chalk21.dim(" bindler tunnel start"));
|
|
2603
|
+
console.log(chalk21.dim(" bindler tunnel create --name mytunnel"));
|
|
2604
|
+
process.exit(1);
|
|
2605
|
+
}
|
|
2606
|
+
if (!isCloudflaredInstalled()) {
|
|
2607
|
+
console.log(chalk21.red("cloudflared is not installed."));
|
|
2608
|
+
console.log(chalk21.dim("\nInstall with: bindler setup"));
|
|
2609
|
+
process.exit(1);
|
|
2610
|
+
}
|
|
2611
|
+
const defaults = getDefaults();
|
|
2612
|
+
const tunnelName = options.name || defaults.tunnelName;
|
|
2613
|
+
switch (action) {
|
|
2614
|
+
case "status": {
|
|
2615
|
+
console.log(chalk21.blue("\nTunnel Status\n"));
|
|
2616
|
+
const info = getTunnelInfo(tunnelName);
|
|
2617
|
+
if (!info.exists) {
|
|
2618
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" does not exist.`));
|
|
2619
|
+
console.log(chalk21.dim(`
|
|
2620
|
+
Create with: bindler tunnel create --name ${tunnelName}`));
|
|
2621
|
+
} else {
|
|
2622
|
+
console.log(chalk21.dim(" Name: ") + chalk21.white(tunnelName));
|
|
2623
|
+
console.log(chalk21.dim(" ID: ") + chalk21.white(info.id || "unknown"));
|
|
2624
|
+
console.log(chalk21.dim(" Running: ") + (info.running ? chalk21.green("yes") : chalk21.red("no")));
|
|
2625
|
+
if (!info.running) {
|
|
2626
|
+
console.log(chalk21.dim(`
|
|
2627
|
+
Start with: bindler tunnel start`));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
console.log("");
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
case "start": {
|
|
2634
|
+
const info = getTunnelInfo(tunnelName);
|
|
2635
|
+
if (!info.exists) {
|
|
2636
|
+
console.log(chalk21.red(`Tunnel "${tunnelName}" does not exist.`));
|
|
2637
|
+
console.log(chalk21.dim(`Create with: bindler tunnel create`));
|
|
2638
|
+
process.exit(1);
|
|
2639
|
+
}
|
|
2640
|
+
if (info.running) {
|
|
2641
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" is already running.`));
|
|
2642
|
+
process.exit(0);
|
|
2643
|
+
}
|
|
2644
|
+
console.log(chalk21.blue(`Starting tunnel "${tunnelName}"...
|
|
2645
|
+
`));
|
|
2646
|
+
console.log(chalk21.dim("Running in foreground. Press Ctrl+C to stop.\n"));
|
|
2647
|
+
const child = spawn2("cloudflared", ["tunnel", "run", tunnelName], {
|
|
2648
|
+
stdio: "inherit"
|
|
2649
|
+
});
|
|
2650
|
+
child.on("error", (err) => {
|
|
2651
|
+
console.log(chalk21.red(`Failed to start tunnel: ${err.message}`));
|
|
2652
|
+
process.exit(1);
|
|
2653
|
+
});
|
|
2654
|
+
child.on("exit", (code) => {
|
|
2655
|
+
console.log(chalk21.dim(`
|
|
2656
|
+
Tunnel exited with code ${code}`));
|
|
2657
|
+
process.exit(code || 0);
|
|
2658
|
+
});
|
|
2659
|
+
break;
|
|
2660
|
+
}
|
|
2661
|
+
case "stop": {
|
|
2662
|
+
console.log(chalk21.blue("Stopping tunnel...\n"));
|
|
2663
|
+
const result = execCommandSafe(`pkill -f "cloudflared.*tunnel.*run.*${tunnelName}"`);
|
|
2664
|
+
if (result.success) {
|
|
2665
|
+
console.log(chalk21.green(`\u2713 Tunnel "${tunnelName}" stopped`));
|
|
2666
|
+
} else {
|
|
2667
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" was not running.`));
|
|
2668
|
+
}
|
|
2669
|
+
console.log("");
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
case "login": {
|
|
2673
|
+
console.log(chalk21.blue("Authenticating with Cloudflare...\n"));
|
|
2674
|
+
console.log(chalk21.dim("A browser window will open. Follow the instructions.\n"));
|
|
2675
|
+
try {
|
|
2676
|
+
execSync4("cloudflared tunnel login", { stdio: "inherit" });
|
|
2677
|
+
console.log(chalk21.green("\n\u2713 Authentication successful!"));
|
|
2678
|
+
} catch {
|
|
2679
|
+
console.log(chalk21.red("\n\u2717 Authentication failed or cancelled."));
|
|
2680
|
+
}
|
|
2681
|
+
console.log("");
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
case "create": {
|
|
2685
|
+
const existingTunnels = listTunnels();
|
|
2686
|
+
const exists = existingTunnels.some((t) => t.name === tunnelName);
|
|
2687
|
+
if (exists) {
|
|
2688
|
+
console.log(chalk21.yellow(`Tunnel "${tunnelName}" already exists.`));
|
|
2689
|
+
console.log(chalk21.dim("\nUse a different name with --name"));
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
console.log(chalk21.blue(`Creating tunnel "${tunnelName}"...
|
|
2693
|
+
`));
|
|
2694
|
+
try {
|
|
2695
|
+
execSync4(`cloudflared tunnel create ${tunnelName}`, { stdio: "inherit" });
|
|
2696
|
+
console.log(chalk21.green(`
|
|
2697
|
+
\u2713 Tunnel "${tunnelName}" created!`));
|
|
2698
|
+
console.log(chalk21.dim("\nNext steps:"));
|
|
2699
|
+
console.log(chalk21.dim(" 1. Create ~/.cloudflared/config.yml"));
|
|
2700
|
+
console.log(chalk21.dim(" 2. Run: bindler tunnel start"));
|
|
2701
|
+
} catch {
|
|
2702
|
+
console.log(chalk21.red("\n\u2717 Failed to create tunnel."));
|
|
2703
|
+
console.log(chalk21.dim("Make sure you're logged in: bindler tunnel login"));
|
|
2704
|
+
}
|
|
2705
|
+
console.log("");
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
case "list": {
|
|
2709
|
+
console.log(chalk21.blue("\nCloudflare Tunnels\n"));
|
|
2710
|
+
const tunnels = listTunnels();
|
|
2711
|
+
if (tunnels.length === 0) {
|
|
2712
|
+
console.log(chalk21.dim("No tunnels found."));
|
|
2713
|
+
console.log(chalk21.dim("\nCreate one with: bindler tunnel create"));
|
|
2714
|
+
} else {
|
|
2715
|
+
for (const tunnel of tunnels) {
|
|
2716
|
+
const isDefault = tunnel.name === tunnelName;
|
|
2717
|
+
const prefix = isDefault ? chalk21.cyan("\u2192 ") : " ";
|
|
2718
|
+
console.log(prefix + chalk21.white(tunnel.name) + chalk21.dim(` (${tunnel.id.slice(0, 8)}...)`));
|
|
2719
|
+
}
|
|
2720
|
+
console.log(chalk21.dim(`
|
|
2721
|
+
${tunnels.length} tunnel(s)`));
|
|
2722
|
+
}
|
|
2723
|
+
console.log("");
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
default:
|
|
2727
|
+
console.log(chalk21.red(`Unknown action: ${action}`));
|
|
2728
|
+
console.log(chalk21.dim("Run `bindler tunnel` for usage."));
|
|
2729
|
+
process.exit(1);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/commands/open.ts
|
|
2734
|
+
import chalk22 from "chalk";
|
|
2735
|
+
import { exec } from "child_process";
|
|
2736
|
+
async function openCommand(name) {
|
|
2737
|
+
if (!name) {
|
|
2738
|
+
console.log(chalk22.red("Usage: bindler open <name>"));
|
|
2739
|
+
console.log(chalk22.dim("\nOpen a project in your browser"));
|
|
2740
|
+
console.log(chalk22.dim("\nExamples:"));
|
|
2741
|
+
console.log(chalk22.dim(" bindler open myapp"));
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
const project = getProject(name);
|
|
2745
|
+
if (!project) {
|
|
2746
|
+
console.log(chalk22.red(`Project "${name}" not found.`));
|
|
2747
|
+
console.log(chalk22.dim("\nAvailable projects:"));
|
|
2748
|
+
const projects = listProjects();
|
|
2749
|
+
for (const p of projects) {
|
|
2750
|
+
console.log(chalk22.dim(` - ${p.name}`));
|
|
2751
|
+
}
|
|
2752
|
+
process.exit(1);
|
|
2753
|
+
}
|
|
2754
|
+
const defaults = getDefaults();
|
|
2755
|
+
const isLocal = project.local || project.hostname.endsWith(".local");
|
|
2756
|
+
const isDirect = defaults.mode === "direct";
|
|
2757
|
+
let url;
|
|
2758
|
+
if (isLocal) {
|
|
2759
|
+
const port = defaults.nginxListen.includes(":") ? defaults.nginxListen.split(":")[1] : defaults.nginxListen;
|
|
2760
|
+
url = `http://${project.hostname}:${port}`;
|
|
2761
|
+
} else if (isDirect && defaults.sslEnabled) {
|
|
2762
|
+
url = `https://${project.hostname}`;
|
|
2763
|
+
} else if (isDirect) {
|
|
2764
|
+
url = `http://${project.hostname}`;
|
|
2765
|
+
} else {
|
|
2766
|
+
url = `https://${project.hostname}`;
|
|
2767
|
+
}
|
|
2768
|
+
if (project.basePath && project.basePath !== "/") {
|
|
2769
|
+
url += project.basePath;
|
|
2770
|
+
}
|
|
2771
|
+
console.log(chalk22.dim(`Opening ${url}...`));
|
|
2772
|
+
const platform = process.platform;
|
|
2773
|
+
let cmd;
|
|
2774
|
+
if (platform === "darwin") {
|
|
2775
|
+
cmd = `open "${url}"`;
|
|
2776
|
+
} else if (platform === "win32") {
|
|
2777
|
+
cmd = `start "${url}"`;
|
|
2778
|
+
} else {
|
|
2779
|
+
cmd = `xdg-open "${url}"`;
|
|
2780
|
+
}
|
|
2781
|
+
exec(cmd, (error) => {
|
|
2782
|
+
if (error) {
|
|
2783
|
+
console.log(chalk22.yellow(`Could not open browser automatically.`));
|
|
2784
|
+
console.log(chalk22.dim(`
|
|
2785
|
+
Open manually: ${chalk22.cyan(url)}`));
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/commands/health.ts
|
|
2791
|
+
import chalk23 from "chalk";
|
|
2792
|
+
async function pingUrl(url, timeout = 5e3) {
|
|
2793
|
+
const start = Date.now();
|
|
2794
|
+
try {
|
|
2795
|
+
const controller = new AbortController();
|
|
2796
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2797
|
+
const response = await fetch(url, {
|
|
2798
|
+
method: "HEAD",
|
|
2799
|
+
redirect: "follow",
|
|
2800
|
+
signal: controller.signal
|
|
2801
|
+
});
|
|
2802
|
+
clearTimeout(timeoutId);
|
|
2803
|
+
return {
|
|
2804
|
+
ok: response.ok,
|
|
2805
|
+
status: response.status,
|
|
2806
|
+
time: Date.now() - start
|
|
2807
|
+
};
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
const err = error;
|
|
2810
|
+
return {
|
|
2811
|
+
ok: false,
|
|
2812
|
+
error: err.name === "AbortError" ? "timeout" : err.message,
|
|
2813
|
+
time: Date.now() - start
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
async function healthCommand() {
|
|
2818
|
+
const projects = listProjects();
|
|
2819
|
+
const defaults = getDefaults();
|
|
2820
|
+
if (projects.length === 0) {
|
|
2821
|
+
console.log(chalk23.yellow("\nNo projects registered."));
|
|
2822
|
+
console.log(chalk23.dim("Run `bindler new` to add a project.\n"));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
console.log(chalk23.blue("\nHealth Check\n"));
|
|
2826
|
+
const results = [];
|
|
2827
|
+
for (const project of projects) {
|
|
2828
|
+
if (project.enabled === false) {
|
|
2829
|
+
console.log(chalk23.dim(` - ${project.name} (disabled)`));
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
const isLocal = project.local || project.hostname.endsWith(".local");
|
|
2833
|
+
const isDirect = defaults.mode === "direct";
|
|
2834
|
+
let url;
|
|
2835
|
+
if (isLocal) {
|
|
2836
|
+
const port = defaults.nginxListen.includes(":") ? defaults.nginxListen.split(":")[1] : defaults.nginxListen;
|
|
2837
|
+
url = `http://${project.hostname}:${port}`;
|
|
2838
|
+
} else if (isDirect && defaults.sslEnabled) {
|
|
2839
|
+
url = `https://${project.hostname}`;
|
|
2840
|
+
} else if (isDirect) {
|
|
2841
|
+
url = `http://${project.hostname}`;
|
|
2842
|
+
} else {
|
|
2843
|
+
url = `https://${project.hostname}`;
|
|
2844
|
+
}
|
|
2845
|
+
if (project.basePath && project.basePath !== "/") {
|
|
2846
|
+
url += project.basePath;
|
|
2847
|
+
}
|
|
2848
|
+
process.stdout.write(chalk23.dim(` Checking ${project.name}...`));
|
|
2849
|
+
const result = await pingUrl(url);
|
|
2850
|
+
results.push({ name: project.name, hostname: project.hostname, ...result });
|
|
2851
|
+
process.stdout.write("\r\x1B[K");
|
|
2852
|
+
if (result.ok) {
|
|
2853
|
+
console.log(chalk23.green(" \u2713 ") + chalk23.white(project.name) + chalk23.dim(` (${result.time}ms)`));
|
|
2854
|
+
} else if (result.status) {
|
|
2855
|
+
console.log(chalk23.yellow(" ! ") + chalk23.white(project.name) + chalk23.dim(` (${result.status})`));
|
|
2856
|
+
} else {
|
|
2857
|
+
console.log(chalk23.red(" \u2717 ") + chalk23.white(project.name) + chalk23.dim(` (${result.error})`));
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
const healthy = results.filter((r) => r.ok).length;
|
|
2861
|
+
const unhealthy = results.filter((r) => !r.ok).length;
|
|
2862
|
+
console.log("");
|
|
2863
|
+
if (unhealthy === 0) {
|
|
2864
|
+
console.log(chalk23.green(`\u2713 All ${healthy} project(s) healthy`));
|
|
2865
|
+
} else if (healthy === 0) {
|
|
2866
|
+
console.log(chalk23.red(`\u2717 All ${unhealthy} project(s) down`));
|
|
2867
|
+
} else {
|
|
2868
|
+
console.log(chalk23.yellow(`! ${healthy} healthy, ${unhealthy} down`));
|
|
2869
|
+
}
|
|
2870
|
+
console.log("");
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/commands/stats.ts
|
|
2874
|
+
import chalk24 from "chalk";
|
|
2875
|
+
function formatBytes2(bytes) {
|
|
2876
|
+
if (bytes === 0) return "0 B";
|
|
2877
|
+
const k = 1024;
|
|
2878
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
2879
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2880
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
2881
|
+
}
|
|
2882
|
+
function formatUptime2(ms) {
|
|
2883
|
+
const seconds = Math.floor(ms / 1e3);
|
|
2884
|
+
const minutes = Math.floor(seconds / 60);
|
|
2885
|
+
const hours = Math.floor(minutes / 60);
|
|
2886
|
+
const days = Math.floor(hours / 24);
|
|
2887
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
2888
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
2889
|
+
if (minutes > 0) return `${minutes}m`;
|
|
2890
|
+
return `${seconds}s`;
|
|
2891
|
+
}
|
|
2892
|
+
async function statsCommand() {
|
|
2893
|
+
const projects = listProjects();
|
|
2894
|
+
const pm2Processes = getPm2List();
|
|
2895
|
+
const npmProjects = projects.filter((p) => p.type === "npm");
|
|
2896
|
+
if (npmProjects.length === 0) {
|
|
2897
|
+
console.log(chalk24.yellow("\nNo npm projects registered."));
|
|
2898
|
+
console.log(chalk24.dim("Stats are only available for npm projects.\n"));
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
console.log(chalk24.blue("\nProject Stats\n"));
|
|
2902
|
+
console.log(
|
|
2903
|
+
chalk24.dim(" ") + chalk24.dim("NAME".padEnd(20)) + chalk24.dim("STATUS".padEnd(10)) + chalk24.dim("CPU".padEnd(8)) + chalk24.dim("MEM".padEnd(10)) + chalk24.dim("UPTIME".padEnd(10)) + chalk24.dim("RESTARTS")
|
|
2904
|
+
);
|
|
2905
|
+
console.log(chalk24.dim(" " + "-".repeat(70)));
|
|
2906
|
+
let totalCpu = 0;
|
|
2907
|
+
let totalMem = 0;
|
|
2908
|
+
for (const project of npmProjects) {
|
|
2909
|
+
const pm2Name = `bindler:${project.name}`;
|
|
2910
|
+
const pm2Process = pm2Processes.find((p) => p.name === pm2Name);
|
|
2911
|
+
const name = project.name.slice(0, 18).padEnd(20);
|
|
2912
|
+
if (!pm2Process) {
|
|
2913
|
+
console.log(
|
|
2914
|
+
" " + chalk24.white(name) + chalk24.dim("not managed".padEnd(10)) + chalk24.dim("-".padEnd(8)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-".padEnd(10)) + chalk24.dim("-")
|
|
2915
|
+
);
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const statusColor = pm2Process.status === "online" ? chalk24.green : chalk24.red;
|
|
2919
|
+
const status = statusColor(pm2Process.status.padEnd(10));
|
|
2920
|
+
const cpu = `${pm2Process.cpu.toFixed(1)}%`.padEnd(8);
|
|
2921
|
+
const mem = formatBytes2(pm2Process.memory).padEnd(10);
|
|
2922
|
+
const uptime = formatUptime2(pm2Process.uptime).padEnd(10);
|
|
2923
|
+
const restarts = String(pm2Process.restarts);
|
|
2924
|
+
totalCpu += pm2Process.cpu;
|
|
2925
|
+
totalMem += pm2Process.memory;
|
|
2926
|
+
console.log(
|
|
2927
|
+
" " + chalk24.white(name) + status + (pm2Process.cpu > 50 ? chalk24.yellow(cpu) : chalk24.dim(cpu)) + (pm2Process.memory > 500 * 1024 * 1024 ? chalk24.yellow(mem) : chalk24.dim(mem)) + chalk24.dim(uptime) + (pm2Process.restarts > 0 ? chalk24.yellow(restarts) : chalk24.dim(restarts))
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
const runningCount = pm2Processes.filter((p) => p.name.startsWith("bindler:") && p.status === "online").length;
|
|
2931
|
+
console.log(chalk24.dim(" " + "-".repeat(70)));
|
|
2932
|
+
console.log(
|
|
2933
|
+
" " + chalk24.bold("TOTAL".padEnd(20)) + chalk24.dim(`${runningCount}/${npmProjects.length}`.padEnd(10)) + chalk24.dim(`${totalCpu.toFixed(1)}%`.padEnd(8)) + chalk24.dim(formatBytes2(totalMem).padEnd(10))
|
|
2934
|
+
);
|
|
2935
|
+
console.log("");
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/commands/completion.ts
|
|
2939
|
+
import chalk25 from "chalk";
|
|
2940
|
+
var BASH_COMPLETION = `
|
|
2941
|
+
# bindler bash completion
|
|
2942
|
+
_bindler_completions() {
|
|
2943
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
2944
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
2945
|
+
|
|
2946
|
+
local commands="new list ls status start stop restart logs update edit remove rm apply doctor ports info check setup init deploy backup restore ssl tunnel open health stats completion"
|
|
2947
|
+
|
|
2948
|
+
case "\${prev}" in
|
|
2949
|
+
bindler)
|
|
2950
|
+
COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
|
|
2951
|
+
return 0
|
|
2952
|
+
;;
|
|
2953
|
+
start|stop|restart|logs|update|edit|remove|rm|deploy|open|check)
|
|
2954
|
+
local projects=$(bindler list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
|
|
2955
|
+
COMPREPLY=($(compgen -W "\${projects}" -- "\${cur}"))
|
|
2956
|
+
return 0
|
|
2957
|
+
;;
|
|
2958
|
+
tunnel)
|
|
2959
|
+
COMPREPLY=($(compgen -W "status start stop login create list" -- "\${cur}"))
|
|
2960
|
+
return 0
|
|
2961
|
+
;;
|
|
2962
|
+
esac
|
|
2963
|
+
|
|
2964
|
+
COMPREPLY=()
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
complete -F _bindler_completions bindler
|
|
2968
|
+
`;
|
|
2969
|
+
var ZSH_COMPLETION = `
|
|
2970
|
+
#compdef bindler
|
|
2971
|
+
|
|
2972
|
+
_bindler() {
|
|
2973
|
+
local -a commands
|
|
2974
|
+
commands=(
|
|
2975
|
+
'new:Create and register a new project'
|
|
2976
|
+
'list:List all registered projects'
|
|
2977
|
+
'ls:List all registered projects'
|
|
2978
|
+
'status:Show detailed status of all projects'
|
|
2979
|
+
'start:Start an npm project with PM2'
|
|
2980
|
+
'stop:Stop an npm project'
|
|
2981
|
+
'restart:Restart an npm project'
|
|
2982
|
+
'logs:Show logs for an npm project'
|
|
2983
|
+
'update:Update project configuration'
|
|
2984
|
+
'edit:Edit project configuration in \\$EDITOR'
|
|
2985
|
+
'remove:Remove a project from registry'
|
|
2986
|
+
'rm:Remove a project from registry'
|
|
2987
|
+
'apply:Generate and apply nginx configuration'
|
|
2988
|
+
'doctor:Run system diagnostics'
|
|
2989
|
+
'ports:Show allocated ports'
|
|
2990
|
+
'info:Show bindler information'
|
|
2991
|
+
'check:Check DNS and HTTP for a hostname'
|
|
2992
|
+
'setup:Install missing dependencies'
|
|
2993
|
+
'init:Initialize bindler'
|
|
2994
|
+
'deploy:Deploy a project (git pull + install + restart)'
|
|
2995
|
+
'backup:Backup configuration'
|
|
2996
|
+
'restore:Restore configuration'
|
|
2997
|
+
'ssl:Request SSL certificate'
|
|
2998
|
+
'tunnel:Manage Cloudflare tunnel'
|
|
2999
|
+
'open:Open project in browser'
|
|
3000
|
+
'health:Check health of all projects'
|
|
3001
|
+
'stats:Show CPU and memory stats'
|
|
3002
|
+
'completion:Generate shell completions'
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
local -a project_commands
|
|
3006
|
+
project_commands=(start stop restart logs update edit remove rm deploy open check)
|
|
3007
|
+
|
|
3008
|
+
_arguments -C \\
|
|
3009
|
+
'1: :->command' \\
|
|
3010
|
+
'*: :->args'
|
|
3011
|
+
|
|
3012
|
+
case "$state" in
|
|
3013
|
+
command)
|
|
3014
|
+
_describe -t commands 'bindler command' commands
|
|
3015
|
+
;;
|
|
3016
|
+
args)
|
|
3017
|
+
case "$words[2]" in
|
|
3018
|
+
start|stop|restart|logs|update|edit|remove|rm|deploy|open|check)
|
|
3019
|
+
local -a projects
|
|
3020
|
+
projects=(\${(f)"$(bindler list --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)"})
|
|
3021
|
+
_describe -t projects 'project' projects
|
|
3022
|
+
;;
|
|
3023
|
+
tunnel)
|
|
3024
|
+
local -a tunnel_actions
|
|
3025
|
+
tunnel_actions=(status start stop login create list)
|
|
3026
|
+
_describe -t actions 'action' tunnel_actions
|
|
3027
|
+
;;
|
|
3028
|
+
esac
|
|
3029
|
+
;;
|
|
3030
|
+
esac
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
_bindler
|
|
3034
|
+
`;
|
|
3035
|
+
var FISH_COMPLETION = `
|
|
3036
|
+
# bindler fish completion
|
|
3037
|
+
complete -c bindler -f
|
|
3038
|
+
|
|
3039
|
+
# Commands
|
|
3040
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'new' -d 'Create and register a new project'
|
|
3041
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'list ls' -d 'List all registered projects'
|
|
3042
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'status' -d 'Show detailed status'
|
|
3043
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'start' -d 'Start an npm project'
|
|
3044
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'stop' -d 'Stop an npm project'
|
|
3045
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'restart' -d 'Restart an npm project'
|
|
3046
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'logs' -d 'Show logs'
|
|
3047
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'update' -d 'Update project configuration'
|
|
3048
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'edit' -d 'Edit project in $EDITOR'
|
|
3049
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'remove rm' -d 'Remove a project'
|
|
3050
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'apply' -d 'Apply nginx configuration'
|
|
3051
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'doctor' -d 'Run diagnostics'
|
|
3052
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'ports' -d 'Show allocated ports'
|
|
3053
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'info' -d 'Show bindler info'
|
|
3054
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'check' -d 'Check DNS and HTTP'
|
|
3055
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'setup' -d 'Install dependencies'
|
|
3056
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'init' -d 'Initialize bindler'
|
|
3057
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'deploy' -d 'Deploy a project'
|
|
3058
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'backup' -d 'Backup configuration'
|
|
3059
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'restore' -d 'Restore configuration'
|
|
3060
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'ssl' -d 'Request SSL certificate'
|
|
3061
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'tunnel' -d 'Manage Cloudflare tunnel'
|
|
3062
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'open' -d 'Open in browser'
|
|
3063
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'health' -d 'Check health'
|
|
3064
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'stats' -d 'Show stats'
|
|
3065
|
+
complete -c bindler -n '__fish_use_subcommand' -a 'completion' -d 'Generate completions'
|
|
3066
|
+
|
|
3067
|
+
# Tunnel subcommands
|
|
3068
|
+
complete -c bindler -n '__fish_seen_subcommand_from tunnel' -a 'status start stop login create list'
|
|
3069
|
+
`;
|
|
3070
|
+
async function completionCommand(shell) {
|
|
3071
|
+
if (!shell) {
|
|
3072
|
+
console.log(chalk25.red("Usage: bindler completion <shell>"));
|
|
3073
|
+
console.log(chalk25.dim("\nSupported shells: bash, zsh, fish"));
|
|
3074
|
+
console.log(chalk25.dim("\nSetup:"));
|
|
3075
|
+
console.log(chalk25.dim(" bash: bindler completion bash >> ~/.bashrc"));
|
|
3076
|
+
console.log(chalk25.dim(" zsh: bindler completion zsh >> ~/.zshrc"));
|
|
3077
|
+
console.log(chalk25.dim(" fish: bindler completion fish > ~/.config/fish/completions/bindler.fish"));
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
switch (shell) {
|
|
3081
|
+
case "bash":
|
|
3082
|
+
console.log(BASH_COMPLETION.trim());
|
|
3083
|
+
break;
|
|
3084
|
+
case "zsh":
|
|
3085
|
+
console.log(ZSH_COMPLETION.trim());
|
|
3086
|
+
break;
|
|
3087
|
+
case "fish":
|
|
3088
|
+
console.log(FISH_COMPLETION.trim());
|
|
3089
|
+
break;
|
|
3090
|
+
default:
|
|
3091
|
+
console.log(chalk25.red(`Unknown shell: ${shell}`));
|
|
3092
|
+
console.log(chalk25.dim("Supported: bash, zsh, fish"));
|
|
3093
|
+
process.exit(1);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// src/lib/update-check.ts
|
|
3098
|
+
import chalk26 from "chalk";
|
|
3099
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
3100
|
+
import { join as join6 } from "path";
|
|
3101
|
+
import { homedir as homedir3 } from "os";
|
|
3102
|
+
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
3103
|
+
var CACHE_DIR = join6(homedir3(), ".config", "bindler");
|
|
3104
|
+
var CACHE_FILE = join6(CACHE_DIR, ".update-check");
|
|
3105
|
+
function readCache() {
|
|
3106
|
+
try {
|
|
3107
|
+
if (existsSync9(CACHE_FILE)) {
|
|
3108
|
+
return JSON.parse(readFileSync6(CACHE_FILE, "utf-8"));
|
|
3109
|
+
}
|
|
3110
|
+
} catch {
|
|
3111
|
+
}
|
|
3112
|
+
return { lastCheck: 0, latestVersion: null };
|
|
3113
|
+
}
|
|
3114
|
+
function writeCache(data) {
|
|
3115
|
+
try {
|
|
3116
|
+
if (!existsSync9(CACHE_DIR)) {
|
|
3117
|
+
mkdirSync5(CACHE_DIR, { recursive: true });
|
|
3118
|
+
}
|
|
3119
|
+
writeFileSync5(CACHE_FILE, JSON.stringify(data));
|
|
3120
|
+
} catch {
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
async function fetchLatestVersion() {
|
|
3124
|
+
try {
|
|
3125
|
+
const controller = new AbortController();
|
|
3126
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
3127
|
+
const response = await fetch("https://registry.npmjs.org/bindler/latest", {
|
|
3128
|
+
signal: controller.signal
|
|
3129
|
+
});
|
|
3130
|
+
clearTimeout(timeout);
|
|
3131
|
+
if (!response.ok) return null;
|
|
3132
|
+
const data = await response.json();
|
|
3133
|
+
return data.version || null;
|
|
3134
|
+
} catch {
|
|
3135
|
+
return null;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function compareVersions(current, latest) {
|
|
3139
|
+
const c = current.split(".").map(Number);
|
|
3140
|
+
const l = latest.split(".").map(Number);
|
|
3141
|
+
for (let i = 0; i < 3; i++) {
|
|
3142
|
+
if ((l[i] || 0) > (c[i] || 0)) return 1;
|
|
3143
|
+
if ((l[i] || 0) < (c[i] || 0)) return -1;
|
|
3144
|
+
}
|
|
3145
|
+
return 0;
|
|
3146
|
+
}
|
|
3147
|
+
async function checkForUpdates() {
|
|
3148
|
+
const cache = readCache();
|
|
3149
|
+
const now = Date.now();
|
|
3150
|
+
if (now - cache.lastCheck < CHECK_INTERVAL) {
|
|
3151
|
+
if (cache.latestVersion && compareVersions("1.1.1", cache.latestVersion) < 0) {
|
|
3152
|
+
showUpdateMessage(cache.latestVersion);
|
|
3153
|
+
}
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
fetchLatestVersion().then((latestVersion) => {
|
|
3157
|
+
writeCache({ lastCheck: now, latestVersion });
|
|
3158
|
+
if (latestVersion && compareVersions("1.1.1", latestVersion) < 0) {
|
|
3159
|
+
showUpdateMessage(latestVersion);
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
function showUpdateMessage(latestVersion) {
|
|
3164
|
+
console.log("");
|
|
3165
|
+
console.log(chalk26.yellow(` Update available: ${"1.1.1"} \u2192 ${latestVersion}`));
|
|
3166
|
+
console.log(chalk26.dim(` Run: npm update -g bindler`));
|
|
3167
|
+
console.log("");
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2194
3170
|
// src/cli.ts
|
|
2195
3171
|
var program = new Command();
|
|
2196
|
-
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.
|
|
2197
|
-
program.hook("preAction", () => {
|
|
3172
|
+
program.name("bindler").description("Manage multiple projects behind Cloudflare Tunnel with Nginx and PM2").version("1.1.1");
|
|
3173
|
+
program.hook("preAction", async () => {
|
|
2198
3174
|
try {
|
|
2199
3175
|
initConfig();
|
|
2200
3176
|
} catch (error) {
|
|
2201
3177
|
}
|
|
3178
|
+
checkForUpdates();
|
|
2202
3179
|
});
|
|
2203
3180
|
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) => {
|
|
2204
3181
|
await newCommand(options);
|
|
@@ -2211,73 +3188,73 @@ program.command("status").description("Show detailed status of all projects").ac
|
|
|
2211
3188
|
});
|
|
2212
3189
|
program.command("start [name]").description("Start an npm project with PM2").option("-a, --all", "Start all npm projects").action(async (name, options) => {
|
|
2213
3190
|
if (!name && !options.all) {
|
|
2214
|
-
console.log(
|
|
2215
|
-
console.log(
|
|
2216
|
-
console.log(
|
|
2217
|
-
console.log(
|
|
3191
|
+
console.log(chalk27.red("Usage: bindler start <name> or bindler start --all"));
|
|
3192
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3193
|
+
console.log(chalk27.dim(" bindler start myapp"));
|
|
3194
|
+
console.log(chalk27.dim(" bindler start --all # start all npm projects"));
|
|
2218
3195
|
process.exit(1);
|
|
2219
3196
|
}
|
|
2220
3197
|
await startCommand(name, options);
|
|
2221
3198
|
});
|
|
2222
3199
|
program.command("stop [name]").description("Stop an npm project").option("-a, --all", "Stop all npm projects").action(async (name, options) => {
|
|
2223
3200
|
if (!name && !options.all) {
|
|
2224
|
-
console.log(
|
|
2225
|
-
console.log(
|
|
2226
|
-
console.log(
|
|
2227
|
-
console.log(
|
|
3201
|
+
console.log(chalk27.red("Usage: bindler stop <name> or bindler stop --all"));
|
|
3202
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3203
|
+
console.log(chalk27.dim(" bindler stop myapp"));
|
|
3204
|
+
console.log(chalk27.dim(" bindler stop --all # stop all npm projects"));
|
|
2228
3205
|
process.exit(1);
|
|
2229
3206
|
}
|
|
2230
3207
|
await stopCommand(name, options);
|
|
2231
3208
|
});
|
|
2232
3209
|
program.command("restart [name]").description("Restart an npm project").option("-a, --all", "Restart all npm projects").action(async (name, options) => {
|
|
2233
3210
|
if (!name && !options.all) {
|
|
2234
|
-
console.log(
|
|
2235
|
-
console.log(
|
|
2236
|
-
console.log(
|
|
2237
|
-
console.log(
|
|
3211
|
+
console.log(chalk27.red("Usage: bindler restart <name> or bindler restart --all"));
|
|
3212
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3213
|
+
console.log(chalk27.dim(" bindler restart myapp"));
|
|
3214
|
+
console.log(chalk27.dim(" bindler restart --all # restart all npm projects"));
|
|
2238
3215
|
process.exit(1);
|
|
2239
3216
|
}
|
|
2240
3217
|
await restartCommand(name, options);
|
|
2241
3218
|
});
|
|
2242
3219
|
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) => {
|
|
2243
3220
|
if (!name) {
|
|
2244
|
-
console.log(
|
|
2245
|
-
console.log(
|
|
2246
|
-
console.log(
|
|
2247
|
-
console.log(
|
|
2248
|
-
console.log(
|
|
3221
|
+
console.log(chalk27.red("Usage: bindler logs <name>"));
|
|
3222
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3223
|
+
console.log(chalk27.dim(" bindler logs myapp"));
|
|
3224
|
+
console.log(chalk27.dim(" bindler logs myapp --follow"));
|
|
3225
|
+
console.log(chalk27.dim(" bindler logs myapp --lines 500"));
|
|
2249
3226
|
process.exit(1);
|
|
2250
3227
|
}
|
|
2251
3228
|
await logsCommand(name, { ...options, lines: parseInt(options.lines, 10) });
|
|
2252
3229
|
});
|
|
2253
3230
|
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) => {
|
|
2254
3231
|
if (!name) {
|
|
2255
|
-
console.log(
|
|
2256
|
-
console.log(
|
|
2257
|
-
console.log(
|
|
2258
|
-
console.log(
|
|
2259
|
-
console.log(
|
|
3232
|
+
console.log(chalk27.red("Usage: bindler update <name> [options]"));
|
|
3233
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3234
|
+
console.log(chalk27.dim(" bindler update myapp --hostname newapp.example.com"));
|
|
3235
|
+
console.log(chalk27.dim(" bindler update myapp --port 4000"));
|
|
3236
|
+
console.log(chalk27.dim(" bindler update myapp --disable"));
|
|
2260
3237
|
process.exit(1);
|
|
2261
3238
|
}
|
|
2262
3239
|
await updateCommand(name, options);
|
|
2263
3240
|
});
|
|
2264
3241
|
program.command("edit [name]").description("Edit project configuration in $EDITOR").action(async (name) => {
|
|
2265
3242
|
if (!name) {
|
|
2266
|
-
console.log(
|
|
2267
|
-
console.log(
|
|
2268
|
-
console.log(
|
|
2269
|
-
console.log(
|
|
3243
|
+
console.log(chalk27.red("Usage: bindler edit <name>"));
|
|
3244
|
+
console.log(chalk27.dim("\nOpens the project config in your $EDITOR"));
|
|
3245
|
+
console.log(chalk27.dim("\nExample:"));
|
|
3246
|
+
console.log(chalk27.dim(" bindler edit myapp"));
|
|
2270
3247
|
process.exit(1);
|
|
2271
3248
|
}
|
|
2272
3249
|
await editCommand(name);
|
|
2273
3250
|
});
|
|
2274
3251
|
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) => {
|
|
2275
3252
|
if (!name) {
|
|
2276
|
-
console.log(
|
|
2277
|
-
console.log(
|
|
2278
|
-
console.log(
|
|
2279
|
-
console.log(
|
|
2280
|
-
console.log(
|
|
3253
|
+
console.log(chalk27.red("Usage: bindler remove <name>"));
|
|
3254
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3255
|
+
console.log(chalk27.dim(" bindler remove myapp"));
|
|
3256
|
+
console.log(chalk27.dim(" bindler remove myapp --force # skip confirmation"));
|
|
3257
|
+
console.log(chalk27.dim(" bindler rm myapp # alias"));
|
|
2281
3258
|
process.exit(1);
|
|
2282
3259
|
}
|
|
2283
3260
|
await removeCommand(name, options);
|
|
@@ -2296,10 +3273,10 @@ program.command("info").description("Show bindler information and stats").action
|
|
|
2296
3273
|
});
|
|
2297
3274
|
program.command("check [hostname]").description("Check DNS propagation and HTTP accessibility for a hostname").option("-v, --verbose", "Show verbose output").action(async (hostname, options) => {
|
|
2298
3275
|
if (!hostname) {
|
|
2299
|
-
console.log(
|
|
2300
|
-
console.log(
|
|
2301
|
-
console.log(
|
|
2302
|
-
console.log(
|
|
3276
|
+
console.log(chalk27.red("Usage: bindler check <hostname>"));
|
|
3277
|
+
console.log(chalk27.dim("\nExamples:"));
|
|
3278
|
+
console.log(chalk27.dim(" bindler check myapp.example.com"));
|
|
3279
|
+
console.log(chalk27.dim(" bindler check myapp # uses project name"));
|
|
2303
3280
|
process.exit(1);
|
|
2304
3281
|
}
|
|
2305
3282
|
await checkCommand(hostname, options);
|
|
@@ -2307,5 +3284,35 @@ program.command("check [hostname]").description("Check DNS propagation and HTTP
|
|
|
2307
3284
|
program.command("setup").description("Install missing dependencies (nginx, PM2, cloudflared)").option("--direct", "Direct mode for VPS (no Cloudflare Tunnel, use port 80/443)").action(async (options) => {
|
|
2308
3285
|
await setupCommand(options);
|
|
2309
3286
|
});
|
|
3287
|
+
program.command("init").description("Initialize bindler with interactive setup wizard").action(async () => {
|
|
3288
|
+
await initCommand();
|
|
3289
|
+
});
|
|
3290
|
+
program.command("deploy [name]").description("Deploy a project (git pull + npm install + restart)").option("--skip-pull", "Skip git pull").option("--skip-install", "Skip npm install").option("--skip-restart", "Skip restart").action(async (name, options) => {
|
|
3291
|
+
await deployCommand(name, options);
|
|
3292
|
+
});
|
|
3293
|
+
program.command("backup").description("Backup bindler configuration").option("-o, --output <file>", "Output file path").action(async (options) => {
|
|
3294
|
+
await backupCommand(options);
|
|
3295
|
+
});
|
|
3296
|
+
program.command("restore [file]").description("Restore bindler configuration from backup").option("-f, --force", "Overwrite existing config").action(async (file, options) => {
|
|
3297
|
+
await restoreCommand(file, options);
|
|
3298
|
+
});
|
|
3299
|
+
program.command("ssl [hostname]").description("Request SSL certificate for a hostname").option("-e, --email <email>", "Email for Let's Encrypt").option("--staging", "Use staging server (for testing)").action(async (hostname, options) => {
|
|
3300
|
+
await sslCommand(hostname, options);
|
|
3301
|
+
});
|
|
3302
|
+
program.command("tunnel [action]").description("Manage Cloudflare tunnel (status, start, stop, login, create, list)").option("-n, --name <name>", "Tunnel name").action(async (action, options) => {
|
|
3303
|
+
await tunnelCommand(action, options);
|
|
3304
|
+
});
|
|
3305
|
+
program.command("open [name]").description("Open a project in your browser").action(async (name) => {
|
|
3306
|
+
await openCommand(name);
|
|
3307
|
+
});
|
|
3308
|
+
program.command("health").description("Check health of all projects").action(async () => {
|
|
3309
|
+
await healthCommand();
|
|
3310
|
+
});
|
|
3311
|
+
program.command("stats").description("Show CPU and memory stats for npm projects").action(async () => {
|
|
3312
|
+
await statsCommand();
|
|
3313
|
+
});
|
|
3314
|
+
program.command("completion [shell]").description("Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
|
|
3315
|
+
await completionCommand(shell);
|
|
3316
|
+
});
|
|
2310
3317
|
program.parse();
|
|
2311
3318
|
//# sourceMappingURL=cli.js.map
|