@hamedb89/localghost 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli.js +319 -107
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +40 -14
- package/dist/index.js +77 -11
- package/dist/index.js.map +1 -1
- package/dist/vite.js +12 -0
- package/dist/vite.js.map +1 -1
- package/docs/localghost.1.md +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,13 +54,13 @@ yarn dlx @hamedb89/localghost
|
|
|
54
54
|
bunx --package @hamedb89/localghost localghost
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
From a Localghost checkout, test a real consumer repository against the local checkout without changing the consumer's manifest or working tree. This is repository development tooling and is not part of the installed npm package:
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
60
|
./bin/ghost consumer test faaast --repo /path/to/faaast-landing -- pnpm run bench:noop
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
The command creates a detached worktree, installs its dependencies, builds and links the local Localghost checkout, runs the command inside the worktree, and removes the worktree afterward. Add `--keep` before `--` when inspecting the isolated checkout after a failure.
|
|
63
|
+
The command creates a detached worktree, installs its dependencies, builds and links the local Localghost checkout, runs the command inside the worktree, and removes the worktree afterward. Add `--keep` before `--` when inspecting the isolated checkout after a failure. The FAAAST adapter uses a separate `.consumer.test` hostname suffix to avoid colliding with a normal `.localhost` development session.
|
|
64
64
|
|
|
65
65
|
On the first interactive run, Localghost can create `.localghost`, explain the `/etc/hosts` change, write `ops/local/Caddyfile`, and print the browser-facing URL:
|
|
66
66
|
|
|
@@ -630,8 +630,8 @@ localghost ps [--json]
|
|
|
630
630
|
localghost update [--json]
|
|
631
631
|
localghost upgrade [--cwd path]
|
|
632
632
|
localghost release [patch|minor|major]
|
|
633
|
-
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust]
|
|
634
|
-
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust] [--dynamic-port] -- command
|
|
633
|
+
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--clean-caddy] [--trust]
|
|
634
|
+
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--clean-caddy] [--trust] [--dynamic-port yes|no] -- command
|
|
635
635
|
localghost routes [--https|--ssl]
|
|
636
636
|
localghost print [--config file] [--config-pattern regex]
|
|
637
637
|
```
|
|
@@ -666,7 +666,7 @@ import { localGhostPlugin } from "@hamedb89/localghost/vite";
|
|
|
666
666
|
- Preview the exact Pages artifact locally with `npm run site:serve`, then open `http://127.0.0.1:4173/`.
|
|
667
667
|
- npm publish is guarded by `prepublishOnly` and the release workflow publishes with npm provenance.
|
|
668
668
|
- To release the CLI, run `localghost release patch`, `localghost release minor`, or `localghost release major`. The command dispatches the **Release** workflow from `main`; it synchronizes version metadata, verifies the package and runtime matrix, commits and tags the bump, publishes npm, and creates a GitHub Release with generated notes. GitHub CLI must be installed and authenticated.
|
|
669
|
-
- From this repository, `./bin/ghost release minor` (or the shorter `./bin/c release minor`) pushes `main` and dispatches the same guarded GitHub release workflow. Use `./bin/ghost check` for the release verification locally, `./bin/ghost release status` to inspect recent runs, or `./bin/ghost release retry v0.
|
|
669
|
+
- From this repository, `./bin/ghost release minor` (or the shorter `./bin/c release minor`) pushes `main` and dispatches the same guarded GitHub release workflow. Use `./bin/ghost check` for the release verification locally, `./bin/ghost release status` to inspect recent runs, or `./bin/ghost release retry v0.4.0` to retry publishing an existing tag.
|
|
670
670
|
- Runtime dependencies are intentionally small: `commander` and `execa`. Vite is an optional peer dependency.
|
|
671
671
|
- No postinstall scripts, hidden Homebrew installs, surprise browser tabs, or broad hosts-file rewrites.
|
|
672
672
|
- Update checks are best-effort, cached for 24 hours, and can be disabled with `LOCALGHOST_NO_UPDATE_CHECK=1` or `--no-update-check`.
|
package/dist/cli.js
CHANGED
|
@@ -487,13 +487,13 @@ import { pathToFileURL } from "url";
|
|
|
487
487
|
// src/port.ts
|
|
488
488
|
import { createServer } from "net";
|
|
489
489
|
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
490
|
-
return new Promise((
|
|
490
|
+
return new Promise((resolve5) => {
|
|
491
491
|
const server = createServer();
|
|
492
492
|
server.once("error", () => {
|
|
493
|
-
|
|
493
|
+
resolve5(false);
|
|
494
494
|
});
|
|
495
495
|
server.once("listening", () => {
|
|
496
|
-
server.close(() =>
|
|
496
|
+
server.close(() => resolve5(true));
|
|
497
497
|
});
|
|
498
498
|
server.listen(port, host);
|
|
499
499
|
});
|
|
@@ -679,6 +679,18 @@ function createLocalghostRegistry(options = {}) {
|
|
|
679
679
|
return lease;
|
|
680
680
|
});
|
|
681
681
|
},
|
|
682
|
+
async renewPort(renewOptions) {
|
|
683
|
+
const projectCwd = canonicalizeLocalghostProjectCwd(renewOptions.projectCwd ?? cwd);
|
|
684
|
+
return withLock(async (registry) => {
|
|
685
|
+
const lease = registry.leases.find(
|
|
686
|
+
(candidate) => candidate.projectCwd === projectCwd && candidate.instanceKey === renewOptions.instanceKey && candidate.ownerToken === ownerToken
|
|
687
|
+
);
|
|
688
|
+
if (!lease || lease.expiresAt <= now() || !isRunning(lease.pid)) return void 0;
|
|
689
|
+
lease.expiresAt = now() + (renewOptions.leaseTtlMs ?? 30 * 60 * 1e3);
|
|
690
|
+
await writeRegistry(registry);
|
|
691
|
+
return lease;
|
|
692
|
+
});
|
|
693
|
+
},
|
|
682
694
|
async releasePort(releaseOptions) {
|
|
683
695
|
const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
|
|
684
696
|
return withLock(async (registry) => {
|
|
@@ -1558,11 +1570,11 @@ function isStopped(signal, localSignal) {
|
|
|
1558
1570
|
}
|
|
1559
1571
|
function wait(ms, signal, localSignal) {
|
|
1560
1572
|
if (isStopped(signal, localSignal)) return Promise.resolve();
|
|
1561
|
-
return new Promise((
|
|
1562
|
-
const timeout = setTimeout(
|
|
1573
|
+
return new Promise((resolve5) => {
|
|
1574
|
+
const timeout = setTimeout(resolve5, ms);
|
|
1563
1575
|
const stop = () => {
|
|
1564
1576
|
clearTimeout(timeout);
|
|
1565
|
-
|
|
1577
|
+
resolve5();
|
|
1566
1578
|
};
|
|
1567
1579
|
signal?.addEventListener("abort", stop, { once: true });
|
|
1568
1580
|
localSignal.addEventListener("abort", stop, { once: true });
|
|
@@ -1784,13 +1796,23 @@ async function removeSystemHosts(projectName) {
|
|
|
1784
1796
|
|
|
1785
1797
|
// src/init.ts
|
|
1786
1798
|
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1787
|
-
import { join as join8 } from "path";
|
|
1799
|
+
import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
|
|
1788
1800
|
function detectPackageManager(cwd = process.cwd()) {
|
|
1789
1801
|
if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1790
1802
|
if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
|
|
1791
1803
|
if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
|
|
1792
1804
|
return "npm";
|
|
1793
1805
|
}
|
|
1806
|
+
function isPnpmWorkspaceRoot(cwd = process.cwd()) {
|
|
1807
|
+
const target = resolve4(cwd);
|
|
1808
|
+
let current = target;
|
|
1809
|
+
while (true) {
|
|
1810
|
+
if (existsSync5(join8(current, "pnpm-workspace.yaml"))) return current === target;
|
|
1811
|
+
const parent = dirname4(current);
|
|
1812
|
+
if (parent === current) return false;
|
|
1813
|
+
current = parent;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1794
1816
|
function packageRunCommand(packageManager, script) {
|
|
1795
1817
|
if (packageManager === "yarn") return `yarn ${script}`;
|
|
1796
1818
|
if (packageManager === "pnpm") return `pnpm ${script}`;
|
|
@@ -1910,6 +1932,59 @@ function initLocalghost(options = {}) {
|
|
|
1910
1932
|
};
|
|
1911
1933
|
}
|
|
1912
1934
|
|
|
1935
|
+
// src/test-session.ts
|
|
1936
|
+
async function createLocalghostTestSession(options) {
|
|
1937
|
+
if (!options.instanceKey) throw new Error("instanceKey is required");
|
|
1938
|
+
const registry = createLocalghostRegistry(options.cwd ? { cwd: options.cwd } : {});
|
|
1939
|
+
const leases = [];
|
|
1940
|
+
const ports = {};
|
|
1941
|
+
const leaseTtlMs = options.leaseTtlMs ?? 30 * 60 * 1e3;
|
|
1942
|
+
if (!Number.isFinite(leaseTtlMs) || leaseTtlMs < 1e3) throw new Error("leaseTtlMs must be at least 1000 milliseconds.");
|
|
1943
|
+
try {
|
|
1944
|
+
for (const [name, service] of Object.entries(options.services)) {
|
|
1945
|
+
const lease = await registry.acquirePort({
|
|
1946
|
+
...options.cwd ? { projectCwd: options.cwd } : {},
|
|
1947
|
+
instanceKey: `test:${options.instanceKey}:${name}`,
|
|
1948
|
+
startPort: service.startPort,
|
|
1949
|
+
...service.maxAttempts !== void 0 ? { maxAttempts: service.maxAttempts } : {},
|
|
1950
|
+
...service.host !== void 0 ? { host: service.host } : {},
|
|
1951
|
+
leaseTtlMs,
|
|
1952
|
+
reservedPorts: Object.values(ports)
|
|
1953
|
+
});
|
|
1954
|
+
leases.push(lease);
|
|
1955
|
+
ports[name] = lease.port;
|
|
1956
|
+
}
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
|
|
1959
|
+
throw error;
|
|
1960
|
+
}
|
|
1961
|
+
let released = false;
|
|
1962
|
+
const renew = async () => {
|
|
1963
|
+
if (released) return;
|
|
1964
|
+
await Promise.all(leases.map(async (lease) => {
|
|
1965
|
+
const renewed = await registry.renewPort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey, leaseTtlMs });
|
|
1966
|
+
if (!renewed) throw new Error(`Localghost test lease expired: ${lease.instanceKey}`);
|
|
1967
|
+
}));
|
|
1968
|
+
};
|
|
1969
|
+
return {
|
|
1970
|
+
instanceKey: options.instanceKey,
|
|
1971
|
+
ports,
|
|
1972
|
+
leases,
|
|
1973
|
+
renew,
|
|
1974
|
+
startHeartbeat(intervalMs = Math.max(1e3, Math.floor(leaseTtlMs / 3))) {
|
|
1975
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1e3) throw new Error("Heartbeat interval must be at least 1000 milliseconds.");
|
|
1976
|
+
const timer = setInterval(() => void renew().catch(() => void 0), intervalMs);
|
|
1977
|
+
timer.unref();
|
|
1978
|
+
return timer;
|
|
1979
|
+
},
|
|
1980
|
+
async release() {
|
|
1981
|
+
if (released) return;
|
|
1982
|
+
released = true;
|
|
1983
|
+
await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1913
1988
|
// src/guide.ts
|
|
1914
1989
|
var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
|
|
1915
1990
|
|
|
@@ -2100,9 +2175,9 @@ function patchLocalghostState(cwd, patch) {
|
|
|
2100
2175
|
// src/update-check.ts
|
|
2101
2176
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2102
2177
|
import { homedir as homedir3 } from "os";
|
|
2103
|
-
import { dirname as
|
|
2178
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2104
2179
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
2105
|
-
var LOCALGHOST_VERSION = "0.
|
|
2180
|
+
var LOCALGHOST_VERSION = "0.5.0";
|
|
2106
2181
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2107
2182
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2108
2183
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -2127,7 +2202,7 @@ function readCache(path = getUpdateCheckCachePath()) {
|
|
|
2127
2202
|
}
|
|
2128
2203
|
function writeCache(cache, path = getUpdateCheckCachePath()) {
|
|
2129
2204
|
try {
|
|
2130
|
-
mkdirSync3(
|
|
2205
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
2131
2206
|
writeFileSync5(path, `${JSON.stringify(cache, null, 2)}
|
|
2132
2207
|
`, "utf8");
|
|
2133
2208
|
} catch {
|
|
@@ -2320,6 +2395,13 @@ function parsePort2(value) {
|
|
|
2320
2395
|
}
|
|
2321
2396
|
return port;
|
|
2322
2397
|
}
|
|
2398
|
+
function parseCount(value) {
|
|
2399
|
+
const count = Number.parseInt(value, 10);
|
|
2400
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
2401
|
+
throw new InvalidArgumentError("Count must be a number between 1 and 100.");
|
|
2402
|
+
}
|
|
2403
|
+
return count;
|
|
2404
|
+
}
|
|
2323
2405
|
function parsePackageManager(value) {
|
|
2324
2406
|
if (value === "npm" || value === "yarn" || value === "pnpm" || value === "bun") return value;
|
|
2325
2407
|
throw new InvalidArgumentError("Package manager must be npm, pnpm, yarn, or bun.");
|
|
@@ -2496,7 +2578,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
2496
2578
|
});
|
|
2497
2579
|
}
|
|
2498
2580
|
function wait2(ms) {
|
|
2499
|
-
return new Promise((
|
|
2581
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2500
2582
|
}
|
|
2501
2583
|
async function runTrust(cwd, caddyfilePath) {
|
|
2502
2584
|
await wait2(350);
|
|
@@ -2527,6 +2609,69 @@ async function maybeTrustCaddy(options) {
|
|
|
2527
2609
|
}
|
|
2528
2610
|
await runTrust(options.cwd, options.caddyfilePath);
|
|
2529
2611
|
}
|
|
2612
|
+
async function readPortStatuses() {
|
|
2613
|
+
const registry = createLocalghostRegistry();
|
|
2614
|
+
const data = await registry.read();
|
|
2615
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2616
|
+
const now = Date.now();
|
|
2617
|
+
for (const allocation of data.allocations) {
|
|
2618
|
+
rows.set(`${allocation.projectCwd}\0${allocation.instanceKey}`, {
|
|
2619
|
+
port: allocation.port,
|
|
2620
|
+
state: "available",
|
|
2621
|
+
projectCwd: allocation.projectCwd,
|
|
2622
|
+
instanceKey: allocation.instanceKey,
|
|
2623
|
+
available: true
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
for (const lease of data.leases) {
|
|
2627
|
+
const running = lease.expiresAt > now && isProcessRunning(lease.pid);
|
|
2628
|
+
const key = `${lease.projectCwd}\0${lease.instanceKey}`;
|
|
2629
|
+
rows.set(key, {
|
|
2630
|
+
port: lease.port,
|
|
2631
|
+
state: running ? "active" : "stale",
|
|
2632
|
+
projectCwd: lease.projectCwd,
|
|
2633
|
+
instanceKey: lease.instanceKey,
|
|
2634
|
+
available: false,
|
|
2635
|
+
pid: lease.pid,
|
|
2636
|
+
processRunning: isProcessRunning(lease.pid),
|
|
2637
|
+
expiresAt: lease.expiresAt
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
const activity = readLocalghostActivity();
|
|
2641
|
+
for (const run of activity.runs) {
|
|
2642
|
+
const ports = [...new Set(run.entries.map((entry) => entry.port))];
|
|
2643
|
+
for (const port of ports) {
|
|
2644
|
+
const key = `${run.cwd}\0activity:${run.id}:${port}`;
|
|
2645
|
+
if (!rows.has(key)) {
|
|
2646
|
+
const running = isProcessRunning(run.pid);
|
|
2647
|
+
rows.set(key, {
|
|
2648
|
+
port,
|
|
2649
|
+
state: running ? "active" : "stale",
|
|
2650
|
+
projectCwd: run.cwd,
|
|
2651
|
+
instanceKey: `activity:${run.mode}`,
|
|
2652
|
+
available: false,
|
|
2653
|
+
pid: run.pid,
|
|
2654
|
+
processRunning: running
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
const statuses = await Promise.all([...rows.values()].map(async (row) => {
|
|
2660
|
+
if (row.state === "active" || row.state === "stale") {
|
|
2661
|
+
return { ...row, available: await isPortAvailable(row.port) };
|
|
2662
|
+
}
|
|
2663
|
+
const available = await isPortAvailable(row.port);
|
|
2664
|
+
return { ...row, available, state: available ? "available" : "occupied" };
|
|
2665
|
+
}));
|
|
2666
|
+
return { registryPath: registry.registryPath, statuses };
|
|
2667
|
+
}
|
|
2668
|
+
async function findFreePorts(startPort, count) {
|
|
2669
|
+
const ports = [];
|
|
2670
|
+
for (let port = startPort; port <= 65535 && ports.length < count; port += 1) {
|
|
2671
|
+
if (await isPortAvailable(port)) ports.push(port);
|
|
2672
|
+
}
|
|
2673
|
+
return ports;
|
|
2674
|
+
}
|
|
2530
2675
|
function maybePid(pid) {
|
|
2531
2676
|
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
2532
2677
|
}
|
|
@@ -2908,6 +3053,9 @@ program.command("upgrade").description("Install or update Localghost in the curr
|
|
|
2908
3053
|
const packageManager = detectPackageManager(options.cwd);
|
|
2909
3054
|
const packageName = "@hamedb89/localghost@latest";
|
|
2910
3055
|
const args = packageManager === "yarn" ? ["add", "--dev", packageName] : packageManager === "pnpm" ? ["add", "--save-dev", packageName] : packageManager === "bun" ? ["add", "--dev", packageName] : ["install", "--save-dev", packageName];
|
|
3056
|
+
if (packageManager === "pnpm" && isPnpmWorkspaceRoot(options.cwd)) {
|
|
3057
|
+
args.splice(1, 0, "--workspace-root");
|
|
3058
|
+
}
|
|
2911
3059
|
console.log(`Upgrading ${packageName} with ${packageManager}...`);
|
|
2912
3060
|
await execa4(packageManager, args, { cwd: options.cwd, stdio: "inherit" });
|
|
2913
3061
|
console.log(`Localghost is upgraded in ${options.cwd}.`);
|
|
@@ -3097,7 +3245,68 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
3097
3245
|
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
3098
3246
|
console.log(`State ${statePath}`);
|
|
3099
3247
|
});
|
|
3100
|
-
program.command("
|
|
3248
|
+
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
|
+
const [binary, ...args] = command;
|
|
3250
|
+
if (!binary) throw new Error("Missing command. Use: localghost test -- <command>");
|
|
3251
|
+
const session = await createLocalghostTestSession({
|
|
3252
|
+
cwd: options.cwd,
|
|
3253
|
+
instanceKey: options.instance,
|
|
3254
|
+
leaseTtlMs: options.leaseTtl,
|
|
3255
|
+
services: { default: { startPort: options.port } }
|
|
3256
|
+
});
|
|
3257
|
+
const heartbeat = session.startHeartbeat();
|
|
3258
|
+
const child = execa4(binary, args, {
|
|
3259
|
+
cwd: options.cwd,
|
|
3260
|
+
stdio: "inherit",
|
|
3261
|
+
detached: process.platform !== "win32",
|
|
3262
|
+
env: {
|
|
3263
|
+
...process.env,
|
|
3264
|
+
LOCALGHOST_INSTANCE: options.instance,
|
|
3265
|
+
LOCALGHOST_PORT: String(session.ports.default),
|
|
3266
|
+
LOCALGHOST_DYNAMIC_PORT: "1",
|
|
3267
|
+
VITE_PORT: String(session.ports.default)
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
const stop = (signal) => signalManagedProcess(child, signal);
|
|
3271
|
+
process.once("SIGINT", stop);
|
|
3272
|
+
process.once("SIGTERM", stop);
|
|
3273
|
+
try {
|
|
3274
|
+
await child;
|
|
3275
|
+
} finally {
|
|
3276
|
+
process.off("SIGINT", stop);
|
|
3277
|
+
process.off("SIGTERM", stop);
|
|
3278
|
+
clearInterval(heartbeat);
|
|
3279
|
+
await session.release();
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
program.command("status").description("Print Localghost's project-local state file").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--ready", "Exit non-zero when setup is missing or stale").option("--https", "Check setup readiness for HTTPS mode").option("--ssl", "Alias for --https").option("--ports", "List Localghost-managed and active ports").option("--from <port>", "First port to probe for free ports", parsePort2, 3e3).option("--count <number>", "Number of free ports to find", parseCount, 5).option("--json", "Print raw JSON").action(async (options) => {
|
|
3283
|
+
if (options.ports) {
|
|
3284
|
+
const [{ registryPath, statuses }, freePorts] = await Promise.all([
|
|
3285
|
+
readPortStatuses(),
|
|
3286
|
+
findFreePorts(options.from, options.count)
|
|
3287
|
+
]);
|
|
3288
|
+
const result = {
|
|
3289
|
+
registryPath,
|
|
3290
|
+
ports: statuses.sort((left, right) => left.port - right.port),
|
|
3291
|
+
free: { from: options.from, count: options.count, ports: freePorts }
|
|
3292
|
+
};
|
|
3293
|
+
if (options.json) {
|
|
3294
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
console.log(`Registry: ${registryPath}`);
|
|
3298
|
+
if (result.ports.length === 0) {
|
|
3299
|
+
console.log("No Localghost ports are currently recorded.");
|
|
3300
|
+
} else {
|
|
3301
|
+
console.log("Port State Project / instance");
|
|
3302
|
+
for (const port of result.ports) {
|
|
3303
|
+
const owner = `${port.projectCwd} / ${port.instanceKey}`;
|
|
3304
|
+
console.log(`${String(port.port).padEnd(5)} ${port.state.padEnd(10)} ${owner}`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
console.log(`Free ports from ${options.from}: ${freePorts.join(", ") || "none found"}`);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3101
3310
|
const state = readLocalghostState(options.cwd);
|
|
3102
3311
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
3103
3312
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
@@ -3230,107 +3439,110 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
3230
3439
|
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
|
|
3231
3440
|
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
3232
3441
|
});
|
|
3233
|
-
const https = context.https;
|
|
3234
|
-
const readiness = getSetupReadiness({
|
|
3235
|
-
...options,
|
|
3236
|
-
https,
|
|
3237
|
-
ignoreCaddyfile: true,
|
|
3238
|
-
entries: context.entries,
|
|
3239
|
-
configPath: context.configPath,
|
|
3240
|
-
projectName: context.projectName
|
|
3241
|
-
});
|
|
3242
|
-
if (!readiness.ready) {
|
|
3243
|
-
if (!options.setup && !context.autoRepair) {
|
|
3244
|
-
throw new Error(
|
|
3245
|
-
[
|
|
3246
|
-
"Localghost setup is missing or stale.",
|
|
3247
|
-
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
3248
|
-
`Run: ${readiness.setupCommand}`,
|
|
3249
|
-
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
3250
|
-
].join("\n")
|
|
3251
|
-
);
|
|
3252
|
-
}
|
|
3253
|
-
console.log("Localghost setup is stale; repairing it now.");
|
|
3254
|
-
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
3255
|
-
console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
|
|
3256
|
-
}
|
|
3257
|
-
if (context.dynamicPort && context.port !== context.requestedPort) {
|
|
3258
|
-
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
3259
|
-
}
|
|
3260
|
-
warnAboutLocalMdns(context.entries);
|
|
3261
|
-
logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
3262
|
-
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
|
|
3263
|
-
await validateCaddyfile(caddyfile);
|
|
3264
|
-
const caddy = startCaddy(caddyfile);
|
|
3265
|
-
const caddyExit = caddy.catch((error) => {
|
|
3266
|
-
if (!caddy.killed) throw error;
|
|
3267
|
-
});
|
|
3268
3442
|
try {
|
|
3269
|
-
|
|
3270
|
-
|
|
3443
|
+
const https = context.https;
|
|
3444
|
+
const readiness = getSetupReadiness({
|
|
3445
|
+
...options,
|
|
3271
3446
|
https,
|
|
3272
|
-
|
|
3273
|
-
|
|
3447
|
+
ignoreCaddyfile: true,
|
|
3448
|
+
entries: context.entries,
|
|
3449
|
+
configPath: context.configPath,
|
|
3450
|
+
projectName: context.projectName
|
|
3274
3451
|
});
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
LOCALGHOST_PORT: String(context.port),
|
|
3290
|
-
LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
|
|
3291
|
-
VITE_PORT: String(context.port)
|
|
3452
|
+
if (!readiness.ready) {
|
|
3453
|
+
if (!options.setup && !context.autoRepair) {
|
|
3454
|
+
throw new Error(
|
|
3455
|
+
[
|
|
3456
|
+
"Localghost setup is missing or stale.",
|
|
3457
|
+
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
3458
|
+
`Run: ${readiness.setupCommand}`,
|
|
3459
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
3460
|
+
].join("\n")
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
3464
|
+
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
3465
|
+
console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
|
|
3292
3466
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
stopManaged("SIGTERM");
|
|
3329
|
-
if (!await waitForPortsToBeAvailable(context.entries, 2e3)) {
|
|
3330
|
-
stopManaged("SIGKILL");
|
|
3467
|
+
if (context.dynamicPort && context.port !== context.requestedPort) {
|
|
3468
|
+
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
3469
|
+
}
|
|
3470
|
+
warnAboutLocalMdns(context.entries);
|
|
3471
|
+
logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
3472
|
+
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
|
|
3473
|
+
await validateCaddyfile(caddyfile);
|
|
3474
|
+
const caddy = startCaddy(caddyfile);
|
|
3475
|
+
const caddyExit = caddy.catch((error) => {
|
|
3476
|
+
if (!caddy.killed) throw error;
|
|
3477
|
+
});
|
|
3478
|
+
try {
|
|
3479
|
+
await maybeTrustCaddy({
|
|
3480
|
+
cwd: options.cwd,
|
|
3481
|
+
https,
|
|
3482
|
+
caddyfilePath: caddyfile,
|
|
3483
|
+
...typeof options.trust === "boolean" ? { trust: options.trust } : {}
|
|
3484
|
+
});
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
signalManagedProcess(caddy, "SIGINT");
|
|
3487
|
+
throw error;
|
|
3488
|
+
}
|
|
3489
|
+
const [binary, ...args] = command;
|
|
3490
|
+
if (!binary) {
|
|
3491
|
+
throw new Error("Missing command. Use: localghost run -- vite");
|
|
3492
|
+
}
|
|
3493
|
+
const child = execa4(binary, args, {
|
|
3494
|
+
cwd: options.cwd,
|
|
3495
|
+
stdio: "inherit",
|
|
3496
|
+
detached: process.platform !== "win32",
|
|
3497
|
+
env: {
|
|
3498
|
+
...process.env,
|
|
3499
|
+
LOCALGHOST_PORT: String(context.port),
|
|
3500
|
+
LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
|
|
3501
|
+
VITE_PORT: String(context.port)
|
|
3331
3502
|
}
|
|
3503
|
+
});
|
|
3504
|
+
const caddyPid = maybePid(caddy.pid);
|
|
3505
|
+
const childPid = maybePid(child.pid);
|
|
3506
|
+
const runRecord = registerLocalghostRun({
|
|
3507
|
+
mode: "run",
|
|
3508
|
+
cwd: context.cwd,
|
|
3509
|
+
projectName: context.projectName,
|
|
3510
|
+
configPath: context.configPath,
|
|
3511
|
+
caddyfilePath: caddyfile,
|
|
3512
|
+
...caddyPid ? { caddyPid } : {},
|
|
3513
|
+
...caddyPid ? { caddyPgid: caddyPid } : {},
|
|
3514
|
+
...childPid ? { childPid } : {},
|
|
3515
|
+
childCommand: command,
|
|
3516
|
+
https,
|
|
3517
|
+
requestedPort: context.requestedPort,
|
|
3518
|
+
port: context.port,
|
|
3519
|
+
dynamicPort: context.dynamicPort,
|
|
3520
|
+
entries: context.entries
|
|
3521
|
+
});
|
|
3522
|
+
const cleanupRun = registerCleanup(runRecord.id);
|
|
3523
|
+
const stopManaged = (signal) => {
|
|
3524
|
+
signalManagedProcess(child, signal);
|
|
3525
|
+
signalManagedProcess(caddy, signal);
|
|
3526
|
+
};
|
|
3527
|
+
const finishShutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
|
|
3528
|
+
try {
|
|
3529
|
+
await Promise.race([child, caddyExit]);
|
|
3530
|
+
} finally {
|
|
3531
|
+
finishShutdown();
|
|
3532
|
+
await waitForProcessShutdown(
|
|
3533
|
+
[child, caddyExit],
|
|
3534
|
+
stopManaged
|
|
3535
|
+
);
|
|
3536
|
+
if (!await waitForPortsToBeAvailable(context.entries)) {
|
|
3537
|
+
console.warn("Localghost: timed out waiting for service ports to be released.");
|
|
3538
|
+
stopManaged("SIGTERM");
|
|
3539
|
+
if (!await waitForPortsToBeAvailable(context.entries, 2e3)) {
|
|
3540
|
+
stopManaged("SIGKILL");
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
cleanupRun();
|
|
3332
3544
|
}
|
|
3333
|
-
|
|
3545
|
+
} finally {
|
|
3334
3546
|
await context.releasePort?.();
|
|
3335
3547
|
}
|
|
3336
3548
|
});
|