clabox 0.0.2 → 0.1.1

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 (52) hide show
  1. package/README.md +30 -0
  2. package/clabox.config.example.mjs +44 -0
  3. package/docs/guideline.md +43 -10
  4. package/lib/aliases-DKGcMHHe.js +60 -0
  5. package/lib/aliases-DKGcMHHe.js.map +1 -0
  6. package/lib/aliases-DXyz-ufw.d.ts +31 -0
  7. package/lib/app-CQmESEdh.js +254 -0
  8. package/lib/app-CQmESEdh.js.map +1 -0
  9. package/lib/app-DzQ5yZfD.d.ts +32 -0
  10. package/lib/cli.js +46 -9
  11. package/lib/cli.js.map +1 -1
  12. package/lib/{config-DXTNeUhH.js → config-BQ44iVWT.js} +56 -4
  13. package/lib/config-BQ44iVWT.js.map +1 -0
  14. package/lib/config-DQWueb4a.d.ts +134 -0
  15. package/lib/ghostty-By4zmOuk.d.ts +34 -0
  16. package/lib/ghostty-DcMEZ6Ey.js +74 -0
  17. package/lib/ghostty-DcMEZ6Ey.js.map +1 -0
  18. package/lib/index.d.ts +9 -4
  19. package/lib/index.js +9 -4
  20. package/lib/init/aliases.d.ts +2 -0
  21. package/lib/init/aliases.js +2 -0
  22. package/lib/init/app.d.ts +2 -0
  23. package/lib/init/app.js +2 -0
  24. package/lib/init/ghostty.d.ts +2 -0
  25. package/lib/init/ghostty.js +2 -0
  26. package/lib/init/raycast.d.ts +2 -0
  27. package/lib/init/raycast.js +2 -0
  28. package/lib/init/scaffold.d.ts +2 -0
  29. package/lib/init/scaffold.js +2 -0
  30. package/lib/{profile-Bw6L1MiV.d.ts → profile-BeM41NXc.d.ts} +2 -2
  31. package/lib/{profile-CxqsgezL.js → profile-DM6NAgb-.js} +9 -12
  32. package/lib/profile-DM6NAgb-.js.map +1 -0
  33. package/lib/raycast-BCdO2Se1.js +35 -0
  34. package/lib/raycast-BCdO2Se1.js.map +1 -0
  35. package/lib/raycast-DM7c559f.d.ts +22 -0
  36. package/lib/{run-Dyp_hW97.js → run-CNehSQ-S.js} +20 -11
  37. package/lib/run-CNehSQ-S.js.map +1 -0
  38. package/lib/{run-BfF3Cwg7.d.ts → run-Cx8cuTh5.d.ts} +10 -3
  39. package/lib/sandbox/profile.d.ts +1 -1
  40. package/lib/sandbox/profile.js +1 -1
  41. package/lib/sandbox/run.d.ts +2 -2
  42. package/lib/sandbox/run.js +2 -2
  43. package/lib/scaffold-B7pUVGoC.js +141 -0
  44. package/lib/scaffold-B7pUVGoC.js.map +1 -0
  45. package/lib/scaffold-ByIbYAeS.d.ts +46 -0
  46. package/lib/utils/config.d.ts +2 -2
  47. package/lib/utils/config.js +2 -2
  48. package/package.json +1 -1
  49. package/lib/config-CUyriGxm.d.ts +0 -67
  50. package/lib/config-DXTNeUhH.js.map +0 -1
  51. package/lib/profile-CxqsgezL.js.map +0 -1
  52. package/lib/run-Dyp_hW97.js.map +0 -1
package/README.md CHANGED
@@ -43,6 +43,9 @@ clabox run --dangerously-skip-permissions
43
43
  # A different Claude profile
44
44
  CLAUDE_CONFIG_DIR=~/.claude_work clabox run --dangerously-skip-permissions
45
45
 
46
+ # A named box from ~/.config/clabox/configs/<name>.config.mjs
47
+ clabox -b ax-root --dangerously-skip-permissions
48
+
46
49
  # Debugging
47
50
  clabox generate # build the profile, print the .sb path
48
51
  clabox profile # just the path (no build)
