buninu-linux 0.11.56 → 0.13.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 CHANGED
@@ -285,8 +285,8 @@ committed source files, but their contents are redistributed in the image.
285
285
  | Package | Version | Package license | Project / package page | Installed license and notices (relative to `/usr/lib/node_modules/`) |
286
286
  |---|---|---|---|---|
287
287
  | `buninu-browser` | 0.2.3 | MIT for the browser's own code; bundled components retain their own terms | https://github.com/jjtseng93/buninu-browser | `buninu-browser/LICENSE`, `THIRD_PARTY_NOTICES.md`, `LICENSES/` and vendor notices within that package |
288
- | `buninu-land` | 0.1.2 | MIT for its own code; bundled components retain their own terms | https://github.com/jjtseng93/buninu-land | `buninu-land/LICENSE`, `THIRD_PARTY_NOTICES.md`, `LICENSES/`, `SOURCES.md`, protocol XML notices and vendor notices |
289
- | `@drxiaozhi/casty` | 1.3.6 | MIT | https://github.com/jjtseng93/casty | `@drxiaozhi/casty/LICENSE` |
288
+ | `buninu-land` | 0.1.3 | MIT for its own code; bundled components retain their own terms | https://github.com/jjtseng93/buninu-land | `buninu-land/LICENSE`, `THIRD_PARTY_NOTICES.md`, `LICENSES/`, `SOURCES.md`, protocol XML notices and vendor notices |
289
+ | `@drxiaozhi/casty` | 1.3.7 | MIT | https://github.com/jjtseng93/casty | `@drxiaozhi/casty/LICENSE` |
290
290
  | `@drxiaozhi/jspulse` | 0.2.1 | LGPL-2.1-or-later, with additional component notices | https://github.com/jjtseng93/jspulse | `@drxiaozhi/jspulse/LICENSE`, `NOTICE.md`, `LICENSES/` and source-file notices within that package |
291
291
  | `@drxiaozhi/jspinyin` | 0.1.1 | MIT; hanzipinyin material under the Unlicense | https://www.npmjs.com/package/@drxiaozhi/jspinyin | `@drxiaozhi/jspinyin/LICENSE` and `@drxiaozhi/jspinyin/LICENSE-hanzipinyin.md` |
292
292
 
@@ -320,8 +320,11 @@ the source archive or rebuild material merely to reduce the image size.
320
320
 
321
321
  ### Buninu Land
322
322
 
323
- The compositor package is extracted in full, including its CanvasKit WASM,
324
- loader, protocol XML, source provenance, patches and license collection.
323
+ The compositor package retains its loader, protocol XML, source provenance,
324
+ patches and license collection. When its CanvasKit WASM is byte-identical to
325
+ the system copy, packing omits the duplicate and the distribution launcher
326
+ creates a relative symlink to `/lib/canvaskit/canvaskit.wasm`. Differing WASM
327
+ versions remain bundled. All associated licenses and notices are retained.
325
328
  Retain `THIRD_PARTY_NOTICES.md`, `SOURCES.md`, `vendor-manifest.json`,
326
329
  `LICENSES/`, `vendor/canvaskit/LICENSE`, and the notices embedded in the
327
330
  Wayland protocol XML files. These cover Buninu Browser-derived code,
package/README.md CHANGED
@@ -763,7 +763,8 @@ The `/bin/buninu-land` launcher sets `CASTY_BROWSER` to
763
763
  `/usr/lib/node_modules/buninu-land/compositor.js` and runs the bundled Casty.
764
764
  `--casty` forwards arguments to Casty; `--headless`, `--help`, `--version`,
765
765
  README options and reserved native frontends go through the package entry.
766
- The package's CanvasKit WASM and license files are retained unchanged.
766
+ The package's CanvasKit WASM shares the system copy when byte-identical;
767
+ its loader and license files are retained.
767
768
 
768
769
  ### Archive tools and other examples
769
770
 
@@ -1193,11 +1194,11 @@ extracts from it. Bumping a version changes both the file name and the hash, so
1193
1194
  it always refetches.
1194
1195
 
1195
1196
  The five application packages are pinned separately in
