akanjs 3.0.0 → 3.0.2-rc.0

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +3670 -0
  2. package/base/base.ts +1 -1
  3. package/client/cookie.ts +4 -6
  4. package/client/csrTypes.ts +2 -2
  5. package/common/Logger.ts +10 -0
  6. package/common/index.ts +0 -1
  7. package/common/routeConvention.ts +1 -9
  8. package/dictionary/base.dictionary.ts +7 -0
  9. package/dictionary/trans.ts +7 -1
  10. package/document/documentQuery.ts +6 -1
  11. package/document/index.ts +1 -0
  12. package/document/into.ts +15 -7
  13. package/document/liveRowScope.ts +79 -0
  14. package/fetch/client/fetchClient.ts +64 -12
  15. package/fetch/client/httpClient.ts +13 -4
  16. package/index.ts +22 -0
  17. package/package.json +1 -1
  18. package/server/akanOption.ts +20 -1
  19. package/server/akanServer.ts +5 -0
  20. package/server/cachePolicy.ts +12 -2
  21. package/server/crawler.ts +6 -0
  22. package/server/head.tsx +0 -4
  23. package/server/mcp/McpAuth.ts +3 -2
  24. package/server/mcp/McpExecutionContext.ts +15 -14
  25. package/server/mcp/McpRateLimiter.ts +18 -67
  26. package/server/proxy/webProxyRunner.ts +2 -0
  27. package/server/resolver/database.resolver.ts +28 -13
  28. package/server/resolver/signal.resolver.ts +75 -35
  29. package/server/robots.ts +1 -23
  30. package/server/routeTreeBuilder.ts +4 -2
  31. package/server/rscWorker.tsx +12 -24
  32. package/server/rscWorkerHost.ts +16 -7
  33. package/server/ssrFromRscRenderer.tsx +19 -15
  34. package/server/ssrTypes.ts +9 -0
  35. package/server/webRouter.ts +9 -2
  36. package/service/index.ts +1 -0
  37. package/service/predefinedAdaptor/index.ts +1 -0
  38. package/service/predefinedAdaptor/rateLimit.adaptor.ts +15 -0
  39. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +119 -27
  40. package/service/predefinedAdaptor/sql/types.ts +14 -12
  41. package/service/rateLimiter.ts +80 -0
  42. package/signal/base.signal.ts +7 -6
  43. package/signal/endpointRateLimit.ts +159 -0
  44. package/signal/index.ts +1 -0
  45. package/signal/middleware.ts +6 -5
  46. package/signal/serializer/fetch.serializer.ts +15 -4
  47. package/signal/signalContext.ts +148 -67
  48. package/signal/slice.ts +30 -9
  49. package/signal/types.ts +8 -0
  50. package/test/signalTestRuntime.ts +2 -1
  51. package/types/base/base.d.ts +1 -1
  52. package/types/client/cookie.d.ts +1 -1
  53. package/types/client/csrTypes.d.ts +2 -2
  54. package/types/common/Logger.d.ts +2 -0
  55. package/types/common/index.d.ts +1 -1
  56. package/types/common/routeConvention.d.ts +0 -2
  57. package/types/dictionary/base.dictionary.d.ts +1 -1
  58. package/types/dictionary/dictionary.d.ts +8 -8
  59. package/types/document/documentQuery.d.ts +5 -1
  60. package/types/document/index.d.ts +1 -0
  61. package/types/document/into.d.ts +7 -7
  62. package/types/document/liveRowScope.d.ts +9 -0
  63. package/types/fetch/client/fetchClient.d.ts +9 -1
  64. package/types/index.d.ts +32 -0
  65. package/types/server/akanOption.d.ts +7 -1
  66. package/types/server/cachePolicy.d.ts +2 -1
  67. package/types/server/crawler.d.ts +1 -0
  68. package/types/server/head.d.ts +0 -3
  69. package/types/server/mcp/McpExecutionContext.d.ts +4 -1
  70. package/types/server/mcp/McpRateLimiter.d.ts +2 -6
  71. package/types/server/rscWorkerHost.d.ts +3 -0
  72. package/types/server/ssrFromRscRenderer.d.ts +3 -1
  73. package/types/server/ssrTypes.d.ts +8 -0
  74. package/types/service/index.d.ts +1 -0
  75. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  76. package/types/service/predefinedAdaptor/rateLimit.adaptor.d.ts +10 -0
  77. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -10
  78. package/types/service/predefinedAdaptor/sql/types.d.ts +13 -17
  79. package/types/service/rateLimiter.d.ts +24 -0
  80. package/types/signal/endpointRateLimit.d.ts +37 -0
  81. package/types/signal/index.d.ts +1 -0
  82. package/types/signal/signalContext.d.ts +33 -15
  83. package/types/signal/slice.d.ts +17 -5
  84. package/types/signal/types.d.ts +8 -0
  85. package/types/test/signalTestRuntime.d.ts +1 -1
  86. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  87. package/types/ui/recipe/buttonRecipe.d.ts +4 -4
  88. package/types/ui/recipe/inputRecipe.d.ts +3 -3
  89. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +33 -0
  90. package/types/vendor/@akanjs/native/packages/cli/src/lib/bplist.d.ts +11 -0
  91. package/types/vendor/@akanjs/native/packages/cli/src/lib/dmg.d.ts +59 -0
  92. package/types/vendor/@akanjs/native/packages/cli/src/lib/dsStore.d.ts +26 -0
  93. package/types/vendor/@akanjs/native/packages/cli/src/lib/macAlias.d.ts +24 -0
  94. package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +0 -1
  95. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +3 -1
  96. package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +6 -1
  97. package/types/vendor/@akanjs/native/packages/desktop/src/deeplinks.d.ts +9 -2
  98. package/types/webkit/RenderFailure.d.ts +6 -0
  99. package/types/webkit/useFrameRuntime.d.ts +0 -1
  100. package/ui/Input.tsx +7 -1
  101. package/ui/recipe/buttonRecipe.ts +1 -0
  102. package/ui/recipe/inputRecipe.ts +13 -7
  103. package/ui/styles.css +1 -0
  104. package/vendor/@akanjs/native/native/desktop/src/lib.rs +8 -1
  105. package/vendor/@akanjs/native/native/macos/dmg/background.html +18 -0
  106. package/vendor/@akanjs/native/native/macos/dmg/background.png +0 -0
  107. package/vendor/@akanjs/native/native/macos/dmg/background@2x.png +0 -0
  108. package/vendor/@akanjs/native/packages/cli/src/config.ts +23 -0
  109. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +2 -1
  110. package/vendor/@akanjs/native/packages/cli/src/lib/bplist.ts +91 -0
  111. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -0
  112. package/vendor/@akanjs/native/packages/cli/src/lib/dmg.ts +331 -0
  113. package/vendor/@akanjs/native/packages/cli/src/lib/dsStore.ts +191 -0
  114. package/vendor/@akanjs/native/packages/cli/src/lib/exec.ts +11 -1
  115. package/vendor/@akanjs/native/packages/cli/src/lib/macAlias.ts +101 -0
  116. package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +1 -28
  117. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +6 -1
  118. package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +17 -7
  119. package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +6 -1
  120. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +5 -2
  121. package/vendor/@akanjs/native/packages/desktop/src/deeplinks.ts +33 -10
  122. package/vendor/@akanjs/native/packages/desktop/src/host.ts +1 -1
  123. package/vendor/use-agentic/CHANGELOG.md +663 -0
  124. package/webkit/CsrRouteTable.ts +0 -2
  125. package/webkit/RenderFailure.tsx +32 -0
  126. package/webkit/RenderLayer.tsx +22 -2
  127. package/webkit/useCsrValues.ts +21 -11
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <!-- The default desktop.dmg background (lib/dmg.ts). background.png and background@2x.png are this page in headless
3
+ Chrome: --window-size=660,400 --force-device-scale-factor=1 (and 2) --screenshot=<file>. The arrow and the text
4
+ sit around the default icon positions, the app at (180, 170) and Applications at (480, 170). -->
5
+ <html><head><meta charset="utf-8"><style>
6
+ html, body { margin: 0; width: 660px; height: 400px; overflow: hidden; }
7
+ body { background: linear-gradient(180deg, #fbfbfd 0%, #eceef2 100%); font-family: -apple-system, system-ui, sans-serif; }
8
+ svg { position: absolute; left: 0; top: 0; }
9
+ p { position: absolute; left: 0; right: 0; top: 304px; margin: 0; text-align: center; font-size: 15px; font-weight: 500; letter-spacing: 0.01em; color: #6b7080; }
10
+ </style></head><body>
11
+ <svg width="660" height="400" viewBox="0 0 660 400">
12
+ <g fill="none" stroke="#a9aeb9" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
13
+ <path d="M272 170 H384" stroke-dasharray="0.1 14" />
14
+ <path d="M372 154 L390 170 L372 186" />
15
+ </g>
16
+ </svg>
17
+ <p>Drag the app to Applications to install</p>
18
+ </body></html>
@@ -275,6 +275,14 @@ export interface AkanNativeConfig {
275
275
  * macOS builds sign every Mach-O file in it.
276
276
  */
277
277
  bin?: string;
278
+ /**
279
+ * macOS: the Finder window the disk image (`--installer`, API `macos.dmg`) opens in: no toolbar, sidebar or status
280
+ * bar, the app's and the Applications link's icons on a background, and the app's icon on the mounted disk. Without
281
+ * it the window is 660×400 pt on akan-native's background, an arrow from the app at (180, 170) to Applications at
282
+ * (480, 170). A position is an icon's center, in points from the window's top left; the default background lines
283
+ * up with the default positions only.
284
+ */
285
+ dmg?: DmgConfig;
278
286
  };
279
287
  /**
280
288
  * App icon for every platform, generated from one square PNG, ideally 1024×1024 (CLI-8).
@@ -300,6 +308,21 @@ export interface AkanNativeConfig {
300
308
  };
301
309
  }
302
310
 
311
+ export interface DmgConfig {
312
+ /** A PNG (or TIFF) the size of the window in points, or false for a plain window. */
313
+ background?: string | false;
314
+ /** The same picture at twice the pixels, for a Retina display: the disk image carries both in one TIFF. */
315
+ background2x?: string;
316
+ /** In points. Default the background's size (660×400 without one) at 200, 120 on the screen. */
317
+ window?: { x?: number; y?: number; width?: number; height?: number };
318
+ /** Default 128. */
319
+ iconSize?: number;
320
+ /** Default 13. */
321
+ textSize?: number;
322
+ app?: { x: number; y: number };
323
+ applications?: { x: number; y: number };
324
+ }
325
+
303
326
  export function defineConfig(config: AkanNativeConfig): AkanNativeConfig {
304
327
  return config;
305
328
  }
@@ -71,7 +71,8 @@ function listFiles(dir: string): string[] {
71
71
  if (!existsSync(dir)) return [];
72
72
  return (readdirSync(dir, { recursive: true }) as string[])
73
73
  .map((file) => join(dir, file))
74
- .filter((file) => statSync(file).isFile());
74
+ .filter((file) => statSync(file).isFile())
75
+ .sort((a, b) => a.localeCompare(b));
75
76
  }
76
77
 
77
78
  /** The folder of the package a file belongs to: the deepest node_modules/<name> (or <@scope>/<name>) above it. */
@@ -0,0 +1,91 @@
1
+
2
+
3
+ /** A number written as a real even when it is whole: Finder reads `iconSize` and `textSize` as reals. */
4
+ export class Real {
5
+ constructor(readonly value: number) {}
6
+ }
7
+
8
+ export type BplistValue = string | number | boolean | Uint8Array | Real | { [key: string]: BplistValue };
9
+
10
+ class DictNode {
11
+ readonly keys: number[] = [];
12
+ readonly values: number[] = [];
13
+ }
14
+ type Node = Exclude<BplistValue, { [key: string]: BplistValue }> | DictNode;
15
+
16
+ const sizeOf = (max: number) => (max < 2 ** 8 ? 1 : max < 2 ** 16 ? 2 : 4);
17
+
18
+ function uint(value: number, size: number): number[] {
19
+ const out: number[] = [];
20
+ for (let i = size - 1; i >= 0; i--) out.push(Math.floor(value / 2 ** (8 * i)) & 0xff);
21
+ return out;
22
+ }
23
+
24
+ function float64(value: number): number[] {
25
+ const data = new Uint8Array(8);
26
+ new DataView(data.buffer).setFloat64(0, value);
27
+ return [0x23, ...data];
28
+ }
29
+
30
+ function lengthMarker(kind: number, length: number): number[] {
31
+ if (length < 15) return [kind | length];
32
+ const size = length < 2 ** 8 ? 0 : length < 2 ** 16 ? 1 : 2;
33
+ return [kind | 0x0f, 0x10 | size, ...uint(length, 2 ** size)];
34
+ }
35
+
36
+ function encode(node: Node, refSize: number): number[] {
37
+ if (typeof node === "boolean") return [node ? 0x09 : 0x08];
38
+ if (node instanceof Real) return float64(node.value);
39
+ if (typeof node === "number") {
40
+ if (!Number.isInteger(node)) return float64(node);
41
+ if (node < 0) {
42
+ const data = new Uint8Array(8);
43
+ new DataView(data.buffer).setBigInt64(0, BigInt(node));
44
+ return [0x13, ...data];
45
+ }
46
+ const size = node < 2 ** 8 ? 0 : node < 2 ** 16 ? 1 : node < 2 ** 32 ? 2 : 3;
47
+ return [0x10 | size, ...uint(node, 2 ** size)];
48
+ }
49
+ if (typeof node === "string") {
50
+ if ([...node].every((c) => c.charCodeAt(0) < 0x80))
51
+ return [...lengthMarker(0x50, node.length), ...[...node].map((c) => c.charCodeAt(0))];
52
+ const out = lengthMarker(0x60, node.length);
53
+ for (let i = 0; i < node.length; i++) out.push(...uint(node.charCodeAt(i), 2));
54
+ return out;
55
+ }
56
+ if (node instanceof Uint8Array) return [...lengthMarker(0x40, node.length), ...node];
57
+ return [
58
+ ...lengthMarker(0xd0, node.keys.length),
59
+ ...[...node.keys, ...node.values].flatMap((ref) => uint(ref, refSize)),
60
+ ];
61
+ }
62
+
63
+ export function writeBplist(root: { [key: string]: BplistValue }): Uint8Array {
64
+ const nodes: Node[] = [];
65
+ const add = (value: BplistValue): number => {
66
+ const idx = nodes.length;
67
+ if (typeof value !== "object" || value instanceof Uint8Array || value instanceof Real) {
68
+ nodes.push(value);
69
+ return idx;
70
+ }
71
+ const dict = new DictNode();
72
+ nodes.push(dict);
73
+ const entries = Object.entries(value);
74
+ for (const [key] of entries) dict.keys.push(add(key));
75
+ for (const [, item] of entries) dict.values.push(add(item));
76
+ return idx;
77
+ };
78
+ add(root);
79
+ const refSize = sizeOf(nodes.length);
80
+ const bytes: number[] = [..."bplist00"].map((c) => c.charCodeAt(0));
81
+ const offsets: number[] = [];
82
+ for (const node of nodes) {
83
+ offsets.push(bytes.length);
84
+ bytes.push(...encode(node, refSize));
85
+ }
86
+ const tableOffset = bytes.length;
87
+ const offsetSize = sizeOf(tableOffset);
88
+ for (const offset of offsets) bytes.push(...uint(offset, offsetSize));
89
+ bytes.push(0, 0, 0, 0, 0, 0, offsetSize, refSize, ...uint(nodes.length, 8), ...uint(0, 8), ...uint(tableOffset, 8));
90
+ return Uint8Array.from(bytes);
91
+ }
@@ -37,6 +37,15 @@ const KNOWN: Shape = {
37
37
  screenCapture: true,
38
38
  server: { dir: true, entry: true, env: true },
39
39
  bin: true,
40
+ dmg: {
41
+ background: true,
42
+ background2x: true,
43
+ window: { x: true, y: true, width: true, height: true },
44
+ iconSize: true,
45
+ textSize: true,
46
+ app: { x: true, y: true },
47
+ applications: { x: true, y: true },
48
+ },
40
49
  },
41
50
  keyboard: { resize: true },
42
51
  push: {
@@ -0,0 +1,331 @@
1
+
2
+
3
+ import {
4
+ closeSync,
5
+ copyFileSync,
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ readSync,
12
+ rmSync,
13
+ statSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { basename, extname, join, resolve } from "node:path";
18
+ import type { DmgConfig } from "../config.ts";
19
+ import { Real, writeBplist } from "./bplist.ts";
20
+ import { type DsStoreRecord, writeDsStore } from "./dsStore.ts";
21
+ import { exec, execOrThrow } from "./exec.ts";
22
+ import { aliasRecord } from "./macAlias.ts";
23
+ import { PACKAGE_ROOT } from "./root.ts";
24
+
25
+ interface Point {
26
+ x: number;
27
+ y: number;
28
+ }
29
+
30
+ export interface ResolvedDmg {
31
+ /** Absolute; null for a plain window. */
32
+ background: { image: string; image2x?: string } | null;
33
+ /** The window's size defaults to the background's. */
34
+ window: { x: number; y: number; width?: number; height?: number };
35
+ iconSize: number;
36
+ textSize: number;
37
+ app: Point;
38
+ applications: Point;
39
+ }
40
+
41
+ const DEFAULT_BACKGROUND = {
42
+ image: join(PACKAGE_ROOT, "native", "macos", "dmg", "background.png"),
43
+ image2x: join(PACKAGE_ROOT, "native", "macos", "dmg", "background@2x.png"),
44
+ };
45
+ const DEFAULT_SIZE = { width: 660, height: 400 };
46
+
47
+ const TITLE_BAR = 28;
48
+ export const DMG_DEFAULTS = {
49
+ window: { x: 200, y: 120 },
50
+ iconSize: 128,
51
+ textSize: 13,
52
+ app: { x: 180, y: 170 },
53
+ applications: { x: 480, y: 170 },
54
+ } as const;
55
+
56
+ const isNumber = (value: unknown, min = 0): value is number =>
57
+ typeof value === "number" && Number.isFinite(value) && value >= min;
58
+
59
+ export function validateDmg(dmg: DmgConfig | undefined, appDir: string, problems: string[]): ResolvedDmg {
60
+ const raw: DmgConfig = dmg && typeof dmg === "object" ? dmg : {};
61
+ if (dmg !== undefined && (!dmg || typeof dmg !== "object")) problems.push("desktop.dmg must be an object");
62
+ const file = (key: "background" | "background2x", value: unknown): string | undefined => {
63
+ if (value === undefined) return undefined;
64
+ const path = typeof value === "string" && value ? resolve(appDir, value) : null;
65
+ if (!path || !existsSync(path) || !statSync(path).isFile()) {
66
+ problems.push(`desktop.dmg.${key} must be an image file (got ${JSON.stringify(value)})`);
67
+ return undefined;
68
+ }
69
+ return path;
70
+ };
71
+ if (raw.background !== undefined && raw.background !== false && typeof raw.background !== "string")
72
+ problems.push(`desktop.dmg.background must be an image file or false (got ${JSON.stringify(raw.background)})`);
73
+ const image = raw.background === false ? undefined : file("background", raw.background);
74
+ const image2x = file("background2x", raw.background2x);
75
+ if (raw.background2x !== undefined && typeof raw.background !== "string")
76
+ problems.push("desktop.dmg.background2x needs desktop.dmg.background, the same picture at its size in points");
77
+ const window = { ...DMG_DEFAULTS.window, ...(raw.window && typeof raw.window === "object" ? raw.window : {}) };
78
+ for (const [key, value] of Object.entries(window))
79
+ if (!isNumber(value, key === "width" || key === "height" ? 1 : 0))
80
+ problems.push(`desktop.dmg.window.${key} must be a positive number of points (got ${JSON.stringify(value)})`);
81
+ for (const key of ["iconSize", "textSize"] as const)
82
+ if (raw[key] !== undefined && !isNumber(raw[key], 1))
83
+ problems.push(`desktop.dmg.${key} must be a positive number of points (got ${JSON.stringify(raw[key])})`);
84
+ const point = (key: "app" | "applications"): Point => {
85
+ const value = raw[key];
86
+ if (value === undefined) return DMG_DEFAULTS[key];
87
+ if (!value || typeof value !== "object" || !isNumber(value.x) || !isNumber(value.y)) {
88
+ problems.push(`desktop.dmg.${key} must be { x, y }, the icon's center in points (got ${JSON.stringify(value)})`);
89
+ return DMG_DEFAULTS[key];
90
+ }
91
+ return { x: value.x, y: value.y };
92
+ };
93
+ return {
94
+ background:
95
+ raw.background === false
96
+ ? null
97
+ : raw.background === undefined
98
+ ? DEFAULT_BACKGROUND
99
+ : image
100
+ ? { image, ...(image2x ? { image2x } : {}) }
101
+ : null,
102
+ window,
103
+ iconSize: raw.iconSize ?? DMG_DEFAULTS.iconSize,
104
+ textSize: raw.textSize ?? DMG_DEFAULTS.textSize,
105
+ app: point("app"),
106
+ applications: point("applications"),
107
+ };
108
+ }
109
+
110
+ /** A PNG's size from its header, anything else's from sips. */
111
+ async function imageSize(path: string): Promise<{ width: number; height: number }> {
112
+ const head = readFileSync(path).subarray(0, 24);
113
+ if (head.length === 24 && head.readUInt32BE(0) === 0x89504e47)
114
+ return { width: head.readUInt32BE(16), height: head.readUInt32BE(20) };
115
+ const { stdout } = await execOrThrow(["sips", "-g", "pixelWidth", "-g", "pixelHeight", path], { echo: false });
116
+ const width = Number(/pixelWidth: (\d+)/.exec(stdout)?.[1]);
117
+ const height = Number(/pixelHeight: (\d+)/.exec(stdout)?.[1]);
118
+ if (!width || !height) throw new Error(`cannot read the size of ${path}`);
119
+ return { width, height };
120
+ }
121
+
122
+ /** The picture the image carries: a 1x and 2x pair as one multi-resolution TIFF, which Finder picks from per display. */
123
+ async function stageBackground(background: NonNullable<ResolvedDmg["background"]>, dir: string): Promise<string> {
124
+ mkdirSync(dir, { recursive: true });
125
+ if (background.image2x) {
126
+ const out = join(dir, "background.tiff");
127
+ await execOrThrow(["tiffutil", "-cathidpicheck", background.image, background.image2x, "-out", out], {
128
+ echo: false,
129
+ });
130
+ return out;
131
+ }
132
+ const out = join(dir, `background${extname(background.image).toLowerCase() || ".png"}`);
133
+ copyFileSync(background.image, out);
134
+ return out;
135
+ }
136
+
137
+ export function dmgRecords(
138
+ layout: ResolvedDmg,
139
+ {
140
+ appName,
141
+ size,
142
+ backgroundAlias,
143
+ }: { appName: string; size: { width: number; height: number }; backgroundAlias?: Uint8Array },
144
+ ): DsStoreRecord[] {
145
+ const { x, y } = layout.window;
146
+ const blob = (value: Uint8Array) => ({ type: "blob" as const, value });
147
+ const iconAt = ({ x, y }: Point) => {
148
+ const value = new Uint8Array(16);
149
+ const view = new DataView(value.buffer);
150
+ view.setUint32(0, Math.round(x));
151
+ view.setUint32(4, Math.round(y));
152
+ view.setUint32(8, 0xffffffff);
153
+ view.setUint32(12, 0xffff0000);
154
+ return blob(value);
155
+ };
156
+ const bwsp = writeBplist({
157
+ WindowBounds: `{{${x}, ${y}}, {${size.width}, ${size.height + TITLE_BAR}}}`,
158
+ ShowToolbar: false,
159
+ ShowSidebar: false,
160
+ ShowStatusBar: false,
161
+ ShowPathbar: false,
162
+ ShowTabView: false,
163
+ ContainerShowSidebar: false,
164
+ PreviewPaneVisibility: false,
165
+ SidebarWidth: 0,
166
+ });
167
+ const icvp = writeBplist({
168
+ viewOptionsVersion: 1,
169
+ backgroundType: backgroundAlias ? 2 : 0,
170
+ ...(backgroundAlias ? { backgroundImageAlias: backgroundAlias } : {}),
171
+ backgroundColorRed: new Real(1),
172
+ backgroundColorGreen: new Real(1),
173
+ backgroundColorBlue: new Real(1),
174
+ gridOffsetX: new Real(0),
175
+ gridOffsetY: new Real(0),
176
+ gridSpacing: new Real(100),
177
+ arrangeBy: "none",
178
+ showIconPreview: true,
179
+ showItemInfo: false,
180
+ labelOnBottom: true,
181
+ textSize: new Real(layout.textSize),
182
+ iconSize: new Real(layout.iconSize),
183
+ scrollPositionX: new Real(0),
184
+ scrollPositionY: new Real(0),
185
+ });
186
+ return [
187
+ { name: ".", code: "bwsp", value: blob(bwsp) },
188
+ { name: ".", code: "icvp", value: blob(icvp) },
189
+ { name: ".", code: "icvl", value: { type: "type", value: "icnv" } },
190
+ { name: ".", code: "vSrn", value: { type: "long", value: 1 } },
191
+ { name: appName, code: "Iloc", value: iconAt(layout.app) },
192
+ { name: "Applications", code: "Iloc", value: iconAt(layout.applications) },
193
+ ];
194
+ }
195
+
196
+ const CUSTOM_ICON_FINDER_INFO = `${"0".repeat(16)}0400${"0".repeat(44)}`;
197
+
198
+ async function detach(mountPoint: string): Promise<void> {
199
+ for (let attempt = 0; ; attempt++) {
200
+ const result = await exec(["hdiutil", "detach", mountPoint, ...(attempt >= 3 ? ["-force"] : [])], { echo: false });
201
+ if (result.code === 0) return;
202
+
203
+ if (attempt >= 5)
204
+ throw new Error(`hdiutil detach ${mountPoint} failed: ${(result.stderr || result.stdout).trim()}`);
205
+ await Bun.sleep(1000 * (attempt + 1));
206
+ }
207
+ }
208
+
209
+ export async function buildDmg(
210
+ appPath: string,
211
+ dmgPath: string,
212
+ volumeName: string,
213
+ workDir: string,
214
+ { layout, volumeIcon }: { layout: ResolvedDmg; volumeIcon?: string },
215
+ ): Promise<void> {
216
+ const staging = join(workDir, "dmg");
217
+ const writable = join(workDir, "dmg-rw.dmg");
218
+ const mountPoint = join(workDir, "dmg-mount");
219
+ rmSync(staging, { recursive: true, force: true });
220
+ rmSync(writable, { force: true });
221
+ mkdirSync(staging, { recursive: true });
222
+ const appName = basename(appPath);
223
+ await execOrThrow(["ditto", appPath, join(staging, appName)], { echo: false });
224
+ symlinkSync("/Applications", join(staging, "Applications"));
225
+
226
+ mkdirSync(join(staging, ".fseventsd"));
227
+ writeFileSync(join(staging, ".fseventsd", "no_log"), "");
228
+ const background = layout.background ? await stageBackground(layout.background, join(staging, ".background")) : null;
229
+ if (volumeIcon && existsSync(volumeIcon)) copyFileSync(volumeIcon, join(staging, ".VolumeIcon.icns"));
230
+ const size = {
231
+ ...(layout.background ? await imageSize(layout.background.image) : DEFAULT_SIZE),
232
+ ...(layout.window.width ? { width: layout.window.width } : {}),
233
+ ...(layout.window.height ? { height: layout.window.height } : {}),
234
+ };
235
+
236
+ const { stdout } = await execOrThrow(["du", "-sk", staging], { echo: false });
237
+ const kilobytes = Number(stdout.trim().split(/\s+/)[0]) || 0;
238
+ await execOrThrow(
239
+ [
240
+ "hdiutil",
241
+ "create",
242
+ "-volname",
243
+ volumeName,
244
+ "-srcfolder",
245
+ staging,
246
+ "-fs",
247
+ "HFS+",
248
+ "-format",
249
+ "UDRW",
250
+ "-size",
251
+ `${Math.ceil(kilobytes * 1.1) + 10 * 1024}k`,
252
+ "-ov",
253
+ writable,
254
+ ],
255
+ { echo: false },
256
+ );
257
+ const volumeCreated = volumeHeaderCreateDate(writable);
258
+ rmSync(mountPoint, { recursive: true, force: true });
259
+ mkdirSync(mountPoint, { recursive: true });
260
+ await execOrThrow(
261
+ ["hdiutil", "attach", writable, "-readwrite", "-noverify", "-noautoopen", "-nobrowse", "-mountpoint", mountPoint],
262
+ { echo: false },
263
+ );
264
+ try {
265
+ const backgroundAlias = background
266
+ ? aliasFor(mountPoint, { volumeName, volumeCreated }, join(".background", basename(background)))
267
+ : undefined;
268
+ const records = dmgRecords(layout, { appName, size, ...(backgroundAlias ? { backgroundAlias } : {}) });
269
+ writeFileSync(join(mountPoint, ".DS_Store"), writeDsStore(records));
270
+ const hidden = [".background", ".VolumeIcon.icns", ".DS_Store", ".fseventsd"].map((name) => join(mountPoint, name));
271
+ await execOrThrow(["chflags", "hidden", ...hidden.filter((path) => existsSync(path))], { echo: false });
272
+ if (existsSync(join(mountPoint, ".VolumeIcon.icns")))
273
+ await execOrThrow(["xattr", "-wx", "com.apple.FinderInfo", CUSTOM_ICON_FINDER_INFO, mountPoint], { echo: false });
274
+ rmSync(join(mountPoint, ".Trashes"), { recursive: true, force: true });
275
+ } finally {
276
+ await detach(mountPoint);
277
+ }
278
+ rmSync(dmgPath, { force: true });
279
+ await execOrThrow(
280
+ ["hdiutil", "convert", writable, "-format", "UDZO", "-imagekey", "zlib-level=9", "-ov", "-o", dmgPath],
281
+ { echo: false },
282
+ );
283
+ rmSync(writable, { force: true });
284
+ rmSync(staging, { recursive: true, force: true });
285
+ rmSync(mountPoint, { recursive: true, force: true });
286
+ }
287
+
288
+ /**
289
+ * The createDate of the HFS+ volume a read-write image holds: its header ("H+", version 4) sits 1024 bytes into the
290
+ * partition, which `hdiutil create` puts within the image's first mebibyte, its data stored as is.
291
+ */
292
+ export function volumeHeaderCreateDate(image: string): number {
293
+ const head = new Uint8Array(1024 * 1024);
294
+ const fd = openSync(image, "r");
295
+ const length = (() => {
296
+ try {
297
+ return readSync(fd, head, 0, head.length, 0);
298
+ } finally {
299
+ closeSync(fd);
300
+ }
301
+ })();
302
+ for (let offset = 1024; offset + 512 <= length; offset += 512)
303
+ if (head[offset] === 0x48 && head[offset + 1] === 0x2b && head[offset + 2] === 0 && head[offset + 3] === 4)
304
+ return new DataView(head.buffer, offset).getUint32(16);
305
+ throw new Error(`no HFS+ volume header in the first MiB of ${image}`);
306
+ }
307
+
308
+ /** The background's alias as the mounted image names it, while it is mounted where it is being written. */
309
+ function aliasFor(
310
+ mountPoint: string,
311
+ { volumeName, volumeCreated }: { volumeName: string; volumeCreated: number },
312
+ relative: string,
313
+ ): Uint8Array {
314
+ const target = join(mountPoint, relative);
315
+ const parent = join(target, "..");
316
+ const file = lstatSync(target);
317
+ const folder = lstatSync(parent);
318
+ return aliasRecord({
319
+ volumeName,
320
+ volumeCreated,
321
+ volumePath: join("/Volumes", volumeName),
322
+ kind: "file",
323
+ name: basename(target),
324
+ cnid: file.ino,
325
+ created: file.birthtime,
326
+ parentName: basename(parent),
327
+ parentCnid: folder.ino,
328
+ cnidPath: [folder.ino],
329
+ posixPath: `/${relative}`,
330
+ });
331
+ }