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,191 @@
1
+
2
+
3
+ export type DsStoreValue =
4
+ | { type: "long"; value: number }
5
+ | { type: "bool"; value: boolean }
6
+ | { type: "type"; value: string }
7
+ | { type: "blob"; value: Uint8Array }
8
+ | { type: "ustr"; value: string };
9
+
10
+ export interface DsStoreRecord {
11
+ /** The file the record is about, "." for the folder itself. */
12
+ name: string;
13
+ /** Four letters: Iloc (icon location), bwsp (window), icvp (icon view), vSrn, icvl … */
14
+ code: string;
15
+ value: DsStoreValue;
16
+ }
17
+
18
+ const PAGE_SIZE = 4096;
19
+
20
+ class Writer {
21
+ #chunks: Uint8Array[] = [];
22
+ length = 0;
23
+ bytes(data: Uint8Array): this {
24
+ this.#chunks.push(data);
25
+ this.length += data.length;
26
+ return this;
27
+ }
28
+ u8(value: number): this {
29
+ return this.bytes(Uint8Array.of(value));
30
+ }
31
+ u32(value: number): this {
32
+ const data = new Uint8Array(4);
33
+ new DataView(data.buffer).setUint32(0, value >>> 0);
34
+ return this.bytes(data);
35
+ }
36
+ fourCC(code: string): this {
37
+ if (!/^[\x20-\x7e]{4}$/.test(code)) throw new Error(`not a four-letter code: ${JSON.stringify(code)}`);
38
+ return this.bytes(new TextEncoder().encode(code));
39
+ }
40
+ utf16(text: string): this {
41
+ const data = new Uint8Array(text.length * 2);
42
+ const view = new DataView(data.buffer);
43
+ for (let i = 0; i < text.length; i++) view.setUint16(i * 2, text.charCodeAt(i));
44
+ return this.bytes(data);
45
+ }
46
+ toBytes(): Uint8Array {
47
+ const out = new Uint8Array(this.length);
48
+ let at = 0;
49
+ for (const chunk of this.#chunks) {
50
+ out.set(chunk, at);
51
+ at += chunk.length;
52
+ }
53
+ return out;
54
+ }
55
+ }
56
+
57
+ /** Finder compares names as HFS+ does, case-insensitively; a window's few ASCII names need nothing more. */
58
+ function compareRecords(a: DsStoreRecord, b: DsStoreRecord): number {
59
+ const left = a.name.toLowerCase();
60
+ const right = b.name.toLowerCase();
61
+ if (left !== right) return left < right ? -1 : 1;
62
+ return a.code < b.code ? -1 : a.code > b.code ? 1 : 0;
63
+ }
64
+
65
+ function writeRecord(out: Writer, { name, code, value }: DsStoreRecord): void {
66
+ out.u32(name.length).utf16(name).fourCC(code).fourCC(value.type);
67
+ if (value.type === "long") out.u32(value.value);
68
+ else if (value.type === "bool") out.u8(value.value ? 1 : 0);
69
+ else if (value.type === "type") out.fourCC(value.value);
70
+ else if (value.type === "blob") out.u32(value.value.length).bytes(value.value);
71
+ else out.u32(value.value.length).utf16(value.value);
72
+ }
73
+
74
+ /** The buddy allocator's free lists after the given allocations, each address a power-of-two block. */
75
+ class Buddy {
76
+ readonly free: number[][] = Array.from({ length: 32 }, () => []);
77
+ constructor() {
78
+ this.free[31]?.push(0);
79
+ }
80
+ allocate(size: number): { offset: number; width: number } {
81
+ const width = Math.max(5, Math.ceil(Math.log2(size)));
82
+ let w = width;
83
+ while (w < 32 && !this.free[w]?.length) w++;
84
+ const offset = this.free[w]?.shift();
85
+ if (offset === undefined) throw new Error(`no free block of ${size} bytes`);
86
+ while (w > width) {
87
+ w--;
88
+ this.free[w]?.push(offset + 2 ** w);
89
+ this.free[w]?.sort((a, b) => a - b);
90
+ }
91
+ return { offset, width };
92
+ }
93
+ }
94
+
95
+ export function writeDsStore(records: DsStoreRecord[]): Uint8Array {
96
+ const sorted = [...records].sort(compareRecords);
97
+ const leaf = new Writer().u32(0).u32(sorted.length);
98
+ for (const record of sorted) writeRecord(leaf, record);
99
+ if (leaf.length > PAGE_SIZE)
100
+ throw new Error(
101
+ `${sorted.length} .DS_Store records need ${leaf.length} bytes, more than one ${PAGE_SIZE}-byte node`,
102
+ );
103
+
104
+ const buddy = new Buddy();
105
+ buddy.allocate(32);
106
+ const root = buddy.allocate(2048);
107
+ const header = buddy.allocate(32);
108
+ const node = buddy.allocate(PAGE_SIZE);
109
+ const address = ({ offset, width }: { offset: number; width: number }) => offset | width;
110
+
111
+ const dsdb = new Writer().u32(2).u32(0).u32(sorted.length).u32(1).u32(PAGE_SIZE);
112
+ const blocks = [root, header, node];
113
+ const allocator = new Writer().u32(blocks.length).u32(0);
114
+ for (const block of blocks) allocator.u32(address(block));
115
+ for (let i = blocks.length; i < 256; i++) allocator.u32(0);
116
+ allocator.u32(1).u8(4).fourCC("DSDB").u32(1);
117
+ for (const list of buddy.free) {
118
+ allocator.u32(list.length);
119
+ for (const offset of list) allocator.u32(offset);
120
+ }
121
+ if (allocator.length > 2 ** root.width) throw new Error(".DS_Store allocator block overflows");
122
+
123
+ const end = Math.max(...blocks.map(({ offset, width }) => offset + 2 ** width));
124
+ const file = new Uint8Array(4 + end);
125
+ const view = new DataView(file.buffer);
126
+ view.setUint32(0, 1);
127
+ file.set(new TextEncoder().encode("Bud1"), 4);
128
+ view.setUint32(8, root.offset);
129
+ view.setUint32(12, 2 ** root.width);
130
+ view.setUint32(16, root.offset);
131
+ file.set(allocator.toBytes(), 4 + root.offset);
132
+ file.set(dsdb.toBytes(), 4 + header.offset);
133
+ file.set(leaf.toBytes(), 4 + node.offset);
134
+ return file;
135
+ }
136
+
137
+ /** Reads back what writeDsStore wrote (one leaf): the tests' check, and a way to look at a .DS_Store. */
138
+ export function readDsStore(file: Uint8Array): DsStoreRecord[] {
139
+ const view = new DataView(file.buffer, file.byteOffset, file.byteLength);
140
+ if (view.getUint32(0) !== 1 || new TextDecoder().decode(file.subarray(4, 8)) !== "Bud1")
141
+ throw new Error("not a .DS_Store");
142
+ const rootOffset = view.getUint32(8);
143
+ const blockAt = (index: number) => {
144
+ const raw = view.getUint32(4 + rootOffset + 8 + index * 4);
145
+ return 4 + (raw & ~0x1f);
146
+ };
147
+ const dsdb = blockAt(1);
148
+ const leafIndex = view.getUint32(dsdb);
149
+ let at = blockAt(leafIndex);
150
+ if (view.getUint32(at) !== 0) throw new Error("only a single-leaf .DS_Store is read");
151
+ const count = view.getUint32(at + 4);
152
+ at += 8;
153
+ const fourCC = () => {
154
+ const code = new TextDecoder().decode(file.subarray(at, at + 4));
155
+ at += 4;
156
+ return code;
157
+ };
158
+ const utf16 = (units: number) => {
159
+ let text = "";
160
+ for (let i = 0; i < units; i++) text += String.fromCharCode(view.getUint16(at + i * 2));
161
+ at += units * 2;
162
+ return text;
163
+ };
164
+ const records: DsStoreRecord[] = [];
165
+ for (let i = 0; i < count; i++) {
166
+ const nameLength = view.getUint32(at);
167
+ at += 4;
168
+ const name = utf16(nameLength);
169
+ const code = fourCC();
170
+ const type = fourCC();
171
+ let value: DsStoreValue;
172
+ if (type === "long") {
173
+ value = { type, value: view.getUint32(at) };
174
+ at += 4;
175
+ } else if (type === "bool") {
176
+ value = { type, value: file[at] === 1 };
177
+ at += 1;
178
+ } else if (type === "type") value = { type, value: fourCC() };
179
+ else if (type === "blob") {
180
+ const length = view.getUint32(at);
181
+ value = { type, value: file.slice(at + 4, at + 4 + length) };
182
+ at += 4 + length;
183
+ } else if (type === "ustr") {
184
+ const length = view.getUint32(at);
185
+ at += 4;
186
+ value = { type, value: utf16(length) };
187
+ } else throw new Error(`unsupported .DS_Store type ${type}`);
188
+ records.push({ name, code, value });
189
+ }
190
+ return records;
191
+ }
@@ -62,14 +62,24 @@ export async function exec(cmd: string[], options: ExecOptions = {}): Promise<Ex
62
62
  const signal = signals.getStore();
