Skip to content

Commit 81ad0a7

Browse files
koki-developclaude
andcommitted
feat: ship cork CLI inside the app bundle and link it onto PATH
Add a skeleton cork CLI as a separate lean crate in a new Cargo workspace, embed it into Cork.app via Tauri's externalBin sidecar, and expose it on PATH through a Homebrew Cask binary stanza — so installing the app also installs the cork command. The CLI version is injected from package.json at build time so it never drifts from the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 41d58cc commit 81ad0a7

13 files changed

Lines changed: 263 additions & 15 deletions

File tree

‎.lintstagedrc.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default {
22
"*.{js,jsx,ts,tsx}": "oxlint --fix",
33
"*.{js,jsx,ts,tsx,css,json,jsonc,md,html}": "oxfmt",
4-
"src-tauri/src/**/*.rs": () =>
5-
"cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- --deny warnings",
4+
"src-tauri/**/*.rs": () =>
5+
"cargo clippy --manifest-path src-tauri/Cargo.toml --workspace --all-targets -- --deny warnings",
66
};

‎AGENTS.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
Frontend = React + atomic design (see `src/AGENTS.md`).
44
Backend = Tauri v2 + Rust (see `src-tauri/AGENTS.md`).
5+
CLI = the `cork` command, a separate lean Rust crate shipped **inside** the app bundle (see `src-tauri/AGENTS.md` → "CLI distribution").
56
Multi-window: a single process can host any number of windows, each with its own workspace. The `File > New Window` menu / `Cmd+Shift+N` opens an empty welcome window, and the macOS Dock-reopen path restores the last-used workspace into a fresh window. Per-window state lives in `AppState` keyed by `WebviewWindow::label()`; see `src-tauri/AGENTS.md` for the state model.
67

78
## Stack
@@ -14,17 +15,18 @@ Multi-window: a single process can host any number of windows, each with its own
1415

1516
## Commands
1617

17-
| Command | What it does |
18-
| ------------------- | -------------------------------------------------------------------------- |
19-
| `bun run dev` | Vite dev server on port 1420 (frontend only — Tauri APIs error in browser) |
20-
| `bun run build` | `tsc && vite build` (full typecheck + bundle) |
21-
| `bun run tauri` | Tauri CLI passthrough (e.g. `bun run tauri dev`, `bun run tauri build`) |
22-
| `bun run format` | `oxlint --fix && oxfmt` (lint fix + format) |
23-
| `bun run lint` | `oxlint` (lint check only) |
24-
| `bun run lint:fix` | `oxlint --fix` (lint with autofix) |
25-
| `bun run fmt` | `oxfmt` (format only) |
26-
| `bun run fmt:check` | `oxfmt --check` (check formatting) |
27-
| `bun run preview` | `vite preview` (serve production build) |
18+
| Command | What it does |
19+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
20+
| `bun run dev` | Vite dev server on port 1420 (frontend only — Tauri APIs error in browser) |
21+
| `bun run build` | `tsc && vite build` (full typecheck + bundle) |
22+
| `bun run build:sidecar` | Build the `cork` CLI and stage it as a Tauri sidecar (`src-tauri/binaries/cork-cli-<triple>`). Run automatically by `tauri dev` / `tauri build` via the before-commands. |
23+
| `bun run tauri` | Tauri CLI passthrough (e.g. `bun run tauri dev`, `bun run tauri build`) |
24+
| `bun run format` | `oxlint --fix && oxfmt` (lint fix + format) |
25+
| `bun run lint` | `oxlint` (lint check only) |
26+
| `bun run lint:fix` | `oxlint --fix` (lint with autofix) |
27+
| `bun run fmt` | `oxfmt` (format only) |
28+
| `bun run fmt:check` | `oxfmt --check` (check formatting) |
29+
| `bun run preview` | `vite preview` (serve production build) |
2830

2931
## Pre-commit
3032

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"prepare": "husky",
88
"dev": "vite",
99
"build": "tsc && vite build",
10+
"build:sidecar": "bun run ./scripts/build-sidecar.ts",
1011
"preview": "vite preview",
1112
"tauri": "tauri",
1213
"format": "oxlint --fix && oxfmt",

‎scripts/build-cask.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ cask "cork" do
3333
3434
app "Cork.app"
3535
36+
# \`cork\` CLI を PATH に公開する。Cork.app に同梱した sidecar バイナリ
37+
# (Contents/MacOS/cork-cli) を \`cork\` という名前でシンボリックリンクする。
38+
binary "#{appdir}/Cork.app/Contents/MacOS/cork-cli", target: "cork"
39+
3640
preflight do
3741
# ad-hoc 署名で designated requirement を identifier のみに設定
3842
# これにより、ビルドが変わっても TCC が同じアプリとして認識する

