Help us improve
Share bugs, ideas, or general feedback.
From app-it-static
Turns a finished or buildable web app into a macOS Dock-launchable .app that serves the built output with no dev server. Detects build command and output directory, builds once, serves via tiny static server or file://.
npx claudepluginhub christian-katzmann/app-it --plugin app-it-staticHow this skill is triggered — by the user, by Claude, or both
Slash command
/app-it-static:app-it-staticThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **The companion to app-it.** `app-it` makes a *live, in-progress* project
templates/app-it.config.example.jsontemplates/desktop-build.shtemplates/desktop-icons.shtemplates/desktop-install.shtemplates/desktop-launcher.md.templatetemplates/desktop-quit.shtemplates/desktop-rebuild.shtemplates/info-plist-template.xmltemplates/inspect-static.shtemplates/placeholder-icon-gen.shtemplates/run-template-static-file.shtemplates/run-template-static-server.shtemplates/static-server.pytemplates/wrapper.swiftCreates macOS .app bundles to launch any project from the Dock. Handles packaging, icons, Swift WKWebView shell, menu bar shortcuts, and warm reattach.
Packages a local web project as a Windows desktop app with a Start Menu shortcut, .exe launcher, and Taskbar identity. Mirrors macOS app-it contract (soft-close vs quit, warm dev-server reuse). Beta — requires Windows maintainer to verify hardware-dependent steps.
Adds production features like CI/CD, auto-updates, logging, SwiftLint, localization, Launch at Login to existing macOS Swift apps after analyzing project status.
Share bugs, ideas, or general feedback.
The companion to app-it.
app-itmakes a live, in-progress project clickable by booting its dev server.app-it-staticmakes a finished or buildable app clickable by serving its built output — no dev server, no bundler in memory. Same native window, same Dock Stack, a fraction of the RAM. If the project is half-built, reads local files at dev time, or has no build step, that's app-it's job, not this one.
A dev server (Vite/Next dev) keeps a bundler, file-watcher, and transpiler
resident — typically 300–700 MB per app. A finished build is just files.
Serving them costs ~15 MB (a tiny static server) or ~0 MB (file://,
no server at all). For someone with a dozen finished little apps on their Dock,
that's the difference between a few hundred megabytes and several gigabytes.
This is the local answer to "finished apps shouldn't need a full dev server." No cloud, no Vercel, no PWA step — which also means it works in corporate environments where external hosting isn't approved.
.app is reversible; a project build writes files and takes time. Confirm before the first build; everything else, pick the default and ship it.npm run dev. It runs npm run build once, then serves the result. If a project can't be served without a live dev server, it's the wrong skill — route to app-it.desktop:rebuild. This honesty is the defining difference from app-it — lead with it, never hide it.file:// (zero server) when the build is confirmed file://-safe; otherwise a tiny static server (~15 MB). Never heavier than it needs to be..app keeps its own Dock icon. Same native Swift WKWebView shell as app-it — the foreground process is ours, so the Dock icon, single-instance activation, and menu bar are ours.~/Applications/App It/ — the same Stack app-it uses, so static and live apps live side by side. Override with APP_IT_INSTALL_DIR.package.json + config files, not from README.md. If docs and disk disagree, trust disk and note it.The user almost never wants:
Trigger on any of: "make this built site an app", "serve the build from the Dock",
"finished app launcher", "lighter app-it", "this doesn't need a dev server",
"app-it-static", "static .app", "serve dist/ as an app", "make this out//build/ clickable".
Use app-it instead when:
npm run dev.Do not use this skill for:
This skill ships working templates next to SKILL.md. Copy them into the project
and customize via app-it.config.json — don't rewrite them.
templates/
wrapper.swift # native WKWebView shell — SHARED, identical to app-it's
info-plist-template.xml # Info.plist with placeholders — SHARED, identical to app-it's
desktop-icons.sh # AppIcon.icns generator — SHARED, identical to app-it's
desktop-install.sh # install to ~/Applications/App It/ — SHARED, identical to app-it's
placeholder-icon-gen.sh # last-resort icon generator — SHARED, identical to app-it's
static-server.py # tiny zero-dependency static server (SPA fallback)
run-template-static-server.sh # launcher → static-server.py → wrapper (server mode, default)
run-template-static-file.sh # launcher → wrapper at file:// (file mode, zero server)
desktop-build.sh # assembles the .app around a built output
desktop-quit.sh # stops warm static servers + wrapper windows
desktop-rebuild.sh # re-runs build_command, then build + install
inspect-static.sh # Phase-1 read-only probe (build tool, output dir, serve mode)
app-it.config.example.json # single source of truth — copy + customize
desktop-launcher.md.template # user-facing doc
The five SHARED templates are byte-identical to app-it's and CI guards them against drift. Don't edit them here — if launcher internals need changing, change app-it's copy and re-sync, so both skills stay in step.
Phases run in order.
Run templates/inspect-static.sh first. It reports the package manager,
the framework → build-command → output-dir mapping, any existing built output,
a serve_mode hint, and toolchain availability. Read it before deciding.
Then confirm:
index.html (dist/, build/, out/),
or it's a hand-written static site (index.html at root). It is not if it
needs a live dev server to function (server-rendered Next without output: 'export', an app whose only entry is npm run dev, a project that reads local
files through dev-server middleware). If not → route to app-it and say why.dist//build//out/ with index.html already exists and the user just wants it served, you may skip the build (note it). Otherwise a build is needed.server. Choose file only when the build is confirmed file://-safe — see Serve mode..app per user-facing app, each with its own static_dir. Don't bundle them.com.user.<slug> (never com.$(id -un).*); best square icon source (see app-it's asset-discovery rules, or placeholder-icon-gen.sh); install to ~/Applications/App It/.swiftc (for the native window) and python3 (for server mode) must be present. Both come with the Xcode Command Line Tools; if missing, stop and say xcode-select --install.For each app:
server (default) — serves the build via static-server.py. Always correct. Handles absolute asset paths, client-side routing, local fetch, and service workers. ~15 MB.file — loads the build directly via file://. Zero server, ~0 MB. Use only when confirmed file://-safe.Build needs an http origin? (absolute asset paths / client-side routing /
fetch() of local files / service worker)
├── YES → serve_mode = server (default, safe)
└── NO → serve_mode = file (zero-server, only when all four are absent)
When unsure, pick server. It is never wrong; file is an optimization.
This is the only phase with an expensive step. Confirm the build with the user before running it (it writes files and can take a while). Then:
build_command, e.g. npm run build) from the project root. Confirm the output dir now contains index.html.scripts/ and write scripts/app-it.config.json (see below)..app with desktop:build, then desktop:install.Allowed additions (all additive, reversible):
scripts/wrapper.swift, scripts/info-plist-template.xml, scripts/static-server.py, scripts/run-template-static-*.sh, scripts/desktop-*.sh, scripts/inspect-static.sh, scripts/placeholder-icon-gen.sh.scripts/app-it.config.json — single source of truth.assets/<slug>-icon.{png,svg}; assets/icons/ (gitignore).desktop/<AppName>.app/ (gitignore — regenerated by build).docs/desktop-launcher.md, docs/desktop-launcher.app-it-static-report.md.package.json scripts: desktop:build, desktop:icons, desktop:install, desktop:quit, desktop:rebuild.Never:
npm run dev or add a dev-server path.static-server.py is Python stdlib only.Single source of truth: scripts/app-it.config.json
{
"apps": [
{
"name": "Fjord",
"slug": "fjord",
"serve_mode": "server",
"static_dir": "dist",
"port": 4100,
"bundle_id": "com.user.fjord",
"version": "0.1.0",
"build_command": "npm run build"
}
]
}
desktop-build.sh, desktop-quit.sh, and desktop-rebuild.sh all read this file.
build_command is used only by desktop:rebuild.
Per app. Two buckets — never claim success in a bucket you can't verify.
| # | Check | Programmatic | Idiom |
|---|---|---|---|
| 1 | Build output exists | [x] | test -f "$PROJECT_ROOT/<static_dir>/index.html" |
| 2 | .app built | [x] | .app exists; file <wrapper> reports Mach-O … executable; .icns is Mac OS X icon |
| 3 | Bundle metadata | [x] | PlistBuddy -c 'Print CFBundleIdentifier'; no __PLACEHOLDER__ left |
| 4 | (server mode) Runtime port recorded | [x] | after first open, cat ~/Library/Application Support/app-it/<slug>/server.port |
| 5 | (server mode) Server responds | [x] | curl -sS -o /dev/null -w "%{http_code}" http://localhost:$RUNTIME_PORT is non-000 |
| 6 | (server mode) Cmd+Q frees the port | [x] | osascript -e 'tell application id "<bundle-id>" to quit'; lsof -ti tcp:$RUNTIME_PORT empty within 2s |
| 7 | Install-path opens cleanly | [x] | open "$HOME/Applications/App It/<App>.app"; echo "exit=$?" is 0 |
| 8 | Window shows the built app (not a 404 / blank) | [ ] needs human | unless a display is available |
| 9 | Dock icon is OUR icon | [ ] needs human | unless a display is available |
Pre-flight smoke test (separates project-broken from launcher-broken): for
server mode, run STATIC_DIR="$PROJECT_ROOT/<static_dir>" PORT=$SMOKE python3 scripts/static-server.py,
curl it, then kill. For file mode, confirm index.html exists and its asset
paths are relative. If the smoke test fails, report build-broken, not launcher-broken.
If GUI verification is impossible (no display), say so under Known limitations — don't claim the window renders.
Two outputs: an inline chat report (format below) and the same content written to
docs/desktop-launcher.app-it-static-report.md with a ## Decision history
section future sessions append to. Stage new files with git add; don't commit
unless asked.
Verify from disk. Default package-manager-aware build command; default output dir:
| Signal | Build command | Output dir | serve_mode default |
|---|---|---|---|
vite.config.* | <pm> build | dist/ | server (Vite uses absolute /assets/ paths) |
astro.config.* (static, the default) | <pm> build | dist/ | server |
astro.config.* with output: 'server'/'hybrid' | — | — | route to app-it (SSR, not static) |
react-scripts in package.json (CRA) | <pm> build | build/ | server (CRA uses absolute /static/) |
svelte.config.js with adapter-static | <pm> build | build/ | server |
svelte.config.js without adapter-static | — | — | route to app-it (SSR by default) |
next.config.* with output: 'export' | <pm> build | out/ | server |
next.config.* without export | — | — | route to app-it (needs a server) |
vue.config.js | <pm> build | dist/ | server |
angular.json | <pm> build | dist/<project>/browser/ (v17+) | server (absolute base href) |
nuxt.config.* via nuxi generate | <pm> generate | .output/public/ | server |
nuxt.config.* plain nuxt build | — | — | route to app-it (SSR / Nitro) |
index.html at root, no build tool | none (build_command: "") | . | file if relative paths, else server |
existing dist//build//out/ + index.html | optional (serve as-is) | that dir | inspect index.html |
<pm> resolves from the lockfile: pnpm-lock.yaml→pnpm build, yarn.lock→yarn build,
bun.lockb→bun run build, package-lock.json/none→npm run build.
Next.js caveat (important): a standard Next app server-renders and cannot
be served as static files. Only with output: 'export' in next.config.* does it
emit a static out/. Without it, this is an app-it (dev-server) project — say so
plainly rather than producing a broken static .app.
file:// is the lightest possible launcher (no process at all), but it breaks for
most framework builds. Use it only when all four hold:
./assets/..., not /assets/.... file:// resolves
absolute paths against the filesystem root → 404. (Vite/CRA default to absolute;
Vite needs base: './', CRA needs "homepage": "." to go relative — note this
in the report rather than editing their config.)file:// has no
server to fall back to index.html.fetch() of local files — file:// origin is null, so CORS blocks it.file://.If any fail, use server. The bundled static-server.py handles all four
(SPA-fallback to index.html, correct MIME types, a real http origin) for ~15 MB.
Don't fight file://. If a build wants an http origin, give it the tiny
server — don't rewrite the app to make file:// work.
npm run dev. That's app-it. This skill builds once and serves the result..app serves a snapshot. Say it plainly; point at desktop:rebuild.file:// for framework builds. Most use absolute asset paths or client routing and will show a blank/404 window. Default to server; use file only when confirmed safe.output: 'export' there's no static output. Route to app-it.static-server.py is stdlib only; python3 already ships with the Xcode CLT this skill requires.vite.config/server for env-driven ports; this skill does not — a static build that needs source edits is an app-it project.0.0.0.0. 127.0.0.1 only. This is a personal launcher, not a host. (static-server.py enforces this.)desktop-build.sh. Build once (Phase 3 / desktop:rebuild); desktop:build only assembles the bundle, so routine rebuilds stay fast and side-effect-free.com.$(id -un).* as the bundle-id prefix. LaunchServices may reject it (error -600). Use com.user.<slug>.PROJECT_ROOT from $0. The .app is copied to ~/Applications/App It/ on install — bake the absolute path at build time.package.json script naming{
"scripts": {
"desktop:icons": "APP_NAME='Fjord' APP_SLUG='fjord' ./scripts/desktop-icons.sh",
"desktop:build": "./scripts/desktop-build.sh",
"desktop:install": "./scripts/desktop-install.sh",
"desktop:quit": "./scripts/desktop-quit.sh",
"desktop:rebuild": "./scripts/desktop-rebuild.sh"
}
}
If the project has no package.json (hand-written static site), expose the same
commands via a Makefile or top-level shell script.
End every session with this report. No section omitted; "n/a" if truly inapplicable.
Inline in chat and written to docs/desktop-launcher.app-it-static-report.md.
## App-it-static report
**1. Project type detected:**
<e.g. Vite + React, build → dist/, pnpm; or hand-written static site at root; swiftc + python3 available>
**2. Static-servable?** <yes / no — if no, why, and "use app-it instead">
**3. Apps detected:** <N>
- **<AppName>** — serves `<static_dir>/`, serve_mode <server|file>, build `<build_command>`
**4. Serve mode per app + why:**
- <AppName>: <server|file> — <one line: e.g. "absolute /assets/ paths need an http origin" or "relative paths, no routing → file://, zero server">
**5. Build:**
- Command run: `<build_command>` <(skipped — fresh output already present)>
- Output confirmed: `<static_dir>/index.html`
**6. Files added/changed:** <scripts/*, assets/<slug>-icon.png, desktop/<App>.app, docs/*, package.json scripts, .gitignore>
**7. Icon source:** <path — resolution, why it beat alternatives>
**8. Commands:**
- Build: `<pm> desktop:build` Install: `<pm> desktop:install` (→ ~/Applications/App It/)
- Refresh snapshot: `<pm> desktop:rebuild` Stop server: `<pm> desktop:quit`
**9. Verification (per app):**
- [x] Build output exists; `.app` built; wrapper universal Mach-O; `.icns` multi-resolution
- [x] Bundle metadata correct (no `__PLACEHOLDER__`)
- [x] (server) server responds on runtime port; Cmd+Q frees it
- [x] Install-path open exits 0
- [ ] needs human: window renders the app, Dock icon identity
**10. Known limitations:**
- Snapshot, not live — re-run `desktop:rebuild` after source changes.
- Unsigned bundle — Gatekeeper warns on first launch (right-click → Open once).
- Baked PROJECT_ROOT — rebuild if the repo moves.
- WebKit, not Chromium.
- <serve_mode=file: relative-path requirement; serve_mode=server: ~15 MB resident while warm>
## Decision history
- <YYYY-MM-DD>: Initial build (serve_mode <X>, static_dir <Y>, build `<cmd>`, port <P>, icon: <source>).
| Signal | Decision |
|---|---|
vite.config.*, no special base | server, dist/ |
vite.config.* with base: './' + no routing/fetch | file candidate, dist/ |
CRA (react-scripts) | server, build/ |
| Astro (static, default) | server, dist/ |
Astro with output: 'server'/'hybrid' | app-it (SSR) |
SvelteKit + adapter-static | server, build/ |
SvelteKit without adapter-static | app-it (SSR) |
Next with output: 'export' | server, out/ |
| Next without export | app-it (dev server) |
Angular (angular.json) | server, dist/<project>/browser/ |
Nuxt via nuxi generate | server, .output/public/ |
Nuxt plain nuxt build | app-it (SSR) |
hand-written index.html, relative assets | file, . |
existing dist/ the user just wants served | inspect index.html, usually server, skip build |
project only runs via npm run dev | app-it |
| needs source edits to serve statically | app-it |
swiftc missing | stop — xcode-select --install |