pi-bridge 0.2.9 → 0.3.1
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/pii.js +5 -0
- package/package.json +1 -1
- package/src/local/child-process-patch.js +73 -14
- package/src/local/http-client.js +27 -21
package/bin/pii.js
CHANGED
|
@@ -63,6 +63,11 @@ if (!pi) {
|
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Expose resolved pi paths so child-process-patch can re-spawn pi correctly
|
|
67
|
+
// when the review skill (or any internal tool) calls `pi` as a subprocess.
|
|
68
|
+
process.env.PI_BRIDGE_CLI = pi.cli;
|
|
69
|
+
process.env.PI_BRIDGE_NODE = pi.nodeExe;
|
|
70
|
+
|
|
66
71
|
const result = spawnSync(
|
|
67
72
|
pi.nodeExe,
|
|
68
73
|
['--require', preload, pi.cli, ...process.argv.slice(2)],
|
package/package.json
CHANGED
|
@@ -5,19 +5,18 @@ const path = require('path');
|
|
|
5
5
|
const { isFakePath, toRemotePath } = require('./path-mapper');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Patch child_process
|
|
8
|
+
* Patch child_process spawn/spawnSync/execSync/execFileSync so that commands
|
|
9
9
|
* whose cwd lands inside fakeRoot are transparently redirected to run on
|
|
10
10
|
* the remote machine via SSH.
|
|
11
11
|
*
|
|
12
|
-
* Pi's bash tool uses spawn/spawnSync exclusively, so this covers all shell
|
|
13
|
-
* command execution (ls, cat, grep, npm run, etc.).
|
|
14
|
-
*
|
|
15
12
|
* @param {string} fakeRoot - Local fake root directory (real path, no symlinks)
|
|
16
13
|
* @param {string} remote - SSH target, e.g. 'user@host'
|
|
17
14
|
*/
|
|
18
15
|
function patchChildProcess(fakeRoot, remote) {
|
|
19
|
-
const origSpawn
|
|
20
|
-
const origSpawnSync
|
|
16
|
+
const origSpawn = cp.spawn.bind(cp);
|
|
17
|
+
const origSpawnSync = cp.spawnSync.bind(cp);
|
|
18
|
+
const origExecSync = cp.execSync.bind(cp);
|
|
19
|
+
const origExecFileSync = cp.execFileSync.bind(cp);
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* Resolve the effective cwd from spawn options.
|
|
@@ -44,6 +43,7 @@ function patchChildProcess(fakeRoot, remote) {
|
|
|
44
43
|
* - node / node.exe: pi's internal tooling (session export, compaction, …)
|
|
45
44
|
* spawns Node with large payloads as args; redirecting those over SSH would
|
|
46
45
|
* produce an ENAMETOOLONG error on Windows and is never correct anyway.
|
|
46
|
+
* - pi / pii: need local internet access for the Claude API and auth.
|
|
47
47
|
*/
|
|
48
48
|
function isLocalOnly(file) {
|
|
49
49
|
const name = normalizeCmd(file).toLowerCase();
|
|
@@ -51,15 +51,30 @@ function patchChildProcess(fakeRoot, remote) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* When pi spawns 'pi' or 'pii' as a subprocess (e.g. the /review skill),
|
|
55
|
+
* the bare command name won't be found in PATH on Windows because pi is a
|
|
56
|
+
* JS file located by pii's findPi(). pii.js sets PI_BRIDGE_CLI / PI_BRIDGE_NODE
|
|
57
|
+
* in the environment so we can resolve the correct executable here.
|
|
58
|
+
*
|
|
59
|
+
* Returns { nodeExe, cli } when applicable, null otherwise.
|
|
60
|
+
*/
|
|
61
|
+
function resolvePiExe(file) {
|
|
62
|
+
const name = normalizeCmd(file).toLowerCase();
|
|
63
|
+
if (name !== 'pi' && name !== 'pii') return null;
|
|
64
|
+
const cli = process.env.PI_BRIDGE_CLI;
|
|
65
|
+
if (!cli) return null;
|
|
66
|
+
return { nodeExe: process.env.PI_BRIDGE_NODE || process.execPath, cli };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build SSH args for a file + args invocation:
|
|
71
|
+
* cd to remote cwd, then run the command with shell-quoted arguments.
|
|
56
72
|
*/
|
|
57
73
|
function buildSshArgs(file, args, opts) {
|
|
58
74
|
file = normalizeCmd(file);
|
|
59
|
-
const fakeCwd
|
|
60
|
-
const remoteCwd
|
|
75
|
+
const fakeCwd = effectiveCwd(opts);
|
|
76
|
+
const remoteCwd = toRemotePath(fakeCwd, fakeRoot);
|
|
61
77
|
|
|
62
|
-
// Reconstruct the command string with shell-safe quoting
|
|
63
78
|
const argStr = (args || [])
|
|
64
79
|
.map(function(a) { return "'" + String(a).replace(/'/g, "'\\''") + "'"; })
|
|
65
80
|
.join(' ');
|
|
@@ -68,15 +83,28 @@ function patchChildProcess(fakeRoot, remote) {
|
|
|
68
83
|
return ['-o', 'BatchMode=yes', remote, remoteCmd];
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Build SSH args for a raw shell command string (used by execSync).
|
|
88
|
+
*/
|
|
89
|
+
function buildSshArgsForShell(command, opts) {
|
|
90
|
+
const fakeCwd = effectiveCwd(opts);
|
|
91
|
+
const remoteCwd = toRemotePath(fakeCwd, fakeRoot);
|
|
92
|
+
const remoteCmd = 'cd ' + JSON.stringify(remoteCwd) + ' && ' + command;
|
|
93
|
+
return ['-o', 'BatchMode=yes', remote, remoteCmd];
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
// ── spawn ────────────────────────────────────────────────────────────────────
|
|
72
97
|
|
|
73
98
|
cp.spawn = function patchedSpawn(file, args, opts) {
|
|
74
|
-
// Normalise overloaded signature: spawn(file) / spawn(file, opts) / spawn(file, args, opts)
|
|
75
99
|
if (!Array.isArray(args)) { opts = args; args = []; }
|
|
76
100
|
opts = opts || {};
|
|
77
101
|
|
|
78
102
|
const cwd = effectiveCwd(opts);
|
|
79
|
-
if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file))
|
|
103
|
+
if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file)) {
|
|
104
|
+
const piExe = resolvePiExe(file);
|
|
105
|
+
if (piExe) return origSpawn(piExe.nodeExe, [piExe.cli, ...args], opts);
|
|
106
|
+
return origSpawn(file, args, opts);
|
|
107
|
+
}
|
|
80
108
|
|
|
81
109
|
return origSpawn('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
|
|
82
110
|
};
|
|
@@ -88,10 +116,41 @@ function patchChildProcess(fakeRoot, remote) {
|
|
|
88
116
|
opts = opts || {};
|
|
89
117
|
|
|
90
118
|
const cwd = effectiveCwd(opts);
|
|
91
|
-
if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file))
|
|
119
|
+
if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file)) {
|
|
120
|
+
const piExe = resolvePiExe(file);
|
|
121
|
+
if (piExe) return origSpawnSync(piExe.nodeExe, [piExe.cli, ...args], opts);
|
|
122
|
+
return origSpawnSync(file, args, opts);
|
|
123
|
+
}
|
|
92
124
|
|
|
93
125
|
return origSpawnSync('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
|
|
94
126
|
};
|
|
127
|
+
|
|
128
|
+
// ── execSync ─────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
cp.execSync = function patchedExecSync(command, opts) {
|
|
131
|
+
opts = opts || {};
|
|
132
|
+
|
|
133
|
+
const cwd = effectiveCwd(opts);
|
|
134
|
+
if (!isFakePath(cwd, fakeRoot)) return origExecSync(command, opts);
|
|
135
|
+
|
|
136
|
+
return origExecFileSync('ssh', buildSshArgsForShell(command, opts), Object.assign({}, opts, { cwd: undefined }));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ── execFileSync ─────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
cp.execFileSync = function patchedExecFileSync(file, args, opts) {
|
|
142
|
+
if (!Array.isArray(args)) { opts = args; args = []; }
|
|
143
|
+
opts = opts || {};
|
|
144
|
+
|
|
145
|
+
const cwd = effectiveCwd(opts);
|
|
146
|
+
if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file)) {
|
|
147
|
+
const piExe = resolvePiExe(file);
|
|
148
|
+
if (piExe) return origExecFileSync(piExe.nodeExe, [piExe.cli, ...args], opts);
|
|
149
|
+
return origExecFileSync(file, args, opts);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return origExecFileSync('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
|
|
153
|
+
};
|
|
95
154
|
}
|
|
96
155
|
|
|
97
156
|
module.exports = { patchChildProcess };
|
package/src/local/http-client.js
CHANGED
|
@@ -93,31 +93,37 @@ req.on('error', (e) => {
|
|
|
93
93
|
* @throws - On HTTP errors or non-2xx status codes
|
|
94
94
|
*/
|
|
95
95
|
function httpPost(port, token, urlPath, body) {
|
|
96
|
+
// Body is passed via stdin (not embedded in the script) to avoid ENAMETOOLONG
|
|
97
|
+
// on Windows when writing large files such as session exports.
|
|
96
98
|
const script = `
|
|
97
99
|
const http = require('http');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
res
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
let raw = '';
|
|
101
|
+
process.stdin.setEncoding('utf8');
|
|
102
|
+
process.stdin.on('data', (d) => { raw += d; });
|
|
103
|
+
process.stdin.on('end', () => {
|
|
104
|
+
const bodyBuf = Buffer.from(raw, 'utf8');
|
|
105
|
+
let resBody = '';
|
|
106
|
+
let statusCode = 0;
|
|
107
|
+
const req = http.request(
|
|
108
|
+
{ method:'POST', hostname:'127.0.0.1', port:${JSON.stringify(port)}, path:${JSON.stringify(urlPath)},
|
|
109
|
+
headers:{'x-token':${JSON.stringify(token)},'Content-Type':'text/plain; charset=utf-8','Content-Length':bodyBuf.length} },
|
|
110
|
+
(res) => {
|
|
111
|
+
statusCode = res.statusCode;
|
|
112
|
+
res.setEncoding('utf8');
|
|
113
|
+
res.on('data', (d) => { resBody += d; });
|
|
114
|
+
res.on('end', () => {
|
|
115
|
+
process.stdout.write(JSON.stringify({ ok: statusCode >= 200 && statusCode < 300, status: statusCode, body: resBody }));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
req.on('error', (e) => {
|
|
120
|
+
process.stdout.write(JSON.stringify({ ok: false, status: 0, body: e.message }));
|
|
121
|
+
});
|
|
122
|
+
req.write(bodyBuf);
|
|
123
|
+
req.end();
|
|
116
124
|
});
|
|
117
|
-
req.write(bodyBuf);
|
|
118
|
-
req.end();
|
|
119
125
|
`;
|
|
120
|
-
const raw = withRetry(() => execFileSync(process.execPath, ['-e', script], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }));
|
|
126
|
+
const raw = withRetry(() => execFileSync(process.execPath, ['-e', script], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], input: body }));
|
|
121
127
|
const result = JSON.parse(raw);
|
|
122
128
|
if (!result.ok) {
|
|
123
129
|
const err = new Error('HTTP ' + result.status + ': ' + result.body);
|