pi-bridge 0.3.1 → 0.3.3
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 +49 -29
- package/package.json +1 -1
- package/src/local/fs-patch.js +117 -0
- package/src/remote/handlers.js +26 -0
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,8 +76,9 @@ 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
|
}
|
package/package.json
CHANGED
package/src/local/fs-patch.js
CHANGED
|
@@ -97,6 +97,10 @@ function patchFs(fakeRoot, port, token) {
|
|
|
97
97
|
renameSync: fs.renameSync.bind(fs),
|
|
98
98
|
copyFileSync: fs.copyFileSync.bind(fs),
|
|
99
99
|
realpathSync: fs.realpathSync.bind(fs),
|
|
100
|
+
mkdtempSync: fs.mkdtempSync.bind(fs),
|
|
101
|
+
rmdirSync: fs.rmdirSync.bind(fs),
|
|
102
|
+
chmodSync: fs.chmodSync.bind(fs),
|
|
103
|
+
cpSync: fs.cpSync ? fs.cpSync.bind(fs) : undefined,
|
|
100
104
|
createWriteStream: fs.createWriteStream.bind(fs),
|
|
101
105
|
createReadStream: fs.createReadStream.bind(fs),
|
|
102
106
|
};
|
|
@@ -116,6 +120,11 @@ function patchFs(fakeRoot, port, token) {
|
|
|
116
120
|
rename: fs.promises.rename.bind(fs.promises),
|
|
117
121
|
copyFile: fs.promises.copyFile.bind(fs.promises),
|
|
118
122
|
open: fs.promises.open.bind(fs.promises),
|
|
123
|
+
mkdtemp: fs.promises.mkdtemp.bind(fs.promises),
|
|
124
|
+
realpath: fs.promises.realpath.bind(fs.promises),
|
|
125
|
+
rmdir: fs.promises.rmdir.bind(fs.promises),
|
|
126
|
+
chmod: fs.promises.chmod.bind(fs.promises),
|
|
127
|
+
cp: fs.promises.cp ? fs.promises.cp.bind(fs.promises) : undefined,
|
|
119
128
|
};
|
|
120
129
|
|
|
121
130
|
// Resolve any path (relative or absolute) to absolute before checking fake root
|
|
@@ -246,6 +255,58 @@ function patchFs(fakeRoot, port, token) {
|
|
|
246
255
|
httpPost(port, token, '/mkdir?path=' + encodeURIComponent(rpath(s)), '');
|
|
247
256
|
};
|
|
248
257
|
|
|
258
|
+
/**
|
|
259
|
+
* The remote runs the real mkdtempSync and returns only the generated
|
|
260
|
+
* directory name, so uniqueness keeps Node's semantics and no remote path has
|
|
261
|
+
* to be mapped back to a local one.
|
|
262
|
+
*/
|
|
263
|
+
fs.mkdtempSync = function patchedMkdtempSync(prefix, opts) {
|
|
264
|
+
const s = abs(prefix);
|
|
265
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.mkdtempSync(prefix, opts);
|
|
266
|
+
const name = httpPost(port, token, '/mkdtemp?path=' + encodeURIComponent(rpath(s)), '');
|
|
267
|
+
return path.join(path.dirname(s), String(name).trim());
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
fs.rmdirSync = function patchedRmdirSync(p, opts) {
|
|
271
|
+
const s = abs(p);
|
|
272
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.rmdirSync(p, opts);
|
|
273
|
+
const recursive = (opts && opts.recursive) ? '1' : '0';
|
|
274
|
+
httpPost(port, token, '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive, '');
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
fs.chmodSync = function patchedChmodSync(p, mode) {
|
|
278
|
+
const s = abs(p);
|
|
279
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.chmodSync(p, mode);
|
|
280
|
+
// Octal on the wire: String(0o755) is "493", which the server would have to
|
|
281
|
+
// guess at. toString(8) keeps the intent explicit for numbers and strings.
|
|
282
|
+
const octal = typeof mode === 'number' ? mode.toString(8) : String(mode).replace(/^0o?/i, '');
|
|
283
|
+
httpPost(port, token, '/chmod?path=' + encodeURIComponent(rpath(s)) + '&mode=' + octal, '');
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Recursive copy: the remote /copy route handles one file, so directories are
|
|
287
|
+
// walked here through the already-patched calls.
|
|
288
|
+
fs.cpSync = function patchedCpSync(src, dest, opts) {
|
|
289
|
+
const srcS = abs(src);
|
|
290
|
+
const destS = abs(dest);
|
|
291
|
+
const involved = (srcS && isFakePath(srcS, fakeRoot)) || (destS && isFakePath(destS, fakeRoot));
|
|
292
|
+
if (!involved) {
|
|
293
|
+
if (!orig.cpSync) throw new Error('fs.cpSync is not available in this Node version');
|
|
294
|
+
return orig.cpSync(src, dest, opts);
|
|
295
|
+
}
|
|
296
|
+
const stats = fs.statSync(srcS);
|
|
297
|
+
if (!stats.isDirectory()) {
|
|
298
|
+
fs.copyFileSync(srcS, destS);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!(opts && opts.recursive)) {
|
|
302
|
+
throw Object.assign(new Error('EISDIR: illegal operation on a directory, cp ' + srcS), { code: 'EISDIR' });
|
|
303
|
+
}
|
|
304
|
+
fs.mkdirSync(destS, { recursive: true });
|
|
305
|
+
for (const entry of fs.readdirSync(srcS)) {
|
|
306
|
+
fs.cpSync(path.join(srcS, entry), path.join(destS, entry), opts);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
249
310
|
fs.unlinkSync = function patchedUnlinkSync(p) {
|
|
250
311
|
const s = abs(p);
|
|
251
312
|
if (!s || !isFakePath(s, fakeRoot)) return orig.unlinkSync(p);
|
|
@@ -501,6 +562,56 @@ function patchFs(fakeRoot, port, token) {
|
|
|
501
562
|
});
|
|
502
563
|
};
|
|
503
564
|
|
|
565
|
+
fs.promises.rename = async function patchedRename(from, to) {
|
|
566
|
+
const fromS = abs(from);
|
|
567
|
+
const toS = abs(to);
|
|
568
|
+
if (!fromS || !isFakePath(fromS, fakeRoot)) return origP.rename(from, to);
|
|
569
|
+
await new Promise(function(resolve, reject) {
|
|
570
|
+
const req = http.request(
|
|
571
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
572
|
+
path: '/rename?from=' + encodeURIComponent(rpath(fromS)) + '&to=' + encodeURIComponent(toS ? rpath(toS) : String(to)),
|
|
573
|
+
headers: { 'x-token': token, 'Content-Length': 0 } },
|
|
574
|
+
function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
|
|
575
|
+
);
|
|
576
|
+
req.on('error', reject); req.end();
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
fs.promises.mkdtemp = async function patchedMkdtemp(prefix, opts) {
|
|
581
|
+
const s = abs(prefix);
|
|
582
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.mkdtemp(prefix, opts);
|
|
583
|
+
return fs.mkdtempSync(prefix, opts);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
fs.promises.realpath = async function patchedRealpath(p, opts) {
|
|
587
|
+
const s = abs(p);
|
|
588
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.realpath(p, opts);
|
|
589
|
+
return s;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
fs.promises.rmdir = async function patchedRmdir(p, opts) {
|
|
593
|
+
const s = abs(p);
|
|
594
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.rmdir(p, opts);
|
|
595
|
+
return fs.promises.rm(p, Object.assign({}, opts));
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
fs.promises.chmod = async function patchedChmod(p, mode) {
|
|
599
|
+
const s = abs(p);
|
|
600
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.chmod(p, mode);
|
|
601
|
+
fs.chmodSync(p, mode);
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
fs.promises.cp = async function patchedCp(src, dest, opts) {
|
|
605
|
+
const srcS = abs(src);
|
|
606
|
+
const destS = abs(dest);
|
|
607
|
+
const involved = (srcS && isFakePath(srcS, fakeRoot)) || (destS && isFakePath(destS, fakeRoot));
|
|
608
|
+
if (!involved) {
|
|
609
|
+
if (!origP.cp) throw new Error('fs.promises.cp is not available in this Node version');
|
|
610
|
+
return origP.cp(src, dest, opts);
|
|
611
|
+
}
|
|
612
|
+
fs.cpSync(src, dest, opts);
|
|
613
|
+
};
|
|
614
|
+
|
|
504
615
|
// fs.promises.open: return a minimal FileHandle for reading
|
|
505
616
|
fs.promises.open = async function patchedOpen(p, flags, mode) {
|
|
506
617
|
const s = typeof p === 'string' ? abs(p) : null;
|
|
@@ -651,6 +762,12 @@ function patchFs(fakeRoot, port, token) {
|
|
|
651
762
|
fsp.mkdir = fs.promises.mkdir;
|
|
652
763
|
fsp.rm = fs.promises.rm;
|
|
653
764
|
fsp.unlink = fs.promises.unlink;
|
|
765
|
+
fsp.rename = fs.promises.rename;
|
|
766
|
+
fsp.mkdtemp = fs.promises.mkdtemp;
|
|
767
|
+
fsp.realpath = fs.promises.realpath;
|
|
768
|
+
fsp.rmdir = fs.promises.rmdir;
|
|
769
|
+
fsp.chmod = fs.promises.chmod;
|
|
770
|
+
fsp.cp = fs.promises.cp;
|
|
654
771
|
fsp.copyFile = fs.promises.copyFile;
|
|
655
772
|
fsp.open = fs.promises.open;
|
|
656
773
|
}
|
package/src/remote/handlers.js
CHANGED
|
@@ -123,6 +123,32 @@ function createHandlers() {
|
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
if (req.method === 'POST' && pathname === '/mkdtemp') {
|
|
127
|
+
const prefix = url.searchParams.get('path');
|
|
128
|
+
if (!prefix) return sendError(res, 400, 'Missing path parameter');
|
|
129
|
+
// Real mkdtemp, so uniqueness is the kernel's problem rather than a
|
|
130
|
+
// check-then-create race. Only the generated name is returned; the
|
|
131
|
+
// caller already knows the directory.
|
|
132
|
+
const created = fs.mkdtempSync(prefix);
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
134
|
+
res.end(require('path').basename(created));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (req.method === 'POST' && pathname === '/chmod') {
|
|
139
|
+
const p = url.searchParams.get('path');
|
|
140
|
+
const mode = url.searchParams.get('mode');
|
|
141
|
+
if (!p) return sendError(res, 400, 'Missing path parameter');
|
|
142
|
+
if (!mode) return sendError(res, 400, 'Missing mode parameter');
|
|
143
|
+
// The client always sends octal digits, so the wire value cannot be
|
|
144
|
+
// mistaken for a decimal bitmask.
|
|
145
|
+
if (!/^[0-7]{3,4}$/.test(mode)) return sendError(res, 400, 'Invalid mode parameter');
|
|
146
|
+
fs.chmodSync(p, parseInt(mode, 8));
|
|
147
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
148
|
+
res.end('ok');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
if (req.method === 'POST' && pathname === '/delete') {
|
|
127
153
|
const p = url.searchParams.get('path');
|
|
128
154
|
if (!p) return sendError(res, 400, 'Missing path parameter');
|