@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.5

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 (67) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +53 -34
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
  9. package/skill-data/ts-core/SKILL.md +2 -0
  10. package/src/app_runner/app_assets.zig +85 -1
  11. package/src/app_runner/root.zig +63 -3
  12. package/src/app_runner/ts_core_main.zig +8 -0
  13. package/src/platform/linux/cef_host.cpp +22 -0
  14. package/src/platform/linux/gtk_host.c +317 -34
  15. package/src/platform/linux/gtk_host.h +37 -0
  16. package/src/platform/linux/root.zig +114 -20
  17. package/src/platform/macos/appkit_host.h +53 -0
  18. package/src/platform/macos/appkit_host.m +667 -67
  19. package/src/platform/macos/cef_host.mm +21 -0
  20. package/src/platform/macos/root.zig +146 -29
  21. package/src/platform/null_platform.zig +136 -0
  22. package/src/platform/root.zig +7 -0
  23. package/src/platform/types.zig +90 -0
  24. package/src/platform/windows/dialog_test.cpp +303 -0
  25. package/src/platform/windows/host_loop_test.cpp +169 -0
  26. package/src/platform/windows/root.zig +262 -39
  27. package/src/platform/windows/session_end_test.cpp +303 -0
  28. package/src/platform/windows/webview2_host.cpp +1435 -306
  29. package/src/primitives/app_manifest/root.zig +1 -0
  30. package/src/primitives/app_manifest/tests.zig +35 -0
  31. package/src/primitives/app_manifest/types.zig +12 -0
  32. package/src/primitives/app_manifest/validation.zig +44 -6
  33. package/src/root.zig +8 -0
  34. package/src/runtime/builtin_bridge.zig +108 -13
  35. package/src/runtime/channel_sources.zig +49 -1
  36. package/src/runtime/core.zig +24 -3
  37. package/src/runtime/dialog_tests.zig +367 -0
  38. package/src/runtime/dialogs.zig +385 -0
  39. package/src/runtime/effects.zig +83 -3
  40. package/src/runtime/effects_host_tests.zig +69 -1
  41. package/src/runtime/flow.zig +20 -6
  42. package/src/runtime/flow_reference.zig +20 -6
  43. package/src/runtime/platform_bridge_tests.zig +1 -0
  44. package/src/runtime/root.zig +9 -0
  45. package/src/runtime/single_instance.zig +893 -0
  46. package/src/runtime/system_services.zig +64 -82
  47. package/src/runtime/ui_app.zig +67 -0
  48. package/src/tooling/manifest.zig +111 -3
  49. package/src/tooling/package.zig +98 -8
  50. package/src/tooling/raw_manifest.zig +7 -0
  51. package/src/tooling/root.zig +2 -0
  52. package/src/tooling/templates.zig +44 -2
  53. package/src/tooling/update.zig +228 -10
  54. package/src/tooling/windows_installer.zig +245 -0
  55. package/src/tooling/zip_writer.zig +211 -0
  56. package/src/updater/engine.zig +936 -0
  57. package/src/updater/feed.zig +261 -12
  58. package/src/updater/macos.zig +187 -0
  59. package/src/updater/root.zig +41 -0
  60. package/src/updater/windows.zig +470 -0
  61. package/src/windows_install/config.zig +247 -0
  62. package/src/windows_install/main.zig +690 -0
  63. package/src/windows_install/payload.zig +146 -0
  64. package/src/windows_install/switch.zig +184 -0
  65. package/src/windows_install/tests.zig +11 -0
  66. package/src/windows_install/win32.zig +248 -0
  67. package/tools/native-sdk/main.zig +10 -9
package/README.md CHANGED
@@ -11,7 +11,7 @@ The default app core uses a checked TypeScript subset and compiles to native cod
11
11
  Requires Node.js 24 or later. The CLI command remains `native`.
12
12
 
13
13
  ```bash
14
- npm install -g @zogien/native-sdk@fork
14
+ npm install -g @zogien/native-sdk
15
15
  ```
16
16
 
17
17
  The `native` binary arrives as a per-platform optional dependency (`@zogien/native-sdk-<platform>`). Installation also downloads the exact scriptc release, verifies its checksum, and installs the matching compiler API and runtime packs into `~/.native/toolchains/scriptc/`. This package carries the SDK source your apps build against, so `native init` and `native dev` work offline after installation. If npm install scripts are disabled, run `node packages/core/scripts/install_scriptc.mjs` from the installed CLI package before building. `NATIVE_SDK_SCRIPTC_CACHE` selects another toolchain cache directory. The pinned Zig toolchain is fetched into `~/.native/toolchains/` on first build unless a compatible `zig` is already on your PATH.
package/build/app.zig CHANGED
@@ -2211,6 +2211,9 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A
2211
2211
  package_run.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
2212
2212
  if (cef_auto_install) package_run.addArg("--cef-auto-install");
2213
2213
  if (app_config.updates_enabled and host_os == .macos) package_run.addArg("--update-archive");
2214
+ // Windows updates reach only installed apps, so an app with updates
2215
+ // ships the per-user setup program beside the archive its feed names.
2216
+ if (app_config.updates_enabled and host_os == .windows) package_run.addArgs(&.{ "--installer", "--update-archive" });
2214
2217
  package_run.has_side_effects = true;
2215
2218
  const package_step = b.step("package", "Create a distributable package via the native CLI");
2216
2219
  package_step.dependOn(&package_run.step);
@@ -2824,8 +2827,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
2824
2827
  // The vendored WebView2 SDK header (third_party/webview2)
2825
2828
  // turns on the host's embedded-WebView layer; the host
2826
2829
  // fails the compile by design if it cannot be found.
