bitpull 0.1.28 → 0.1.29
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/README.md +4 -2
- package/bitpull.cjs +297 -44
- package/package.json +1 -1
- package/ui/ide-terminal.js +1 -1
- package/ui/terminal.js +63 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bitpull",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "BitPull (bitpull.cloud): share signed, AI-readable code, computing power and open AI models with AI tools through MCP. CLI and MCP server.",
|
|
5
5
|
"mcpName": "io.github.SamuealPK/bitpull",
|
|
6
6
|
"bin": {
|
package/ui/ide-terminal.js
CHANGED
|
@@ -12,7 +12,7 @@ function register() {
|
|
|
12
12
|
let current = null;
|
|
13
13
|
const start = () => {
|
|
14
14
|
if (term || !host || !opts || !opts.folder) return;
|
|
15
|
-
term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t, fontSize: opts.phone ? 12 : 13 });
|
|
15
|
+
term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t, fontSize: opts.phone ? 12 : 13, keep: !!opts.phone });
|
|
16
16
|
};
|
|
17
17
|
ide.terminal.register({
|
|
18
18
|
mount(container, o) {
|
package/ui/terminal.js
CHANGED
|
@@ -88,11 +88,25 @@ export function mountTerminalSetting(el, { token, t = (k) => k, onChange } = {})
|
|
|
88
88
|
terminalStatus(token).then(render, () => render({ enabled: false, available: false }));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** This phone's name for its shells on the computer: the same every time the page opens. */
|
|
92
|
+
function keptName() {
|
|
93
|
+
try {
|
|
94
|
+
let n = localStorage.getItem("bitpull_term_keep");
|
|
95
|
+
if (!n || !/^[A-Za-z0-9_-]{16,64}$/.test(n)) {
|
|
96
|
+
n = Array.from(crypto.getRandomValues(new Uint8Array(18)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
97
|
+
localStorage.setItem("bitpull_term_keep", n);
|
|
98
|
+
}
|
|
99
|
+
return n;
|
|
100
|
+
} catch {
|
|
101
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(18)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* Mounts the terminal in `el`. `cwd` must be a folder the person authorised; `t` translates.
|
|
93
107
|
* Returns the controls the page and the Projects view use.
|
|
94
108
|
*/
|
|
95
|
-
export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } = {}) {
|
|
109
|
+
export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13, keep = false } = {}) {
|
|
96
110
|
const ended = (code) => String(t("term.exited", { code })).replace("{code}", String(code));
|
|
97
111
|
addCss();
|
|
98
112
|
el.replaceChildren();
|
|
@@ -126,6 +140,9 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
126
140
|
const waiting = new Map();
|
|
127
141
|
let nextRef = 1;
|
|
128
142
|
const byId = new Map();
|
|
143
|
+
// On a phone the shells wait on the computer for an hour when the phone goes away, under this name.
|
|
144
|
+
const keepName = keep ? keptName() : null;
|
|
145
|
+
let firstReady = true;
|
|
129
146
|
|
|
130
147
|
const message = (key, withEnable) => {
|
|
131
148
|
panesEl.replaceChildren();
|
|
@@ -201,11 +218,16 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
201
218
|
return { term, fit, box, readonly };
|
|
202
219
|
};
|
|
203
220
|
|
|
204
|
-
const startShell = (pane) => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
const startShell = (pane, existing) => {
|
|
222
|
+
if (existing) {
|
|
223
|
+
pane.id = existing;
|
|
224
|
+
byId.set(existing, pane);
|
|
225
|
+
} else {
|
|
226
|
+
pane.ref = nextRef++;
|
|
227
|
+
pane.go = () => ws.send(JSON.stringify({ t: "open", ref: pane.ref, cwd, cols: pane.term.cols, rows: pane.term.rows }));
|
|
228
|
+
waiting.set(pane.ref, pane);
|
|
229
|
+
if (ready) pane.go();
|
|
230
|
+
}
|
|
209
231
|
pane.term.onData((d) => {
|
|
210
232
|
// "Ctrl" pressed on a phone's extra keys: the next letter becomes Ctrl+letter.
|
|
211
233
|
if (ctrlNext && d.length === 1) {
|
|
@@ -224,7 +246,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
224
246
|
if (ws && ws.readyState <= 1) return;
|
|
225
247
|
ready = false;
|
|
226
248
|
ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/api/terminal`);
|
|
227
|
-
ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token }));
|
|
249
|
+
ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token, ...(keepName ? { keep: keepName } : {}) }));
|
|
228
250
|
ws.onmessage = (e) => {
|
|
229
251
|
let m;
|
|
230
252
|
try {
|
|
@@ -234,7 +256,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
234
256
|
}
|
|
235
257
|
if (m.t === "ready") {
|
|
236
258
|
ready = true;
|
|
259
|
+
if (keepName) pickUp(m.kept || []);
|
|
237
260
|
for (const p of waiting.values()) p.go();
|
|
261
|
+
if (keepName && firstReady && !tabs.length) newTab();
|
|
262
|
+
firstReady = false;
|
|
263
|
+
} else if (m.t === "attached") {
|
|
264
|
+
// The computer sends the screen again as it is now: it replaces what this page still showed.
|
|
265
|
+
byId.get(m.id)?.term.reset();
|
|
266
|
+
fitAll();
|
|
238
267
|
} else if (m.t === "opened") {
|
|
239
268
|
const p = waiting.get(m.ref);
|
|
240
269
|
if (!p) return;
|
|
@@ -250,6 +279,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
250
279
|
byId.delete(m.id);
|
|
251
280
|
p.id = undefined;
|
|
252
281
|
}
|
|
282
|
+
} else if (m.t === "error" && m.error === "no_terminal" && typeof m.id === "number") {
|
|
283
|
+
// Gone while the phone was away (more than an hour, or the computer restarted).
|
|
284
|
+
const p = byId.get(m.id);
|
|
285
|
+
if (p) {
|
|
286
|
+
byId.delete(m.id);
|
|
287
|
+
p.id = undefined;
|
|
288
|
+
p.term.write(`\r\n\x1b[2m${t("term.gone")}\x1b[0m\r\n`);
|
|
289
|
+
}
|
|
253
290
|
} else if (m.t === "error") {
|
|
254
291
|
const p = waiting.get(m.ref);
|
|
255
292
|
waiting.delete(m.ref);
|
|
@@ -260,6 +297,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
260
297
|
ws.onclose = (e) => {
|
|
261
298
|
ready = false;
|
|
262
299
|
if (disposed) return;
|
|
300
|
+
// A phone's connection comes back by itself: its shells are still there.
|
|
301
|
+
if (keepName && e.code !== 4409 && e.code !== 4410 && e.code !== 4401) setTimeout(() => !disposed && connect(), 2000);
|
|
263
302
|
if (e.code === 4409 || e.code === 1006) {
|
|
264
303
|
terminalStatus(token).then((s) => {
|
|
265
304
|
if (!s.available) message("term.unavailable", false);
|
|
@@ -269,6 +308,20 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
269
308
|
};
|
|
270
309
|
};
|
|
271
310
|
|
|
311
|
+
/** Shows again the shells this phone left running: those of this page first, then the others in this folder. */
|
|
312
|
+
function pickUp(kept) {
|
|
313
|
+
for (const p of byId.values()) ws.send(JSON.stringify({ t: "attach", id: p.id, cols: p.term.cols, rows: p.term.rows }));
|
|
314
|
+
for (const k of kept) {
|
|
315
|
+
if (byId.has(k.id) || k.cwd !== cwd) continue;
|
|
316
|
+
const pane = makePane(false);
|
|
317
|
+
const tab = { title: "", panes: [pane] };
|
|
318
|
+
tabs.push(tab);
|
|
319
|
+
show(tab);
|
|
320
|
+
startShell(pane, k.id);
|
|
321
|
+
ws.send(JSON.stringify({ t: "attach", id: k.id, cols: pane.term.cols, rows: pane.term.rows }));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
272
325
|
function newTab() {
|
|
273
326
|
const pane = makePane(false);
|
|
274
327
|
const tab = { title: "", panes: [pane] };
|
|
@@ -337,7 +390,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
337
390
|
bar.append(tabsEl, button("term.new", "tip.term.new", () => newTab()), button("term.split", "tip.term.split", () => split()), button("term.close", "tip.term.close", () => close()));
|
|
338
391
|
const resize = new ResizeObserver(() => fitAll());
|
|
339
392
|
resize.observe(panesEl);
|
|
340
|
-
// Closing the page ends its shells: the server closes them with the connection too.
|
|
393
|
+
// Closing the page ends its shells: the server closes them with the connection too. A phone's wait an hour.
|
|
341
394
|
window.addEventListener("pagehide", dispose);
|
|
342
395
|
|
|
343
396
|
terminalStatus(token).then(
|
|
@@ -346,7 +399,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
|
|
|
346
399
|
if (!s.available) return message("term.unavailable", false);
|
|
347
400
|
if (!s.enabled) return message("term.off", true);
|
|
348
401
|
connect();
|
|
349
|
-
|
|
402
|
+
// On a phone, the first answer says whether shells are waiting; a new one opens only if none is.
|
|
403
|
+
if (!keepName) newTab();
|
|
350
404
|
},
|
|
351
405
|
() => message("term.failed", false),
|
|
352
406
|
);
|