63
63
 
64
64
  const stream = options.inherit && isTerminal() ? "inherit" : "pipe";
65
+
66
+ const group = !!signal && process.platform !== "win32";
65
67
  const proc = Bun.spawn(cmd, {
66
68
  cwd: options.cwd,
67
69
  env: options.env ? { ...process.env, ...options.env } : process.env,
68
70
  stdin: "ignore",
69
71
  stdout: stream,
70
72
  stderr: stream,
71
- ...(signal ? { signal } : {}),
73
+ ...(group ? { detached: true } : signal ? { signal } : {}),
72
74
  });
75
+ const killGroup = () => {
76
+ try {
77
+ process.kill(-proc.pid, "SIGTERM");
78
+ } catch {
79
+ }
80
+ };
81
+ if (group && signal) signal.addEventListener("abort", killGroup, { once: true });
82
+ void proc.exited.finally(() => signal?.removeEventListener("abort", killGroup));
73
83
  let stdout = "";
74
84
  let stderr = "";
75
85
  if (options.inherit && stream === "pipe") {
@@ -0,0 +1,101 @@
1
+
2
+
3
+ export interface AliasTarget {
4
+ /** The HFS+ volume name (the disk image's label). */
5
+ volumeName: string;
6
+ /**
7
+ * The volume header's createDate as it stands, in HFS seconds. HFS+ writes it on the local clock of whatever formatted
8
+ * the volume (TN1150), unlike every other date, so it is read off the image rather than computed here.
9
+ */
10
+ volumeCreated: number;
11
+ /** Where the volume mounts: /Volumes/<name>. */
12
+ volumePath: string;
13
+ kind: "file" | "folder";
14
+ name: string;
15
+ cnid: number;
16
+ created: Date;
17
+ parentName: string;
18
+ parentCnid: number;
19
+ /** The CNIDs of the folders above the target, nearest first, the volume's root left out. */
20
+ cnidPath: number[];
21
+ /** From the volume's root, starting with "/". */
22
+ posixPath: string;
23
+ }
24
+
25
+ /** Seconds since 1904-01-01 UTC, the HFS epoch. */
26
+ export function hfsTime(date: Date): number {
27
+ return Math.max(0, Math.floor(date.getTime() / 1000) + 2_082_844_800) >>> 0;
28
+ }
29
+
30
+ const EJECTABLE_DISK = 5;
31
+
32
+ /** A Carbon name: ':' is the path separator there, so a name spells it '/'. */
33
+ function pascal(text: string, size: number): Uint8Array {
34
+ const bytes = new TextEncoder().encode(text.replaceAll(":", "/")).subarray(0, size - 1);
35
+ const out = new Uint8Array(size);
36
+ out[0] = bytes.length;
37
+ out.set(bytes, 1);
38
+ return out;
39
+ }
40
+
41
+ function utf16WithLength(text: string): Uint8Array {
42
+ const out = new Uint8Array(2 + text.length * 2);
43
+ const view = new DataView(out.buffer);
44
+ view.setUint16(0, text.length);
45
+ for (let i = 0; i < text.length; i++) view.setUint16(2 + i * 2, text.charCodeAt(i));
46
+ return out;
47
+ }
48
+
49
+ export function aliasRecord(target: AliasTarget): Uint8Array {
50
+ const fixed = new Uint8Array(150);
51
+ const view = new DataView(fixed.buffer);
52
+ view.setUint16(6, 2);
53
+ view.setUint16(8, target.kind === "folder" ? 1 : 0);
54
+ fixed.set(pascal(target.volumeName, 28), 10);
55
+
56
+ view.setUint32(38, target.volumeCreated);
57
+ fixed.set(new TextEncoder().encode("H+"), 42);
58
+ view.setUint16(44, EJECTABLE_DISK);
59
+ view.setUint32(46, target.parentCnid);
60
+ fixed.set(pascal(target.name, 64), 50);
61
+ view.setUint32(114, target.cnid);
62
+ view.setUint32(118, hfsTime(target.created));
63
+ view.setInt16(130, -1);
64
+ view.setInt16(132, -1);
65
+
66
+ const encoder = new TextEncoder();
67
+ const cnids = new Uint8Array(target.cnidPath.length * 4);
68
+ for (const [idx, cnid] of target.cnidPath.entries()) new DataView(cnids.buffer).setUint32(idx * 4, cnid);
69
+ const carbonPath = [target.volumeName, ...target.posixPath.split("/").filter(Boolean)]
70
+ .map((part) => part.replaceAll(":", "/"))
71
+ .join(":");
72
+ const extras: [number, Uint8Array][] = [
73
+ [0, encoder.encode(target.parentName.replaceAll(":", "/"))],
74
+ [1, cnids],
75
+ [2, encoder.encode(carbonPath)],
76
+ [14, utf16WithLength(target.name)],
77
+ [15, utf16WithLength(target.volumeName)],
78
+ [18, encoder.encode(target.posixPath)],
79
+ [19, encoder.encode(target.volumePath)],
80
+ ];
81
+ const parts: Uint8Array[] = [fixed];
82
+ for (const [tag, data] of extras) {
83
+ const head = new Uint8Array(4);
84
+ new DataView(head.buffer).setInt16(0, tag);
85
+ new DataView(head.buffer).setUint16(2, data.length);
86
+ parts.push(head, data, ...(data.length % 2 ? [new Uint8Array(1)] : []));
87
+ }
88
+ const end = new Uint8Array(4);
89
+ new DataView(end.buffer).setInt16(0, -1);
90
+ parts.push(end);
91
+
92
+ const length = parts.reduce((sum, part) => sum + part.length, 0);
93
+ const record = new Uint8Array(length);
94
+ let at = 0;
95
+ for (const part of parts) {
96
+ record.set(part, at);
97
+ at += part.length;
98
+ }
99
+ new DataView(record.buffer).setUint16(4, length);
100
+ return record;
101
+ }
@@ -1,7 +1,7 @@
1
1
 
2
2
 
3
3
  import { randomBytes } from "node:crypto";
4
- import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { basename, join } from "node:path";
7
7
  import { exec, execOrThrow } from "./exec.ts";
@@ -283,33 +283,6 @@ export async function zipForNotary(appPath: string, zip: string): Promise<void>
283
283
  }
