@mpgd/cli 0.47.0 → 0.49.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 (59) hide show
  1. package/dist/command-launcher.d.ts +13 -0
  2. package/dist/command-launcher.js +24 -0
  3. package/dist/devvit-output-collisions.d.ts +49 -0
  4. package/dist/devvit-output-collisions.js +120 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +186 -10
  7. package/dist/microsoft-store-submission.js +11 -6
  8. package/dist/native-build-info.json +2 -2
  9. package/dist/native-build-target.js +2134 -351
  10. package/dist/tooling-migration.d.ts +24 -0
  11. package/dist/tooling-migration.js +497 -0
  12. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  13. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  14. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  15. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  16. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  17. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  18. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  19. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  20. package/dist/windows-host/README.md +118 -0
  21. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  22. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  23. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  24. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  25. package/dist/windows-native-tools.d.ts +4 -0
  26. package/dist/windows-native-tools.js +21 -0
  27. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  28. package/package.json +30 -18
  29. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  30. package/templates/lite-overlay/AGENTS.md +32 -0
  31. package/templates/lite-overlay/README.md +37 -0
  32. package/templates/lite-overlay/agent/acceptance.md +27 -0
  33. package/templates/lite-overlay/agent/brief.md +17 -0
  34. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  35. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  36. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  37. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  38. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  39. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  40. package/templates/phaser-game/AGENTS.md +5 -0
  41. package/templates/phaser-game/README.md +5 -3
  42. package/templates/phaser-game/apps/target-ait/package.json +4 -4
  43. package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
  44. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
  45. package/templates/phaser-game/apps/target-devvit/package.json +2 -2
  46. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  47. package/templates/phaser-game/apps/target-devvit/vite.config.ts +2 -0
  48. package/templates/phaser-game/package.json +8 -8
  49. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  50. package/templates/phaser-game/src/env.d.ts +2 -0
  51. package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
  52. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
  53. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
  54. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
  55. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
  56. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
  57. package/templates/phaser-game/tools/{run-game-acceptance.mjs → run-game-acceptance.ts} +1 -8
  58. package/templates/phaser-game/tsconfig.tools.json +12 -0
  59. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -0,0 +1,118 @@
