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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bridge",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "Transparent filesystem bridge that makes pi run natively on a remote machine over SSH",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -5,19 +5,18 @@ const path = require('path');
5
5
  const { isFakePath, toRemotePath } = require('./path-mapper');
6
6
 
7
7
  /**
8
- * Patch child_process.spawn and child_process.spawnSync so that commands
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 = cp.spawn.bind(cp);
20
- const origSpawnSync = cp.spawnSync.bind(cp);
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
- * Build the SSH-redirected command: cd to remote cwd, then run the
55
- * original command with its arguments, all shell-quoted.
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 = effectiveCwd(opts);
60
- const remoteCwd = toRemotePath(fakeCwd, fakeRoot);
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)) return origSpawn(file, args, opts);
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)) return origSpawnSync(file, args, opts);
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 };
@@ -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
- const body = ${JSON.stringify(body)};
99
- const bodyBuf = Buffer.from(body, 'utf8');
100
- let resBody = '';
101
- let statusCode = 0;
102
- const req = http.request(
103
- { method:'POST', hostname:'127.0.0.1', port:${JSON.stringify(port)}, path:${JSON.stringify(urlPath)},
104
- headers:{'x-token':${JSON.stringify(token)},'Content-Type':'text/plain; charset=utf-8','Content-Length':bodyBuf.length} },
105
- (res) => {
106
- statusCode = res.statusCode;
107
- res.setEncoding('utf8');
108
- res.on('data', (d) => { resBody += d; });
109
- res.on('end', () => {
110
- process.stdout.write(JSON.stringify({ ok: statusCode >= 200 && statusCode < 300, status: statusCode, body: resBody }));
111
- });
112
- }
113
- );
114
- req.on('error', (e) => {
115
- process.stdout.write(JSON.stringify({ ok: false, status: 0, body: e.message }));
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);