akanjs 3.0.0-beta.26 → 3.0.0-beta.28

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 (226) hide show
  1. package/base/baseEnv.ts +19 -5
  2. package/client/csrTypes.ts +2 -0
  3. package/client/device.ts +28 -12
  4. package/client/index.ts +1 -0
  5. package/client/native/accessibility.ts +1 -0
  6. package/client/native/app-state.ts +1 -0
  7. package/client/native/app.ts +1 -0
  8. package/client/native/appearance.ts +1 -0
  9. package/client/native/auth-session.ts +1 -0
  10. package/client/native/autostart.ts +1 -0
  11. package/client/native/badge.ts +1 -0
  12. package/client/native/biometric.ts +1 -0
  13. package/client/native/browser.ts +1 -0
  14. package/client/native/camera.ts +1 -0
  15. package/client/native/clipboard.ts +1 -0
  16. package/client/native/contacts.ts +1 -0
  17. package/client/native/device.ts +1 -0
  18. package/client/native/dialog.ts +1 -0
  19. package/client/native/dock.ts +1 -0
  20. package/client/native/file-picker.ts +1 -0
  21. package/client/native/filesystem.ts +1 -0
  22. package/client/native/geolocation.ts +1 -0
  23. package/client/native/global-shortcut.ts +1 -0
  24. package/client/native/haptics.ts +1 -0
  25. package/client/native/http.ts +1 -0
  26. package/client/native/iap.ts +1 -0
  27. package/client/native/keep-awake.ts +1 -0
  28. package/client/native/keyboard.ts +1 -0
  29. package/client/native/local-notifications.ts +1 -0
  30. package/client/native/menu.ts +1 -0
  31. package/client/native/network.ts +1 -0
  32. package/client/native/opener.ts +1 -0
  33. package/client/native/preferences.ts +1 -0
  34. package/client/native/push.ts +1 -0
  35. package/client/native/screen-orientation.ts +1 -0
  36. package/client/native/screen.ts +1 -0
  37. package/client/native/secure-storage.ts +1 -0
  38. package/client/native/share.ts +1 -0
  39. package/client/native/single-instance.ts +1 -0
  40. package/client/native/splash-screen.ts +1 -0
  41. package/client/native/sqlite.ts +1 -0
  42. package/client/native/toast.ts +1 -0
  43. package/client/native/tray.ts +1 -0
  44. package/client/native/updates.ts +1 -0
  45. package/client/native/volume.ts +1 -0
  46. package/client/native/window-state.ts +1 -0
  47. package/client/native/window.ts +1 -0
  48. package/client/native.ts +31 -1
  49. package/client/resolveServerUrl.ts +11 -0
  50. package/client/router.ts +8 -0
  51. package/common/Logger.ts +26 -2
  52. package/common/RouteLayering.ts +52 -0
  53. package/common/index.ts +1 -0
  54. package/index.ts +115 -6
  55. package/native/desktop.ts +5 -0
  56. package/package.json +11 -1
  57. package/server/akanApp.ts +23 -3
  58. package/server/akanAppHeaders.ts +4 -1
  59. package/server/akanServer.ts +27 -7
  60. package/server/index.ts +1 -0
  61. package/server/lifecycle/orphanGroup.ts +21 -0
  62. package/server/nativeFile.ts +107 -0
  63. package/server/ops/opsRoute.ts +3 -2
  64. package/server/routeTreeBuilder.ts +14 -39
  65. package/server/routing/apiRouter.ts +26 -3
  66. package/server/routing/hostAllowlist.ts +24 -0
  67. package/server/selfExec.ts +25 -0
  68. package/server/types.tsx +3 -1
  69. package/server/webRouter.ts +7 -4
  70. package/service/ipcTypes.ts +4 -0
  71. package/signal/CrossSiteGuard.ts +7 -0
  72. package/test/signalTestRuntime.ts +8 -2
  73. package/types/base/baseEnv.d.ts +1 -0
  74. package/types/client/csrTypes.d.ts +2 -0
  75. package/types/client/index.d.ts +1 -0
  76. package/types/client/native/accessibility.d.ts +1 -0
  77. package/types/client/native/app-state.d.ts +1 -0
  78. package/types/client/native/app.d.ts +1 -0
  79. package/types/client/native/appearance.d.ts +1 -0
  80. package/types/client/native/auth-session.d.ts +1 -0
  81. package/types/client/native/autostart.d.ts +1 -0
  82. package/types/client/native/badge.d.ts +1 -0
  83. package/types/client/native/biometric.d.ts +1 -0
  84. package/types/client/native/browser.d.ts +1 -0
  85. package/types/client/native/camera.d.ts +1 -0
  86. package/types/client/native/clipboard.d.ts +1 -0
  87. package/types/client/native/contacts.d.ts +1 -0
  88. package/types/client/native/device.d.ts +1 -0
  89. package/types/client/native/dialog.d.ts +1 -0
  90. package/types/client/native/dock.d.ts +1 -0
  91. package/types/client/native/file-picker.d.ts +1 -0
  92. package/types/client/native/filesystem.d.ts +1 -0
  93. package/types/client/native/geolocation.d.ts +1 -0
  94. package/types/client/native/global-shortcut.d.ts +1 -0
  95. package/types/client/native/haptics.d.ts +1 -0
  96. package/types/client/native/http.d.ts +1 -0
  97. package/types/client/native/iap.d.ts +1 -0
  98. package/types/client/native/keep-awake.d.ts +1 -0
  99. package/types/client/native/keyboard.d.ts +1 -0
  100. package/types/client/native/local-notifications.d.ts +1 -0
  101. package/types/client/native/menu.d.ts +1 -0
  102. package/types/client/native/network.d.ts +1 -0
  103. package/types/client/native/opener.d.ts +1 -0
  104. package/types/client/native/preferences.d.ts +1 -0
  105. package/types/client/native/push.d.ts +1 -0
  106. package/types/client/native/screen-orientation.d.ts +1 -0
  107. package/types/client/native/screen.d.ts +1 -0
  108. package/types/client/native/secure-storage.d.ts +1 -0
  109. package/types/client/native/share.d.ts +1 -0
  110. package/types/client/native/single-instance.d.ts +1 -0
  111. package/types/client/native/splash-screen.d.ts +1 -0
  112. package/types/client/native/sqlite.d.ts +1 -0
  113. package/types/client/native/toast.d.ts +1 -0
  114. package/types/client/native/tray.d.ts +1 -0
  115. package/types/client/native/updates.d.ts +1 -0
  116. package/types/client/native/volume.d.ts +1 -0
  117. package/types/client/native/window-state.d.ts +1 -0
  118. package/types/client/native/window.d.ts +1 -0
  119. package/types/client/native.d.ts +14 -2
  120. package/types/client/resolveServerUrl.d.ts +1 -0
  121. package/types/client/router.d.ts +3 -0
  122. package/types/common/RouteLayering.d.ts +28 -0
  123. package/types/common/index.d.ts +1 -0
  124. package/types/index.d.ts +112 -6
  125. package/types/native/desktop.d.ts +3 -0
  126. package/types/server/akanAppHeaders.d.ts +2 -0
  127. package/types/server/index.d.ts +1 -0
  128. package/types/server/lifecycle/orphanGroup.d.ts +7 -0
  129. package/types/server/nativeFile.d.ts +10 -0
  130. package/types/server/routing/apiRouter.d.ts +3 -1
  131. package/types/server/routing/hostAllowlist.d.ts +8 -0
  132. package/types/server/selfExec.d.ts +8 -0
  133. package/types/server/types.d.ts +3 -1
  134. package/types/service/ipcTypes.d.ts +16 -0
  135. package/types/signal/CrossSiteGuard.d.ts +1 -0
  136. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  137. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +57 -4
  138. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +48 -0
  139. package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +8 -0
  140. package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +8 -2
  141. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +10 -0
  142. package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +24 -0
  143. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +24 -0
  144. package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +4 -1
  145. package/types/vendor/@akanjs/native/packages/cli/src/platforms/macos.d.ts +5 -0
  146. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +38 -0
  147. package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +8 -1
  148. package/types/vendor/@akanjs/native/packages/desktop/src/grants.d.ts +11 -0
  149. package/types/vendor/@akanjs/native/packages/desktop/src/main.d.ts +11 -0
  150. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +13 -2
  151. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +27 -0
  152. package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +21 -0
  153. package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +79 -0
  154. package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +5 -0
  155. package/types/vendor/@akanjs/native/plugins/file-picker/src/index.d.ts +20 -1
  156. package/types/vendor/@akanjs/native/plugins/push/src/index.d.ts +9 -0
  157. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +28 -0
  158. package/types/vendor/@akanjs/native/plugins/volume/src/common.d.ts +10 -0
  159. package/types/vendor/@akanjs/native/plugins/volume/src/desktop.d.ts +18 -0
  160. package/types/vendor/@akanjs/native/plugins/volume/src/index.d.ts +28 -0
  161. package/types/vendor/@akanjs/native/plugins/volume/src.d.ts +1 -0
  162. package/types/webkit/csrFrameDump.d.ts +42 -0
  163. package/types/webkit/nativeNavigation.d.ts +1 -0
  164. package/types/webkit/nativePurchase.d.ts +6 -2
  165. package/types/webkit/nativeUpdates.d.ts +8 -0
  166. package/ui/Agent/Attach.tsx +6 -2
  167. package/ui/CsrImage.tsx +3 -2
  168. package/ui/System/CSR.tsx +4 -0
  169. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +5 -1
  170. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
  171. package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
  172. package/vendor/@akanjs/native/native/desktop/src/lib.rs +161 -25
  173. package/vendor/@akanjs/native/native/desktop/src/volume.rs +184 -0
  174. package/vendor/@akanjs/native/native/desktop/src/win/mod.rs +3 -1
  175. package/vendor/@akanjs/native/native/desktop/src/win/volume.rs +71 -0
  176. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
  177. package/vendor/@akanjs/native/packages/cli/src/api.ts +142 -12
  178. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +5 -3
  179. package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +10 -71
  180. package/vendor/@akanjs/native/packages/cli/src/config.ts +41 -0
  181. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -2
  182. package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +58 -0
  183. package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +2 -0
  184. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +10 -2
  185. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +38 -2
  186. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +104 -0
  187. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +85 -5
  188. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +1 -0
  189. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +6 -4
  190. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +16 -0
  191. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +44 -1
  192. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +436 -0
  193. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +2 -0
  194. package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
  195. package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +20 -0
  196. package/vendor/@akanjs/native/packages/desktop/src/grants.ts +47 -0
  197. package/vendor/@akanjs/native/packages/desktop/src/host.ts +102 -14
  198. package/vendor/@akanjs/native/packages/desktop/src/main.ts +59 -11
  199. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +38 -4
  200. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +28 -0
  201. package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +159 -0
  202. package/vendor/@akanjs/native/packages/desktop/src/server.ts +493 -0
  203. package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +14 -0
  204. package/vendor/@akanjs/native/plugins/app/native-plugin.json +2 -2
  205. package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
  206. package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
  207. package/vendor/@akanjs/native/plugins/app/src/web.ts +3 -0
  208. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +39 -2
  209. package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +36 -1
  210. package/vendor/@akanjs/native/plugins/push/android/PushPlugin.kt +9 -1
  211. package/vendor/@akanjs/native/plugins/push/ios/PushPlugin.swift +4 -0
  212. package/vendor/@akanjs/native/plugins/push/src/index.ts +10 -0
  213. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +464 -452
  214. package/vendor/@akanjs/native/plugins/volume/android/VolumePlugin.kt +107 -0
  215. package/vendor/@akanjs/native/plugins/volume/native-plugin.json +14 -0
  216. package/vendor/@akanjs/native/plugins/volume/src/common.ts +28 -0
  217. package/vendor/@akanjs/native/plugins/volume/src/desktop.ts +185 -0
  218. package/vendor/@akanjs/native/plugins/volume/src/index.ts +32 -0
  219. package/webkit/CsrRouteTable.ts +15 -35
  220. package/webkit/RenderLayer.tsx +71 -6
  221. package/webkit/bootCsr.tsx +11 -8
  222. package/webkit/csrFrameDump.ts +67 -0
  223. package/webkit/nativeNavigation.ts +17 -0
  224. package/webkit/nativePurchase.ts +12 -2
  225. package/webkit/nativeUpdates.ts +55 -0
  226. package/webkit/useCsrValues.ts +27 -0