1196
- `scripts/npm-packages.md`: `buninu-browser@0.2.3`, `buninu-land@0.1.2`,
1197
- `@drxiaozhi/casty@1.3.6`, `@drxiaozhi/jspulse@0.2.1`, and
1197
+ `scripts/npm-packages.md`: `buninu-browser@0.2.3`, `buninu-land@0.1.3`,
1198
+ `@drxiaozhi/casty@1.3.7`, `@drxiaozhi/jspulse@0.2.1`, and
1198
1199
  `@drxiaozhi/jspinyin@0.1.1`. These were npm's `latest` versions when checked
1199
1200
  on 2026-10-11, with buninu-browser updated on 2026-10-12, and
1200
- casty 1.3.6 and buninu-land 0.1.2 verified on 2026-10-16;
1201
+ casty 1.3.7 and buninu-land 0.1.3 verified on 2026-10-16;
1201
1202
  fetch never resolves `latest` or a version range at runtime.
1202
1203
  Each registry tarball was checked against its published SHA-512 integrity
1203
1204
  before its SHA-256 was recorded in the shared pins file.
@@ -1696,9 +1697,9 @@ when redistributing the image.
1696
1697
  | systemd EFI stub | LGPL-2.1-or-later | image only |
1697
1698
  | Bun | MIT, plus the licenses of what it statically links | image only |
1698
1699
  | buninu-browser 0.2.3 | MIT for its own code; bundled components have their own licenses | npm download → image |
1699
- | buninu-land 0.1.2 | MIT for its own code; bundled components retain their own licenses and notices | npm download → image |
1700
+ | buninu-land 0.1.3 | MIT for its own code; bundled components retain their own licenses and notices | npm download → image |
1700
1701
  | wasmpeg / FFmpeg (`@wasmpeg/cpu` 1.0.1, bundled in buninu-browser) | LGPL-2.1-or-later, plus component notices; source archive and rebuild documentation retained | browser package → image |
1701
- | `@drxiaozhi/casty` 1.3.6 | MIT | npm download → image |
1702
+ | `@drxiaozhi/casty` 1.3.7 | MIT | npm download → image |
1702
1703
  | `@drxiaozhi/jspulse` 0.2.1 | LGPL-2.1-or-later, plus component notices | npm download → image |
1703
1704
  | `@drxiaozhi/jspinyin` 0.1.1 | MIT; hanzipinyin material under the Unlicense | npm download → image |
1704
1705
  | CanvasKit 0.41.1 (`initramfs/lib/canvaskit/`, Skia compiled to WebAssembly) | BSD-3-Clause (`LICENSES/BSD-3-Clause-Skia.txt`) | committed |
@@ -3,10 +3,12 @@
3
3
  // preserve the distribution entry point.
