@hamedb89/localghost 0.1.6 → 0.1.9

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.
@@ -0,0 +1,249 @@
1
+ # Ghost Tunnel
2
+
3
+ Ghost Tunnel is the production-facing Localghost entrypoint for apps that want a stable wildcard route on top of their existing Vite product:
4
+
5
+ ```txt
6
+ <route>-<project>-<owner>.ghost.<your-domain>
7
+ ```
8
+
9
+ For Social Workouts, the default namespace is:
10
+
11
+ ```txt
12
+ <route>-<project>-<owner>.ghost.socialworkouts.app
13
+ ```
14
+
15
+ The feature is off by default. Opt in from `localghost.config.mjs`:
16
+
17
+ ```js
18
+ import { defineLocalghostConfig } from "@hamedb89/localghost";
19
+
20
+ export default defineLocalghostConfig({
21
+ ghostTunnel: {
22
+ domains: "socialworkouts.app",
23
+ mode: "manual"
24
+ }
25
+ });
26
+ ```
27
+
28
+ With `ghostTunnel: { domains }`, route output and Vite startup output use local defaults for `route`, `project`, and `owner`, then fill the configured domain:
29
+
30
+ ```txt
31
+ localghost ghost tunnel
32
+ mode: manual
33
+ expected: https://app-decision-layer-hamed.ghost.socialworkouts.app/
34
+ ```
35
+
36
+ Without `domains`, the log keeps the production domain wildcarded with `*`:
37
+
38
+ ```txt
39
+ localghost ghost tunnel
40
+ mode: manual
41
+ expected: https://app-decision-layer-hamed.ghost.*/
42
+ ```
43
+
44
+ `manual` is the default activation mode. `ghostTunnel: "manual"` and `ghostTunnel: "public"` are shorthand modes. Use `enabled: false` to keep domains/config in the file without exposing the tunnel surface.
45
+
46
+ Use object form to override defaults or provide a concrete preview URL:
47
+
48
+ ```js
49
+ export default defineLocalghostConfig({
50
+ ghostTunnel: {
51
+ domains: "socialworkouts.app",
52
+ preview: {
53
+ route: "plan",
54
+ project: "summer-base",
55
+ owner: "hamed"
56
+ }
57
+ }
58
+ });
59
+ ```
60
+
61
+ ## Flow
62
+
63
+ 1. Add `ghostTunnel: { domains: "your-domain.com" }`, `ghostTunnel: "manual"`, or `ghostTunnel.preview` to `localghost.config.mjs`.
64
+ 2. Point the wildcard DNS record for `*.ghost.<your-domain>` at the deployed app.
65
+ 3. Route `*.ghost.<your-domain>` to the same production app that serves the Vite build.
66
+ 4. In production request handling, read the Localghost project config without resolving local `.localghost` setup.
67
+ 5. Construct tunnel URLs from `route`, `project`, and `owner`.
68
+ 6. Validate the incoming request host, protocol, and auth before serving the tunnel surface.
69
+
70
+ ```ts
71
+ import {
72
+ assertSecureGhostTunnelRequest,
73
+ constructGhostTunnelUrl,
74
+ readLocalghostProjectConfig
75
+ } from "@hamedb89/localghost";
76
+
77
+ const { config } = await readLocalghostProjectConfig();
78
+
79
+ const url = constructGhostTunnelUrl({
80
+ domain: "socialworkouts.app",
81
+ route: "plan",
82
+ project: "summer-base",
83
+ owner: "hamed",
84
+ ghostTunnel: config.ghostTunnel
85
+ });
86
+
87
+ const route = assertSecureGhostTunnelRequest({
88
+ host: request.headers.get("host") ?? "",
89
+ domain: "socialworkouts.app",
90
+ protocol: request.url.startsWith("https:") ? "https" : "http",
91
+ authenticated: Boolean(session),
92
+ ghostTunnel: config.ghostTunnel
93
+ });
94
+
95
+ // url is https://plan-summer-base-hamed.ghost.socialworkouts.app/
96
+ // route.namespace is { route: "plan", project: "summer-base", owner: "hamed" }.
97
+ ```
98
+
99
+ When `ghostTunnel.preview` is configured, Localghost logs the concrete preview URL in route output and Vite startup output:
100
+
101
+ ```txt
102
+ localghost ghost tunnel
103
+ mode: manual
104
+ expected: https://plan-summer-base-hamed.ghost.socialworkouts.app/
105
+ ```
106
+
107
+ In an interactive Vite terminal, press `g` to show the Ghost Tunnel configuration and open a numbered concrete URL. Wildcard `*` URLs are shown for observability, but the menu only opens configured concrete domains.
108
+
109
+ When the app is behind a trusted deployment proxy, derive `protocol` from the platform's trusted request metadata. Do not trust arbitrary forwarded headers unless the platform has already normalized them.
110
+
111
+ ## Namespace DSL
112
+
113
+ The default namespace tags are `route`, `project`, and `owner`, joined with `-`. The `project` tag is the default spread tag, so project slugs may contain hyphens:
114
+
115
+ ```js
116
+ export default defineLocalghostConfig({
117
+ ghostTunnel: {
118
+ domains: "socialworkouts.app"
119
+ }
120
+ });
121
+ ```
122
+
123
+ That is equivalent to:
124
+
125
+ ```js
126
+ export default defineLocalghostConfig({
127
+ ghostTunnel: {
128
+ namespace: {
129
+ tags: ["route", "project", "owner"],
130
+ spreadTag: "project"
131
+ }
132
+ }
133
+ });
134
+ ```
135
+
136
+ Apps can change the tag order, spread tag, or choose different tag names:
137
+
138
+ ```js
139
+ export default defineLocalghostConfig({
140
+ ghostTunnel: {
141
+ namespace: {
142
+ tags: ["owner", "project", "route"],
143
+ spreadTag: "project"
144
+ }
145
+ }
146
+ });
147
+ ```
148
+
149
+ For custom tags, pass extra values to the constructor:
150
+
151
+ ```ts
152
+ constructGhostTunnelUrl({
153
+ domain: "socialworkouts.app",
154
+ route: "plan",
155
+ project: "summer-base",
156
+ owner: "hamed",
157
+ values: { environment: "preview" },
158
+ ghostTunnel: {
159
+ namespace: ["environment", "route", "project", "owner"]
160
+ }
161
+ });
162
+ ```
163
+
164
+ ## Guardrails
165
+
166
+ - `ghostTunnel` is opt-in and resolves to disabled unless the project config enables it.
167
+ - The default production entry host is `ghost.<your-domain>`, with a wildcard of `*.ghost.<your-domain>`.
168
+ - The default wildcard label must be `route-project-owner`, such as `plan-summer-base-hamed.ghost.socialworkouts.app`.
169
+ - The configured spread tag may contain the namespace separator. By default, that is `project`.
170
+ - Other namespace values cannot include the namespace separator, because parsing must be reversible.
171
+ - Host labels must be DNS-safe lowercase ASCII labels after normalization.
172
+ - HTTPS is required by default. Set `ghostTunnel: { requireHttps: false }` only for controlled non-production testing.
173
+ - Auth is required by default. `assertSecureGhostTunnelRequest` rejects the request unless the app passes `authenticated: true`.
174
+ - Local Caddy and `/etc/hosts` setup do not manage Ghost Tunnel. They stay local-development-only.
175
+
176
+ ## Relay Security
177
+
178
+ Localghost relay is private by default. Public requests can select a Ghost Tunnel route, but they must never select the local target URL, hostname, IP, or port. There must be no `/proxy?url=...` style endpoint.
179
+
180
+ Route registration goes through an authenticated local agent:
181
+
182
+ ```ts
183
+ import {
184
+ createRelayRouteRegistration,
185
+ signRelayRouteClaim
186
+ } from "@hamedb89/localghost";
187
+
188
+ const claim = signRelayRouteClaim({
189
+ host: "plan-summer-base-hamed.ghost.socialworkouts.app",
190
+ scope: "socialworkouts:preview",
191
+ agentId: "local-agent-1",
192
+ expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString()
193
+ }, signingSecret);
194
+
195
+ const route = createRelayRouteRegistration({
196
+ authorizationHeader: request.headers.get("authorization"),
197
+ agentToken,
198
+ claimToken: claim.token,
199
+ signingSecret,
200
+ expectedScope: "socialworkouts:preview",
201
+ target: { host: "127.0.0.1", port: 5173 },
202
+ passwordProtected: true
203
+ });
204
+ ```
205
+
206
+ The relay helpers enforce these rules:
207
+
208
+ - Route registration requires a matching `Bearer <agentToken>`.
209
+ - Route claims are exact hostnames, signed, scoped, and expiring.
210
+ - Wildcard route claims are rejected.
211
+ - Targets must be explicit `{ host, port, protocol }` objects, never arbitrary URL strings.
212
+ - Default target hosts are only `localhost`, `127.0.0.1`, and `::1`.
213
+ - Blocked ports are `22`, `2375`, `2376`, `5432`, `6379`, `9200`, `9229`, and `27017`.
214
+ - LAN/private-network targets require explicit target-policy opt-in and explicit allowed hosts.
215
+ - Private previews require password or app auth unless `publicMode: true` is explicitly set.
216
+ - `isRelayRouteActive(route, { agentConnected })` expires routes when the local agent disconnects or the claim expires.
217
+ - Default limits cover request body size, response size, timeout, concurrency, per-route rate, and per-IP rate.
218
+ - `stripRelayForwardHeaders()` removes hop-by-hop and `x-localghost-*` internal headers before forwarding.
219
+ - `redactRelayHeaders()` and `redactRelayLogUrl()` redact `Authorization`, `Cookie`, `Set-Cookie`, and token-like query params from logs.
220
+ - `renderRelayOfflineResponse()` returns a safe offline page with no secrets or stack traces.
221
+ - Vite integration continues to generate explicit `allowedHosts`; it never sets `allowedHosts: true`.
222
+
223
+ Run the guardrail tests locally:
224
+
225
+ ```sh
226
+ npm test
227
+ npm run test:cli
228
+ npm run test:coverage
229
+ ```
230
+
231
+ `npm test` checks the built package surface. `npm run test:cli` runs the local CLI smoke checks. `npm run test:coverage` imports the source modules and enforces coverage thresholds for `src/relay.ts` and `src/tunnel.ts`.
232
+
233
+ ## Custom Subdomain
234
+
235
+ Use a custom entry label only when the production route truly needs it:
236
+
237
+ ```js
238
+ export default defineLocalghostConfig({
239
+ ghostTunnel: {
240
+ subdomain: "preview"
241
+ }
242
+ });
243
+ ```
244
+
245
+ That changes the wildcard to:
246
+
247
+ ```txt
248
+ <route>-<project>-<owner>.preview.example.app
249
+ ```
package/docs/github.md CHANGED
@@ -4,11 +4,11 @@ Use this copy for the GitHub repo About box, topics, and social cards. Keep it s
4
4
 