@@ -0,0 +1,71 @@
1
+ //! The system output volume on Windows (volume plugin), with the ops and JSON of volume.rs: the default
2
+ //! render endpoint's IAudioEndpointVolume, which is what the taskbar's volume slider moves.
3
+ //!
4
+ //! - volume.watch is not an op here: the plugin host polls volume.get. Hearing changes would take an
5
+ //! IAudioEndpointVolumeCallback per endpoint and an IMMNotificationClient for a new default endpoint,
6
+ //! two COM objects kept alive for one event.
7
+ //! - Every endpoint has a software volume, so it is always settable.
8
+
9
+ use windows::Win32::Media::Audio::Endpoints::IAudioEndpointVolume;
10
+ use windows::Win32::Media::Audio::{eConsole, eRender, IMMDeviceEnumerator, MMDeviceEnumerator};
11
+ use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_ALL, CLSCTX_INPROC_SERVER};
12
+
13
+ use crate::json;
14
+
15
+ fn endpoint() -> Result<IAudioEndpointVolume, String> {
16
+ // COM is initialized on this thread (tao's OleInitialize, wry's CoInitializeEx).
17
+ unsafe {
18
+ let devices: IMMDeviceEnumerator =
19
+ CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER).map_err(|e| format!("INTERNAL: cannot list the audio devices: {e}"))?;
20
+ let device = devices.GetDefaultAudioEndpoint(eRender, eConsole).map_err(|_| "NOT_FOUND: there is no audio output".to_string())?;
21
+ device.Activate::<IAudioEndpointVolume>(CLSCTX_ALL, None).map_err(|e| format!("UNSUPPORTED: the audio output has no volume control: {e}"))
22
+ }
23
+ }
24
+
25
+ pub fn state_json(level: f32, muted: bool) -> String {
26
+ format!(r#"{{"level":{},"muted":{},"settable":true}}"#, (f64::from(level) * 1000.0).round() / 1000.0, muted)
27
+ }
28
+
29
+ fn state(volume: &IAudioEndpointVolume) -> Result<String, String> {
30
+ unsafe {
31
+ let level = volume.GetMasterVolumeLevelScalar().map_err(|e| format!("INTERNAL: cannot read the volume: {e}"))?;
32
+ let muted = volume.GetMute().map_err(|e| format!("INTERNAL: cannot read the mute: {e}"))?.as_bool();
33
+ Ok(state_json(level, muted))
34
+ }
35
+ }
36
+
37
+ fn set(cmd: &json::V) -> Result<String, String> {
38
+ let level = cmd.get("level").and_then(|v| v.as_f64()).filter(|v| (0.0..=1.0).contains(v)).ok_or("level must be a number from 0 to 1")?;
39
+ let volume = endpoint()?;
40
+ unsafe { volume.SetMasterVolumeLevelScalar(level as f32, std::ptr::null()) }.map_err(|e| format!("INTERNAL: cannot set the volume: {e}"))?;
41
+ state(&volume)
42
+ }
43
+
44
+ fn mute(cmd: &json::V) -> Result<String, String> {
45
+ let muted = cmd.get("muted").and_then(|v| v.as_bool()).ok_or("muted must be a boolean")?;
46
+ let volume = endpoint()?;
47
+ unsafe { volume.SetMute(muted, std::ptr::null()) }.map_err(|e| format!("INTERNAL: cannot set the mute: {e}"))?;
48
+ state(&volume)
49
+ }
50
+
51
+ /// volume.* shell ops; None for other ops.
52
+ pub fn shell_op(op: &str, cmd: &json::V) -> Option<Result<String, String>> {
53
+ let result = match op {
54
+ "volume.get" => endpoint().and_then(|volume| state(&volume)),
55
+ "volume.set" => set(cmd),
56
+ "volume.mute" => mute(cmd),
57
+ _ if op.starts_with("volume.") => Err(format!("unknown op {op}")),
58
+ _ => return None,
59
+ };
60
+ Some(result)
61
+ }
62
+
63
+ #[cfg(test)]
64
+ mod tests {
65
+ use super::*;
66
+
67
+ #[test]
68
+ fn state_shape() {
69
+ assert_eq!(state_json(0.49999997, true), r#"{"level":0.5,"muted":true,"settable":true}"#);
70
+ }
71
+ }
@@ -13,10 +13,20 @@ enum AkanNativeLog {
13
13
  logger.info("\(text, privacy: .public)")
14
14
  }
15
15
 
16
+ /// One `[page<+> <level>]` tag per line, the desktop host's grammar, which the akan CLI reads the level back from;
17
+ /// os_log takes the page's level too, so `log stream` and Console.app filter on it.
16
18
  static func page(_ level: String, _ text: String) {
17
- print("[page \(level)] \(text)")
19
+ let lines = text.replacingOccurrences(of: "\\n+$", with: "", options: .regularExpression)
20
+ .components(separatedBy: "\n")
21
+ print(lines.enumerated().map { "[page\($0.offset == 0 ? "" : "+") \(level)] \($0.element)" }.joined(separator: "\n"))
18
22
  fflush(stdout)
19
- logger.info("[page \(level, privacy: .public)] \(text, privacy: .public)")
23
+ let message = lines.joined(separator: "\n")
24
+ switch level {
25
+ case "error": logger.error("\(message, privacy: .public)")
26
+ case "warn": logger.warning("\(message, privacy: .public)")
27
+ case "trace", "verbose", "debug": logger.debug("\(message, privacy: .public)")
28
+ default: logger.info("\(message, privacy: .public)")
29
+ }
20
30
  }
21
31
  }
22
32
 
@@ -1,8 +1,11 @@
1
1
 
2
2
 
3
- import { resolve } from "node:path";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
4
5
  import { type DoctorCheck, doctorChecks, hostPlatforms, runFixes } from "./commands/doctor.ts";
5
6
  import type { AkanNativeConfig } from "./config.ts";
7
+ import { makeNativeBoot } from "./lib/boot.ts";
8
+ import { BUNDLE_FILE, compatProblems, readBundleInfo } from "./lib/compat.ts";
6
9
  import { CancelledError, withSignal } from "./lib/exec.ts";
7
10
  import { type HmrContext, lanAddress, resolveDevEntry, startHmrServer } from "./lib/hmr.ts";
8
11
  import type { Device, DeviceSelector, Launched } from "./lib/launch.ts";
@@ -19,8 +22,18 @@ import {
19
22
  SigningError,
20
23
  WebBuildError,
21
24
  WebInputError,
25
+ type WindowsBuild,
22
26
  } from "./lib/prepare.ts";
23
27
  import { ConfigError, type Project, projectFromConfig } from "./lib/project.ts";
28
+ import { assertSigningKey, publishRelease } from "./lib/publish.ts";
29
+ import {
30
+ assertChannel,
31
+ generateUpdateKey,
32
+ type UpdateManifest,
33
+ updateKeyPath,
34
+ webManifest,
35
+ writeWebBundle,
36
+ } from "./lib/updates.ts";
24
37
  import { androidDevices, launchAndroid } from "./platforms/android.ts";
25
38
  import { PLATFORM_TARGETS, type TargetPlatform } from "./platforms/index.ts";
26
39
  import { iosDevices, physicalIosDevice } from "./platforms/ios.ts";
@@ -36,10 +49,11 @@ export type {
36
49
  IosSigningResult,
37
50
  LogEvent,
38
51
  TargetPlatform,
52
+ WindowsBuild,
39
53
  };
40
54
 
41
55
  /** semver of this API. A caller checks the major before it relies on anything here. */
42
- export const API_VERSION = "0.1.0";
56
+ export const API_VERSION = "0.8.0";
43
57
 
44
58
  export type AkanNativeErrorCode =
45
59
  | "CONFIG_INVALID"
@@ -97,7 +111,7 @@ export interface TaskOptions {
97
111
  }
98
112
 
99
113
  export interface Artifact {
100
- kind: "app" | "apk" | "aab" | "ipa" | "folder" | "web";
114
+ kind: "app" | "apk" | "aab" | "ipa" | "folder" | "web" | "installer";
101
115
  path: string;
102
116
  signing: "none" | "adhoc" | "debug" | "development" | "distribution";
103
117
  device?: "simulator" | "device";
@@ -141,18 +155,17 @@ export function validateConfig(config: AkanNativeConfig, options: { appDir: stri
141
155
  }
142
156
 
143
157
  /** Builds the app. Default profile release (like `akan-native build`). `ios.device`: an iPhone build (signed; release adds an .ipa). */
144
- export function build(options: TaskOptions & { profile?: BuildProfile; ios?: IosBuild }): Promise<BuildResult> {
158
+ export function build(
159
+ options: TaskOptions & { profile?: BuildProfile; ios?: IosBuild; windows?: WindowsBuild },
160
+ ): Promise<BuildResult> {
145
161
  return task(
146
162
  options,
147
163
  async (warnings) =>
148
164
  (
149
- await buildIn(
150
- options,
151
- options.profile ?? "release",
152
- options.mode ?? "production",
153
- warnings,
154
- options.ios ? { ios: options.ios } : {},
155
- )
165
+ await buildIn(options, options.profile ?? "release", options.mode ?? "production", warnings, {
166
+ ...(options.ios ? { ios: options.ios } : {}),
167
+ ...(options.windows ? { windows: options.windows } : {}),
168
+ })
156
169
  ).result,
157
170
  );
158
171
  }
@@ -184,6 +197,123 @@ export function release(
184
197
  });
185
198
  }
186
199
 
200
+ /**
201
+ * An unsigned web bundle update (UP-2), for a signer that keeps the key off the build machine. Writes
202
+ * `<out>/files/<sha256>`, `<out>/manifest.template.json` (the manifest with `channel`, `sequence` and `bundle` left
203
+ * empty for the signer) and `<out>/bundle.json` (the native API the bundle needs, UP-3). Prepares the web bundle as a
204
+ * release build does, without building the native app.
205
+ */
206
+ export function packUpdate(
207
+ options: TaskOptions & { platform: "ios" | "android"; out: string },
208
+ ): Promise<{ manifest: UpdateManifest; out: string }> {
209
+ return task(options, async () => {
210
+ const appDir = resolve(options.appDir);
211
+ let project: Project;
212
+ try {
213
+ project = projectFromConfig(options.config, appDir, { resolveFrom: options.resolveFrom });
214
+ } catch (error) {
215
+ throw toAkanNativeError(error, "CONFIG_INVALID");
216
+ }
217
+
218
+ const out = resolve(options.out);
219
+ mkdirSync(out, { recursive: true });
220
+ const ctx = await prepare(project, options.platform, {
221
+ mode: options.mode ?? "production",
222
+ profile: "release",
223
+ skipWebBuild: options.skipWebBuild ?? false,
224
+ outDir: out,
225
+ envFiles: options.envFiles,
226
+ env: options.env,
227
+ api: true,
228
+ }).catch((error) => {
229
+ throw toAkanNativeError(error, "CONFIG_INVALID");
230
+ });
231
+ makeNativeBoot(ctx, options.platform);
232
+ const info = readBundleInfo(join(out, BUNDLE_FILE));
233
+ const manifest = webManifest({
234
+ app: project.config.app,
235
+ platform: options.platform,
236
+ channel: "",
237
+ nativeApi: info.nativeApi.hash,
238
+ sequence: 0,
239
+ bundle: "",
240
+ files: writeWebBundle(out, ctx.webDir, ctx.html, ctx.env),
241
+ });
242
+ writeFileSync(join(out, "manifest.template.json"), `${JSON.stringify(manifest, null, 2)}\n`);
243
+ return { manifest, out };
244
+ });
245
+ }
246
+
247
+ export interface PublishResult {
248
+ build: BuildResult;
249
+ /** `<out>/<os>-<arch>` (desktop) or `<out>/<platform>` (a phone's web bundle): what to upload under updates.url. */
250
+ dir: string;
251
+ bundle: string;
252
+ channel: string;
253
+ files: number;
254
+ /** Bytes of the files the manifest names. */
255
+ size: number;
256
+ }
257
+
258
+ /**
259
+ * A signed release for the updates plugin (UP-1, UP-2): a release build, then `<channel>.json`, its signature and
260
+ * its files under `out` (default <appDir>/.akan/native/updates), signed with the key updateKeygen() made.
261
+ */
262
+ export function publishUpdate(
263
+ options: TaskOptions & { platform: Exclude<TargetPlatform, "web">; channel?: string; out?: string },
264
+ ): Promise<PublishResult> {
265
+ return task(options, async (warnings) => {
266
+ if (!options.config.updates)
267
+ throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url, publicKey } to publish for");
268
+ try {
269
+ if (options.channel !== undefined) assertChannel(options.channel);
270
+ assertSigningKey(options.config);
271
+ } catch (error) {
272
+ throw toAkanNativeError(error, "CONFIG_INVALID");
273
+ }
274
+ const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings);
275
+ const channel = options.channel ?? ctx.project.config.updates?.channel ?? "production";
276
+ const out = resolve(options.out ?? resolve(options.appDir, ".akan", "native", "updates"));
277
+ const { dir, manifest } = await publishRelease(ctx, options.platform, artifact, out, channel).catch((error) => {
278
+ throw toAkanNativeError(error, "CONFIG_INVALID");
279
+ });
280
+ return {
281
+ build: result,
282
+ dir,
283
+ bundle: manifest.bundle,
284
+ channel,
285
+ files: manifest.files.length,
286
+ size: manifest.files.reduce((n, f) => n + f.size, 0),
287
+ };
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Whether a web bundle can run in the binary a store release shipped (UP-3): `shipped` is that build's bundle.json,
293
+ * `bundle` the one `packUpdate` wrote. Anything in `problems` means a new binary.
294
+ */
295
+ export function compareBundles(shipped: string, bundle: string): { compatible: boolean; problems: string[] } {
296
+ const [before, now] = [readBundleInfo(resolve(shipped)), readBundleInfo(resolve(bundle))];
297
+ const problems =
298
+ before.app.id !== now.app.id
299
+ ? [`${shipped} is for ${before.app.id}, the bundle for ${now.app.id}`]
300
+ : compatProblems(before.nativeApi.inputs, now.nativeApi.inputs);
301
+ return { compatible: problems.length === 0, problems };
302
+ }
303
+
304
+ /**
305
+ * The app's update signing key: made once (AKAN_NATIVE_UPDATE_KEY, else ~/.akan/native/keys/<app id>.update.key),
306
+ * then read. `publicKey` goes into the config's `updates.publicKey`; the key file never leaves the machine.
307
+ */
308
+ export function updateKeygen(options: { config: AkanNativeConfig }): {
309
+ publicKey: string;
310
+ keyPath: string;
311
+ created: boolean;
312
+ } {
313
+ const keyPath = updateKeyPath(options.config.app.id);
314
+ return { ...generateUpdateKey(keyPath), keyPath };
315
+ }
316
+
187
317
  /** Builds (default profile debug) and launches the app on a simulator, emulator, desktop or browser. */
188
318
  export function run(
189
319
  options: TaskOptions & {
@@ -483,7 +613,7 @@ async function buildIn(
483
613
  profile: BuildProfile,
484
614
  mode: string,
485
615
  warnings: string[],
486
- extra: Pick<BuildOptions, "android" | "ios" | "devServer" | "startPath"> = {},
616
+ extra: Pick<BuildOptions, "android" | "ios" | "windows" | "devServer" | "startPath"> = {},
487
617
  ): Promise<{ ctx: BuildContext; artifact: string; result: BuildResult }> {
488
618
  const started = performance.now();
489
619
  const appDir = resolve(options.appDir);
@@ -8,14 +8,14 @@ import { findAppDir, loadProject } from "../lib/project.ts";
8
8
  import { PLATFORM_TARGETS, TARGETS, type TargetPlatform } from "../platforms/index.ts";
9
9
  import { physicalIosDevice } from "../platforms/ios.ts";
10
10
 
11
- export const BOOLEAN_FLAGS = ["skip-web-build", "open", "release", "debug", "headless", "aab"];
11
+ export const BOOLEAN_FLAGS = ["skip-web-build", "open", "release", "debug", "headless", "aab", "installer"];
12
12
 
13
13
  /** Flags of every build: the app, the env mode (.env.<mode>), the profile, the web build. */
14
- export const BUILD_FLAGS = ["app", "mode", "release", "debug", "skip-web-build", "aab", "device", "build"];
14
+ export const BUILD_FLAGS = ["app", "mode", "release", "debug", "skip-web-build", "aab", "installer", "device", "build"];
15
15
  /** Flags of launching a build (web server, simulators, emulators). */
16
16
  export const LAUNCH_FLAGS = ["port", "host", "open", "headless", "device", "avd"];
17
17
 
18
- export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--device] [--build <n>]`;
18
+ export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>]`;
19
19
  export const RUN_USAGE = `akan-native run <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--release] [--skip-web-build] [--port <n>] [--open] [--headless] [--device <name>] [--avd <name>] [--start <path>]`;
20
20
 
21
21
  export interface BuildDefaults {
@@ -57,6 +57,7 @@ export async function buildFromArgs(
57
57
  if (project.plugins.length) log.info(dim(`plugins: ${project.plugins.map((p) => p.manifest.id).join(", ")}`));
58
58
 
59
59
  if (args.flags.aab === true && platform !== "android") throw new CliError("--aab is for android", 2);
60
+ if (args.flags.installer === true && platform !== "windows") throw new CliError("--installer is for windows", 2);
60
61
 
61
62
  const device = args.flags.device;
62
63
  const iphone = platform === "ios" && typeof device === "string" ? await physicalIosDevice(device) : null;
@@ -69,6 +70,7 @@ export async function buildFromArgs(
69
70
  startPath: extra.startPath,
70
71
 
71
72
  ...(args.flags.aab === true ? { android: { bundle: true } } : {}),
73
+ ...(args.flags.installer === true ? { windows: { installer: true } } : {}),
72
74
 
73
75
  ...(iosDevice
74
76
  ? { ios: { device: true, ...(iphone ? { target: { udid: iphone.udid, name: iphone.name } } : {}) } }
@@ -1,22 +1,12 @@
1
1
 
2
2
 
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, statSync } from "node:fs";
4
4
  import { join, relative, resolve, sep } from "node:path";
5
5
  import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
6
- import { runtimeVersion } from "../lib/boot.ts";
7
- import { BUNDLE_FILE, readBundleInfo } from "../lib/compat.ts";
8
6
  import { bold, CliError, dim, log } from "../lib/log.ts";
9
7
  import { findAppDir, loadProject } from "../lib/project.ts";
10
- import {
11
- generateUpdateKey,
12
- hostArch,
13
- sha256,
14
- signManifest,
15
- type UpdateFile,
16
- type UpdateManifest,
17
- updateKeyPath,
18
- } from "../lib/updates.ts";
19
- import { publishAppUpdate } from "../platforms/desktop-update.ts";
8
+ import { assertSigningKey, publishRelease } from "../lib/publish.ts";
9
+ import { assertChannel, generateUpdateKey, updateKeyPath } from "../lib/updates.ts";
20
10
  import { BOOLEAN_FLAGS, buildFromArgs } from "./build.ts";
21
11
 
22
12
  export const UPDATE_USAGE =
@@ -42,80 +32,29 @@ async function keygen(args: ParsedArgs): Promise<number> {
42
32
  return 0;
43
33
  }
44
34
 
45
- /** Every file of a web bundle as the app serves it: index.html with the init script and CSP, public/, env.runtime.json. */
46
- function webFiles(webDir: string, html: string, env: Record<string, string>): { path: string; data: Uint8Array }[] {
47
- const files: { path: string; data: Uint8Array }[] = [];
48
- const walk = (dir: string) => {
49
- for (const name of readdirSync(dir).sort()) {
50
- const path = join(dir, name);
51
- if (statSync(path).isDirectory()) walk(path);
52
- else {
53
- const rel = relative(webDir, path);
54
- files.push({ path: rel, data: rel === "index.html" ? new TextEncoder().encode(html) : readFileSync(path) });
55
- }
56
- }
57
- };
58
- walk(webDir);
59
- files.push({ path: "env.runtime.json", data: new TextEncoder().encode(JSON.stringify(env, null, 2)) });
60
- return files;
61
- }
62
-
63
35
  async function publish(args: ParsedArgs): Promise<number> {
64
36
  const arg = args.positional[1];
65
37
  const desktop = arg === "macos" || arg === "windows" || arg === "linux" ? arg : null;
66
38
  if (!desktop && arg !== "ios" && arg !== "android") throw new CliError(`usage: ${UPDATE_USAGE}`, 2);
67
39
  const platform = desktop ?? (arg as "ios" | "android");
40
+ const requested = stringFlag(args, "channel");
41
+ if (requested !== undefined) assertChannel(requested);
42
+ assertSigningKey((await loadProject(findAppDir(stringFlag(args, "app") ?? process.cwd()))).config);
68
43
  const { ctx, artifact } = await buildFromArgs(
69
44
  { positional: [platform], flags: args.flags },
70
45
  { mode: "production", profile: "release" },
71
46
  platform,
72
47
  );
73
48
  const { config, appDir } = ctx.project;
74
- if (!config.updates)
75
- throw new CliError("akan-native.config.ts has no updates: { url, publicKey } (run `akan-native update keygen`)");
76
- const channel = stringFlag(args, "channel") ?? config.updates.channel;
77
-
78
- const out = join(outDir(args, appDir), desktop ? `${desktop}-${hostArch()}` : platform);
79
- const keyPath = updateKeyPath(config.app.id);
80
- const sequence = Math.floor(Date.now() / 1000);
81
-
82
- let manifest: UpdateManifest;
83
- if (desktop) {
84
- manifest = await publishAppUpdate(ctx, desktop, artifact, out, channel, sequence);
85
- } else {
86
- const info = readBundleInfo(join(ctx.outDir, BUNDLE_FILE));
87
- mkdirSync(join(out, "files"), { recursive: true });
88
- const files: UpdateFile[] = webFiles(ctx.webDir, ctx.html, ctx.env).map(({ path, data }) => {
89
- const hash = sha256(data);
90
- const target = join(out, "files", hash);
91
- if (!existsSync(target)) writeFileSync(target, data);
92
- return { path, sha256: hash, size: data.length };
93
- });
94
- manifest = {
95
- schema: 1,
96
- kind: "web",
97
- app: config.app.id,
98
- platform,
99
- channel,
100
- nativeApi: info.nativeApi.hash,
101
- sequence,
102
- bundle: `${sequence}-${info.web.hash.slice(0, 8)}`,
103
- runtimeVersion: runtimeVersion(),
104
- version: config.app.version,
105
- files,
106
- };
107
- }
108
- const bytes = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`);
109
- mkdirSync(out, { recursive: true });
110
- writeFileSync(join(out, `${channel}.json`), bytes);
111
- writeFileSync(join(out, `${channel}.json.sig`), `${signManifest(bytes, keyPath, config.updates.publicKey)}\n`);
49
+ const channel = requested ?? config.updates?.channel ?? "production";
50
+ const { dir, manifest } = await publishRelease(ctx, platform, artifact, outDir(args, appDir), channel);
112
51
  const size = manifest.files.reduce((n, f) => n + f.size, 0);
113
52
  log.ok(
114
- `published ${bold(manifest.bundle)} to ${relative(process.cwd(), out) || out}/${channel}.json ${dim(`(${manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB${manifest.nativeApi ? `, native API ${manifest.nativeApi}` : ""})`)}`,
53
+ `published ${bold(manifest.bundle)} to ${relative(process.cwd(), dir) || dir}/${channel}.json ${dim(`(${manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB${manifest.nativeApi ? `, native API ${manifest.nativeApi}` : ""})`)}`,
115
54
  );
116
55
  log.info(
117
56
  dim(
118
- `Upload ${relative(process.cwd(), join(out, ".."))} to ${config.updates.url} (or try it with \`akan-native update serve\`).`,
57
+ `Upload ${relative(process.cwd(), join(dir, ".."))} to ${config.updates?.url} (or try it with \`akan-native update serve\`).`,
119
58
  ),
120
59
  );
121
60
  return 0;
@@ -181,6 +181,11 @@ export interface AkanNativeConfig {
181
181
  * or APIs the bundle uses. Android 10+ devices with Google Play update WebView themselves.
182
182
  */
183
183
  minWebViewVersion?: number;
184
+ /**
185
+ * Media plays with sound without a tap first, as it does on iOS and the desktop (WebView's
186
+ * mediaPlaybackRequiresUserGesture off): an app nobody taps, such as a signage screen. Default false.
187
+ */
188
+ autoplay?: boolean;
184
189
  /**
185
190
  * Appended to app.id in debug builds (e.g. ".debug"), so a debug build installs next to the
186
191
  * release app. Deep links and app links must list the suffixed id for debug builds.
@@ -227,6 +232,42 @@ export interface AkanNativeConfig {
227
232
  * hides the window: the app keeps running, a Dock click shows it again, Cmd+Q quits. Default true.
228
233
  */
229
234
  quitOnLastWindowClosed?: boolean;
235
+ /**
236
+ * What a window does when its page's web process ends: a crash, a hang, out of memory. "errorPage"
237
+ * (default) loads the page again once and shows an error page when it ends again within a minute, so a
238
+ * page that crashes on load does not loop; the app quits when the webview's browser process ends
239
+ * (Windows). "reload" loads it again every time, waiting longer after each end in a row (1 s, doubling
240
+ * to a minute), and relaunches the app when the browser process ends: an app nobody attends, such as a
241
+ * kiosk or a signage screen. iOS and Android always start the page over.
242
+ */
243
+ recovery?: "errorPage" | "reload";
244
+ /**
245
+ * The main window from its first frame: `fullscreen` (borderless, on the display the window opens on)
246
+ * and `skipTaskbar` (no taskbar button; Windows and Linux). A plugin's launch phase may still decide
247
+ * otherwise (DesktopContext.launch.setWindow). Default false for both.
248
+ */
249
+ window?: { fullscreen?: boolean; skipTaskbar?: boolean };
250
+ /**
251
+ * Windows: "auto" answers the page's getDisplayMedia() with the first screen at once, without the picker or a
252
+ * user gesture: a remote-support session on an unattended screen. It is Chromium's switch for automated media
253
+ * tests (--use-fake-ui-for-media-stream), made for every media request, so keep it to an app whose pages ask
254
+ * for no camera or microphone. Default "picker". WebView2's choice by title follows the UI language, so none is
255
+ * offered. macOS and Linux ignore it.
256
+ */
257
+ screenCapture?: "picker" | "auto";
258
+ /**
259
+ * A server the app starts beside its window (akanjs `native.desktop.server`). `dir` is copied to
260
+ * the app's resources (`server/`), and `entry` there runs on the app's own Bun with `env`, bound to
261
+ * a loopback port picked at launch; the page reads its URL as PUBLIC_AKAN_SERVER_URL. The launcher
262
+ * sets PORT, JWT_SECRET, the data folders (`<app local data>/server`) and the listen host itself.
263
+ */
264
+ server?: { dir: string; entry: string; env?: Record<string, string> };
265
+ /**
266
+ * A folder of executables the app carries (akanjs `bin`): copied to the app's resources (`bin/`) and put
267
+ * first on the app's PATH at launch, so its plugins and its server find them by name before the computer's.
268
+ * macOS builds sign every Mach-O file in it.
269
+ */
270
+ bin?: string;
230
271
  };
231
272
  /**
232
273
  * App icon for every platform, generated from one square PNG, ideally 1024×1024 (CLI-8).
@@ -28,8 +28,15 @@ const KNOWN: Shape = {
28
28
  resources: { "[]": { from: true, to: true } },
29
29
  },
30
30
  security: { csp: true, shell: { externalSchemes: true } },
31
- android: { minWebViewVersion: true, debugAppIdSuffix: true, googleServices: true },
32
- desktop: { quitOnLastWindowClosed: true },
31
+ android: { minWebViewVersion: true, debugAppIdSuffix: true, googleServices: true, autoplay: true },
32
+ desktop: {
33
+ quitOnLastWindowClosed: true,
34
+ recovery: true,
35
+ window: { fullscreen: true, skipTaskbar: true },
36
+ screenCapture: true,
37
+ server: { dir: true, entry: true, env: true },
38
+ bin: true,
39
+ },
33
40
  keyboard: { resize: true },
34
41
  push: {
35
42
  android: { channel: { id: true, name: true, importance: true, description: true }, smallIcon: true, color: true },
@@ -0,0 +1,58 @@
1
+
2
+
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { LAUNCHER_ENV_KEYS } from "../../../desktop/src/server.ts";
6
+ import type { AkanNativeConfig } from "../config.ts";
7
+
8
+ export interface DesktopServerConfig {
9
+ /** Absolute. */
10
+ dir: string;
11
+ entry: string;
12
+ env: Record<string, string>;
13
+ }
14
+
15
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ const LAUNCHER_KEYS = new Set<string>(LAUNCHER_ENV_KEYS);
17
+
18
+ export function validateDesktopServer(
19
+ desktop: AkanNativeConfig["desktop"],
20
+ appDir: string,
21
+ problems: string[],
22
+ ): DesktopServerConfig | null {
23
+ const server = desktop?.server;
24
+ if (server === undefined) return null;
25
+ if (!server || typeof server !== "object") {
26
+ problems.push("desktop.server must be an object with dir and entry");
27
+ return null;
28
+ }
29
+ const before = problems.length;
30
+ const dir = typeof server.dir === "string" && server.dir ? resolve(appDir, server.dir) : null;
31
+ if (!dir) problems.push("desktop.server.dir is required: the folder the server runs from");
32
+ else if (!existsSync(dir) || !statSync(dir).isDirectory())
33
+ problems.push(`desktop.server.dir: ${dir} is not a folder`);
34
+ const entry = typeof server.entry === "string" ? server.entry : "";
35
+ if (!entry || isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))
36
+ problems.push(
37
+ `desktop.server.entry must be a file inside desktop.server.dir (got ${JSON.stringify(server.entry)})`,
38
+ );
39
+ else if (dir && existsSync(dir) && !existsSync(join(dir, entry)))
40
+ problems.push(`desktop.server.entry: ${join(dir, entry)} not found`);
41
+ const env: Record<string, string> = {};
42
+ if (server.env !== undefined && (!server.env || typeof server.env !== "object" || Array.isArray(server.env)))
43
+ problems.push("desktop.server.env must be an object of strings");
44
+ else
45
+ for (const [key, value] of Object.entries(server.env ?? {})) {
46
+
47
+ const upper = key.toUpperCase();
48
+ if (!ENV_NAME.test(key)) problems.push(`desktop.server.env: ${JSON.stringify(key)} is not a variable name`);
49
+ else if (LAUNCHER_KEYS.has(upper))
50
+ problems.push(`desktop.server.env.${key}: the launcher sets it at every start`);
51
+ else if (upper === "PATH" && key !== "PATH") problems.push(`desktop.server.env.${key}: write it as PATH`);
52
+ else if (Object.keys(env).some((kept) => kept.toUpperCase() === upper))
53
+ problems.push(`desktop.server.env.${key}: Windows reads it as the same variable as another one here`);
54
+ else if (typeof value !== "string") problems.push(`desktop.server.env.${key} must be a string`);
55
+ else env[key] = value;
56
+ }
57
+ return problems.length === before && dir ? { dir, entry, env } : null;
58
+ }
@@ -413,6 +413,8 @@ export async function startHmrServer(opts: HmrOptions): Promise<HmrServer> {
413
413
  Bun.serve({
414
414
  hostname: opts.hostname,
415
415
  port: opts.port,
416
+
417
+ idleTimeout: 0,
416
418
  async fetch(req, srv) {
417
419
  const url = new URL(req.url);
418
420
  if (relayed.has(url.pathname) && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
@@ -26,6 +26,10 @@ const DEV_SERVER_PAGE = `<!doctype html>
26
26
  </html>
27
27
  `;
28
28
 
29
+ export interface WindowsBuild {
30
+ installer?: boolean;
31
+ }
32
+
29
33
  export interface BuildOptions {
30
34
  mode: string;
31
35
  profile: BuildProfile;
@@ -57,6 +61,8 @@ export interface BuildOptions {
57
61
  android?: AndroidRelease;
58
62
  /** iOS devices (akanjs readiness O1-2, O1-3). */
59
63
  ios?: IosBuild;
64
+ /** Windows: an NSIS setup program next to the app folder (platforms/windows-installer.ts). */
65
+ windows?: WindowsBuild;
60
66
  }
61
67
 
62
68
  /** Which identity and profile an iOS device build signs with; unset: found in the keychain and Xcode's profiles. */
@@ -138,8 +144,9 @@ export interface BuildContext {
138
144
  startPath?: string;
139
145
  android?: AndroidRelease;
140
146
  ios?: IosBuild;
141
- /** Artifacts besides the one the builder returns (an .aab next to the .apk). */
142
- artifacts: { kind: "aab" | "ipa"; path: string }[];
147
+ windows?: WindowsBuild;
148
+ /** Artifacts besides the one the builder returns (an .aab next to the .apk, a Windows setup program). */
149
+ artifacts: { kind: "aab" | "ipa" | "installer"; path: string }[];
143
150
  /** What the builder signed with, when it signs (Android: the app's release key or the debug key). */
144
151
  signedAs?: "debug" | "development" | "distribution";
145
152
  /** Called through the API (BuildOptions.api). */
@@ -271,6 +278,7 @@ export async function prepare(project: Project, platform: Platform, options: Bui
271
278
  ...(options.profile === "debug" && options.startPath ? { startPath: checkStartPath(options.startPath) } : {}),
272
279
  ...(options.android ? { android: options.android } : {}),
273
280
  ...(options.ios ? { ios: options.ios } : {}),
281
+ ...(options.windows ? { windows: options.windows } : {}),
274
282
  artifacts: [],
275
283
  };
276
284
  }