@hamedb89/localghost 0.1.6 → 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 +55 -6
- package/apps/macos-widget/LocalghostWidget.swift +218 -0
- package/apps/macos-widget/build.sh +53 -0
- package/dist/cli.js +224 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +85 -19
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +76 -21
- package/dist/vite.js.map +1 -1
- package/docs/localghost.1.md +20 -5
- package/docs/macos-widget.md +46 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Buh. Friendly local hostnames for app repos.
|
|
|
9
9
|
[](https://github.com/hamedb89/localghost/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/hamedb89/localghost/actions/workflows/pages.yml)
|
|
11
11
|
[](https://github.com/hamedb89/localghost/actions/workflows/publish-npm.yml)
|
|
12
|
-
[](https://www.npmjs.com/package/@hamedb89/localghost)
|
|
13
13
|
|
|
14
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
|
|
|
@@ -102,6 +102,12 @@ Use HTTPS only when you explicitly want Caddy local certificates:
|
|
|
102
102
|
yarn localghost:proxy:https
|
|
103
103
|
```
|
|
104
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
|
+
|
|
105
111
|
Reset generated setup without deleting `.localghost`:
|
|
106
112
|
|
|
107
113
|
```sh
|
|
@@ -146,6 +152,7 @@ The Vite plugin accepts the same shape through `fileName`, `configFiles`, or `co
|
|
|
146
152
|
"localghost:proxy:https": "localghost dev --https",
|
|
147
153
|
"localghost:run": "localghost run --",
|
|
148
154
|
"localghost:ready": "localghost status --ready",
|
|
155
|
+
"localghost:trust": "localghost trust",
|
|
149
156
|
"localghost:ps": "localghost ps",
|
|
150
157
|
"localghost:print": "localghost print",
|
|
151
158
|
"localghost:routes": "localghost routes",
|
|
@@ -165,7 +172,7 @@ A full app might compose them with its own servers:
|
|
|
165
172
|
```json
|
|
166
173
|
{
|
|
167
174
|
"scripts": {
|
|
168
|
-
"dev": "localghost run --
|
|
175
|
+
"dev": "localghost run -- yarn dev:raw",
|
|
169
176
|
"dev:dynamic": "localghost run --dynamic-port -- vite",
|
|
170
177
|
"dev:raw": "vite"
|
|
171
178
|
}
|
|
@@ -177,7 +184,7 @@ In Turborepo, let Localghost wrap the dev runner and keep dev uncached:
|
|
|
177
184
|
```json
|
|
178
185
|
{
|
|
179
186
|
"scripts": {
|
|
180
|
-
"dev": "localghost run --dynamic-port --
|
|
187
|
+
"dev": "localghost run --dynamic-port -- yarn dev:raw",
|
|
181
188
|
"dev:raw": "turbo dev"
|
|
182
189
|
}
|
|
183
190
|
}
|
|
@@ -193,7 +200,33 @@ Then keep persistent dev tasks uncached:
|
|
|
193
200
|
}
|
|
194
201
|
```
|
|
195
202
|
|
|
196
|
-
`localghost run` resolves one shared Localghost context, starts Caddy, 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.
|
|
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`.
|
|
197
230
|
|
|
198
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:
|
|
199
232
|
|
|
@@ -209,6 +242,19 @@ app run: vite
|
|
|
209
242
|
|
|
210
243
|
Pass `--json` when another helper, such as a menu bar app, needs to poll the same state.
|
|
211
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
|
+
|
|
212
258
|
## Vite
|
|
213
259
|
|
|
214
260
|
```ts
|
|
@@ -219,14 +265,13 @@ export default defineConfig({
|
|
|
219
265
|
plugins: [
|
|
220
266
|
localGhostPlugin({
|
|
221
267
|
port: 5173,
|
|
222
|
-
dynamicPort: true,
|
|
223
268
|
configFiles: [".localghost.private", ".localghost"]
|
|
224
269
|
})
|
|
225
270
|
]
|
|
226
271
|
});
|
|
227
272
|
```
|
|
228
273
|
|
|
229
|
-
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
|
|
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.
|
|
230
275
|
|
|
231
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.
|
|
232
277
|
|
|
@@ -253,6 +298,7 @@ localghost setup
|
|
|
253
298
|
localghost setup --project app
|
|
254
299
|
localghost setup --config .localghost.preview
|
|
255
300
|
localghost setup --https
|
|
301
|
+
localghost trust
|
|
256
302
|
localghost status
|
|
257
303
|
localghost status --ready
|
|
258
304
|
localghost ps
|
|
@@ -263,6 +309,7 @@ localghost teardown --remove-caddyfile
|
|
|
263
309
|
localghost update
|
|
264
310
|
localghost --no-update-check doctor
|
|
265
311
|
localghost run -- vite
|
|
312
|
+
localghost run --trust -- vite
|
|
266
313
|
localghost run --dynamic-port -- turbo dev
|
|
267
314
|
localghost dev --config-pattern '^\.localghost\.'
|
|
268
315
|
localghost dev --https
|
|
@@ -273,6 +320,8 @@ Localghost checks npm for newer releases after successful commands. The check ha
|
|
|
273
320
|
|
|
274
321
|
`setup`, `dev`, and `teardown` refuse to run in production-like environments such as `NODE_ENV=production`, `VERCEL_ENV=production`, or `LOCALGHOST_ENV=production`.
|
|
275
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
|
+
|
|
276
325
|
`setup` writes only a managed block in the system hosts file:
|
|
277
326
|
|
|
278
327
|
```txt
|
|
@@ -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"
|