5
5
  ## Repository Description
6
6
 
7
- Friendly local hostnames for app repos. A tiny CLI for `.localghost` configs, `/etc/hosts` blocks, Caddy reverse proxies, and Vite `allowedHosts`.
7
+ Friendly local hostnames for app repos. Install the dev dependency, run `yarn dev`, and get clean `.localhost` URLs with Caddy and Vite-safe hosts.
8
8
 
9
9
  Shorter alternative:
10
10
 
11
- Friendly local hostnames for app repos, powered by `.localghost`, Caddy, `/etc/hosts`, and Vite.
11
+ Friendly local hostnames for app repos. `yarn add -D`, `yarn dev`, ready.
12
12
 
13
13
  ## Topics
14
14
 
@@ -62,7 +62,7 @@ After creating `hamedb89/localghost`, this sets the public repo metadata:
62
62
 
63
63
  ```sh
64
64
  gh repo edit hamedb89/localghost \
65
- --description "Friendly local hostnames for app repos. A tiny CLI for .localghost configs, /etc/hosts blocks, Caddy reverse proxies, and Vite allowedHosts." \
65
+ --description "Friendly local hostnames for app repos. Install the dev dependency, run yarn dev, and get clean .localhost URLs with Caddy and Vite-safe hosts." \
66
66
  --homepage "https://hamedb89.github.io/localghost/" \
67
67
  --add-topic localhost \
68
68
  --add-topic local-development \
@@ -78,13 +78,13 @@ gh repo edit hamedb89/localghost \
78
78
 
79
79
  ## README Opening Shape
80
80
 
81
- The first visible paragraph should say what it is, who it is for, and what tools it touches:
81
+ The first visible paragraph should make the entrypoint feel obvious before it gets into configuration:
82
82
 
83
83
  ```txt
