@workerstack/system 0.0.0-stage → 0.3.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lovro Žagar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,320 @@
1
- # Temporary Holding Version
1
+ # @workerstack/system
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Formerly [`@lovrozagar/crossdeps`](https://www.npmjs.com/package/@lovrozagar/crossdeps). Same code and history, new name.
4
+
5
+ Every OS dependency a workerstack repo needs, from one config file. Name a built-in tool and its version, `go: "1.27.2"`, and `system install` installs it on macOS, Linux (apt, dnf, pacman) and Windows, puts it first on PATH, and `system check` verifies the version. Anything outside the catalog keeps the full custom form with your own commands.
6
+
7
+ This README is the full usage contract. Agents: read it end to end before writing a config or running the CLI.
8
+
9
+ ## Contents
10
+
11
+ - [Requirements](#requirements)
12
+ - [Quick start](#quick-start)
13
+ - [Built-in catalog](#built-in-catalog)
14
+ - [Config](#config)
15
+ - [CLI](#cli)
16
+ - [Versions](#versions)
17
+ - [Shell environment](#shell-environment)
18
+ - [Library API](#library-api)
19
+ - [Gotchas](#gotchas)
20
+
21
+ ## Requirements
22
+
23
+ | Need | Detail |
24
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | Bun | The CLI is TypeScript with `#!/usr/bin/env bun`. bun is the only supported package manager. |
26
+ | OS | macOS, Linux with apt, dnf or pacman, Windows (PowerShell). |
27
+ | CPU | Built-ins ship x64 and arm64 recipes. Custom deps run anywhere. |
28
+ | sudo | Only OS packages use it (`git`, `curl`, `chromium`, `ffmpeg`, `docker` on Linux, playwright's apt deps). Everything else installs per user. |
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ bun add -d @workerstack/system
34
+ ```
35
+
36
+ `system.config.ts`:
37
+
38
+ ```ts
39
+ import { defineConfig } from "@workerstack/system"
40
+
41
+ export default defineConfig({
42
+ deps: {
43
+ bun: "1.4.4",
44
+ go: "1.27.2",
45
+ uv: "0.13.1",
46
+ ruff: "0.17.1",
47
+ java: "21",
48
+ ffmpeg: { required: false, version: "latest" },
49
+ "stripe-cli": {
50
+ description: "Stripe CLI",
51
+ os: { macos: "brew install stripe/stripe-cli/stripe", windows: false },
52
+ required: false,
53
+ version: "1.53.0",
54
+ },
55
+ },
56
+ })
57
+ ```
58
+
59
+ ```bash
60
+ bunx system install # everything, prerequisites first
61
+ bunx system check # installed vs pinned, exit 1 when a required dep is missing
62
+ bunx system install go --upgrade
63
+ bunx system list # the built-in catalog
64
+ ```
65
+
66
+ ## Built-in catalog
67
+
68
+ A built-in takes only a version. The catalog supplies install commands for every OS and CPU, the version check, PATH entries and prerequisites. `system list` prints this table.
69
+
70
+ | Name | Group | Version | Where | What |
71
+ | -------------------- | ---------- | --------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
72
+ | `brew` | bootstrap | `latest` only | macOS | Homebrew, the macOS package manager. |
73
+ | `choco` | bootstrap | `latest` only | Windows | Chocolatey, the Windows package manager (needs an admin shell). |
74
+ | `bun` | runtimes | exact pin | all | Bun: runtime and the only package manager (its version also pins packageManager). |
75
+ | `deno` | runtimes | exact pin | all | Deno, for the generated TypeScript SDK checks. |
76
+ | `node` | runtimes | exact pin | all | Node.js runtime, for tools that need it (bun stays the package manager). |
77
+ | `dotnet` | languages | exact pin | all | .NET SDK, for the generated C# SDK (dotnet format included). |
78
+ | `go` | languages | exact pin | all | Go toolchain (gofmt included), for the generated Go SDK and CLI. |
79
+ | `google-java-format` | languages | exact pin | all | google-java-format, the generated Java SDK's formatter. Needs `java`. |
80
+ | `gradle` | languages | exact pin | all | Gradle, the generated Java SDK's build. Needs `java`. |
81
+ | `java` | languages | exact pin | all | Eclipse Temurin JDK: a feature major (21) or an exact release (21.0.13+9). |
82
+ | `mypy` | languages | exact pin | all | mypy, the generated Python SDK's type check (uv tool). Needs `uv`. |
83
+ | `python` | languages | exact pin | all | Python, managed by uv (python and python3 shims). Needs `uv`. |
84
+ | `ruff` | languages | exact pin | all | ruff, the generated Python SDK's formatter and linter (uv tool). Needs `uv`. |
85
+ | `rust` | languages | exact pin | all | Rust via rustup (rustfmt and clippy), for the generated Rust SDK. |
86
+ | `uv` | languages | exact pin | all | uv, the Python toolchain manager. |
87
+ | `curl` | release | `latest` only | all | HTTP client the install commands use (the OS package manager's current build). Needs `brew`, `choco`. |
88
+ | `git` | release | `latest` only | all | Version control (the OS package manager's current build). Needs `brew`, `choco`. |
89
+ | `gh` | release | exact pin | all | GitHub CLI, for releases, PRs and the API. |
90
+ | `goreleaser` | release | exact pin | all | Release automation for the generated Go CLI. |
91
+ | `cosign` | release | exact pin | all | Sigstore signing for release artifacts. |
92
+ | `jq` | release | exact pin | all | JSON processor. |
93
+ | `pwsh` | release | exact pin | all | PowerShell 7, for the generated Windows install scripts. |
94
+ | `shellcheck` | release | exact pin | all | Shell script linter, for the generated install scripts. |
95
+ | `cf` | cloudflare | exact pin | all | Cloudflare CLI: deploy, workers types, account operations (bun global, runs on node). Needs `bun`, `node`. |
96
+ | `cloudflared` | cloudflare | exact pin | all | Cloudflare Tunnel client, for remote bindings. |
97
+ | `atlas` | database | exact pin | all | Atlas schema migrations. |
98
+ | `sqld` | database | exact pin | apt, dnf, pacman, macOS | libSQL server, for local database tests (no Windows build). |
99
+ | `chromium` | browsers | `latest` only | apt, dnf, pacman | Chromium, the browser rail fallback (Linux only: brew and choco ship no usable build). |
100
+ | `docker` | browsers | `latest` only | all | Container engine with its daemon (Docker Engine on Linux, Docker Desktop elsewhere). Needs `brew`, `choco`. |
101
+ | `lightpanda` | browsers | `latest` only | apt, dnf, pacman, macOS | Lightpanda, the default headless browser rail (nightly; no Windows build). |
102
+ | `playwright` | browsers | exact pin | all | Playwright's Chromium for browser tests (version = your @playwright/test). Needs `bun`. |
103
+ | `claude` | agents | pin or `latest` | all | Claude Code CLI (native installer; self-updates unless DISABLE_AUTOUPDATER=1). |
104
+ | `codex` | agents | exact pin | all | OpenAI Codex CLI (bun global, runs on node). Needs `bun`, `node`. |
105
+ | `ffmpeg` | agents | `latest` only | all | ffmpeg, for /listen mic capture (the OS package manager's current build). Needs `brew`, `choco`. |
106
+ | `grok` | agents | pin or `latest` | all | Grok Build CLI by xAI (native installer). |
107
+ | `rtk` | agents | exact pin | all | rtk, shell output compression behind the agent Bash hook. |
108
+
109
+ Where things land:
110
+
111
+ - Single binaries: `$HOME/.local/bin`, or `%LOCALAPPDATA%\bin` on Windows (added to the user PATH).
112
+ - Toolchains that need their files together (go, node, java, gradle, dotnet, pwsh): `$HOME/.local/share/<tool>`, or `%LOCALAPPDATA%\Programs\<tool>`.
113
+ - Installers with their own home keep it: bun `~/.bun`, deno `~/.deno`, rust `~/.cargo`, grok `~/.grok`, claude and uv's Python and tools `~/.local/bin`.
114
+ - Global JS CLIs (`cf`, `codex`) install with `bun i -g` into `~/.bun/bin`; their launchers run on `node`.
115
+
116
+ Version policy, checked when the config loads:
117
+
118
+ - **exact pin**: `"latest"` is an error.
119
+ - **`latest` only**: the source cannot pin (OS package managers, nightly builds). Any other version is an error.
120
+ - **pin or `latest`**: either.
121
+
122
+ Prerequisites: a built-in that needs `brew` or `choco` gets it added automatically. Any other missing prerequisite is an error that names the fix:
123
+
124
+ ```
125
+ Invalid system config:
126
+ - deps.ruff: needs uv; add uv: "<version>"
127
+ ```
128
+
129
+ ## Config
130
+
131
+ ### Discovery
132
+
133
+ `system.config.ts`, `system.config.js` or `system.config.mjs` in the working directory, or `--config <path>`. Export the config as `export default defineConfig(...)`, or as any named export holding `deps`.
134
+
135
+ | Field | Type | Default | Meaning |
136
+ | ----------------- | ------ | ---------------- | ---------------------------------------------------------------------------------------- |
137
+ | `deps` | object | required | One entry per dependency, by name |
138
+ | `packageJsonPath` | string | `"package.json"` | The package.json whose `packageManager` follows the bun pin, relative to the config file |
139
+
140
+ ### Entry forms
141
+
142
+ A built-in name takes one of two forms:
143
+
144
+ ```ts
145
+ go: "1.27.2"
146
+
147
+ go: {
148
+ version: "1.27.2",
149
+ required: false, // default true
150
+ description: "Go for the CLI", // replaces the catalog's
151
+ os: { windows: false }, // merges per OS key over the catalog's commands
152
+ check: { command: "go version" }, // replaces
153
+ env: [{ path: "/opt/go/bin" }], // replaces
154
+ dependsOn: [], // replaces
155
+ }
156
+ ```
157
+
158
+ Any other name is a custom dep and needs the full form. `defineConfig` checks each key: a version string under a custom name is a type error.
159
+
160
+ ```ts
161
+ "stripe-cli": {
162
+ description: "Stripe CLI", // required
163
+ version: "1.53.0", // required; "latest" allowed
164
+ required: false, // required
165
+ os: { // required, at least one key
166
+ all: "curl … {{version}} …", // every OS not listed
167
+ macos: "brew install stripe/stripe-cli/stripe",
168
+ windows: false, // explicitly unavailable
169
+ },
170
+ check: { command: "stripe version", pattern: "(\\d+\\.\\d+\\.\\d+)" }, // default "{{name}} --version"
171
+ dependsOn: ["curl"],
172
+ env: [{ path: "$HOME/.stripe/bin" }],
173
+ }
174
+ ```
175
+
176
+ `os` keys: `all`, `linux-apt`, `linux-dnf`, `linux-pacman`, `macos`, `windows`. An OS key overrides `all`, `false` makes the dep unavailable there, and a missing key without `all` means unavailable.
177
+
178
+ Templates in `os` commands and `check.command`: `{{name}}`, `{{version}}`, `{{major}}` (the first dot segment) and `{{arch}}` (`arm64`, else `amd64`).
179
+
180
+ `check.pattern` is a regex source whose first capture is the version, for tools whose output the default rule misreads.
181
+
182
+ ### `env`
183
+
184
+ | Entry | Writes |
185
+ | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
186
+ | `{ path: "$HOME/tool/bin" }` | The directory first on PATH, so the pinned binary wins |
187
+ | `{ key: "GOPATH", value: "$HOME/go" }` | The variable |
188
+ | `{ key: "ANDROID_HOME", detect: ["$HOME/Android/Sdk", "/opt/android-sdk"], fallback: "$HOME/Android/Sdk" }` | The first `detect` path that exists, else `fallback`. Nothing when neither applies |
189
+
190
+ Values are written as-is; the shell expands `$HOME`. `detect` paths expand `$VAR`, `${VAR}` and `%VAR%` from the environment to test for existence.
191
+
192
+ ### Validation
193
+
194
+ The config is validated before anything installs. Every problem is listed with its path:
195
+
196
+ ```
197
+ Invalid system config:
198
+ - deps.go.verison: unknown key (allowed: check, dependsOn, description, env, os, required, version)
199
+ - deps.tool.os: must name at least one OS key or all
200
+ - deps.tool.env[0]: appendToPath is gone; use { path: "/opt/tool/bin" } (it goes first on PATH)
201
+ ```
202
+
203
+ A `dependsOn` name missing from the config and a dependency cycle (`dependency cycle: a → b → a`) are errors too.
204
+
205
+ ## CLI
206
+
207
+ ```
208
+ system <command> [args] [flags]
209
+ ```
210
+
211
+ | Command | Does |
212
+ | ---------------- | ----------------------------------------------------------------------------------- |
213
+ | `install` | Install every dep for this OS, prerequisites first |
214
+ | `install <name>` | Install one dep |
215
+ | `check` | Report installed vs pinned for every dep, plus `packageManager` against the bun pin |
216
+ | `check <name>` | Report one dep |
217
+ | `env` | Write every dep's env block into the shell rc files |
218
+ | `sync-pm` | Set package.json `packageManager` to `bun@<bun pin>`, keeping its formatting |
219
+ | `list` | Print the built-in catalog |
220
+
221
+ | Flag | Meaning |
222
+ | ----------------- | ------------------------------------------------------------------------------------- |
223
+ | `--config <path>` | Config file instead of discovery |
224
+ | `--os <target>` | Use another OS's command set (or set `WORKERSTACK_SYSTEM_OS`). For dry-runs and tests |
225
+ | `--dry-run` | Print the install commands without running them |
226
+ | `--upgrade` | Reinstall a dep whose installed version does not match |
227
+ | `--here` | `check` with this process's PATH instead of a new interactive shell's |
228
+
229
+ `install` skips a dep already at the pinned version. A present but mismatched dep is skipped with a `run: system install <name> --upgrade` hint unless `--upgrade` is set. After a successful install, the dep's env block is written, and a warning appears if another binary still wins on PATH.
230
+
231
+ `check` reads PATH from a new interactive shell (`bash -ic`, `zsh -lic`, Windows PowerShell with its profile), so it sees what a fresh terminal sees, including blocks `install` just wrote. If that shell cannot start, it falls back to this process's PATH with one warning. A check command that runs longer than 30 seconds is killed and reported.
232
+
233
+ Exit codes:
234
+
235
+ | Command | 0 | 1 |
236
+ | --------- | ---------------------------------------------------- | ---------------------------------------------------- |
237
+ | `install` | every required dep installed, skipped or unavailable | a required dep failed |
238
+ | `check` | no required dep missing (mismatches only warn) | a required dep is missing |
239
+ | `env` | every block written | a dep's variables could not be resolved |
240
+ | `sync-pm` | updated, already correct, or skipped | package.json could not be read or written |
241
+ | any | | bad flag, unknown command, missing or invalid config |
242
+
243
+ ## Versions
244
+
245
+ The installed version is the first `N.N…` in the check output, or `check.pattern`'s capture. It matches the pin when:
246
+
247
+ - the pin is `latest`;
248
+ - the pin's dot segments prefix the installed ones: `22` matches `22.20.0`, and `1.27.1` matches `1.27.1` but never `1.27.10`;
249
+ - a prerelease (`-beta.12`) is exact on both sides;
250
+ - build metadata (`+9`) on the installed side is ignored unless the pin has some, so `java: "21"` matches `21.0.13+9`.
251
+
252
+ ## Shell environment
253
+
254
+ `env` and `install` write one managed block per dep:
255
+
256
+ ```bash
257
+ # go environment (managed by @workerstack/system)
258
+ export PATH="$HOME/.local/share/go/bin:$PATH"
259
+ export PATH="$HOME/go/bin:$PATH"
260
+ # end go environment
261
+ ```
262
+
263
+ | Shell | File |
264
+ | ---------- | --------------------------------------------------------------------------------------- |
265
+ | bash | `~/.bashrc` |
266
+ | zsh | `~/.zshrc` |
267
+ | fish | `$XDG_CONFIG_HOME/fish/conf.d/workerstack-system.fish` (default `~/.config/fish/...`) |
268
+ | PowerShell | both `Documents\PowerShell\` (pwsh 7) and `Documents\WindowsPowerShell\` (5.1) profiles |
269
+
270
+ Rewriting a block replaces it in place, and re-running is idempotent. A block written by `crossdeps` (`(managed by crossdeps)`) is replaced, never duplicated.
271
+
272
+ ## Library API
273
+
274
+ ```ts
275
+ import {
276
+ BUILTINS,
277
+ BUILTIN_GROUPS,
278
+ BUILTIN_NAMES, // the catalog
279
+ resolveDeps, // entries → full SystemDepConfig for { arch, os }
280
+ validateConfig, // throws ConfigError listing every problem
281
+ defineConfig,
282
+ resolveOsCommand,
283
+ resolveCheckCommand,
284
+ interpolate,
285
+ sortByDependencies, // install order; throws on cycles and unknown names
286
+ versionsMatch,
287
+ parseVersion,
288
+ detectOs,
289
+ detectOsFromPlatform,
290
+ detectLinuxDistro,
291
+ parseOsTarget,
292
+ OS_TARGETS,
293
+ whichBinary,
294
+ commandExists,
295
+ ConfigError,
296
+ } from "@workerstack/system"
297
+
298
+ const deps = resolveDeps(config.deps, { arch: process.arch, os: detectOs() }, BUILTINS)
299
+ resolveOsCommand("go", deps.go!, "macos")
300
+ // tmp="$(mktemp -d)" && curl -fsSL "https://go.dev/dl/go1.27.2.darwin-arm64.tar.gz" ...
301
+ ```
302
+
303
+ Types: `SystemConfig`, `DepEntry`, `SystemDepConfig`, `OsCommands`, `OsTarget`, `EnvVar`, `BuiltinDep`, `BuiltinOverride`, `BuiltinName`, `RecipeContext`, `Arch`, `RawEntry`.
304
+
305
+ ## Gotchas
306
+
307
+ - `choco` and the Windows `choco install` recipes need an admin shell.
308
+ - `docker` on Linux adds you to the `docker` group. Log out and back in before using it without sudo.
309
+ - `playwright` installs OS libraries only on apt (`--with-deps`); on dnf, pacman, macOS and Windows it installs the browser alone.
310
+ - `claude` and `grok` update themselves. Pin them only when you need a fixed version, and set `DISABLE_AUTOUPDATER=1` for claude.
311
+ - `chromium` is Linux-only, and on Ubuntu apt hands it to snap.
312
+ - A new terminal (or `source` of the rc file) picks up PATH changes; `system check` already reads a new shell's PATH.
313
+
314
+ ## Changelog
315
+
316
+ [CHANGELOG.md](./CHANGELOG.md)
317
+
318
+ ## License
319
+
320
+ MIT
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
- "name": "@workerstack/system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@workerstack/system",
3
+ "version": "0.3.0",
4
+ "description": "Every OS dependency workerstack needs, installed by one command. Define deps in system.config.ts, run system install.",
5
+ "keywords": [
6
+ "cli",
7
+ "cross-platform",
8
+ "dependencies",
9
+ "installer",
10
+ "system"
11
+ ],
12
+ "homepage": "https://github.com/workerstack/system#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/workerstack/system/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "workerstack",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/workerstack/system.git"
21
+ },
22
+ "bin": {
23
+ "system": "./src/bin.ts"
24
+ },
25
+ "files": [
26
+ "src"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": "./src/index.ts"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "test": "vitest run --coverage",
38
+ "test:unit": "vitest run",
39
+ "test:watch": "vitest",
40
+ "type:check": "tsc --noEmit -p tsconfig.src.json && tsc --noEmit",
41
+ "type:check:tests": "tsc --noEmit"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "catalog:",
45
+ "@vitest/coverage-v8": "catalog:",
46
+ "typescript": "catalog:",
47
+ "vitest": "catalog:"
48
+ }
49
+ }
package/src/args.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * CLI argument parsing. Pure: returns the parsed invocation or an error message, never exits.
3
+ */
4
+
5
+ import { type OsTarget, OS_TARGETS } from "./config.ts"
6
+
7
+ export interface Invocation {
8
+ command: string | undefined
9
+ config: string | undefined
10
+ dryRun: boolean
11
+ here: boolean
12
+ os: OsTarget | undefined
13
+ target: string | undefined
14
+ upgrade: boolean
15
+ }
16
+
17
+ export type ParseResult = { ok: true; value: Invocation } | { error: string; ok: false }
18
+
19
+ const VALUE_FLAGS = { "--config": "a path", "--os": "a target" } as const
20
+ const BOOL_FLAGS = ["--dry-run", "--here", "--upgrade"] as const
21
+
22
+ function isOsTarget(value: string): value is OsTarget {
23
+ return (OS_TARGETS as readonly string[]).includes(value)
24
+ }
25
+
26
+ export function parseArgs(argv: readonly string[]): ParseResult {
27
+ const values: Partial<Record<keyof typeof VALUE_FLAGS, string>> = {}
28
+ const bools = new Set<string>()
29
+ const positional: string[] = []
30
+
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const arg = argv[i] as string
33
+ if (arg in VALUE_FLAGS) {
34
+ const flag = arg as keyof typeof VALUE_FLAGS
35
+ const value = argv[i + 1]
36
+ if (value === undefined || value.startsWith("--")) {
37
+ return { error: `${flag} requires ${VALUE_FLAGS[flag]} argument`, ok: false }
38
+ }
39
+ values[flag] = value
40
+ i++
41
+ } else if ((BOOL_FLAGS as readonly string[]).includes(arg)) {
42
+ bools.add(arg)
43
+ } else if (arg.startsWith("--")) {
44
+ return { error: `Unknown flag: ${arg}`, ok: false }
45
+ } else {
46
+ positional.push(arg)
47
+ }
48
+ }
49
+
50
+ const os = values["--os"]
51
+ if (os !== undefined && !isOsTarget(os)) {
52
+ return { error: `Unknown OS target: ${os}. Expected one of: ${OS_TARGETS.join(", ")}`, ok: false }
53
+ }
54
+
55
+ return {
56
+ ok: true,
57
+ value: {
58
+ command: positional[0],
59
+ config: values["--config"],
60
+ dryRun: bools.has("--dry-run"),
61
+ here: bools.has("--here"),
62
+ os,
63
+ target: positional[1],
64
+ upgrade: bools.has("--upgrade"),
65
+ },
66
+ }
67
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ import { main } from "./main.ts"
3
+ import { nodeRuntime } from "./runtime.ts"
4
+
5
+ process.exit(await main(process.argv.slice(2), nodeRuntime()))
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Agent tooling: claude, codex, grok, rtk, ffmpeg.
3
+ */
4
+
5
+ import { archive, bunGlobal, LOCAL_BIN, localBinEnv, pick, ps, unix, winArchive } from "./recipes.ts"
6
+ import type { BuiltinDep } from "./types.ts"
7
+
8
+ /** The installer's version argument: none for latest, so it takes its default channel. */
9
+ function versionArg(version: string, flag = ""): string {
10
+ return version === "latest" ? "" : ` ${flag}${version}`
11
+ }
12
+
13
+ export const AGENTS = {
14
+ claude: {
15
+ description: "Claude Code CLI (native installer; self-updates unless DISABLE_AUTOUPDATER=1)",
16
+ env: (os) => [{ path: os === "windows" ? "$HOME\\.local\\bin" : LOCAL_BIN }],
17
+ os: ({ version }) => ({
18
+ ...unix(
19
+ `curl -fsSL https://claude.ai/install.sh | bash -s${versionArg(version)}`,
20
+ `curl -fsSL https://claude.ai/install.sh | bash -s${versionArg(version)}`,
21
+ ),
22
+ windows: ps(`& ([scriptblock]::Create((irm https://claude.ai/install.ps1)))${versionArg(version)}`),
23
+ }),
24
+ versions: "any",
25
+ },
26
+ codex: {
27
+ /* installed by bun; its launcher is a node script */
28
+ dependsOn: ["bun", "node"],
29
+ description: "OpenAI Codex CLI (bun global, runs on node)",
30
+ os: ({ version }) => bunGlobal("@openai/codex", version),
31
+ versions: "pinned",
32
+ },
33
+ ffmpeg: {
34
+ check: { command: "ffmpeg -version" },
35
+ dependsOn: ["brew", "choco"],
36
+ description: "ffmpeg, for /listen mic capture (the OS package manager's current build)",
37
+ os: () => ({
38
+ "linux-apt": "sudo apt-get install -y ffmpeg",
39
+ "linux-dnf": "sudo dnf install -y ffmpeg-free",
40
+ "linux-pacman": "sudo pacman -S --noconfirm ffmpeg",
41
+ macos: "brew install ffmpeg",
42
+ windows: "choco install ffmpeg -y",
43
+ }),
44
+ versions: "latest",
45
+ },
46
+ grok: {
47
+ description: "Grok Build CLI by xAI (native installer)",
48
+ env: (os) => [{ path: os === "windows" ? "$HOME\\.grok\\bin" : "$HOME/.grok/bin" }],
49
+ os: ({ version }) => ({
50
+ ...unix(
51
+ `curl -fsSL https://x.ai/cli/install.sh | bash -s${versionArg(version)}`,
52
+ `curl -fsSL https://x.ai/cli/install.sh | bash -s${versionArg(version)}`,
53
+ ),
54
+ windows: ps(`& ([scriptblock]::Create((irm https://x.ai/cli/install.ps1)))${versionArg(version, "-Version ")}`),
55
+ }),
56
+ versions: "any",
57
+ },
58
+ rtk: {
59
+ description: "rtk, shell output compression behind the agent Bash hook",
60
+ env: localBinEnv,
61
+ os: ({ arch, version }) => {
62
+ const base = `https://github.com/rtk-ai/rtk/releases/download/v${version}/rtk`
63
+ /* x64 Linux ships a static musl build; arm64 Linux only a glibc one */
64
+ const linux = pick(arch, { arm64: "aarch64-unknown-linux-gnu", x64: "x86_64-unknown-linux-musl" })
65
+ const mac = pick(arch, { arm64: "aarch64-apple-darwin", x64: "x86_64-apple-darwin" })
66
+ return {
67
+ ...unix(archive(`${base}-${linux}.tar.gz`, "rtk", "rtk"), archive(`${base}-${mac}.tar.gz`, "rtk", "rtk")),
68
+ windows: winArchive(`${base}-x86_64-pc-windows-msvc.zip`, "rtk.exe", "rtk"),
69
+ }
70
+ },
71
+ versions: "pinned",
72
+ },
73
+ } satisfies Record<string, BuiltinDep>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Package-manager bootstraps other recipes depend on: Homebrew on macOS, Chocolatey on Windows.
3
+ * Both are `latest`-only and added automatically when a dep needs them.
4
+ */
5
+
6
+ import { ps } from "./recipes.ts"
7
+ import type { BuiltinDep } from "./types.ts"
8
+
9
+ export const BOOTSTRAP = {
10
+ brew: {
11
+ description: "Homebrew, the macOS package manager",
12
+ /* arm64 Macs install to /opt/homebrew, which is not on the default PATH */
13
+ env: (os) => (os === "macos" ? [{ path: "/opt/homebrew/bin" }] : []),
14
+ os: () => ({
15
+ "linux-apt": false,
16
+ "linux-dnf": false,
17
+ "linux-pacman": false,
18
+ macos:
19
+ 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
20
+ windows: false,
21
+ }),
22
+ versions: "latest",
23
+ },
24
+ choco: {
25
+ description: "Chocolatey, the Windows package manager (needs an admin shell)",
26
+ os: () => ({
27
+ "linux-apt": false,
28
+ "linux-dnf": false,
29
+ "linux-pacman": false,
30
+ macos: false,
31
+ windows: ps(
32
+ "Set-ExecutionPolicy Bypass -Scope Process -Force",
33
+ "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072",
34
+ "iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
35
+ ),
36
+ }),
37
+ versions: "latest",
38
+ },
39
+ } satisfies Record<string, BuiltinDep>