‎scripts/build-sidecar.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { execFileSync } from "node:child_process";
2+
import { chmodSync, copyFileSync, mkdirSync } from "node:fs";
3+
import { dirname, join } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
// Builds the `cork` CLI and stages it as a Tauri sidecar so `tauri build` / `tauri dev`
7+
// embed it into `Cork.app/Contents/MacOS/cork-cli`. Tauri requires the binary to carry a
8+
// `-<target-triple>` suffix (see tauri.conf.json `bundle.externalBin`), which it strips when
9+
// bundling. The Homebrew Cask then symlinks the embedded binary onto PATH as `cork`.
10+
11+
const scriptDir = dirname(fileURLToPath(import.meta.url));
12+
const srcTauri = join(scriptDir, "..", "src-tauri");
13+
14+
function hostTargetTriple(): string {
15+
const out = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
16+
const line = out.split("\n").find((l) => l.startsWith("host:"));
17+
if (!line) {
18+
throw new Error("could not determine host target triple from `rustc -vV`");
19+
}
20+
return line.slice("host:".length).trim();
21+
}
22+
23+
function main() {
24+
console.log("Building cork CLI (release)...");
25+
execFileSync("cargo", ["build", "--release", "-p", "cork-cli"], {
26+
cwd: srcTauri,
27+
stdio: "inherit",
28+
});
29+
30+
const triple = hostTargetTriple();
31+
const source = join(srcTauri, "target", "release", "cork-cli");
32+
const binariesDir = join(srcTauri, "binaries");
33+
const dest = join(binariesDir, `cork-cli-${triple}`);
34+
35+
mkdirSync(binariesDir, { recursive: true });
36+
copyFileSync(source, dest);
37+
chmodSync(dest, 0o755);
38+
39+
console.log(`Sidecar staged: ${dest}`);
40+
}
41+
42+
main();

‎src-tauri/.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55
# Generated by Tauri
66
# will have schema files for capabilities auto-completion
77
/gen/schemas
8+
9+
# Tauri sidecar binaries staged by scripts/build-sidecar.ts (target-triple suffixed)
10+
/binaries/

‎src-tauri/AGENTS.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Rust backend (`src-tauri/`)
22

3+
## Cargo workspace
4+
5+
`src-tauri/Cargo.toml` is both the **app package** (`cork`) and the **workspace root** (`resolver = "2"`). Members:
6+
7+
- `cork` (this dir) — the Tauri desktop app. `main.rs` → `cork_lib::run()`.
8+
- `cork-cli` (`cli/`) — the `cork` command-line tool. A deliberately lean crate (depends only on `clap`) so it never drags Tauri / axum / rmcp / objc2 into its compile-and-link. Shared logic, if it ever appears, should be factored into a third `core` member rather than pulling `cork_lib` (which is Tauri-bound) into the CLI.
9+
10+
Build them separately: `tauri build` builds only the `cork` package; the CLI is built with `cargo build -p cork-cli` (wrapped by `bun run build:sidecar`). Both share `src-tauri/target/`. The app executable inside the bundle is `Contents/MacOS/cork` (named after the Cargo package, lowercase), which is why the CLI binary is named `cork-cli` — `cork` would collide on the case-insensitive APFS.
11+
12+
## CLI distribution
13+
14+
The `cork` CLI ships **inside** `Cork.app` and is exposed on `PATH` by Homebrew — installing the app gives you the command, with nothing extra to download.
15+
16+
1. **Build + stage** — `scripts/build-sidecar.ts` (`bun run build:sidecar`) runs `cargo build --release -p cork-cli`, then copies the binary to `src-tauri/binaries/cork-cli-<host-target-triple>` (the suffix Tauri's `externalBin` requires; the dir is gitignored). It's wired into `tauri.conf.json` → `build.beforeDevCommand` / `beforeBuildCommand`, so `tauri dev` and `tauri build` stage it with no extra step (and dev won't fail on a missing sidecar).
17+
2. **Embed** — `tauri.conf.json` → `bundle.externalBin: ["binaries/cork-cli"]` tells Tauri to strip the triple suffix and embed the binary as `Cork.app/Contents/MacOS/cork-cli`.
18+
3. **Expose on PATH** — `scripts/build-cask.ts` emits a Homebrew `binary "#{appdir}/Cork.app/Contents/MacOS/cork-cli", target: "cork"` stanza, symlinking the embedded binary to `$(brew --prefix)/bin/cork`. Invoked as `cork`, argv[0] makes `clap` print `cork` in usage; `--version` reads from the `name = "cork"` attribute. The cask's existing ad-hoc `codesign --deep` preflight re-signs the embedded CLI too, so it runs from the terminal without Gatekeeper complaints.
19+
20+
The CLI version is **not** maintained separately: `cli/build.rs` reads the repo-root `package.json` `version` (the single source release-please bumps) and injects it as `CORK_VERSION` at compile time. DMG-only installs (drag-to-Applications, no Homebrew) don't get the `cork` symlink — a future `cork`-self-install subcommand can cover that.
21+
322
## Layout
423

524
`main.rs` is the entry point and calls `cork_lib::run()`. `lib.rs` only declares modules and wires plugins, state, `#[tauri::command]`s, the `on_window_event` cleanup hook, and the macOS `RunEvent::Reopen` handler in `run()` — domain logic lives in the per-module files below.

‎src-tauri/Cargo.lock‎

Lines changed: 122 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[workspace]
2+
members = ["cli"]
3+
resolver = "2"
4+
15
[package]
26
name = "cork"
37
description = "A Tauri App"

‎src-tauri/cli/Cargo.toml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "cork-cli"
3+
version = "0.0.0"
4+
description = "Command-line interface for Cork"
5+
edition = "2021"
6+
7+
# The user-facing command is `cork`; the binary is named `cork-cli` to avoid
8+
# colliding with the Tauri app executable (also `cork`) inside the bundle's
9+
# `Contents/MacOS/` directory. The Homebrew Cask symlinks it onto PATH as `cork`.
10+
[[bin]]
11+
name = "cork-cli"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
clap = { version = "=4.6.1", features = ["derive"] }
16+
17+
[build-dependencies]
18+
serde_json = "=1.0.150"

0 commit comments

Comments
 (0)