84
- Localghost is a tiny Node.js CLI for local domains in app repos. It gives each project one small contract for `.localhost` hostnames, Caddy reverse proxies, Vite `allowedHosts`, and the system hosts file.
84
+ Localghost is a tiny Node.js CLI for friendly local domains in app repos. Add it as a dev dependency, run `yarn dev`, and use `http://app.localhost/` instead of remembering which localhost port belongs to which process.
85
85
  ```
86
86
 
87
- That phrasing helps GitHub search and npm search without making the README feel like SEO sludge.
87
+ Then the next docs layer can explain `.localghost`, Caddy, `/etc/hosts`, Vite `allowedHosts`, and configuration options.
88
88
 
89
89
  ## GitHub Pages
90
90
 
@@ -10,24 +10,27 @@ localghost - friendly local hostnames for app repos
10
10
  localghost init [--write-scripts] [--config file] [--host host] [--port port]
11
11
  localghost doctor
12
12
  localghost setup [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
13
+ localghost trust [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
13
14
  localghost reset [--project name]
14
15
  localghost teardown [--project name] [--remove-caddyfile]
15
16
  localghost status [--ready] [--json]
16
17
  localghost ps [--json]
17
18
  localghost update [--json]
18
- localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--setup]
19
- localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--setup] [--dynamic-port] -- command
19
+ localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--setup] [--trust]
20
+ localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--setup] [--trust] [--dynamic-port] -- command
20
21
  localghost print [--config file] [--config-pattern regex]
21
22
  ```
