@bulletproof-sh/ctrl-daemon 0.2.0-beta.5 → 0.2.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/bin/ctrl-daemon.cjs +1 -1
- package/bin/install.js +266 -0
- package/package.json +7 -6
package/bin/ctrl-daemon.cjs
CHANGED
|
@@ -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
|
-
'
|
|
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
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
|
31
|
-
"@bulletproof-sh/ctrl-daemon-darwin-x64": "0.2.0
|
|
32
|
-
"@bulletproof-sh/ctrl-daemon-linux-arm64": "0.2.0
|
|
33
|
-
"@bulletproof-sh/ctrl-daemon-linux-x64": "0.2.0
|
|
34
|
-
"@bulletproof-sh/ctrl-daemon-windows-x64": "0.2.0
|
|
31
|
+
"@bulletproof-sh/ctrl-daemon-darwin-arm64": "0.2.0",
|
|
32
|
+
"@bulletproof-sh/ctrl-daemon-darwin-x64": "0.2.0",
|
|
33
|
+
"@bulletproof-sh/ctrl-daemon-linux-arm64": "0.2.0",
|
|
34
|
+
"@bulletproof-sh/ctrl-daemon-linux-x64": "0.2.0",
|
|
35
|
+
"@bulletproof-sh/ctrl-daemon-windows-x64": "0.2.0"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"posthog-node": "^5.26.0"
|