pi-bridge 0.3.0 → 0.3.2

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
@@ -14,37 +14,56 @@ if (process.argv.includes('--version') || process.argv.includes('-v') || process
14
14
  process.exit(0);
15
15
  }
16
16
 
17
- function findPi() {
18
- // 1. Standard npm global: both packages share the same node_modules prefix
19
- const shared = path.resolve(__dirname, '..', '..', '@mariozechner', 'pi-coding-agent', 'dist', 'cli.js');
20
- if (fs.existsSync(shared)) {
21
- return { cli: shared, nodeExe: process.execPath };
17
+ // List every `pi` on the PATH that pii inherited from its launching shell.
18
+ // We resolve against that PATH (not a fresh `-NoProfile` shell) so that bundled
19
+ // installs added to PATH via the user's profile are actually visible.
20
+ function locateOnPath(cmd) {
21
+ const finder = os.platform() === 'win32' ? 'where.exe' : 'which';
22
+ try {
23
+ const out = execFileSync(finder, [cmd], { encoding: 'utf8' });
24
+ return out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
25
+ } catch (_) {
26
+ return [];
22
27
  }
28
+ }
23
29
 
24
- // 2. Unix GitHub clone: which pi returns the JS file directly
25
- if (os.platform() !== 'win32') {
26
- try {
27
- const wrapper = execFileSync('which', ['pi'], { encoding: 'utf8' }).trim();
28
- if (fs.existsSync(wrapper)) return { cli: wrapper, nodeExe: process.execPath };
29
- } catch (_) {}
30
+ function findPi() {
31
+ // 1. Standard npm global: pii and pi share the same node_modules prefix.
32
+ // Prefer the current @earendil-works scope; keep @mariozechner as a legacy
33
+ // fallback for machines that installed pi before it moved off that scope.
34
+ const sharedCandidates = [
35
+ path.resolve(__dirname, '..', '..', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js'),
36
+ path.resolve(__dirname, '..', '..', '@mariozechner', 'pi-coding-agent', 'dist', 'cli.js'),
37
+ ];
38
+ for (const cli of sharedCandidates) {
39
+ if (fs.existsSync(cli)) return { cli, nodeExe: process.execPath };
30
40
  }
31
41
 
32
- // 3. Fallback: PowerShell Get-Command — handles custom/bundled Windows installs
33
- // where pi lives outside the standard npm prefix. Also picks up the bundled
34
- // node.exe that shipped alongside pi (required for version-matched execution).
35
- if (os.platform() === 'win32') {
36
- try {
37
- const wrapper = execFileSync(
38
- 'powershell',
39
- ['-NoProfile', '-Command', '(Get-Command pi -ErrorAction Stop).Source'],
40
- { encoding: 'utf8' }
41
- ).trim();
42
- const basedir = path.dirname(wrapper);
43
- const cli = wrapper.replace(/\.(ps1|cmd)$/i, '.js');
44
- const localNode = path.join(basedir, 'node.exe');
45
- const nodeExe = fs.existsSync(localNode) ? localNode : process.execPath;
46
- if (fs.existsSync(cli)) return { cli, nodeExe };
47
- } catch (_) {}
42
+ // 2. Resolve `pi` from PATH and follow it to its real JS entrypoint. The
43
+ // pi.dev installer (curl https://pi.dev/install.sh | sh) drops a bundled
44
+ // Node plus a bare `bin/pi` symlink that points straight at the package's
45
+ // cli.js — often outside any npm prefix — so we realpath the symlink rather
46
+ // than guessing filenames. Also covers npm shims and dev clones (which
47
+ // expose pi as a symlink to cli.js too), and a bundled node next to pi is
48
+ // preferred for version-matched execution.
49
+ const exe = os.platform() === 'win32' ? '.exe' : '';
50
+ for (const cmd of locateOnPath('pi')) {
51
+ let real;
52
+ try { real = fs.realpathSync(cmd); } catch (_) { real = cmd; }
53
+
54
+ let cli = null;
55
+ if (/\.[cm]?js$/i.test(real) && fs.existsSync(real)) {
56
+ cli = real; // symlink/shim -> cli.js
57
+ } else if (fs.existsSync(path.join(path.dirname(cmd), 'pi.js'))) {
58
+ cli = path.join(path.dirname(cmd), 'pi.js'); // bundled wrapper + sibling
59
+ } else if (!/\.(cmd|ps1|bat|exe)$/i.test(real)) {
60
+ cli = real; // shebang JS with no extension
61
+ }
62
+ if (!cli) continue;
63
+
64
+ const localNode = path.join(path.dirname(cmd), 'node' + exe);
65
+ const nodeExe = fs.existsSync(localNode) ? localNode : process.execPath;
66
+ return { cli, nodeExe };
48
67
  }
49
68
 
50
69
  return null;
@@ -57,12 +76,18 @@ try {
57
76
 
58
77
  if (!pi) {
59
78
  process.stderr.write(
60
- 'pii: Could not find pi.\n' +
61
- 'Install it with: npm install -g @mariozechner/pi-coding-agent\n'
79
+ 'pii: Could not find pi on your PATH.\n' +
80
+ 'If pi is already installed, re-open your shell so the install is on PATH, then retry.\n' +
81
+ 'Otherwise install pi: curl -fsSL https://pi.dev/install.sh | sh\n'
62
82
  );
63
83
  process.exit(1);
64
84
  }
65
85
 
86
+ // Expose resolved pi paths so child-process-patch can re-spawn pi correctly
87
+ // when the review skill (or any internal tool) calls `pi` as a subprocess.
88
+ process.env.PI_BRIDGE_CLI = pi.cli;
89
+ process.env.PI_BRIDGE_NODE = pi.nodeExe;
90
+
66
91
  const result = spawnSync(
67
92
  pi.nodeExe,
68
93
  ['--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.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Transparent filesystem bridge that makes pi run natively on a remote machine over SSH",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -50,6 +50,22 @@ function patchChildProcess(fakeRoot, remote) {
50
50
  return name === 'node' || name === 'pi' || name === 'pii';
51
51
  }
52
52
 
53
+ /**
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
+
53
69
  /**
54
70
  * Build SSH args for a file + args invocation:
55
71
  * cd to remote cwd, then run the command with shell-quoted arguments.
@@ -84,7 +100,11 @@ function patchChildProcess(fakeRoot, remote) {
84
100
  opts = opts || {};
85
101
 
86
102
  const cwd = effectiveCwd(opts);
87
- 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
+ }
88
108
 
89
109
  return origSpawn('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
90
110
  };
@@ -96,7 +116,11 @@ function patchChildProcess(fakeRoot, remote) {
96
116
  opts = opts || {};
97
117
 
98
118
  const cwd = effectiveCwd(opts);
99
- 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
+ }
100
124
 
101
125
  return origSpawnSync('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
102
126
  };
@@ -119,7 +143,11 @@ function patchChildProcess(fakeRoot, remote) {
119
143
  opts = opts || {};
120
144
 
121
145
  const cwd = effectiveCwd(opts);
122
- if (!isFakePath(cwd, fakeRoot) || isLocalOnly(file)) return origExecFileSync(file, args, 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
+ }
123
151
 
124
152
  return origExecFileSync('ssh', buildSshArgs(file, args, opts), Object.assign({}, opts, { cwd: undefined }));
125
153
  };
@@ -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);