22
23
 
23
24
  ## Description
24
25
 
25
- Localghost reads `.localghost`, writes a managed `/etc/hosts` block, records `ops/local/localghost-state.json`, generates `ops/local/Caddyfile`, and runs a Caddy local proxy. HTTP is the default; local HTTPS is explicit with `--https` or `--ssl`. It is intentionally small and explicit: no hidden installs, no full hosts-file rewrites, no surprise browser tabs, and no broad Vite `allowedHosts: true` shortcut.
26
+ Localghost reads `.localghost`, optionally reads `localghost.config.mjs`, writes a managed `/etc/hosts` block, records `ops/local/localghost-state.json`, generates `ops/local/Caddyfile`, and runs a Caddy local proxy. The project name is derived from `package.json`, port `5173` is the fallback, HTTP is the default, dynamic ports are on by default, and local HTTPS is explicit with `--https`, `--ssl`, or `https: true` in `localghost.config.mjs`. It is intentionally small and explicit: no hidden installs, no full hosts-file rewrites, no surprise browser tabs, and no broad Vite `allowedHosts: true` shortcut.
26
27
 
27
28
  Localghost checks npm for newer releases after successful commands. The check is best-effort, cached for 24 hours, and can be disabled with `LOCALGHOST_NO_UPDATE_CHECK=1` or `--no-update-check`.
