buninu-linux 0.8.55 → 0.9.56
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 +7 -6
- package/initramfs/bin/tar +34 -46
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +103 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +133 -15
- package/initramfs/buninu/apps/bunmsh/help/awk.md +38 -0
- package/initramfs/buninu/apps/bunmsh/help/break.md +21 -0
- package/initramfs/buninu/apps/bunmsh/help/chmod.md +8 -2
- package/initramfs/buninu/apps/bunmsh/help/cmp.md +29 -0
- package/initramfs/buninu/apps/bunmsh/help/continue.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/cp.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/diff.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/egrep.md +26 -0
- package/initramfs/buninu/apps/bunmsh/help/export.md +2 -2
- package/initramfs/buninu/apps/bunmsh/help/expr.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/fgrep.md +25 -0
- package/initramfs/buninu/apps/bunmsh/help/grep.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/ls.md +4 -2
- package/initramfs/buninu/apps/bunmsh/help/mkdir.md +1 -1
- package/initramfs/buninu/apps/bunmsh/help/print.md +3 -0
- package/initramfs/buninu/apps/bunmsh/help/return.md +23 -0
- package/initramfs/buninu/apps/bunmsh/help/sed.md +8 -3
- package/initramfs/buninu/apps/bunmsh/help/set.md +12 -3
- package/initramfs/buninu/apps/bunmsh/help/tr.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/trap.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/uniq.md +27 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/awk.js +203 -0
- package/initramfs/buninu/apps/bunmsh/src/diff.js +222 -0
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +15 -4
- package/initramfs/buninu/apps/bunmsh/src/main.js +21 -5
- package/initramfs/buninu/apps/bunmsh/src/sed.js +159 -0
- package/initramfs/buninu/apps/bunmsh/src/shell.js +1283 -540
- package/initramfs/buninu/apps/bunmsh/test/awk-reference.test.js +35 -0
- package/initramfs/buninu/apps/bunmsh/test/awk.test.js +62 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference.test.js +23 -0
- package/initramfs/buninu/apps/jsgotty/client-kitty.mjs +61 -0
- package/initramfs/buninu/apps/jsgotty/client.mjs +16 -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/tests/client-kitty.test.mjs +43 -0
- package/initramfs/etc/resolv.conf +2 -1
- package/initramfs/usr/share/doc/buninu-linux/tar.md +12 -1
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch.js +2 -0
- package/scripts/npm-packages.md +4 -3
- package/test/fetch-npm.test.js +1 -1
- package/test/tar-strip.test.js +36 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Record system-tool results separately from the isolated bootstrap runtime:
|
|
2
|
+
// node test/utility-reference-cases.mjs OUTPUT.json
|
|
3
|
+
// BUNMSH_UTILITY_REFERENCE=/guest/OUTPUT.json bun test test/utility-reference.test.js
|
|
4
|
+
import {mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync, statSync} from 'node:fs';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
import {join, resolve, dirname} from 'node:path';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
import {spawnSync} from 'node:child_process';
|
|
9
|
+
const diff = (name, before, after, args=[]) => ({name, tool:'diff', files:{before,after}, args:[...args,'before','after']});
|
|
10
|
+
const sed = (name, script, input, args=[]) => ({name, tool:'sed', files:{script}, args:[...args,'-f','script'], input});
|
|
11
|
+
export const cases = [
|
|
12
|
+
...[
|
|
13
|
+
['adjacent only', [], 'a\na\nb\na\n'],
|
|
14
|
+
['count', ['-c'], 'a\na\nb\n'],
|
|
15
|
+
['repeated', ['-d'], 'a\na\nb\na\n'],
|
|
16
|
+
['single', ['-u'], 'a\na\nb\na\n'],
|
|
17
|
+
['count repeated', ['-cd'], 'a\na\nb\n'],
|
|
18
|
+
['no repeated singles', ['-du'], 'a\na\nb\n'],
|
|
19
|
+
['empty input', [], ''],
|
|
20
|
+
['blank lines', [], '\n\nfirst\n\n'],
|
|
21
|
+
['unterminated duplicate', [], 'a\na'],
|
|
22
|
+
['unterminated distinct', [], 'a\nb'],
|
|
23
|
+
['binary NUL', [], 'a\0b\na\0b\na\0c\n'],
|
|
24
|
+
['file operand', ['--','input'], null],
|
|
25
|
+
].map(([name,args,input])=>({name,tool:'uniq',args,files:input===null?{input:'a\na\nb\n'}:{},input:input??''})),
|
|
26
|
+
...[
|
|
27
|
+
['remove all write bits',0o666,'a-w'],
|
|
28
|
+
['add user read and write',0o004,'u+rw'],
|
|
29
|
+
['assign group and other permissions',0o770,'go=rx'],
|
|
30
|
+
['sequential classes',0o777,'u=rw,g=r,o='],
|
|
31
|
+
['conditional execute on plain file',0o644,'a+X'],
|
|
32
|
+
['conditional execute on executable',0o744,'a+X'],
|
|
33
|
+
['conditional execute after removal',0o755,'a-x,a+X'],
|
|
34
|
+
['assignment clears file special bits',0o4766,'a=rw'],
|
|
35
|
+
['multiple operations for one class',0o777,'u+rw-x'],
|
|
36
|
+
].map(([name,mode,spec])=>({name,tool:'chmod',files:{file:'content'},modes:{file:mode},args:[spec,'--','file']})),
|
|
37
|
+
...[
|
|
38
|
+
['identical bytes', 'abc\n', 'abc\n', []],
|
|
39
|
+
['first differing byte', 'same\nabc\n', 'same\nabX\n', []],
|
|
40
|
+
['quiet difference', 'abc', 'ab', ['-s']],
|
|
41
|
+
['shorter file', 'abc', 'abcd', []],
|
|
42
|
+
['empty file', '', 'abc', []],
|
|
43
|
+
['binary difference', 'a\0b', 'a\0c', []],
|
|
44
|
+
].map(([name,before,after,args])=>({name,tool:'cmp',files:{before,after},args:[...args,'--','before','after']})),
|
|
45
|
+
{name:'squeeze spaces without translating s',tool:'tr',files:{},args:['-s',' '],input:'size_t inbytesleft\n'},
|
|
46
|
+
{name:'translate then squeeze',tool:'tr',files:{},args:['-s','ab','x'],input:'aabbbcaa\n'},
|
|
47
|
+
{name:'delete then squeeze',tool:'tr',files:{},args:['-ds','x','a'],input:'axaxa bbb\n'},
|
|
48
|
+
{name:'delete only',tool:'tr',files:{},args:['-d','x'],input:'axxbbb\n'},
|
|
49
|
+
{name:'squeeze repeated newlines',tool:'tr',files:{},args:['-s','\\n'],input:'a\n\n\nb\n'},
|
|
50
|
+
{name:'literal hyphen set',tool:'tr',files:{},args:['--','-s','XY'],input:'-s-size_t\n'},
|
|
51
|
+
sed("make object extension","s=$[\\(\\{]OBJEXT[\\)\\}]=o=g\n","file.$(OBJEXT) file.${OBJEXT}\n"),
|
|
52
|
+
sed("BRE interior anchors","s/a^b/X/;s/c$d/Y/;s/^z$/Z/\n","a^b c$d\nz\n"),
|
|
53
|
+
{name:'read file after address',tool:'sed',files:{extra:'inserted\n'},args:['/mark/r extra'],input:'before\nmark\nafter\n'},
|
|
54
|
+
{name:'read missing file',tool:'sed',files:{},args:['r missing'],input:'kept\n'},
|
|
55
|
+
{name:'read file with deleted pattern',tool:'sed',files:{extra:'inserted\n'},args:['-n','-e','r extra','-e','d'],input:'one\ntwo\n'},
|
|
56
|
+
diff('identical','a\nb\n','a\nb\n'),
|
|
57
|
+
diff('middle insertion','a\nb\nc\n','a\nx\nb\nc\n'),
|
|
58
|
+
diff('separated insertions','a\nb\nc\nd\n','a\nx\nb\nc\ny\nd\n'),
|
|
59
|
+
diff('separated deletions','a\nx\nb\nc\ny\nd\n','a\nb\nc\nd\n'),
|
|
60
|
+
diff('insertion then replacement','a\nb\nc\nd\ne\n','a\nx\nb\nc\nD\ne\n'),
|
|
61
|
+
diff('repeated lines','a\nsame\nsame\nz\n','a\nsame\nnew\nsame\nz\n'),
|
|
62
|
+
diff('empty left','','added\n'), diff('empty right','deleted\n',''),
|
|
63
|
+
diff('missing final newline','a\n','a'), diff('both missing final newline','old','new'),
|
|
64
|
+
diff('Chinese','前\n後\n','前\n中\n後\n'),
|
|
65
|
+
diff('CRLF','a\r\nb\r\n','a\r\nx\r\nb\r\n'),
|
|
66
|
+
diff('brief difference','a\n','b\n',['-q']),
|
|
67
|
+
sed('file script and branch','s/a/b/\nt done\n:done\np\n','a\n',['-n']),
|
|
68
|
+
sed('address range','2,3p\n','one\ntwo\nthree\nfour\n',['-n']),
|
|
69
|
+
sed('inverted address','/skip/!p\n','keep\nskip\nkeep again\n',['-n']),
|
|
70
|
+
sed('hold exchange','1h\n2{x;p;}\n','first\nsecond\n',['-n']),
|
|
71
|
+
sed('hold append','1h\n2{H;g;p;}\n','first\nsecond\n',['-n']),
|
|
72
|
+
sed('multiline N','N;s/\n/:/;p\n'.replace('s/\n','s/\\n'),'first\nsecond\n',['-n']),
|
|
73
|
+
sed('branch loop',':again\ns/aa/a/\nt again\np\n','aaaa\n',['-n']),
|
|
74
|
+
sed('translation','y/abc/ABC/\n','abc cab\n'),
|
|
75
|
+
sed('escaped delimiter','s|a\\|b|X|g\n','a|b a|b\n'),
|
|
76
|
+
sed('backreference','s/\\(ab\\)/[\\1]/g\n','abab\n'),
|
|
77
|
+
sed('missing input newline','s/a/b/\n','a'),
|
|
78
|
+
{name:'attached -f',tool:'sed',files:{script:'s/a/b/\n'},args:['-fscript'],input:'a\n'},
|
|
79
|
+
];
|
|
80
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
81
|
+
if (!process.argv[2]) throw new Error('Output JSON path required');
|
|
82
|
+
const root=mkdtempSync(join(tmpdir(),'bunmsh-system-reference-'));
|
|
83
|
+
try {
|
|
84
|
+
const results=cases.map((c,index) => {
|
|
85
|
+
const cwd=join(root,String(index)); mkdirSync(cwd);
|
|
86
|
+
for (const [name,content] of Object.entries(c.files)) {
|
|
87
|
+
mkdirSync(dirname(join(cwd,name)),{recursive:true});writeFileSync(join(cwd,name),content);
|
|
88
|
+
}
|
|
89
|
+
for (const [name,mode] of Object.entries(c.modes??{})) chmodSync(join(cwd,name),mode);
|
|
90
|
+
const r=spawnSync(`/usr/bin/${c.tool}`,c.args,{cwd,input:c.input??'',encoding:'utf8',env:{...process.env,LC_ALL:'C'}});
|
|
91
|
+
if (r.error || r.signal || r.status===null) throw r.error??new Error(`Reference failed: ${c.name}`);
|
|
92
|
+
return {...c,expected:{status:r.status,stdout:r.stdout,stderr:r.stderr,...(c.modes?{modes:Object.fromEntries(Object.keys(c.modes).map(name=>[name,statSync(join(cwd,name)).mode&0o7777]))}:{})}};
|
|
93
|
+
});
|
|
94
|
+
const versions=Object.fromEntries(['diff','sed','tr','cmp','chmod','uniq'].map(tool=>[tool,spawnSync(`/usr/bin/${tool}`,['--version'],{encoding:'utf8'}).stdout.split('\n')[0]]));
|
|
95
|
+
writeFileSync(process.argv[2],JSON.stringify({versions,cases:results},null,2)+'\n');
|
|
96
|
+
console.log(JSON.stringify({versions,cases:results.length}));
|
|
97
|
+
} finally {rmSync(root,{recursive:true,force:true});}
|
|
98
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,chmodSync,statSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join,dirname} from 'node:path';
|
|
5
|
+
import {createState,execute,decode} from '../src/shell.js';
|
|
6
|
+
|
|
7
|
+
// Only data crosses into the guest. No host executables are mounted or spawned.
|
|
8
|
+
const path=process.env.BUNMSH_UTILITY_REFERENCE;
|
|
9
|
+
const fixtures=path ? JSON.parse(readFileSync(path,'utf8')).cases : [];
|
|
10
|
+
if (!path) test.skip('system diff/sed reference (set BUNMSH_UTILITY_REFERENCE)',()=>{});
|
|
11
|
+
for (const c of fixtures) test(`system ${c.tool} reference: ${c.name}`,async()=>{
|
|
12
|
+
const cwd=mkdtempSync(join(tmpdir(),'bunmsh-utility-reference-'));
|
|
13
|
+
try {
|
|
14
|
+
for (const [name,content] of Object.entries(c.files)) {
|
|
15
|
+
mkdirSync(dirname(join(cwd,name)),{recursive:true});writeFileSync(join(cwd,name),content);
|
|
16
|
+
}
|
|
17
|
+
for (const [name,mode] of Object.entries(c.modes??{})) chmodSync(join(cwd,name),mode);
|
|
18
|
+
const state=createState({cwd,env:{LC_ALL:'C'},shellOptions:{posix:true}});
|
|
19
|
+
const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
20
|
+
const r=await execute(['builtin',c.tool,...c.args].map(quote).join(' '),state,{capture:true,input:new Response(c.input??'').body});
|
|
21
|
+
expect({status:r.status,stdout:decode(r.stdout),stderr:decode(r.stderr),...(c.modes?{modes:Object.fromEntries(Object.keys(c.modes).map(name=>[name,statSync(join(cwd,name)).mode&0o7777]))}:{})}).toEqual(c.expected);
|
|
22
|
+
} finally {rmSync(cwd,{recursive:true,force:true});}
|
|
23
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const KITTY_CHUNK_SIZE = 4096;
|
|
2
|
+
const KITTY_TRANSPORT_KEYS = new Set(["m", "o", "O", "S", "t", "U", "mime"]);
|
|
3
|
+
|
|
4
|
+
function kittyPacket(control, payload = "") {
|
|
5
|
+
const entries = Object.entries(control)
|
|
6
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
7
|
+
.map(([key, value]) => value === true ? key : `${key}=${value}`);
|
|
8
|
+
return `\u001b_G${entries.join(",")};${payload}\u001b\\`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Convert the server's browser-neutral Kitty message back to native APCs. */
|
|
12
|
+
export function encodeKittyGraphics(message) {
|
|
13
|
+
if (!message || typeof message !== "object") return "";
|
|
14
|
+
if (message.kind === "delete") {
|
|
15
|
+
const deletion = message.delete || {};
|
|
16
|
+
return kittyPacket({
|
|
17
|
+
a: "d",
|
|
18
|
+
d: deletion.scope || "a",
|
|
19
|
+
i: deletion.imageId || undefined,
|
|
20
|
+
p: deletion.placementId || undefined,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (message.kind !== "placement" || !message.image?.data) return "";
|
|
24
|
+
|
|
25
|
+
const original = { ...(message.control || {}) };
|
|
26
|
+
const action = original.a || "t";
|
|
27
|
+
const placementOnly = action === "p";
|
|
28
|
+
const transfer = {};
|
|
29
|
+
for (const [key, value] of Object.entries(original)) {
|
|
30
|
+
if (!KITTY_TRANSPORT_KEYS.has(key)) transfer[key] = value;
|
|
31
|
+
}
|
|
32
|
+
transfer.a = placementOnly ? "t" : action;
|
|
33
|
+
transfer.t = "d";
|
|
34
|
+
transfer.f = message.image.format || transfer.f || 32;
|
|
35
|
+
if (message.image.width > 0) transfer.s = message.image.width;
|
|
36
|
+
if (message.image.height > 0) transfer.v = message.image.height;
|
|
37
|
+
if (!transfer.i && /^\d+$/.test(String(message.image.id || ""))) transfer.i = message.image.id;
|
|
38
|
+
|
|
39
|
+
const payload = String(message.image.data);
|
|
40
|
+
let output = "";
|
|
41
|
+
for (let offset = 0; offset < payload.length; offset += KITTY_CHUNK_SIZE) {
|
|
42
|
+
const more = offset + KITTY_CHUNK_SIZE < payload.length;
|
|
43
|
+
const control = offset === 0 ? { ...transfer, m: more ? 1 : 0 } : { m: more ? 1 : 0 };
|
|
44
|
+
output += kittyPacket(control, payload.slice(offset, offset + KITTY_CHUNK_SIZE));
|
|
45
|
+
}
|
|
46
|
+
if (placementOnly) output += kittyPacket(original);
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cursor motion the server adds for browser overlays; native Kitty does it itself. */
|
|
51
|
+
export function syntheticKittyCursorMotion(message) {
|
|
52
|
+
if (message?.kind !== "placement") return "";
|
|
53
|
+
const control = message.control || {};
|
|
54
|
+
if (control.C === "1") return "";
|
|
55
|
+
const cols = Math.max(0, Number.parseInt(control.c || "0", 10) || 0);
|
|
56
|
+
const rows = Math.max(0, Number.parseInt(control.r || "0", 10) || 0);
|
|
57
|
+
const cursorCol = Math.max(1, Number.parseInt(message.cursor?.col || "1", 10) || 1);
|
|
58
|
+
const targetCol = Math.max(1, cursorCol + cols);
|
|
59
|
+
if (rows > 0) return "\r\n".repeat(rows) + `\u001b[${targetCol}G`;
|
|
60
|
+
return cols > 0 ? `\u001b[${targetCol}G` : "";
|
|
61
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
|
+
import { encodeKittyGraphics, syntheticKittyCursorMotion } from "./client-kitty.mjs";
|
|
6
7
|
|
|
7
8
|
const MSG_INPUT = "1";
|
|
8
9
|
const MSG_PING = "2";
|
|
@@ -15,6 +16,7 @@ const MSG_SET_WINDOW_TITLE = "3";
|
|
|
15
16
|
const MSG_SET_PREFERENCES = "4";
|
|
16
17
|
const MSG_SET_RECONNECT = "5";
|
|
17
18
|
const MSG_SET_BUFFER_SIZE = "6";
|
|
19
|
+
const MSG_KITTY_GRAPHICS = "7";
|
|
18
20
|
const MSG_SET_ROLE = "a";
|
|
19
21
|
const MSG_CONTROL_RESULT = "b";
|
|
20
22
|
|
|
@@ -420,6 +422,7 @@ async function main() {
|
|
|
420
422
|
let fallbackOutputTail = "";
|
|
421
423
|
let viewerInputActive = false;
|
|
422
424
|
let sessionPid = "(unknown)";
|
|
425
|
+
let pendingKittyCursorMotion = "";
|
|
423
426
|
|
|
424
427
|
const normalizeEcho = (value) => String(value)
|
|
425
428
|
.replaceAll("\r", "")
|
|
@@ -603,6 +606,11 @@ pid:
|
|
|
603
606
|
case MSG_OUTPUT:
|
|
604
607
|
{
|
|
605
608
|
const output = Buffer.from(payload, "base64");
|
|
609
|
+
if (pendingKittyCursorMotion && output.equals(Buffer.from(pendingKittyCursorMotion, "utf8"))) {
|
|
610
|
+
pendingKittyCursorMotion = "";
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
pendingKittyCursorMotion = "";
|
|
606
614
|
process.stdout.write(output);
|
|
607
615
|
if (fallbackProbe && accessRole === "") {
|
|
608
616
|
fallbackOutputTail = normalizeEcho(
|
|
@@ -625,6 +633,14 @@ pid:
|
|
|
625
633
|
process.stdout.write(`\u001b]0;${payload}\u0007`);
|
|
626
634
|
}
|
|
627
635
|
break;
|
|
636
|
+
case MSG_KITTY_GRAPHICS:
|
|
637
|
+
try {
|
|
638
|
+
const message = JSON.parse(payload);
|
|
639
|
+
const encoded = encodeKittyGraphics(message);
|
|
640
|
+
pendingKittyCursorMotion = syntheticKittyCursorMotion(message);
|
|
641
|
+
if (encoded) process.stdout.write(encoded);
|
|
642
|
+
} catch {}
|
|
643
|
+
break;
|
|
628
644
|
case MSG_SET_RECONNECT: {
|
|
629
645
|
try {
|
|
630
646
|
const reconnect = JSON.parse(payload);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.15",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { encodeKittyGraphics, syntheticKittyCursorMotion } from "../client-kitty.mjs";
|
|
3
|
+
|
|
4
|
+
test("CLI converts a server Kitty placement to native direct-transfer APC", () => {
|
|
5
|
+
const encoded = encodeKittyGraphics({
|
|
6
|
+
kind: "placement",
|
|
7
|
+
control: { a: "T", t: "f", f: "100", i: "7", p: "9", c: "4", r: "2", o: "z" },
|
|
8
|
+
image: { id: "7", format: 100, width: 30, height: 20, data: "AA==" },
|
|
9
|
+
});
|
|
10
|
+
expect(encoded).toBe("\x1b_Ga=T,f=100,i=7,p=9,c=4,r=2,t=d,s=30,v=20,m=0;AA==\x1b\\");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("CLI retransmits cached data before a placement-only Kitty command", () => {
|
|
14
|
+
const data = "A".repeat(4100);
|
|
15
|
+
const encoded = encodeKittyGraphics({
|
|
16
|
+
kind: "placement",
|
|
17
|
+
control: { a: "p", i: "8", p: "2", C: "1" },
|
|
18
|
+
image: { id: "8", format: 100, width: 1, height: 1, data },
|
|
19
|
+
});
|
|
20
|
+
expect(encoded).toContain("\x1b_Ga=t,i=8,p=2,C=1,t=d,f=100,s=1,v=1,m=1;");
|
|
21
|
+
expect(encoded).toContain("\x1b_Gm=0;AAAA\x1b\\");
|
|
22
|
+
expect(encoded.endsWith("\x1b_Ga=p,i=8,p=2,C=1;\x1b\\")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("CLI converts Kitty deletion messages", () => {
|
|
26
|
+
expect(encodeKittyGraphics({
|
|
27
|
+
kind: "delete",
|
|
28
|
+
delete: { scope: "I", imageId: "7", placementId: "3" },
|
|
29
|
+
})).toBe("\x1b_Ga=d,d=I,i=7,p=3;\x1b\\");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("CLI identifies browser-only cursor motion after native Kitty placement", () => {
|
|
33
|
+
expect(syntheticKittyCursorMotion({
|
|
34
|
+
kind: "placement",
|
|
35
|
+
cursor: { row: 3, col: 4 },
|
|
36
|
+
control: { c: "6", r: "2" },
|
|
37
|
+
})).toBe("\r\n\r\n\x1b[10G");
|
|
38
|
+
expect(syntheticKittyCursorMotion({
|
|
39
|
+
kind: "placement",
|
|
40
|
+
cursor: { row: 3, col: 4 },
|
|
41
|
+
control: { c: "6", r: "2", C: "1" },
|
|
42
|
+
})).toBe("");
|
|
43
|
+
});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
nameserver
|
|
1
|
+
nameserver 1.1.1.1
|
|
2
|
+
nameserver 8.8.8.8
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
`/bin/tar` is a small Bun script over `Bun.Archive`. It handles uncompressed
|
|
4
4
|
tar archives plus gzip and zstd without invoking another program.
|
|
5
5
|
|
|
6
|
+
Alpine APK v2 archives are supported directly, including their concatenated
|
|
7
|
+
gzip streams and tar sections. For example:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
tar -tf musl-dev-1.2.6-r2.apk
|
|
11
|
+
tar -xf musl-dev-1.2.6-r2.apk -C ./musl-dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This extracts archive contents, including package metadata; it does not
|
|
15
|
+
install the package or execute its package scripts.
|
|
16
|
+
|
|
6
17
|
## Synopsis
|
|
7
18
|
|
|
8
19
|
```sh
|
|
@@ -50,7 +61,7 @@ tar --zstd -xf backup.tar.zst -C /mnt/restore
|
|
|
50
61
|
tar -xzf package.tgz -C /tmp/package --strip-components=1
|
|
51
62
|
|
|
52
63
|
# Exclude browser resources; match before removing package/
|
|
53
|
-
tar -xzf buninu-browser
|
|
64
|
+
tar -xzf buninu-browser-*.tgz -C /tmp/browser --strip-components=1 --exclude=package/usr
|
|
54
65
|
|
|
55
66
|
# An Alpine root filesystem, symbolic links and all; then enter it
|
|
56
67
|
tar -xzf alpine-minirootfs-3.24.1-x86_64.tar.gz -C /mnt/alpine
|
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: "755deb045cfeb8c2bcaa2b2febe01ab2101d747921e64d4cd54d0312321e8d5d",
|
|
20
|
+
linuxLtsSha256: "b0d8cc4d7e031b5619221e7978920d051bf1dd1e335f6d05d03964b2285fc3b4",
|
|
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: "e3839bfd751ff0a4421484f712b9b29efb514024523736a0ef62521521571b12",
|
|
36
|
+
linuxLtsSha256: "795bcd49840be7f4ff9566f5f669201bd41202376a455b720a98eb814f0e72f8",
|
|
37
37
|
muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
|
|
38
38
|
stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
|
|
39
39
|
libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
|
|
@@ -67,7 +67,7 @@ export const config = ({
|
|
|
67
67
|
// The Alpine repository keeps only the newest build of a package, so a pin
|
|
68
68
|
// stops downloading when Alpine bumps it; the hashes then fail loudly.
|
|
69
69
|
const alpineBase = `${mirror}/v3.24/main/${arch}`;
|
|
70
|
-
const kernelVersion = "6.18.
|
|
70
|
+
const kernelVersion = "6.18.56";
|
|
71
71
|
const kernelRelease = `${kernelVersion}-0-${flavor}`;
|
|
72
72
|
const bunVersion = "1.4.2";
|
|
73
73
|
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.56
|
|
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=755deb045cfeb8c2bcaa2b2febe01ab2101d747921e64d4cd54d0312321e8d5d
|
|
67
|
+
linux_lts_sha256=b0d8cc4d7e031b5619221e7978920d051bf1dd1e335f6d05d03964b2285fc3b4
|
|
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=e3839bfd751ff0a4421484f712b9b29efb514024523736a0ef62521521571b12
|
|
86
|
+
linux_lts_sha256=795bcd49840be7f4ff9566f5f669201bd41202376a455b720a98eb814f0e72f8
|
|
87
87
|
musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
|
|
88
88
|
stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
|
|
89
89
|
libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
|
package/scripts/fetch.js
CHANGED
|
@@ -98,6 +98,8 @@ export const readTar = (path) => {
|
|
|
98
98
|
if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
|
|
99
99
|
}
|
|
100
100
|
return {
|
|
101
|
+
// Enumerate regular files without exposing the Map's mutation methods.
|
|
102
|
+
entries() { return files.entries(); },
|
|
101
103
|
get(name) {
|
|
102
104
|
const bytes = files.get(name);
|
|
103
105
|
if (!bytes) throw new Error(`${path}: no ${name}`);
|
package/scripts/npm-packages.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# npm packages
|
|
2
2
|
# Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
|
|
3
|
+
# buninu-browser updated and verified on 2026-10-12.
|
|
3
4
|
|
|
4
|
-
"buninu-browser@0.2.
|
|
5
|
-
- https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.
|
|
6
|
-
- sha256:
|
|
5
|
+
"buninu-browser@0.2.3":
|
|
6
|
+
- https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.3.tgz
|
|
7
|
+
- sha256:0f7c9cd9df689d7b0900fdf3172851125ebd8aba12bed555be7e091d427146e8
|
|
7
8
|
|
|
8
9
|
"@drxiaozhi/casty@1.3.4":
|
|
9
10
|
- https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.4.tgz
|
package/test/fetch-npm.test.js
CHANGED
|
@@ -23,7 +23,7 @@ const url = `http://127.0.0.1:${server.port}/archive`;
|
|
|
23
23
|
|
|
24
24
|
test("four exact registry pins share an architecture-independent cache", () => {
|
|
25
25
|
expect(npmPackages.map(({ name, version }) => `${name}@${version}`)).toEqual([
|
|
26
|
-
"buninu-browser@0.2.
|
|
26
|
+
"buninu-browser@0.2.3", "@drxiaozhi/casty@1.3.4",
|
|
27
27
|
"@drxiaozhi/jspulse@0.2.1", "@drxiaozhi/jspinyin@0.1.1",
|
|
28
28
|
]);
|
|
29
29
|
expect(basename(npmDownloadsDir)).toBe("npm");
|
package/test/tar-strip.test.js
CHANGED
|
@@ -216,3 +216,39 @@ test("does not follow a pre-existing symlink parent or an archive symlink parent
|
|
|
216
216
|
expect(result.status).toBe(2);
|
|
217
217
|
expect(existsSync(join(outside, "nested"))).toBe(false);
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
test("APK concatenated gzip/tar streams list and extract every section", () => {
|
|
221
|
+
const signature = tar([{ name: ".SIGN.test", data: "signature" }]);
|
|
222
|
+
const control = tar([{ name: ".PKGINFO", data: "pkgname = sample\n" }]);
|
|
223
|
+
const data = tar([
|
|
224
|
+
{ name: "usr/include/stdio.h", data: "header\n" },
|
|
225
|
+
{ name: "usr/lib/crt1.o", data: "object\n" },
|
|
226
|
+
{ name: "usr/lib/link", type: "2", target: "crt1.o" },
|
|
227
|
+
]);
|
|
228
|
+
// APK signature/control streams can omit end markers; also cover complete
|
|
229
|
+
// concatenated tar archives, and a single gzip stream containing them all.
|
|
230
|
+
for (const raw of [false, true]) {
|
|
231
|
+
const parts = [raw ? signature.subarray(0, -1024) : signature,
|
|
232
|
+
raw ? control.subarray(0, -1024) : control, data];
|
|
233
|
+
for (const bytes of [Buffer.concat(parts.map(p => Bun.gzipSync(p))), Bun.gzipSync(Buffer.concat(parts))]) {
|
|
234
|
+
const result = extract(bytes, [], true);
|
|
235
|
+
expect(result.status).toBe(0);
|
|
236
|
+
expect(readFileSync(join(result.dir, ".PKGINFO"), "utf8")).toBe("pkgname = sample\n");
|
|
237
|
+
expect(readFileSync(join(result.dir, "usr/include/stdio.h"), "utf8")).toBe("header\n");
|
|
238
|
+
expect(readlinkSync(join(result.dir, "usr/lib/link"))).toBe("crt1.o");
|
|
239
|
+
const listed = run(["-tf", "-"], bytes);
|
|
240
|
+
expect(listed.status).toBe(0);
|
|
241
|
+
expect(listed.stdout.trim().split("\n")).toEqual([
|
|
242
|
+
".SIGN.test", ".PKGINFO", "usr/include/stdio.h", "usr/lib/crt1.o", "usr/lib/link",
|
|
243
|
+
]);
|
|
244
|
+
const selected = extract(bytes, ["usr/include"], true);
|
|
245
|
+
expect(selected.status).toBe(0);
|
|
246
|
+
expect(existsSync(join(selected.dir, ".PKGINFO"))).toBe(false);
|
|
247
|
+
expect(readFileSync(join(selected.dir, "usr/include/stdio.h"), "utf8")).toBe("header\n");
|
|
248
|
+
const stripped = extract(bytes, ["--strip-components=2", "--exclude=usr/lib", "usr"], true);
|
|
249
|
+
expect(stripped.status).toBe(0);
|
|
250
|
+
expect(readFileSync(join(stripped.dir, "stdio.h"), "utf8")).toBe("header\n");
|
|
251
|
+
expect(existsSync(join(stripped.dir, "crt1.o"))).toBe(false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|