2827
- app_mod.addIncludePath(dep.path("third_party/webview2/include"));
2828
- if (windowsSdkWinrtIncludeDir(b, target)) |winrt| app_mod.addSystemIncludePath(.{ .cwd_relative = winrt });
2830
+ addWebView2HostIncludes(b, app_mod, dep.path("third_party/webview2/include"), target);
2829
2831
  app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
2830
2832
  app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
2831
2833
  // WebView2Loader.dll rides next to the installed app
@@ -2868,36 +2870,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
2868
2870
  app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
2869
2871
  },
2870
2872
  }
2871
- app_mod.linkSystemLibrary("c", .{});
2872
- // The MSVC target compiles the hosts against the toolchain's own
2873
- // STL, which pulls in msvcprt/vcruntime through its default-lib
2874
- // directives; Zig's libc++/libc++abi cannot be built against the
2875
- // MSVC CRT headers (cxxabi.h collides with vcruntime_typeinfo.h).
2876
- if (target.result.abi != .msvc) app_mod.linkSystemLibrary("c++", .{});
2877
- app_mod.linkSystemLibrary("user32", .{});
2878
- app_mod.linkSystemLibrary("gdi32", .{});
2879
- // Retained gpu_surface packets are composited into a hardware
2880
- // Direct2D target; DirectWrite draws the engine-measured text
2881
- // runs (including registered in-memory fonts) on that target.
2882
- app_mod.linkSystemLibrary("d2d1", .{});
2883
- app_mod.linkSystemLibrary("dwrite", .{});
2884
- app_mod.linkSystemLibrary("imm32", .{});
2885
- app_mod.linkSystemLibrary("comctl32", .{});
2886
- app_mod.linkSystemLibrary("ole32", .{});
2887
- app_mod.linkSystemLibrary("oleacc", .{});
2888
- app_mod.linkSystemLibrary("shell32", .{});
2889
- // TypeScript cores link ScriptC's host runtime, whose network-interface
2890
- // helpers use GetAdaptersAddresses and Winsock address conversion; the
2891
- // in-process service archive shares the same runtime and additionally
2892
- // reaches advapi32 (crypto-backed randomness).
2893
- app_mod.linkSystemLibrary("iphlpapi", .{});
2894
- app_mod.linkSystemLibrary("ws2_32", .{});
2895
- app_mod.linkSystemLibrary("advapi32", .{});
2896
- // The audio backend: Media Foundation (session + source resolver
2897
- // + streaming audio renderer) and WinHTTP (the cache fill).
2898
- app_mod.linkSystemLibrary("mf", .{});
2899
- app_mod.linkSystemLibrary("mfplat", .{});
2900
- app_mod.linkSystemLibrary("winhttp", .{});
2873
+ linkWindowsHostLibraries(app_mod, target);
2901
2874
  if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
2902
2875
  }
2903
2876
  }