28
29
 
29
30
  `setup`, `dev`, and `teardown` refuse to run in production-like environments such as `NODE_ENV=production`, `VERCEL_ENV=production`, or `LOCALGHOST_ENV=production`.
30
31
 
32
+ When HTTPS is enabled, `dev` and `run` can trust Caddy's local HTTPS CA before the child app starts. Localghost asks once in interactive terminals, records the answer in `ops/local/localghost-state.json`, and supports `--trust` or `localghost trust` when you want to rerun the trust step intentionally.
33
+
31
34
  ## Commands
32
35
 
33
36
  ### init
@@ -67,6 +70,14 @@ Updates the managed Localghost block in `/etc/hosts`, writes `ops/local/Caddyfil
67
70
  localghost setup --project app
68
71
  ```
69
72
 
73
+ ### trust
74
+
75
+ Validates the HTTPS Caddyfile and runs `caddy trust --config <Caddyfile>` so browsers can trust Caddy's local development certificates. macOS may ask for your password to add Caddy's local CA to Keychain.
76
+
77
+ ```sh
78
+ localghost trust
79
+ ```
80
+
70
81
  ### teardown
71
82
 
72
83
  Removes the managed Localghost block from `/etc/hosts` for the selected project and records the action in `ops/local/localghost-state.json`. It leaves `ops/local/Caddyfile` in place unless `--remove-caddyfile` is passed.
@@ -119,7 +130,7 @@ localghost routes
119
130
 
120
131
  ### dev
121
132
 
122
- Requires setup to be ready, writes `ops/local/Caddyfile`, validates it, and runs Caddy. Supports `--config` and `--config-pattern`. HTTP is the default. Pass `--https` or `--ssl` to run a local HTTPS proxy. Pass `--setup` to explicitly allow `dev` to run setup first when setup is missing or stale.
133
+ Requires setup to be ready, writes `ops/local/Caddyfile`, validates it, and runs Caddy. Supports `--config` and `--config-pattern`. HTTP is the default. Pass `--https` or `--ssl` to run a local HTTPS proxy. Pass `--setup` to explicitly allow `dev` to run setup first when setup is missing or stale. Pass `--trust` to force the Caddy trust step before the proxy stays running.
123
134
 
124
135
  ```sh
125
136
  localghost dev
@@ -127,14 +138,23 @@ localghost dev
127
138
 
128
139
  ### run
129
140
 
130
- Resolves one Localghost context, ensures setup is ready, writes the runtime Caddyfile, starts Caddy, and runs a child dev command. The selected port is passed to the child as `LOCALGHOST_PORT` and `VITE_PORT`.
141
+ Resolves one Localghost context, ensures setup is ready, writes the runtime Caddyfile, starts Caddy, handles the optional HTTPS trust prompt, and then runs a child dev command. The selected port is passed to the child as `LOCALGHOST_PORT` and `VITE_PORT`.
131
142
 
132
143
  ```sh
133
144
  localghost run -- vite
