code2app 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.
- package/README.md +111 -0
- package/assets/icon-source.png +0 -0
- package/bin/cli.js +248 -0
- package/dist/index.html +24 -0
- package/docs/APP_STORES.md +129 -0
- package/docs/BUILDING.md +134 -0
- package/docs/LOCAL_APPS.md +116 -0
- package/docs/MOBILE.md +78 -0
- package/docs/OAUTH.md +76 -0
- package/package.json +66 -0
- package/profiles/README.md +56 -0
- package/profiles/example.json +37 -0
- package/scripts/build-sidecar.mjs +80 -0
- package/scripts/configure.mjs +222 -0
- package/scripts/generate-icons.mjs +53 -0
- package/scripts/generate-placeholder-icons.mjs +186 -0
- package/scripts/lib/png.mjs +141 -0
- package/scripts/profile.mjs +147 -0
- package/src-tauri/Cargo.lock +5832 -0
- package/src-tauri/Cargo.toml +47 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +15 -0
- package/src-tauri/capabilities/remote.json +17 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/64x64.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/generated_config.rs +22 -0
- package/src-tauri/src/lib.rs +212 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/tauri.conf.json +84 -0
- package/vitest.config.mjs +35 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Packaging a CLI as a native app
|
|
2
|
+
|
|
3
|
+
Remote mode wraps a website. Local mode wraps something that has no website at all — a
|
|
4
|
+
command-line tool — and turns it into an installable GUI app that needs no runtime on the user's
|
|
5
|
+
machine. `packages/about-system-info/native` is the worked example: the `about-system` CLI, a
|
|
6
|
+
Node program, shipped as a `.msi` / `.dmg` / `.AppImage` that a user who has never installed Node
|
|
7
|
+
can double-click.
|
|
8
|
+
|
|
9
|
+
## The three pieces
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌──────────────────────────┐
|
|
13
|
+
│ dist/index.html │ the app's entire UI, bundled into the binary
|
|
14
|
+
│ invoke("sidecar_output")│
|
|
15
|
+
└───────────┬──────────────┘
|
|
16
|
+
│ Tauri IPC
|
|
17
|
+
┌───────────▼──────────────┐
|
|
18
|
+
│ src-tauri/src/lib.rs │ sidecar_output: fixed args, bundled-content-only
|
|
19
|
+
│ shell().sidecar(NAME) │
|
|
20
|
+
└───────────┬──────────────┘
|
|
21
|
+
│ spawn + capture stdout
|
|
22
|
+
┌───────────▼──────────────┐
|
|
23
|
+
│ src-tauri/binaries/ │ the CLI, compiled to one self-contained executable
|
|
24
|
+
│ <name>-<target triple> │ per platform, bundled into the installer
|
|
25
|
+
└──────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
1. **The frontend** replaces `dist/index.html`. It's plain bundled content, so it can use
|
|
29
|
+
`window.__TAURI__.core.invoke` directly (the profile sets `withGlobalTauri`) with no build step
|
|
30
|
+
and no npm dependency.
|
|
31
|
+
2. **The bridge** is `sidecar_output`, defined in `src-tauri/src/lib.rs` and available to any
|
|
32
|
+
local-mode app for free. It runs the bundled binary with the profile's fixed `sidecar.args` and
|
|
33
|
+
returns its stdout as a string.
|
|
34
|
+
3. **The binary** is whatever compiles your CLI into one file. For a Node/TypeScript CLI,
|
|
35
|
+
`bun build --compile` does it; Go and Rust CLIs are already single files; Python needs
|
|
36
|
+
PyInstaller or similar. It must land at `src-tauri/binaries/<name>-<rust target triple>` —
|
|
37
|
+
Tauri appends the triple itself so one installer per platform picks up the right build.
|
|
38
|
+
`scripts/build-sidecar.mjs` runs your build command and puts the result there under the right
|
|
39
|
+
name; `npm run build:sidecar` calls it, and the `predev`/`prebuild:desktop` hooks call it too,
|
|
40
|
+
so the binary is never a step you have to remember.
|
|
41
|
+
|
|
42
|
+
## Configuring it
|
|
43
|
+
|
|
44
|
+
```jsonc
|
|
45
|
+
{
|
|
46
|
+
"mode": "local",
|
|
47
|
+
// and no "url" — local mode loads dist/
|
|
48
|
+
"sidecar": {
|
|
49
|
+
"name": "about-system", // the binary's base name
|
|
50
|
+
"args": ["--json"], // the fixed argv it is always run with
|
|
51
|
+
"build": "bun build --compile src/cli.ts --outfile {out}",
|
|
52
|
+
"buildCwd": ".." // where to run that command, relative to the app dir
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`{out}` is the only substitution `build-sidecar.mjs` makes — it expands to the full path, target
|
|
58
|
+
triple and `.exe` suffix included, that Tauri will look for. The build command owns everything
|
|
59
|
+
else, so the wrapper never has to know what language the CLI is written in. A `build` without
|
|
60
|
+
`{out}` is rejected by profile validation rather than quietly producing a binary in the wrong
|
|
61
|
+
place.
|
|
62
|
+
|
|
63
|
+
`configure` turns that into `bundle.externalBin: ["binaries/about-system"]` plus the Rust
|
|
64
|
+
constants. `tauri build` then **fails** if no binary exists for the platform being built, which is
|
|
65
|
+
the behavior you want: a silently sidecar-less installer would ship a GUI that can't load anything.
|
|
66
|
+
|
|
67
|
+
## Why the arguments are fixed
|
|
68
|
+
|
|
69
|
+
`sidecar_output` takes no parameters. The frontend asks for "the output this app is built around";
|
|
70
|
+
it can't choose the argument list. A command that forwarded arbitrary argv to a bundled executable
|
|
71
|
+
would be a process spawner reachable from page content, and in a remote-mode app that page is a
|
|
72
|
+
website.
|
|
73
|
+
|
|
74
|
+
It also refuses to run unless the calling window is showing bundled content (`tauri://localhost`,
|
|
75
|
+
or `http://tauri.localhost` on Windows). Tauri does already reject app-defined commands from a
|
|
76
|
+
remote origin unless a capability explicitly grants them, so that check is a second line of
|
|
77
|
+
defense — but it's the line that survives someone adding the command to `capabilities/remote.json`
|
|
78
|
+
without thinking it through.
|
|
79
|
+
|
|
80
|
+
If your app genuinely needs several different invocations, add them as separate commands with
|
|
81
|
+
separate fixed argument lists rather than opening up the one.
|
|
82
|
+
|
|
83
|
+
**One gotcha if you add your own ACL manifest.** Tauri skips the ACL for app-defined commands
|
|
84
|
+
called from local content *only while the app has no `src-tauri/permissions/` directory of its
|
|
85
|
+
own*. Create one — for any reason — and every app command, `sidecar_output` included, starts
|
|
86
|
+
needing an explicit entry in `capabilities/default.json`, and the app fails with "not allowed by
|
|
87
|
+
ACL" until it gets one.
|
|
88
|
+
|
|
89
|
+
## Getting the target triple right
|
|
90
|
+
|
|
91
|
+
Tauri resolves `binaries/<name>` to `binaries/<name>-<triple>` using the *Rust* target triple, not
|
|
92
|
+
Node's `process.platform`. Read it from the toolchain rather than mapping it by hand:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
rustc -vV | sed -n 's/^host: //p' # e.g. x86_64-unknown-linux-gnu, aarch64-apple-darwin
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`scripts/build-sidecar.mjs` does exactly this: it reads the host triple from the toolchain,
|
|
99
|
+
compiles the CLI, names the output accordingly, and marks it executable.
|
|
100
|
+
|
|
101
|
+
macOS universal builds (`--target universal-apple-darwin`) need **both**
|
|
102
|
+
`<name>-x86_64-apple-darwin` and `<name>-aarch64-apple-darwin` present, since the sidecar isn't
|
|
103
|
+
lipo'd for you.
|
|
104
|
+
|
|
105
|
+
## Offline, updates, and size
|
|
106
|
+
|
|
107
|
+
A local-mode app has no network dependency at all, which is the main reason to choose it. The
|
|
108
|
+
tradeoffs:
|
|
109
|
+
|
|
110
|
+
- **Size.** A compiled Node CLI is 50–100 MB before the Tauri binary. Nothing about that is
|
|
111
|
+
avoidable if you want a runtime-free install; a Go or Rust CLI is a fraction of it.
|
|
112
|
+
- **Updates.** There's no server to change, so shipping a fix means shipping a new installer.
|
|
113
|
+
Tauri's updater (see `BUILDING.md`) is the answer if you want that to be automatic.
|
|
114
|
+
- **Permissions.** The CLI runs with the user's own privileges, exactly as it would in their
|
|
115
|
+
terminal. If it reads system state, the app reads system state; if it needs elevation, the app
|
|
116
|
+
needs elevation.
|
package/docs/MOBILE.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Android + iOS
|
|
2
|
+
|
|
3
|
+
The Rust core (`src-tauri/`) is already mobile-ready: `lib.rs`'s `run()` is gated with
|
|
4
|
+
`#[cfg_attr(mobile, tauri::mobile_entry_point)]`, the crate builds as a `staticlib`/`cdylib` for
|
|
5
|
+
FFI, and every desktop-only plugin (single-instance, updater, global-shortcut) is behind
|
|
6
|
+
`#[cfg(not(any(target_os = "android", target_os = "ios")))]`. What's missing is the generated
|
|
7
|
+
native project each platform needs around that core — and generating it needs host tooling this
|
|
8
|
+
package can't assume you have (an Android SDK/NDK install, or a Mac with Xcode), so it isn't
|
|
9
|
+
run automatically. Do it once per machine you build mobile from:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run android:init # writes src-tauri/gen/android
|
|
13
|
+
npm run ios:init # writes src-tauri/gen/apple (macOS host only)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Both read `src-tauri/tauri.conf.json` (already configured for the active profile — see
|
|
17
|
+
`../profiles/README.md`) to set the package name / bundle id, deep-link scheme, and icons, so run
|
|
18
|
+
`npm run configure` first if you've changed profiles.
|
|
19
|
+
|
|
20
|
+
## Android
|
|
21
|
+
|
|
22
|
+
**Requirements** (from the [Tauri mobile prerequisites](https://v2.tauri.app/start/prerequisites/)):
|
|
23
|
+
- Android SDK + **NDK r28 or newer**. NDK ≥28 builds 16KB-page-aligned native libraries, which
|
|
24
|
+
Google Play now requires for new apps/updates targeting recent devices — an older NDK needs a
|
|
25
|
+
manual linker workaround this wrapper doesn't set up for you.
|
|
26
|
+
- `ANDROID_HOME` and `NDK_HOME` environment variables pointing at your SDK/NDK install.
|
|
27
|
+
- The Android Rust targets: `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`.
|
|
28
|
+
- Minimum supported OS: **API 24** (set via the profile's `android.minSdkVersion`);
|
|
29
|
+
Google Play itself now requires new/updated apps to **target** API 36 — see
|
|
30
|
+
`APP_STORES.md`.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run android:dev # runs on a connected device/emulator with hot reload
|
|
34
|
+
npm run android:build # release build; add --apk for a plain APK instead of the .aab Play wants
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## iOS
|
|
38
|
+
|
|
39
|
+
**Requirements**: a macOS host with full **Xcode** installed (not just the Command Line Tools),
|
|
40
|
+
and the iOS Rust targets: `rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run ios:dev
|
|
44
|
+
npm run ios:build -- --export-method app-store-connect # or ad-hoc / development / enterprise
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`ios:build` produces an unsigned/ad-hoc `.ipa` unless you've set up a signing identity and
|
|
48
|
+
provisioning profile in `src-tauri/gen/apple`'s Xcode project first (`npm run ios:init` opens it
|
|
49
|
+
for you to configure once) — see `APP_STORES.md` for what App Store distribution needs
|
|
50
|
+
beyond that, including the "minimum functionality" review risk a webview-wrapper app like this one
|
|
51
|
+
should expect on iOS specifically.
|
|
52
|
+
|
|
53
|
+
## Should `gen/android` / `gen/apple` be committed?
|
|
54
|
+
|
|
55
|
+
The scaffold's `.gitignore` excludes them by default, since a freshly generated tree matches
|
|
56
|
+
`tauri.conf.json` exactly and re-running `init` reproduces it. If you hand-edit anything inside
|
|
57
|
+
`gen/` directly (native permissions, a custom Gradle/Xcode build step, manual signing config),
|
|
58
|
+
commit that tree instead — Tauri's mobile tooling is built to have `gen/` checked in and patched
|
|
59
|
+
in place for exactly that case, and future `init`/`build` runs preserve local edits rather than
|
|
60
|
+
clobbering them.
|
|
61
|
+
|
|
62
|
+
## Sidecars don't apply here
|
|
63
|
+
|
|
64
|
+
A local-mode app that bundles a CLI (`LOCAL_APPS.md`) has no mobile build: neither Android nor iOS
|
|
65
|
+
lets an app spawn a bundled executable, so `bundle.externalBin` is desktop-only. Packaging that
|
|
66
|
+
kind of app for mobile means reimplementing what the CLI does as a Tauri mobile plugin, which is a
|
|
67
|
+
different project. Remote-mode apps have no such limit.
|
|
68
|
+
|
|
69
|
+
## CI
|
|
70
|
+
|
|
71
|
+
Android builds on any Linux runner once the SDK/NDK are installed — `android-actions/setup-android`
|
|
72
|
+
plus `rustup target add` for the four Android targets is the usual recipe — and
|
|
73
|
+
`tauri-apps/tauri-action` can build the `.aab` from there. Sign it with an upload key held as
|
|
74
|
+
repository secrets; skip the signing step when they're absent so forks still get an unsigned build.
|
|
75
|
+
|
|
76
|
+
iOS needs a `macos-*` runner with Xcode plus a distribution certificate and provisioning profile in
|
|
77
|
+
secrets. Whether to wire that up is a decision for whoever holds the Apple Developer account, not
|
|
78
|
+
something to default to for every copy of this wrapper.
|
package/docs/OAUTH.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Why login needs a deep link, and how it works
|
|
2
|
+
|
|
3
|
+
This applies to **remote-mode** apps only, and only to ones whose wrapped site has a login. A
|
|
4
|
+
profile with no `deepLinkScheme` compiles none of this into the app's behavior — local-mode apps
|
|
5
|
+
(`LOCAL_APPS.md`) never need it.
|
|
6
|
+
|
|
7
|
+
Google (and most OAuth providers) refuse to run their login flow inside an embedded webview —
|
|
8
|
+
Tauri's WebView2/WKWebView/WebKitGTK included. Loading the site and clicking "Continue with Google"
|
|
9
|
+
straight inside the wrapper's window either fails outright or gets flagged as insecure. So sign-in
|
|
10
|
+
has to happen in the user's actual default browser, and the resulting session has to be handed back
|
|
11
|
+
to the wrapper's window afterward — those are two different cookie jars that can't otherwise see
|
|
12
|
+
each other's session.
|
|
13
|
+
|
|
14
|
+
## The three steps
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Wrapper window System browser Wrapper window
|
|
18
|
+
(the site) (the site) (the site)
|
|
19
|
+
┌────────────────┐ ┌──────────────────┐ ┌────────────────┐
|
|
20
|
+
│ login UI │ opener │ /login │ │ /auth/ │
|
|
21
|
+
│ "Continue in │ ────────► │ → Google OAuth │ │ native-callback│
|
|
22
|
+
│ your browser" │ plugin │ → /auth/ │ deep │ verifies token │
|
|
23
|
+
│ │ │ native-complete │ link │ → session │
|
|
24
|
+
│ │ │ generates token, │ ─────────► │ cookie set │
|
|
25
|
+
│ │ │ redirects to │ myapp:// │ → redirect "/" │
|
|
26
|
+
│ │ │ myapp://... │ │ │
|
|
27
|
+
└────────────────┘ └──────────────────┘ └────────────────┘
|
|
28
|
+
step 1 step 2 step 3
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
1. **The site's login page** detects it's running inside the wrapper — check for the
|
|
32
|
+
`window.__TAURI__` global the wrapper injects (the profile sets `withGlobalTauri`, and
|
|
33
|
+
`capabilities/remote.json` is what makes the opener plugin reachable from the site's origin) —
|
|
34
|
+
and, instead of rendering the normal social/magic-link buttons, renders one "Continue in your
|
|
35
|
+
browser" button. Clicking it calls the wrapper's opener plugin (`plugin:opener|open_url`) to
|
|
36
|
+
open `https://<your-site>/login?callbackURL=/auth/native-complete` in the OS default browser.
|
|
37
|
+
2. The user signs in normally there — whichever provider they pick; none of this page's code needs
|
|
38
|
+
to know which. Once a session cookie exists, the browser lands on **`/auth/native-complete`**.
|
|
39
|
+
That page generates a **single-use, short-lived token bound to that session** and redirects the
|
|
40
|
+
browser to `<scheme>://auth-callback?token=<token>`. The custom scheme (registered by the
|
|
41
|
+
wrapper from the profile's `deepLinkScheme`) hands the OS back to the installed app.
|
|
42
|
+
3. `src-tauri/src/lib.rs`'s `on_open_url` handler catches that deep link and navigates the
|
|
43
|
+
wrapper's own window to **`https://<your-site>/auth/native-callback?token=<token>`** — the same
|
|
44
|
+
origin the window already had loaded, so this is same-origin from here on. That page spends the
|
|
45
|
+
token against the site's verify endpoint, which sets a session cookie scoped to *this* window's
|
|
46
|
+
cookie jar, then redirects to `/`. The wrapper's window is now signed in.
|
|
47
|
+
|
|
48
|
+
## Why the Rust side doesn't call the verify endpoint directly
|
|
49
|
+
|
|
50
|
+
A verify endpoint is normally a **POST** expecting a JSON body — not something a plain window
|
|
51
|
+
navigation (a GET) can hit. Routing through `/auth/native-callback`'s own client-side `fetch` is
|
|
52
|
+
what actually performs that POST, and doing it from a page already loaded at the site's own origin
|
|
53
|
+
is what makes the resulting `Set-Cookie` land in the right cookie jar with no CORS complications —
|
|
54
|
+
a cross-origin fetch from a `tauri://localhost` asset page would need those, a same-origin one
|
|
55
|
+
doesn't.
|
|
56
|
+
|
|
57
|
+
## Windows/Linux: why `tauri-plugin-single-instance` is in `Cargo.toml`
|
|
58
|
+
|
|
59
|
+
Clicking a `<scheme>://` link while the app is already running launches a **second OS process** on
|
|
60
|
+
Windows and Linux instead of delivering the URL to the running one — `on_open_url` alone doesn't
|
|
61
|
+
fire for the already-running instance there. `tauri-plugin-single-instance` intercepts that second
|
|
62
|
+
launch and forwards its argv (which contains the deep-link URL) to `handle_deep_link()` in the
|
|
63
|
+
first, already-running process instead, and focuses its window. macOS doesn't need this —
|
|
64
|
+
`on_open_url` fires correctly there without it.
|
|
65
|
+
|
|
66
|
+
## What the wrapped site has to provide
|
|
67
|
+
|
|
68
|
+
Everything above is generic except the page routes — `/login`, `/auth/native-complete`,
|
|
69
|
+
`/auth/native-callback` — which have to exist on whatever the profile's `url` points at. Any auth
|
|
70
|
+
stack with a one-time-token generate/verify pair will do;
|
|
71
|
+
[better-auth's one-time-token plugin](https://better-auth.com/docs/plugins/one-time-token) is one
|
|
72
|
+
ready-made option. The deep-link and Rust-side handoff mechanics in `src-tauri/` don't change
|
|
73
|
+
either way.
|
|
74
|
+
|
|
75
|
+
`debate/debate-ai.com`'s `apps/debate-native-wrapper` — the wrapper this package was generalized
|
|
76
|
+
from — is a full worked implementation of both halves if you want a reference.
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "code2app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tauri scaffold that packages a website or a bundled CLI as a native desktop + mobile app, configured from a single JSON profile",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "vtempest",
|
|
7
|
+
"license": "rights.institute/prosper",
|
|
8
|
+
"bin": {
|
|
9
|
+
"native-app-wrapper": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"init": "node bin/cli.js init",
|
|
13
|
+
"configure": "node bin/cli.js configure",
|
|
14
|
+
"icons": "node scripts/generate-icons.mjs",
|
|
15
|
+
"icons:placeholder": "node scripts/generate-placeholder-icons.mjs",
|
|
16
|
+
"build:sidecar": "node scripts/build-sidecar.mjs",
|
|
17
|
+
"predev": "node bin/cli.js configure",
|
|
18
|
+
"dev": "tauri dev",
|
|
19
|
+
"prebuild:desktop": "node bin/cli.js configure",
|
|
20
|
+
"build:desktop": "tauri build",
|
|
21
|
+
"android:init": "tauri android init",
|
|
22
|
+
"preandroid:dev": "node bin/cli.js configure",
|
|
23
|
+
"android:dev": "tauri android dev",
|
|
24
|
+
"preandroid:build": "node bin/cli.js configure",
|
|
25
|
+
"android:build": "tauri android build",
|
|
26
|
+
"ios:init": "tauri ios init",
|
|
27
|
+
"preios:dev": "node bin/cli.js configure",
|
|
28
|
+
"ios:dev": "tauri ios dev",
|
|
29
|
+
"preios:build": "node bin/cli.js configure",
|
|
30
|
+
"ios:build": "tauri ios build",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"coverage": "vitest run --coverage"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"tauri",
|
|
37
|
+
"desktop-app",
|
|
38
|
+
"webview",
|
|
39
|
+
"app-wrapper",
|
|
40
|
+
"windows",
|
|
41
|
+
"macos",
|
|
42
|
+
"linux",
|
|
43
|
+
"android",
|
|
44
|
+
"ios",
|
|
45
|
+
"sidecar"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
|
|
50
|
+
"directory": "packages/native-app-wrapper"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/native-app-wrapper",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@tauri-apps/cli": "^2",
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
56
|
+
"vitest": "^4.1.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@tauri-apps/api": "^2",
|
|
60
|
+
"@tauri-apps/plugin-deep-link": "^2",
|
|
61
|
+
"@tauri-apps/plugin-opener": "^2",
|
|
62
|
+
"@tauri-apps/plugin-process": "^2",
|
|
63
|
+
"@tauri-apps/plugin-shell": "^2",
|
|
64
|
+
"@tauri-apps/plugin-updater": "^2"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Profiles
|
|
2
|
+
|
|
3
|
+
A profile is the one JSON file you edit to say what app this wrapper is. Everything else —
|
|
4
|
+
`src-tauri/tauri.conf.json`, the Rust constants, the remote capability's origin scope, the icon
|
|
5
|
+
set — is generated from it by `node bin/cli.js configure`, so the profile is the only place an app
|
|
6
|
+
identity is written down.
|
|
7
|
+
|
|
8
|
+
Copy `example.json`, fill it in, and either drop it in this folder or hand it straight to
|
|
9
|
+
`node bin/cli.js init <dir> --profile-file <path>` to scaffold a standalone copy of the wrapper
|
|
10
|
+
around it.
|
|
11
|
+
|
|
12
|
+
## Fields
|
|
13
|
+
|
|
14
|
+
| Field | Meaning |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `appName` / `productName` | Display name used in window titles, installers, and store listings. `productName` defaults to `appName`. |
|
|
17
|
+
| `identifier` | Reverse-DNS app id (e.g. `com.example.app`). Used as the Tauri `identifier`, the Android `applicationId`, and the iOS bundle id unless overridden under `android`/`ios`. Changing this after a store release changes the app's identity — don't. |
|
|
18
|
+
| `version` | Semantic version written into `tauri.conf.json`. Bump this to trigger a new store/release version. |
|
|
19
|
+
| `mode` | `"remote"` (default) loads `url` in the main window; `"local"` bundles `dist/` as the app's frontend. See below. |
|
|
20
|
+
| `url` | **Remote mode only, required.** The site the wrapper loads. Must be HTTPS (`http://localhost` is allowed for local dev). Setting it in local mode is an error rather than a silently ignored field. |
|
|
21
|
+
| `sidecar` | **Local mode only.** `{ "name", "args", "build", "buildCwd" }` — a CLI bundled next to the app binary and exposed to the bundled frontend through the `sidecar_output` command. `args` is the fixed argument list it's always run with (default: none); `build` is the command that compiles it, with `{out}` standing in for the path Tauri expects. See `../docs/LOCAL_APPS.md`. |
|
|
22
|
+
| `deepLinkScheme` | Optional custom URL scheme (e.g. `exampleapp` → `exampleapp://...`) registered with the OS so the site's OAuth login page can hand a session back to the app window. Omit it and the whole deep-link path compiles out of the app's behavior. See `../docs/OAUTH.md`. |
|
|
23
|
+
| `trustedOrigins` | Origins allowed to use the scoped Tauri IPC bridge — keep this to exactly the domains you control. `capabilities/remote.json` is generated from this list; leave it empty (or omit it) and the wrapper loads no remote capability at all, which is what a local-mode app wants. |
|
|
24
|
+
| `iconSource` | Path (relative to the profile file) to a single square PNG, at least 1024x1024, used to generate every platform's icon set via `npm run icons`. |
|
|
25
|
+
| `placeholderIcon` | `{ "background": ["#top", "#bottom"], "accent": "#fg" }` — colors for the generated stand-in icon set (`node bin/cli.js icons`), used until you have real artwork. |
|
|
26
|
+
| `copyright` / `publisher` / `category` / `shortDescription` / `longDescription` | Metadata surfaced in installers and store listings. `category` should match the target store's taxonomy (e.g. Apple's `public.app-category.*`, Microsoft Store, Google Play categories — see `../docs/APP_STORES.md`). |
|
|
27
|
+
| `window` | Initial window size/behavior. `fullscreen: true` opens the app in true OS fullscreen (no window chrome); users can leave it with the in-app fullscreen toggle (F11 / Ctrl+Shift+F). |
|
|
28
|
+
| `updater` | Optional `{ "pubkey", "endpoints": [...] }` — turns on `tauri-plugin-updater`. Leave it out and the plugin isn't registered at all; see `../docs/BUILDING.md` for why an "off" updater config is a crash rather than a disabled feature. |
|
|
29
|
+
| `macos` | `{ "minimumSystemVersion": "10.15" }`. |
|
|
30
|
+
| `android` / `ios` | Mobile-specific overrides (package name, minimum OS version). Omit both if the app is desktop-only. |
|
|
31
|
+
|
|
32
|
+
## Remote vs. local mode
|
|
33
|
+
|
|
34
|
+
**Remote** is for packaging a website: the window loads the live site, `dist/` stays an unused
|
|
35
|
+
placeholder, and the app is useless offline — exactly like a browser tab pointed at the same URL.
|
|
36
|
+
The wrapper's added value is OS-level: an icon, an installer, a deep-link login handoff.
|
|
37
|
+
|
|
38
|
+
**Local** is for packaging something that isn't a website: the window loads `dist/index.html`, the
|
|
39
|
+
app ships everything it needs, and it works with no network. Pair it with a `sidecar` when the data
|
|
40
|
+
comes from a CLI you already have — the bundled binary runs on demand and the frontend renders its
|
|
41
|
+
output.
|
|
42
|
+
|
|
43
|
+
The two modes are mutually exclusive by validation, not by convention: a local profile with a `url`
|
|
44
|
+
and a remote profile with a `sidecar` are both rejected by `scripts/profile.mjs` rather than
|
|
45
|
+
half-working.
|
|
46
|
+
|
|
47
|
+
## Regenerating
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node bin/cli.js configure # tauri.conf.json + generated_config.rs + capabilities/remote.json
|
|
51
|
+
node bin/cli.js icons # placeholder icon set from placeholderIcon's colors
|
|
52
|
+
npm run icons # real icon set from iconSource, via the Tauri CLI
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A directory with exactly one profile (what `init` produces) needs no `--profile` flag. This package
|
|
56
|
+
itself keeps only `example.json`, so its own scripts fall back to that.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy this file to profiles/<your-app>.json and fill in every field, or scaffold a fresh copy of the wrapper somewhere else with `node bin/cli.js init <dir> --profile-file <your-profile.json>`. JSON has no comments, so field docs live in profiles/README.md.",
|
|
3
|
+
|
|
4
|
+
"appName": "Example App",
|
|
5
|
+
"productName": "Example App",
|
|
6
|
+
"identifier": "com.example.app",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"mode": "remote",
|
|
9
|
+
"url": "https://example.com",
|
|
10
|
+
"deepLinkScheme": "exampleapp",
|
|
11
|
+
"iconSource": "../assets/icon-source.png",
|
|
12
|
+
"placeholderIcon": {
|
|
13
|
+
"background": ["#1e293b", "#0b1220"],
|
|
14
|
+
"accent": "#f8fafc"
|
|
15
|
+
},
|
|
16
|
+
"copyright": "Example, Inc.",
|
|
17
|
+
"category": "Productivity",
|
|
18
|
+
"shortDescription": "One-line description shown in store listings.",
|
|
19
|
+
"trustedOrigins": ["https://example.com", "https://*.example.com"],
|
|
20
|
+
"window": {
|
|
21
|
+
"title": "Example App",
|
|
22
|
+
"width": 1280,
|
|
23
|
+
"height": 800,
|
|
24
|
+
"minWidth": 480,
|
|
25
|
+
"minHeight": 480,
|
|
26
|
+
"fullscreen": false,
|
|
27
|
+
"resizable": true
|
|
28
|
+
},
|
|
29
|
+
"android": {
|
|
30
|
+
"packageName": "com.example.app",
|
|
31
|
+
"minSdkVersion": 24
|
|
32
|
+
},
|
|
33
|
+
"ios": {
|
|
34
|
+
"bundleId": "com.example.app",
|
|
35
|
+
"minimumSystemVersion": "14.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiles the profile's sidecar CLI into src-tauri/binaries/<name>-<target triple>,
|
|
3
|
+
// which is where Tauri's `bundle.externalBin` looks for it.
|
|
4
|
+
//
|
|
5
|
+
// The triple is read from the installed Rust toolchain rather than mapped from
|
|
6
|
+
// process.platform: Tauri appends the *Rust* target triple, and getting it from
|
|
7
|
+
// the toolchain is the only way the name can't disagree with what the bundler
|
|
8
|
+
// then searches for. A mismatch shows up as "binary not found" at bundle time,
|
|
9
|
+
// after the whole Rust build has already run.
|
|
10
|
+
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { loadProfile, resolveProfileName } from "./profile.mjs";
|
|
16
|
+
|
|
17
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
|
|
19
|
+
export function hostTargetTriple() {
|
|
20
|
+
const verbose = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
|
21
|
+
const match = /^host:\s*(\S+)$/m.exec(verbose);
|
|
22
|
+
if (!match) throw new Error("could not read the host target triple from `rustc -vV`");
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildSidecar(rootDir, profile) {
|
|
27
|
+
if (!profile.sidecar) {
|
|
28
|
+
return { skipped: "the profile bundles no sidecar" };
|
|
29
|
+
}
|
|
30
|
+
if (!profile.sidecar.build) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`profiles/${profile.name}.json's sidecar has no "build" command, so there is nothing to ` +
|
|
33
|
+
"compile. Add one (see docs/LOCAL_APPS.md) or drop the binary into src-tauri/binaries/ yourself.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const triple = hostTargetTriple();
|
|
38
|
+
const suffix = triple.includes("windows") ? ".exe" : "";
|
|
39
|
+
const outDir = path.join(rootDir, "src-tauri", "binaries");
|
|
40
|
+
const outPath = path.join(outDir, `${profile.sidecar.name}-${triple}${suffix}`);
|
|
41
|
+
mkdirSync(outDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
// {out} is the only substitution: the build command owns everything else about
|
|
44
|
+
// how the CLI is compiled, so this script never has to know the language or
|
|
45
|
+
// bundler involved.
|
|
46
|
+
const command = profile.sidecar.build.replaceAll("{out}", outPath);
|
|
47
|
+
const cwd = path.resolve(rootDir, profile.sidecar.buildCwd ?? ".");
|
|
48
|
+
|
|
49
|
+
console.log(`[native-app-wrapper] building sidecar for ${triple}`);
|
|
50
|
+
console.log(` ${command}`);
|
|
51
|
+
console.log(` (in ${cwd})`);
|
|
52
|
+
|
|
53
|
+
const result = spawnSync(command, { cwd, stdio: "inherit", shell: true });
|
|
54
|
+
if (result.status !== 0) {
|
|
55
|
+
throw new Error(`sidecar build command failed with exit code ${result.status}`);
|
|
56
|
+
}
|
|
57
|
+
if (!existsSync(outPath)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`sidecar build command succeeded but wrote nothing to ${outPath} — the command must honor {out}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!suffix) chmodSync(outPath, 0o755);
|
|
63
|
+
|
|
64
|
+
return { outPath: path.relative(rootDir, outPath), triple };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
68
|
+
const profile = loadProfile(rootDir, resolveProfileName(rootDir));
|
|
69
|
+
try {
|
|
70
|
+
const result = buildSidecar(rootDir, profile);
|
|
71
|
+
if (result.skipped) {
|
|
72
|
+
console.log(`[native-app-wrapper] nothing to build: ${result.skipped}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`[native-app-wrapper] wrote ${result.outPath}`);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`[native-app-wrapper] ${error.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|