284
284
 
285
285
  /** A compressed disk image of the app beside a link to /Applications, the window people drag it into. */
286
- export async function buildDmg(appPath: string, dmgPath: string, volumeName: string, workDir: string): Promise<void> {
287
- const staging = join(workDir, "dmg");
288
- rmSync(staging, { recursive: true, force: true });
289
- mkdirSync(staging, { recursive: true });
290
- await execOrThrow(["ditto", appPath, join(staging, basename(appPath))], { echo: false });
291
- symlinkSync("/Applications", join(staging, "Applications"));
292
- rmSync(dmgPath, { force: true });
293
- await execOrThrow(
294
- [
295
- "hdiutil",
296
- "create",
297
- "-volname",
298
- volumeName,
299
- "-srcfolder",
300
- staging,
301
- "-ov",
302
- "-fs",
303
- "HFS+",
304
- "-format",
305
- "UDZO",
306
- dmgPath,
307
- ],
308
- { echo: false },
309
- );
310
- rmSync(staging, { recursive: true, force: true });
311
- }
312
-
313
286
  /** What Gatekeeper says of a notarized app or disk image, as a downloaded copy would be judged. */
314
287
  export async function assessGatekeeper(file: string, type: "execute" | "open"): Promise<void> {
315
288
  const args =
@@ -8,6 +8,7 @@ import { manifestPermissionProblems, type PermissionSet, resolveAcl } from "./ac
8
8
  import { unknownConfigKeys } from "./configkeys.ts";
9
9
  import { validateCsp, validateExternalSchemes } from "./csp.ts";
10
10
  import { type DesktopServerConfig, validateDesktopServer } from "./desktop-server.ts";
11
+ import { type ResolvedDmg, validateDmg } from "./dmg.ts";
11
12
  import { type EnvConfig, validateEnvConfig } from "./env.ts";
12
13
  import { CliError } from "./log.ts";
13
14
  import { type PermissionsConfig, validatePermissions } from "./permissions.ts";
@@ -18,7 +19,8 @@ import { type UpdatesConfig, validateUpdates } from "./updates.ts";
18
19
 
19
20
  const CONFIG_FILES = ["akan-native.config.ts", "akan-native.config.js", "akan-native.config.mjs"];
20
21
 
21
- export interface ResolvedConfig extends Omit<AkanNativeConfig, "icon" | "splash" | "permissions" | "updates"> {
22
+ export interface ResolvedConfig
23
+ extends Omit<AkanNativeConfig, "icon" | "splash" | "permissions" | "updates" | "desktop"> {
22
24
  app: Required<AkanNativeConfig["app"]>;
23
25
  web: Required<Pick<AkanNativeConfig["web"], "dir" | "base">> & { build?: string; devEntry?: string };
24
26
  plugins: string[];
@@ -35,6 +37,7 @@ export interface ResolvedConfig extends Omit<AkanNativeConfig, "icon" | "splash"
35
37
  server?: DesktopServerConfig;
36
38
  /** Absolute. */
37
39
  bin?: string;
40
+ dmg: ResolvedDmg;
38
41
  };
39
42
  updates: UpdatesConfig | null;
40
43
  /** Absolute image path. */
@@ -212,6 +215,7 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
212
215
  if (bin !== undefined && !(binDir && existsSync(binDir) && statSync(binDir).isDirectory()))
213
216
  problems.push(`desktop.bin must be a folder (got ${JSON.stringify(bin)})`);
214
217
  const desktopServer = validateDesktopServer(raw.desktop, appDir, problems);
218
+ const dmg = validateDmg(raw.desktop?.dmg, appDir, problems);
215
219
  if (raw.keyboard?.resize !== undefined && raw.keyboard.resize !== "resize" && raw.keyboard.resize !== "none") {
216
220
  problems.push(`keyboard.resize must be "resize" or "none" (got ${JSON.stringify(raw.keyboard.resize)})`);
217
221
  }
@@ -281,6 +285,7 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
281
285
  },
282
286
  ...(desktopServer ? { server: desktopServer } : {}),
283
287
  ...(binDir ? { bin: binDir } : {}),
288
+ dmg,
284
289
  },