134
- localghost run --dynamic-port -- turbo dev
145
+ localghost run --trust -- vite
146
+ localghost run --dynamic-port=no -- vite
135
147
  ```
136
148
 
137
- Pass `--dynamic-port` or `--dynamic-port=yes` to start at the configured port and walk upward until `127.0.0.1:<port>` is free. Pass `--setup` to explicitly allow setup when the hosts block is missing or stale.
149
+ By default, Localghost starts at the configured port and walks upward until `127.0.0.1:<port>` is free. Pass `--dynamic-port=no` when you want strict fixed-port behavior. Pass `--setup` to explicitly allow setup when the hosts block is missing or stale.
150
+
151
+ When `localghost.config.mjs` exists, `run`, `dev`, `setup`, `status`, `routes`, and the Vite plugin use it as an override layer. Most repos can skip it; add it only for decisions like `https: true`, `dynamicPort: false`, `wwwAlias: false`, custom ports, explicit project names, or the production `ghostTunnel` opt-in.
152
+
153
+ `ghostTunnel` does not change local Caddy or `/etc/hosts` setup. It marks `<route>-<project>-<owner>.ghost.<domain>` as a production app entrypoint. Use `ghostTunnel: { domains: "example.com", mode: "manual" }` when the production base domain is known, or omit `domains` to keep logs wildcarded as `https://<route>-<project>-<owner>.ghost.*/`. Production code can call `readLocalghostProjectConfig()`, `constructGhostTunnelUrl()`, and `assertSecureGhostTunnelRequest()` to read the flag, construct default tunnel URLs, validate the wildcard host shape, require HTTPS by default, and require an app-authenticated request by default.
154
+
155
+ Relay helpers are private by default. Registration requires an authenticated local-agent bearer token plus an exact signed route claim. Targets must be explicit local host/port objects, dangerous ports are blocked, private/LAN targets require explicit opt-in, internal and hop-by-hop headers are stripped, sensitive logs are redacted, and offline agents get a safe 503 page.
156
+
157
+ When `ghostTunnel` is configured, route output and Vite startup logs print local defaults for `route`, `project`, and `owner`. Add `ghostTunnel.domains` to fill one or more production base domains. When `ghostTunnel.preview` is configured with `route`, `project`, and `owner`, they print the concrete URL, inheriting `ghostTunnel.domains` unless `preview.domain` is set. In an interactive Vite terminal, press `g` to show Ghost Tunnel configuration and open a numbered concrete URL.
138
158
 
139
159
  `dev` and `run` register active sessions in a user-local activity file so `localghost ps` can show what is running across projects.
140
160
 
@@ -149,6 +169,7 @@ localghost print
149
169
  ## Files
150
170
 
151
171
  - `.localghost`: default project hostname config.
172
+ - `localghost.config.mjs`: optional shared context for CLI and Vite settings.
152
173
  - custom config files: pass `--config <file>` or `--config-pattern <regex>`.
153
174
  - `ops/local/Caddyfile`: generated local Caddy config.
154
175
  - `ops/local/localghost-state.json`: last setup or teardown action.