4
4
  import { resolve, join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { ensureLandResources } from "../lib/browser-resources.js";
6
7
 
7
8
  const root = resolve(import.meta.dir, "..");
8
9
  const modules = join(root, "usr/lib/node_modules");
9
10
  const browser = join(modules, "buninu-land");
11
+ ensureLandResources(browser, root);
10
12
  const entry = join(browser, "index.js");
11
13
  const { chooseFrontend } = await import(pathToFileURL(entry).href);
12
14
  const args = process.argv.slice(2);
@@ -1001,7 +1001,9 @@ class CursorStateTracker {
1001
1001
  }
1002
1002
 
1003
1003
  class KittyGraphicsParser {
1004
- constructor() {
1004
+ constructor({ binaryData = false } = {}) {
1005
+ // Local renderers can consume bytes directly; browser clients retain base64.
1006
+ this.binaryData = binaryData;
1005
1007
  this.pending = Buffer.alloc(0);
1006
1008
  this.transfers = new Map();
1007
1009
  this.images = new Map();
@@ -1220,7 +1222,7 @@ class KittyGraphicsParser {
1220
1222
  width: Number.parseInt(existing.control.s || "0", 10),
1221
1223
  height: Number.parseInt(existing.control.v || "0", 10),
1222
1224
  mime: existing.control.U || existing.control.mime || null,
1223
- data: binary.toString("base64")
1225
+ data: this.binaryData ? binary : binary.toString("base64")
1224
1226
  };
1225
1227
 
1226
1228
  this.images.set(imageId, image);
@@ -23,3 +23,14 @@ export function ensureBrowserResources(browser, systemRoot = "/") {
23
23
  catch (error) { if (error.code !== "EEXIST") throw error; }
24
24
  }
25
25
  }
26
+
27
+ // Land keeps its own loader and notices; only the identical WASM is shared.
28
+ export function ensureLandResources(land, systemRoot = "/") {
29
+ const path = join(land, "vendor/canvaskit/canvaskit.wasm");
30
+ if (lstat(path)) return;
31
+ const target = resolve(systemRoot, "lib/canvaskit/canvaskit.wasm");
32
+ if (!statSync(target).isFile()) throw new Error(`missing compositor resource: ${target}`);
33
+ mkdirSync(dirname(path), { recursive: true });
34
+ try { symlinkSync(relative(dirname(path), target), path); }
35
+ catch (error) { if (error.code !== "EEXIST") throw error; }
36
+ }
@@ -16,7 +16,8 @@ const number = (value, fallback = 0) => {
16
16
  // encoded format Skia knows); 24 and 32 are raw RGB / RGBA. Bun.Image, when
17
17
  // present, transcodes encodings Skia cannot read on its own.
18
18
  export const decodeImage = async (CanvasKit, image) => {
19
- const bytes = Buffer.from(image.data, "base64");
19
+ // The local parser supplies bytes; retain compatibility with base64 callers.
20
+ const bytes = typeof image.data === "string" ? Buffer.from(image.data, "base64") : image.data;
20
21
  if (image.format === 24 || image.format === 32) {
21
22
  const { width, height } = image;
22
23
  if (!width || !height) throw new Error("raw image without dimensions");
@@ -70,7 +70,7 @@ export const createSession = ({
70
70
  term.loadAddon(new UnicodeGraphemesAddon());
71
71
  term.unicode.activeVersion = "15-graphemes";
72
72
  const images = new ImageStore({ CanvasKit, term, renderer });
73
- const kitty = new KittyGraphicsParser();
73
+ const kitty = new KittyGraphicsParser({ binaryData: true });
74
74
 
75
75
  const backend = new BunPtyBackend({
76
76
  command,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.11.56",
3
+ "version": "0.13.56",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -1,19 +1,19 @@
1
1
  # npm packages
2
2
  # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
3
  # buninu-browser updated and verified on 2026-10-12.
4
- # casty 1.3.6 and buninu-land 0.1.2 verified on 2026-10-16.
4
+ # casty 1.3.7 and buninu-land 0.1.3 verified on 2026-10-16.
5
5
 
6
6
  "buninu-browser@0.2.3":
7
7
  - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.3.tgz
8
8
  - sha256:0f7c9cd9df689d7b0900fdf3172851125ebd8aba12bed555be7e091d427146e8
9
9
 
10
- "buninu-land@0.1.2":
11
- - https://registry.npmjs.org/buninu-land/-/buninu-land-0.1.2.tgz
12
- - sha256:0b682e1d57f953d9fa9415369c320e4e35bf7575c2859673c26a0789f5369490
10
+ "buninu-land@0.1.3":
11
+ - https://registry.npmjs.org/buninu-land/-/buninu-land-0.1.3.tgz
12
+ - sha256:b2c94f7fa0f7275ed739f4949d0be3ef45a99bf0b61e63a71ea9d6459f9f1c2e
13
13
 
14
- "@drxiaozhi/casty@1.3.6":
15
- - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.6.tgz
16
- - sha256:2b5f84e3a63c2c16fa92dc88faa43eec54edc2fe69154e5270af3cd9e7f472b8
14
+ "@drxiaozhi/casty@1.3.7":
15
+ - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.7.tgz
16
+ - sha256:acc0641fb68c00222ba1cd2b7520ba99c269b996e145e82f18466be4d126b811
17
17
 
18
18
  "@drxiaozhi/jspulse@0.2.1":
19
19
  - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.1.tgz
@@ -1,7 +1,7 @@
1
1
  // Assemble fetched packages in a disposable pack staging tree, never in the
2
2
  // source initramfs. No registry access, dependency resolution or lifecycle code.
3
3
  import { createHash } from "node:crypto";
4
- import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { join, relative, resolve } from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { jsRuntime, rootDir, runIfMain } from "./arch.js";
@@ -34,6 +34,13 @@ export function stageNpm(staging, { packages = npmPackages, downloadsDir = npmDo
34
34
  "--exclude=package/usr/lib/canvaskit", "--exclude=package/usr/share/fonts");
35
35
  const result = spawnSync(runtime, args, { stdio: "inherit" });
36
36
  if (result.status !== 0) throw new Error(`npm extraction failed: ${pkg.name}`);
37
+ if (pkg.name === "buninu-land") {
38
+ const wasm = join(destination, "vendor/canvaskit/canvaskit.wasm");
39
+ const shared = join(staging, "lib/canvaskit/canvaskit.wasm");
40
+ const systemWasm = existsSync(shared) ? shared : join(rootDir, "initramfs/lib/canvaskit/canvaskit.wasm");
41
+ // Deduplicate only byte-identical releases; future differing WASM stays bundled.
42
+ if (existsSync(wasm) && existsSync(systemWasm) && readFileSync(wasm).equals(readFileSync(systemWasm))) unlinkSync(wasm);
43
+ }
37
44
  const info = JSON.parse(readFileSync(join(destination, "package.json"), "utf8"));
38
45
  if (info.name !== pkg.name || info.version !== pkg.version) throw new Error(`npm identity mismatch: ${pkg.name}`);
39
46
  const entries = typeof info.bin === "string" ? { [pkg.name.split("/").pop()]: info.bin } : info.bin ?? {};
@@ -45,7 +45,15 @@ for ((i=0; i<${#names[@]}; i++)); do
45
45
  extra=()
46
46
  [[ "$name" != buninu-browser ]] || extra+=(--exclude=package/usr/lib/canvaskit --exclude=package/usr/share/fonts)
47
47
  tar -xf "downloads/npm/${archives[i]}" -C "$destination" --strip-components=1 "${extra[@]}"
48
- # Bin entries of the four SHA-pinned packages; no package scripts execute.
48
+ if [[ "$name" == buninu-land ]]; then
49
+ wasm="$destination/vendor/canvaskit/canvaskit.wasm"
50
+ shared="$staging/lib/canvaskit/canvaskit.wasm"
51
+ [[ -f "$shared" ]] || shared=initramfs/lib/canvaskit/canvaskit.wasm
52
+ if [[ -f "$wasm" && -f "$shared" ]] && cmp -s "$wasm" "$shared"; then
53
+ rm -- "$wasm"
54
+ fi
55
+ fi
56
+ # Bin entries of the SHA-pinned packages; no package scripts execute.
49
57
  bin=${name##*/}; entry=index.js
50
58
  [[ "$name" != @drxiaozhi/jspulse ]] || entry=jspulse.js
51
59
  chmod 755 "$destination/$entry"
@@ -0,0 +1,55 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { createRequire } from 'node:module';
3
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { deflateSync } from 'node:zlib';
7
+ import { decodeImage } from '../initramfs/lib/bunterm/images.js';
8
+ const { KittyGraphicsParser } = createRequire(import.meta.url)('../initramfs/buninu/apps/jsgotty/gotty.js');
9
+ const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aOc8AAAAASUVORK5CYII=', 'base64');
10
+ function parse(parser, control, payload = '') {
11
+ const { events } = parser.consume(Buffer.from(`\x1b_G${control};${payload}\x1b\\`));
12
+ return parser.parsePacket(events[0].packet, { row: 1, col: 1 });
13
+ }
14
+ async function checkDecode(image) {
15
+ const decoded = {};
16
+ let received;
17
+ expect(await decodeImage({ MakeImageFromEncoded(bytes) { received = bytes; return decoded; } }, image)).toBe(decoded);
18
+ // Identity verifies that rendering neither copies nor base64-roundtrips bytes.
19
+ expect(received).toBe(image.data);
20
+ expect(received.equals(png)).toBe(true);
21
+ }
22
+ test('binary parser assembles inline chunks and renderer consumes the same bytes', async () => {
23
+ const parser = new KittyGraphicsParser({ binaryData: true });
24
+ const encoded = png.toString('base64');
25
+ expect(parse(parser, 'a=T,f=100,i=1,m=1', encoded.slice(0, 32))).toBeNull();
26
+ const graphic = parse(parser, 'm=0', encoded.slice(32));
27
+ await checkDecode(graphic.image);
28
+ expect(parse(parser, 'a=p,i=1').image).toBe(graphic.image);
29
+ });
30
+ test('binary file transfer reads bytes without re-encoding; compressed inline also works', async () => {
31
+ const dir = mkdtempSync(join(tmpdir(), 'bunterm-image-bytes-'));
32
+ try {
33
+ const path = join(dir, 'frame.png');
34
+ writeFileSync(path, png);
35
+ const parser = new KittyGraphicsParser({ binaryData: true });
36
+ await checkDecode(parse(parser, 'a=T,t=f,f=100,i=1', Buffer.from(path).toString('base64')).image);
37
+ await checkDecode(parse(parser, 'a=T,f=100,o=z,i=2', deflateSync(png).toString('base64')).image);
38
+ } finally { rmSync(dir, { recursive: true, force: true }); }
39
+ });
40
+ test('default parser and legacy renderer callers retain base64 compatibility', async () => {
41
+ const image = parse(new KittyGraphicsParser(), 'a=T,f=100,i=1', png.toString('base64')).image;
42
+ expect(image.data).toBe(png.toString('base64'));
43
+ let received;
44
+ await decodeImage({ MakeImageFromEncoded(bytes) { received = bytes; return {}; } }, image);
45
+ expect(received.equals(png)).toBe(true);
46
+ });
47
+ test('raw RGBA bytes pass directly to CanvasKit', async () => {
48
+ const pixels = Buffer.from([1, 2, 3, 255]);
49
+ const image = parse(new KittyGraphicsParser({ binaryData: true }), 'a=T,f=32,s=1,v=1,i=3', pixels.toString('base64')).image;
50
+ let received;
51
+ await decodeImage({ ColorType: { RGBA_8888: 1 }, AlphaType: { Unpremul: 1 }, ColorSpace: { SRGB: 1 },
52
+ MakeImage(info, bytes, stride) { received = bytes; expect(stride).toBe(4); return {}; } }, image);
53
+ expect(received).toBe(image.data);
54
+ expect(received.equals(pixels)).toBe(true);
55
+ });
@@ -23,7 +23,7 @@ const url = `http://127.0.0.1:${server.port}/archive`;
23
23
 
24
24
  test("five exact registry pins share an architecture-independent cache", () => {
25
25
  expect(npmPackages.map(({ name, version }) => `${name}@${version}`)).toEqual([
26
- "buninu-browser@0.2.3", "buninu-land@0.1.2", "@drxiaozhi/casty@1.3.6",
26
+ "buninu-browser@0.2.3", "buninu-land@0.1.3", "@drxiaozhi/casty@1.3.7",
27
27
  "@drxiaozhi/jspulse@0.2.1", "@drxiaozhi/jspinyin@0.1.1",
28
28
  ]);
29
29
  expect(basename(npmDownloadsDir)).toBe("npm");
@@ -6,7 +6,7 @@ import { join, resolve } from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { stageNpm } from "../scripts/stage-npm.js";
8
8
  import { npmPackages, npmDownloadsDir } from "../scripts/fetch-npm.js";
9
- import { ensureBrowserResources } from "../initramfs/lib/browser-resources.js";
9
+ import { ensureBrowserResources, ensureLandResources } from "../initramfs/lib/browser-resources.js";
10
10
 
11
11
  const work = mkdtempSync(join(tmpdir(), "buninu-stage-npm-"));
12
12
  afterAll(() => rmSync(work, { recursive: true, force: true }));
@@ -26,6 +26,7 @@ async function fixture() {
26
26
  "package/usr/lib/canvaskit/data": "only browser excludes this",
27
27
  "package/usr/share/fonts/data": "only browser excludes this",
28
28
  "package/lib/data": "keep data",
29
+ "package/vendor/canvaskit/canvaskit.wasm": "different wasm must be retained",
29
30
  }, { compress: "gzip" }).bytes();
30
31
  const archive = bin + ".tgz";
31
32
  writeFileSync(join(downloadsDir, archive), data);
@@ -47,6 +48,7 @@ test("stages standard scoped layout, manifest, bins and browser-only exclusion",
47
48
  expect(readFileSync(join(dir, "usr/README.md"), "utf8")).toBe("keep resource documentation");
48
49
  expect(existsSync(join(dir, "usr/lib/canvaskit"))).toBe(pkg.name !== "buninu-browser");
49
50
  expect(existsSync(join(dir, "usr/share/fonts"))).toBe(pkg.name !== "buninu-browser");
51
+ expect(readFileSync(join(dir, "vendor/canvaskit/canvaskit.wasm"), "utf8")).toBe("different wasm must be retained");
50
52
  expect(statSync(join(dir, "index.js")).mode & 0o111).toBe(0o111);
51
53
  expect(readlinkSync(join(lib, "node_modules/.bin", pkg.name.split("/").pop())))
52
54
  .toBe(`../${pkg.name}/index.js`);
@@ -109,6 +111,8 @@ test("land launcher selects bundled casty and preserves package frontend dispatc
109
111
  stageNpm(root, cfg);
110
112
  const land = join(root, "usr/lib/node_modules/buninu-land");
111
113
  mkdirSync(join(root, "bin"));
114
+ mkdirSync(join(root, "lib"), { recursive: true });
115
+ copyFileSync(new URL("../initramfs/lib/browser-resources.js", import.meta.url), join(root, "lib/browser-resources.js"));
112
116
  const launcher = join(root, "bin/buninu-land");
113
117
  copyFileSync(new URL("../initramfs/bin/buninu-land", import.meta.url), launcher);
114
118
  writeFileSync(join(land, "index.js"), `
@@ -144,6 +148,7 @@ test.skipIf(!npmPackages.every(pkg => existsSync(join(npmDownloadsDir, pkg.archi
144
148
  expect(result.status).toBe(0);
145
149
  expect(readFileSync(join(shell, "usr/lib/package.json"), "utf8")).toBe(readFileSync(join(js, "usr/lib/package.json"), "utf8"));
146
150
  for (const root of [js, shell]) {
151
+ expect(existsSync(join(root, "usr/lib/node_modules/buninu-land/vendor/canvaskit/canvaskit.wasm"))).toBe(false);
147
152
  expect(existsSync(join(root, "usr/lib/node_modules/buninu-browser/usr/README.md"))).toBe(true);
148
153
  expect(existsSync(join(root, "usr/lib/node_modules/buninu-browser/usr/lib/canvaskit"))).toBe(false);
149
154
  expect(existsSync(join(root, "usr/lib/node_modules/buninu-browser/usr/share/fonts"))).toBe(false);
@@ -164,8 +169,23 @@ test.skipIf(!npmPackages.every(pkg => existsSync(join(npmDownloadsDir, pkg.archi
164
169
  for (const args of [["--help"], ["--headless", "--help"], ["--version"]]) {
165
170
  const landHelp = spawnSync(process.execPath, [join(js, "bin/buninu-land"), ...args], { encoding: "utf8", timeout: 15000 });
166
171
  expect(landHelp.status).toBe(0);
167
- expect(landHelp.stdout).toContain(args[0] === "--version" ? "0.1.2" : args[0] === "--headless" ? "Wayland compositor" : "Usage");
172
+ expect(landHelp.stdout).toContain(args[0] === "--version" ? "0.1.3" : args[0] === "--headless" ? "Wayland compositor" : "Usage");
168
173
  }
169
- expect(existsSync(join(js, "usr/lib/node_modules/buninu-land/vendor/canvaskit/canvaskit.wasm"))).toBe(true);
174
+ const landWasm = join(js, "usr/lib/node_modules/buninu-land/vendor/canvaskit/canvaskit.wasm");
175
+ expect(readlinkSync(landWasm).startsWith("/")).toBe(false);
176
+ expect(readFileSync(landWasm).equals(readFileSync(join(js, "lib/canvaskit/canvaskit.wasm")))).toBe(true);
170
177
  expect(existsSync(join(js, "usr/lib/node_modules/buninu-land/LICENSES/hyprland.txt"))).toBe(true);
171
178
  }, 30000);
179
+
180
+ test("land resource link is relative, repeatable, and preserves a package-owned WASM", () => {
181
+ const root = fresh(), land = join(root, "usr/lib/node_modules/buninu-land");
182
+ mkdirSync(join(root, "lib/canvaskit"), { recursive: true });
183
+ writeFileSync(join(root, "lib/canvaskit/canvaskit.wasm"), "shared");
184
+ ensureLandResources(land, root); ensureLandResources(land, root);
185
+ const wasm = join(land, "vendor/canvaskit/canvaskit.wasm");
186
+ expect(readlinkSync(wasm).startsWith("/")).toBe(false);
187
+ expect(readFileSync(wasm, "utf8")).toBe("shared");
188
+ rmSync(wasm); writeFileSync(wasm, "package-owned");
189
+ ensureLandResources(land, root);
190
+ expect(readFileSync(wasm, "utf8")).toBe("package-owned");
191
+ });