@bulletproof-sh/ctrl-daemon 0.2.0-beta.4 → 0.2.0-beta.6

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.
@@ -53,7 +53,7 @@ if (!binaryPath) {
53
53
  console.error(
54
54
  `ctrl-daemon: no binary available for ${key}.\n\n` +
55
55
  `Supported platforms: ${supported}\n` +
56
- 'If you installed with --no-optional, reinstall without that flag.',
56
+ 'Try reinstalling, or set CTRL_DAEMON_BINARY_PATH to a compiled binary.',
57
57
  );
58
58
  process.exit(1);
59
59
  }
package/bin/install.js ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable */
3
+ // @ts-nocheck
4
+
5
+ /**
6
+ * ctrl-daemon postinstall — fallback binary downloader.
7
+ *
8
+ * Runs automatically after `npm install @bulletproof-sh/ctrl-daemon`.
9
+ * npm has a known bug where `npx` sometimes skips optionalDependencies,
10
+ * leaving the platform binary missing. This script detects that case and
11
+ * downloads the tarball directly from the npm registry.
12
+ *
13
+ * Resolution order:
14
+ * 1. Binary already installed via optionalDependencies → exit 0 silently
15
+ * 2. Download tarball from npm registry, extract binary, write to node_modules
16
+ * 3. Any failure → log warning + exit 0 (never break npm install)
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import https from 'node:https';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import zlib from 'node:zlib';
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+
27
+ const platformPackages = {
28
+ 'darwin-arm64': '@bulletproof-sh/ctrl-daemon-darwin-arm64',
29
+ 'darwin-x64': '@bulletproof-sh/ctrl-daemon-darwin-x64',
30
+ 'linux-arm64': '@bulletproof-sh/ctrl-daemon-linux-arm64',
31
+ 'linux-x64': '@bulletproof-sh/ctrl-daemon-linux-x64',
32
+ 'win32-x64': '@bulletproof-sh/ctrl-daemon-windows-x64',
33
+ };
34
+
35
+ function getPlatformKey() {
36
+ return `${process.platform}-${process.arch}`;
37
+ }
38
+
39
+ function getBinName() {
40
+ return process.platform === 'win32' ? 'ctrl-daemon.exe' : 'ctrl-daemon';
41
+ }
42
+
43
+ /**
44
+ * __dirname is .../node_modules/@bulletproof-sh/ctrl-daemon/bin/
45
+ * Go up: bin/ -> ctrl-daemon/ -> @bulletproof-sh/ -> node_modules/
46
+ */
47
+ function getNodeModulesDir() {
48
+ return path.resolve(__dirname, '..', '..', '..');
49
+ }
50
+
51
+ function getInstalledBinaryPath(platformPkg) {
52
+ const nodeModules = getNodeModulesDir();
53
+ const binName = getBinName();
54
+ return path.join(nodeModules, platformPkg, 'bin', binName);
55
+ }
56
+
57
+ function binaryAlreadyInstalled(platformPkg) {
58
+ const binPath = getInstalledBinaryPath(platformPkg);
59
+ try {
60
+ fs.accessSync(binPath, fs.constants.X_OK);
61
+ return true;
62
+ } catch {
63
+ return fs.existsSync(binPath);
64
+ }
65
+ }
66
+
67
+ function getOwnVersion() {
68
+ const pkgPath = path.resolve(__dirname, '..', 'package.json');
69
+ const raw = fs.readFileSync(pkgPath, 'utf8');
70
+ const pkg = JSON.parse(raw);
71
+ return pkg.version;
72
+ }
73
+
74
+ function getRegistryBase() {
75
+ const configured = process.env.npm_config_registry;
76
+ if (configured) {
77
+ return configured.replace(/\/$/, '');
78
+ }
79
+ return 'https://registry.npmjs.org';
80
+ }
81
+
82
+ function buildTarballUrl(platformPkg, version) {
83
+ const registry = getRegistryBase();
84
+ const shortName = platformPkg.split('/')[1];
85
+ return `${registry}/${platformPkg}/-/${shortName}-${version}.tgz`;
86
+ }
87
+
88
+ /**
89
+ * Fetch a URL, following up to `maxRedirects` 3xx redirects.
90
+ * Resolves with the IncomingMessage of the final response.
91
+ */
92
+ function fetchWithRedirects(url, maxRedirects) {
93
+ return new Promise((resolve, reject) => {
94
+ function doFetch(currentUrl, remaining) {
95
+ https
96
+ .get(currentUrl, (res) => {
97
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
98
+ if (remaining === 0) {
99
+ reject(new Error(`Too many redirects fetching ${url}`));
100
+ return;
101
+ }
102
+ res.resume();
103
+ doFetch(res.headers.location, remaining - 1);
104
+ return;
105
+ }
106
+ resolve(res);
107
+ })
108
+ .on('error', reject);
109
+ }
110
+ doFetch(url, maxRedirects);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Parse the NUL-terminated filename from a tar header block (bytes 0-99).
116
+ */
117
+ function parseTarFilename(header) {
118
+ let nameEnd = 0;
119
+ while (nameEnd < 100 && header[nameEnd] !== 0) {
120
+ nameEnd++;
121
+ }
122
+ return header.slice(0, nameEnd).toString('utf8');
123
+ }
124
+
125
+ /**
126
+ * Parse the file size from a tar header block (bytes 124-135, octal ASCII).
127
+ */
128
+ function parseTarFileSize(header) {
129
+ let sizeStr = '';
130
+ for (let i = 124; i < 136; i++) {
131
+ const c = header[i];
132
+ if (c === 0 || c === 0x20) {
133
+ break;
134
+ }
135
+ sizeStr += String.fromCharCode(c);
136
+ }
137
+ return Number.parseInt(sizeStr, 8) || 0;
138
+ }
139
+
140
+ /**
141
+ * Scan the inflated tar buffer for a file matching targetRelativePath.
142
+ * Returns the file's raw bytes, or null if not found.
143
+ *
144
+ * npm tarballs are gzipped POSIX ustar archives. Each entry consists of:
145
+ * - A 512-byte header block
146
+ * - ceil(fileSize / 512) * 512 bytes of data (padded with NUL bytes)
147
+ *
148
+ * Header layout (bytes):
149
+ * 0-99: filename (NUL-terminated)
150
+ * 124-135: file size in octal ASCII (NUL/space-terminated)
151
+ * 156: type flag ('0' or NUL = regular file)
152
+ *
153
+ * Files inside npm tarballs are prefixed with `package/`.
154
+ */
155
+ function scanTarBuffer(buf, targetRelativePath) {
156
+ let offset = 0;
157
+ while (offset + 512 <= buf.length) {
158
+ const header = buf.slice(offset, offset + 512);
159
+
160
+ // Two consecutive zero blocks = end of archive
161
+ if (isZeroBlock(header)) {
162
+ break;
163
+ }
164
+
165
+ const name = parseTarFilename(header);
166
+ const fileSize = parseTarFileSize(header);
167
+ // Type flag byte 156: '0' (0x30) or NUL = regular file
168
+ const typeFlag = header[156];
169
+ const isRegularFile = typeFlag === 0x30 || typeFlag === 0;
170
+
171
+ offset += 512;
172
+
173
+ if (isRegularFile && name === targetRelativePath) {
174
+ return buf.slice(offset, offset + fileSize);
175
+ }
176
+
177
+ offset += Math.ceil(fileSize / 512) * 512;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ function isZeroBlock(header) {
183
+ for (let i = 0; i < 512; i++) {
184
+ if (header[i] !== 0) {
185
+ return false;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Parse a gzipped POSIX tar stream and extract a single file by name.
193
+ */
194
+ function extractBinaryFromTarball(tarballStream, targetRelativePath) {
195
+ return new Promise((resolve, reject) => {
196
+ const gunzip = zlib.createGunzip();
197
+ tarballStream.pipe(gunzip);
198
+
199
+ const chunks = [];
200
+ gunzip.on('data', (chunk) => chunks.push(chunk));
201
+ gunzip.on('error', reject);
202
+ gunzip.on('end', () => {
203
+ const buf = Buffer.concat(chunks);
204
+ const data = scanTarBuffer(buf, targetRelativePath);
205
+ if (data !== null) {
206
+ resolve(data);
207
+ } else {
208
+ reject(new Error(`Binary '${targetRelativePath}' not found in tarball`));
209
+ }
210
+ });
211
+ });
212
+ }
213
+
214
+ async function downloadAndInstall(platformPkg, version) {
215
+ const nodeModules = getNodeModulesDir();
216
+ const binName = getBinName();
217
+ const tarballUrl = buildTarballUrl(platformPkg, version);
218
+
219
+ console.log(`ctrl-daemon: downloading ${platformPkg}@${version} from npm...`);
220
+
221
+ const res = await fetchWithRedirects(tarballUrl, 5);
222
+
223
+ if (res.statusCode !== 200) {
224
+ throw new Error(`HTTP ${res.statusCode} fetching ${tarballUrl}`);
225
+ }
226
+
227
+ const targetRelativePath = `package/bin/${binName}`;
228
+ const binaryData = await extractBinaryFromTarball(res, targetRelativePath);
229
+
230
+ const pkgDir = path.join(nodeModules, platformPkg);
231
+ const binDir = path.join(pkgDir, 'bin');
232
+ fs.mkdirSync(binDir, { recursive: true });
233
+
234
+ const binPath = path.join(binDir, binName);
235
+ fs.writeFileSync(binPath, binaryData);
236
+ fs.chmodSync(binPath, 0o755);
237
+
238
+ const pkgJson = { name: platformPkg, version: version };
239
+ fs.writeFileSync(path.join(pkgDir, 'package.json'), `${JSON.stringify(pkgJson, null, 2)}\n`);
240
+
241
+ console.log(`ctrl-daemon: installed binary to ${binPath}`);
242
+ }
243
+
244
+ async function main() {
245
+ const key = getPlatformKey();
246
+ const platformPkg = platformPackages[key];
247
+
248
+ if (!platformPkg) {
249
+ return;
250
+ }
251
+
252
+ if (binaryAlreadyInstalled(platformPkg)) {
253
+ return;
254
+ }
255
+
256
+ const version = getOwnVersion();
257
+
258
+ try {
259
+ await downloadAndInstall(platformPkg, version);
260
+ } catch (err) {
261
+ console.warn(`ctrl-daemon: postinstall fallback download failed: ${err.message}`);
262
+ console.warn('ctrl-daemon: if this persists, set CTRL_DAEMON_BINARY_PATH to a compiled binary.');
263
+ }
264
+ }
265
+
266
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulletproof-sh/ctrl-daemon",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.6",
4
4
  "description": "WebSocket daemon for ctrl — watches Claude Code sessions and broadcasts agent state",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -11,6 +11,7 @@
11
11
  "bin/"
12
12
  ],
13
13
  "scripts": {
14
+ "postinstall": "node bin/install.js",
14
15
  "dev": "bun --watch src/index.ts",
15
16
  "start": "bun src/index.ts",
16
17
  "build": "bun build src/index.ts --outdir dist --target bun --minify",
@@ -27,11 +28,11 @@
27
28
  "vitest": "^4.0.18"
28
29
  },
29
30
  "optionalDependencies": {
30
- "@bulletproof-sh/ctrl-daemon-darwin-arm64": "0.2.0-beta.4",
31
- "@bulletproof-sh/ctrl-daemon-darwin-x64": "0.2.0-beta.4",
32
- "@bulletproof-sh/ctrl-daemon-linux-arm64": "0.2.0-beta.4",
33
- "@bulletproof-sh/ctrl-daemon-linux-x64": "0.2.0-beta.4",
34
- "@bulletproof-sh/ctrl-daemon-windows-x64": "0.2.0-beta.4"
31
+ "@bulletproof-sh/ctrl-daemon-darwin-arm64": "0.2.0-beta.6",
32
+ "@bulletproof-sh/ctrl-daemon-darwin-x64": "0.2.0-beta.6",
33
+ "@bulletproof-sh/ctrl-daemon-linux-arm64": "0.2.0-beta.6",
34
+ "@bulletproof-sh/ctrl-daemon-linux-x64": "0.2.0-beta.6",
35
+ "@bulletproof-sh/ctrl-daemon-windows-x64": "0.2.0-beta.6"
35
36
  },
36
37
  "dependencies": {
37
38
  "posthog-node": "^5.26.0"