285
290
  updates,
286
291
  icon: icon ? { ...icon, image: resolve(appDir, icon.image) } : null,
@@ -11,15 +11,20 @@ import type { BuildContext } from "../lib/prepare.ts";
11
11
  import { ensureInstalled, TOOLCHAIN } from "../lib/toolchains.ts";
12
12
  import { targetArch } from "./desktop.ts";
13
13
 
14
- /** The launcher entry (freedesktop Desktop Entry): what a menu shows and which link schemes open the app. */
15
- export function desktopEntry(input: { name: string; fileName: string; schemes: string[] }): string {
14
+ /**
15
+ * The launcher entry (freedesktop Desktop Entry): what a menu shows and which link schemes open the app. Named and
16
+ * matched by the app id, the window's program name (native/desktop lib.rs): AppImageLauncher and appimaged rename the
17
+ * file they install, so StartupWMClass is what still ties the window to it.
18
+ */
19
+ export function desktopEntry(input: { id: string; name: string; fileName: string; schemes: string[] }): string {
16
20
  const escape = (value: string) => value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
17
21
  return [
18
22
  "[Desktop Entry]",
19
23
  "Type=Application",
20
24
  `Name=${escape(input.name)}`,
21
25
  `Exec=${input.fileName} %u`,
22
- `Icon=${input.fileName}`,
26
+ `Icon=${input.id}`,
27
+ `StartupWMClass=${input.id}`,
23
28
  "Terminal=false",
24
29
  "Categories=Utility;",
25
30
  ...(input.schemes.length ? [`MimeType=${input.schemes.map((s) => `x-scheme-handler/${s};`).join("")}`] : []),
@@ -59,13 +64,18 @@ export async function buildAppImage(ctx: BuildContext, folder: string): Promise<
59
64
  writeFileSync(join(appDir, "AppRun"), appRun(config.app.fileName));
60
65
  chmodSync(join(appDir, "AppRun"), 0o755);
61
66
  writeFileSync(
62
- join(appDir, `${config.app.fileName}.desktop`),
63
- desktopEntry({ name: config.app.name, fileName: config.app.fileName, schemes: config.deepLinks.schemes }),
67
+ join(appDir, `${config.app.id}.desktop`),
68
+ desktopEntry({
69
+ id: config.app.id,
70
+ name: config.app.name,
71
+ fileName: config.app.fileName,
72
+ schemes: config.deepLinks.schemes,
73
+ }),
64
74
  );
65
75
  const art = iconArt(config);
66
76
  if (art) {
67
- writeFileSync(join(appDir, `${config.app.fileName}.png`), encodePng(resize(art.master, 256, 256)));
68
- symlinkSync(`${config.app.fileName}.png`, join(appDir, ".DirIcon"));
77
+ writeFileSync(join(appDir, `${config.app.id}.png`), encodePng(resize(art.master, 256, 256)));
78
+ symlinkSync(`${config.app.id}.png`, join(appDir, ".DirIcon"));
69
79
  }
70
80
 
71
81
  log.step("installer: AppImage");
@@ -4,8 +4,10 @@ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import type { ParsedArgs } from "../lib/args.ts";
6
6
  import { iconArt, windowIcon } from "../lib/icons.ts";
7
+ import { resize } from "../lib/image.ts";
7
8
  import type { Launched, LaunchOptions } from "../lib/launch.ts";
8
9
  import { dim, log } from "../lib/log.ts";
10
+ import { encodePng } from "../lib/png.ts";
9
11
  import type { BuildContext } from "../lib/prepare.ts";
10
12
  import {
11
13
  buildNativeLibrary,
@@ -39,7 +41,10 @@ export async function buildLinux(ctx: BuildContext): Promise<string> {
39
41
  const resources = resourcesOf("linux", dir);
40
42
  writeDesktopResources(ctx, resources, "linux");
41
43
  const art = iconArt(config);
42
- if (art) writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
44
+ if (art) {
45
+ writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
46
+ writeFileSync(join(resources, "icon.png"), encodePng(resize(art.master, 256, 256)));
47
+ }
43
48
  if (ctx.linux?.appImage) ctx.artifacts.push({ kind: "installer", path: await buildAppImage(ctx, dir) });
44
49
  const size = await folderSize(dir);
45
50
  if (size) log.info(dim(`size ${size}`));
@@ -14,6 +14,7 @@ import {
14
14
  } from "node:fs";
15
15
  import { basename, join } from "node:path";
16
16
  import type { ParsedArgs } from "../lib/args.ts";
17
+ import { buildDmg } from "../lib/dmg.ts";
17
18
  import { execOrThrow } from "../lib/exec.ts";
18
19
  import { icns, iconArt, macosIconImage } from "../lib/icons.ts";
19
20
  import type { Launched, LaunchOptions } from "../lib/launch.ts";
@@ -21,7 +22,6 @@ import { dim, log } from "../lib/log.ts";
21
22
  import {
22
23
  appEntitlements,
23
24
  assessGatekeeper,
24
- buildDmg,
25
25
  codesign,
26
26
  notarize,
27
27
  type ResolvedMacosIdentity,
@@ -144,7 +144,10 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
144
144
  if (ctx.macos?.dmg) {
145
145
  const dmg = join(outDir, `${config.app.fileName}-${config.app.version}-${ctx.arch ?? hostArch()}.dmg`);
146
146
  log.step(`dmg: ${basename(dmg)}`);
147
- await buildDmg(appPath, dmg, config.app.name, join(outDir, "gen"));
147
+ await buildDmg(appPath, dmg, config.app.name, join(outDir, "gen"), {
148
+ layout: config.desktop.dmg,
149
+ volumeIcon: join(resources, "AppIcon.icns"),
150
+ });
148
151
 
149
152
  if (identity.sign !== "-") await codesign(identity, dmg, { hardened: false, timestamp: team });
150
153
  if (notarization) {
@@ -25,12 +25,19 @@ export function desktopExec(exe: string): string {
25
25
  return `"${exe.replace(/[\\"`$]/g, (c) => `\\${c}`)}" %u`;
26
26
  }
27
27
 
28
- export function desktopEntry(name: string, exe: string, schemes: readonly string[]): string {
28
+ export function desktopEntry(
29
+ name: string,
30
+ exe: string,
31
+ schemes: readonly string[],
32
+ { id, icon = false }: { id?: string; icon?: boolean } = {},
33
+ ): string {
29
34
  return [
30
35
  "[Desktop Entry]",
31
36
  "Type=Application",
32
37
  `Name=${name.replace(/[\r\n]/g, " ")}`,
33
38
  `Exec=${desktopExec(exe)}`,
39
+ ...(id && icon ? [`Icon=${id}`] : []),
40
+ ...(id ? [`StartupWMClass=${id}`] : []),
34
41
  "Terminal=false",
35
42
 
36
43
  "NoDisplay=true",
@@ -69,17 +76,28 @@ function writeIfChanged(path: string, text: string): boolean {
69
76
  return true;
70
77
  }
71
78
 
79
+ /** Copies the app's icon into the user's hicolor theme as <app id>.png; false without one. */
80
+ function installIcon(dataHome: string, id: string, icon: string | undefined): boolean {
81
+ if (!icon || !existsSync(icon)) return false;
82
+ const target = join(dataHome, "icons", "hicolor", "256x256", "apps", `${id}.png`);
83
+ const bytes = readFileSync(icon);
84
+ if (existsSync(target) && readFileSync(target).equals(bytes)) return true;
85
+ mkdirSync(dirname(target), { recursive: true });
86
+ writeFileSync(target, bytes);
87
+ return true;
88
+ }
89
+
72
90
  async function registerLinux(
73
91
  app: { id: string; name: string },
74
92
  exe: string,
75
93
  schemes: readonly string[],
76
- env: Env,
77
- home: string,
78
- runner: Runner,
94
+ { env, home, runner, icon }: { env: Env; home: string; runner: Runner; icon?: string },
79
95
  ): Promise<void> {
80
96
  const file = `${app.id}.desktop`;
81
- const applications = join(xdg(env.XDG_DATA_HOME, join(home, ".local", "share")), "applications");
82
- const entryChanged = writeIfChanged(join(applications, file), desktopEntry(app.name, exe, schemes));
97
+ const dataHome = xdg(env.XDG_DATA_HOME, join(home, ".local", "share"));
98
+ const applications = join(dataHome, "applications");
99
+ const entry = desktopEntry(app.name, exe, schemes, { id: app.id, icon: installIcon(dataHome, app.id, icon) });
100
+ const entryChanged = writeIfChanged(join(applications, file), entry);
83
101
  const listPath = join(xdg(env.XDG_CONFIG_HOME, join(home, ".config")), "mimeapps.list");
84
102
  const list = existsSync(listPath) ? readFileSync(listPath, "utf8") : "";
85
103
  writeIfChanged(listPath, withDefaults(list, file, schemes));
@@ -128,16 +146,21 @@ async function registerWindows(
128
146
  }
129
147
  }
130
148
 
131
- /** Makes this executable the current user's handler of the app's schemes (Windows, Linux). */
149
+ /**
150
+ * Makes this executable the current user's handler of the app's schemes (Windows, Linux). `icon`: a 256 px PNG the
151
+ * Linux entry shows (resources/icon.png).
152
+ */
132
153
  export async function registerDeepLinks(
133
154
  app: { id: string; name: string },
134
155
  schemes: readonly string[],
135
- options: { exe?: string; platform?: NodeJS.Platform; env?: Env; home?: string; run?: Runner } = {},
156
+ options: { exe?: string; platform?: NodeJS.Platform; env?: Env; home?: string; run?: Runner; icon?: string } = {},
136
157
  ): Promise<void> {
137
158
  const platform = options.platform ?? process.platform;
138
159
  if (!schemes.length || (platform !== "win32" && platform !== "linux")) return;
139
- const exe = options.exe ?? process.execPath;
160
+ const env = options.env ?? process.env;
161
+
162
+ const exe = options.exe ?? (env.APPIMAGE || process.execPath);
140
163
  const runner = options.run ?? run;
141
164
  if (platform === "win32") await registerWindows(app, exe, schemes, runner);
142
- else await registerLinux(app, exe, schemes, options.env ?? process.env, options.home ?? homedir(), runner);
165
+ else await registerLinux(app, exe, schemes, { env, home: options.home ?? homedir(), runner, icon: options.icon });
143
166
  }
@@ -447,7 +447,7 @@ export function startHost(plugins: DesktopPlugin[]): void {
447
447
  if (typeof launch.exit === "number") return;
448
448
 
449
449
  openLinks(process.argv.slice(2));
450
- registerDeepLinks(boot.app, deepLinkSchemes).catch((error) =>
450
+ registerDeepLinks(boot.app, deepLinkSchemes, { icon: join(paths.resources, "icon.png") }).catch((error) =>
451
451
  console.warn("[akan-native] cannot register the deep link schemes", error),
452
452
  );
453
453
  })