buninu-linux 0.18.60 → 0.18.62
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/NOTICE.md +46 -46
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/initramfs/bin/tar +32 -15
- package/initramfs/bin/unzip +74 -19
- package/initramfs/buninu/CHANGELOG.md +19 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +21 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/find.js +135 -56
- package/initramfs/buninu/apps/bunmsh/src/main.js +12 -1
- package/initramfs/buninu/apps/bunmsh/src/shell.js +8 -2
- package/initramfs/buninu/apps/bunmsh/test/find.test.js +103 -0
- package/initramfs/buninu/apps/bunmsh/test/interactive-typeahead.test.js +40 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +9 -5
- package/initramfs/buninu/apps/bunmsh/test/reference.test.js +2 -5
- package/initramfs/buninu/apps/bunmsh/test/shell2.test.js +6 -5
- package/initramfs/buninu/apps/bunmsh/test/shell3.test.js +8 -7
- package/initramfs/buninu/apps/bunmsh/test/support/terminal.js +10 -0
- package/initramfs/buninu/apps/jsgotty/bunfig.toml +3 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +14 -1
- package/initramfs/buninu/apps/jsgotty/kitty-flow.js +40 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +32 -4
- package/initramfs/buninu/apps/jsgotty/tests/kitty-flow-browser.test.js +43 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-flow.test.js +55 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/fps-collector.mjs +18 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/fps-debug.js +31 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/js-eval-show-fps.js +35 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-background-debug.js +24 -0
- package/initramfs/buninu/package.json +1 -1
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch.js +50 -15
- package/scripts/npm-packages.md +11 -11
- package/test/archive-bounds.test.js +128 -0
- package/test/fetch-npm.test.js +1 -1
|
@@ -16,6 +16,7 @@ const { TerminalModeTracker } = require("./terminal-modes.js");
|
|
|
16
16
|
const { WebSocketServer } = WebSocket;
|
|
17
17
|
const { ClipboardHost, handleOSC52 } = require("./clipboard.js");
|
|
18
18
|
const clipboardHost = new ClipboardHost();
|
|
19
|
+
const { KittyFrameFlow } = require("./kitty-flow.js");
|
|
19
20
|
|
|
20
21
|
const MSG_INPUT = "1";
|
|
21
22
|
const MSG_PING = "2";
|
|
@@ -1856,7 +1857,8 @@ class PtySession {
|
|
|
1856
1857
|
remoteAddr,
|
|
1857
1858
|
encoding: "null",
|
|
1858
1859
|
readOnly,
|
|
1859
|
-
sendQueue: Promise.resolve()
|
|
1860
|
+
sendQueue: Promise.resolve(),
|
|
1861
|
+
kittyFlow: new KittyFrameFlow(graphic => this.queueSend(ws, MSG_KITTY_GRAPHICS + JSON.stringify(graphic)))
|
|
1860
1862
|
});
|
|
1861
1863
|
if (this.permitWrite && !readOnly && !this.writerWs) {
|
|
1862
1864
|
this.writerWs = ws;
|
|
@@ -1919,6 +1921,7 @@ class PtySession {
|
|
|
1919
1921
|
const writable = this.isWriter(ws);
|
|
1920
1922
|
this.send(MSG_SET_ROLE, JSON.stringify({
|
|
1921
1923
|
role: writable ? "writer" : "viewer",
|
|
1924
|
+
kittyFrameFlow: 1,
|
|
1922
1925
|
writable
|
|
1923
1926
|
}), ws);
|
|
1924
1927
|
}
|
|
@@ -2452,6 +2455,9 @@ class PtySession {
|
|
|
2452
2455
|
}
|
|
2453
2456
|
|
|
2454
2457
|
switch (type) {
|
|
2458
|
+
case "G":
|
|
2459
|
+
try { client.kittyFlow.control(JSON.parse(payload)); } catch {}
|
|
2460
|
+
break;
|
|
2455
2461
|
case "C":
|
|
2456
2462
|
handleOSC52(this, ws, payload, clipboardHost);
|
|
2457
2463
|
break;
|
|
@@ -2580,6 +2586,13 @@ class PtySession {
|
|
|
2580
2586
|
return;
|
|
2581
2587
|
}
|
|
2582
2588
|
|
|
2589
|
+
// Negotiate browser-only image credits; never throttle terminal byte output.
|
|
2590
|
+
if (!binary && client.kittyFlow.enabled && payload[0] === MSG_KITTY_GRAPHICS) {
|
|
2591
|
+
const graphic = JSON.parse(payload.slice(1));
|
|
2592
|
+
if (!graphic.frameSequence && client.kittyFlow.graphic(graphic)) return;
|
|
2593
|
+
}
|
|
2594
|
+
if (!binary && payload[0] === MSG_OUTPUT) client.kittyFlow.output(payload.slice(1));
|
|
2595
|
+
|
|
2583
2596
|
const sendOne = async () => {
|
|
2584
2597
|
await new Promise((resolve, reject) => {
|
|
2585
2598
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Per browser connection. Only replaceable C=1 placements use frame credits;
|
|
2
|
+
// ordinary terminal output, scrolling placements and CLI clients stay ordered.
|
|
3
|
+
class KittyFrameFlow {
|
|
4
|
+
constructor(send) { this.send=send;this.enabled=false;this.visible=true;this.sequence=0;this.inFlight=new Set();this.pending=new Map();this.tail=''; }
|
|
5
|
+
control(message) {
|
|
6
|
+
if(message?.version!==1)return;
|
|
7
|
+
this.enabled=true;
|
|
8
|
+
if(typeof message.visible==='boolean')this.visible=message.visible;
|
|
9
|
+
if(Number.isSafeInteger(message.ack))this.inFlight.delete(message.ack);
|
|
10
|
+
this.flush();
|
|
11
|
+
}
|
|
12
|
+
graphic(graphic) {
|
|
13
|
+
if(!this.enabled)return false;
|
|
14
|
+
if(graphic.kind==='delete'){
|
|
15
|
+
const d=graphic.delete||{};
|
|
16
|
+
for(const [key,g] of this.pending)if(d.scope==='a'||d.scope==='A'||String(g.image.id)===String(d.imageId))this.pending.delete(key);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const c=graphic.control||{},id=c.i||c.I||graphic.image?.id;
|
|
20
|
+
if(graphic.kind!=='placement'||!graphic.image||!id)return false;
|
|
21
|
+
if(String(c.C)!=='1'){this.pending.delete(`${id}:${c.p||'0'}`);return false;}
|
|
22
|
+
const key=`${id}:${c.p||'0'}`;
|
|
23
|
+
// Preserve ordering among different placements when replacing an update.
|
|
24
|
+
this.pending.delete(key);this.pending.set(key,graphic);this.flush();return true;
|
|
25
|
+
}
|
|
26
|
+
output(payload) {
|
|
27
|
+
if(!this.enabled)return;
|
|
28
|
+
const text=this.tail+Buffer.from(payload,'base64').toString('utf8');
|
|
29
|
+
if(text.includes('\x1bc')||text.includes('\f')||/\x1b\[\?(?:1049|1047|47)[hl]/.test(text))this.pending.clear();
|
|
30
|
+
this.tail=text.slice(-32);
|
|
31
|
+
}
|
|
32
|
+
flush() {
|
|
33
|
+
while(this.enabled&&this.visible&&this.inFlight.size<2&&this.pending.size){
|
|
34
|
+
const [key,graphic]=this.pending.entries().next().value;this.pending.delete(key);
|
|
35
|
+
const sequence=++this.sequence;this.inFlight.add(sequence);
|
|
36
|
+
this.send({...graphic,frameSequence:sequence});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
module.exports={KittyFrameFlow};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.4.
|
|
9
|
+
"version": "1.4.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -1079,7 +1079,31 @@
|
|
|
1079
1079
|
state.outputTail = text.slice(-64);
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
1082
|
-
|
|
1082
|
+
// Only a server advertising this capability receives the private control.
|
|
1083
|
+
const frameFlowSockets = new WeakSet();
|
|
1084
|
+
function sendFrameControl(socket, fields) {
|
|
1085
|
+
if (socket?.readyState === 1 && frameFlowSockets.has(socket))
|
|
1086
|
+
socket.send("G" + JSON.stringify({version: 1, ...fields}));
|
|
1087
|
+
}
|
|
1088
|
+
function inspectFrameCapability(socket, data) {
|
|
1089
|
+
if (typeof data !== "string" || data[0] !== "a") return;
|
|
1090
|
+
try {
|
|
1091
|
+
if (JSON.parse(data.slice(1)).kittyFrameFlow !== 1 || frameFlowSockets.has(socket)) return;
|
|
1092
|
+
frameFlowSockets.add(socket);
|
|
1093
|
+
sendFrameControl(socket, {visible: !document.hidden});
|
|
1094
|
+
} catch {}
|
|
1095
|
+
}
|
|
1096
|
+
document.addEventListener("visibilitychange", () => {
|
|
1097
|
+
sendFrameControl(state.socket, {visible: !document.hidden});
|
|
1098
|
+
});
|
|
1099
|
+
function acknowledgeFrame(socket, sequence) {
|
|
1100
|
+
if (!Number.isSafeInteger(sequence)) return;
|
|
1101
|
+
// rAF stops while hidden and also bounds delivery if WebView is suspended
|
|
1102
|
+
// without a visibility event. Credits belong to their original connection.
|
|
1103
|
+
requestAnimationFrame(() => sendFrameControl(socket, {ack: sequence}));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function handleKittyMessage(payload, socket) {
|
|
1083
1107
|
const message = JSON.parse(payload);
|
|
1084
1108
|
sendBrowserDebug("kitty-message", {
|
|
1085
1109
|
kind: message.kind,
|
|
@@ -1091,7 +1115,9 @@
|
|
|
1091
1115
|
// The server records this cursor exactly where the Kitty APC occurred.
|
|
1092
1116
|
// Do not replace it with xterm's later cursor, which may already have
|
|
1093
1117
|
// moved because of redraw or scrolling output queued after the APC.
|
|
1094
|
-
placeImage(message)
|
|
1118
|
+
Promise.resolve(placeImage(message)).catch(error => {
|
|
1119
|
+
console.warn("Kitty image decode failed", error);
|
|
1120
|
+
}).finally(() => acknowledgeFrame(socket, message.frameSequence));
|
|
1095
1121
|
return true;
|
|
1096
1122
|
}
|
|
1097
1123
|
if (message.kind === "delete") {
|
|
@@ -1119,9 +1145,10 @@
|
|
|
1119
1145
|
const wrapped = function (event) {
|
|
1120
1146
|
state.socket = this;
|
|
1121
1147
|
watchTouchSocket(this);
|
|
1148
|
+
inspectFrameCapability(this, event.data);
|
|
1122
1149
|
inspectTerminalOutput(event.data);
|
|
1123
1150
|
if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
|
|
1124
|
-
if (handleKittyMessage(event.data.slice(1))) {
|
|
1151
|
+
if (handleKittyMessage(event.data.slice(1), this)) {
|
|
1125
1152
|
return;
|
|
1126
1153
|
}
|
|
1127
1154
|
}
|
|
@@ -1166,9 +1193,10 @@
|
|
|
1166
1193
|
const wrapped = function (event) {
|
|
1167
1194
|
state.socket = this;
|
|
1168
1195
|
watchTouchSocket(this);
|
|
1196
|
+
inspectFrameCapability(this, event.data);
|
|
1169
1197
|
inspectTerminalOutput(event.data);
|
|
1170
1198
|
if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
|
|
1171
|
-
if (handleKittyMessage(event.data.slice(1))) {
|
|
1199
|
+
if (handleKittyMessage(event.data.slice(1), this)) {
|
|
1172
1200
|
return;
|
|
1173
1201
|
}
|
|
1174
1202
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// CHROMIUM_HEADLESS=/usr/local/bin/chromium-headless-shell bun test tests/kitty-flow-browser.test.js
|
|
2
|
+
const test=require('node:test'),assert=require('node:assert/strict');
|
|
3
|
+
const fs=require('node:fs'),path=require('node:path'),http=require('node:http');
|
|
4
|
+
const {spawn}=require('node:child_process'),{once}=require('node:events');
|
|
5
|
+
const WebSocket=require('ws'),{KittyFrameFlow}=require('../kitty-flow.js');
|
|
6
|
+
test('real browser resumes latest image after visibility loss or suspended animation callbacks',{skip:!process.env.CHROMIUM_HEADLESS,timeout:30000},async()=>{
|
|
7
|
+
const source=fs.readFileSync(path.join(__dirname,'../static/js/kitty.js'));
|
|
8
|
+
const server=http.createServer((req,res)=>{
|
|
9
|
+
if(req.url==='/kitty.js'){res.setHeader('Content-Type','text/javascript');res.end(source);return;}
|
|
10
|
+
res.end(`<div id="terminal" style="width:800px;height:600px"><div class="xterm-screen"></div></div><div id="kitty-overlay"></div>
|
|
11
|
+
<script>window.hidden=false;Object.defineProperty(document,'hidden',{get:()=>window.hidden});window.held=[];window.suspended=false;const raf=window.requestAnimationFrame.bind(window);window.requestAnimationFrame=fn=>suspended?(held.push(fn),0):raf(fn);</script>
|
|
12
|
+
<script src="/kitty.js"></script><script>window.messages=[];window.socket=new WebSocket('ws://'+location.host);socket.onmessage=e=>messages.push(e.data);</script>`);
|
|
13
|
+
});server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
14
|
+
const wss=new WebSocket.WebSocketServer({server});let flow,socket,count=0,last=null;
|
|
15
|
+
wss.on('connection',ws=>{socket=ws;flow=new KittyFrameFlow(g=>{count++;last=g;ws.send('7'+JSON.stringify(g));});ws.on('message',m=>{if(String(m)[0]==='G')flow.control(JSON.parse(String(m).slice(1)))});ws.send('a{"kittyFrameFlow":1}');});
|
|
16
|
+
const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{stdio:['ignore','ignore','pipe']});let cdp;
|
|
17
|
+
try{
|
|
18
|
+
const endpoint=await new Promise((resolve,reject)=>{let log='';child.on('error',reject);child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m)resolve(m[1]);});});
|
|
19
|
+
let tab;for(let i=0;i<100;i++){tab=(await(await fetch('http://'+new URL(endpoint).host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,20));}
|
|
20
|
+
cdp=new WebSocket(tab.webSocketDebuggerUrl);await once(cdp,'open');let id=0;const pending=new Map();cdp.on('message',data=>{const m=JSON.parse(data),p=pending.get(m.id);if(p){pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result)}});
|
|
21
|
+
const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});cdp.send(JSON.stringify({id,method,params}));});
|
|
22
|
+
const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
|
|
23
|
+
const until=async check=>{for(let i=0;i<100;i++){if(await check())return;await new Promise(r=>setTimeout(r,20));}throw Error('fixture timed out')};
|
|
24
|
+
await until(()=>flow?.enabled);
|
|
25
|
+
const png=await evaluate(`(()=>{const c=document.createElement('canvas');c.width=c.height=1;c.getContext('2d').fillRect(0,0,1,1);return c.toDataURL().split(',')[1]})()`);
|
|
26
|
+
const frame=n=>({kind:'placement',control:{i:'1',C:'1',c:'2',r:'2'},cursor:{row:1,col:1},image:{id:'1',format:100,data:png},testFrame:n});
|
|
27
|
+
flow.graphic(frame(0));await until(()=>flow.inFlight.size===0);
|
|
28
|
+
await evaluate('window.hidden=true;document.dispatchEvent(new Event("visibilitychange"))');await until(()=>flow.visible===false);
|
|
29
|
+
const before=count;for(let n=1;n<=1800;n++)flow.graphic(frame(n));assert.equal(count,before);assert.equal(flow.pending.size,1);
|
|
30
|
+
socket.send('1'+Buffer.from('TEXT_WHILE_HIDDEN').toString('base64'));
|
|
31
|
+
await until(async()=>await evaluate('messages.some(x=>x.startsWith("1"))'));
|
|
32
|
+
await evaluate('window.hidden=false;document.dispatchEvent(new Event("visibilitychange"))');await until(()=>flow.inFlight.size===0&&last.testFrame===1800);
|
|
33
|
+
assert.equal(count,before+1);
|
|
34
|
+
// A renderer can freeze without delivering visibilitychange. Missing rAF
|
|
35
|
+
// acknowledgements still bound the server to two outstanding frames.
|
|
36
|
+
await evaluate('window.suspended=true');const baseline=count;
|
|
37
|
+
for(let n=1801;n<=3600;n++)flow.graphic(frame(n));assert.equal(count,baseline+2);assert.equal(flow.pending.size,1);
|
|
38
|
+
await until(async()=>await evaluate('held.length>=2'));
|
|
39
|
+
await evaluate('window.suspended=false;for(const fn of held.splice(0))requestAnimationFrame(fn)');
|
|
40
|
+
await until(()=>flow.inFlight.size===0&&last.testFrame===3600);assert.equal(count,baseline+3);
|
|
41
|
+
await until(async()=>await evaluate('document.querySelector("img.kitty-image__content").naturalWidth===1'));
|
|
42
|
+
}finally{cdp?.close();for(const ws of wss.clients)ws.terminate();wss.close();server.close();child.kill();}
|
|
43
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const {test}=require('node:test');
|
|
2
|
+
const assert=require('node:assert/strict');
|
|
3
|
+
const {KittyFrameFlow}=require('../kitty-flow.js');
|
|
4
|
+
const graphic=(n,id='1')=>({kind:'placement',control:{i:id,C:'1'},cursor:{row:2,col:1},image:{id,data:String(n)}});
|
|
5
|
+
test('browser credits bound suspension to two frames and latest replacement',()=>{
|
|
6
|
+
const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));
|
|
7
|
+
assert.equal(flow.graphic(graphic(0)),false); // CLI and old WebViews unchanged
|
|
8
|
+
flow.control({version:1,visible:true});
|
|
9
|
+
for(let n=0;n<1800;n++)assert.equal(flow.graphic(graphic(n)),true);
|
|
10
|
+
assert.equal(sent.length,2);assert.equal(flow.pending.size,1);
|
|
11
|
+
flow.control({version:1,ack:9999});assert.equal(sent.length,2);
|
|
12
|
+
flow.control({version:1,ack:sent[0].frameSequence});
|
|
13
|
+
assert.equal(sent.length,3);assert.equal(sent[2].image.data,'1799');
|
|
14
|
+
flow.control({version:1,ack:sent[0].frameSequence});assert.equal(sent.length,3);
|
|
15
|
+
});
|
|
16
|
+
test('hidden viewers keep only latest per placement and resume without another update',()=>{
|
|
17
|
+
const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));flow.control({version:1,visible:false});
|
|
18
|
+
for(let n=0;n<100;n++){flow.graphic(graphic(n,'1'));flow.graphic(graphic(n,'2'));}
|
|
19
|
+
assert.equal(sent.length,0);assert.equal(flow.pending.size,2);
|
|
20
|
+
flow.control({version:1,visible:true});assert.deepEqual(sent.map(g=>g.image.data),['99','99']);
|
|
21
|
+
});
|
|
22
|
+
test('delete, terminal reset and alternate screen do not resurrect pending images',()=>{
|
|
23
|
+
const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));flow.control({version:1,visible:false});
|
|
24
|
+
flow.graphic(graphic(1));flow.graphic(graphic(2,'2'));
|
|
25
|
+
assert.equal(flow.graphic({kind:'delete',delete:{imageId:'1'}}),false);
|
|
26
|
+
assert.equal(flow.pending.size,1);
|
|
27
|
+
flow.output(Buffer.from('\x1b[?104').toString('base64'));
|
|
28
|
+
flow.output(Buffer.from('9l').toString('base64'));assert.equal(flow.pending.size,0);
|
|
29
|
+
flow.graphic(graphic(3));flow.graphic({kind:'delete',delete:{scope:'a'}});
|
|
30
|
+
flow.control({version:1,visible:true});assert.equal(sent.length,0);
|
|
31
|
+
});
|
|
32
|
+
test('connections have independent credits; normal scrolling images are unchanged',()=>{
|
|
33
|
+
const one=[],two=[],a=new KittyFrameFlow(g=>one.push(g)),b=new KittyFrameFlow(g=>two.push(g));
|
|
34
|
+
a.control({version:1,visible:false});b.control({version:1,visible:true});
|
|
35
|
+
a.graphic(graphic(1));b.graphic(graphic(1));assert.equal(one.length,0);assert.equal(two.length,1);
|
|
36
|
+
const scroll=graphic(2);delete scroll.control.C;assert.equal(a.graphic(scroll),false);
|
|
37
|
+
// A later ordinary placement replaces the same deferred position immediately.
|
|
38
|
+
assert.equal(a.pending.size,0);a.graphic(graphic(3));
|
|
39
|
+
a.output(Buffer.from('ordinary text').toString('base64'));assert.equal(a.pending.size,1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('browser negotiates only on advertisement and acknowledges after animation opportunity',async()=>{
|
|
43
|
+
const fs=require('node:fs'),vm=require('node:vm'),source=fs.readFileSync(require('node:path').join(__dirname,'../static/js/kitty.js'),'utf8');
|
|
44
|
+
const start=source.indexOf(' // Only a server advertising'),end=source.indexOf(' function deleteImages');
|
|
45
|
+
const helpers=source.slice(start,source.indexOf(' function handleKittyMessage',start));
|
|
46
|
+
const frames=[],events={},sent=[],socket={readyState:1,send:m=>sent.push(JSON.parse(m.slice(1)))};
|
|
47
|
+
const document={hidden:true,addEventListener:(k,fn)=>events[k]=fn};
|
|
48
|
+
const api=vm.runInNewContext(`(()=>{${helpers};return {inspectFrameCapability,acknowledgeFrame}})()`,{document,state:{socket},requestAnimationFrame:fn=>frames.push(fn)});
|
|
49
|
+
api.inspectFrameCapability(socket,'a{"role":"viewer"}');assert.equal(sent.length,0);
|
|
50
|
+
api.inspectFrameCapability(socket,'a{"kittyFrameFlow":1}');assert.equal(sent[0].visible,false);
|
|
51
|
+
api.acknowledgeFrame(socket,1);assert.equal(sent.length,1); // suspended rAF
|
|
52
|
+
document.hidden=false;events.visibilitychange();assert.equal(sent[1].visible,true);
|
|
53
|
+
frames.shift()();assert.equal(sent[2].ack,1);
|
|
54
|
+
socket.readyState=3;api.acknowledgeFrame(socket,2);frames.shift()();assert.equal(sent.length,3);
|
|
55
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// bun tests/manual/fps-collector.mjs [log.jsonl]
|
|
2
|
+
import {appendFileSync} from 'node:fs';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
const log=resolve(process.argv[2]||'jsgotty-fps.jsonl'),token=crypto.randomUUID();
|
|
5
|
+
const source=await Bun.file(new URL('./fps-debug.js',import.meta.url)).text();
|
|
6
|
+
let rows=0;
|
|
7
|
+
const headers={'Access-Control-Allow-Origin':'*','Access-Control-Allow-Methods':'POST, OPTIONS','Access-Control-Allow-Headers':'Content-Type','Access-Control-Allow-Private-Network':'true','Cache-Control':'no-store'};
|
|
8
|
+
const server=Bun.serve({hostname:'127.0.0.1',port:0,maxRequestBodySize:262144,async fetch(req){
|
|
9
|
+
if(new URL(req.url).pathname!=='/'+token)return new Response('',{status:404});
|
|
10
|
+
if(req.method==='OPTIONS')return new Response(null,{status:204,headers});
|
|
11
|
+
if(req.method!=='POST')return new Response('',{status:405,headers});
|
|
12
|
+
if(rows>=1200)return new Response('',{status:429,headers});
|
|
13
|
+
try{const row=await req.json();appendFileSync(log,JSON.stringify({receivedAt:Date.now(),...row})+'\n',{mode:0o600});rows++;return new Response(null,{status:204,headers});}
|
|
14
|
+
catch{return new Response('',{status:400,headers});}
|
|
15
|
+
}});
|
|
16
|
+
const endpoint=`http://127.0.0.1:${server.port}/${token}`;
|
|
17
|
+
const script=log+'.inject.js';await Bun.write(script,source+'\nwindow.startGottyFpsDebug({endpoint:'+JSON.stringify(endpoint)+'});\n');
|
|
18
|
+
console.log(JSON.stringify({endpoint,log,script,pid:process.pid}));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Eval manually; counts completed Kitty <img> loads, not physical display flips.
|
|
2
|
+
window.startGottyFpsDebug = ({endpoint, duration=600000}={}) => {
|
|
3
|
+
window.stopGottyFpsDebug?.(); window.stopKittyFPS?.();
|
|
4
|
+
const box=document.createElement('div');
|
|
5
|
+
box.style.cssText='position:fixed;top:0;right:0;z-index:2147483647;background:#000b;color:cyan;padding:4px 8px;font:16px monospace;pointer-events:none';
|
|
6
|
+
box.textContent='Kitty: 0 FPS';document.body.append(box);
|
|
7
|
+
const session=Date.now().toString(36),ids=new WeakMap();let id=0,previous=0,started=performance.now(),last=started,frames=[],busy=false,dropped=0,sequence=0,stopped=false;
|
|
8
|
+
const records=[];window.__gottyFpsRecords=records;
|
|
9
|
+
const load=e=>{const el=e.target;if(!el.matches?.('img.kitty-image__content'))return;
|
|
10
|
+
const now=performance.now();if(!ids.has(el))ids.set(el,++id);
|
|
11
|
+
if(frames.length<2000)frames.push({t:now,id:ids.get(el),gap:previous?now-previous:null,w:el.naturalWidth,h:el.naturalHeight});else dropped++;
|
|
12
|
+
previous=now;
|
|
13
|
+
};
|
|
14
|
+
document.addEventListener('load',load,true);
|
|
15
|
+
const flush=()=>{
|
|
16
|
+
const now=performance.now(),batch=frames;frames=[];
|
|
17
|
+
const gaps=batch.map(f=>f.gap).filter(g=>g!==null).sort((a,b)=>a-b);
|
|
18
|
+
const row={session,sequence:sequence++,time:Date.now(),timeOrigin:performance.timeOrigin,elapsed:now-last,fps:batch.length*1000/(now-last),hidden:document.hidden,frames:batch,dropped,
|
|
19
|
+
p95:gaps[Math.floor(gaps.length*.95)]??null,p99:gaps[Math.floor(gaps.length*.99)]??null,max:gaps.at(-1)??null};last=now;
|
|
20
|
+
box.textContent=`Kitty: ${row.fps.toFixed(1)} FPS`;
|
|
21
|
+
records.push(row);if(records.length>600)records.shift();window.__gottyFpsLatest=row;
|
|
22
|
+
if(endpoint&&!busy){busy=true;fetch(endpoint,{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify(row),signal:AbortSignal.timeout(5000)})
|
|
23
|
+
.then(r=>{if(!r.ok)throw Error('HTTP '+r.status);window.__gottyFpsDeliveryError=null;})
|
|
24
|
+
.catch(e=>{window.__gottyFpsDeliveryError=String(e);dropped+=batch.length;}).finally(()=>{busy=false;});}
|
|
25
|
+
else if(endpoint)dropped+=batch.length;
|
|
26
|
+
};
|
|
27
|
+
const timer=setInterval(flush,1000);
|
|
28
|
+
const expiry=setTimeout(()=>window.stopGottyFpsDebug?.(),Math.max(1000,Math.min(duration,600000)));
|
|
29
|
+
window.stopGottyFpsDebug=()=>{if(stopped)return;stopped=true;document.removeEventListener('load',load,true);clearInterval(timer);clearTimeout(expiry);box.remove();};
|
|
30
|
+
return 'Kitty FPS enabled (image loads; auto-stop in at most 10 minutes)';
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
window.stopKittyFPS?.();
|
|
3
|
+
|
|
4
|
+
const box = document.createElement("div");
|
|
5
|
+
box.style.cssText =
|
|
6
|
+
"position:fixed;top:0;right:0;z-index:2147483647;" +
|
|
7
|
+
"background:#000b;color:cyan;padding:4px 8px;" +
|
|
8
|
+
"font:16px monospace;pointer-events:none";
|
|
9
|
+
box.textContent = "Kitty: 0 FPS";
|
|
10
|
+
document.body.append(box);
|
|
11
|
+
|
|
12
|
+
let count = 0, last = performance.now();
|
|
13
|
+
const loaded = e => {
|
|
14
|
+
if (e.target.matches?.("img.kitty-image__content"))
|
|
15
|
+
count++;
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener("load", loaded, true);
|
|
18
|
+
|
|
19
|
+
const timer = setInterval(() => {
|
|
20
|
+
const now = performance.now();
|
|
21
|
+
box.textContent =
|
|
22
|
+
`Kitty: ${(count * 1000 / (now - last)).toFixed(1)}
|
|
23
|
+
FPS`;
|
|
24
|
+
count = 0;
|
|
25
|
+
last = now;
|
|
26
|
+
}, 1000);
|
|
27
|
+
|
|
28
|
+
window.stopKittyFPS = () => {
|
|
29
|
+
clearInterval(timer);
|
|
30
|
+
document.removeEventListener("load", loaded, true);
|
|
31
|
+
box.remove();
|
|
32
|
+
delete window.stopKittyFPS;
|
|
33
|
+
};
|
|
34
|
+
return "Kitty FPS enabled";
|
|
35
|
+
})()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Eval manually. Counts delivery, bitmap decoding, image loads and animation ticks.
|
|
2
|
+
// Observational only: no frame dropping, focus changes or socket reconnects.
|
|
3
|
+
window.startKittyBackgroundDebug = (endpoint) => {
|
|
4
|
+
window.stopKittyBackgroundDebug?.();
|
|
5
|
+
const parse=JSON.parse,bitmap=window.createImageBitmap,history=[];
|
|
6
|
+
let received=0,bytes=0,decoded=0,loads=0,ticks=0,pending=0,maxPending=0,decodeMs=0,last=performance.now(),busy=false,stopped=false,raf;
|
|
7
|
+
const wrappedParse=function(...args){const v=parse.apply(this,args);if(v?.kind==='placement'&&v.image){received++;bytes+=v.image.data?.length||0;}return v;};
|
|
8
|
+
const wrappedBitmap=async function(...args){const start=performance.now();pending++;maxPending=Math.max(maxPending,pending);try{return await bitmap.apply(this,args)}finally{pending--;decoded++;decodeMs+=performance.now()-start;}};
|
|
9
|
+
JSON.parse=wrappedParse;if(bitmap)window.createImageBitmap=wrappedBitmap;
|
|
10
|
+
const load=e=>{if(e.target.matches?.('img.kitty-image__content'))loads++;};
|
|
11
|
+
document.addEventListener('load',load,true);
|
|
12
|
+
const tick=()=>{ticks++;if(!stopped)raf=requestAnimationFrame(tick)};raf=requestAnimationFrame(tick);
|
|
13
|
+
function flush(reason='interval'){
|
|
14
|
+
const now=performance.now(),row={kind:'background-profile',time:Date.now(),elapsed:now-last,reason,hidden:document.hidden,received,bytes,decoded,loads,ticks,pending,maxPending,decodeMs};
|
|
15
|
+
last=now;received=bytes=decoded=loads=ticks=decodeMs=0;maxPending=pending;
|
|
16
|
+
history.push(row);if(history.length>600)history.shift();window.__kittyBackgroundLatest=row;
|
|
17
|
+
if(endpoint&&!busy){busy=true;fetch(endpoint,{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify(row),signal:AbortSignal.timeout(5000)}).catch(()=>{}).finally(()=>busy=false);}
|
|
18
|
+
}
|
|
19
|
+
const visibility=()=>flush('visibilitychange');document.addEventListener('visibilitychange',visibility);
|
|
20
|
+
const timer=setInterval(flush,1000),expiry=setTimeout(()=>window.stopKittyBackgroundDebug(),600000);
|
|
21
|
+
window.__kittyBackgroundHistory=history;
|
|
22
|
+
window.stopKittyBackgroundDebug=()=>{stopped=true;if(JSON.parse===wrappedParse)JSON.parse=parse;if(window.createImageBitmap===wrappedBitmap)window.createImageBitmap=bitmap;document.removeEventListener('load',load,true);document.removeEventListener('visibilitychange',visibility);cancelAnimationFrame(raf);clearInterval(timer);clearTimeout(expiry);};
|
|
23
|
+
return 'background diagnostic enabled';
|
|
24
|
+
};
|
package/package.json
CHANGED
package/scripts/arch.js
CHANGED
|
@@ -16,8 +16,8 @@ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
16
16
|
|
|
17
17
|
const pins = {
|
|
18
18
|
x86_64: {
|
|
19
|
-
linuxVirtSha256: "
|
|
20
|
-
linuxLtsSha256: "
|
|
19
|
+
linuxVirtSha256: "e121b547d9b86194a641718b52ae1c3e605085c59f31525b1649a59b3d4db0c7",
|
|
20
|
+
linuxLtsSha256: "2e03d0d13959a369764d8d24d51844634d1fd6973a523f4e49166a300c782a06",
|
|
21
21
|
muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
|
|
22
22
|
stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
|
|
23
23
|
libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
|
|
@@ -32,8 +32,8 @@ const pins = {
|
|
|
32
32
|
serialMajor: 4,
|
|
33
33
|
},
|
|
34
34
|
aarch64: {
|
|
35
|
-
linuxVirtSha256: "
|
|
36
|
-
linuxLtsSha256: "
|
|
35
|
+
linuxVirtSha256: "3a9a0f16adaabee0e4e05e373682249e2a99ab000faebf6aaaf682ae575c661d",
|
|
36
|
+
linuxLtsSha256: "29d6cbf6f432667d5d17080037b3cfd4d28d71c3b0ecb4ad7e53438abf9f24e7",
|
|
37
37
|
muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
|
|
38
38
|
stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
|
|
39
39
|
libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
|
|
@@ -68,7 +68,7 @@ export const config = ({
|
|
|
68
68
|
// The Alpine repository keeps only the newest build of a package, so a pin
|
|
69
69
|
// stops downloading when Alpine bumps it; the hashes then fail loudly.
|
|
70
70
|
const alpineBase = `${mirror}/v3.24/main/${arch}`;
|
|
71
|
-
const kernelVersion = "6.18.
|
|
71
|
+
const kernelVersion = "6.18.62";
|
|
72
72
|
const kernelRelease = `${kernelVersion}-0-${flavor}`;
|
|
73
73
|
const bunVersion = "1.4.2";
|
|
74
74
|
const downloadsDir = join(rootDir, "downloads", arch);
|
package/scripts/arch.sh
CHANGED
|
@@ -44,7 +44,7 @@ fi
|
|
|
44
44
|
alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
|
|
45
45
|
alpine_base="$alpine_mirror/v3.24/main/$arch"
|
|
46
46
|
|
|
47
|
-
kernel_version=6.18.
|
|
47
|
+
kernel_version=6.18.62
|
|
48
48
|
kernel_release="$kernel_version-0-$linux_flavor"
|
|
49
49
|
linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
|
|
50
50
|
musl_package=musl-1.2.6-r2.apk
|
|
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
|
|
|
63
63
|
|
|
64
64
|
case $arch in
|
|
65
65
|
x86_64)
|
|
66
|
-
linux_virt_sha256=
|
|
67
|
-
linux_lts_sha256=
|
|
66
|
+
linux_virt_sha256=e121b547d9b86194a641718b52ae1c3e605085c59f31525b1649a59b3d4db0c7
|
|
67
|
+
linux_lts_sha256=2e03d0d13959a369764d8d24d51844634d1fd6973a523f4e49166a300c782a06
|
|
68
68
|
musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
|
|
69
69
|
stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
|
|
70
70
|
libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
|
|
@@ -82,8 +82,8 @@ case $arch in
|
|
|
82
82
|
serial_major=4
|
|
83
83
|
;;
|
|
84
84
|
aarch64)
|
|
85
|
-
linux_virt_sha256=
|
|
86
|
-
linux_lts_sha256=
|
|
85
|
+
linux_virt_sha256=3a9a0f16adaabee0e4e05e373682249e2a99ab000faebf6aaaf682ae575c661d
|
|
86
|
+
linux_lts_sha256=29d6cbf6f432667d5d17080037b3cfd4d28d71c3b0ecb4ad7e53438abf9f24e7
|
|
87
87
|
musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
|
|
88
88
|
stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
|
|
89
89
|
libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
|
package/scripts/fetch.js
CHANGED
|
@@ -76,34 +76,69 @@ export const readTar = (path) => {
|
|
|
76
76
|
const links = new Map();
|
|
77
77
|
let longName = null;
|
|
78
78
|
let longLink = null;
|
|
79
|
-
|
|
79
|
+
let extended = {}, global = {};
|
|
80
|
+
const invalid = (reason) => { throw new Error(`${path}: ${reason}`); };
|
|
81
|
+
const number = (header, start, length) => {
|
|
82
|
+
if (header[start] & 0x80) {
|
|
83
|
+
let value = BigInt(header[start] & 0x7f);
|
|
84
|
+
for (let i = start + 1; i < start + length; i++) value = value * 256n + BigInt(header[i]);
|
|
85
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) invalid("unsafe tar numeric field");
|
|
86
|
+
return Number(value);
|
|
87
|
+
}
|
|
88
|
+
const digits = header.toString("ascii", start, start + length).replace(/\0.*$/s, "").trim();
|
|
89
|
+
if (digits && !/^[0-7]+$/.test(digits)) invalid("invalid tar numeric field");
|
|
90
|
+
return digits ? Number.parseInt(digits, 8) : 0;
|
|
91
|
+
};
|
|
92
|
+
let offset = 0;
|
|
93
|
+
for (; offset + 512 <= data.length;) {
|
|
80
94
|
const header = data.subarray(offset, offset + 512);
|
|
81
|
-
if (header.every((byte) => byte === 0)) {
|
|
95
|
+
if (header.every((byte) => byte === 0)) {
|
|
96
|
+
offset += 512; longName = longLink = null; extended = {}; global = {}; continue;
|
|
97
|
+
}
|
|
98
|
+
let checksum = 0;
|
|
99
|
+
for (let i = 0; i < 512; i++) checksum += i >= 148 && i < 156 ? 32 : header[i];
|
|
100
|
+
if (checksum !== number(header, 148, 8)) invalid("invalid tar header checksum");
|
|
82
101
|
const field = (start, length) => header.toString("utf8", start, start + length).replace(/\0.*$/s, "");
|
|
83
|
-
const size = Number.parseInt(field(124, 12).trim() || "0", 8);
|
|
84
102
|
const type = String.fromCharCode(header[156] || 0x30);
|
|
103
|
+
const metadata = ["x", "X", "g", "L", "K"].includes(type);
|
|
104
|
+
const attrs = { ...global, ...extended };
|
|
105
|
+
if (!metadata && attrs.size !== undefined && !/^[0-9]+$/.test(attrs.size)) invalid("invalid pax size");
|
|
106
|
+
const size = !metadata && attrs.size !== undefined ? Number(attrs.size) : number(header, 124, 12);
|
|
107
|
+
const padded = Math.ceil(size / 512) * 512;
|
|
108
|
+
if (!Number.isSafeInteger(size) || size < 0 || !Number.isSafeInteger(padded)
|
|
109
|
+
|| padded > data.length - offset - 512) invalid("invalid or truncated tar member size");
|
|
85
110
|
const body = data.subarray(offset + 512, offset + 512 + size);
|
|
86
|
-
offset += 512 +
|
|
87
|
-
if (type === "x") {
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
offset += 512 + padded;
|
|
112
|
+
if (type === "x" || type === "X" || type === "g") {
|
|
113
|
+
// PAX lengths count bytes, not UTF-16 characters. Reject malformed records.
|
|
114
|
+
const attrs = type === "g" ? global : extended;
|
|
115
|
+
for (let at = 0; at < body.length;) {
|
|
116
|
+
const space = body.indexOf(32, at);
|
|
117
|
+
if (space < 0) invalid("invalid pax record length");
|
|
118
|
+
const digits = body.toString("ascii", at, space);
|
|
119
|
+
const length = /^[0-9]+$/.test(digits) ? Number(digits) : NaN;
|
|
120
|
+
if (!Number.isSafeInteger(length) || length <= space - at + 1
|
|
121
|
+
|| length > body.length - at || body[at + length - 1] !== 10) invalid("invalid pax record length");
|
|
122
|
+
const record = body.toString("utf8", space + 1, at + length - 1);
|
|
123
|
+
const equals = record.indexOf("=");
|
|
124
|
+
if (equals > 0) Object.defineProperty(attrs, record.slice(0, equals), {
|
|
125
|
+
value: record.slice(equals + 1), writable: true, enumerable: true, configurable: true,
|
|
126
|
+
});
|
|
127
|
+
at += length;
|
|
94
128
|
}
|
|
95
129
|
continue;
|
|
96
130
|
}
|
|
97
131
|
if (type === "L") { longName = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
|
|
98
132
|
if (type === "K") { longLink = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
|
|
133
|
+
const prefix = field(257, 5) === "ustar" ? field(345, 155) : "";
|
|
134
|
+
const name = attrs.path ?? longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
|
|
102
135
|
longName = null;
|
|
103
136
|
if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
|
|
104
|
-
if (type === "1" || type === "2") links.set(name.replace(/^\.\//, ""), { target: longLink ?? field(157, 100), hard: type === "1" });
|
|
137
|
+
if (type === "1" || type === "2") links.set(name.replace(/^\.\//, ""), { target: attrs.linkpath ?? longLink ?? field(157, 100), hard: type === "1" });
|
|
105
138
|
longLink = null;
|
|
139
|
+
extended = {};
|
|
106
140
|
}
|
|
141
|
+
if (offset !== data.length) invalid("truncated tar header");
|
|
107
142
|
return {
|
|
108
143
|
// Enumerate regular files without exposing the Map's mutation methods.
|
|
109
144
|
entries() { return files.entries(); },
|
package/scripts/npm-packages.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# npm packages
|
|
2
|
-
#
|
|
3
|
-
# buninu-browser 0.9.
|
|
2
|
+
# SHA-256 pins verified against registry SHA-512 integrity.
|
|
3
|
+
# buninu-browser 0.9.2, buninu-land 0.4.1, and casty 1.6.2 verified on 2026-11-03.
|
|
4
4
|
# jspulse 0.2.3 verified on 2026-10-22.
|
|
5
5
|
# jspinyin 0.2.0 verified on 2026-10-22.
|
|
6
6
|
|
|
7
|
-
"buninu-browser@0.9.
|
|
8
|
-
- https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.9.
|
|
9
|
-
- sha256:
|
|
7
|
+
"buninu-browser@0.9.2":
|
|
8
|
+
- https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.9.2.tgz
|
|
9
|
+
- sha256:faba0d55272882da02cc5993047d831dae11845137b53b7f5c7642e99567391a
|
|
10
10
|
|
|
11
|
-
"buninu-land@0.4.
|
|
12
|
-
- https://registry.npmjs.org/buninu-land/-/buninu-land-0.4.
|
|
13
|
-
- sha256:
|
|
11
|
+
"buninu-land@0.4.1":
|
|
12
|
+
- https://registry.npmjs.org/buninu-land/-/buninu-land-0.4.1.tgz
|
|
13
|
+
- sha256:18281a77725cdce53cd26aa447c657c530616e0f0bcb44819352d3420023a34a
|
|
14
14
|
|
|
15
|
-
"@drxiaozhi/casty@1.6.
|
|
16
|
-
- https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.6.
|
|
17
|
-
- sha256:
|
|
15
|
+
"@drxiaozhi/casty@1.6.2":
|
|
16
|
+
- https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.6.2.tgz
|
|
17
|
+
- sha256:8aa8e59998aa82ef0e28ca172828eba116e0f19d890490fc19d7ca4dd295a77c
|
|
18
18
|
|
|
19
19
|
"@drxiaozhi/jspulse@0.2.3":
|
|
20
20
|
- https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.3.tgz
|