@@ -0,0 +1,108 @@
1
+ # Localghost macOS Widget
2
+
3
+ The Localghost widget is a tiny native macOS helper. It does not start or stop apps. It reads Localghost's shared activity file directly and shows the known setup/running instances.
4
+
5
+ The menu-bar title is `LG n`, where `n` is the number of known Localghost setups across the machine. The menu lists each project, working directory, route, target port, and whether the upstream port is listening. One widget tracks all setup instances; you do not run one widget per project. Running/listening routes use the green status dot, while configured but idle routes stay visible without the green state.
6
+
7
+ The app also opens a small floating desktop widget using the visual direction from `Resources/localghost-widget-ui-reference.png`: dark rounded panel, Localghost title, online count, route rows, ports, and an open-first-host footer. The black/white logo source at `Resources/localghost-logo-source.png` is bundled and processed at runtime into the app image, template menu-bar icon, and white panel logo.
8
+
9
+ ## Build
10
+
11
+ Build the CLI first, then build the app bundle:
12
+
13
+ ```sh
14
+ npm run build
15
+ npm run macos:widget:build
16
+ ```
17
+
18
+ The app is written to:
19
+
20
+ ```txt
21
+ dist/LocalghostWidget.app
22
+ ```
23
+
24
+ The bundle includes:
25
+
26
+ ```txt
27
+ Contents/Resources/localghost-logo-source.png
28
+ Contents/Resources/localghost-widget-ui-reference.png
29
+ ```
30
+
31
+ ## Targets
32
+
33
+ The macOS widget code is split into three slices:
34
+
35
+ ```txt
36
+ apps/macos-widget/LocalghostWidget.swift
37
+ apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift
38
+ apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift
39
+ apps/macos-widget/project.yml
40
+ ```
41
+
42
+ - `LocalghostWidget.swift`: menu-bar helper and floating glass desktop panel.
43
+ - `Shared/LocalghostWidgetSnapshot.swift`: Codable snapshot contract shared by the helper app and WidgetKit extension.
44
+ - `WidgetExtension/LocalghostDesktopWidget.swift`: WidgetKit extension source for a real macOS desktop widget.
45
+ - `project.yml`: XcodeGen target definition for the containing app and the WidgetKit extension.
46
+
47
+ The helper app reads Localghost's live activity file and writes a smaller snapshot to the shared widget store. The WidgetKit target reads that snapshot because system widgets run in an extension context and should not depend on shell commands or arbitrary home-directory paths.
48
+
49
+ ## Run
50
+
51
+ If `localghost` is installed on your shell path, launch the app bundle normally.
52
+
53
+ For source development, point the widget at the repo build:
54
+
55
+ ```sh
56
+ LOCALGHOST_CLI="$PWD/dist/cli.js" dist/LocalghostWidget.app/Contents/MacOS/LocalghostWidget
57
+ ```
58
+
59
+ ## Desktop Widget Model
60
+
61
+ This helper behaves like a desktop widget: it is a small glass panel that can sit on the desktop and is shown or hidden from the menu-bar icon. Install or run it like a normal macOS app, then use the Localghost icon in the top bar to show or hide the panel.
62
+
63
+ The separate WidgetKit target is the source needed for a system desktop widget. To make it addable from macOS "Edit Widgets":
64
+
65
+ 1. Create or open an Xcode macOS app project for `LocalghostWidget`.
66
+ 2. Add `LocalghostWidget.swift`, `Shared/LocalghostWidgetSnapshot.swift`, and the resources to the app target.
67
+ 3. Add a Widget Extension target named `LocalghostDesktopWidgetExtension`.
68
+ 4. Add `WidgetExtension/LocalghostDesktopWidget.swift` and `Shared/LocalghostWidgetSnapshot.swift` to the extension target.
69
+ 5. Enable the same App Group on both targets: `group.app.localghost`.
70
+ 6. Sign and run/open the containing app once.
71
+ 7. Control-click the desktop, choose `Edit Widgets`, search for `Localghost`, and add the widget.
72
+
73
+ The raw `npm run macos:widget:build` script builds only the standalone menu-bar helper. WidgetKit discovery requires the Xcode app + extension bundle/signing flow above.
74
+
75
+ If XcodeGen is installed, generate the Xcode project with:
76
+
77
+ ```sh
78
+ cd apps/macos-widget
79
+ xcodegen generate
80
+ open LocalghostWidget.xcodeproj
81
+ ```
82
+
83
+ Then set your development team and App Group identifier before building/running the app from Xcode.
84
+
85
+ ## Data Source
86
+
87
+ The helper reads:
88
+
89
+ ```txt
90
+ ~/.local/state/localghost/activity.json
91
+ ```
92
+
93
+ The helper writes the WidgetKit snapshot to the App Group container when available:
94
+
95
+ ```txt
96
+ group.app.localghost/LocalghostWidgetSnapshot.json
97
+ ```
98
+
99
+ The CLI can inspect the same state with:
100
+
101
+ ```sh
102
+ localghost ps
103
+ localghost ps --json
104
+ ```
105
+
106
+ The activity file stores setup records plus active run records. `localghost setup` registers configured projects in that shared file. `localghost dev`, `localghost run`, and the Vite plugin overlay active process data on top. `localghost reset` and `localghost teardown` remove the setup from the shared file.
107
+
108
+ Set `LOCALGHOST_ACTIVITY_PATH` when you want the CLI and widget to share a custom activity file during tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamedb89/localghost",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Friendly local hostnames for app repos with .localghost, Caddy, /etc/hosts, and Vite.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
+ "apps/macos-widget",
20
21
  "dist",