@@ -2925,6 +2898,53 @@ fn addPlatformLinkSearchPaths(b: *std.Build, platform: PlatformOption, web_engin
2925
2898
 
2926
2899
  /// The vendored WebView2Loader.dll for the target architecture, relative
2927
2900
  /// to the framework root.
2901
+ /// The system libraries the Windows host links, for an app executable
2902
+ /// and for the host's own test harness alike.
2903
+ pub fn linkWindowsHostLibraries(module: *std.Build.Module, target: std.Build.ResolvedTarget) void {
2904
+ module.linkSystemLibrary("c", .{});
2905
+ // The MSVC target compiles the hosts against the toolchain's own
2906
+ // STL, which pulls in msvcprt/vcruntime through its default-lib
2907
+ // directives; Zig's libc++/libc++abi cannot be built against the
2908
+ // MSVC CRT headers (cxxabi.h collides with vcruntime_typeinfo.h).
2909
+ if (target.result.abi != .msvc) module.linkSystemLibrary("c++", .{});
2910
+ module.linkSystemLibrary("user32", .{});
2911
+ module.linkSystemLibrary("gdi32", .{});
2912
+ // Retained gpu_surface packets are composited into a hardware
2913
+ // Direct2D target; DirectWrite draws the engine-measured text
2914
+ // runs (including registered in-memory fonts) on that target.
2915
+ module.linkSystemLibrary("d2d1", .{});
2916
+ module.linkSystemLibrary("dwrite", .{});
2917
+ module.linkSystemLibrary("imm32", .{});
2918
+ module.linkSystemLibrary("comctl32", .{});
2919
+ module.linkSystemLibrary("ole32", .{});
2920
+ module.linkSystemLibrary("oleacc", .{});
2921
+ module.linkSystemLibrary("shell32", .{});
2922
+ // TypeScript cores link ScriptC's host runtime, whose network-interface
2923
+ // helpers use GetAdaptersAddresses and Winsock address conversion; the
2924
+ // in-process service archive shares the same runtime and additionally
2925
+ // reaches advapi32 (crypto-backed randomness).
2926
+ module.linkSystemLibrary("iphlpapi", .{});
2927
+ module.linkSystemLibrary("ws2_32", .{});
2928
+ module.linkSystemLibrary("advapi32", .{});
2929
+ // The audio backend: Media Foundation (session + source resolver
2930
+ // + streaming audio renderer) and WinHTTP (the cache fill).
2931
+ module.linkSystemLibrary("mf", .{});
2932
+ module.linkSystemLibrary("mfplat", .{});
2933
+ module.linkSystemLibrary("winhttp", .{});
2934
+ }
2935
+
2936
+ /// The WebView2 host's include paths: the vendored WebView2 SDK header
2937
+ /// directory, plus WRL for MSVC targets (`windowsSdkWinrtIncludeDir`).
2938
+ pub fn addWebView2HostIncludes(b: *std.Build, module: *std.Build.Module, sdk_include: std.Build.LazyPath, target: std.Build.ResolvedTarget) void {
2939
+ module.addIncludePath(sdk_include);
2940
+ if (windowsSdkWinrtIncludeDir(b, target)) |winrt| module.addSystemIncludePath(.{ .cwd_relative = winrt });
2941
+ }
2942
+
2943
+ /// The vendored WebView2Loader.dll's directory, relative to the SDK root.
2944
+ pub fn webView2LoaderDir(target: std.Build.ResolvedTarget) []const u8 {
2945
+ return std.fs.path.dirname(webView2LoaderSubPath(target)).?;
2946
+ }
2947
+
2928
2948
  fn webView2LoaderSubPath(target: std.Build.ResolvedTarget) []const u8 {
2929
2949
  return if (target.result.cpu.arch == .aarch64)
2930
2950
  "third_party/webview2/arm64/WebView2Loader.dll"
@@ -2954,8 +2974,7 @@ fn addWebView2RuntimeRunFiles(dep: *std.Build.Dependency, target: std.Build.Reso
2954
2974
  if (web_engine != .system) return;
2955
2975
  if (!web_layer) return;
2956
2976
  if (target.result.os.tag != .windows) return;
2957
- const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?;
2958
- run.addPathDir(dep.builder.pathFromRoot(loader_dir));
2977
+ run.addPathDir(dep.builder.pathFromRoot(webView2LoaderDir(target)));
2959
2978
  }
2960
2979
 
2961
2980
  fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
package/build/cli.zig CHANGED
@@ -43,6 +43,12 @@ pub fn createModules(b: *std.Build, target: std.Build.ResolvedTarget, optimize:
43
43
  tooling.addImport("android_host", module(b, target, optimize, "src/platform/android/files.zig"));
44
44
  tooling.addImport("sqlite_engine", module(b, target, optimize, "src/runtime/sqlite_engine.zig"));
45
45
  tooling.addImport("update_feed", module(b, target, optimize, "src/updater/feed.zig"));
46
+ tooling.addImport("windows_install_config", module(b, target, optimize, "src/windows_install/config.zig"));
47
+ tooling.addImport("windows_install_payload", module(b, target, optimize, "src/windows_install/payload.zig"));
48
+ // `native package --installer` writes the Windows setup program and
49
+ // launcher from these prebuilt copies, so packaging needs no compiler.
50
+ tooling.addAnonymousImport("windows_launcher_x86_64", .{ .root_source_file = windowsLauncher(b, .x86_64).getEmittedBin() });
51
+ tooling.addAnonymousImport("windows_launcher_aarch64", .{ .root_source_file = windowsLauncher(b, .aarch64).getEmittedBin() });
46
52
  tooling.addIncludePath(b.path("third_party/sqlite"));
47
53
  tooling.addCSourceFile(.{
48
54
  .file = b.path("third_party/sqlite/sqlite3.c"),
@@ -101,6 +107,22 @@ pub fn buildCommit(b: *std.Build) []const u8 {
101
107
  return trimmed;
102
108
  }
103
109
 
110
+ /// The Windows setup program, launcher and uninstaller of installed apps
111
+ /// (src/windows_install), built for one Windows architecture.
112
+ fn windowsLauncher(b: *std.Build, arch: std.Target.Cpu.Arch) *std.Build.Step.Compile {
113
+ const launcher = b.addExecutable(.{
114
+ .name = "native-launcher",
115
+ .root_module = b.createModule(.{
116
+ .root_source_file = b.path("src/windows_install/main.zig"),
117
+ .target = b.resolveTargetQuery(.{ .cpu_arch = arch, .os_tag = .windows }),
118
+ .optimize = .ReleaseSmall,
119
+ .strip = true,
120
+ }),
121
+ });
122
+ launcher.subsystem = .windows;
123
+ return launcher;
124
+ }
125
+
104
126
  fn module(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
105
127
  return b.createModule(.{
106
128
  .root_source_file = b.path(path),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zogien/native-sdk",
3
- "version": "0.11.0-fork.3",
3
+ "version": "0.11.0-fork.5",
4
4
  "description": "Community-maintained fork of Native SDK: TypeScript, Native markup, and native desktop tooling",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,14 +41,14 @@
41
41
  "tar": "7.5.22"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.3",
45
- "@zogien/native-sdk-darwin-x64": "0.11.0-fork.3",
46
- "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.3",
47
- "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.3",
48
- "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.3",
49
- "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.3",
50
- "@zogien/native-sdk-win32-arm64": "0.11.0-fork.3",
51
- "@zogien/native-sdk-win32-x64": "0.11.0-fork.3"
44
+ "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.5",
45
+ "@zogien/native-sdk-darwin-x64": "0.11.0-fork.5",
46
+ "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.5",
47
+ "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.5",
48
+ "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.5",
49
+ "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.5",
50
+ "@zogien/native-sdk-win32-arm64": "0.11.0-fork.5",
51
+ "@zogien/native-sdk-win32-x64": "0.11.0-fork.5"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "publishConfig": {
74
74
  "access": "public",
75
- "tag": "fork",
75
+ "tag": "latest",
76
76
  "registry": "https://registry.npmjs.org/"
77
77
  }
78
78
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.3",
3
+ "version": "0.11.0-fork.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@native-sdk/core",
9
- "version": "0.11.0-fork.3",
9
+ "version": "0.11.0-fork.5",
10
10
  "hasInstallScript": true,
11
11
  "dependencies": {
12
12
  "@scriptc/types": "0.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.3",
3
+ "version": "0.11.0-fork.5",
4
4
  "description": "The TypeScript authoring tier: the app-core subset, its checker and contract frontend, the exact-pinned core compiler dependency, and the SDK module cores import",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,6 +34,7 @@
34
34
  }
35
35
  },
36
36
  "dock_visible": { "type": "boolean", "default": true },
37
+ "single_instance": { "type": "boolean", "default": false },
37
38
  "persist": { "$ref": "#/$defs/persist" },
38
39
  "images": { "$ref": "#/$defs/images" },
39
40
  "service_packages": {
@@ -138,7 +139,14 @@
138
139
  "properties": {
139
140
  "feed_url": { "type": "string", "pattern": "^https://", "maxLength": 4096 },
140
141
  "public_key": { "type": "string", "pattern": "^[A-Za-z0-9+/]{43}=$" },
141
- "check_on_start": { "type": "boolean", "default": false }
142
+ "public_keys": {
143
+ "type": "array",
144
+ "maxItems": 3,
145
+ "uniqueItems": true,
146
+ "items": { "type": "string", "pattern": "^[A-Za-z0-9+/]{43}=$" }
147
+ },
148
+ "check_on_start": { "type": "boolean", "default": false },
149
+ "mode": { "type": "string", "enum": ["host", "app"], "default": "host" }
142
150
  }
143
151
  },
144
152
  "bridge": {
@@ -206,6 +206,10 @@ const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
206
206
  });
207
207
  ```
208
208
 
209
+ Each promise resolves when its dialog closes; the app keeps running meanwhile. `openFile` gives an array of paths and `saveFile` a path, or `null` when cancelled; `showMessage` gives `"primary"`, `"secondary"`, `"tertiary"`, or `"dismissed"` when closed without a button.
210
+
211
+ From Zig, use `runtime.requestOpenDialog`, `requestSaveDialog` or `requestMessageDialog` with a `DialogCompletion`: the answer arrives in `on_result` on a later dispatch. The older `runtime.showOpenDialog` family blocks the event dispatch until the dialog closes; do not use it in new code.
212
+
209
213
  Use native dialogs for trusted app UI. Do not expose arbitrary filesystem access to remote or untrusted origins.
210
214
 
211
215
  ## Error handling
@@ -172,6 +172,8 @@ Four effect families deliver MANY results from one command — a keyed stream th
172
172
  - `Cmd.channelOpen(key, { event })` — open an EXTERNAL-SOURCE channel under the app's numeric key: the host stages a long-lived, thread-safe posting seam its NATIVE side feeds — embedders and platform-services extensions post bytes from their own threads (sockets, watchers, workers), and each accepted post dispatches the `event` arm as one `"data"` event. Posting is deliberately not a TS verb — compiled cores are single-threaded, so the core opens, closes, and receives while the posting handle lives on the native side (`Effects.channelHandle(key)`). `key` may be any number expression, a positive integer below 2^53 (a certain-to-be-refused literal stops the build, NS1030). The `event` arm is a five-field record matched by NAME: `key` (the channel key echoed verbatim, so concurrent channels sharing one arm stay distinguishable; a key the wire cannot carry exactly echoes 0), `state` (the `ChannelState` union — import it from `@native-sdk/core` or declare an alias with exactly the three members `"data" | "closed" | "rejected"` in any order; checked BOTH directions, since a narrower union would silently drop states the host emits), `bytes` (`Uint8Array` — the post's payload on `"data"` events, empty otherwise), and `droppedPending`/`droppedTotal` (numbers — the honest back-pressure counters: posts the native handle refused since the previous delivered event, and over the channel's whole life; refused posts count, never silence). One channel per key at a time — a duplicate live key dispatches `"rejected"` — and the key shares the engine's effect-key space (a same-key fetch is blocked while the channel lives). No timer polling anywhere: the source wakes the loop itself. Channel events journal at the effect boundary, so recorded sessions replay the whole stream from the journal — the native posting side is never needed at replay (a native producer that consults `ChannelHandle.live()` before launching keeps replay fully offline; one that launches unconditionally is stopped at its first post, which answers `.closed`).
173
173
  - `Cmd.channelClose(key)` — close the open channel under the key, if any: staged posts flush, exactly one `"closed"` event (final drop totals aboard) dispatches the event arm, and the key frees. A key with no open channel no-ops.
174
174
  - `Cmd.channelOpenSource(key, source, payload, { event, started })` opens a channel and starts an explicit native producer only for that newly accepted occupancy. The `started` arm has exactly `key: number`, `state: "started" | "skipped" | "failed"`, and `bytes: Uint8Array`; it runs in command order before the settled view. A refused open skips startup and still delivers the ordinary channel rejection. Startup failure carries the native error name; close that channel with `Cmd.channelClose`. Source names are 1–255 UTF-8 bytes and arguments at most 4096 bytes. The built-in `"native-sdk.process.samples"` source takes empty arguments and posts process readings every 500 ms. Native embedders can inject `Effects.channel_sources`; starters copy borrowed arguments before returning and must be quick and nonreentrant. Native recording journals startup results and replay never invokes the producer. The fake and virtual hosts report `UnsupportedChannelSource` unless a native test injects a starter; virtual-host recording of external producers requires the native runtime. See `examples/channel-monitor`.
175
+ - The built-in `"native-sdk.app.launches"` source delivers later launches of a `"single_instance": true` app (manifest; Windows today). The owning process's core opens it with empty arguments, usually from `initialModel`; each later launch arrives as one `"data"` event whose `bytes` are the launch's working directory, then each argument (argv[0] excluded), NUL-separated. Split with `.indexOf(0)`/`.slice`, then decide what to do, typically `Cmd.showWindow(label)` (the later process has already granted the owner foreground permission). Launches that arrive before the open are held for up to 5 seconds. A process that does not own the app gets `started` `"failed"` with `SingleInstanceNotOwned` (`UnsupportedChannelSource` on hosts without single-instance support). The later process never runs core code: it exits with code 0 once the owner accepts the launch, or a distinct non-zero code (see the manifest docs' Single instance section).
176
+ - The built-in `"native-sdk.updates"` source follows the app-driven updater (an `updates` manifest block; Windows installs from the SDK setup program, and macOS apps whose manifest sets `"mode": "app"`). Open it with empty arguments from `initialModel`; it posts the current status at open, then at every change and about every percent of a download. A status is `name value` lines — `state` (`unsupported`/`idle`/`checking`/`up_to_date`/`available`/`downloading`/`ready`/`error`), `installed`, then `version`, `size`, `received`, `percent`, `reason`, `error`, `last updated` when they apply; read lines by name with a byte loop (`status[i] === 10` ends a line, `subarray` slices the value). Drive it with `Cmd.request("native-sdk.updates.<verb>", new Uint8Array(0), { ok, err })` — `status`, `check`, `download`, `cancel`, `discard`, `restart` (quits the app; the staged release starts in its place), `notes` (plain-text release notes): each answers at once — `status` with the status, `notes` with the notes, the rest with an empty ok once accepted — or with an err reason (`unsupported`, `busy`, `not_available`, `not_ready`, `not_downloading`, `invalid_request`, `restart_failed`). Keep the model's status from the channel only: it delivers every change in order, while a request's answer may arrive after a newer post. The updater never shows UI or restarts on its own; the core decides. Elsewhere (macOS with the default host-owned flow, Linux) `state` is `unsupported` with `reason platform`. See the Updates docs.
175
177
  - `Cmd.audioCaptureStart(key, { source, sampleRate?, channels? }, { event })` — start a native `"microphone"` or `"system"` audio stream. The host converts into interleaved signed-16 little-endian PCM at 16/24/48 kHz and mono/stereo (default 48 kHz mono), in chunks no larger than 20 ms. The ten-field event arm is matched by NAME: `key`, `state` (exactly `"started" | "data" | "failed" | "stopped" | "rejected"`), `source` (exactly `"microphone" | "system"`), `sampleRate`, `channels`, `timestampMs`, `frames`, `pcm`, `droppedPending`, and `droppedTotal`. PCM is dispatch-lifetime bytes; the commit walker copies it when stored in the model. The bounded queue, wake behavior, drop accounting, journaling, and offline replay are the channel transport's. Microphone and system may run concurrently; a second start for one source replaces that source's prior key.
176
178
  - `Cmd.audioCaptureStop(key)` — synchronously quiesce native callbacks, flush already accepted chunks, deliver exactly one `"stopped"` terminal, and free the key. The key remains occupied until that terminal is delivered, so wait for `"stopped"` before reusing it; an earlier restart is rejected. A missing key no-ops. Declare `"microphone"` and/or `"system_audio"` in `app.zon` permissions so macOS dev executables and packaged apps carry the required usage descriptions.
177
179
  - `Cmd.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })` — load an image at runtime under the model-owned NUMERIC ImageId your markup binds (`<image image="{cover}"/>`, `<avatar image="{avatar}"/>`); `id` may be any number expression (ids are model data), a positive integer below 2^53 (a certain-to-be-refused literal like 0 stops the build, NS1030). The source cascade is `audioPlay`'s exactly: local `path` first, a missing file falls through to `url` (fetched whole, installed at the cache path and integrity-gated by `expectedBytes`); at least one of `path`/`url` (NS1029), and prefer OMITTING `cachePath` — the wiring's caches directory (`TsUiApp`'s `image_cache_dir`) derives the content-addressed path from the URL. Exactly ONE `event` arm dispatches per load — a five-field record matched by NAME: `id` (the requested ImageId echoed verbatim, so concurrent loads sharing one arm stay distinguishable; an id the wire cannot carry exactly echoes 0), `state` (the `ImageState` union — exactly the fifteen members `"loaded" | "rejected" | "not_found" | "io_failed" | "connect_failed" | "tls_failed" | "protocol_failed" | "timed_out" | "http_status" | "cancelled" | "too_large" | "unsupported" | "decode_failed" | "registry_full" | "alloc_failed"`, any order; `"alloc_failed"` is resource exhaustion at registration — the host refused the memory, the bytes may be fine, retry when memory frees), `width`/`height` (the actual registered dimensions on `"loaded"`, 0 otherwise), and `status` (the HTTP status for url loads that performed an exchange; 0 when none occurred — local paths, cache hits — so a cached `"loaded"` is distinguishable from a network one). On `"loaded"` the pixels are already registered under the id — store the id in the model then (the store-on-success discipline keeps a fallback rendering until the load lands). One load per id at a time: a duplicate live id dispatches `"rejected"` (the spawn discipline — a load in flight is never replaced implicitly), and image loads are not the string-keyed `Cmd.cancel`'s to end — `Cmd.imageCancel(id)` is their cancel, LOUD like spawn's: the one terminal still arrives as the event arm's `"cancelled"`, and the id frees for a fresh load once it lands (an id with no live load no-ops; the same NS1030 literal gate as `imageLoad`). The default registry has 16 slots and a 1 MiB decoded-pixel target per slot: platform codecs downscale photo-size sources at decode time, preserving aspect, instead of rejecting them. Image-centric apps may declare `.images = .{ .max_image_pixel_bytes = 8388608 }` in app.zon (1–8 MiB accepted); allocation stays lazy per used slot, but filling 16 ceiling-sized slots is a declared 128 MiB high-water. Encoded sources have their own flat 8 MiB bound; only over-bound source bytes report `"too_large"` in normal operation, never a cut decode. `Cmd.imageUnregister(id)` releases a loaded image's registry slot — the gallery eviction move when the 17th distinct image would answer `"registry_full"`: views bound to the id fall back, and the slot accepts the next load. Unregister is synchronous registry surgery, NOT an effect — no result Msg, an unregistered id no-ops (the same NS1030 literal gate) — and it frees only the CURRENT registration: a load in flight under the id still registers at its terminal, so cancel the load first (`Cmd.imageCancel`) to keep the slot free.
@@ -3,7 +3,10 @@
3
3
  //! Relative paths keep their app-directory meaning during development. In a
4
4
  //! packaged macOS app, the same tree lives below `Contents/Resources`; Finder
5
5
  //! and `open` do not preserve a useful working directory, so resolve that tree
6
- //! from the executable's bundle layout before reading it.
6
+ //! from the executable's bundle layout before reading it. A packaged Windows
7
+ //! or Linux artifact keeps the tree in `resources/` beside the executable's
8
+ //! `bin/`, and a shortcut or file manager launch starts in some other
9
+ //! directory, so resolve that tree the same way.
7
10
 
8
11
  const std = @import("std");
9
12
  const builtin = @import("builtin");
@@ -26,10 +29,56 @@ pub fn readFileAlloc(
26
29
  limit,
27
30
  );
28
31
  }
32
+ if (comptime builtin.os.tag == .windows or builtin.os.tag == .linux) {
33
+ var executable_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
34
+ const executable_len = std.process.executablePath(io, &executable_buffer) catch
35
+ return std.Io.Dir.cwd().readFileAlloc(io, path, allocator, limit);
36
+ return readFileAllocFromDesktopExecutable(
37
+ io,
38
+ executable_buffer[0..executable_len],
39
+ path,
40
+ allocator,
41
+ limit,
42
+ );
43
+ }
29
44
 
30
45
  return std.Io.Dir.cwd().readFileAlloc(io, path, allocator, limit);
31
46
  }
32
47
 
48
+ pub fn readFileAllocFromDesktopExecutable(
49
+ io: std.Io,
50
+ executable_path: []const u8,
51
+ path: []const u8,
52
+ allocator: std.mem.Allocator,
53
+ limit: std.Io.Limit,
54
+ ) std.Io.Dir.ReadFileAllocError![]u8 {
55
+ var packaged_path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
56
+ if (desktopPackagedResourcePath(executable_path, path, &packaged_path_buffer) catch null) |packaged_path| {
57
+ return std.Io.Dir.cwd().readFileAlloc(io, packaged_path, allocator, limit) catch |err| switch (err) {
58
+ // As on macOS: a package miss keeps the cwd-relative development
59
+ // behavior, while a real read failure is not hidden.
60
+ error.FileNotFound => std.Io.Dir.cwd().readFileAlloc(io, path, allocator, limit),
61
+ else => |read_err| read_err,
62
+ };
63
+ }
64
+ return std.Io.Dir.cwd().readFileAlloc(io, path, allocator, limit);
65
+ }
66
+
67
+ /// The packaged copy of a relative asset path when the executable runs
68
+ /// from a Windows or Linux artifact's `bin/`: `<artifact>/resources/<path>`.
69
+ pub fn desktopPackagedResourcePath(
70
+ executable_path: []const u8,
71
+ asset_path: []const u8,
72
+ output: []u8,
73
+ ) error{NoSpaceLeft}!?[]const u8 {
74
+ if (asset_path.len == 0 or std.fs.path.isAbsolute(asset_path)) return null;
75
+ const executable_dir = std.fs.path.dirname(executable_path) orelse return null;
76
+ if (!std.ascii.eqlIgnoreCase(std.fs.path.basename(executable_dir), "bin")) return null;
77
+ const artifact_dir = std.fs.path.dirname(executable_dir) orelse return null;
78
+ return std.fmt.bufPrint(output, "{s}{c}resources{c}{s}", .{ artifact_dir, std.fs.path.sep, std.fs.path.sep, asset_path }) catch
79
+ error.NoSpaceLeft;
80
+ }
81
+
33
82
  pub fn readFileAllocFromMacosExecutable(
34
83
  io: std.Io,
35
84
  executable_path: []const u8,
@@ -136,3 +185,38 @@ test "packaged macOS startup assets read from Contents Resources" {
136
185
  defer std.testing.allocator.free(bytes);
137
186
  try std.testing.expectEqualStrings("packaged-avatar", bytes);
138
187
  }
188
+
189
+ test "desktop packaged resource path mirrors the app-relative asset tree beside bin" {
190
+ var buffer: [256]u8 = undefined;
191
+ const sep = std.fs.path.sep_str;
192
+ const executable = "pkg" ++ sep ++ "demo" ++ sep ++ "bin" ++ sep ++ "demo.exe";
193
+ try std.testing.expectEqualStrings(
194
+ "pkg" ++ sep ++ "demo" ++ sep ++ "resources" ++ sep ++ "assets/fonts/mono.ttf",
195
+ (try desktopPackagedResourcePath(executable, "assets/fonts/mono.ttf", &buffer)).?,
196
+ );
197
+ const uppercase = "pkg" ++ sep ++ "demo" ++ sep ++ "BIN" ++ sep ++ "demo.exe";
198
+ try std.testing.expect((try desktopPackagedResourcePath(uppercase, "assets/logo.png", &buffer)) != null);
199
+ // A development binary outside bin/ keeps reading relative to the
200
+ // working directory, and absolute paths are never redirected.
201
+ try std.testing.expectEqual(null, try desktopPackagedResourcePath("zig-cache" ++ sep ++ "o" ++ sep ++ "demo.exe", "assets/logo.png", &buffer));
202
+ try std.testing.expectEqual(null, try desktopPackagedResourcePath(executable, "", &buffer));
203
+ const absolute = if (builtin.os.tag == .windows) "C:\\logo.png" else "/tmp/logo.png";
204
+ try std.testing.expectEqual(null, try desktopPackagedResourcePath(executable, absolute, &buffer));
205
+ }
206
+
207
+ test "packaged desktop startup assets read from resources beside bin" {
208
+ var tmp = std.testing.tmpDir(.{});
209
+ defer tmp.cleanup();
210
+ try tmp.dir.createDirPath(std.testing.io, "demo/bin");
211
+ try tmp.dir.createDirPath(std.testing.io, "demo/resources/assets/fonts");
212
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "demo/resources/assets/fonts/mono.ttf", .data = "packaged-font" });
213
+
214
+ var executable_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
215
+ const executable_path = try std.fmt.bufPrint(&executable_buffer, ".zig-cache/tmp/{s}/demo/bin/demo.exe", .{tmp.sub_path[0..]});
216
+ const bytes = try readFileAllocFromDesktopExecutable(std.testing.io, executable_path, "assets/fonts/mono.ttf", std.testing.allocator, .limited(1024));
217
+ defer std.testing.allocator.free(bytes);
218
+ try std.testing.expectEqualStrings("packaged-font", bytes);
219
+
220
+ // A package miss falls back to the working directory.
221
+ try std.testing.expectError(error.FileNotFound, readFileAllocFromDesktopExecutable(std.testing.io, executable_path, "assets/fonts/missing.ttf", std.testing.allocator, .limited(1024)));
222
+ }
@@ -98,8 +98,9 @@ pub const RunOptions = struct {
98
98
  .bundle_id = self.bundle_id,
99
99
  .icon_path = self.icon_path,
100
100
  .update_feed_url = manifestUpdateString("feed_url"),
101
- .update_public_key = manifestUpdateString("public_key"),
101
+ .update_public_key = manifestUpdatePublicKeys(),
102
102
  .update_check_on_start = manifestUpdateCheckOnStart(),
103
+ .update_mode = manifestUpdateMode(),
103
104
  .main_window = .{
104
105
  .id = 1,
105
106
  .label = "main",
@@ -172,12 +173,30 @@ fn manifestUpdateString(comptime field: []const u8) []const u8 {
172
173
  };
173
174
  }
174
175
 
176
+ /// The trusted update keys the updater verifies feeds against:
177
+ /// `public_key` followed by any `public_keys`, comma-joined.
178
+ fn manifestUpdatePublicKeys() []const u8 {
179
+ const primary = comptime manifestUpdateString("public_key");
180
+ if (comptime primary.len == 0) return "";
181
+ if (comptime !@hasField(@TypeOf(app_manifest.updates), "public_keys")) return primary;
182
+ comptime var keys: []const u8 = primary;
183
+ inline for (app_manifest.updates.public_keys) |key| keys = keys ++ "," ++ key;
184
+ return keys;
185
+ }
186
+
175
187
  fn manifestUpdateCheckOnStart() bool {
176
188
  if (comptime !@hasField(@TypeOf(app_manifest), "updates")) return false;
177
189
  if (comptime !@hasField(@TypeOf(app_manifest.updates), "check_on_start")) return false;
178
190
  return app_manifest.updates.check_on_start;
179
191
  }
180
192
 
193
+ fn manifestUpdateMode() native_sdk.platform.UpdateMode {
194
+ if (comptime !@hasField(@TypeOf(app_manifest), "updates")) return .host;
195
+ if (comptime !@hasField(@TypeOf(app_manifest.updates), "mode")) return .host;
196
+ return comptime std.meta.stringToEnum(native_sdk.platform.UpdateMode, app_manifest.updates.mode) orelse
197
+ @compileError("app manifest updates.mode must be \"host\" or \"app\"");
198
+ }
199
+
181
200
  const CommandStorage = struct {
182
201
  commands: [native_sdk.app_manifest.max_commands]native_sdk.Command = undefined,
183
202
 
@@ -651,6 +670,30 @@ fn manifestDeclaresCredentials() bool {
651
670
  return false;
652
671
  }
653
672
 
673
+ fn manifestSingleInstance() bool {
674
+ return if (comptime @hasField(@TypeOf(app_manifest), "single_instance")) app_manifest.single_instance else false;
675
+ }
676
+
677
+ /// Manifest `single_instance`: claims ownership before anything else
678
+ /// starts. A later launch hands its working directory and arguments to
679
+ /// the owner and exits here, its exit code naming the delivery result.
680
+ /// Replay never claims. Only the first call claims, so an entry point may
681
+ /// call it before its own startup work and the call in `runWithOptions`
682
+ /// is then a no-op.
683
+ pub fn enforceSingleInstance(init: std.process.Init) void {
684
+ if (comptime !manifestSingleInstance()) return;
685
+ if (init.environ_map.get("NATIVE_SDK_SESSION_REPLAY") != null) return;
686
+ const outcome = native_sdk.single_instance.claim(init.io, init.gpa, init.minimal.args, app_manifest.id) orelse return;
687
+ switch (outcome) {
688
+ .primary => {},
689
+ .standalone => |reason| std.log.warn("single_instance: {s}; running without instance ownership", .{reason}),
690
+ .secondary => |result| {
691
+ if (result != .delivered) std.debug.print("{s}: {s}\n", .{ app_manifest.name, result.describe() });
692
+ std.process.exit(result.exitCode());
693
+ },
694
+ }
695
+ }
696
+
654
697
  fn menuItem(comptime item: anytype) native_sdk.MenuItem {
655
698
  return .{
656
699
  .label = if (@hasField(@TypeOf(item), "label")) item.label else "",
@@ -694,6 +737,7 @@ pub fn runWithOptions(app: native_sdk.App, options: RunOptions, init: std.proces
694
737
  if (init.environ_map.get("NATIVE_SDK_SESSION_REPLAY")) |journal_path| {
695
738
  return runSessionReplay(app, options, init, journal_path);
696
739
  }
740
+ enforceSingleInstance(init);
697
741
  const RecordStoreType = if (comptime manifestDeclaresStore()) native_sdk.RecordStore else void;
698
742
  var record_store_value: RecordStoreType = undefined;
699
743
  var record_store_open = false;
@@ -859,6 +903,7 @@ fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v
859
903
  native_sdk.runtime.launch_timing.lap("runner_main");
860
904
  var buffers: StateBuffers = undefined;
861
905
  var app_info = options.appInfo(&buffers);
906
+ configureUpdates(&app_info, init.environ_map);
862
907
  const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
863
908
  const session_recorder = setupSessionRecorder(init, app_info, app.recording_policy, app.blob_policy);
864
909
  // Heap wrapper, latch-gated free: worker threads hold this address
@@ -931,6 +976,7 @@ fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v
931
976
  fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
932
977
  var buffers: StateBuffers = undefined;
933
978
  var app_info = options.appInfo(&buffers);
979
+ configureUpdates(&app_info, init.environ_map);
934
980
  const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
935
981
  const session_recorder = setupSessionRecorder(init, app_info, app.recording_policy, app.blob_policy);
936
982
  // Heap wrapper, latch-gated free: worker threads hold this address
@@ -998,9 +1044,22 @@ fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v
998
1044
  finishSessionRecorder(session_recorder);
999
1045
  }
1000
1046
 
1047
+ /// Points the app-driven updater at this installation (see
1048
+ /// `updater.configureApp`).
1049
+ fn configureUpdates(app_info: *native_sdk.platform.AppInfo, environ_map: *std.process.Environ.Map) void {
1050
+ const host: native_sdk.updater.Host = comptime if (std.mem.eql(u8, build_options.platform, "windows"))
1051
+ .windows
1052
+ else if (std.mem.eql(u8, build_options.platform, "macos") and std.mem.eql(u8, build_options.web_engine, "system"))
1053
+ .macos_appkit
1054
+ else
1055
+ .other;
1056
+ native_sdk.updater.configureApp(host, app_info, environ_map);
1057
+ }
1058
+
1001
1059
  fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
1002
1060
  var buffers: StateBuffers = undefined;
1003
1061
  var app_info = options.appInfo(&buffers);
1062
+ configureUpdates(&app_info, init.environ_map);
1004
1063
  const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
1005
1064
  const session_recorder = setupSessionRecorder(init, app_info, app.recording_policy, app.blob_policy);
1006
1065
  // Heap wrapper, latch-gated free: worker threads hold this address
@@ -1293,9 +1352,10 @@ test "RunOptions resolves manifest commands menus and shortcuts" {
1293
1352
  };
1294
1353
  var info_buffers: StateBuffers = .{};
1295
1354
  const app_info = options.appInfo(&info_buffers);
1296
- try std.testing.expectEqualStrings("https://example.com/native-update.json", app_info.update_feed_url);
1297
- try std.testing.expectEqualStrings("11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=", app_info.update_public_key);
1355
+ try std.testing.expectEqualStrings("https://example.com/releases/{target}/native-update.json", app_info.update_feed_url);
1356
+ try std.testing.expectEqualStrings("11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=,wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=", app_info.update_public_key);
1298
1357
  try std.testing.expect(app_info.update_check_on_start);
1358
+ try std.testing.expectEqual(native_sdk.platform.UpdateMode.app, app_info.update_mode);
1299
1359
 
1300
1360
  var command_storage: CommandStorage = .{};
1301
1361
  const commands = options.resolvedCommands(&command_storage);
@@ -54,6 +54,11 @@
54
54
  //! when a core requests it, so packaged apps never
55
55
  //! depend on their process working directory for
56
56
  //! durable files.
57
+ //! single instance app.zon's `.single_instance` claims ownership first
58
+ //! thing in `main`; a later launch exits after handing
59
+ //! its cwd and arguments to the owner, whose core
60
+ //! receives them on the `native-sdk.app.launches`
61
+ //! channel source.
57
62
  //!
58
63
  //! Editing this file is never core-level work: it carries no app logic and
59
64
  //! regenerates from the SDK on every build.
@@ -169,6 +174,9 @@ pub fn testMain(init: std.process.Init) !void {
169
174
  }
170
175
 
171
176
  pub fn main(init: std.process.Init) !void {
177
+ // Before persistence, services, or any window: a later launch of a
178
+ // `single_instance` app exits here once the owner has its arguments.
179
+ runner.enforceSingleInstance(init);
172
180
  var options = appOptions(init.io);
173
181
  // The platform caches directory for this app: when the core's
174
182
  // `Cmd.audioPlay` names a URL with no cachePath, the bridge derives
@@ -610,4 +610,26 @@ int native_sdk_gtk_show_message_dialog(Host *host, const void *opts) {
610
610
  return 0;
611
611
  }
612
612
 
613
+ /* Asynchronous dialogs are not offered here: root.zig leaves them unbound
614
+ * for the Chromium engine, so the runtime falls back to the blocking
615
+ * calls above. */
616
+ int native_sdk_gtk_request_dialog(Host *host, const void *request) {
617
+ (void)host;
618
+ (void)request;
619
+ return 0;
620
+ }
621
+
622
+ void native_sdk_gtk_cancel_dialog(Host *host, uint64_t id) {
623
+ (void)host;
624
+ (void)id;
625
+ }
626
+
627
+ int native_sdk_gtk_poll_dialog_result(Host *host, char *buffer, size_t buffer_len, void *result) {
628
+ (void)host;
629
+ (void)buffer;
630
+ (void)buffer_len;
631
+ (void)result;
632
+ return 0;
633
+ }
634
+
613
635
  }