portless 0.11.0 → 0.11.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 +2 -1
- package/dist/{chunk-6PDLZVDS.js → chunk-RRH2JUIU.js} +53 -0
- package/dist/cli.js +113 -24
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,6 +289,7 @@ portless alias --remove <name> # Remove a static route
|
|
|
289
289
|
portless list # Show active routes
|
|
290
290
|
portless trust # Add local CA to system trust store
|
|
291
291
|
portless clean # Remove state, CA trust entry, and hosts block
|
|
292
|
+
portless prune # Kill orphaned dev servers from crashed sessions
|
|
292
293
|
portless hosts sync # Add routes to /etc/hosts (fixes Safari)
|
|
293
294
|
portless hosts clean # Remove portless entries from /etc/hosts
|
|
294
295
|
|
|
@@ -344,7 +345,7 @@ PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
|
344
345
|
NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
|
|
345
346
|
```
|
|
346
347
|
|
|
347
|
-
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
|
|
348
|
+
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, `prune`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
|
|
348
349
|
|
|
349
350
|
## Uninstall / reset
|
|
350
351
|
|
|
@@ -853,6 +853,59 @@ var RouteStore = class _RouteStore {
|
|
|
853
853
|
}
|
|
854
854
|
return killedPid;
|
|
855
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Load all routes from disk without filtering out dead PIDs. Used by
|
|
858
|
+
* `portless prune` to discover stale entries whose owning CLI is gone
|
|
859
|
+
* but whose dev server may still be holding a port.
|
|
860
|
+
*/
|
|
861
|
+
loadRoutesRaw() {
|
|
862
|
+
if (!fs3.existsSync(this.routesPath)) {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const raw = fs3.readFileSync(this.routesPath, "utf-8");
|
|
867
|
+
let parsed;
|
|
868
|
+
try {
|
|
869
|
+
parsed = JSON.parse(raw);
|
|
870
|
+
} catch {
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
if (!Array.isArray(parsed)) {
|
|
874
|
+
return [];
|
|
875
|
+
}
|
|
876
|
+
return parsed.filter(isValidRoute);
|
|
877
|
+
} catch {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Remove all route entries whose owning process is dead and persist the
|
|
883
|
+
* result. Returns the removed stale entries so the caller can act on them.
|
|
884
|
+
*/
|
|
885
|
+
pruneStaleRoutes() {
|
|
886
|
+
this.ensureDir();
|
|
887
|
+
if (!this.acquireLock()) {
|
|
888
|
+
throw new Error("Failed to acquire route lock");
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const all = this.loadRoutesRaw();
|
|
892
|
+
const alive = [];
|
|
893
|
+
const stale = [];
|
|
894
|
+
for (const r of all) {
|
|
895
|
+
if (r.pid === 0 || this.isProcessAlive(r.pid)) {
|
|
896
|
+
alive.push(r);
|
|
897
|
+
} else {
|
|
898
|
+
stale.push(r);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (stale.length > 0) {
|
|
902
|
+
this.saveRoutes(alive);
|
|
903
|
+
}
|
|
904
|
+
return stale;
|
|
905
|
+
} finally {
|
|
906
|
+
this.releaseLock();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
856
909
|
removeRoute(hostname) {
|
|
857
910
|
this.ensureDir();
|
|
858
911
|
if (!this.acquireLock()) {
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseHostname,
|
|
14
14
|
shouldAutoSyncHosts,
|
|
15
15
|
syncHostsFile
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-RRH2JUIU.js";
|
|
17
17
|
|
|
18
18
|
// src/colors.ts
|
|
19
19
|
function supportsColor() {
|
|
@@ -1044,6 +1044,23 @@ var SIGNAL_CODES = {
|
|
|
1044
1044
|
SIGKILL: 9,
|
|
1045
1045
|
SIGTERM: 15
|
|
1046
1046
|
};
|
|
1047
|
+
function killTree(child, signal = "SIGTERM") {
|
|
1048
|
+
if (!child.pid) {
|
|
1049
|
+
child.kill(signal);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (!isWindows) {
|
|
1053
|
+
try {
|
|
1054
|
+
process.kill(-child.pid, signal);
|
|
1055
|
+
return;
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
child.kill(signal);
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1047
1064
|
function getProtocolPort(tls2) {
|
|
1048
1065
|
return tls2 ? 443 : 80;
|
|
1049
1066
|
}
|
|
@@ -1372,6 +1389,25 @@ function parsePidFromNetstat(output, port) {
|
|
|
1372
1389
|
}
|
|
1373
1390
|
return null;
|
|
1374
1391
|
}
|
|
1392
|
+
function findPidsOnPort(port) {
|
|
1393
|
+
try {
|
|
1394
|
+
if (isWindows) {
|
|
1395
|
+
const output2 = execSync("netstat -ano -p tcp", {
|
|
1396
|
+
encoding: "utf-8",
|
|
1397
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
1398
|
+
});
|
|
1399
|
+
const pid = parsePidFromNetstat(output2, port);
|
|
1400
|
+
return pid === null ? [] : [pid];
|
|
1401
|
+
}
|
|
1402
|
+
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
1403
|
+
encoding: "utf-8",
|
|
1404
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
1405
|
+
});
|
|
1406
|
+
return output.trim().split("\n").map((s) => parseInt(s, 10)).filter((n) => !isNaN(n) && n > 0);
|
|
1407
|
+
} catch {
|
|
1408
|
+
return [];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1375
1411
|
function findPidOnPort(port) {
|
|
1376
1412
|
try {
|
|
1377
1413
|
if (isWindows) {
|
|
@@ -1442,7 +1478,8 @@ function spawnCommand(commandArgs, options) {
|
|
|
1442
1478
|
env
|
|
1443
1479
|
}) : spawn("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
|
|
1444
1480
|
stdio: "inherit",
|
|
1445
|
-
env
|
|
1481
|
+
env,
|
|
1482
|
+
detached: true
|
|
1446
1483
|
});
|
|
1447
1484
|
let exiting = false;
|
|
1448
1485
|
const cleanup = () => {
|
|
@@ -1453,7 +1490,7 @@ function spawnCommand(commandArgs, options) {
|
|
|
1453
1490
|
const handleSignal = (signal) => {
|
|
1454
1491
|
if (exiting) return;
|
|
1455
1492
|
exiting = true;
|
|
1456
|
-
child
|
|
1493
|
+
killTree(child, signal);
|
|
1457
1494
|
cleanup();
|
|
1458
1495
|
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
1459
1496
|
};
|
|
@@ -3219,6 +3256,7 @@ ${colors_default.bold("Usage:")}
|
|
|
3219
3256
|
${colors_default.cyan("portless list")} Show active routes
|
|
3220
3257
|
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
3221
3258
|
${colors_default.cyan("portless clean")} Remove portless state, trust entry, and hosts block
|
|
3259
|
+
${colors_default.cyan("portless prune")} Kill orphaned dev servers from crashed sessions
|
|
3222
3260
|
${colors_default.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
3223
3261
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
3224
3262
|
|
|
@@ -3331,14 +3369,14 @@ ${colors_default.bold("Skip portless:")}
|
|
|
3331
3369
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
3332
3370
|
|
|
3333
3371
|
${colors_default.bold("Reserved names:")}
|
|
3334
|
-
run, get, alias, hosts, list, trust, clean, proxy are subcommands and
|
|
3335
|
-
be used as app names directly. Use "portless run" to infer the name,
|
|
3372
|
+
run, get, alias, hosts, list, trust, clean, prune, proxy are subcommands and
|
|
3373
|
+
cannot be used as app names directly. Use "portless run" to infer the name,
|
|
3336
3374
|
or "portless --name <name>" to force any name including reserved ones.
|
|
3337
3375
|
`);
|
|
3338
3376
|
process.exit(0);
|
|
3339
3377
|
}
|
|
3340
3378
|
function printVersion() {
|
|
3341
|
-
console.log("0.11.
|
|
3379
|
+
console.log("0.11.1");
|
|
3342
3380
|
process.exit(0);
|
|
3343
3381
|
}
|
|
3344
3382
|
async function handleTrust() {
|
|
@@ -3464,6 +3502,63 @@ Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
|
3464
3502
|
}
|
|
3465
3503
|
console.log(colors_default.green("Clean finished."));
|
|
3466
3504
|
}
|
|
3505
|
+
async function handlePrune(args) {
|
|
3506
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
3507
|
+
console.log(`
|
|
3508
|
+
${colors_default.bold("portless prune")} - Kill orphaned dev servers left behind by crashed portless sessions.
|
|
3509
|
+
|
|
3510
|
+
When portless is killed with SIGKILL (kill -9) or crashes, child dev servers
|
|
3511
|
+
may survive and continue holding their ports. This command finds those orphans
|
|
3512
|
+
by checking routes whose owning CLI process is dead but whose port is still in
|
|
3513
|
+
use, then terminates them and cleans up the stale route entries.
|
|
3514
|
+
|
|
3515
|
+
${colors_default.bold("Usage:")}
|
|
3516
|
+
${colors_default.cyan("portless prune")}
|
|
3517
|
+
${colors_default.cyan("portless prune --force")} Send SIGKILL instead of SIGTERM
|
|
3518
|
+
|
|
3519
|
+
${colors_default.bold("Options:")}
|
|
3520
|
+
--force Send SIGKILL instead of SIGTERM
|
|
3521
|
+
--help, -h Show this help
|
|
3522
|
+
`);
|
|
3523
|
+
process.exit(0);
|
|
3524
|
+
}
|
|
3525
|
+
const forceKill = args.includes("--force");
|
|
3526
|
+
const { dir } = await discoverState();
|
|
3527
|
+
const store = new RouteStore(dir, {
|
|
3528
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
3529
|
+
});
|
|
3530
|
+
const stale = store.pruneStaleRoutes();
|
|
3531
|
+
if (stale.length === 0) {
|
|
3532
|
+
console.log("No orphaned routes found.");
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
let killed = 0;
|
|
3536
|
+
for (const route of stale) {
|
|
3537
|
+
const pids = findPidsOnPort(route.port);
|
|
3538
|
+
if (pids.length === 0) {
|
|
3539
|
+
console.log(` ${route.hostname} :${route.port} - route removed (port already free)`);
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
const signal = forceKill ? "SIGKILL" : "SIGTERM";
|
|
3543
|
+
for (const pid of pids) {
|
|
3544
|
+
try {
|
|
3545
|
+
process.kill(pid, signal);
|
|
3546
|
+
killed++;
|
|
3547
|
+
console.log(` ${route.hostname} :${route.port} - killed PID ${pid} (${signal})`);
|
|
3548
|
+
} catch {
|
|
3549
|
+
console.log(` ${route.hostname} :${route.port} - PID ${pid} already exited`);
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
const routeWord = stale.length === 1 ? "route" : "routes";
|
|
3554
|
+
const procWord = killed === 1 ? "process" : "processes";
|
|
3555
|
+
console.log(
|
|
3556
|
+
colors_default.green(
|
|
3557
|
+
`
|
|
3558
|
+
Pruned ${stale.length} stale ${routeWord}, killed ${killed} orphaned ${procWord}.`
|
|
3559
|
+
)
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3467
3562
|
async function handleList() {
|
|
3468
3563
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
3469
3564
|
const store = new RouteStore(dir, {
|
|
@@ -4207,7 +4302,8 @@ function spawnChildProcess(commandArgs, env, cwd) {
|
|
|
4207
4302
|
return spawn3(commandArgs[0], commandArgs.slice(1), {
|
|
4208
4303
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4209
4304
|
env,
|
|
4210
|
-
cwd
|
|
4305
|
+
cwd,
|
|
4306
|
+
...isWindows ? {} : { detached: true }
|
|
4211
4307
|
});
|
|
4212
4308
|
}
|
|
4213
4309
|
function prefixStream(stream, output, prefix) {
|
|
@@ -4480,23 +4576,18 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
4480
4576
|
env: {
|
|
4481
4577
|
...process.env,
|
|
4482
4578
|
NODE_OPTIONS: buildNodeOptions()
|
|
4483
|
-
}
|
|
4579
|
+
},
|
|
4580
|
+
...isWindows ? {} : { detached: true }
|
|
4484
4581
|
});
|
|
4485
4582
|
const SIGKILL_TIMEOUT_MS = 5e3;
|
|
4486
4583
|
let cleanedUp = false;
|
|
4487
4584
|
const cleanup = () => {
|
|
4488
4585
|
if (cleanedUp) return;
|
|
4489
4586
|
cleanedUp = true;
|
|
4490
|
-
|
|
4491
|
-
turboChild.kill("SIGTERM");
|
|
4492
|
-
} catch {
|
|
4493
|
-
}
|
|
4587
|
+
killTree(turboChild, "SIGTERM");
|
|
4494
4588
|
setTimeout(() => {
|
|
4495
4589
|
if (turboChild.exitCode === null && !turboChild.killed) {
|
|
4496
|
-
|
|
4497
|
-
turboChild.kill("SIGKILL");
|
|
4498
|
-
} catch {
|
|
4499
|
-
}
|
|
4590
|
+
killTree(turboChild, "SIGKILL");
|
|
4500
4591
|
}
|
|
4501
4592
|
}, SIGKILL_TIMEOUT_MS).unref();
|
|
4502
4593
|
for (const { hostname } of routes) {
|
|
@@ -4554,18 +4645,12 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
|
|
|
4554
4645
|
if (cleanedUp) return;
|
|
4555
4646
|
cleanedUp = true;
|
|
4556
4647
|
for (const child of children) {
|
|
4557
|
-
|
|
4558
|
-
child.kill("SIGTERM");
|
|
4559
|
-
} catch {
|
|
4560
|
-
}
|
|
4648
|
+
killTree(child, "SIGTERM");
|
|
4561
4649
|
}
|
|
4562
4650
|
setTimeout(() => {
|
|
4563
4651
|
for (const child of children) {
|
|
4564
4652
|
if (child.exitCode === null && !child.killed) {
|
|
4565
|
-
|
|
4566
|
-
child.kill("SIGKILL");
|
|
4567
|
-
} catch {
|
|
4568
|
-
}
|
|
4653
|
+
killTree(child, "SIGKILL");
|
|
4569
4654
|
}
|
|
4570
4655
|
}
|
|
4571
4656
|
}, SIGKILL_TIMEOUT_MS).unref();
|
|
@@ -4812,6 +4897,10 @@ async function main() {
|
|
|
4812
4897
|
await handleClean(args);
|
|
4813
4898
|
return;
|
|
4814
4899
|
}
|
|
4900
|
+
if (args[0] === "prune") {
|
|
4901
|
+
await handlePrune(args);
|
|
4902
|
+
return;
|
|
4903
|
+
}
|
|
4815
4904
|
if (args[0] === "list") {
|
|
4816
4905
|
await handleList();
|
|
4817
4906
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -108,6 +108,17 @@ declare class RouteStore {
|
|
|
108
108
|
* log it.
|
|
109
109
|
*/
|
|
110
110
|
addRoute(hostname: string, port: number, pid: number, force?: boolean): number | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Load all routes from disk without filtering out dead PIDs. Used by
|
|
113
|
+
* `portless prune` to discover stale entries whose owning CLI is gone
|
|
114
|
+
* but whose dev server may still be holding a port.
|
|
115
|
+
*/
|
|
116
|
+
loadRoutesRaw(): RouteMapping[];
|
|
117
|
+
/**
|
|
118
|
+
* Remove all route entries whose owning process is dead and persist the
|
|
119
|
+
* result. Returns the removed stale entries so the caller can act on them.
|
|
120
|
+
*/
|
|
121
|
+
pruneStaleRoutes(): RouteMapping[];
|
|
111
122
|
removeRoute(hostname: string): void;
|
|
112
123
|
}
|
|
113
124
|
|
package/dist/index.js
CHANGED