portless 0.4.2 → 0.5.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/README.md +260 -0
- package/dist/chunk-P3DHZHEZ.js +1091 -0
- package/dist/cli.js +839 -252
- package/dist/index.d.ts +37 -1
- package/dist/index.js +17 -3
- package/package.json +3 -3
- package/dist/chunk-JMRTQAVX.js +0 -783
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
PRIVILEGED_PORT_THRESHOLD,
|
|
5
5
|
RouteConflictError,
|
|
6
6
|
RouteStore,
|
|
7
|
+
cleanHostsFile,
|
|
7
8
|
createProxyServer,
|
|
8
9
|
discoverState,
|
|
9
10
|
findFreePort,
|
|
@@ -20,14 +21,15 @@ import {
|
|
|
20
21
|
readTlsMarker,
|
|
21
22
|
resolveStateDir,
|
|
22
23
|
spawnCommand,
|
|
24
|
+
syncHostsFile,
|
|
23
25
|
waitForProxy,
|
|
24
26
|
writeTlsMarker
|
|
25
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-P3DHZHEZ.js";
|
|
26
28
|
|
|
27
29
|
// src/cli.ts
|
|
28
30
|
import chalk from "chalk";
|
|
29
|
-
import * as
|
|
30
|
-
import * as
|
|
31
|
+
import * as fs3 from "fs";
|
|
32
|
+
import * as path3 from "path";
|
|
31
33
|
import { spawn, spawnSync } from "child_process";
|
|
32
34
|
|
|
33
35
|
// src/certs.ts
|
|
@@ -246,8 +248,50 @@ function loginKeychainPath() {
|
|
|
246
248
|
const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
|
|
247
249
|
return path.join(home, "Library", "Keychains", "login.keychain-db");
|
|
248
250
|
}
|
|
251
|
+
var LINUX_CA_TRUST_CONFIGS = {
|
|
252
|
+
debian: {
|
|
253
|
+
certDir: "/usr/local/share/ca-certificates",
|
|
254
|
+
updateCommand: "update-ca-certificates"
|
|
255
|
+
},
|
|
256
|
+
arch: {
|
|
257
|
+
certDir: "/etc/ca-certificates/trust-source/anchors",
|
|
258
|
+
updateCommand: "update-ca-trust"
|
|
259
|
+
},
|
|
260
|
+
fedora: {
|
|
261
|
+
certDir: "/etc/pki/ca-trust/source/anchors",
|
|
262
|
+
updateCommand: "update-ca-trust"
|
|
263
|
+
},
|
|
264
|
+
suse: {
|
|
265
|
+
certDir: "/etc/pki/trust/anchors",
|
|
266
|
+
updateCommand: "update-ca-certificates"
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
function detectLinuxDistro() {
|
|
270
|
+
try {
|
|
271
|
+
const osRelease = fs.readFileSync("/etc/os-release", "utf-8").toLowerCase();
|
|
272
|
+
if (osRelease.includes("arch")) return "arch";
|
|
273
|
+
if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos"))
|
|
274
|
+
return "fedora";
|
|
275
|
+
if (osRelease.includes("suse")) return "suse";
|
|
276
|
+
if (osRelease.includes("debian") || osRelease.includes("ubuntu")) return "debian";
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
for (const [distro, config] of Object.entries(LINUX_CA_TRUST_CONFIGS)) {
|
|
280
|
+
try {
|
|
281
|
+
execFileSync("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
|
|
282
|
+
if (fs.existsSync(path.dirname(config.certDir))) return distro;
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
function getLinuxCATrustConfig() {
|
|
289
|
+
const distro = detectLinuxDistro();
|
|
290
|
+
return LINUX_CA_TRUST_CONFIGS[distro ?? "debian"];
|
|
291
|
+
}
|
|
249
292
|
function isCATrustedLinux(stateDir) {
|
|
250
|
-
const
|
|
293
|
+
const config = getLinuxCATrustConfig();
|
|
294
|
+
const systemCertPath = path.join(config.certDir, "portless-ca.crt");
|
|
251
295
|
if (!fileExists(systemCertPath)) return false;
|
|
252
296
|
try {
|
|
253
297
|
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
@@ -386,9 +430,13 @@ function trustCA(stateDir) {
|
|
|
386
430
|
);
|
|
387
431
|
return { trusted: true };
|
|
388
432
|
} else if (process.platform === "linux") {
|
|
389
|
-
const
|
|
433
|
+
const config = getLinuxCATrustConfig();
|
|
434
|
+
if (!fs.existsSync(config.certDir)) {
|
|
435
|
+
fs.mkdirSync(config.certDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
390
438
|
fs.copyFileSync(caCertPath, dest);
|
|
391
|
-
execFileSync(
|
|
439
|
+
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
392
440
|
return { trusted: true };
|
|
393
441
|
}
|
|
394
442
|
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
@@ -404,6 +452,151 @@ function trustCA(stateDir) {
|
|
|
404
452
|
}
|
|
405
453
|
}
|
|
406
454
|
|
|
455
|
+
// src/auto.ts
|
|
456
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
457
|
+
import * as fs2 from "fs";
|
|
458
|
+
import * as path2 from "path";
|
|
459
|
+
function sanitizeForHostname(name) {
|
|
460
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
461
|
+
}
|
|
462
|
+
function inferProjectName(cwd = process.cwd()) {
|
|
463
|
+
const pkgResult = findPackageJsonName(cwd);
|
|
464
|
+
if (pkgResult) {
|
|
465
|
+
const sanitized2 = sanitizeForHostname(pkgResult);
|
|
466
|
+
if (sanitized2) {
|
|
467
|
+
return { name: sanitized2, source: "package.json" };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const gitRoot = findGitRoot(cwd);
|
|
471
|
+
if (gitRoot) {
|
|
472
|
+
const sanitized2 = sanitizeForHostname(path2.basename(gitRoot));
|
|
473
|
+
if (sanitized2) {
|
|
474
|
+
return { name: sanitized2, source: "git root" };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const sanitized = sanitizeForHostname(path2.basename(cwd));
|
|
478
|
+
if (sanitized) {
|
|
479
|
+
return { name: sanitized, source: "directory name" };
|
|
480
|
+
}
|
|
481
|
+
throw new Error("Could not infer a project name from package.json, git root, or directory name");
|
|
482
|
+
}
|
|
483
|
+
function findPackageJsonName(startDir) {
|
|
484
|
+
let dir = startDir;
|
|
485
|
+
for (; ; ) {
|
|
486
|
+
const pkgPath = path2.join(dir, "package.json");
|
|
487
|
+
try {
|
|
488
|
+
const raw = fs2.readFileSync(pkgPath, "utf-8");
|
|
489
|
+
const pkg = JSON.parse(raw);
|
|
490
|
+
if (typeof pkg.name === "string" && pkg.name) {
|
|
491
|
+
return pkg.name.replace(/^@[^/]+\//, "");
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
const parent = path2.dirname(dir);
|
|
496
|
+
if (parent === dir) break;
|
|
497
|
+
dir = parent;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function findGitRoot(startDir) {
|
|
502
|
+
try {
|
|
503
|
+
const toplevel = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
504
|
+
cwd: startDir,
|
|
505
|
+
encoding: "utf-8",
|
|
506
|
+
timeout: 5e3,
|
|
507
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
508
|
+
}).trim();
|
|
509
|
+
if (toplevel) return toplevel;
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
let dir = startDir;
|
|
513
|
+
for (; ; ) {
|
|
514
|
+
const gitPath = path2.join(dir, ".git");
|
|
515
|
+
try {
|
|
516
|
+
const stat = fs2.statSync(gitPath);
|
|
517
|
+
if (stat.isDirectory()) return dir;
|
|
518
|
+
if (stat.isFile()) return dir;
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
const parent = path2.dirname(dir);
|
|
522
|
+
if (parent === dir) break;
|
|
523
|
+
dir = parent;
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
var DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
|
|
528
|
+
function branchToPrefix(branch) {
|
|
529
|
+
if (!branch || branch === "HEAD" || DEFAULT_BRANCHES.has(branch)) return null;
|
|
530
|
+
const lastSegment = branch.split("/").pop();
|
|
531
|
+
const prefix = sanitizeForHostname(lastSegment);
|
|
532
|
+
return prefix || null;
|
|
533
|
+
}
|
|
534
|
+
function detectWorktreePrefix(cwd = process.cwd()) {
|
|
535
|
+
const cliResult = detectWorktreeViaCli(cwd);
|
|
536
|
+
if (cliResult !== void 0) return cliResult;
|
|
537
|
+
return detectWorktreeViaFilesystem(cwd);
|
|
538
|
+
}
|
|
539
|
+
function detectWorktreeViaCli(cwd) {
|
|
540
|
+
try {
|
|
541
|
+
const listOutput = execFileSync2("git", ["worktree", "list", "--porcelain"], {
|
|
542
|
+
cwd,
|
|
543
|
+
encoding: "utf-8",
|
|
544
|
+
timeout: 5e3,
|
|
545
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
546
|
+
});
|
|
547
|
+
const worktreeCount = listOutput.split("\n").filter((l) => l.startsWith("worktree ")).length;
|
|
548
|
+
if (worktreeCount <= 1) return null;
|
|
549
|
+
const branch = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
550
|
+
cwd,
|
|
551
|
+
encoding: "utf-8",
|
|
552
|
+
timeout: 5e3,
|
|
553
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
554
|
+
}).trim();
|
|
555
|
+
const prefix = branchToPrefix(branch);
|
|
556
|
+
if (!prefix) return null;
|
|
557
|
+
return { prefix, source: "git branch" };
|
|
558
|
+
} catch {
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function detectWorktreeViaFilesystem(startDir) {
|
|
563
|
+
let dir = startDir;
|
|
564
|
+
for (; ; ) {
|
|
565
|
+
const gitPath = path2.join(dir, ".git");
|
|
566
|
+
try {
|
|
567
|
+
const stat = fs2.statSync(gitPath);
|
|
568
|
+
if (stat.isDirectory()) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (stat.isFile()) {
|
|
572
|
+
const content = fs2.readFileSync(gitPath, "utf-8").trim();
|
|
573
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
574
|
+
if (!match) return null;
|
|
575
|
+
const gitdir = match[1];
|
|
576
|
+
if (!gitdir.match(/\/worktrees\/[^/]+$/)) return null;
|
|
577
|
+
const branch = readBranchFromHead(path2.resolve(dir, gitdir));
|
|
578
|
+
const prefix = branchToPrefix(branch ?? "");
|
|
579
|
+
if (!prefix) return null;
|
|
580
|
+
return { prefix, source: "git branch" };
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
const parent = path2.dirname(dir);
|
|
585
|
+
if (parent === dir) break;
|
|
586
|
+
dir = parent;
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
function readBranchFromHead(gitdir) {
|
|
591
|
+
try {
|
|
592
|
+
const head = fs2.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
|
|
593
|
+
const refMatch = head.match(/^ref: refs\/heads\/(.+)$/);
|
|
594
|
+
return refMatch ? refMatch[1] : null;
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
407
600
|
// src/cli.ts
|
|
408
601
|
var DEBOUNCE_MS = 100;
|
|
409
602
|
var POLL_INTERVAL_MS = 3e3;
|
|
@@ -413,11 +606,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
413
606
|
store.ensureDir();
|
|
414
607
|
const isTls = !!tlsOptions;
|
|
415
608
|
const routesPath = store.getRoutesPath();
|
|
416
|
-
if (!
|
|
417
|
-
|
|
609
|
+
if (!fs3.existsSync(routesPath)) {
|
|
610
|
+
fs3.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
418
611
|
}
|
|
419
612
|
try {
|
|
420
|
-
|
|
613
|
+
fs3.chmodSync(routesPath, FILE_MODE);
|
|
421
614
|
} catch {
|
|
422
615
|
}
|
|
423
616
|
fixOwnership(routesPath);
|
|
@@ -425,14 +618,18 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
425
618
|
let debounceTimer = null;
|
|
426
619
|
let watcher = null;
|
|
427
620
|
let pollingInterval = null;
|
|
621
|
+
const autoSyncHosts = process.env.PORTLESS_SYNC_HOSTS === "1";
|
|
428
622
|
const reloadRoutes = () => {
|
|
429
623
|
try {
|
|
430
624
|
cachedRoutes = store.loadRoutes();
|
|
625
|
+
if (autoSyncHosts) {
|
|
626
|
+
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
627
|
+
}
|
|
431
628
|
} catch {
|
|
432
629
|
}
|
|
433
630
|
};
|
|
434
631
|
try {
|
|
435
|
-
watcher =
|
|
632
|
+
watcher = fs3.watch(routesPath, () => {
|
|
436
633
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
437
634
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
438
635
|
});
|
|
@@ -440,6 +637,9 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
440
637
|
console.warn(chalk.yellow("fs.watch unavailable; falling back to polling for route changes"));
|
|
441
638
|
pollingInterval = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
442
639
|
}
|
|
640
|
+
if (autoSyncHosts) {
|
|
641
|
+
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
642
|
+
}
|
|
443
643
|
const server = createProxyServer({
|
|
444
644
|
getRoutes: () => cachedRoutes,
|
|
445
645
|
proxyPort,
|
|
@@ -465,8 +665,8 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
465
665
|
process.exit(1);
|
|
466
666
|
});
|
|
467
667
|
server.listen(proxyPort, () => {
|
|
468
|
-
|
|
469
|
-
|
|
668
|
+
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
669
|
+
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
470
670
|
writeTlsMarker(store.dir, isTls);
|
|
471
671
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
472
672
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
@@ -482,14 +682,15 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
482
682
|
watcher.close();
|
|
483
683
|
}
|
|
484
684
|
try {
|
|
485
|
-
|
|
685
|
+
fs3.unlinkSync(store.pidPath);
|
|
486
686
|
} catch {
|
|
487
687
|
}
|
|
488
688
|
try {
|
|
489
|
-
|
|
689
|
+
fs3.unlinkSync(store.portFilePath);
|
|
490
690
|
} catch {
|
|
491
691
|
}
|
|
492
692
|
writeTlsMarker(store.dir, false);
|
|
693
|
+
if (autoSyncHosts) cleanHostsFile();
|
|
493
694
|
server.close(() => process.exit(0));
|
|
494
695
|
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
495
696
|
};
|
|
@@ -502,7 +703,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
502
703
|
const pidPath = store.pidPath;
|
|
503
704
|
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
504
705
|
const sudoHint = needsSudo ? "sudo " : "";
|
|
505
|
-
if (!
|
|
706
|
+
if (!fs3.existsSync(pidPath)) {
|
|
506
707
|
if (await isProxyRunning(proxyPort, tls2)) {
|
|
507
708
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
508
709
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -510,7 +711,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
510
711
|
try {
|
|
511
712
|
process.kill(pid, "SIGTERM");
|
|
512
713
|
try {
|
|
513
|
-
|
|
714
|
+
fs3.unlinkSync(store.portFilePath);
|
|
514
715
|
} catch {
|
|
515
716
|
}
|
|
516
717
|
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -541,19 +742,19 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
541
742
|
return;
|
|
542
743
|
}
|
|
543
744
|
try {
|
|
544
|
-
const pid = parseInt(
|
|
745
|
+
const pid = parseInt(fs3.readFileSync(pidPath, "utf-8"), 10);
|
|
545
746
|
if (isNaN(pid)) {
|
|
546
747
|
console.error(chalk.red("Corrupted PID file. Removing it."));
|
|
547
|
-
|
|
748
|
+
fs3.unlinkSync(pidPath);
|
|
548
749
|
return;
|
|
549
750
|
}
|
|
550
751
|
try {
|
|
551
752
|
process.kill(pid, 0);
|
|
552
753
|
} catch {
|
|
553
754
|
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
554
|
-
|
|
755
|
+
fs3.unlinkSync(pidPath);
|
|
555
756
|
try {
|
|
556
|
-
|
|
757
|
+
fs3.unlinkSync(store.portFilePath);
|
|
557
758
|
} catch {
|
|
558
759
|
}
|
|
559
760
|
return;
|
|
@@ -565,13 +766,13 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
565
766
|
)
|
|
566
767
|
);
|
|
567
768
|
console.log(chalk.yellow("Removing stale PID file."));
|
|
568
|
-
|
|
769
|
+
fs3.unlinkSync(pidPath);
|
|
569
770
|
return;
|
|
570
771
|
}
|
|
571
772
|
process.kill(pid, "SIGTERM");
|
|
572
|
-
|
|
773
|
+
fs3.unlinkSync(pidPath);
|
|
573
774
|
try {
|
|
574
|
-
|
|
775
|
+
fs3.unlinkSync(store.portFilePath);
|
|
575
776
|
} catch {
|
|
576
777
|
}
|
|
577
778
|
console.log(chalk.green("Proxy stopped."));
|
|
@@ -598,18 +799,26 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
598
799
|
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
599
800
|
for (const route of routes) {
|
|
600
801
|
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
802
|
+
const label = route.pid === 0 ? "(alias)" : `(pid ${route.pid})`;
|
|
601
803
|
console.log(
|
|
602
|
-
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(
|
|
804
|
+
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(label)}`
|
|
603
805
|
);
|
|
604
806
|
}
|
|
605
807
|
console.log();
|
|
606
808
|
}
|
|
607
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force) {
|
|
809
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, autoInfo, desiredPort) {
|
|
608
810
|
const hostname = parseHostname(name);
|
|
609
811
|
console.log(chalk.blue.bold(`
|
|
610
812
|
portless
|
|
611
813
|
`));
|
|
612
814
|
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
815
|
+
if (autoInfo) {
|
|
816
|
+
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
817
|
+
console.log(chalk.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
818
|
+
if (autoInfo.prefix) {
|
|
819
|
+
console.log(chalk.gray(`-- Prefix "${autoInfo.prefix}" (from ${autoInfo.prefixSource})`));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
613
822
|
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
614
823
|
const defaultPort = getDefaultPort();
|
|
615
824
|
const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
@@ -664,10 +873,10 @@ portless
|
|
|
664
873
|
const autoTls = readTlsMarker(stateDir);
|
|
665
874
|
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
666
875
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
667
|
-
const logPath =
|
|
876
|
+
const logPath = path3.join(stateDir, "proxy.log");
|
|
668
877
|
console.error(chalk.blue("Try starting the proxy manually to see the error:"));
|
|
669
878
|
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
670
|
-
if (
|
|
879
|
+
if (fs3.existsSync(logPath)) {
|
|
671
880
|
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
672
881
|
}
|
|
673
882
|
process.exit(1);
|
|
@@ -677,8 +886,12 @@ portless
|
|
|
677
886
|
} else {
|
|
678
887
|
console.log(chalk.gray("-- Proxy is running"));
|
|
679
888
|
}
|
|
680
|
-
const port = await findFreePort();
|
|
681
|
-
|
|
889
|
+
const port = desiredPort ?? await findFreePort();
|
|
890
|
+
if (desiredPort) {
|
|
891
|
+
console.log(chalk.green(`-- Using port ${port} (fixed)`));
|
|
892
|
+
} else {
|
|
893
|
+
console.log(chalk.green(`-- Using port ${port}`));
|
|
894
|
+
}
|
|
682
895
|
try {
|
|
683
896
|
store.addRoute(hostname, port, process.pid, force);
|
|
684
897
|
} catch (err) {
|
|
@@ -693,13 +906,18 @@ portless
|
|
|
693
906
|
-> ${finalUrl}
|
|
694
907
|
`));
|
|
695
908
|
injectFrameworkFlags(commandArgs, port);
|
|
696
|
-
console.log(
|
|
697
|
-
|
|
909
|
+
console.log(
|
|
910
|
+
chalk.gray(
|
|
911
|
+
`Running: PORT=${port} HOST=127.0.0.1 PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
|
|
912
|
+
`
|
|
913
|
+
)
|
|
914
|
+
);
|
|
698
915
|
spawnCommand(commandArgs, {
|
|
699
916
|
env: {
|
|
700
917
|
...process.env,
|
|
701
918
|
PORT: port.toString(),
|
|
702
919
|
HOST: "127.0.0.1",
|
|
920
|
+
PORTLESS_URL: finalUrl,
|
|
703
921
|
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
|
|
704
922
|
},
|
|
705
923
|
onCleanup: () => {
|
|
@@ -710,31 +928,120 @@ portless
|
|
|
710
928
|
}
|
|
711
929
|
});
|
|
712
930
|
}
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
process.stdin.setRawMode(false);
|
|
718
|
-
} catch {
|
|
719
|
-
}
|
|
720
|
-
});
|
|
931
|
+
function parseAppPort(value) {
|
|
932
|
+
if (!value || value.startsWith("--")) {
|
|
933
|
+
console.error(chalk.red("Error: --app-port requires a port number."));
|
|
934
|
+
process.exit(1);
|
|
721
935
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (isNpx || isPnpmDlx) {
|
|
726
|
-
console.error(chalk.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
727
|
-
console.error(chalk.blue("Install globally instead:"));
|
|
728
|
-
console.error(chalk.cyan(" npm install -g portless"));
|
|
936
|
+
const port = parseInt(value, 10);
|
|
937
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
938
|
+
console.error(chalk.red(`Error: Invalid app port "${value}". Must be 1-65535.`));
|
|
729
939
|
process.exit(1);
|
|
730
940
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
941
|
+
return port;
|
|
942
|
+
}
|
|
943
|
+
function appPortFromEnv() {
|
|
944
|
+
const envVal = process.env.PORTLESS_APP_PORT;
|
|
945
|
+
if (!envVal) return void 0;
|
|
946
|
+
const port = parseInt(envVal, 10);
|
|
947
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
948
|
+
console.error(chalk.red(`Error: Invalid PORTLESS_APP_PORT="${envVal}". Must be 1-65535.`));
|
|
949
|
+
process.exit(1);
|
|
735
950
|
}
|
|
736
|
-
|
|
737
|
-
|
|
951
|
+
return port;
|
|
952
|
+
}
|
|
953
|
+
function parseRunArgs(args) {
|
|
954
|
+
let force = false;
|
|
955
|
+
let appPort;
|
|
956
|
+
let i = 0;
|
|
957
|
+
while (i < args.length && args[i].startsWith("-")) {
|
|
958
|
+
if (args[i] === "--") {
|
|
959
|
+
i++;
|
|
960
|
+
break;
|
|
961
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
962
|
+
console.log(`
|
|
963
|
+
${chalk.bold("portless run")} - Infer project name and run through the proxy.
|
|
964
|
+
|
|
965
|
+
${chalk.bold("Usage:")}
|
|
966
|
+
${chalk.cyan("portless run [options] <command...>")}
|
|
967
|
+
|
|
968
|
+
${chalk.bold("Options:")}
|
|
969
|
+
--force Override an existing route registered by another process
|
|
970
|
+
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
971
|
+
--help, -h Show this help
|
|
972
|
+
|
|
973
|
+
${chalk.bold("Name inference (in order):")}
|
|
974
|
+
1. package.json "name" field (walks up directories)
|
|
975
|
+
2. Git repo root directory name
|
|
976
|
+
3. Current directory basename
|
|
977
|
+
|
|
978
|
+
In git worktrees, the branch name is prepended as a subdomain prefix
|
|
979
|
+
(e.g. feature-auth.myapp.localhost).
|
|
980
|
+
|
|
981
|
+
${chalk.bold("Examples:")}
|
|
982
|
+
portless run next dev # -> http://<project>.localhost:1355
|
|
983
|
+
portless run vite dev # -> http://<project>.localhost:1355
|
|
984
|
+
portless run --app-port 3000 pnpm start
|
|
985
|
+
`);
|
|
986
|
+
process.exit(0);
|
|
987
|
+
} else if (args[i] === "--force") {
|
|
988
|
+
force = true;
|
|
989
|
+
} else if (args[i] === "--app-port") {
|
|
990
|
+
i++;
|
|
991
|
+
appPort = parseAppPort(args[i]);
|
|
992
|
+
} else {
|
|
993
|
+
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
994
|
+
console.error(chalk.blue("Known flags: --force, --app-port, --help"));
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
i++;
|
|
998
|
+
}
|
|
999
|
+
if (!appPort) appPort = appPortFromEnv();
|
|
1000
|
+
return { force, appPort, commandArgs: args.slice(i) };
|
|
1001
|
+
}
|
|
1002
|
+
function parseAppArgs(args) {
|
|
1003
|
+
let force = false;
|
|
1004
|
+
let appPort;
|
|
1005
|
+
let i = 0;
|
|
1006
|
+
while (i < args.length && args[i].startsWith("-")) {
|
|
1007
|
+
if (args[i] === "--") {
|
|
1008
|
+
i++;
|
|
1009
|
+
break;
|
|
1010
|
+
} else if (args[i] === "--force") {
|
|
1011
|
+
force = true;
|
|
1012
|
+
} else if (args[i] === "--app-port") {
|
|
1013
|
+
i++;
|
|
1014
|
+
appPort = parseAppPort(args[i]);
|
|
1015
|
+
} else {
|
|
1016
|
+
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
1017
|
+
console.error(chalk.blue("Known flags: --force, --app-port"));
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
i++;
|
|
1021
|
+
}
|
|
1022
|
+
const name = args[i];
|
|
1023
|
+
i++;
|
|
1024
|
+
while (i < args.length && args[i].startsWith("--")) {
|
|
1025
|
+
if (args[i] === "--") {
|
|
1026
|
+
i++;
|
|
1027
|
+
break;
|
|
1028
|
+
} else if (args[i] === "--force") {
|
|
1029
|
+
force = true;
|
|
1030
|
+
} else if (args[i] === "--app-port") {
|
|
1031
|
+
i++;
|
|
1032
|
+
appPort = parseAppPort(args[i]);
|
|
1033
|
+
} else {
|
|
1034
|
+
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
1035
|
+
console.error(chalk.blue("Known flags: --force, --app-port"));
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
i++;
|
|
1039
|
+
}
|
|
1040
|
+
if (!appPort) appPort = appPortFromEnv();
|
|
1041
|
+
return { force, appPort, name, commandArgs: args.slice(i) };
|
|
1042
|
+
}
|
|
1043
|
+
function printHelp() {
|
|
1044
|
+
console.log(`
|
|
738
1045
|
${chalk.bold("portless")} - Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
739
1046
|
|
|
740
1047
|
Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
@@ -750,8 +1057,13 @@ ${chalk.bold("Usage:")}
|
|
|
750
1057
|
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
751
1058
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
752
1059
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1060
|
+
${chalk.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
1061
|
+
${chalk.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
1062
|
+
${chalk.cyan("portless alias --remove <name>")} Remove a static route
|
|
753
1063
|
${chalk.cyan("portless list")} Show active routes
|
|
754
1064
|
${chalk.cyan("portless trust")} Add local CA to system trust store
|
|
1065
|
+
${chalk.cyan("portless hosts sync")} Add routes to /etc/hosts (fixes Safari)
|
|
1066
|
+
${chalk.cyan("portless hosts clean")} Remove portless entries from /etc/hosts
|
|
755
1067
|
|
|
756
1068
|
${chalk.bold("Examples:")}
|
|
757
1069
|
portless proxy start # Start proxy on port 1355
|
|
@@ -759,21 +1071,25 @@ ${chalk.bold("Examples:")}
|
|
|
759
1071
|
portless myapp next dev # -> http://myapp.localhost:1355
|
|
760
1072
|
portless myapp vite dev # -> http://myapp.localhost:1355
|
|
761
1073
|
portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
|
|
1074
|
+
portless run next dev # -> http://<project>.localhost:1355
|
|
1075
|
+
portless run next dev # in worktree -> http://<worktree>.<project>.localhost:1355
|
|
1076
|
+
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
762
1077
|
|
|
763
1078
|
${chalk.bold("In package.json:")}
|
|
764
1079
|
{
|
|
765
1080
|
"scripts": {
|
|
766
|
-
"dev": "portless
|
|
1081
|
+
"dev": "portless run next dev"
|
|
767
1082
|
}
|
|
768
1083
|
}
|
|
769
1084
|
|
|
770
1085
|
${chalk.bold("How it works:")}
|
|
771
1086
|
1. Start the proxy once (listens on port 1355 by default, no sudo needed)
|
|
772
1087
|
2. Run your apps - they auto-start the proxy and register automatically
|
|
1088
|
+
(apps get a random port in the 4000-4999 range via PORT)
|
|
773
1089
|
3. Access via http://<name>.localhost:1355
|
|
774
1090
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
775
|
-
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular
|
|
776
|
-
--port and --host flags injected automatically
|
|
1091
|
+
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular,
|
|
1092
|
+
Expo, React Native) get --port and --host flags injected automatically
|
|
777
1093
|
|
|
778
1094
|
${chalk.bold("HTTP/2 + HTTPS:")}
|
|
779
1095
|
Use --https for HTTP/2 multiplexing (faster dev server page loads).
|
|
@@ -781,6 +1097,8 @@ ${chalk.bold("HTTP/2 + HTTPS:")}
|
|
|
781
1097
|
system trust store. No browser warnings. No sudo required on macOS.
|
|
782
1098
|
|
|
783
1099
|
${chalk.bold("Options:")}
|
|
1100
|
+
run <cmd> Infer project name from package.json / git / cwd
|
|
1101
|
+
Adds worktree prefix in git worktrees
|
|
784
1102
|
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
785
1103
|
Ports < 1024 require sudo
|
|
786
1104
|
--https Enable HTTP/2 + TLS with auto-generated certs
|
|
@@ -788,252 +1106,423 @@ ${chalk.bold("Options:")}
|
|
|
788
1106
|
--key <path> Use a custom TLS private key (implies --https)
|
|
789
1107
|
--no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
790
1108
|
--foreground Run proxy in foreground (for debugging)
|
|
1109
|
+
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
791
1110
|
--force Override an existing route registered by another process
|
|
1111
|
+
--name <name> Use <name> as the app name (bypasses subcommand dispatch)
|
|
1112
|
+
-- Stop flag parsing; everything after is passed to the child
|
|
792
1113
|
|
|
793
1114
|
${chalk.bold("Environment variables:")}
|
|
794
1115
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
795
|
-
|
|
1116
|
+
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1117
|
+
PORTLESS_HTTPS=1|true Always enable HTTPS (set in .bashrc / .zshrc)
|
|
1118
|
+
PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (requires sudo proxy start)
|
|
796
1119
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
797
1120
|
PORTLESS=0 | PORTLESS=skip Run command directly without proxy
|
|
798
1121
|
|
|
1122
|
+
${chalk.bold("Child process environment:")}
|
|
1123
|
+
PORT Ephemeral port the child should listen on
|
|
1124
|
+
HOST Always 127.0.0.1
|
|
1125
|
+
PORTLESS_URL Public URL of the app (e.g. http://myapp.localhost:1355)
|
|
1126
|
+
|
|
1127
|
+
${chalk.bold("Safari / DNS:")}
|
|
1128
|
+
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1129
|
+
Safari relies on the system DNS resolver, which may not handle them.
|
|
1130
|
+
If Safari can't find your .localhost URL, run:
|
|
1131
|
+
${chalk.cyan("sudo portless hosts sync")}
|
|
1132
|
+
This adds entries to /etc/hosts. Clean up later with:
|
|
1133
|
+
${chalk.cyan("sudo portless hosts clean")}
|
|
1134
|
+
To auto-sync whenever routes change, set PORTLESS_SYNC_HOSTS=1 and
|
|
1135
|
+
start the proxy with sudo.
|
|
1136
|
+
|
|
799
1137
|
${chalk.bold("Skip portless:")}
|
|
800
1138
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
801
1139
|
PORTLESS=skip pnpm dev # Same as above
|
|
1140
|
+
|
|
1141
|
+
${chalk.bold("Reserved names:")}
|
|
1142
|
+
run, alias, hosts, list, trust, proxy are subcommands and cannot be
|
|
1143
|
+
used as app names directly. Use "portless run" to infer the name, or
|
|
1144
|
+
"portless --name <name>" to force any name including reserved ones.
|
|
1145
|
+
`);
|
|
1146
|
+
process.exit(0);
|
|
1147
|
+
}
|
|
1148
|
+
function printVersion() {
|
|
1149
|
+
console.log("0.5.1");
|
|
1150
|
+
process.exit(0);
|
|
1151
|
+
}
|
|
1152
|
+
async function handleTrust() {
|
|
1153
|
+
const { dir } = await discoverState();
|
|
1154
|
+
const result = trustCA(dir);
|
|
1155
|
+
if (result.trusted) {
|
|
1156
|
+
console.log(chalk.green("Local CA added to system trust store."));
|
|
1157
|
+
console.log(chalk.gray("Browsers will now trust portless HTTPS certificates."));
|
|
1158
|
+
} else {
|
|
1159
|
+
console.error(chalk.red(`Failed to trust CA: ${result.error}`));
|
|
1160
|
+
if (result.error?.includes("sudo")) {
|
|
1161
|
+
console.error(chalk.blue("Run with sudo:"));
|
|
1162
|
+
console.error(chalk.cyan(" sudo portless trust"));
|
|
1163
|
+
}
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async function handleList() {
|
|
1168
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1169
|
+
const store = new RouteStore(dir, {
|
|
1170
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1171
|
+
});
|
|
1172
|
+
listRoutes(store, port, tls2);
|
|
1173
|
+
}
|
|
1174
|
+
async function handleAlias(args) {
|
|
1175
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
1176
|
+
console.log(`
|
|
1177
|
+
${chalk.bold("portless alias")} - Register a static route for services not managed by portless.
|
|
1178
|
+
|
|
1179
|
+
${chalk.bold("Usage:")}
|
|
1180
|
+
${chalk.cyan("portless alias <name> <port>")} Register a route
|
|
1181
|
+
${chalk.cyan("portless alias --remove <name>")} Remove a route
|
|
1182
|
+
${chalk.cyan("portless alias <name> <port> --force")} Override existing route
|
|
1183
|
+
|
|
1184
|
+
${chalk.bold("Examples:")}
|
|
1185
|
+
portless alias my-postgres 5432 # -> http://my-postgres.localhost:1355
|
|
1186
|
+
portless alias redis 6379 # -> http://redis.localhost:1355
|
|
1187
|
+
portless alias --remove my-postgres # Remove the alias
|
|
802
1188
|
`);
|
|
803
1189
|
process.exit(0);
|
|
804
1190
|
}
|
|
805
|
-
|
|
806
|
-
|
|
1191
|
+
const { dir } = await discoverState();
|
|
1192
|
+
const store = new RouteStore(dir, {
|
|
1193
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1194
|
+
});
|
|
1195
|
+
if (args[1] === "--remove") {
|
|
1196
|
+
const aliasName2 = args[2];
|
|
1197
|
+
if (!aliasName2) {
|
|
1198
|
+
console.error(chalk.red("Error: No alias name provided."));
|
|
1199
|
+
console.error(chalk.cyan(" portless alias --remove <name>"));
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
const hostname2 = parseHostname(aliasName2);
|
|
1203
|
+
const routes = store.loadRoutes();
|
|
1204
|
+
const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
|
|
1205
|
+
if (!existing) {
|
|
1206
|
+
console.error(chalk.red(`Error: No alias found for "${hostname2}".`));
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
store.removeRoute(hostname2);
|
|
1210
|
+
console.log(chalk.green(`Removed alias: ${hostname2}`));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const aliasName = args[1];
|
|
1214
|
+
const aliasPort = args[2];
|
|
1215
|
+
if (!aliasName || !aliasPort) {
|
|
1216
|
+
console.error(chalk.red("Error: Missing arguments."));
|
|
1217
|
+
console.error(chalk.blue("Usage:"));
|
|
1218
|
+
console.error(chalk.cyan(" portless alias <name> <port>"));
|
|
1219
|
+
console.error(chalk.cyan(" portless alias --remove <name>"));
|
|
1220
|
+
console.error(chalk.blue("Example:"));
|
|
1221
|
+
console.error(chalk.cyan(" portless alias my-postgres 5432"));
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const hostname = parseHostname(aliasName);
|
|
1225
|
+
const port = parseInt(aliasPort, 10);
|
|
1226
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1227
|
+
console.error(chalk.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
const force = args.includes("--force");
|
|
1231
|
+
store.addRoute(hostname, port, 0, force);
|
|
1232
|
+
console.log(chalk.green(`Alias registered: ${hostname} -> localhost:${port}`));
|
|
1233
|
+
}
|
|
1234
|
+
async function handleHosts(args) {
|
|
1235
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
1236
|
+
console.log(`
|
|
1237
|
+
${chalk.bold("portless hosts")} - Manage /etc/hosts entries for .localhost subdomains.
|
|
1238
|
+
|
|
1239
|
+
Safari relies on the system DNS resolver, which may not handle .localhost
|
|
1240
|
+
subdomains. This command adds entries to /etc/hosts as a workaround.
|
|
1241
|
+
|
|
1242
|
+
${chalk.bold("Usage:")}
|
|
1243
|
+
${chalk.cyan("sudo portless hosts sync")} Add current routes to /etc/hosts
|
|
1244
|
+
${chalk.cyan("sudo portless hosts clean")} Remove portless entries from /etc/hosts
|
|
1245
|
+
|
|
1246
|
+
${chalk.bold("Auto-sync:")}
|
|
1247
|
+
Set PORTLESS_SYNC_HOSTS=1 and start the proxy with sudo to auto-sync
|
|
1248
|
+
/etc/hosts whenever routes change.
|
|
1249
|
+
`);
|
|
807
1250
|
process.exit(0);
|
|
808
1251
|
}
|
|
809
|
-
if (args[
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (result.trusted) {
|
|
813
|
-
console.log(chalk.green("Local CA added to system trust store."));
|
|
814
|
-
console.log(chalk.gray("Browsers will now trust portless HTTPS certificates."));
|
|
1252
|
+
if (args[1] === "clean") {
|
|
1253
|
+
if (cleanHostsFile()) {
|
|
1254
|
+
console.log(chalk.green("Removed portless entries from /etc/hosts."));
|
|
815
1255
|
} else {
|
|
816
|
-
console.error(chalk.red(
|
|
817
|
-
|
|
818
|
-
console.error(chalk.blue("Run with sudo:"));
|
|
819
|
-
console.error(chalk.cyan(" sudo portless trust"));
|
|
820
|
-
}
|
|
1256
|
+
console.error(chalk.red("Failed to update /etc/hosts (requires sudo)."));
|
|
1257
|
+
console.error(chalk.cyan(" sudo portless hosts clean"));
|
|
821
1258
|
process.exit(1);
|
|
822
1259
|
}
|
|
823
1260
|
return;
|
|
824
1261
|
}
|
|
825
|
-
if (args[
|
|
826
|
-
|
|
827
|
-
|
|
1262
|
+
if (!args[1]) {
|
|
1263
|
+
console.log(`
|
|
1264
|
+
${chalk.bold("Usage: portless hosts <command>")}
|
|
1265
|
+
|
|
1266
|
+
${chalk.cyan("sudo portless hosts sync")} Add current routes to /etc/hosts
|
|
1267
|
+
${chalk.cyan("sudo portless hosts clean")} Remove portless entries from /etc/hosts
|
|
1268
|
+
`);
|
|
1269
|
+
process.exit(0);
|
|
1270
|
+
}
|
|
1271
|
+
if (args[1] !== "sync") {
|
|
1272
|
+
console.error(chalk.red(`Error: Unknown hosts subcommand "${args[1]}".`));
|
|
1273
|
+
console.error(chalk.blue("Usage:"));
|
|
1274
|
+
console.error(chalk.cyan(" portless hosts sync # Add routes to /etc/hosts"));
|
|
1275
|
+
console.error(chalk.cyan(" portless hosts clean # Remove portless entries"));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
const { dir } = await discoverState();
|
|
1279
|
+
const store = new RouteStore(dir, {
|
|
1280
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1281
|
+
});
|
|
1282
|
+
const routes = store.loadRoutes();
|
|
1283
|
+
if (routes.length === 0) {
|
|
1284
|
+
console.log(chalk.yellow("No active routes to sync."));
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const hostnames = routes.map((r) => r.hostname);
|
|
1288
|
+
if (syncHostsFile(hostnames)) {
|
|
1289
|
+
console.log(chalk.green(`Synced ${hostnames.length} hostname(s) to /etc/hosts:`));
|
|
1290
|
+
for (const h of hostnames) {
|
|
1291
|
+
console.log(chalk.cyan(` 127.0.0.1 ${h}`));
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
console.error(chalk.red("Failed to update /etc/hosts (requires sudo)."));
|
|
1295
|
+
console.error(chalk.cyan(" sudo portless hosts sync"));
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async function handleProxy(args) {
|
|
1300
|
+
if (args[1] === "stop") {
|
|
1301
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1302
|
+
const store2 = new RouteStore(dir, {
|
|
828
1303
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
829
1304
|
});
|
|
830
|
-
|
|
1305
|
+
await stopProxy(store2, port, tls2);
|
|
831
1306
|
return;
|
|
832
1307
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
838
|
-
});
|
|
839
|
-
await stopProxy(store3, port2, tls3);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
if (args[1] !== "start") {
|
|
843
|
-
console.log(`
|
|
844
|
-
${chalk.bold("Usage: portless proxy <command>")}
|
|
1308
|
+
const isProxyHelp = args[1] === "--help" || args[1] === "-h";
|
|
1309
|
+
if (isProxyHelp || args[1] !== "start") {
|
|
1310
|
+
console.log(`
|
|
1311
|
+
${chalk.bold("portless proxy")} - Manage the portless proxy server.
|
|
845
1312
|
|
|
1313
|
+
${chalk.bold("Usage:")}
|
|
846
1314
|
${chalk.cyan("portless proxy start")} Start the proxy (daemon)
|
|
847
1315
|
${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
|
|
848
1316
|
${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
849
1317
|
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
850
1318
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
851
1319
|
`);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
process.exit(1);
|
|
865
|
-
}
|
|
866
|
-
proxyPort = parseInt(portValue, 10);
|
|
867
|
-
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
868
|
-
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
869
|
-
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
870
|
-
process.exit(1);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
const hasNoTls = args.includes("--no-tls");
|
|
874
|
-
const hasHttpsFlag = args.includes("--https");
|
|
875
|
-
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
876
|
-
let customCertPath = null;
|
|
877
|
-
let customKeyPath = null;
|
|
878
|
-
const certIdx = args.indexOf("--cert");
|
|
879
|
-
if (certIdx !== -1) {
|
|
880
|
-
customCertPath = args[certIdx + 1] || null;
|
|
881
|
-
if (!customCertPath || customCertPath.startsWith("-")) {
|
|
882
|
-
console.error(chalk.red("Error: --cert requires a file path."));
|
|
883
|
-
process.exit(1);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
const keyIdx = args.indexOf("--key");
|
|
887
|
-
if (keyIdx !== -1) {
|
|
888
|
-
customKeyPath = args[keyIdx + 1] || null;
|
|
889
|
-
if (!customKeyPath || customKeyPath.startsWith("-")) {
|
|
890
|
-
console.error(chalk.red("Error: --key requires a file path."));
|
|
891
|
-
process.exit(1);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
|
|
895
|
-
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
1320
|
+
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1321
|
+
}
|
|
1322
|
+
const isForeground = args.includes("--foreground");
|
|
1323
|
+
let proxyPort = getDefaultPort();
|
|
1324
|
+
let portFlagIndex = args.indexOf("--port");
|
|
1325
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1326
|
+
if (portFlagIndex !== -1) {
|
|
1327
|
+
const portValue = args[portFlagIndex + 1];
|
|
1328
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
1329
|
+
console.error(chalk.red("Error: --port / -p requires a port number."));
|
|
1330
|
+
console.error(chalk.blue("Usage:"));
|
|
1331
|
+
console.error(chalk.cyan(" portless proxy start -p 8080"));
|
|
896
1332
|
process.exit(1);
|
|
897
1333
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (await isProxyRunning(proxyPort)) {
|
|
904
|
-
if (isForeground) {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
908
|
-
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
909
|
-
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
910
|
-
console.log(
|
|
911
|
-
chalk.blue(`To restart: portless proxy stop && ${sudoPrefix}portless proxy start`)
|
|
912
|
-
);
|
|
913
|
-
return;
|
|
1334
|
+
proxyPort = parseInt(portValue, 10);
|
|
1335
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1336
|
+
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
1337
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
1338
|
+
process.exit(1);
|
|
914
1339
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1340
|
+
}
|
|
1341
|
+
const hasNoTls = args.includes("--no-tls");
|
|
1342
|
+
const hasHttpsFlag = args.includes("--https");
|
|
1343
|
+
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
1344
|
+
let customCertPath = null;
|
|
1345
|
+
let customKeyPath = null;
|
|
1346
|
+
const certIdx = args.indexOf("--cert");
|
|
1347
|
+
if (certIdx !== -1) {
|
|
1348
|
+
customCertPath = args[certIdx + 1] || null;
|
|
1349
|
+
if (!customCertPath || customCertPath.startsWith("-")) {
|
|
1350
|
+
console.error(chalk.red("Error: --cert requires a file path."));
|
|
921
1351
|
process.exit(1);
|
|
922
1352
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const certStr = cert.toString("utf-8");
|
|
931
|
-
const keyStr = key.toString("utf-8");
|
|
932
|
-
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
933
|
-
console.error(chalk.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
|
|
934
|
-
console.error(chalk.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
|
|
935
|
-
process.exit(1);
|
|
936
|
-
}
|
|
937
|
-
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
938
|
-
console.error(chalk.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
939
|
-
console.error(
|
|
940
|
-
chalk.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
941
|
-
);
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
tlsOptions = { cert, key };
|
|
945
|
-
} catch (err) {
|
|
946
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
947
|
-
console.error(chalk.red(`Error reading certificate files: ${message}`));
|
|
948
|
-
process.exit(1);
|
|
949
|
-
}
|
|
950
|
-
} else {
|
|
951
|
-
console.log(chalk.gray("Ensuring TLS certificates..."));
|
|
952
|
-
const certs = ensureCerts(stateDir);
|
|
953
|
-
if (certs.caGenerated) {
|
|
954
|
-
console.log(chalk.green("Generated local CA certificate."));
|
|
955
|
-
}
|
|
956
|
-
if (!isCATrusted(stateDir)) {
|
|
957
|
-
console.log(chalk.yellow("Adding CA to system trust store..."));
|
|
958
|
-
const trustResult = trustCA(stateDir);
|
|
959
|
-
if (trustResult.trusted) {
|
|
960
|
-
console.log(
|
|
961
|
-
chalk.green("CA added to system trust store. Browsers will trust portless certs.")
|
|
962
|
-
);
|
|
963
|
-
} else {
|
|
964
|
-
console.warn(chalk.yellow("Could not add CA to system trust store."));
|
|
965
|
-
if (trustResult.error) {
|
|
966
|
-
console.warn(chalk.gray(trustResult.error));
|
|
967
|
-
}
|
|
968
|
-
console.warn(
|
|
969
|
-
chalk.yellow("Browsers will show certificate warnings. To fix this later, run:")
|
|
970
|
-
);
|
|
971
|
-
console.warn(chalk.cyan(" portless trust"));
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
const cert = fs2.readFileSync(certs.certPath);
|
|
975
|
-
const key = fs2.readFileSync(certs.keyPath);
|
|
976
|
-
tlsOptions = {
|
|
977
|
-
cert,
|
|
978
|
-
key,
|
|
979
|
-
SNICallback: createSNICallback(stateDir, cert, key)
|
|
980
|
-
};
|
|
981
|
-
}
|
|
1353
|
+
}
|
|
1354
|
+
const keyIdx = args.indexOf("--key");
|
|
1355
|
+
if (keyIdx !== -1) {
|
|
1356
|
+
customKeyPath = args[keyIdx + 1] || null;
|
|
1357
|
+
if (!customKeyPath || customKeyPath.startsWith("-")) {
|
|
1358
|
+
console.error(chalk.red("Error: --key requires a file path."));
|
|
1359
|
+
process.exit(1);
|
|
982
1360
|
}
|
|
1361
|
+
}
|
|
1362
|
+
if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
|
|
1363
|
+
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1367
|
+
const stateDir = resolveStateDir(proxyPort);
|
|
1368
|
+
const store = new RouteStore(stateDir, {
|
|
1369
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1370
|
+
});
|
|
1371
|
+
if (await isProxyRunning(proxyPort)) {
|
|
983
1372
|
if (isForeground) {
|
|
984
|
-
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
985
|
-
startProxyServer(store2, proxyPort, tlsOptions);
|
|
986
1373
|
return;
|
|
987
1374
|
}
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
1375
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1376
|
+
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1377
|
+
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1378
|
+
console.log(chalk.blue(`To restart: portless proxy stop && ${sudoPrefix}portless proxy start`));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1382
|
+
console.error(chalk.red(`Error: Port ${proxyPort} requires sudo.`));
|
|
1383
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
1384
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
1385
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
1386
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
let tlsOptions;
|
|
1390
|
+
if (useHttps) {
|
|
1391
|
+
store.ensureDir();
|
|
1392
|
+
if (customCertPath && customKeyPath) {
|
|
992
1393
|
try {
|
|
993
|
-
|
|
994
|
-
|
|
1394
|
+
const cert = fs3.readFileSync(customCertPath);
|
|
1395
|
+
const key = fs3.readFileSync(customKeyPath);
|
|
1396
|
+
const certStr = cert.toString("utf-8");
|
|
1397
|
+
const keyStr = key.toString("utf-8");
|
|
1398
|
+
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
1399
|
+
console.error(chalk.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
|
|
1400
|
+
console.error(chalk.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1404
|
+
console.error(chalk.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1405
|
+
console.error(chalk.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----"));
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
tlsOptions = { cert, key };
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1411
|
+
console.error(chalk.red(`Error reading certificate files: ${message}`));
|
|
1412
|
+
process.exit(1);
|
|
995
1413
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1414
|
+
} else {
|
|
1415
|
+
console.log(chalk.gray("Ensuring TLS certificates..."));
|
|
1416
|
+
const certs = ensureCerts(stateDir);
|
|
1417
|
+
if (certs.caGenerated) {
|
|
1418
|
+
console.log(chalk.green("Generated local CA certificate."));
|
|
1000
1419
|
}
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
|
|
1420
|
+
if (!isCATrusted(stateDir)) {
|
|
1421
|
+
console.log(chalk.yellow("Adding CA to system trust store..."));
|
|
1422
|
+
const trustResult = trustCA(stateDir);
|
|
1423
|
+
if (trustResult.trusted) {
|
|
1424
|
+
console.log(
|
|
1425
|
+
chalk.green("CA added to system trust store. Browsers will trust portless certs.")
|
|
1426
|
+
);
|
|
1004
1427
|
} else {
|
|
1005
|
-
|
|
1428
|
+
console.warn(chalk.yellow("Could not add CA to system trust store."));
|
|
1429
|
+
if (trustResult.error) {
|
|
1430
|
+
console.warn(chalk.gray(trustResult.error));
|
|
1431
|
+
}
|
|
1432
|
+
console.warn(
|
|
1433
|
+
chalk.yellow("Browsers will show certificate warnings. To fix this later, run:")
|
|
1434
|
+
);
|
|
1435
|
+
console.warn(chalk.cyan(" portless trust"));
|
|
1006
1436
|
}
|
|
1007
1437
|
}
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
fs2.closeSync(logFd);
|
|
1438
|
+
const cert = fs3.readFileSync(certs.certPath);
|
|
1439
|
+
const key = fs3.readFileSync(certs.keyPath);
|
|
1440
|
+
tlsOptions = {
|
|
1441
|
+
cert,
|
|
1442
|
+
key,
|
|
1443
|
+
SNICallback: createSNICallback(stateDir, cert, key)
|
|
1444
|
+
};
|
|
1016
1445
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1446
|
+
}
|
|
1447
|
+
if (isForeground) {
|
|
1448
|
+
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
1449
|
+
startProxyServer(store, proxyPort, tlsOptions);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
store.ensureDir();
|
|
1453
|
+
const logPath = path3.join(stateDir, "proxy.log");
|
|
1454
|
+
const logFd = fs3.openSync(logPath, "a");
|
|
1455
|
+
try {
|
|
1456
|
+
try {
|
|
1457
|
+
fs3.chmodSync(logPath, FILE_MODE);
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
fixOwnership(logPath);
|
|
1461
|
+
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
1462
|
+
if (portFlagIndex !== -1) {
|
|
1463
|
+
daemonArgs.push("--port", proxyPort.toString());
|
|
1464
|
+
}
|
|
1465
|
+
if (useHttps) {
|
|
1466
|
+
if (customCertPath && customKeyPath) {
|
|
1467
|
+
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1468
|
+
} else {
|
|
1469
|
+
daemonArgs.push("--https");
|
|
1024
1470
|
}
|
|
1025
|
-
process.exit(1);
|
|
1026
1471
|
}
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1472
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
1473
|
+
detached: true,
|
|
1474
|
+
stdio: ["ignore", logFd, logFd],
|
|
1475
|
+
env: process.env
|
|
1476
|
+
});
|
|
1477
|
+
child.unref();
|
|
1478
|
+
} finally {
|
|
1479
|
+
fs3.closeSync(logFd);
|
|
1030
1480
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1481
|
+
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1482
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1483
|
+
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1484
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1485
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1486
|
+
if (fs3.existsSync(logPath)) {
|
|
1487
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
1488
|
+
}
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
1492
|
+
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1493
|
+
}
|
|
1494
|
+
async function handleRunMode(args) {
|
|
1495
|
+
const parsed = parseRunArgs(args);
|
|
1496
|
+
if (parsed.commandArgs.length === 0) {
|
|
1497
|
+
console.error(chalk.red("Error: No command provided."));
|
|
1498
|
+
console.error(chalk.blue("Usage:"));
|
|
1499
|
+
console.error(chalk.cyan(" portless run <command...>"));
|
|
1500
|
+
console.error(chalk.blue("Example:"));
|
|
1501
|
+
console.error(chalk.cyan(" portless run next dev"));
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
const inferred = inferProjectName();
|
|
1505
|
+
const worktree = detectWorktreePrefix();
|
|
1506
|
+
const effectiveName = worktree ? `${worktree.prefix}.${inferred.name}` : inferred.name;
|
|
1507
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1508
|
+
const store = new RouteStore(dir, {
|
|
1509
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1510
|
+
});
|
|
1511
|
+
await runApp(
|
|
1512
|
+
store,
|
|
1513
|
+
port,
|
|
1514
|
+
dir,
|
|
1515
|
+
effectiveName,
|
|
1516
|
+
parsed.commandArgs,
|
|
1517
|
+
tls2,
|
|
1518
|
+
parsed.force,
|
|
1519
|
+
{ nameSource: inferred.source, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
1520
|
+
parsed.appPort
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
async function handleNamedMode(args) {
|
|
1524
|
+
const parsed = parseAppArgs(args);
|
|
1525
|
+
if (parsed.commandArgs.length === 0) {
|
|
1037
1526
|
console.error(chalk.red("Error: No command provided."));
|
|
1038
1527
|
console.error(chalk.blue("Usage:"));
|
|
1039
1528
|
console.error(chalk.cyan(" portless <name> <command...>"));
|
|
@@ -1045,7 +1534,105 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1045
1534
|
const store = new RouteStore(dir, {
|
|
1046
1535
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1047
1536
|
});
|
|
1048
|
-
await runApp(
|
|
1537
|
+
await runApp(
|
|
1538
|
+
store,
|
|
1539
|
+
port,
|
|
1540
|
+
dir,
|
|
1541
|
+
parsed.name,
|
|
1542
|
+
parsed.commandArgs,
|
|
1543
|
+
tls2,
|
|
1544
|
+
parsed.force,
|
|
1545
|
+
void 0,
|
|
1546
|
+
parsed.appPort
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
async function main() {
|
|
1550
|
+
if (process.stdin.isTTY) {
|
|
1551
|
+
process.on("exit", () => {
|
|
1552
|
+
try {
|
|
1553
|
+
process.stdin.setRawMode(false);
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
const args = process.argv.slice(2);
|
|
1559
|
+
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
1560
|
+
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
1561
|
+
if (isNpx || isPnpmDlx) {
|
|
1562
|
+
console.error(chalk.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
1563
|
+
console.error(chalk.blue("Install globally instead:"));
|
|
1564
|
+
console.error(chalk.cyan(" npm install -g portless"));
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
if (args[0] === "--name") {
|
|
1568
|
+
args.shift();
|
|
1569
|
+
if (!args[0]) {
|
|
1570
|
+
console.error(chalk.red("Error: --name requires an app name."));
|
|
1571
|
+
console.error(chalk.cyan(" portless --name <name> <command...>"));
|
|
1572
|
+
process.exit(1);
|
|
1573
|
+
}
|
|
1574
|
+
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1575
|
+
if (skipPortless2) {
|
|
1576
|
+
const { commandArgs } = parseAppArgs(args);
|
|
1577
|
+
if (commandArgs.length === 0) {
|
|
1578
|
+
console.error(chalk.red("Error: No command provided."));
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
spawnCommand(commandArgs);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
await handleNamedMode(args);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const isRunCommand = args[0] === "run";
|
|
1588
|
+
if (isRunCommand) {
|
|
1589
|
+
args.shift();
|
|
1590
|
+
}
|
|
1591
|
+
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1592
|
+
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
1593
|
+
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
1594
|
+
if (commandArgs.length === 0) {
|
|
1595
|
+
console.error(chalk.red("Error: No command provided."));
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
spawnCommand(commandArgs);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (!isRunCommand) {
|
|
1602
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
1603
|
+
printHelp();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
1607
|
+
printVersion();
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (args[0] === "trust") {
|
|
1611
|
+
await handleTrust();
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (args[0] === "list") {
|
|
1615
|
+
await handleList();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (args[0] === "alias") {
|
|
1619
|
+
await handleAlias(args);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (args[0] === "hosts") {
|
|
1623
|
+
await handleHosts(args);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (args[0] === "proxy") {
|
|
1627
|
+
await handleProxy(args);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (isRunCommand) {
|
|
1632
|
+
await handleRunMode(args);
|
|
1633
|
+
} else {
|
|
1634
|
+
await handleNamedMode(args);
|
|
1635
|
+
}
|
|
1049
1636
|
}
|
|
1050
1637
|
main().catch((err) => {
|
|
1051
1638
|
const message = err instanceof Error ? err.message : String(err);
|