@hamedb89/localghost 0.1.3 → 0.1.8

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 CHANGED
@@ -9,9 +9,9 @@ Buh. Friendly local hostnames for app repos.
9
9
  [![CI](https://github.com/hamedb89/localghost/actions/workflows/ci.yml/badge.svg)](https://github.com/hamedb89/localghost/actions/workflows/ci.yml)
10
10
  [![GitHub Pages](https://github.com/hamedb89/localghost/actions/workflows/pages.yml/badge.svg)](https://github.com/hamedb89/localghost/actions/workflows/pages.yml)
11
11
  [![Publish npm](https://github.com/hamedb89/localghost/actions/workflows/publish-npm.yml/badge.svg)](https://github.com/hamedb89/localghost/actions/workflows/publish-npm.yml)
12
- [![npm version](https://img.shields.io/badge/npm-v0.1.3-CB3837?logo=npm)](https://www.npmjs.com/package/@hamedb89/localghost)
12
+ [![npm version](https://img.shields.io/badge/npm-v0.1.8-CB3837?logo=npm)](https://www.npmjs.com/package/@hamedb89/localghost)
13
13
 
14
- Localghost is a tiny Node.js CLI for local HTTPS domains in app repos. It gives each project one small contract for `.localhost` hostnames, Caddy reverse proxies, Vite `allowedHosts`, and the system hosts file, so developers can open `https://app.localhost/` instead of remembering which localhost port belongs to which process.
14
+ Localghost is a tiny Node.js CLI for friendly 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, so developers can open `http://app.localhost/` instead of remembering which localhost port belongs to which process.
15
15
 
16
16
  [Website](https://hamedb89.github.io/localghost/) · [npm](https://www.npmjs.com/package/@hamedb89/localghost) · [GitHub](https://github.com/hamedb89/localghost)
17
17
 
@@ -20,9 +20,10 @@ Localghost is a tiny Node.js CLI for local HTTPS domains in app repos. It gives
20
20
  - Creates and reads `.localghost` in your app repo.
21
21
  - Lets repos choose explicit config files or filename patterns when `.localghost` is not enough.
22
22
  - Updates only a managed Localghost block in `/etc/hosts` during explicit setup.
23
- - Generates `ops/local/Caddyfile` for local HTTPS reverse proxying.
23
+ - Generates `ops/local/Caddyfile` for local reverse proxying. HTTP is the default; HTTPS is explicit with `--https` or `--ssl`.
24
24
  - Checks whether Caddy is installed, but does not run Homebrew for you.
25
25
  - Provides a Vite plugin that sets explicit `server.allowedHosts` entries.
26
+ - Defaults Vite dev to the configured Localghost domain and no-ops during production/build.
26
27
  - Prints parsed config and project-local state as JSON for scripts, Codex, agents, and future MCP tools.
27
28
  - Checks npm for newer Localghost releases at most once per day, with an explicit opt-out.
28
29
 
@@ -83,12 +84,37 @@ First time on a machine:
83
84
  yarn localghost:setup
84
85
  ```
85
86
 
87
+ Check that the hosts block and Caddyfile are ready:
88
+
89
+ ```sh
90
+ yarn localghost:ready
91
+ ```
92
+
86
93
  Daily proxy:
87
94
 
88
95
  ```sh
89
96
  yarn localghost:proxy
90
97
  ```
91
98
 
99
+ Use HTTPS only when you explicitly want Caddy local certificates:
100
+
101
+ ```sh
102
+ yarn localghost:proxy:https
103
+ ```
104
+
105
+ Trust Caddy's local HTTPS CA when you want browsers to stop showing local certificate warnings:
106
+
107
+ ```sh
108
+ yarn localghost:trust
109
+ ```
110
+
111
+ Reset generated setup without deleting `.localghost`:
112
+
113
+ ```sh
114
+ yarn localghost:reset
115
+ yarn localghost:setup
116
+ ```
117
+
92
118
  Prefer `.localhost` names. `.local` is supported, but Localghost warns because `.local` can collide with mDNS/Bonjour.
93
119
 
94
120
  ## Config Files
@@ -123,12 +149,20 @@ The Vite plugin accepts the same shape through `fileName`, `configFiles`, or `co
123
149
  "scripts": {
124
150
  "localghost:setup": "localghost setup",
125
151
  "localghost:proxy": "localghost dev",
152
+ "localghost:proxy:https": "localghost dev --https",
153
+ "localghost:run": "localghost run --",
154
+ "localghost:ready": "localghost status --ready",
155
+ "localghost:trust": "localghost trust",
156
+ "localghost:ps": "localghost ps",
126
157
  "localghost:print": "localghost print",
127
158
  "localghost:routes": "localghost routes",
128
159
  "localghost:status": "localghost status",
160
+ "localghost:reset": "localghost reset",
129
161
  "localghost:teardown": "localghost teardown",
130
162
  "localghost:doctor": "localghost doctor",
131
- "localghost:update": "localghost update"
163
+ "localghost:update": "localghost update",
164
+ "caddy:setup": "localghost setup",
165
+ "caddy:dev": "localghost dev"
132
166
  }
133
167
  }
134
168
  ```
@@ -138,13 +172,89 @@ A full app might compose them with its own servers:
138
172
  ```json
139
173
  {
140
174
  "scripts": {
141
- "dev:web": "vite --host 127.0.0.1 --port 5173 --strictPort",
142
- "dev:api": "wrangler dev --port 8787",
143
- "dev:local": "concurrently -k \"npm run dev:web\" \"npm run dev:api\" \"npm run localghost:proxy\""
175
+ "dev": "localghost run -- yarn dev:raw",
176
+ "dev:dynamic": "localghost run --dynamic-port -- vite",
177
+ "dev:raw": "vite"
144
178
  }
145
179
  }
146
180
  ```
147
181
 
182
+ In Turborepo, let Localghost wrap the dev runner and keep dev uncached:
183
+
184
+ ```json
185
+ {
186
+ "scripts": {
187
+ "dev": "localghost run --dynamic-port -- yarn dev:raw",
188
+ "dev:raw": "turbo dev"
189
+ }
190
+ }
191
+ ```
192
+
193
+ Then keep persistent dev tasks uncached:
194
+
195
+ ```json
196
+ {
197
+ "tasks": {
198
+ "dev": { "cache": false, "persistent": true }
199
+ }
200
+ }
201
+ ```
202
+
203
+ `localghost run` resolves one shared Localghost context, starts Caddy, handles the optional HTTPS trust prompt, then starts the child command. That keeps Localghost setup/proxy output before Vite's ready log. It passes the selected port to the child command through `LOCALGHOST_PORT` and `VITE_PORT`, and stops Caddy when the child exits. With `--dynamic-port`, Localghost starts at the configured port, checks `127.0.0.1:<port>`, and walks upward until it finds a free port.
204
+
205
+ When settings need to be shared by the CLI wrapper and the Vite plugin, put them in `localghost.config.mjs`:
206
+
207
+ ```js
208
+ import { defineLocalghostConfig } from "@hamedb89/localghost";
209
+
210
+ export default defineLocalghostConfig({
211
+ project: "app",
212
+ port: 5173,
213
+ https: true,
214
+ dynamicPort: true
215
+ });
216
+ ```
217
+
218
+ Then the daily script can stay small:
219
+
220
+ ```json
221
+ {
222
+ "scripts": {
223
+ "dev": "localghost run -- yarn dev:raw",
224
+ "dev:raw": "vite"
225
+ }
226
+ }
227
+ ```
228
+
229
+ `www.` aliases are enabled by default. A `.localghost` entry like `app.localhost 5173` also sets up `www.app.localhost` unless `wwwAlias: false` is set in `localghost.config.mjs`.
230
+
231
+ `localghost dev` and `localghost run` also register their active sessions in a user-local activity file. Use `localghost ps` to see the Localghost apps currently running on the machine:
232
+
233
+ ```txt
234
+ localghost ps
235
+
236
+ app run: vite
237
+ cwd: /Users/you/Projects/app
238
+ pid: 12345, caddy: 12346, child: 12347
239
+ started: 2026-07-05T12:00:00.000Z
240
+ app.localhost -> 127.0.0.1:5173 (listening)
241
+ ```
242
+
243
+ Pass `--json` when another helper, such as a menu bar app, needs to poll the same state.
244
+
245
+ ## macOS Widget
246
+
247
+ Localghost includes a tiny native macOS menu-bar widget in `apps/macos-widget`. It shows `LG n` in the top bar, where `n` is the number of active Localghost-managed apps, and its menu lists each project, route, target port, and listening state.
248
+
249
+ Build it from source:
250
+
251
+ ```sh
252
+ npm run build
253
+ npm run macos:widget:build
254
+ ```
255
+
256
+ The app bundle is written to `dist/LocalghostWidget.app`. See [docs/macos-widget.md](./docs/macos-widget.md) for local development notes.
257
+
148
258
  ## Vite
149
259
 
150
260
  ```ts
@@ -155,26 +265,26 @@ export default defineConfig({
155
265
  plugins: [
156
266
  localGhostPlugin({
157
267
  port: 5173,
158
- https: true,
159
268
  configFiles: [".localghost.private", ".localghost"]
160
269
  })
161
270
  ]
162
271
  });
163
272
  ```
164
273
 
165
- The plugin generates an explicit `server.allowedHosts` list from the selected config file; it does not set `allowedHosts: true`.
274
+ The plugin binds Vite to `127.0.0.1` by default, prints the selected Localghost domain, generates an explicit `server.allowedHosts` list from the selected config file, and does not set `allowedHosts: true`. It runs only during local `vite serve`; production/build mode no-ops. When `dynamicPort` is enabled in plugin options or `localghost.config.mjs`, the plugin uses the configured port when available and otherwise moves to the next free port before Vite starts.
275
+
276
+ If `.localghost` is missing and Vite is running in an interactive terminal, the plugin asks whether to create one, prompts for the primary domain and optional extra domains, and then asks whether to run setup. Before touching `/etc/hosts`, it explains why macOS may ask for your password and confirms that only Localghost's managed block is changed.
166
277
 
167
278
  When Vite starts, Localghost prints the browser-facing URLs:
168
279
 
169
280
  ```txt
170
281
  localghost
171
- open: https://app.localhost/
172
- also: https://www.app.localhost/
282
+ local: http://app.localhost/
283
+ also: http://www.app.localhost/
173
284
  target: http://127.0.0.1:5173/
174
- proxy: Caddy local HTTPS
175
285
  ```
176
286
 
177
- `https: true` means the browser-facing URL is expected to go through Caddy on HTTPS, while Vite still runs behind it on `127.0.0.1:<port>`. The plugin uses that to set Vite websocket/HMR proxy settings and to print `https://...` local host URLs.
287
+ `https: true` means the browser-facing URL is expected to go through Caddy on HTTPS, while Vite still runs behind it on `127.0.0.1:<port>`. The plugin uses that to set Vite websocket/HMR proxy settings and to print `https://...` local host URLs. Localghost never opens browser tabs by default.
178
288
 
179
289
  Set `log: false` if you want to keep Vite's default terminal output only.
180
290
 
@@ -187,17 +297,31 @@ localghost doctor
187
297
  localghost setup
188
298
  localghost setup --project app
189
299
  localghost setup --config .localghost.preview
300
+ localghost setup --https
301
+ localghost trust
190
302
  localghost status
303
+ localghost status --ready
304
+ localghost ps
305
+ localghost ps --json
306
+ localghost reset
191
307
  localghost teardown
192
308
  localghost teardown --remove-caddyfile
193
309
  localghost update
194
310
  localghost --no-update-check doctor
311
+ localghost run -- vite
312
+ localghost run --trust -- vite
313
+ localghost run --dynamic-port -- turbo dev
195
314
  localghost dev --config-pattern '^\.localghost\.'
315
+ localghost dev --https
196
316
  localghost print
197
317
  ```
198
318
 
199
319
  Localghost checks npm for newer releases after successful commands. The check has a short timeout, is cached for 24 hours, and never fails the command. Disable it with `LOCALGHOST_NO_UPDATE_CHECK=1` or `--no-update-check`. Run `localghost update` when you want an explicit update check.
200
320
 
321
+ `setup`, `dev`, and `teardown` refuse to run in production-like environments such as `NODE_ENV=production`, `VERCEL_ENV=production`, or `LOCALGHOST_ENV=production`.
322
+
323
+ When HTTPS is enabled, `localghost dev` and `localghost run` ask once whether to trust Caddy's local HTTPS CA. If you accept, macOS may ask for your password so Caddy can add its local CA to Keychain. Localghost records the result in `ops/local/localghost-state.json`; use `localghost trust` or `localghost run --trust -- ...` when you want to rerun the trust step intentionally.
324
+
201
325
  `setup` writes only a managed block in the system hosts file:
202
326
 
203
327
  ```txt
@@ -210,19 +334,29 @@ Localghost does not rewrite the whole hosts file. It replaces only its own manag
210
334
 
211
335
  ## Teardown And State
212
336
 
213
- `setup` writes a project-local state file at `ops/local/localghost-state.json`. It records the last Localghost action, selected config path, generated Caddyfile path, hosts file path, and the host entries that were applied. This is durable enough for project tooling and avoids relying on OS temp folders for tracking. Most apps should treat it as generated local state and ignore it in git.
337
+ `setup` writes a project-local state file at `ops/local/localghost-state.json`. It records the last Localghost action, selected config path, generated Caddyfile path, hosts file path, proxy mode, and the host entries that were applied. This is durable enough for project tooling and avoids relying on OS temp folders for tracking. Most apps should treat it as generated local state and ignore it in git.
214
338
 
215
339
  ```sh
216
340
  localghost status
341
+ localghost status --ready
217
342
  localghost status --json
218
343
  ```
219
344
 
345
+ `localghost dev` requires setup to be ready before it starts Caddy. If setup is missing or stale, it prints the exact `localghost setup` command instead of silently running `sudo`. Use `localghost dev --setup` only when you explicitly want the dev command to perform setup first.
346
+
220
347
  When a project no longer needs Localghost, teardown removes only the managed hosts block for the selected project:
221
348
 
222
349
  ```sh
223
350
  localghost teardown
224
351
  ```
225
352
 
353
+ When you want to retest setup without deleting `.localghost`, use reset:
354
+
355
+ ```sh
356
+ localghost reset
357
+ localghost setup
358
+ ```
359
+
226
360
  The generated Caddyfile is left in place by default. Remove it explicitly when you want a fuller cleanup:
227
361
 
228
362
  ```sh
@@ -0,0 +1,218 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ struct LocalghostPsResponse: Decodable {
5
+ let activityPath: String?
6
+ let runs: [LocalghostRun]
7
+ }
8
+
9
+ struct LocalghostRun: Decodable {
10
+ let mode: String
11
+ let pid: Int
12
+ let cwd: String
13
+ let projectName: String
14
+ let startedAt: String
15
+ let childCommand: [String]?
16
+ let routes: [LocalghostRoute]
17
+ }
18
+
19
+ struct LocalghostRoute: Decodable {
20
+ let host: String
21
+ let port: Int
22
+ let target: String
23
+ let listening: Bool
24
+ }
25
+
26
+ final class LocalghostWidgetApp: NSObject, NSApplicationDelegate {
27
+ private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
28
+ private var timer: Timer?
29
+ private var latestRuns: [LocalghostRun] = []
30
+ private var latestError: String?
31
+
32
+ func applicationDidFinishLaunching(_ notification: Notification) {
33
+ NSApp.setActivationPolicy(.accessory)
34
+ statusItem.button?.title = "LG ..."
35
+ rebuildMenu()
36
+ refresh()
37
+ timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
38
+ self?.refresh()
39
+ }
40
+ }
41
+
42
+ private func refresh() {
43
+ DispatchQueue.global(qos: .utility).async { [weak self] in
44
+ let result = Self.loadRuns()
45
+ DispatchQueue.main.async {
46
+ switch result {
47
+ case .success(let runs):
48
+ self?.latestRuns = runs
49
+ self?.latestError = nil
50
+ case .failure(let error):
51
+ self?.latestRuns = []
52
+ self?.latestError = error.localizedDescription
53
+ }
54
+
55
+ self?.updateStatusTitle()
56
+ self?.rebuildMenu()
57
+ }
58
+ }
59
+ }
60
+
61
+ private func updateStatusTitle() {
62
+ if latestError != nil {
63
+ statusItem.button?.title = "LG ?"
64
+ return
65
+ }
66
+
67
+ statusItem.button?.title = "LG \(latestRuns.count)"
68
+ }
69
+
70
+ private func rebuildMenu() {
71
+ let menu = NSMenu()
72
+
73
+ if let latestError {
74
+ let item = NSMenuItem(title: "Localghost unavailable", action: nil, keyEquivalent: "")
75
+ item.isEnabled = false
76
+ menu.addItem(item)
77
+
78
+ let detail = NSMenuItem(title: latestError, action: nil, keyEquivalent: "")
79
+ detail.isEnabled = false
80
+ menu.addItem(detail)
81
+ } else if latestRuns.isEmpty {
82
+ let item = NSMenuItem(title: "No Localghost apps running", action: nil, keyEquivalent: "")
83
+ item.isEnabled = false
84
+ menu.addItem(item)
85
+ } else {
86
+ let title = latestRuns.count == 1 ? "1 Localghost app running" : "\(latestRuns.count) Localghost apps running"
87
+ let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
88
+ item.isEnabled = false
89
+ menu.addItem(item)
90
+
91
+ menu.addItem(.separator())
92
+
93
+ for run in latestRuns {
94
+ addRun(run, to: menu)
95
+ menu.addItem(.separator())
96
+ }
97
+ }
98
+
99
+ let refreshItem = NSMenuItem(title: "Refresh", action: #selector(refreshFromMenu), keyEquivalent: "r")
100
+ refreshItem.target = self
101
+ menu.addItem(refreshItem)
102
+
103
+ let quitItem = NSMenuItem(title: "Quit Localghost Widget", action: #selector(quit), keyEquivalent: "q")
104
+ quitItem.target = self
105
+ menu.addItem(quitItem)
106
+
107
+ statusItem.menu = menu
108
+ }
109
+
110
+ private func addRun(_ run: LocalghostRun, to menu: NSMenu) {
111
+ let command = run.childCommand?.joined(separator: " ")
112
+ let mode = command.map { "\(run.mode): \($0)" } ?? run.mode
113
+ let title = "\(run.projectName) \(mode)"
114
+ let projectItem = NSMenuItem(title: title, action: nil, keyEquivalent: "")
115
+ projectItem.isEnabled = false
116
+ menu.addItem(projectItem)
117
+
118
+ let cwdItem = NSMenuItem(title: " \(run.cwd)", action: nil, keyEquivalent: "")
119
+ cwdItem.isEnabled = false
120
+ menu.addItem(cwdItem)
121
+
122
+ let pidItem = NSMenuItem(title: " pid \(run.pid)", action: nil, keyEquivalent: "")
123
+ pidItem.isEnabled = false
124
+ menu.addItem(pidItem)
125
+
126
+ for route in run.routes {
127
+ let state = route.listening ? "listening" : "not listening"
128
+ let routeItem = NSMenuItem(title: " \(route.host) -> \(route.target) (\(state))", action: nil, keyEquivalent: "")
129
+ routeItem.isEnabled = false
130
+ menu.addItem(routeItem)
131
+ }
132
+ }
133
+
134
+ @objc private func refreshFromMenu() {
135
+ refresh()
136
+ }
137
+
138
+ @objc private func quit() {
139
+ NSApp.terminate(nil)
140
+ }
141
+
142
+ private static func loadRuns() -> Result<[LocalghostRun], Error> {
143
+ do {
144
+ let output = try runLocalghostPs()
145
+ let response = try JSONDecoder().decode(LocalghostPsResponse.self, from: output)
146
+ return .success(response.runs)
147
+ } catch {
148
+ return .failure(error)
149
+ }
150
+ }
151
+
152
+ private static func runLocalghostPs() throws -> Data {
153
+ let task = Process()
154
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
155
+ task.arguments = localghostArguments()
156
+ task.environment = [
157
+ "PATH": pathValue()
158
+ ]
159
+
160
+ let stdout = Pipe()
161
+ let stderr = Pipe()
162
+ task.standardOutput = stdout
163
+ task.standardError = stderr
164
+
165
+ try task.run()
166
+ task.waitUntilExit()
167
+
168
+ let output = stdout.fileHandleForReading.readDataToEndOfFile()
169
+ if task.terminationStatus == 0 {
170
+ return output
171
+ }
172
+
173
+ let errorData = stderr.fileHandleForReading.readDataToEndOfFile()
174
+ let message = String(data: errorData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
175
+ throw LocalghostWidgetError.commandFailed(message?.isEmpty == false ? message! : "localghost ps failed")
176
+ }
177
+
178
+ private static func localghostCommand() -> String {
179
+ ProcessInfo.processInfo.environment["LOCALGHOST_CLI"] ?? "localghost"
180
+ }
181
+
182
+ private static func localghostArguments() -> [String] {
183
+ let command = localghostCommand()
184
+ if command.hasSuffix(".js") {
185
+ return ["node", command, "--no-update-check", "ps", "--json"]
186
+ }
187
+
188
+ return [command, "--no-update-check", "ps", "--json"]
189
+ }
190
+
191
+ private static func pathValue() -> String {
192
+ let existing = ProcessInfo.processInfo.environment["PATH"] ?? ""
193
+ let defaults = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
194
+ return existing.isEmpty ? defaults : "\(existing):\(defaults)"
195
+ }
196
+ }
197
+
198
+ enum LocalghostWidgetError: LocalizedError {
199
+ case commandFailed(String)
200
+
201
+ var errorDescription: String? {
202
+ switch self {
203
+ case .commandFailed(let message):
204
+ return message
205
+ }
206
+ }
207
+ }
208
+
209
+ @main
210
+ struct LocalghostWidgetMain {
211
+ private static let delegate = LocalghostWidgetApp()
212
+
213
+ static func main() {
214
+ let app = NSApplication.shared
215
+ app.delegate = delegate
216
+ app.run()
217
+ }
218
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+ APP_DIR="$ROOT_DIR/dist/LocalghostWidget.app"
6
+ CONTENTS_DIR="$APP_DIR/Contents"
7
+ MACOS_DIR="$CONTENTS_DIR/MacOS"
8
+ SOURCE_FILE="$ROOT_DIR/apps/macos-widget/LocalghostWidget.swift"
9
+ EXECUTABLE="$MACOS_DIR/LocalghostWidget"
10
+ MODULE_CACHE_DIR="${TMPDIR:-/tmp}/localghost-swift-module-cache"
11
+
12
+ rm -rf "$APP_DIR"
13
+ mkdir -p "$MACOS_DIR"
14
+ mkdir -p "$MODULE_CACHE_DIR"
15
+
16
+ swiftc \
17
+ -parse-as-library \
18
+ -O \
19
+ -module-cache-path "$MODULE_CACHE_DIR" \
20
+ -framework AppKit \
21
+ "$SOURCE_FILE" \
22
+ -o "$EXECUTABLE"
23
+
24
+ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
25
+ <?xml version="1.0" encoding="UTF-8"?>
26
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
+ <plist version="1.0">
28
+ <dict>
29
+ <key>CFBundleExecutable</key>
30
+ <string>LocalghostWidget</string>
31
+ <key>CFBundleIdentifier</key>
32
+ <string>app.localghost.widget</string>
33
+ <key>CFBundleName</key>
34
+ <string>Localghost Widget</string>
35
+ <key>CFBundleDisplayName</key>
36
+ <string>Localghost Widget</string>
37
+ <key>CFBundlePackageType</key>
38
+ <string>APPL</string>
39
+ <key>CFBundleShortVersionString</key>
40
+ <string>0.1.0</string>
41
+ <key>CFBundleVersion</key>
42
+ <string>1</string>
43
+ <key>LSMinimumSystemVersion</key>
44
+ <string>13.0</string>
45
+ <key>LSUIElement</key>
46
+ <true/>
47
+ <key>NSHighResolutionCapable</key>
48
+ <true/>
49
+ </dict>
50
+ </plist>
51
+ PLIST
52
+
53
+ echo "Built $APP_DIR"