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 +54 -29
- package/package.json +1 -1
- package/src/local/child-process-patch.js +31 -3
- package/src/local/http-client.js +27 -21
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
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
|
-
'
|
|
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
|
@@ -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))
|
|
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))
|
|
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))
|
|
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
|
};
|
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);
|