@@ -86,6 +89,31 @@ export default {
86
89
  You may also export a function `(defaults) => config` for full control. `~` is
87
90
  expanded to `$HOME`.
88
91
 
92
+ ### Named boxes (`-b` / `--box`)
93
+
94
+ Keep a directory of named configs and switch between them by name from anywhere:
95
+
96
+ ```bash
97
+ # ~/.config/clabox/configs/ax-root.config.mjs
98
+ clabox -b ax-root --dangerously-skip-permissions
99
+ ```
100
+
101
+ `-b <name>` resolves `~/.config/clabox/configs/<name>.config.mjs` (falling back
102
+ to a bare `<name>.mjs`) and loads it like `--config` — so it wins over `--config`
103
+ / `CLABOX_CONFIG`. Override the dir with `CLABOX_CONFIGS_DIR`. Files named
104
+ `_*.mjs` are treated as shared partials (e.g. `_presets.mjs`), not boxes.
105
+
106
+ A box can pin its own `cwd` so it always targets one project, no matter where you
107
+ run `clabox` from:
108
+
109
+ ```js
110
+ // ~/.config/clabox/configs/ax-root.config.mjs
111
+ export default {
112
+ cwd: '~/projects/my-app', // claude runs here; this dir is the RW project dir
113
+ configDir: '~/.claude_axiomus',
114
+ };
115
+ ```
116
+
89
117
  ### Environment variables
90
118
 
91
119
  | Variable | Purpose | Default |
@@ -95,6 +123,8 @@ expanded to `$HOME`.
95
123
  | `CLABOX_BOT_NAME` / `CLABOX_BOT_EMAIL` | git identity | `claudeBOT` / `bot@example.com` |
96
124
  | `CLABOX_BOT_SSH_DIR` | bot key dir (`id_ed25519`, `config`) | `~/.ssh/claudebot` |
97
125
  | `CLABOX_CONFIG` | path to the JS config file (the `--config` flag overrides it) | — |
126
+ | `CLABOX_CONFIGS_DIR` | global dir of named boxes for `-b`/`--box <name>` (`<name>.config.mjs`) | `~/.config/clabox/configs` |
127
+ | `CLABOX_CWD` | working dir to run `claude` in (also the RW project dir); `~` expanded | — (the shell CWD) |
98
128
  | `CLABOX_HOOKS_DIR` | hooks dir (RO + exec inside the sandbox) | — (off) |
99
129
  | `CLABOX_DEBUG` | print diagnostics on launch | — |
100
130
  | `TMPDIR` | where the generated profile is stored | `/tmp` |
@@ -5,6 +5,11 @@
5
5
  // a function `(defaults) => config` for full control. `~` is expanded to $HOME.
6
6
 
7
7
  export default {
8
+ // Working directory to run `claude` in (also granted RW as the project dir).
9
+ // null → the shell's CWD. Set it on a named box that should always target one
10
+ // project regardless of where you launch `clabox` from. `~` is expanded.
11
+ cwd: null, // e.g. '~/projects/my-app'
12
+
8
13
  // Which Claude profile/account to use.
9
14
  configDir: '~/.claude',
10
15
 
@@ -20,6 +25,15 @@ export default {
20
25
  sshDir: '~/.ssh/claudebot',
21
26
  },
22
27
 
28
+ // Extra environment variables forced onto the sandboxed `claude` process.
29
+ // Layered after the built-in vars (so a key here wins) and on top of the
30
+ // inherited shell env. Handy for secrets like GITHUB_TOKEN. Don't hard-code
31
+ // secrets in a config committed to the repo — read them from process.env, or
32
+ // keep this file in ~/.config/clabox/config.mjs (outside the project).
33
+ env: {
34
+ // GITHUB_TOKEN: process.env.MY_GH_TOKEN ?? '',
35
+ },
36
+
23
37
  // Outbound network (set false to cut it off entirely).
24
38
  network: true,
25
39
 
@@ -43,4 +57,34 @@ export default {
43
57
  // Dotfile config dirs under $HOME denied entirely (.config/git is re-allowed
44
58
  // read-only regardless, so git keeps working).
45
59
  denyDotConfigs: ['aws', 'gnupg', 'kube', 'docker', 'config'],
60
+
61
+ // Opt-in: turn this box into a standalone Ghostty app. With `app` present,
62
+ // `clabox init` writes a Ghostty config (with a `command` that runs
63
+ // `clabox -b <box>`), a Raycast command (`<dir>/raycast/<name>.sh` → opens the
64
+ // app), and clones Ghostty.app into <appsDir>/<name>.app. Omit `app` entirely
65
+ // on boxes that should only get a shell alias (the default).
66
+ // app: {
67
+ // name: 'AX Manager', // → ~/Applications/AX Manager.app
68
+ // title: '🐈‍⬛ AX Manager', // ghostty window title (default: name)
69
+ // emoji: '🐈‍⬛', // Raycast icon (default: the title's emoji)
70
+ // icon: '~/icons/ax.png', // .icns or .png (.png is converted); .app icon
71
+ // macosIcon: 'retro', // ghostty built-in macos-icon
72
+ // ghostty: { // extra raw `key = value` ghostty lines
73
+ // background: '#0d1117',
74
+ // 'background-opacity': '0.92',
75
+ // },
76
+ // // bundleId: 'com.me.ax', // default: com.ghostty.custom.<box-dotted>
77
+ // },
78
+
79
+ // Machine-wide settings for the `clabox init` Ghostty-app builder. Shared by
80
+ // every `app` box — handy to set once in a shared preset. Env overrides:
81
+ // CLABOX_GHOSTTY_APP, CLABOX_APPS_DIR, CLABOX_SIGN_ID,
82
+ // CLABOX_GHOSTTY_BASE_CONFIG, CLABOX_CLABOX_BIN.
83
+ appBuilder: {
84
+ ghosttyApp: '/Applications/Ghostty.app', // donor app to clone
85
+ appsDir: '~/Applications', // where built apps land
86
+ signId: null, // codesign identity; null → ad-hoc (`codesign -s -`)
87
+ baseGhosttyConfig: null, // optional leading `config-file = …`
88
+ claboxBin: null, // clabox path baked into `command`; null → autodetect
89
+ },
46
90
  };
package/docs/guideline.md CHANGED
@@ -31,16 +31,26 @@ Guidelines for clabox — run Claude Code in a sandbox for super-safe YOLO mode,
31
31
 
32
32
  ```
33
33
  src/
34
- ├── index.ts # public API aggregator — re-exports config / profile / run
35
- ├── cli.ts # CLI entry (yargs): run / generate / profile → lib/cli.js (bin)
34
+ ├── index.ts # public API aggregator — re-exports config / profile / run / init
35
+ ├── cli.ts # CLI entry (yargs): run / generate / profile / init → lib/cli.js (bin)
36
36
  ├── sandbox/
37
37
  │ ├── profile.ts # pure SBPL builder: buildProfile, detectPackagePaths,
38
38
  │ │ # subpath/literal/regex/globalName/ipcName/reEscape helpers
39
39
  │ └── run.ts # I/O: profilePath, generateProfile, runClaude (sandbox-exec launch)
40
+ ├── init/
41
+ │ ├── aliases.ts # pure: aliasName, buildIndex, buildWrapper, buildAliasFiles
42
+ │ ├── ghostty.ts # pure: buildGhosttyConfig, buildCommand, buildLauncherSource,
43
+ │ │ # appBundlePath, bundleId
44
+ │ ├── raycast.ts # pure: buildRaycastCommand, raycastIcon
45
+ │ ├── app.ts # I/O (macOS): buildApp, canBuildApps (clone Ghostty.app + sign)
46
+ │ └── scaffold.ts # I/O: discoverProfiles, runInit (scan configs → scripts + apps + raycast)
40
47
  └── utils/
41
- └── config.ts # defaultConfig, expandHome, mergeConfig, findConfigFile, loadConfig
48
+ └── config.ts # defaultConfig, expandHome, mergeConfig, findConfigFile, loadConfig,
49
+ # configsDir/resolveBox/listBoxes (named-box resolution)
42
50
  tests/
43
- └── profile.test.ts # bun:test — unit (profile text) + functional (real sandbox-exec)
51
+ ├── profile.test.ts # bun:test — unit (profile text) + functional (real sandbox-exec)
52
+ ├── init.test.ts # bun:test — alias/ghostty text + scaffold & app boxes (tmp-dir fs)
53
+ └── box.test.ts # bun:test — named-box resolution (tmp-dir fs)
44
54
  lib/ # build output (tsdown) — gitignored
45
55
  docs/
46
56
  ├── guideline.md # this file
@@ -63,6 +73,9 @@ bun run dev # tsdown --watch
63
73
  # Run
64
74
  bun run cli # bun run src/cli.ts (pass args after `--`)
65
75
  bun run generate # bun run src/cli.ts generate (build a profile, print its path)
76
+ bun run cli -- init # aliases per box + build Ghostty apps for `app` boxes
77
+ bun run cli -- init --no-apps # aliases only (skip the Ghostty-app build)
78
+ bun run cli -- init --app "AX Manager" # (re)build just one app box
66
79
 
67
80
  # Testing
68
81
  bun run test # lint + types + unit + size (the full gate)
@@ -95,24 +108,37 @@ clabox run → loadConfig() → buildProfile() → <TMPDIR>/…sb
95
108
  ### `utils/config.ts`
96
109
  Builds the effective config in three layers (later wins): `defaultConfig` → env vars → a JS config file. `findConfigFile(explicit?)` looks up the explicit path (the `--config` CLI flag, falling back to `CLABOX_CONFIG`) → `./clabox.config.mjs` / `./clabox.config.js` → `~/.config/clabox/config.mjs`; the `--config` flag wins over `CLABOX_CONFIG`. `loadConfig(explicit?)` forwards that path, dynamically imports the file and accepts either a plain object (merged via `mergeConfig`, a deep merge over the defaults) or a function `(defaults) => config`. `expandHome()` expands a leading `~`. Exports the `Config`/`BotConfig`/`PathRules`/`LoadedConfig` types.
97
110
 
111
+ **Named boxes.** `configsDir()` returns the global box dir (`~/.config/clabox/configs`, overridable via `CLABOX_CONFIGS_DIR`). `resolveBox(name, dir?)` maps a name to its config file there, preferring `<name>.config.mjs` over a bare `<name>.mjs` and throwing (with the available boxes listed) when none exists; a `_`-prefixed name is refused (it's a shared partial, not a box), so `-b` matches exactly what `listBoxes` advertises. `listBoxes(dir?)` returns the sorted, de-duplicated box names, skipping `_`-prefixed shared partials (e.g. `_presets.mjs`). The CLI's `-b`/`--box <name>` flag resolves the name and feeds the path to `loadConfig` as the explicit config (so `-b` wins over `--config`).
112
+
98
113
  ### `sandbox/profile.ts` (pure)
99
114
  Assembles the SBPL profile text from typed helpers (`subpath`, `literal`, `regex`, `globalName`, `ipcName`, `reEscape`) — no I/O beyond `fs.existsSync` for autodetection. `detectPackagePaths()` finds installed package managers (Homebrew `/opt/homebrew` or `/usr/local/Homebrew`, `~/.local`, Nix `/nix/store`) to grant read/exec. `buildProfile(config, { projectDir, detectedPaths })` returns the full profile and sanity-checks it carries `(version 1)`.
100
115
 
101
116
  ### `sandbox/run.ts` (I/O)
102
- `profilePath()` returns the deterministic `$TMPDIR/clabox-<dir>-<hash>.sb` path. `generateProfile()` requires `sandbox-exec`, builds the profile and writes it. `runClaude()` resolves the `claude` binary (config → PATH → `~/.local/bin/claude`), forces the bot git identity + hardening env (`buildEnvArgs`), sets the terminal title, and execs `sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env … claude …'`, returning the exit code.
117
+ `profilePath()` returns the deterministic `$TMPDIR/clabox-<dir>-<hash>.sb` path. `resolveProjectDir(config)` returns the effective project dir — `config.cwd` (with `~` expanded) when set, else `process.cwd()` — and is what `generateProfile`/`runClaude` default to. `generateProfile()` requires `sandbox-exec`, builds the profile and writes it. `runClaude()` resolves the project dir + `claude` binary (config → PATH → `~/.local/bin/claude`), forces the bot git identity + hardening env (`buildEnvArgs`, with `config.env` appended last so it wins), sets the terminal title, and execs `sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env … claude …'` **in the resolved project dir** (`spawnSync` `cwd`), returning the exit code.
118
+
119
+ ### `init/aliases.ts` (pure) + `init/scaffold.ts` (I/O)
120
+ `clabox init` turns a directory of box configs into ready-to-use shell commands. `aliases.ts` is pure text: `aliasName(profile)` yields `clabox-<name>`; `buildIndex()` renders the source-able `index.sh` (a `_clabox_run` helper that runs `CLABOX_CONFIGS_DIR=<configsDir> clabox -b "$1"` plus one function per box); `buildWrapper()` renders a standalone `.sh` that sources `index.sh` and calls one function; `buildAliasFiles()` returns the full file set. There is **one command per box** — yolo vs. safe is decided by the box's own preset (`claudeArgs`), not by a `-safe` alias. `scaffold.ts` does the I/O: `discoverProfiles(configsDir)` returns the sorted box names via `listBoxes` (both `<name>.mjs`/`<name>.config.mjs`, `_`-partials skipped; throws if the dir is missing or has no boxes), and `runInit({ baseDir, buildApps, only })` (async) resolves `<baseDir>/configs` + `<baseDir>/scripts` (default `<cwd>/__`), prunes its own prior artifacts (`index.sh`, `clabox-*.sh`), then writes the new ones `chmod +x`. Absolute paths are baked in so the scripts run from any cwd. It returns `{ profiles, written, apps, ghosttyConfigs, warnings, … }`.
121
+
122
+ ### `init/ghostty.ts` + `init/raycast.ts` (pure) + `init/app.ts` (I/O) — standalone Ghostty apps
123
+ A box becomes a real macOS app by carrying an `app` object (`AppConfig`: `name`, `title?`, `emoji?`, `icon?`, `macosIcon?`, `ghostty?`, `bundleId?`). When `runInit` runs with `buildApps` (the default), it `loadConfig`s each box and, for every box with an `app`, writes `<baseDir>/ghostty/<name>.config`, a `<baseDir>/raycast/<name>.sh` Raycast command, and clones a `.app`. `ghostty.ts` is pure text: `buildGhosttyConfig()` renders the config (a `command = zsh -lic 'cd <cwd> && CLABOX_CONFIGS_DIR=<dir> <claboxBin> -b <name>; exec zsh'` — a **login + interactive** zsh so the GUI-launched app inherits the user's PATH (`/etc/zprofile`→`path_helper` for Homebrew, `~/.zshrc` for fnm/nvm/volta); a bare `bash -c` gets only launchd's minimal PATH and can't find `node` for clabox's `#!/usr/bin/env node` shebang — plus `title`/`macos-icon`/extra `app.ghostty` lines and an optional leading `config-file`); `buildLauncherSource()` renders a tiny C launcher that finds `ghostty.real` next to itself and re-execs it with `--config-file=<config>` baked in; `appBundlePath()`/`bundleId()` derive the bundle path/id. `raycast.ts` renders the Raycast script command (`buildRaycastCommand()`: `@raycast.*` metadata + `open <appPath>`; `raycastIcon()` picks `app.emoji`, else the title's leading emoji, else 👻). `app.ts` is the macOS-only I/O (replaces the old `ghostty-app-builder.sh`): `canBuildApps(builder)` gates on darwin + donor app + `cc`; `buildApp()` extracts the donor's entitlements, `cp -R` clones Ghostty.app into `<appsDir>/<name>.app`, patches `Info.plist` (identity + Sparkle off), swaps the binary for the compiled launcher, installs the icon (`.icns` copy or `.png`→`sips`+`iconutil`, plus `plutil -remove CFBundleIconName` — Ghostty's plist references an icon inside its compiled `Assets.car`, which macOS prefers over the loose `.icns`, so the name must be dropped for our icon to take effect), and `codesign`s (`appBuilder.signId`, else ad-hoc `-`). Machine-wide build settings live in `config.appBuilder` (`ghosttyApp`, `appsDir`, `signId`, `baseGhosttyConfig`, `claboxBin`). Builds are best-effort: a non-buildable host or a thrown build records a `warning` and the aliases (plus the ghostty config + raycast script) are still emitted. `init` prunes its own `<baseDir>/ghostty/*.config` and `<baseDir>/raycast/*.sh` on a full run (not under `--app`, which would orphan other apps' artifacts); built `.app` bundles are **not** auto-deleted.
103
124
 
104
125
  ### `cli.ts`
105
- The yargs CLI (`scriptName('clabox')`). Commands: `run [claudeArgs..]` (default), `generate`, `profile`. `unknown-options-as-args` keeps unknown flags as positionals so they pass straight through to `claude` (e.g. `--dangerously-skip-permissions`). The one clabox-owned flag is `--config <path>` (a JS config file that overrides `CLABOX_CONFIG`), forwarded to `loadConfig()` by `run` and `generate`.
126
+ The yargs CLI (`scriptName('clabox')`). Commands: `run [claudeArgs..]` (default), `generate`, `profile`, `init`. `unknown-options-as-args` keeps unknown flags as positionals so they pass straight through to `claude` (e.g. `--dangerously-skip-permissions`). clabox-owned flags: `--config <path>` (a JS config file that overrides `CLABOX_CONFIG`) and `-b`/`--box <name>` (a named box from the global configs dir; resolved via `resolveBox` and winning over `--config`) — both forwarded to `loadConfig()` by `run` and `generate` through the `explicitConfig(argv)` helper. (`-b` deliberately avoids `-p`/`-c`/`-r`/`-d`/`-v`, which are claude's own `--print`/`--continue`/`--resume`/`--debug`/`--verbose`.) `init` takes `--dir <path>` (the base dir holding `configs/` and `scripts/`, default `./__`), `--no-apps` (skip the Ghostty-app build), and `--app <box|name>` ((re)build a single app box, by box name or `app.name`).
106
127
 
107
128
  ### What the profile allows and denies
108
129
  - **Read-only:** system dirs (`/System`, `/usr`, `/bin`, `/sbin`, `/Library/Frameworks`), Command Line Tools / Xcode, tzdata, system + user `Library/Preferences`, detected package paths.
109
- - **Read-write:** the project dir (CWD), the Claude config dir (`configDir`), `/tmp`, `/private/tmp`, `/private/var/folders/…`, `~/Library/Keychains` (OAuth refresh), plus `paths.readWrite`.
130
+ - **Read-write:** the project dir (`config.cwd` if set, else the shell CWD), the Claude config dir (`configDir`), `/tmp`, `/private/tmp`, `/private/var/folders/…`, `~/Library/Keychains` (OAuth refresh), plus `paths.readWrite`.
110
131
  - **Network:** `(allow network*)` when `network: true` (default).
111
- - **Explicit deny wins over the allows:** `denyHome` (`~/Documents`, `~/Desktop`, …), `denyDotConfigs` (`~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`, `~/.config`) with a carve-out for `~/.config/git`, and personal SSH keys `~/.ssh/id_*`, `*.pem`, `*.key`. Only the bot key subdir (`bot.sshDir`) is readable.
132
+ - **Two deny tiers** (Seatbelt evaluates rules in order the *last* match wins so placement is deliberate):
133
+ - **Soft privacy deny (overridable):** `denyHome` (`~/Documents`, `~/Desktop`, …) and `paths.deny`, emitted *before* the extra `readOnly`/`readWrite` and the project dir. An explicit grant therefore overrides it — handy for a project that lives under `~/Documents`, or a debug box that wants `readOnly: ['/']` to roam the disk.
134
+ - **Hard secret deny (always wins):** `denyDotConfigs` (`~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`, `~/.config`) and personal SSH keys `~/.ssh/id_*`, `*.pem`, `*.key`, emitted as the **very last file rule** — after every allow, the extra paths and the project dir. No `readOnly`/`readWrite` grant can re-expose these. Only the bot key subdir (`bot.sshDir`, not matched by the patterns) stays readable. (The `~/.config/git` RO carve-out is shadowed by the hard deny on `~/.config`; git still reads `~/.gitconfig` outside `~/.config`.)
112
135
 
113
136
  ### Git/ssh bot identity
114
137
  `ulimit -u <ulimitProcs>` (fork-bomb guard, `0` to disable); `GIT_AUTHOR_*` / `GIT_COMMITTER_*` from `bot.name`/`bot.email`; if `bot.sshDir/id_ed25519` exists, `GIT_SSH_COMMAND` is pinned to it (`IdentitiesOnly=yes`, `IdentityAgent=none`); gpg signing disabled; `NPM_CONFIG_USERCONFIG=/dev/null`; `DISABLE_AUTOUPDATER=1`.
115
138
 
139
+ ### Passing environment variables
140
+ `sandbox-exec` restricts files and network, not the environment, and `runClaude` spawns through `/bin/sh` without an `env` option — so the sandboxed `claude` **inherits the parent shell env** (e.g. `export GITHUB_TOKEN=… && clabox`). For a declarative alternative, `config.env` (a `KEY=VALUE` map) is appended **last** in `buildEnvArgs`, so it layers over both the inherited env and the built-in hardening vars and a colliding key wins. Don't hard-code secrets in a repo-committed `clabox.config.mjs` (the project dir is mounted RW and readable from inside): read them from `process.env`, or keep the config in `~/.config/clabox/config.mjs`. Anything in the env is readable by `claude` and, with `network: true`, exfiltratable.
141
+
116
142
  ## Lint
117
143
 
118
144
  Biome (`biome.json`), scoped to `src/**/*.ts` + `tests/**/*.ts`:
@@ -136,10 +162,10 @@ Both workflows run on `macos-latest` so the functional tests can exercise the re
136
162
  ## Package Exports
137
163
 
138
164
  ```typescript
139
- import { loadConfig, buildProfile, runClaude } from 'clabox'; // lib/index.js
165
+ import { loadConfig, buildProfile, runClaude, runInit } from 'clabox'; // lib/index.js
140
166
  import { loadConfig, defaultConfig, mergeConfig } from 'clabox/config'; // lib/utils/config.js
141
167
  import { buildProfile, detectPackagePaths } from 'clabox/profile'; // lib/sandbox/profile.js
142
- import { generateProfile, profilePath, runClaude } from 'clabox/run'; // lib/sandbox/run.js
168
+ import { generateProfile, profilePath, resolveProjectDir, runClaude } from 'clabox/run'; // lib/sandbox/run.js
143
169
  // CLI entry: clabox/cli (lib/cli.js) — also the `clabox` bin
144
170
  ```
145
171
 
@@ -148,11 +174,18 @@ import { generateProfile, profilePath, runClaude } from 'clabox/run'; // lib/san
148
174
  | Variable | Purpose | Default |
149
175
  |---|---|---|
150
176
  | `CLAUDE_CONFIG_DIR` | Claude config/profile dir (multi-account); passed through to `claude` | `~/.claude` |
177
+ | `CLABOX_CWD` | working dir to run `claude` in (also the RW project dir); `~` expanded | — (the shell CWD) |
151
178
  | `CLABOX_CLAUDE_BIN` | path to the `claude` binary | PATH, then `~/.local/bin/claude` |
152
179
  | `CLABOX_BOT_NAME` / `CLABOX_BOT_EMAIL` | git identity inside the sandbox | `claudeBOT` / `bot@example.com` |
153
180
  | `CLABOX_BOT_SSH_DIR` | bot key dir (`id_ed25519`, `config`) | `~/.ssh/claudebot` |
154
181
  | `CLABOX_CONFIG` | path to the JS config file (the `--config` flag overrides it) | — |
182
+ | `CLABOX_CONFIGS_DIR` | global dir of named boxes for `-b`/`--box <name>` (`<name>.config.mjs`) | `~/.config/clabox/configs` |
155
183
  | `CLABOX_HOOKS_DIR` | hooks dir (RO + exec inside the sandbox) | — (off) |
184
+ | `CLABOX_GHOSTTY_APP` | donor app cloned by `init` for `app` boxes (`config.appBuilder.ghosttyApp`) | `/Applications/Ghostty.app` |
185
+ | `CLABOX_APPS_DIR` | where `init` writes built `.app`s (`config.appBuilder.appsDir`) | `~/Applications` |
186
+ | `CLABOX_SIGN_ID` | codesign identity for built apps (`config.appBuilder.signId`); unset → ad-hoc | — |
187
+ | `CLABOX_GHOSTTY_BASE_CONFIG` | leading `config-file = …` in generated Ghostty configs | — |
188
+ | `CLABOX_CLABOX_BIN` | `clabox` path baked into the Ghostty `command` (`config.appBuilder.claboxBin`) | autodetect |
156
189
  | `CLABOX_DEBUG` | print profile/config/dir diagnostics on launch | — |
157
190
  | `TMPDIR` | where the generated profile is stored | `/tmp` |
158
191
 
@@ -0,0 +1,60 @@
1
+ import path from "node:path";
2
+ //#region src/init/aliases.ts
3
+ /** The `clabox-<name>` command name for a box. */
4
+ function aliasName(profile) {
5
+ return `clabox-${profile}`;
6
+ }
7
+ /** Build the source-able `index.sh` defining one function per box. */
8
+ function buildIndex(profiles, { configsDir, scriptsDir }) {
9
+ const indexPath = path.join(scriptsDir, "index.sh");
10
+ const usage = profiles.map((p) => `# ${aliasName(p)}`);
11
+ const defs = profiles.map((p) => `${aliasName(p)}() { _clabox_run ${p} "$@"; }`);
12
+ return `${[
13
+ "#!/usr/bin/env bash",
14
+ "# Generated by `clabox init` — do not edit; rerun it after changing the configs dir.",
15
+ "#",
16
+ "# Source this from ~/.zshrc or ~/.bashrc:",
17
+ `# source ${indexPath}`,
18
+ "#",
19
+ "# Commands (run from any cwd). yolo vs. safe is set by each box's preset:",
20
+ ...usage,
21
+ "",
22
+ "_clabox_run() {",
23
+ ` CLABOX_CONFIGS_DIR="${configsDir}" clabox -b "$1" "\${@:2}"`,
24
+ "}",
25
+ "",
26
+ ...defs
27
+ ].join("\n")}\n`;
28
+ }
29
+ /** Build a standalone wrapper script that sources `index.sh` and calls one fn. */
30
+ function buildWrapper(fnName, indexPath) {
31
+ return `${[
32
+ "#!/usr/bin/env bash",
33
+ "# Generated by `clabox init`.",
34
+ "set -euo pipefail",
35
+ `source "${indexPath}"`,
36
+ `${fnName} "$@"`
37
+ ].join("\n")}\n`;
38
+ }
39
+ /** All files `clabox init` writes: the `index.sh` plus a wrapper per box. */
40
+ function buildAliasFiles(profiles, paths) {
41
+ const indexPath = path.join(paths.scriptsDir, "index.sh");
42
+ const files = [{
43
+ path: indexPath,
44
+ content: buildIndex(profiles, paths),
45
+ executable: true
46
+ }];
47
+ for (const p of profiles) {
48
+ const fn = aliasName(p);
49
+ files.push({
50
+ path: path.join(paths.scriptsDir, `${fn}.sh`),
51
+ content: buildWrapper(fn, indexPath),
52
+ executable: true
53
+ });
54
+ }
55
+ return files;
56
+ }
57
+ //#endregion
58
+ export { buildWrapper as i, buildAliasFiles as n, buildIndex as r, aliasName as t };
59
+
60
+ //# sourceMappingURL=aliases-DKGcMHHe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aliases-DKGcMHHe.js","names":[],"sources":["../src/init/aliases.ts"],"sourcesContent":["// Pure builder for the shell aliases emitted by `clabox init`.\n//\n// For each box config (`<name>.mjs` / `<name>.config.mjs`) it produces one\n// command `clabox-<name>` that runs that box. Whether it runs yolo or asks for\n// confirmation is decided by the box's own preset (its `claudeArgs`), not here.\n// Plus an `index.sh` defining them all as shell functions to `source`.\n\nimport path from 'node:path';\n\n/** A file to be written by `clabox init`. */\nexport interface InitFile {\n /** Absolute path to write. */\n path: string;\n /** File contents. */\n content: string;\n /** Whether to `chmod +x` after writing. */\n executable: boolean;\n}\n\n/** Absolute locations for the generated artifacts. */\nexport interface AliasPaths {\n /** Dir holding the box config files. */\n configsDir: string;\n /** Dir to emit `index.sh` and the per-box wrappers into. */\n scriptsDir: string;\n}\n\n/** The `clabox-<name>` command name for a box. */\nexport function aliasName(profile: string): string {\n return `clabox-${profile}`;\n}\n\n/** Build the source-able `index.sh` defining one function per box. */\nexport function buildIndex(profiles: string[], { configsDir, scriptsDir }: AliasPaths): string {\n const indexPath = path.join(scriptsDir, 'index.sh');\n const usage = profiles.map((p) => `# ${aliasName(p)}`);\n const defs = profiles.map((p) => `${aliasName(p)}() { _clabox_run ${p} \"$@\"; }`);\n return `${[\n '#!/usr/bin/env bash',\n '# Generated by `clabox init` — do not edit; rerun it after changing the configs dir.',\n '#',\n '# Source this from ~/.zshrc or ~/.bashrc:',\n `# source ${indexPath}`,\n '#',\n \"# Commands (run from any cwd). yolo vs. safe is set by each box's preset:\",\n ...usage,\n '',\n '_clabox_run() {',\n // Point `-b` at this dir so resolveBox picks the right .mjs/.config.mjs file.\n ` CLABOX_CONFIGS_DIR=\"${configsDir}\" clabox -b \"$1\" \"\\${@:2}\"`,\n '}',\n '',\n ...defs,\n ].join('\\n')}\\n`;\n}\n\n/** Build a standalone wrapper script that sources `index.sh` and calls one fn. */\nexport function buildWrapper(fnName: string, indexPath: string): string {\n return `${[\n '#!/usr/bin/env bash',\n '# Generated by `clabox init`.',\n 'set -euo pipefail',\n `source \"${indexPath}\"`,\n `${fnName} \"$@\"`,\n ].join('\\n')}\\n`;\n}\n\n/** All files `clabox init` writes: the `index.sh` plus a wrapper per box. */\nexport function buildAliasFiles(profiles: string[], paths: AliasPaths): InitFile[] {\n const indexPath = path.join(paths.scriptsDir, 'index.sh');\n const files: InitFile[] = [\n { path: indexPath, content: buildIndex(profiles, paths), executable: true },\n ];\n for (const p of profiles) {\n const fn = aliasName(p);\n files.push({\n path: path.join(paths.scriptsDir, `${fn}.sh`),\n content: buildWrapper(fn, indexPath),\n executable: true,\n });\n }\n return files;\n}\n"],"mappings":";;;AA4BA,SAAgB,UAAU,SAAyB;CACjD,OAAO,UAAU;AACnB;;AAGA,SAAgB,WAAW,UAAoB,EAAE,YAAY,cAAkC;CAC7F,MAAM,YAAY,KAAK,KAAK,YAAY,UAAU;CAClD,MAAM,QAAQ,SAAS,KAAK,MAAM,OAAO,UAAU,CAAC,GAAG;CACvD,MAAM,OAAO,SAAS,KAAK,MAAM,GAAG,UAAU,CAAC,EAAE,mBAAmB,EAAE,SAAS;CAC/E,OAAO,GAAG;EACR;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACA,GAAG;EACH;EACA;EAEA,yBAAyB,WAAW;EACpC;EACA;EACA,GAAG;CACL,CAAC,CAAC,KAAK,IAAI,EAAE;AACf;;AAGA,SAAgB,aAAa,QAAgB,WAA2B;CACtE,OAAO,GAAG;EACR;EACA;EACA;EACA,WAAW,UAAU;EACrB,GAAG,OAAO;CACZ,CAAC,CAAC,KAAK,IAAI,EAAE;AACf;;AAGA,SAAgB,gBAAgB,UAAoB,OAA+B;CACjF,MAAM,YAAY,KAAK,KAAK,MAAM,YAAY,UAAU;CACxD,MAAM,QAAoB,CACxB;EAAE,MAAM;EAAW,SAAS,WAAW,UAAU,KAAK;EAAG,YAAY;CAAK,CAC5E;CACA,KAAK,MAAM,KAAK,UAAU;EACxB,MAAM,KAAK,UAAU,CAAC;EACtB,MAAM,KAAK;GACT,MAAM,KAAK,KAAK,MAAM,YAAY,GAAG,GAAG,IAAI;GAC5C,SAAS,aAAa,IAAI,SAAS;GACnC,YAAY;EACd,CAAC;CACH;CACA,OAAO;AACT"}
@@ -0,0 +1,31 @@
1
+ //#region src/init/aliases.d.ts
2
+ /** A file to be written by `clabox init`. */
3
+ interface InitFile {
4
+ /** Absolute path to write. */
5
+ path: string;
6
+ /** File contents. */
7
+ content: string;
8
+ /** Whether to `chmod +x` after writing. */
9
+ executable: boolean;
10
+ }
11
+ /** Absolute locations for the generated artifacts. */
12
+ interface AliasPaths {
13
+ /** Dir holding the box config files. */
14
+ configsDir: string;
15
+ /** Dir to emit `index.sh` and the per-box wrappers into. */
16
+ scriptsDir: string;
17
+ }
18
+ /** The `clabox-<name>` command name for a box. */
19
+ declare function aliasName(profile: string): string;
20
+ /** Build the source-able `index.sh` defining one function per box. */
21
+ declare function buildIndex(profiles: string[], {
22
+ configsDir,
23
+ scriptsDir
24
+ }: AliasPaths): string;
25
+ /** Build a standalone wrapper script that sources `index.sh` and calls one fn. */
26
+ declare function buildWrapper(fnName: string, indexPath: string): string;
27
+ /** All files `clabox init` writes: the `index.sh` plus a wrapper per box. */
28
+ declare function buildAliasFiles(profiles: string[], paths: AliasPaths): InitFile[];
29
+ //#endregion
30
+ export { buildIndex as a, buildAliasFiles as i, InitFile as n, buildWrapper as o, aliasName as r, AliasPaths as t };
31
+ //# sourceMappingURL=aliases-DXyz-ufw.d.ts.map
@@ -0,0 +1,254 @@
1
+ import { i as expandHome } from "./config-BQ44iVWT.js";
2
+ import { a as bundleId, i as buildLauncherSource, t as appBundlePath } from "./ghostty-DcMEZ6Ey.js";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ //#region src/init/app.ts
8
+ function run(cmd, args) {
9
+ execFileSync(cmd, args, { stdio: [
10
+ "ignore",
11
+ "ignore",
12
+ "pipe"
13
+ ] });
14
+ }
15
+ function has(bin) {
16
+ try {
17
+ execFileSync("command", ["-v", bin], {
18
+ shell: "/bin/sh",
19
+ stdio: "ignore"
20
+ });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ /** True when the host can build apps (macOS with the donor app + a C compiler). */
27
+ function canBuildApps(builder) {
28
+ if (process.platform !== "darwin") return {
29
+ ok: false,
30
+ reason: "not macOS"
31
+ };
32
+ if (!fs.existsSync(expandHome(builder.ghosttyApp))) return {
33
+ ok: false,
34
+ reason: `Ghostty not found at ${builder.ghosttyApp}`
35
+ };
36
+ if (!has("cc")) return {
37
+ ok: false,
38
+ reason: "no C compiler (cc) — install Xcode CLT"
39
+ };
40
+ return { ok: true };
41
+ }
42
+ /** Extract the donor app's entitlements to a tmp file, or null if it has none. */
43
+ function extractEntitlements(ghosttyApp, tmpDir) {
44
+ let xml;
45
+ try {
46
+ xml = execFileSync("codesign", [
47
+ "-d",
48
+ "--entitlements",
49
+ "-",
50
+ "--xml",
51
+ ghosttyApp
52
+ ], {
53
+ encoding: "utf8",
54
+ stdio: [
55
+ "ignore",
56
+ "pipe",
57
+ "ignore"
58
+ ]
59
+ });
60
+ } catch {
61
+ return null;
62
+ }
63
+ const start = xml.indexOf("<?xml");
64
+ if (start < 0) return null;
65
+ const file = path.join(tmpDir, "entitlements.xml");
66
+ fs.writeFileSync(file, xml.slice(start));
67
+ return file;
68
+ }
69
+ /** Name of the icon resource referenced by the bundle (default Ghostty.icns). */
70
+ function iconResourceName(plist) {
71
+ try {
72
+ const name = execFileSync("plutil", [
73
+ "-extract",
74
+ "CFBundleIconFile",
75
+ "raw",
76
+ plist
77
+ ], {
78
+ encoding: "utf8",
79
+ stdio: [
80
+ "ignore",
81
+ "pipe",
82
+ "ignore"
83
+ ]
84
+ }).trim();
85
+ return name.endsWith(".icns") ? name : `${name}.icns`;
86
+ } catch {
87
+ return "Ghostty.icns";
88
+ }
89
+ }
90
+ /** Convert a PNG into a multi-resolution .icns at `out`. */
91
+ function pngToIcns(png, out, tmpDir) {
92
+ const iconset = path.join(tmpDir, "icon.iconset");
93
+ fs.mkdirSync(iconset, { recursive: true });
94
+ for (const size of [
95
+ 16,
96
+ 32,
97
+ 128,
98
+ 256,
99
+ 512
100
+ ]) {
101
+ run("sips", [
102
+ "-z",
103
+ `${size}`,
104
+ `${size}`,
105
+ png,
106
+ "--out",
107
+ path.join(iconset, `icon_${size}x${size}.png`)
108
+ ]);
109
+ const d = size * 2;
110
+ run("sips", [
111
+ "-z",
112
+ `${d}`,
113
+ `${d}`,
114
+ png,
115
+ "--out",
116
+ path.join(iconset, `icon_${size}x${size}@2x.png`)
117
+ ]);
118
+ }
119
+ run("iconutil", [
120
+ "-c",
121
+ "icns",
122
+ iconset,
123
+ "-o",
124
+ out
125
+ ]);
126
+ }
127
+ /** Install the box icon into the cloned bundle, if `app.icon` is set. */
128
+ function installIcon(app, appPath, tmpDir) {
129
+ if (!app.icon) return;
130
+ const icon = expandHome(app.icon);
131
+ if (!fs.existsSync(icon)) throw new Error(`icon not found: ${app.icon}`);
132
+ const plist = path.join(appPath, "Contents", "Info.plist");
133
+ const dest = path.join(appPath, "Contents", "Resources", iconResourceName(plist));
134
+ if (icon.endsWith(".icns")) fs.copyFileSync(icon, dest);
135
+ else if (icon.endsWith(".png")) pngToIcns(icon, dest, tmpDir);
136
+ else throw new Error(`unsupported icon type (need .icns/.png): ${app.icon}`);
137
+ try {
138
+ run("plutil", [
139
+ "-remove",
140
+ "CFBundleIconName",
141
+ plist
142
+ ]);
143
+ } catch {}
144
+ }
145
+ /**
146
+ * Build the standalone Ghostty app for a box. Throws on any failure (the caller
147
+ * decides whether to abort or carry on with the other boxes).
148
+ */
149
+ function buildApp(opts) {
150
+ const { app, builder, boxName, configPath } = opts;
151
+ const check = canBuildApps(builder);
152
+ if (!check.ok) throw new Error(`cannot build app: ${check.reason}`);
153
+ const ghosttyApp = expandHome(builder.ghosttyApp);
154
+ const appsDir = expandHome(builder.appsDir);
155
+ const appPath = appBundlePath(appsDir, app);
156
+ const plist = path.join(appPath, "Contents", "Info.plist");
157
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clabox-app-"));
158
+ try {
159
+ const entitlements = extractEntitlements(ghosttyApp, tmpDir);
160
+ fs.mkdirSync(appsDir, { recursive: true });
161
+ fs.rmSync(appPath, {
162
+ recursive: true,
163
+ force: true
164
+ });
165
+ run("cp", [
166
+ "-R",
167
+ ghosttyApp,
168
+ appPath
169
+ ]);
170
+ run("plutil", [
171
+ "-replace",
172
+ "CFBundleIdentifier",
173
+ "-string",
174
+ bundleId(boxName, app),
175
+ plist
176
+ ]);
177
+ run("plutil", [
178
+ "-replace",
179
+ "CFBundleName",
180
+ "-string",
181
+ app.name,
182
+ plist
183
+ ]);
184
+ run("plutil", [
185
+ "-replace",
186
+ "CFBundleDisplayName",
187
+ "-string",
188
+ app.name,
189
+ plist
190
+ ]);
191
+ run("plutil", [
192
+ "-replace",
193
+ "CFBundleExecutable",
194
+ "-string",
195
+ "ghostty",
196
+ plist
197
+ ]);
198
+ run("plutil", [
199
+ "-replace",
200
+ "SUEnableAutomaticChecks",
201
+ "-bool",
202
+ "NO",
203
+ plist
204
+ ]);
205
+ try {
206
+ run("plutil", [
207
+ "-replace",
208
+ "SUFeedURL",
209
+ "-string",
210
+ "",
211
+ plist
212
+ ]);
213
+ } catch {}
214
+ const bin = path.join(appPath, "Contents", "MacOS", "ghostty");
215
+ fs.renameSync(bin, `${bin}.real`);
216
+ const src = path.join(tmpDir, "launcher.c");
217
+ fs.writeFileSync(src, buildLauncherSource(configPath));
218
+ run("cc", [
219
+ "-o",
220
+ bin,
221
+ src
222
+ ]);
223
+ installIcon(app, appPath, tmpDir);
224
+ const signId = builder.signId;
225
+ const entArgs = entitlements ? ["--entitlements", entitlements] : [];
226
+ const idArgs = signId ? ["--sign", signId] : ["--sign", "-"];
227
+ run("codesign", [
228
+ "--force",
229
+ ...idArgs,
230
+ ...entArgs,
231
+ `${bin}.real`
232
+ ]);
233
+ run("codesign", [
234
+ "--force",
235
+ "--deep",
236
+ ...idArgs,
237
+ ...entArgs,
238
+ appPath
239
+ ]);
240
+ return {
241
+ appPath,
242
+ signed: signId ? "identity" : "adhoc"
243
+ };
244
+ } finally {
245
+ fs.rmSync(tmpDir, {
246
+ recursive: true,
247
+ force: true
248
+ });
249
+ }
250
+ }
251
+ //#endregion
252
+ export { canBuildApps as n, installIcon as r, buildApp as t };
253
+
254
+ //# sourceMappingURL=app-CQmESEdh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-CQmESEdh.js","names":[],"sources":["../src/init/app.ts"],"sourcesContent":["// I/O for the `clabox init` Ghostty-app builder (macOS-only).\n//\n// Clones Ghostty.app into `<appsDir>/<name>.app`, swaps the binary for a tiny\n// compiled launcher that bakes in `--config-file=<config>`, sets the icon,\n// disables Sparkle, and re-signs. Mirrors the old ghostty-app-builder.sh. The\n// pure text builders live in init/ghostty.ts.\n\nimport { execFileSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { type AppBuilderConfig, type AppConfig, expandHome } from '../utils/config.js';\nimport { appBundlePath, buildLauncherSource, bundleId } from './ghostty.js';\n\n/** Inputs for {@link buildApp}. */\nexport interface BuildAppOptions {\n /** The `-b` box name (drives the default bundle id). */\n boxName: string;\n app: AppConfig;\n builder: AppBuilderConfig;\n /** Absolute path to the already-written Ghostty config to bake in. */\n configPath: string;\n}\n\n/** Result of a successful {@link buildApp}. */\nexport interface BuildAppResult {\n appPath: string;\n signed: 'identity' | 'adhoc';\n}\n\nfunction run(cmd: string, args: string[]): void {\n execFileSync(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] });\n}\n\nfunction has(bin: string): boolean {\n try {\n execFileSync('command', ['-v', bin], { shell: '/bin/sh', stdio: 'ignore' });\n return true;\n } catch {\n return false;\n }\n}\n\n/** True when the host can build apps (macOS with the donor app + a C compiler). */\nexport function canBuildApps(builder: AppBuilderConfig): { ok: boolean; reason?: string } {\n if (process.platform !== 'darwin') return { ok: false, reason: 'not macOS' };\n if (!fs.existsSync(expandHome(builder.ghosttyApp))) {\n return { ok: false, reason: `Ghostty not found at ${builder.ghosttyApp}` };\n }\n if (!has('cc')) return { ok: false, reason: 'no C compiler (cc) — install Xcode CLT' };\n return { ok: true };\n}\n\n/** Extract the donor app's entitlements to a tmp file, or null if it has none. */\nfunction extractEntitlements(ghosttyApp: string, tmpDir: string): string | null {\n let xml: string;\n try {\n xml = execFileSync('codesign', ['-d', '--entitlements', '-', '--xml', ghosttyApp], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n } catch {\n return null;\n }\n const start = xml.indexOf('<?xml');\n if (start < 0) return null;\n const file = path.join(tmpDir, 'entitlements.xml');\n fs.writeFileSync(file, xml.slice(start));\n return file;\n}\n\n/** Name of the icon resource referenced by the bundle (default Ghostty.icns). */\nfunction iconResourceName(plist: string): string {\n try {\n const name = execFileSync('plutil', ['-extract', 'CFBundleIconFile', 'raw', plist], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return name.endsWith('.icns') ? name : `${name}.icns`;\n } catch {\n return 'Ghostty.icns';\n }\n}\n\n/** Convert a PNG into a multi-resolution .icns at `out`. */\nfunction pngToIcns(png: string, out: string, tmpDir: string): void {\n const iconset = path.join(tmpDir, 'icon.iconset');\n fs.mkdirSync(iconset, { recursive: true });\n for (const size of [16, 32, 128, 256, 512]) {\n run('sips', [\n '-z',\n `${size}`,\n `${size}`,\n png,\n '--out',\n path.join(iconset, `icon_${size}x${size}.png`),\n ]);\n const d = size * 2;\n run('sips', [\n '-z',\n `${d}`,\n `${d}`,\n png,\n '--out',\n path.join(iconset, `icon_${size}x${size}@2x.png`),\n ]);\n }\n run('iconutil', ['-c', 'icns', iconset, '-o', out]);\n}\n\n/** Install the box icon into the cloned bundle, if `app.icon` is set. */\nexport function installIcon(app: AppConfig, appPath: string, tmpDir: string): void {\n if (!app.icon) return;\n const icon = expandHome(app.icon);\n if (!fs.existsSync(icon)) throw new Error(`icon not found: ${app.icon}`);\n const plist = path.join(appPath, 'Contents', 'Info.plist');\n const dest = path.join(appPath, 'Contents', 'Resources', iconResourceName(plist));\n if (icon.endsWith('.icns')) fs.copyFileSync(icon, dest);\n else if (icon.endsWith('.png')) pngToIcns(icon, dest, tmpDir);\n else throw new Error(`unsupported icon type (need .icns/.png): ${app.icon}`);\n\n // Ghostty ships a compiled asset catalog (Assets.car) and a `CFBundleIconName`\n // pointing into it, which macOS prefers over the loose `CFBundleIconFile`\n // .icns we just replaced — so our icon would be ignored. Drop the asset-catalog\n // reference so macOS falls back to the .icns. (May be absent on other donors.)\n try {\n run('plutil', ['-remove', 'CFBundleIconName', plist]);\n } catch {\n // donor app may not define CFBundleIconName — ignore\n }\n}\n\n/**\n * Build the standalone Ghostty app for a box. Throws on any failure (the caller\n * decides whether to abort or carry on with the other boxes).\n */\nexport function buildApp(opts: BuildAppOptions): BuildAppResult {\n const { app, builder, boxName, configPath } = opts;\n const check = canBuildApps(builder);\n if (!check.ok) throw new Error(`cannot build app: ${check.reason}`);\n\n const ghosttyApp = expandHome(builder.ghosttyApp);\n const appsDir = expandHome(builder.appsDir);\n const appPath = appBundlePath(appsDir, app);\n const plist = path.join(appPath, 'Contents', 'Info.plist');\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clabox-app-'));\n\n try {\n const entitlements = extractEntitlements(ghosttyApp, tmpDir);\n\n // Full clone (cp -R keeps bundle symlinks/frameworks intact).\n fs.mkdirSync(appsDir, { recursive: true });\n fs.rmSync(appPath, { recursive: true, force: true });\n run('cp', ['-R', ghosttyApp, appPath]);\n\n // Identity.\n run('plutil', ['-replace', 'CFBundleIdentifier', '-string', bundleId(boxName, app), plist]);\n run('plutil', ['-replace', 'CFBundleName', '-string', app.name, plist]);\n run('plutil', ['-replace', 'CFBundleDisplayName', '-string', app.name, plist]);\n run('plutil', ['-replace', 'CFBundleExecutable', '-string', 'ghostty', plist]);\n\n // Disable Sparkle auto-update (would clobber the clone).\n run('plutil', ['-replace', 'SUEnableAutomaticChecks', '-bool', 'NO', plist]);\n try {\n run('plutil', ['-replace', 'SUFeedURL', '-string', '', plist]);\n } catch {\n // donor app may not define SUFeedURL — ignore\n }\n\n // Swap the binary for a launcher that prepends --config-file.\n const bin = path.join(appPath, 'Contents', 'MacOS', 'ghostty');\n fs.renameSync(bin, `${bin}.real`);\n const src = path.join(tmpDir, 'launcher.c');\n fs.writeFileSync(src, buildLauncherSource(configPath));\n run('cc', ['-o', bin, src]);\n\n installIcon(app, appPath, tmpDir);\n\n // Re-sign: inner real binary first, then the whole bundle.\n const signId = builder.signId;\n const entArgs = entitlements ? ['--entitlements', entitlements] : [];\n const idArgs = signId ? ['--sign', signId] : ['--sign', '-'];\n run('codesign', ['--force', ...idArgs, ...entArgs, `${bin}.real`]);\n run('codesign', ['--force', '--deep', ...idArgs, ...entArgs, appPath]);\n\n return { appPath, signed: signId ? 'identity' : 'adhoc' };\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { n as AppConfig, t as AppBuilderConfig } from "./config-DQWueb4a.js";
2
+
3
+ //#region src/init/app.d.ts
4
+ /** Inputs for {@link buildApp}. */
5
+ interface BuildAppOptions {
6
+ /** The `-b` box name (drives the default bundle id). */
7
+ boxName: string;
8
+ app: AppConfig;
9
+ builder: AppBuilderConfig;
10
+ /** Absolute path to the already-written Ghostty config to bake in. */
11
+ configPath: string;
12
+ }
13
+ /** Result of a successful {@link buildApp}. */
14
+ interface BuildAppResult {
15
+ appPath: string;
16
+ signed: 'identity' | 'adhoc';
17
+ }
18
+ /** True when the host can build apps (macOS with the donor app + a C compiler). */
19
+ declare function canBuildApps(builder: AppBuilderConfig): {
20
+ ok: boolean;
21
+ reason?: string;
22
+ };
23
+ /** Install the box icon into the cloned bundle, if `app.icon` is set. */
24
+ declare function installIcon(app: AppConfig, appPath: string, tmpDir: string): void;
25
+ /**
26
+ * Build the standalone Ghostty app for a box. Throws on any failure (the caller
27
+ * decides whether to abort or carry on with the other boxes).
28
+ */
29
+ declare function buildApp(opts: BuildAppOptions): BuildAppResult;
30
+ //#endregion
31
+ export { installIcon as a, canBuildApps as i, BuildAppResult as n, buildApp as r, BuildAppOptions as t };
32
+ //# sourceMappingURL=app-DzQ5yZfD.d.ts.map