@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 +21 -0
- package/README.md +298 -5
- package/bin/zero.js +84 -0
- package/package.json +33 -2
- package/scripts/postinstall.mjs +305 -0
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
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/zero-logo.png" alt="Zero" width="385">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
enables npm trusted publishing for automated releases.
|
|
5
|
+
<p align="center"><strong>A terminal coding agent you own.</strong></p>
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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));
|