Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# app-it

Turn a local web project into a macOS Dock-launchable `.app` bundle — a native window, its own Dock icon, and clean start/stop — **without Electron, Tauri, or a rewrite.**
Turn a local web project or hosted Claude Artifact into a macOS Dock-launchable `.app` bundle — a native window, its own Dock icon, and clean start/stop — **without Electron, Tauri, or a rewrite.**

> **Unofficial community project** — not affiliated with, endorsed by, or sponsored by Anthropic or OpenAI. "Claude Code" and "Codex" are their respective owners' marks, named here only to say which assistants app-it plugs into. This is an independent open-source tool built by one developer.

Expand All @@ -12,14 +12,14 @@ Turn a local web project into a macOS Dock-launchable `.app` bundle — a native

**Windows beta** — macOS is in daily use; Windows is an early beta, now with its first real-hardware fixes but still needing more. A complete sibling plugin (`plugins/app-it-windows/`), gated by a required `windows-latest` CI job (build · PowerShell lint · manifest parse · icon round-trip), mirrors the macOS contract with Windows primitives. The author runs only macOS, so for a long time it had never touched real Windows hardware; that changed with [#8](https://github.com/Christian-Katzmann/app-it/pull/8), the first run on an actual Windows machine, and a run of fixes since — a real start, not a finish line. If you're on Windows and want to help harden it, the doorway is [docs/WINDOWS.md](docs/WINDOWS.md).

**Local-only** — app-it reads your project *on your machine* to choose a launcher strategy. It uploads nothing, runs no telemetry, adds no runtime dependencies, and never touches your business-logic source. The only thing it produces is an `.app` on your own Dock. See [Privacy](PRIVACY.md) and [Terms](TERMS.md) for the plain-language policy.
**Local-first** — app-it reads your project *on your machine* to choose a launcher strategy. It uploads nothing, runs no telemetry, adds no runtime dependencies, and never touches your business-logic source. The only thing it produces is an `.app` on your own Dock. For Claude Artifacts, the supported networked path is explicit: wrap a published/shared `claude.ai` artifact URL so Claude handles login and usage against each user's own plan. See [Privacy](PRIVACY.md) and [Terms](TERMS.md) for the plain-language policy.

`app-it` is an assistant-agnostic plugin/skill. It works with **Claude Code** and **Codex**, and builds a small, repeatable launcher around an existing local project so that double-clicking starts the dev server, opens a native window, keeps the Dock icon as *your* app, and cleans up when you quit.
`app-it` is an assistant-agnostic plugin/skill. It works with **Claude Code** and **Codex**, and builds a small, repeatable launcher around an existing local project or hosted artifact so that double-clicking starts the right runtime, opens a native window, keeps the Dock icon as *your* app, and cleans up when you quit.

## What app-it is not

- **Not Electron, Tauri, or a native rewrite.** It wraps your existing dev setup; it doesn't replace it, migrate it, or add a bundler to your dependency tree.
- **Not a way to ship apps to other people.** No notarization, no App Store, no auto-update, no signed distribution. These are personal, ad-hoc-signed, local-use launchers.
- **Not a general signed distribution system.** No notarization, no App Store, no auto-update, no installer. Claude Artifact URL wrappers are self-contained enough to zip and share, but they are still ad-hoc-signed local bundles; recipients must trust the bundle and sign into Claude with their own account.
- **Not cross-platform.** macOS only — and on purpose. Windows is a genuinely different problem (WebView2, `.lnk`, `.ico`, SmartScreen), so it belongs in a separate plugin rather than a blurred promise. See [Compatibility](docs/COMPATIBILITY.md).
- **Not a hosted service.** Nothing runs in the cloud and there is no live demo to visit — the proof is the apps on your own Dock (the Stack further down is real).

Expand All @@ -30,14 +30,15 @@ Turn a local web project into a macOS Dock-launchable `.app` bundle — a native
─────────────────────── ────────────────────── ───────────────────────────
a local web project inspects it from disk, YourApp.app on your Dock
Vite React, SvelteKit, picks a strategy, then · its own icon
Astro, Next, or static ──▶ builds & signs a .app ──▶ · native window, one click
sites (`npm run dev`) around a WebKit shell · ⌘Q quits & frees the port
Astro, Next, static, or ──▶ builds & signs a .app ──▶ · native window, one click
Claude Artifact URL around a WebKit shell · ⌘Q quits & frees the port
```

Under the hood, app-it:

- **Inspects before it touches anything** — project type, dev scripts, ports, browser-API needs, icon sources.
- **Picks a launcher strategy** — a native Swift `WKWebView` shell by default (so the Dock icon stays *yours*), Chrome `--app` mode only when a project needs Chromium-only APIs.
- **Wraps hosted Claude Artifacts without shared secrets** — set `external_url`/`artifact_url` to a published artifact link; each user signs into Claude in the app window and uses their own Claude plan.
- **Copies proven, hard-won templates** into the project rather than re-deriving fragile launcher logic each time.
- **Builds and ad-hoc-signs a real `.app`** — universal (arm64 + x86_64), Gatekeeper-friendly, with a generated `.icns`.
- **Gets the lifecycle right** — closing the window (⌘W / red-X) leaves the dev server warm for a ~250 ms re-launch; ⌘Q quits the app *and* frees the port.
Expand Down Expand Up @@ -77,6 +78,8 @@ Then, from inside any local web project, ask your assistant:

Natural triggers work too: *"make this clickable from the Dock"*, *"give this an icon"*, *"dockify this"*, *"package this as a local app"*.

For Claude Artifacts that call Claude from inside the artifact, use the hosted artifact link, not copied JSX source. A raw `.jsx` file can be packaged as an ordinary local React app only when it does not depend on Claude's hosted artifact runtime (`window.claude`, `window.storage`, MCP prompts, or Claude-provided auth).

*Optional:* for finished apps, also install the lighter companion — `claude plugin install app-it-static@app-it` (or `codex plugin add app-it-static@app-it`), then run `/app-it-static`.

### Local development (before publication)
Expand Down
16 changes: 15 additions & 1 deletion docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,23 @@
| OS | macOS |
| Primary shell | Swift `WKWebView` wrapper |
| Fallback shell | Chrome `--app` mode |
| Common targets | Vite + React, SvelteKit, Astro, Next.js, static sites, local multi-server apps |
| Common targets | Vite + React, SvelteKit, Astro, Next.js, static sites, local multi-server apps, published Claude Artifact URLs |
| Install destination | `~/Applications/App It/` by default |
| Signing | Ad-hoc local code signing only |

## Claude Artifact URL wrappers

`app-it` can build a URL-only `.app` around a published/shared Claude Artifact
link. That path intentionally keeps Claude in charge of authentication and
artifact runtime APIs: the bundle loads the hosted `claude.ai` URL, the user
signs into Claude inside the app window, and usage belongs to that user's plan.

Raw JSX/TSX exported from an artifact is only a normal local web app source
file. If it calls Claude's hosted artifact APIs (`window.claude`,
`window.storage`, MCP prompts, or Claude-provided auth), do not shim credentials,
cookies, or API keys into the local bundle. Publish/share the artifact in Claude
and set `external_url` or `artifact_url` in `scripts/app-it.config.json`.

## Companion · `app-it-static`

For **finished or buildable** apps, the `app-it-static` sibling plugin serves the
Expand Down Expand Up @@ -51,6 +64,7 @@ The author runs only macOS and will not dogfood the Windows build, so it ships a
- Notarized distribution to other users.
- Auto-update.
- Installer generation.
- Local redistribution of Claude Artifact auth, session cookies, or API keys.
- Production Electron or Tauri migrations.

## Why Windows Should Be Separate
Expand Down
21 changes: 17 additions & 4 deletions plugins/app-it-static/skills/app-it-static/templates/wrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// .app bundle owns its window (and therefore its Dock icon, activation, and
// single-instance semantics).
//
// Usage: wrapper <url> [app-name] [port] [pid-file] [polyfill-js-path]
// Usage: wrapper <url> [app-name] [port] [pid-file] [polyfill-js-path] [allow-external-hosts]
// url — http(s) URL to load (typically http://localhost:PORT)
// app-name — window title and Dock badge (e.g. "Momó Studio")
// port — optional, used by Cmd+Q only with PID ownership proof
Expand All @@ -19,6 +19,10 @@
// The polyfill is the agent's responsibility — see the
// skill's `fsa-polyfill-template.js` for a worked
// example. Pass an empty string to skip injection.
// allow-external-hosts — optional literal. Used by URL-only launchers such as
// Claude Artifact wrappers, where auth, iframes, and API
// bridges must remain inside the hosted web app instead
// of being kicked out to the default browser.
//
// Build: swiftc wrapper.swift -o <out> -framework Cocoa -framework WebKit
//
Expand Down Expand Up @@ -53,6 +57,7 @@ final class AppDelegate: NSObject,
private let port: Int?
private let pidFilePath: String?
private let polyfillJSPath: String?
private let allowExternalHosts: Bool
private var window: NSWindow!
private var webView: WKWebView!
private var quittingViaWindowClose = false
Expand All @@ -65,13 +70,15 @@ final class AppDelegate: NSObject,
appName: String,
port: Int?,
pidFilePath: String?,
polyfillJSPath: String?
polyfillJSPath: String?,
allowExternalHosts: Bool
) {
self.url = url
self.appName = appName
self.port = port
self.pidFilePath = pidFilePath
self.polyfillJSPath = polyfillJSPath
self.allowExternalHosts = allowExternalHosts
super.init()
}

Expand Down Expand Up @@ -295,6 +302,10 @@ final class AppDelegate: NSObject,
decisionHandler(.allow)
return
}
if allowExternalHosts {
decisionHandler(.allow)
return
}
// External links open in the user's default browser, not in the .app
// window. Anything localhost stays in-window.
if let host = target.host,
Expand Down Expand Up @@ -950,21 +961,23 @@ final class FindBar: NSView, NSTextFieldDelegate, NSSearchFieldDelegate {
let arguments = CommandLine.arguments
guard arguments.count >= 2, let url = URL(string: arguments[1]) else {
FileHandle.standardError.write(
Data("usage: wrapper <url> [app-name] [port] [pid-file] [polyfill-js-path]\n".utf8))
Data("usage: wrapper <url> [app-name] [port] [pid-file] [polyfill-js-path] [allow-external-hosts]\n".utf8))
exit(2)
}
let appName = arguments.count >= 3 ? arguments[2] : "App"
let port = arguments.count >= 4 ? Int(arguments[3]) : nil
let pidFilePath = arguments.count >= 5 && !arguments[4].isEmpty ? arguments[4] : nil
let polyfillJSPath = arguments.count >= 6 && !arguments[5].isEmpty ? arguments[5] : nil
let allowExternalHosts = arguments.count >= 7 && arguments[6] == "allow-external-hosts"

let app = NSApplication.shared
let delegate = AppDelegate(
url: url,
appName: appName,
port: port,
pidFilePath: pidFilePath,
polyfillJSPath: polyfillJSPath
polyfillJSPath: polyfillJSPath,
allowExternalHosts: allowExternalHosts
)
app.delegate = delegate
app.setActivationPolicy(.regular)
Expand Down
16 changes: 10 additions & 6 deletions plugins/app-it/skills/app-it/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
name: app-it
description: >-
Turn a local project into a macOS Dock-launchable .app. Use when the user
wants a clickable Dock app, local app package, icon, App It install, or
repeatable desktop launcher. Defaults to native Swift WebKit, shipped
templates, and verification of build, launch, ports, quit, warm relaunch, and
cleanup.
Turn a local project or hosted Claude Artifact URL into a macOS
Dock-launchable .app. Use when the user wants a clickable Dock app, local app
package, icon, App It install, Artifact wrapper, or repeatable desktop
launcher. Defaults to native Swift WebKit, shipped templates, and
verification of build, launch, ports, quit, warm relaunch, and cleanup.
---

# app-it - Make a local project launchable from the Dock
# app-it - Make a local project or hosted Artifact launchable from the Dock

App It installs local projects under `~/Applications/App It/` as clickable
macOS apps: click opens, window close stays warm, Cmd+Q cleans up.
Expand All @@ -26,6 +26,10 @@ macOS apps: click opens, window close stays warm, Cmd+Q cleans up.
project already has one or Strategy A cannot satisfy the requirement.
6. Verify the installed app path, runtime port truth, warm relaunch, Cmd+Q
cleanup, and report honestly when GUI-only checks need a human.
7. Keep Claude Artifact auth with Claude. If an Artifact uses hosted runtime
APIs (`window.claude`, `window.storage`, MCP prompts, or Claude-provided
auth), package a published/shared `claude.ai` URL. Never copy sessions,
cookies, API keys, or another user's Claude auth into a local bundle.

## Reference Map

Expand Down
10 changes: 8 additions & 2 deletions plugins/app-it/skills/app-it/references/generated-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Copy from `templates/`:
- `run-template.sh`
- `run-template-chrome.sh`
- `run-template-multiserver.sh`
- `run-template-url.sh`
- `run-template-url-chrome.sh`
- `native-run-stub.c`
- `desktop-build.sh`
- `desktop-icons.sh`
Expand Down Expand Up @@ -59,13 +61,16 @@ Use `scripts/app-it.config.json`:
"start_command": "npm run dev -- --host 127.0.0.1 --port $PORT --strictPort",
"bundle_id": "com.user.my-app",
"version": "0.1.0",
"polyfill_path": ""
"polyfill_path": "",
"external_url": ""
}
]
}
```

For A3, add `backend_port` and `backend_start_command`.
For A3, add `backend_port` and `backend_start_command`. For Strategy E
URL-only apps, set `external_url` (or alias `artifact_url` / `url`) and leave
local-server fields empty or `null`; URL-only mode wins if both are present.

Use `port_mode: "fixed"` only when the frontend origin must stay exact. The
default `fallback` mode is friendlier for sibling local apps because it scans
Expand All @@ -81,6 +86,7 @@ Build-time substitution writes:
- `__PORT__`
- `__PORT_MODE__`
- `__START_COMMAND__`
- `__APP_URL__`
- `__BUNDLE_ID__`
- `__VERSION__`
- `__POLYFILL_PATH__`
Expand Down
5 changes: 5 additions & 0 deletions plugins/app-it/skills/app-it/references/project-inspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ read-only and prints the signals that decide the packaging route.
- `dev` and `start` script inventory, especially scripts with hardcoded
`-p`/`--port` values or multi-process orchestrators.
- Framework port literals in config files.
- Claude Artifact URLs and hosted Artifact runtime API usage.
- Two-stage File System Access usage.
- Existing App It sibling apps and current port listeners.
- Toolchain availability, especially `swiftc`.
Expand All @@ -35,6 +36,10 @@ persistent absolute path.
- Existing desktop: `electron`, `electron-builder`, `src-tauri/`, `nw.js`.
- PWA: `manifest.json` and service worker. Still build a Strategy A `.app` and
mention PWA install as optional.
- Published/shared Claude Artifact URL: choose Strategy E URL-only.
- Raw Claude Artifact source using `window.claude`, `window.storage`, MCP
prompts, or Claude-provided auth: block local credential shims and ask for a
published/shared Artifact URL.

Ignore stale docs when they disagree with the files that actually run.

Expand Down
45 changes: 34 additions & 11 deletions plugins/app-it/skills/app-it/references/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
Choose exactly one strategy per user-facing app.

```text
Existing Electron/Tauri/NW.js config?
yes -> Strategy B
no -> native desktop requirements beyond web shell?
yes -> Strategy D
no -> FSA real-I/O or Chromium-only API?
yes -> A1 Chrome fallback
no -> static built bundle, no server?
yes -> A2
no -> cohabiting frontend + backend?
yes -> A3
no -> A1 native WebKit
Published/shared Claude Artifact URL?
yes -> Strategy E
no -> raw Artifact source uses window.claude/window.storage/MCP/auth?
yes -> blocked until user provides/publishes a Claude Artifact URL
no -> Existing Electron/Tauri/NW.js config?
yes -> Strategy B
no -> native desktop requirements beyond web shell?
yes -> Strategy D
no -> FSA real-I/O or Chromium-only API?
yes -> A1 Chrome fallback
no -> static built bundle, no server?
yes -> A2
no -> cohabiting frontend + backend?
yes -> A3
no -> A1 native WebKit
```

## A1 Native WebKit - Default
Expand Down Expand Up @@ -84,6 +88,25 @@ proxy targets read `API_PORT`, and backend entrypoints read `API_PORT` before
Only for scripts with no UI. It spawns Terminal and should be flagged clearly in
the report. Do not choose this for web apps.

## Strategy E URL-Only Hosted App

Use when the app already lives at a hosted URL and the host must own auth and
runtime behavior. The primary case is a published/shared Claude Artifact:
Claude provides the artifact sandbox, AI bridge, storage, login, and plan usage.
App It wraps the URL; it does not recreate Claude's runtime locally.

Set `external_url`, `artifact_url`, or `url` in `scripts/app-it.config.json`.
The generated launcher starts no local daemon, writes no `server.port`, and
passes `allow-external-hosts` to the Swift wrapper so Claude auth redirects,
hosted iframe traffic, and API bridge navigation remain in-window.

Raw JSX/TSX exported from a Claude Artifact is only normal local web app source
when it does not depend on Claude's hosted runtime APIs. If it calls
`window.claude`, `window.storage`, MCP prompts, or Claude-provided auth, do not
shim local credentials, cookies, sessions, or API keys. Publish/share the
artifact in Claude and package that hosted URL so each recipient signs in with
their own Claude account and plan.

## Strategy B Existing Desktop Config

If the repo already has Electron, Tauri, or NW.js, use it instead of stacking
Expand Down
8 changes: 8 additions & 0 deletions plugins/app-it/skills/app-it/references/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ For fixed-port apps, verify that `ports.mode` is `fixed` and the recorded
runtime port equals the configured preferred port. A busy preferred port should
fail launch with an explanation; fallback is deliberately disabled.

For Strategy E URL-only apps, rows 3-9 are `n/a - no local server`.
Programmatic verification is: `APP_IT_SMOKE=1 desktop/<App>.app/Contents/MacOS/run`
prints the configured URL, no `server.port` is written, `run.sh` contains the
URL and `allow-external-hosts` in Swift mode, and rows 1-2 still pass. GUI
verification is opening the installed app, signing into Claude if needed, and
confirming the hosted artifact runs in-window. Do not `curl` private Artifact
URLs as proof; auth-protected Artifacts may correctly redirect.

| # | Check | Idiom |
| ---: | --- | --- |
| 1 | Build succeeded | `.app` exists; `file Contents/MacOS/run` reports Mach-O; `run.sh` executable; wrapper Mach-O; icon file valid |
Expand Down
Loading
Loading