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.
Files changed (45) hide show
  1. package/README.md +111 -0
  2. package/assets/icon-source.png +0 -0
  3. package/bin/cli.js +248 -0
  4. package/dist/index.html +24 -0
  5. package/docs/APP_STORES.md +129 -0
  6. package/docs/BUILDING.md +134 -0
  7. package/docs/LOCAL_APPS.md +116 -0
  8. package/docs/MOBILE.md +78 -0
  9. package/docs/OAUTH.md +76 -0
  10. package/package.json +66 -0
  11. package/profiles/README.md +56 -0
  12. package/profiles/example.json +37 -0
  13. package/scripts/build-sidecar.mjs +80 -0
  14. package/scripts/configure.mjs +222 -0
  15. package/scripts/generate-icons.mjs +53 -0
  16. package/scripts/generate-placeholder-icons.mjs +186 -0
  17. package/scripts/lib/png.mjs +141 -0
  18. package/scripts/profile.mjs +147 -0
  19. package/src-tauri/Cargo.lock +5832 -0
  20. package/src-tauri/Cargo.toml +47 -0
  21. package/src-tauri/build.rs +3 -0
  22. package/src-tauri/capabilities/default.json +15 -0
  23. package/src-tauri/capabilities/remote.json +17 -0
  24. package/src-tauri/icons/128x128.png +0 -0
  25. package/src-tauri/icons/128x128@2x.png +0 -0
  26. package/src-tauri/icons/32x32.png +0 -0
  27. package/src-tauri/icons/64x64.png +0 -0
  28. package/src-tauri/icons/Square107x107Logo.png +0 -0
  29. package/src-tauri/icons/Square142x142Logo.png +0 -0
  30. package/src-tauri/icons/Square150x150Logo.png +0 -0
  31. package/src-tauri/icons/Square284x284Logo.png +0 -0
  32. package/src-tauri/icons/Square30x30Logo.png +0 -0
  33. package/src-tauri/icons/Square310x310Logo.png +0 -0
  34. package/src-tauri/icons/Square44x44Logo.png +0 -0
  35. package/src-tauri/icons/Square71x71Logo.png +0 -0
  36. package/src-tauri/icons/Square89x89Logo.png +0 -0
  37. package/src-tauri/icons/StoreLogo.png +0 -0
  38. package/src-tauri/icons/icon.icns +0 -0
  39. package/src-tauri/icons/icon.ico +0 -0
  40. package/src-tauri/icons/icon.png +0 -0
  41. package/src-tauri/src/generated_config.rs +22 -0
  42. package/src-tauri/src/lib.rs +212 -0
  43. package/src-tauri/src/main.rs +6 -0
  44. package/src-tauri/tauri.conf.json +84 -0
  45. 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
+ }