21
22
  "assets",
22
23
  "docs",
@@ -41,13 +42,18 @@
41
42
  "scripts": {
42
43
  "clean": "rm -rf dist",
43
44
  "build": "tsup src/index.ts src/vite.ts src/cli.ts --format esm --dts",
45
+ "macos:widget:build": "bash apps/macos-widget/build.sh",
44
46
  "dev": "tsx src/cli.ts",
47
+ "test": "npm run build && node --test tests/*.test.mjs",
48
+ "test:coverage": "npm run build && LOCALGHOST_TEST_SOURCE=1 node --import tsx --test --experimental-test-coverage --test-coverage-include=src/relay.ts --test-coverage-include=src/tunnel.ts --test-coverage-lines=90 --test-coverage-branches=75 --test-coverage-functions=100 tests/*.test.mjs",
49
+ "test:cli": "npm run build && node --test tests/cli.test.mjs",
45
50
  "typecheck": "tsc --noEmit",
46
51
  "prepack": "npm run build",
47
52
  "prepublishOnly": "npm run release:check",
48
53
  "pack:dry": "npm pack --dry-run",
49
- "release:check": "npm run version:check && npm run typecheck && npm run build && npm run site:build && npm pack --dry-run",
54
+ "release:check": "npm run version:check && npm run typecheck && npm test && npm run site:build && npm pack --dry-run",
50
55
  "site:build": "node scripts/build-site.mjs",
56
+ "site:serve": "node scripts/serve-site.mjs",
51
57
  "sync:version": "node scripts/sync-readme-version.mjs",
52
58
  "version:check": "node scripts/sync-readme-version.mjs --check",
53
59
  "version": "npm run sync:version && git add README.md src/update-check.ts",
@@ -1,31 +0,0 @@
1
- type DevHostEntry = {
2
- host: string;
3
- port: number;
4
- target: string;
5
- };
6
- declare function parseDevHosts(input: string, fileName?: string): DevHostEntry[];
7
- declare function findLocalMdnsHosts(entries: DevHostEntry[]): string[];
8
-
9
- declare const LOCALGHOST_CONFIG_FILE = ".localghost";
10
- type ConfigPattern = string | RegExp;
11
- type ReadDevHostsOptions = {
12
- cwd?: string;
13
- fileName?: string;
14
- configFiles?: string[];
15
- configPattern?: ConfigPattern;
16
- };
17
- type ResolvedDevHostsPath = {
18
- path: string;
19
- fileName: string;
20
- exists: boolean;
21
- searchedFiles: string[];
22
- configPattern?: ConfigPattern;
23
- };
24
- declare function getConfigFileCandidates(options?: ReadDevHostsOptions): string[];
25
- declare function resolveDevHostsPath(options?: ReadDevHostsOptions): ResolvedDevHostsPath;
26
- declare function getDevHostsPath(options?: ReadDevHostsOptions): string;
27
- declare function readDevHosts(options?: ReadDevHostsOptions | string): DevHostEntry[];
28
- declare function getProjectName(cwd?: string): string;
29
- declare function sanitizeProjectName(value: string): string;
30
-
31
- export { type ConfigPattern as C, type DevHostEntry as D, LOCALGHOST_CONFIG_FILE as L, type ReadDevHostsOptions as R, type ResolvedDevHostsPath as a, getDevHostsPath as b, getProjectName as c, resolveDevHostsPath as d, findLocalMdnsHosts as f, getConfigFileCandidates as g, parseDevHosts as p, readDevHosts as r, sanitizeProjectName as s };