procdeck 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.
@@ -0,0 +1,147 @@
1
+ import { Effect, Schema } from "effect";
2
+ export declare const PreflightSchema: Schema.Struct<{
3
+ /** Shell command; exit 0 lets the proc spawn. */
4
+ readonly shell: Schema.String;
5
+ /**
6
+ * Regex that must also match the check's output. For checks that exit 0
7
+ * either way — `wrangler whoami` reports "not authenticated" with exit 0 —
8
+ * so the config doesn't need `| grep` pipelines.
9
+ */
10
+ readonly expect: Schema.optionalKey<Schema.String>;
11
+ /** What the human should do when the check fails. */
12
+ readonly hint: Schema.optionalKey<Schema.String>;
13
+ }>;
14
+ export type Preflight = typeof PreflightSchema.Type;
15
+ /** A regex watched in the pane output; a match raises a badge in the UI. */
16
+ export declare const AlertSchema: Schema.Struct<{
17
+ readonly pattern: Schema.String;
18
+ /** Badge text. */
19
+ readonly label: Schema.String;
20
+ }>;
21
+ export type Alert = typeof AlertSchema.Type;
22
+ export declare const ProcSpecSchema: Schema.Struct<{
23
+ /** Stable identifier, also the tab label. */
24
+ readonly id: Schema.String;
25
+ /** Command line handed to `$SHELL -c`. */
26
+ readonly shell: Schema.optionalKey<Schema.String>;
27
+ /** argv, executed without a shell. */
28
+ readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
29
+ /** Working directory; defaults to the config file's directory. */
30
+ readonly cwd: Schema.optionalKey<Schema.String>;
31
+ /** Extra environment on top of the inherited one. */
32
+ readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
33
+ /** Spawn on startup. Defaults to true. */
34
+ readonly autostart: Schema.optionalKey<Schema.Boolean>;
35
+ /** URL this process serves, shown in the UI as a link. */
36
+ readonly url: Schema.optionalKey<Schema.String>;
37
+ /**
38
+ * Names of procs that must be ready before this one spawns. Declarative:
39
+ * readiness is auto-detected, no port numbers in the config.
40
+ */
41
+ readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
42
+ /**
43
+ * What "ready" means for dependents: "listening" (default) — the process
44
+ * tree opened a TCP port; "started" — it merely spawned (for procs that
45
+ * never listen).
46
+ */
47
+ readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
48
+ /** Check that must pass before this proc spawns; failure shows as "blocked". */
49
+ readonly preflight: Schema.optionalKey<Schema.Struct<{
50
+ /** Shell command; exit 0 lets the proc spawn. */
51
+ readonly shell: Schema.String;
52
+ /**
53
+ * Regex that must also match the check's output. For checks that exit 0
54
+ * either way — `wrangler whoami` reports "not authenticated" with exit 0 —
55
+ * so the config doesn't need `| grep` pipelines.
56
+ */
57
+ readonly expect: Schema.optionalKey<Schema.String>;
58
+ /** What the human should do when the check fails. */
59
+ readonly hint: Schema.optionalKey<Schema.String>;
60
+ }>>;
61
+ /** Output patterns that raise a badge when they appear in the pane. */
62
+ readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
63
+ readonly pattern: Schema.String;
64
+ /** Badge text. */
65
+ readonly label: Schema.String;
66
+ }>>>;
67
+ }>;
68
+ export type ProcSpec = typeof ProcSpecSchema.Type;
69
+ /** Default port of the UI + proxy server. */
70
+ export declare const DEFAULT_UI_PORT = 4820;
71
+ /** Whether the spec asks procdeck to assign it a port (`${port}` anywhere). */
72
+ export declare const usesOwnPort: (spec: ProcSpec) => boolean;
73
+ /** Substitute `${port}` / `${port:id}` with the ports assigned at startup. */
74
+ export declare const resolveSpec: (spec: ProcSpec, assigned: Map<string, number>) => ProcSpec;
75
+ export declare const ProcdeckConfigSchema: Schema.Struct<{
76
+ /**
77
+ * Deck name — the tab title and the name of the installed web app.
78
+ * Defaults to the config directory's basename.
79
+ */
80
+ readonly name: Schema.optionalKey<Schema.String>;
81
+ /** Port for the web UI. Defaults to 4820. */
82
+ readonly port: Schema.optionalKey<Schema.Number>;
83
+ readonly procs: Schema.$Array<Schema.Struct<{
84
+ /** Stable identifier, also the tab label. */
85
+ readonly id: Schema.String;
86
+ /** Command line handed to `$SHELL -c`. */
87
+ readonly shell: Schema.optionalKey<Schema.String>;
88
+ /** argv, executed without a shell. */
89
+ readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
90
+ /** Working directory; defaults to the config file's directory. */
91
+ readonly cwd: Schema.optionalKey<Schema.String>;
92
+ /** Extra environment on top of the inherited one. */
93
+ readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
94
+ /** Spawn on startup. Defaults to true. */
95
+ readonly autostart: Schema.optionalKey<Schema.Boolean>;
96
+ /** URL this process serves, shown in the UI as a link. */
97
+ readonly url: Schema.optionalKey<Schema.String>;
98
+ /**
99
+ * Names of procs that must be ready before this one spawns. Declarative:
100
+ * readiness is auto-detected, no port numbers in the config.
101
+ */
102
+ readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
103
+ /**
104
+ * What "ready" means for dependents: "listening" (default) — the process
105
+ * tree opened a TCP port; "started" — it merely spawned (for procs that
106
+ * never listen).
107
+ */
108
+ readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
109
+ /** Check that must pass before this proc spawns; failure shows as "blocked". */
110
+ readonly preflight: Schema.optionalKey<Schema.Struct<{
111
+ /** Shell command; exit 0 lets the proc spawn. */
112
+ readonly shell: Schema.String;
113
+ /**
114
+ * Regex that must also match the check's output. For checks that exit 0
115
+ * either way — `wrangler whoami` reports "not authenticated" with exit 0 —
116
+ * so the config doesn't need `| grep` pipelines.
117
+ */
118
+ readonly expect: Schema.optionalKey<Schema.String>;
119
+ /** What the human should do when the check fails. */
120
+ readonly hint: Schema.optionalKey<Schema.String>;
121
+ }>>;
122
+ /** Output patterns that raise a badge when they appear in the pane. */
123
+ readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
124
+ readonly pattern: Schema.String;
125
+ /** Badge text. */
126
+ readonly label: Schema.String;
127
+ }>>>;
128
+ }>>;
129
+ }>;
130
+ export type ProcdeckConfig = typeof ProcdeckConfigSchema.Type;
131
+ /** Identity helper that gives config files their types. */
132
+ export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
133
+ export type LoadedConfig = {
134
+ config: ProcdeckConfig;
135
+ /** Directory of the config file — the default cwd for every process. */
136
+ root: string;
137
+ /** `config.name`, or the root directory's basename. */
138
+ name: string;
139
+ };
140
+ declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
141
+ readonly file: Schema.String;
142
+ readonly message: Schema.String;
143
+ }>, import("effect/Cause").YieldableError>;
144
+ export declare class ConfigError extends ConfigError_base {
145
+ }
146
+ export declare const loadConfig: (file: string) => Effect.Effect<LoadedConfig, ConfigError, never>;
147
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,24 @@
1
+ import {
2
+ AlertSchema,
3
+ ConfigError,
4
+ DEFAULT_UI_PORT,
5
+ PreflightSchema,
6
+ ProcSpecSchema,
7
+ ProcdeckConfigSchema,
8
+ defineConfig,
9
+ loadConfig,
10
+ resolveSpec,
11
+ usesOwnPort
12
+ } from "./chunk-SBVZ6G4G.js";
13
+ export {
14
+ AlertSchema,
15
+ ConfigError,
16
+ DEFAULT_UI_PORT,
17
+ PreflightSchema,
18
+ ProcSpecSchema,
19
+ ProcdeckConfigSchema,
20
+ defineConfig,
21
+ loadConfig,
22
+ resolveSpec,
23
+ usesOwnPort
24
+ };
package/license ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Kondaurov
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/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "procdeck",
3
+ "version": "0.1.0",
4
+ "description": "Interactive dev-process multiplexer with a web UI: terminal panes in a browser tab, declarative dependencies, assigned ports and a *.localhost reverse proxy.",
5
+ "keywords": [
6
+ "cli",
7
+ "dev-server",
8
+ "multiplexer",
9
+ "process-manager",
10
+ "pty",
11
+ "reverse-proxy",
12
+ "terminal",
13
+ "xterm"
14
+ ],
15
+ "homepage": "https://github.com/kondaurovDev/procdeck",
16
+ "bugs": "https://github.com/kondaurovDev/procdeck/issues",
17
+ "license": "MIT",
18
+ "author": {
19
+ "name": "Aleksandr Kondaurov",
20
+ "email": "kondaurov.dev@gmail.com"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/kondaurovDev/procdeck",
25
+ "directory": "packages/procdeck"
26
+ },
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=22.18"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "ui/dist"
34
+ ],
35
+ "bin": {
36
+ "procdeck": "./dist/cli.js"
37
+ },
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/config.d.ts",
41
+ "import": "./dist/config.js"
42
+ }
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "effect": "4.0.0-beta.106",
49
+ "node-pty": "^1.1.0"
50
+ },
51
+ "devDependencies": {
52
+ "@effect/platform-browser": "4.0.0-beta.106",
53
+ "@types/node": "^22.10.1",
54
+ "@xterm/addon-fit": "^0.11.0",
55
+ "@xterm/addon-search": "^0.16.0",
56
+ "@xterm/xterm": "^6.0.0",
57
+ "foldkit": "^0.141.2",
58
+ "tsup": "^8.5.1",
59
+ "typescript": "^7.0.2",
60
+ "vite": "^6.2.0",
61
+ "vitest": "^3.2.4"
62
+ },
63
+ "scripts": {
64
+ "ui:dev": "vite ui",
65
+ "build": "pnpm run build:meta && pnpm run build:ui && pnpm run build:js && pnpm run build:types",
66
+ "build:meta": "cp ../../readme.md ../../license .",
67
+ "build:ui": "vite build ui",
68
+ "build:js": "tsup",
69
+ "build:types": "tsc -p tsconfig.build.json",
70
+ "check:types": "tsc --noEmit && tsc -p ui --noEmit",
71
+ "test": "vitest run"
72
+ }
73
+ }
package/readme.md ADDED
@@ -0,0 +1,203 @@
1
+ # procdeck
2
+
3
+ An interactive dev-process multiplexer with a **web UI** instead of a TUI — the thing
4
+ `mprocs`/`overmind` do, but the panes live in a browser tab, and the tool also solves
5
+ the two problems terminal multiplexers can't touch: **port conflicts** and **remembering
6
+ which port is which**.
7
+
8
+ ```
9
+ ┌──────────┬───────────────────────────────────────────────┐
10
+ │ ● api │ Restart Stop Start api.localhost:4820 │
11
+ │ ● web ├───────────────────────────────────────────────┤
12
+ │ ● clock │ $ node servers/api.mjs │
13
+ │ ● flaky ⚠│ api listening on :52341 │
14
+ │ │ GET / │
15
+ └──────────┴───────────────────────────────────────────────┘
16
+ ```
17
+
18
+ ## What it does
19
+
20
+ - **Terminal panes in the browser.** Every process runs in a real PTY and renders in
21
+ xterm.js — colours, spinners, cursor redraws and interactive hotkeys work exactly like
22
+ in a terminal. Keystrokes are posted back into the PTY.
23
+ - **Assigned ports.** Write `${port}` in a proc's command, env or url and procdeck hands
24
+ it a free port before spawning (also exported as `PORT`). `${port:api}` references
25
+ another proc's assigned port — so dependents get wired
26
+ (`API_URL=http://localhost:${port:api}`) without a single hardcoded number. Nothing
27
+ can conflict: ports are assigned once per run and never collide.
28
+ - **A reverse proxy on `*.localhost`.** Every pane is reachable at a stable, memorable
29
+ address on the UI's own port — `api.localhost:4820`, `web.localhost:4820` — whatever
30
+ port the process actually got. Browsers hardcode `*.localhost` → 127.0.0.1, so there
31
+ is zero system setup. WebSocket upgrades pass through (vite HMR works); the `Host`
32
+ header is rewritten upstream so host-allowlisting dev servers need no config.
33
+ - **Port auto-detection.** A 1s poll asks the OS (`pgrep` + `lsof`) which TCP ports each
34
+ process _group_ listens on; they render as links in the UI. No cooperation from the
35
+ process needed.
36
+ - **Declarative dependencies.** `needs: ["api"]` parks a proc in `waiting` until every
37
+ dependency is ready. Ready = "listening on its port" by default, or
38
+ `readyWhen: "started"` for procs that never listen. Unknown names, duplicates and
39
+ cycles are rejected by the config schema.
40
+ - **Preflight gates.** A shell command that must pass before the proc spawns (plus an
41
+ optional `expect` regex for checks that exit 0 either way, like `wrangler whoami`).
42
+ Failure parks the pane in `blocked` with the output and a hint for the human — keeps
43
+ interactive auth flows out of supervised panes, where a restart would kill an OAuth
44
+ callback server mid-handshake.
45
+ - **Alerts.** Regexes matched against a rolling tail of pane output; a match raises a
46
+ badge in the UI ("needs login").
47
+ - **Whole-tree restarts.** `pnpm` → `wrangler` → `workerd`: killing the pid would leave
48
+ the grandchild holding the port. procdeck signals the process group, escalating
49
+ SIGTERM → SIGKILL, and Ctrl-C tears down every tree it spawned.
50
+ - **Single or grid layout.** One pane at a time with a sidebar, or every pane tiled
51
+ (⌥G); each pane header carries its address, status and restart/stop/clear.
52
+ - **Installable as an app.** The UI ships a web-app manifest named after the deck
53
+ (`name` in the config, else the directory), so "⤓ install" in the bar — or File →
54
+ Add to Dock in Safari — gives each project its own window and Dock icon. Works over
55
+ plain `http://localhost`, no HTTPS needed.
56
+
57
+ ## Install
58
+
59
+ ```sh
60
+ pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
61
+ ```
62
+
63
+ Requires **Node ≥ 22.18**: `procdeck.config.ts` is a TypeScript file, loaded through
64
+ Node's built-in type stripping.
65
+
66
+ Write a `procdeck.config.ts` next to your `package.json` (see [Config](#config)), then:
67
+
68
+ ```sh
69
+ npx procdeck # defaults to ./procdeck.config.ts
70
+ npx procdeck decks/backend.ts # or point it at any config file
71
+ ```
72
+
73
+ Open <http://localhost:4820> for the panes.
74
+
75
+ ## Try the example
76
+
77
+ In a clone of this repo:
78
+
79
+ ```sh
80
+ pnpm install
81
+ pnpm dev # builds the UI, starts the example stack
82
+ ```
83
+
84
+ > **Native dependency note.** procdeck runs panes through [node-pty](https://github.com/microsoft/node-pty),
85
+ > which ships a native prebuild. npm and yarn set it up automatically. pnpm ≥ 10 and Bun
86
+ > block dependency install scripts by default — in your own project run
87
+ > `pnpm approve-builds` and allow `node-pty` (Bun: add it to `trustedDependencies`).
88
+ > procdeck also repairs the most common fallout by itself at startup (the darwin
89
+ > `spawn-helper` prebuild arriving without its execute bit), so approving is only
90
+ > strictly required when node-pty must compile from source (no prebuild for your
91
+ > platform).
92
+
93
+ Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
94
+ <http://api.localhost:4820> — the example's two servers found each other (and their own
95
+ ports) entirely through `${port}` templates. See [`example/`](example/).
96
+
97
+ ## Config
98
+
99
+ `procdeck.config.ts` — a TypeScript file, validated by an Effect schema:
100
+
101
+ ```ts
102
+ import { defineConfig } from "procdeck"
103
+
104
+ export default defineConfig({
105
+ port: 4820, // UI + proxy port
106
+ procs: [
107
+ {
108
+ id: "api", // pane name and the proxy subdomain
109
+ shell: "pnpm --filter api dev", // or cmd: ["node", "server.js"]
110
+ env: { PORT: "${port}" }, // ask procdeck for a free port
111
+ preflight: { // gate: must pass before spawning
112
+ shell: "wrangler whoami",
113
+ expect: "You are logged in",
114
+ hint: "run `wrangler login`, then Start",
115
+ },
116
+ alerts: [{ pattern: "ERROR", label: "check me" }],
117
+ },
118
+ {
119
+ id: "web",
120
+ shell: "pnpm --filter web dev",
121
+ env: { PORT: "${port}", API_URL: "http://localhost:${port:api}" },
122
+ needs: ["api"], // wait until api is listening
123
+ },
124
+ {
125
+ id: "worker",
126
+ shell: "node worker.js",
127
+ readyWhen: "started", // never listens; running = ready
128
+ },
129
+ ],
130
+ })
131
+ ```
132
+
133
+ Full field reference: [`packages/procdeck/src/config.ts`](packages/procdeck/src/config.ts)
134
+ (the schema is the documentation).
135
+
136
+ Tips:
137
+
138
+ - Give tools that auto-increment on a busy port (vite) a `strictPort` flag, so a
139
+ violated port assignment fails loudly instead of drifting.
140
+ - A good pattern for app dev scripts: `vite dev --port ${PORT:-3000} --strictPort` —
141
+ fixed default when run by hand, assigned port under procdeck.
142
+ - The proxy is for humans and browsers. Scripts and server-to-server calls should use
143
+ the injected env (`${port:api}`) — system resolvers don't all know `*.localhost`.
144
+
145
+ ## How it fits together
146
+
147
+ | Path | What |
148
+ | ----------------------- | ----------------------------------------------------------------------------------------------------- |
149
+ | `packages/procdeck/src` | The server: plain-Node PTY layer, Effect supervisor, HTTP/SSE server with the reverse proxy. |
150
+ | `packages/procdeck/ui` | [Foldkit](https://foldkit.dev) (Elm-on-Effect) app; built by vite, served statically by the server. |
151
+ | `example/` | Self-contained demo stack (Node one-liners) exercising every feature. |
152
+
153
+ | File | What |
154
+ | --------------- | ---------------------------------------------------------------------------------------------------- |
155
+ | `proc.ts` | Plain Node: PTY spawn, process-group kill, SIGTERM → SIGKILL escalation. |
156
+ | `config.ts` | Effect `Schema` for the config, `${port}` templating, loader. |
157
+ | `supervisor.ts` | Effect layer: scoped lifecycle, port assignment, `PubSub` fan-out, per-proc state. |
158
+ | `ports.ts` | Free-port allocation and `pgrep`+`lsof` listening-port discovery. |
159
+ | `server.ts` | `effect/unstable/http` router behind a small Node↔web-handler bridge; SSE downstream, POST upstream; `Host`-routed reverse proxy with WebSocket pass-through. |
160
+ | `cli.ts` | Entry point and signal handling. |
161
+
162
+ Design notes worth keeping:
163
+
164
+ - **Effect end to end.** The server is Effect; the UI is a Foldkit app — one Model (a
165
+ Schema), pure `update`, side effects as Commands. Terminals are imperative islands via
166
+ `Mount.defineStream`; PTY log chunks bypass the Model (`ReceivedLog` → `WriteTerminal`
167
+ Command into a module-level terminal registry — the view never renders them); the SSE
168
+ subscription is gated on a Model condition ("every pane mounted"), so the replayed
169
+ backlog ordering is declarative, not sequenced by hand.
170
+ - **The server has no bundler.** It runs as `.ts` via Node type stripping (Node ≥ 22.18).
171
+ - **SSE, not WebSocket.** Logs only flow towards the browser; commands are ordinary
172
+ POSTs. No upgrade handshake, no extra dependency, and `EventSource` reconnects by
173
+ itself.
174
+ - **`PubSub.sliding({ capacity, replay })`** doubles as the fan-out to every open tab
175
+ _and_ the scrollback ring buffer — a tab opened late replays the backlog for free.
176
+ - **The supervisor is `Effect.acquireRelease`.** Shutdown is not a code path anyone has
177
+ to remember to call: closing the scope terminates every process tree. The SSE response
178
+ stream lives in the request scope, so a dropped tab cleans up its subscription too.
179
+
180
+ ## Tests
181
+
182
+ ```sh
183
+ pnpm test
184
+ ```
185
+
186
+ Integration tests with real (tiny) processes — `node -e` one-liners and shells with
187
+ background children — because the risks live at the OS boundary: PTY detection, group
188
+ kills, SIGTERM-ignoring survivors, scope teardown. Mocking any of that would test the
189
+ mock. They need a real PTY (`/dev/ptmx`), so they won't run inside sandboxes that block
190
+ PTY allocation.
191
+
192
+ Gotcha: if every spawn fails with `posix_spawnp failed.`, the usual culprit is the
193
+ node-pty `spawn-helper` prebuild arriving without its execute bit (pnpm/Bun skipping
194
+ its install script). `proc.ts` restores the bit at import time, so this should
195
+ self-heal — if it still fails, PTY allocation itself is probably blocked (sandboxes).
196
+
197
+ ## Status
198
+
199
+ A working prototype, macOS/Linux only (PTYs, `pgrep`, `lsof`). Known prototype-grade
200
+ shortcuts: the replay buffer counts events rather than bytes (a chatty proc can evict a
201
+ quiet one's history), and statuses are a mutated map rather than `SubscriptionRef`s.
202
+ Not done yet: an injected FAB overlay inside the developed apps, dependency-death
203
+ policy (a dependency dying does _not_ cascade — deliberately), and an npm release.
Binary file