@gitlawb/zero 0.0.0 → 0.1.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 Gitlawb
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,8 +1,301 @@
1
- # @gitlawb/zero
1
+ <p align="center">
2
+ <img src="docs/assets/zero-logo.png" alt="Zero" width="385">
3
+ </p>
2
4
 
3
- This version (0.0.0) is a placeholder that reserves the package name and
4
- enables npm trusted publishing for automated releases.
5
+ <p align="center"><strong>A terminal coding agent you own.</strong></p>
5
6
 
6
- **Zero** — a terminal coding agent you own — ships here starting with v0.1.0.
7
+ <p align="center">
8
+ <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-blue"></a>
9
+ <img alt="Go 1.25+" src="https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go&logoColor=white">
10
+ <img alt="25+ providers" src="https://img.shields.io/badge/providers-25+-34E2EA">
11
+ </p>
7
12
 
8
- Until then, see [github.com/Gitlawb/zero](https://github.com/Gitlawb/zero).
13
+ Zero is an AI coding agent for your local terminal. It can inspect a repository,
14
+ edit files, run commands, use browser/terminal helpers, and keep durable local
15
+ sessions while you choose the model and the permission level.
16
+
17
+ ```bash
18
+ zero
19
+ zero exec "fix the failing test in ./pkg"
20
+ zero exec --output-format stream-json < turns.jsonl
21
+ ```
22
+
23
+ ## Why Zero
24
+
25
+ - **Use the model you want.** Bring OpenAI, Anthropic, Gemini, Groq, OpenRouter,
26
+ DeepSeek, Mistral, xAI, Qwen, Kimi, GitHub Models, Ollama, LM Studio, or any
27
+ OpenAI-/Anthropic-compatible endpoint.
28
+ - **Stay in control.** File writes, shell commands, network access, and
29
+ out-of-workspace writes go through Zero's permission and sandbox policy.
30
+ - **Works in the terminal.** The TUI has model/provider pickers, image input,
31
+ slash commands, live plan/tool rendering, scrollback, themes, and resume/fork
32
+ support.
33
+ - **Works without the TUI.** `zero exec` is scriptable, supports text/JSON/
34
+ stream-JSON I/O, isolated worktrees, spec-first runs, and meaningful exit
35
+ codes for CI.
36
+ - **Keeps context local.** Sessions are stored on disk, searchable, resumable,
37
+ and never uploaded as telemetry by Zero.
38
+ - **Extensible when you need it.** Use MCP servers, skills, plugins, hooks, and
39
+ specialist subagents from the same CLI.
40
+
41
+ ## Install
42
+
43
+ ### npm
44
+
45
+ ```bash
46
+ npm install -g @gitlawb/zero
47
+ zero
48
+ ```
49
+
50
+ The npm package installs a small wrapper plus the matching Zero binary for your
51
+ platform from GitHub Releases. It supports Linux, macOS, and Windows on x64 and
52
+ arm64.
53
+
54
+ ### Install scripts
55
+
56
+ Linux/macOS:
57
+
58
+ ```bash
59
+ curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash
60
+ ```
61
+
62
+ Windows PowerShell:
63
+
64
+ ```powershell
65
+ irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex
66
+ ```
67
+
68
+ ### From source
69
+
70
+ Source builds require Go 1.25+.
71
+
72
+ ```bash
73
+ git clone https://github.com/Gitlawb/zero.git
74
+ cd zero
75
+ go run ./cmd/zero
76
+ ```
77
+
78
+ Release installers and the npm wrapper require published GitHub Release assets.
79
+ If you are testing before the first public release, build from source:
80
+
81
+ ```bash
82
+ go build -o zero ./cmd/zero
83
+ ```
84
+
85
+ On Linux, build the sandbox helper too if you want native sandboxing:
86
+
87
+ ```bash
88
+ go build -o zero-linux-sandbox ./cmd/zero-linux-sandbox
89
+ go build -o zero-seccomp ./cmd/zero-seccomp # optional compatibility wrapper
90
+ ```
91
+
92
+ Put `zero` and `zero-linux-sandbox` in the same directory on `PATH`
93
+ (`~/.local/bin` is a good default). macOS does not need an extra helper binary.
94
+ Windows source builds can use the main `zero.exe` as their sandbox helper; release
95
+ archives still ship standalone Windows helper executables.
96
+
97
+ More install details: [docs/INSTALL.md](docs/INSTALL.md).
98
+
99
+ ## First Run
100
+
101
+ Start the TUI:
102
+
103
+ ```bash
104
+ zero
105
+ ```
106
+
107
+ The setup wizard helps you pick a provider and model. You can also configure
108
+ providers from the command line:
109
+
110
+ ```bash
111
+ zero setup
112
+ zero providers list
113
+ zero models list
114
+ zero doctor
115
+ ```
116
+
117
+ For API providers, set the matching environment variable before setup or enter
118
+ the key in the wizard:
119
+
120
+ ```bash
121
+ export OPENAI_API_KEY=sk-...
122
+ export ANTHROPIC_API_KEY=...
123
+ export GEMINI_API_KEY=...
124
+ ```
125
+
126
+ For local models, run Ollama or LM Studio and then use `zero setup` or
127
+ `zero providers detect`.
128
+
129
+ ## Daily Use
130
+
131
+ ### Interactive TUI
132
+
133
+ ```bash
134
+ zero
135
+ ```
136
+
137
+ Useful controls:
138
+
139
+ | Control | Action |
140
+ |---|---|
141
+ | `Enter` | send the prompt |
142
+ | `/` | open slash-command suggestions |
143
+ | `Shift+Tab` | cycle permission mode |
144
+ | `Ctrl+B` | show/hide the sidebar |
145
+ | `Ctrl+C` | cancel or exit |
146
+
147
+ Common slash commands:
148
+
149
+ | Command | Purpose |
150
+ |---|---|
151
+ | `/model`, `/provider` | switch the active model/provider |
152
+ | `/spec`, `/plan` | draft and review a plan before building |
153
+ | `/image` | attach an image for vision-capable models |
154
+ | `/resume`, `/rewind` | continue or roll back local sessions |
155
+ | `/compact`, `/context` | manage context usage |
156
+ | `/permissions`, `/tools` | inspect available tools and policy |
157
+ | `/add-dir` | allow an extra write directory for this session |
158
+ | `/theme`, `/doctor`, `/config` | adjust appearance and inspect setup |
159
+
160
+ ### Headless `exec`
161
+
162
+ ```bash
163
+ zero exec "explain internal/agent/loop.go"
164
+ zero exec --model claude-sonnet-4.5 "refactor the config loader"
165
+ zero exec --use-spec "add rate limiting to the API client"
166
+ zero exec --worktree "try the migration in an isolated worktree"
167
+ zero exec --resume
168
+ zero exec --fork <session-id> "try the other approach"
169
+ ```
170
+
171
+ Programmatic use:
172
+
173
+ ```bash
174
+ zero exec --input-format stream-json --output-format stream-json < turns.jsonl
175
+ ```
176
+
177
+ The stream-JSON contract is documented in
178
+ [docs/STREAM_JSON_PROTOCOL.md](docs/STREAM_JSON_PROTOCOL.md).
179
+
180
+ ## Safety Model
181
+
182
+ Zero is designed to make side effects visible.
183
+
184
+ - Workspace reads are allowed by default.
185
+ - File writes are limited to the workspace unless you grant another directory.
186
+ - Shell commands, network access, destructive commands, and elevated actions are
187
+ permission-gated.
188
+ - `--add-dir <path>` and `/add-dir <path>` grant additional write roots without
189
+ giving the agent the whole filesystem.
190
+ - Unsafe/autonomous modes are explicit opt-ins.
191
+ - Secrets are redacted from tool output and logs where Zero controls the surface.
192
+
193
+ Example:
194
+
195
+ ```bash
196
+ zero --add-dir ../docs-site
197
+ zero exec --add-dir ../shared "update both repos"
198
+ ```
199
+
200
+ Sandbox behavior can be inspected with:
201
+
202
+ ```bash
203
+ zero sandbox policy
204
+ zero sandbox grants list
205
+ ```
206
+
207
+ ## Web And Local Control
208
+
209
+ Zero includes local file/search/edit/shell tools, `web_fetch` for public URLs,
210
+ and MCP support for additional tools.
211
+
212
+ For local dev servers, use shell commands such as `curl` through `exec_command`
213
+ so the normal sandbox and permission policy applies. Long-running commands stay
214
+ attached to a background terminal session and can be listed or stopped from the
215
+ TUI.
216
+
217
+ The npm package also includes browser and terminal helper packages used by local
218
+ browser/terminal tools. Source builds can use the same helpers when they are on
219
+ `PATH` or configured in Zero's local-control settings.
220
+
221
+ ## Common Commands
222
+
223
+ ```text
224
+ zero interactive TUI
225
+ zero exec one-shot or scripted agent run
226
+ zero setup first-run provider setup
227
+ zero auth OAuth/login helpers for supported providers
228
+ zero models model registry and capabilities
229
+ zero providers provider profiles and detection
230
+ zero doctor setup, key, and connectivity checks
231
+ zero context context-budget report
232
+ zero repo-map deterministic repository map
233
+ zero repo-info local repository summary
234
+ zero search | find search local session history
235
+ zero sessions inspect, resume, fork, and rewind sessions
236
+ zero spec manage spec-mode drafts
237
+ zero specialist manage specialist subagents
238
+ zero skills manage markdown instruction skills
239
+ zero plugins manage plugins
240
+ zero hooks manage lifecycle hooks
241
+ zero mcp manage MCP servers and tools
242
+ zero serve --mcp expose Zero tools over MCP stdio
243
+ zero sandbox inspect sandbox policy and grants
244
+ zero worktrees prepare isolated git worktrees
245
+ zero verify detect and run local verification checks
246
+ zero changes inspect and commit local git changes
247
+ zero usage token usage and estimated cost
248
+ zero cron scheduled agent jobs
249
+ zero update check for newer releases
250
+ ```
251
+
252
+ ## Appearance And Accessibility
253
+
254
+ | Control | Effect |
255
+ |---|---|
256
+ | `NO_COLOR=<anything>` | disables color output |
257
+ | `ZERO_THEME=<name>` | selects the startup theme (`auto`, `dark`, `light`, or a color theme like `dracula`, `nord`, `gruvbox`, `tokyo-night`, `catppuccin`, `one-dark`, `solarized-dark`, `rose-pine`, `everforest`, `solarized-light`) |
258
+ | `--theme <name>` | selects the TUI theme from the CLI (same names) |
259
+ | `/theme` | opens the theme picker inside the TUI (live preview; `/theme <name>` switches directly) |
260
+ | `ZERO_NO_FADE=1` | disables streaming fade animation |
261
+
262
+ Meaning does not rely on color alone; diffs, permissions, and statuses also use
263
+ text or glyph markers.
264
+
265
+ ## Development
266
+
267
+ ```bash
268
+ go test ./...
269
+ go run ./cmd/zero-release build
270
+ go run ./cmd/zero-release smoke
271
+ go run ./cmd/zero-perf-bench
272
+ ```
273
+
274
+ Cross-compile examples:
275
+
276
+ ```bash
277
+ go run ./cmd/zero-release build --goos linux --goarch amd64
278
+ go run ./cmd/zero-release build --goos windows --goarch amd64 --output dist/zero.exe
279
+ ```
280
+
281
+ ## Documentation
282
+
283
+ - [Install](docs/INSTALL.md)
284
+ - [Update flow](docs/UPDATE.md)
285
+ - [Stream-JSON protocol](docs/STREAM_JSON_PROTOCOL.md)
286
+ - [Specialists](docs/SPECIALISTS.md)
287
+ - [GitHub Action](docs/GITHUB_ACTION.md)
288
+ - [Benchmarks](docs/BENCHMARK.md)
289
+ - [Performance](docs/PERFORMANCE.md)
290
+ - [Agent evals](docs/AGENT_EVALS.md)
291
+
292
+ ## Contributing
293
+
294
+ Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md), run the
295
+ relevant tests, and open a focused pull request.
296
+
297
+ Security reports should follow [SECURITY.md](SECURITY.md).
298
+
299
+ ## License
300
+
301
+ Zero is released under the [MIT License](LICENSE).
package/bin/zero.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ function zeroBinaryName(platform = process.platform) {
9
+ return platform === 'win32' ? 'zero.exe' : 'zero';
10
+ }
11
+
12
+ function helperShimNames(name, platform = process.platform) {
13
+ if (platform === 'win32') {
14
+ return [`${name}.cmd`, `${name}.exe`, name];
15
+ }
16
+ return [name];
17
+ }
18
+
19
+ function commandForShim(path, platform = process.platform) {
20
+ if (platform === 'win32' && path.toLowerCase().endsWith('.cmd')) {
21
+ return {
22
+ command: process.env.ComSpec || 'cmd.exe',
23
+ prefixArgs: ['/d', '/s', '/c', `"${path.replace(/"/g, '""')}"`],
24
+ };
25
+ }
26
+ return { command: path, prefixArgs: [] };
27
+ }
28
+
29
+ function resolveHelper(packageRoot, name) {
30
+ const binDir = join(packageRoot, 'node_modules', '.bin');
31
+ for (const shimName of helperShimNames(name)) {
32
+ const candidate = join(binDir, shimName);
33
+ if (!existsSync(candidate)) continue;
34
+ return {
35
+ ...commandForShim(candidate),
36
+ pathPrepend: [binDir],
37
+ };
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function localControlHelperManifest(packageRoot) {
43
+ const helpers = {};
44
+ for (const name of ['agent-browser', 'tuistory']) {
45
+ const helper = resolveHelper(packageRoot, name);
46
+ if (helper) helpers[name] = helper;
47
+ }
48
+ if (Object.keys(helpers).length === 0) return '';
49
+ return JSON.stringify({ version: 1, helpers });
50
+ }
51
+
52
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
53
+ const nativePath = join(packageRoot, zeroBinaryName());
54
+ const localControlHelpers = localControlHelperManifest(packageRoot);
55
+
56
+ if (!existsSync(nativePath)) {
57
+ console.error(
58
+ '[zero] No native binary found next to the npm wrapper. Reinstall the zero package or run `go run ./cmd/zero-release build` from the repository.'
59
+ );
60
+ process.exit(1);
61
+ }
62
+
63
+ const env = { ...process.env };
64
+ if (localControlHelpers) {
65
+ env.ZERO_LOCAL_CONTROL_HELPERS = localControlHelpers;
66
+ } else {
67
+ delete env.ZERO_LOCAL_CONTROL_HELPERS;
68
+ }
69
+
70
+ const child = spawnSync(nativePath, process.argv.slice(2), {
71
+ stdio: 'inherit',
72
+ env,
73
+ });
74
+
75
+ if (child.error) {
76
+ console.error(`[zero] Failed to launch wrapper target: ${child.error.message}`);
77
+ process.exit(1);
78
+ }
79
+
80
+ if (child.signal) {
81
+ process.kill(process.pid, child.signal);
82
+ }
83
+
84
+ process.exit(child.status ?? 1);
package/package.json CHANGED
@@ -1,11 +1,42 @@
1
1
  {
2
2
  "name": "@gitlawb/zero",
3
- "version": "0.0.0",
4
- "description": "Placeholder for Zero, a terminal coding agent. The real package ships with v0.1.0 — see https://github.com/Gitlawb/zero.",
3
+ "version": "0.1.0",
4
+ "description": "Zero a fast, multi-provider terminal coding agent.",
5
+ "type": "module",
6
+ "module": "bin/zero.js",
7
+ "bin": {
8
+ "zero": "bin/zero.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node scripts/postinstall.mjs"
12
+ },
13
+ "files": [
14
+ "bin/zero.js",
15
+ "scripts/postinstall.mjs"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "dependencies": {
21
+ "agent-browser": "^0.30.1",
22
+ "tuistory": "^0.10.0"
23
+ },
24
+ "os": [
25
+ "linux",
26
+ "darwin",
27
+ "win32"
28
+ ],
29
+ "cpu": [
30
+ "x64",
31
+ "arm64"
32
+ ],
5
33
  "repository": {
6
34
  "type": "git",
7
35
  "url": "git+https://github.com/Gitlawb/zero.git"
8
36
  },
9
37
  "homepage": "https://github.com/Gitlawb/zero#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/Gitlawb/zero/issues"
40
+ },
10
41
  "license": "MIT"
11
42
  }
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ // postinstall: fetch the prebuilt `zero` binary for this platform from the
3
+ // matching GitHub Release and place it next to bin/zero.js, which execs it.
4
+ //
5
+ // Mirrors scripts/install.sh and scripts/install.ps1. The asset-name scheme is
6
+ // the source of truth in internal/release/release.go:
7
+ // zero-v{version}-{linux|macos|windows}-{x64|arm64}.{tar.gz|zip} (+ .sha256)
8
+ //
9
+ // Safety: HTTPS-only download from the pinned repo, SHA-256 verification against
10
+ // the release's own .sha256, and extraction that never trusts archive paths —
11
+ // we extract to a temp dir and copy only known binary basenames into place, so a
12
+ // crafted archive cannot write outside the package (no zip-slip).
13
+ //
14
+ // Env overrides (testing / mirrors / locked-down installs):
15
+ // ZERO_SKIP_DOWNLOAD=1 skip entirely, exit 0 (wrapper will guide if run)
16
+ // ZERO_INSTALL_DRY_RUN=1 print the resolved plan as JSON, no network, exit 0
17
+ // ZERO_INSTALL_PLATFORM=… override process.platform (linux|darwin|win32)
18
+ // ZERO_INSTALL_ARCH=… override process.arch (x64|arm64)
19
+ // ZERO_REPO=owner/name override the GitHub repo (default Gitlawb/zero)
20
+ // ZERO_GITHUB_BASE_URL=… override the download host (default https://github.com)
21
+
22
+ import {
23
+ chmodSync,
24
+ copyFileSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ mkdtempSync,
28
+ readdirSync,
29
+ readFileSync,
30
+ rmSync,
31
+ statSync,
32
+ writeFileSync,
33
+ } from 'node:fs';
34
+ import { createHash } from 'node:crypto';
35
+ import { tmpdir } from 'node:os';
36
+ import { dirname, join } from 'node:path';
37
+ import { fileURLToPath } from 'node:url';
38
+ import { spawnSync } from 'node:child_process';
39
+
40
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
41
+ const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
42
+ const VERSION = pkg.version;
43
+
44
+ const REPO = process.env.ZERO_REPO || 'Gitlawb/zero';
45
+ const BASE = (process.env.ZERO_GITHUB_BASE_URL || 'https://github.com').replace(/\/+$/, '');
46
+ // The .sha256 is fetched from the same origin as the archive, so TLS authenticity
47
+ // is the only real integrity control — require https unless explicitly overridden
48
+ // for local testing (e.g. a localhost mirror in tests).
49
+ const ALLOW_INSECURE = process.env.ZERO_ALLOW_INSECURE_DOWNLOAD === '1';
50
+ const MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
51
+
52
+ function fail(message) {
53
+ console.error(`[zero] ${message}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ function warnSkip(message) {
58
+ // Exit 0 so an unsupported platform or an opt-out does not break `npm install`;
59
+ // bin/zero.js reports a clear "no native binary" message if the user runs zero.
60
+ console.error(`[zero] ${message}`);
61
+ process.exit(0);
62
+ }
63
+
64
+ // Maps mirror internal/release/release.go (ReleasePlatform / ReleaseArch).
65
+ function resolvePlatform(p) {
66
+ switch (p) {
67
+ case 'linux':
68
+ return 'linux';
69
+ case 'darwin':
70
+ return 'macos';
71
+ case 'win32':
72
+ return 'windows';
73
+ default:
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function resolveArch(a) {
79
+ switch (a) {
80
+ case 'x64':
81
+ return 'x64';
82
+ case 'arm64':
83
+ return 'arm64';
84
+ default:
85
+ return null;
86
+ }
87
+ }
88
+
89
+ if (process.env.ZERO_SKIP_DOWNLOAD === '1') {
90
+ warnSkip('ZERO_SKIP_DOWNLOAD=1 set — skipping native binary download.');
91
+ }
92
+
93
+ const rawPlatform = process.env.ZERO_INSTALL_PLATFORM || process.platform;
94
+ const rawArch = process.env.ZERO_INSTALL_ARCH || process.arch;
95
+ const platform = resolvePlatform(rawPlatform);
96
+ const arch = resolveArch(rawArch);
97
+
98
+ if (!platform || !arch) {
99
+ warnSkip(
100
+ `no prebuilt binary for ${rawPlatform}/${rawArch}. Build from source: ` +
101
+ `https://github.com/${REPO} (go run ./cmd/zero).`,
102
+ );
103
+ }
104
+
105
+ // The release matrix builds linux/macos {x64,arm64} and windows-x64 — there is no
106
+ // windows-arm64 artifact. (win32, arm64) otherwise resolves to a valid
107
+ // platform/arch, so it would proceed to download a non-existent asset and hard
108
+ // -fail npm install on a 404. Guard it explicitly to skip gracefully; Windows on
109
+ // ARM runs the x64 build under emulation, so the x64 package is the fallback.
110
+ if (platform === 'windows' && arch === 'arm64') {
111
+ warnSkip(
112
+ `no prebuilt binary for windows-arm64 (the windows-x64 build runs under ` +
113
+ `emulation on Windows on ARM). Build from source or install the x64 package: ` +
114
+ `https://github.com/${REPO} (go run ./cmd/zero).`,
115
+ );
116
+ }
117
+
118
+ const ext = platform === 'windows' ? 'zip' : 'tar.gz';
119
+ const binaryName = platform === 'windows' ? 'zero.exe' : 'zero';
120
+ const tag = `v${VERSION}`;
121
+ const assetName = `zero-v${VERSION}-${platform}-${arch}.${ext}`;
122
+ const assetUrl = `${BASE}/${REPO}/releases/download/${tag}/${assetName}`;
123
+ const checksumUrl = `${assetUrl}.sha256`;
124
+
125
+ // Extra binaries the archive may carry; copied when present so the platform
126
+ // sandbox finds its adjacent helpers (tier-1 discovery), but never required.
127
+ const optionalBinaries =
128
+ platform === 'windows'
129
+ ? ['zero-windows-command-runner.exe', 'zero-windows-sandbox-setup.exe']
130
+ : platform === 'linux'
131
+ ? ['zero-linux-sandbox', 'zero-seccomp']
132
+ : [];
133
+
134
+ if (process.env.ZERO_INSTALL_DRY_RUN === '1') {
135
+ process.stdout.write(
136
+ JSON.stringify({
137
+ version: VERSION,
138
+ platform,
139
+ arch,
140
+ tag,
141
+ assetName,
142
+ assetUrl,
143
+ checksumUrl,
144
+ binaryName,
145
+ optionalBinaries,
146
+ }) + '\n',
147
+ );
148
+ process.exit(0);
149
+ }
150
+
151
+ // Idempotent: skip if the binary for this exact version is already in place.
152
+ const markerPath = join(packageRoot, '.zero-binary-version');
153
+ const installedBinary = join(packageRoot, binaryName);
154
+ if (
155
+ existsSync(installedBinary) &&
156
+ existsSync(markerPath) &&
157
+ readFileSync(markerPath, 'utf8').trim() === VERSION
158
+ ) {
159
+ process.exit(0);
160
+ }
161
+
162
+ async function download(url) {
163
+ if (!ALLOW_INSECURE && !url.startsWith('https://')) {
164
+ fail(
165
+ `refusing insecure download origin for ${url}: only https:// is allowed ` +
166
+ `(set ZERO_ALLOW_INSECURE_DOWNLOAD=1 to override for local testing).`,
167
+ );
168
+ }
169
+ let response;
170
+ try {
171
+ response = await fetch(url, { redirect: 'follow' });
172
+ } catch (error) {
173
+ fail(`download failed for ${url}: ${error.message}`);
174
+ }
175
+ // Catch an https->http downgrade across redirects: the verified bytes must
176
+ // still have arrived over TLS for the same-origin checksum to mean anything.
177
+ if (!ALLOW_INSECURE && response.url && !response.url.startsWith('https://')) {
178
+ fail(`refusing insecure redirect to ${response.url}`);
179
+ }
180
+ if (!response.ok) {
181
+ fail(
182
+ `download failed (HTTP ${response.status}) for ${url} (release tag ${tag}). ` +
183
+ `If no release exists yet, or the tag and package.json version disagree, ` +
184
+ `build from source: https://github.com/${REPO} (go run ./cmd/zero).`,
185
+ );
186
+ }
187
+ const declared = Number(response.headers.get('content-length') || 0);
188
+ if (declared && declared > MAX_DOWNLOAD_BYTES) {
189
+ fail(`refusing oversized download (${declared} bytes) from ${url}`);
190
+ }
191
+ const buffer = Buffer.from(await response.arrayBuffer());
192
+ if (buffer.length > MAX_DOWNLOAD_BYTES) {
193
+ fail(`download from ${url} exceeded ${MAX_DOWNLOAD_BYTES} bytes`);
194
+ }
195
+ return buffer;
196
+ }
197
+
198
+ // Parse a sha256sum-style file. Anchored per line, and when the line carries a
199
+ // filename field it must equal the requested asset — mirrors the Go
200
+ // ParseSHA256Checksum so a checksum file cannot misattribute a digest.
201
+ function parseSha256(text, wantName) {
202
+ for (const rawLine of text.split(/\r?\n/)) {
203
+ const line = rawLine.trim();
204
+ if (!line) continue;
205
+ const match = line.match(/^([a-fA-F0-9]{64})(?:\s+\*?(.+))?$/);
206
+ if (!match) continue;
207
+ const [, hex, name] = match;
208
+ if (!name || name.trim() === wantName) {
209
+ return hex.toLowerCase();
210
+ }
211
+ }
212
+ fail(`could not find a SHA-256 digest for ${wantName} in ${wantName}.sha256`);
213
+ }
214
+
215
+ function findByBasename(dir, name) {
216
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
217
+ const full = join(dir, entry.name);
218
+ if (entry.isDirectory()) {
219
+ const nested = findByBasename(full, name);
220
+ if (nested) return nested;
221
+ } else if (entry.name === name) {
222
+ return full;
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+
228
+ // extract runs from `workDir` and passes RELATIVE names so no Windows drive
229
+ // letter (e.g. C:\) appears in tar's archive argument — GNU tar otherwise reads
230
+ // `C:foo` as a remote `host:path` and fails. archiveName and destName are both
231
+ // relative to workDir.
232
+ function extract(workDir, archiveName, destName) {
233
+ const opts = { stdio: 'inherit', cwd: workDir };
234
+ if (ext === 'zip') {
235
+ // Windows 10+ ships bsdtar as tar.exe, which extracts zips; fall back to
236
+ // PowerShell Expand-Archive if tar is unavailable or cannot read the zip.
237
+ const viaTar = spawnSync('tar', ['-xf', archiveName, '-C', destName], opts);
238
+ if (viaTar.status === 0) return;
239
+ const absArchive = join(workDir, archiveName);
240
+ const absDest = join(workDir, destName);
241
+ const viaPwsh = spawnSync(
242
+ 'powershell',
243
+ [
244
+ '-NoProfile',
245
+ '-NonInteractive',
246
+ '-Command',
247
+ `Expand-Archive -LiteralPath '${absArchive.replace(/'/g, "''")}' ` +
248
+ `-DestinationPath '${absDest.replace(/'/g, "''")}' -Force`,
249
+ ],
250
+ { stdio: 'inherit' },
251
+ );
252
+ if (viaPwsh.status !== 0) fail(`failed to extract ${assetName}`);
253
+ return;
254
+ }
255
+ const viaTar = spawnSync('tar', ['-xzf', archiveName, '-C', destName], opts);
256
+ if (viaTar.status !== 0) fail(`failed to extract ${assetName} (tar exited ${viaTar.status})`);
257
+ }
258
+
259
+ async function main() {
260
+ const archiveBuffer = await download(assetUrl);
261
+ const checksumText = (await download(checksumUrl)).toString('utf8');
262
+ const expected = parseSha256(checksumText, assetName);
263
+ const actual = createHash('sha256').update(archiveBuffer).digest('hex');
264
+ if (actual !== expected) {
265
+ fail(`checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`);
266
+ }
267
+
268
+ const tempDir = mkdtempSync(join(tmpdir(), 'zero-install-'));
269
+ try {
270
+ const archivePath = join(tempDir, assetName);
271
+ writeFileSync(archivePath, archiveBuffer);
272
+ const extractDir = join(tempDir, 'extracted');
273
+ mkdirSync(extractDir);
274
+ extract(tempDir, assetName, 'extracted');
275
+
276
+ // Copy only known basenames into the package root. We never honor
277
+ // archive-relative paths, so a crafted entry cannot escape the package.
278
+ const primarySource = findByBasename(extractDir, binaryName);
279
+ if (!primarySource) fail(`archive ${assetName} did not contain ${binaryName}`);
280
+ copyFileSync(primarySource, installedBinary);
281
+ if (platform !== 'windows') chmodSync(installedBinary, 0o755);
282
+
283
+ for (const name of optionalBinaries) {
284
+ const source = findByBasename(extractDir, name);
285
+ if (!source) {
286
+ console.error(
287
+ `[zero] note: optional helper ${name} was not in ${assetName}; ` +
288
+ `the sandbox may run with reduced isolation.`,
289
+ );
290
+ continue;
291
+ }
292
+ const dest = join(packageRoot, name);
293
+ copyFileSync(source, dest);
294
+ if (platform !== 'windows') chmodSync(dest, 0o755);
295
+ }
296
+
297
+ writeFileSync(markerPath, VERSION + '\n');
298
+ const sizeKb = Math.round(statSync(installedBinary).size / 1024);
299
+ console.error(`[zero] installed ${binaryName} ${VERSION} (${platform}-${arch}, ${sizeKb} KB).`);
300
+ } finally {
301
+ rmSync(tempDir, { recursive: true, force: true });
302
+ }
303
+ }
304
+
305
+ main().catch((error) => fail(error.message));