@mwguerra/hull 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +631 -0
  3. package/assets/hull-logo.png +0 -0
  4. package/assets/hull-logo.svg +5 -0
  5. package/bin/hull.js +4 -0
  6. package/devtools/dist/index.html +29 -0
  7. package/host/CMakeLists.txt +101 -0
  8. package/host/README.md +94 -0
  9. package/host/linux.Dockerfile +26 -0
  10. package/host/src/bindings/credentials.hpp +35 -0
  11. package/host/src/bindings/database.hpp +51 -0
  12. package/host/src/bindings/files.hpp +58 -0
  13. package/host/src/bindings/http.hpp +84 -0
  14. package/host/src/bindings/printer.hpp +281 -0
  15. package/host/src/bindings/storage.hpp +71 -0
  16. package/host/src/db_core.hpp +198 -0
  17. package/host/src/dispatcher.hpp +81 -0
  18. package/host/src/file_store.hpp +91 -0
  19. package/host/src/keychain.hpp +157 -0
  20. package/host/src/main.cpp +386 -0
  21. package/host/src/paths.hpp +62 -0
  22. package/host/src/secure.hpp +124 -0
  23. package/host/src/serve.hpp +113 -0
  24. package/host/test/db_test.cpp +80 -0
  25. package/host/test/secure_files_test.cpp +68 -0
  26. package/host/third_party/sqlite/sqlite3.c +269376 -0
  27. package/host/third_party/sqlite/sqlite3.h +14347 -0
  28. package/package.json +58 -0
  29. package/src/bridge/bridge-core.js +92 -0
  30. package/src/bridge/index.js +139 -0
  31. package/src/bridge/native-store.js +34 -0
  32. package/src/cli/build.js +122 -0
  33. package/src/cli/config.js +102 -0
  34. package/src/cli/dev.js +158 -0
  35. package/src/cli/eject.js +39 -0
  36. package/src/cli/host.js +61 -0
  37. package/src/cli/index.js +54 -0
  38. package/src/cli/installer.js +265 -0
  39. package/src/cli/release.js +178 -0
  40. package/src/cli/start.js +45 -0
  41. package/src/cli/timing.js +22 -0
  42. package/src/cli/vite.js +16 -0
  43. package/src/react/index.js +30 -0
  44. package/src/vue/index.js +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcelo Guerra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,631 @@
