@hamedb89/localghost 0.5.0 → 0.6.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 +82 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -2
- package/dist/index.js.map +1 -1
- package/dist/vite.js +34 -0
- package/dist/vite.js.map +1 -1
- package/docs/localghost.1.md +10 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -546,6 +546,9 @@ function validRegistry(value) {
|
|
|
546
546
|
function pruneRegistry(registry, now, isRunning) {
|
|
547
547
|
registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
|
|
548
548
|
}
|
|
549
|
+
function isTestSessionKey(instanceKey) {
|
|
550
|
+
return instanceKey.startsWith("test:");
|
|
551
|
+
}
|
|
549
552
|
async function readJson(path) {
|
|
550
553
|
try {
|
|
551
554
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -642,6 +645,37 @@ function createLocalghostRegistry(options = {}) {
|
|
|
642
645
|
await releaseLock();
|
|
643
646
|
}
|
|
644
647
|
},
|
|
648
|
+
async pruneTestSessions() {
|
|
649
|
+
const releaseLock = await lock();
|
|
650
|
+
try {
|
|
651
|
+
const registry = await readRegistry();
|
|
652
|
+
const staleTestKeys = new Set(
|
|
653
|
+
registry.leases.filter((lease) => isTestSessionKey(lease.instanceKey) && (lease.expiresAt <= now() || !isRunning(lease.pid))).map((lease) => leaseKey(lease.projectCwd, lease.instanceKey))
|
|
654
|
+
);
|
|
655
|
+
const beforeLeases = registry.leases.length;
|
|
656
|
+
registry.leases = registry.leases.filter((lease) => !staleTestKeys.has(leaseKey(lease.projectCwd, lease.instanceKey)));
|
|
657
|
+
const activeKeys = new Set(registry.leases.map((lease) => leaseKey(lease.projectCwd, lease.instanceKey)));
|
|
658
|
+
const beforeAllocations = registry.allocations.length;
|
|
659
|
+
registry.allocations = registry.allocations.filter(
|
|
660
|
+
(allocation) => !isTestSessionKey(allocation.instanceKey) || activeKeys.has(leaseKey(allocation.projectCwd, allocation.instanceKey))
|
|
661
|
+
);
|
|
662
|
+
await writeRegistry(registry);
|
|
663
|
+
return {
|
|
664
|
+
removedLeases: beforeLeases - registry.leases.length,
|
|
665
|
+
removedAllocations: beforeAllocations - registry.allocations.length
|
|
666
|
+
};
|
|
667
|
+
} finally {
|
|
668
|
+
await releaseLock();
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
async reset() {
|
|
672
|
+
const releaseLock = await lock();
|
|
673
|
+
try {
|
|
674
|
+
await writeRegistry({ version: 1, allocations: [], leases: [] });
|
|
675
|
+
} finally {
|
|
676
|
+
await releaseLock();
|
|
677
|
+
}
|
|
678
|
+
},
|
|
645
679
|
async acquirePort(acquireOptions) {
|
|
646
680
|
const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
|
|
647
681
|
if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
|
|
@@ -2177,7 +2211,7 @@ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as rea
|
|
|
2177
2211
|
import { homedir as homedir3 } from "os";
|
|
2178
2212
|
import { dirname as dirname5, join as join10 } from "path";
|
|
2179
2213
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
2180
|
-
var LOCALGHOST_VERSION = "0.
|
|
2214
|
+
var LOCALGHOST_VERSION = "0.6.1";
|
|
2181
2215
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2182
2216
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2183
2217
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -2345,7 +2379,7 @@ import { execa as execa4 } from "execa";
|
|
|
2345
2379
|
|
|
2346
2380
|
// src/process.ts
|
|
2347
2381
|
function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
|
|
2348
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid
|
|
2382
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
|
|
2349
2383
|
try {
|
|
2350
2384
|
killProcess(process.platform === "win32" ? pid : -pid, signal);
|
|
2351
2385
|
return true;
|
|
@@ -2697,10 +2731,13 @@ function registerSignalShutdown(stop) {
|
|
|
2697
2731
|
};
|
|
2698
2732
|
process.once("SIGINT", request);
|
|
2699
2733
|
process.once("SIGTERM", request);
|
|
2700
|
-
return
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2734
|
+
return {
|
|
2735
|
+
wasRequested: () => requested,
|
|
2736
|
+
finish: () => {
|
|
2737
|
+
process.off("SIGINT", request);
|
|
2738
|
+
process.off("SIGTERM", request);
|
|
2739
|
+
if (!requested) stop();
|
|
2740
|
+
}
|
|
2704
2741
|
};
|
|
2705
2742
|
}
|
|
2706
2743
|
async function resolveServiceRuntimeEntries(services, dynamicPort, projectCwd) {
|
|
@@ -2752,11 +2789,22 @@ async function waitForPortsToBeAvailable(entries, timeoutMs = 1e4) {
|
|
|
2752
2789
|
const deadline = Date.now() + timeoutMs;
|
|
2753
2790
|
const ports = [...new Set(entries.map((entry) => entry.port))];
|
|
2754
2791
|
while (Date.now() < deadline) {
|
|
2755
|
-
const
|
|
2756
|
-
if (
|
|
2792
|
+
const availability2 = await Promise.all(ports.map((port) => isPortAvailable(port)));
|
|
2793
|
+
if (availability2.every(Boolean)) return { available: true, blocked: [] };
|
|
2757
2794
|
await wait2(50);
|
|
2758
2795
|
}
|
|
2759
|
-
|
|
2796
|
+
const availability = await Promise.all(entries.map(async (entry) => ({
|
|
2797
|
+
entry,
|
|
2798
|
+
available: await isPortAvailable(entry.port)
|
|
2799
|
+
})));
|
|
2800
|
+
const blocked = availability.filter(({ available }) => !available).map(({ entry }) => entry);
|
|
2801
|
+
return { available: blocked.length === 0, blocked };
|
|
2802
|
+
}
|
|
2803
|
+
function formatBlockedPorts(entries) {
|
|
2804
|
+
return [...new Map(entries.map((entry) => [
|
|
2805
|
+
`${entry.host}:${entry.port}`,
|
|
2806
|
+
`${entry.host} (port ${entry.port})`
|
|
2807
|
+
])).values()].join(", ");
|
|
2760
2808
|
}
|
|
2761
2809
|
async function waitForProcessShutdown(processes, forceStop, timeoutMs = 1e4) {
|
|
2762
2810
|
const settled = Promise.allSettled(processes);
|
|
@@ -2847,7 +2895,7 @@ async function runDetectedServices(options) {
|
|
|
2847
2895
|
}
|
|
2848
2896
|
signalManagedProcess(caddy, signal);
|
|
2849
2897
|
};
|
|
2850
|
-
const
|
|
2898
|
+
const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
|
|
2851
2899
|
try {
|
|
2852
2900
|
const ready = await Promise.race([
|
|
2853
2901
|
waitForServicePorts(entries),
|
|
@@ -2859,15 +2907,18 @@ async function runDetectedServices(options) {
|
|
|
2859
2907
|
}
|
|
2860
2908
|
await processExit;
|
|
2861
2909
|
} finally {
|
|
2862
|
-
|
|
2910
|
+
shutdown.finish();
|
|
2863
2911
|
await waitForProcessShutdown(
|
|
2864
2912
|
[caddyExit, ...children],
|
|
2865
2913
|
stopManaged
|
|
2866
2914
|
);
|
|
2867
|
-
|
|
2868
|
-
|
|
2915
|
+
const drained = await waitForPortsToBeAvailable(entries);
|
|
2916
|
+
if (!drained.available && !shutdown.wasRequested()) {
|
|
2917
|
+
console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
|
|
2869
2918
|
stopManaged("SIGTERM");
|
|
2870
|
-
|
|
2919
|
+
const terminated = await waitForPortsToBeAvailable(entries, 2e3);
|
|
2920
|
+
if (!terminated.available) {
|
|
2921
|
+
console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
|
|
2871
2922
|
stopManaged("SIGKILL");
|
|
2872
2923
|
}
|
|
2873
2924
|
}
|
|
@@ -3245,6 +3296,15 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
3245
3296
|
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
3246
3297
|
console.log(`State ${statePath}`);
|
|
3247
3298
|
});
|
|
3299
|
+
program.command("ports").description("Manage Localghost port registry state").addCommand(new Command("prune").description("Remove expired or dead port leases").option("--cwd <path>", "Project directory", process.cwd()).option("--test-only", "Remove only stale test-session leases and records").option("--json", "Print raw JSON").action(async (options) => {
|
|
3300
|
+
const result = options.testOnly ? await createLocalghostRegistry({ cwd: options.cwd }).pruneTestSessions() : await createLocalghostRegistry({ cwd: options.cwd }).prune();
|
|
3301
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
3302
|
+
else console.log(`Pruned ${result.removedLeases} stale registry lease${result.removedLeases === 1 ? "" : "s"}${"removedAllocations" in result ? ` and ${result.removedAllocations} test allocation${result.removedAllocations === 1 ? "" : "s"}` : ""}.`);
|
|
3303
|
+
})).addCommand(new Command("reset").description("Clear all remembered allocations and leases").option("--cwd <path>", "Project directory", process.cwd()).option("--yes", "Confirm the destructive registry reset").action(async (options) => {
|
|
3304
|
+
if (!options.yes) throw new Error("Resetting the port registry is destructive. Re-run with --yes.");
|
|
3305
|
+
await createLocalghostRegistry({ cwd: options.cwd }).reset();
|
|
3306
|
+
console.log("Localghost port registry reset.");
|
|
3307
|
+
}));
|
|
3248
3308
|
program.command("test").description("Run a test command with an isolated Localghost port").option("--cwd <path>", "Project directory", process.cwd()).option("--instance <name>", "Parallel test instance name", String(process.pid)).option("--port <number>", "Initial test port", parsePort2, 5173).option("--lease-ttl <milliseconds>", "Lease lifetime before heartbeat renewal", Number, 30 * 60 * 1e3).argument("<command...>", "Command to run after --").action(async (command, options) => {
|
|
3249
3309
|
const [binary, ...args] = command;
|
|
3250
3310
|
if (!binary) throw new Error("Missing command. Use: localghost test -- <command>");
|
|
@@ -3524,19 +3584,22 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
3524
3584
|
signalManagedProcess(child, signal);
|
|
3525
3585
|
signalManagedProcess(caddy, signal);
|
|
3526
3586
|
};
|
|
3527
|
-
const
|
|
3587
|
+
const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
|
|
3528
3588
|
try {
|
|
3529
3589
|
await Promise.race([child, caddyExit]);
|
|
3530
3590
|
} finally {
|
|
3531
|
-
|
|
3591
|
+
shutdown.finish();
|
|
3532
3592
|
await waitForProcessShutdown(
|
|
3533
3593
|
[child, caddyExit],
|
|
3534
3594
|
stopManaged
|
|
3535
3595
|
);
|
|
3536
|
-
|
|
3537
|
-
|
|
3596
|
+
const drained = await waitForPortsToBeAvailable(context.entries);
|
|
3597
|
+
if (!drained.available && !shutdown.wasRequested()) {
|
|
3598
|
+
console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
|
|
3538
3599
|
stopManaged("SIGTERM");
|
|
3539
|
-
|
|
3600
|
+
const terminated = await waitForPortsToBeAvailable(context.entries, 2e3);
|
|
3601
|
+
if (!terminated.available) {
|
|
3602
|
+
console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
|
|
3540
3603
|
stopManaged("SIGKILL");
|
|
3541
3604
|
}
|
|
3542
3605
|
}
|