Packaging a webpage as a desktop app traditionally means Electron: hundreds of megabytes, high memory use, and a slow toolchain. Pake replaces that with a Rust/Tauri base, producing installers roughly 5 MB in size. The goal is a one-command path from URL to native app for macOS, Windows, and Linux.
What This Does
Pake is a CLI and GitHub Action that wraps a webpage in a Tauri shell. The TypeScript CLI (bin/cli.ts) orchestrates the build: it resolves options (bin/options/index.ts), generates icons (bin/options/icon.ts), and delegates to platform-specific builders (bin/builders/MacBuilder.ts, WinBuilder.ts, LinuxBuilder.ts). The Rust side (src-tauri/src/) is the compiled app runtime, with injected JavaScript (src-tauri/src/inject/) handling window behavior, theming, and shortcuts.
The repo ships with prebuilt icons and configs for popular sites (Twitter, DeepSeek, WeRead) and supports Docker builds via the Dockerfile.
How It Is Wired
Execution starts at start in bin/builders/BaseBuilder.ts:137, which reaches 22 functions. The core path is start -> mergeConfig -> injectCustomCode -> combineFiles, ending in a fs.writeFile call—so a run writes generated config and injected code to disk before invoking the Rust build.
The highest-traffic functions are generateLinuxPackageName (called from 9 places) and shellExec (6 places). BaseBuilder.ts is the hub: 23 functions, called from 2 files, calling into 11 others. It owns build, start, and buildAndCopy. The bin/utils/name.ts module (6 functions) is imported by 5 files and controls all package naming—a change there ripples across every platform builder.
The module graph has no circular dependencies (0 in 33 edges), so refactoring is safe. The main cost is in src-tauri/src/app/setup.rs and window.rs, where nesting depth reaches 9 levels—hard to follow but isolated from the TypeScript build pipeline.
How To Use It
Setup: Install with pnpm (the repo uses pnpm-lock.yaml and .pnpmrc):
pnpm install
Configuration: No required env vars. Optional: src-tauri/pake.json holds app defaults; src-tauri/tauri.conf.json and platform-specific tauri.*.conf.json files control window and build settings.
Running it: The CLI entry point is bin/cli.ts:
pnpm pake https://example.com --name MyApp
For Docker builds, the Dockerfile exists but no build command is documented in the repo.
Real-World Use
A team shipping an internal dashboard can package it as a native app per platform:
CI then runs the GitHub Action (action.yml) to produce signed installers for all three OSes, with the single-app.yaml workflow handling a single-site build.
Code Health & Issues
Static analysis found 12 issues (4 high, 8 medium). The high-severity items:
High – Deep nesting in src-tauri/src/app/setup.rs, window.rs, lib.rs (max depth 9). Flatten with early returns.
High – Oversized files: tests/index.js (1352 lines), src-tauri/src/inject/event.js, find.js. Split by responsibility.
Medium issues include an unclosed file handle in icns2png.py, an empty catch {} in event.js, and duplicated code in bin/options/icon.ts and bin/utils/ico.ts.
SDLC observations from the workflow files: CI uses mutable action tags (pnpm/action-setup@v4) instead of commit SHAs, quality-and-test.yml discards exit codes and pushes to the default branch, and no dependency scan or Dependabot is configured. The Dockerfile uses rust:latest and runs as root.
The Bottom Line
Pake is a solid, well-structured tool for turning webpages into lightweight desktop apps. The TypeScript build layer is clean and testable; the Rust runtime has some readability debt but is isolated. Use it if you need fast, small native wrappers and can accept the CI hygiene gaps—pin those actions and add a non-root user before production use.
What the analyser found
Deployment readiness
7/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✓No committed secrets
Composition
209 files
TypeScript41
Markdown25
JavaScript18
YAML13
Rust10
JSON9
ReactDocker
Architecture
Top-level areas of the codebase, sized by module count. Arrows show how many imports cross from one area into another.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
high3
medium6
low1
Pin third-party GitHub Actions to a commit SHAhigh9 occurrences
A tag can be moved, so the action running with your token and secrets is whatever its owner last pushed; this is how tj-actions/changed-files leaked secrets from thousands of repos.
Fix: Replace each @vN with the 40-character commit SHA, keep # vN as a comment, and let Dependabot bump the SHAs.
Stop discarding the exit code of steps whose failure mattershigh2 occurrences
.github/workflows/quality-and-test.yml
lines 128, 130 discard failure
The job reports green while the artifact it was supposed to produce was never produced, so a crash becomes a successful run built on stale data.
Fix: Let the step fail, or test the specific expected condition instead of discarding all exit codes.
Open a pull request instead of pushing to the default branchhigh
.github/workflows/quality-and-test.yml
git push
Automated commits land on the branch that deploys, with no test having run against the result.
Fix: Push to a bot branch and open a pull request, or restrict the push to a tag ref.
Declare least-privilege permissions for GITHUB_TOKENmedium
.github/workflows/pake-cli.yaml
1 workflow(s) declare no permissions
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
2 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Pin the container base image by digestmedium2 occurrences
Dockerfile
rust:latest, rust:latest (mutable tag)
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
Set persist-credentials: false on checkoutmedium
.github/workflows/npm-publish.yml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Add a non-root USER to the imagemedium
Dockerfile
CMD or ENTRYPOINT with no USER directive
A process running as root in the container is root against every mounted volume, and it turns any container escape or writable-mount mistake from a contained problem into a host one.
Fix: Create an unprivileged user, chown what it needs, and end the Dockerfile with USER.
Set timeout-minutes on the workflow jobslow2 occurrences
.github/workflows/npm-publish.yml
2 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.