1
+ # Microsoft Store native host support
2
+
3
+ These C# sources ship inside the existing `@mpgd/cli` npm package. No additional
4
+ npm or NuGet package is required. The core targets .NET 8 without SDK or NuGet
5
+ dependencies. The optional WinUI files compile inside your existing Windows
6
+ App SDK / WebView2 host.
7
+
8
+ ```js
9
+ import { getMicrosoftStoreNativeHostSourceDirectory } from '@mpgd/cli/windows-native-tools';
10
+ const sources = getMicrosoftStoreNativeHostSourceDirectory();
11
+ ```
12
+
13
+ The locator returns the installed `dist/windows-host` directory. SDK-dependent
14
+ sources are maintained in `native-plugins/microsoft-store-host/windows` and copied
15
+ there during the CLI build. SDK-independent sources stay in the CLI source tree.
16
+
17
+ Copy the source tree into a game-owned, ignored build directory. Reference
18
+ `Core/Mpgd.MicrosoftStore.Host.csproj` or link `Core/*.cs` directly; link
19
+ `WinUI/*.cs` only into a Windows host. Keep `bin` and `obj` outside the installed
20
+ npm package. CI builds and exercises these files after packing and installing
21
+ the CLI, without relying on a workspace project reference.
22
+
23
+ ## Attach to a window
24
+
25
+ On the window's UI thread, initialize WebView2, then attach before navigating:
26
+
27
+ ```csharp
28
+ await webView.EnsureCoreWebView2Async();
29
+ StoreContext store = WindowStoreContext.Create(window);
30
+ var bridge = await WebView2NativeBridge.AttachAsync(
31
+ webView, new Uri(gameConfiguration.StartUri),
32
+ gameConfiguration.AllowedItemIds, gameOwnedOperations);
33
+ window.Closed += (_, _) => bridge.Dispose();
34
+ webView.CoreWebView2.Navigate(gameConfiguration.StartUri);
35
+ ```
36
+
37
+ The adapter checks HTTPS host and port, rejects credentials in navigation URLs,
38
+ blocks new windows, disables host objects and devtools, and accepts messages
39
+ from a committed, allowed document as it starts loading, including startup
40
+ requests before the load-complete event. Failed navigation and every accepted navigation
41
+ start invalidates the document. Blocked external navigation keeps the intact
42
+ page's bridge active. Browser or main renderer exit invalidates it; auto-recovered
43
+ GPU, utility and subframe failures and renderer unresponsiveness preserve it.
44
+ Queued operations are cancelled and late replies from the previous document are
45
+ suppressed, including navigation
46
+ back to the same URL. Closure detaches handlers and suppresses responses. Use
47
+ one adapter and operation implementation per window.
48
+
49
+ Await `AttachAsync` before navigating. It installs a document-created script
50
+ that wraps only the native v1 requests with a random per-document nonce; the
51
+ public TypeScript client and v1 payload remain unchanged. Incoming messages must
52
+ match the live document's nonce after a navigation recheck, so queued messages
53
+ from an old page cannot start checkout after a same-origin reload. Other WebView2
54
+ message protocols are preserved. Disposing removes the script registration.
55
+
56
+ `WindowStoreContext.Create` initializes the SDK with that window's HWND on its
57
+ UI thread. Keep the context in native code. Initialization does not perform
58
+ login, checkout or grant an entitlement.
59
+
60
+ ## Game-owned operations
61
+
62
+ Implement `INativeStoreOperations` with typed callbacks for catalog, sign-in,
63
+ purchase listing and checkout. Callbacks begin on the window UI thread and are
64
+ serialized. Observe cancellation whenever the SDK permits it. An SDK operation
65
+ that ignores cancellation retains its serialization lease until it finishes;
66
+ its result is still suppressed after navigation or closure.
67
+
68
+ The common code parses `mpgd.microsoft-store.native.v1`, enforces message limits,
69
+ allowed products, duplicate IDs and pending-request limits, and serializes only
70
+ the declared bounded result records. Invalid results and unexpected exceptions
71
+ become `NATIVE_OPERATION_FAILED`; exception messages never reach the web page.
72
+ Use `NativeBridgeOperationException` for declared user-cancelled,
73
+ identity-unavailable or store-unavailable errors.
74
+ User cancellation of a purchase callback produces a `not-purchased` result,
75
+ matching the existing web commerce runtime's abort semantics.
76
+
77
+ The game owns Store IDs and offers, MSAL/WAM account selection, backend endpoints,
78
+ SDK product validation and session issuance. Sign-in returns the game backend's
79
+ short-lived player session, never an SDK access token. Collections IDs, Microsoft
80
+ service tickets and account credentials stay in private native/backend state.
81
+ Client checkout produces evidence; the backend ledger verifies and commits all
82
+ durable grants.
83
+
84
+ `NativePlayerSession.ExpiresAt` uses Unix epoch milliseconds, with 2 minutes to
85
+ 24 hours remaining at serialization, leaving a minute of transport headroom
86
+ above the web client's 1-minute acceptance minimum. It accepts a bounded player ID
87
+ and a whitespace-free token of 32–4096 characters. Games may enforce a lower
88
+ maximum lifetime. Product and purchase results must refer to their allowlist. Purchase
89
+ tokens equal their item ID, matching the existing web client contract; the
90
+ authoritative backend still verifies and commits the purchase.
91
+
92
+ ## Core-only integration
93
+
94
+ For a custom transport, construct `NativeBridgeHost` with an
95
+ `INativeBridgeDispatcher`. Run queued callbacks on the owning UI thread, prevent
96
+ cancelled queued callbacks from starting, and await started operations until
97
+ they finish. `postResponse` synchronously sends JSON on that thread. Call
98
+ `InvalidateDocument` at every accepted navigation start, `ActivateDocument` only after an
99
+ allowed document commits, invalidate again on load failure, and `Dispose` at
100
+ closure. Invalidate on process failure only when the top-level document exits;
101
+ auto-recovered helper/subframe failures retain it. Lifecycle calls
102
+ and response delivery must share the window UI dispatcher.
103
+ Authenticate each incoming message's document generation before calling the
104
+ host; a source URI alone cannot distinguish queued messages after a same-origin
105
+ reload. The WinUI adapter supplies this transport check automatically.
106
+
107
+ ## Acceptance scope
108
+
109
+ Contract tests cover two independent game configurations, protocol and product
110
+ rejection, sanitized errors, duplicate/busy requests, cancelled queues, navigation
111
+ generation and closure. Windows CI links the installed package's core and WinUI
112
+ sources into x64/ARM64 MSIX, binds StoreContext to the native window, and exercises
113
+ four methods through WebView2 with fixture callbacks. It also checks requests
114
+ before load completion, real blocked external navigation, and simulated
115
+ recoverable process notification kinds on the live adapter. It does not terminate
116
+ GPU processes. Receipts identify fixture
117
+ results separately. They do not claim real Store login, settled purchases,
118
+ backend grants or Store upload.
@@ -0,0 +1,39 @@
1
+ using Microsoft.UI.Dispatching;
2
+
3
+ namespace Mpgd.MicrosoftStore.Host;
4
+
5
+ public sealed class DispatcherQueueBridgeDispatcher(DispatcherQueue queue) : INativeBridgeDispatcher
6
+ {
7
+ public async Task RunAsync(Func<Task> callback, CancellationToken cancellation)
8
+ {
9
+ ArgumentNullException.ThrowIfNull(callback);
10
+ cancellation.ThrowIfCancellationRequested();
11
+ if (queue.HasThreadAccess)
12
+ {
13
+ await callback();
14
+ return;
15
+ }
16
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
17
+ int state = 0; // queued, running, cancelled-before-start
18
+ using var registration = cancellation.Register(() =>
19
+ {
20
+ if (Interlocked.CompareExchange(ref state, 2, 0) == 0)
21
+ completion.TrySetCanceled(cancellation);
22
+ });
23
+ if (!queue.TryEnqueue(async () =>
24
+ {
25
+ if (Interlocked.CompareExchange(ref state, 1, 0) != 0) return;
26
+ try
27
+ {
28
+ cancellation.ThrowIfCancellationRequested();
29
+ await callback();
30
+ completion.TrySetResult();
31
+ }
32
+ catch (OperationCanceledException error) { completion.TrySetCanceled(error.CancellationToken); }
33
+ catch (Exception error) { completion.TrySetException(error); }
34
+ })) completion.TrySetException(new InvalidOperationException("The window dispatcher is unavailable."));
35
+ // After starting, wait for the native operation to finish even if it
36
+ // ignores cancellation; the host's serialization lease must stay held.
37
+ await completion.Task.ConfigureAwait(false);
38
+ }
39
+ }
@@ -0,0 +1,46 @@
1
+ using System.Text.Json;
2
+
3
+ namespace Mpgd.MicrosoftStore.Host;
4
+
5
+ // The public v1 request stays unchanged inside a document-scoped envelope.
6
+ internal static class WebView2DocumentTransport
7
+ {
8
+ internal const string Protocol = "mpgd.microsoft-store.document.v1";
9
+ internal const string ReadNonceScript = "globalThis.__mpgdMicrosoftStoreDocumentNonce ?? null";
10
+ internal const string Bootstrap = """
11
+ (() => {
12
+ const view = globalThis.chrome?.webview;
13
+ if (!view) return;
14
+ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)),
15
+ byte => byte.toString(16).padStart(2, '0')).join('');
16
+ const post = view.postMessage.bind(view);
17
+ Object.defineProperty(globalThis, '__mpgdMicrosoftStoreDocumentNonce', { value: nonce });
18
+ Object.defineProperty(view, 'postMessage', {
19
+ writable: false, configurable: false,
20
+ value: message => post(message?.protocol === 'mpgd.microsoft-store.native.v1'
21
+ ? { transport: 'mpgd.microsoft-store.document.v1', documentNonce: nonce, request: message }
22
+ : message),
23
+ });
24
+ })();
25
+ """;
26
+
27
+ internal static bool TryReadRequest(string json, out string nonce, out string request)
28
+ {
29
+ nonce = request = "";
30
+ if (string.IsNullOrEmpty(json) || json.Length > NativeBridgeProtocol.MaximumMessageLength + 512) return false;
31
+ try
32
+ {
33
+ using var document = JsonDocument.Parse(json, new JsonDocumentOptions { MaxDepth = 10 });
34
+ var root = document.RootElement;
35
+ if (root.ValueKind != JsonValueKind.Object || root.EnumerateObject().Count() != 3
36
+ || !root.TryGetProperty("transport", out var transport) || transport.ValueKind != JsonValueKind.String || transport.GetString() != Protocol
37
+ || !root.TryGetProperty("documentNonce", out var token) || token.ValueKind != JsonValueKind.String
38
+ || !Guid.TryParseExact(token.GetString(), "N", out _)
39
+ || !root.TryGetProperty("request", out var payload) || payload.ValueKind != JsonValueKind.Object) return false;
40
+ nonce = token.GetString()!;
41
+ request = payload.GetRawText();
42
+ return request.Length <= NativeBridgeProtocol.MaximumMessageLength;
43
+ }
44
+ catch (JsonException) { return false; }
45
+ }
46
+ }
@@ -0,0 +1,185 @@
1
+ using Microsoft.UI.Xaml.Controls;
2
+ using Microsoft.Web.WebView2.Core;
3
+ using System.Text.Json;
4
+
5
+ namespace Mpgd.MicrosoftStore.Host;
6
+
7
+ /// <summary>Attach after EnsureCoreWebView2Async, before the first navigation.
8
+ /// Dispose on Window.Closed. Host objects and devtools are disabled.</summary>
9
+ public sealed class WebView2NativeBridge : IDisposable
10
+ {
11
+ private readonly WebView2 view;
12
+ private readonly CoreWebView2 core;
13
+ private readonly NativeBridgeOrigin origin;
14
+ private readonly NativeBridgeHost host;
15
+ private readonly Action<Exception>? onTransportError;
16
+ private ulong navigationId;
17
+ private bool documentActivated;
18
+ private bool documentRejected;
19
+ private bool navigationRejected;
20
+ private bool navigationCommitted;
21
+ private string? lastCommittedSource;
22
+ private bool closed;
23
+ private string? bootstrapScriptId;
24
+ private int pendingDocumentChecks;
25
+
26
+ private WebView2NativeBridge(WebView2 view, Uri startUri, IEnumerable<string> allowedItemIds,
27
+ INativeStoreOperations operations, Action<Exception>? onTransportError = null)
28
+ {
29
+ ArgumentNullException.ThrowIfNull(view);
30
+ if (!view.DispatcherQueue.HasThreadAccess)
31
+ throw new InvalidOperationException("Attach the bridge on the window's UI thread.");
32
+ this.view = view;
33
+ core = view.CoreWebView2 ?? throw new InvalidOperationException("Initialize WebView2 before attaching the bridge.");
34
+ origin = new(startUri);
35
+ this.onTransportError = onTransportError;
36
+ host = new(origin, allowedItemIds, operations, new DispatcherQueueBridgeDispatcher(view.DispatcherQueue),
37
+ response => core.PostWebMessageAsJson(response));
38
+ core.Settings.AreDevToolsEnabled = false;
39
+ core.Settings.AreHostObjectsAllowed = false;
40
+ core.NavigationStarting += NavigationStarting;
41
+ core.ContentLoading += ContentLoading;
42
+ core.NavigationCompleted += NavigationCompleted;
43
+ core.NewWindowRequested += NewWindowRequested;
44
+ core.WebMessageReceived += WebMessageReceived;
45
+ core.ProcessFailed += ProcessFailed;
46
+ }
47
+
48
+ public static async Task<WebView2NativeBridge> AttachAsync(WebView2 view, Uri startUri,
49
+ IEnumerable<string> allowedItemIds, INativeStoreOperations operations, Action<Exception>? onTransportError = null)
50
+ {
51
+ var bridge = new WebView2NativeBridge(view, startUri, allowedItemIds, operations, onTransportError);
52
+ try
53
+ {
54
+ // Register before the first navigation so even boot-time requests
55
+ // carry the nonce of the document that actually posted them.
56
+ bridge.bootstrapScriptId = await bridge.core.AddScriptToExecuteOnDocumentCreatedAsync(WebView2DocumentTransport.Bootstrap);
57
+ return bridge;
58
+ }
59
+ catch { bridge.Dispose(); throw; }
60
+ }
61
+
62
+ private void NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args)
63
+ {
64
+ if (!origin.Allows(args.Uri))
65
+ {
66
+ args.Cancel = true;
67
+ // A cancelled navigation keeps the previous document. A blocked
68
+ // redirect can follow an allowed start that already revoked it.
69
+ if (args.NavigationId == navigationId)
70
+ {
71
+ navigationRejected = true;
72
+ if (!documentActivated && lastCommittedSource is not null)
73
+ documentActivated = host.ActivateDocument(lastCommittedSource);
74
+ }
75
+ return;
76
+ }
77
+ navigationId = args.NavigationId;
78
+ documentActivated = false;
79
+ documentRejected = false;
80
+ navigationRejected = false;
81
+ navigationCommitted = false;
82
+ host.InvalidateDocument();
83
+ }
84
+
85
+ private void ContentLoading(object? sender, CoreWebView2ContentLoadingEventArgs args)
86
+ {
87
+ if (closed || args.NavigationId != navigationId || navigationRejected) return;
88
+ navigationCommitted = true;
89
+ documentRejected = args.IsErrorPage;
90
+ // The new document is committed before its startup scripts can send
91
+ // messages. Waiting for NavigationCompleted drops valid boot requests.
92
+ if (!documentRejected) documentActivated = host.ActivateDocument(core.Source);
93
+ else
94
+ {
95
+ documentActivated = false;
96
+ host.InvalidateDocument();
97
+ }
98
+ lastCommittedSource = documentActivated ? core.Source : null;
99
+ }
100
+
101
+ private void NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args)
102
+ {
103
+ if (closed || args.NavigationId != navigationId || navigationRejected) return;
104
+ if (!args.IsSuccess)
105
+ {
106
+ if (args.WebErrorStatus == CoreWebView2WebErrorStatus.OperationCanceled && documentActivated && !documentRejected)
107
+ return; // The committed page remains usable after a cancelled load.
108
+ if (!navigationCommitted && lastCommittedSource is not null)
109
+ {
110
+ documentActivated = host.ActivateDocument(lastCommittedSource);
111
+ return;
112
+ }
113
+ documentActivated = false;
114
+ lastCommittedSource = null;
115
+ host.InvalidateDocument();
116
+ }
117
+ else if (!documentActivated && !documentRejected)
118
+ {
119
+ documentActivated = host.ActivateDocument(core.Source);
120
+ lastCommittedSource = documentActivated ? core.Source : null;
121
+ }
122
+ }
123
+
124
+ private static void NewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs args) => args.Handled = true;
125
+
126
+ private void ProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args) =>
127
+ HandleProcessFailure(args.ProcessFailedKind);
128
+
129
+ internal void HandleProcessFailure(CoreWebView2ProcessFailedKind kind)
130
+ {
131
+ // GPU/utility/subframe failures and an unresponsive renderer leave the
132
+ // top-level document intact or recover without a new navigation.
133
+ if (closed || kind is not (CoreWebView2ProcessFailedKind.BrowserProcessExited
134
+ or CoreWebView2ProcessFailedKind.RenderProcessExited)) return;
135
+ documentActivated = false;
136
+ lastCommittedSource = null;
137
+ host.InvalidateDocument();
138
+ }
139
+
140
+ private async void WebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs args)
141
+ {
142
+ if (closed) return;
143
+ try { await HandleTransportMessageAsync(args.Source, args.WebMessageAsJson); }
144
+ catch (Exception error) { onTransportError?.Invoke(error); }
145
+ }
146
+
147
+ internal async Task HandleTransportMessageAsync(string source, string json)
148
+ {
149
+ if (closed || !documentActivated || !origin.Allows(source) || pendingDocumentChecks >= 64
150
+ || !WebView2DocumentTransport.TryReadRequest(json, out var nonce, out var request)) return;
151
+ ulong currentNavigation = navigationId;
152
+ pendingDocumentChecks++;
153
+ try
154
+ {
155
+ // WebMessageReceived can arrive after a same-origin reload commits.
156
+ // Read the live document, then recheck navigation before entering
157
+ // the host. Origin alone cannot authenticate a queued old message.
158
+ string liveNonce = await core.ExecuteScriptAsync(WebView2DocumentTransport.ReadNonceScript);
159
+ if (closed || currentNavigation != navigationId || !documentActivated
160
+ || JsonSerializer.Deserialize<string>(liveNonce) != nonce) return;
161
+ await host.HandleMessageAsync(source, request);
162
+ }
163
+ finally { pendingDocumentChecks--; }
164
+ }
165
+
166
+ public void Dispose()
167
+ {
168
+ if (!view.DispatcherQueue.HasThreadAccess)
169
+ throw new InvalidOperationException("Detach the bridge on the window's UI thread.");
170
+ if (closed) return;
171
+ closed = true;
172
+ host.Dispose();
173
+ core.NavigationStarting -= NavigationStarting;
174
+ core.ContentLoading -= ContentLoading;
175
+ core.NavigationCompleted -= NavigationCompleted;
176
+ core.NewWindowRequested -= NewWindowRequested;
177
+ core.WebMessageReceived -= WebMessageReceived;
178
+ core.ProcessFailed -= ProcessFailed;
179
+ if (bootstrapScriptId is not null)
180
+ {
181
+ try { core.RemoveScriptToExecuteOnDocumentCreated(bootstrapScriptId); }
182
+ catch (Exception error) when (error is InvalidOperationException or System.Runtime.InteropServices.COMException) { }
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,20 @@
1
+ using Microsoft.UI.Xaml;
2
+ using Windows.Services.Store;
3
+
4
+ namespace Mpgd.MicrosoftStore.Host;
5
+
6
+ /// <summary>Call on the owning window's UI thread before starting Store UI.</summary>
7
+ public static class WindowStoreContext
8
+ {
9
+ public static StoreContext Create(Window window)
10
+ {
11
+ ArgumentNullException.ThrowIfNull(window);
12
+ if (!window.DispatcherQueue.HasThreadAccess)
13
+ throw new InvalidOperationException("StoreContext must be initialized on its window's UI thread.");
14
+ var handle = WinRT.Interop.WindowNative.GetWindowHandle(window);
15
+ if (handle == IntPtr.Zero) throw new InvalidOperationException("The native window is not initialized.");
16
+ var context = StoreContext.GetDefault();
17
+ WinRT.Interop.InitializeWithWindow.Initialize(context, handle);
18
+ return context;
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ /** Locate the PowerShell 5.1 MSIX acceptance module shipped with this CLI. */
2
+ export declare function getMicrosoftStoreNativeAcceptanceModulePath(): string;
3
+ /** Locate the C# core and WinUI integration sources shipped with this CLI. */
4
+ export declare function getMicrosoftStoreNativeHostSourceDirectory(): string;
@@ -0,0 +1,21 @@
1
+ import { statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** Locate the PowerShell 5.1 MSIX acceptance module shipped with this CLI. */
5
+ export function getMicrosoftStoreNativeAcceptanceModulePath() {
6
+ const modulePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../native-tools/windows-msix/WindowsMsixAcceptance.psm1');
7
+ if (!statSync(modulePath).isFile()) {
8
+ throw new Error('The installed CLI is missing its Windows MSIX acceptance module.');
9
+ }
10
+ return modulePath;
11
+ }
12
+ /** Locate the C# core and WinUI integration sources shipped with this CLI. */
13
+ export function getMicrosoftStoreNativeHostSourceDirectory() {
14
+ const sourceDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), './windows-host');
15
+ for (const file of ['Core/Mpgd.MicrosoftStore.Host.csproj', 'WinUI/WebView2NativeBridge.cs']) {
16
+ if (!statSync(path.join(sourceDirectory, file)).isFile()) {
17
+ throw new Error('The installed CLI is missing its Microsoft Store native host sources.');
18
+ }
19
+ }
20
+ return sourceDirectory;
21
+ }