1
+ # @mwguerra/hull
2
+
3
+ > Tiny native desktop apps from your Vanilla-JS / React / Vue UI — a prebuilt C++
4
+ > web-view host you drive with npm scripts. No compiler, no Electron, no bundled
5
+ > browser engine.
6
+
7
+ Hull ships a small prebuilt native binary that renders your existing Vite app in the
8
+ operating system's web view (WebView2 / WebKit / WebKitGTK) and exposes a JSON bridge
9
+ to a C++ backend with batteries included: **TLS HTTP, encrypted storage, OS keychain,
10
+ SQLite, files, and printing**. Your app stays plain JS/React/Vue.
11
+
12
+ This README is the full reference for now.
13
+
14
+ ## Contents
15
+
16
+ - [Quick start](#quick-start)
17
+ - [Integrate your project](#integrate-your-project) — **Vanilla JS · React · Vue**
18
+ - [Try it from a blank project](#try-it-from-a-blank-project)
19
+ - [Talking to the backend](#talking-to-the-backend)
20
+ - [Bridge API reference](#bridge-api-reference)
21
+ - [CLI commands](#cli-commands)
22
+ - [Configuration (`.hullrc`)](#configuration-hullrc)
23
+ - [Develop in the browser (no recompile)](#develop-in-the-browser-no-recompile)
24
+ - [Versioned releases](#versioned-releases)
25
+ - [Security (at-rest crypto is a build option)](#security-at-rest-crypto-is-a-build-option)
26
+ - [Custom native code (eject)](#custom-native-code)
27
+ - [Platform support](#platform-support)
28
+ - [How it works](#how-it-works)
29
+
30
+ ## Quick start
31
+
32
+ In any existing Vite app (Vanilla JS, React, or Vue):
33
+
34
+ ```bash
35
+ npm i -D @mwguerra/hull
36
+ ```
37
+
38
+ Add scripts to `package.json`:
39
+
40
+ ```jsonc
41
+ {
42
+ "scripts": {
43
+ "dev": "hull dev", // Vite dev server in a native window (HMR)
44
+ "build": "hull build", // single-file UI, packaged with the host -> ./release
45
+ "start": "hull start" // run the packaged build
46
+ }
47
+ }
48
+ ```
49
+
50
+ `npm run dev` opens your app as a desktop window. **Zero config** — the window title
51
+ and a per-app storage namespace are derived from `package.json`, and the window ships
52
+ with the Hull logo as its icon until you set your own. Installing `@mwguerra/hull`
53
+ also pulls the prebuilt host for your OS/CPU automatically (an os/cpu-gated optional
54
+ dependency, e.g. `@mwguerra/hull-win32-x64`).
55
+
56
+ Starting a **brand-new** project? Copy one of the recipes below.
57
+
58
+ ## Integrate your project
59
+
60
+ The C++ backend and the JSON bridge (`@mwguerra/hull/bridge`) are **identical across
61
+ frameworks** — only the UI layer and the optional state hook differ. Each recipe below
62
+ is the exact shape of a runnable example in the repo (`examples/vanilla-js`,
63
+ `examples/react`, `examples/vue`); every example exercises **all** features (bridge,
64
+ settings + C++→UI events, credentials, HTTP, printing, SQLite, files, single-image
65
+ upload). Copy the one you want and trim.
66
+
67
+ Every Hull project, regardless of framework, has:
68
+
69
+ - the `@mwguerra/hull` dev dependency + the npm scripts,
70
+ - a normal `vite.config.js` (Hull injects the single-file plugin only at build time),
71
+ - an `index.html` Vite entry,
72
+ - an optional [`.hullrc`](#configuration-hullrc),
73
+ - your UI code, which imports from `@mwguerra/hull/bridge`.
74
+
75
+ Project layout (same for all three):
76
+
77
+ ```
78
+ my-app/
79
+ ├── package.json
80
+ ├── vite.config.js
81
+ ├── .hullrc # optional
82
+ ├── index.html
83
+ └── src/
84
+ ├── main.js|.jsx # Vite entry
85
+ ├── App.vue|.jsx # (React/Vue) your root component
86
+ └── style.css
87
+ ```
88
+
89
+ ### Vanilla JS
90
+
91
+ `package.json`:
92
+
93
+ ```jsonc
94
+ {
95
+ "name": "my-app",
96
+ "private": true,
97
+ "type": "module",
98
+ "scripts": {
99
+ "dev": "hull dev",
100
+ "dev:browser": "hull dev --browser",
101
+ "build": "hull build",
102
+ "start": "hull start",
103
+ "web": "vite"
104
+ },
105
+ "devDependencies": {
106
+ "@mwguerra/hull": "^0.1.0",
107
+ "vite": "^6.0.0"
108
+ }
109
+ }
110
+ ```
111
+
112
+ `vite.config.js`:
113
+
114
+ ```js
115
+ import { defineConfig } from "vite";
116
+ // Plain Vite — no framework plugin. `hull build` adds the single-file plugin.
117
+ export default defineConfig({});
118
+ ```
119
+
120
+ `index.html`:
121
+
122
+ ```html
123
+ <!doctype html>
124
+ <html lang="en">
125
+ <head><meta charset="UTF-8" /><title>My App</title></head>
126
+ <body>
127
+ <button id="ping">Send to C++</button>
128
+ <pre id="out"></pre>
129
+ <script type="module" src="/src/main.js"></script>
130
+ </body>
131
+ </html>
132
+ ```
133
+
134
+ `src/main.js` — the bridge with no framework (use `nativeSetting` for two-way state):
135
+
136
+ ```js
137
+ import { ping, db, nativeSetting, hasBridge } from "@mwguerra/hull/bridge";
138
+
139
+ // 1) call C++ and show the result
140
+ document.querySelector("#ping").addEventListener("click", async () => {
141
+ const res = await ping("hello"); // -> { ok: true, echo: "hello" }
142
+ document.querySelector("#out").textContent = JSON.stringify(res);
143
+ });
144
+
145
+ // 2) a two-way persisted setting (C++ stores it; C++ pushes changes back)
146
+ const theme = nativeSetting("theme");
147
+ theme.subscribe((v) => document.documentElement.classList.toggle("dark", v === "dark"));
148
+ theme.load(); // initial pull (no-op in a plain browser)
149
+ // theme.set("dark") persists and notifies subscribers
150
+
151
+ // 3) SQLite — works in the native host or browser dev mode
152
+ if (hasBridge()) {
153
+ db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"])
154
+ .then(() => db.query("SELECT * FROM notes ORDER BY id DESC"))
155
+ .then((notes) => console.log(notes))
156
+ .catch(console.error);
157
+ }
158
+ ```
159
+
160
+ ### React
161
+
162
+ `package.json`:
163
+
164
+ ```jsonc
165
+ {
166
+ "name": "my-app",
167
+ "private": true,
168
+ "type": "module",
169
+ "scripts": {
170
+ "dev": "hull dev",
171
+ "dev:browser": "hull dev --browser",
172
+ "build": "hull build",
173
+ "start": "hull start",
174
+ "web": "vite"
175
+ },
176
+ "dependencies": { "react": "^18.3.0", "react-dom": "^18.3.0" },
177
+ "devDependencies": {
178
+ "@mwguerra/hull": "^0.1.0",
179
+ "@vitejs/plugin-react": "^4.3.0",
180
+ "vite": "^6.0.0"
181
+ }
182
+ }
183
+ ```
184
+
185
+ `vite.config.js`:
186
+
187
+ ```js
188
+ import { defineConfig } from "vite";
189
+ import react from "@vitejs/plugin-react";
190
+ export default defineConfig({ plugins: [react()] });
191
+ ```
192
+
193
+ `index.html`:
194
+
195
+ ```html
196
+ <!doctype html>
197
+ <html lang="en">
198
+ <head><meta charset="UTF-8" /><title>My App</title></head>
199
+ <body>
200
+ <div id="root"></div>
201
+ <script type="module" src="/src/main.jsx"></script>
202
+ </body>
203
+ </html>
204
+ ```
205
+
206
+ `src/main.jsx`:
207
+
208
+ ```jsx
209
+ import { StrictMode } from "react";
210
+ import { createRoot } from "react-dom/client";
211
+ import App from "./App.jsx";
212
+ import "./style.css";
213
+
214
+ createRoot(document.getElementById("root")).render(
215
+ <StrictMode><App /></StrictMode>
216
+ );
217
+ ```
218
+
219
+ `src/App.jsx` — bridge + the `useNativeState` hook:
220
+
221
+ ```jsx
222
+ import { useEffect, useState } from "react";
223
+ import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
224
+ import { useNativeState } from "@mwguerra/hull/react";
225
+
226
+ export default function App() {
227
+ const [out, setOut] = useState(null);
228
+ const [theme, setTheme] = useNativeState("theme"); // like useState, persisted in C++
229
+ const [notes, setNotes] = useState([]);
230
+
231
+ useEffect(() => {
232
+ document.documentElement.classList.toggle("dark", theme === "dark");
233
+ }, [theme]);
234
+
235
+ useEffect(() => {
236
+ if (!hasBridge()) return; // native host or browser dev mode
237
+ (async () => {
238
+ await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
239
+ setNotes(await db.query("SELECT * FROM notes ORDER BY id DESC"));
240
+ })();
241
+ }, []);
242
+
243
+ return (
244
+ <>
245
+ <button onClick={async () => setOut(await ping("hello"))}>Send to C++</button>
246
+ {out && <pre>{JSON.stringify(out)}</pre>}
247
+ <select value={theme ?? ""} onChange={(e) => setTheme(e.target.value)}>
248
+ <option value="light">Light</option>
249
+ <option value="dark">Dark</option>
250
+ </select>
251
+ <ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
252
+ </>
253
+ );
254
+ }
255
+ ```
256
+
257
+ ### Vue
258
+
259
+ `package.json`:
260
+
261
+ ```jsonc
262
+ {
263
+ "name": "my-app",
264
+ "private": true,
265
+ "type": "module",
266
+ "scripts": {
267
+ "dev": "hull dev",
268
+ "dev:browser": "hull dev --browser",
269
+ "build": "hull build",
270
+ "start": "hull start",
271
+ "web": "vite"
272
+ },
273
+ "dependencies": { "vue": "^3.5.0" },
274
+ "devDependencies": {
275
+ "@mwguerra/hull": "^0.1.0",
276
+ "@vitejs/plugin-vue": "^5.2.0",
277
+ "vite": "^6.0.0"
278
+ }
279
+ }
280
+ ```
281
+
282
+ `vite.config.js`:
283
+
284
+ ```js
285
+ import { defineConfig } from "vite";
286
+ import vue from "@vitejs/plugin-vue";
287
+ export default defineConfig({ plugins: [vue()] });
288
+ ```
289
+
290
+ `index.html`:
291
+
292
+ ```html
293
+ <!doctype html>
294
+ <html lang="en">
295
+ <head><meta charset="UTF-8" /><title>My App</title></head>
296
+ <body>
297
+ <div id="app"></div>
298
+ <script type="module" src="/src/main.js"></script>
299
+ </body>
300
+ </html>
301
+ ```
302
+
303
+ `src/main.js`:
304
+
305
+ ```js
306
+ import { createApp } from "vue";
307
+ import App from "./App.vue";
308
+ import "./style.css";
309
+
310
+ createApp(App).mount("#app");
311
+ ```
312
+
313
+ `src/App.vue` — bridge + the `useNativeState` hook:
314
+
315
+ ```vue
316
+ <script setup>
317
+ import { ref, watch, onMounted } from "vue";
318
+ import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
319
+ import { useNativeState } from "@mwguerra/hull/vue";
320
+
321
+ const out = ref(null);
322
+ async function send() { out.value = await ping("hello"); }
323
+
324
+ const theme = useNativeState("theme"); // a ref; edits persist in C++, C++ pushes back
325
+ watch(theme, (v) => document.documentElement.classList.toggle("dark", v === "dark"),
326
+ { immediate: true });
327
+
328
+ const notes = ref([]);
329
+ onMounted(async () => {
330
+ if (!hasBridge()) return; // native host or browser dev mode
331
+ await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
332
+ notes.value = await db.query("SELECT * FROM notes ORDER BY id DESC");
333
+ });
334
+ </script>
335
+
336
+ <template>
337
+ <button @click="send">Send to C++</button>
338
+ <pre v-if="out">{{ out }}</pre>
339
+ <select v-model="theme">
340
+ <option value="light">Light</option>
341
+ <option value="dark">Dark</option>
342
+ </select>
343
+ <ul><li v-for="n in notes" :key="n.id">{{ n.body }}</li></ul>
344
+ </template>
345
+ ```
346
+
347
+ ### Shared `.hullrc` + run (all three)
348
+
349
+ `.hullrc` (optional — see [Configuration](#configuration-hullrc)):
350
+
351
+ ```json
352
+ {
353
+ "appId": "com.you.my-app",
354
+ "window": { "title": "My App", "width": 1100, "height": 760 }
355
+ }
356
+ ```
357
+
358
+ Then:
359
+
360
+ ```bash
361
+ npm install
362
+ npm run dev # native window with HMR (+ a dev inspector tab)
363
+ npm run dev:browser # run the UI in your browser with the full bridge, no recompile
364
+ npm run build # single-file the UI + package with the host -> ./release
365
+ npm run start # run the packaged app
366
+ ```
367
+
368
+ > The three recipes differ **only** in the UI layer. `@mwguerra/hull/bridge` (ping, db,
369
+ > files, settings, credentials, http, printers) is the same in all of them;
370
+ > `@mwguerra/hull/vue` and `@mwguerra/hull/react` add the `useNativeState` hook (Vanilla
371
+ > JS uses `nativeSetting` directly). For the complete, feature-by-feature versions, see
372
+ > the `examples/` apps in the repo.
373
+
374
+ ## Try it from a blank project
375
+
376
+ Scaffold a fresh Vite app, add Hull, package it, and open the desktop window. The same
377
+ commands work on **Windows, macOS, and Linux** — `@mwguerra/hull` pulls the prebuilt
378
+ host for your OS/CPU automatically, and `hull start` opens the packaged app.
379
+
380
+ **Vue:**
381
+
382
+ ```bash
383
+ npm create vite@latest my-hull-app -- --template vue
384
+ cd my-hull-app
385
+ npm install
386
+ npm i -D @mwguerra/hull
387
+ npx hull build # bundle the UI + package it with the native host
388
+ npx hull start # open the desktop app
389
+ ```
390
+
391
+ **React:**
392
+
393
+ ```bash
394
+ npm create vite@latest my-hull-app -- --template react
395
+ cd my-hull-app
396
+ npm install
397
+ npm i -D @mwguerra/hull
398
+ npx hull build
399
+ npx hull start
400
+ ```
401
+
402
+ > Want a live-reload window without packaging first? Run `npx hull dev` instead of
403
+ > `build` + `start`. Plain JS works too — use `--template vanilla`. To wire `hull` into
404
+ > your npm scripts, see the [recipes above](#integrate-your-project).
405
+
406
+ ## Talking to the backend
407
+
408
+ Every call goes UI → C++ and returns a Promise; all the real work happens in the
409
+ native host.
410
+
411
+ ```js
412
+ import { ping, httpPost, saveCredential, isNative } from "@mwguerra/hull/bridge";
413
+
414
+ await ping("hello"); // -> { ok: true, echo: "hello" }
415
+ const res = await httpPost("https://api.example.com/x", { a: 1 }); // TLS, in C++
416
+ await saveCredential("api.example.com", "default", token); // -> OS keychain
417
+ ```
418
+
419
+ Structured persistence with embedded SQLite (parameterized, stored per-user):
420
+
421
+ ```js
422
+ import { db } from "@mwguerra/hull/bridge";
423
+ await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
424
+ await db.exec("INSERT INTO notes (body) VALUES (?)", ["hello"]);
425
+ const notes = await db.query("SELECT * FROM notes ORDER BY id DESC");
426
+ ```
427
+
428
+ Files / uploads (e.g. show an uploaded image — the pattern the examples use):
429
+
430
+ ```js
431
+ import { files } from "@mwguerra/hull/bridge";
432
+ await files.write(file.name, file); // string | Uint8Array | ArrayBuffer | Blob
433
+ const bytes = await files.read(file.name); // Uint8Array
434
+ const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
435
+ imgEl.src = url; // preview; URL.revokeObjectURL(url) later
436
+ ```
437
+
438
+ Two-way persisted state (plaintext by default; encrypted at rest in the secure build):
439
+
440
+ ```js
441
+ // Vue
442
+ import { useNativeState } from "@mwguerra/hull/vue";
443
+ const theme = useNativeState("theme"); // a ref; edits persist, C++ pushes sync back
444
+
445
+ // React
446
+ import { useNativeState } from "@mwguerra/hull/react";
447
+ const [theme, setTheme] = useNativeState("theme");
448
+
449
+ // Vanilla JS
450
+ import { nativeSetting } from "@mwguerra/hull/bridge";
451
+ const theme = nativeSetting("theme"); // .get() / .set(v) / .subscribe(fn) / .load()
452
+ ```
453
+
454
+ ## Bridge API reference
455
+
456
+ All from `@mwguerra/hull/bridge`:
457
+
458
+ | Function | Backend |
459
+ |----------|---------|
460
+ | `ping(text)` | sync echo (diagnostics) |
461
+ | `httpPost(url, body)` / `httpGet(url)` | cpp-httplib + OpenSSL, on a worker thread; injects a `Bearer` token from the keychain |
462
+ | `saveSetting` / `loadSetting` / `loadAllSettings` | per-user store (plaintext by default; AES in the secure build) |
463
+ | `nativeSetting(key)` | two-way setting store: `.get()` / `.set(v)` / `.subscribe(fn)` / `.load()` |
464
+ | `saveCredential` / `credentialExists` / `eraseCredential` | OS keychain; **write-only** — secrets never return to JS |
465
+ | `listPrinters` | discover printers (Winspool / CUPS) |
466
+ | `printMessage(printer, text)` | print a **text document** — works with any printer (Print to PDF, OneNote, laser) |
467
+ | `printReceipt(printer, text)` / `printNetwork(host, port, text)` | raw **ESC/POS** for thermal receipt printers (spooler / TCP port-9100) |
468
+ | `db.query` / `db.get` / `db.exec` / `db.batch` / `db.migrate` | embedded SQLite, parameterized, per-user storage |
469
+ | `files.write` / `read` / `readText` / `list` / `remove` | file/upload storage in the per-user dir (through the secure layer) |
470
+ | `appInfo()` | `{ ok, appId, secure }` — `secure` true on a crypto build |
471
+ | `bridge.on(event, fn)` | subscribe to C++ → UI push events (e.g. `settings:changed`); returns an unsubscribe fn |
472
+ | `hasBridge()` / `isNative()` / `bridgeMode()` | `hasBridge` = reachable (native or browser dev); `isNative` = native web view; `bridgeMode` = `"native"`/`"http"`/`"none"` |
473
+
474
+ Framework hooks: `useNativeState(key)` from `@mwguerra/hull/vue` (returns a ref) and
475
+ `@mwguerra/hull/react` (returns `[value, setValue]`).
476
+
477
+ ## CLI commands
478
+
479
+ | Command | What it does |
480
+ |---------|--------------|
481
+ | `hull dev` | Vite dev server rendered in a native window (HMR) + a dev inspector tab |
482
+ | `hull dev --browser` | run the UI in your browser with the full bridge over HTTP/SSE (no recompile) |
483
+ | `hull build [vX.Y.Z]` | single-file the UI and package it with the host into `release/<version\|development>/<platform>/` + an archive |
484
+ | `hull build … --platform <key\|all>` | also package other platforms whose host binary is present; `--format zip\|tar.gz` |
485
+ | `hull start [vX.Y.Z]` | run a packaged build |
486
+ | `hull installer [vX.Y.Z]` | wrap the build into a native installer — `.dmg` (macOS), `.deb` (Linux), `.exe` (Windows) |
487
+ | `hull eject` | copy the C++ host project into `./desktop` to add native bindings |
488
+
489
+ Add `-v` / `--verbose` to any command for per-step timings (every command prints its
490
+ total time). The version argument must match `vX.Y.Z` (optionally `-suffix`); with no
491
+ version, output goes to a `development/` folder.
492
+
493
+ > Pass **flags** like `--platform` via `npx hull …` (or the binary directly). `npm run`
494
+ > swallows unknown flags, so `npm run build -- v1.2.3` works for the version but
495
+ > `--platform` won't reach Hull through it.
496
+
497
+ ## Configuration (`.hullrc`)
498
+
499
+ Drop a `.hullrc` (JSON) in your project root — only the keys you set override the
500
+ package defaults. Lookup order: `.hullrc` → `.hullrc.json` → `hull.config.json`.
501
+
502
+ ```json
503
+ {
504
+ "appId": "com.you.notes",
505
+ "secure": false,
506
+ "window": { "title": "Notes", "width": 1200, "height": 800, "icon": "build/icon.png" }
507
+ }
508
+ ```
509
+
510
+ | Key | Default | Meaning |
511
+ |-----|---------|---------|
512
+ | `appId` | `com.hull.<pkg name>` | namespaces the store, DB, files, and keychain entries so multiple Hull apps never collide |
513
+ | `window.title` | pkg `productName`/`name` | native window title |
514
+ | `window.width` / `window.height` | `1100` / `760` | window size |
515
+ | `window.icon` (or top-level `icon`) | bundled Hull logo | PNG/ICO for the window/app icon; set at runtime on Windows (GDI+), via the app bundle on macOS/Linux; SVG is not a valid native icon |
516
+ | `secure` | `false` | run the crypto host build (`hull-host-secure`): AES files/settings + SQLCipher DB |
517
+ | `debug` | `false` | open the web-view dev tools |
518
+ | `outDir` | `dist` | Vite UI build dir |
519
+ | `releaseDir` | `release` | packaged-app output dir |
520
+
521
+ ## Develop in the browser (no recompile)
522
+
523
+ ```bash
524
+ npm run dev -- --browser # or: npx hull dev --browser
525
+ ```
526
+
527
+ Runs the UI in your **browser** with full Vite HMR while bridge calls still reach the
528
+ real native backend over HTTP/SSE — change a label, hit reload, no recompile. Both
529
+ `hull dev` and `--browser` also open a dev-only **inspector** (live bridge calls,
530
+ events, DB/file ops, timings) that is **stripped from production builds**
531
+ (`import.meta.env.DEV` dead-code elimination).
532
+
533
+ ## Versioned releases
534
+
535
+ ```bash
536
+ npm run build # -> release/development/<platform>/ + archive
537
+ npm run build -- v1.2.3 # -> release/v1.2.3/<platform>/ + archive
538
+ ```
539
+
540
+ Each build emits a self-contained, versioned bundle and a ready-to-ship archive (`.zip`
541
+ on Windows, `.tar.gz` on macOS/Linux) with the minimal runnable set — the host binary,
542
+ the libraries it needs, your inlined `app.html`, a double-click launcher, and `icon.png`
543
+ if you configured one. Unpack on the target and run. `--platform all` also packages
544
+ other platforms whose host binary is installed (realistically produced via CI, one
545
+ runner per OS). With `secure: true`, bundle dirs and archives get a `-secure` suffix.
546
+
547
+ ### Native installers
548
+
549
+ After a build, wrap it into a native installer for the **current** OS:
550
+
551
+ ```bash
552
+ npm run build && npx hull installer # -> release/<version>/<App>-<version>-<key>.<dmg|deb|exe>
553
+ ```
554
+
555
+ | OS | Output | Tooling |
556
+ |----|--------|---------|
557
+ | macOS | `.dmg` (the `.app` + an Applications drop-link) | `hdiutil` (built in) |
558
+ | Linux | `.deb` (installs to `/opt`, registers the `.desktop` + icon, deps via `dpkg-shlibdeps`) | `dpkg-deb` (built in) |
559
+ | Windows | `.exe` (per-user install, Start-Menu/Desktop shortcuts, uninstaller) | [Inno Setup](https://jrsoftware.org/isinfo.php) — `winget install JRSoftware.InnoSetup` |
560
+
561
+ Each is built on its own OS (the tools are OS-native), like the host. Install with:
562
+ double-click the `.dmg` and drag to Applications; `sudo apt install ./<app>.deb`; run the
563
+ `.exe`. Unsigned for now — for distribution to other machines, add code-signing
564
+ (macOS notarization / Windows Authenticode) as a later step.
565
+
566
+ ## Security (at-rest crypto is a build option)
567
+
568
+ Default build = **no crypto, everything fast** (plaintext at rest; secrets still in the
569
+ keychain). For encryption at rest, use the **secure build**:
570
+
571
+ ```bash
572
+ npm run build:host:secure # AES for files/settings + SQLCipher for the DB
573
+ # then in .hullrc: { "secure": true }
574
+ ```
575
+
576
+ Files and the DB go through one crypto **layer** — nothing calls cryptography directly.
577
+ SQLite is also hardened in all builds: `PRAGMA trusted_schema=OFF` on every connection,
578
+ and the default build compiles with `SQLITE_OMIT_LOAD_EXTENSION` and `SQLITE_DQS=0`.
579
+ Queries are always parameterized (bound in C++), and `exec`/`query`/`get` run one
580
+ statement each.
581
+
582
+ ## Custom native code
583
+
584
+ Need your own C++ binding? Run `hull eject` to copy the host project into `./desktop`,
585
+ add a binding (`d.on("myThing", (args, reply) => reply({ ok: true }))`), and build it
586
+ with CMake. The standard bindings (HTTP / storage / keychain / printing / DB / files)
587
+ are already there to extend. See `desktop/README.md`.
588
+
589
+ ## Platform support
590
+
591
+ | | Windows | macOS | Linux |
592
+ |---|---------|-------|-------|
593
+ | Web view | WebView2 (Edge) | WebKit | WebKitGTK 6 |
594
+ | Credentials | Credential Manager | Keychain | libsecret |
595
+ | Printing | Winspool | CUPS | CUPS |
596
+ | Window icon | runtime (GDI+) | `.app` bundle (built by `hull build`) | auto `.desktop` + icon-theme install |
597
+ | `hull build` output | folder + `.cmd` launcher (zip) | **`.app` bundle** (tar.gz) | folder + `.sh` launcher (tar.gz) |
598
+ | Build the host on | Windows | macOS | any OS via Docker, or native Linux |
599
+
600
+ End users only need the OS web-view runtime (preinstalled on Windows 11 and macOS;
601
+ `libwebkitgtk-6.0` on Linux). A host must be built on its own OS — true cross-compile
602
+ isn't realistic for WebView2/WebKit — except Linux, which builds from any OS via Docker.
603
+
604
+ **Linux sandbox note:** WebKitGTK sandboxes its subprocesses with bubblewrap, which
605
+ needs unprivileged user namespaces. They're blocked on Ubuntu 24.04 (AppArmor default)
606
+ and in many containers, which otherwise crashes the app with
607
+ `bwrap: setting up uid map: Permission denied`. Hull's host **auto-detects** this and
608
+ disables the sandbox so the app still runs (with a notice). Override with
609
+ `hull start --no-sandbox`, `.hullrc` `{ "linux": { "sandbox": false } }`, or keep it by
610
+ enabling userns (`sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0`). See
611
+ [platforms.md](https://github.com/mwguerra/hull/blob/main/docs/platforms.md#troubleshooting).
612
+
613
+ **Linux icon note:** GTK4 has no runtime "set icon from a PNG", so on `hull dev`/`start`
614
+ the host auto-installs desktop integration — it writes `~/.local/share/applications/<appId>.desktop`
615
+ and the icon into the user icon theme, and sets the window's app-id so the compositor
616
+ shows it (Wayland + X11). A new icon may need a moment or a re-login for the shell to
617
+ pick it up. See [configuration.md](https://github.com/mwguerra/hull/blob/main/docs/configuration.md#the-windowicon-key).
618
+
619
+ ## How it works
620
+
621
+ - Prebuilt host binaries are delivered as platform-gated optional dependencies
622
+ (`@mwguerra/hull-win32-x64`, …) — npm installs only the one for your machine.
623
+ - `hull build` uses your project's Vite plus `vite-plugin-singlefile` to inline the
624
+ whole UI into one HTML file, then bundles it with the host.
625
+ - The host loads that file at runtime (`--app`) in production, or your dev server
626
+ (`--url`) during development. The bridge is exposed over the web view natively, or
627
+ over HTTP/SSE in browser dev mode.
628
+
629
+ ## License
630
+
631
+ MIT
Binary file
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none">
2
+ <rect width="32" height="32" rx="8" fill="#10b981" fill-opacity="0.15"></rect>
3
+ <path d="M7 13H25" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"></path>
4
+ <path d="M8 13Q16 26 24 13" stroke="#10b981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path>
5
+ </svg>
package/bin/hull.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/cli/index.js";
3
+
4
+ run(process.argv.slice(2));